【实验一】彩色空间变换

来源:互联网 发布:linux ctrl u 编辑:程序博客网 时间:2024/05/03 13:14

实验一、彩色空间转换

 

一、概述

本次实验的重点是为了进一步理解彩色空间的概念并掌握不同彩色空间转换的基本方程。不仅如此,本次实验还涉及到了图像的色度取样格式,查找表的算法思想以及C环境下的文件操作、数据类型等知识。

本次实验可以说是对与程序设计分手已久的我们的一次唤醒,让我们重新捡起了编程这把武器,真正做到理论与实操结合。

 

二、实验涉及的基本原理

1、涉及原理汇总

涉及原理有:YUV与RGB相互转换公式,YUV图像色度取样格式,YUV图像的码电平的分配,以及一些关于C的数据类型以及算法思想。

 

2、YUV与RGB的相互转换公式

YUV与RGB的相互转换公式运用中学的方程同解变形的知识便能够相互推出,如何通过RGBTOYUV方程组得到YUVTORGB方程组并不重要。重要的是公式中蕴含的恒定亮度原理以及人眼的视觉特性(Y = 0.2990R + 0.5870G + 0.1140B)。

(1)RGB TO YCbCr

Y = 0.2990R + 0.5870G + 0.1140B;

R-Y = 0.7010R - 0.5870G - 0.1140B;

B-Y = -0.2990R - 0.5870G + 0.8860B;

(2)RGB TO YUV

Y = 0.2990R + 0.5870G + 0.1140B;

V = 0.5R - 4187G - 0.0831B;

U = -0.1684R - 0.3316G + 0.5B;

(3)YUV TO RGB

R = Y + 1.14022V;

G = Y - 0.34414U - 0.71420V;

B = Y + 1.7720U;

 

3、YUV图像色度取样格式

本次实验需要转换得到的YUV色度取样格式是4:2:0型的,即U、V的取样频率均为Y的1/4,也就是说在水平与垂直方向上的取样点均为Y的一半。

因此,我们完成RGB到YUV后要进行下采样,而从YUV到RGB前要先进行插值。

【图】

 

4、YUV图像码电平的分配

YUV图像在进行8bit均匀量化时,共分为256级。

在彩色数字电视系统中,对Y分量信号在256级上端留20级,下端留16级作为信号动态范围的保护带。

而对UV分量除此之外还需要通过加128码电平的操作,将原本的-0.5至0.5的动态范围变为与Y分量相同的0至1的动态范围,并在256级上端留15级,下端留16级作为信号动态范围的保护带。

 

5、C的基本数据类型以及算法思想

数据类型这一部分内容是比较琐碎的,但却是真正反映同学们编程熟练度的地方,例如unsigned char是8bit的,而float 则是32bit的。如果像我一样用错会出各种各样的问题,这一部分我将在第四章节——关键代码及其分析中介绍。

查找表法这种算法能很模块化的实现彩色空间变换这步操作,并便于修改相关系数。同时对一个8bit量化的图像来说,只需要256*k次计算(k为系数个数,本实验中为4或7个)便可完成查找表本身的计算。如果不使用查找表,遇到一个像素计算一次(以256*256的图像为例),那我们面临的至少是256*256次以上的计算,其中就包含着大量重复计算,更别说对更大的图像或视频进行彩色空间变换。

 

三、实验的流程分析

1、实验的两大模块

本次实验分为两大模块,首先是通过已给出的RGB2YUV程序熟悉编程环境并了解大体的彩色空间变换原理,然后是基于RGB2YUV的程序仿写出自己的YUV2RGB程序并验证正确性。

2、现有的RGB2YUV的流程

(1)初始化;

(2)对RGB图像的操作处理;

其中调用RGB2YUV()并完成码电平分配在main()中实现,而查找表的计算、用RGB2YUV方程组实现彩色空间转换以及UV的下采样通过RGB2YUV实现。

总结下对RGB图像操作的流程:计算查找表加128电平偏置完成彩色空间转换UV下采样YUV码电平的动态范围保护。大致可以分为以上4个步骤以及4个功能。

(3)关文件、释放内存。

 

3、我编写的YUV2RGB的流程

(1)初始化;

(2)对YUV图像的操作处理;

