樹莓派OpenCV系列教程3:IDE及圖像存儲的數(shù)據(jù)結(jié)構(gòu)

本章將首先講解開發(fā)OpenCV的基本的IDE,以方便代碼提示,同時方便閱讀底層源碼,接著,講解圖像存儲的基本數(shù)據(jù)結(jié)構(gòu)。

1 IDE
之前兩章開發(fā)OpenCV均未使用到IDE,比如開發(fā)Python的代碼,用記事本即可寫,需要時執(zhí)行該腳本即可,當(dāng)開發(fā)C++的代碼則需要先Cmake生成Makefile,再make編譯,最后執(zhí)行,為提高效率,首先介紹常用的IDE的相關(guān)配置,以及如何將OpenCV集成到該工程中。
說明:
OpenCV1采用C語言編寫,OpenCV2,OpenCV3,OpenCV4均采用C++編寫,在Python中,CV1指代底 層算法為C語言的OpenCV版本,CV2指代底層算法為C ++語言的版本,所以,在Python中,import CV2可能是OpenCV2,OpenCV3,OpenCV4等版本。
Python版本的OpenCV能不能看到底層的Python實現(xiàn)?
不能,因為底層實現(xiàn)為C++,OpenCV僅僅提供了Python接口,同時OpenCV還提供了C#,java,Andriod等的接口。
C++版本的OpenCV,推薦用什么IDE呢?
因為工程是通過Cmake構(gòu)建的,首先應(yīng)該選擇支持構(gòu)建Cmake工程的IDE,在Linux(Raspbian)平臺下,推薦Qt Creator,除了采用Cmake構(gòu)建OpenCV工程外,Qt Creator還支持采用qmake構(gòu)建OpenCV工程;此外,Qt creator還支持底層接口函數(shù)跳轉(zhuǎn),方便跳轉(zhuǎn)到對應(yīng)的函數(shù)中,方便OpenCV的算法研究,算法移植等工作。
1.1 Python3
若在樹莓派平臺采用Raspbian系統(tǒng)結(jié)合Python3開發(fā)OpenCV,由于Python僅提供相應(yīng)的接口,無法查閱底層的實現(xiàn),加上Python本身的簡潔優(yōu)雅,使用樹莓派自帶的Thonny Python IDE即可,如下圖所示:


1.2 C++
在C++開發(fā)環(huán)境中,首選Qt Creator,方便添加依賴,同時支持跳轉(zhuǎn)底層代碼,開發(fā)與研究兩不誤;同時,結(jié)合Qt自帶的UI界面,方便做成桌面應(yīng)用;并且,支持添加樹莓派自帶bcm2835,wiringPi等庫,方便進(jìn)行底層開發(fā);結(jié)合C ++的特點,向上支持應(yīng)用開發(fā),向下支持底層開發(fā)。
1.2.1 安裝Qt Creator
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install qt5-default
sudo apt-get install qtcreator
安裝好之后,即可在菜單->編程界面中找到Qt Creator,如下圖所示:

1.2.2 創(chuàng)建并運行Cmake項目
打開Qt Creator,新建工程,選擇純C++項目:

選擇合適的路徑:

選擇Cmake:

一般選擇默認(rèn)編譯套件即可:

默認(rèn)的工程中,已經(jīng)包含了Cmakelists.txt和main.cpp文件,現(xiàn)在修改Cmakelists.txt和main.cpp文件為《樹莓派OpenCV系列教程1:開發(fā)環(huán)境搭建》的內(nèi)容,如下圖所示:

若Qt Creator提示有錯誤,一般Cmakelists.txt有誤。
此時,點擊Qt Creator左下角的綠色三角形,將運行程序,打開攝像頭,并輸出相關(guān)信息,如下圖所示:

程序設(shè)定了按下q鍵即可退出窗口,此時,按下q鍵即可退出。
1.2.3 其它使用說明
若想跳轉(zhuǎn)到具體的函數(shù),可按住Ctrl,再單擊相應(yīng)的函數(shù)即可,跳轉(zhuǎn)到具體的函數(shù)之后,有相應(yīng)的函數(shù)說明,方便查閱,如下圖:

并且,Qt Creator支持編程提示,錯誤提示等功能,能大幅提高效率,如下圖所示:

2 圖像存儲的數(shù)據(jù)結(jié)構(gòu)
任何圖像處理算法,都是從操作每個像素點開始的。即使我們不會使用OpenCV提供的圖像處理算法,只要了解圖像處理算法的基本原理,也可以寫出具有相同功能的程序。接下來,我們首先講解圖像存儲的基本數(shù)據(jù)結(jié)構(gòu),接著,講解如何訪問圖像中的具體某個像素點,將分為Python3和C++進(jìn)行講解。
2.1 Python3
2.1.1 numpy基礎(chǔ)
在Python中OpenCV圖像讀取(imread) 讀入的數(shù)據(jù)格式是numpy的ndarray數(shù)據(jù)格式,此外,Python在數(shù)據(jù)計算領(lǐng)域火爆,numpy功不可沒,所以,在講解Python圖像存儲數(shù)據(jù)結(jié)構(gòu)之前,有必要先講解涉及到的numpy的相關(guān)操作:
ndarray初始化數(shù)組
Python3:
import numpy as np
# One-dimensional array
A1 = np.array([1, 2, 3])
# Two-dimensional arrayA2 = np.array([[1, 2, 3], [4, 5, 6]])
print('A1: \n%s'%A1)?
print('A2: \n%s'%A2)
輸出:
A1:
[1 2 3]?
A2:?
[[1 2 3]
[4 5 6]]
ndarray的屬性

Python3:
A2 = np.array([[1, 2, 3], [4, 5, 6]])?
print('A2.ndim = %d' % A2.ndim)?
print('A2.shape')?
print(A2.shape)?
print('A2.size = %d' % A2.size)?
print('A2.dtype = %s'%A2.dtype)?
print('A2.itemsize = %d'%A2.itemsize)
輸出:
A2.ndim = 2A2.shape?
(2, 3)?
A2.size = 6
A2.dtype = int64?
A2.itemsize = 8
ndarray的切片操作
對數(shù)組進(jìn)行切片操作,指的是獲取數(shù)組的其中某一個子區(qū)域,具體切片操作,詳情見如下圖:
一維數(shù)組的切片操作

其中:
A = np.arange(10)表示生成0到9,步長為1的一維數(shù)組。
A[0:3:1]是A[0:3]及A[:3]的完整寫法,表示取一維數(shù)組A中索引從0到3(但不包含3),步長為1的元素。
A[-1:5:-1]中:這里start=-1代表最后一個元素,表示取一維數(shù)組A中索引從最后一個到第5個(但不包含5),步長為-1的元素。
A[ : : -1]表示將一維數(shù)組A中的元素逆序取出
同理:
A[ : : 1]表示將一維數(shù)組A中的元素順序取出
多維數(shù)組的切片操作
對于多維數(shù)組的切片操作,中間需要使用逗號進(jìn)行分隔,如下圖所示:

對于圖像數(shù)據(jù)結(jié)構(gòu)ndarray的切片操作,可參考上圖。
2.1.2 OpenCV中圖像數(shù)據(jù)結(jié)構(gòu)ndarray
下圖是OpenCV中BGR格式的數(shù)據(jù)結(jié)構(gòu)

第一維度 : Height 高度, 對應(yīng)這張圖片的 nRow行數(shù)
第二維度 : Width 寬度, 對應(yīng)這張圖片的nCol 列數(shù)
第三維度: Value BGR三通道的值.
BGR 分別代表:B: Blue 藍(lán)色,G: Green 綠色,R: Red 紅色
2.1.3 從ndarray中取出像素點值
注意,由于歷史原因,OpenCV存儲圖像的數(shù)據(jù)結(jié)構(gòu)采用的BGR格式,而非RGB,下面,將讀取一幅圖片,并把這幅圖片的像素分片打印出來。
import CV2
img = CV2.imread('color.jpg')
CV2.imshow('image',img)
print(img[100:102,100:102])
CV2.waitKey(0)
CV2.destroyAllWindows()
輸出結(jié)果如下圖所示:

