实验二 图像文件的读写和转换(BMP转YUV)

一、实验原理

1. BMP文件的组成

  BMP文件由四部分组成:位图文件头数据、位图信息头数据、调色板和位图数据。下面结合具体图片说明。为了方便在查看数据项在图片里的具体内容,每行前面加了当前项在文件中的地址值。
  (1)文件头 BITMAP_FILE_HEADER,包含如下内容

typedef struct tagBITMAPFILEHEADER {
    //0x00~0x01,说明文件的类型
    WORD bfType; 
    //0x02~0x05,说明文件的大小,用字节B为单位
    DWORD bfSize;
    //0x06~0x07,保留,设置为0
    WORD bfReserved1;
    //0x08~0x09,保留,设置为0
    WORD bfReserved2;
    //0x0a~0x0d,说明从BITMAP_FILE_HEADER结构开始到实际的图像数据之间的字节偏移量
    DWORD bfOffBits;
} BITMAPFILEHEADER;

  (2)信息头BITMAP_INFO_HEADER,包含如下内容

typedef struct tagBITMAPINFOHEADER {
    //0x0e~0x11,说明当前结构体所需字节数
    DWORD biSize;
    //0x12~0x15,以像素为单位说明图像的宽度
    LONG biWidth;
    //0x16~0x19,以像素为单位说明图像的高度
    LONG biHeight;
    //0x1a~0x1b,说明位面数,必须为1
    WORD biPlanes;
    //0x1c~0x1d,说明图像的位深度
    WORD biBitCount;
    //0x1e~0x21,说明图像是否压缩及压缩类型
    DWORD biCompression;
    //0x22~0x25,以字节为单位说明图像大小,必须是4的整数倍
    DWORD biSizeImage;
    //0x26~0x29,目标设备的水平分辨率,像素/米 
    LONG biXPelsPerMeter;
    //0x2a~0x2d,目标设备的垂直分辨率,像素/米
    LONG biYPelsPerMeter;
    //0x2e~0x31,说明图像实际用到的颜色数,如果为0,则颜色数为2的biBitCount次方
    DWORD biClrUsed;
    //0x32~0x35,说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。
    DWORD biClrImportant;
} BITMAPINFOHEADER;

  (3)调色板Palette。当图像位深度小于等于8时称为索引颜色,这时整个文件不保存每个像素的RGB值,而是把图像中的所有颜色编制成颜色表。在表示图片中每个像素的颜色信息时,使用颜色表的索引。由于位深度不超过8位,颜色表不超过256个数值。
  调色板决定于位于0x2e~0x31的biClrUsed字段和位于0x1c~0x1d的biBitCount字段,每个元素的类型是一个 RGBQUAD 结构,顺序为B,G,R及保留字,每一个元素占用一个字节。

typedef struct tagRGBQUAD {
    BYTE rgbBlue; /*指定蓝色分量*/
    BYTE rgbGreen; /*指定绿色分量*/
    BYTE rgbRed; /*指定红色分量*/
    BYTE rgbReserved; /*保留,指定为0*/
} RGBQUAD;

  (4)位图数据ImageData。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值。对于真彩色图,图像数据就是实际的RGB值。
  图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是 4 的整倍数,也就是DWORD 对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。
  表1是两幅具体图片及其文件内容,表格里只列出了文件头和信息头中不同部分的值。





这里写图片描述
这里写图片描述



二进制方式打开
这里写图片描述这里写图片描述

图像基本信息
分辨率:1280×720
位深度:8位索引颜色
文件大小:901KB=922,680B
分辨率:1280×1280
位深度:24位真彩色
文件大小:4.68MB=4,915,256B
0x0002~0x000538 14 0e 0038 00 4b 00
0x000a~0x000d36 04 00 0036 00 00 00
0x0016~0x0019d0 02 00 0000 05 00 00
0x001c~0x001d08 0018 00
0x0022~0x002502 10 0e 0002 00 4b 00

