OpenCV–Python 图像增强

1.灰度直方图
在讲解图像增强的方法之前先来认识一下灰度直方图,灰度直方图是图像灰度级的函数,用来描述每个灰度级在图像矩阵中的像素个数或者占有率。接下来使用程序实现直方图:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

def calcGrayHist(I):
# 计算灰度直方图
h, w = I.shape[:2]
grayHist = np.zeros([256], np.uint64)
for i in range(h):
for j in range(w):
grayHist[I[i][j]] += 1
return grayHist

img = cv.imread(“../testImages/4/img1.jpg”, 0)
grayHist = calcGrayHist(img)
x = np.arange(256)
# 绘制灰度直方图
plt.plot(x, grayHist, ‘r’, linewidth=2, c=’black’)
plt.xlabel(“gray Label”)
plt.ylabel(“number of pixels”)
plt.show()
# cv.imshow(“img”, img)
# cv.waitKey()
Matplotlib本身也提供了计算直方图的函数hist,以下由matplotlib实现直方图的生成:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt
img = cv.imread(“../testImages/4/img1.jpg”, 0)
h, w = img.shape[:2]
pixelSequence = img.reshape([h * w, ])
numberBins = 256
histogram, bins, patch = plt.hist(pixelSequence, numberBins,
facecolor=’black’, histtype=’bar’)
plt.xlabel(“gray label”)
plt.ylabel(“number of pixels”)
plt.axis([0, 255, 0, np.max(histogram)])
plt.show()
cv.imshow(“img”, img)
cv.waitKey()
图像的对比度是通过灰度级范围来度量的,而灰度级范围可通过观察灰度直方图得到,灰度级范围越大代表对比度越高;反之对比度越低,低对比度的图像在视觉上给人的感觉是看起来不够清晰,所以通过算法调整图像的灰度值,从而调整图像的对比度是有必要的。最简单的一种对比度增强的方法是通过灰度值的线性变换实现的。

2.线性变换
假设输入图像为,宽为,高为,输出图像记为,图像的线性变换可以用以下公式定义:

 

如下图所示,当a=1,b=0时,为的一个副本;如果a>1,则输出图像的对比度比有所增大;如果0<a<1,则的对比度比有所减小。而b值的改变,影响的是输出图像的亮度,当b>0时,亮度增加;当b<0时,亮度减小。下面代码实现:

import numpy as np
a = np.array([[0, 200], [23, 4]], np.uint8)
b = 2 * a
print(b.dtype)
print(b)
”’
uint8
[[ 0 144]
[ 46 8]]
”’
在上面代码中,输入的是一个uint8类型的ndarray,用数字2乘以该数组,返回的ndarray的数据类型是uint8。注意输出第0行第1列,200*2应该等于400,但是400超出了uint8的数据范围,Numpy是通过模运算归到uint8范围的,即400%256=144,从而转换成uint8类型。如果将常数2改为2.0,虽然这个常数只是整型和浮点型的区别,但是结果却不一样。代码如下:

import numpy as np
a = np.array([[0, 200], [23, 4]], np.uint8)
b = 2.0 * a
print(b.dtype)
print(b)
”’
float64
[[ 0. 400.]
[ 46. 8.]]
”’
可以发现返回的ndarray的数据类型变成了float64,也就是说,相乘的常数是2和2.0会导致返回的ndarray的数据类型不一样,就会造成200*2的返回值是144,而200*2.0的值却是400;而对8位图进行对比度增强来说,线性变换计算出的输出值可能要大于255,需要将这些值截断为255,而不是取模运算,所以不能简单地只是用“*”运算来实现线性变换。具体代码如下:

import cv2 as cv
import numpy as np
import matplotlib.pyplot as plt

# 绘制直方图函数
def grayHist(img):
h, w = img.shape[:2]
pixelSequence = img.reshape([h * w, ])
numberBins = 256
histogram, bins, patch = plt.hist(pixelSequence, numberBins,
facecolor=’black’, histtype=’bar’)
plt.xlabel(“gray label”)
plt.ylabel(“number of pixels”)
plt.axis([0, 255, 0, np.max(histogram)])
plt.show()

img = cv.imread(“../testImages/4/img4.jpg”, 0)
out = 2.0 * img
# 进行数据截断,大于255的值截断为255
out[out > 255] = 255
# 数据类型转换
out = np.around(out)
out = out.astype(np.uint8)
# 分别绘制处理前后的直方图
# grayHist(img)
# grayHist(out)
cv.imshow(“img”, img)
cv.imshow(“out”, out)
cv.waitKey()

