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,第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("http://////////////////////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:每個通道的大小,也就是單通道數據類型
}
以上內容整理自: OpenCV中Mat屬性step,size,step1,elemSize,elemSize1 - QQ哥的專欄
圖片在計算機中一般是按行(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字節對齊問題 - 程序園
Mat中的圖像數據是不對齊的,而IplImage中的圖像數據是4字節對齊的,所以在訪問IplImage圖像數據的時候,要特別注意widthStep這個屬性,每行的字節數不是width*nchannels而是widthStep,因為每行可能會有字節填充。
//測試圖片,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++);//挨個打印出來,填充的數據
}
}
其中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直接轉換
對齊公式:
內容來自: 位圖4字節對齊問題 - fujilove的專欄
在自己對圖像數據進行處理的時候,會有字節對其的問題,由于之前使用的圖像大都是8bit或者是24bit,32bit的圖像,使用的對其公式是(pixelwidth*channel+3)/4*4。后面也有看到有些寫法如:(width * bitCounts + 31) / 32 * 4,不是很理解原理。在網上查找,發現有解釋的非常透澈的,下面借來用用。
原文鏈接: 位圖4字節對齊問題 - niloc
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字節對齊方法 - shaoxiaohu的專欄
使用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這樣的深拷貝比較穩妥。這是實驗過程中發現的問題,也困擾了我很長時間。記錄下來,以防下次出錯。
參考:
- Mat和IplImage的4字節對齊問題
- OpenCV中cv::Mat字節對齊方法 - shaoxiaohu的專欄
- OpenCV中Mat屬性step,size,step1,elemSize,elemSize1 - QQ哥的專欄
- 位圖4字節對齊問題 - fujilove的專欄
- 位圖4字節對齊問題 - niloc
來自:https://zhuanlan.zhihu.com/p/25213142