0%

图片显示

图片显示

BMP格式解码

BMP是一种常见的图片文件格式,广泛的用于存储像素数据。

1、BMP文件的基本构成

BMP文件主要由以下部分组成:

  • 头文件:描述文件的基本信息,例如文件大小和数据偏移量
  • 信息头:描述图像的详细信息,例如宽度、高度、颜色等。
  • 调色板【可选】:对于地位深度的图像,会包含调色板,用于将索引映射带具体的RGB颜色。
  • 像素数据:实际的图像数据,存储方式却决于颜色深度和压缩格式。

2、解码步骤

  • 读取文件头(14字节)

    • 2字节:文件标识符,通常BM(0x424D),表示这是BMP文件。
    • 4字节:文件总大小,字节为单位。
    • 4字节:保留字段,通常为0,无意义。
    • 4字节:像素数据起始偏移量,告诉我们从文件开头到像素数据的距离。
  • 读取信息头(通常为40字节)

    • 4字节:信息头大小。

    • 4字节:图像宽度。

    • 4字节:图像高度。

    • 2字节:颜色平面数,通产为1

    • 2字节:每像素位数,常见的1、4、8、16、24、32.

    • 4字节:压缩方式,0:无压缩、1:PLE8位;2、RLE 4位。

    • 偏移 34-37(4字节):像素数据大小(字节),若无压缩可忽略。

      偏移 38-41(4字节):水平分辨率(像素/米),可忽略。

      偏移 42-45(4字节):垂直分辨率(像素/米),可忽略。

      偏移 46-49(4字节):调色板颜色数,0表示默认值(2^n,n为颜色深度)。

      偏移 50-53(4字节):重要颜色数,通常为0。

  • 读取调色板(如果存在)

    • 如果颜色深度≤8(如1位、4位、8位),会有调色板。

      每个颜色占用4字节,按BGR顺序存储(蓝、绿、红、保留字节,通常为0)。

      调色板大小 = 颜色数 × 4字节,颜色数由信息头中的字段或默认值(2^颜色深度)确定。

      24位及以上颜色深度通常没有调色板,直接使用RGB值。

  • 读取像素数据

    • 像素数据的存储方式取决于颜色深度和压缩格式。
    • 对于无压缩的BMP:
      • 行字节数:根据宽度和颜色深度计算。每行字节数必须是4的倍数,若不足则填充0
      • 存储顺序:通常从图像底部开始,自左向右逐行向上(若高度为正值)。
      • 颜色格式:
        • 24位:每个像素3字节,按BGR顺序(蓝、绿、红)。
        • 32位:每个像素4字节,按BGRA顺序(蓝、绿、红、透明度)。

代码分析

1、全局变量定义

1
2
3
4
5
6
char bmp_files[MAX_BMP_FILES][MAX_PATH_LEN];
uint8_t bmp_file_count = 0;
int8_t current_bmp_index = -1;
FIL img_file;
uint8_t *img_buffer = NULL;
lv_img_dsc_t img_dsc;
  • bmp_files:二维字符数组,用于存储 BMP 文件的完整路径(如 “0:/PICTURE/image.bmp”)。MAX_BMP_FILES 和 MAX_PATH_LEN 在头文件中定义。

    bmp_file_count:记录扫描到的 BMP 文件数,初始为 0。

    current_bmp_index:当前显示的 BMP 文件索引,初始为 -1(未选择)。

    img_file:FATFS 的文件对象,用于操作 SD 卡上的文件。

    img_buffer:指向动态分配的内存,用于存储 BMP 像素数据,初始为 NULL。

    img_dsc:LVGL 的图像描述符,定义图像的宽度、高度、颜色格式等。

2、stricmp 函数:不区分大小写的字符串比较

