51c视觉~CV~合集4
前景提取是计算机视觉领域中非常流行的任务。使用前景提取方法,我们尝试提取任何我们感兴趣的图像或对象,并丢弃其余的背景。最近基于深度学习的图像分割技术使这变得非常容易。但我们也可以使用纯计算机视觉技术来实现这一点。在基于计算机视觉的图像前景提取方面,Grabcut 是最流行的方法之一。在 Grabcut 中,我们提供了一个矩形区域,其中可能存在感兴趣的对象。之后,Grabcut 算法会处理其余部分。
我自己的原文哦~ https://blog.51cto.com/whaosoft/11720595
一、轮廓检测提取图像前景
背景和介绍
前景提取是计算机视觉领域中非常流行的任务。使用前景提取方法,我们尝试提取任何我们感兴趣的图像或对象,并丢弃其余的背景。最近基于深度学习的图像分割技术使这变得非常容易。但我们也可以使用纯计算机视觉技术来实现这一点。
在基于计算机视觉的图像前景提取方面,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',
actinotallow='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 算法并按照预期的步骤进行图像前景提取。
我们可以使用深度学习分割技术来提取选择的对象。
二、图像修复技术去除眩光
眩光是一种因过度和不受控制的亮度而引起的视觉感觉。眩光可能会使人丧失能力或只是让人感到不舒服。眩光是一种主观感受,对眩光的敏感度可能有很大差异。老年人通常对眩光更敏感,这是由于眼睛的老化特性。
首先,我们需要检测眩光存在的位置。我们可以使用全局二值化轻松识别它们,因为当眩光通常存在时,该像素值大于 180。使用它可以检测到眩光。因此,我们需要获取大于 180 的像素,然后进行移除部分。
下面的函数用于获取图像的蒙版,其中当像素大于 180 且低于其黑色时,像素为白色。我们将图像的实际眩光位置设为白色,而其他地方设为黑色。
def create_mask(image):
gray = cv2.cvtColor( image, cv2.COLOR_BGR2GRAY )
blurred = cv2.GaussianBlur( gray, (9,9), 0 )
_,thresh_img = cv2.threshold( blurred, 180, 255, cv2.THRESH_BINARY)
thresh_img = cv2.erode( thresh_img, None, iteratinotallow=2 )
thresh_img = cv2.dilate( thresh_img, None, iteratinotallow=4 )
# perform a connected component analysis on the thresholded image,
# then initialize a mask to store only the "large" components
labels = measure.label( thresh_img, neighbors=8, background=0 )
mask = np.zeros( thresh_img.shape, dtype="uint8" )
# loop over the unique components
for label in np.unique( labels ):
# if this is the background label, ignore it
if label == 0:
continue
# otherwise, construct the label mask and count the
# number of pixels
labelMask = np.zeros( thresh_img.shape, dtype="uint8" )
labelMask[labels == label] = 255
numPixels = cv2.countNonZero( labelMask )
# if the number of pixels in the component is sufficiently
# large, then add it to our mask of "large blobs"
if numPixels > 300:
mask = cv2.add( mask, labelMask )
return mask
我们在这个函数中所做的是,我们首先将图像转换为灰度,使用高斯矩阵(9x9)模糊图像以减少噪音。在全局阈值方法中将阈值设置为 180,将模糊图像转换为二进制图像,其中像素值高于 180 为白色,其他为黑色。我们可能会有小块噪音;为此,我们对二进制图像进行了一系列侵蚀和扩张。
经过这种膨胀、腐蚀之后,我们的图像可能会出现小噪音。为此,我们对阈值图像进行了连通分量分析。scikit-image 库的 measure.labels 方法用于连通分量分析。使用 np.zeros 方法创建一个新的黑色图像,其形状与二值图像完全相同。它被称为掩码。
我们开始循环遍历每个唯一标签。如果标签为零,那么我们知道我们正在检查背景区域,并且可以安全地忽略它。否则,我们只为当前区域构建一个掩码。然后计算 labelMask 中非零像素的数量。如果 numPixels 超过预定义的阈值(在本例中,总共300 个像素),那么我们认为该 blob“足够大”并将其添加到我们的掩码中。这种检测方法的灵感来自这里。作者在那里很好地解释了这种方法。
所以我们的面具会像下面这样:
我们发现了图像中的眩光/明亮之处。我们可以使用各种方法去除这些斑点。
修复方法:
CLAHE 方法
OpenCV的修复方法
在图像预处理中,用不同方法填充图像的某些区域称为修复。基本上,修复就是填补空白。
那么我们可以在 python OpenCV 中使用哪些方法来填充它呢?您可以使用Naiver-Stokes 方法或 Fast — Marching 方法进行填充。
Naiver-Stokes 方法
可以使用偏微分方程更新区域的图像强度,并且可以通过图像拉普拉斯算子计算图像的平滑度(拉普拉斯算子是图像的二阶空间导数的二维各向同性度量。图像的拉普拉斯算子突出显示强度变化迅速的区域,因此经常用于边缘检测(参见零交叉边缘检测器)。拉普拉斯算子通常应用于首先用近似高斯平滑滤波器进行平滑的图像,以降低其对噪声的敏感性,因此这里将一起描述这两个变体。运算符通常将单个灰度图像作为输入并产生另一个灰度图像作为输出)
拉普拉斯算子和偏微分方程可用于保留边缘并继续在平滑区域传播颜色信息。这是进行图像修复的方法之一。
https://www.math.ucla.edu/~bertozzi/papers/cvpr01.pdf
快速行进法
像素已知图像邻域的加权平均值用于修复图像平滑度。已知邻域像素和梯度用于估计要修复的像素的颜色。
https://www.semanticscholar.org/paper/An-Image-Inpainting-Technique-Based-on-the-Fast-Telea/67d0cb47d14150daff08980efbea9f1267d3a4e5
我们可以使用上述任何一种算法来修复。
如何在 OpenCV python 中使用:
dst = cv2.inpaint( src, inpaintMask,inpaintRadius,flags)
- src → 输入的眩光图像
- inpaintMask → 指示要修复的像素的二进制掩码。
- dst → 输出图像
- inpaintRadius → 要修复的像素周围的邻域。
- flags → INPAINT_NS,(基于 Navier-Stokes 的方法) 或 INPAINT_TELEA (基于快速行进的方法)
当我们选择 inpaintRadius 时,如果要修复的区域很薄,则较小的值会产生更好的效果(更少模糊)。
让我们将其应用到我们的图像中:
参考链接:
https://dsp.stackexchange.com/questions/1215/how-to-remove-a-glare-clipped-brig
三、半自动标注工具
手教你用Python和OpenCV搭建一个半自动标注工具
样本标注是深度学习项目中最关键的部分,甚至在模型学习效果上起决定性作用。但是,标注工作往往非常繁琐且耗时。一种解决方案是使用自动图像标注工具,它可以大大减少标注的时间。
本文主要介绍的半自动标注工具为pyOpenAnnotate,此工具是基于Python和OpenCV实现,最新版本为0.4.0,可通过下面指令安装使用:
pip install pyOpenAnnotate
详细介绍与使用步骤参考链接:
https://pypi.org/project/pyOpenAnnotate/

标注效果:



效果如上图所示,标注完成后可以生成标注文件,后面部分将详细介绍其实现步骤。
实现步骤
实现原理流程:

说明:
【1】Threshold(二值化)只接受单通道图像,但这里并不是直接使用灰度转换图来处理,而是从灰度图、R、G、B、H、S、V通道图像中找到对比度最高的图像来做二值化。
【2】二值化之后并不能保证总是得到我们需要的掩码,有时会有噪声、斑点、边缘的干扰,所以加入了膨胀、腐蚀等形态学处理。
【3】最后通过轮廓分析得到对象的边界框,也就是左上角和右下角坐标。
代码讲解与演示
首先需要导入所需库:
import cv2
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['image.cmap'] = 'gray'
加载图像:
stags = cv2.imread('stags.jpg')
boars = cv2.imread('boar.jpg')
berries = cv2.imread('strawberries.jpg')
fishes = cv2.imread('fishes.jpg')
coins = cv2.imread('coins.png')
boxes = cv2.imread('boxes2.jpg')
选择色彩空间(这里添加了 RGB和HSV,存储在字典中,方便验证使用):
defselect_colorsp(img, colorsp='gray'):
# Convert to grayscale.
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Split BGR.
red, green, blue = cv2.split(img)
# Convert to HSV.
im_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Split HSV.
hue, sat, val = cv2.split(im_hsv)
# Store channels in a dict.
channels = {'gray':gray, 'red':red, 'green':green,
'blue':blue, 'hue':hue, 'sat':sat, 'val':val}
return channels[colorsp]
显示 1×2 图像的实用函数(display()函数接受两个图像并并排绘制。可选参数是绘图的标题和图形大小):
defdisplay(im_left, im_right, name_l='Left', name_r='Right', figsize=(10,7)):
# Flip channels for display if RGB as matplotlib requires RGB.
im_l_dis = im_left[...,::-1] if len(im_left.shape) > 2else im_left
im_r_dis = im_right[...,::-1] if len(im_right.shape) > 2else im_right
plt.figure(figsize=figsize)
plt.subplot(121); plt.imshow(im_l_dis);
plt.title(name_l); plt.axis(False);
plt.subplot(122); plt.imshow(im_r_dis);
plt.title(name_r); plt.axis(False);
阈值处理(thresh()函数接受1通道灰度图像,默认阈值设置为 127。执行逆阈值处理,方便轮廓分析,它返回单通道阈值图像):
defthreshold(img, thresh=127, mode='inverse'):
im = img.copy()
if mode == 'direct':
thresh_mode = cv2.THRESH_BINARY
else:
thresh_mode = cv2.THRESH_BINARY_INV
ret, thresh = cv2.threshold(im, thresh, 255, thresh_mode)
return thresh
实例:雄鹿红外图像标注
整体实现步骤:
【1】选择色彩空间
# Select colorspace.
gray_stags = select_colorsp(stags)
# Perform thresholding.
thresh_stags = threshold(gray_stags, thresh=110)
# Display.
display(stags,thresh_stags,
name_l='Stags original infrared',
name_r='Thresholded Stags',
figsize=(20,14))
【2】执行阈值

【3】执行形态学操作
defmorph_op(img, mode='open', ksize=5, iteratinotallow=1):
im = img.copy()
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(ksize, ksize))
ifmode == 'open':
morphed = cv2.morphologyEx(im, cv2.MORPH_OPEN, kernel)
elifmode == 'close':
morphed = cv2.morphologyEx(im, cv2.MORPH_CLOSE, kernel)
elifmode == 'erode':
morphed = cv2.erode(im, kernel)
else:
morphed = cv2.dilate(im, kernel)
returnmorphed
# Perform morphological operation.
morphed_stags = morph_op(thresh_stags)
# Display.
display(thresh_stags,morphed_stags,
name_l='Thresholded Stags',
name_r='Morphological Operations Result',
figsize=(20,14))

【4】轮廓分析以找到边界框
bboxes = get_bboxes(morphed_stags)
ann_morphed_stags = draw_annotations(stags, bboxes, thickness=5, color=(0,0,255))
# Display.
display(ann_stags,ann_morphed_stags,
name_l='Annotating Thresholded Stags',
name_r='Annotating Morphed Stags',
figsize=(20,14))

