数据压缩原理与应用 BMP转YUV

BMP到YUV转换:数据压缩原理及实现
本文介绍了RGB到YUV颜色空间转换的原理,详细讲解了BMP文件格式,包括位深度、存储方式和文件头结构。通过实验流程分析,展示了24Bit BMP图像的处理,包括读取、转换和写入YUV文件的过程。实验结果验证了转换的正确性,并指出了可能的错误和解决方案。

1.实验原理

RGB转YUV的公式
  • Y=0.30 * R+0.59 * G+0.11 * B
  • U=0.493 * (B-Y)
  • V=0.877 * (R-Y)
位深度和存储方式

BMP 文件的图像深度可选 lbit、4bit、8bit、16bit 及 24bit,8位图像可以是索引彩色图像外,也可以是灰阶图像。
如果图像是16位、24位,则图像文件中不保存调色板,则不存在调色板数据给出。如果图像是1位、4位或者8位,则紧跟其后的是位图数据,位图数据是指向调色板的索引序号。
BMP文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。根据颜色深度的不同,图像上的一个像素可以用一个或者多个字节表示,它由n/8所确定(n是位深度,1字节包含8个数据位)。图片浏览器等基于字节的ASCII值计算像素的颜色,然后从调色板中读出相应的值。
n位2n种颜色的位图近似字节数可以用下面的公式计算:BMP文件大小约等于 54+42n+(whn)/8,其中高度和宽度都是像素数。需要注意的是上面公式中的54是位图文件的文件头,是彩色调色板的大小。另外需要注意的是这是一个近似值,对于n位的位图图像来说,尽管可能有最多2n中颜色,一个特定的图像可能并不会使用这些所有的颜色。由于彩色调色板仅仅定义了图像所用的颜色,所以实际的彩色调色板将小于计算的值。

bmp文件格式

这里写图片描述

  • bmp位图文件头
    typedef struct tagBITMAPFILEHEADER {
            WORD  bfType; /* 说明文件的类型  */
            DWORD  bfSize; /* 说明文件的大小,用字节为单位  */                                                                    
                                           /*注意此处的字节序问题
            WORD  bfReserved1; /* 保留,设置为0 */
            WORD  bfReserved2; /* 保留,设置为0 */
            DWORD  bfOffBits; /* 说明从BITMAPFILEHEADER结构开始到实际的图像数据之间的字节偏移量 */
    }   BITMAPFILEHEADER;
  • bmp位图信息头
    typedef struct tagBITMAPINFOHEADER { 
            DWORD  biSize; /* 说明结构体所需字节数 */
            LONG  biWidth; /* 以像素为单位说明图像的宽度 */
            LONG  biHeight; /* 以像素为单位说明图像的高速 */
            WORD  biPlanes; /* 说明位面数,必须为1 */
            WORD  biBitCount; /* 说明位数/像素,1、2、4、8、24 */
            DWORD  biCompression; /* 说明图像是否压缩及压缩类型:BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS */
            DWORD  biSizeImage; /*  以字节为单位说明图像大小 ,必须是4的整数倍*/
            LONG  biXPelsPerMeter; /*  目标设备的水平分辨率,像素/米 */
            LONG  biYPelsPerMeter; /*目标设备的垂直分辨率,像素/米 */
            DWORD  biClrUsed; /* 说明图像实际用到的颜色数,如果为0,则颜色数为2的biBitCount次方 */
            DWORD  biClrImportant; /*说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。*/
    }  BITMAPINFOHEADER;
  • bmp调色板

调色板实际上是一个数组,它所包含的元素与位图所具有的颜色数相同,决定于biClrUsed和biBitCount字段。数组中每个元素的类型是一个RGBQUAD结构,每个1字节。真彩色无调色板部分。所以深度小于等于8时,位图有调色板,称为索引颜色此时文件不存储RGB而从LUT中寻找对应的颜色。由于位深度不超过8位,颜色表不超过256个数值。

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

    紧跟在调色板之后的是图像数据字节阵列。对于用到调色板的位图,图像数据就是该像素颜色在调色板中的索引值(逻辑色)。对于真彩色图,图像数据就是实际的 R、G、B值。图像的每一扫描行由表示图像像素的连续的字节组成,每一行的字节数取决于图像的颜色数目和用像素表示的图像宽度。规定每一扫描行的字节数必须是 4 的整倍数,也就是DWORD 对齐的。扫描行是由底向上存储的,这就是说,阵列中的第一个字节表示位图左下角的像素,而最后一个字节表示位图右上角的像素。


下面通过一张24Bit的BMP图像(512x288)说明文件数据的内容
这里写图片描述
每行16个数据,存储bmp图像。
可看出0x00-0x0D存储位图文件头,0x0E-0x35存储位图信息头,0x36-0x39存储调色板。
0x00~0x01,标记了此文件为BMP文件,内容是B和M两个字母的ASCII码,0x4D42.
0x02~0x05,以字节为单位的文件大小,可以看出其文件大小是0x0006C036,转换成十六进制即为442422,而文件本身是432KB,二者近似相等。而且文件大小的存储方式是低位在前高位在后。
0x12~0x15,00000200,即文件宽度为512。
0x16~0x19,00000120,即文件高度为288。
0x1c~0x1d,0018,即位数为24bit。


