Android OpenCV 实战:图片轮廓提取与重叠轮廓合并处理

在Android开发中,基于OpenCV实现图像轮廓的提取、处理是计算机视觉的常见需求,本文将分享一套完整的OpenCV轮廓处理方案,实现Bitmap与Mat互转图像轮廓提取轮廓按中心缩放重叠轮廓检测重叠轮廓合并的全流程开发,最终将处理后的轮廓绘制回图像并展示。

一、技术基础与核心思路

1. 核心依赖

基于OpenCV for Android 实现,核心操作围绕OpenCV的Imgproc(图像处理)、Core(核心运算)模块,以及Mat(图像矩阵)、MatOfPoint(轮廓点集)等核心数据结构展开。

2. 整体处理流程

  1. 将Android原生Bitmap转换为OpenCV可处理的Mat矩阵;
  2. 对图像进行灰度化、高斯模糊、二值化预处理,提取外部轮廓;
  3. 以轮廓中心为基准,对提取的轮廓进行等比例缩放;
  4. 检测缩放后轮廓的边界框重叠关系,通过并查集对重叠轮廓分组;
  5. 对每组重叠轮廓进行掩码并集运算,合并为单个轮廓;
  6. 将处理后的轮廓绘制回原始图像,再转回Bitmap展示;
  7. 及时释放OpenCV的Mat资源,避免内存泄漏。

二、环境准备

  1. 集成OpenCV for Android到项目,在build.gradle中添加依赖并配置OpenCV库;
  2. 初始化OpenCV,确保在使用OpenCV API前完成库的加载(可通过OpenCVLoader.initDebug()调试,正式环境使用静态初始化);
  3. 申请图像读取/绘制的相关权限(如存储、相机,根据业务需求)。

三、核心代码实现与解析

工具类前置

本文涉及两个自定义工具类,职责分工明确:

  • MyOpenCvUtils:封装轮廓缩放、重叠检测、并查集分组等核心逻辑;
  • ShapeOpUtils:封装轮廓与掩码互转、图像并集/交集运算、多轮廓合并等底层图形运算。

第一步:Bitmap转Mat并初始化轮廓处理

将AndroidBitmap转换为OpenCV的Mat(默认使用CV_8UC4四通道格式,兼容RGBA),作为轮廓处理的原始图像数据,同时触发轮廓提取流程。

java 复制代码
private Mat originalMat;

/**
 * Bitmap转Mat,触发轮廓提取
 * @param bitmap 待处理的原生Bitmap
 */
private void processImage(Bitmap bitmap) {
    // 初始化Mat,与Bitmap宽高、通道匹配
    originalMat = new Mat(bitmap.getHeight(), bitmap.getWidth(), CvType.CV_8UC4);
    // OpenCV工具类实现Bitmap→Mat的转换
    Utils.bitmapToMat(bitmap, originalMat);
    // 核心:提取并处理轮廓
    extractContours();
}

第二步:图像预处理与原始轮廓提取

对原始Mat进行灰度化→高斯模糊→二值化 三步预处理,消除图像噪声并将图像转为黑白二值图,再通过findContours提取外部轮廓,并过滤出最外层轮廓(parent=-1)。