通過對圖像數(shù)據(jù)結(jié)構(gòu)ndarray的切片操作,打印出了4個像素點的值,需要注意的是,每個點的像素值是以(B,G,R)的形式存儲。
2.2 C++
與Python不同,在OpenCV4版本中(OpenCV1例外),提供了Mat類作為圖像容器,該對象利用了內(nèi)存管理(非嚴(yán)格意義上的),可以避免在退出程序前忘記釋放內(nèi)存造成的內(nèi)存泄露。
總而言之,Mat就是一個類,由兩個數(shù)據(jù)部分組成:矩陣頭(包含矩陣尺寸、存儲方法、存儲地址等信息)和一個存儲所有像素值的矩陣(根據(jù)所選存儲方法的不同,矩陣可以是不同的維數(shù))的指針。
2.2.1 創(chuàng)建矩陣及輸出矩陣的常用方法
當(dāng)使用拷貝構(gòu)造函數(shù),或?qū)仃囘M(jìn)行復(fù)制時,只復(fù)制信息頭和矩陣指針,而不復(fù)制矩陣。
來看下面這段代碼:
Mat A,C
A = imread("1.jpg",CV_LOAD_IMAGE_COLOR);?
Mat B(A);?
C = A;
在以上代碼中,構(gòu)造函數(shù)Mat B(A),賦值操作C=A,均只是復(fù)制矩陣A的信息頭和矩陣指針,而不復(fù)制矩陣。
如果需要復(fù)制矩陣進(jìn)行操作(實際不建議大量復(fù)制矩陣,因為圖像一般比較占內(nèi)存),可使用以下操作:
Mat A,B,C;?
A = imread("1.jpg",CV_LOAD_IMAGE_COLOR);?
B = A.clone();?
A.copyTo(C);
這樣一來,B和C均復(fù)制了A的圖像矩陣。
此外,可直觀地使用以下方法創(chuàng)建矩陣:
Mat M(2,2,CV_8UC3,Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
輸出結(jié)果如下:
M=?
[0,0,255,0,0,255;
0,0,255,0,0,255]
下面將通過一個綜合示例來演示創(chuàng)建矩陣及矩陣的輸出方法:

#include "openCV2/core/core.hpp"
#include "openCV2/highgui/highgui.hpp"
#include?
using namespace std;
using namespace cv;
int main(int,char**)
{
? ? //Create
? ? Mat I = Mat::eye(4, 4, CV_64F);
? ? I.at<double>(1,1) = CV_PI;
? ? //Display
? ? cout << "I=\n" << I << ";\n" << endl;
? ? //Create
? ? Mat r = Mat(3, 4, CV_8UC3);
? ? randu(r, Scalar::all(0), Scalar::all(255));
? ? //Display
? ? cout << "(OpenCV default Style)=\n" << r << ";" << endl << endl;
? ? cout << "(Python Style)=\n" << format(r, Formatter::FMT_PYTHON) << ";" << endl << endl;
? ? cout << "(Numpy Style)=\n" <<? format(r, Formatter::FMT_NUMPY)<< ";" << endl << endl;
? ? cout << "(Comma Style)=\n" << format(r, Formatter::FMT_CSV)<< ";" << endl<< endl;
? ? cout << "(C Style)=\n" <<? format(r, Formatter::FMT_C) << ";" << endl << endl;
? ? //Create
? ? Point2f p(6, 2);
? ? //Display
? ? cout << "Two Dimension Point p =\n" << p << ";\n" << endl;
? ? //Create
? ? Point3f p3f(8, 2, 0);
? ? //Display
? ? cout << "Three Dimension Point p3f =\n" << p3f << ";\n" << endl;
? ? //Create
? ? vector<float> v;
? ? v.push_back(3);
? ? v.push_back(5);
? ? v.push_back(7);
? ? //Display
? ? cout << "Point based on vector shortvec =\n" << Mat(v) << ";\n"<//Create
? ? vector points(10);
? ? for (size_t i = 0; i < points.size(); ++i)
? ? {
? ? ? ? points[i] = Point2f((float)(i * 5), (float)(i % 7));
? ? }
? ? //Display
? ? cout << "Two Dimension points =\n" << points<<";";
? ? return 0;
}

相應(yīng)的輸出結(jié)果如下所示:

I=
[1, 0, 0, 0;
?0, 3.141592653589793, 0, 0;
?0, 0, 1, 0;
?0, 0, 0, 1];
(OpenCV default Style)=
[ 91,? ?2,? 79, 179,? 52, 205, 236,? ?8, 181, 239,? 26, 248;
?207, 218,? 45, 183, 158, 101, 102,? 18, 118,? 68, 210, 139;
?198, 207, 211, 181, 162, 197, 191, 196,? 40,? ?7, 243, 230];
(Python Style)=
[[[ 91,? ?2,? 79], [179,? 52, 205], [236,? ?8, 181], [239,? 26, 248]],
?[[207, 218,? 45], [183, 158, 101], [102,? 18, 118], [ 68, 210, 139]],
?[[198, 207, 211], [181, 162, 197], [191, 196,? 40], [? 7, 243, 230]]];
(Numpy Style)=
array([[[ 91,? ?2,? 79], [179,? 52, 205], [236,? ?8, 181], [239,? 26, 248]],
? ? ? ?[[207, 218,? 45], [183, 158, 101], [102,? 18, 118], [ 68, 210, 139]],
? ? ? ?[[198, 207, 211], [181, 162, 197], [191, 196,? 40], [? 7, 243, 230]]], dtype='uint8');
(Comma Style)=
?91,? ?2,? 79, 179,? 52, 205, 236,? ?8, 181, 239,? 26, 248
207, 218,? 45, 183, 158, 101, 102,? 18, 118,? 68, 210, 139
198, 207, 211, 181, 162, 197, 191, 196,? 40,? ?7, 243, 230
;
(C Style)=
{ 91,? ?2,? 79, 179,? 52, 205, 236,? ?8, 181, 239,? 26, 248,
?207, 218,? 45, 183, 158, 101, 102,? 18, 118,? 68, 210, 139,
?198, 207, 211, 181, 162, 197, 191, 196,? 40,? ?7, 243, 230};
Two Dimension Point p =
[6, 2];
Three Dimension Point p3f =
[8, 2, 0];
Point based on vector shortvec =
[3;
?5;
?7];
Two Dimension points =
[0, 0;
?5, 1;
?10, 2;
?15, 3;
?20, 4;
?25, 5;
?30, 6;
?35, 0;
?40, 1;
?45, 2];

2.2.2 從Mat中取出像素點
本節(jié),將介紹C++中常用的從Mat類的實例化對象中取出像素點的3種方法,并且,每種方法均對顏色空間進(jìn)行縮減,即:
0~100范圍的像素值為0;
100~200范圍的像素值為100;
200~255范圍的像素值為200。
并且,每種方法均統(tǒng)計了運行時間。
用指針訪問像素

#include?
#include?
#include?
using namespace std;
using namespace cv;
void colorReduce(Mat& inputImage, Mat& outputImage, int div);
int main( )
{
? ? Mat srcImage = imread("color.jpg");
? ? imshow("srcImage",srcImage);
? ? Mat dstImage;
? ? dstImage.create(srcImage.rows,srcImage.cols,srcImage.type());
? ? double time0 = static_cast<double>(getTickCount());
? ? colorReduce(srcImage,dstImage,100);
? ? time0 = ((double)getTickCount() - time0)/getTickFrequency();
? ? cout<<"This function? waste time:"<" second"<"dstImage",dstImage);
? ? waitKey(0);
}
void colorReduce(Mat& inputImage, Mat& outputImage, int div)
{
? ? outputImage = inputImage.clone();
? ? int rowNumber = outputImage.rows;
? ? //Number of columns * Number of channels = number of elements per line
? ? int colNumber = outputImage.cols*outputImage.channels();
? ? for(int i = 0;i < rowNumber;i++)
? ? {
? ? ? ? //Get the first address of the i-th line
? ? ? ? uchar* data = outputImage.ptr(i);
? ? ? ? for(int j = 0;j < colNumber;j++)
? ? ? ? {
? ? ? ? ? ? data[j] = data[j]/div*div;
? ? ? ? }
? ? }
}

