在 Android 中使用 OpenCV 进行凸包(Convex Hull)检测是一种常见的图像处理任务,常用于形状分析、轮廓检测和物体识别等。凸包检测可以找出图像中的物体的最小外包围区域,通常用于简化轮廓的形状或者检查物体的形状。
什么是凸包(Convex Hull)?
在数学和计算机图形学中,凸包是指通过一组点构成的最小凸多边形。简单来说,凸包就是将所有的点包围起来的最小的凸形状。对于图像中的物体,凸包是该物体的外边界,任何在物体内部的点都不会出现在凸包的边界上。
OpenCV 中的 凸包 函数
在 OpenCV 中,计算凸包的函数是 Imgproc.convexHull(),这个函数的作用是根据给定的轮廓计算出一个新的凸包轮廓。
函数签名:
Imgproc.convexHull(MatOfPoint contour, MatOfInt hull, boolean clockwise, boolean returnPoints);
• contour:输入轮廓(一个点的集合)。
• hull:输出的凸包,返回的点集合。
• clockwise:是否按顺时针方向计算(通常设为 true 或 false)。
• returnPoints:如果为 true,则返回点的坐标;如果为 false,则返回点的索引。
1. 在 Android 中使用 OpenCV 进行凸包检测
完整代码示例
import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfInt;
import org.opencv.core.Point;
import org.opencv.imgproc.Imgproc;
import org.opencv.core.Scalar;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取图像并转换为OpenCV的Mat格式
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.sample_image);
Mat imgMat = new Mat();
Utils.bitmapToMat(bitmap, imgMat);
// 将图像转换为灰度图
Mat grayMat = new Mat();
Imgproc.cvtColor(imgMat, grayMat, Imgproc.COLOR_RGB2GRAY);
// 使用阈值化将图像转换为二值图像
Mat thresholdMat = new Mat();
Imgproc.threshold(grayMat, thresholdMat, 100, 255, Imgproc.THRESH_BINARY);
// 查找图像中的轮廓
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(thresholdMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
// 创建一个新的Mat用于显示结果
Mat resultMat = imgMat.clone();
// 对每个轮廓计算凸包并绘制
for (MatOfPoint contour : contours) {
MatOfInt hull = new MatOfInt();
// 计算凸包
Imgproc.convexHull(contour, hull, false, false);
// 绘制凸包
List<Point> hullPoints = new ArrayList<>();
for (int i = 0; i < hull.rows(); i++) {
int index = (int) hull.get(i, 0)[0];
hullPoints.add(contour.toList().get(index));
}
// 将凸包绘制为多边形
MatOfPoint hullMat = new MatOfPoint();
hullMat.fromList(hullPoints);
Imgproc.polylines(resultMat, java.util.Collections.singletonList(hullMat), true, new Scalar(0, 255, 0), 2);
}
// 将处理后的Mat转换为Bitmap并显示
Bitmap resultBitmap = Bitmap.createBitmap(resultMat.cols(), resultMat.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(resultMat, resultBitmap);
// 显示处理后的图像
ImageView imageView = findViewById(R.id.imageView);
imageView.setImageBitmap(resultBitmap);
}
}
代码解释:
- 加载图像并转换为 Mat 格式:
• BitmapFactory.decodeResource() 用于加载图像资源,并使用 Utils.bitmapToMat() 转换为 OpenCV 的 Mat 类型,方便后续处理。
- 图像预处理:
• 将图像转换为 灰度图像,然后通过 Imgproc.threshold() 方法进行 二值化,便于后续轮廓的提取。
- 查找轮廓:
• 使用 Imgproc.findContours() 查找图像中的所有轮廓,并存储到 contours 列表中。
- 计算凸包:
• 对于每个轮廓,使用 Imgproc.convexHull() 计算凸包,得到每个轮廓的最小凸多边形。
- 绘制凸包:
• 将计算出的凸包点转换为 MatOfPoint,并使用 Imgproc.polylines() 在图像上绘制凸包。
- 显示结果:
• 将处理后的 Mat 转换为 Bitmap,然后通过 ImageView 显示出来。
2. 凸包的应用场景
• 物体识别:凸包可以用来简化物体的轮廓,并提取出物体的外部边界。
• 形状分析:通过计算图像中的凸包,可以了解物体的形状特征,如是否为凸形状、凹形状等。
• 图像分割:在图像处理中,凸包用于从复杂的图像中提取并标记出特定区域的边界。
• 边缘检测与简化:通过计算轮廓的凸包,可以去除一些不必要的小细节,简化图像分析过程。
3. 总结
• Imgproc.convexHull() 是 OpenCV 中非常有用的函数,用于计算图像中轮廓的最小凸包。通过该函数,可以获取图像中物体的外包围区域,并进行进一步的图像分析。
• 在 Android 中使用 OpenCV 进行凸包检测时,通常包括图像预处理、轮廓检测、计算凸包和绘制凸包等步骤。通过这些步骤,可以高效地处理图像中的物体边界。
这个过程非常适用于物体识别、图像分割和形状分析等领域。如果需要进一步优化,可以结合其他算法(如形态学操作)来改善轮廓提取和凸包计算的效果。
4.综合运用
public class ConvexHullActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_convex_hull);
Bitmap bitmap = getSkinArea();
((ImageView) findViewById(R.id.imageView2)).setImageBitmap(bitmap);
Bitmap bitmap2 = detectConvexHull(bitmap);
((ImageView) findViewById(R.id.imageView3)).setImageBitmap(bitmap2);
}
/**
* 提取肤色区域
* @return
*/
private Bitmap getSkinArea() {
// 读取资源图片
Bitmap bitmap = ImgHelper.readImg(R.drawable.hand);
if (bitmap == null) {
Log.e("OpenCV", "无法加载 Bitmap!");
return null;
}
// 转换 Bitmap -> Mat
Mat img = new Mat();
Utils.bitmapToMat(bitmap, img);
if (img.empty()) {
Log.e("OpenCV", "无法加载 Mat 图像!");
return null;
}
// **确保 img 只有 3 通道(避免 Alpha 透明通道问题)**
if (img.channels() == 4) {
Imgproc.cvtColor(img, img, Imgproc.COLOR_RGBA2BGR);
} else if (img.channels() == 1) {
Imgproc.cvtColor(img, img, Imgproc.COLOR_GRAY2BGR);
}
// **转换为 HSV 颜色空间**
Mat imgHSV = new Mat();
Imgproc.cvtColor(img, imgHSV, Imgproc.COLOR_BGR2HSV);
// **设置肤色范围(HSV)**
Scalar lower = new Scalar(0, 48, 80);
Scalar upper = new Scalar(20, 255, 255);
Mat skinMask = new Mat();
Core.inRange(imgHSV, lower, upper, skinMask);
// 膨胀,消除缝隙
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, new Size(9, 9));
Imgproc.dilate(skinMask, skinMask, kernel);
// **检查尺寸是否匹配**
Log.d("OpenCV", "img size: " + img.size() + ", channels: " + img.channels());
Log.d("OpenCV", "skinMask size: " + skinMask.size() + ", channels: " + skinMask.channels());
// **转换 skinMask 为 3 通道**
Imgproc.cvtColor(skinMask, skinMask, Imgproc.COLOR_GRAY2BGR);
// **确保尺寸仍然匹配**
if (!img.size().equals(skinMask.size())) {
Log.e("OpenCV", "尺寸不匹配!调整中...");
Imgproc.resize(skinMask, skinMask, img.size());
}
// **检查最终尺寸是否匹配**
Log.d("OpenCV", "调整后 skinMask size: " + skinMask.size());
// **位运算提取肤色区域**
Mat skin = new Mat();
Core.bitwise_and(img, skinMask, skin); // ✅ 现在尺寸和通道数匹配
//二值化变成白色
//Imgproc.threshold(skin, skin, 0, 255, Imgproc.THRESH_BINARY);
///消除噪声
Imgproc.erode(skin, skin,kernel, new org.opencv.core.Point(-1, -1), 3, Core.BORDER_CONSTANT, new Scalar(0));
// **转换颜色通道(BGR -> RGB)**
Imgproc.cvtColor(skin, skin, Imgproc.COLOR_BGR2RGB);//显示的时候需要的步骤
// **转换 Mat -> Bitmap**
Bitmap resultBitmap = Bitmap.createBitmap(skin.cols(), skin.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(skin, resultBitmap);
return resultBitmap;
}
/**
* 凸包检测,手掌标记
* @param bitmap
* @return
*/
private Bitmap detectConvexHull(Bitmap bitmap) {
// 1️⃣ 读取资源图片
if (bitmap == null) {
Log.e("OpenCV", "无法加载 Bitmap!");
return null;
}
// 2️⃣ 转换 Bitmap -> Mat
Mat img = new Mat();
Utils.bitmapToMat(bitmap, img);
if (img.empty()) {
Log.e("OpenCV", "无法加载 Mat 图像!");
return null;
}
// 3️⃣ 转换为灰度图
Mat gray = new Mat();
Imgproc.cvtColor(img, gray, Imgproc.COLOR_BGR2GRAY);
// 4️⃣ 进行二值化(或者使用 Canny 边缘检测)
Mat binary = new Mat();
Imgproc.threshold(gray, binary, 0, 255, Imgproc.THRESH_BINARY);
// 5️⃣ 查找轮廓
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(binary, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
// 6️⃣ 计算凸包并绘制
MatOfInt hull = new MatOfInt();
List<Point> allPoint=new ArrayList<>();
for (MatOfPoint contour : contours) {
// 计算凸包
Imgproc.convexHull(contour, hull);
// 提取凸包点
List<Point> hullPoints = new ArrayList<>();
for (int index : hull.toList()) {
hullPoints.add(contour.toList().get(index));
}
allPoint.addAll(hullPoints);//收集所有的点
// 绘制凸包
// MatOfPoint hullMat = new MatOfPoint();
// hullMat.fromList(hullPoints);
// List<MatOfPoint> hullList = Collections.singletonList(hullMat);
// Imgproc.drawContours(img, hullList, -1, new Scalar(0, 255, 0), 3);
}
List<List<Point>> cps = KMeansClusterUtil.cluster2DPoints(allPoint, 7);//聚类
List<Point> ccps = KMeansClusterUtil.getClusterCenters(cps);//获取聚类后的质心
for(int i=0,isize=ccps.size();i<isize;i++){
Point p=ccps.get(i);
Imgproc.circle(img,p,100,new Scalar(255,0,0),10);
}
// 7️⃣ 转换 Mat -> Bitmap
Bitmap resultBitmap = Bitmap.createBitmap(img.cols(), img.rows(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(img, resultBitmap);
return resultBitmap;
}
}
效果图