这是Adrain大神的形状识别系列教程的最后一个,完结撒花

这个项目我历尽千辛万苦,终于在查阅各种资料后能够成功转换成Android代码,实属不易啊。不过也确实是因为自己对OpenCV都不怎么了解,才导致了虽然看懂Adrain大神的博客后,但写成Android版本却频频出现bug。最近这两天也开始看Adrain大神的教程,也开始了解了OpenCV的一些基础,希望以后能够越来越顺利的在Andoid利用OpenCV。

话不多说,开始记录下最后一个教程的项目。以下,是本次项目要用到的图片。


首先,需要构建一个类ColorLabeler来标识颜色。以下是初始化一些数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义一个颜色名称数组
private String[] colorNames= {"blue","green","red"};
//用mat存放rgb和lab
private Mat[] rgbMat = new Mat[3];
private Mat[] labMat = new Mat[3];

public ColorLabeler(){
//对应颜色数组的蓝色
rgbMat[0] = new Mat(1,1,CvType.CV_8UC3,new Scalar(0,0,255));
//绿色
rgbMat[1] = new Mat(1,1,CvType.CV_8UC3,new Scalar(0,255,0));
//红色
rgbMat[2] = new Mat(1,1,CvType.CV_8UC3,new Scalar(255,0,0));
//把rgb转换成lab
for (int i=0;i<3;i++){
labMat[i] = new Mat();
Imgproc.cvtColor(rgbMat[i],labMat[i],Imgproc.COLOR_RGB2Lab);
}

}

在以上初始化数据的代码中,会发现有一个新东西–LAB。据Adrain大神说,他的这篇教程是基于计算已知颜色和所给图像区域的平均值的欧式距离来进行标识,而之所以选择LAB而不是HSV或RGB,是因为LAB在欧氏距离中有意义。所以,我们需要把已知颜色的RGB转换成LAB。

