前面我们提到轮廓发现、轮廓周长以及轮廓面积,然后通过轮廓面积和周长的固定关系来判断轮廓形状。但是针对不规则的形状,其实我们是很难通过数量关系来进行判断的。参考之前直线拟合的方式,我们也可以通过形状拟合的方式来对轮廓进行一定的分析。最常见的是将轮廓拟合成矩形等多边形。
API
最大外接矩形
public static Rect
boundingRect(Mat array
)
参数一:array,输入的灰度图或者二维点集合。
该方法用于求取包含输入图像中物体轮廓或者二维点集的最大外接矩形。返回值为Rect对象,可直接用rectangle()方法绘制矩形。
最小外接矩形
public static RotatedRect
minAreaRect(MatOfPoint2f points
)
参数一:points,输入的二维点集合。
该方法用于求取输入二维点集合的最小外接矩形。返回值为RotateRect对象。RotateRect类型和Rect类型虽然都是表示矩形,但是在表示方式上有一定的区别。通过查看成员变量可以很明显的看到差异。Rect是通过左上角的坐标来定位,默认横平竖直,然后通过宽高确定大小。而RotateRect则是通过center确定位置,angle结合宽高,计算各顶点的坐标,从而确定矩形。
public class Rect {
public int x
, y
, width
, height
;
public Rect(int x
, int y
, int width
, int height
) {
this.x
= x
;
this.y
= y
;
this.width
= width
;
this.height
= height
;
}
……
}
public class RotatedRect {
public Point center
;
public Size size
;
public double angle
;
public RotatedRect() {
this.center
= new Point();
this.size
= new Size();
this.angle
= 0;
}
……
}
轮廓多边形
public static void approxPolyDP(MatOfPoint2f curve
, MatOfPoint2f approxCurve
, double epsilon
, boolean closed
)
参数一:curve,输入轮廓像素点。
参数二:approxCurve,多边形逼近结果,包含多边形顶点坐标集。
参数三:epsilon,多边形逼近精度,原始曲线与逼近曲线之间的最大距离。
参数四:closed,逼近曲线是否闭合的标志,true表示封闭,false,表示不封闭。
该方法使用的是Douglas-Peucker algorithm(道格拉斯-普克算法)。Douglas-Peukcer算法由D.Douglas和T.Peueker于1973年提出,也称为拉默-道格拉斯-普克算法、迭代适应点算法、分裂与合并算法、D-P算法)是将曲线近似表示为一系列点,并减少点的数量的一种算法,是线状要素抽稀的经典算法。用它处理大量冗余的几何数据点,既可以达到数据量精简的目的,又可以在很大程度上保留几何形状的骨架。现有的线化简算法中,有相当一部分都是在该算法基础上进行改进产生的。它的特点是具有平移和旋转不变性,给定曲线与阈值后,抽样结果一定。
算法的基本思路为:
对每一条曲线的首末点虚连一条直线,求所有点与直线的距离,并找出最大距离值dmax,用dmax与限差D相比: 若dmax<D,这条曲线上的中间点全部舍去; 若dmax≥D,保留dmax对应的坐标点,并以该点为界,把曲线分为两部分,对这两部分重复使用该方法
操作
class ContourPolyActivity
: AppCompatActivity() {
private lateinit var mBinding
: ActivityContourPolyBinding
private var mSource
: Mat
= Mat()
private var mGray
: Mat
= Mat()
private var mBinary
: Mat
= Mat()
override fun onCreate(savedInstanceState
: Bundle
?) {
super.onCreate(savedInstanceState
)
mBinding
= DataBindingUtil
.setContentView(this, R
.layout
.activity_contour_poly
)
mBinding
.presenter
= this
val bgr
= Utils
.loadResource(this, R
.drawable
.contourpoly
)
Imgproc
.cvtColor(bgr
, mSource
, Imgproc
.COLOR_BGR2RGB
)
Imgproc
.cvtColor(bgr
, mGray
, Imgproc
.COLOR_BGR2GRAY
)
Imgproc
.GaussianBlur(mGray
, mGray
, Size(5.0, 5.0), 2.0, 2.0)
Imgproc
.threshold(
mGray
,
mBinary
,
20.0,
255.0,
Imgproc
.THRESH_BINARY
or Imgproc
.THRESH_OTSU
)
mBinding
.ivLena
.showMat(mBinary
)
}
fun findRect(flag
: Int
) {
val tmp
= mSource
.clone()
val contours
= mutableListOf
<MatOfPoint
>()
val hierarchy
= Mat()
Imgproc
.findContours(
mBinary
,
contours
,
hierarchy
,
Imgproc
.RETR_TREE
,
Imgproc
.CHAIN_APPROX_SIMPLE
)
for (i
in 0 until contours
.size
) {
when (flag
) {
0 -> {
title
= "最大外接矩形"
val rect
= Imgproc
.boundingRect(contours
[i
])
Imgproc
.rectangle(tmp
, rect
, Scalar(255.0, 255.0, 0.0), 4, Imgproc
.LINE_8
)
}
1 -> {
title
= "最小外接矩形"
val source
= MatOfPoint2f()
source
.fromList(contours
[i
].toList())
val rect
= Imgproc
.minAreaRect(source
)
val points
= arrayOfNulls
<Point
>(4)
val center
= rect
.center
rect
.points(points
)
Log
.d(App
.TAG
, "RotateRect: ${points.toList()}, Center:$center")
for (j
in 0..3) {
Imgproc
.line(
tmp
,
points
[j
% 4],
points
[(j
+ 1) % 4],
Scalar(255.0, 255.0, 0.0),
4,
Imgproc
.LINE_8
)
}
}
else -> {
title
= "轮廓多边形"
val result
= MatOfPoint2f()
val source
= MatOfPoint2f()
source
.fromList(contours
[i
].toList())
Imgproc
.approxPolyDP(source
, result
, 4.0, true)
Log
.d(App
.TAG
, "Poly: ${result.dump()}")
val points
= result
.toArray()
for (j
in points
.indices
) {
Imgproc
.line(
tmp
,
points
[j
% points
.size
],
points
[(j
+ 1) % points
.size
],
Scalar(255.0, 255.0, 0.0),
4,
Imgproc
.LINE_8
)
}
}
}
}
mBinding
.ivResult
.showMat(tmp
)
tmp
.release()
hierarchy
.release()
}
override fun onDestroy() {
mSource
.release()
mGray
.release()
mBinary
.release()
super.onDestroy()
}
}
效果
源码
https://github.com/onlyloveyd/LearningAndroidOpenCV
onlyloveyd
认证博客专家
Android
Kotlin
OpenCV
个人公众号【OpenCV or Android】,热爱Android、Kotlin、Flutter和OpenCV。毕业于华中科技大学计算机专业,曾就职于华为武汉研究所。目前在三线小城市生活,专注Android、OpenCV、Kotlin、Flutter等有趣的技术。