表1 BMP文件的组成

  • 0x00~0x01,标记了此文件为BMP文件,内容是B和M两个字母的ASCII码。
  • 0x02~0x05,以字节为单位的文件大小。左图大小为922,680字节,写成十六进制就是E1438。在这里可以看出BMP文件的小端保存方式,先保存低位字节,再保存高位字节,保存顺序为38、14、0e。同理,右图为4,915,256(4B0038H)个字节,保存顺序为38、00、4b。
  • 0x0a~0x0d,实际数据的字节偏移量。BMP文件共有的前两部分文件头占了54(36H)个字节,左图包含了8位也就是256种索引颜色的调色板,调色板中每个颜色的RGBQUAD结构占用4字节,总共1024个字节的调色板信息。因此左图的偏移量为54+1024=1078(436H)个字节。
  • 0x16~0x19,左图高度为720(2D0H)个像素,右图高度为1280(500H)个像素。
  • 0x1c~0x1d,左图位深度为8(8H),右图位深度为24(18H)。
  • 0x22~0x25,BMP图像大小biSizeImage可由下式计算
    biSizeImage=cx×biBitCount+3132×4×cy+2
    其中, cxcy 表示水平和垂直方向的像素数。 cx×biBitCount 表示一行图像占了多少位。BMP规定这个图像大小必须是4字节的整数倍,也就是32位的整数倍,因此需要把 cx×biBitCount 加31再除以32后下取整,就保证了计算结果是离这个数最近的而且是比它大的32的倍数,也就保证了是4字节的整数倍。乘以4和行数,得到4字节整数倍的图像大小。
    另外,BMP文件的末尾两个字节是保留位,无论图像是什么这两个字节都为0,因此最后计算结果还要加上2字节。图像大小biSizeImage+字节偏移量bfOffBits=文件大小bfSize。左图的图像大小经过计算为921,602(E1002H)字节,右图图像大小为4,915,202(4B0002H)字节。
  • 0x26~0x29,水平分辨率。两幅图分辨率都是计算机上常见的72dpi,也就是72像素/英寸=28.346像素/厘米。以像素/米单位表示就是2834(B12H)。后面四个字节的垂直分辨率同理。

2.位深度的各种存储方式

  BMP文件的位深度有以下几种:1,2,4,8,16,24,32。其中1,2,4,8位对应索引颜色,图像包含调色板数据。16,24,32位直接保存像素的颜色值,这三种位深度的像素保存方式有多种,见图1。

这里写图片描述
图1 Photoshop中BMP文件的位深度选项
  位深度选项中RGB代表红绿蓝三通道值,X代表保留位(空),A代表透明度信息(Alpha),数字代表位数。例如16位的A1 R5 G5 B5表示第一位为透明度信息,0代表像素不透明,1代表像素透明,2~6位为R通道信息,7~11位为G通道信息,12~16位为B通道信息。在32位的BMP文件中,透明度还可以用更精细的8位方式表示,实现像素的不同透明程度。本实验中对于非索引颜色的位图不考虑保留位和透明度信息,仅对16位R5 G6 B5方式保存的BMP文件和24位R8 G8 B8方式保存的BMP文件进行数据读写操作。

3.颜色转换公式

YUV=0.29900.16840.50000.58700.33160.41870.11400.50000.0813RGB+0128128
  转换公式与实验一相同,具体分析可见第一次的实验报告。

二、实验流程及代码分析

1. 主函数的流程

  BMP转YUV可由以下几步构成

Created with Raphaël 2.1.0 读取参数,打开文件 读取BMP文件头信息 创建缓冲区 调用函数进行转换 清理内存

  下面是关键代码分析,代码说明在每段代码的下方。

//--------main_bmp2yuv.cpp--------
//头文件,读取命令行参数,打开文件与之前类似,略过
#include <windows.h>
...
//读取BMP文件头,信息头,读取错误时的处理代码略过
BITMAPFILEHEADER file_header;
BITMAPINFOHEADER info_header;
if (fread(&file_header, sizeof(BITMAPFILEHEADER), 1, bmpFile) != 1)
...
if (file_header.bfType != 0x4D42)
{
    printf("Not BMP file.\n");
    exit(1);
}
if (fread(&info_header, sizeof(BITMAPINFOHEADER), 1, bmpFile) != 1)
...
//读取图像尺寸
int width = info_header.biWidth;
int height = info_header.biHeight;

  BMP文件的结构组成包含在windows.h中,可以使用BITMAPFILEHEADER和BITMAPINFOHEADER两种数据类型方便地对文件头和信息头进行组织,这两种数据类型包含了第一部分中的全部属性。另外还有RGBQUAD类型,用来组织调色板。
  首先读取文件头file_header,如果前两字节不是42H和4DH(B字母和M字母的ASCII码)说明不是BMP文件,退出。读完文件头之后文件指针就指向了信息头的起始,这时就可以进行读取信息头info_header的操作。读完两部分数据之后,使用信息头的biWidth和biHeight属性读取图像宽高信息,为下一步创建缓冲区做准备。

//开辟缓冲区,yuv缓冲区部分略过
unsigned char* rgbBuf = (unsigned char*)malloc(width*height * 3);
...
//BMP与RGB的转换,得到RGB数据
if (BMP2RGB(file_header, info_header, bmpFile, rgbBuf))
{
    printf("BMP2RGB error\n");
    exit(1);
}
//RGB与YUV的转换,得到YUV数据
int flip = 0if (RGB2YUV(width, height, rgbBuf, yBuf, uBuf, vBuf, flip))
{
    printf("RGB2YUV error\n");
    exit(1);
}
//之后与RGB转YUV类似,例如对超出电平值的处理,关闭缓冲区等,略过
...

  BMP文件先通过BMP2RGB函数转成RGB数据,再通过RGB2YUV函数转成YUV数据。读取到的图像数据是倒序存放的,flip=0保证了RGB2YUV可以正确地对其转换,具体会在后面的分析中说明。下面开始分析BMP2RGB函数的具体细节。

2. BMP2YUV函数的流程

Created with Raphaël 2.1.0 判断实际字节数 创建缓冲区,读取像素值 根据位深度不同进行相应处理
//--------bmp2rgb.cpp--------
//头文件,函数声明等略过
...
//判断像素的实际点阵数
int w, h;
w = (info_h.biWidth*info_h.biBitCount + 31) / 32 * 4;//w为实际一行的字节数
h = info_h.biHeight;//h为列数

//开辟实际字节数量的缓冲区,读数据,一次读取一个字节
unsigned char* dataBuf = (unsigned char*)malloc(w*h);
fseek(pFile, file_h.bfOffBits, 0);
fread(dataBuf, 1, w*h, pFile);
unsigned char* data = dataBuf;
unsigned char* rgb = rgbBuf;

  在BMP文件组成中说到,BMP文件规定一行的字节数必须是4字节的倍数,如果不是要补充相应的位数使之成为4的倍数。因此实际的字节数可能比像素数×位深度要多。使用上面说到的计算biSizeImage的公式可以得到实际每行有多少字节。由于整个文件最后两字节为保留字节,与获取像素值无关,因此这里w最后没有加2。
  得到了实际存储的字节数后就可以开辟数据缓冲区了,使用文件头的字节偏移属性bfOffBits直接把文件指针定位到像素值数据的起始,开始读取数据,一次读取一个字节。
  之后就是要根据量化位数的不同进行不同的操作,下面分别对其进行说明。
  (1)8位及以下索引文件的读取
  对于索引颜色,读取流程如下

Created with Raphaël 2.1.0 创建调色板缓冲区,读调色板数据 根据量化位数的不同计算蒙版值 通过移位操作获取索引值
//调色板的字节数的初始值colorCnt
int colorCnt = pow(2, (float)info_h.biBitCount);
//如果实际用到的颜色不为0,则以biClrUsed为准
if (info_h.biClrUsed != 0)
    colorCnt = info_h.biClrUsed;