在创建rgbMat的时候耗费我巨多时间,因为我一直都对RGB在Mat中的保存毫不清楚,虽然原博客是用三个一行三列的数组去存,但是在JAVA中却不能。首先需要再了解一下Mat,上次说过Mat是一个矩阵指针,但更重要的它的类型。Mat具有多种数据深度,如下(摘自:https://blog.csdn.net/yang_xian521/article/details/7107786

• CV_8U - 8-bit unsigned integers ( 0..255 )
• CV_8S - 8-bit signed integers ( -128..127 )
• CV_16U - 16-bit unsigned integers ( 0..65535 )
• CV_16S - 16-bit signed integers ( -32768..32767 )
• CV_32S - 32-bit signed integers ( -2147483648..2147483647 )
• CV_32F - 32-bit floating-point numbers ( -FLT_MAX..FLT_MAX, INF, NAN )
• CV_64F - 64-bit floating-point numbers ( -DBL_MAX..DBL_MAX, INF, NAN )

然后,Mat更厉害的是,它还涉及一个叫做channel,即通道。以前我们接触的二维数组,都是一行一列存一个数据,但是在Mat中,有几个通道,一行一列中就可以存储几个数组,而我们如果使用Mat来存RGB明显是需要三通道来存,且看以下图片(摘自:https://blog.csdn.net/xiahouzuoxin/article/details/38298165):

单通道
单通道

RGB三通道
RGB三通道

因此,在这里我选择了CV_8UC3这种类型的Mat来存放RGB。

还有一个坑,我看到网上说Scarla存放的顺序其实是BGR,所以我一开始也是按这个顺序去定颜色,结果居然蓝色给我说成了红色,红色又说是蓝色。我才意识到,在JAVA这里,Scarla还是按照RGB的顺序,简直欲哭无泪~

OK,继续往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String label(Mat image, MatOfPoint contour){
//传入每个图形的轮廓
//由于画轮廓的时候是需要一个list,因此这里新建一个list来存放每一个图形轮廓,然后再画出来
List<MatOfPoint> contours = new ArrayList<>();
contours.add(contour);

String label = "unknown";
//定义一个新的mat来为图形增添蒙版
Mat mask = Mat.zeros(image.rows(),image.cols(),0);
//根据蒙版来画轮廓
Imgproc.drawContours(mask,contours,-1,new Scalar(255,255,255),-1);
//腐蚀化图像的结构元素,默认采用3*3的正方形
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));
Imgproc.erode(mask,mask,kernel,new Point(-1,-1),2);
//计算lab和蒙版的平均值,返回一个scalar对象
Scalar scalar = Core.mean(image,mask);
//由于下面要计算scalar和lab的欧氏距离,所以需要创建一个与lab的大小和类型都一样的mat
Mat mean = new Mat(1,1,CvType.CV_8UC3,scalar);

先定义一个函数label,我们需要识别图像image和轮廓contour作为参数。以上代码解决的是,如何计算欧式距离。首先看到drawContours那里,当我们执行到那里的时候会发现,其实是图上的一个形状被画出来,而且还是实心白色的。(PS:Adrain大神的代码基本都是把前景变白色,背景为黑色来进行操作。),画出来的效果如下:


这样,在计算的时候,就只计算蒙版了的形状,即白色形状,简化了计算量。此外,还使用图像腐蚀,用图像腐蚀,更有利于形状的分割。在erode的时候遇到了问题,由于原本的python代码只是简单的mask = cv2.erode(mask, None, iterations=2),而JAVA代码要求的参数比较多,是这样public static void erode(Mat src, Mat dst, Mat kernel, Point anchor, int iterations)。最后在opencv的library中找到解释,发现kernel和anchor其实有默认值,所以这里只需把默认值代入就行:
• element – structuring element used for erosion; if element=Mat() , a 3 x 3 rectangular structuring element is used.
• anchor – position of the anchor within the element; default value (-1, -1) means that the anchor is at the element center.

接着要算图片区域的平均值,这里需要注意的是mean这个函数很坑。一开始看着教程我传入了两个参数,但一直报错,说mask.empty()||mask.type==0,于是我一直以为是我传入的mask的值是空值所以报错,但是我测试的时候发现我传进去的mask不为空啊,而且类型也不是0。在opencv的library中指出:C++: Scalar mean(InputArray src, InputArray mask=noArray()),但由于它后面对mask的解释是“可选择”对象,所以我一直忽略了mask=noArray()这个东西,后来不断查资料改代码后才发现,之前报错的所说的东西不是指我哪里错,而是在提示我mask应该是要按照提示那样的。于是我最后把mask的类型改成0,才终于成功(开心到泪流满面ㄒoㄒ)。激动完毕,继续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//计算平均值跟各个颜色的lab的欧式距离
double dis = Integer.MAX_VALUE;
int min = 0;
for (int i=0;i<3;i++){

double d = Core.norm(labMat[i],mean);

if (d<dis){
dis = d;
min = i;
}
}
//得到的最小距离的颜色即为该形状的颜色
label = colorNames[min];
return label;

接下来就是计算欧式距离啦。把之前算出来的平均值,与红色、绿色、蓝色的lab值做欧式距离计算,选出距离最小的那个lab作为该形状的颜色。ok,颜色标识这部分代码总算是完成了,接下来要做的就是跟以前一样啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//读入图片
Bitmap srcBitmap;
srcBitmap = BitmapFactory.decodeResource(getResources(), id);

//建立几个Mat类型的对象
Mat rgbMat = new Mat();
Mat grayMat = new Mat();
Mat blur1 = new Mat();

//将原始的bitmap转换为mat型.
Utils.bitmapToMat(srcBitmap, rgbMat);

//以两个不同的模糊半径对图像做模糊处理,前两个参数分别是输入和输出图像,第三个参数指定应用滤波器时所用核的尺寸,最后一个参数指定高斯函数中的标准差数值
Imgproc.GaussianBlur(rgbMat, blur1, new Size(5, 5), 0);
//将图像转换为灰度
Imgproc.cvtColor(blur1, grayMat, Imgproc.COLOR_BGR2GRAY);
//将图像转换为lab
Mat labMat = new Mat(blur1.size(),blur1.type());
Imgproc.cvtColor(blur1,labMat,Imgproc.COLOR_RGB2Lab);
//图片二值化
Imgproc.threshold(grayMat, grayMat, 60, 255, Imgproc.THRESH_BINARY);

//寻找图形的轮廓
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(grayMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);

//加载颜色标签的类
ColorLabeler cl = new ColorLabeler();

for (MatOfPoint c : contours) {
//计算轮廓的中心,并根据中心确定形状
Moments m = Imgproc.moments(c);
int cx = (int) (m.m10 / m.m00 );
int cy = (int) (m.m01 / m.m00 );

//传入图片和每个形状的轮廓
String label = cl.label(labMat,c);

//画轮廓
Imgproc.drawContours(rgbMat, contours, -1, new Scalar(0, 255, 0), 2);
//在中心显示文字
Imgproc.putText(rgbMat, label, new Point(cx , cy ), Core.FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(255, 255, 255), 2);
//将Mat转换为位图
Bitmap grayBitmap= Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.RGB_565);
Utils.matToBitmap(rgbMat, grayBitmap);
img.setImageBitmap(grayBitmap);

}

以上代码就是传入图片,然后把每个图形的颜色写出来,具体就不再解释啦。最终效果图如下:


补上Adrain大神的教程:https://www.pyimagesearch.com/2016/02/15/determining-object-color-with-opencv/

于是终于,我学完了Adrain大神的这三个教程,简直感动。Adrain大神真的很好人,不仅推荐了书籍,而且他还有一个17天教程,所谓教程其实就是他把他写过的教程文章每天给我推送一篇。虽然全英很痛苦,但好在自己有那么一点底子,看起来也不算很累,也幸好python代码不算很难看,我也才能运用在Android上。希望经过17天后,我能大概了解OpenCV,以及提升英语能力2333。最后补上这三个教程我写的Android完整代码:https://github.com/SecretLin/shape-detection-with-OpenCV/tree/master

代码可能写得不好,希望懂行的大佬多多指教。