【5】过滤不需要的轮廓
defget_filtered_bboxes(img, min_area_ratio=0.001):
contours,hierarchy = cv2.findContours(img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# Sort the contours according to area, larger to smaller.
sorted_cnt = sorted(contours, key=cv2.contourArea, reverse = True)
# Remove max area, outermost contour.
sorted_cnt.remove(sorted_cnt[0])
# Container to store filtered bboxes.
bboxes = []
# Image area.
im_area = img.shape[0] * img.shape[1]
forcnt in sorted_cnt:
x,y,w,h = cv2.boundingRect(cnt)
cnt_area = w * h
# Remove very small detections.
ifcnt_area > min_area_ratio * im_area:
bboxes.append((x,y, x+w, y+h))
returnbboxes
【6】绘制边界框
bboxes = get_filtered_bboxes(thresh_stags, min_area_ratio=0.001)
filtered_ann_stags = draw_annotations(stags, bboxes, thickness=5, color=(0,0,255))
# Display.
display(ann_stags,filtered_ann_stags,
name_l='Annotating Thresholded Stags',
name_r='Annotation After Filtering Smaller Boxes',
figsize=(20,14))

视频标注:

【7】以需要的格式保存
Pascal VOC、YOLO和COCO 是对象检测中使用的三种流行注释格式。让我们研究一下它们的结构。
I. Pascal VOC 以 XML 格式存储注释

II. YOLO标注结果保存在文本文件中。对于每个边界框,它看起来如下所示。这些值相对于图像的高度和宽度进行了归一化。
0 0.0123 0.2345 0.123 0.754
<object-class> <x_centre_norm> <y_centre_norm> <box_width_norm> <box_height_norm>
让边界框的左上角和右下角坐标表示为(x1, y1)和(x2, y2)。然后:

III. MS COCO

这里以YOLO Darknet保存格式为例(当然,你可以保存其他格式):
def save_annotations(img, bboxes):
img_height = img.shape[0]
img_width = img.shape[1]
withopen('image.txt', 'w') as f:
for box in boxes:
x1, y1 = box[0], box[1]
x2, y2 = box[2], box[3]
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
width = x2 - x1
height = y2 - y1
x_centre, y_centre = int(width/2), int(height/2)
norm_xc = x_centre/img_width
norm_yc = y_centre/img_height
norm_width = width/img_width
norm_height = height/img_height
yolo_annotations = ['0', ' ' + str(norm_xc),
' ' + str(norm_yc),
' ' + str(norm_width),
' ' + str(norm_height), '\n']
f.writelines(yolo_annotations)
标注结果显示与保存:


简单演示:

四、CV传统方法实现密集圆形分割与计数
本文主要介绍基于OpenCV传统方法实现密集圆形分割与计数应用,并给详细步骤和代码。
背景介绍
实例图片来源于网络,目标是分割下图中圆形目标并计数。

本文实现效果如下:

实现步骤
【1】灰度转换 + 均值滤波 + 二值化,得到参考背景
img = cv2.imread('src.jpg')
cv2.imshow("src",img)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
cv2.imshow("gray",gray)
blur = cv2.medianBlur(gray,7)
cv2.imshow("blur",blur)
_,thres = cv2.threshold(gray, 199, 255, cv2.THRESH_BINARY_INV )
cv2.imshow("thresh",thres)

【2】对灰度图做拉普拉斯变换,提取边缘,并做阈值分割
lap =cv2.Laplacian(gray, -1, ksize = 5)
cv2.imshow("laplacian",lap)
_,lap_thres = cv2.threshold(lap, 250, 255, cv2.THRESH_BINARY)
cv2.imshow("lap_thres",lap_thres)

【3】将上图做膨胀操作,增粗边缘
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(3,3))
dilation = cv2.dilate(lap_thres,kernel,iterations = 1)
cv2.imshow("dilation",dilation)

【4】将第【1】步中的二值图与上图做差,腐蚀去除噪点,凸显圆形内部区域:
diff = thres - dilation
erode = cv2.erode(diff,kernel,iterations = 1)
cv2.imshow("diff",erode)

【5】轮廓分析:获取最小外接圆和轮廓面积,筛选轮廓面积/圆面积>0.2的有效轮廓,绘制外接圆标注,并计数。
contours,hierarchy = cv2.findContours(erode, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
count = 0
for i in range(0,len(contours)):
center,radius = cv2.minEnclosingCircle(contours[i])
if radius > 10:
area = cv2.contourArea(contours[i])
if area / (math.pi * radius * radius) > 0.2:
count += 1
cv2.circle(img,(int(center[0]),int(center[1])),int(radius),colors[i%9],-1)
strCount = 'count=%d'%count
cv2.putText(img,strCount,(10,100),0,2,(255,255,0),3)
最终结果如下:


总 结
本例中核心思想是用目标前景区域(二值化得到)和边缘区域(拉普拉斯变化得到,不用Canny)做差得到圆内部区域轮廓,然后做后续处理。当然也可以使用距离变换 + 分水岭方法来实现,有兴趣的话可以自己尝试一下。
五、OCR识别票据原理
光学字符识别技术(OCR)目前被广泛利用在手写识别、打印识别及文本图像识别等相关领域。小到文档识别、银行卡身份证识别,大到广告、海报。因为OCR技术的发明,极大简化了我们处理数据的方式。
同时,机器学习(ML)和卷积神经网络(CNN)的快速发展也让文本识别出现了巨大的飞跃!我们在本文的研究中也将使用卷积神经网络CNN技术来识别零售店的纸质票据。为了方便演示,我们本次将仅采用俄语版的票据进行测试。
我们的目标是项目开发一个客户端来识别来获取相关文档,在有服务器端去识别解析数据。准备好了吗?让我们一起去看看怎么做吧!
预处理
首先,我们需要接收图像相关数据,使其水平竖直方向垂直,接下来使用算法进行检测是否为票据,最终二值化方便识别。
旋转图像识别收据
我们有三种方案来识别票据,下文对这三种方案做了测试。
1. 高阈值的自适应二值化技术。2. 卷积神经网络(CNN)。3. Haar特征分类器。
自适应二值化技术

首先,我们看到,图中图像上包含了完整的数据,同时票据又与背景有些差距。为了能更好识别相关数据,我们需要将图片进行旋转。使其水平沿竖直方向对齐。

我们使用Opencv中的自适应阈值化函数adaptive_threshold和scikit-image框架来调整收据数据。利用这两项函数,我们可以在高梯度区域保留白色像素,低梯度区域保留黑色像素。这使得我们获得了一个高反差的样本图片。这样,通过裁剪,我们就能得到票据的相关信息了。

使用卷积神经网络(CNN)
起初我们决定使用CNN来做相关位置检测的接收点,就像我们之前做对象检测项目一样。我们使用判断角度来拾取相关关键点。这种方案虽然好用,但是和高阈值对比检测裁剪更差。
因为CNN只能找到文本的角度坐标,而文字的角度变化很大,这就意味着CNN模型不是很精准。详情请参考下面CNN测试的结果。

使用Haar特征分类器来识别收据
作为第三种选择,我们尝试使用Haar特征分类器来做分类筛选。然而经过一周的分类训练和改变相关参数,我们并没有得到什么比较积极的结果,甚至发现CNN都比Haar表现好得多。

二值化
最终我们使用opencv中的adaptive_threshold方法进行二值化,经过二值化处理,我们得到了一个不错的图片。

文本检测
接下来我们来介绍几个不同的文本检测组件。
通过链接组件检测文本
首先,我们使用Opencv中的find Contours函数找到链接的文本组。大多数链接的组件是字符,但是也有二值化留下来嘈杂的文本,这里我们通过设置阈值的大小来过滤相关文本。
然后,我们执行合成算法来合成字符,如:Й和=。通过搜索最临近的字符组合合成单词。这种算法需要你找到每个相关字字母最临近的字符,然后从若干字母中找到最佳选择展示。

接下来文字形成文字行。我们通过判断文字是否高度一致来判断文本是否属于同一行。

当然,这个方案的缺点是不能识别有噪声的文本。
使用网格对文本进行检测
我们发现几乎所有票据都是相同宽度的文本,所以我们设法在收据上画出一个网格,并利用网格分割每个字符:

网格一下子精简了票据识别的难度。神经网络可以精准识别每个网格内的字符。这样就解决了文本嘈杂的情况。最终可以精确统计文本数量。
我们使用了以下算法来识别网格。
首先,我在二值化镜像中使用这个连接组件算法。

然后我们发现图中左下角有些是真,所哟我们通过二维周期函数来调整网格识别。


修正网格失真背后主要的思想是利用图形峰值点找到非线性几何失真,换句话说,我们必须找到这个函数的最大值的和。另外,我们还需要一个最佳失真值才行。
我们使用ScipyPython模块中的RectBivariateSpline函数来参数化几何失真。并用Scipy函数进行优化。得到如下结果:

总而言之,这个方法缓慢且不稳定,所以坚决不打算使用这个方案。
光学字符识别
我们通过组连接识别发现文本,并识别完整的单词。
识别通过连接组发现的文本
对于文本识别,我们使用卷积神经网络(CNN)接收相关字体进行培训。输出部分,我们通过对比来提升概率。我们那个几个最初的几个选项多对比,发现有99%的准确识别率后。又通过对比字典来提高准确度,并消除相关类似的字符,如"З" 和 "Э"造成的错误。

然而,当涉及嘈杂的文本时,该方法性能却十分低下。
识别完整的单词
当文本太嘈杂的时候,需要找到完整的单词才能进行单个字母的识别。我们使用下面两个方法来解决这个问题:
LSTM网络
图像非均匀分割技术
LSTM网络
您可以阅读这些文章,以更加深入了解使用卷积神经网络识别序列中的文本 ,或我们可以使用神经网络建立与语言无关的OCR吗?为此,我们使用了OCRopus库来进行识别。
我们使用了等宽的字体来作为人工识别样本进行训练。

训练结束后,我们由利用其他数据来测试我们的神经网络,当然,测试结果非常积极。这是我们得到的数据:

训练好的神经网络在简单的例子上表现十分优秀。同样,我们也识别到了网格不适合的复杂情况。
我们抽取的相关的训练样本,并让他通过神经网络进行训练。

为了避免神经网络过度拟合,我们多次停止并修正训练结果,并不断加入新数据作为训练样本。最后我们得到以下结果:

新的网络擅长识别复杂的词汇,但是简单的文字识别却并不好。
我们觉得这个卷积神经网络可以细化识别单个字符来使文本识别更加优秀。
图像非均匀分割技术
因为收据字体是等宽的字体,所以我们决定按照字符分割字体。首先,我们需要知道每个字母的宽度。因此,字符的宽度尤为重要,我们需要估计每个字母的长度,利用函数,我们得到下图。选择多种模式来选取特定的字母宽度。

我们得到一个单词的近似宽度,通过除以字符中的字母数,给出一个近似分类:

区分最佳的是:

这种分割方案的准确度是非常高的:

当然,也有识别不太好的情况:

分割后我们在使用CNN做识别处理。
从收据中提取含义
我们使用正则表达式来查找收据中购买情况。所有收据都有一个共通点:购买价格以XX.XX格式来撰写。因此,可以通过提取购买的行来提取相关信息。个人纳税号码是十位数,也可以通过正则表达式轻松获取。同样,也可以通过正则表达式找到NAME / SURNAME等信息。

总结
- 不论你选择什么方法,LSTM或者其他更加复杂的方案,都没有错误,有些方法很难用,但是有些方法却很简单,因识别样本而异。
- 我们将继续优化这个项目。目前来看,在没有噪声的情况下,系统性能更加优秀。
原文链接:https://dzone.com/articles/using-ocr-for-receipt-recognition
六、低对比度缺陷检测应用实例--LCD屏幕脏污检测
实例一(LCD屏幕脏污检测)
参考实例来源:
https://stackoverflow.com/questions/27281884/low-contrast-image-segmentation
测试图像:


标注脏污区域:


分析与说明:上图中的脏污图像因为对比度较低,所以无法通过常用的阈值方法处理提取,有时人眼观察也较费劲。常用的方法有梯度提取或频域提取。
链接主题中提到了Kmeans聚类分割后提取:
二分类:

三分类:

乍一看效果还不错,但问题是我到底应该设置几个类别?第一张图我如何确定哪个区域正好是我的缺陷部分?本文采用了梯度方法来检测。
实现步骤与演示
实现步骤:
① 图像滤波--滤除杂讯;
② Sobel提取边缘;
③ 形态学处理剔除杂讯;
④ 阈值提取--分割脏污区域;
⑤ 轮廓提取与标注。
图像一:
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray,(15,15),0)

x = cv2.Sobel(blur,cv2.CV_16S,1,0,ksize=7)
y = cv2.Sobel(blur,cv2.CV_16S,0,1,ksize=7)
absX = cv2.convertScaleAbs(x) # 转回uint8
absY = cv2.convertScaleAbs(y)
edged = cv2.addWeighted(absX,1,absY,1,0)
cv2.imshow('Sobel', edged)

