Android 中使用 OpenCV 从图像中去除背景

kul*_*der 3 python java android opencv

我想在 Android 中使用 Open CV 删除图像背景。代码工作正常,但输出质量不符合预期。我按照java文档获取代码参考:

https://opencv-java-tutorials.readthedocs.io/en/latest/07-image-segmentation.html

谢谢

原图 原图

我的输出 结果 预期产出 预期输出

我在Android中的代码片段:

private fun doBackgroundRemoval(frame: Mat): Mat? {
    // init
    val hsvImg = Mat()
    val hsvPlanes: List<Mat> = ArrayList()
    val thresholdImg = Mat()
    var thresh_type = Imgproc.THRESH_BINARY_INV
    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
    val threshValue: Double = getHistAverage(hsvImg, hsvPlanes[0])
    threshold(hsvPlanes[0], thresholdImg, threshValue, 78.0, thresh_type)
    Imgproc.blur(thresholdImg, thresholdImg, Size(1.toDouble(), 1.toDouble()))

    val kernel1 =
        Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, Size(11.toDouble(), 11.toDouble()))
    val kernel2 = Mat.ones(3, 3, CvType.CV_8U)
    // dilate to fill gaps, erode to smooth edges
    Imgproc.dilate(thresholdImg, thresholdImg, kernel1, Point(-1.toDouble(), -1.toDouble()), 1)
    Imgproc.erode(thresholdImg, thresholdImg, kernel2, Point(-1.toDouble(), -1.toDouble()), 7)
    threshold(thresholdImg, thresholdImg, threshValue, 255.0, Imgproc.THRESH_BINARY_INV)

    // create the new image
    val foreground = Mat(
        frame.size(), CvType.CV_8UC3, Scalar(
            255.toDouble(),
            255.toDouble(),
            255.toDouble()
        )
    )
    frame.copyTo(foreground, thresholdImg)
    val img_bitmap =
        Bitmap.createBitmap(foreground!!.cols(), foreground!!.rows(), Bitmap.Config.ARGB_8888)
    Utils.matToBitmap(foreground!!, img_bitmap)
    imageView.setImageBitmap(img_bitmap)

    return foreground
}
Run Code Online (Sandbox Code Playgroud)

sta*_*ine 6

正如您所看到的,这项任务一点也不简单。OpenCV 有一种名为“GrabCut”的分割算法,试图解决这个特定问题。该算法非常擅长对背景和前景像素进行分类,但它需要非常具体的信息才能工作。它可以在两种模式下运行:

  • 第一种模式(蒙版模式):使用二进制蒙版(与原始输入大小相同),其中标记 100% 确定的背景像素以及 100% 确定的前景像素。您不必标记图像上的每个像素,只需标记您确信算法会找到任一类像素的区域即可。

  • 第二种模式(前景 ROI):使用包围 100% 明确前景像素的边界框。

现在,我使用“100% 确定”符号来标记那些您 100% 确定它们对应于前景背景的像素。该算法将像素分为四种可能的类别:“确定背景”“可能背景”“确定前景”“可能前景”。它将预测可能的背景和可能的前景像素,但它需要在哪里至少找到“最终前景”像素的先验信息。

话虽如此,我们可以使用GrabCut其第二种模式(矩形 ROI)来尝试对输入图像进行分割。我们可以尝试获取输入的第一个粗略二进制掩码。这将标记我们确定算法可以找到前景像素的位置。我们将把这个粗略的掩码输入算法并检查结果。现在,该方法并不容易,其自动化也并不简单,我们将设置一些手动信息,这些信息对于该输入图像特别有效。我不知道OpenCV的Java实现,所以我给你Python的解决方案。希望您能够移植它。这是该算法的总体轮廓:

  1. 通过阈值处理获取前景对象的第一个粗略掩模
  2. 检测粗糙掩模上的轮廓以检索边界矩形
  3. 边界矩形将作为GrabCut算法的输入 ROI
  4. 设置GrabCut算法所需的参数
  5. 清理GrabCut获得的分割掩模
  6. 使用分割掩码最终分割前景对象

这是代码:

# imports:
import cv2
import numpy as np

# image path
path = "D://opencvImages//"
fileName = "backgroundTest.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# (Optional) Deep copy for results:
inputImageCopy = inputImage.copy()

# Convert RGB to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)

# Adaptive Thresholding
windowSize = 31
windowConstant = 11
binaryImage = cv2.adaptiveThreshold(grayscaleImage, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, windowSize, windowConstant)
Run Code Online (Sandbox Code Playgroud)

第一步是使用自适应阈值获得粗略的前景蒙版。在这里,我使用了该ADAPTIVE_THRESH_MEAN_C方法,其中(局部)阈值是输入图像上邻域区域的平均值。这会产生以下图像:

这很粗糙,对吧?我们可以使用一些形态学来稍微清理一下。我使用Closing具有rectangular kernel大小3 x 310迭代的 a 来加入白色像素的大斑点。我将 OpenCV 函数包装在自定义函数中,这样可以节省我输入一些行的时间。这些辅助函数将在本文末尾介绍。目前,该步骤如下:

# Apply a morphological closing with:
# Rectangular SE size 3 x 3 and 10 iterations
binaryImage = morphoOperation(binaryImage, 3, 10, "Closing")
Run Code Online (Sandbox Code Playgroud)

这是过滤后的粗略掩模:

好一点了。好的,我们现在可以搜索最大轮廓的边界框。对于这个例子来说,通过搜索外部轮廓cv2.RETR_EXTERNAL就足够了,因为我们可以安全地忽略子轮廓,如下所示:

# Find the EXTERNAL contours on the binary image:
contours, hierarchy = cv2.findContours(binaryImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# This list will store the target bounding box
maskRect = []
Run Code Online (Sandbox Code Playgroud)

此外,让我们准备好list存储目标边界矩形的位置。现在让我们搜索检测到的轮廓。我还实现了区域过滤器,以防存在一些噪声,因此低于特定区域阈值的像素将被忽略:

# Look for the outer bounding boxes (no children):
for i, c in enumerate(contours):

    # Get blob area:
    currentArea = cv2.contourArea(c)

    # Get the bounding rectangle:
    boundRect = cv2.boundingRect(c)

    # Set a minimum area
    minArea = 1000

    # Look for the target contour:
    if currentArea > minArea:

        # Found the target bounding rectangle:
        maskRect = boundRect

        # (Optional) Draw the rectangle on the input image:
        # Get the dimensions of the bounding rect:
        rectX = boundRect[0]
        rectY = boundRect[1]
        rectWidth = boundRect[2]
        rectHeight = boundRect[3]

        # (Optional) Set color and draw:
        color = (0, 0, 255)
        cv2.rectangle( inputImageCopy, (int(rectX), int(rectY)),
                    (int(rectX + rectWidth), int(rectY + rectHeight)), color, 2 )
        
        # (Optional) Show image:
        cv2.imshow("Bounding Rectangle", inputImageCopy)
        cv2.waitKey(0)
Run Code Online (Sandbox Code Playgroud)

您也可以选择绘制算法找到的边界框。这是生成的图像:

看起来不错。请注意,一些明显的背景像素也包含在ROI. GrabCut将尝试将这些像素重新分类为适当的类别,即“最终背景”。好吧,让我们准备数据GrabCut

# Create mask for Grab n Cut,
# The mask is a uint8 type, same dimensions as
# original input:
mask = np.zeros(inputImage.shape[:2], np.uint8)

# Grab n Cut needs two empty matrices of
# Float type (64 bits) and size 1 (rows) x 65 (columns):
bgModel = np.zeros((1, 65), np.float64)
fgModel = np.zeros((1, 65), np.float64)
Run Code Online (Sandbox Code Playgroud)

我们需要准备三个矩阵/numpy 数组/任何用于在 Java 中表示图像的数据类型。第一个是将存储mask获得的分段的位置。GrabCut该掩码的值从03来表示原始输入上每个像素的类别。和矩阵被算法内部使用来存储前景和背景的统计模型bgModelfgModel请注意,这两个矩阵都是float矩阵。最后GrabCut是迭代算法。它将运行n迭代。好的,让我们运行GrabCut

# Run Grab n Cut on INIT_WITH_RECT mode:
grabCutIterations = 5
mask, bgModel, fgModel = cv2.grabCut(inputImage, mask, maskRect, bgModel, fgModel, grabCutIterations, mode=cv2.GC_INIT_WITH_RECT)
Run Code Online (Sandbox Code Playgroud)

好了,分类完成了。您可以尝试转换mask为(图像)可见类型以检查每个像素的标签。这是可选的,但如果您愿意这样做,您将获得4矩阵。每个班级各一份。例如,对于“最终背景”类,GrabCut发现这些是属于该类的像素(白色):

属于“可能背景”类的像素如下:

这很好,是吧?以下是属于“可能前景”类的像素:

很不错。让我们创建最终的分割掩模,因为它mask不是图像,它只是array每个像素的包含标签。我们将使用确定背景和可能背景像素来设置最终掩模,然后我们可以“标准化”数据范围并将其转换为uint8获得实际图像

# Set all definite background (0) and probable background pixels (2)
# to 0 while definite foreground and probable foreground pixels are
# set to 1
outputMask = np.where((mask == cv2.GC_BGD) | (mask == cv2.GC_PR_BGD), 0, 1)

# Scale the mask from the range [0, 1] to [0, 255]
outputMask = (outputMask * 255).astype("uint8")
Run Code Online (Sandbox Code Playgroud)

这是实际的分割掩码:

好吧,我们可以稍微清理一下这张图像,因为将前景像素错误分类为背景像素会产生一些小孔。让我们应用另一种形态学closing,这次使用5迭代:

# (Optional) Apply a morphological closing with:
# Rectangular SE size 3 x 3 and 5 iterations:
outputMask = morphoOperation(outputMask, 3, 5, "Closing")
Run Code Online (Sandbox Code Playgroud)

最后,将其与原始图像outputMask一起使用以生成最终的分割结果:AND

# Apply a bitwise AND to the image using our mask generated by
# GrabCut to generate the final output image:
segmentedImage = cv2.bitwise_and(inputImage, inputImage, mask=outputMask)

cv2.imshow("Segmented Image", segmentedImage)
cv2.waitKey(0)
Run Code Online (Sandbox Code Playgroud)

这是最终结果:

如果您需要此图像的透明度,则可以非常简单地使用outputMaskas alpha channel。这是我之前使用的辅助函数:

# Applies a morpho operation:
def morphoOperation(binaryImage, kernelSize, opIterations, opString):
    # Get the structuring element:
    morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
    # Perform Operation:
    if opString == "Closing":
        op = cv2.MORPH_CLOSE
    else:
        print("Morpho Operation not defined!")
        return None

    outImage = cv2.morphologyEx(binaryImage, op, morphKernel, None, None, opIterations, cv2.BORDER_REFLECT101)

    return outImage
Run Code Online (Sandbox Code Playgroud)