文章目录
- 直方图增强基本逻辑-均衡化
- calcHist && equalizeHist
- calcHist
- equalizeHist
- 自适应直方图均衡化
前段时间忙活深度网络和android的东西去了,好久没讲讲传统图像处理了,这一篇继续来说说opencv中的传统图像处理部分——图像增强之直方图增强。
图像增强是一种基本的图像处理操作,简单的来说就是把图像变的更清晰,或者说感兴趣的某个区域需要变的更加清晰。
而清晰度这个概念,在清晰度计算这一章节中提到过,一般来说,像素之间的梯度越大,图像就越清晰。
而直方图是用于统计像素分布的一个工具,计算每幅图像的直方图是传统图像处理中的一种基本操作,直方图表现了一张图像所有的像素的分布情况,直方图增强就是通过调整直方图的分布来实现图像的增强,简单的说就是把图像像素重新分布一下,提高图像中像素的整体梯度,让图像变得更清晰。
这个过程被称作直方图均衡化。
附上之前直方图的基本计算方式:
https://blog.csdn.net/pcgamer/article/details/124989015?spm=1001.2014.3001.5501
清晰度计算:
https://blog.csdn.net/pcgamer/article/details/127942102?spm=1001.2014.3001.5501
直方图增强基本逻辑-均衡化
上面提到了均衡化的过程,其实就是把图像像素的分布改变一下。那么问题来了,怎么变?根据什么变?
首先来看一下opencv中的函数*equalizeHist()*的方式。
-
首先说下累计分布函数CDF(cumulative distribution function),这个函数可以这么理解:
-
直方图就是统计了某个灰度值的像素个数,比如灰度为100的像素个数有50个,总共有256中灰度值,那么可以记做:
n 100 = 50 n_{100} = 50 n100=50
,或者是$ n_i = 100, i = 100, 0<i<255$ -
归一化到[0, 1]的范围内,其实就是求这个灰度值出现的概率: p ( i ) = n i n p(i) = \frac{n_i}{n} p(i)=nni,n为像素总数。
-
那么累计分布函数就是:
H ( x ) = ∑ j = 1 i p x ( j ) H(x) = \sum_{j=1}^ip_x(j) H(x)=j=1∑ipx(j)
也就是某个灰度值所有的累计分布,比如灰度值100的H(x)就是从0-100的所有灰度值概率分布之和。 -
通过把每个点的像素值通过 H ( x ) H(x) H(x)来进行转换获得新的目标图像的灰度值。
-
-
有意思的是为什么要这么进行转换,这个证明过程不复杂,可以简单列一列。
-
首先可以认为原始图S,和目标图D。
-
原始图S的直方图分布记做 H A ( S ) H_A(S) HA(S),目标图D的直方图分布记做 H B ( D ) H_B(D) HB(D)。
-
我们的目的就是要找到一个映射关系 f f f,可以把原始图S中的像素值映射到目标图D,也就是说 D = f ( S ) D = f(S) D=f(S)。
-
直方图归一化到[0, 1]后,实际上就是某个灰度值的概率,所有的概率之和都是等于。
-
对于原图的直方图表示: ∑ 0 S H ( S ) \sum_0^SH(S) ∑0SH(S)就表示所有的概率之和。那么目标图的表示就是 ∑ 0 D H ( D ) \sum_0^DH(D) ∑0DH(D), 两者是相等的。可以表示为:
∑ 0 S H ( S ) = ∑ 0 D H ( D ) \sum_0^SH(S) = \sum_0^DH(D) 0∑SH(S)=0∑DH(D) -
最理想的目标图分布是均匀分布,也就是 H ( D ) = A N H(D) = \frac{A}{N} H(D)=NA, 其中的A表示每种像素值的值(每种都是相同的)。那么上面的公式就可以写成:
∑ 0 S H ( S ) = ∑ 0 D H ( D ) = D A N \sum_0^SH(S) = \sum_0^DH(D) = \frac{DA}{N} 0∑SH(S)=0∑DH(D)=NDA
其中 D = f ( S ) D=f(S) D=f(S)
所有有可以写成:
∑ 0 S H ( S ) = f ( S ) A N \sum_0^SH(S) = \frac{f(S)A}{N} 0∑SH(S)=Nf(S)A换一下项就可以得到:
f ( S ) = N A ∑ 0 S H ( S ) f(S) = \frac{N}{A}\sum_0^SH(S) f(S)=AN0∑SH(S)最右边的那一坨中的H(S)就是上面提到的累积概率分布,只是这里要对整张图像的像素再做一次求和或者积分。
-
具体怎么计算,这里就不说了,有兴趣的朋友可以去了解下。
-
当然,这里有个问题,上面的理想状态是不太可能达到的,如果某些图像的直方图在某个小区域出现比较大的聚集的话,可能就没法非常好的进行平均分布了。
-
calcHist && equalizeHist
在opencv中,用于直方图均衡化的函数就是equalizeHist:
先上代码:
img = cv2.imread("xxxx")
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
hist = cv2.calcHist([img], [0], None, [256], [0, 255])
plt.title('Gray Histogram Contour')
plt.xlabel('gray level')
plt.ylabel('number of pixels')
plt.figure(1)
plt.plot(hist)
dst = cv2.equalizeHist(img)
hist_new = cv2.calcHist([dst], [0], None, [256], [0, 255])
plt.title('Gray Histogram Contour new')
plt.xlabel('gray level')
plt.ylabel('number of pixels')
plt.figure(2)
plt.plot(hist_new)
plt.show()
cv2.imshow("src", img)
cv2.imshow("new", dst)
cv2.waitKey()
cv2.destroyAllWindows()
calcHist
在调用均衡化之前,先要计算图像的直方图,用于后续进行对比实验。
计算图像直方图的函数:调用opencv中的calcHist函数:
- calcHist函数接受下面几个参数
- [img],以列表的形式作为参数,img为需要计算直方图的图像。
- [0],通道数,如果是灰度图,就传0
- None,mask,用于ROI的掩码,传None就是统计整张图像。
- [256],histSize,就是说分成多少类,如果全部统计的话就是256个类。
- [0, 255],就是说哪些像素值需要被统计,[0, 255]就表示所有的像素值都需要被统计(8位)
然后再通过plt库进行绘图展示。
equalizeHist
直方图均衡化,这个函数就相对比较简单了,直接从源图到目标图进行转换。
我们看一下上面代码的结果,用来处理经典的一张图:
-
源图:
-
源图直方图
-
目标图
-
目标图直方图
从直方图分布可以看出,均衡化已经将像素点从相对集中变成了相对平衡的分布了。而这个累积分布概率函数的转换就是表明希望通过这样一个转换,使得像素尽量去满足一个像素值平均分布。
从最终形成的图像上来看,也是把一幅雾蒙蒙的图像变得相对清晰了。
但是存在一个这个方法典型的确定,马赛克现象比较严重。
自适应直方图均衡化
上面提到的均衡化方法有两个比较明显的不足:
- 马赛克现象,我理解是因为灰度是一个离散的点,会造成某个小区域发生阶跃性的变化,造成这种现象。
- 噪声被放大的现象,因为在整张图像上把像素值拉平的原因。
进一步的一个算法就是自适应直方图均衡化。
简单来说就是在上面的算法上做了两点改动:
- 利用局部特征,或者说局部的ROI特征进行CDF变换。也就是某个像素点周边的一个W*W的区域。这样就可以让灰度的阶跃变小。因为在一个小的区域里变换,噪声被放大的影响也不会太大。
- 上一步的改进中,会造成区域与区域之间被认为造成一些“边界”,所以这些边界需要通过双线性插值来进行“模糊”,让图像过渡比较连续。(双线性插值可以参考https://blog.csdn.net/pcgamer/article/details/125426351?spm=1001.2014.3001.5502)
- 为了防止局部对比度过于夸张,增加了一个限制对比度的参数,如果超过这个阈值,则会通过某种规则把这些灰度值分摊到区域中的其他像素值上去,让整个局部直方图更加的平缓。
代码也挺简单:
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
cl1 = clahe.apply(img)
hist_cl1 = cv2.calcHist([cl1], [0], None, [256], [0, 255])
plt.title('Gray Histogram Contour hist_cl1')
plt.xlabel('gray level')
plt.ylabel('number of pixels')
plt.figure(3)
plt.plot(hist_cl1)
plt.show()
cv2.imshow("hist_cl1", cl1)
其中的方法cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))是创建了一个CLANE类,其中的两个参数
- clipLimit = 2.0,对比度限制这个参数是用每块的直方图的每个bins的数和整图的平均灰度分布数的比值来限制的。 裁剪则是将每块图像直方图中超过ClipLimit的bins多出的灰度像素数去除超出部分,然后将所有bins超出的像素数累加后平均分配到所有bins。具体怎么分配的就不是特别清楚了。
- tileGridSize就是每个小区域的大小
- 再通过apply应用到图像上去
- CLANE是opencv算法库中的一种。
相比之前算法的图,有两点改进:
- 对比度与源图更类似,而不是整体上改变了源图的对比度,相对于普通的均衡化方法,分布的不是那么的均匀,但是更好的代表了源图的某些特征。
- 马赛克现象得到了缓解。
当然,不是在所有的图像上,自适应方法都可以比普通的均衡化方法更好的,需要根据图像的特征来进行判断。``