我将关于RGB码电平操作的部分也放入了我的YUV2RGB()中,所有图像处理的步骤都在我的YUV2RGB()中完成。然而,我的函数并没有上述的RGB2YUV那般功能分明,为了省力气往往一个步骤便完成了多个功能。

总结下流程:计算查找表并减128电平偏置完成彩色空间转换的同时完成UV的插值RGB的防溢出。2个步骤就做完了4个功能,模块化的程度是不够的啊!

(3)关文件、释放内存。

 

四、关键代码及其分析

1、Main()

Main()函数大家都一样,再此我就不再赘述了。在这里我想重点分享给大家的是我的YUV2RGB()函数,这个函数有4个功能。但是哪怕实现这4个简单的功能,我抱着不正确的心态去写,都能让我错了3个,并付出了比初次编写更多的时间去调试,去查错。

YUV2RGB的功能有4个,分别为:实现查找表彩色空间转换UV插值RGB的防溢出

2、YUV2RGB()功能1——实现查找表

先上第一版的效果图:

 

当时还是3月7号,开开心心下了数据压缩的实验课,调一调就能跑了。虽然图像上有很多蓝蓝红红的点而且整体偏暗,但是至少看得见山山水水啊!这不就说明整体的思路是正确的吗!哈哈哈哈,真是得来全不费工夫。但事实刚好相反,存在的问题相当严重

想到整体偏暗而且有红红蓝蓝的点,第一时间想到的就是查找表的参数有问题,因为参数是参考网上的数据的,并没有亲手验证。

原先的代码如下:

【代码一】

void myTable(){int i;for (i = 0; i < 256; i++) YUVRGB14075[i] = (float)1.4075 * (i - 128);for (i = 0; i < 256; i++) YUVRGB03455[i] = (float)0.3455 * (i - 128);for (i = 0; i < 256; i++) YUVRGB07169[i] = (float)0.7169 * (i - 128);for (i = 0; i < 256; i++) YUVRGB17790[i] = (float)1.7790 * (i - 128);}


拿起了纸笔老老实实的算了一遍,果然得出了略微不一样的结果,但是隐隐约约觉得事情并没有那么简单,毕竟只有小数点后3位不一样,应该不至于造成那么大的影响。毕竟图像整体都偏暗了,影响还是蛮大的。

修改过后的代码如下:

【代码二】

