OpenCV与AI深度学习 | 使用OpenCV轮廓检测提取图像前景

本文来源公众号“OpenCV与AI深度学习”,仅用于学术分享,侵权删,干货满满。

原文链接:使用OpenCV轮廓检测提取图像前景

背景和介绍

    前景提取是计算机视觉领域中非常流行的任务。使用前景提取方法,我们尝试提取任何我们感兴趣的图像或对象,并丢弃其余的背景。最近基于深度学习的图像分割技术使这变得非常容易。但我们也可以使用纯计算机视觉技术来实现这一点。

    在基于计算机视觉的图像前景提取方面,Grabcut 是最流行的方法之一。在 Grabcut 中,我们提供了一个矩形区域,其中可能存在感兴趣的对象。之后,Grabcut 算法会处理其余部分。

    那么,如果不使用 Grabcut 算法,我们该怎么做呢?

使用OpenCV轮廓检测进行图像前景提取

    简单来说,我们需要找到感兴趣对象的边界区域或像素。之后,我们可以将其视为前景图像,而将其余部分视为背景图像。

    我们可以使用轮廓检测技术来实现这一点。使用轮廓检测,我们可以找到我们想要提取的对象周围的像素,然后继续进行。我们将在本文中详细介绍如何使用 OpenCV 轮廓检测实现图像前景提取。

    不仅如此,我们还将尝试改变结果前景的背景,使事情变得更有趣。所以,你可以期待类似下图的效果。

    在上图中,顶部图像显示原始未经编辑的图像,背景为白色。没有什么特别之处。中间图像显示前景图像。这是我们仅从顶部图像中提取人物时的图像。没问题。您在最底部图像中看到的图像是我们将提取的前景图像与新的彩色背景合并后的图像。

库和依赖项

    对于本教程,我们只需要一个主要库。那就是OpenCV计算机视觉库。

    我使用的是 4.2.0.32 版本。虽然我建议使用与我相同的版本,但如果使用任何 4.x 版本,您仍然不会遇到任何问题。

目录结构

    在本教程中,我们将使用以下目录结构。

│   extract_foreground.py│   utils.py├───input│       background.jpg│       image_1.jpg│       image_2.jpg│       image_3.jpg├───outputs│       ...
    • 在父项目目录中,我们有两个 Python 文件,extract_foreground.py和utils.py。

    • 输入文件夹包含我们将在本教程中使用的输入图像。总共有四张图片。

    • 最后,输出运行 Python 脚本后,文件夹将包含输出图像。

    下载后,只需将文件解压到项目目录中即可。所有图片均取自Pixabay,可免费使用。

https://pixabay.com/

使用OpenCV轮廓检测进行图像前景提取

    从这里开始,我们将在编写代码时深入了解这两个Python文件的细节。

    我们将从utils.py然后进入Python脚本extract_foreground.py文件。

    这里的所有代码都将进入utils.py文件。此 Python 文件包含一些实用函数,我们可以在需要时执行这些函数。我们将这些函数分开,以便我们的代码尽可能保持干净和易读。

    下面的代码块包含实用函数所需的两个导入。

import cv2
import numpy as np

    查找最大轮廓的函数

    我们要编写的第一个函数是找到图像中最大的轮廓区域。

    这查找find_largest_contour()函数接受二值图像,找出图像中的所有轮廓,并返回最大的轮廓面积。

def find_largest_contour(image):
    """
    This function finds all the contours in an image and return the largest
    contour area.
    :param image: a binary image
    """
    image = image.astype(np.uint8)
    contours, hierarchy = cv2.findContours(
        image,
        cv2.RETR_TREE,
        cv2.CHAIN_APPROX_SIMPLE
    )
    largest_contour = max(contours, key=cv2.contourArea)
    return largest_contour

显示OpenCV图像的函数

    我们可能需要在extract_foreground.py文件。而不是执行 OpenCV 的imshow()和waitKey()几次,我们可以定义一个函数,只用一行代码来处理可视化。

