该系列文章是讲解Python OpenCV图像处理知识,前期主要讲解图像入门、OpenCV基础用法,中期讲解图像处理的各种算法,包括图像锐化算子、图像增强技术、图像分割等,后期结合深度学习研究图像识别、图像分类应用。希望文章对您有所帮助,如果有不足之处,还请海涵~
前面一篇文章介绍了图像分类知识,包括常见的图像分类算法,并介绍Python环境下的贝叶斯图像分类算法、基于KNN算法的图像分类和基于神经网络算法的图像分类等案例。这篇文章将详细讲解图像分割知识,包括阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位等。万字长文整理,希望对您有所帮助。 同时,该部分知识均为作者查阅资料撰写总结,并且开设成了收费专栏,为小宝赚点奶粉钱,感谢您的抬爱。当然如果您是在读学生或经济拮据,可以私聊我给你每篇文章开白名单,或者转发原文给你,更希望您能进步,一起加油喔~
代码下载地址(如果喜欢记得star,一定喔):
前文参考:
图像分割是将图像分成若干具有独特性质的区域并提取感兴趣目标的技术和过程,它是图像处理和图像分析的关键步骤。主要分为基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法和基于特定理论的分割方法。本章节将重点围绕图像处理实例,详细讲解各种图像分割的方法。
图像分割(Image Segmentation)技术是计算机视觉领域的重要研究方向,是图像语义理解和图像识别的重要一环。它是指将图像分割成若干具有相似性质的区域的过程,研究方法包括基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法和基于特定理论的分割方法(含图论、聚类、深度语义等)。该技术广泛应用于场景物体分割、人体背景分割、三维重建、车牌识别、人脸识别、无人驾驶、增强现实等行业。如图1所示,它将鲜花颜色划分为四个层级。
图像分割的目标是根据图像中的物体将图像的像素分类,并提取感兴趣的目标。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像索赋予相同的编号。
图像分割是图像识别和计算机视觉至关重要的预处理,没有正确的分割就不可能有正确的识别。图像分割主要依据图像中像素的亮度及颜色,但计算机在自动处理分割时,会遇到各种困难,如光照不均匀、噪声影响、图像中存在不清晰的部分以及阴影等,常常发生图像分割错误。同时,随着深度学习和神经网络的发展,基于深度学习和神经网络的图像分割技术有效提高了分割的准确率,能够较好地解决图像中噪声和不均匀问题。
最常用的图像分割方法是将图像灰度分为不同的等级,然后用设置灰度门限的方法确定有意义的区域或欲分割的物体边界。图像阈值化(Binarization)旨在剔除掉图像中一些低于或高于一定值的像素,从而提取图像中的物体,将图像的背景和噪声区分开来。图像阈值化可以理解为一个简单的图像分割操作,阈值又称为临界值,它的目的是确定出一个范围,然后这个范围内的像素点使用同一种方法处理,而阈值之外的部分则使用另一种处理方法或保持原样。
阈值化处理可以将图像中的像素划分为两类颜色,常见的阈值化算法如公式(1)所示。
在Python的OpenCV库中,提供了固定阈值化函数threshold()和自适应阈值化函数adaptiveThreshold(),将一幅图像进行阈值化处理,前文7.5小节详细介绍了图像阈值化处理方法,下面代码对比了不同阈值化算法的图像分割结果。
# -*- coding: utf-8 -*-
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取图像
img=cv2.imread('scenery.png')
grayImage=cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#阈值化处理
ret,thresh1=cv2.threshold(grayImage,127,255,cv2.THRESH_BINARY)
ret,thresh2=cv2.threshold(grayImage,127,255,cv2.THRESH_BINARY_INV)
ret,thresh3=cv2.threshold(grayImage,127,255,cv2.THRESH_TRUNC)
ret,thresh4=cv2.threshold(grayImage,127,255,cv2.THRESH_TOZERO)
ret,thresh5=cv2.threshold(grayImage,127,255,cv2.THRESH_TOZERO_INV)
#显示结果
titles = ['Gray Image','BINARY','BINARY_INV','TRUNC',
'TOZERO','TOZERO_INV']
images = [grayImage, thresh1, thresh2, thresh3, thresh4, thresh5]
for i in range(6):
plt.subplot(2,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
输出结果如图2所示,它将彩色风景图像转换成五种对应的阈值处理效果,包括二进制阈值化(BINARY)、反二进制阈值化(BINARY_INV)、截断阈值化(THRESH_TRUNC)、阈值化为0(THRESH_TOZERO)、反阈值化为0(THRESH_TOZERO_INV)。
图像中相邻区域之间的像素集合共同构成了图像的边缘。基于边缘检测的图像分割方法是通过确定图像中的边缘轮廓像素,然后将这些像素连接起来构建区域边界的过程。由于沿着图像边缘走向的像素值变化比较平缓,而沿着垂直于边缘走向的像素值变化比较大,根据该特点,通常会采用一阶导数和二阶导数来描述和检测边缘。
在下一篇文章中,我们将详细讲解了Python边缘检测的方法,下面的代码先对比常用的微分算子,如Roberts、Prewitt、Sobel、Laplacian、Scharr、Canny、LOG等算子。
# -*- coding: utf-8 -*-
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取图像
img = cv2.imread('scenery.png')
rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
#灰度化处理图像
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#阈值处理
ret, binary = cv2.threshold(grayImage, 127, 255, cv2.THRESH_BINARY)
#Roberts算子
kernelx = np.array([[-1,0],[0,1]], dtype=int)
kernely = np.array([[0,-1],[1,0]], dtype=int)
x = cv2.filter2D(binary, cv2.CV_16S, kernelx)
y = cv2.filter2D(binary, cv2.CV_16S, kernely)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Roberts = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
#Prewitt算子
kernelx = np.array([[1,1,1],[0,0,0],[-1,-1,-1]], dtype=int)
kernely = np.array([[-1,0,1],[-1,0,1],[-1,0,1]], dtype=int)
x = cv2.filter2D(binary, cv2.CV_16S, kernelx)
y = cv2.filter2D(binary, cv2.CV_16S, kernely)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Prewitt = cv2.addWeighted(absX,0.5,absY,0.5,0)
#Sobel算子
x = cv2.Sobel(binary, cv2.CV_16S, 1, 0)
y = cv2.Sobel(binary, cv2.CV_16S, 0, 1)
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Sobel = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
#拉普拉斯算法
dst = cv2.Laplacian(binary, cv2.CV_16S, ksize = 3)
Laplacian = cv2.convertScaleAbs(dst)
# Scharr算子
x = cv2.Scharr(binary, cv2.CV_32F, 1, 0) #X方向
y = cv2.Scharr(binary, cv2.CV_32F, 0, 1) #Y方向
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Scharr = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
#Canny算子
gaussianBlur = cv2.GaussianBlur(binary, (3,3), 0) #高斯滤波
Canny = cv2.Canny(gaussianBlur , 50, 150)
#LOG算子
gaussianBlur = cv2.GaussianBlur(binary, (3,3), 0) #高斯滤波
dst = cv2.Laplacian(gaussianBlur, cv2.CV_16S, ksize = 3)
LOG = cv2.convertScaleAbs(dst)
#效果图
titles = ['Source Image', 'Binary Image', 'Roberts Image',
'Prewitt Image','Sobel Image', 'Laplacian Image',
'Scharr Image', 'Canny Image', 'LOG Image']
images = [rgb_img, binary, Roberts, Prewitt,
Sobel, Laplacian, Scharr, Canny, LOG]
for i in np.arange(9):
plt.subplot(3,3,i+1),plt.imshow(images[i],'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
输出结果如图3所示,它依次为原始图像、二值化图像、Roberts算子分割图、Prewitt算子分割图、Sobel算子分割图、Laplacian算子分割图、Scharr算子分割图、Canny算子分割图和LOG算子分割图。
下面讲解另一种边缘检测的方法。在OpenCV中,可以通过cv2.findContours()函数从二值图像中寻找轮廓,其函数原型如下所示:
在使用findContours()函数检测图像边缘轮廓后,通常需要和drawContours()函数联合使用,接着绘制检测到的轮廓,drawContours()函数的原型如下:
下面的代码是使用cv2.findContours()检测图像轮廓,并调用cv2.drawContours()函数绘制出轮廓线条。
# -*- coding: utf-8 -*-
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取图像
img = cv2.imread('scenery.png')
rgb_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
#灰度化处理图像
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#阈值化处理
ret, binary = cv2.threshold(grayImage, 0, 255,
cv2.THRESH_BINARY+cv2.THRESH_OTSU)
#边缘检测
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
#轮廓绘制
cv2.drawContours(img, contours, -1, (0, 255, 0), 1)
#显示图像5
cv2.imshow('gray', binary)
cv2.imshow('res', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
图4为图像阈值化处理效果图。
图5为最终提取的风景图的边缘线条。
该小节主要讲解基于图像纹理信息(颜色)、边界信息(反差)和背景信息的图像分割算法。在OpenCV中,GrabCut算法能够有效地利用纹理信息和边界信息分割背景,提取图像目标物体。该算法是微软研究院基于图像分割和抠图的课题,它能有效地将目标图像分割提取,如图6所示。
GrabCut算法原型如下所示:
下面是Python的实现代码,通过调用np.zeros()函数创建掩码、fgbModel和bgModel,接着定义rect矩形范围,调用函数grabCut()实现图像分割。由于该方法会修改掩码,像素会被标记为不同的标志来指明它们是背景或前景。接着将所有的0像素和2像素点赋值为0(背景),而所有的1像素和3像素点赋值为1(前景),完整代码如下所示。
# -*- coding: utf-8 -*-
import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
#读取图像
img = cv2.imread('nv.png')
#灰度化处理图像
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#设置掩码、fgbModel、bgModel
mask = np.zeros(img.shape[:2], np.uint8)
bgdModel = np.zeros((1,65), np.float64)
fgdModel = np.zeros((1,65), np.float64)
#矩形坐标
rect = (100, 100, 500, 800)
#图像分割
cv2.grabCut(img, mask, rect, bgdModel, fgdModel, 5,
cv2.GC_INIT_WITH_RECT)
#设置新掩码:0和2做背景
mask2 = np.where((mask==2)|(mask==0), 0, 1).astype('uint8')
#设置字体
matplotlib.rcParams['font.sans-serif']=['SimHei']
#显示原图
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.subplot(1,2,1)
plt.imshow(img)
plt.title(u'(a)原始图像')
plt.xticks([]), plt.yticks([])
#使用蒙板来获取前景区域
img = img*mask2[:, :, np.newaxis]
plt.subplot(1,2,2)
plt.imshow(img)
plt.title(u'(b)目标图像')
plt.colorbar()
plt.xticks([]), plt.yticks([])
plt.show()
输出图像如图7所示,图7(a)为原始图像,图7(b)为图像分割后提取的目标人物,但人物右部分的背景仍然存在。如何移除这些背景呢?这里需要使用自定义的掩码进行提取,读取一张灰色背景轮廓图,从而分离背景与前景,希望读者下来实现该功能。
K-Means聚类是最常用的聚类算法,最初起源于信号处理,其目标是将数据点划分为K个类簇,找到每个簇的中心并使其度量最小化。该算法的最大优点是简单、便于理解,运算速度较快,缺点是只能应用于连续型数据,并且要在聚类前指定聚集的类簇数。
下面是K-Means聚类算法的分析流程,步骤如下:
图8是对身高和体重进行聚类的算法,将数据集的人群聚集成三类。
在图像处理中,通过K-Means聚类算法可以实现图像分割、图像聚类、图像识别等操作,本小节主要用来进行图像颜色分割。假设存在一张100×100像素的灰度图像,它由10000个RGB灰度级组成,我们通过K-Means可以将这些像素点聚类成K个簇,然后使用每个簇内的质心点来替换簇内所有的像素点,这样就能实现在不改变分辨率的情况下量化压缩图像颜色,实现图像颜色层级分割。
在OpenCV中,Kmeans()函数原型如下所示:
下面使用该方法对灰度图像颜色进行分割处理,需要注意,在进行K-Means聚类操作之前,需要将RGB像素点转换为一维的数组,再将各形式的颜色聚集在一起,形成最终的颜色分割。
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取原始图像灰度颜色
img = cv2.imread('scenery.png', 0)
print(img.shape)
#获取图像高度、宽度和深度
rows, cols = img.shape[:]
#图像二维像素转换为一维
data = img.reshape((rows * cols, 1))
data = np.float32(data)
#定义中心 (type,max_iter,epsilon)
criteria = (cv2.TERM_CRITERIA_EPS +
cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
#设置标签
flags = cv2.KMEANS_RANDOM_CENTERS
#K-Means聚类 聚集成4类
compactness, labels, centers = cv2.kmeans(data, 4, None, criteria, 10, flags)
#生成最终图像
dst = labels.reshape((img.shape[0], img.shape[1]))
#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']
#显示图像
titles = [u'原始图像', u'聚类图像']
images = [img, dst]
for i in range(2):
plt.subplot(1,2,i+1), plt.imshow(images[i], 'gray'),
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
输出结果如图9所示,左边为灰度图像,右边为K-Means聚类后的图像,它将灰度级聚集成四个层级,相似的颜色或区域聚集在一起。
下面代码是对彩色图像进行颜色分割处理,它将彩色图像聚集成2类、4类和64类。
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取原始图像
img = cv2.imread('scenery.png')
print(img.shape)
#图像二维像素转换为一维
data = img.reshape((-1,3))
data = np.float32(data)
#定义中心 (type,max_iter,epsilon)
criteria = (cv2.TERM_CRITERIA_EPS +
cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
#设置标签
flags = cv2.KMEANS_RANDOM_CENTERS
#K-Means聚类 聚集成2类
compactness, labels2, centers2 = cv2.kmeans(data, 2, None, criteria, 10, flags)
#K-Means聚类 聚集成4类
compactness, labels4, centers4 = cv2.kmeans(data, 4, None, criteria, 10, flags)
#K-Means聚类 聚集成8类
compactness, labels8, centers8 = cv2.kmeans(data, 8, None, criteria, 10, flags)
#K-Means聚类 聚集成16类
compactness, labels16, centers16 = cv2.kmeans(data, 16, None, criteria, 10, flags)
#K-Means聚类 聚集成64类
compactness, labels64, centers64 = cv2.kmeans(data, 64, None, criteria, 10, flags)
#图像转换回uint8二维类型
centers2 = np.uint8(centers2)
res = centers2[labels2.flatten()]
dst2 = res.reshape((img.shape))
centers4 = np.uint8(centers4)
res = centers4[labels4.flatten()]
dst4 = res.reshape((img.shape))
centers8 = np.uint8(centers8)
res = centers8[labels8.flatten()]
dst8 = res.reshape((img.shape))
centers16 = np.uint8(centers16)
res = centers16[labels16.flatten()]
dst16 = res.reshape((img.shape))
centers64 = np.uint8(centers64)
res = centers64[labels64.flatten()]
dst64 = res.reshape((img.shape))
#图像转换为RGB显示
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
dst2 = cv2.cvtColor(dst2, cv2.COLOR_BGR2RGB)
dst4 = cv2.cvtColor(dst4, cv2.COLOR_BGR2RGB)
dst8 = cv2.cvtColor(dst8, cv2.COLOR_BGR2RGB)
dst16 = cv2.cvtColor(dst16, cv2.COLOR_BGR2RGB)
dst64 = cv2.cvtColor(dst64, cv2.COLOR_BGR2RGB)
#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']
#显示图像
titles = [u'原始图像', u'聚类图像 K=2', u'聚类图像 K=4',
u'聚类图像 K=8', u'聚类图像 K=16', u'聚类图像 K=64']
images = [img, dst2, dst4, dst8, dst16, dst64]
for i in range(6):
plt.subplot(2,3,i+1), plt.imshow(images[i], 'gray'),
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
输出结果如图10所示,它对比了原始图像和各K-Means聚类处理后的图像。当K=2时,聚集成2种颜色;当K=4时,聚集成4种颜色;当K=8时,聚集成8种颜色;当K=16时,聚集成16种颜色;当K=64时,聚集成64种颜色。
均值漂移(Mean Shfit)算法是一种通用的聚类算法,最早是1975年Fukunaga等人在一篇关于概率密度梯度函数的估计论文中提出[6]。它是一种无参估计算法,沿着概率梯度的上升方向寻找分布的峰值。Mean Shift算法先算出当前点的偏移均值,移动该点到其偏移均值,然后以此为新的起始点,继续移动,直到满足一定的条件结束。
图像分割中可以利用均值漂移算法的特性,实现彩色图像分割。在OpenCV中提供的函数为pyrMeanShiftFiltering(),该函数严格来说并不是图像分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域,所以在OpenCV中它的后缀是滤波“Filter”,而不是分割“segment”。该函数原型如下所示:
均值漂移pyrMeanShiftFiltering()函数的执行过程是如下:
(1) 构建迭代空间。以输入图像上任一点P0为圆心,建立以sp为物理空间半径,sr为色彩空间半径的球形空间,物理空间上坐标为x和y,色彩空间上坐标为RGB或HSV,构成一个空间球体。其中x和y表示图像的长和宽,色彩空间R、G、B在0至255之间。
(2) 求迭代空间的向量并移动迭代空间球体重新计算向量,直至收敛。 在上一步构建的球形空间中,求出所有点相对于中心点的色彩向量之和,移动迭代空间的中心点到该向量的终点,并再次计算该球形空间中所有点的向量之和,如此迭代,直到在最后一个空间球体中所求得向量和的终点就是该空间球体的中心点Pn,迭代结束。
(3) 更新输出图像dst上对应的初始原点P0的色彩值为本轮迭代的终点Pn的色彩值,完成一个点的色彩均值漂移。
(4) 对输入图像src上其他点,依次执行上述三个步骤,直至遍历完所有点后,整个均值偏移色彩滤波完成。
下面的代码是图像均值漂移的实现过程:
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取原始图像灰度颜色
img = cv2.imread('scenery.png')
spatialRad = 100 #空间窗口大小
colorRad = 100 #色彩窗口大小
maxPyrLevel = 2 #金字塔层数
#图像均值漂移分割
dst = cv2.pyrMeanShiftFiltering( img, spatialRad, colorRad, maxPyrLevel)
#显示图像
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
当漂移物理空间半径设置为50,漂移色彩空间半径设置为50,金字塔层数 为2,输出的效果图如图11所示。
当漂移物理空间半径设置为20,漂移色彩空间半径设置为20,金字塔层数 为2,输出的效果图如图12所示。对比可以发现,半径为20时,图像色彩细节大部分存在,半径为50时,森林和水面的色彩细节基本都已经丢失。
写到这里,均值偏移算法对彩色图像的分割平滑操作就完成了,为了达到更好地分割目的,借助漫水填充函数进行下一步处理,在第八部分将详细介绍,这里只是引入该函数。完整代码如下所示:
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取原始图像灰度颜色
img = cv2.imread('scenery.png')
#获取图像行和列
rows, cols = img.shape[:2]
#mask必须行和列都加2且必须为uint8单通道阵列
mask = np.zeros([rows+2, cols+2], np.uint8)
spatialRad = 100 #空间窗口大小
colorRad = 100 #色彩窗口大小
maxPyrLevel = 2 #金字塔层数
#图像均值漂移分割
dst = cv2.pyrMeanShiftFiltering( img, spatialRad, colorRad, maxPyrLevel)
#图像漫水填充处理
cv2.floodFill(dst, mask, (30, 30), (0, 255, 255),
(100, 100, 100), (50, 50, 50),
cv2.FLOODFILL_FIXED_RANGE)
#显示图像
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
输出的效果图如图13所示,它将天空染成黄色。
图像分水岭算法(Watershed Algorithm)是将图像的边缘轮廓转换为“山脉”,将均匀区域转换为“山谷”,从而提升分割效果的算法[3]。分水岭算法是基于拓扑理论的数学形态学的分割方法,灰度图像根据灰度值把像素之间的关系看成山峰和山谷的关系,高亮度(灰度值高)的地方是山峰,低亮度(灰度值低)的地方是山谷。接着给每个孤立的山谷(局部最小值)不同颜色的水(Label),当水涨起来,根据周围的山峰(梯度),不同的山谷也就是不同颜色的像素点开始合并,为了避免这个现象,可以在水要合并的地方建立障碍,直到所有山峰都被淹没。所创建的障碍就是分割结果,这个就是分水岭的原理。
分水岭算法的计算过程是一个迭代标注过程,主要包括排序和淹没两个步骤。由于图像会存在噪声或缺失等问题,该方法会造成分割过度。OpenCV提供了watershed()函数实现图像分水岭算法,并且能够指定需要合并的点,其函数原型如下所示:
下面是分水岭算法实现图像分割的过程。假设存在一幅彩色硬币图像,如图14所示,硬币相互之间挨着。
第一步,通过图像灰度化和阈值化处理提取图像灰度轮廓,采用OTSU二值化处理获取硬币的轮廓。
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import numpy as np
import cv2
from matplotlib import pyplot as plt
#读取原始图像
img = cv2.imread('test01.png')
#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#图像阈值化处理
ret, thresh = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
#显示图像
cv2.imshow('src', img)
cv2.imshow('res', thresh)
cv2.waitKey()
cv2.destroyAllWindows()
输出结果如图15所示。
第二步,通过形态学开运算过滤掉小的白色噪声。同时,由于图像中的硬币是紧挨着的,所以不能采用图像腐蚀去掉边缘的像素,而是选择距离转换,配合一个适当的阈值进行物体提取。这里引入一个图像膨胀操作,将目标边缘扩展到背景,以确定结果的背景区域。
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import numpy as np
import cv2
from matplotlib import pyplot as plt
#读取原始图像
img = cv2.imread('test01.png')
#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#图像阈值化处理
ret, thresh = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
#图像开运算消除噪声
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
#图像膨胀操作确定背景区域
sure_bg = cv2.dilate(opening,kernel,iterations=3)
#距离运算确定前景区域
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
#寻找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']
#显示图像
titles = [u'原始图像', u'阈值化', u'开运算',
u'背景区域', u'前景区域', u'未知区域']
images = [img, thresh, opening, sure_bg, sure_fg, unknown]
for i in range(6):
plt.subplot(2,3,i+1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
输出结果如图16所示,包括原始图像、阈值化处理、开运算、背景区域、前景区域、未知区域等。由图可知,在使用阈值过滤的图像里,确认了图像的硬币区域,而在有些情况,可能对前景分割更感兴趣,而不关心目标是否需要分开或挨着,那时可以采用腐蚀操作来求解前景区域。
第三步,当前处理结果中,已经能够区分出前景硬币区域和背景区域。接着我们创建标记变量,在该变量中标记区域,已确认的区域(前景或背景)用不同的正整数标记出来,不确认的区域保持0,使用cv2.connectedComponents()函数来将图像背景标记成0,其他目标用从1开始的整数标记。注意,如果背景被标记成0,分水岭算法会认为它是未知区域,所以要用不同的整数来标记。
最后,调用watershed()函数实现分水岭图像分割,标记图像会被修改,边界区域会被标记成0,完整代码如所示。
# coding: utf-8
# 2021-05-17 Eastmount CSDN
import numpy as np
import cv2
from matplotlib import pyplot as plt
#读取原始图像
img = cv2.imread('test01.png')
#图像灰度化处理
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#图像阈值化处理
ret, thresh = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
#图像开运算消除噪声
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
#图像膨胀操作确定背景区域
sure_bg = cv2.dilate(opening,kernel,iterations=3)
#距离运算确定前景区域
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform, 0.7*dist_transform.max(), 255, 0)
#寻找未知区域
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg, sure_fg)
#标记变量
ret, markers = cv2.connectedComponents(sure_fg)
#所有标签加一,以确保背景不是0而是1
markers = markers+1
#用0标记未知区域
markers[unknown==255]=0
#分水岭算法实现图像分割
markers = cv2.watershed(img, markers)
img[markers == -1] = [255,0,0]
#用来正常显示中文标签
plt.rcParams['font.sans-serif']=['SimHei']
#显示图像
titles = [u'标记区域', u'图像分割']
images = [markers, img]
for i in range(2):
plt.subplot(1,2,i+1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]),plt.yticks([])
plt.show()
最终分水岭算法的图像分割如图17所示,它将硬币的轮廓成功提取。
图18是采用分水岭算法提取图像Windows中心轮廓的效果图。
分水岭算法对微弱边缘具有良好的响应,图像中的噪声、物体表面细微的灰度变化,都会产生过度分割的现象。但同时应当看出,分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证。另外,分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。
图像漫水填充(FloodFill)是指用一种特定的颜色填充联通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果。漫水填充通常被用来标记或分离图像的一部分以便对其进行深入的处理或分析。本书将该知识点划分为图像分割的一种特殊案例。
图像漫水填充主要是遴选出与种子点联通且颜色相近的像素点,接着对像素点的值进行处理。如果遇到掩码,则根据掩码进行处理。其原理类似Photoshop的魔术棒选择工具,漫水填充将查找和种子点联通的颜色相同的点,而魔术棒选择工具是查找和种子点联通的颜色相近的点,将和初始种子像素颜色相近的点压进栈作为新种子。
基本工作步骤如下:
在OpenCV中,主要通过floodFill()函数实现漫水填充分割,它将用指定的颜色从种子点开始填充一个连接域。其函数原型如下所示:
在Python和OpenCV实现代码中,它设置种子点位置为(10,200);设置颜色为黄色(0,255,255);连通区范围设定为loDiff和upDiff;标记参数设置为CV_FLOODFILL_FIXED_RANGE ,它表示待处理的像素点与种子点作比较,在范围之内,则填充此像素,即种子漫水填充满足:
最终完整代码如下:
#coding:utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
#读取原始图像
img = cv2.imread('test.png')
#获取图像行和列
rows, cols = img.shape[:2]
#目标图像
dst = img.copy()
#mask必须行和列都加2且必须为uint8单通道阵列
#mask多出来的2可以保证扫描的边界上的像素都会被处理
mask = np.zeros([rows+2, cols+2], np.uint8)
#图像漫水填充处理
#种子点位置(30,30) 设置颜色(0,255,255) 连通区范围设定loDiff upDiff
#src(seed.x, seed.y) - loDiff <= src(x, y) <= src(seed.x, seed.y) +upDiff
cv2.floodFill(dst, mask, (30, 30), (0, 255, 255),
(100, 100, 100), (50, 50, 50),
cv2.FLOODFILL_FIXED_RANGE)
#显示图像
cv2.imshow('src', img)
cv2.imshow('dst', dst)
cv2.waitKey()
cv2.destroyAllWindows()
输出结果如图19所示,左边为原始图像,右边为将Windows图标周围填充为黄色的图像。
下面补充另一段代码,它将打开一幅图像,点击鼠标选择种子节点,移动滚动条设定连通区范围的loDiff和upDiff值,并产生动态的漫水填充分割。注意,该部分代码中涉及鼠标、键盘、滚动条等操作,希望读者下来学习相关知识,本书更多是讲解Python图像处理的算法原理及代码实现。
# coding:utf-8
# 2021-05-17 Eastmount CSDN
import cv2
import random
import sys
import numpy as np
#使用说明 点击鼠标选择种子点
help_message = '''USAGE: floodfill.py []
Click on the image to set seed point
Keys:
f - toggle floating range
c - toggle 4/8 connectivity
ESC - exit
'''
if __name__ == '__main__':
#输出提示文本
print(help_message)
#读取原始图像
img = cv2.imread('nv.png')
#获取图像高和宽
h, w = img.shape[:2]
#设置掩码 长和宽都比输入图像多两个像素点
mask = np.zeros((h+2, w+2), np.uint8)
#设置种子节点和4邻接
seed_pt = None
fixed_range = True
connectivity = 4
#图像漫水填充分割更新函数
def update(dummy=None):
if seed_pt is None:
cv2.imshow('floodfill', img)
return
#建立图像副本并漫水填充
flooded = img.copy()
mask[:] = 0 #掩码初始为全0
lo = cv2.getTrackbarPos('lo', 'floodfill') #观察点像素邻域负差最大值
hi = cv2.getTrackbarPos('hi', 'floodfill') #观察点像素邻域正差最大值
print('lo=', lo, 'hi=', hi)
#低位比特包含连通值 4 (缺省) 或 8
flags = connectivity
#考虑当前象素与种子象素之间的差(高比特也可以为0)
if fixed_range:
flags |= cv2.FLOODFILL_FIXED_RANGE
#以白色进行漫水填充
cv2.floodFill(flooded, mask, seed_pt,
(random.randint(0,255), random.randint(0,255),
random.randint(0,255)), (lo,)*3, (hi,)*3, flags)
#选定基准点用红色圆点标出
cv2.circle(flooded, seed_pt, 2, (0, 0, 255), -1)
print("send_pt=", seed_pt)
#显示图像
cv2.imshow('floodfill', flooded)
#鼠标响应函数
def onmouse(event, x, y, flags, param):
global seed_pt #基准点
#鼠标左键响应选择漫水填充基准点
if flags & cv2.EVENT_FLAG_LBUTTON:
seed_pt = x, y
update()
#执行图像漫水填充分割更新操作
update()
#鼠标更新操作
cv2.setMouseCallback('floodfill', onmouse)
#设置进度条
cv2.createTrackbar('lo', 'floodfill', 20, 255, update)
cv2.createTrackbar('hi', 'floodfill', 20, 255, update)
#按键响应操作
while True:
ch = 0xFF & cv2.waitKey()
#退出
if ch == 27:
break
#选定时flags的高位比特位0
#此时邻域的选定为当前像素与相邻像素的差, 联通区域会很大
if ch == ord('f'):
fixed_range = not fixed_range
print('using %s range' % ('floating', 'fixed')[fixed_range])
update()
#选择4方向或则8方向种子扩散
if ch == ord('c'):
connectivity = 12-connectivity
print('connectivity =', connectivity)
update()
cv2.destroyAllWindows()
当鼠标选定的种子点为(242,96),观察点像素邻域负差最大值“lo”为138,观察点像素邻域正差最大值“hi”为147时,图像漫水填充效果如图20所示,它将天空和中心水面填充成黄色。
当鼠标选定的种子点为(328, 202),观察点像素邻域负差最大值“lo”为142,观察点像素邻域正差最大值“hi”为45时,图像漫水填充效果如图21所示,它将图像两旁的森林和水面填充成蓝紫色。
女神的填充如图22所示,哈哈。
接下来讲述一个定位文字区域并进行文字提取的案例,下一篇文章将详细介绍,作者水族文字识别课题用到了相关算法。该算法依次经历如下步骤:
完整代码如下所示:
# coding:utf8
# 2021-05-17 Eastmount CSDN
import cv2
import numpy as np
import matplotlib.pyplot as plt
#读取原始图像
img = cv2.imread("word.png" )
#中值滤波去除噪声
median = cv2.medianBlur(img, 3)
#转换成灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
#Sobel算子锐化处理
sobel = cv2.Sobel(gray, cv2.CV_8U, 1, 0, ksize = 3)
#图像二值化处理
ret, binary = cv2.threshold(sobel, 0, 255,
cv2.THRESH_OTSU+cv2.THRESH_BINARY)
#膨胀和腐蚀处理
#设置膨胀和腐蚀操作的核函数
element1 = cv2.getStructuringElement(cv2.MORPH_RECT, (30, 9))
element2 = cv2.getStructuringElement(cv2.MORPH_RECT, (24, 6))
#膨胀突出轮廓
dilation = cv2.dilate(binary, element2, iterations = 1)
#腐蚀去掉细节
erosion = cv2.erode(dilation, element1, iterations = 1)
#查找文字轮廓
region = []
contours, hierarchy = cv2.findContours(erosion,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
#筛选面积
for i in range(len(contours)):
#遍历所有轮廓
cnt = contours[i]
#计算轮廓面积
area = cv2.contourArea(cnt)
#寻找最小矩形
rect = cv2.minAreaRect(cnt)
#轮廓的四个点坐标
box = cv2.boxPoints(rect)
box = np.int0(box)
# 计算高和宽
height = abs(box[0][1] - box[2][1])
width = abs(box[0][0] - box[2][0])
#过滤太细矩形
if(height > width * 1.5):
continue
region.append(box)
#定位的文字用绿线绘制轮廓
for box in region:
print(box)
cv2.drawContours(img, [box], 0, (0, 255, 0), 2)
#显示图像
cv2.imshow('Median Blur', median)
cv2.imshow('Gray Image', gray)
cv2.imshow('Sobel Image', sobel)
cv2.imshow('Binary Image', binary)
cv2.imshow('Dilation Image', dilation)
cv2.imshow('Erosion Image', erosion)
cv2.imshow('Result Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
第一步,将原始图像进行中值滤波去噪处理,得到如图23所示的图像(作者个人网站博客)。
第二步,将彩色图像转换成灰度图像,如图24所示。
第三步,通过Sobel算子提取文字的基本轮廓线条,如图25所示。
第四步,二值化处理将图像转换为黑色和白色两种像素级,如图26所示。
第五步,通过膨胀处理扩大文字轮廓,腐蚀处理过滤图像的细节,处理效果分别如图27和图28所示。
最后,调用findContours()函数寻找轮廓,并过滤掉面积异常区域,采用函数drawContours()绘制文字轮廓,最终输出如图29所示的图像,它有效地将原图中所有文字区域定位并提取出来。
该方法是图像分割和图像识别前的重要环节,可以广泛应用于文字识别、车牌提取、区域定位等领域。
写到这里,本文就介绍完毕。本文主要讲解了常用的图像分割方法,包括基于阈值的图像分割方法、基于边缘检测的图像分割方法、基于纹理背景的图像分割方法和基于特定理论的图像分割方法。其中,基于特定理论的分割方法又分别讲解了基于K-Means聚类、均值漂移、分水岭算法的图像分割方法。最后通过漫水填充分割和文字区域定位案例加深了读者的印象。希望读者能结合本章知识点,围绕自己的研究领域或工程项目进行深入的学习,实现所需的图像处理特效。
源代码下载地址,记得帮忙点star和关注喔。
大学之道在明明德,
在亲民,在止于至善。
这周又回答了很多博友的问题,有大一学生的困惑,有论文的咨询,也有老乡和考博的疑问,还有无数博友奋斗路上的相互勉励。虽然自己早已忙成狗,但总忍不住去解答别人的问题。最后那一句感谢和祝福,永远是我最大的满足。虽然会花费我一些时间,但也挺好的,无所谓了,跟着心走。不负遇见,感恩同行。莫愁前路无知己,继续加油。晚安娜和珞。
(By:Eastmount 2021-05-18 晚上12点 http://blog.csdn.net/eastmount/ )
参考文献: