《ESP32-P4开发指南—V1.0》第四十二章 照相机实验

第四十二章 照相机实验


       在前面的章节中,我们已经深入学习了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编码函数
 *   @note     将图像源保存为16位格式的BMP文件 RGB565格式.
 *              保存为rgb555格式则需要颜色转换,耗时间比较久,所以保存为565是最快速的办法.
 *
 * @param       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 拍照样图


请使用浏览器的分享功能分享到微信等