def show(name, image):
    """
    A simple function to visualize OpenCV images on screen.
    :param name: a string signifying the imshow() window name
    :param image: NumPy image to show 
    """
    cv2.imshow(name, image)
    cv2.waitKey(0)

    每当我们想要可视化图像时,我们都会调用show()函数,同时传递窗口名称字符串和图像数组作为参数。每次至少可以减少一行代码。

将新背景应用到提取的前景图像的函数

    现在,您已经在图中看到了我们如何向提取的前景图像添加新背景。我们可能不想对每个前景图像都这样做。因此,我们将为此编写一个函数。每当我们想要将新背景应用于提取的前景图像时,我们都会调用该函数。

def apply_new_background(mask3d, foreground, save_name):
    """
    This function applies a new background to the extracted foreground image
    if `--new-background` flag is `True` while executing the file.
    :param mask3d: mask3d mask containing the foreground binary pixels
    :param foreground: mask containg the extracted foreground image
    :param save_name: name of the input image file
    """
    # normalization of mask3d mask, keeping values between 0 and 1
    mask3d = mask3d / 255.0
    # get the scaled product by multiplying
    foreground = cv2.multiply(mask3d, foreground)
    # read the new background image
    background = cv2.imread('input/background.jpg')
    # resize it according to the foreground image
    background = cv2.resize(background, (foreground.shape[1], foreground.shape[0]))
    background = background.astype(np.float)
    # get the scaled product by multiplying
    background = cv2.multiply(1.0 - mask3d, background)
    # add the foreground and new background image
    new_image = cv2.add(foreground, background)
    show('New image', new_image.astype(np.uint8))
    cv2.imwrite(f"outputs/{save_name}_new_background.jpg", new_image)

    这应用新背景()函数接受三个参数。一个是mask3d,即前景图像蒙版。前景参数是提取的前景对象(RGB 格式)。保存名称是字符串,我们将用它将新图像保存到磁盘。

    第一步是实现正常化mask3d并得到缩放后的图像mask3d和前景使用cv2.multiply(第 34 和 36 行)。

    然后我们读取背景图像,调整其大小以匹配前景图像的形状,并转换其数据类型以进行进一步的操作。

    在第 43 行,我们再次使用cv2.multiply得到缩放后的产品1-mask3d和新的背景。

    然后我们通过添加前景和背景图像来获得带有背景的新图像。

    最后,我们在屏幕上显示图像并将其保存到磁盘。

    我们已经完成了所需的所有实用函数。现在我们可以继续编写使用 OpenCV 轮廓检测进行图像前景提取的代码。

使用 OpenCV 轮廓检测进行图像前景提取的代码

    接下来,我们将在extract_foreground.py文件。此 Python 文件将包含我们使用 OpenCV 轮廓检测方法提取前景图像/对象所需的所有代码。

    让我们开始导入我们需要的所有模块和库。

import numpy as np
import cv2
import argparse
from utils import show, apply_new_background, find_largest_contour

    我们正在导入所有函数utils.py我们在上一节中已经介绍过了。

    现在,让我们定义参数解析器来解析命令行参数。

# define the argument parser
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', help='path to the input image',
                    required=True)
parser.add_argument('-n', '--new-background', dest='new_background',
                    action='store_true')
args = vars(parser.parse_args())

    上面的代码块中有两个标志。

    -input是我们在执行代码时提供的输入图像文件的路径。

    -new-background确定是否将新的背景图像应用于提取的前景。默认情况下,它将值存储为错误的执行代码时,如果我们传递-n或者--新背景然后我们才会调用函数将新的背景应用到提取的前景图像。

读取图像并转换为二值图像

    现在,我们将读取要从中提取前景对象的图像。我们还将应用阈值处理将其转换为仅包含黑色和白色像素的二值图像。

