学习 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, // 距离分辨率 ρ 的精度(以像素为单位)。通常设为1 double 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;
@Override
protected 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]);
// 用红色绘制线段, 线宽为 3
Imgproc.line(result, pt1, pt2, new Scalar(255, 0, 0), 3);
}
}
// 将结果显示在 ImageView 上
showMat(mBinding.ivHoughLinesP, result);
// 释放 Mat 对象, 防止内存泄漏哦
safeRelease(blurred);
safeRelease(edges);
safeRelease(lines);
safeRelease(result);
}
@Override
protected 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_GRADIENT
double 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;
@Override
protected 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() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mParam1 = progress;
updateParamDisplay();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
mBinding.seekBarParam2.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mParam2 = progress;
updateParamDisplay();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public 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);
}
@Override
protected 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;
@Override
protected 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();
}
@Override
protected 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链近似算法L1CHAIN_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;
@Override
protected 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);
}
}
@Override
protected 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或MatOfPoint2foriented
:是否有方向的面积(默认false)false:返回绝对值
true:返回有符号面积(顺时针为负,逆时针为正)
轮廓周长函数
public static double arcLength(MatOfPoint2f curve, boolean closed)
参数说明:
curve
:输入曲线,通常是MatOfPoint2fclosed
:曲线是否闭合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;
@Override
protected 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();
}
}
@Override
protected 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;
@Override
protected 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;
}
@Override
protected 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;
@Override
protected 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();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mOriginalMat != null) OpenCVHelper.safeRelease(mOriginalMat); // 释放内存
}
}