[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)

该系列文章是讲解Python OpenCV图像处理知识,前期主要讲解图像入门、OpenCV基础用法,中期讲解图像处理的各种算法,包括图像锐化算子、图像增强技术、图像分割等,后期结合深度学习研究图像识别、图像分类应用。希望文章对您有所帮助,如果有不足之处,还请海涵~

前面一篇文章介绍了图像分类知识,包括常见的图像分类算法,并介绍Python环境下的贝叶斯图像分类算法、基于KNN算法的图像分类和基于神经网络算法的图像分类等案例。这篇文章将详细讲解图像分割知识,包括阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位等。万字长文整理,希望对您有所帮助。 同时,该部分知识均为作者查阅资料撰写总结,并且开设成了收费专栏,为小宝赚点奶粉钱,感谢您的抬爱。当然如果您是在读学生或经济拮据,可以私聊我给你每篇文章开白名单,或者转发原文给你,更希望您能进步,一起加油喔~

代码下载地址(如果喜欢记得star,一定喔):

  • https://github.com/eastmountyxz/ImageProcessing-Python

文章目录

  • 一.图像分割概述
  • 二.基于阈值的图像分割
  • 三.基于边缘检测的图像分割
  • 四.基于纹理背景的图像分割
  • 五.基于K-Means聚类的区域分割
  • 六.基于均值漂移算法的图像分割
  • 七.基于分水岭算法的图像分割
  • 八.图像漫水填充分割
  • 九.文字区域定位及提取案例
  • 十.本章小结

前文参考:

  • [Python图像处理] 一.图像处理基础知识及OpenCV入门函数
  • [Python图像处理] 二.OpenCV+Numpy库读取与修改像素
  • [Python图像处理] 三.获取图像属性、兴趣ROI区域及通道处理
  • [Python图像处理] 四.图像平滑之均值滤波、方框滤波、高斯滤波及中值滤波
  • [Python图像处理] 五.图像融合、加法运算及图像类型转换
  • [Python图像处理] 六.图像缩放、图像旋转、图像翻转与图像平移
  • [Python图像处理] 七.图像阈值化处理及算法对比
  • [Python图像处理] 八.图像腐蚀与图像膨胀
  • [Python图像处理] 九.形态学之图像开运算、闭运算、梯度运算
  • [Python图像处理] 十.形态学之图像顶帽运算和黑帽运算
  • [Python图像处理] 十一.灰度直方图概念及OpenCV绘制直方图
  • [Python图像处理] 十二.图像几何变换之图像仿射变换、图像透视变换和图像校正
  • [Python图像处理] 十三.基于灰度三维图的图像顶帽运算和黑帽运算
  • [Python图像处理] 十四.基于OpenCV和像素处理的图像灰度化处理
  • [Python图像处理] 十五.图像的灰度线性变换
  • [Python图像处理] 十六.图像的灰度非线性变换之对数变换、伽马变换
  • [Python图像处理] 十七.图像锐化与边缘检测之Roberts算子、Prewitt算子、Sobel算子和Laplacian算子
  • [Python图像处理] 十八.图像锐化与边缘检测之Scharr算子、Canny算子和LOG算子
  • [Python图像处理] 十九.图像分割之基于K-Means聚类的区域分割
  • [Python图像处理] 二十.图像量化处理和采样处理及局部马赛克特效
  • [Python图像处理] 二十一.图像金字塔之图像向下取样和向上取样
  • [Python图像处理] 二十二.Python图像傅里叶变换原理及实现
  • [Python图像处理] 二十三.傅里叶变换之高通滤波和低通滤波
  • [Python图像处理] 二十四.图像特效处理之毛玻璃、浮雕和油漆特效
  • [Python图像处理] 二十五.图像特效处理之素描、怀旧、光照、流年以及滤镜特效
  • [Python图像处理] 二十六.图像分类原理及基于KNN、朴素贝叶斯算法的图像分类案例
  • [Python图像处理] 二十七.OpenGL入门及绘制基本图形(一)
  • [Python图像处理] 二十八.OpenCV快速实现人脸检测及视频中的人脸
  • [Python图像处理] 二十九.MoviePy视频编辑库实现抖音短视频剪切合并操作
  • [Python图像处理] 三十.图像量化及采样处理万字详细总结(推荐)
  • [Python图像处理] 三十一.图像点运算处理两万字详细总结(灰度化处理、阈值化处理)
  • [Python图像处理] 三十二.傅里叶变换(图像去噪)与霍夫变换(特征识别)万字详细总结
  • [Python图像处理] 三十三.图像各种特效处理及原理万字详解(毛玻璃、浮雕、素描、怀旧、流年、滤镜等)
  • [Python图像处理] 三十四.数字图像处理基础与几何图形绘制万字详解(推荐)
  • [Python图像处理] 三十五.OpenCV图像处理入门、算数逻辑运算与图像融合(推荐)
  • [Python图像处理] 三十六.OpenCV图像几何变换万字详解(平移缩放旋转、镜像仿射透视)
  • [Python图像处理] 三十七.OpenCV和Matplotlib绘制直方图万字详解(掩膜直方图、H-S直方图、黑夜白天判断)
  • [Python图像处理] 三十八.OpenCV图像增强万字详解(直方图均衡化、局部直方图均衡化、自动色彩均衡化)
  • [Python图像处理] 三十九.Python图像分类万字详解(贝叶斯图像分类、KNN图像分类、DNN图像分类)
  • [[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)]

