opencv图片保存0字节_OpenCV图片对齐问题
1.对齐问题是什么?要回答这个问题先搞清楚图片有哪些属性?1.1矩阵例子我们先结合Opencv的Mat类型看一个矩阵例子。高为8像素,宽为10像素,深度为5,像素类型为CV_16UC3的矩阵如下:Opencv的Mat类型有如下属性:size[i]:每一维元素的个数: 上图 是三维图片,第0维是Z轴,元素是“面”,有五个面所以 size[0]=5,同理第1维是Y轴,元素是“线”,size[1]=8,
1.对齐问题是什么?
要回答这个问题先搞清楚图片有哪些属性?
1.1矩阵例子
我们先结合Opencv的Mat类型看一个矩阵例子。高为8像素,宽为10像素,深度为5,像素类型为CV_16UC3的矩阵如下:
Opencv的Mat类型有如下属性:size[i]:每一维元素的个数: 上图 是三维图片,第0维是Z轴,元素是“面”,有五个面所以 size[0]=5,同理第1维是Y轴,元素是“线”,size[1]=8,第2维是X轴,元素是“点”,size[2] =10
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。
step1(i): 每一维元素的通道数。第2维元素是”点“,有三个通道:step1(2) = channels = 3。第1维元素是”线“:step1(1) = 10*3 = 30。第0维元素是”面“:step1(0) = 8*10*3 = 240。
elemSize():每个元素大小,单位字节:元素就是CV_16UC3类型的像素点,6字节.
elemSize1():每个通道大小,单位字节:每个通道2字节。
测试代码:
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:每个通道的大小}
结果:
1.2一张图片示例
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:每个通道的大小,也就是单通道数据类型}
图片在计算机中一般是按行(row)存储。因此每行包含的字节数是一个很重要的概念(叫做widthStep)。它对应上面图片案例中的第0维度元素大小:step[0] = 512(宽为512个像素) * 3(每个像素包含3个通道)*1(每个通道1字节) = 1536。很多情况下要求这个值必须是4的倍数,即实现字节对齐,有利于提高运算速度。
2.如何对齐?
最简单的思路是计算widthStep并补齐为4的倍数。不过因为旧版本的Opencv提供了自动补齐。所以我们可以借助旧版本的接口得到旧版本图片类型(IplImage类型),然后转换为Mat类型(Mat类型操作更方便)。
2.1 方法1 IplImage转Mat
Mat中的图像数据是不对齐的,而IplImage中的图像数据是4字节对齐的,所以在访问IplImage图像数据的时候,要特别注意widthStep这个属性,每行的字节数不是width*nchannels而是widthStep,因为每行可能会有字节填充。
//测试图片,9*7单通道灰度图CV_8UC1void 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;
//打印出Matuchar *imageData=mat.data;
printf("Mat\n");
for (int i=0;i<=pixelCount-1;++i)
{
printf("%d,",*imageData++);//挨个打印出来,没有填充的数据}
printf("\n\n");
//打印出IplImageuchar *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];//9uchar *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];//12uchar *imageData3=ipl2Mat_false.data;
printf("Mat ipl2Mat_false(iplImage,false)\n");
for (int i=0;i<=pixelCount-1;++i)
{
printf("%d,",*imageData3++);//挨个打印出来,填充的数据}
}
其中IplImage中每行都会多出3个字节,因为IplImage4字节对齐,而Mat就不会存在这个问题。当将IplImage转为Mat的时候flag参数设置为false每行还是4字节对齐。
IplImage iplImage;
Mat mat(iplImage,true);//拷贝数据,mat是非4字节对齐Mat mat(iplImage,false);//不拷贝数据,mat是4字节对齐
注意点: 这里还有一个重要细节需要注意。调用旧版接口只会按照图片被调用时所设置的通道补齐。 例如: 输入图片是5(高)*6(宽),3通道,像素类型CV_8U3C的图片,调用IplImage* iplimg = cvLoadImage(imagepath)并转换为Mat类型。根据对齐原则: 6*3=18被自动补齐为20 (widthStep=20)。如果你此时需要把该三通道图片转为单通道灰度图使用:需要手动把6补齐为8(widthStep=8)。
为了解决这个棘手的问题,可以将图片宽度(单位:像素)设置为4的倍数。就不用担心通道转换所带来的问题了。我使用的代码如下:
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);//自定义的扩展图片函数
2.2 方法2直接转换
对齐公式:在自己对图像数据进行处理的时候,会有字节对其的问题,由于之前使用的图像大都是8bit或者是24bit,32bit的图像,使用的对其公式是(pixelwidth*channel+3)/4*4。后面也有看到有些写法如:(width * bitCounts + 31) / 32 * 4,不是很理解原理。在网上查找,发现有解释的非常透澈的,下面借来用用。
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字节,数字再往上就更加多余,所以补上最大余数刚刚合适。
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。
对齐代码:
使用OpenCV过程中,cv::Mat比IplImage更容易操作,也符合C++使用者的习惯。但是一般Mat的数据并不是字节对齐的,对于需要字节对齐数据的函数(比如控件上的位图显示)来说,就会产生相应的问题。下面介绍将Mat数据转换为字节对齐的uchar数据的方法,以三通道图像为例,代码如下:
// 这里 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;
}
}
附加说明:
1、对应IplImage的cvLoadImage函数加载的图片数据是字节对齐的,而直接将cv::Mat转换为IplImage类型,并不会将字节对齐,只是加了个文件头而已。
2、我在写代码的过程中,还发现另一个问题(与主题无关),提取ROI区域时,如果采用:
cv::Mat roiImg = frame(cv::Rect(10, 10, 50, 50));这样的拷贝为浅拷贝,对后续采用指针引用逐个复制数据的过程中,实际访问的是原frame的数据。roiImg的许多参数都仍为frame的参数,并没有相应的改变,容易引发错误。这里还是采用copyTo这样的深拷贝比较稳妥。这是实验过程中发现的问题,也困扰了我很长时间。记录下来,以防下次出错。
参考:
更多推荐
所有评论(0)