2.实验流程分析

  1. 程序初始化(打开两个文件、定义变量和缓冲区等)
  2. 读取BMP文件,抽取或生成RGB数据写入缓冲区
读取BMP文件的具体流程
读位图文件头(判断是否可读出;判断是否为BMP文件)
读位图信息头(判断是否读出)
判断像素的实际点阵数
开辟缓冲区,读数据,倒序存放
根据每像素位数的不同,执行不同的操作
8bit以下16bit24bit
构造调色板位与移位取像素数据转换为8bit彩色分量写RGB缓冲区直接取像素数据写RGB缓冲区
位与移位取像素数据查调色板写RGB缓冲区

3. 调用RGB2YUV的函数实现RGB到YUV数据的转换
4. 写YUV文件
5. 程序收尾工作(关闭文件,释放缓冲区)


3.关键代码分析

头文件和函数与上个实验相同,在此省去
共有5幅图像需要读,所以用 for(j = 0; j < 5; j++) 设置循环
设置命令行参数,即先输入bmp文件名称再输入需要显示的帧数。

    bmpFileName = argv[j+1];
    bmpFrame = atoi(argv[j+6]); 

输入bmp文件后,需要证明bmp文件是否存在,读取bmp文件的文件头和信息头,若文件不存在,则说明文件损坏或者读取有错误,可调试

    if (((Info_header.biWidth / 8 *Info_header.biBitCount) % 4) == 0)
        width= Info_header.biWidth/ 8 * Info_header.biBitCount;
    else
        width =(Info_header.biWidth*Info_header.biBitCount + 31) / 32 * 4;
/*bmp的一行所占的字节数需要是4的倍数,若不满足,则要补齐位数使其成为4的整数倍,本实验中图片设置的大小是512x288,512÷8=64,满足是4的倍数,所以不需要补齐,若大小是600x338,则需要补齐,但若希望yuv的大小依然是600x338,则需要舍弃补齐的那一位。*/
    if ((Info_header.biHeight % 2) == 0)
        height = Info_header.biHeight;
    else
        height = Info_header.biHeight + 1;
/*同样bmp的字节高度也应该是偶数,否则需要加上一行成为偶数。*/

开空间

    bmpBuf = (u_int8_t*)malloc(Info_header.biWidth * Info_header.biHeight * 3);//存放rgb缓存数据
    /* get the output buffers for a frame */
    yBuf = (u_int8_t*)malloc(frameWidth * frameHeight);
    uBuf = (u_int8_t*)malloc((frameWidth * frameHeight) / 4);
    vBuf = (u_int8_t*)malloc((frameWidth * frameHeight) / 4);
    rgbDataOut = (u_int8_t*)malloc(width * height );//存放bmp的数据,开的空间大小为字节宽度x高度
    unsigned char *rgb;
    rgb = bmpBuf;//定义一个指针指向rgb缓存数据空间

判断是否有调色板

RGBQUAD *pRGB = (RGBQUAD *)malloc(sizeof(RGBQUAD)*(unsigned int)pow(2, Info_header.biBitCount));

在判断调色板是否存在时,注意因为调色板的大小是2的比特数次方,而bmp图片在位深度为1bit,4bit,8bit时存在调色板,而biBitCount=8时,调色板的大小为256,对于之前(unsigned char)pow(2, Info_header.biBitCount)的程序会超出范围,所以改为unsigned int
下面对不同位深度进行分析
1. 24bit
因为24bit位真色彩,没有调色板也不需要补充字节数,直接倒序读出数据再进行rgb转yuv的程序。

    fread(bmpBuf, 1, frameWidth*frameHeight * 3, bmpFile);

bmpBuf是存放rgb缓存数据,所以直接读取。
2. 16bit
位与移位取像素数据转换为8bit,彩色分量写RGB缓冲区

    fread(rgbDataOut, 1, width * height, bmpFile);
    if (Info_header.biCompression == BI_RGB)
    {
        for (Loop = 0; Loop < height * width; Loop += 2)
        {   
            *rgb = (rgbDataOut[Loop] & 0x1F) << 3;//B
            *(rgb + 1) = ((rgbDataOut[Loop] & 0xE0) >> 2) + ((rgbDataOut[Loop + 1] & 0x03) << 6);//G
            *(rgb + 2) = (rgbDataOut[Loop + 1] & 0x7C) << 1;//R
            rgb += 3;
        }
    }