图像分割是将图像分成若干具有独特性质的区域并提取感兴趣目标的技术和过程,它是图像处理和图像分析的关键步骤。主要分为基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法和基于特定理论的分割方法。本章节将重点围绕图像处理实例,详细讲解各种图像分割的方法。

一.图像分割概述

图像分割(Image Segmentation)技术是计算机视觉领域的重要研究方向,是图像语义理解和图像识别的重要一环。它是指将图像分割成若干具有相似性质的区域的过程,研究方法包括基于阈值的分割方法、基于区域的分割方法、基于边缘的分割方法和基于特定理论的分割方法(含图论、聚类、深度语义等)。该技术广泛应用于场景物体分割、人体背景分割、三维重建、车牌识别、人脸识别、无人驾驶、增强现实等行业。如图1所示,它将鲜花颜色划分为四个层级。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第1张图片

图像分割的目标是根据图像中的物体将图像的像素分类,并提取感兴趣的目标。从数学角度来看,图像分割是将数字图像划分成互不相交的区域的过程。图像分割的过程也是一个标记过程,即把属于同一区域的像索赋予相同的编号。

图像分割是图像识别和计算机视觉至关重要的预处理,没有正确的分割就不可能有正确的识别。图像分割主要依据图像中像素的亮度及颜色,但计算机在自动处理分割时,会遇到各种困难,如光照不均匀、噪声影响、图像中存在不清晰的部分以及阴影等,常常发生图像分割错误。同时,随着深度学习和神经网络的发展,基于深度学习和神经网络的图像分割技术有效提高了分割的准确率,能够较好地解决图像中噪声和不均匀问题。


二.基于阈值的图像分割

最常用的图像分割方法是将图像灰度分为不同的等级,然后用设置灰度门限的方法确定有意义的区域或欲分割的物体边界。图像阈值化(Binarization)旨在剔除掉图像中一些低于或高于一定值的像素,从而提取图像中的物体,将图像的背景和噪声区分开来。图像阈值化可以理解为一个简单的图像分割操作,阈值又称为临界值,它的目的是确定出一个范围,然后这个范围内的像素点使用同一种方法处理,而阈值之外的部分则使用另一种处理方法或保持原样。

阈值化处理可以将图像中的像素划分为两类颜色,常见的阈值化算法如公式(1)所示。

  • 当某个像素点的灰度Gray(i,j)小于阈值T时,其像素设置为0,表示黑色;
  • 当灰度Gray(i,j)大于或等于阈值T时,其像素值为255,表示白色。

在这里插入图片描述

在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图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第2张图片


三.基于边缘检测的图像分割

图像中相邻区域之间的像素集合共同构成了图像的边缘。基于边缘检测的图像分割方法是通过确定图像中的边缘轮廓像素,然后将这些像素连接起来构建区域边界的过程。由于沿着图像边缘走向的像素值变化比较平缓,而沿着垂直于边缘走向的像素值变化比较大,根据该特点,通常会采用一阶导数和二阶导数来描述和检测边缘。