1
2
3
4
5
6
7
8
9
10
int stricmp(const char *s1, const char *s2) {
while (*s1 && *s2) {
char c1 = (*s1 >= 'A' && *s1 <= 'Z') ? *s1 + 32 : *s1;
char c2 = (*s2 >= 'A' && *s2 <= 'Z') ? *s2 + 32 : *s2;
if (c1 != c2) return c1 - c2;
s1++;
s2++;
}
return *s1 - *s2;
}
  • 如果字符是大写字母A到Z,则通过+32转换为小写。
  • 如果字符不相同,返回插值。

3、scan_bmp_files 函数:扫描BMP文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void scan_bmp_files(void) {
DIR dir;
FILINFO fno;
FRESULT res;

bmp_file_count = 0;
memset(bmp_files, 0, sizeof(bmp_files));

res = f_opendir(&dir, "0:/PICTURE");
if (res != FR_OK) {
printf("打开目录 'PICTURE' 失败,错误码: %d\r\n", res);
return;
}

while (bmp_file_count < MAX_BMP_FILES) {
res = f_readdir(&dir, &fno);
if (res != FR_OK || fno.fname[0] == 0) break;

char *ext = strrchr(fno.fname, '.');
if (ext && stricmp(ext, ".bmp") == 0) {
snprintf(bmp_files[bmp_file_count], MAX_PATH_LEN, "0:/PICTURE/%s", fno.fname);
printf("添加 BMP 文件 [%d]: %s\r\n", bmp_file_count, bmp_files[bmp_file_count]);
bmp_file_count++;
}
}

f_closedir(&dir);
printf("共找到 %d 个 BMP 文件\r\n", bmp_file_count);
}
  • 打开图片目录。
  • 读取目录项:使用f_readdir 逐个读取目录中文件的信息,存储在fno中。
  • 使用strrchr找文件名中.后面的名字,提取字符串。
  • 输出打印找到的每个BMP文件路劲和总数。