rgbDataOut存放bmp的数据,rgb是指向bmpBuf即rgb缓存数据的指针,16bit不是真色彩,所以将读取的bmp文件数据存入rgbDataOut进行处理,再将处理过的数据存入bmpBuf,最后读出。
这里写图片描述
将第一个字节rgbDataOut[Loop]与0x1F(0001 1111)与运算且左移3位(1111 1000)得B。将第一个字节rgbDataOut[Loop]与0xE0(1110 0000)与运算后右移两位(0011 1000),再将第二个字节rgbDataOut[Loop+1]与0x03(0000 0011)与运算后左移6位(1100 0000),加起来(1100 0011)得G;第一个字节与0x7C(0111 1100)与运算左移1位(1111 1000)得R。
3. 1、4、8bit

    for (Loop = 0; Loop < height*width; Loop++)
        {
            switch (Info_header.biBitCount)
                {
                    case 1:
                        mask = 0x80;//1000 0000
                        break;
                    case 4:
                        mask = 0xF0;//1111 0000
                        break;
                    case 8:
                        mask = 0xFF;//1111 1111
                        break;
                    }
            int shiftCnt = 1;//控制调色板的移位
            while (mask)
            {
                unsigned char index = mask == 0xFF ? rgbDataOut[Loop] : ((rgbDataOut[Loop] & mask) >> (8 - shiftCnt * Info_header.biBitCount));
                *rgb = pRGB[index].rgbBlue;
                *(rgb + 1) = pRGB[index].rgbGreen;
                *(rgb + 2) = pRGB[index].rgbRed;

                if (Info_header.biBitCount == 8)
                    mask = 0;
                else
                    mask >>= Info_header.biBitCount;
        /* 如果是8位,开始读下一个字节,如果不是,继续读当前字节*/
                rgb += 3;
                shiftCnt++;
            }
        }

由于读取文件的缓冲区rgbdataOut是一个字节一个字节读取的,一个字节里包含了多个像素的信息。因此在读取像素索引值时需要定义一个遮罩,遮住不需要的值,读完当前像素值把该蒙版向后移动相应的位数,显示出下一个像素值的信息。不需要的为0,需要的为1。
因为8bit的图像索引值是一个字节,其它的都没有1个字节,比如8bit第一个像素就在第一个字节,4bit的第一个像素在第一个字节的前4位,1bit的第一个像素在第一个字节的第一个,所以8bit的遮罩为1111 1111,4bit的遮罩为1111 0000,1bit的遮罩为1000 0000,然后不断右移以获得不同像素值。
最后读文件

    if (RGB2YUV(frameWidth, frameHeight, bmpBuf, yBuf, uBuf, vBuf, flip))
                {
                    printf("error");
                    return 0;
                }

                for (i = 0; i < frameWidth*frameHeight; i++)
                {
                    if (yBuf[i] < 16) yBuf[i] = 16;
                    if (yBuf[i] > 235) yBuf[i] = 235;
                }

                for (i = 0; i < frameWidth*frameHeight / 4; i++)
                {
                    if (uBuf[i] < 16) uBuf[i] = 16;
                    if (uBuf[i] > 240) uBuf[i] = 240;
                    if (vBuf[i] < 16) vBuf[i] = 16;
                    if (vBuf[i] > 240) vBuf[i] = 240;
                }

                for (i = 0; i < bmpFrame; i++)
                {
                    fwrite(yBuf, 1, frameWidth * frameHeight, yuvFile);
                    fwrite(uBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
                    fwrite(vBuf, 1, (frameWidth * frameHeight) / 4, yuvFile);
                    printf("\r...%d", ++videoFramesWritten);
                }

4.实验结果

1.错误分析

-读取文件个数的循环和读取帧数的循环共用了一个变量i,所以只能读出第一幅画面的内容。
-因为调色板的大小是2的比特数次方,而bmp图片在位深度为1bit,4bit,8bit时存在调色板,而biBitCount=8时,调色板的大小为256,对于之前(unsigned char)pow(2, Info_header.biBitCount)的程序会超出范围,所以改为unsigned int
-下为错误的程序。最开始没有理清bmpBuf,rgbDataOut的关系,在写程序的时候,用来计算的数据用的是bmpBuf内的数据,而指针用的是rgbDataOut,此时出现的问题是,将rgn缓存数据赋给了bmp本身的数据。与整个实验的思路相反,将顺序颠倒后,也没有再设置一个指向写入rgb缓存数据的指针,导致在执行完这段循环后,继续使用bmpBuf时,超出了空间范围。

    for (Loop = 0; Loop < height * width; Loop += 2)
            {   
                *rgbDataOut= (bmpBuf[Loop] & 0x1F) << 3;//B
                *(rgbDataOut+ 1) = ((bmpBuf[Loop] & 0xE0) >> 2) + ((bmpBuf[Loop + 1] & 0x03) << 6);//G
                *(rgbDataOut + 2) = (bmpBuf[Loop + 1] & 0x7C) << 1;//R
                rgbDataOut+= 3;
            }

-在调用函数if(RGB2YUV(frameWidth, frameHeight, bmpBuf, yBuf, uBuf, vBuf, flip))时,依然是没有理清两个数据空间的关系,将bmpBuf写成rgbDataOut ,但因为调用函数时应用的是rgb缓存数据,所以未读出图像。
-开空间的时候,用到了width和height,但是给width和height赋值的程序写在了开空间程序之后,导致开的空间大小错误。

2.实验结果

图像展示(512x288)
这里写图片描述
命令行参数的输入
这里写图片描述
结果展示
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
将命令行参数输入的参数乱序,得到另一个结果,说明实验的正确性
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值