image = cv2.imread(args['input'])
show('Input image', image)
# blur the image to smmooth out the edges a bit, also reduces a bit of noise
blurred = cv2.GaussianBlur(image, (5, 5), 0)
# convert the image to grayscale 
gray = cv2.cvtColor(blurred, cv2.COLOR_BGR2GRAY)
# apply thresholding to conver the image to binary format
# after this operation all the pixels below 200 value will be 0...
# and all th pixels above 200 will be 255
ret, gray = cv2.threshold(gray, 200 , 255, cv2.CHAIN_APPROX_NONE)

    读取图像后,我们应用高斯模糊来平滑边缘。这还可以消除背景中非常小的噪音。然后我们将图像转换为灰度格式并应用阈值将其转换为二进制图像。

找到最大的轮廓面积

    由于我们已将图像转换为二进制格式,因此我们可以轻松找到图像中的所有轮廓。

# find the largest contour area in the image
contour = find_largest_contour(gray)
image_contour = np.copy(image)
cv2.drawContours(image_contour, [contour], 0, (0, 255, 0), 2, cv2.LINE_AA, maxLevel=1)
show('Contour', image_contour)

    我们称之为find_largest_contour()在第 24 行,同时将二进制图像作为参数传递。该函数返回最大的轮廓区域。然后我们创建原始图像的副本并将该轮廓区域应用于图像。我们用绿色标记所有像素,以完美地可视化轮廓区域。我们将在执行代码时看到此输出。

创建蒙版并标记确定和可能的像素

    要进行任何进一步的操作,我们首先必须创建一个新的蒙版(黑色背景)。这将具有与灰度图像相同的大小。由于我们尚未调整图像大小,这意味着此蒙版将与原始图像的大小相同。

    我们先看一下接下来几个操作的代码,然后进入解释部分。

# create a black `mask` the same size as the original grayscale image 
mask = np.zeros_like(gray)
# fill the new mask with the shape of the largest contour
# all the pixels inside that area will be white 
cv2.fillPoly(mask, [contour], 255)
# create a copy of the current mask
res_mask = np.copy(mask)
res_mask[mask == 0] = cv2.GC_BGD # obvious background pixels
res_mask[mask == 255] = cv2.GC_PR_BGD # probable background pixels
res_mask[mask == 255] = cv2.GC_FGD # obvious foreground pixels

    首先,我们在第 29 行创建上面讨论的掩码。

    在第 32 行,我们用白色像素填充创建的蒙版上的一个区域,该区域的形状将与我们迄今为止获得的最大轮廓的形状相同。例如,如果最大的轮廓区域是人的,那么我们在新蒙版上创建该形状并用白色像素填充该区域。

    接下来的几行很重要。第 35 行创建了面具以免编辑原始蒙版。

    在创建新蒙版时,我们将所有像素值都设为零。这意味着蒙版全是黑色。然后我们用白色轮廓形状填充它,将所有像素标记为 255。这意味着我们确切地知道所有黑色像素构成背景,所有白色像素构成前景或对象。

    因此,在第 36 行,我们说任何值为 0 的像素肯定是背景像素。我们使用cv2. GC_BGD。

    第 37 行表示,任何值为 255 的像素都可能是前景。我们使用cv2. GC_PR_BGD。

    但由于所有像素都是 0 或 255,我们确信值为 255 的像素肯定是前景。因此,我们在第 38 行也使用以下代码标记了明显的前景:cv2. GC_FGD。

    执行上述步骤非常重要,否则,新掩码上的任何未来处理都将无法正常进行。

使用已知的前景和背景像素创建最终蒙版

    现在,我们知道哪些像素肯定是背景,哪些像素可能是前景,哪些像素肯定是前景。利用这些知识,我们将创建最终的二进制掩码。

    以下代码块包含该代码。