4、load_image_from_sd 函数:加载并显示BMP图像

  • 参数检查

    1
    2
    3
    4
    if (index < 0 || index >= bmp_file_count) {
    printf("无效的 BMP 索引: %d\r\n", index);
    return;
    }
  • 释放就缓冲区

    1
    2
    3
    4
    if (img_buffer != NULL) {
    myfree(SRAMEX, img_buffer);
    img_buffer = NULL;
    }
  • 打开找到的BMP文件

    1
    2
    3
    4
    5
    res = f_open(&img_file, bmp_files[index], FA_READ);
    if (res != FR_OK) {
    printf("打开文件失败: %s,错误码: %d\r\n", bmp_files[index], res);
    return;
    }
  • 检查文件大小

    1
    2
    3
    4
    5
    6
    uint32_t file_size = f_size(&img_file);
    if (file_size > MAX_BMP_SIZE) {
    printf("BMP 文件过大: %s,大小: %lu 字节\r\n", bmp_files[index], file_size);
    f_close(&img_file);
    return;
    }
  • 读取BMP文件头

    1
    2
    3
    4
    5
    6
    res = f_read(&img_file, header_buf, sizeof(header_buf), &br);
    if (res != FR_OK || br != sizeof(header_buf)) {
    printf("读取 BMP 文件头失败\r\n");
    f_close(&img_file);
    return;
    }
    • 读取前 54 字节(文件头 14 字节 + 信息头 40 字节),存储到 header_buf 中。
  • 解析BMP信息

    1
    2
    3
    4
    5
    6
    bmp_header = (BITMAPINFOHEADER *)(header_buf + 14);
    width = bmp_header->biWidth;
    height = bmp_header->biHeight;
    bpp = bmp_header->biBitCount;
    data_offset = *(uint32_t *)(header_buf + 10);
    img_size = width * height * (bpp / 8);
    • 从头信息中提取高度、宽度、位深度、像素偏移量
    • 计算像素数据大小。
  • 检查位深度

    1
    2
    3
    4
    5
    if (bpp != 16 && bpp != 24 && bpp != 32) {
    printf("不支持的 BMP 格式,位深度: %d\r\n", bpp);
    f_close(&img_file);
    return;
    }
  • 分配缓冲区,并读取像素数据

    1
    2
    3
    img_buffer = mymalloc(SRAMEX, img_size);
    f_lseek(&img_file, data_offset);
    res = f_read(&img_file, img_buffer, img_size, &br);
  • 转换为RGB565格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if (bpp == 32) {
    uint8_t *src = img_buffer;
    uint16_t *dst = (uint16_t *)mymalloc(SRAMEX, width * height * 2);
    for (uint32_t i = 0; i < width * height; i++) {
    uint8_t b = src[i * 4 + 0] >> 3;
    uint8_t g = src[i * 4 + 1] >> 2;
    uint8_t r = src[i * 4 + 2] >> 3;
    uint8_t a = src[i * 4 + 3];
    dst[i] = (a < 128) ? 0x0000 : (r << 11) | (g << 5) | b;
    }
    myfree(SRAMEX, img_buffer);
    img_buffer = (uint8_t *)dst;
    img_size = width * height * 2;
    }
    • 将 BGRA(蓝绿红透明)转换为 RGB565(5位红、6位绿、5位蓝)。

    • 如果透明度(a)< 128,则像素设为透明(0x0000)。

    • RGB565是一种16位的颜色编码格式:

      • R:5位

      • G:6位

      • B:5位

      • 在内存中表示如下:

        1
        RRRRRGGG GGGBBBBB
      • 32位代表着R=8位、G=8位、B=8位、透明色=8位。

      • 8位颜色代表着颜色的每个颜色,就像8位的红色右2^8=256种颜色细分。

      • 5 位颜色:用 5 位表示,只能表示 0 到 31 的值(2⁵ = 32 种可能)。

      • 6 位颜色:用 6 位表示,能表示 0 到 63 的值(2⁶ = 64 种可能)。

    • BMP文件种假设使用32位的,而屏幕使用是RGB565.则需要从8位转化为5位或者6位。需要一一对应起来。

      • 从8位转换为5位:相当于256种颜色转换为32种颜色。所以要比例是:256/32=8.
      • 在二进制种8可以用右移3位来表示。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    else if (bpp == 24) {
    uint8_t *src = img_buffer;
    uint16_t *dst = (uint16_t *)img_buffer;
    for (uint32_t i = 0; i < width * height; i++) {
    uint8_t b = src[i * 3 + 0] >> 3;
    uint8_t g = src[i * 3 + 1] >> 2;
    uint8_t r = src[i * 3 + 2] >> 3;
    dst[i] = (r << 11) | (g << 5) | b;
    }
    img_size = width * height * 2;
    }
    • 将 BGR 转换为 RGB565,直接覆盖原缓冲区。
  • 垂直翻转图像

    1
    2
    3
    4
    5
    6
    7
    8
    uint16_t *buf = (uint16_t *)img_buffer;
    for (uint16_t y = 0; y < height / 2; y++) {
    for (uint16_t x = 0; x < width; x++) {
    uint16_t temp = buf[y * width + x];
    buf[y * width + x] = buf[(height - 1 - y) * width + x];
    buf[(height - 1 - y) * width + x] = temp;
    }
    }
    • BMP 文件存储顺序:BMP 文件的像素数据通常从图像的底部开始,按行顺序存储到顶部(如果信息头中的高度字段 biHeight 为正值)。
  • 配置LVGL并显示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    img_dsc.header.w = width;
    img_dsc.header.h = height;
    img_dsc.header.cf = LV_IMG_CF_TRUE_COLOR;
    img_dsc.data_size = img_size;
    img_dsc.data = img_buffer;

    lv_img_set_src(img_obj, &img_dsc);
    lv_img_set_zoom(img_obj, 128); // 50% 缩放
    lv_obj_set_pos(img_obj, 100, 100);