在Android上玩转Opencv 系列:13.基础知识( 凸包,肤色提取,聚类综合运用提取手指)

205 阅读5分钟

在 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);
    }
}

代码解释:

  1. 加载图像并转换为 Mat 格式

• BitmapFactory.decodeResource() 用于加载图像资源,并使用 Utils.bitmapToMat() 转换为 OpenCV 的 Mat 类型,方便后续处理。

  1. 图像预处理

• 将图像转换为 灰度图像,然后通过 Imgproc.threshold() 方法进行 二值化,便于后续轮廓的提取。

  1. 查找轮廓

• 使用 Imgproc.findContours() 查找图像中的所有轮廓,并存储到 contours 列表中。

  1. 计算凸包

• 对于每个轮廓,使用 Imgproc.convexHull() 计算凸包,得到每个轮廓的最小凸多边形。

  1. 绘制凸包

• 将计算出的凸包点转换为 MatOfPoint,并使用 Imgproc.polylines() 在图像上绘制凸包。

  1. 显示结果

• 将处理后的 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;
    }


}

效果图

handprocess.png