在Android中使用OpenCV从图像中删除背景

2024-09-30 14:28:40 发布

您现在位置:Python中文网/ 问答频道 /正文

我想删除Android中打开CV的图像背景。代码运行良好,但输出质量不符合预期。我遵循java文档进行代码参考:

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

谢谢

原始图像 Original image

我的输出 Result 预期产量 Expected Output

我在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
}

Tags: theimagetypevalframecvforegroundmat
2条回答

代码

import cv2
import numpy as np

def process(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_canny = cv2.Canny(img_gray, 10, 20)
    kernel = np.ones((13, 13))
    img_dilate = cv2.dilate(img_canny, kernel, iterations=1)
    return cv2.erode(img_dilate, kernel, iterations=1)
    
def get_mask(img):
    contours, _ = cv2.findContours(process(img), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    blank = np.zeros(img.shape[:2]).astype('uint8')
    for cnt in contours:
        if cv2.contourArea(cnt) > 500:
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, peri * 0.004, True)
            cv2.drawContours(blank, [approx], -1, 255, -1) 
    return blank

img = cv2.imread("crystal.jpg")
img_masked = cv2.bitwise_and(img, img, mask=get_mask(img))

cv2.imshow("Masked", img_masked)
cv2.waitKey(0)

输出

enter image description here

解释

  1. 导入必要的库:
import cv2
import numpy as np
  1. 定义一个函数来处理图像,使其适合于正确的轮廓检测。在函数中,首先将图像转换为灰度,然后使用canny边缘检测器检测其边缘。检测到边缘后,我们可以对其进行一次放大和腐蚀,以使边缘更加完整:
def process(img):
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    img_canny = cv2.Canny(img_gray, 10, 20)
    kernel = np.ones((13, 13))
    img_dilate = cv2.dilate(img_canny, kernel, iterations=1)
    return cv2.erode(img_dilate, kernel, iterations=1)
  1. 定义一个函数以生成图像的遮罩。在找到图像的轮廓后,用图像的形状定义灰度空白图像,绘制每个轮廓Em>(大于^ {CD1> }的区域以滤除噪声)< EEM >填充到空白图像上。我还对轮廓进行了近似处理,以稍微平滑一些:
def get_mask(img):
    contours, _ = cv2.findContours(process(img), cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
    blank = np.zeros(img.shape[:2]).astype('uint8')
    for cnt in contours:
        if cv2.contourArea(cnt) > 500:
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, peri * 0.004, True)
            cv2.drawContours(blank, [approx], -1, 255, -1) 
    return blank
  1. 最后,读入图像,并使用cv2.bitwise_and方法以及我们定义的get_mask函数屏蔽图像,该函数使用我们定义的process函数。最后显示蒙版图像:
img = cv2.imread("crystal.jpg")
img_masked = cv2.bitwise_and(img, img, mask=get_mask(img))

cv2.imshow("Masked", img_masked)
cv2.waitKey(0)

透明背景

您可以使用cv2.merge方法代替cv2.bitwise_and方法:

img = cv2.imread("crystal.jpg")
img_masked = cv2.merge(cv2.split(img) + [get_mask(img)])
cv2.imwrite("masked_crystal.png", img_masked)

生成的图像(屏幕截图):

enter image description here

说明:

  1. 请记住,我们已经将cv2模块和numpy模块作为np导入。我们还定义了一个process函数和一个get_mask函数,我们可以在图像中读取:
img = cv2.imread("crystal.jpg")
  1. cv2.split方法接收图像数组并返回图像中存在的每个通道的列表。在我们的例子中,我们只有3个通道,为了使图像透明,我们需要第四个通道:alpha通道。cv2.merge方法的作用与cv2.split相反;它接收单个通道的列表,并返回带有通道的图像数组。接下来,我们在列表中获取图像的bgr通道,并将图像的掩码连接为alpha通道:
img_masked = cv2.merge(cv2.split(img) + [get_mask(img)])
  1. 最后,我们可以将四通道图像写入文件:
cv2.imwrite("masked_crystal.png", img_masked)

下面是cv2.merge方法的更多示例:Python cv2.merge() Examples

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

  • 第一模式(遮罩模式):使用二进制遮罩(与原始输入大小相同),其中100%确定的背景像素被标记,如下所示: 以及100%确定的前景像素。你不必每次都做标记 图像上的像素,只是一个区域,您可以确定算法 将找到任意一类像素

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

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

也就是说,我们可以在第二种模式(矩形ROI)中使用GrabCut来尝试分割输入图像。我们可以尝试获得输入的第一个粗略的二进制掩码。这将标记我们确信算法可以找到前景像素的位置。我们将把这个粗略的掩码提供给算法,并检查结果。现在,这个方法不容易,自动化也不简单,我们将设置一些手动信息,这些信息对于这个输入图像特别有效。我不知道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)

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

很粗糙,对吧?我们可以用一些形态学来清理一下。我使用Closing3 x 3大小的rectangular kernel10迭代来加入白色像素的大斑点。我已经将OpenCV函数包装在自定义函数中,这样可以节省键入某些行的时间。这些助手函数将在本文末尾介绍。目前,该步骤如下所示:

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

这是过滤后的粗糙遮罩:

好一点。好的,我们现在可以搜索最大轮廓的边界框。通过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 = []

另外,让我们准备一个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)

(可选)可以绘制由算法找到的边界框。这是生成的图像:

看起来不错。注意,一些明显的背景像素也被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)

我们需要准备三个矩阵/numpy数组/Java中用于表示图像的任何数据类型。第一个是存储由GrabCut获得的分段mask的位置。此掩码将具有从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)

好的,分类完成了。您可以尝试将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")

这是实际的分段掩码:

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

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

最后,在原始图像的AND中使用此outputMask生成最终分割结果:

# 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)

这是最终结果:

如果您需要此图像的透明度,可以直接使用outputMask作为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

相关问题 更多 >