在下一篇文章中,我们将详细讲解了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算子分割图。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第3张图片

下面讲解另一种边缘检测的方法。在OpenCV中,可以通过cv2.findContours()函数从二值图像中寻找轮廓,其函数原型如下所示:

  • image, contours, hierarchy = findContours(image, mode, method[, contours[, hierarchy[, offset]]])
    – image表示输入图像,即用于寻找轮廓的图像,为8位单通道
    – contours表示检测到的轮廓,其函数运行后的结果存在该变量中,每个轮廓存储为一个点向量
    – hierarchy表示输出变量,包含图像的拓扑信息,作为轮廓数量的表示,它包含了许多元素,每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0]至hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号
    – mode表示轮廓检索模式。cv2.RETR_EXTERNAL表示只检测外轮廓;cv2.RETR_LIST表示提取所有轮廓,且检测的轮廓不建立等级关系;cv2.RETR_CCOMP提取所有轮廓,并建立两个等级的轮廓,上面的一层为外边界,里面一层为内孔的边界信;cv2.RETR_TREE表示提取所有轮廓,并且建立一个等级树或网状结构的轮廓
    – method表示轮廓的近似方法。cv2.CHAIN_APPROX_NONE存储所有的轮廓点,相邻的两个点的像素位置差不超过1,即max(abs(x1-x2), abs(y1-y2))=1;cv2.CHAIN_APPROX_SIMPLE压缩水平方向、垂直方向、对角线方向的元素,只保留该方向的终点坐标,例如一个矩阵轮廓只需4个点来保存轮廓信息; cv2.CHAIN_APPROX_TC89_L1和cv2.CHAIN_APPROX_TC89_KCOS使用Teh-Chinl Chain近似算法
    – offset表示每个轮廓点的可选偏移量

在使用findContours()函数检测图像边缘轮廓后,通常需要和drawContours()函数联合使用,接着绘制检测到的轮廓,drawContours()函数的原型如下:

  • image = drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]])
    – image表示目标图像,即所要绘制轮廓的背景图片
    – contours表示所有的输入轮廓,每个轮廓存储为一个点向量
    – contourldx表示轮廓绘制的指示变量,如果为负数表示绘制所有轮廓
    – color表示绘制轮廓的颜色
    – thickness表里绘制轮廓线条的粗细程度,默认值为1
    – lineType表示线条类型,默认值为8,可选线包括8(8连通线型)、4(4连通线型)、CV_AA(抗锯齿线型)
    – hierarchy表示可选的层次结构信息
    – maxLevel表示用于绘制轮廓的最大等级,默认值为INT_MAX
    – offset表示每个轮廓点的可选偏移量

下面的代码是使用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为图像阈值化处理效果图。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第4张图片

图5为最终提取的风景图的边缘线条。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第5张图片


四.基于纹理背景的图像分割

该小节主要讲解基于图像纹理信息(颜色)、边界信息(反差)和背景信息的图像分割算法。在OpenCV中,GrabCut算法能够有效地利用纹理信息和边界信息分割背景,提取图像目标物体。该算法是微软研究院基于图像分割和抠图的课题,它能有效地将目标图像分割提取,如图6所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第6张图片

GrabCut算法原型如下所示:

  • mask, bgdModel, fgdModel = grabCut(img, mask, rect, bgdModel, fgdModel, iterCount[, mode])
    – image表示输入图像,为8位三通道图像
    – mask表示蒙板图像,输入/输出的8位单通道掩码,确定前景区域、背景区域、不确定区域。当模式设置为GC_INIT_WITH_RECT时,该掩码由函数初始化
    – rect表示前景对象的矩形坐标,其基本格式为(x, y, w, h),分别为左上角坐标和宽度、高度
    – bdgModel表示后台模型使用的数组,通常设置为大小为(1, 65)np.float64的数组
    – fgdModel表示前台模型使用的数组,通常设置为大小为(1, 65)np.float64的数组
    – iterCount表示算法运行的迭代次数
    – mode是cv::GrabCutModes操作模式之一,cv2.GC_INIT_WITH_RECT 或 cv2.GC_INIT_WITH_MASK表示使用矩阵模式或蒙板模式

