超级版主
 
- 积分
- 5450
- 金钱
- 5450
- 注册时间
- 2019-5-8
- 在线时间
- 1422 小时
|
|
第四十二章 照相机实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
在前面的章节中,我们已经深入学习了MIPI CSI(Mobile Industry Processor Interface Camera Serial Interface)相关的基本原理,了解了MIPI摄像头如何通过CSI接口与处理器进行数据传输,以及图像数据的处理流程。本章节将基于MIPI CSI摄像头实验的基础,进一步实现照相机功能,并将拍摄的图像数据保存到SD卡的BMP文件中。
本章分为如下几个小节:
42.1 BMP编码简介
42.2 硬件设计
42.3 程序设计
42.4 下载验证
42.1 BMP编码简介
在前面的章节中,我们学习了各种图片格式的解码方法,并了解了如何解析图像文件以及如何从中提取图像数据。为进一步掌握图像处理的基本原理,本章将介绍最简单的图片编码方法——BMP(Bitmap)图片编码。BMP格式是一种非常基础且广泛使用的图像格式,它不使用压缩,因此解码和编码都相对简单。通过这个例程,我们将学习如何将图像数据编码为BMP格式,并理解BMP文件的结构。下面的程序是在bmp.c/.h文件下定义的。
BMP文件由以下四个主要部分组成:
1,文件头(File Header): 文件头是BMP格式的第一部分,它包含了文件的基本信息,如文件大小、图像的起始位置等。文件头是BMP文件中固定长度的部分,长度通常为14个字节。如下代码所示。
- /* BMP头文件 */
- typedef struct
- {
- uint16_t bfType; /* 文件标志.只对'BM',用来识别BMP位图类型 */
- uint32_t bfSize; /* 文件大小,占四个字节 */
- uint16_t bfReserved1; /* 保留 */
- uint16_t bfReserved2; /* 保留 */
- uint32_t bfOffBits; /* 从文件开始到位图数据(bitmap data)开始之间的的偏移量 */
- } __attribute__ ((packed)) BITMAPFILEHEADER;
复制代码 2,位图信息头(Bitmap Info Header): 位图信息头紧跟在文件头后面,包含了图像的详细信息,如图像的宽度、高度、颜色深度、压缩类型等。这个部分的长度通常为40个字节(对于Windows BMP格式)。它告诉我们如何解码图像数据,并且包含了关于图像的维度、颜色空间等关键信息,如下代码所示:
- /* BMP信息头 */
- typedef struct
- {
- uint32_t biSize; /* 说明BITMAPINFOHEADER结构所需要的字数。 */
- long biWidth; /* 说明图象的宽度,以象素为单位 */
- long biHeight; /* 说明图象的高度,以象素为单位 */
- uint16_t biPlanes; /* 为目标设备说明位面数,其值将总是被设为1 */
- uint16_t biBitCount; /* 说明比特数/象素,其值为1、4、8、16、24、或32 */
- uint32_t biCompression; /* 说明图象数据压缩的类型。其值可以是下述值之一
- * BI_RGB :没有压缩
- * BI_RLE8 :每个象素8比特的RLE压缩编码
- * BI_RLE4 :每个象素4比特的RLE压缩编码
- * BI_BITFIELDS:每个象素的比特由指定的掩码决定
- */
- uint32_t biSizeImage; /* 说明图象的大小,以字节为单位*/
- long biXPelsPerMeter; /* 说明水平分辨率,用象素/米表示 */
- long biYPelsPerMeter; /* 说明垂直分辨率,用象素/米表示 */
- uint32_t biClrUsed; /* 说明位图实际使用的彩色表中的颜色索引数 */
- uint32_t biClrImportant; /* 说明对图象显示有重要影响的颜色索引的数目 */
- } __attribute__ ((packed)) BITMAPINFOHEADER;
复制代码 3,颜色表(Color Table): BMP图像的颜色表用于存储图像中使用的颜色。对于8位或更低位深的BMP图像,颜色表是必需的。颜色表的大小由位深(如8位、16位等)决定,每个颜色由4个字节(包括红、绿、蓝和透明度或alpha值)来表示,如下代码所示:
- /* 彩色表 */
- typedef struct
- {
- uint8_t rgbBlue; /* 指定蓝色强度 */
- uint8_t rgbGreen; /* 指定绿色强度 */
- uint8_t rgbRed; /* 指定红色强度 */
- uint8_t rgbReserved; /* 保留,设置为0 */
- } RGBQUAD;
复制代码 5,图形数据(Pixel Data): 位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图的一个像素值所占的字节数:
当biBitCount=1时,8个像素占1个字节;
当biBitCount=4时,2个像素占1个字节;
当biBitCount=8时,1个像素占1个字节;
当biBitCount=16时,1个像素占2个字节;
当biBitCount=24时,1个像素占3个字节;
当biBitCount=32时,1个像素占4个字节;
biBitCount=1 表示位图最多有两种颜色,缺省情况下是黑色和白色,你也可以自己定义这两种颜色。图像信息头装调色板中将有两个调色板项,称为索引0和索引1。图象数据阵列中的每一位表示一个像素。如果一个位是0,显示时就使用索引0的RGB值,如果位是1,则使用索引1的RGB值。
biBitCount=16 表示位图最多有65536种颜色。每个像素用16位(2个字节)表示。这种格式叫作高彩色,或叫增强型16位色,或64K色。它的情况比较复杂,当biCompression成员的值是BI_RGB时,它没有调色板。16位中,最低的5位表示蓝色分量,中间的5位表示绿色分量,高的5位表示红色分量,一共占用了15位,最高的一位保留,设为0。这种格式也被称作555 16位位图。如果biCompression成员的值是BI_BITFIELDS,那么情况就复杂了,首先是原来调色板的位置被三个DWORD变量占据,称为红、绿、蓝掩码。分别用于描述红、绿、蓝分量在16位中所占的位置。在Windows 95(或98)中,系统可接受两种格式的位域:555和565,在555格式下,红、绿、蓝的掩码分别是:0x7C00、0x03E0、0x001F,而在565格式下,它们则分别为:0xF800、0x07E0、0x001F。你在读取一个像素之后,可以分别用掩码“与”上像素值,从而提取出想要的颜色分量(当然还要再经过适当的左右移操作)。在NT系统中,则没有格式限制,只不过要求掩码之间不能有重叠。(注:这种格式的图像使用起来是比较麻烦的,不过因为它的显示效果接近于真彩,而图像数据又比真彩图像小的多,所以,它更多的被用于游戏软件)。
biBitCount=32 表示位图最多有4294967296(2的32次方)种颜色。这种位图的结构与16位位图结构非常类似,当biCompression成员的值是BI_RGB时,它也没有调色板,32位中有24位用于存放RGB值,顺序是:最高位—保留,红8位、绿8位、蓝8位。这种格式也被成为888 32位图。如果 biCompression成员的值是BI_BITFIELDS时,原来调色板的位置将被三个DWORD变量占据,成为红、绿、蓝掩码,分别用于描述红、绿、蓝分量在32位中所占的位置。在Windows 95(or 98)中,系统只接受888格式,也就是说三个掩码的值将只能是:0xFF0000、0xFF00、0xFF。而NT系统,只要注意使掩码之间不产生重叠就行。(注:这种图像格式比较规整,因为它是DWORD对齐的,所以在内存中进行图像处理时可进行汇编级的代码优化(简单)。
通过以上了解,我们对BMP有了一个比较深入的了解,本章,我们采用16位BMP编码(因为我们的LCD就是16位色的,而且16位BMP编码比24位BMP编码更省空间),故我们需要设置biBitCount的值为16,这样得到新的位图信息(BITMAPINFO)结构体:
- /* 位图信息头 */
- typedef struct
- {
- BITMAPFILEHEADER bmfHeader;
- BITMAPINFOHEADER bmiHeader;
- uint32_t RGB_MASK[3]; /* 调色板用于存放RGB掩码 */
- //RGBQUAD bmiColors[256];
- } __attribute__ ((packed)) BITMAPINFO;
复制代码 上述的结构体其实就是颜色表由3个RGB掩码代替。最后,我们来看看将LCD的显存保存为BMP格式的图片文件的步骤:
1)创建BMP位图信息,并初始化各个相关信息
这里,我们要设置BMP图片的分辨率为LCD分辨率、BMP图片的大小(整个BMP文件大小)、BMP的像素位数(16位)和掩码等信息。
2)创建新BMP文件,写入BMP位图信息
我们要保存BMP,当然要存放在某个地方(文件),所以需要先创建文件,同时先保存BMP位图信息,之后才开始BMP数据的写入。
3)保存位图数据。
这里就比较简单了,只需要从LCD的GRAM里面读取各点的颜色值,依次写入第二步创建的BMP文件即可。注意:保存顺序(即读GRAM顺序)是从左到右,从下到上。
4)关闭文件。
使用FATFS,在文件创建之后,必须调用f_close,文件才会真正体现在文件系统里面,否则是不会写入的!这个要特别注意,写完之后,一定要调用f_close。
下面是基于之前的四个步骤实现的BMP编码函数。我们将按照文件头、位图信息头、颜色表、像素数据的顺序来编写BMP编码的函数。假设图像数据已经存在并且已知其宽度、高度以及颜色信息:
- /**
- * @brief BMP编码函数
- * [url=home.php?mod=space&uid=60778]@note[/url] 将图像源保存为16位格式的BMP文件 RGB565格式.
- * 保存为rgb555格式则需要颜色转换,耗时间比较久,所以保存为565是最快速的办法.
- *
- * [url=home.php?mod=space&uid=271674]@param[/url] filename : 包含存储路径的文件名(.bmp)
- * @param image_addr : 图像源
- * @param width,height: 区域大小
- * @param mode : 保存模式
- * @arg 0, 仅仅创建新文件的方式编码;
- * @arg 1, 如果之前存在文件,则覆盖之前的文件。若没有,则创建新文件;
- * @retval 操作结果
- * @arg 0 , 成功
- * @arg 其他, 错误码
- */
- uint8_t bmp_encode( uint8_t *filename, uint16_t *image_addr, uint16_t width,
- uint16_t height, uint8_t mode)
- {
- #if SHOW_TIME == 1
- TickType_t startTick, endTick, diffTick;
- startTick = xTaskGetTickCount();
- #endif
- FIL *f_bmp;
- uint32_t bw = 0;
- uint16_t bmpheadsize; /* bmp头大小 */
- BITMAPINFO hbmp; /* bmp头 */
- uint8_t res = 0;
- uint16_t *databuf; /* 数据缓存区地址 */
- uint16_t pixcnt; /* 像素计数器 */
- uint16_t bi4width; /* 水平像素字节数 */
- uint16_t row_index = 0;
- uint16_t *img_addr = (uint16_t *)image_addr;
- if (width == 0 || height == 0) return PIC_WINDOW_ERR; /* 区域错误 */
-
- /* 开辟至少bi4width大小的字节的内存区域 ,对240宽的屏,480个字节就够了.
- 最大支持1024宽度的bmp编码 */
- databuf = (uint16_t *)piclib_mem_malloc(2160);
- if (databuf == NULL) return PIC_MEM_ERR; /* 内存申请失败. */
- f_bmp = (FIL *)piclib_mem_malloc(sizeof(FIL)); /* 开辟FIL字节的内存区域 */
- if (f_bmp == NULL) /* 内存申请失败 */
- {
- piclib_mem_free(databuf);
- return PIC_MEM_ERR;
- }
- /* BMP头部设置 */
- bmpheadsize = sizeof(hbmp); /* 得到bmp文件头的大小 */
- memset((uint8_t *)&hbmp, 0, sizeof(hbmp)); /* 置零空申请到的内存 */
- hbmp.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); /* 信息头大小 */
- hbmp.bmiHeader.biWidth = width; /* bmp的宽度 */
- hbmp.bmiHeader.biHeight = height; /* bmp的高度 */
- hbmp.bmiHeader.biPlanes = 1; /* 恒为1 */
- hbmp.bmiHeader.biBitCount = 16; /* bmp为16位色bmp */
- hbmp.bmiHeader.biCompression = BI_BITFIELDS;/* 每个象素的比特由指定的掩码决定 */
- /* bmp数据区大小 */
- hbmp.bmiHeader.biSizeImage = hbmp.bmiHeader.biHeight *
- hbmp.bmiHeader.biWidth * hbmp.bmiHeader.biBitCount / 8;
- hbmp.bmfHeader.bfType = ((uint16_t)'M' << 8) + 'B'; /* BM格式标志 */
- /* 整个bmp的大小 */
- hbmp.bmfHeader.bfSize = bmpheadsize + hbmp.bmiHeader.biSizeImage;
- hbmp.bmfHeader.bfOffBits = bmpheadsize; /* 到数据区的偏移 */
- hbmp.RGB_MASK[0] = 0X00F800; /* 红色掩码 */
- hbmp.RGB_MASK[1] = 0X0007E0; /* 绿色掩码 */
- hbmp.RGB_MASK[2] = 0X00001F; /* 蓝色掩码 */
- if (mode == 1)
- {
- /* 尝试打开之前的文件 */
- res = f_open(f_bmp, (const TCHAR *)filename, FA_READ | FA_WRITE);
- }
-
- if (mode == 0 || res == 0x04)
- {
- /* 模式0,或者尝试打开失败,则创建新文件 */
- res = f_open(f_bmp, (const TCHAR *)filename, FA_WRITE | FA_CREATE_NEW);
- }
- if ((hbmp.bmiHeader.biWidth * 2) % 4) /* 水平像素(字节)不为4的倍数 */
- {
- /* 实际要写入的宽度像素,必须为4的倍数 */
- bi4width = ((hbmp.bmiHeader.biWidth * 2) / 4 + 1) * 4;
- }
- else
- {
- bi4width = hbmp.bmiHeader.biWidth * 2; /* 刚好为4的倍数 */
- }
- /* 写入图像数据 */
- if (res == FR_OK) /* 创建成功 */
- {
- /* 写入BMP首部 */
- res = f_write(f_bmp, (uint8_t *)&hbmp, bmpheadsize, &bw);
- /* 按一行一行操作 */
- for (uint16_t ty = height - 1; hbmp.bmiHeader.biHeight; ty--)
- {
- pixcnt = 0;
- for (uint16_t xpix_index = 0; pixcnt != (bi4width / 2);)
- {
- if (pixcnt < hbmp.bmiHeader.biWidth)
- {
- databuf[pixcnt] = img_addr[pixcnt + hbmp.bmiHeader.biWidth
- * row_index];
- }
- else
- {
- databuf[pixcnt] = 0Xffff; /* 补充白色的像素 */
- }
- pixcnt++;
- xpix_index++;
- }
- hbmp.bmiHeader.biHeight--;
- row_index++;
- /* 写入一行数据 */
- res = f_write(f_bmp, (uint8_t *)databuf, bi4width, &bw);
- }
- f_close(f_bmp);
- }
- #if SHOW_TIME == 1
- endTick = xTaskGetTickCount();
- diffTick = endTick - startTick;
- ESP_LOGI(__FUNCTION__, "elapsed time[ms]:%"PRIu32, diffTick * portTICK_PERIOD_MS);
- #endif
- piclib_mem_free(databuf);
- piclib_mem_free(f_bmp);
- return res;
- }
复制代码 BMP编码过程是将图像数据保存为BMP格式文件的操作。通过设置BMP文件的头部信息(如图像宽度、高度、颜色深度等),并将图像的每一行像素数据逐行写入文件(必须是从左到右,从下到上依次写入)。该过程使用RGB565格式保存图像数据,通过内存分配和文件操作来完成图像的存储,并确保数据按4字节对齐。最终,BMP文件包含图像的元数据和实际像素数据,成功生成BMP图像文件。
42.2 硬件设计
42.2.1 程序功能
该实验通过检测SD卡根目录中的PHOTO文件夹是否存在并创建它,确保拍照功能的正常使用。成功创建文件夹后,系统初始化MIPI摄像头并在LCD屏幕上显示相关信息,按下BOOT按键触发拍照功能。同时,LED0会闪烁,提示程序正常运行。如果SD卡或摄像头初始化失败,系统会显示相应的错误信息。
42.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)MIPI CSI
3)RGBLCD/MIPILCD(引脚太多,不罗列出来)
4)KEY按键
BOOT - IO35
42.2.3 原理图
MIPI CSI原理图已在37.2.3小节中详细阐述,为避免重复,此处不再赘述。
42.3 程序设计
42.3.1 程序流程图
图42.3.1 照相机实验程序流程图
42.3.2 程序解析
在33_photograph例程中,基于28_mipicamera例程进行了修改,主要是在33_photograph\components\Middlewares文件夹下添加了MYFATFS、PICTURE和TEXT这三个组件。这些组件的具体功能和作用,笔者已经在前面的章节中详细讲解过,因此在此不再重复描述。
1,图像存储代码
在main/APP/MIPI_CAM/mipi_cam.c文件下的lcd_cam_task摄像头任务函数中,首先通过PPA(Pixel Processing Accelerator)将摄像头模块采集到的原始图像数据转换为适合显示的格式和大小,然后将转换后的图像数据展示在LCD屏幕上。当按下BOOT按键时,系统会触发拍照功能,将当前显示的图像数据传递给bmp_encode函数进行BMP格式编码,最终将编码后的图片保存到SD卡的PHOTO文件夹中:
- if (key_scan(0) == BOOT_PRES) /* 按下BOOT按键进行拍照 */
- {
- camera_new_pathname(pname, 0); /* 得到文件名 */
- res=bmp_encode(pname,(uint16_t *)draw_buffer,lcddev.width,lcddev.height, 1);
- text_show_string(30, 130, 240, 16, "请耐心等待,正在进行文件写入!", 16, 0, RED);
- if (res)
- {
- text_show_string(30, 130, 240, 16, "写入文件错误!", 16, 0, RED);
- }
- else
- {
- text_show_string(30, 130, 240, 16, "拍照成功!", 16, 0, RED);
- text_show_string(30, 150, 240, 16, "保存为:", 16, 0, RED);
- text_show_string(30 + 56, 150, 240, 16, (char *)pname, 16, 0, RED);
- vTaskDelay(pdMS_TO_TICKS(500));
- }
- }
复制代码 lcd_cam_task摄像头任务函数实现已在第三十七章节中介绍过了,因此在此不再重复描述。
2,CMakeLists.txt文件
CMakeLists.txt文件跟摄像头实验一致,代码如下:
- idf_component_register(
- SRC_DIRS
- "."
- "APP/MIPI_CAM"
- INCLUDE_DIRS
- "."
- "APP/MIPI_CAM"
- )
复制代码 3,main.c驱动代码
在main.c里面编写如下代码。
- void app_main(void)
- {
- esp_err_t ret;
- uint8_t key = 0;
- ret = nvs_flash_init(); /* 初始化NVS */
- if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
- {
- ESP_ERROR_CHECK(nvs_flash_erase());
- ESP_ERROR_CHECK(nvs_flash_init());
- }
- led_init(); /* LED初始化 */
- key_init(); /* KEY初始化 */
- myiic_init(); /* MYIIC初始化 */
- lcd_init(); /* LCD屏初始化 */
- while (sdmmc_init()) /* 检测不到SD卡 */
- {
- lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
- vTaskDelay(pdMS_TO_TICKS(200));
- lcd_fill(30, 110, 239, 126, WHITE);
- vTaskDelay(pdMS_TO_TICKS(200));
- }
- ret = exfuns_init(); /* 为fatfs相关变量申请内存 */
- while (fonts_init()) /* 检查字库 */
- {
- lcd_clear(WHITE);
- lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
-
- key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED); /* 更新字库 */
- while (key) /* 更新失败 */
- {
- lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
- vTaskDelay(pdMS_TO_TICKS(200));
- lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
- vTaskDelay(pdMS_TO_TICKS(200));
- }
- lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
- vTaskDelay(pdMS_TO_TICKS(1000));
- lcd_clear(WHITE);
- }
- text_show_string(30, 50, 200, 16, "正点原子ESP32-P4开发板", 16, 0, RED);
- text_show_string(30, 70, 200, 16, "照相机 实验", 16, 0, RED);
- text_show_string(30, 90, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
- text_show_string(30, 110, 200, 16, "BOOT:TAKE PHOTO", 16, 0, RED);
- vTaskDelay(pdMS_TO_TICKS(1000));
- lcd_clear(WHITE);
-
- mipi_cam_init(); /* 摄像头初始化 */
- while (1)
- {
- LED0_TOGGLE();
- vTaskDelay(pdMS_TO_TICKS(500));
- }
- }
复制代码 上述函数主要实现了硬件初始化、SD卡检测、字库更新、LCD显示和摄像头初始化等功能。程序首先初始化了NVS、LED、按键、I2C总线和LCD屏,随后检测SD卡是否存在并可用,如果未检测到SD卡则提示错误信息并反复重试。接着,程序检查并更新字库,确保显示文字的正确性。字库更新成功后,屏幕会显示开发板和实验相关的信息。在完成硬件和系统初始化后,摄像头被初始化准备拍照,同时LED灯每500毫秒闪烁,提示程序正常运行。
42.4 下载验证
将程序下载到开发板后,可以看到LCD首先显示一些实验相关的信息,如下图所示:
图42.4.1 显示实验相关信息
显示了上图的信息后,自动进入监控界面。可以看到LED0不停的闪烁,提示程序已经在运行了。此时,我们可以按下BOOT,即可进行bmp拍照。拍照得到的照片效果如下图所示:
图42.4.2 拍照样图 |
|