void myTable(){int i;//debuged by Wu Ruocheng 2017.3.7//The following equation is from the internet./*for ( i = 0; i < 256; i++) YUVRGB14075[i] = (float)1.4075 * (i-128);for ( i = 0; i < 256; i++) YUVRGB03455[i] = (float)0.3455 * (i-128);for ( i = 0; i < 256; i++) YUVRGB07169[i] = (float)0.7169 * (i-128);for ( i = 0; i < 256; i++) YUVRGB17790[i] = (float)1.7790 * (i-128);*///debuged by Wu Ruocheng 2017.3.7//This is my equation.for ( i = 0; i < 256; i++) YUVRGB14075[i] = (float)1.14022 * (i - 128);for ( i = 0; i < 256; i++) YUVRGB03455[i] = (float)0.34414 * (i - 128);for ( i = 0; i < 256; i++) YUVRGB07169[i] = (float)0.71424 * (i - 128);for ( i = 0; i < 256; i++) YUVRGB17790[i] = (float)1.7720 * (i - 128);}



修改后的第二版的效果图:

虽然参数修改正确了红蓝点也少了,但是可怕的是一方面整体偏暗的问题并没有消失,另一方面红红蓝蓝的小点数量如同鬼魅一般挥之不去,我意识到我的错误并没有这么简单。之前高兴的心情也随之烟消云散。

3、YUV2RGB()功能2——彩色空间转换

彩色空间转换本身比较简单,只不过执行size*3次为以bgr为次序的加减运算与赋值操作,本身并不会出错。但是我并没有采用模块化的思想来写这段程序,而是将UV插值,RGB防溢出也一并放入了循环当中。

原来的代码如下:

【代码三】

//convert yuv_in to r,g,bfor (i = 0; i < height;i++ ){for (j = 0; j < width; j++){*rgb = (unsigned char)(*y + YUVRGB17790[*u]);if (*rgb < 0) *rgb = 0;if (*rgb > 255) *rgb = 255;rgb++;*rgb = (unsigned char)(*y - YUVRGB03455[*u] - YUVRGB07169[*v]);if (*rgb < 0) *rgb = 0;if (*rgb > 255) *rgb = 255;rgb++;*rgb = (unsigned char)(*y + YUVRGB14075[*v]);if (*rgb < 0) *rgb = 0;if (*rgb > 255) *rgb = 255;rgb++;y++;if (!((i % 2) || (j % 2))){u++;v++;}}}



4、YUV2RGB()功能3——UV插值

3月8号晚,临近一天结束的我无所事事,便再次打开了笔记本。

除了红蓝点和偏暗的问题之外,我又发现了新问题!真是见了鬼了,我的图像最后一行像素怎么没了?

 

这个问题令我不寒而栗,因为老师上课说过,整体都有问题反而不可怕,一两行有问题找起来才是真麻烦。

我隐约的觉得最容易出问题的地方是UV插值的部分,因为那一部分也是从我以前写的对Y分量下采样的其他程序中改来的。

原来的代码如下:

【代码四】

//convert yuv_in to r,g,bfor (i = 0; i < height;i++ ){for (j = 0; j < width; j++){/*...*/y++;if (!((i % 2) || (j % 2))){u++;v++;}}}

我一眼扫过去,没问题啊!每一次u++和v++都发生在i和j都为偶数的像素啊!但是再细细一品就发现了内在的逻辑错误u++和v++的发生应该只是i和j都为偶数的充分条件。实际上,逻辑应该是这样的:当u与v发生累加时,i与j为偶数。但这并没有什么实际价值。


我想象了一个规模比较小的图像,并且最终能够解释图像整体偏暗以及最后一行像素消失的现象。

设有一4:2:0的YUV图像为width=8,height=4,则共有UV的采样点数为8个

【图】

当遇到ij均为偶数的情况下UV便累加

【图】


由上图可以解释,由于奇数行UV不再累加,且整个奇数行的UV都会是上一行的最后一个UV。现有的RGB2YUV的UV下采样算法是求4个像素均值而且图像的左边缘普遍较暗,故造成整体较暗。

 

左V分量图中的横向的纹理证明了我的分析。

而且像do...while 和while的混淆一样,让程序在i=0,j=0时就直接u++,v++,让最后一个偶数行的最后一个像素没有了UV分量,直接导致最后一行没有了UV分量

 

左图中254行左侧和255行的异常证明了我的分析。

最后我基于以上修改了我的UV插值。

首先,行扫描时以2为步长对uv累加;其次,在偶数行扫描结束时,uv的指针回到一行的开始,即对uv减去width/2。

最后代码的实现如下:

【代码五】

//convert yuv_in to r,g,bfor (i = 0; i < height;i++ ){for (j = 0; j < width; j++){/*...*///y move to the next pixel.y++;//When we finished the processing of an odd pixel,we move u and v forward to the next pixel.if ((j % 2)){u++;v++;}}//When we finished the processing of an even line ,we move u and v back to the beginning of the line.if (!(i%2)){u -= (width / 2);v -= (width / 2);}}


修改后的效果图:

 

可喜的发现之前的整体较暗和最后一行异常的问题消失了,由于UV错误插值产生的大面积的红蓝点也都消失了。这一次是真的高兴了,而且不是那种天上掉下馅饼不明所以的高兴。这一次高兴是因为第一次明明白白从本质分析出问题并予以解决的。

以前调规模比较小的程序,调试的问题可以说都是选择题,A不行换B试,B不对选C试,总能debug出正确结果。而如今,我第一次去踏踏实实做了一道简答题,那种明明白白的喜悦真的难以忘怀。

也幸亏3月8号是在晚上电脑不在身边的时候,突然觉得UV插值可能有问题,无奈之下拿起纸笔画了个图,发现了本质的问题。如果当时电脑在身边,我估计又会像以前一样觉得A不对改改A试试,B不对改改B最后试到Z和Y也发现不了到底错在哪里。

5、YUV2RGB()功能4——RGB码电平防溢出

这一个错误是没有理解unsigned char 导致的,错误如下:

【代码六】

*rgb = (unsigned char)(*y + YUVRGB17790[*u]);if (*rgb < 0) *rgb = 0;if (*rgb > 255) *rgb = 255;rgb++;


很容易发现我的这一步RGB码电平动态范围保护是错误的,unsigned char的取值范围本身就是0255

后来我的解决方案是用了float rgb_temp来进行RGB的彩色空间变换。

这个错误是在蔡德俊同学的帮助下发现并得以修改的,而且用三目运算符进行判断也比用好多个if看起来精简。

  【代码七】
//b = y + 1.7720*urgb_temp = (*y + YUVRGB17790[*u]);*rgb = (unsigned char)((rgb_temp < 0) ? 0 : (rgb_temp > 255) ? 255 : rgb_temp);rgb++;

五、实验结果及分析

为了更有效率的进行实验,我并没有使用命令行来输入参数,而是使用批处理文档进行的实验。

实验结果一是使用256*256down.rgb图像作为测试序列的;

实验结果二是使用640*360big_buck_bunny_1_640_360.yuv图像作为测试序列的;

实验结果三是使用1920*1080Lion_1920x1080.yuv 250帧高清视频作为测试序列的;

实验结果一:


实验结果二:


实验结果三:


分析:

无论是rgb格式还是yuv格式,无论是256*256的还是1920*1080的,程序都是可以良好运行的,并且图像和视频的质量在1366*768TN电脑屏上我观看起来没有差异。我认为是合格的。

当在对256*256以及640*360的图像进行彩色空间转换的过程中,计算机没有明显的延迟,很快就完成了转换。但在对1920*1080250帧视频进行转换的过程中,就出现了明显的延迟,在我的电脑上(操作系统:windows 10 家庭中文版 1607处理器:intel Core i5-4200M @2.5GHz),程序可能经过了约20多秒才完成转换。原YUV文件大小约为741MB转换后的RGB文件为1.44GB

这一方面说明了数据压缩的必要性,毕竟10秒的高清视频就要741MB对用户来说是不可容忍的;另一方面也留给了我们一个问题,我们现在这样做彩色空间转换的效率真的是最高的吗?很可惜的是,目前的我并不能很好的回答这个问题。希望通过日后的学习,我能给出问题的答案。

 

六、结论

那么来总结一下本次实验。本次实验可以说是一次热身,实验本身的原理与实现都是比较基础的,都是电视原理与C的基础内容,而且大部分的代码都已经给出了。但虽说基础,第一次犯错的经历对我来说却弥足珍贵。对我来说,这一次实验让我学到了很多。例如,模块化的编程思想很重要,回到问题的本质的调试方法也值得重视。

 

【经验一】模块化的思想

用模块化思想的去编程,本身就能够缩小问题的规模,让我们更容易去思考问题的解决方案。因为在模块化的过程中,就会将一个大功能分解为一个一个相互分立的小功能,从而让我们更深入的理解每一个小功能。这样的思想,既将问题简单化,也提高了人本身分析并解决复杂问题的能力,百利无一害。

以已给出的RGB2YUV.cpp为例,这位作者本身的水平肯定是比我们高到不知道哪里去,但是Adam Li在实现计算查找表、RGBYUV的彩色空间变换、UV分量下采样以及YUV码电平动态范围保护的过程中,都是一板一眼,一段代码实现一个功能。绝对不会像我一样,在实现彩色空间变换的同时还一步登天,把UV分量插值以及RGB码电平的防溢出也同时放进一段代码里面。

曾经可能还会觉得,你看示例代码多用了好多个for循环,我只用了一个,我真厉害。但现在,我只会觉得这段代码是我自身愚昧的由内而外的具现化。一方面,for循环数量的减少并没有减少算法本身的复杂度,只不过是把1+1+1=3变成1*3=3,算法本身没有任何本质的变化;而另一方面,这段代码却极大的牺牲了可读性和实现的难易程度。

可以不夸张的说,没有重视模块化的思想,是这一次实验漏洞百出的根本原因。

 

【经验二】回到问题的本质

有些夸张的说,有时候debug的功能真的会蚕食独立分析并解决问题的能力,至少对我来说是这样的。就像玩windows扫雷游戏的提示功能一样,扫雷本身是个数学游戏,但你靠左键右键一起按就变成了一个纯粹的考验手速的游戏了。

VS自带的debug功能可以检测语法错误,可以快速告诉你错在哪里,减轻了人的负担,本身存在是好的。但是把debug功能当成是debug的一切,出错了除了按debug就什么都不会,这便是本末倒置了。

有时候真的需要拿出纸和笔涂涂画画,或者打开内存自己看看。这是我这一次实验的一项重大收获之一。


1 0