下面是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)为图像分割后提取的目标人物,但人物右部分的背景仍然存在。如何移除这些背景呢?这里需要使用自定义的掩码进行提取,读取一张灰色背景轮廓图,从而分离背景与前景,希望读者下来实现该功能。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第7张图片


五.基于K-Means聚类的区域分割

K-Means聚类是最常用的聚类算法,最初起源于信号处理,其目标是将数据点划分为K个类簇,找到每个簇的中心并使其度量最小化。该算法的最大优点是简单、便于理解,运算速度较快,缺点是只能应用于连续型数据,并且要在聚类前指定聚集的类簇数。

下面是K-Means聚类算法的分析流程,步骤如下:

  • 第一步,确定K值,即将数据集聚集成K个类簇或小组;
  • 第二步,从数据集中随机选择K个数据点作为质心(Centroid)或数据中心;
  • 第三步,分别计算每个点到每个质心之间的距离,并将每个点划分到离最近质心的小组,跟定了那个质心;
  • 第四步,当每个质心都聚集了一些点后,重新定义算法选出新的质心;
  • 第五步,比较新的质心和老的质心,如果新质心和老质心之间的距离小于某一个阈值,则表示重新计算的质心位置变化不大,收敛稳定,则认为聚类已经达到了期望的结果,算法终止;
  • 第六步,如果新的质心和老的质心变化很大,即距离大于阈值,则继续迭代执行第三步到第五步,直到算法终止。

图8是对身高和体重进行聚类的算法,将数据集的人群聚集成三类。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第8张图片

在图像处理中,通过K-Means聚类算法可以实现图像分割、图像聚类、图像识别等操作,本小节主要用来进行图像颜色分割。假设存在一张100×100像素的灰度图像,它由10000个RGB灰度级组成,我们通过K-Means可以将这些像素点聚类成K个簇,然后使用每个簇内的质心点来替换簇内所有的像素点,这样就能实现在不改变分辨率的情况下量化压缩图像颜色,实现图像颜色层级分割。

在OpenCV中,Kmeans()函数原型如下所示:

  • retval, bestLabels, centers = kmeans(data, K, bestLabels, criteria, attempts, flags[, centers])
    – data表示聚类数据,最好是np.flloat32类型的N维点集
    – K表示聚类类簇数
    – bestLabels表示输出的整数数组,用于存储每个样本的聚类标签索引
    – criteria表示算法终止条件,即最大迭代次数或所需精度。在某些迭代中,一旦每个簇中心的移动小于criteria.epsilon,算法就会停止
    – attempts表示重复试验kmeans算法的次数,算法返回产生最佳紧凑性的标签
    – flags表示初始中心的选择,两种方法是cv2.KMEANS_PP_CENTERS ;和cv2.KMEANS_RANDOM_CENTERS
    – centers表示集群中心的输出矩阵,每个集群中心为一行数据

下面使用该方法对灰度图像颜色进行分割处理,需要注意,在进行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聚类后的图像,它将灰度级聚集成四个层级,相似的颜色或区域聚集在一起。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第9张图片

下面代码是对彩色图像进行颜色分割处理,它将彩色图像聚集成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种颜色。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第10张图片


六.基于均值漂移算法的图像分割

均值漂移(Mean Shfit)算法是一种通用的聚类算法,最早是1975年Fukunaga等人在一篇关于概率密度梯度函数的估计论文中提出[6]。它是一种无参估计算法,沿着概率梯度的上升方向寻找分布的峰值。Mean Shift算法先算出当前点的偏移均值,移动该点到其偏移均值,然后以此为新的起始点,继续移动,直到满足一定的条件结束。

