OpenCV 如何提高阈值的准确性

23

我正在开发一个应用程序,希望使用OpenCV删除图像背景。起初,我尝试使用grabcut,但速度太慢,而且结果不总是准确的。然后我尝试使用threshold,虽然结果还没有接近grabcut,但非常快速并且看起来更好。所以我的代码首先查看图像色调,并分析哪一部分出现得更多,该部分被作为背景取出。问题在于有时它会将前景作为背景。以下是我的代码:

private Bitmap backGrndErase()
{

    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.skirt);
    Log.d(TAG, "bitmap: " + bitmap.getWidth() + "x" + bitmap.getHeight());


    bitmap = ResizeImage.getResizedBitmap(bitmap, calculatePercentage(40, bitmap.getWidth()), calculatePercentage(40, bitmap.getHeight()));

    Mat frame = new Mat();
    Utils.bitmapToMat(bitmap, frame);

    Mat hsvImg = new Mat();
    List<Mat> hsvPlanes = new ArrayList<>();
    Mat thresholdImg = new Mat();

    // int thresh_type = Imgproc.THRESH_BINARY_INV;
    //if (this.inverse.isSelected())
    int thresh_type = Imgproc.THRESH_BINARY;

    // threshold the image with the average hue value
    hsvImg.create(frame.size(), CvType.CV_8U);
    Imgproc.cvtColor(frame, hsvImg, Imgproc.COLOR_BGR2HSV);
    Core.split(hsvImg, hsvPlanes);

    // get the average hue value of the image
    double threshValue = this.getHistAverage(hsvImg, hsvPlanes.get(0));

    Imgproc.threshold(hsvPlanes.get(0), thresholdImg, threshValue, mThresholdValue, thresh_type);
   // Imgproc.adaptiveThreshold(hsvPlanes.get(0), thresholdImg, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 11, 2);

    Imgproc.blur(thresholdImg, thresholdImg, new Size(5, 5));

    // dilate to fill gaps, erode to smooth edges
    Imgproc.dilate(thresholdImg, thresholdImg, new Mat(), new Point(-1, -1), 1);
    Imgproc.erode(thresholdImg, thresholdImg, new Mat(), new Point(-1, -1), 3);

    Imgproc.threshold(thresholdImg, thresholdImg, threshValue, mThresholdValue, Imgproc.THRESH_BINARY);
    //Imgproc.adaptiveThreshold(thresholdImg, thresholdImg, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 11, 2);

    // create the new image
    Mat foreground = new Mat(frame.size(), CvType.CV_8UC3, new Scalar(255, 255, 255));
    frame.copyTo(foreground, thresholdImg);


    Utils.matToBitmap(foreground,bitmap);
    //return foreground;

    alreadyRun = true;
    return  bitmap;

}

负责色调的方法:

    private double getHistAverage(Mat hsvImg, Mat hueValues)
{
    // init
    double average = 0.0;
    Mat hist_hue = new Mat();
    // 0-180: range of Hue values
    MatOfInt histSize = new MatOfInt(180);
    List<Mat> hue = new ArrayList<>();
    hue.add(hueValues);

    // compute the histogram
    Imgproc.calcHist(hue, new MatOfInt(0), new Mat(), hist_hue, histSize, new MatOfFloat(0, 179));

    // get the average Hue value of the image
    // (sum(bin(h)*h))/(image-height*image-width)
    // -----------------
    // equivalent to get the hue of each pixel in the image, add them, and
    // divide for the image size (height and width)
    for (int h = 0; h < 180; h++)
    {
        // for each bin, get its value and multiply it for the corresponding
        // hue
        average += (hist_hue.get(h, 0)[0] * h);
    }

    // return the average hue of the image
    average = average / hsvImg.size().height / hsvImg.size().width;
    return average;
}

输入输出样例:

输入图片1 输出图片

输入图片2和输出: 图片描述 图片描述

输入图片3和输出: 图片描述 图片描述


1
衬衫示例中的边框可能是因为您正在使用JPEG图像。对于其他示例,这种在非平凡背景上执行任务并不容易 :D。 - Miki
我已经在这上面工作了3周,明天将标志着第4周,但仍然没有太多进展。 - life evader
我可以建议您采用以下方法:从原始图像的颜色降低版本(例如,考虑256色图像)开始创建“背景掩码”。这样,您将拥有更少的颜色桶,因此在颜色边缘附近可能会有更高的容差。 - bonnyz
1
看这里:https://dev59.com/SG025IYBdhLWcg3wnXSf#10179800 这是Python实现,可以减少图像的颜色数量(实际上,页面中有许多类似的方法),您需要将其中一个适应到Java中(但执行速度可能非常慢)。 - bonnyz
Indicator1 正在显示我的当前 mThresholdValue 值,而 runs 则表示我已经改变了 mThresholdValue 值的次数,它基本上是为了进行“调试”。 - life evader
显示剩余6条评论
3个回答