java 复制代码
private void extractContours() {
    // 1. 灰度化:RGBA四通道转单通道灰度图
    Mat gray = new Mat();
    Imgproc.cvtColor(originalMat, gray, Imgproc.COLOR_RGBA2GRAY);

    // 2. 高斯模糊:5x5卷积核去噪,避免噪点被识别为轮廓
    Mat blurred = new Mat();
    Imgproc.GaussianBlur(gray, blurred, new Size(5, 5), 2.0, 2.0);

    // 3. 二值化:反相二值化,阈值240,超过240设为0,低于设为255(突出目标轮廓)
    Mat binary = new Mat();
    Imgproc.threshold(blurred, binary, 240, 255, Imgproc.THRESH_BINARY_INV);

    // 4. 提取外部轮廓:RETR_EXTERNAL仅提取最外层,CHAIN_APPROX_SIMPLE压缩轮廓点
    List<MatOfPoint> contours = new ArrayList<>();
    Mat hierarchy = new Mat();
    Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
    Log.d("Contours", "Found " + contours.size() + " contours");

    // 过滤:仅保留最外层轮廓(parent=-1,hierarchy中每个元素为[next, prev, child, parent])
    List<MatOfPoint> externalContours = new ArrayList<>();
    for (int i = 0; i < contours.size(); i++) {
        int parent = (int) hierarchy.get(0, i)[3];
        if (parent == -1) {
            externalContours.add(contours.get(i));
        }
    }

    // 5. 轮廓处理核心:缩放→重叠检测→合并
    // 轮廓放大1.1倍,以轮廓中心为基准
    List<MatOfPoint> scaleExternalContours = MyOpenCvUtils.scaleImg(externalContours,1.1);
    // 检测重叠并合并重叠轮廓
    List<MatOfPoint> finalContours = MyOpenCvUtils.finalContours(scaleExternalContours,originalMat);

    // 6. 绘制轮廓到原始图像:绿色轮廓,线宽10
    Mat resultMat = originalMat.clone(); // 克隆原图,避免修改原始数据
    Scalar colorG = new Scalar(0, 255, 0, 255); // RGBA绿色
    int thickness = 10;
    Imgproc.drawContours(resultMat, finalContours, -1, colorG, thickness); // -1绘制所有轮廓

    // 7. 处理后图像转回Bitmap并展示
    showMat(resultMat);

    // 关键:释放Mat资源,避免内存泄漏
    gray.release();
    blurred.release();
    binary.release();
    hierarchy.release();
    resultMat.release();
}

/**
 * Mat转Bitmap并展示(需自行实现ImageView展示逻辑)
 * @param mat 处理后的图像矩阵
 */
private void showMat(Mat mat) {
    Bitmap bitmap = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888);
    Utils.matToBitmap(mat, bitmap);
    // 示例:ivImage.setImageBitmap(bitmap);
}

第三步:轮廓按中心等比例缩放(MyOpenCvUtils.scaleImg)

轮廓缩放的核心是以轮廓的几何中心为基准 ,避免缩放后轮廓偏移,同时做了空值校验除零防护无效点过滤,保证缩放后轮廓的有效性(至少3个点构成轮廓)。

核心原理:计算轮廓每个点相对于中心的偏移量,将偏移量按缩放倍数放大,再重新计算点的新坐标。

java 复制代码
/**
 * 以轮廓几何中心为基准,等比例缩放轮廓列表
 * @param externalContours 待缩放的轮廓列表
 * @param scale 缩放倍数(>1放大,<1缩小)
 * @return 缩放后的有效轮廓列表
 */
public static List<MatOfPoint> scaleImg(List<MatOfPoint> externalContours, double scale){
    if(externalContours==null || externalContours.isEmpty()){
        return externalContours;
    }
    List<MatOfPoint> scaledContours = new ArrayList<>();
    for (MatOfPoint contour : externalContours) {
        if (contour.total() < 3) { // 少于3个点,不是有效轮廓,跳过
            continue;
        }
        // 计算轮廓的几何中心:通过矩(Moments)计算
        Moments moments = Imgproc.moments(contour);
        double cx, cy;
        // 除零防护:矩的m00为0时,用轮廓边界框中心作为兜底
        if (Math.abs(moments.m00) < 1e-5) {
            Rect rect = Imgproc.boundingRect(contour);
            cx = rect.x + rect.width / 2.0;
            cy = rect.y + rect.height / 2.0;
        } else {
            cx = moments.m10 / moments.m00;
            cy = moments.m01 / moments.m00;
        }
        // 处理单个轮廓的缩放
        processContourWithCenter(contour, cx, cy, scale, scaledContours);
    }
    return scaledContours;
}