在該程序中,先獲取每一行的元素的個數(shù),在雙重遍歷中,先獲取第i行的首地址,然后通過指針獲取第i的第j個元素,再對該元素進(jìn)行處理。
該函數(shù)的運行效果如下圖所示:

可見,遍歷所有像素點并進(jìn)行處理的時間為0.02秒左右
用迭代器訪問像素
用迭代器訪問像素點的操作如下程序所示:
#include?
#include?
#include?
using namespace std;
using namespace cv;
void colorReduce(Mat& inputImage, Mat& outputImage, int div);
int main( )
{
? ? Mat srcImage = imread("color.jpg");
? ? imshow("srcImage",srcImage);
? ? Mat dstImage;
? ? dstImage.create(srcImage.rows,srcImage.cols,srcImage.type());
? ? double time0 = static_cast<double>(getTickCount());
? ? colorReduce(srcImage,dstImage,100);
? ? time0 = ((double)getTickCount() - time0)/getTickFrequency();
? ? cout<<"This function? waste time:"<" second"<"dstImage",dstImage);
? ? waitKey(0);
}
void colorReduce(Mat& inputImage, Mat& outputImage, int div)??
{??
? ? outputImage = inputImage.clone();
? ? //Start position iterator
? ? Mat_::iterator it = outputImage.begin();?
? ? //End position iterator
? ? Mat_::iterator itend = outputImage.end();
? ? for(;it != itend;++it)??
? ? {
? ? ? ? (*it)[0] = (*it)[0]/div*div;??
? ? ? ? (*it)[1] = (*it)[1]/div*div;??
? ? ? ? (*it)[2] = (*it)[2]/div*div;
? ? }??
}

該函數(shù)運行效果如下圖所示:

在該方法中,直接使用迭代器進(jìn)行處理,采用迭代器訪問相對于數(shù)組越界的可能性,還是非常安全的,經(jīng)實測,該方法遍歷所有像素點并進(jìn)行處理的時間為0.04秒左右。
可見,采用迭代器訪問像素點的方法比采用指針訪問像素點的方法慢了近一倍,因此,為提高處理速度,建議采用指針訪問像素點。