<p>正如你所看到的,这项任务一点也不琐碎。OpenCV有一个名为<a href="https://docs.opencv.org/master/d8/d83/tutorial_py_grabcut.html" rel="nofollow noreferrer">"GrabCut"</a>的分段算法,试图解决这个特殊问题。该算法非常擅长于对背景和前景像素进行<strong>分类,但是它需要非常具体的信息才能工作。它可以在两种模式下运行:</p>
<ul>
<li><p><strong>第一模式(遮罩模式)</strong>:使用二进制遮罩(与原始输入大小相同),其中100%确定的背景像素被标记,如下所示:
以及100%确定的前景像素。你不必每次都做标记
图像上的像素,只是一个区域,您可以确定算法
将找到任意一类像素</p>
</li>
<li><p><strong>第二模式(前景ROI)</strong>:使用包围100%确定前景像素的边界框</p>
</li>
</ul>
<p>现在,我使用符号“100%确定”来标记那些像素,您100%确定它们对应于前景的背景。该算法将像素分为四类:<strong>“确定背景”</strong>、<strong>“可能背景”</strong>、<strong>“确定前景”</strong>和<strong>“可能前景”</strong>。它将预测可能的背景和可能的前景像素,但它需要一个先验信息,至少在哪里可以找到“确定的前景”像素</p>
<p>也就是说,我们可以在第二种模式(<em>矩形ROI</em>)中使用<code>GrabCut</code>来尝试分割输入图像。我们可以尝试获得输入的第一个粗略的二进制掩码。这将标记我们确信算法可以找到前景像素的位置。我们将把这个粗略的掩码提供给算法,并检查结果。现在,这个方法不容易,自动化也不简单,我们将设置一些手动信息,这些信息对于这个输入图像特别有效。我不知道OpenCV的Java实现,所以我将为您提供Python的解决方案。希望你能移植它。这是该算法的概述:</p>
<ol>
<li>通过阈值化获得前景对象的第一个粗糙遮罩</li>
<li>在粗糙遮罩上检测<strong>轮廓</strong>,以检索<strong>边界矩形</strong></li>
<li>边框将用作<strong>GrabCut</strong>算法的输入ROI</li>
<li>设置GrabCut算法所需的参数</li>
<li><strong>清洁</strong>通过GrabCut获得的分割遮罩</li>
<li>使用分割遮罩最后分割前景对象</li>
</ol>
<p>代码如下:</p>
<pre><code># 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)
</code></pre>
<p>第一步是使用<strong>自适应阈值</strong>获得粗略的前景遮罩。这里,我使用了<code>ADAPTIVE_THRESH_MEAN_C</code>方法,其中(局部)阈值是输入图像上邻域区域的平均值。这将产生以下图像:</p>
<img src="https://i.imgur.com/SoS4rCo.png" width="300"/>
<p>很粗糙,对吧?我们可以用一些形态学来清理一下。我使用<code>Closing</code>和<code>3 x 3</code>大小的<code>rectangular kernel</code>和<code>10</code>迭代来加入白色像素的大斑点。我已经将OpenCV函数包装在自定义函数中,这样可以节省键入某些行的时间。这些助手函数将在本文末尾介绍。目前,该步骤如下所示:</p>
<pre><code># Apply a morphological closing with:
# Rectangular SE size 3 x 3 and 10 iterations
binaryImage = morphoOperation(binaryImage, 3, 10, "Closing")
</code></pre>
<p>这是过滤后的粗糙遮罩:</p>
<img src="https://i.imgur.com/TDZMD7r.png" width="300"/>
<p>好一点。好的,我们现在可以搜索最大轮廓的边界框。通过<code>cv2.RETR_EXTERNAL</code>搜索外部轮廓就足够了,因为我们可以安全地忽略子轮廓,如下所示:</p>
<pre><code># 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 = []
</code></pre>
<p>另外,让我们准备一个<code>list</code>存储目标边界矩形。现在让我们搜索检测到的轮廓。我还实现了一个<strong>区域过滤器</strong>,以防出现噪声,因此忽略低于某个区域阈值的像素:</p>
<pre><code># 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)
</code></pre>
<p>(可选)可以绘制由算法找到的边界框。这是生成的图像:</p>
<img src="https://i.imgur.com/8em0OsB.png" width="300"/>
<p>看起来不错。注意,一些明显的背景像素也被<code>ROI</code>所包围<code>GrabCut</code>将尝试将这些像素重新分类为其适当的类别,即“确定背景”。好的,让我们为<code>GrabCut</code>准备数据:</p>
<pre><code># 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)
</code></pre>
<p>我们需要准备三个矩阵/numpy数组/Java中用于表示图像的任何数据类型。第一个是存储由<code>GrabCut</code>获得的分段<code>mask</code>的位置。此掩码将具有从<code>0</code>到<code>3</code>的值,以表示原始输入上每个像素的类别。算法内部使用<code>bgModel</code>和<code>fgModel</code>矩阵来存储前景和背景的统计模型。请注意,这两个矩阵都是<code>float</code>矩阵。最后,<code>GrabCut</code>是一种迭代算法。它将运行<code>n</code>次迭代。好的,让我们运行<code>GrabCut</code>:</p>
<pre><code># 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)
</code></pre>
<p>好的,分类完成了。您可以尝试将<code>mask</code>转换为(图像)<em>可见类型</em>,以检查每个像素的标签。这是可选的,但是如果您希望这样做,您将得到<code>4</code>矩阵。每节课一个。例如,对于“确定背景”类,<code>GrabCut</code>发现这些是属于此类的像素(白色):</p>
<img src="https://i.imgur.com/Shplroe.png" width="300"/>
<p>属于“可能背景”类别的像素如下:</p>
<img src="https://i.imgur.com/h6RRCpZ.png" width="300"/>
<p>很好,是吗?以下是属于“可能前景”类别的像素:</p>
<img src="https://i.imgur.com/tnzlTFm.png" width="300"/>
<p>非常好。让我们创建最终的分割掩码,因为<code>mask</code>不是图像,它只是一个包含每个像素标签的<code>array</code>。我们将使用确定的背景和可能的背景像素来设置最终遮罩,然后我们可以“规范化”数据范围并将其转换为<code>uint8</code>以获得实际图像</p>
<pre><code># 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")
</code></pre>
<p>这是实际的分段掩码:</p>
<img src="https://i.imgur.com/1wqwC1m.png" width="300"/>
<p>好的,我们可以清理一下这个图像,因为将前景像素误分类为背景像素会产生一些小孔。让我们仅应用另一个形态学<code>closing</code>,这次使用<code>5</code>迭代:</p>
<pre><code># (Optional) Apply a morphological closing with:
# Rectangular SE size 3 x 3 and 5 iterations:
outputMask = morphoOperation(outputMask, 3, 5, "Closing")
</code></pre>
<p>最后,在原始图像的<code>AND</code>中使用此<code>outputMask</code>生成最终分割结果:</p>
<pre><code># 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)
</code></pre>
<p>这是最终结果:</p>
<img src="https://i.imgur.com/mJcoG7c.png" width="300"/>
<p>如果您需要此图像的透明度,可以直接使用<code>outputMask</code>作为<code>alpha channel</code>。这是我前面使用的助手函数:</p>
<pre><code># 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
</code></pre>