学习 Android (二十一) 学习 OpenCV (六)

发布于:2025-09-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

学习 Android (二十一) 学习 OpenCV (六)

在上一章节,我们对图像形态学操作有了一定的学习了解,接下来让我们继续学习 OpenCV 相关的内容吧

29 霍夫直线检测

29.1 什么是霍夫直线

霍夫直线变换是一种用于在图像中检测直线的经典算法。其核心思想是将图像空间中的直线转换到参数空间进行检测,利用投票机制来判断哪些直线是存在的。

  • 图像空间 (x, y): 我们看到的普通图像。在这里,一条直线可以用方程 y = kx + b 表示。但当直线是垂直时,斜率 k 为无穷大,这种表示法有缺陷。

  • 霍夫参数空间 (ρ, θ): 为了解决上述问题,我们使用法线表示法。一条直线可以用两个参数唯一确定:

    • ρ (rho): 原点到该直线的垂直距离。

    • θ (theta): 该垂线与x轴正方向的夹角(弧度制)。

      在这里插入图片描述

      (图像空间的一条直线 L 对应霍夫空间的一个点 (ρ₀, θ₀)

      直线的方程变为:
      ρ = x * cosθ + y * sinθ

29.1.1 点的转换与投票机制

这是理解霍夫变换最关键的一步

  1. 图像空间的一个点对应霍夫空间的一条正弦曲线

    • 图像空间中的一个点 (x₀, y₀) 可以穿过无数条直线。这些直线在霍夫空间中满足方程 ρ = x₀ * cosθ + y₀ * sinθ,这是一条正弦曲线。
  2. 图像空间的一条直线对应霍夫空间的一个点

    • 图像空间中,同一条直线 L 上的所有点 (x₁, y₁), (x₂, y₂), ...,每个点都对应一条霍夫空间的正弦曲线。

    • 所有这些曲线会相交于同一个点 (ρ’, θ’)

    • 这个交点 (ρ’, θ’) 就是直线 L 的参数。

  3. 投票机制

    • 算法创建一个二维数组(称为累加器),用来代表离散化的 (ρ, θ) 空间。

    • 对于边缘图像中的每一个边缘点(例如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应用中用途广泛:

  1. 文档扫描与透视校正: 检测文档的边缘直线,然后通过透视变换将其“拉直”。

  2. 道路车道线检测: 在自动驾驶或ADAS系统中,用于识别车辆行驶的车道。

  3. 建筑和工业检测: 检测物体的边缘是否平直,用于质量控制和测量。

  4. 增强现实 (AR): 检测现实世界中的平面(如桌面、墙壁)来放置虚拟物体。

  5. 艺术创作与图像处理: 从图像中提取线条元素用于创作。

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 使用的是优化的霍夫梯度法,主要步骤:

  1. 边缘检测:使用Canny算子或Sobel算子检测图像边缘

  2. 计算梯度:对边缘点计算梯度方向(使用Sobel算子)

  3. 沿梯度方向投票:对于每个边缘点,沿梯度方向在参数空间中投票给可能的圆心

  4. 累加器峰值检测:找到累加器中的峰值,这些就是候选圆心

  5. 确定半径:对于每个候选圆心,计算边缘点到圆心的距离,统计这些距离确定最可能的半径

这种方法大大减少了计算量,因为它:

  • 将三维问题降为二维(先找圆心,再确定半径)

  • 利用梯度方向信息缩小搜索空间

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应用中有着广泛用途:

  1. 工业检测:检测零件中的孔洞、轴承、瓶盖等圆形物体

  2. 生物医学:细胞计数、瞳孔检测、显微镜图像分析

  3. 交通监控:检测车辆轮胎、交通标志中的圆形部分

  4. 日常生活:硬币识别、瓶盖检测、眼球追踪

  5. 增强现实:识别现实世界中的圆形标记或物体

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 使用的是 梯度信息 + 累加器投票

  1. Canny 得到边缘点。

  2. 每个边缘点根据梯度方向去“投票”,看它可能属于哪些圆。

  3. 最终在累加器中找“峰值”,这些峰值就被当作圆心 + 半径。

为什么降低 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 应用场景

直线拟合在计算机视觉中有广泛的应用:

  1. 车道线检测

    • 从道路图像中检测和拟合车道线

    • 用于自动驾驶和辅助驾驶系统

  2. 文档校正

    • 检测文档边缘并校正透视变形

    • 用于扫描和OCR应用

  3. 工业检测

    • 检测产品的直线边缘

    • 测量物体的方向和位置

  4. 机器人导航

    • 从传感器数据中提取环境的结构信息

    • 用于路径规划和导航

  5. 建筑测量

    • 从图像中提取建筑物的直线特征

    • 用于建筑测量和建模

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链近似算法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 应用场景

  1. 物体检测与识别

    • 从图像中提取物体形状

    • 基于形状特征进行物体分类

  2. 文档分析

    • 检测文档边界

    • 表格结构提取

    • 文字区域定位

  3. 工业检测

    • 产品缺陷检测

    • 尺寸测量

    • 形状匹配

  4. 医学图像处理

    • 细胞计数和分类

    • 器官边界提取

    • 病变区域检测

  5. 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或MatOfPoint2f

  • oriented:是否有方向的面积(默认false)

    • false:返回绝对值

    • true:返回有符号面积(顺时针为负,逆时针为正)

轮廓周长函数

public static double arcLength(MatOfPoint2f curve, boolean closed)

参数说明

  • curve:输入曲线,通常是MatOfPoint2f

  • closed:曲线是否闭合

    • true:计算闭合轮廓的周长

    • false:计算开放曲线的长度

33.3 使用场景

  1. 物体筛选与过滤

    • 通过面积大小过滤掉小噪声点或过大物体

    • 根据周长筛选特定形状的物体

  2. 形状识别与分类

    • 结合面积和周长计算形状特征(如圆形度)

    • 识别不同大小的同类物体

  3. 形状识别与分类

    • 结合面积和周长计算形状特征(如圆形度)

    • 识别不同大小的同类物体

  4. 质量控制

    • 检测产品尺寸是否符合标准

    • 识别缺陷或异常物体

  5. 医学图像分析

    • 计算细胞或组织的面积

    • 测量生物特征尺寸

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提供了多种外接多边形计算方法,每种方法适用于不同的应用场景。

  • 基本概念

    外接多边形是通过一组顶点来近似描述轮廓形状的多边形。与原始轮廓相比,外接多边形具有以下特点:

    • 顶点数量更少:减少了数据量,提高了处理效率

    • 形状近似:保持了轮廓的基本形状特征

    • 计算高效:多边形操作比复杂轮廓操作更快速

  • 主要的外接多边形类型

    1. 外接矩形:包括轴对齐矩形和旋转矩形

    2. 最小外接矩形:面积最小的旋转矩形

    3. 凸包:包含轮廓所有点的最小凸多边形

    4. 多边形近似:使用更少的点近似轮廓形状

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 应用场景

  1. 物体检测与识别

    • 使用外接矩形定位物体位置

    • 通过多边形近似识别物体形状

  2. 工业检测

    • 检测产品的尺寸和方向

    • 判断产品是否符合规格要求

  3. 文档处理

    • 检测文档边界并进行透视校正

    • 识别表格和文字区域

  4. 机器人视觉

    • 识别和定位环境中的物体

    • 计算物体的方向和姿态

  5. 医学图像分析

    • 测量器官或细胞的尺寸

    • 分析生物组织的形状特征

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 提供了多种方法来实现这一功能。

轮廓形状判断基于以下核心概念:

  1. 轮廓发现:首先需要从图像中提取出所有连续的边缘点集(轮廓)

  2. 特征提取:对每个轮廓计算各种几何特征

  3. 形状分类:基于提取的特征判断轮廓属于哪种几何形状

关键集合特征:

  • 轮廓面积:轮廓包围的区域大小

  • 轮廓周长:轮廓的周长长度

  • 凸包:包含轮廓的最小凸形

  • 轮廓近似:用更少的点近似轮廓,保留基本形状

  • 最小外接矩形:能够包含轮廓的最小矩形

  • 最小外接圆:能够包含轮廓的最小圆形

  • 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应用中有着广泛用途:

  1. 文档扫描与识别:检测和矫正文档边缘

  2. 工业检测:检测零件的形状是否符合标准

  3. 物体识别:识别不同形状的物体并进行分类

  4. 增强现实:检测现实世界中的特定形状标记

  5. 教育应用:几何学习工具,识别用户绘制的形状

  6. 游戏开发:基于形状的交互和控制

  7. 机器人视觉:导航和物体抓取中的形状识别

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扫描算法

    1. 找到点集中y坐标最小的点(如有多个,取x最小的)作为基准点

    2. 将其余点按与基准点的极角排序

    3. 使用栈结构依次处理排序后的点,排除会导致凹性的点

    4. 最终栈中 points 即为凸包顶点

  • Andrew算法

    1. 将所有点按x坐标(x相同时按y)排序

    2. 构建下凸包:从左到右处理点,排除会导致右转的点

    3. 构建上凸包:从右到左处理点,排除会导致右转的点

    4. 合并上下凸包得到完整凸包

凸包与轮廓的关系

  • 凸包是轮廓的凸性近似

  • 凸包点集是轮廓点集的子集

  • 凸包保持了原始形状的整体凸性特征但忽略了凹性细节

36.2 什么是凸缺陷

凸缺陷(Convexity Defects) 是指轮廓与其凸包之间的差异区域。具体来说,它是轮廓中凹陷部分的几何描述,用于量化轮廓偏离凸性的程度

数学定义:对于轮廓上的任意一点,凸缺陷可以通过以下方式定义:

  • 轮廓点与凸包边界之间的最大距离

  • 该距离对应的最远点(缺陷点)

  • 缺陷的起始和结束点(凸包上的点)

凸缺陷的四个关键点:

每个凸缺陷由4个整数值描述:

  1. 起始点索引(start_index):缺陷开始的凸包点索引

  2. 结束点索引(end_index):缺陷结束的凸包点索引

  3. 最远点索引(far_index):轮廓上离凸包最远的点索引

  4. 最远距离(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 使用场景

凸包检测使用场景:

  1. 物体形状分析

    • 凸性判断:检测物体是否是凸形状

    • 形状简化:用凸包近似复杂形状,减少计算量

    • 轮廓特征提取:分析物体的凸性特征

  2. 手势识别

    • 手指计数:通过凸性缺陷分析手指数量

    • 手势分类:识别不同手部姿态

  3. 工业检测

    • 零件完整性检测:检测零件是否有凹陷或缺陷

    • 物体方向确定:通过凸包确定物体的主方向

  4. 图像处理

    • 图像裁剪:根据凸包进行智能裁剪

    • 区域提取:提取凸包区域进行后续处理

  5. 增强现实

    • 跟踪标记检测:检测和跟踪凸形状的AR标记

    • 虚拟物体放置:根据凸包确定虚拟物体的放置位置

凸缺陷使用场景:

  1. 手势识别

    • 手指计数:通过凸缺陷识别手指之间的凹陷

    • 手势分类:区分不同的手部姿态和手势

    • 手语识别:识别特定的手语动作

  2. 工业检测

    • 零件缺陷检测:检测零件表面的凹陷或缺陷

    • 质量控制:检查产品是否符合形状规格

    • 表面检测:分析物体表面的不平整度

  3. 生物特征分析

    • 叶片形状分析:研究植物叶片的形态特征

    • 细胞形态分析:分析细胞的形状异常

    • 医学图像分析:检测器官或组织的形态变化

  4. 物体识别

    • 形状分类:区分凸形物体和凹形物体

    • 特征提取:提取物体的形状特征用于识别

    • 轮廓分析:深入分析复杂轮廓的结构特征

  5. 增强现实

    • 手势交互:基于手势的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); // 释放内存
    }
}

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到