图像分割中可以利用均值漂移算法的特性,实现彩色图像分割。在OpenCV中提供的函数为pyrMeanShiftFiltering(),该函数严格来说并不是图像分割,而是图像在色彩层面的平滑滤波,它可以中和色彩分布相近的颜色,平滑色彩细节,侵蚀掉面积较小的颜色区域,所以在OpenCV中它的后缀是滤波“Filter”,而不是分割“segment”。该函数原型如下所示:

  • dst = pyrMeanShiftFiltering(src, sp, sr[, dst[, maxLevel[, termcrit]]])
    – src表示输入图像,8位三通道的彩色图像
    – dst表示输出图像,需同输入图像具有相同的大小和类型
    – sp表示定义漂移物理空间半径的大小
    – sr表示定义漂移色彩空间半径的大小
    – maxLevel表示定义金字塔的最大层数
    – termcrit表示定义的漂移迭代终止条件,可以设置为迭代次数满足终止,迭代目标与中心点偏差满足终止,或者两者的结合

均值漂移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所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第11张图片

当漂移物理空间半径设置为20,漂移色彩空间半径设置为20,金字塔层数 为2,输出的效果图如图12所示。对比可以发现,半径为20时,图像色彩细节大部分存在,半径为50时,森林和水面的色彩细节基本都已经丢失。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第12张图片

写到这里,均值偏移算法对彩色图像的分割平滑操作就完成了,为了达到更好地分割目的,借助漫水填充函数进行下一步处理,在第八部分将详细介绍,这里只是引入该函数。完整代码如下所示:

# 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所示,它将天空染成黄色。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第13张图片


七.基于分水岭算法的图像分割

图像分水岭算法(Watershed Algorithm)是将图像的边缘轮廓转换为“山脉”,将均匀区域转换为“山谷”,从而提升分割效果的算法[3]。分水岭算法是基于拓扑理论的数学形态学的分割方法,灰度图像根据灰度值把像素之间的关系看成山峰和山谷的关系,高亮度(灰度值高)的地方是山峰,低亮度(灰度值低)的地方是山谷。接着给每个孤立的山谷(局部最小值)不同颜色的水(Label),当水涨起来,根据周围的山峰(梯度),不同的山谷也就是不同颜色的像素点开始合并,为了避免这个现象,可以在水要合并的地方建立障碍,直到所有山峰都被淹没。所创建的障碍就是分割结果,这个就是分水岭的原理。

分水岭算法的计算过程是一个迭代标注过程,主要包括排序和淹没两个步骤。由于图像会存在噪声或缺失等问题,该方法会造成分割过度。OpenCV提供了watershed()函数实现图像分水岭算法,并且能够指定需要合并的点,其函数原型如下所示:

  • markers = watershed(image, markers)
    – image表示输入图像,需为8位三通道的彩色图像
    – markers表示用于存储函数调用之后的运算结果,输入/输出32位单通道图像的标记结构,输出结果需和输入图像的尺寸和类型一致。

下面是分水岭算法实现图像分割的过程。假设存在一幅彩色硬币图像,如图14所示,硬币相互之间挨着。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第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所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第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所示,包括原始图像、阈值化处理、开运算、背景区域、前景区域、未知区域等。由图可知,在使用阈值过滤的图像里,确认了图像的硬币区域,而在有些情况,可能对前景分割更感兴趣,而不关心目标是否需要分开或挨着,那时可以采用腐蚀操作来求解前景区域。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第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所示,它将硬币的轮廓成功提取。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第17张图片

图18是采用分水岭算法提取图像Windows中心轮廓的效果图。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第18张图片

分水岭算法对微弱边缘具有良好的响应,图像中的噪声、物体表面细微的灰度变化,都会产生过度分割的现象。但同时应当看出,分水岭算法对微弱边缘具有良好的响应,是得到封闭连续边缘的保证。另外,分水岭算法所得到的封闭的集水盆,为分析图像的区域特征提供了可能。


八.图像漫水填充分割

图像漫水填充(FloodFill)是指用一种特定的颜色填充联通区域,通过设置可连通像素的上下限以及连通方式来达到不同的填充效果。漫水填充通常被用来标记或分离图像的一部分以便对其进行深入的处理或分析。本书将该知识点划分为图像分割的一种特殊案例。

图像漫水填充主要是遴选出与种子点联通且颜色相近的像素点,接着对像素点的值进行处理。如果遇到掩码,则根据掩码进行处理。其原理类似Photoshop的魔术棒选择工具,漫水填充将查找和种子点联通的颜色相同的点,而魔术棒选择工具是查找和种子点联通的颜色相近的点,将和初始种子像素颜色相近的点压进栈作为新种子。