//分配调色板缓冲区
RGBQUAD* pRGBBuf = (RGBQUAD*)malloc(sizeof(RGBQUAD)*colorCnt);
if (pRGBBuf == NULL)
{
    printf("Not enough memory\n");
    return 1;
}
//调色板与文件起始处相距(文件头 + 信息头)个字节
fseek(pFile, sizeof(BITMAPFILEHEADER) + info_h.biSize, 0);
//读调色板数据
fread(pRGBBuf, sizeof(RGBQUAD), colorCnt, pFile);
RGBQUAD* pRGB = pRGBBuf;

  索引颜色都有调色板,也就是索引值表。因此首先要把调色板保存下来。这里与课件中创建调色板的代码略有不同:

RGBQUAD *pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(unsigned char)pow(2,info_h.biBitCount));
...
fread(pRGB_out,sizeof(RGBQUAD),(unsigned int)pow(2,info_h.biBitCount),pFile);

  课件中调色板的大小设为了2的biBitCount次方,一个n比特量化的BMP图像调色板的实际颜色数取决于biClrUsed的值。图2展示了这样一个例子。这是一幅30×30像素的图像,图像在右下角,4bit量化,但是整幅图里只有黑白灰三种颜色。

这里写图片描述
图2 一张只有三种颜色的BMP图像
  信息头的几个关键字节用红色标出。0x0a~0x0d处的bfOffBits为42H,也就是从0x42开始为实际图像数据。0x1c~0x1d处的biBitCount表明图像是4位的,但是0x2e~0x31处的biClrUsed却表明图像只使用了三种颜色。调色板从0x36开始到0x41结束,共有12字节,储存了三种颜色。因此应以biClrUsed为准构建调色板。
  从图2中也可以看出图像的存储方式是由底向上存储的。索引值表中第0个颜色是白色,第1个颜色是灰色,第2个颜色是黑色。0x42中前4位的值为2,说明存储的是黑色。图像左下角像素为黑色,左上角像素为白色,因此扫描行是由底向上存储的。
  得到了调色板颜色数目colorCnt之后把文件指针移动到调色板的起始位置,也就是文件头加信息头个字节后,把调色板读取到缓冲区内。

