5.1 简介
该部分的学习内容是对经典的阈值分割算法进行回顾,图像阈值化分割是一种传统的最常用的图像分割方法,因其实现简单、计算量小、性能较稳定而成为图像分割中最基本和应用最广泛的分割技术。它特别适用于目标和背景占据不同灰度级范围的图像。它不仅可以极大的压缩数据量,而且也大大简化了分析和处理步骤,因此在很多情况下,是进行图像分析、特征提取与模式识别之前的必要的图像预处理过程。图像阈值化的目的是要按照灰度级,对像素集合进行一个划分,得到的每个子集形成一个与现实景物相对应的区域,各个区域内部具有一致的属性,而相邻区域不具有这种一致属性。这样的划分可以通过从灰度级出发选取一个或多个阈值来实现。
5.2 学习目标
- 了解阈值分割基本概念
- 理解最大类间方差法(大津法)、自适应阈值分割的原理
- 掌握OpenCV框架下上述阈值分割算法API的使用
5.3 内容介绍
1、最大类间方差法、自适应阈值分割的原理
2、OpenCV代码实践
3、动手实践并打卡(读者完成)
5.4 算法理论介绍
5.4.1 最大类间方差法(大津阈值法)
大津法(OTSU)是一种确定图像二值化分割阈值的算法,由日本学者大津于1979年提出。从大津法的原理上来讲,该方法又称作最大类间方差法,因为按照大津法求得的阈值进行图像二值化分割后,前景与背景图像的类间方差最大。
它被认为是图像分割中阈值选取的最佳算法,计算简单,不受图像亮度和对比度的影响,因此在数字图像处理上得到了广泛的应用。它是按图像的灰度特性,将图像分成背景和前景两部分。因方差是灰度分布均匀性的一种度量,背景和前景之间的类间方差越大,说明构成图像的两部分的差别越大,当部分前景错分为背景或部分背景错分为前景都会导致两部分差别变小。因此,使类间方差最大的分割意味着错分概率最小。
应用: 是求图像全局阈值的最佳方法,应用不言而喻,适用于大部分需要求图像全局阈值的场合。
优点: 计算简单快速,不受图像亮度和对比度的影响。
缺点: 对图像噪声敏感;只能针对单一目标分割;当目标和背景大小比例悬殊、类间方差函数可能呈现双峰或者多峰,这个时候效果不好。
原理非常简单,涉及的知识点就是均值、方差等概念和一些公式推导。为了便于理解,我们从目的入手,反推一下这著名的OTSU算法。
求类间方差:
OTSU算法的假设是存在阈值TH将图像所有像素分为两类C1(小于TH)和C2(大于TH),则这两类像素各自的均值就为m1、m2,图像全局均值为mG。同时像素被分为C1和C2类的概率分别为p1、p2。因此就有:
根据原文,式(4)还可以进一步变形:
分割:
这个分割就是二值化,OpenCV给了以下几种方式,很简单,可以参考:
5.4.2 自适应阈值
前面介绍了OTSU算法,但这算法属于全局阈值法,所以对于某些光照不均的图像,这种全局阈值分割的方法会显得苍白无力,如下图:
显然,这样的阈值处理结果不是我们想要的,那么就需要一种方法来应对这样的情况。
这种办法就是自适应阈值法(adaptiveThreshold),它的思想不是计算全局图像的阈值,而是根据图像不同区域亮度分布,计算其局部阈值,所以对于图像不同区域,能够自适应计算不同的阈值,因此被称为自适应阈值法。(其实就是局部阈值法)
如何确定局部阈值呢?可以计算某个邻域(局部)的均值、中值、高斯加权平均(高斯滤波)来确定阈值。值得说明的是:如果用局部的均值作为局部的阈值,就是常说的移动平均法。
5.5 基于OpenCV的实现
5.5.1 图像二值化
import cv2 import matplotlib.pyplot as plt img = cv2.imread('cat.jpg',0) #直接读为灰度图像 ret,thresh1 = cv2.threshold(img,127,255,cv2.THRESH_BINARY) ret,thresh2 = cv2.threshold(img,127,255,cv2.THRESH_BINARY_INV) ret,thresh3 = cv2.threshold(img,127,255,cv2.THRESH_TRUNC) ret,thresh4 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO) ret,thresh5 = cv2.threshold(img,127,255,cv2.THRESH_TOZERO_INV) titles = ['img','BINARY','BINARY_INV','TRUNC','TOZERO','TOZERO_INV'] images = [img,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()
5.5.2 图像分割
""" 任何一副灰度图像 可以 看成拓扑平面 灰度值 的区域可以 看成是 山峰 灰度值低的区域可以 看成是山谷。 我们向每一个山谷中灌不同颜色的水。随着水的位的升 不同山谷的水就会相遇汇合 为了防止不同山 的水 汇合 我们需要在水汇合的地方构建 堤坝。不停的灌水 不停的构建堤坝 直到所有的山峰都被水淹没。 我们构建好的堤坝就是对图像的分割。 这就是分水岭算法的背后哲理。 每一次灌水 我们的标签就会 更新 当两个不同 色的标签相 时就构建堤 坝 直到将所有山峰淹没 最后我们得到的 界对 堤坝 的值为 -1 """ import numpy as np import cv2 from matplotlib import pyplot as plt img = cv2.imread('water.jpg') gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) cv2.imshow('thresh', thresh) plt.imshow(thresh,) # noise removal kernel = np.ones((3, 3), np.uint8) opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2) # sure background area sure_bg = cv2.dilate(opening, kernel, iterations=3) # Finding sure foreground area # 距离变换的基本含义是计算一个图像中非像素点到最近的零像素点的距离 # 也就是到零像素点的最短距离 # 个最常见的距离变换算法就是通过连续的腐蚀操作来实现, # 腐蚀操作的停止条件是所有前景像素都被完全腐蚀。 # 这样根据腐蚀的先后顺序,我们就得到各个前景像素点到前景中心骨架像素点的距离 # 根据各个像素点的距离值,设置为不同的灰度值。这样就完成了二值图像的距离变换 # cv2.distanceTransform(src, distanceType, maskSize) # 第二个参数 0,1,2 分别 示 CV_DIST_L1, CV_DIST_L2 , CV_DIST_C dist_transform = cv2.distanceTransform(opening, 1, 5) ret, sure_fg = cv2.threshold(dist_transform, 0.7 * dist_transform.max(), 255, 0) # Finding unknown region sure_fg = np.uint8(sure_fg) unknown = cv2.subtract(sure_bg, sure_fg)#图像相减 cv2.imshow('unknown', unknown) plt.imshow(unknown) # 边界 # 腐蚀 # Marker labelling创建标签 ret, markers1 = cv2.connectedComponents(sure_fg) # Add one to all labels so that sure background is not 0, but 1 # 把将背景标 为 0 其他的对 使用从 1 开始的正整数标 markers = markers1 + 1 # Now, mark the region of unknown with zero markers[unknown == 255] = 0 # cv2.imshow('markers', markers1) # 到最后一步 实施分水岭算法了。标签图像将会 修 改 界区域的标 将变为 -1 markers3 = cv2.watershed(img, markers) img[markers3 == -1] = [255, 0, 0] cv2.imshow('watershed', img) plt.imshow(img) cv2.waitKey(0) cv2.destroyAllWindows()