本章将首先讲解开发OpenCV的基本的IDE,以方便代码提示,同时方便阅读底层源码,接着,讲解图像存储的基本数据结构。
1 IDE
之前两章开发OpenCV均未使用到IDE,比如开发Python的代码,用记事本即可写,需要时执行该脚本即可,当开发C++的代码则需要先Cmake生成Makefile,再make编译,最后执行,为提高效率,首先介绍常用的IDE的相关配置,以及如何将OpenCV集成到该工程中。
说明:
- 关于Python中,目前采用的是OpenCV4,为什么却是import cv2呢?
OpenCV1采用C语言编写,OpenCV2,OpenCV3,OpenCV4均采用C++编写,在Python中,cv1指代底 层算法为C语言的OpenCV版本,cv2指代底层算法为C ++语言的版本,所以,在Python中,import cv2可能是OpenCV2,OpenCV3,OpenCV4等版本。
- Python版本的OpenCV能不能看到底层的Python实现?
不能,因为底层实现为C++,OpenCV仅仅提供了Python接口,同时OpenCV还提供了C#,java,Andriod等的接口。
因为工程是通过Cmake构建的,首先应该选择支持构建Cmake工程的IDE,在Linux(Raspbian)平台下,推荐Qt Creator,除了采用Cmake构建OpenCV工程外,Qt Creator还支持采用qmake构建OpenCV工程;此外,Qt creator还支持底层接口函数跳转,方便跳转到对应的函数中,方便OpenCV的算法研究,算法移植等工作。
1.1 Python3
若在树莓派平台采用Raspbian系统结合Python3开发OpenCV,由于Python仅提供相应的接口,无法查阅底层的实现,加上Python本身的简洁优雅,使用树莓派自带的Thonny Python IDE即可,如下图所示:
点击Load,打开00_Test_OpenCV.py,点击Run,即可运行相应的Python脚本,此外,该IDE支持断点调试,如下图所示:
1.2 C++
在C++开发环境中,首选Qt Creator,方便添加依赖,同时支持跳转底层代码,开发与研究两不误;同时,结合Qt自带的UI界面,方便做成桌面应用;并且,支持添加树莓派自带bcm2835,wiringPi等库,方便进行底层开发;结合C ++的特点,向上支持应用开发,向下支持底层开发。
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 创建并运行Cmake项目
打开Qt Creator,新建工程,选择纯C++项目:
选择合适的路径:
选择Cmake:
一般选择默认编译套件即可:
默认的工程中,已经包含了Cmakelists.txt和main.cpp文件,现在修改Cmakelists.txt和main.cpp文件为《树莓派OpenCV系列教程1:开发环境搭建》的内容,如下图所示:
若Qt Creator提示有错误,一般Cmakelists.txt有误。
此时,点击Qt Creator左下角的绿色三角形,将运行程序,打开摄像头,并输出相关信息,如下图所示:
程序设定了按下q键即可退出窗口,此时,按下q键即可退出。
1.2.3 其它使用说明
若想跳转到具体的函数,可按住Ctrl,再单击相应的函数即可,跳转到具体的函数之后,有相应的函数说明,方便查阅,如下图:
并且,Qt Creator支持编程提示,错误提示等功能,能大幅提高效率,如下图所示:
2 图像存储的数据结构
任何图像处理算法,都是从操作每个像素点开始的。即使我们不会使用OpenCV提供的图像处理算法,只要了解图像处理算法的基本原理,也可以写出具有相同功能的程序。接下来,我们首先讲解图像存储的基本数据结构,接着,讲解如何访问图像中的具体某个像素点,将分为Python3和C++进行讲解。
2.1 Python3
2.1.1 numpy基础
在Python中OpenCV图像读取(imread) 读入的数据格式是numpy的ndarray数据格式,此外,Python在数据计算领域火爆,numpy功不可没,所以,在讲解Python图像存储数据结构之前,有必要先讲解涉及到的numpy的相关操作:
ndarray初始化数组
Python3:
import numpy as np
A1 = np.array([1, 2, 3])
A2 = 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的属性
属性名称 |
含义 |
ndarray.ndim |
数组的维度,等于Rank |
ndarray.shape |
(行数, 列数) |
ndarray.size |
元素总个数 = 列数 * 行数 |
ndarray.dtype |
数组元素数据类型 |
ndarray.itemsize |
数组中每个元素,字节大小 |
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 = 2
A2.shape
(2, 3)
A2.size = 6
A2.dtype = int64
A2.itemsize = 8
ndarray的切片操作
对数组进行切片操作,指的是获取数组的其中某一个子区域,具体切片操作,详情见如下图:
一维数组的切片操作
其中:
A = np.arange(10)表示生成0到9,步长为1的一维数组。
A[0:3:1]是A[0:3]及A[:3]的完整写法,表示取一维数组A中索引从0到3(但不包含3),步长为1的元素。
A[-1:5:-1]中:这里start=-1代表最后一个元素,表示取一维数组A中索引从最后一个到第5个(但不包含5),步长为-1的元素。
A[ : : -1]表示将一维数组A中的元素逆序取出
同理:
A[ : : 1]表示将一维数组A中的元素顺序取出
多维数组的切片操作
对于多维数组的切片操作,中间需要使用逗号进行分隔,如下图所示:
对于图像数据结构ndarray的切片操作,可参考上图。
2.1.2 OpenCV中图像数据结构ndarray
下图是OpenCV中BGR格式的数据结构:
第一维度 : Height 高度, 对应这张图片的 nRow行数
第二维度 : Width 宽度, 对应这张图片的nCol 列数
第三维度: Value BGR三通道的值.
BGR 分别代表:B: Blue 蓝色,G: Green 绿色,R: Red 红色
2.1.3 从ndarray中取出像素点值
注意,由于历史原因,OpenCV存储图像的数据结构采用的BGR格式,而非RGB,下面,将读取一幅图片,并把这幅图片的像素分片打印出来。
import cv2
img = cv2.imread('color.jpg')
cv2.imshow('image',img)
print(img[100:102,100:102])
cv2.waitKey(0)
cv2.destroyAllWindows()
输出结果如下图所示:
通过对图像数据结构ndarray的切片操作,打印出了4个像素点的值,需要注意的是,每个点的像素值是以(B,G,R)的形式存储。
2.2 C++
与Python不同,在OpenCV4版本中(OpenCV1例外),提供了Mat类作为图像容器,该对象利用了内存管理(非严格意义上的),可以避免在退出程序前忘记释放内存造成的内存泄露。
总而言之,Mat就是一个类,由两个数据部分组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等信息)和一个存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。
2.2.1 创建矩阵及输出矩阵的常用方法
当使用拷贝构造函数,或对矩阵进行复制时,只复制信息头和矩阵指针,而不复制矩阵。
来看下面这段代码:
Mat A,C
A = imread("1.jpg",CV_LOAD_IMAGE_COLOR);
Mat B(A);
C = A;
在以上代码中,构造函数Mat B(A),赋值操作C=A,均只是复制矩阵A的信息头和矩阵指针,而不复制矩阵。
如果需要复制矩阵进行操作(实际不建议大量复制矩阵,因为图像一般比较占内存),可使用以下操作:
Mat A,B,C;
A = imread("1.jpg",CV_LOAD_IMAGE_COLOR);
B = A.clone();
A.copyTo(C);
这样一来,B和C均复制了A的图像矩阵。
此外,可直观地使用以下方法创建矩阵:
Mat M(2,2,CV_8UC3,Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;
输出结果如下:
M=
[0,0,255,0,0,255;
0,0,255,0,0,255]
下面将通过一个综合示例来演示创建矩阵及矩阵的输出方法:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include
using namespace std;
using namespace cv;
int main(int,char**)
{
Mat I = Mat::eye(4, 4, CV_64F);
I.at<double>(1,1) = CV_PI;
cout << "I=\n" << I << ";\n" << endl;
Mat r = Mat(3, 4, CV_8UC3);
randu(r, Scalar::all(0), Scalar::all(255));
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;
Point2f p(6, 2);
cout << "Two Dimension Point p =\n" << p << ";\n" << endl;
Point3f p3f(8, 2, 0);
cout << "Three Dimension Point p3f =\n" << p3f << ";\n" << endl;
vector<float> v;
v.push_back(3);
v.push_back(5);
v.push_back(7);
cout << "Point based on vector shortvec =\n" << Mat(v) << ";\n"<
相应的输出结果如下所示:
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中取出像素点
本节,将介绍C++中常用的从Mat类的实例化对象中取出像素点的3种方法,并且,每种方法均对颜色空间进行缩减,即:
0~100范围的像素值为0;
100~200范围的像素值为100;
200~255范围的像素值为200。
并且,每种方法均统计了运行时间。
用指针访问像素
#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;
int colNumber = outputImage.cols*outputImage.channels();
for(int i = 0;i < rowNumber;i++)
{
uchar* data = outputImage.ptr(i);
for(int j = 0;j < colNumber;j++)
{
data[j] = data[j]/div*div;
}
}
}
在该程序中,先获取每一行的元素的个数,在双重遍历中,先获取第i行的首地址,然后通过指针获取第i的第j个元素,再对该元素进行处理。
该函数的运行效果如下图所示:
可见,遍历所有像素点并进行处理的时间为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();
Mat_::iterator it = outputImage.begin();
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;
}
}
该函数运行效果如下图所示:
在该方法中,直接使用迭代器进行处理,采用迭代器访问相对于数组越界的可能性,还是非常安全的,经实测,该方法遍历所有像素点并进行处理的时间为0.04秒左右。
可见,采用迭代器访问像素点的方法比采用指针访问像素点的方法慢了近一倍,因此,为提高处理速度,建议采用指针访问像素点。