k1=np.ones((11,11), np.uint8)
thres = cv2.morphologyEx(thres, cv2.MORPH_ERODE, k1)#膨胀操作
cv2.imshow('MORPH_ERODE',thres) #结果显示

contours,hierarchy = cv2.findContours(thres, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
for cnt in contours:
(x, y, w, h) = cv2.boundingRect(cnt)
if w > 2 and h > 2:
cv2.drawContours(img,cnt,-1,(0,0,255),1)

图像二:


七、粘连物体分割与计数应用--密集粘连药片分割+计数
在实际的视觉应用场景中,我们常常会遇到物体/元件的计数问题,而计数时比较常见的情形就是物体相邻或粘连,对相邻或粘连物体的分割将直接影响着最终计数的准确性。后面将分篇介绍粘连物体分割计数的常用方法,包括:
【3】其他方法(具体问题具体分析)
本文将对第【3】种方法以案例形式具体讲解。
实例演示与实现步骤
* 应用实例:密集粘连药片分割与计数
测试图像(图片来源--网络):

简单分析:
- 上图中粘连区域较多,且粘连部分与药片本身高度差异不是很大,使用形态学或者分水岭算法很难将其简单分割出来。
- 考虑可行的方法[1]层层突破,先分割独立药片,在逐步分割粘连药片。[2] 深度学习实例分割方法。
- 本文还是采用传统方法[1]来实现。
实现步骤:
【1】先分割独立药片:
- 阈值分割

- 形态学腐蚀 + 开运算

- 根据面积大小筛选,提取单独分离的药片

- 膨胀--使轮廓接近原始大小(并备份此区域 + 计数)

【2】循环分割粘连药片:
- 区域做差,提取剩余粘连药片部分

- 求各区域对应凸包

- 凸包与凸包处理前区域做差

- 开运算

- 闭运算(这个时候就凸显了Halcon Region的好处,可以对各个Region单独处理,如果是OpenCV基本会粘连成一片)

- 闭运算结果与粘连药片区域做差

- 根据面积大小筛选,剩余药片部分

- 膨胀回原来大小

- 把刚刚提取的药片叠加到第【1】步结果

循环步骤【2】,直到当前轮廓数量为0,计数累加


八、生成和检测ArUco标记
什么是 ArUco 标记
S. Garrido-Jurado 最初于 2014 年开发了 ArUco 标记,它代表科尔多瓦增强现实大学,这就是它们背后的历史,让我们来谈谈,什么是 ArUco 标记以及计算机如何在图像中定位这些标记?
ArUco 标记是一种基准标记,可用于各种应用,例如增强现实、机器人和机器视觉。它们由黑白方形网格组成,网格中编码了唯一的 ID 号。ArUco 标记可以很容易地在图像中检测和识别,使其成为各种应用的宝贵工具。
ArUco 标记图像

这些标记是由黑色和白色方形网格组成的二进制图像图案,周围是黑色方形边界,这使得它们在图像中更容易被检测到。这由动画说明。它们代表信息,可由计算机视觉系统读取,以识别标记在图像中的位置和方向。

这些方块以不同的模式排列,使它们能够为每个标记实现唯一的标识 (ID),它们有不同的大小,这与标记的腰带数量无关,这使得它们能够放置在从小到大尺寸的任何尺寸物体上,您可以创建数千个独特的 ArUco 标记。

您可以使用 Aruco 标记库中的标记,通过在标记上增强图像或视频来创建简单的增强现实项目,也可以创建一个可以放置不同标记的环境。这种环境允许机器人根据标记的位置唯一地识别标记,并在环境中移动、执行某些操作等。

如何检测标记
首先,你需要在 Python 环境中安装 OpenCV 模块,你可以使用下面的 pip 命令来安装模块。
pip install opencv-contrib-python
此行将安装软件包,其中包括标准软件包中未包含的其他模块。这些模块支持多种功能,例如对象检测、ArUco 标记检测、图像拼接等。opencv-contrib-pythonopencv-python
生成 ArUco 标记
使用最新版本的 OpenCV python 生成标记
import cv2
# create the dictionary for markers type
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
size_of_marker = 400 # size of marker.
# generating IDs with for loop
for marker_id in range(20):
# generating the marker
img = cv2.aruco.generateImageMarker(dictionary, marker_id, size_of_marker)
print("Dimension of Marker: ", img.shape)
# save/write the image
cv2.imwrite("marker_image{}.png".format(marker_id), img)
# display the image(marker) on windows
cv2.imshow("Marker", img)
cv2.waitKey(0)

代码创建了 20 个具有不同 ID 的 ArUco 标记图像,并将它们保存到图像文件中。
cv2getPredefinedDictionary()创建的,该函数将 dictionary 的类型作为参数。在这种情况下,字典的类型是,它表示一个包含 50 个标记的字典,大小为 4x4 位,另一个单词为黑白方块,如前所述。
下一行代码定义标记的大小。标记的大小是标记的宽度和高度(以像素为单位)。
import cv2
# create the dictionary for markers type
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
size_of_marker = 400 # size of marker.
generateImageMarker()函数用于创建标记图像。此函数将 dictionary 对象、标记 ID 和边长 (size) 作为参数。imwrite()函数用于将标记图像保存到文件中。
# generating IDs with for loop
for marker_id in range(20):
# generating the marker
img = cv2.aruco.generateImageMarker(dictionary, marker_id, size_of_marker)
print("Dimension of Marker: ", img.shape)
# save/write the image
cv2.imwrite("marker_image{}.png".format(marker_id), img)
imshow()函数用于显示标记图像。waitKey()函数用于等待用户按下某个键。
# display the image(marker) on windows
cv2.imshow("Marker", img)
cv2.waitKey(0)
标记检测
aruco.detectMarkers(),可用于检测图像中的 Aruco 标记。以下是执行标记检测的源代码
import cv2
import numpy as np
# define the fonts for draw text on image
font = cv2.FONT_HERSHEY_PLAIN
# create the dictionary for markers type
dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
# Read the image.
image = cv2.imread("./image.jpg")
# Detect ArUco markers in the image.
corners, marker_ids, rejected = cv2.aruco.detectMarkers(image, dictionary)
# If markers are detected, draw them on the image.
if corners:
# looping through detected markers and marker ids at same time.
for corner, marker_id in zip(corners, marker_ids):
# Draw the marker corners.
cv2.polylines(
image, [corner.astype(np.int32)], True, (0, 255, 255), 3, cv2.LINE_AA
)
# Get the top-right, top-left, bottom-right, and bottom-left corners of the marker.
# change the shape of numpy array to 4 by 2
corner = corner.reshape(4, 2)
# change the type of numpy array values integers
corner = corner.astype(int)
# extracting the corner of marker
top_right, top_left, bottom_right, bottom_left = corner
# Write the marker ID on the image.
cv2.putText(
image, f"id: {marker_id[0]}", top_right, font, 1.3, (255, 0, 255), 2
)
# Save the image.
cv2.imwrite("out_image1.png", image)
# Show the image.
cv2.imshow("image", image)
# wait until any key press on keyboard
cv2.waitKey(0)
# Close all windows.
cv2.destroyAllWindows()
输入图像:

检测标记结果:

Aruco 标记的一些应用:
- 增强现实 (AR)。Aruco 标记可用于通过跟踪标记在现实世界中的位置和方向来创建 AR 体验。这可用于在现实世界中叠加数字内容,例如 3D 模型、文本或图像。
- 机器人。Aruco 标记可用于跟踪机器人在现实世界中的位置和方向。这可用于控制机器人、绘制其环境图或检测碰撞。
- 计算机视觉。Aruco 标记可用于跟踪图像中对象的位置和方向。这可用于各种任务,例如对象跟踪、姿势估计和摄像机校准。
- 质量控制。Aruco 标记可用于跟踪制造过程中零件的位置和方向。这可用于确保零件正确组装并检测缺陷。
- 教育。Aruco 标记可用于创建教育游戏和模拟。这可以帮助学生学习各种主题,例如物理、化学和生物学。
九、CV中八种不同的目标追踪算法
目标跟踪作为机器学习的一个重要分支,加之其在日常生活、军事行动中的广泛应用,受到极大的关注。在AI潮流中,大家对于深度学习,目标跟踪肯定都会有过接触了解:在GPU上通过大量的数据集训练出自己想使用的垂直场景后再在实际场景中使用。但麻烦的是,大数人拥有的是CPU,有没有办法能在自己的电脑上用CPU就能实现自己的目标跟踪能力。OpenCV的跟踪API给出了答案:我行。
在这篇文章中,我们会介绍在OpenCV上的8种目标检测算法,优势和局限性, 然后会给出代码示例,如何使用它。我们的目标不是对每一个跟踪器都有深入的理论理解,而是从实际使用的角度来理解它们。
目标跟踪基本原则
在视频目标跟踪中的期望是在当前帧中正确找到跟踪的对象,因为我们已经在所有(或几乎所有)以前的帧中成功跟踪了该对象,所以我们知道对象是如何移动的。换句话说,我们知道运动模型的参数。运动模型只是一种奇特的表达方式,它会知道物体在前一帧中的位置和速度(速度+运动方向)。而如果对这个物体一无所知,则可以根据当前的运动模型来预测新的位置,从而非常接近物体的新位置。
我们还可以建立一个外观模型来编码对象的外观。外观模型可用于在运动模型预测的位置的邻域内搜索,以便更准确地预测物体的位置。
目标跟踪可以描述为:运动模型预测物体的大致位置。外观模型微调此估计,以便根据外观提供更准确的估计。
如果对象非常简单,并且没有改变它的外观,我们可以使用一个简单的模板作为外观模型并查找该模板。然而,现实生活并不那么简单。对象的外观可能会发生显著变化。为了解决这个问题,在许多现代追踪器中,这个外观模型是一个在线训练的分类器。
分类器的任务是将图像的矩形区域分类为对象或背景。分类器接受图像区域作为输入,并返回介于0和1之间的分数,以指示图像区域包含对象的概率。如果确定图像区域是背景,则分数为0;如果确定区域是对象,则分数为1。
在机器学习中,我们使用“在线”这个词来指的是在运行时动态训练的算法。离线分类器可能需要数千个示例来训练分类器,但是在线分类器通常在运行时使用很少的示例进行训练。
分类器是通过向其提供正(对象)和负(背景)示例来训练的。如果您想构建一个用于检测猫的分类器,您可以使用包含猫的数千个图像和不包含猫的数千个图像对其进行训练。通过这种方式,分类器学习区分什么是猫,什么不是猫。
OpenCV八种目标跟踪算法
1、GOTURN Tracker
Goturn是一种基于深度学习的对象跟踪算法。最初的实现是在Caffe,目前已经移植到OpenCV跟踪API。
Goturn是一种基于深度学习的跟踪算法,是回归网络的一般对象跟踪的缩写。大多数跟踪算法都是在线训练的。换句话说,跟踪算法学习运行时跟踪的对象的外观。
因此,许多实时追踪器依赖于在线学习算法,这通常比基于深度学习的解决方案快得多。
Goturn改变了我们将深度学习应用于跟踪问题的方式,通过离线方式学习对象的运动。Goturn模型接受了数千个视频序列的训练,不需要在运行时执行任何学习。
Goturn如何工作?
Goturn由David Holded、Sebastian Thrun和Silvio Savarese在题为“用深度回归网络学习100 fps跟踪”的论文中介绍。

图1 GoTurn示意图
如图1所示,Goturn使用一对来自数千个视频的裁剪帧进行培训。
在第一帧(也称为前一帧)中,对象的位置是已知的,帧被裁剪为对象周围边界框大小的两倍。第一个裁剪帧中的对象始终居中。
需要预测对象在第二帧(也称为当前帧)中的位置。用于裁剪第一帧的边界框也用于裁剪第二帧。因为对象可能已移动,所以对象可能未在第二帧中居中是大概率事件。
训练卷积神经网络(CNN)预测第二帧边界框的位置。
Goturn架构
在Goturn如何工作,看到的是一个黑盒,Goturn架构则让我们了解了盒子里面藏着什么。

图2 Goturn架构
图2显示了Goturn的体系结构。如前所述,它将两个裁剪的帧作为输入。
注:在图2中,上一帧显示在底部,是居中的,我们的目标是找到当前帧的边界框,显示在顶部。
两帧都通过一组卷积层。这些层只是caffenet架构的前五个卷积层。这些卷积层(即pool5特性)的输出被连接成长度为4096的单个矢量。这个向量被输入到3个完全连接的层中。最后一个完全连接的层最终连接到包含4个节点的输出层,这些节点表示边界框的顶部和底部点。
( 每当我们看到一组卷积层,并对其含义感到困惑时,可将它们视为改变原始图像的过滤器,这样可以保留重要信息,并丢弃图像中不重要的信息。
通过简单地展开张量,将卷积滤波器末端获得的多维图像(张量)转换成一个长的数字矢量。这个向量作为输入到几个完全连接的层,最后是输出层。全连通层可以看作是一种学习算法,它利用卷积层从图像中提取的有用信息来解决现有的分类或回归问题。)
与其他基于深度学习的追踪器相比,Goturn速度更快。它在caffe的gpu上以100fps的速度运行,在opencv cpu上以20fps的速度运行。尽管跟踪器是通用的,但理论上,通过将传输集与特定类型的对象进行偏移,可以在特定对象(例如行人)上获得更好的结果。
局限性:神经网络体现的优势,往往就是它的劣势。神经网络依赖于训练集中样本所能代表的场景种类,对于不存在的场景,就会存在问题。如在实际使用中,希望跟踪手掌,把手掌移到脸上时,跟踪器锁定在脸上,并不会在手掌上。而跟踪脸,并用手堵遮住脸,但追踪器能够跟踪通过遮挡的脸,这说明训练集存在大量的手掌遮脸的场景。
2、BOOSTING Tracker助推跟踪器
该跟踪器基于ADaboost的在线版本,ADaboost是基于HAAR级联的人脸检测器内部使用的算法。这个分类器需要在运行时用对象的正负示例进行培训。以用户(或其他对象检测算法)提供的初始边界框为对象的正例,边界框外的许多图像部位作为背景。给定一个新的帧,分类器在前一个位置附近的每个像素上运行,并记录分类器的得分。对象的新位置是得分最大的位置。
缺点:速度较慢,并且表现不好,跟踪失败后,不能及时呈现错误报告。
3、MIL Tracker 密尔跟踪器
这个跟踪器的概念与上面描述的BOOSTING Tracker相似。最大的区别是,它不只是将对象的当前位置视为一个正示例,还会在当前位置周围的一个小邻域中查找,以生成几个潜在的正示例。你可能认为这是一个坏主意,因为在这些“积极”的例子中,大多数的对象都不是中心。
这就是多实例学习(mil)来拯救的地方。在mil中,您不指定正负示例,而是指定正负“bags”。正面的图像收集并非都是正面的例子。一个正面的bag包含了以对象当前位置为中心的区域,以及它周围的一个小邻域中的区域。即使被跟踪对象的当前位置不准确,当来自当前位置附近的样本放入正袋中时,很有可能该袋至少包含一个图像,并且该对象很好地居中。
优点:性能不错。它不会像助推跟踪器那样漂移,并且在部分遮挡下也能正常工作。
缺点:失败率较高。
4、KCF跟踪器
KCF代表kernelized correlation filters。这个追踪器建立在前两个追踪器中提出的想法之上。该跟踪器利用了这样一个事实:在MIL跟踪器中使用的多个正样本具有较大的重叠区域。这些重叠的数据导致了一些很好的数学特性,这些特性被跟踪器利用,从而使跟踪速度更快、更准确。
优点:准确度和速度都比MIL跟踪器好,它报告跟踪故障比BOOSTING和MIL这两个追踪算法好。
缺点:无法从完全遮挡中恢复。
5、TLD跟踪器
TLD代表跟踪、学习和检测。顾名思义,这个跟踪器将长期跟踪任务分解为三个组件(短期)跟踪、学习和检测。在作者的论文中,“跟踪器跟踪对象从一帧到另一帧。探测器定位到目前为止观察到的所有外观,并在必要时纠正跟踪器。学习估计检测器的错误并更新它以避免将来出现这些错误。”这个跟踪器的输出有点跳跃。例如,如果您正在跟踪一个行人,并且场景中还有其他行人,则此跟踪器有时可以临时跟踪一个与您要跟踪的行人不同的行人。在积极的一面,这条轨迹似乎是在更大的比例、运动和遮挡上跟踪一个对象。如果你有一个隐藏在另一个物体后面的视频序列,这个跟踪器可能是个不错的选择。
优点:在多帧遮挡下效果最好。此外,跟踪最佳的超比例变化。
缺点:很多误报使它几乎不可用。
6、MEDIANFLOW跟踪器
在内部,这个跟踪器可以实时地跟踪物体的前后方向,并测量这两个轨迹之间的差异。最大限度地减少这种向前向后的误差,使他们能够可靠地检测跟踪故障,并在视频序列中选择可靠的轨迹。
在测试中发现这个跟踪器在运动可预测和对象小的情况下工作得最好。与其他跟踪者不同的是,即使跟踪明显失败,跟踪者也知道跟踪何时失败。
优点:出色的跟踪故障报告。当运动是可预测的并且没有遮挡时,效果非常好。
缺点:大幅度运动跟踪,模型会失效。
7、MOSSE 莫斯跟踪器
最小平方误差输出和(mosse)使用自适应相关进行对象跟踪,当使用单帧进行初始化时,可产生稳定的相关滤波器。Mosse跟踪器对光照、比例、姿势和非刚性变形的变化具有鲁棒性。对于遮挡,跟踪器能够在对象重新出现时暂停并恢复到停止的位置。
优点:速度快。
缺点:准确率不如CSRT和KCF高。
8、CSRT跟踪器
在具有信道和空间可靠性的鉴别相关滤波器(DCF-CSR)中,我们使用空间可靠性图从帧中调整滤波器支持到所选区域的一部分进行跟踪。这样可以确保选定区域的放大和定位,并改进对非矩形区域或对象的跟踪。它只使用两个标准功能(HoGs and Colornames)。它也在相对较低的fps(25 fps)下工作,但提供了更高的目标跟踪精度。
优点:比KCR精度高。
缺点:速度较慢。
OpenCV跟踪算法使用代码实现(C)
1、创建跟踪算法

2、对跟踪算法做初始化操作

3、跟踪监测刷新,获取到下一帧跟踪目标的坐标(x、y、宽、高)

4、利用坐标做跟踪所需要的操作,如画框等

实际检测结果对比

图片1起始跟踪;图片2运动后跟踪
如上跟踪算法显示,以CBA比赛作为跟踪对象。从跟踪过程中两帧数据可以感知不同跟踪算法在对动态人物变动所体现的差异。
1、MEDIANFLOW、BOOSTING、MIL在针对人物大小有变化时跟踪比较不理想,跟踪不到目标。
2、CSRT、KCF能对运动画面有较好跟踪,但是在遮挡场景下,不能支持,效果较差。
十、CV快速傅里叶变换(FFT)用于图像和视频流的模糊检测
翻译自【OpenCV Fast Fourier Transform (FFT) for blur detection in images and video streams】,原文链接:
本文仅作学习分享。

在本教程中,您将学习如何使用OpenCV和快速傅里叶变换(FFT)在图像和实时视流中执行模糊检测。
今天的教程是我上一篇关于OpenCV模糊检测的博客文章的扩展
(https://www.pyimagesearch.com/2015/09/07/blur-detection-with-opencv/)。
原始模糊检测方法:
- 依赖于计算图像Laplacian算子的方差
- 可以仅用一行代码实现
- 使用起来非常简单
缺点是,Laplacian方法需要大量手动调整用于定义图像是否模糊的”阈值“。如果你能控制你的光线条件,环境和图像捕捉过程,这个方法工作得很好,但如果不是,那你很可能得到杂乱不堪的效果。
我们今天要讲的方法依赖于计算图像的快速傅里叶变换。它仍然需要一些手动调整,但正如我们将发现的,FFT模糊检测器比Laplacian方差更加可靠与稳定。
在本教程结束时,你将拥有一个可以应用于图像和视频流,且功能齐全的FFT模糊检测器。
OpenCV快速傅里叶变换(FFT)模糊检测
在本教程的第一部分,我们将简要讨论:
- 什么是模糊检测
- 为什么我们想检测图像/视频流中的模糊
- 快速傅里叶变换如何让我们检测模糊
什么是模糊检测,什么时候我们需要检测模糊?

图1:如何使用OpenCV和快速傅里叶变换(FFT)算法自动检测照片是否模糊?(图片来源:https://www.cs.unm.edu/~brayer/vision/fourier.html)
模糊检测,顾名思义,是检测图像是否模糊的算法。
模糊检测可能的应用包括:
- 图像质量的自动分级
- 帮助专业摄影师在100到1000张的照片拍摄过程中自动丢弃模糊/低质量的照片
- 将OCR应用于实时视频流,但仅对非模糊帧应用昂贵的OCR计算
这里的关键要点是,为在理想条件下捕获的图像编写计算机视觉代码总是比较容易的。
与其尝试处理质量非常差的图像的边缘情况,不如检测并丢弃质量差的图像(比如有明显模糊的图像)。
这种模糊检测程序既可以自动丢弃质量差的图像,也可以简单地告诉终端用户:”嘿,老兄,再试一次,让我们在这里捕捉一个更好的画面”。
请记住,计算机视觉应用程序应该是智能的,因此有了“人工智能”这个术语——有时候,“智能”可以只是检测输入数据的质量是否太差,而不是试图弄懂它。
什么是快速傅立叶变换(FFT)?

图2:在本教程中,我们将使用OpenCV和NumPy的组合在图像和视流中进行基于快速傅立叶变换(FFT)的模糊检测。
快速傅里叶变换是计算离散傅里叶变换的一种方便的数学算法。它用于将信号从一个域转换为另一个域。
FFT在许多学科中都很有用,包括音乐、数学、科学和工程。例如,电气工程师,特别是那些与无线、电源和音频信号打交道的工程师,需要FFT计算来将时间序列信号转换到频域,因为有些计算在频域更容易进行。相反,使用FFT可以将频域信号转换回时域。
在计算机视觉方面,我们通常认为FFT是一种图像处理工具,它可以将图片在两个图像域内转换:
- 傅里叶(即频率)域
- 空间域
此外,FFT同时用实分量和虚分量来表示图像。
通过分析这些值,我们可以执行图像处理程序,如模糊,边缘检测,阈值,纹理分析,以及模糊检测。
回顾快速傅里叶变换的数学细节超出了这篇博客文章的范围,所以如果你有兴趣学习更多关于它的知识,我建议你阅读这篇关于FFT及其与图像处理的关系的文章。
对于有学术倾向的读者,可以看看Aaron Bobick在佐治亚理工学院计算机视觉课程上的精彩幻灯片。
https://www.cc.gatech.edu/~afb/classes/CS4495-Fall2014/slides/CS4495-Frequency.pdf
最后,维基百科关于傅里叶变换的页面更详细地介绍了数学,包括它在非图像处理任务中的应用。
项目结构
首先使用本教程的“下载”部分下载源代码和示例图像。一旦你解压缩文件,你将有一个目录组织如下:
$ tree --dirsfirst
.
├── images
│ ├── adrian_01.png
│ ├── adrian_02.png
│ ├── jemma.png
│ └── resume.png
├── pyimagesearch
│ ├── __init__.py
│ └── blur_detector.py
├── blur_detector_image.py
└── blur_detector_video.py
2 directories, 8 files
我们基于FFT的模糊检测算法位于blur_detector.py文件中的pyimagesearch模块中。内部实现了一个函数detect_blur_fft。
我们在两个Python驱动程序脚本中使用detect_blur_fft方法:
- blur_detector_image:对静态图像进行模糊检测。我在images/目录中为我们提供了一些测试图像,您也应该在自己的图像(模糊的和不模糊的)上尝试这种算法。
- blur_detector_video。在视频流中实现实时模糊检测。
使用OpenCV实现我们的FFT模糊检测器
现在我们准备用OpenCV实现我们的快速傅里叶变换模糊检测器。
我们将要介绍的方法是基于Liu等人在2008年CVPR出版物《图像部分模糊检测和分类》中实现的。
http://www.cse.cuhk.edu.hk/leojia/all_final_papers/blur_detect_cvpr08.pdf
在我们的目录结构中打开blur_detector.py文件,插入以下代码:
# import the necessary packages
import matplotlib.pyplot as plt
import numpy as np
def detect_blur_fft(image, size=60, thresh=10, vis=False):
# grab the dimensions of the image and use the dimensions to
# derive the center (x, y)-coordinates
(h, w) = image.shape
(cX, cY) = (int(w / 2.0), int(h / 2.0))
我们的模糊检测器实现需要matplotlib和NumPy。我们将使用内建在NumPy中的快速傅里叶变换算法作为我们方法的基础;
第4行定义detect_blur_fft函数,接受四个参数:
- 图片image:我们对模糊检测输入图像
- 大小size:以图像中心点为中心的半径的大小,我们将使FFT偏移为零
- 阈值thresh:用于确定图像是否被认为是模糊的,将与震级的平均值(稍后详细说明)进行比较的一个值
- 标识符vis:一个布尔值,指示是否使用matplotlib可视化/绘制原始输入图像和大小图像
给定输入图像,首先获取它的尺寸(第7行)并计算中心(x, y)坐标(第8行)。
接下来,我们将使用NumPy的快速傅里叶变换(FFT)算法实现来计算离散傅里叶变换(DFT):
# compute the FFT to find the frequency transform, then shift
# the zero frequency component (i.e., DC component located at
# the top-left corner) to the center where it will be more
# easy to analyze
fft = np.fft.fft2(image)
fftShift = np.fft.fftshift(fft)
在这里,我们使用NumPy的内置算法计算FFT(第5行)。
然后我们将结果的零频率分量(直流分量)移到中心以便于分析(第6行)。
现在我们已经有了图像的FFT,如果设置了vis标志,让我们可视化一下结果:
# check to see if we are visualizing our output
if vis:
# compute the magnitude spectrum of the transform
magnitude = 20 * np.log(np.abs(fftShift))
# display the original input image
(fig, ax) = plt.subplots(1, 2, )
ax[0].imshow(image, cmap="gray")
ax[0].set_title("Input")
ax[0].set_xticks([])
ax[0].set_yticks([])
# display the magnitude image
ax[1].imshow(magnitude, cmap="gray")
ax[1].set_title("Magnitude Spectrum")
ax[1].set_xticks([])
ax[1].set_yticks([])
# show our plots
plt.show()
出于调试和好奇的目的,您可能希望通过设置vis=True来绘制输入图像的FFT幅度谱。
如果你选择这样做,首先我们计算变换的振幅谱(第4行)。
然后,我们将原始输入图像绘制在幅度谱图像旁边(第6-16行),并显示结果(第19行)。
现在我们有了可视化振幅谱的方法,让我们来确定输入图像是否模糊:
# zero-out the center of the FFT shift (i.e., remove low
# frequencies), apply the inverse shift such that the DC
# component once again becomes the top-left, and then apply
# the inverse FFT
fftShift[cY - size:cY + size, cX - size:cX + size] = 0
fftShift = np.fft.ifftshift(fftShift)
recon = np.fft.ifft2(fftShift)
在这里,我们:
- 设置我们的FFT移动为0(即,去除低频率)第5行
- 应用反向位移将DC组件放回左上角(第6行)
- 应用逆FFT(第7行)
到此,我们还有三个步骤来确定我们的图像是否模糊:
# compute the magnitude spectrum of the reconstructed image,
# then compute the mean of the magnitude values
magnitude = 20 * np.log(np.abs(recon))
mean = np.mean(magnitude)
# the image will be considered "blurry" if the mean value of the
# magnitudes is less than the threshold value
return (mean, mean <= thresh
其余步骤包括:
- 在我们已经将中心DC值归零之后,再次计算重建图像的幅度值(第3行)。
- 计算幅度值的平均值(第4行)。
- 返回一个2元组的平均值以及一个指示输入图像是否模糊的布尔值(第8行)。查看代码,我们可以看到,通过比较平均值和阈值,我们已经确定了模糊布尔值(判断图像是否模糊)。
我们实现了一个基于fft的模糊检测算法。但还没有完成。在下一节中,我们将对静态图像应用我们的算法,以确保它按照我们的期望执行。
用FFT检测图像中的模糊
现在我们的detect_blur_fft 辅助函数已经实现,让我们通过创建一个Python驱动程序脚本来使用它,该脚本从磁盘加载一个输入图像,然后对其应用FFT模糊检测。
打开一个新文件,命名为detect_blur_image.py,并插入以下代码:
# import the necessary packages
from pyimagesearch.blur_detector import detect_blur_fft
import numpy as np
import argparse
import imutils
import cv2
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", type=str, required=True,
help="path input image that we'll detect blur in")
ap.add_argument("-t", "--thresh", type=int, default=20,
help="threshold for our blur detector to fire")
ap.add_argument("-v", "--vis", type=int, default=-1,
help="whether or not we are visualizing intermediary steps")
ap.add_argument("-d", "--test", type=int, default=-1,
help="whether or not we should progressively blur the image")
args = vars(ap.parse_args())
第2-6行进行导入,特别的是,我们需要导入我们在上一节中实现的detect_blur_fft函数。
从这里,我们解析四个命令行参数:
- --image:用于模糊检测的输入图像的路径。
- --thresh:我们的模糊检测器计算阈值。
- --vis:我们的标志符,指示是否将输入图像的幅度值图像可视化。
- --test:为了测试,我们可以逐步模糊输入图像,并对每个示例进行基于fft的模糊检测;此标志指示我们是否将执行此测试。
--image、--thresh和--vis参数分别对应于我们在上一节实现的detect_blur_fft函数的image、thresh和vis参数。
让我们继续,加载我们的输入图像,执行快速傅里叶变换模糊检测:
# load the input image from disk, resize it, and convert it to
# grayscale
orig = cv2.imread(args["image"])
orig = imutils.resize(orig, width=500)
gray = cv2.cvtColor(orig, cv2.COLOR_BGR2GRAY)
# apply our blur detector using the FFT
(mean, blurry) = detect_blur_fft(gray, size=60,
thresh=args["thresh"], vis=args["vis"] > 0)
进行FFT模糊检测,我们:
- 加载输入图像--image,并将其转换为灰度(第3-5行)
- 使用detect_blur_fft函数应用我们的FFT模糊检测器(第7和8行)
接下来,我们将注释并显示我们的图像:
# draw on the image, indicating whether or not it is blurry
image = np.dstack([gray] * 3)
color = (0, 0, 255) if blurry else (0, 255, 0)
text = "Blurry ({:.4f})" if blurry else "Not Blurry ({:.4f})"
text = text.format(mean)
cv2.putText(image, text, (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
color, 2)
print("[INFO] {}".format(text))
# show the output image
cv2.imshow("Output", image)
cv2.waitKey(0)
在这里,我们:
- 向我们的单通道灰度图像添加两个通道,将结果存储在图像中(第2行)
- 通过第32行将颜色设置为红色(如果模糊)和绿色(如果不模糊)
- 在图像的左上角绘制模糊的文本指示和平均值(第4-7行),并在终端中打印相同的信息(第37行)
- 显示输出图像,直到按下一个键为止(第11和12行)
至此,我们已经完成了确定输入图像是否模糊的目标。
我们可以就此打住。但是为了更严格地测试我们的算法,让我们实现一个健壮的方法来测试我们的图像在不同层次上的模糊:
# check to see if are going to test our FFT blurriness detector using
# various sizes of a Gaussian kernel
if args["test"] > 0:
# loop over various blur radii
for radius in range(1, 30, 2):
# clone the original grayscale image
image = gray.copy()
# check to see if the kernel radius is greater than zero
if radius > 0:
# blur the input image by the supplied radius using a
# Gaussian kernel
image = cv2.GaussianBlur(image, (radius, radius), 0)
# apply our blur detector using the FFT
(mean, blurry) = detect_blur_fft(image, size=60,
thresh=args["thresh"], vis=args["vis"] > 0)
# draw on the image, indicating whether or not it is
# blurry
image = np.dstack([image] * 3)
color = (0, 0, 255) if blurry else (0, 255, 0)
text = "Blurry ({:.4f})" if blurry else "Not Blurry ({:.4f})"
text = text.format(mean)
cv2.putText(image, text, (10, 25), cv2.FONT_HERSHEY_SIMPLEX,
0.7, color, 2)
print("[INFO] Kernel: {}, Result: {}".format(radius, text))
# show the image
cv2.imshow("Test Image", image)
cv2.waitKey(0)
当设置了--test标志时,我们将进入从第3行开始的条件块。第3-31行代码完成了以下工作:
- 在逐渐增加的半径范围内对我们的灰度图像应用高斯模糊
- 对每个人为模糊的图像进行快速的基于傅里叶变换的模糊检测
- 注释并显示结果
为了完成我们的测试特性,第5行开始在[0,30]范围内的所有奇数半径上进行循环。从这里开始,第13行应用OpenCV的GaussianBlur方法有意地在我们的图像中引入模糊。
其他的都是一样的,包括模糊检测算法和注释步骤。您可以通过在屏幕上按一个键来循环测试结果图像,直到模糊半径在该范围内耗尽。
当然,我们测试例程的目的是让我们能够有效地感受和调整模糊阈值参数(—thresh)。
FFT模糊检测在图像结果
现在我们准备使用OpenCV和快速傅里叶变换来检测图像中的模糊。
首先,请确保使用本教程的“下载”部分下载源代码和示例图像。
然后打开终端,执行以下命令:
$ python blur_detector_image.py --image images/adrian_01.png
[INFO] Not Blurry (42.4630)

图3:结合快速傅里叶变换(FFT)算法,使用Python和OpenCV来确定照片是否模糊
这里你可以看到我在锡安国家公园的地铁徒步旅行的输入图像-图像被正确地标记为不模糊。
让我们试试另一张图片,这是我家的狗,Jemma:
$ python blur_detector_image.py --image images/jemma.png
[INFO] Blurry (12.4738)

图4:基于Python、OpenCV和NumPy的快速傅里叶变换(FFT)模糊检测算法已经自动判定Janie的这张图像模糊。
这幅图像有明显的模糊,因此被标记为模糊。
为了了解当图像变得越来越模糊时,FFT的平均幅度值是如何变化的,让我们提供——test命令行参数:
$ python blur_detector_image.py --image images/adrian_02.png --test 1
[INFO] Not Blurry (32.0934)
[INFO] Kernel: 1, Result: Not Blurry (32.0934)
[INFO] Kernel: 3, Result: Not Blurry (25.1770)
[INFO] Kernel: 5, Result: Not Blurry (20.5668)
[INFO] Kernel: 7, Result: Blurry (13.4830)
[INFO] Kernel: 9, Result: Blurry (7.8893)
[INFO] Kernel: 11, Result: Blurry (0.6506)
[INFO] Kernel: 13, Result: Blurry (-5.3609)
[INFO] Kernel: 15, Result: Blurry (-11.4612)
[INFO] Kernel: 17, Result: Blurry (-17.0109)
[INFO] Kernel: 19, Result: Blurry (-19.6464)
[INFO] Kernel: 21, Result: Blurry (-20.4758)
[INFO] Kernel: 23, Result: Blurry (-20.7365)
[INFO] Kernel: 25, Result: Blurry (-20.9362)
[INFO] Kernel: 27, Result: Blurry (-21.1911)
[INFO] Kernel: 29, Result: Blurry (-21.3853)

图5:使用Python模糊检测器脚本的——测试例程,我们应用了一系列有意的模糊以及快速傅里叶变换(FFT)方法来确定图像是否模糊。这个测试例程非常有用,因为它允许您调优模糊阈值参数。
在这里,你可以看到,当我们的图像变得越来越模糊,FFT的平均幅度值下降。
我们的FFT模糊检测方法也适用于非自然场景图像。
例如,假设我们想要构建一个自动文档扫描器应用程序——这样的计算机视觉项目应该会自动拒绝模糊图像。
然而,文档图像与自然场景图像有很大的不同,从本质上来说,文档图像对模糊更加敏感。
任何类型的模糊都会严重影响OCR的精度。
因此,我们应该增加我们的——thresh值(我还将使用——vis参数,以便我们可以可视化FFT幅度值的变化):
$ python blur_detector_image.py --image images/resume.png --thresh 27 --test 1 --vis 1
[INFO] Not Blurry (34.6735)
[INFO] Kernel: 1, Result: Not Blurry (34.6735)
[INFO] Kernel: 3, Result: Not Blurry (29.2539)
[INFO] Kernel: 5, Result: Blurry (26.2893)
[INFO] Kernel: 7, Result: Blurry (21.7390)
[INFO] Kernel: 9, Result: Blurry (18.3632)
[INFO] Kernel: 11, Result: Blurry (12.7235)
[INFO] Kernel: 13, Result: Blurry (9.1489)
[INFO] Kernel: 15, Result: Blurry (2.3377)
[INFO] Kernel: 17, Result: Blurry (-2.6372)
[INFO] Kernel: 19, Result: Blurry (-9.1908)
[INFO] Kernel: 21, Result: Blurry (-15.9808)
[INFO] Kernel: 23, Result: Blurry (-20.6240)
[INFO] Kernel: 25, Result: Blurry (-29.7478)
[INFO] Kernel: 27, Result: Blurry (-29.0728)
[INFO] Kernel: 29, Result: Blurry (-37.7561)

图6:OpenCV快速傅里叶变换(FFT)用于图像和视视频中的模糊检测,可以判断简历等文档是否模糊。
在这里,您可以看到我们的图像很快变得模糊和不可读,正如输出所示,我们的OpenCV FFT模糊检测器正确地将这些图像标记为模糊。
下面是一个可视化的快速傅里叶变换幅度值,图像变得越来越模糊:

图7:当图像变得越来越模糊时,我们可以看到幅度谱可视化的变化。本教程使用OpenCV和NumPy在图像和视流中执行快速傅里叶变换(FFT)模糊检测。
利用OpenCV和FFT检测视频中的模糊
到目前为止,我们已经对图像应用了快速傅里叶变换模糊检测器。
但是有可能将FFT模糊检测应用到视频流吗?
整个过程也能实时完成吗?
打开一个新文件,命名为blur_detector_video.py,并插入以下代码:
# import the necessary packages
from imutils.video import VideoStream
from pyimagesearch.blur_detector import detect_blur_fft
import argparse
import imutils
import time
import cv2
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-t", "--thresh", type=int, default=10,
help="threshold for our blur detector to fire")
args = vars(ap.parse_args())
我们从导入开始,特别是我们的VideoStream类和detect_blur_fft函数。
对于这个Python脚本,我们只有一个命令行参数:FFT模糊检测的阈值(——thresh)。
从这里,我们准备初始化我们的视频流,并开始循环从我们的摄像头的帧:
# initialize the video stream and allow the camera sensor to warm up
print("[INFO] starting video stream...")
vs = VideoStream(src=0).start()
time.sleep(2.0)
# loop over the frames from the video stream
while True:
# grab the frame from the threaded video stream and resize it
# to have a maximum width of 400 pixels
frame = vs.read()
frame = imutils.resize(frame, width=500)
# convert the frame to grayscale and detect blur in it
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
(mean, blurry) = detect_blur_fft(gray, size=60,
thresh=args["thresh"], vis=False)
第3行和第4行初始化了我们的摄像头图像流,并允许相机有时间预热。
从这里开始,我们在第7行开始帧处理循环。在内部,我们抓取一帧并将其转换为灰度(第10-14行),就像在我们的单一图像模糊检测脚本。
然后,第15和16行应用我们的快速傅里叶变换模糊检测算法,同时传递我们的灰色框架和——thresh命令行参数。我们不会把幅度谱的表示形象化,所以vis=False。
接下来,我们将处理这个特定帧的结果:
# draw on the frame, indicating whether or not it is blurry
color = (0, 0, 255) if blurry else (0, 255, 0)
text = "Blurry ({:.4f})" if blurry else "Not Blurry ({:.4f})"
text = text.format(mean)
cv2.putText(frame, text, (10, 25), cv2.FONT_HERSHEY_SIMPLEX,
0.7, color, 2)
# show the output frame
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
# if the `q` key was pressed, break from the loop
if key == ord("q"):
break
# do a bit of cleanup
cv2.destroyAllWindows()
vs.stop()
最后一个代码块此时看起来应该非常熟悉,因为这是我们第三次看到这些代码行了。我们在这里:
- 注释模糊(红色文本)或不模糊(绿色文本)以及平均值(第2-6行)
- 显示结果(第9行)
- 如果按下q键就退出(第10-14行),并执行家务清理(第17和18行)
快速傅里叶变换视频模糊检测结果
我们现在准备看看我们的OpenCV FFT模糊检测器是否可以应用于实时视频流。
请确保使用本教程的“下载”部分下载源代码。
然后打开终端,执行以下命令:
$ python blur_detector_video.py
[INFO] starting video stream...

当我移动我的笔记本电脑,运动模糊被引入帧。
如果我们要实现一个计算机视觉系统来自动提取关键、重要的帧,或者创建一个自动的视频OCR系统,我们会想要丢弃这些模糊的帧——使用我们的OpenCV FFT模糊检测器,我们可以做到这一点!
十一、CV颜色检测
本文我们将学习如何使用 HSV(色相、饱和度、值)颜色模型来检测图像中的黄色。我们将逐步完成每个步骤,包括导入库、加载图像、将图像转换为 HSV 色彩空间、创建黄色蒙版、查找轮廓以及在黄色区域周围绘制边界框。在这篇文章的最后,您将了解如何使用 HSV 进行颜色检测,以及如何将其与 OpenCV 一起应用于图像中的黄色。
第 1 步:安装并导入所需的库
首先,我们需要导入有助于我们处理图像和显示结果的库。我们需要的三个库是:
- OpenCV (cv2):用于图像处理任务,如读取、显示和作图像。
- NumPy (np)
- Matplotlib (plt) :用于以更用户友好的方式显示图像。
让我们从导入这些库开始:
import cv2
import numpy as np
import matplotlib.pyplot as plt
解释:
- cv2是计算机视觉任务的主库。
- numpy帮助我们以数组的形式处理图像。
- matplotlib.pyplot用于在 Jupyter Notebook 中显示图像。
第 2 步:什么是 HSV,我们为什么要使用它?
在我们深入研究代码之前,让我们先了解 HSV(色相、饱和度、值),我们将使用它来检测图像中的黄色。
HSV 细分:
- 色相:表示颜色类型(例如,红色、蓝色、绿色)。它表示为色轮上的角度(0° 到 360°)。
- 饱和度:表示颜色的强度或鲜艳度。完全饱和的颜色是明亮纯净的,而饱和度较低的颜色是褪色或灰色。
- 亮度:表示颜色的亮度或亮度。值 0 表示完全暗(黑色),值 255 表示完全亮(白色)。
在 RGB 颜色模型(红色、绿色、蓝色)中,颜色是通过组合不同数量的红光、绿光和蓝光来创建的。但是,此模型并不适合颜色检测,因为相同的颜色在各种照明条件下可能看起来不同。HSV 模型将颜色信息(色调)与亮度和强度分开,这使得检测黄色、红色或绿色等颜色变得更加容易,尤其是在不同的光照条件下。
第 3 步:加载图像
接下来,让我们加载一个将用于检测黄色的图像。替换'your_image.jpg'为图像的路径。
image_path = 'vvv.jpg' # Replace with your image path
image = cv2.imread(image_path)
# Display the original image
plt.figure(figsize=(6,6))
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.title("Original Image")
plt.axis('off')
plt.show()
解释:
- cv2.imread(image_path)从指定路径加载图像。
- cv2.cvtColor(image, cv2.COLOR_BGR2RGB)使用 将图像从 BGR(蓝色、绿色、红色)转换为 RGB 以便正确显示。matplotlib
- plt.imshow()显示图像。
输出:
将显示原始图像。

第 4 步:将图像转换为 HSV
现在,我们将图像从 BGR 色彩空间(OpenCV 中的默认色彩空间)转换为 HSV 色彩空间。
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Display the HSV image
plt.figure(figsize=(6,6))
plt.imshow(cv2.cvtColor(hsv_image, cv2.COLOR_BGR2RGB))
plt.title("HSV Image")
plt.axis('off')
plt.show()
解释:
- cv2.cvtColor(image, cv2.COLOR_BGR2HSV)将图像从 BGR 转换为 HSV 色彩空间。这将使分离黄色变得更加容易。
- plt.imshow()在 HSV 色彩空间中显示图像。
输出:
您将看到一个图像,其中颜色现在在 HSV 模型中表示。

第 5 步:定义黄色的 HSV 范围
接下来,我们将定义与黄色对应的 HSV 范围。黄色的色调通常在 20° 到 30° 之间。
yellow_lower = np.array([20, 100, 100]) # Lower boundary for yellow in HSV
yellow_upper = np.array([30, 255, 255]) # Upper boundary for yellow in HSV
解释:
- 黄色的下限范围从 20° 的色相开始,饱和度和值设置为 100(中等强度和亮度)。
- 黄色的上限范围以 30° 的色相结束,饱和度和值设置为 255(最大强度和亮度)。
第 6 步:为黄色创建蒙版
现在,我们将创建一个蒙版,以突出显示图像中的所有黄色区域。蒙版将是二进制图像,其中黄色区域为白色 (255),非黄色区域为黑色 (0)。
mask = cv2.inRange(hsv_image, yellow_lower, yellow_upper)
# Display the mask
plt.figure(figsize=(6,6))
plt.imshow(mask, cmap='gray')
plt.title("Yellow Mask")
plt.axis('off')
plt.show()
解释:
- cv2.inRange(hsv_image, yellow_lower, yellow_upper)生成一个蒙版,其中定义的黄色范围内的像素为白色 (255),其他像素为黑色 (0)。
- plt.imshow(mask, cmap='gray')以灰度显示蒙版。
输出:
黑白图像,其中白色区域表示检测到的黄色区域。

第 7 步:查找等值线并绘制边界框
现在,我们将找到蒙版中黄色区域的轮廓。
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Draw the contours on the original image (before bounding boxes)
output_image_contours = image.copy()
cv2.drawContours(output_image_contours, contours, -1, (255, 0, 0), 3)
# Display the image with contours
plt.figure(figsize=(6,6))
plt.imshow(cv2.cvtColor(output_image_contours, cv2.COLOR_BGR2RGB))
plt.title("Contours of Yellow Areas")
plt.axis('off')
plt.show()
解释:
- cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)查找黄色区域的轮廓(轮廓)。
参数:
- mask:这是二进制图像(黑白图像),其中白色区域代表黄色区域,黑色区域代表其他区域。该蒙版是之前创建的,用于过滤掉 HSV 图像中的黄色区域。cv2.inRange()
- cv2.RETR_EXTERNAL:此标志指定检索模式。 意味着我们只检索最外层的等值线,而忽略其中的任何嵌套等值线。如果我们只关心检测更大的孤立黄色区域,这将非常有用。RETR_EXTERNAL
- cv2.CHAIN_APPROX_SIMPLE:此标志指定等值线近似方法。 通过将定义等值线的点近似为更少的点来减少这些点的数量,从而简化等值线表示。这有助于降低计算复杂性。
输出:
- contours:这是一个等值线列表,其中每个等值线都由一个点(顶点)列表表示,这些点(顶点)勾勒出检测到的黄色区域。
- _:这是第二个返回值的占位符,即等值线的层次结构(在本例中不使用)。
cv2.drawContours(output_image_contours, contours, -1, (255, 0, 0), 3):
此函数用于在图像上绘制上一步中找到的轮廓。
output_image_contours:将在其中绘制轮廓的图像。
contours:我们想在图像上绘制的轮廓列表。
-1:这表示我们要绘制所有等值线(如果指定特定的等值线索引,则只会绘制该等值线)。
(255, 0, 0):等值线的颜色。在这里,它被设置为蓝色(BGR 格式),这将用于在图像上绘制轮廓。
3:等值线的粗细。
结果:
此步骤使用蓝色和 3 像素的粗细在图像上绘制所有检测到的轮廓(黄色区域的轮廓)。
输出:

第 8 步:显示带有轮廓的图像
最后,让我们显示检测到黄色区域的处理后的图像。我们只在足够大的轮廓周围绘制框(面积> 500 像素)。
output_image = image.copy()
for contour in contours:
# Get the bounding box for each contour
x, y, w, h = cv2.boundingRect(contour)
# Draw the bounding box
if cv2.contourArea(contour) > 500: # Filter small areas
cv2.rectangle(output_image, (x, y), (x + w, y + h), (0, 255, 0), 2) # Green box
# Display the image with bounding boxes
plt.figure(figsize=(6,6))
plt.imshow(cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB))
plt.title("Yellow Detection with Bounding Boxes")
plt.axis('off')
plt.show()
输出:

现在,让我们通过创建一个函数来结束本教程,该函数接受图像作为输入并检测黄色,并在黄色区域周围绘制边界框。这将使代码可重用且更有条理。
以下是完整的代码:
import cv2
import numpy as np
import matplotlib.pyplot as plt
def detect_yellow_color(image_path):
"""
This function takes an image file path, detects yellow color areas using HSV color space,
and returns the image with bounding boxes drawn around the detected yellow areas.
"""
# Load the image
image = cv2.imread(image_path)
# Convert the image to HSV color space
hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# Define the HSV range for yellow color
yellow_lower = np.array([20, 100, 100]) # Lower bound for yellow color
yellow_upper = np.array([30, 255, 255]) # Upper bound for yellow color
# Create a mask to isolate yellow color
mask = cv2.inRange(hsv_image, yellow_lower, yellow_upper)
# Find contours of the yellow areas
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create a copy of the original image to draw bounding boxes on
output_image = image.copy()
# Loop through each contour and draw a bounding box around the detected yellow areas
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
if cv2.contourArea(contour) > 500: # Filter out small contours
cv2.rectangle(output_image, (x, y), (x + w, y + h), (0, 255, 0), 2) # Draw green bounding box
# Convert the output image to RGB for display with matplotlib
output_image_rgb = cv2.cvtColor(output_image, cv2.COLOR_BGR2RGB)
# Display the original and processed images side by side
plt.figure(figsize=(12, 6))
# Display original image
plt.subplot(1, 2, 1)
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.title("Original Image")
plt.axis('off')
# Display processed image with yellow color detection
plt.subplot(1, 2, 2)
plt.imshow(output_image_rgb)
plt.title("Yellow Color Detection with Bounding Boxes")
plt.axis('off')
# Show the plots
plt.show()
return output_image
# Example usage
image_path = 'vvv.jpg' # Replace with your image path
output_image = detect_yellow_color(image_path)
在本教程中,我们学习了如何使用 HSV 颜色模型检测图像中的黄色。通过将图像转换为 HSV、定义黄色范围、创建蒙版和查找轮廓,我们成功地用边界框隔离并突出显示了黄色区域。这种方法对于计算机视觉中的颜色检测任务非常强大。
十二、CV图像轮廓检测与绘制
在计算机视觉和图像处理中,轮廓是沿着物体边界连接具有相同强度或颜色的连续点的曲线或边界。换句话说,轮廓表示图像中物体的轮廓。
图像轮廓绘制是检测和提取图像中物体的边界或轮廓的过程。本质上,图像轮廓绘制涉及识别形成连续曲线的相似强度或颜色的点,从而勾勒出物体的形状。
轮廓在许多图像处理和计算机视觉任务中至关重要,因为它们可以有效地分割、分析和提取图像中各种对象的特征。图像轮廓通常用于简化图像,只关注重要的结构元素(即对象边界),同时忽略不相关的细节,如纹理或对象内的细微变化。下图显示了轮廓的一个示例。在图像中检测到了苹果的形状。

图像轮廓的工作原理
图像轮廓绘制通常涉及以下步骤:
- 预处理
- 二进制转换
- 轮廓检测
- 轮廓绘制
让我们逐一讨论一下每个步骤。
预处理
如果是彩色图像,则将其转换为灰度图像。可选地,应用平滑滤波器(例如高斯模糊)来减少噪声,这有助于实现更好的轮廓检测结果。
# Convert the image to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Apply Gaussian Blur to reduce noise
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
二值图转换
应用阈值或边缘检测(例如,使用 Canny 边缘检测器)将灰度图像转换为二进制图像。在二进制图像中,对象由白色像素(前景)表示,背景由黑色像素表示。
阈值处理有助于根据像素强度突出显示感兴趣的区域,而边缘检测则可以强调发生急剧强度变化的边界。
# Apply binary thresholding
# The threshold value (150) may need adjustment based on your image's lighting and contrast
ret, thresh = cv2.threshold(blurred, 150, 255, cv2.THRESH_BINARY_INV)
轮廓检测
使用算法检测二值图像中的轮廓。OpenCV 中用于轮廓检测的常用函数是 cv2.findContours(),它返回图像中所有检测到的轮廓的列表。
轮廓由点列表表示,每个点代表沿对象边界的像素位置。
# Find contours in the threshold image
contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
轮廓绘制
一旦检测到轮廓,就可以通过使用 cv2.drawContours() 在原始图像或空白画布上绘制轮廓来实现可视化。这有助于可视化对象的边界。
# Draw the largest contour on both images
cv2.drawContours(image, contours, -1, (14, 21, 239), 5)
Here’s is an Example
# Read the image
image = cv2.imread('shapes_2.jpg')
# Convert to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Apply thresholding to create a binary image
_, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
# Find contours in the binary image
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create copies of the original image to draw contours
contour_image = image.copy()
contour_on_original = image.copy()
# Draw the contours on the binary (black) image
contour_black = np.zeros_like(image)
cv2.drawContours(contour_black, contours, -1, (0, 255, 0), 2)
# Draw the contours on the original image
cv2.drawContours(contour_on_original, contours, -1, (255,0, 0), 5)
输出如下:

cv2.findContours() 函数
cv2.findContours() 方法是 OpenCV 库中用于检测图像轮廓的重要函数。下面,我将详细解释此函数,介绍其所有参数及其用途。
以下是函数定义:
contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
该函数接受各种参数/参数,如下所述。
- image-图像 (输入图像)
这是要从中找到轮廓的源图像。输入图像必须是二进制图像,这意味着它应该只包含两个像素值,通常为 0(黑色)表示背景,255(白色)表示对象。
通常,这是通过使用阈值或边缘检测来创建合适的二进制图像来实现的。请注意,cv2.findContours() 会修改输入图像,因此如果您需要保留原始图像,通常建议传递副本。
- mode-模式 (轮廓检索模式)
此参数指定如何检索轮廓。它控制轮廓的层次结构,对于嵌套或重叠对象特别有用。它包括 cv2.RETR_EXTERNAL、cv2.RETR_LIST、cv2.RETR_CCOMP、cv2.RETR_TREE 等模式。所有这些模式都在下面的“轮廓检索模式”部分中进行了说明。
- method-方法 (轮廓近似法)
此参数控制轮廓点的近似方法,本质上影响精度和用于表示轮廓的点数。常用方法包括 cv2.CHAIN_APPROX_NONE、cv2.CHAIN_APPROX_SIMPLE、cv2.CHAIN_APPROX_TC89_L1、cv2.CHAIN_APPROX_TC89_KCOS。这些方法在下面的“轮廓近似方法”部分中解释。
- contours-轮廓 (输出参数)
这是所有检测到的轮廓的输出列表。每个单独的轮廓都表示为定义轮廓边界的点(x,y 坐标)的 NumPy 数组。轮廓通常存储为包含一个或多个轮廓的 Python 列表。
- hierarchy-层次结构 (输出参数)
这是一个可选的输出参数,存储了轮廓之间的层级关系信息,每条轮廓有四条信息[Next, Previous, First_Child, Parent]。
下一个:同一层次结构的下一个轮廓的索引。
上一个:同一层次结构中前一个轮廓的索引。
First_Child:第一个子轮廓的索引。
父级:父轮廓的索引。
如果值为 -1,则表示没有相应的轮廓(例如,没有父轮廓或没有子轮廓)。
- offset-偏移量(可选)
此参数用于将轮廓点移动一定偏移量(x 和 y 值的元组)。如果您正在处理子图像并需要将检测到的轮廓移回以匹配其在原始图像中的位置,这将非常有用。它在大多数常见场景中很少使用。
- 该函数返回以下值。
contours-轮廓:检测到的轮廓列表。
hierarchy-层次结构:轮廓的层次结构,如果您需要了解轮廓之间的父子关系,这很有用。
OpenCV 中的轮廓检索模式
OpenCV 的 cv2.findContours() 函数中的轮廓检索模式控制轮廓的检索方式,包括是否考虑轮廓的层次结构(即父子关系)。此模式对于确定检测到的轮廓的结构及其在图像中的关系至关重要,尤其是在处理嵌套或重叠对象时。对于本节中的示例,我们将创建合成二进制图像,其中包含四个嵌套的黑白交替同心圆。

OpenCV 中有四种主要的轮廓检索模式。让我们分别了解一下。
cv2.RETR_EXTERNAL
此模式用于仅检索最外层轮廓。它会忽略包含在另一个轮廓(轮廓的层次结构级别)内的任何轮廓。它实际上只考虑外部边界,而忽略内部嵌套轮廓。
如果您只对物体的外部边界感兴趣,例如检测桌子上的硬币或将物体与背景隔离,此模式非常有用。当嵌套轮廓与您的分析无关时,此模式非常有效。
例如,在我们输入的嵌套圆圈图像中,cv2.RETR_EXTERNAL 将仅检索外圆,而忽略内圆。
以下是我们可以用来处理上述图像的代码:
# Find contours using cv2.RETR_EXTERNAL
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Convert image to color for visualization
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Draw the outer contours in green
cv2.drawContours(image_color, contours, -1, (0, 255, 0), 2)
在上面的代码中,cv2.RETR_EXTERNAL 模式仅查找最外层轮廓(完全忽略内层圆)。外层轮廓用绿色绘制。下图左侧显示代码返回的输出,其中仅检测到外层圆,右侧显示 cv2.findContours() 函数返回的层次结构,其中只有一个轮廓(针对外层圆)。在这种情况下,所有四个值(即下一个、上一个、第一个子项、父项)均为 -1,这意味着轮廓完全孤立,没有层次关系。

cv2.RETR_LIST
此模式用于检索图像中的所有轮廓,但不建立它们之间的任何父子关系。所有轮廓都存储在一个简单的列表中。此模式本质上使层次结构扁平化。它不区分外部和内部轮廓,并将每个轮廓视为独立的,没有层次分组。
当您不需要有关不同轮廓之间关系的信息而只需要整个轮廓集时,此模式很有用,例如分析图像中每个对象的形状或边界。
例如,在同样的嵌套圆的绘制中,cv2.RETR_LIST 会把所有的外圆和内圆都取出来,但是不会定义任何层级关系(父子关系或者多级关系),每个圆都是独立处理的。以下是示例代码。
# Find contours using cv2.RETR_LIST
contours, _ = cv2.findContours(image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# Convert image to color for visualization
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Draw all contours in green
cv2.drawContours(image_color, contours, -1, (0, 255, 0), 2)
cv2.RETR_LIST 模式检测所有轮廓,并单独处理每个轮廓(外圆以及所有内圆)。轮廓以绿色绘制。
下图显示了代码的输出(左侧),检测到了所有圆。图像的右侧显示了 cv2.findContours() 函数返回的层次结构,该层次结构表示图像中检测到的四个轮廓之间的关系,其中每个轮廓都处于同一级别(级别 0)并且没有父轮廓或子轮廓。此处,在层次结构中,所有轮廓的“第一个子轮廓”和“父轮廓”值均为 -1,表示未检测到父子关系。

cv2.RETR_CCOMP
此模式用于检索所有轮廓,并将它们组织成两级层次结构。轮廓按层次排列。层次结构中只有两个级别。第一级表示对象的外部边界,第二级表示对象内部的轮廓。
当您需要一个简单的层次结构,并且外部边界和各自的内部对象边界之间有明显区分时,此模式很有用。
例如,在我们的输入图像中,cv2.RETR_CCOMP 将把外圆存储为第一级(级别 0)轮廓,将内圆存储为第二级(级别 1)轮廓。每对轮廓(外轮廓在级别 0,内轮廓在级别 1)都被视为一个组件。以下是示例代码。
# Find contours using cv2.RETR_CCOMP
contours, hierarchy = cv2.findContours(image, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
# Convert image to color for visualization
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Draw contours based on the two-level hierarchy
for i, contour in enumerate(contours):
if hierarchy[0][i][3] == -1: # If it is an outer contour
cv2.drawContours(image_color, [contour], -1, (0, 255, 0), 2) # Green for outer contours
else: # If it is an inner contour (hole)
cv2.drawContours(image_color, [contour], -1, (0, 0, 255), 2) # Blue for inner contours
cv2.RETR_CCOMP 模式查找轮廓并将其组织成两级层次结构。下图显示了代码的输出(左侧),检测到两级层次结构中的所有圆。图像的右侧显示了 cv2.findContours() 函数返回的层次结构,该层次结构表示四个轮廓之间的关系,但与前面的示例不同,此层次结构包括父子关系。

cv2.RETR_TREE
此模式用于检索所有轮廓并将它们组织成完整的层次结构(即树结构)。此模式提供轮廓之间层次关系的最全面表示,包括父轮廓、子轮廓、兄弟轮廓和嵌套轮廓。所有轮廓都以完整的父子关系组织,这意味着不仅跟踪外部和内部轮廓,还跟踪这些内部轮廓中存在的任何嵌套。这形成了一个完整的树状层次结构。
此模式适用于需要了解嵌套对象完整结构的复杂图像,例如分析文档中的轮廓或了解重叠形状。它提供有关轮廓如何相互嵌套的详细信息。
例如,对于嵌套圆,cv2.RETR_TREE 会将它们组织成一个完整的树结构,其中最大的圆是根(父圆),每个连续的小圆都是前一个圆的子圆。以下是示例代码。
# Find contours using cv2.RETR_TREE
contours, hierarchy = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Convert image to color for visualization
image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Draw contours based on hierarchy levels
for i, contour in enumerate(contours):
level = 0
# Traverse the hierarchy to determine the level of the contour
parent = hierarchy[0][i][3]
while parent != -1:
level += 1
parent = hierarchy[0][parent][3]
# Use different colors for the first and second levels
if level == 0:
cv2.drawContours(image_color, [contour], -1, (0, 255, 0), 2) # Green for first level (outermost)
elif level == 1:
cv2.drawContours(image_color, [contour], -1, (0, 0, 255), 2) # Red for second level (inner)
else:
cv2.drawContours(image_color, [contour], -1, (255, 0, 0), 2) # Blue for deeper levels
cv2.RETR_TREE 模式会找到所有轮廓,并将它们组织成具有完整层次结构的多个级别。最大的外圆是根(父圆),第二个圆是最大外圆的子圆,第三个内圆是第二个圆的子圆,依此类推。
下图显示了代码的输出(左侧),该代码检测到了所有圆的完整层次结构。图像的右侧显示了 cv2.findContours() 函数返回的层次结构,该层次结构表示具有父子关系的四个轮廓之间的关系。它创建了一个嵌套轮廓链,其中每个轮廓都包含下一个轮廓,形成一直到第四个轮廓的父子关系。每个轮廓都嵌套在前一个轮廓内,形成严格的父子链。同一级别没有“兄弟”轮廓,每个轮廓都是前一个轮廓的子轮廓。这种层次结构在处理同心形状或其他形式时很常见,其中一个轮廓以严格嵌套的方式包围另一个轮廓。

轮廓法比较
以下是所有模式的比较:

cv2.RETR_EXTERNAL:仅检索最外层轮廓,忽略内层轮廓。
cv2.RETR_LIST:所有轮廓都被独立处理,并且不应用任何层次结构。
cv2.RETR_CCOMP:轮廓被组织成两级层次结构。
cv2.RETR_TREE:轮廓被组织成完整的层次结构,显示嵌套轮廓的所有父子关系。
轮廓检索模式用于确定如何在图像中提取和组织轮廓。它允许控制轮廓之间的层次关系,例如区分外部边界和嵌套结构。
轮廓逼近法
OpenCV 的 cv2.findContours() 函数中的轮廓近似方法决定了如何通过近似轮廓点来表示轮廓,这直接影响轮廓表示的存储和精度。通过使用不同的近似方法,可以控制用于表示轮廓的点数,在内存效率和轮廓精度之间取得平衡。
cv2.findContours() 函数提供了不同的轮廓近似方法,这些方法决定了检测到的边界点的表示方式。让我们详细看看这些方法。
轮廓近似方法由 cv2.findContours() 中的 method 参数定义。它们控制轮廓边界点的存储精度。常用方法有:
cv2.CHAIN_APPROX_NONE
此方法存储轮廓边界上的所有点。通过保留轮廓上的每个点,它提供了轮廓的最详细表示。由于存储了每个边界点,因此此方法占用的内存最多,并且计算成本很高。
cv2.CHAIN_APPROX_NONE 适用于需要高度精确的轮廓表示(例如需要精确测量时)。它还可用于涉及轮廓边界上所有点的进一步分析,例如跟踪点或执行变换。
例如,如果您有一个不规则的波浪形轮廓,cv2.CHAIN_APPROX_NONE 将确保捕获轮廓的最小细节。以下是示例代码。
# Draw a star shape
pts = np.array([[200, 50], [220, 150], [300, 150], [240, 200], [260, 300],
[200, 250], [140, 300], [160, 200], [100, 150], [180, 150]], np.int32)
pts = pts.reshape((-1, 1, 2))
cv2.polylines(image, [pts], isClosed=True, color=255, thickness=2)
# Find contours using cv2.CHAIN_APPROX_NONE
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
# Convert image to color for visualization
image_contour = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for contours
image_points = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for points
# Draw the contours in green on the contour image
for contour in contours:
cv2.drawContours(image_contour, [contour], -1, (0, 255, 0), 2) # Draw contours in green
# Draw the points in red on the points image
for contour in contours:
for point in contour:
cv2.circle(image_points, tuple(point[0]), 1, (255, 0, 0), -1) # Red dots for points

此代码可让您看到 cv2.CHAIN_APPROX_NONE 如何捕获并显示轮廓边界上的所有点,从而展示其表示形状的精确度。您可以清楚地看到代表整个轮廓的绿线。
红点表示沿轮廓存储的每个点,强调 cv2.CHAIN_APPROX_NONE 捕获每个点,即使是沿直线和曲线。这种方法允许您验证所有点确实已存储和可视化,突出了 cv2.CHAIN_APPROX_NONE 的精度和细节。
cv2.CHAIN_APPROX_SIMPLE
该方法通过删除直线上的所有冗余点来压缩轮廓的表示。它仅存储每个段的端点,从而有效地用更少的点近似轮廓。这显著减少了内存使用量,同时保留了轮廓的一般形状。
它最常用于典型的轮廓检测任务,在这些任务中,您需要物体的整体形状,但不需要边界上的每个细节。适用于检测简单形状(例如矩形、圆形),这些形状只需要关键点来表示轮廓的结构。
例如,如果您有一个矩形对象,CHAIN_APPROX_SIMPLE 将仅存储四个角,消除边缘上的冗余点。以下是示例代码。
# Create a synthetic image with a rectangle
image = np.zeros((400, 400), dtype=np.uint8)
cv2.rectangle(image, (100, 100), (300, 300), 255, -1) # Draw a filled rectangle
# Find contours using cv2.CHAIN_APPROX_SIMPLE
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Convert image to color for visualization
image_contours = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
image_keypoints = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
# Draw the simplified contours (keypoints only) on `image_contours`
for contour in contours:
# Draw contours in green
cv2.drawContours(image_contours, [contour], -1, (0, 255, 0), 2)
# Draw keypoints (points of the contour) on `image_keypoints`
for contour in contours:
for point in contour:
# Draw key points (red dots) for each contour
cv2.circle(image_keypoints, tuple(point[0]), 5, (0, 0, 255), -1)

对于矩形等简单形状,cv2.CHAIN_APPROX_SIMPLE 将轮廓点减少到仅角落,有效地显示了此方法的效率。通过消除冗余点,此方法减少了内存使用量,非常适合整体形状比精确边界细节更重要的场景。
cv2.CHAIN_APPROX_TC89_L1
该方法使用 Teh-Chin 链近似算法,与 CHAIN_APPROX_NONE 相比,该算法使用更少的点来近似轮廓。
这是一种中间方法,试图在轮廓精度和点数之间取得平衡。CHAIN_APPROX_TC89_L1 提供了一种优化的表示,它使用的点比 CHAIN_APPROX_NONE 少,但可能比 CHAIN_APPROX_SIMPLE 保留更多细节。
当您需要优化内存使用但与 CHAIN_APPROX_SIMPLE 相比仍需要更高的精度时很有用。
例如,如果您需要平滑的轮廓表示以进行跟踪或用于涉及简化波浪边缘的应用程序,则 CHAIN_APPROX_TC89_L1 可能是一个合适的选择。以下是示例代码。
# Create a synthetic image with a wavy contour (e.g., sinusoidal wave shape)
image = np.zeros((400, 400), dtype=np.uint8)
# Draw a wavy contour using a sinusoidal function
for x in range(50, 350):
y = int(200 + 50 * np.sin(x * np.pi / 50))
cv2.circle(image, (x, y), 2, 255, -1)
# Find contours using cv2.CHAIN_APPROX_TC89_KCOS
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_L1)
# Convert the image to color for different visualizations
image_contour = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for contours
image_points = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for points
# Draw the contours on the contour image
for contour in contours:
cv2.drawContours(image_contour, [contour], -1, (0, 255, 0), 2) # Draw contours in green
# Draw the points on the points image
for contour in contours:
for point in contour:
cv2.circle(image_points, tuple(point[0]), 1, (0, 0, 255), -1) # Red dots for key points

与 cv2.CHAIN_APPROX_NONE 相比,此方法使用更少的点,同时保留了大量细节。它对于不需要完整细节但又不理想的波浪形或不规则形状特别有用。结果显示了 cv2.CHAIN_APPROX_TC89_L1 如何提供平滑且优化的轮廓表示,适用于内存效率和轮廓精度都很重要的应用。
cv2.CHAIN_APPROX_TC89_KCOS
与 CHAIN_APPROX_TC89_L1 类似,此方法也使用 Teh-Chin 链近似,但与 CHAIN_APPROX_TC89_L1 相比,近似程度略有不同。此方法通常会稍微更积极地减少点数,同时仍能捕捉轮廓的关键特征。
它减少了点数,但与 CHAIN_APPROX_TC89_L1 相比,确切的行为可能略有不同。当您想要比 CHAIN_APPROX_TC89_L1 更积极的轮廓点压缩并且只需要保留最关键的点时,使用此方法。
例如,当内存和速度至关重要,并且轮廓不是很复杂时,CHAIN_APPROX_TC89_KCOS 可以在效率和轮廓细节之间提供良好的平衡。以下是示例代码。
# Create a synthetic image with a wavy contour (e.g., sinusoidal wave shape)
image = np.zeros((400, 400), dtype=np.uint8)
# Draw a wavy contour using a sinusoidal function
for x in range(50, 350):
y = int(200 + 50 * np.sin(x * np.pi / 50))
cv2.circle(image, (x, y), 2, 255, -1)
# Find contours using cv2.CHAIN_APPROX_TC89_KCOS
contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_TC89_KCOS)
# Convert the image to color for different visualizations
image_contour = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for contours
image_points = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # Image for points
# Draw the contours on the contour image
for contour in contours:
cv2.drawContours(image_contour, [contour], -1, (0, 255, 0), 2) # Draw contours in green
# Draw the points on the points image
for contour in contours:
for point in contour:
cv2.circle(image_points, tuple(point[0]), 1, (0, 0, 255), -1) # Red dots for key points

cv2.CHAIN_APPROX_TC89_KCOS 的输出
与 cv2.CHAIN_APPROX_TC89_L1 相比,此方法可更积极地减少轮廓点,适用于需要最少点数同时保留一般形状的应用。通过进一步减少点数,此方法可优化内存使用率和处理速度,非常适合轮廓不需要高度详细的场景。
轮廓近似方法很重要,因为它有助于控制用于表示轮廓形状的细节级别。通过选择适当的近似方法,您可以在计算效率和给定应用所需的精度之间取得平衡。当轮廓细节级别会影响性能和准确性时,这种灵活性尤其有用。
更多推荐


所有评论(0)