在Android开发中,基于OpenCV实现图像轮廓的提取、处理是计算机视觉的常见需求,本文将分享一套完整的OpenCV轮廓处理方案,实现Bitmap与Mat互转 、图像轮廓提取 、轮廓按中心缩放 、重叠轮廓检测 和重叠轮廓合并的全流程开发,最终将处理后的轮廓绘制回图像并展示。
一、技术基础与核心思路
1. 核心依赖
基于OpenCV for Android 实现,核心操作围绕OpenCV的Imgproc(图像处理)、Core(核心运算)模块,以及Mat(图像矩阵)、MatOfPoint(轮廓点集)等核心数据结构展开。
2. 整体处理流程
- 将Android原生
Bitmap转换为OpenCV可处理的Mat矩阵; - 对图像进行灰度化、高斯模糊、二值化预处理,提取外部轮廓;
- 以轮廓中心为基准,对提取的轮廓进行等比例缩放;
- 检测缩放后轮廓的边界框重叠关系,通过并查集对重叠轮廓分组;
- 对每组重叠轮廓进行掩码并集运算,合并为单个轮廓;
- 将处理后的轮廓绘制回原始图像,再转回
Bitmap展示; - 及时释放OpenCV的Mat资源,避免内存泄漏。
二、环境准备
- 集成OpenCV for Android到项目,在
build.gradle中添加依赖并配置OpenCV库; - 初始化OpenCV,确保在使用OpenCV API前完成库的加载(可通过
OpenCVLoader.initDebug()调试,正式环境使用静态初始化); - 申请图像读取/绘制的相关权限(如存储、相机,根据业务需求)。
三、核心代码实现与解析
工具类前置
本文涉及两个自定义工具类,职责分工明确:
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()释放,否则会导致内存泄漏、应用崩溃。
全链路空值与无效值校验
- 轮廓点数量校验:至少3个点才构成有效轮廓;
- 矩的除零防护:
moments.m00接近0时,用边界框中心兜底; - 坐标值校验:过滤NaN/Infinity的无效坐标点;
- 空列表校验:对轮廓列表、掩码列表做非空判断,避免空指针。
性能优化
- 轮廓提取使用
CHAIN_APPROX_SIMPLE压缩轮廓点,减少后续计算量; - 重叠检测使用边界框相交替代轮廓点逐点检测,大幅降低计算复杂度;
- 并查集使用路径压缩优化,保证合并/查询的高效性;
- 避免频繁创建Mat对象,尽量复用已有对象(如克隆后及时释放)。
视觉效果优化
- 高斯模糊的卷积核和标准差根据实际图像调整,平衡去噪和轮廓保留;
- 二值化阈值根据业务场景调优(本文用240,可通过自适应阈值
adaptiveThreshold优化); - 轮廓缩放以几何中心为基准,避免缩放后轮廓偏移;
- 绘制轮廓时克隆原始Mat,避免修改原始图像数据。
五、扩展与定制化
- 轮廓缩放倍数动态调整:根据图像分辨率、轮廓大小动态设置缩放倍数,而非固定1.1倍;
- 自适应二值化 :替换固定阈值
threshold为Imgproc.adaptiveThreshold,适配不同光照的图像; - 轮廓面积过滤 :提取轮廓后,过滤面积过小的轮廓(噪点),如
if (Imgproc.contourArea(contour) < minArea) continue;; - 其他轮廓运算 :
ShapeOpUtils已实现交集、差集、异或运算,可根据需求扩展轮廓的其他组合处理; - 轮廓近似 :对合并后的轮廓使用
Imgproc.approxPolyDP做多边形近似,压缩轮廓点数量,提升后续处理效率。
六、总结
本文实现了一套基于Android OpenCV的端侧轮廓处理全流程方案,核心解决了轮廓提取的预处理 、轮廓的中心缩放 、重叠轮廓的高效检测 和重叠轮廓的稳定合并四大问题,可直接移植到实际项目中。
该方案适用于图像目标检测 、图形轮廓分析 、手写体识别 、物体边缘检测等计算机视觉场景,在此基础上可根据业务需求扩展轮廓的特征提取、匹配、跟踪等高级功能。
核心亮点:
- 采用掩码并集法合并重叠轮廓,比直接合并点集更稳定;
- 并查集实现重叠轮廓分组,高效处理多轮廓的重叠关系;
- 全链路的无效值校验和内存管理,保证工程化可用性;
- 工具类解耦,代码复用性高,便于后续扩展。