# create a mask for obvious and probable foreground pixels
# all the obvious foreground pixels will be white and...
# ... all the probable foreground pixels will be black
mask2 = np.where(
    (res_mask == cv2.GC_FGD) | (res_mask == cv2.GC_PR_FGD),
    255,
    0
).astype('uint8')

    在缓冲区掩码,我们已经标记了明显且可能的前景像素。因此,在创建新的掩码2,无论哪个像素肯定是前景缓冲区掩码填充值为 255。并且任何像素都是可能的前景缓冲区掩码用 0 值填充掩码2最终,我们将整个新掩码2转换为 8 位无符号整数格式。最后,上述步骤为我们提供了一个二进制掩码(二维),其中所有像素均为黑色或白色。

    目前,想象一切可能有点困难。执行代码时一切都会清楚。

使mask三维化并获取最终的前景图像

    现在,mask2也是二进制和二维的。但如果我们想在未来将它与彩色图像(三维)一起用于任何操作,那么我们将无法以当前形式进行操作。因此,我们将创建一个最终的mask2它将是三维的。

# create `new_mask3d` from `mask2` but with 3 dimensions instead of 2
new_mask3d = np.repeat(mask2[:, :, np.newaxis], 3, axis=2)
mask3d = new_mask3d
mask3d[new_mask3d > 0] = 255.0
mask3d[mask3d > 255] = 255.0
# apply Gaussian blurring to smoothen out the edges a bit
# `mask3d` is the final foreground mask (not extracted foreground image)
mask3d = cv2.GaussianBlur(mask3d, (5, 5), 0)
show('Foreground mask', mask3d)

    使用mask2,我们创建一个new_mask3d最后再增加一个维度来复制 3D 图像。然后mask3d成为我们最终的蒙版,我们在第 50 行和第 51 行对其进行像素级操作。在第 54 行,我们对最终的 3D 蒙版应用高斯模糊,使边缘更平滑一些。

    现在,让我们得到最终的前景图像。

# create the foreground image by zeroing out the pixels where `mask2`...
# ... has black pixels
foreground = np.copy(image).astype(float)
foreground[mask2 == 0] = 0
show('Foreground', foreground.astype(np.uint8))

    在第 58 行,我们创建原始图像的副本并将其保存为前景. 然后,掩码2为零,我们让它们在前景也是。它们是我们不需要的背景像素。我们在第 59 行执行此操作。我们有最终的前景图像。这意味着我们已成功使用 OpenCV 轮廓检测进行图像前景提取。

    只剩下几个步骤了。首先是保存所有前景图像、最终的 3D 蒙版以及检测到轮廓的图像。

# save the images to disk
save_name = args['input'].split('/')[-1].split('.')[0]
cv2.imwrite(f"outputs/{save_name}_foreground.png", foreground)
cv2.imwrite(f"outputs/{save_name}_foreground_mask.png", mask3d)
cv2.imwrite(f"outputs/{save_name}_contour.png", image_contour)

    如果你还记得的话,我们讨论过在前景图像上应用新的背景,如果--新背景国旗是真的。我们已经在utils.py,对于我们来说现在只需要两行代码。

# the `--new-background` flag is `True`, then apply the new background...
# ... to the extracted foreground image
if args['new_background']:
    apply_new_background(mask3d, foreground, save_name)

    这标志着使用 OpenCV 轮廓检测进行前景提取的编码结束。下一步是执行代码并分析输出。

执行代码并分析输出

    现在是时候看看执行代码后我们会得到什么结果了。

    我希望你已经下载了输入图像。我们将从图片3.jpg在输入文件夹。

python extract_foreground.py --input input / image_3.jpg --new -background

写在最后

    我们在上一步中了解了使用 OpenCV 轮廓检测进行图像前景提取的局限性。但也有一些方法可以克服这个问题。

    在应用轮廓检测之前使用良好的边缘检测技术。

    使用 Grabcut 算法并按照预期的步骤进行图像前景提取。

    我们可以使用深度学习分割技术来提取选择的对象。

    也可以参考下面两篇文章:

    背景去除:传统方法与深度学习方法(演示与对比)

    就这么丝滑!三行代码实现换底色和背景

THE END !

文章结束,感谢阅读。您的点赞,收藏,评论是我继续更新的动力。大家有推荐的公众号可以评论区留言,共同学习,一起进步。

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值