/**
 * 辅助方法:单个轮廓的缩放逻辑
 */
private static void processContourWithCenter(MatOfPoint contour, double cx, double cy,
                                             double scale, List<MatOfPoint> output) {
    List<Point> scaledPoints = new ArrayList<>();
    boolean hasValidPoint = false;
    for (Point p : contour.toList()) {
        // 过滤NaN/Infinity的无效点
        if (!Double.isFinite(p.x) || !Double.isFinite(p.y)) {
            continue;
        }
        // 计算点相对于中心的偏移量,按倍数缩放
        double dx = p.x - cx;
        double dy = p.y - cy;
        double sx = cx + dx * scale;
        double sy = cy + dy * scale;
        // 保留有效缩放点
        if (Double.isFinite(sx) && Double.isFinite(sy)) {
            scaledPoints.add(new Point(sx, sy));
            hasValidPoint = true;
        }
    }
    // 仅保留至少3个有效点的轮廓
    if (hasValidPoint && scaledPoints.size() >= 3) {
        MatOfPoint scaled = new MatOfPoint();
        scaled.fromList(scaledPoints);
        output.add(scaled);
    }
}

第四步:重叠轮廓检测与分组(并查集实现)

缩放后的轮廓可能出现重叠,本文通过轮廓边界框(boundingRect)的相交检测 判断轮廓是否重叠,并使用并查集(Union-Find) 数据结构对重叠轮廓进行分组,核心优势是高效处理元素的合并与查询,时间复杂度接近O(1)。

并查集工具类(MyOpenCvUtils.UnionFind)

java 复制代码
/**
 * 并查集:用于轮廓重叠分组,支持合并(union)和查找(find)
 */
static class UnionFind {
    int[] parent;
    // 初始化:每个元素的父节点是自身
    UnionFind(int n) {
        parent = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;
    }
    // 查找根节点,路径压缩优化
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }
    // 合并两个元素:将x的根节点指向y的根节点
    void union(int x, int y) {
        parent[find(x)] = find(y);
    }
}

重叠检测与分组核心逻辑

java 复制代码
/**
 * 核心:检测轮廓重叠,合并重叠轮廓,返回最终轮廓列表
 * @param scaledContours 缩放后的轮廓列表
 * @param originalMat 原始图像Mat(用于获取图像宽高)
 * @return 去重叠后的最终轮廓列表
 */
public static List<MatOfPoint> finalContours(List<MatOfPoint> scaledContours,Mat originalMat){
    if(scaledContours==null || scaledContours.isEmpty()){
        return scaledContours;
    }
    List<MatOfPoint> finalContours = new ArrayList<>();
    int n = scaledContours.size();
    // 1. 为每个轮廓计算边界框
    List<Rect> boundingRects = new ArrayList<>();
    for (MatOfPoint contour : scaledContours) {
        boundingRects.add(Imgproc.boundingRect(contour));
    }
    // 2. 并查集分组:重叠的轮廓归为同一组
    UnionFind uf = new UnionFind(n);
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            // 检测两个边界框是否相交,相交则合并
            if (rectIntersect(boundingRects.get(i),(boundingRects.get(j)))) {
                uf.union(i, j);
            }
        }
    }
    // 3. 按并查集的根节点分组收集轮廓
    Map<Integer, List<MatOfPoint>> groups = new HashMap<>();
    for (int i = 0; i < n; i++) {
        int root = uf.find(i);
        groups.computeIfAbsent(root, k -> new ArrayList<>()).add(scaledContours.get(i));
    }
    // 4. 处理每组轮廓:单轮廓直接保留,多轮廓合并为一个
    for (List<MatOfPoint> group : groups.values()) {
        if (group.size() == 1) {
            finalContours.add(group.get(0));
        } else {
            // 调用ShapeOpUtils合并多轮廓为单轮廓
            MatOfPoint merged = ShapeOpUtils.contoursToUnionContour(originalMat.cols(),originalMat.rows(),group);
            finalContours.add(merged);
        }
    }
    return finalContours;
}

