文章目录
- 一、形态学
- 1.1 阈值处理
- 1.1.1 全局阈值处理
- 1.1.2 全局阈值处理之Otsu's 阈值法
- 1.1.3 自适应阈值处理
- 1.2 腐蚀与膨胀
- 1.2.1 腐蚀操作
- 1.2.2 创建形态学卷积核
- 1.2.3 膨胀操作
- 1.3 开运算和闭运算
- 1.4 形态学梯度
- 1.5 顶帽操作(tophat)
- 1.6 黑帽操作(Black Hat)
- 二、图像轮廓
- 2.1 轮廓的查找与绘制
- 2.2 计算轮廓面积和周长
- 2.3 多边形近似
- 2.4 凸包
- 2.5 外接矩形和外接圆
- 三、图像金字塔
- 3.1 高斯金字塔
- 3.2 拉普拉斯金字塔
- 四、图像直方图
- 4.1 图像直方图基本概念
- 4.2 统计直方图
- 4.2.1 直接统计
- 4.2.2 使用OpenCV统计图像直方图
- 4.2.3 使用掩膜
- 4.3 直方图均衡化
- 4.4 自适应直方图均衡化 (CLAHE)
- 4.4.1 实现原理
- 4.4.2 代码实现
- 《OpenCV系列课程一:图像处理入门(读写、拆分合并、变换、注释)、视频处理》
- 《OpenCV系列教程二:基本图像增强(数值运算)、滤波器(去噪、边缘检测)》
- 《OpenCV系列教程三:形态学、图像轮廓、直方图》
一、形态学
形态学(Morphology
)是指一系列用于处理图像形状和结构的算法,其基本思想是利用一种特殊的结构元(本质上就是卷积核)来测量或提取输入图像中相应的形状或特征。形态学操作通常用于预处理、图像分割、特征提取、图像滤波和图像增强等任务。形态学的基本操作包括:
- 腐蚀(
Erosion
):它将图像中的前景物体缩小,这种操作可以去除图像中的小物体,分离相互接触的物体,以及平滑物体的边界。 - 膨胀(
Dilation
):与腐蚀相反,膨胀操作将图像中的前景物体增大,可以用来填补物体中的小洞,连接相邻的物体,或者增加物体的面积。 - 开运算(
Opening
):先腐蚀后膨胀的过程,用于去除小的物体,平滑较大物体的边界,而不改变其面积。 - 闭运算(
Closing
):先膨胀后腐蚀的过程,用于填充物体内的小洞,连接邻近的物体,而不明显改变物体的边界。 - 形态学梯度(Morphological Gradient):膨胀图与腐蚀图之差,可以突出物体的边缘。
- 顶帽(
Top Hat
)和黑帽(Black Hat
):这两种操作分别是原图与开运算结果之差(顶帽)和闭运算结果与原图之差(黑帽),用于突出比周围区域亮或暗的区域。
1.1 阈值处理
阈值处理的主要意义是将图像中的某些区域分离出来,通常是为了突出前景(如物体)和背景(如场景)。通过二值化,可以将灰度图像转化为黑白图像(即二值图像),使后续的图像分析、边缘检测、目标识别等任务更加简便和高效。
例如,图像由暗色背景上的亮目标组成,这时可以通过设定适当的阈值 T,将图像的像素划分为两类:灰度值大于 T 的像素集是目标,小于 T 的像素集是背景。当 T 是应用于整幅图像的常数,称为全局阈值处理;当 T 对于整幅图像发生变化时,称为可变阈值处理。有时,对应于图像中任一点的 T 值取决于该点的邻域的限制,称为局部阈值处理。
1.1.1 全局阈值处理
全局阈值处理(Global Thresholding)是对图像的所有像素点应用同一个阈值。如果像素值高于阈值,则将其设为一个值(通常是白色),否则设为另一个值(通常是黑色)。OpenCV 提供了函数 cv2.threshold
函数来实现此功能,其语法为:
retval, dst = cv2.threshold( src, thresh, maxval, type[, dst] )
-
retval
:阈值,浮点型 -
dst
:阈值处理后的图像(numpy数组),与src具有相同大小和类型以及通道数。 -
src
:输入数组,最好是灰度图。 -
thresh
:阈值。 -
maxval
:用于THRESH_BINARY
和THRESH_MINARY_INV
阈值类型的最大值,一般取 255。 -
type
:阈值类型(详见阈值类型)。
Type 类型 | 描述 |
---|---|
cv2.THRESH_BINARY (输出二值图像) | 超过阈值的像素值设为maxValue ,否则设为0 |
cv2.THRESH_BINARY_INV (输出二值图像) | 超过阈值的像素值设为0,否则设为maxValue |
cv2.THRESH_TRUNC | 超过阈值时置为阈值 thresh ,否则不变 |
cv2.THRESH_TOZERO | 超过阈值的像素值保持不变,否则置0 |
cv2.THRESH_TOZERO_INV | 超过阈值的像素值设为0,否则不变 |
cv2.THRESH_OTSU | 使用 OTSU 算法选择阈值,需要与其他类型(如cv2.THRESH_BINARY )结合使用。 |
cv2.THRESH_TRIANGLE | 使用三角算法自动计算阈值,需要与其他类型结合使用。 |
特殊值THRESH_OTSU
或THRESH_TRIANGLE
可以与上述值之一组合。在这些情况下,函数使用Otsu或Triangle算法确定最佳阈值,并使用它代替指定的阈值。Otsu和Triangle方法仅用于8位单通道图像。
当图像中存在高斯噪声时,通常难以通过全局阈值将图像的边界完全分开。如果图像的边界是在局部对比下出现的,不同位置的阈值也不同,使用全局阈值的效果将会很差。如果图像的直方图存在明显边界,容易找到图像的分割阈值;但如果图像直方图分界不明显,则很难找到合适的阈值,甚至可能无法找到固定的阈值有效地分割图像。
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img_read = cv2.imread("building-windows.jpg", 0) # 灰度图
retval, img_thresh = cv2.threshold(img_read, 100, 255, cv2.THRESH_BINARY)
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(121); plt.imshow(img_read, cmap="gray"); plt.title("Original");
plt.subplot(122); plt.imshow(img_thresh, cmap="gray"); plt.title("Thresholded");
print(retval,img_thresh.shape)
(572, 800) 100.0 (572, 800)
1.1.2 全局阈值处理之Otsu’s 阈值法
Otsu’s 方法是一种自动选择全局阈值的算法,通过最大化类间方差自动确定最优阈值。
当图像中的目标和背景的灰度分布较为明显时,可以对整个图像使用固定阈值进行全局阈值处理。 为了获得适当的全局阈值,可以基于灰度直方图进行迭代计算(详见【youcans 的 OpenCV 例程200篇】159. 图像分割之全局阈值处理),另一种改进算法是OTSU 方法(又称大津算法)。使用最大化类间方差(intra-class variance)作为评价准则,基于对图像直方图的计算,可以给出类间最优分离的最优阈值。
任取一个灰度值 T,可以将图像分割为两个集合 F 和 B,集合 F、B 的像素数的占比分别为 pF、pB,集合 F、B 的灰度值均值分别为 mF、mB,图像灰度值为 m,定义类间方差为:
I
C
V
=
p
F
∗
(
m
F
−
m
)
2
+
p
B
∗
(
m
B
−
m
)
2
ICV = p_F * (m_F - m)^2 + p_B * (m_B - m)^2
ICV=pF∗(mF−m)2+pB∗(mB−m)2
使类间方差 ICV 最大化的灰度值 T 就是最优阈值。因此,只要遍历所有的灰度值,就可以得到使 ICV 最大的最优阈值 T。
OpenCV 提供了函数 cv.threshold 可以对图像进行阈值处理,将参数 type 设为 cv.THRESH_OTSU
,就可以使用使用 OTSU 算法进行最优阈值分割。
img = cv2.imread("../images/Fig1039a.tif", flags=0)
deltaT = 1 # 预定义值
histCV = cv2.calcHist([img], [0], None, [256], [0, 256]) # 灰度直方图
grayScale = range(256) # 灰度级 [0,255]
totalPixels = img.shape[0] * img.shape[1] # 像素总数
totalGray = np.dot(histCV[:,0], grayScale) # 内积, 总和灰度值
T = round(totalGray/totalPixels) # 平均灰度
while True:
numC1, sumC1 = 0, 0
for i in range(T): # 计算 C1: (0,T) 平均灰度
numC1 += histCV[i,0] # C1 像素数量
sumC1 += histCV[i,0] * i # C1 灰度值总和
numC2, sumC2 = (totalPixels-numC1), (totalGray-sumC1) # C2 像素数量, 灰度值总和
T1 = round(sumC1/numC1) # C1 平均灰度
T2 = round(sumC2/numC2) # C2 平均灰度
Tnew = round((T1+T2)/2) # 计算新的阈值
print("T={}, m1={}, m2={}, Tnew={}".format(T, T1, T2, Tnew))
if abs(T-Tnew) < deltaT: # 等价于 T==Tnew
break
else:
T = Tnew
# 阈值处理
ret1, imgBin = cv2.threshold(img, T, 255, cv2.THRESH_BINARY) # 阈值分割, thresh=T
ret2, imgOtsu = cv2.threshold(img, T, 255, cv2.THRESH_OTSU) # 阈值分割, thresh=T
print(ret1, ret2)
plt.figure(figsize=(7,7))
plt.subplot(221), plt.axis('off'), plt.title("Origin"), plt.imshow(img, 'gray')
plt.subplot(222, yticks=[]), plt.title("Gray Hist") # 直方图
histNP, bins = np.histogram(img.flatten(), bins=255, range=[0, 255], density=True)
plt.bar(bins[:-1], histNP[:])
plt.subplot(223), plt.title("global binary(T={})".format(T)), plt.axis('off')
plt.imshow(imgBin, 'gray')
plt.subplot(224), plt.title("OTSU binary(T={})".format(round(ret2))), plt.axis('off')
plt.imshow(imgOtsu, 'gray')
plt.tight_layout()
plt.show()
全局阈值处理还有一些其它改进方法,比如处理前先对图像进行平滑、基于边缘信息改进全局阈值处理等等。
1.1.3 自适应阈值处理
噪声和非均匀光照等因素对阈值处理的影响很大,例如光照复杂时 全局阈值分割方法的效果往往不太理想,需要使用可变阈值处理。
自适应阈值处理(Adaptive Thresholding)对图像中的每个点,根据其邻域计算其对应的阈值,非常适合处理光照条件不均匀的图像。cv2.adaptiveThreshold
函数语法为:
adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C[, dst]) -> dst
maxValue
:为满足条件的像素指定的非零值,详见阈值类型说明。adaptiveMethod
:要使用的自适应阈值算法,详见AdaptiveThresholdTypes。cv.ADAPTIVE_THRESH_MEAN_C
:阈值是邻域的均值;cv.ADAPTIVE_THRESH_GAUSSIAN_C
:阈值是邻域的高斯核加权平均值;
thresholdType
:阈值类型,只有两种cv2.THRESH_BINARY
:大于阈值时置 maxValue,否则置 0cv2.THRESH_BINARY_INV
:大于阈值时置 0,否则置 maxValue
blockSize
:用于计算像素阈值的像素邻域的尺寸,例如3、5、7。C
: 偏移量,从平均值或加权平均值中减去该常数。
假设您想构建一个可以读取(解码)乐谱的应用程序,这类似于文本文档的光学字符识别(OCR)。处理管道的第一步是隔离文档图像中的重要信息(将其与背景分离)。这项任务可以通过阈值技术来完成。
# 示例:乐谱阅读器
img_read = cv2.imread("Piano_Sheet_Music.png", 0)
# 全局阈值1
retval, img_thresh_gbl_1 = cv2.threshold(img_read,50, 255, cv2.THRESH_BINARY)
# 全局阈值2
retval, img_thresh_gbl_2 = cv2.threshold(img_read,130, 255, cv2.THRESH_BINARY)
# 自适应阈值
img_thresh_adp = cv2.adaptiveThreshold(img_read, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 11, 7)
# Show the images
plt.figure(figsize=[18,15])
plt.subplot(221); plt.imshow(img_read, cmap="gray"); plt.title("Original");
plt.subplot(222); plt.imshow(img_thresh_gbl_1,cmap="gray"); plt.title("Thresholded (global: 50)");
plt.subplot(223); plt.imshow(img_thresh_gbl_2,cmap="gray"); plt.title("Thresholded (global: 130)");
plt.subplot(224); plt.imshow(img_thresh_adp, cmap="gray"); plt.title("Thresholded (adaptive)");
1.2 腐蚀与膨胀
- 腐蚀用于消除小的白色噪声,减小前景区域。
- 膨胀用于填补物体中的空洞,增加前景区域。
1.2.1 腐蚀操作
腐蚀是一种将前景(白色区域)缩小的操作,其原理是滑动窗口中的结构元素(kernel),如果卷积区域内所有被覆盖的像素都是前景像素(白色),中心像素保留为前景(白色),否则变为背景(黑色)。这使得前景物体逐渐缩小,细小的噪声点会被消除。
如上图所示,腐蚀操作的卷积核设为5×5,只有图中虚线方框内的像素,被卷积时区域内都是白色像素,所以卷积后也是白色(设为255)。其它区域都将被置为黑色(设为0)。
腐蚀操作使用erode
函数,其语法为:
erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
src
: 输入图像,一般是二值图像。kernel
: 卷积核(即结构元素)。- 核越大,腐蚀的效果越强。
- 不同的形状会影响腐蚀的方向性和图像特征的保留。矩形核适合均匀腐蚀,而椭圆核能更好地保留圆滑的边缘。
iterations
: 腐蚀操作的迭代次数,默认为1。次数越多,腐蚀效果越明显。
下面是一个简单的示例,处理之后,白色的字体像是被橡皮擦去了一圈,变小了。
img = cv2.imread('msb.png')
# 定义核
kernel = np.ones((5, 5), np.uint8)
dst = cv2.erode(img, kernel, iterations=1)
plt.figure(figsize=[18,15])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("dst");
1.2.2 创建形态学卷积核
cv2.getStructuringElement
是 OpenCV 中用于生成 结构元素(也叫形态学核)的函数,常用于形态学操作(如腐蚀、膨胀、开运算、闭运算等)。结构元素决定了形态学操作(卷积核)的形状和尺寸。
getStructuringElement(shape, ksize[, anchor]) -> retval
-
shape
:指定结构元素(卷积核)的形状,常见的形状有:cv2.MORPH_RECT
:矩形cv2.MORPH_ELLIPSE
:椭圆形cv2.MORPH_CROSS
:十字形
-
ksize
:结构元素的大小,通常以(width, height)
的形式给出。例如(5, 5)
表示 5x5 的结构元素。 -
anchor
(可选):结构元素的锚点,表示结构元素的参考中心点。默认是结构元素的中心(ksize[0]//2, ksize[1]//2)
,但也可以指定其他锚点。
kernel_RECT = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
kernel_ELLIPSE=cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
kernel_CROSS=cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))
kernel_RECT,kernel_ELLIPSE,kernel_CROSS
array([[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1]], dtype=uint8
array([[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0]], dtype=uint8)
array([[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0],
[1, 1, 1, 1, 1],
[0, 0, 1, 0, 0],
[0, 0, 1, 0, 0]], dtype=uint8)
1.2.3 膨胀操作
膨胀(Dilation)是一种将前景(白色区域)扩大的操作。膨胀的原理与腐蚀相反,只要滑动窗口中的结构元素覆盖下有一个像素是前景像素(白色),中心像素就保留为前景。这可以使前景区域扩大,填补物体中的小孔,并连接分离的小物体,其函数语法为:
dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
src
:输入图像,一般是二值图像。kernel
:卷积核,定义操作的结构元素。iterations
: 膨胀操作的迭代次数,默认为1。
img = cv2.imread('./j.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dst = cv2.dilate(img, kernel, iterations=1)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("dst");
1.3 开运算和闭运算
cv2.morphologyEx
是 OpenCV 中一个用于执行更复杂的形态学操作的函数,它基于基础的腐蚀和膨胀操作,并提供了一系列的高级形态学变换。通过这个函数,我们可以实现诸如开运算、闭运算、形态学梯度、顶帽操作和黑帽操作等操作。
操作类型 | 操作顺序 | 用途 |
---|---|---|
开运算 | 先腐蚀,后膨胀 | 消除小的噪声点,保留前景物体的整体形状 |
闭运算 | 先膨胀,后腐蚀 | 填补前景物体中的小孔,连接分散的小区域 |
梯度 | 膨胀与腐蚀的差 | 提取物体的边缘,提取图像中的轮廓 |
顶帽 | 输入图像 - 开运算 | 提取前景外的亮区域,常用于不均匀光照的图像处理中 |
黑帽 | 输入图像 - 闭运算 | 提取前景中的暗区域,适合分析背景中的暗部特征 |
cv2.morphologyEx
语法为:
morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
src
:输入图像,通常是二值图像(黑白图像)。op
:表示要执行的形态学操作。常见的操作包括:cv2.MORPH_OPEN
:开运算。先腐蚀,后膨胀 ,在消除噪声的同时保持前景部分不变。cv2.MORPH_CLOSE
:闭运算。先膨胀扩大前景,再腐蚀,擦掉前景中的黑色部分。cv2.MORPH_GRADIENT
:形态学梯度;cv2.MORPH_TOPHAT
:顶帽操作;cv2.MORPH_BLACKHAT
:黑帽操作;
kernel
:结构元素(卷积核),通常由cv2.getStructuringElement()
生成(包括形状和大小)。iterations
:迭代次数,默认为 1。
# 开运算,先腐蚀后膨胀,前景保持不变
img = cv2.imread('./dotj.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 直接调用cv2.morphologyEx更方便
# dst = cv2.erode(img, kernel, iterations=2)
# dst = cv2.dilate(dst, kernel, iterations=2)
dst = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel, iterations=2)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("open");
# 闭运算
img = cv2.imread('dotinj.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dst = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel, iterations=2)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("close");
1.4 形态学梯度
形态学梯度 = 原图 - 腐蚀,也就是得到被腐蚀掉的部分。这会突出显示物体的边缘,生成的是前景物体的轮廓。
img = cv2.imread('./j.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
dst = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel, iterations=1)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("GRADIENT");
1.5 顶帽操作(tophat)
顶帽 = 原图 - 开运算。开运算的效果是去除图形外的噪点,,原图 - 开运算就得到了图形外的噪点,可以用于突出显示图像中的亮区域。
import cv2
import numpy as np
img = cv2.imread('./dotj.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dst = cv2.morphologyEx(img, cv2.MORPH_TOPHAT, kernel, iterations=2)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("TOPHAT");
1.6 黑帽操作(Black Hat)
黑帽 = 原图 - 闭运算。闭运算可以将图形内部的噪点去掉,那么原图 - 闭运算的结果就是图形内部的噪点,用于突出显示图像中的暗区域。
img = cv2.imread('./dotinj.png')
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
dst = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel, iterations=2)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img,cmap="gray"); plt.title("img");
plt.subplot(122); plt.imshow(dst,cmap="gray"); plt.title("BLACKHAT");
二、图像轮廓
2.1 轮廓的查找与绘制
轮廓可以看作是具有相同强度或颜色的所有连续点的边界,通常在处理二值图像时使用。通过轮廓,图像中的物体形状和结构可以被有效提取,这在对象检测、识别和分析中非常有用。
cv2.findContours
是 OpenCV 中用于检测图像中轮廓的函数,其语法为:
findContours(image, mode, method[, contours[, hierarchy[, offset]]]) -> contours, hierarchy
-
image
:输入图像,通常是二值图像(黑白图像)。可以使用cv2.threshold
或cv2.Canny
将图像转换为二值图像。 -
mode
:轮廓检索模式,决定如何检索轮廓。常见的模式有:cv2.RETR_EXTERNAL
:只检测最外层轮廓。cv2.RETR_TREE
:按照树型检测所有轮廓, 从里到外,从右到左
-
method
:轮廓近似方法,决定如何处理轮廓点。常见的方法有:cv2.CHAIN_APPROX_NONE
:存储所有轮廓点,但这通常是没必要的,会产生很多冗余。cv2.CHAIN_APPROX_SIMPLE
:常用,只保留轮廓的关键拐点。
函数最终返回两个值:
contours
:轮廓点列表。列表中每个元素是一个 ndarray 数组,表示一个轮廓(轮廓上所有点的坐标)。hierarchy
:层级信息。对于每个轮廓,存储其父轮廓、子轮廓、下一轮廓和前一轮廓的索引。
轮廓查找完之后,返回的只是轮廓点的坐标信息。我们可以使用cv2.drawContours
函数,将轮廓绘制出来,其语法为:
drawContours(image, contours, contourIdx, color[, thickness[, lineType[, hierarchy[, maxLevel[, offset]]]]]) -> image
- image:将要绘制轮廓的图像,会被直接修改,可以考虑拷贝一份来绘制。
- contours:轮廓列表,每个轮廓都是一个
numpy
数组,表示轮廓上的点。 - contourIdx: 轮廓的索引,如果设置为负数,所有的轮廓都会被绘制。
- color: 轮廓线的颜色,用
(B, G, R)
元组表示。 - thickness: 轮廓线的厚度。如果为负数,轮廓内部会被填充指定的颜色。
- lineType:轮廓线类型,默认是
cv2.LINE_8
。其他选项包括cv2.LINE_4
、cv2.LINE_AA
等。 - hierarchy: 轮廓的层次结构信息,只有在绘制轮廓的子集时才需要。
- maxLevel: 绘制轮廓的最大级别。如果为0,只绘制指定的轮廓;如果为1,绘制轮廓及其子轮廓;以此类推。
- offset: 轮廓的偏移量,所有的轮廓都会按照这个偏移量进行移动。
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
# 原图是一个3通道彩色图,但显示出来是黑白图。
img = cv2.imread('./contours1.jpeg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 应用二值化处理
thresh, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓会直接修改原图,如果想保持原图不变, 建议copy一份
img_copy = img.copy()
# -1表示绘制所有轮廓,2为轮廓线厚度
cv2.drawContours(img_copy, contours, -1, (0, 0, 255), 2)
plt.figure(figsize=[8,4])
plt.subplot(121); plt.imshow(img[:,:,::-1]); plt.title("img");
plt.subplot(122); plt.imshow(img_copy[:,:,::-1]); plt.title("img_copy");
2.2 计算轮廓面积和周长
轮廓面积是指每个轮廓中所有的像素点围成区域的面积,单位为像素。使用 cv2.contourArea()
函数可以计算轮廓的面积,其语法为:
contourArea(contour[, oriented]) -> retval
使用 cv2.arcLength()
函数可以计算轮廓的周长,其语法为:
arcLength(curve, closed) -> retval
curve
:轮廓,一般是用findContours
函数返回的轮廓列表中的一个轮廓closed
:布尔值,如果为True
,表示轮廓是封闭的,计算周长;如果为False
,表示轮廓是开放的,计算曲线长度。
轮廓面积和周长有多种应用:
- 物体大小分析:通过计算面积,可以比较不同物体的大小。例如,机器人视觉可以通过面积判断不同物体的大小,从而做出选择和处理;
- 形状特征提取:结合面积和周长可以分析物体形状。例如,通过周长和面积的比率,可以判断轮廓是接近圆形、方形还是其他形状,以便检测图像中的指定形状的物体;
- 形状筛选:在特定场景中,可能需要过滤掉面积或周长过小或过大的轮廓。例如,在车牌识别中,可以通过设定面积阈值只保留符合条件的轮廓;
- 物体检测与分类:在图像中通过轮廓的面积来分类不同类型的物体。例如,根据物体的大小将它们分为大、中、小三类。
- 过滤噪声:在物体检测任务中,可能会检测到一些非常小的噪声点,可以通过面积筛选将它们过滤掉。
下面是车牌识别中,轮廓面积的简单应用示例:
import cv2
import numpy as np
# 读取图像并转换为灰度图像
image = cv2.imread('car.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 应用高斯模糊,减少噪声
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 使用Canny边缘检测
edges = cv2.Canny(blurred, 50, 150)
# 进行轮廓检测,返回轮廓列表及其索引
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 设定车牌的面积阈值
min_area = 1000 # 车牌的最小面积
max_area = 15000 # 车牌的最大面积
# 遍历所有轮廓,筛选符合面积条件的轮廓
for contour in contours:
area = cv2.contourArea(contour)
# 过滤掉不在面积范围内的轮廓
if min_area < area < max_area:
# 在图像上绘制轮廓
cv2.drawContours(image, [contour], -1, (0, 255, 0), 2)
# 计算轮廓的边界框(矩形)
x, y, w, h = cv2.boundingRect(contour)
# 提取轮廓对应的区域并显示
plate_region = image[y:y+h, x:x+w]
cv2.imshow("Plate Region", plate_region)
# 显示最终筛选后的图像
cv2.imshow("Filtered Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
2.3 多边形近似
findContours
找到的轮廓比较精细,有时候我们只想得到一个大致的轮廓。cv2.approxPolyDP
是 OpenCV 中用于轮廓近似的算法,可以对找出的轮廓进行多边形近似,来简化轮廓。
cv2.approxPolyDP
的实现是基于Douglas-Peucker 算法,其原理如下(详见《DP算法——道格拉斯-普克 Douglas-Peuker》):
- 初始设定:选择轮廓的两个端点作为多边形的起点和终点。
- 寻找最大距离点:在轮廓中找到离这条线段距离最远的点,如果该距离大于设定的阈值
epsilon
,则保留该点。 - 递归处理:将轮廓分成两个子段,分别递归执行该过程,直到所有剩余点的距离小于
epsilon
,最终形成近似的多边形。
approxPolyDP(curve, epsilon, closed[, approxCurve]) -> approxCurve
curve
:要简化的轮廓epsilon
:DP算法使用的阈值,阈值越大精度越低,保留的轮廓点数越少closed
:布尔值,指示轮廓是否封闭。如果为 True,输出的近似轮廓是封闭的。
2.4 凸包
逼近多边形是轮廓的高度近似,但是有时候,我们希望使用一个多边形的凸包来进一步简化它。
凸包是包含给定点集的最小凸多边形。换句话说,它是能够包围所有给定点的最小凸形状。凸包的每一处都是凸的,即在凸包内连接任意两点的直线都在凸包的内部。
cv2.convexHull
是OpenCV库中用于计算凸包(convex hull)的函数,其语法是:
convexHull(points[, hull[, clockwise[, returnPoints]]]) -> hull
points
:要简化的轮廓colckwise
:方向标志,默认为False。如果为True,输出的凸包为顺时针方向。returnPoints
:默认为True,表示返回凸包顶点的坐标,否则只返回凸包顶点的索引。
下面进行多边形近似和凸包的演示:
import cv2
import numpy as np
img = cv2.imread('./hand.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_,binary= cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
# 查找轮廓并画出,contours[0]是手的轮廓
contours, _ = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img_contours = img.copy()
cv2.drawContours(img_contours, contours, 0, (0, 0, 255), 2)
# 进行多边形逼近, 返回的是多边形上一系列的点, 即多边形逼近之后的轮廓
# 凸包和多边形都可以使用drawContours函数绘制,只是其接受的是轮廓列表格式
approx = cv2.approxPolyDP(contours[0], 20, True)
img_approx=img.copy()
cv2.drawContours(img_approx, [approx], 0, (0, 255, 0), 2)
# 计算凸包
hull = cv2.convexHull(contours[0])
img_hull=img.copy()
cv2.drawContours(img_hull, [hull], 0, (255, 0, 0), 2)
plt.figure(figsize=[16,8])
plt.subplot(141); plt.imshow(img[:,:,::-1]); plt.title("img");
plt.subplot(142); plt.imshow(img_contours[:,:,::-1]); plt.title("img_contours");
plt.subplot(143); plt.imshow(img_approx[:,:,::-1]); plt.title("img_approx");
plt.subplot(144); plt.imshow(img_hull[:,:,::-1]); plt.title("img_hull");
2.5 外接矩形和外接圆
关于轮廓还有一些其它的操作,比如最小外接矩阵、最大外接矩阵和最小外接圆。
cv2.minAreaRect(points) -> retval
points
:轮廓- 返回一个元组
(center(x, y), (width, height), angle)
,表示最小外接矩形的 中心点坐标,高宽,以及矩形相对于水平轴的旋转角度。
cv2.minAreaRect
返回的结果是外接矩形的中心点坐标、高宽以及旋转角度,可以使用opencv提供的cv2.boxPoints
函数,自动计算出矩形的四个角点坐标,也就得到了轮廓数据。然后就可以使用cv2.drawContours
将其标记出来。
cv2.boundingRect(array) -> retval
array
:轮廓- 返回一个元组
(center(x, y), (width, height))
。最大外接矩形一定是水平的,所以没有旋转角度,所以可以直接用画矩形的函数在图像上cv2.rectangle
画出来。
minEnclosingCircle(points) -> center, radius
下面进行简单的演示:
img = cv2.imread('./hello.jpeg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_,binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
contours,_= cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 最外面的轮廓是整个图像, contours[1]才是Hello语的轮廓
# rect是一个元组,包括(x, y), (w, h), angle
rect = cv2.minAreaRect(contours[1])
# 快速把rect转化为轮廓数据,得到的结果是浮点类型,要转为整型
box = cv2.boxPoints(rect)
box = np.round(box).astype('int64')
# 绘制最小外接矩形
img1=img.copy()
cv2.drawContours(img1, [box], 0, (255, 0, 0), thickness=2)
# 绘制最大外接矩形
x,y, w, h = cv2.boundingRect(contours[1])
img2=img.copy()
cv2.rectangle(img2, (x, y), (x + w, y + h), (0, 255, 0), thickness=2)
# 绘制最小外接圆,返回的也是浮点类型
center, radius=cv2.minEnclosingCircle(contours[1])
center, radius= np.round(center).astype('int64'),np.round(radius).astype('int64')
img3=img.copy()
cv2.circle(img3,center,radius,(0, 0, 255),thickness=2)
plt.figure(figsize=[16,8])
plt.subplot(141); plt.imshow(img[:,:,::-1]); plt.title("img");
plt.subplot(142); plt.imshow(img1[:,:,::-1]); plt.title("minAreaRect");
plt.subplot(143); plt.imshow(img2[:,:,::-1]); plt.title("boundingRect");
plt.subplot(144); plt.imshow(img3[:,:,::-1]); plt.title("minEnclosingCircle");
三、图像金字塔
图像金字塔是图像处理中的一种常用技术,它通过对原始图像进行一系列的降采样操作来创建一组图像。在OpenCV中,图像金字塔有两种类型:高斯金字塔和拉普拉斯金字塔。
图像金字塔在多种图像处理任务中有应用,包括:
- 图像缩放:快速放大或缩小图像。
- 图像融合:将不同分辨率的图像融合在一起。
- 图像分割:在不同的分辨率层次上分析图像。
- 多尺度目标检测:在不同尺度上检测目标。
3.1 高斯金字塔
高斯金字塔 (Gaussian Pyramid)是通过连续应用高斯模糊和降采样来构建的。每一层的图像都是上一层的图像经过高斯模糊后,删除其偶数行和列得到的。这样,金字塔的每一层都比上一层小,分辨率也低。
在构建高斯金字塔时,通常使用的是5x5的高斯卷积核来进行高斯模糊。这个卷积核的权重是根据高斯分布(正态分布)计算得出的。
将
G
i
G_i
Gi(表示不同层级的图像)与高斯卷积核进行卷积之后,去除所有偶数行和列,就得到一次下采样的结果。每次下采样之后,图像尺寸都减半,多次处理就得到整个高斯金字塔。
高斯模糊的过程,类似于将每个像素的特征分配一部分到邻域像素中,所以减去一半的行和列,图像基础特征不变。不过每次下采样,还是会丢失部分图像信息。
具体来说,我们使用下面两个函数进行操作:
cv2.pyrDown
:使用高斯金字塔进行一次降采样。cv2.pyrUp
:上采样,通过插入0来扩大图像,然后使用与pyrDown相同的卷积核进行卷积。
使用下采样时相同的高斯卷积核进行卷积,可以达到将原先像素分配到邻近插入的0像素的效果,近似恢复原图信息。下面进行演示:
import cv2
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread('./lena.png')
dst1 = cv2.pyrDown(img)
dst2 = cv2.pyrUp(dst1)
print(f' {img.shape=} {dst1.shape=} {dst2.shape=}')
plt.figure(figsize=[16,8])
plt.subplot(131); plt.imshow(img[:,:,::-1]); plt.title("img");
plt.subplot(132); plt.imshow(dst1[:,:,::-1]); plt.title("pyrDown");
plt.subplot(133); plt.imshow(dst2[:,:,::-1]); plt.title("pyrDown+pyrUp");
可以看到,先下采样再上采样,原图的信息还是有部分丢失,处理后的图像没有原图清晰。
3.2 拉普拉斯金字塔
拉普拉斯金字塔是基于高斯金字塔构建的,它主要用于图像的重建。拉普拉斯金字塔的每一层都是通过当前层原图减去其先下采样后上采样的图像得到的,代表了二者之间的残差。用数学公式表示就是:
L
i
=
G
i
−
P
y
r
U
p
(
P
y
r
D
o
w
n
(
G
i
)
)
L_{i}=G_{i}-PyrUp(PyrDown(G_{i}))
Li=Gi−PyrUp(PyrDown(Gi))
其中,
L
i
,
G
i
L_{i},G_{i}
Li,Gi分别是某一层的原始图像及其拉普拉斯金字塔图像。
lap0=img-dst2
plt.figure(figsize=[16,8])
plt.subplot(131); plt.imshow(img[:,:,::-1]); plt.title("img");
plt.subplot(132); plt.imshow(dst2[:,:,::-1]); plt.title("pyrDown+pyrUp");
plt.subplot(133); plt.imshow(lap0[:,:,::-1]); plt.title("lap0");
拉普拉斯金字塔包含了图像的细节和高频信息,所以可以间接地包含轮廓信息,多用于图像的多尺度表示和图像重建任务。
四、图像直方图
4.1 图像直方图基本概念
参考《相机直方图:色调和对比度》
图像直方图是一种显示图像中像素值分布情况的统计图表。它表示图像中各个像素强度值出现的频率,可以用来分析图像的对比度、亮度、动态范围等特性。直方图的横轴表示像素值(如果是灰度图,0表示最暗,255表示最亮),纵轴表示各像素值的像素数量。
如上图所示,左图水面整体偏亮,此部分对应于图像直方图右侧高亮度区域。右图将其分为上中下三个部分分别进行统计,上部像素分布均匀;中间是水面,像素过于集中;下方整体偏亮。
图像直方图既可以统计灰度图,也可以统计彩色图:
- 灰度图像直方图:横轴为0-255的灰度值,纵轴为该灰度值出现的频率。
- 彩色图像直方图:对RGB图像,可以为每个通道(红、绿、蓝)绘制单独的直方图,显示各通道像素值的分布。
4.2 统计直方图
4.2.1 直接统计
由于图像直方图是统计图像中像素值分布情况,所以可以直接使用plt.hist
对图像的灰度值进行统计。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像并转换为灰度
img = cv2.imread('./lena.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 创建图形
plt.figure(figsize=(12, 5))
ax1 = plt.subplot(121);ax1.imshow(gray, cmap='gray');ax1.axis('off');ax1.set_title('Grayscale Image')
ax2 = plt.subplot(122);ax2.hist(gray.ravel(), 256, [0, 256]);ax2.set_title('Histogram')
# tight_layout自调整子图参数,使之填充整个图像区域,同时确保子图之间的标签和标题不会重叠。
plt.tight_layout()
plt.show()
使用
plt.subplot
的方式并排显示,由于其默认显示坐标轴,两张图坐标会互相重叠
4.2.2 使用OpenCV统计图像直方图
OpenCV 中可以使用cv2.calcHist
进行图像直方图计算,其语法为:
calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]]) -> hist
-
images
:输入图像(列表形式,可以对一批图像进行统计)。即使只传入一张图像,也要放在列表中。 -
channels
:需要计算直方图的通道。对于灰度图只能为[0]
(单通道);对于彩色图像,[0]
、[1]
、[2]
分别表示蓝、绿、红三个通道。 -
mask
:掩膜图像。如果只想计算图像某一部分的直方图,可以传入一个与原图像大小相同的二值掩膜图像,白色部分表示计算区域,黑色部分忽略。若不需要则设None
-
histSize
:直方图的 bins 数量,一般设置为[256]
,表示256个灰度值都单独统计。假设设为16,则每15个像素区间统计一次。
-
ranges
:统计的像素值范围,一般为[0, 256]
。 -
accumulate
:是否累积,默认为False
。如果对一组图像进行统计,可以设为True
,表示统计图像时,在上一个直方图的基础上累积结果,而不是从0开始。
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图像
img = cv2.imread('./lena.png')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 统计直方数据
histb = cv2.calcHist([img], [0], None, [256], [0, 255])
histg = cv2.calcHist([img], [1], None, [256], [0, 255])
histr = cv2.calcHist([img], [2], None, [256], [0, 255])
# 创建一个图形窗口,包含两个子图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# 在第一个子图中显示原图
ax1.imshow(img_rgb);ax1.set_title('Original Image');ax1.axis('off') # 不显示坐标轴
# 在第二个子图中绘制直方图
ax2.plot(histb, color='b', label='blue');ax2.plot(histg, color='g', label='green');
ax2.plot(histr, color='r', label='red');ax2.set_title('Histogram using opencv');ax2.legend()
plt.tight_layout()
plt.show()
4.2.3 使用掩膜
我们可以通过使用掩膜,只统计图中感兴趣的区域。掩膜是与原图像大小相同的二值掩膜图像,只有白色区域会被统计。
# 生成灰度图,并创建掩膜
img = cv2.imread('./lena.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
mask = np.zeros(gray.shape, np.uint8) # 生成掩膜图像
mask[200:400, 200: 400] = 255 # 直接设置掩码区域
# 生成掩码部分的灰度图
# gray和gray做与运算结果还是gray, 结果再和mask做与运算,黑色部分置0,白色部分不变
gray_mask=cv2.bitwise_and(gray, gray, mask=mask)
# 对是否使用掩膜进行分别统计
hist_mask = cv2.calcHist([gray], [0], mask, [256], [0, 255])
hist_gray = cv2.calcHist([gray], [0], None, [256], [0, 255])
plt.figure(figsize=[10,5])
plt.subplot(121); plt.imshow(gray_mask,cmap='gray'); plt.title("gray_mask");
plt.subplot(122); plt.plot(hist_mask, label='mask');plt.plot(hist_gray, label='gray');plt.legend();
4.3 直方图均衡化
有的时候拍出的图片整体偏亮或偏暗,或者亮度很不均匀。直方图均衡化可以改善图像的对比度。它通过重新分配图像像素的灰度值,使得图像中灰度值的分布更加均匀,从而增强细节,使图像看起来更清晰(使灰度值扩展到整个范围,从而增加图像的全局对比度)。
直方图均衡化的实现原理:
-
计算图像的直方图: 首先统计出原始图像中每个灰度级(0-255)所出现的频率,即构建图像的直方图。
-
计算累积直方图:
|
- 映射原始像素值:将累计直方图结果直接乘以255就是最终均衡直方图的结果
在OpenCV中,可以使用cv2.equalizeHist()
函数来实现直方图均衡化。该函数只适用于灰度图像。下面我们对一张图进行整体增亮和增暗处理,然后进行直方图均衡化看看效果。
img=cv2.imread('lena.png')
matrix = np.ones(img.shape, dtype = "uint8") * 50
img_brighter = cv2.add(img, matrix)
img_darker = cv2.subtract(img, matrix)
# Show the images
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(img_darker[:,:,::-1]); plt.title("Darker");
plt.subplot(132); plt.imshow(img[:,:,::-1]); plt.title("Original");
plt.subplot(133); plt.imshow(img_brighter[:,:,::-1]);plt.title("Brighter");
彩色图像有多个通道(如 RGB 或 HSV 颜色空间),直接对每个通道进行直方图均衡化可能会导致颜色失真。因此,通常不会对 RGB 三个通道直接进行均衡化。比较常见的方法是将图像转换到亮度通道可分离的颜色空间(如 YUV 或 HSV),然后只对亮度通道进行直方图均衡化,再将处理后的图像转换回原来的颜色空间。
# 将图像从 BGR 转换到 YUV 颜色空间
yuv_darker=cv2.cvtColor(img_darker, cv2.COLOR_BGR2YUV)
yuv_brighter=cv2.cvtColor(img_brighter, cv2.COLOR_BGR2YUV)
# 对 Y 通道(亮度通道)进行直方图均衡化
yuv_darker[:,:,0] = cv2.equalizeHist(yuv_darker[:,:,0])
yuv_brighter[:,:,0] = cv2.equalizeHist(yuv_brighter[:,:,0])
# 将图像从 YUV 转换回 BGR 颜色空间
darker_equ=cv2.cvtColor(yuv_darker, cv2.COLOR_YUV2BGR)
brighter_equ=cv2.cvtColor(yuv_brighter, cv2.COLOR_YUV2BGR)
# # 显示原图和均衡化后的图像
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(darker_equ[:,:,::-1]); plt.title("darker_equ");
plt.subplot(132); plt.imshow(img[:,:,::-1]); plt.title("Original");
plt.subplot(133); plt.imshow(brighter_equ[:,:,::-1]);plt.title("brighter_equ");
4.4 自适应直方图均衡化 (CLAHE)
4.4.1 实现原理
直方图均衡化的局限性:
- 噪声增强:对于含有大量噪声的图像,均衡化可能会使噪声也得到增强,导致图像质量下降。
- 细节丢失:直方图均衡化是一种全局处理方法,无法处理局部区域对比度问题。如果图像中存在不同亮度的区域,全局均衡化可能会使局部细节丢失。
针对上述问题,OpenCV提供了自适应直方图均衡化(CLAHE, Contrast Limited Adaptive Histogram Equalization),它通过对图像的局部区域(称为“子图块”)分别进行直方图均衡化,从而增强局部对比度,同时避免过度增强噪声。
-
将图像分割成多个子图块: CLAHE将图像划分为多个较小的矩形区域(称为“子图块”或“窗口”,通常是8x8或16x16的网格)。每个子图块会单独进行直方图均衡化,这样可以增强每个局部区域的对比度。
-
对每个子图块进行直方图均衡化: 在每个子图块上执行和普通直方图均衡化类似的操作,计算该子图块的直方图,然后根据该直方图的累积分布函数 (CDF) 来重新分配像素值。
-
应用对比度限制: 在局部直方图均衡化时,某些子图块中的像素可能集中在特定的灰度范围内,导致对比度过度增强,尤其是在图像包含噪声时。因此,CLAHE引入了一个对比度限制参数
clipLimit
,用于限制每个灰度级的像素频率。当某个灰度级的频率超过clipLimit
时,多余的部分会均匀分配到其他灰度级。- clipLimit:表示限制直方图中某个灰度级出现的最大频率,防止噪声被过度放大。
-
插值平滑: 对于每个像素,由于它位于多个子图块的边界上,CLAHE对这些子图块的均衡化结果进行插值平滑,避免由于直接均衡化子图块而产生块状效应(blocky effect)。通过插值,这些子图块的边界变得平滑,使得过渡更加自然。
CLAHE的效果:
- 局部对比度增强:相比全局直方图均衡化,CLAHE能够有效增强图像中不同区域的对比度,因此在处理具有复杂光照或局部对比度差异大的图像时效果更好。
- 防止过度增强噪声:由于引入了对比度限制参数,CLAHE可以防止对比度过度增强,从而避免了噪声的放大。
- 适合自然图像:CLAHE常用于医学图像、卫星图像和低光照图像的处理,这些图像通常需要增强局部区域的对比度,而不希望整体图像变得太过刺眼。
属性 | 普通直方图均衡化 | CLAHE(自适应直方图均衡化) |
---|---|---|
处理范围 | 全局 | 局部,分块处理 |
效果 | 提高全局对比度,可能导致局部细节丢失 | 提高局部对比度,增强细节 |
噪声处理 | 可能过度增强噪声 | 使用clipLimit限制对比度增强,避免噪声过度增强 |
适用场景 | 适用于灰度值集中分布的图像,全局对比度不高 | 适用于包含复杂光照或局部对比度差异大的图像(如医学、卫星图像) |
常见问题 | 对局部细节处理不佳,可能丢失对比度 | 使用不当时,可能引入分块效应,不过插值技术可以有效减缓 |
4.4.2 代码实现
OpenCV 中使用cv2.createCLAHE
函数进行自适应直方图均衡化,它生成一个 CLAHE 对象,可以通过该对象对图像应用自适应直方图均衡化。
createCLAHE([, clipLimit[, tileGridSize]]) -> retval
-
clipLimit
:对比度限制阈值,浮点型,默认为2.0
。clipLimit
限制了每个灰度级像素频率的最大值,超过clipLimit
的频率会被平摊到其他灰度级,从而避免过度增强局部噪声。- 如果
clipLimit
值较低,对比度增强较弱。 - 如果
clipLimit
值较高,则会增强对比度。
- 如果
-
tileGridSize
:整型元组,表示子图块的大小。默认为(8, 8)
,即将图像分为 8×8 个子图块。对每个子图块单独进行直方图均衡化,然后在子图块之间进行插值以避免边界出现突变现象。- 值越大:处理的大块区域更多,图像整体的对比度调整幅度更大,但局部细节增强不明显。
- 值越小:处理的小块区域更多,图像局部对比度更强,但可能会引入噪声和块效应。
# 将图像从 BGR 转换到 YUV 颜色空间
yuv_darker=cv2.cvtColor(img_darker, cv2.COLOR_BGR2YUV)
yuv_brighter=cv2.cvtColor(img_brighter, cv2.COLOR_BGR2YUV)
# 创建CLAHE对象,设定clipLimit和tileGridSize
clahe = cv2.createCLAHE(clipLimit=1.0, tileGridSize=(4, 4))
# 对 Y 通道(亮度通道)进行直方图均衡化
yuv_darker[:,:,0] = clahe.apply(yuv_darker[:,:,0])
yuv_brighter[:,:,0] = clahe.apply(yuv_brighter[:,:,0])
# 将图像从 YUV 转换回 BGR 颜色空间
darker_equ=cv2.cvtColor(yuv_darker, cv2.COLOR_YUV2BGR)
brighter_equ=cv2.cvtColor(yuv_brighter, cv2.COLOR_YUV2BGR)
# # 显示原图和均衡化后的图像
plt.figure(figsize=[18,5])
plt.subplot(131); plt.imshow(darker_equ[:,:,::-1]); plt.title("darker_equ");
plt.subplot(132); plt.imshow(img[:,:,::-1]); plt.title("Original");
plt.subplot(133); plt.imshow(brighter_equ[:,:,::-1]);plt.title("brighter_equ");