for (j = 0; j < h; j++)//j控制行循环
{
    int pixCnt = 0;//pixCnt用于判断一行是否结束
    for (i = 0; i < w; i++)//i控制列循环
    {
        unsigned char mask;                         
        //根据不同的量化位数计算蒙版值
        switch (info_h.biBitCount)
        {
        case 1:
            mask = 0x80;//1000 0000
            break;
        case 2:
            mask = 0xC0;//1100 0000
            break;
        case 4:
            mask = 0xF0;//1111 0000
            break;
        case 8:
            mask = 0xFF;//1111 1111
            break;
        }
...

  由于读取文件的缓冲区data是一个字节一个字节读取的,但是只有8位量化的索引值才足够一个字节,1,2,4位的索引值都不足一个字节,也就是一个字节里包含了多个像素的信息。因此在读取像素索引值时需要一个“蒙版”遮住不需要的索引值,读完当前像素值把该蒙版向后移动相应的位数,显示出下一个像素值的信息。“遮住”的具体操作就是与运算,把不需要的位与0,需要的位与1,根据这个规则,计算出蒙版值。
  对于1位的图像,一个字节里的第一位为第一个像素的值,因此蒙版值为1000 0000,也就是0x80。2,4,8位的图像同理。

int shiftCnt = 1;//shiftCnt控制蒙版的移位
while (mask)
{
    //通过移位操作获取当前像素的索引值
    unsigned char index = mask == 0xFF ?
        data[i + w*j] : ((data[i + w*j] & mask) >> (8 - shiftCnt * info_h.biBitCount));
    *rgb = pRGB[index].rgbBlue;//B
    *(rgb + 1) = pRGB[index].rgbGreen;//G
    *(rgb + 2) = pRGB[index].rgbRed;//R
    //如果是8位,开始读下一个字节,如果不是继续读当前字节
    if (info_h.biBitCount == 8)
        mask = 0;
    else
        mask >>= info_h.biBitCount;
    rgb += 3;
    shiftCnt++;
    //如果到了行尾补充位则直接转到下一行
    pixCnt++;
    if (pixCnt == info_h.biWidth)
    {
        i = w - 1;
        break;
    }
//释放缓冲区等操作略过
...

  得到了蒙版,就可以读取索引值了。使用条件运算符判断当前图像是不是8位的图像,如果是,则直接逐字节读取索引值index,while循环结束,继续下一字节的读取。
  如果不是8位的图像,需要将当前字节data[i + w*j]与蒙版mask做与运算。与运算结束后,有效位还在高位,比如2位的索引值,这时是××000000,与需要的索引值××(000000××)相比需要向右移动6位,也就是8-biBitCount。读完之后一个字节还有后面其它像素的索引值,因此需要把mask向右移动相应的位数biBitCount,继续while循环,这时读到的索引值为00××0000,需要向右移动4位得到正确的索引值,也就是8-2*biBitCount。因此可以归纳出每次移位的位数是8-shiftCnt*biBitCount。
  读取像素信息时行列分别进行循环,是为了方便判断是否达到行结尾处的补充位。pixCnt保存了当前一行里已经读了多少个像素的值,如果达到一行的像素数biWidth,则跳过后面的补充位,进到下一行。仍以图2中30×30像素4位的BMP图像为例,根据biSizeImage的计算公式算出一行实际字节数为16字节,有效位数为30*4=120bit,需要补充8位。实际图像数据从0x42开始,经过120bit(15字节)后到0x50,接下来的8位0x51就是补充的数据位,读取图像索引值时要跳过这样的位,把行循环i置为行结尾w-1,这样就可以进行新一行的循环。
  另外在图2中可以看出,补充位不一定都是0,比如0xc1,0x101,0x111处都是补充位,但是这些字节里的某些位不是0。
  (2)16位R5 G6 B5格式图像的读取

for (j = 0; j < h; j++)//j控制行循环
{
    int pixCnt = 0;
    for (i = 0; i < w; i+=2)//i控制列循环
    {
        *rgb = (data[i + w*j] & 0x1F) << 3;//B
        *(rgb + 1) = ((data[i + w*j] & 0xE0) >> 3) + ((data[i + w*j + 1] & 0x07) << 5);//G低位+G高位
        *(rgb + 2) = data[i + w*j + 1] & 0xF8; //R
        rgb += 3;
        //如果到了行尾补充位则直接转到下一行
        pixCnt++;
        if (pixCnt == info_h.biWidth)
            i = w - 1;
    }
}

  以这种方式保存的图像RGB也不是整字节的值,仍需要用蒙版进行与运算然后移位。16位2个字节中RGB的保存顺序如下

×××××R×××G1×××G2×××××BG
  第一个字节前5位保存红色R,后3位保存绿色G的高3位G1;第二个字节前3位保存绿色G的低3位G2,后5位保存蓝色B。存储时使用小端存储,就变成了这样
×××G2×××××B×××××R×××G1
  因此在读取实际的R,G,B值时,一次要读两个字节,并做以下操作

  • 将第一个字节data[i + w*j]与蒙版0x1F(0001 1111)与运算得到蓝色值,这时得到的值都在字节的低5位,范围是0~31,所以还要左移3位扩大到0~248,由于后3位都是0,因此B的值以8为单位递增。
  • 将第二个字节data[i + w*j + 1]与蒙版0x07(0000 0111)与运算得到绿色的高3位G1,左移5位搬移到整个字节的高位;第一个字节与蒙版0xE0(1110 0000)与运算得到绿色的低3位G2,右移3位接到G1的后面。绿色值为0~252,后2位都是0,以4为单位递增。
  • 将第二个字节与蒙版0xF8(1111 1000)与运算得到红色值,此时已经在高5位,范围是0~248,不需要移位,也以8为单位递增。

  (3)24位R8 G8 B8格式图像的读取

for (j = 0; j < h; j++)//j控制行循环
{
    int pixCnt = 0;
    for (i = 0; i < w; i += 3)//i控制列循环
    {
        *rgb = data[i + w*j];//B
        *(rgb + 1) = data[i + w*j + 1];//G
        *(rgb + 2) = data[i + w*j + 2];//R
        rgb += 3;
        //如果到了行尾补充位则直接转到下一行
        pixCnt++;
        if (pixCnt == info_h.biWidth)
            i = w - 1;
    }
}

  这种方式下RGB各占1字节,因此每次只要顺序读取即可。小端方式保存,保存顺序是B,G,R。读完之后前进3字节读取下一颜色值。

三、实验结果与总结

  使用5张不同位深度的BMP图像进行测试,其中4位、8位、24位图各一张,16位图两张。分辨率均为800×540。

这里写图片描述
图3 测试用图像序列
  写入YUV序列时,遍历图像中的每一像素值,相邻两图按照一张图的权重递增,另一张图权重递减的顺序相加,最终实现一个简单的渐变效果。部分代码如下

//--------main_yuvSequence.cpp--------
//命令行参数:输入图像数,输入图像文件名列表,输出文件名,图像宽度,图像高度
//包含头文件等略过
const int FRAME_NUM = 200;//总帧数
...
int imgCount = atoi(argv[1]);//图像总数
char** yuvFileName = (char**)malloc(imgCount);//图像名的数组
FILE** yuvFile = (FILE**)malloc(imgCount);//图像文件指针的数组
for (i = 0; i < imgCount; i++)
{
    yuvFileName[i] = argv[i + 2];//依次获取各文件名
    yuvFile[i] = fopen(yuvFileName[i], "rb");//依次打开各文件
    if (yuvFile[i] == NULL)
    ...
}
...
int width = atoi(argv[imgCount + 3]);//图像宽度
int height = atoi(argv[imgCount + 4]);//图像高度
unsigned char* yBuf1 = (unsigned char*)malloc(width*height*1.5);//前一幅图像的缓冲区
unsigned char* yBuf2 = (unsigned char*)malloc(width*height*1.5);//后一幅图像的缓冲区
unsigned char* yBuf3 = (unsigned char*)malloc(width*height*1.5);//计算结果图像的缓冲区
...

  输入图像为一个文件数组,数组大小由命令行参数控制,循环读入各个文件。实现此效果需要对YUV三个分量都进行处理,因此直接把YUV一起读入到缓冲区中,缓冲区大小为Y通道的1.5倍。

int delay = FRAME_NUM / (imgCount - 1);//过渡的持续时间
fread(yBuf1, 1, width*height*1.5, yuvFile[0]);//读入第一张图像
for (i = 0; i < (imgCount - 1); i++)//每张图像的循环
{
    fread(yBuf2, 1, width*height*1.5, yuvFile[i+1]);//读入后一幅图像
    for (j = 0; j < delay; j++)//每一段过渡的循环
    {
        double alpha = (double)j / (double)delay;//计算权值系数
        for (k = 0; k < width*height*1.5; k++)//每一帧的循环
        {
            yBuf3[k] = (1 - alpha)*(yBuf1[k]) + alpha*(yBuf2[k]);//相邻两幅图按照权值相加,得到目标像素值
        }
        fwrite(yBuf3, 1, width*height*1.5, yuvSeqFile);//将计算后的叠加图像写入到文件
    }
    memccpy(yBuf1, yBuf2, 1, width*height*1.5);//把后一幅图像的数据复制到前一幅图像的缓冲区中,为下一张图像做准备
}
//释放缓冲区等略过
...

  实现方法很简单,但是效率比较低,对每张图的像素值要单独计算得出结果,一次写入一帧画面。由于读取时的顺序为YUV,输出时也是先计算完Y分量的值写入文件,再计算U分量的值写入文件,等等。实验结果如表2所示,其中截取了整个序列中的某些帧

这里写图片描述这里写图片描述这里写图片描述
这里写图片描述这里写图片描述这里写图片描述
这里写图片描述这里写图片描述这里写图片描述

表2 测试结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值