在上一篇文章中,写出了如何画出形状的轮廓。而这一次,则在上一次的基础上,通过对轮廓的特征来判断图形是什么形状。

这一次使用的图还是上一次的那张图。


首先,先来构造一个类ShapeDetector,然后构建一个函数detect(MatOfPoint2f c)用于识别形状识别。
1
2
3
4
5
6
String shape = "unknown";
//计算轮廓的周长
double peri = Imgproc.arcLength(c,true);
MatOfPoint2f approx = new MatOfPoint2f();
//得到轮廓的大概
Imgproc.approxPolyDP(c,approx,0.04 * peri,true);

这里引入了contour approximation,轮廓近似,由于怕翻译得不好,下文继续采用contour approximation。这种近似是源于Ramer–Douglas–Peucker algorithm算法,这个算法的大概意思就是说,利用线段来近似描绘曲线,以减少源曲线的点数。(想要了解更多的小伙伴可以去百度了解,这里就不详细解释。)在OpenCV库中,使用Imgproc.approxPolyDP()就可以得到contour approximation。而为了取得contour approximation,我们要先得到轮廓的周长,然后再以此来得到近似。

public static void approxPolyDP(MatOfPoint2f curve, MatOfPoint2f approxCurve, double epsilon, boolean closed)参数详解如下:

MatOfPoint2f curve:一般是由图像的轮廓点组成的点集,这里我们是使用之前所得到的轮廓

MatOfPoint2f approxCurve:表示输出的轮廓近似contour approximation

double epsilon:主要表示输出的精度,就是两个轮廓点之间最大距离数,常用值通常在原始轮廓周长的1-5%范围内。

boolean closed:表示输出的多边形是否封闭

ps:想要了解更多contour approximation,我之前提到的外国作者Adrian也给出了一个链接:https://www.pyimagesearch.com/pyimagesearch-gurus/

当我们得到了轮廓的近似曲线后,我们可以通过轮廓曲线有多少个顶点来判断是什么形状:

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
//如果是三角形形状,则有三个顶点
if (approx.toList().size()==3){
shape = "triangle";
}

//如果有四个顶点,则是正方形或者长方形
else if (approx.toList().size()==4){

//计算轮廓的边界框并使用边界框来计算宽高比
Rect rect = new Rect();
rect = Imgproc.boundingRect(new MatOfPoint(approx.toArray()));
float ar = rect.width/(float)rect.height;

//正方形的宽高比接近为1,除此之外就为长方形
if (ar>=0.95 && ar<=1.05){
shape = "square";
}else {
shape = "rectangle";
}
}

//如果是五角形,则有五个顶点
else if (approx.toList().size()==5){
shape = "pentagon";
}

//除了以上情况之外,我们假设为圆形
else {
shape = "circle";
}

这里必须要注意的是,轮廓曲线是由顶点列表组成,所以我们才可以以此来判断形状。如上代码,比如说轮廓有三个点,那么就说明该形状有三个顶点,那么我们可以确认是三角形。而对于四个点,我们还可以进一步判断是正方形还是长方形。我们知道,正方形四条边一样长,那么我们就可以用图形的宽高比来做进一步确认。这里使用boundingRect()是指用一个最小的矩形,把找到的形状包起来,所以我们就可以得到一个矩形,然后再计算宽高比。从一开始所给的图片中,当排除了所有的有角的图形后,只剩下圆形,因此这里可以粗略把其他情况视为圆形。
ps:需要注意的是,这里需要不断转换MatOfPoint2f和MatOfPoint
至此,形状检测的函数完成。

接下来,就是像之前一样,寻找各个图形的轮廓。由于之前已经解释过了,这里就不再解释下面各个函数的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//读入图片
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.cvtColor(rgbMat, grayMat, Imgproc.COLOR_BGR2GRAY);
//以两个不同的模糊半径对图像做模糊处理,前两个参数分别是输入和输出图像,第三个参数指定应用滤波器时所用核的尺寸,最后一个参数指定高斯函数中的标准差数值
Imgproc.GaussianBlur(grayMat, blur1, new Size(5, 5), 0);
//图片二值化
Imgproc.threshold(blur1, blur1, 60, 255, Imgproc.THRESH_BINARY);

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

当找到轮廓后,就要开始画轮廓并标记所属形状。标记是把之前的文字center变成了形状的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//加载形状检测的类
ShapeDetector sd = new ShapeDetector();
for (MatOfPoint c : contours) {
//计算轮廓的中心,并根据中心确定形状
Moments m = Imgproc.moments(c);
int cx = (int) (m.m10 / m.m00 );
int cy = (int) (m.m01 / m.m00 );
String shape = sd.detect(new MatOfPoint2f(c.toArray()));

//画轮廓
Imgproc.drawContours(rgbMat, contours, -1, new Scalar(0, 255, 0), 2);
//在中心显示文字
Imgproc.putText(rgbMat, shape, 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);

}

效果图如下:

效果图
效果图

最后再次附上Adrian的教程:https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/