3

确实,正如其他人所说,仅通过色调阈值很难获得良好的结果。您可以使用类似于GrabCut但更快的方法。

在背后,GrabCut计算前景和背景直方图,然后根据这些直方图计算每个像素成为FG/BG的概率,然后使用图割优化得到的概率图以获得分割。

最后一步是最昂贵的,并且根据应用程序可以忽略它。相反,您可以将阈值应用于概率图以获得分割。它可能(并且会)比GrabCut差,但会比您当前的方法更好。

对于此方法需要考虑几点。直方图模型的选择在这里非常重要。您可以在YUV或HSV等空间中考虑2个通道,考虑RGB的3个通道,或考虑规范化的RGB的2个通道。您还必须为这些直方图选择适当的bin大小。太小的bin会导致“过度训练”,而太大会降低精度。其中的权衡是另一个讨论的话题,简而言之 - 我建议使用RGB和每个通道64个bin开始,然后查看哪些更适合您的数据。

此外,如果使用插值在粗略分bin时可以获得更好的结果。过去我曾使用三线性插值,效果还不错,相比没有插值。

但请记住,无论是GrabCut、阈值还是这种方法,在没有物体形状的先验知识的情况下,不能保证分割结果是正确的。


0

你计算平均色调的方法是错误的! 你很可能知道,色调被表示为角度,并在[0,360]范围内取值。因此,色调为360的像素实际上与色调为0的像素具有相同的颜色(两者都是纯红色)。同样,色调为350的像素实际上比色调为300的像素更接近于色调为10的像素。

至于opencv,cvtColor函数实际上将计算出的色调值除以2以适应8位整数。因此,在opencv中,色调值在180之后包装。现在,考虑我们有两个色调为10和170的红色(ish)像素。如果我们取它们的平均值,我们将得到90-青色的纯色调,即红色的完全相反色-这不是我们想要的值。

因此,要正确地找到平均色调,您需要首先在RGB颜色空间中找到平均像素值,然后从该RGB值计算色调。您可以创建一个带有平均RGB像素的1x1矩阵,并将其转换为HSV / HSL。

按照同样的推理,对色调图像应用threshold并不能完美地工作。它没有考虑到色调值的包装。

如果我理解正确,您想要找到与背景色相似的像素。假设我们知道背景的颜色,我会在RGB空间中进行分割。我会引入一些“容差”变量。我会以背景像素值为中心,并以此容差为半径,在RGB颜色空间中定义一个球体。现在,剩下的就是检查每个像素值,如果它落在这个球体内,则将其分类为背景;否则,将其视为前景像素。


0

我会再次尝试使用Grabcut,它是最好的分割方法之一。这是我得到的result结果

cv::Mat bgModel,fgModel; // the models (internally used)
cv::grabCut(image,// input image
            object_mask,// segmentation result
            rectang,// rectangle containing foreground
            bgModel,fgModel, // models
            5,// number of iterations
            cv::GC_INIT_WITH_RECT); // use rectangle
// Get the pixels marked as likely foreground
cv::compare(object_mask,cv::GC_PR_FGD,object_mask,cv::CMP_EQ);
cv::threshold(object_mask, object_mask, 0,255, CV_THRESH_BINARY);  //ensure the mask is binary

Grabcut 的唯一问题是你必须输入一个包含你想要提取的对象的矩形。除此之外,它运行得相当不错。


说实话,这非常令人印象深刻。我尝试过GrabCut,速度有点慢,但是由于结果非常好,我可以再次尝试使用它。但即使在使用它时,它也会像您的结果一样将图像的右下角标记出来,我该如何防止这种情况发生? - life evader
自动完成这项任务很困难,因为算法会迭代地选择前景和背景。但是你可以给算法一个像GC_BGD或GC_PR_FGD值的真实数据。更多信息请参见http://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#grabcut和http://docs.opencv.org/master/d8/d83/tutorial_py_grabcut.html#gsc.tab=0。 - hoaphumanoid
加速Grabcut的一个技巧是先缩小图像,然后应用GC并获得分割矩阵,然后将矩阵放大到原始大小并乘以原始图像。但可能会损失质量。 - hoaphumanoid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接