/**
 * 检测两个矩形是否相交(边界框重叠判断核心)
 */
public static boolean rectIntersect(Rect a, Rect b) {
    return a.x <= b.x + b.width &&
            b.x <= a.x + a.width &&
            a.y <= b.y + b.height &&
            b.y <= a.y + a.height;
}

第五步:重叠轮廓合并(ShapeOpUtils核心实现)

重叠轮廓的合并是本文的核心难点,本文采用掩码并集法 实现,核心思路是:将轮廓转换为二值掩码→对掩码做按位或(并集)运算→从合并后的掩码中重新提取轮廓,该方法比直接合并轮廓点更稳定,能有效生成连续的合并轮廓。

轮廓与掩码互转

轮廓转掩码:创建与图像同尺寸的黑色掩码,将轮廓内部填充为白色(255); 掩码转轮廓:从二值掩码中提取面积最大的外层轮廓,作为合并后的轮廓。

java 复制代码
/**
 * 单个轮廓转二值掩码:轮廓内为白色(255),其余为黑色(0)
 */
public static Mat contourToMask(int width, int height, MatOfPoint contour) {
    Mat mask = Mat.zeros(height, width, CvType.CV_8UC1); // 单通道二值掩码
    List<MatOfPoint> contours = new ArrayList<>();
    contours.add(contour);
    Imgproc.drawContours(mask, contours, -1, new Scalar(255), Imgproc.FILLED); // 填充轮廓内部
    return mask;
}

/**
 * 二值掩码提取最大外层轮廓:合并后掩码仅保留最大轮廓
 */
