一、轮廓检测提取图像前景
背景和介绍
前景提取是计算机视觉领域中非常流行的任务。使用前景提取方法,我们尝试提取任何我们感兴趣的图像或对象,并丢弃其余的背景。最近基于深度学习的图像分割技术使这变得非常容易。但我们也可以使用纯计算机视觉技术来实现这一点。
在基于计算机视觉的图像前景提取方面,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 轮廓检测进行前景提取的编码结束。下一步是执行代码并分析输出。 开发板商城 天皓智联 TB上有视觉设备哦 支持AI相关~ 大模型相关也可用
执行代码并分析输出
现在是时候看看执行代码后我们会得到什么结果了。
我希望你已经下载了输入图像。我们将从图片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
三、火焰检测
完整项目源码下载:
https://github.com/mushfiq1998/fire-detection-python-opencv?source=post_page-----e55c8fc6fa54--------------------------------
项目结构:
fireDetection.py
import cv2 # Library for openCV
import threading # Library for threading -- which allows code to run in backend
import playsound # Library for alarm sound
import smtplib # Library for email sending
# To access xml file which includes positive and negative images of fire.
# (Trained images) File is also provided with the code.
fire_cascade = cv2.CascadeClassifier('fire_detection_cascade_model.xml')
vid = cv2.VideoCapture("videos\\fire2.mp4")
runOnce = False # created boolean
# defined function to play alarm post fire detection using threading
def play_alarm_sound_function():
# to play alarm # mp3 audio file is also provided with the code.
playsound.playsound('fire_alarm.mp3',True)
print("Fire alarm end") # to print in consol
# Defined function to send mail post fire detection using threading
def send_mail_function():
recipientmail = "add recipients mail" # recipients mail
recipientmail = recipientmail.lower() # To lower case mail
try:
server = smtplib.SMTP('smtp.gmail.com', 587)
server.ehlo()
server.starttls()
# Senders mail ID and password
server.login("add senders mail", 'add senders password')
# recipients mail with mail message
server.sendmail('add recipients mail', recipientmail, "Warning fire accident has been reported")
# to print in consol to whome mail is sent
print("Alert mail sent sucesfully to {}".format(recipientmail))
server.close() ## To close server
except Exception as e:
print(e) # To print error if any
while(True):
Alarm_Status = False
# Value in ret is True # To read video frame
ret, frame = vid.read()
# To convert frame into gray color
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# to provide frame resolution
fire = fire_cascade.detectMultiScale(frame, 1.2, 5)
## to highlight fire with square
for (x,y,w,h) in fire:
cv2.rectangle(frame,(x-20,y-20),(x+w+20,y+h+20),(255,0,0),2)
roi_gray = gray[y:y+h, x:x+w]
roi_color = frame[y:y+h, x:x+w]
print("Fire alarm initiated")
# To call alarm thread
threading.Thread(target=play_alarm_sound_function).start()
if runOnce == False:
print("Mail send initiated")
# To call alarm thread
threading.Thread(target=send_mail_function).start()
runOnce = True
if runOnce == True:
print("Mail is already sent once")
runOnce = True
cv2.imshow('frame', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
要运行该项目,请完成以下步骤:
创建虚拟环境:
python -m venv myenv
开发板 激活虚拟环境:
myenv\Scripts\activate
安装playsound:
pip install playsound
更新pip版本:
安装OpenCV:
pip install opencv-python
运行脚本fireDetection.py:
现在,我们将从网络摄像头捕获视频。
在下图中,我们可以看到系统正在检测打火机火焰绘制边界框的效果:
我们的系统从上述视频中检测到火灾,绘制带有警报的矩形框
fireDetection.py文件中的代码说明:
上面代码的解释
此 Python 代码是使用 OpenCV、线程、声音和电子邮件功能的火灾探测系统的简单示例。以下是它的功能的简单描述:
1. 导入库:代码首先导入必要的库:
cv2:用于图像和视频处理,特别是用于检测火灾。
threading:用于同时运行代码的某些部分(在后台)。
playsound:用于播放报警声音。
smtplib:用于发送电子邮件。
2. 加载训练模型:代码加载预训练的机器学习模型(XML 文件),该模型可以检测图像中的火灾。
3. 设置视频源:设置视频输入源,可以是笔记本电脑内置摄像头,也可以是外接USB 摄像头。该代码当前配置为从名为“fire2.mp4”的文件中读取视频。
4. play_alarm_sound_function()4. 播放报警声音:定义播放报警声音的函数。该函数在后台运行(线程)并播放名为“fire_alarm.mp3”的警报声音文件。
5. 发送电子邮件:send_mail_function()定义了另一个函数来发送电子邮件。它使用 Gmail 的 SMTP 服务器向指定收件人发送有关火灾检测的警告电子邮件。代码中需要提供发件人的电子邮件和密码。
6. 主循环:主循环处理视频的每一帧。它执行以下操作:
- 将帧转换为灰度以便于处理。
- 使用加载的模型检测框架中的火灾。
- 如果检测到火灾,它会用蓝色矩形突出显示该区域。
- 如果第一次检测到火灾(由 控制runOnce),则会触发警报声并使用线程发送电子邮件。警报和电子邮件功能在后台运行。
- 一旦警报和电子邮件被触发一次,系统就不会为后续发生火灾的帧重复此过程。
7. 显示视频:代码显示处理后的帧,并在检测到的火灾周围绘制矩形。视频将一直显示,直到您按“q”键。
简而言之,此代码读取视频帧,在帧中查找火灾,如果检测到火灾,它会播放警报声音并发送电子邮件警报。它使用单独的线程来播放警报和发送电子邮件,因此这些任务不会阻塞主视频处理循环。
四、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,计数累加