OpenCV图片对齐问题
tv410356
8年前
<p style="text-align:center"><img src="https://simg.open-open.com/show/c1097c7af09da22bddfc6ed41c660799.png"></p> <h2>1.对齐问题是什么?</h2> <p>要回答这个问题先搞清楚图片有哪些属性?</p> <p><strong>1.1矩阵例子</strong></p> <p>我们先结合Opencv的Mat类型看一个矩阵例子。高为8像素,宽为10像素,深度为5,像素类型为CV_16UC3的矩阵如下:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/6cde327d0d569c45f4fbd63e734dd7f5.png"></p> <p>Opencv的Mat类型有如下属性:</p> <ul> <li>size[i]:每一维元素的个数: 上图 是三维图片,第0维是Z轴,元素是“面”,有五个面所以 size[0]=5,同理第1维是Y轴,元素是“线”,size[1]=8,第2维是X轴,元素是“点”,size[2] =10</li> <li>step[i]:每一维元素的大小,单位字节: 第2维元素是“点”,也就是CV_16UC3类型的像素点。其中C3表示每个像素点有3个channel(通道),16U表示每个通道有16bit也就是2Byte(字节)。综合起来每个像素点有 step[2] = Channels*Bytes = 3 * 2 = 6 个Byte(字节)。10个“点元素”组成第1维的“线”元素: step[1] = 10 * 6 = 60 Byte。8个“线元素”组成一个第0维的“面”元素:step[0] = 8*10*6=480 Byte。</li> <li>step1(i): 每一维元素的通道数。第2维元素是”点“,有三个通道:step1(2) = channels = 3。第1维元素是”线“:step1(1) = 10*3 = 30。第0维元素是”面“:step1(0) = 8*10*3 = 240。</li> <li>elemSize():每个元素大小,单位字节:元素就是CV_16UC3类型的像素点,6字节.</li> <li>elemSize1():每个通道大小,单位字节:每个通道2字节。</li> </ul> <p>测试代码:</p> <pre> <code class="language-cpp">void Learn_Mat_Definiton()//测试一下step[]的各个维度大小 { //////////////////Demo1(3维矩阵)/////////////////////////////////////////// printf("//////////////////////Demo1(3维矩阵)////////////////////////\n"); //最后面的两个数:(行,列),确定了一个面 //是一个依次降维的过程 //8,10组成了面,5个面,组成了立方体 int matSize[] = {5,8,10};//每一维元素的个数:8:行,10:列 Mat mat1(3,matSize, CV_16UC3, Scalar::all(0)); //求step[i]的大小:每一维元素的大小(单位字节) printf("\n///////step[i]的大小//////////\n"); printf("step[0]:%d\n",mat1.step[0]);//480:面的大小(第一维) printf("step[1]:%d\n",mat1.step[1]);//60:线的大小(第二维) printf("step[2]:%d\n",mat1.step[2]);//6:点的大小(第三维) //求size[i]:每一维元素的个数 printf("\n///////size[i]的大小///////\n"); printf("size[0]:%d\n",mat1.size[0]);//5:面 printf("size[1]:%d\n",mat1.size[1]);//8:线 printf("size[2]:%d\n",mat1.size[2]);//10:点 //求step1(i):每一维元素的通道数 printf("\n///////step1(i)的大小///////\n"); printf("step1(0):%d\n",mat1.step1(0));//240:面 printf("step1(1):%d\n",mat1.step1(1));//30:线 printf("step1(2):%d\n",mat1.step1(2));//3:点 //求elemSize:每个元素的大小(单位字节) printf("\n///////elemSize的大小///////\n"); printf("elemSize:%d\n",mat1.elemSize());//6:每个元素的大小 //求elemSize1:每个通道的大小(单位字节) printf("\n///////elemSize1的大小///////\n"); printf("elemSize1:%d\n",mat1.elemSize1());//2:每个通道的大小 }</code></pre> <p>结果:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/2ef4255af4ebc25c79e705875b5ab413.png"></p> <p><strong>1.2一张图片示例</strong></p> <pre> <code class="language-cpp">void Test() { /////////////Demo2(512*512二维图像)/////////////////////////////// printf("\n\n///////////////////Demo2(512*512二维图像)//////////////////////////\n"); Mat mat2=imread("D:/Image/Color/Lena512.bmp",-1);//512*512的彩色Lena图 //step[i] printf("\n///////step[i]的大小///////\n"); printf("step[0]:%d\n",mat2.step[0]);//1536:线 printf("step[1]:%d\n",mat2.step[1]);//3:点 //size[i] printf("\n///////size[i]的大小///////\n"); printf("size[0]:%d\n",mat2.size[0]);//512:线 printf("size[1]:%d\n",mat2.size[1]);//512:点 //step1(i) printf("\n///////step1(i)的大小///////\n"); printf("step1(0):%d\n",mat2.step1(0));//1536:第一维的通道数 printf("step1(1):%d\n",mat2.step1(1));//3:第二维的通道数 //elemSize printf("\n///////elemSize的大小///////\n"); printf("elemSize:%d\n",mat2.elemSize());//3:每个元素的大小 //elemSize1 printf("\n///////elemSize1的大小///////\n"); printf("elemSize1:%d\n",mat2.elemSize1());//1:每个通道的大小,也就是单通道数据类型 }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/4490ae067879d16be5b56c4b92f090b0.png"></p> <p>以上内容整理自: <a href="/misc/goto?guid=4959737023376779231" rel="nofollow,noindex"> OpenCV中Mat属性step,size,step1,elemSize,elemSize1 - QQ哥的专栏 </a></p> <p>图片在计算机中一般是按行(row)存储。因此每行包含的字节数是一个很重要的概念(叫做widthStep)。它对应上面图片案例中的第0维度元素大小:step[0] = 512(宽为512个像素) * 3(每个像素包含3个通道)*1(每个通道1字节) = 1536。很多情况下要求这个值必须是4的倍数,即实现字节对齐,有利于提高运算速度。</p> <h2>2.如何对齐?</h2> <p>最简单的思路是计算widthStep并补齐为4的倍数。不过因为旧版本的Opencv提供了自动补齐。所以我们可以借助旧版本的接口得到旧版本图片类型(IplImage类型),然后转换为Mat类型(Mat类型操作更方便)。</p> <p><strong>2.1 方法1 IplImage转Mat</strong></p> <p>内容来自: <a href="/misc/goto?guid=4959737023466053342" rel="nofollow,noindex"> Mat和IplImage的4字节对齐问题 - 程序园 </a></p> <p>Mat中的图像数据是不对齐的,而IplImage中的图像数据是4字节对齐的,所以在访问IplImage图像数据的时候,要特别注意widthStep这个属性,每行的字节数不是width*nchannels而是widthStep,因为每行可能会有字节填充。</p> <pre> <code class="language-cpp">//测试图片,9*7单通道灰度图CV_8UC1 void TestMat4ALigned()//测试Mat是否字节对齐 { Mat mat=imread("D:/Image/Small/White.bmp",-1); int widthStep_Mat=mat.step[0];//9 IplImage *iplImage=cvLoadImage("D:/Image/Small/White.bmp",-1); int widthStep_Ipl=iplImage->widthStep;//12 包含补齐的3字节 int pixelCount=mat.cols*mat.rows; //打印出Mat uchar *imageData=mat.data; printf("Mat\n"); for (int i=0;i<=pixelCount-1;++i) { printf("%d,",*imageData++);//挨个打印出来,没有填充的数据 } printf("\n\n"); //打印出IplImage uchar *imageData_Ipl=(uchar *)iplImage->imageData; printf("IplImage\n"); for (int i=0;i<=pixelCount-1;++i) { printf("%d,",*imageData_Ipl++);//挨个打印出来,填充的数据 } printf("\n\n"); ////////////////////////////IplImage转为Mat////////////////////////////////////////////// //将字节对齐的IplImage转化为Mat,看看是否还是字节对齐 Mat ipl2Mat_True(iplImage,true);//拷贝数据 int withStep3=ipl2Mat_True.step[0];//9 uchar *imageData2=ipl2Mat_True.data; printf("Mat ipl2Mat_True(iplImage,true)\n"); for (int i=0;i<=pixelCount-1;++i) { printf("%d,",*imageData2++);//挨个打印出来,填充的数据 } printf("\n\n"); //将字节对齐的IplImage转化为Mat,看看是否还是字节对齐 Mat ipl2Mat_false(iplImage,false);//修改为非拷贝数据 int withStep4=ipl2Mat_false.step[0];//12 uchar *imageData3=ipl2Mat_false.data; printf("Mat ipl2Mat_false(iplImage,false)\n"); for (int i=0;i<=pixelCount-1;++i) { printf("%d,",*imageData3++);//挨个打印出来,填充的数据 } }</code></pre> <p style="text-align:center"><img src="https://simg.open-open.com/show/8f7edb4e164153f99adff00b2f774229.png"></p> <p>其中IplImage中每行都会多出3个字节,因为IplImage4字节对齐,而Mat就不会存在这个问题。当将IplImage转为Mat的时候flag参数设置为false每行还是4字节对齐。</p> <pre> <code class="language-cpp">IplImage iplImage; Mat mat(iplImage,true);//拷贝数据,mat是非4字节对齐 Mat mat(iplImage,false);//不拷贝数据,mat是4字节对齐</code></pre> <p><strong>注意点</strong> : 这里还有一个重要细节需要注意。调用旧版接口只会按照图片被调用时所设置的通道补齐。 例如: 输入图片是5(高)*6(宽),3通道,像素类型CV_8U3C的图片,调用IplImage* iplimg = cvLoadImage(imagepath)并转换为Mat类型。根据对齐原则: 6*3=18被 <strong>自动补齐</strong> 为20 (widthStep=20)。如果你此时需要把该三通道图片转为单通道灰度图使用:需要 <strong>手动</strong> 把6补齐为8(widthStep=8)。</p> <p>为了解决这个棘手的问题,可以将图片宽度(单位:像素)设置为4的倍数。就不用担心通道转换所带来的问题了。我使用的代码如下:</p> <pre> <code class="language-cpp">IplImage* iplimg = cvLoadImage(imgPath.c_str()); cv::Mat frame = cv::Mat(iplimg, false); //cv::Mat(iplimg, true)是非字节对齐 int x = 0; int y = 0; int w = frame.cols + (4 - frame.cols%4); //宽度 + 待补充的像素数 int h = frame.rows; extensionImage(frame, x, y, w, h);//自定义的扩展图片函数</code></pre> <p><strong>2.2 方法2直接转换</strong></p> <p>对齐公式:</p> <p>内容来自: <a href="/misc/goto?guid=4959737023549175866" rel="nofollow,noindex"> 位图4字节对齐问题 - fujilove的专栏 </a></p> <p>在自己对图像数据进行处理的时候,会有字节对其的问题,由于之前使用的图像大都是8bit或者是24bit,32bit的图像,使用的对其公式是(pixelwidth*channel+3)/4*4。后面也有看到有些写法如:(width * bitCounts + 31) / 32 * 4,不是很理解原理。在网上查找,发现有解释的非常透澈的,下面借来用用。</p> <p>原文链接: <a href="/misc/goto?guid=4959737023629564400" rel="nofollow,noindex"> 位图4字节对齐问题 - niloc </a></p> <p>1. 首先来自于这样一个公式:(width * bitCounts / 8 + 3) / 4 * 4,该公式含义比上面的公式要容易理解一些,比如biWidth * biBitCount代表了对齐前每行的总位数,位图有1位、2位、4位、8位、16位、24位、32位等,大于8位的都是8的倍数,所以biWidth * biBitCount / 8是对齐前的总字节数,要4字节对齐,除以4,看余数多少,不够多少补多少。而因为除以4的余数只能是0、1、2、3这四种情况,0就是刚好整除不需要再补,1、2、3分别需要补3、2、1个字节才能凑足4字节。那么 我们在除之前先补上3个字节会是什么情况呢,对于四种余数情形,分别是余3(3+0)、0(3+1)、1(3+2)、2(3+3),对于整数操作(width * bitCounts / 8 + 3) / 4得到的结果不会有余数,刚好达到了我们需要补足4字节的目的;另外再考虑能不能先补上别的数字,例如1、2,根据前面余数情况分析,补1会漏掉余数为1、2的情况,补2会漏掉余数为1的情况; 再考虑补上4或者更大数字的情况,余数为0的情形补4就多了4字节,数字再往上就更加多余,所以补上最大余数刚刚合适。</p> <p>2. 上面的分析对于位宽大于等于8的位图已经正确,但小于8位的情况,width * bitCounts不一定是8的整数倍,所以我们先不要除以8,而是按照4字节等于32个bit位来计算,我们看看需要补多少位使得刚好32位对齐,那么就有公式:(width * bitCounts + n)/ 32 * 4 跟1中的分析方法相同,n应为32的最大余数31,所以得到最终公式:(width * bitCounts + 31)/ 32 * 4。</p> <p>对齐代码:</p> <p>内容来自: <a href="/misc/goto?guid=4959737023713363279" rel="nofollow,noindex"> OpenCV中cv::Mat字节对齐方法 - shaoxiaohu的专栏 </a></p> <p>使用OpenCV过程中,cv::Mat比IplImage更容易操作,也符合C++使用者的习惯。但是一般Mat的数据并不是字节对齐的,对于需要字节对齐数据的函数(比如控件上的位图显示)来说,就会产生相应的问题。下面介绍将Mat数据转换为字节对齐的uchar数据的方法,以三通道图像为例,代码如下:</p> <pre> <code class="language-cpp">// 这里 frame 为三通道图像 cv::Mat roiImg; frame.rowRange(frame.rows/2, frame.rows). colRange(frame.cols/4, frame.cols*3/4). copyTo(roiImg); // 提取ROI区域 int widthStep = (roiImg.cols*roiImg.elemSize()+3)/4*4; // 补齐行字节数,使它能够被4整除 uchar *frameData = (uchar *)calloc(roiImg.rows*widthStep, sizeof(uchar)); // 申请内存 memset(frameData, 0, roiImg.rows*widthStep); // 逐一复制数据 uchar *p1, *p2; for (int i = 0; i < roiImg.rows; i++) { p1 = roiImg.data + i*roiImg.cols*roiImg.channels(); p2 = frameData + i * widthStep; for (int j = 0; j < roiImg.cols; j++) { *(p2) = *(p1); *(p2+1) = *(p1+1); *(p2+2) = *(p1+2); p1 += 3; p2 += 3; } }</code></pre> <p>附加说明:</p> <p>1、对应IplImage的cvLoadImage函数加载的图片数据是字节对齐的,而直接将cv::Mat转换为IplImage类型,并不会将字节对齐,只是加了个文件头而已。</p> <p>2、我在写代码的过程中,还发现另一个问题(与主题无关),提取ROI区域时,如果采用:</p> <pre> <code class="language-cpp">cv::Mat roiImg = frame(cv::Rect(10, 10, 50, 50));</code></pre> <p>这样的拷贝为浅拷贝,对后续采用指针引用逐个复制数据的过程中,实际访问的是原frame的数据。roiImg的许多参数都仍为frame的参数,并没有相应的改变,容易引发错误。这里还是采用copyTo这样的深拷贝比较稳妥。这是实验过程中发现的问题,也困扰了我很长时间。记录下来,以防下次出错。</p> <h2>参考:</h2> <ul> <li><a href="/misc/goto?guid=4959737023466053342" rel="nofollow,noindex">Mat和IplImage的4字节对齐问题 </a></li> <li><a href="/misc/goto?guid=4959737023713363279" rel="nofollow,noindex">OpenCV中cv::Mat字节对齐方法 - shaoxiaohu的专栏 </a></li> <li><a href="/misc/goto?guid=4959737023376779231" rel="nofollow,noindex">OpenCV中Mat属性step,size,step1,elemSize,elemSize1 - QQ哥的专栏 </a></li> <li><a href="/misc/goto?guid=4959737023549175866" rel="nofollow,noindex">位图4字节对齐问题 - fujilove的专栏 </a></li> <li><a href="/misc/goto?guid=4959737023850171070" rel="nofollow,noindex">位图4字节对齐问题 - niloc </a></li> </ul> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/25213142</p> <p> </p>