学习 Android (二十一) 学习 OpenCV (六)
学习 Android (二十一) 学习 OpenCV (六)
在上一章节,我们对图像形态学操作有了一定的学习了解,接下来让我们继续学习 OpenCV 相关的内容吧
29 霍夫直线检测
29.1 什么是霍夫直线
霍夫直线变换是一种用于在图像中检测直线的经典算法。其核心思想是将图像空间中的直线转换到参数空间进行检测,利用投票机制来判断哪些直线是存在的。
-
图像空间 (x, y): 我们看到的普通图像。在这里,一条直线可以用方程
y = kx + b
表示。但当直线是垂直时,斜率k
为无穷大,这种表示法有缺陷。 -
霍夫参数空间 (ρ, θ): 为了解决上述问题,我们使用法线表示法。一条直线可以用两个参数唯一确定:
-
ρ
(rho): 原点到该直线的垂直距离。 -
θ
(theta): 该垂线与x轴正方向的夹角(弧度制)。(图像空间的一条直线
L
对应霍夫空间的一个点(ρ₀, θ₀)
)直线的方程变为:
ρ = x * cosθ + y * sinθ
-
29.1.1 点的转换与投票机制
这是理解霍夫变换最关键的一步:
-
图像空间的一个点对应霍夫空间的一条正弦曲线。
- 图像空间中的一个点
(x₀, y₀)
可以穿过无数条直线。这些直线在霍夫空间中满足方程ρ = x₀ * cosθ + y₀ * sinθ
,这是一条正弦曲线。
- 图像空间中的一个点
-
图像空间的一条直线对应霍夫空间的一个点。
-
图像空间中,同一条直线
L
上的所有点(x₁, y₁), (x₂, y₂), ...
,每个点都对应一条霍夫空间的正弦曲线。 -
所有这些曲线会相交于同一个点
(ρ’, θ’)
。 -
这个交点
(ρ’, θ’)
就是直线L
的参数。
-
-
投票机制:
-
算法创建一个二维数组(称为累加器),用来代表离散化的
(ρ, θ)
空间。 -
对于边缘图像中的每一个边缘点(例如Canny检测后的白色点),算法根据其
(x, y)
坐标,计算所有可能的θ
值下的ρ
值。 -
对于每一对
(ρ, θ)
,就在累加器对应的格子中投一票。 -
最终,得票数(累加值)最高的
(ρ, θ)
格子,就最有可能是图像中存在的一条直线。
-
29.2 核心函数详解
在 Android OpenCV Java API 中,我们主要使用 Imgproc
类中的两个函数。
-
HoughLinesP()
(概率霍夫变换)// 函数签名 public static void HoughLinesP(Mat image, // 输入图像:必须是8位单通道二值图像(通常是Canny边缘检测后的结果)Mat lines, // 输出向量:检测到的直线。每条线由一个4元素Vec4i表示,即 [x1, y1, x2, y2]double rho, // 距离分辨率 ρ 的精度(以像素为单位)。通常设为1double theta, // 角度分辨率 θ 的精度(以弧度为单位)。通常设为 Math.PI/180 (1度)int threshold, // 投票阈值。只有得票数超过此值的直线才会被返回。这是最重要的参数。double minLineLength, // 最小直线长度。小于此值的线段会被忽略。double maxLineGap // 最大允许间隙。在同一条直线上的两点之间,如果间隙小于此值,则它们会被连接起来。 )// 输出 Mat `lines` 的结构: // 它是一个 Rows x 1 的矩阵,数据类型是 CvType.CV_32SC4(32位有符号4通道)。 // 通过 lines.get(i, 0) 可以获取一个 double[4] 数组,包含 [x1, y1, x2, y2]。
-
HoughLines()
(标准霍夫变换)public static void HoughLines(Mat image,Mat lines, // 输出向量:每条线由一个2元素Vec2f表示,即 [ρ, θ]double rho,double theta,int threshold // 投票阈值 ) // 输出 Mat `lines` 是一个 Rows x 1 的矩阵,数据类型是 CvType.CV_32FC2。 // 通过 lines.get(i, 0) 可以获取一个 double[2] 数组,包含 [ρ, θ]。
特性维度 | 标准霍夫变换 (SHT) - HoughLines | 概率霍夫变换 (PHT) - HoughLinesP |
---|---|---|
基本原理 | 全局处理:对边缘图像中的每一个边缘点进行计算和投票。 | 随机抽样:随机选取一个边缘点子集进行计算和投票。 |
输出结果 | 直线的参数:(ρ, θ) 对的集合。表示无限长的直线。 | 线段的端点:(x1, y1, x2, y2) 的集合。表示有起止点的有限长线段。 |
计算效率 | 计算量大,速度慢。因为要处理所有点。 | 计算量小,速度快。是SHT的一种优化,通常快几倍甚至一个数量级。 |
准确性 | 更全面、更精确。理论上不会漏掉任何符合条件的直线。 | 由于随机抽样,可能遗漏一些得票数刚好超过阈值但未被抽到的线段。 |
可控性 | 只能控制投票阈值 (threshold )。 | 控制参数更多,除了阈值,还能控制最小线段长度 (minLineLength ) 和最大线段间隙 (maxLineGap )。 |
结果直观性 | 不直观。得到 (ρ, θ) 后,需要自行转换才能绘制或使用,且是无限长的直线。 | 非常直观。直接得到线段的两个端点坐标,可以直接用于绘制和后续几何计算。 |
内存占用 | 较高。需要构建一个完整的、高分辨率的累加器数组。 | 较低。算法过程更高效,内存开销相对较小。 |
29.3 应用场景
霍夫直线检测在计算机视觉和Android应用中用途广泛:
-
文档扫描与透视校正: 检测文档的边缘直线,然后通过透视变换将其“拉直”。
-
道路车道线检测: 在自动驾驶或ADAS系统中,用于识别车辆行驶的车道。
-
建筑和工业检测: 检测物体的边缘是否平直,用于质量控制和测量。
-
增强现实 (AR): 检测现实世界中的平面(如桌面、墙壁)来放置虚拟物体。
-
艺术创作与图像处理: 从图像中提取线条元素用于创作。
29.4 示例
HoughLinesActivity.java
public class HoughLinesPActivity extends AppCompatActivity {private ActivityHoughLinesPactivityBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat, mGrayMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityHoughLinesPactivityBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mOriginalMat = Utils.loadResource(this, R.drawable.lena);mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);showMat(mBinding.ivOriginal, mOriginalMat);detectLinesStandard(mGrayMat);detectLines(mGrayMat);} catch (Exception e) {e.printStackTrace();}}/*** 标准霍夫变换:返回的是 (rho, theta),需要手动转为直线坐标*/private void detectLinesStandard(Mat srcMat) {// 使用高斯模糊降噪Mat blurred = new Mat();Imgproc.GaussianBlur(srcMat, blurred, new Size(5, 5), 1.2);// Canny 边缘检测Mat edges = new Mat();double cannyLowThreshold = 50;double cannyHighThreshold = cannyLowThreshold * 3;Imgproc.Canny(blurred, edges, cannyLowThreshold, cannyHighThreshold);// 标准霍夫变换 (只返回 rho 和 theta)Mat lines = new Mat();double rho = 1; // 像素精度double theta = Math.PI / 180; // 角度精度(1度)int threshold = 100; // 最小投票数Imgproc.HoughLines(edges, lines, rho, theta, threshold);// 在原始图像上绘制检测到的直线Mat result = srcMat.clone();for (int i = 0; i < lines.rows(); i++) {double[] line = lines.get(i, 0); // [rho, theta]double r = line[0], t = line[1];double cosT = Math.cos(t), sinT = Math.sin(t);double x0 = r * cosT, y0 = r * sinT;// 在图像上绘制一条足够长的直线Point pt1 = new Point(x0 + 1000 * (-sinT), y0 + 1000 * (cosT));Point pt2 = new Point(x0 - 1000 * (-sinT), y0 - 1000 * (cosT));Imgproc.line(result, pt1, pt2, new Scalar(0, 0, 255), 2); // 红色}// 显示结果showMat(mBinding.ivHoughLines, result);safeRelease(blurred);safeRelease(edges);safeRelease(lines);safeRelease(result);}/*** 概率霍夫变换:返回的是 (rho, theta),需要手动转为直线坐标*/private void detectLines(Mat srcMat) {// 使用高斯模糊降噪Mat blurred = new Mat();Imgproc.GaussianBlur(srcMat, blurred, new Size(5, 5), 1.2);// Canny 边缘检测Mat edges = new Mat();double cannyLowThreshold = 50; // 低阈值double cannyHighThreshold = cannyLowThreshold * 3; // 高阈值通常是低阈值的三倍Imgproc.Canny(blurred, edges, cannyLowThreshold, cannyHighThreshold);// 进行霍夫变换Mat lines = new Mat();double rho = 1; // 像素精度double theta = Math.PI / 180; // 角度精度(1度)int threshold = 50; // 最小投票数double minLineLength = 100; // 最小线长度double maxLineGap = 20; // 最大允许间隙Imgproc.HoughLinesP(edges, lines, rho, theta, threshold, minLineLength, maxLineGap);// 在原始图像上绘制检测到的直线Mat result = srcMat.clone();for (int i = 0; i < lines.rows(); i++) {double[] line = lines.get(i, 0);if (line != null) {Point pt1 = new Point(line[0], line[1]);Point pt2 = new Point(line[2], line[3]);// 用红色绘制线段, 线宽为 3Imgproc.line(result, pt1, pt2, new Scalar(255, 0, 0), 3);}}// 将结果显示在 ImageView 上showMat(mBinding.ivHoughLinesP, result);// 释放 Mat 对象, 防止内存泄漏哦safeRelease(blurred);safeRelease(edges);safeRelease(lines);safeRelease(result);}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) safeRelease(mOriginalMat);if (mGrayMat != null) safeRelease(mGrayMat);}
}
30 霍夫圆检测
30.1 什么是霍夫圆检测
霍夫圆检测是霍夫变换的一种扩展,用于检测图像中的圆形。与直线检测相比,圆检测更加复杂,因为圆形需要三个参数来描述:圆心坐标 (x, y) 和半径 r。
霍夫直线到霍夫圆
-
直线检测:使用两个参数 (ρ, θ) 的极坐标系统
-
圆检测:需要三个参数 (x, y, r) 的三维参数空间
圆的标准方程
圆的数学表达式为:(x - a)² + (y - b)² = r²
其中:
-
(a, b) 是圆心坐标
-
r 是圆的半径
三位累加器
-
霍夫圆检测需要在三维参数空间 (a, b, r) 中创建累加器
-
每个边缘点投票给所有可能包含它的圆
-
得票数最多的 (a, b, r) 组合对应图像中最可能存在的圆
实现步骤
由于标准霍夫圆检测计算量极大(三维空间),OpenCV 使用的是优化的霍夫梯度法,主要步骤:
-
边缘检测:使用Canny算子或Sobel算子检测图像边缘
-
计算梯度:对边缘点计算梯度方向(使用Sobel算子)
-
沿梯度方向投票:对于每个边缘点,沿梯度方向在参数空间中投票给可能的圆心
-
累加器峰值检测:找到累加器中的峰值,这些就是候选圆心
-
确定半径:对于每个候选圆心,计算边缘点到圆心的距离,统计这些距离确定最可能的半径
这种方法大大减少了计算量,因为它:
-
将三维问题降为二维(先找圆心,再确定半径)
-
利用梯度方向信息缩小搜索空间
30.2 核心函数详解
OpenCV 提供了 HoughCircles()
函数来实现霍夫圆检测。
public static void HoughCircles(Mat image, // 输入图像:8位单通道灰度图像Mat circles, // 输出向量:检测到的圆,每个圆表示为3元素向量 (x, y, radius)int method, // 检测方法:目前只实现了一种方法:Imgproc.HOUGH_GRADIENTdouble dp, // 累加器分辨率与图像分辨率的反比double minDist, // 检测到的圆心之间的最小距离double param1, // 第一个方法特定参数:Canny边缘检测的高阈值double param2, // 第二个方法特定参数:圆心检测阈值int minRadius, // 最小圆半径int maxRadius // 最大圆半径
)
参数 | 说明 | 调优建议 |
---|---|---|
image | 输入图像,必须是8位单通道灰度图 | 必须先转换为灰度图 |
circles | 输出向量,存储检测到的圆 | 每个圆是一个包含3个值的数组:[圆心x, 圆心y, 半径] |
method | 检测方法 | 目前只支持 Imgproc.HOUGH_GRADIENT |
dp | 累加器分辨率与图像分辨率的反比 | 通常设为1。设为2表示累加器是图像一半的大小,计算更快但精度降低 |
minDist | 检测到的圆心之间的最小距离 | 值太小会检测到多个相邻圆,值太大会漏掉一些圆。一般设为图像宽高的1/8-1/10 |
param1 | Canny边缘检测的高阈值 | 低阈值自动设为高阈值的一半。值越高,检测到的边缘越少 |
param2 | 圆心检测阈值 | 最重要的参数。值越小,检测到的圆越多(包括假圆);值越大,只检测更明显的圆 |
minRadius | 最小圆半径 | 设为0表示不限制,但设置合适的范围可提高检测速度和准确性 |
maxRadius | 最大圆半径 | 设为0表示不限制,设置合适的范围可显著提高性能 |
30.3 应用场景
霍夫圆检测在计算机视觉和Android应用中有着广泛用途:
-
工业检测:检测零件中的孔洞、轴承、瓶盖等圆形物体
-
生物医学:细胞计数、瞳孔检测、显微镜图像分析
-
交通监控:检测车辆轮胎、交通标志中的圆形部分
-
日常生活:硬币识别、瓶盖检测、眼球追踪
-
增强现实:识别现实世界中的圆形标记或物体
30.4 示例
CircleDetectionActivity.java
public class CircleDetectionActivity extends AppCompatActivity {private ActivityCircleDetectionBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat, mGrayMat, mResultMat;private int mParam1 = 100, mParam2 = 30;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityCircleDetectionBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mOriginalMat = Utils.loadResource(this, R.drawable.hat_transforms);// 转换为灰度图mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);mResultMat = new Mat(mOriginalMat.size(), mOriginalMat.type());showMat(mBinding.imageView, mOriginalMat);mBinding.btnDetect.setOnClickListener(view -> {detectCircles();});mBinding.seekBarParam1.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {mParam1 = progress;updateParamDisplay();}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});mBinding.seekBarParam2.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {mParam2 = progress;updateParamDisplay();}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});} catch (Exception e) {e.printStackTrace();}}private void updateParamDisplay() {mBinding.textParam1Value.setText("Param1 (Canny阈值): " + mParam1);mBinding.textParam2Value.setText("Param2 (圆心阈值): " + mParam2);}private void detectCircles() {// 应用中值模糊降噪(比高斯模糊更适合保留边缘)Mat blurred = new Mat();Imgproc.medianBlur(mGrayMat, blurred, 5);// 霍夫圆检测Mat circles = new Mat();Imgproc.HoughCircles(blurred, // 输入图像(已模糊的灰度图)circles, // 输出圆向量Imgproc.HOUGH_GRADIENT, // 检测方法1.0, // dp=1: 累加器与图像相同分辨率blurred.rows() / 8.0, // minDist: 圆心间最小距离(图像高度的1/8)mParam1, // param1: Canny边缘检测高阈值mParam2, // param2: 圆心检测阈值(越小检测到的圆越多)0, // minRadius: 最小半径(0表示不限制)0 // maxRadius: 最大半径(0表示不限制));// 清空 mResultMat 并拷贝原图数据mOriginalMat.copyTo(mResultMat);if (circles.empty()) {safeRelease(blurred);safeRelease(circles);return;}for (int i = 0; i < circles.cols(); i++) {double[] circle = circles.get(0, i);if (circle != null) {Point center = new Point(Math.round(circle[0]), Math.round(circle[1]));int radius = (int) Math.round(circle[2]);// 绘制圆周边Imgproc.circle(mResultMat, center, radius, new Scalar(0, 255, 0), 3);// 绘制圆心Imgproc.circle(mResultMat, center, 3, new Scalar(0, 0, 255), -1);}}// 显示结果showMat(mBinding.imageView, mResultMat);// 释放资源safeRelease(blurred);safeRelease(circles);}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) safeRelease(mOriginalMat);if (mGrayMat != null) safeRelease(mGrayMat);if (mResultMat != null) safeRelease(mResultMat);}
}
通过观察结果我们可以发现,为什么如果我把圆心阈值调低,在只有一个圆的图形中进行检测,还是会画出很多圆?
它其实就是 霍夫圆检测的原理导致的必然现象。
霍夫圆检测的机制:
HoughCircles
使用的是 梯度信息 + 累加器投票:
-
Canny 得到边缘点。
-
每个边缘点根据梯度方向去“投票”,看它可能属于哪些圆。
-
最终在累加器中找“峰值”,这些峰值就被当作圆心 + 半径。
为什么降低 param2
(圆心阈值)会多出一堆圆
-
aram2
是 累加器阈值:数值越大,必须有足够多的边缘点支持才能算作一个圆。 -
如果把
param2
调得很低,就会允许很多 置信度很低的圆心 也被输出。 -
在“只有一个圆”的图像中:
-
理论上确实只有一个真实圆,
-
但由于噪声、边缘检测的不精确(比如边缘像素不是完美的圆,而是锯齿状),累加器会在同一个真实圆附近产生多个相似的“候选圆”。
-
当阈值低时,这些候选圆也被当作检测结果,所以你看到 很多大小相近、位置接近的圆被画出来。
-
换句话说:
它们不是“新圆”,而是“同一个真实圆的重复候选解”。
31 直线拟合
31.1 什么是直线拟合
直线拟合是图像处理中一种常用的技术,用于从一组点中找到最能代表这些点分布趋势的最佳拟合直线。在 OpenCV 中,直线拟合基于最小二乘法原理。
-
最小二乘法原理
最小二乘法的目标是找到一条直线,使得所有数据点到这条直线的垂直距离的平方和最小。
对于直线方程
y = kx + b
,最小二乘法的解为:k = (nΣxy - ΣxΣy) / (nΣx² - (Σx)²) b = (Σy - kΣx) / n
31.2 核心函数详解
核心函数原型为
public static void fitLine(Mat points, Mat line, int distType, double param, double reps, double aeps)
参数详解
-
points
: 输入点集,可以是2D或3D点 -
line
: 输出直线参数-
对于2D:
[vx, vy, x0, y0]
-
对于3D:
[vx, vy, vz, x0, y0, z0]
-
-
distType
: 距离类型(见上述距离类型) -
param
: 某些距离类型的参数(通常设为0) -
reps
: 半径精度(通常设为0.01) -
aeps
: 角度精度(通常设为0.01)
31.3 应用场景
直线拟合在计算机视觉中有广泛的应用:
-
车道线检测
-
从道路图像中检测和拟合车道线
-
用于自动驾驶和辅助驾驶系统
-
-
文档校正
-
检测文档边缘并校正透视变形
-
用于扫描和OCR应用
-
-
工业检测
-
检测产品的直线边缘
-
测量物体的方向和位置
-
-
机器人导航
-
从传感器数据中提取环境的结构信息
-
用于路径规划和导航
-
-
建筑测量
-
从图像中提取建筑物的直线特征
-
用于建筑测量和建模
-
31.4 示例
FitLineActivity.java
public class FitLineActivity extends AppCompatActivity {private ActivityFitLineBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityFitLineBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mOriginalMat = Utils.loadResource(this, R.drawable.lena);if (mOriginalMat.empty()) {Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();return;}OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);} catch (Exception e) {e.printStackTrace();}mBinding.btnProcess.setOnClickListener(view -> {if (mOriginalMat == null || mOriginalMat.empty()) {Toast.makeText(this, "Please load image first", Toast.LENGTH_SHORT).show();return;}Mat grayMat = new Mat();Mat edges = new Mat();Mat resultMat = mOriginalMat.clone();Mat houghRresultMat = mOriginalMat.clone();Mat humanResultMat = mOriginalMat.clone();try {// 转换为灰度图Imgproc.cvtColor(mOriginalMat, grayMat, Imgproc.COLOR_BGR2GRAY);// 边缘检测Imgproc.Canny(grayMat, edges, 50, 150);// 显示边缘检测结果OpenCVHelper.showMat(mBinding.ivEdges, edges);// 从边缘中提取点集并进行直线拟合fitLinesFromEdges(edges, resultMat);compareWithHoughLines(edges, houghRresultMat);demonstrateWithArtificialPoints(humanResultMat);// 显示直线拟合结果OpenCVHelper.showMat(mBinding.ivLineFitting, resultMat);OpenCVHelper.showMat(mBinding.ivHoughLineFitting, houghRresultMat);OpenCVHelper.showMat(mBinding.ivHumanLineFitting, humanResultMat);} catch (Exception e) {e.printStackTrace();} finally {OpenCVHelper.safeRelease(grayMat);OpenCVHelper.safeRelease(edges);OpenCVHelper.safeRelease(resultMat);OpenCVHelper.safeRelease(houghRresultMat);OpenCVHelper.safeRelease(humanResultMat);}});}private void fitLinesFromEdges(Mat edges, Mat resultMat) {// 查找轮廓List<MatOfPoint> contours = new ArrayList<>();Mat hierarchy = new Mat();Imgproc.findContours(edges, contours, hierarchy,Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);// 对每个轮廓进行直线拟合for (MatOfPoint contour : contours) {// 忽略太小的轮廓if (contour.total() < 50) {continue;}// 将轮廓转换为点集Mat points = new Mat();contour.convertTo(points, CvType.CV_32F);// 拟合直线Mat line = new Mat(4, 1, CvType.CV_32F);Imgproc.fitLine(points, line, Imgproc.DIST_L2, 0, 0.01, 0.01);// 从直线参数中提取信息float[] lineData = new float[4];line.get(0, 0, lineData);float vx = lineData[0];float vy = lineData[1];float x0 = lineData[2];float y0 = lineData[3];// 计算直线上的两个点(延长到图像边界)Point pt1 = calculateLinePoint(x0, y0, vx, vy, resultMat.width(), resultMat.height(), true);Point pt2 = calculateLinePoint(x0, y0, vx, vy, resultMat.width(), resultMat.height(), false);// 在图像上绘制拟合的直线Imgproc.line(resultMat, pt1, pt2, new Scalar(0, 0, 255), 2);// 释放资源points.release();line.release();}hierarchy.release();}/*** 计算直线与图像边界的交点*/private Point calculateLinePoint(double x0, double y0, double vx, double vy, int imgWidth, int imgHeight, boolean leftTop) {// 计算参数 t 范围double t;if (leftTop) {t = Math.max(Math.max((-x0) / vx, (-y0) / vy),Math.max((imgWidth - x0) / vx, (imgHeight - y0) / vy));} else {t = Math.min(Math.min((-x0) / vx, (-y0) / vy),Math.min((imgWidth - x0) / vx, (imgHeight - y0) / vy));}// 计算交点坐标double x = x0 + t * vx;double y = y0 + t * vy;return new Point(x, y);}/*** 使用Hough变换检测直线并进行拟合比较*/private void compareWithHoughLines(Mat edges, Mat resultMat) {Mat lines = new Mat();try {// 使用 Hough 变换检测直线Imgproc.HoughLinesP(edges, lines, 1, Math.PI / 180, 50, 50, 10);// 绘制检测到的直线for (int i = 0; i < lines.rows(); i++) {double[] line = lines.get(i, 0);double x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3];Imgproc.line(resultMat, new Point(x1, y1), new Point(x2, y2), new Scalar(255, 0, 0), 2);}} finally {OpenCVHelper.safeRelease(lines);}}/*** 从人工生成的点集演示直线拟合*/private void demonstrateWithArtificialPoints(Mat resultMat) {// 创建一些近似在一条直线上的点Mat points = new Mat(20, 1, CvType.CV_32FC2);float[] pointsData = new float[40];// 生成近似直线的点(加入一些噪声)for (int i = 0; i < 20; i++) {pointsData[i * 2] = i * 20 + (float)(Math.random() * 10 - 5); // x坐标pointsData[i * 2 + 1] = i * 15 + 50 + (float)(Math.random() * 10 - 5); // y坐标}points.put(0, 0, pointsData);// 拟合直线Mat line = new Mat(4, 1, CvType.CV_32F);Imgproc.fitLine(points, line, Imgproc.DIST_L2, 0, 0.01, 0.01);// 提取直线参数float[] lineData = new float[4];line.get(0, 0, lineData);float vx = lineData[0];float vy = lineData[1];float x0 = lineData[2];float y0 = lineData[3];// 计算直线上的两个点Point pt1 = calculateLinePoint(x0, y0, vx, vy, resultMat.width(), resultMat.height(), true);Point pt2 = calculateLinePoint(x0, y0, vx, vy, resultMat.width(), resultMat.height(), false);// 绘制点和拟合的直线for (int i = 0; i < 20; i++) {Imgproc.circle(resultMat, new Point(pointsData[i * 2], pointsData[i * 2 + 1]),3, new Scalar(0, 255, 0), -1);}Imgproc.line(resultMat, pt1, pt2, new Scalar(0, 255, 255), 2);points.release();line.release();}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);}
}
方法 | 输入 | 输出 | 数学思想 | 结果特征 | 适合用途 |
---|---|---|---|---|---|
fitLinesFromEdges | 边缘图的轮廓点集 | 无限延长直线 | 最小二乘拟合 | 一条趋势直线 | 点集接近直线时做拟合 |
compareWithHoughLines | 边缘图像像素 | 有限长直线段 | 霍夫变换(投票) | 一组实际直线段 | 检测图像中的真实直线 |
demonstrateWithArtificialPoints | 人造点集 | 无限延长直线 | 最小二乘拟合 | 演示拟合结果 | 教学/验证拟合算法 |
32 轮廓发现与绘制
32.1 轮廓发现原理
轮廓发现是图像处理中的重要技术,用于检测和提取图像中物体的边界。在 OpenCV 中,轮廓可以理解为将连续的点(沿着边界)连接在一起的曲线,这些点具有相同的颜色或强度。
-
轮廓的基本概念
-
轮廓:一组连续的点,表示物体的边界
-
层次结构:轮廓之间的父子关系(嵌套关系)
-
近似方法:如何简化轮廓的表示
-
-
轮廓发现的工作原理
-
二值化处理:将图像转换为二值图像(黑白)
-
边缘检测: 使用算法如Canny检测边缘
-
轮廓查找: 连接边缘点形成轮廓
-
层次构建: 建立轮廓之间的嵌套关系
-
-
数学基础
轮廓发现基于拓扑学和计算机视觉理论,使用边界跟踪算法(如Suzuki85算法)来连接边缘点并构建轮廓层次结构。
32.2 核心函数详解
轮廓发现主要函数
public static void findContours(Mat image, List<MatOfPoint> contours, Mat hierarchy, int mode, int method, Point offset)
函数详解
-
image
:输入图像(8位单通道二值图像) -
contours
:输出的轮廓列表(每个轮廓是Point的集合) -
hierarchy
:可选的输出层次结构信息 -
mode
:轮廓检索模式 -
method
:轮廓近似方法 -
offset
:可选偏移量,用于移动所有轮廓
轮廓检索模式
-
RETR_EXTERNAL
:只检索最外层轮廓 -
RETR_LIST
:检索所有轮廓,不建立层次关系 -
RETR_CCOMP
:检索所有轮廓,并组织为两层层次结构 -
RETR_TREE
:检索所有轮廓,并建立完整的层次结构树
轮廓近视方法
-
CHAIN_APPROX_NONE
:存储所有轮廓点 -
CHAIN_APPROX_SIMPLE
:压缩水平、垂直和对角线段,只保留端点 -
CHAIN_APPROX_TC89_L1
:使用Teh-Chin链近似算法L1 -
CHAIN_APPROX_TC89_KCOS
:使用Teh-Chin链近似算法KCOS
轮廓绘制主要函数
public static void drawContours(Mat image, List<MatOfPoint> contours, int contourIdx, Scalar color, int thickness, int lineType, Mat hierarchy, int maxLevel, Point offset)
参数详解
-
image
:目标图像(绘制轮廓的位置) -
contours
:输入的轮廓列表 -
contourIdx
:要绘制的轮廓索引(负值表示绘制所有轮廓) -
color
:轮廓颜色 -
thickness
:轮廓线厚度(负值表示填充轮廓) -
lineType
:线型(如LINE_8、LINE_AA等) -
hierarchy
:可选的层次结构信息 -
maxLevel
:绘制轮廓的最大级别 -
offset
:可选偏移量
32.3 应用场景
-
物体检测与识别
-
从图像中提取物体形状
-
基于形状特征进行物体分类
-
-
文档分析
-
检测文档边界
-
表格结构提取
-
文字区域定位
-
-
工业检测
-
产品缺陷检测
-
尺寸测量
-
形状匹配
-
-
医学图像处理
-
细胞计数和分类
-
器官边界提取
-
病变区域检测
-
-
AR应用
-
实时物体跟踪
-
场景理解
-
虚拟物体放置
-
32.4 示例
ContourActivity.java
public class ContourActivity extends AppCompatActivity {private ActivityContourBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityContourBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mOriginalMat = Utils.loadResource(this, R.drawable.yingbi);OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);// 查找轮廓findContours();} catch (Exception e) {e.printStackTrace();}}private void findContours() {Mat grayMat = new Mat();Mat binaryMat = new Mat();Mat resultMat = mOriginalMat.clone();try {// 转换为灰度图Imgproc.cvtColor(mOriginalMat, grayMat, Imgproc.COLOR_BGR2GRAY);// 高斯模糊Imgproc.GaussianBlur(grayMat, grayMat, new Size(9, 9), 2, 2);// 二值化(使用 OTSU 自适应阈值)Imgproc.threshold(grayMat, binaryMat, 170, 255, Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);List<MatOfPoint> contours = new ArrayList<>();Mat hierarchy = new Mat();// 查找轮廓Imgproc.findContours(binaryMat, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_SIMPLE, new Point());// 输出轮廓StringBuilder hierarchyInfo = new StringBuilder();hierarchyInfo.append("轮廓层次信息: \n");for (int i = 0; i < hierarchy.cols(); i++) {double[] hierarchyData = hierarchy.get(0, i);hierarchyInfo.append(String.format("[%d, %d, %d, %d]\n",(int)hierarchyData[0], (int)hierarchyData[1],(int)hierarchyData[2], (int)hierarchyData[3]));}mBinding.tvHierarchy.setText(hierarchyInfo.toString());// 绘制轮廓for (int i = 0; i < contours.size(); i++) {Imgproc.drawContours(resultMat, contours, i, new Scalar(0, 0, 255), 2, 8);}OpenCVHelper.showMat(mBinding.ivFindContours, resultMat);} catch (Exception e) {e.printStackTrace();} finally {OpenCVHelper.safeRelease(grayMat);OpenCVHelper.safeRelease(binaryMat);OpenCVHelper.safeRelease(resultMat);}}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);}
}
33 轮廓面积与周长
33.1 什么是轮廓的面积与周长
轮廓的面积和周长是图像处理中非常重要的特征,它们可以用于物体识别、形状分析、尺寸测量等多种应用。
轮廓面积
数学原理:
轮廓面积表示轮廓所包围的区域大小。在离散图像中,OpenCV使用以下方法计算面积:
-
像素计数法:对于二值图像,面积就是轮廓内白色像素的数量
-
格林公式:对于连续轮廓,使用积分方法计算面积
计算公式:
对于多边形轮廓,可以使用鞋带公式(Shoelace formula):
Area = 1/2 * |Σ(x_i*y_{i+1} - x_{i+1}*y_i)|
轮廓周长
数学原理:
轮廓周长是轮廓边界上所有相邻点之间的欧几里得距离之和。
计算公式:
Perimeter = Σ sqrt((x_i - x_{i-1})² + (y_i - y_{i-1})²)
33.2 核心函数详解
轮廓面积函数
public static double contourArea(Mat contour)
public static double contourArea(Mat contour, boolean oriented)
参数说明:
-
contour
:输入轮廓,可以是MatOfPoint或MatOfPoint2f -
oriented
:是否有方向的面积(默认false)-
false:返回绝对值
-
true:返回有符号面积(顺时针为负,逆时针为正)
-
轮廓周长函数
public static double arcLength(MatOfPoint2f curve, boolean closed)
参数说明:
-
curve
:输入曲线,通常是MatOfPoint2f -
closed
:曲线是否闭合-
true:计算闭合轮廓的周长
-
false:计算开放曲线的长度
-
33.3 使用场景
-
物体筛选与过滤
-
通过面积大小过滤掉小噪声点或过大物体
-
根据周长筛选特定形状的物体
-
-
形状识别与分类
-
结合面积和周长计算形状特征(如圆形度)
-
识别不同大小的同类物体
-
-
形状识别与分类
-
结合面积和周长计算形状特征(如圆形度)
-
识别不同大小的同类物体
-
-
质量控制
-
检测产品尺寸是否符合标准
-
识别缺陷或异常物体
-
-
医学图像分析
-
计算细胞或组织的面积
-
测量生物特征尺寸
-
33.4 示例
我们原先轮廓发现与绘制的基础findContours
方法上添加
// 计算并输出轮廓面积for (int i = 0; i < contours.size(); i++) {double area = Imgproc.contourArea(contours.get(i));System.out.println("Outline area" + i + ": " + area);}// 计算并输出轮廓周长for (int i = 0; i < contours.size(); i++) {MatOfPoint2f matOfPoint2f = new MatOfPoint2f();matOfPoint2f.fromList(contours.get(i).toList());double length = Imgproc.arcLength(matOfPoint2f, true);System.out.println("Outline length" + i + ": " + length);}
34 轮廓外接多边形
34.1 什么是轮廓外接多边形
-
原理
轮廓外接多边形是图像处理中用于近似描述轮廓形状的重要技术。它通过使用更少的点来近似轮廓,同时保持轮廓的基本形状特征。OpenCV提供了多种外接多边形计算方法,每种方法适用于不同的应用场景。
-
基本概念
外接多边形是通过一组顶点来近似描述轮廓形状的多边形。与原始轮廓相比,外接多边形具有以下特点:
-
顶点数量更少:减少了数据量,提高了处理效率
-
形状近似:保持了轮廓的基本形状特征
-
计算高效:多边形操作比复杂轮廓操作更快速
-
-
主要的外接多边形类型
-
外接矩形:包括轴对齐矩形和旋转矩形
-
最小外接矩形:面积最小的旋转矩形
-
凸包:包含轮廓所有点的最小凸多边形
-
多边形近似:使用更少的点近似轮廓形状
-
34.2 核心函数详解
外接矩形函数
-
轴对齐外接矩形
public static Rect boundingRect(Mat array)
参数说明:
-
array
:输入轮廓(MatOfPoint或MatOfPoint2f) -
返回值:Rect对象,包含(x, y, width, height)
-
-
旋转外接矩形
public static RotatedRect minAreaRect(MatOfPoint2f points)
参数说明:
-
points
:输入轮廓(必须是MatOfPoint2f) -
返回值:RotatedRect对象,包含中心点、尺寸和旋转角度
-
凸包计算函数
public static void convexHull(MatOfPoint points, MatOfInt hull, boolean clockwise)
参数说明:
-
points
:输入轮廓点集 -
hull
:输出凸包点的索引 -
clockwise
:方向标志(true为顺时针)
多边形近视函数
public static void approxPolyDP(MatOfPoint2f curve, MatOfPoint2f approxCurve, double epsilon, boolean closed)
参数说明:
-
curve
:输入轮廓 -
approxCurve
:输出近似多边形 -
epsilon
:近似精度(原始轮廓与近似多边形之间的最大距离) -
closed
:是否闭合曲线
34.3 应用场景
-
物体检测与识别
-
使用外接矩形定位物体位置
-
通过多边形近似识别物体形状
-
-
工业检测
-
检测产品的尺寸和方向
-
判断产品是否符合规格要求
-
-
文档处理
-
检测文档边界并进行透视校正
-
识别表格和文字区域
-
-
机器人视觉
-
识别和定位环境中的物体
-
计算物体的方向和姿态
-
-
医学图像分析
-
测量器官或细胞的尺寸
-
分析生物组织的形状特征
-
34.4 示例
ContourPolygonActivity.java
public class ContourPolygonActivity extends AppCompatActivity {private ActivityContourPolygonBinding mBinding;static {System.loadLibrary("opencv_java4");}private Mat mOriginalMat, mGrayMat, mMaxMat, mMinMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);;mBinding = ActivityContourPolygonBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {mOriginalMat = Utils.loadResource(this, R.drawable.contour_polygon);mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);mMaxMat = mOriginalMat.clone();mMinMat = mOriginalMat.clone();calculateBoundingPolygons();} catch (Exception e) {e.printStackTrace();}}private void calculateBoundingPolygons() {try {// 去噪和二值化Mat cannyMat = new Mat();Imgproc.Canny(mGrayMat, cannyMat, 80, 160, 3, false);OpenCVHelper.showMat(mBinding.ivCanny, cannyMat);// 膨胀 将细小缝隙填补Mat kernel = Imgproc.getStructuringElement(0, new Size(3, 3));Imgproc.dilate(cannyMat, cannyMat, kernel);List<MatOfPoint> contours = new ArrayList<>(); // 轮廓Mat hierarchy = new Mat(); // 存放轮廓结构变量Imgproc.findContours(cannyMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, 2, new Point()); // 只提取最外层的轮廓。// 计算输出轮廓的外接矩形for (int i = 0; i < contours.size(); i++) {// 计算最大外接矩形Rect rect = Imgproc.boundingRect(contours.get(i));Imgproc.rectangle(mMaxMat, rect, new Scalar(0, 0, 255), 2, 8, 0);// 计算最小外接矩形MatOfPoint2f matOfPoint2f = new MatOfPoint2f();matOfPoint2f.fromList(contours.get(i).toList());RotatedRect rrect = Imgproc.minAreaRect(matOfPoint2f);Point[] points = new Point[4];rrect.points(points); // 读取最小外接矩形的 4 个顶点Point cpt = rrect.center; // 最小外接矩形的中心for (int j = 0; j < 4; j++) {Imgproc.line(mMinMat, points[j], points[(j + 1) % 4], new Scalar(0, 255, 0), 2, 8, 0);}// 绘制矩形中心Imgproc.circle(mOriginalMat, cpt, 2, new Scalar(255, 0, 0), 2, 8, 0);Imgproc.circle(mMaxMat, cpt, 2, new Scalar(255, 0, 0), 2, 8, 0);Imgproc.circle(mMinMat, cpt, 2, new Scalar(255, 0, 0), 2, 8, 0);}OpenCVHelper.showMat(mBinding.ivMax, mMaxMat);OpenCVHelper.showMat(mBinding.ivMin, mMinMat);} catch (Exception e) {e.printStackTrace();}}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);if (mGrayMat != null) OpenCVHelper.safeRelease(mGrayMat);if (mMaxMat != null) OpenCVHelper.safeRelease(mMaxMat);if (mMinMat != null) OpenCVHelper.safeRelease(mMinMat);}
}
35 判断轮廓的几何形状
35.1 什么是轮廓形状判断
轮廓形状判断是计算机视觉中的基本任务,其核心思想是通过分析轮廓的特征来识别其几何形状。OpenCV 提供了多种方法来实现这一功能。
轮廓形状判断基于以下核心概念:
-
轮廓发现:首先需要从图像中提取出所有连续的边缘点集(轮廓)
-
特征提取:对每个轮廓计算各种几何特征
-
形状分类:基于提取的特征判断轮廓属于哪种几何形状
关键集合特征:
-
轮廓面积:轮廓包围的区域大小
-
轮廓周长:轮廓的周长长度
-
凸包:包含轮廓的最小凸形
-
轮廓近似:用更少的点近似轮廓,保留基本形状
-
最小外接矩形:能够包含轮廓的最小矩形
-
最小外接圆:能够包含轮廓的最小圆形
-
Hu矩:对轮廓形状的数学描述,具有平移、旋转和缩放不变性
35.2 核心函数详解
轮廓发现函数
// 查找轮廓
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(binaryImage, // 输入二值图像contours, // 输出的轮廓列表hierarchy, // 轮廓的层次结构Imgproc.RETR_EXTERNAL, // 检索模式:只检索最外层轮廓Imgproc.CHAIN_APPROX_SIMPLE // 轮廓近似方法
);
参数说明:
-
RETR_EXTERNAL
:只检测最外层轮廓 -
RETR_LIST
:检测所有轮廓,不建立层次关系 -
RETR_CCOMP
:检测所有轮廓,建立两层层次结构 -
RETR_TREE
:检测所有轮廓,建立完整的层次结构 -
CHAIN_APPROX_NONE
:存储所有轮廓点 -
CHAIN_APPROX_SIMPLE
:压缩水平、垂直和对角线段,只保留端点
35.3 应用场景
轮廓形状判断在Android应用中有着广泛用途:
-
文档扫描与识别:检测和矫正文档边缘
-
工业检测:检测零件的形状是否符合标准
-
物体识别:识别不同形状的物体并进行分类
-
增强现实:检测现实世界中的特定形状标记
-
教育应用:几何学习工具,识别用户绘制的形状
-
游戏开发:基于形状的交互和控制
-
机器人视觉:导航和物体抓取中的形状识别
35.4 示例
ApproxPolyActivity.java
public class ApproxPolyActivity extends AppCompatActivity {private ActivityApproxPolyBinding mBinding;static {System.loadLibrary("opencv_java4"); // 加载 OpenCV 库}// 定义用于处理的 Mat 对象:原图、灰度图、模糊图、Canny 边缘图、膨胀图private Mat mOriginalMat, mGrayMat, mBlurMat, mCannyMat, mDilMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityApproxPolyBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 从资源文件加载原图mOriginalMat = Utils.loadResource(this, R.drawable.geometry);// 显示原始图像OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);// 灰度处理mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);// 高斯模糊处理mBlurMat = new Mat();Imgproc.GaussianBlur(mGrayMat, mBlurMat, new Size(3, 3), 3, 0);// Canny 边缘检测算法mCannyMat = new Mat();Imgproc.Canny(mBlurMat, mCannyMat, 25, 75);// 膨胀处理Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(3, 3));mDilMat = new Mat();Imgproc.dilate(mCannyMat, mDilMat, kernel);// 获取轮廓边界、绘制边界包围盒、形状描述getContours(mDilMat);} catch (Exception e) {e.printStackTrace();}}private void getContours(Mat mDilMat) {if (mDilMat.empty() || mOriginalMat.empty()) return;List<MatOfPoint> contours = new ArrayList<>(); // 存放轮廓Mat hierarchy = new Mat(); // 轮廓层级信息// 从膨胀化的二值图像中检查轮廓Imgproc.findContours(mDilMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);// 在原图绘制出所有轮廓(紫色)Imgproc.drawContours(mOriginalMat, contours, -1, new Scalar(255, 0, 255), 2);// 逼近的多边形曲线集合List<MatOfPoint> conPoly = new ArrayList<>(contours.size());// 轮廓的边界矩形集合List<Rect> boundRect = new ArrayList<>(contours.size());// 遍历每一个轮廓for (int i = 0; i < contours.size(); i++) {double area = Imgproc.contourArea(contours.get(i)); // 计算轮廓面积Log.d("NPC", "轮廓面积: " + area);if (area > 1000) { // 过滤面积过小的噪声// 将轮廓点转为浮点型,用于计算周长MatOfPoint2f curve = new MatOfPoint2f(contours.get(i).toArray());// 计算轮廓周长double peri = Imgproc.arcLength(curve, true);Log.d("NPC", "轮廓周长: " + peri);// 多边形逼近MatOfPoint2f approxCurve = new MatOfPoint2f();Imgproc.approxPolyDP(curve, approxCurve, 0.02 * peri, true);// 转换为整数点集合MatOfPoint points = new MatOfPoint(approxCurve.toArray());conPoly.add(points);// 计算边界矩形Rect rect = Imgproc.boundingRect(points);boundRect.add(rect);// 顶点数int objCor = points.toArray().length;String objectType = ""; // 识别的形状类型// 计算圆度:用于区分圆/椭圆和多边形double circularity = 0;if (peri > 0) circularity = 4 * Math.PI * area / (peri * peri);// 根据顶点数判断图形类别if (objCor == 3) {objectType = "Triangle: " + objCor; // 三角形} else if (objCor == 4) { // 四边形float aspRatio = (float) rect.width / (float) rect.height; // 宽高比if (aspRatio > 0.95 && aspRatio < 1.05) {objectType = "Square: " + objCor; // 正方形} else {// 进一步判断梯形(至少一对平行边)Point[] pts = points.toArray();if (isTrapezoid(pts)) {objectType = "Trapezoid: " + objCor; // 梯形} else {objectType = "Rectangle: " + objCor; // 矩形}}} else if (objCor == 5) {objectType = "Pentagon: " + objCor; // 五边形} else if (objCor == 6) {objectType = "Hexagon: " + objCor; // 六边形} else if (objCor == 10) {// 五角星常常逼近为 10 个点(凹凸交错)objectType = "Pentagram: " + objCor; // 五角星} else {// 顶点数大于 6,进一步通过圆度判断是否为圆或椭圆if (circularity > 0.75) {float asp = (float) rect.width / (float) rect.height; // 宽高比if (asp > 0.9 && asp < 1.1) {objectType = "Rotundity: " + objCor; // 圆形} else {objectType = "Oval: " + objCor; // 椭圆}} else {objectType = "Polygon(" + objCor + ")"; // 其他多边形}}// 绘制当前轮廓(紫色)Imgproc.drawContours(mOriginalMat, conPoly, conPoly.size() - 1, new Scalar(255, 0, 255), 2);// 绘制边界矩形(绿色)Imgproc.rectangle(mOriginalMat, rect.tl(), rect.br(), new Scalar(0, 255, 0), 5);// 在边界矩形上方绘制识别结果文字Log.e("NPC", "objectType: " + objectType);Imgproc.putText(mOriginalMat, objectType, new Point(rect.x, rect.y - 5),Imgproc.FONT_HERSHEY_PLAIN, 2, new Scalar(0, 69, 255), 2);// 显示结果图OpenCVHelper.showMat(mBinding.ivApproxPolyDp, mOriginalMat);}}}/*** 判断四点是否为梯形(启发式):* 思路:计算四条边的斜率,若存在一对相近的平行边而另一对不平行,则视为梯形*/private boolean isTrapezoid(Point[] pts) {if (pts == null || pts.length != 4) return false;double[] slopes = new double[4]; // 保存四条边的斜率for (int i = 0; i < 4; i++) {Point p1 = pts[i];Point p2 = pts[(i + 1) % 4];double dx = p2.x - p1.x;double dy = p2.y - p1.y;if (Math.abs(dx) < 1e-6) slopes[i] = Double.POSITIVE_INFINITY; // 垂直线斜率无穷大else slopes[i] = dy / dx;}// 判断是否存在一对平行边int parallelPairs = 0;double tol = 0.2; // 斜率比较的容差if (isParallel(slopes[0], slopes[2], tol)) parallelPairs++;if (isParallel(slopes[1], slopes[3], tol)) parallelPairs++;// 梯形:恰有一对平行边return (parallelPairs >= 1 && parallelPairs < 2);}// 判断两条边是否平行(斜率差是否在容差范围内)private boolean isParallel(double s1, double s2, double tol) {if (Double.isInfinite(s1) && Double.isInfinite(s2)) return true;if (Double.isInfinite(s1) || Double.isInfinite(s2))return Math.abs(1.0 / (s1 + 1e-9) - 1.0 / (s2 + 1e-9)) < tol;return Math.abs(s1 - s2) < tol;}@Overrideprotected void onDestroy() {super.onDestroy();// 释放 Mat 内存,防止内存泄漏if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat);if (mGrayMat != null) OpenCVHelper.safeRelease(mGrayMat);if (mBlurMat != null) OpenCVHelper.safeRelease(mBlurMat);if (mCannyMat != null) OpenCVHelper.safeRelease(mCannyMat);if (mDilMat != null) OpenCVHelper.safeRelease(mDilMat);}
}
36 凸包检测和凸缺陷
36.1 什么是凸包检测
凸包(Convex Hull)是计算几何中的一个重要概念,指的是包含给定点集的最小凸多边形。所谓凸多边形,是指多边形内任意两点的连线都完全位于多边形内部。
数学定义:对于平面上的点集S,凸包是包含S中所有点的最小凸集。
几何意义:
-
凸性:凸包上的任意两点连线都在凸包内部或边界上
-
最小性:凸包是包含所有点的最小凸多边形
-
唯一性:给定点集的凸包是唯一的
OpenCV中主要使用Graham扫描算法和Andrew单调链算法来计算凸包:
-
Graham扫描算法
-
找到点集中y坐标最小的点(如有多个,取x最小的)作为基准点
-
将其余点按与基准点的极角排序
-
使用栈结构依次处理排序后的点,排除会导致凹性的点
-
最终栈中 points 即为凸包顶点
-
-
Andrew算法
-
将所有点按x坐标(x相同时按y)排序
-
构建下凸包:从左到右处理点,排除会导致右转的点
-
构建上凸包:从右到左处理点,排除会导致右转的点
-
合并上下凸包得到完整凸包
-
凸包与轮廓的关系:
-
凸包是轮廓的凸性近似
-
凸包点集是轮廓点集的子集
-
凸包保持了原始形状的整体凸性特征但忽略了凹性细节
36.2 什么是凸缺陷
凸缺陷(Convexity Defects) 是指轮廓与其凸包之间的差异区域。具体来说,它是轮廓中凹陷部分的几何描述,用于量化轮廓偏离凸性的程度
数学定义:对于轮廓上的任意一点,凸缺陷可以通过以下方式定义:
-
轮廓点与凸包边界之间的最大距离
-
该距离对应的最远点(缺陷点)
-
缺陷的起始和结束点(凸包上的点)
凸缺陷的四个关键点:
每个凸缺陷由4个整数值描述:
-
起始点索引(start_index):缺陷开始的凸包点索引
-
结束点索引(end_index):缺陷结束的凸包点索引
-
最远点索引(far_index):轮廓上离凸包最远的点索引
-
最远距离(depth):最远点到凸包的距离(近似值,实际是固定点数的倍数)
几何意义:
凸缺陷提供了关于轮廓形状的详细信息:
-
凹陷深度:表示轮廓凹陷的程度
-
凹陷位置:标识轮廓中非凸区域的位置
-
形状特征:用于区分不同形状的物体
36.3 核心函数详解
凸包检测主要函数
// 凸包检测核心函数
Imgproc.convexHull(MatOfPoint points, MatOfInt hull, boolean clockwise)
Imgproc.convexHull(MatOfPoint points, MatOfInt hull)
参数说明:
-
points
:输入的点集,通常是轮廓的点集 -
hull
:输出的凸包点索引(在原始点集中的索引位置) -
clockwise
:凸包方向,true为顺时针,false为逆时针
凸缺陷检测函数
// 凸缺陷检测核心函数
Imgproc.convexityDefects(MatOfPoint contour, MatOfInt convexhull, MatOfInt4 convexityDefects)
参数说明:
-
contour
:输入轮廓,MatOfPoint类型 -
convexhull
:凸包点索引(必须是索引,不是点集),MatOfInt类型 -
convexityDefects
:输出的凸缺陷信息,MatOfInt4类型
36.4 使用场景
凸包检测使用场景:
-
物体形状分析
-
凸性判断:检测物体是否是凸形状
-
形状简化:用凸包近似复杂形状,减少计算量
-
轮廓特征提取:分析物体的凸性特征
-
-
手势识别
-
手指计数:通过凸性缺陷分析手指数量
-
手势分类:识别不同手部姿态
-
-
工业检测
-
零件完整性检测:检测零件是否有凹陷或缺陷
-
物体方向确定:通过凸包确定物体的主方向
-
-
图像处理
-
图像裁剪:根据凸包进行智能裁剪
-
区域提取:提取凸包区域进行后续处理
-
-
增强现实
-
跟踪标记检测:检测和跟踪凸形状的AR标记
-
虚拟物体放置:根据凸包确定虚拟物体的放置位置
-
凸缺陷使用场景:
-
手势识别
-
手指计数:通过凸缺陷识别手指之间的凹陷
-
手势分类:区分不同的手部姿态和手势
-
手语识别:识别特定的手语动作
-
-
工业检测
-
零件缺陷检测:检测零件表面的凹陷或缺陷
-
质量控制:检查产品是否符合形状规格
-
表面检测:分析物体表面的不平整度
-
-
生物特征分析
-
叶片形状分析:研究植物叶片的形态特征
-
细胞形态分析:分析细胞的形状异常
-
医学图像分析:检测器官或组织的形态变化
-
-
物体识别
-
形状分类:区分凸形物体和凹形物体
-
特征提取:提取物体的形状特征用于识别
-
轮廓分析:深入分析复杂轮廓的结构特征
-
-
增强现实
-
手势交互:基于手势的AR交互控制
-
物体跟踪:跟踪具有特定形状特征的物体
-
36.4 示例
ConvexHullActivit.java
public class ConvexHullActivity extends AppCompatActivity {private ActivityConvexHullBinding mBinding;static {System.loadLibrary("opencv_java4"); // 加载 OpenCV 库}private Mat mOriginalMat;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mBinding = ActivityConvexHullBinding.inflate(getLayoutInflater());setContentView(mBinding.getRoot());try {// 1. 加载原始图片(hand.png)mOriginalMat = Utils.loadResource(this, R.drawable.hand);OpenCVHelper.showMat(mBinding.ivOriginal, mOriginalMat);// 2. 转灰度图Mat mGrayMat = new Mat();Imgproc.cvtColor(mOriginalMat, mGrayMat, Imgproc.COLOR_BGR2GRAY);// 3. 二值化(阈值分割)Mat threMat = new Mat();Imgproc.threshold(mGrayMat, threMat, 128, 255, Imgproc.THRESH_BINARY);// 4. 查找轮廓List<MatOfPoint> contours = new ArrayList<>(); // 保存所有轮廓点集Mat hierarchy = new Mat(); // 轮廓层级信息(父子关系)Imgproc.findContours(threMat,contours,hierarchy,Imgproc.RETR_TREE, // 检索模式:树形结构(包含层级)Imgproc.CHAIN_APPROX_SIMPLE, // 压缩水平、垂直冗余点new Point(0, 0));// 绘制轮廓图像Mat contoursImg = Mat.zeros(mGrayMat.size(), CvType.CV_8UC1);Imgproc.drawContours(contoursImg, contours, -1, new Scalar(255), 1);// 5. 凸包检测 & 缺陷分析List<MatOfPoint> pointHulls = new ArrayList<>(); // 用于保存凸包点坐标List<MatOfInt> intHulls = new ArrayList<>(); // 用于保存凸包点索引List<MatOfInt4> hullDefects = new ArrayList<>(); // 用于保存凸包缺陷(start, end, far, depth)for (MatOfPoint contour : contours) {// (a) 计算凸包索引MatOfInt hull = new MatOfInt();Imgproc.convexHull(contour, hull, false);intHulls.add(hull);// (b) 将凸包索引转换成点集MatOfPoint hullPoints = new MatOfPoint();Point[] contourArray = contour.toArray(); // 原始轮廓点int[] indices = hull.toArray(); // 凸包索引List<Point> pts = new ArrayList<>();for (int idx : indices) {pts.add(contourArray[idx]);}hullPoints.fromList(pts);pointHulls.add(hullPoints);// (c) 计算凸缺陷(非凸区域的凹陷点)MatOfInt4 defects = new MatOfInt4();Imgproc.convexityDefects(contour, hull, defects);hullDefects.add(defects);}// 6. 绘制凸包和缺陷Mat convexHullImg = new Mat();Imgproc.cvtColor(contoursImg, convexHullImg, Imgproc.COLOR_GRAY2BGR);for (int i = 0; i < contours.size(); i++) {Scalar color = new Scalar(0, 0, 255); // 红色// 绘制凸包Imgproc.drawContours(convexHullImg, pointHulls, i, color, 1, 8, new Mat(), 0, new Point());// 忽略太小的轮廓(噪声)if (contours.get(i).size().height < 300) continue;// 获取缺陷数组 [startIdx, endIdx, farIdx, depth]int[] defectArr = hullDefects.get(i).toArray();Point[] contourPts = contours.get(i).toArray();for (int j = 0; j < defectArr.length; j += 4) {int startIdx = defectArr[j]; // 缺陷起点int endIdx = defectArr[j + 1]; // 缺陷终点int farIdx = defectArr[j + 2]; // 缺陷最远点(凹陷)int depth = defectArr[j + 3] / 256; // 缺陷深度(需要缩放)// 过滤过浅或过深的缺陷if (depth > 10 && depth < 300) {Point ptStart = contourPts[startIdx];Point ptEnd = contourPts[endIdx];Point ptFar = contourPts[farIdx];// 绘制缺陷连接线Imgproc.line(convexHullImg, ptStart, ptFar, new Scalar(0, 255, 0), 2);Imgproc.line(convexHullImg, ptEnd, ptFar, new Scalar(0, 255, 0), 2);// 绘制关键点(起点、终点、最远点)Imgproc.circle(convexHullImg, ptStart, 4, new Scalar(255, 0, 0), 2); // 蓝色Imgproc.circle(convexHullImg, ptEnd, 4, new Scalar(255, 0, 128), 2); // 紫色Imgproc.circle(convexHullImg, ptFar, 4, new Scalar(128, 0, 255), 2); // 粉色}}}// 7. 显示结果OpenCVHelper.showMat(mBinding.ivContours, contoursImg); // 显示轮廓OpenCVHelper.showMat(mBinding.ivConvexityDefects, convexHullImg); // 显示凸包 & 缺陷} catch (Exception e) {e.printStackTrace();}}@Overrideprotected void onDestroy() {super.onDestroy();if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat); // 释放内存}
}