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[]的各個維度大小 n{ n n //////////////////Demo1(3維矩陣)/////////////////////////////////////////// n printf("//////////////////////Demo1(3維矩陣)////////////////////////n"); n //最後面的兩個數:(行,列),確定了一個面 n //是一個依次降維的過程 n //8,10組成了面,5個面,組成了立方體 n int matSize[] = {5,8,10};//每一維元素的個數:8:行,10:列 n Mat mat1(3,matSize, CV_16UC3, Scalar::all(0)); n n //求step[i]的大小:每一維元素的大小(單位位元組) n printf("n///////step[i]的大小//////////n"); n printf("step[0]:%dn",mat1.step[0]);//480:面的大小(第一維) n printf("step[1]:%dn",mat1.step[1]);//60:線的大小(第二維) n printf("step[2]:%dn",mat1.step[2]);//6:點的大小(第三維) n n //求size[i]:每一維元素的個數 n printf("n///////size[i]的大小///////n"); n printf("size[0]:%dn",mat1.size[0]);//5:面 n printf("size[1]:%dn",mat1.size[1]);//8:線 n printf("size[2]:%dn",mat1.size[2]);//10:點 n n //求step1(i):每一維元素的通道數 n printf("n///////step1(i)的大小///////n"); n printf("step1(0):%dn",mat1.step1(0));//240:面 n printf("step1(1):%dn",mat1.step1(1));//30:線 n printf("step1(2):%dn",mat1.step1(2));//3:點 n n //求elemSize:每個元素的大小(單位位元組) n printf("n///////elemSize的大小///////n"); n printf("elemSize:%dn",mat1.elemSize());//6:每個元素的大小 n n //求elemSize1:每個通道的大小(單位位元組) n printf("n///////elemSize1的大小///////n"); n printf("elemSize1:%dn",mat1.elemSize1());//2:每個通道的大小 n} n
結果:
1.2一張圖片示例
void Test() n{ n n /////////////Demo2(512*512二維圖像)/////////////////////////////// n printf("nn///////////////////Demo2(512*512二維圖像)//////////////////////////n"); n Mat mat2=imread("D:/Image/Color/Lena512.bmp",-1);//512*512的彩色Lena圖 n n //step[i] n printf("n///////step[i]的大小///////n"); n printf("step[0]:%dn",mat2.step[0]);//1536:線 n printf("step[1]:%dn",mat2.step[1]);//3:點 n n //size[i] n printf("n///////size[i]的大小///////n"); n printf("size[0]:%dn",mat2.size[0]);//512:線 n printf("size[1]:%dn",mat2.size[1]);//512:點 n n //step1(i) n printf("n///////step1(i)的大小///////n"); n printf("step1(0):%dn",mat2.step1(0));//1536:第一維的通道數 n printf("step1(1):%dn",mat2.step1(1));//3:第二維的通道數 n n //elemSize n printf("n///////elemSize的大小///////n"); n printf("elemSize:%dn",mat2.elemSize());//3:每個元素的大小 n n //elemSize1 n printf("n///////elemSize1的大小///////n"); n printf("elemSize1:%dn",mat2.elemSize1());//1:每個通道的大小,也就是單通道數據類型 n} n
以上內容整理自: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,因為每行可能會有位元組填充。
其中IplImage中每行都會多出3個位元組,因為IplImage4位元組對齊,而Mat就不會存在這個問題。當將IplImage轉為Mat的時候flag參數設置為false每行還是4位元組對齊。
//測試圖片,9*7單通道灰度圖CV_8UC1nvoid TestMat4ALigned()//測試Mat是否位元組對齊n{ntntMat mat=imread("D:/Image/Small/White.bmp",-1);ntint widthStep_Mat=mat.step[0];//9nntIplImage *iplImage=cvLoadImage("D:/Image/Small/White.bmp",-1);ntint widthStep_Ipl=iplImage->widthStep;//12 包含補齊的3位元組nntint pixelCount=mat.cols*mat.rows;ntnt//列印出Matntuchar *imageData=mat.data;ntprintf("Matn");ntfor (int i=0;i<=pixelCount-1;++i)nt{nttprintf("%d,",*imageData++);//挨個列印出來,沒有填充的數據nt}ntprintf("nn");ntnt//列印出IplImagentuchar *imageData_Ipl=(uchar *)iplImage->imageData;ntprintf("IplImagen");ntfor (int i=0;i<=pixelCount-1;++i)nt{nttprintf("%d,",*imageData_Ipl++);//挨個列印出來,填充的數據nt}ntprintf("nn");ntnt////////////////////////////IplImage轉為Mat//////////////////////////////////////////////ntnt//將位元組對齊的IplImage轉化為Mat,看看是否還是位元組對齊ntMat ipl2Mat_True(iplImage,true);//拷貝數據ntint withStep3=ipl2Mat_True.step[0];//9ntuchar *imageData2=ipl2Mat_True.data;ntprintf("Mat ipl2Mat_True(iplImage,true)n");ntfor (int i=0;i<=pixelCount-1;++i)nt{nttprintf("%d,",*imageData2++);//挨個列印出來,填充的數據nt}ntprintf("nn");nntnt//將位元組對齊的IplImage轉化為Mat,看看是否還是位元組對齊ntMat ipl2Mat_false(iplImage,false);//修改為非拷貝數據ntint withStep4=ipl2Mat_false.step[0];//12ntuchar *imageData3=ipl2Mat_false.data;ntprintf("Mat ipl2Mat_false(iplImage,false)n");ntfor (int i=0;i<=pixelCount-1;++i)nt{nttprintf("%d,",*imageData3++);//挨個列印出來,填充的數據nt}nn}n
IplImage iplImage;nMat mat(iplImage,true);//拷貝數據,mat是非4位元組對齊nMat mat(iplImage,false);//不拷貝數據,mat是4位元組對齊n
注意點: 這裡還有一個重要細節需要注意。調用舊版介面只會按照圖片被調用時所設置的通道補齊。 例如: 輸入圖片是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());ncv::Mat frame = cv::Mat(iplimg, false); //cv::Mat(iplimg, true)是非位元組對齊ttnnint x = 0;nint y = 0;nint w = frame.cols + (4 - frame.cols%4); //寬度 + 待補充的像素數nint h = frame.rows;nextensionImage(frame, x, y, w, h);//自定義的擴展圖片函數n
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 為三通道圖像 n cv::Mat roiImg; n frame.rowRange(frame.rows/2, frame.rows). n colRange(frame.cols/4, frame.cols*3/4). n copyTo(roiImg); // 提取ROI區域 n n int widthStep = (roiImg.cols*roiImg.elemSize()+3)/4*4; // 補齊行位元組數,使它能夠被4整除 n uchar *frameData = (uchar *)calloc(roiImg.rows*widthStep, sizeof(uchar)); // 申請內存 n memset(frameData, 0, roiImg.rows*widthStep); n n// 逐一複製數據 n uchar *p1, *p2; n for (int i = 0; i < roiImg.rows; i++) n { n p1 = roiImg.data + i*roiImg.cols*roiImg.channels(); n p2 = frameData + i * widthStep; n for (int j = 0; j < roiImg.cols; j++) n { n *(p2) = *(p1); n *(p2+1) = *(p1+1); n *(p2+2) = *(p1+2); n p1 += 3; n p2 += 3; n } n } n
附加說明:
1、對應IplImage的cvLoadImage函數載入的圖片數據是位元組對齊的,而直接將cv::Mat轉換為IplImage類型,並不會將位元組對齊,只是加了個文件頭而已。
2、我在寫代碼的過程中,還發現另一個問題(與主題無關),提取ROI區域時,如果採用:
cv::Mat roiImg = frame(cv::Rect(10, 10, 50, 50));n
這樣的拷貝為淺拷貝,對後續採用指針引用逐個複製數據的過程中,實際訪問的是原frame的數據。roiImg的許多參數都仍為frame的參數,並沒有相應的改變,容易引發錯誤。這裡還是採用copyTo這樣的深拷貝比較穩妥。這是實驗過程中發現的問題,也困擾了我很長時間。記錄下來,以防下次出錯。
參考:
- Mat和IplImage的4位元組對齊問題
- OpenCV中cv::Mat位元組對齊方法 - shaoxiaohu的專欄
- OpenCV中Mat屬性step,size,step1,elemSize,elemSize1 - QQ哥的專欄
- 點陣圖4位元組對齊問題 - fujilove的專欄
- 點陣圖4位元組對齊問題 - niloc
推薦閱讀:
※圖像處理方面有什麼好書?
※大霧 | 易經與數字圖像處理
※一本Photoshop混合模式專著
※PS教程—圖層應用(變暗模式與圖層樣式)合成圖像
※[讀論文]用多尺度卷積網路預測深度、表面法向量和語義標籤
TAG:OpenCV | 深度学习DeepLearning | 图像处理 |