public static MatOfPoint maskToContour(Mat mask) {
    List<MatOfPoint> contours = new ArrayList<>();
    Mat hierarchy = new Mat();
    Imgproc.findContours(mask, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
    hierarchy.release();
    if (contours.isEmpty()) return null;
    // 找到面积最大的轮廓(合并后的主轮廓)
    MatOfPoint maxContour = contours.get(0);
    double maxArea = Imgproc.contourArea(maxContour);
    for (MatOfPoint cnt : contours) {
        double area = Imgproc.contourArea(cnt);
        if (area > maxArea) {
            maxArea = area;
            maxContour = cnt;
        }
    }
    return maxContour;
}

多轮廓合并核心:掩码并集运算

java 复制代码
/**
 * 多个轮廓合并为单个轮廓:基于掩码并集实现
 * @param width 图像宽度
 * @param height 图像高度
 * @param contours 待合并的重叠轮廓列表
 * @return 合并后的单个轮廓
 */
public static MatOfPoint contoursToUnionContour(int width, int height, List<MatOfPoint> contours) {
    // 1. 所有轮廓转换为对应的二值掩码
    List<Mat> masks = new ArrayList<>();
    for (MatOfPoint cnt : contours) {
        masks.add(contourToMask(width, height, cnt));
    }
    // 2. 多掩码求并集:按位或运算,重叠区域合并为白色
    Mat unionMask = multiUnion(masks);
    // 3. 从合并后的掩码中提取最大轮廓
    MatOfPoint resultContour = maskToContour(unionMask);
    // 4. 释放掩码资源
    unionMask.release();
    for (Mat m : masks) m.release();
    return resultContour;
}

/**
 * 多掩码并集:遍历掩码,依次做按位或运算
 */
public static Mat multiUnion(List<Mat> masks) {
    Mat result = Mat.zeros(masks.get(0).rows(), masks.get(0).cols(), CvType.CV_8UC1);
    for (Mat mask : masks) {
        Mat temp = result.clone();
        Core.bitwise_or(temp, mask, result); // 按位或:有白色则为白色
        temp.release();
    }
    return result;
}

四、关键优化点与注意事项

内存优化:及时释放Mat资源

OpenCV的Mat对象基于native层内存,Java的GC无法自动回收,所有创建的Mat对象必须在使用后调用release()释放,否则会导致内存泄漏、应用崩溃。

全链路空值与无效值校验

  1. 轮廓点数量校验:至少3个点才构成有效轮廓;
  2. 矩的除零防护:moments.m00接近0时,用边界框中心兜底;
  3. 坐标值校验:过滤NaN/Infinity的无效坐标点;
  4. 空列表校验:对轮廓列表、掩码列表做非空判断,避免空指针。

性能优化

  1. 轮廓提取使用CHAIN_APPROX_SIMPLE压缩轮廓点,减少后续计算量;
  2. 重叠检测使用边界框相交替代轮廓点逐点检测,大幅降低计算复杂度;
  3. 并查集使用路径压缩优化,保证合并/查询的高效性;
  4. 避免频繁创建Mat对象,尽量复用已有对象(如克隆后及时释放)。

视觉效果优化

  1. 高斯模糊的卷积核和标准差根据实际图像调整,平衡去噪和轮廓保留;
  2. 二值化阈值根据业务场景调优(本文用240,可通过自适应阈值adaptiveThreshold优化);
  3. 轮廓缩放以几何中心为基准,避免缩放后轮廓偏移;
  4. 绘制轮廓时克隆原始Mat,避免修改原始图像数据。

五、扩展与定制化

  1. 轮廓缩放倍数动态调整:根据图像分辨率、轮廓大小动态设置缩放倍数,而非固定1.1倍;
  2. 自适应二值化 :替换固定阈值thresholdImgproc.adaptiveThreshold,适配不同光照的图像;
  3. 轮廓面积过滤 :提取轮廓后,过滤面积过小的轮廓(噪点),如if (Imgproc.contourArea(contour) < minArea) continue;
  4. 其他轮廓运算ShapeOpUtils已实现交集、差集、异或运算,可根据需求扩展轮廓的其他组合处理;
  5. 轮廓近似 :对合并后的轮廓使用Imgproc.approxPolyDP做多边形近似,压缩轮廓点数量,提升后续处理效率。

六、总结

本文实现了一套基于Android OpenCV的端侧轮廓处理全流程方案,核心解决了轮廓提取的预处理轮廓的中心缩放重叠轮廓的高效检测重叠轮廓的稳定合并四大问题,可直接移植到实际项目中。

该方案适用于图像目标检测图形轮廓分析手写体识别物体边缘检测等计算机视觉场景,在此基础上可根据业务需求扩展轮廓的特征提取、匹配、跟踪等高级功能。

核心亮点

  1. 采用掩码并集法合并重叠轮廓,比直接合并点集更稳定;
  2. 并查集实现重叠轮廓分组,高效处理多轮廓的重叠关系;
  3. 全链路的无效值校验和内存管理,保证工程化可用性;
  4. 工具类解耦,代码复用性高,便于后续扩展。
相关推荐
hz_zhangrl2 小时前
CCF-GESP 等级考试 2026年3月认证C++三级真题解析
c++·算法·程序设计·gesp·gesp2026年3月·gesp c++三级
x_xbx2 小时前
LeetCode:1. 两数之和
数据结构·算法·leetcode
x_xbx2 小时前
LeetCode:49. 字母异位词分组
算法·leetcode·职场和发展
玲娜贝儿--努力学习买大鸡腿版2 小时前
hot 100 刷题记录(1)
数据结构·python·算法
123过去2 小时前
pixiewps使用教程
linux·网络·测试工具·算法·哈希算法
tangweiguo030519872 小时前
Android SSE 流式接收:从手写到框架的进阶之路
android
深圳市快瞳科技有限公司3 小时前
低空经济下,鸟类识别算法与无人机硬件的兼容性优化策略
算法·无人机
努力中的编程者3 小时前
二叉树(C语言底层实现)
c语言·开发语言·数据结构·c++·算法
大尚来也3 小时前
PHP 反序列化漏洞深度解析:从原理利用到 allowed_classes 防御实战
android·开发语言·php