(a)原图                     (b)图(a)的灰度直方图                    (c)a=2的线性变换                       (d)图(c)的灰度直方图
以上线性变换是对整个灰度级范围使用了相同的参数,有的时候也需要针对不同灰度级范围进行不同的线性变换,这就是常用的分段线性变换,经常用于降低较亮或较暗区域的对比度来增强灰度级处于中间范围的对比度,或者压低中间灰度级处的对比度来增强较亮或者较暗区域的对比度。从下图(a)的灰度直方图(b)可以看出,图像的灰度主要集中在 [100,150]之间,可以通过以下分段线性变换将主要的灰度级拉伸到[50,230],结果如图(c)所示,对比度拉伸后显然比原图能够更加清晰地看到更多的细节。
(a)原图                                (b)图(a)的灰度直方图                     (c)分段线性变换                     (d)图(c)的灰度直方图
img = cv.imread(“../testImages/4/img7.jpg”, 0)
img = cv.resize(img, None, fx=0.3, fy=0.3)
h, w = img.shape[:2]
out = np.zeros(img.shape, np.uint8)
for i in range(h):
for j in range(w):
pix = img[i][j]
if pix < 50:
out[i][j] = 0.5 * pix
elif pix < 150:
out[i][j] = 3.6 * pix – 310
else:
out[i][j] = 0.238 * pix + 194
# 数据类型转换
out = np.around(out)
out = out.astype(np.uint8)
# grayHist(img)
# grayHist(out)
cv.imshow(“img”, img)
cv.imshow(“out”, out)
cv.waitKey()
线性变换的参数需要根据不同的应用及图像自身的信息进行合理的选择,可能需要进行多次测试,所以选择合适的参数是相当麻烦的。直方图正规化就是基于当前图像情况自动选取a和b的值的方法,下面介绍这种方法。

3.直方图正规化
假设输入图像为,宽为,高为,代表的第r行第c列的灰度值,将中出现的最小灰度级记为,最大灰度级记为,即,为使输出图像的灰度级范围为 ,和做以下映射关系:

这个过程就是直方图正规化,直方图正规化是一种自动选取a和b的值的线性变换方法,其中

下面使用Python代码实现直方图正规化:
img = cv.imread(“../testImages/4/img6.jpg”, 0)
# 计算原图中出现的最小灰度级和最大灰度级
# 使用函数计算
Imin, Imax = cv.minMaxLoc(img)[:2]
# 使用numpy计算
# Imax = np.max(img)
# Imin = np.min(img)
Omin, Omax = 0, 255
# 计算a和b的值
a = float(Omax – Omin) / (Imax – Imin)
b = Omin – a * Imin
out = a * img + b
out = out.astype(np.uint8)
cv.imshow(“img”, img)
cv.imshow(“out”, out)
cv.waitKey()
代码中计算原图中出现的最小灰度级和最大灰度级可以使用OpenCV提供的函数

minVal, maxVal, minLoc, maxLoc  = cv.minMaxLoc(src[, mask])

返回值分别为:最小值,最大值,最小值的位置索引,最大值的位置索引。

正规化函数normalize: dst=cv.normalize(src, dst[, alpha[, beta[, norm_type[, dtype[, mask]]]]])

使用函数normalize对图像进行对比度增强时,经常令参数norm_type=NORM_MINMAX,此函数内部实现和咱们上边讲的计算方法是相同的,参数alpha相当于,参数beta相当于。注意,使用normalize可以处理多通道矩阵,分别对每一个通道进行正规化操作。使用该函数的代码如下,实现结果和上边是相同的:

img = cv.imread(“../testImages/4/img6.jpg”, 0)
out = np.zeros(img.shape, np.uint8)
cv.normalize(img, out, 255, 0, cv.NORM_MINMAX, cv.CV_8U)
cv.imshow(“img”, img)
cv.imshow(“out”, out)
cv.waitKey()
4.伽马变换
假设输入图像为,宽为,高为,首先将其灰度值归一化到[0,1]范围,对于8位图来说,除以255即可。代表归一化后的第r行第c列的灰度值,输出图像记为,伽马变换就是。当时,图像不变。如果图像整体或者感兴趣区域较暗,则令可以增加图像对比度;相反,如果图像整体或者感兴趣区域较亮,则令可以降低图像对比度。图像的伽马变换实质上是对图像矩阵中的每一个值进行幂运算,Numpy提供的幂函数power实现了该功能,代码实现如下:

img = cv.imread(“../testImages/4/img8.jpg”, 0)
# 图像归一化
fi = img / 255.0
# 伽马变换
gamma = 0.4
out = np.power(fi, gamma)
cv.imshow(“img”, img)
cv.imshow(“out”, out)
cv.waitKey()
伽马变换在提升对比度上有比较好的效果,但是需要手动调节值。下面介绍一种利用图像的直方图自动调节图像对比度的方法。

5.全局直方图均衡化
对于直方图均衡化的实现主要分为四个步骤:
1、计算图像的灰度直方图
2、计算灰度直方图的累加直方图
3、输入灰度级和输出灰度级之间的映射关系
4、根据映射关系循环输出图像的每一个像素的灰度级
其中的映射关系是:,其中q为输出的像素,p为输入的像素。可以这么理解,这一项相当于是灰度直方图的累加概率直方图(范围在0~1之间),再将此范围放大至0~255之间便得到输出图像的像素。下面使用程序来实现:

def equalHist(img):
# 灰度图像矩阵的高、宽
h, w = img.shape
# 第一步:计算灰度直方图
grayHist = calcGrayHist(img)
# 第二步:计算累加灰度直方图
zeroCumuMoment = np.zeros([256], np.uint32)
for p in range(256):
if p == 0:
zeroCumuMoment[p] = grayHist[0]
else:
zeroCumuMoment[p] = zeroCumuMoment[p – 1] + grayHist[p]
# 第三步:根据累加灰度直方图得到输入灰度级和输出灰度级之间的映射关系
outPut_q = np.zeros([256], np.uint8)
cofficient = 256.0 / (h * w)
for p in range(256):
q = cofficient * float(zeroCumuMoment[p]) – 1
if q >= 0:
outPut_q[p] = math.floor(q)
else:
outPut_q[p] = 0
# 第四步:得到直方图均衡化后的图像
equalHistImage = np.zeros(img.shape, np.uint8)
for i in range(h):
for j in range(w):
equalHistImage[i][j] = outPut_q[img[i][j]]
return equalHistImage

img = cv.imread(“../testImages/4/img1.jpg”, 0)
# 使用自己写的函数实现
equa = equalHist(blur)
# grayHist(img, equa)
# 使用OpenCV提供的直方图均衡化函数实现
# equa = cv.equalizeHist(img)
cv.imshow(“img”, img)
cv.imshow(“equa”, equa)
cv.waitKey()
理解了上述代码,对于OpenCV提供的函数 equalizeHist() 就可以轻松掌握了,使用方法很简单,只支持对8位图的处理。虽然全局直方图均衡化方法对提高对比度很有效,但是均衡化处理以后暗区域的噪声可能会被放大,变得清晰可见,而亮区域可能会损失信息。为了解决该问题,提出了自适应直方图均衡化(Aptive Histogram Equalization)方法。

6.限制对比度的自适应直方图均衡化
自适应直方图均衡化首先将图像划分为不重叠的区域块,然后对每一个块分别进行直方图均衡化。显然,在没有噪声影响的情况下,每一个小区域的灰度直方图会被限制在一个小的灰度级范围内;但是如果有噪声,每一个分割的区域块执行直方图均衡化后,噪声会被放大。为了避免出现噪声这种情况,提出了“限制对比度”(Contrast Limiting),如果直方图的bin超过了提前预设好的“限制对比度”,那么会被裁剪,然后将裁剪的部分均匀分布到其他的bin,这样就重构了直方图。下面介绍OpenCV实现的限制对比度的自适应直方图均衡化函数,

img = cv.imread(“../testImages/4/img3.jpg”, 0)
img = cv.resize(img, None, fx=0.5, fy=0.5)
# 创建CLAHE对象
clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
# 限制对比度的自适应阈值均衡化
dst = clahe.apply(img)
# 使用全局直方图均衡化
equa = cv.equalizeHist(img)
# 分别显示原图,CLAHE,HE
cv.imshow(“img”, img)
cv.imshow(“dst”, dst)
cv.imshow(“equa”, equa)
cv.waitKey()
OpenCV提供的函数:cv.createCLAHE([, clipLimit[, tileGridSize]])

参数 解释
clipLimit 对比度限制的阈值,默认为40
tileGridSize 用于直方图均衡的区域块大小。 输入图像将被分成相同大小的矩形块。 tileGridSize定义行和列中的块数。默认为(8,8)
上图显示了对原图(a)进行限制对比度自适应直方图均衡化(CLAHE)和全局直方图均衡化(HE)的效果,会发现,原图中比较亮的区域,经过HE处理后出现了失真的情况,而且出现了明显的噪声,而CLAHE避免了这两种情况。