详解BMP文件格式和转换

Last Updated: 2023-03-27 09:34:57 Monday

-- TOC --

BMP图像文件,即bitmap文件,其实就是没有任何压缩的RGB格式,但这种Raw格式,也是一种存储格式。(就像用wav格式存放raw声音数据一样,也需要一点规范)

Bitmap是Windows操作系统中的标准图像文件格式,可以分成两类:设备相关位图(DDB)和设备无关位图(DIB),DDB已经基本停用。

Bitmap文件格式由4部分组成:

现在,我们来拆解一个bmp文件。

文件头占14bytes

>>> f = open('desk.bmp', 'rb')
>>> head = f.read(14)
>>> head[:2]
b'BM'
>>> int.from_bytes(head[2:6], 'little')
4196406  # byte
>>> head[6:10]
b'\x00\x00\x00\x00'
>>> int.from_bytes(head[10:14], 'little')
54   # means no color table

图像描述区占40 bytes,如果没有颜色表,整个bmp文件的头刚好是54bytes!

图像描述区,一般占40bytes

说一般,是因为这个区域的前4个byte,定义了这个区域的大小,也就有可能存在这个区域不是40bytes的情况:

>>> desc = f.read(40)
>>> int.from_bytes(desc[:4], 'little')
40

宽和高:

>>> int.from_bytes(desc[4:8], 'little')
1366
>>> int.from_bytes(desc[8:12], 'little')
768

plane数和bit-depth:

>>> int.from_bytes(desc[12:14], 'little')
1
>>> int.from_bytes(desc[14:16], 'little')
32  # 4 bytes per pixel
>>> 
>>> 1366*768*4 + 54
4196406  # file size

这个区域后面的数据(测试用bmp文件,是微信截图保存的):

>>> int.from_bytes(desc[16:20], 'little')
0
>>> int.from_bytes(desc[20:24], 'little')
0
>>> int.from_bytes(desc[24:28], 'little')
3780
>>> int.from_bytes(desc[28:32], 'little')
3780
>>> int.from_bytes(desc[32:36], 'little')
0
>>> int.from_bytes(desc[36:40], 'little')
0

颜色表

在真彩色(24或32位)模式无颜色表。当存在颜色表的时候,每bit depth数据作为颜色表索引值,获取到对应的颜色。

其他色彩bmp图像的颜色表的大小,根据所使用的颜色模式而定:

每4字节表示一种颜色,分别是B(蓝色)、G(绿色)、R(红色)、Alpha通道(像素的透明度值,一般不需要,不透明为0xFF)。即首先4字节表示颜色号0的颜色,接下来表示颜色号1的颜色,依此类推。

数据区

32 bit depth,每4bytes表示一个pixel数据,难道不是吗?

扫描是从左到右,从下到上,raster scan。

24位RGB按照BGR的顺序来存储每个像素的各颜色通道的值,一个像素的所有颜色分量值都存完后才存下一个下一个像素,不进行交织存储。32位数据按照BGRA的顺序存储。(这种存储方式,有个专业术语,叫做packed

位图全部像素数据,在存储的过程中是按照自下而上,自左往右的顺序进行排列的,这点为什么是这样,我也不太清楚,我估计是历史遗留问题。还有一点,单个像素的通道存储顺序也是反着的,实际是按照BGR的顺序来存储的,这点很好理解,因为在存储过程中,RGB三个字节是同时写入的,低字节在前,所以实际在读取的时候顺序却是BGR。这两点一定要注意,在实际使用过程中很容易出错。

行padding

24位的Bitmap文件,存在行padding的问题。

有一张照片的大小为2127*1200*3=7657200字节,再加上两个文件头占用的54个字节,总共大小应该为7657200+54=7657254,但是实际大小却是7660854字节,与实际结果还有差距,这是怎么回事呢?

实际上,这是Windows操作系统中的内存对齐造成的,Windows要求位图的每一行像素所占字节数必须被4整除,因为这样对操作系统读取数据非常方便,若不能被4整除,则在该位图每一行的十六进制码末尾"补"1至3个字节的"00"。

例如我们这张图片每行为2127个像素,每行占用2127*3=6381字节,6381除以4还余1,所以需要在每行再补齐3个字节,这样每行实际占用6381+3=6384个字节,总共像素占用6384*1200=7660800字节,另外再加上两个文件头信息占用的54个字节,整张图片的大小为7660854,刚好与我们右键信息的大小一致。

用libjpeg-turbo将32位bmp文件转为jpeg

这段代码转换的jpeg图像,与bmp图像,正好上下翻转了,因为bmp文件数据区的数据存储,是从左到右,从下到上的!

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include "jpeglib.h"

#pragma comment(lib, "jpeg.lib")
#pragma comment(lib, "turbojpeg.lib")


int main(int argc, char* argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s input.bmp output.jpg\n", argv[0]);
        return 1;
    }

    char* input_file = argv[1];
    char* output_file = argv[2];

    FILE* infile = fopen(input_file, "rb");
    if (infile == NULL) {
        fprintf(stderr, "Error opening input file: %s\n", input_file);
        return 1;
    }

    // Read the BMP file header and extract the image width and height
    unsigned char bmp_header[54];
    fread(bmp_header, sizeof(unsigned char), 54, infile);
    int bmp_width = *(int*)&bmp_header[18];
    int bmp_height = *(int*)&bmp_header[22];

    // Allocate memory for the BMP image data
    int input_components = 4;
    int bmp_size = bmp_width * bmp_height * input_components;
    unsigned char* bmp_data = (unsigned char*)malloc(bmp_size);
    fread(bmp_data, sizeof(unsigned char), bmp_size, infile);

    fclose(infile);

    // Initialize the JPEG compression parameters
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);

    FILE* outfile = fopen(output_file, "wb");
    if (outfile == NULL) {
        fprintf(stderr, "Error opening output file: %s\n", output_file);
        return 1;
    }

    jpeg_stdio_dest(&cinfo, outfile);

    cinfo.image_width = bmp_width;
    cinfo.image_height = bmp_height;
    cinfo.input_components = input_components;
    cinfo.in_color_space = JCS_EXT_BGRA;

    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, 80, TRUE);

    jpeg_start_compress(&cinfo, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride = bmp_width * input_components;

    // Convert and write the BMP image data to JPEG format
    while (cinfo.next_scanline < cinfo.image_height) {
        row_pointer[0] = &bmp_data[cinfo.next_scanline * row_stride];
        jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }

    jpeg_finish_compress(&cinfo);
    jpeg_destroy_compress(&cinfo);
    fclose(outfile);

    // Free the BMP image data memory
    free(bmp_data);

    return 0;
}

先将BGRA数据现在内存中上下翻转,然后再走jpeg压缩,就好了!不要先jpeg压缩,再flip,这样做画面有轻微漂移。

libjpeg-turbo项目内有example.c和tjexample.c!

本文链接:https://cs.pynote.net/ag/image/202206242/

-- EOF --

-- MORE --