基本工作步骤如下:

  • 选定种子点(x,y);
  • 检查种子点的颜色,如果该点颜色与周围连接点的颜色不相同,则将周围点颜色设置为该点颜色;如果相同则不做处理。但是周围点不一定都会变成和种子点的颜色相同,如果周围连接点在给定的范围(从loDiff到upDiff)内或在种子点的像素范围内才会改变颜色;
  • 检测其他连接点,进行第2个步骤的处理,直到没有连接点,即到达检测区域边界停止。

在OpenCV中,主要通过floodFill()函数实现漫水填充分割,它将用指定的颜色从种子点开始填充一个连接域。其函数原型如下所示:

  • floodFill(image, mask, seedPoint, newVal[, loDiff[, upDiff[, flags]]])
    – image表示输入/输出1通道或3通道,6位或浮点图像
    – mask表示操作掩码,必须为8位单通道图像,其长宽都比输入图像大两个像素点。注意,漫水填充不会填充掩膜mask的非零像素区域,mask中与输入图像(x,y)像素点相对应的点的坐标为(x+1,y+1)。
    – seedPoint为Point类型,表示漫水填充算法的起始点
    – newVal表示像素点被染色的值,即在重绘区域像素的新值
    – loDiff表示当前观察像素值与其部件邻域像素值或待加入该部件的种子像素之间的亮度或颜色之负差的最大值,默认值为Scalar( )
    – upDiff表示当前观察像素值与其部件邻域像素值或待加入该部件的种子像素之间的亮度或颜色之正差的最大值,默认值为Scalar( )
    – flags表示操作标识符,此参数包括三个部分:低八位0-7bit表示邻接性(4邻接或8邻接);中间八位8-15bit表示掩码的填充颜色,如果中间八位为0则掩码用1来填充;高八位16-31bit表示填充模式,可以为0或者以下两种标志符的组合,FLOODFILL_FIXED_RANGE表示此标志会考虑当前像素与种子像素之间的差,否则就考虑当前像素与相邻像素的差。FLOODFILL_MASK_ONLY表示函数不会去填充改变原始图像,而是去填充掩码图像mask,mask指定的位置为零时才填充,不为零不填充。

在Python和OpenCV实现代码中,它设置种子点位置为(10,200);设置颜色为黄色(0,255,255);连通区范围设定为loDiff和upDiff;标记参数设置为CV_FLOODFILL_FIXED_RANGE ,它表示待处理的像素点与种子点作比较,在范围之内,则填充此像素,即种子漫水填充满足:

  • src(seed.x, seed.y) - loDiff <= src(x, y) <= src(seed.x, seed.y) +upDiff

最终完整代码如下:

#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图标周围填充为黄色的图像。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第19张图片

下面补充另一段代码,它将打开一幅图像,点击鼠标选择种子节点,移动滚动条设定连通区范围的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所示,它将天空和中心水面填充成黄色。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第20张图片

当鼠标选定的种子点为(328, 202),观察点像素邻域负差最大值“lo”为142,观察点像素邻域正差最大值“hi”为45时,图像漫水填充效果如图21所示,它将图像两旁的森林和水面填充成蓝紫色。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第21张图片

女神的填充如图22所示,哈哈。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第22张图片


九.文字区域定位及提取案例

接下来讲述一个定位文字区域并进行文字提取的案例,下一篇文章将详细介绍,作者水族文字识别课题用到了相关算法。该算法依次经历如下步骤:

  • 读取文字原始图像,并利用中值滤波算法消除图像噪声,同时保留图像边缘细节;
  • 通过图像灰度转换将中值滤波处理后的彩色图像转换为灰度图像;
  • 采用Sobel算子锐化突出文字图像的边缘细节,改善图像的对比度,提取文字轮廓;
  • 经过二值化处理提取图像中的文字区域,将图像的背景和文字区域分离;
  • 将阈值化处理后的图像进行膨胀处理和腐蚀处理,突出图像轮廓的同时过滤掉图像的细节;
  • 最后采用findContours()函数寻找文字轮廓,定位并提取目标文字,接着调用drawContours()函数绘制相关轮廓,输出最终图像。

