这是我参与更文挑战的第 25 天,活动详情查看: 更文挑战
前面我们介绍的两种角点检测方法Harris角点检测和Shi-Timasi角点检测的精度都是像素级别。但是在跟踪、三维重建,相机校正等应用上我们都需要精确的角点位置坐标,即亚像素级别。
原理
OpenCV亚像素角点检测方法是从亚像素角点到周围像素点的矢量应垂直于图像的灰度梯度这个观察事实得到的,通过最小化误差函数的迭代方法来获得亚像素级精度的坐标值。
图中:
- ,即待求的亚像素点;
- ,即周围的点
- ,第一个向量
- 处的梯度,第二个向量
如上图所示:
- 位于白色区域,,所以
- 位于边缘区域,,但是梯度方向与垂直,所以
所以,要寻找的亚像素点满足条件:
然后利用最小二乘法求解:
要求出,需要知道和:
通过Harris或者Shi-Tomasi算法检测出来的角点坐标为整数,设为 ,以 为中心,设定一个窗口,窗口内的点组成,针对,其梯度、梯度转置以及两者乘积分别为:
按照上述公式,我们每次都可以求出一个亚像素点,以求出的点为中心,我们可以继续求取亚像素点,不断迭代。计算何时终止呢?存在以下两种方式:
- 指定迭代次数
- 指定结果精度
API
public static void cornerSubPix(Mat image, Mat corners, Size winSize, Size zeroZone, TermCriteria criteria)
- 参数一:image,输入图像,必须是类型为CV_8U或者CV_32F的单通道图像;
- 参数二:corners,既是输入又是输出的角点坐标,传入像素级角点,传出亚像素级角点;
- 参数三:winSize,搜索窗口尺寸的一半。例如winSize=Size(5, 5),那么搜索窗口的大小为Size(5*2+1, 5*2+1) = Size(11, 11);
- 参数四:zeroZone,搜索区域中间"盲区”大小的一半,扫过该区域时不参与计算,避免自相关矩阵出现可能的奇异值。如果该参数为(-1, -1)表示不设置“盲区”;
- 参数五:criteria,终止角点优化迭代的条件。
TermCriteria模板类
TermCriteria代表的是迭代算法的终止条件。类中有三个变量第一个是终止条件类型,第二个参数为迭代的最大次数,最后一个是特定的阈值。
public int type; // 终止条件类型
public int maxCount; // 迭代的最大次数
public double epsilon; // 特定阈值
针对第一个参数终止条件类型,只有三种类型可选:
public static final int COUNT = 1; // 迭代终止条件为达到最大迭代次数终止
public static final int MAX_ITER = COUNT; // 迭代到阈值终止
public static final int EPS = 2; // 两者都作为迭代终止条件
操作
private fun doCornerSubPix() {
val gray = bgr.toGray()
val corners = MatOfPoint()
val maxCorners = 100
val qualityLevel = 0.01
val minDistance = 10.0
Imgproc.goodFeaturesToTrack(
gray,
corners,
maxCorners,
qualityLevel,
minDistance,
Mat(),
3,
false,
0.04
)
Log.v(App.TAG, "Number of corners detected: ${corners.rows()}")
val cornersData = IntArray((corners.total() * corners.channels()).toInt())
corners.get(0, 0, cornersData)
for (i in 0 until corners.rows()) {
Log.v(App.TAG,
"Corner [" + i + "] = (" + cornersData[i * 2] + "," + cornersData[i * 2 + 1] + ")"
)
}
val matCorners = Mat(corners.rows(), 2, CV_32F)
val matCornersData = FloatArray((matCorners.total() * matCorners.channels()).toInt())
matCorners.get(0, 0, matCornersData);
for (i in 0 until corners.rows()) {
Imgproc.circle(
rgb, Point(
cornersData[i * 2].toDouble(),
cornersData[i * 2 + 1].toDouble()
), 4,
Scalar(0.0, 255.0, 0.0), Imgproc.FILLED
)
matCornersData[i * 2] = cornersData[i * 2].toFloat()
matCornersData[i * 2 + 1] = cornersData[i * 2 + 1].toFloat()
}
GlobalScope.launch(Dispatchers.Main) {
mBinding.ivResult.showMat(rgb)
}
matCorners.put(0, 0, matCornersData);
val winSize = Size(5.0, 5.0)
val zeroSize = Size(-1.0, -1.0)
val criteria = TermCriteria(TermCriteria.EPS + TermCriteria.MAX_ITER, 40, 0.01)
Imgproc.cornerSubPix(gray, matCorners, winSize, zeroSize, criteria)
matCorners.get(0, 0, matCornersData);
for (i in 0 until corners.rows()) {
Log.v(App.TAG,
"Corner SubPix [" + i + "] = (" + matCornersData[i * 2] + "," + matCornersData[i * 2 + 1] + ")"
)
}
}