完整代码如下所示:

# 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所示的图像(作者个人网站博客)。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第23张图片

第二步,将彩色图像转换成灰度图像,如图24所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第24张图片

第三步,通过Sobel算子提取文字的基本轮廓线条,如图25所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第25张图片

第四步,二值化处理将图像转换为黑色和白色两种像素级,如图26所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第26张图片

第五步,通过膨胀处理扩大文字轮廓,腐蚀处理过滤图像的细节,处理效果分别如图27和图28所示。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第27张图片

最后,调用findContours()函数寻找轮廓,并过滤掉面积异常区域,采用函数drawContours()绘制文字轮廓,最终输出如图29所示的图像,它有效地将原图中所有文字区域定位并提取出来。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第28张图片

该方法是图像分割和图像识别前的重要环节,可以广泛应用于文字识别、车牌提取、区域定位等领域。


十.本章小结

写到这里,本文就介绍完毕。本文主要讲解了常用的图像分割方法,包括基于阈值的图像分割方法、基于边缘检测的图像分割方法、基于纹理背景的图像分割方法和基于特定理论的图像分割方法。其中,基于特定理论的分割方法又分别讲解了基于K-Means聚类、均值漂移、分水岭算法的图像分割方法。最后通过漫水填充分割和文字区域定位案例加深了读者的印象。希望读者能结合本章知识点,围绕自己的研究领域或工程项目进行深入的学习,实现所需的图像处理特效。

  • 一.图像分割概述
  • 二.基于阈值的图像分割
  • 三.基于边缘检测的图像分割
  • 四.基于纹理背景的图像分割
  • 五.基于K-Means聚类的区域分割
  • 六.基于均值漂移算法的图像分割
  • 七.基于分水岭算法的图像分割
  • 八.图像漫水填充分割
  • 九.文字区域定位及提取案例

源代码下载地址,记得帮忙点star和关注喔。

  • https://github.com/eastmountyxz/ImageProcessing-Python

大学之道在明明德,
在亲民,在止于至善。
这周又回答了很多博友的问题,有大一学生的困惑,有论文的咨询,也有老乡和考博的疑问,还有无数博友奋斗路上的相互勉励。虽然自己早已忙成狗,但总忍不住去解答别人的问题。最后那一句感谢和祝福,永远是我最大的满足。虽然会花费我一些时间,但也挺好的,无所谓了,跟着心走。不负遇见,感恩同行。莫愁前路无知己,继续加油。晚安娜和珞。

[Python图像处理] 四十.全网首发Python图像分割万字详解(阈值分割、边缘分割、纹理分割、分水岭算法、K-Means分割、漫水填充分割、区域定位)_第29张图片

(By:Eastmount 2021-05-18 晚上12点 http://blog.csdn.net/eastmount/ )


参考文献:

  • [1] 罗子江. Python中的图像处理[M]. 科学出版社 2020.
  • [2]冈萨雷斯著. 数字图像处理(第3版)[M]. 北京:电子工业出版社,2013.
  • [3]阮秋琦. 数字图像处理学(第3版)[M]. 北京:电子工业出版社,2008.
  • [4]毛星云,冷雪飞. OpenCV3编程入门[M]. 北京:电子工业出版社,2015.
  • [5]张铮,王艳平,薛桂香等. 数字图像处理与机器视觉——Visual C++与Matlab实现[M]. 北京:人民邮电出版社,2014.
  • [6]杨秀璋, 颜娜. Python网络数据爬取及分析从入门到精通(分析篇)[M]. 北京:北京航天航空大学出版社, 2018.
  • [7]Fukunaga K. and Hostetler L.D. The Estimation of the Gradient of a Density Function, with Applications in Pattern Recognition[J]. IEEE Transactions on Information Theory, 1975, 21, 32-10.
  • [8]Robert Laganiere. OpenCV2计算机视觉编程手册[M]. 北京:科学出版社,2013.

你可能感兴趣的:(Python图像处理及图像识别,Python图像处理,图像分割,OpenCV,阈值分割,满水填充)