使用改进Gamma矫正修正图片人脸光照
本篇笔记主要用来记录自己完成一个项目数据预处理部分的一个功能——让人脸更加清晰突出过程中的思路和细节,主要使用了Python配合OpenCV实现。
引言
这次的主要目的是要完成把图片数据集中已经提取出的人脸进行进一步处理,使其光照更加趋中,并且突出人脸的一些边缘结构。由于要处理的图片很多,所以算法不能过于复杂,不然会导致训练时时间过长,万一效果不好反而不会起到优化的作用。
基于此,由于之前学过《数字图像处理》这门课程,首先想到的就是在处理光照和对比度时,可以采用两个比较简单的方法——直方图均衡和Gamma灰度校正;而关于突出人脸的结构,则联想到了利用拉普拉斯算子提取人脸结构边缘,再将其与原图叠加,这样能够让边缘更加突出。
那么大致思路基本就明确了,由于之前Python基础不太好,可以说是几乎没学过,在利用一下午入门Python之后,我打算先分别实现这三个功能,然后再将他们组合在一起,看看是否能有更好的效果。
读取图片
图片格式转换:BGR → RGB
在进行具体操作之前非常重要的一点就是一定要了解Python中的不同的 imread() 函数读取图片的格式是什么,以及后续用什么格式处理图片最好。
我在这里就被狠狠地坑了一次:由于第一次使用 OpenCV ,一直在本地使用 cv2.imread() 和 cv2.imshow() ,而换到服务器上的 Jupyter Notebook 编程时不能使用 cv2.imshow() ,在换成 plt.imshow() 显示图片之后图像色调发生明显变换我反反复复不能解决,花了很久才意识到是图片读取的问题。
这里必须要注意的是用 cv2.imread() 读取的图片是 BGR 格式的,这意味着它和RGB格式的图片通道是完全相反的,在使用 plt.imshow() 时一定要把通道换一下,或者说,在图片读取之后为了方便后续的处理,最好在这里就把通道换成 RGB 格式的,而不是到了图片显示之前才换成 RGB 。
关于换通道,这里有两种方法:
img2 = img[:,:,::-1]
########或者########
img2 = img[:,:,[2,1,0]]
这样就实现了 BGR → RGB的转换。
YCrCb图像
在搜寻彩色直方图均衡的代码过程中,我看到了很多人都没有在RGB的基础上直接转换图像,而是先把 RGB 转化成了 YCrCb 图像之后再进行直方图均衡,于是我去维基百科查询了 YCrCb 图像的概念—— 维基百科:YCrCb 。
由此我们可以知道:Y 为光强成分、而 CB 和 CR 则为蓝色和红色的浓度偏移量成分。
之所以在很多直方图均衡方法中大家选用 YCrCb 图像进行处理而不用 RGB 图像进行处理是因为RGB图像直方图均衡会出现颜色失真的情况。
出现这种情况的主要原因是:
直方图均衡是一个非线性过程。 分别对每个通道进行通道分割和均衡不是对比度均衡的正确方法。 均衡涉及图像的强度值而不是颜色分量。 因此,对于简单的RGB彩色图像,不应在每个通道上单独应用直方图均衡化,应该应用它以使强度值均衡而不干扰图像的色彩平衡。 因此,第一步是将图像的颜色空间从RGB转换为将强度值与颜色分量分开的颜色空间。
直方图均衡化
直方图均衡化就是将原始的直方图拉伸,使之均匀分布在全部灰度范围内,从而增强图像的对比度。
直方图均衡化的中心思想是把原始图像的的灰度直方图从比较集中的某个区域变成在全部灰度范围内的均匀分布。旨在使得图像整体效果均匀,黑与白之间的各个像素级之间的点更均匀一点。
在直方图均衡这一部分,了解到了 OpenCV 已经内置了直方图均衡的函数 cv2.equalizeHist() ,所以,在 OpenCV 读取图片之后可以使用这个函数,避免了自己列出一堆算法出现Bug。
下面来看一下代码:
import cv2
import numpy as np
from matplotlib import pyplot as plt
def hisEqulColor(img):
plt.figure(figsize=(10, 8))
ax1 = plt.subplot2grid((2,2),(0, 0))
ax1.imshow(img[:,:,::-1])
ax1.set_title('orignal image')
plt.axis('off')
ycrcb = cv2.cvtColor(img, cv2.COLOR_BGR2YCR_CB)
channels = cv2.split(ycrcb)
equ = cv2.equalizeHist(channels[0], channels[0])
equ = cv2.merge(channels, ycrcb)
equ = cv2.cvtColor(ycrcb, cv2.COLOR_YCR_CB2BGR)
ax1 = plt.subplot2grid((2, 2), (0, 1))
ax1.imshow(equ[:,:,::-1])
ax1.set_title('histogram equalization')
plt.axis('off')
return
img1 = cv2.imread('00001.png')
img2 = cv2.imread('00002.png')
hisEqulColor(img1)
hisEqulColor(img2)
Gamma矫正
Gamma校正,亦称“伽马校正”,通过非线性变换来调整像素值,进而改善图像的整体亮度。该算法由三个主要步骤组成:
首先,将输入图像的所有像素值进行归一化,使其值域分布在[0,1]之间。
其次,通过给定的γ值对像素值进行非线性映射,其计算过程如下所示:
\[f(I)=I^{\gamma}\]
其中,I 为输入图像的像素值,f(I) 为输出的非线性映射值,γ 为非线性映射参数。需要指出,不同的γ值对图像亮度质量的改善效果不尽相同。当 γ<1 时,图像的灰度值增大,整体亮度提高并增强对比度;而当 γ<1 时,图像的灰度值减小,整体亮度降低,但也能在一定程度上增强图像的对比度。
最后,对所得像素值进行反归一化,使得像素值的值域扩大到[0,255],从而获得伽马校正后的图像。
为了说明上述处理对图像的影响,下图给出了一幅标准人脸测试图像经过Gamma校正后的效果。
通过了解Gamma矫正的基本原理,我们可以知道它的确可以改善图像的整体亮度,但是其中的参数 γ 在程序中针对不同光强的图片是不可能一张一张输入具体值的,这样无法达到自适应矫正的目的。
为了能够实现自适应的要求,这里利用一个很简单的算法:
首先求出图像整体的像素平均值,并依次判断待处理图像的整体曝光质量。以灰度128为基准,整幅图像的像素平均值若超过128,则视为偏亮,否则视为偏暗。进而,以图像均值偏离128的归一化幅度作为Gamma矫正的自适应参数,具体计算公式如下:
\[f(I)=I^{\gamma}\: ,\: \gamma =\frac{\left | mean(I)-128 \right |}{128}\: ,\: (mean(I)<128)\]
\[f(I)=I^{\gamma}\: ,\: \gamma =\frac{128}{ \left | mean(I)-128 \right | }\: ,\: (mean(I)>128)\]
下面来看一下代码:
import cv2 as cv
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
def modifiedgamma(im):
############################
#####通道转换!!!!!#####
img2 = im[:,:,[2,1,0]]
############################
############################
plt.figure()
plt.subplot(1,2,1)
plt.title("input")
plt.imshow(img2)
plt.axis('off')
img2 = np.array(img2)
mean = np.mean(img2)
if mean < 128:
gamma = (128-mean)/128
else :
gamma = 128/(mean-128)
ycrcb = cv.cvtColor(img2,cv.COLOR_BGR2YCR_CB)
channels = cv.split(ycrcb)
channels[0] = np.power(channels[0]/255.0,gamma)*255.0
channels[0] = channels[0].astype(np.uint8)
dst1 = cv.merge(channels, ycrcb)
dst1 = cv.cvtColor(dst1, cv.COLOR_YCR_CB2BGR)
plt.subplot(1,2,2)
plt.title("Gamma")
plt.imshow(dst1)
plt.axis('off')
src1 = cv.imread("./00001.png")
src2 = cv.imread("./00023.png")
src3 = cv.imread("./00002.png")
modifiedgamma(src1)
modifiedgamma(src2)
modifiedgamma(src3)
可以看到效果还是不错的,其中第二张图本身因为人脸图像的质量就不高所以矫正之后效果也不是很好,这里只是用来测试不同亮度是否有自适应的效果。
Laplace算子提取图像边缘
拉普拉斯算子可以用来提取图像的边缘特征,具体的概念这里不再赘述了,下图即为拉普拉斯算子:

我们来看一下利用它提取图像边缘特征的代码:
import cv2 as cv
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
def mylaplace(im):
img2 = im[:,:,[2,1,0]]
plt.figure(figsize=(10, 8))
plt.subplot(2,2,1)
plt.title("input")
plt.imshow(img2)
plt.axis('off')
img2 = np.array(img2)
mean = np.mean(img2)
if mean < 128:
gamma = (128-mean)/128
else :
gamma = 128/(mean-128)
ycrcb = cv.cvtColor(img2,cv.COLOR_BGR2YCR_CB)
channels = cv.split(ycrcb)
channels[0] = np.power(channels[0]/255.0,gamma)*255.0
channels[0] = channels[0].astype(np.uint8)
dst1 = cv.merge(channels, ycrcb)
dst1 = cv.cvtColor(dst1, cv.COLOR_YCR_CB2BGR)
plt.subplot(2,2,2)
plt.title("Gamma")
plt.imshow(dst1)
plt.axis('off')
kernel = np.array([[0,1,0],[1,-4,1],[0,1,0]], np.float32)
dst21 = cv.filter2D(img2,-1,kernel = kernel)
plt.subplot(2,2,3)
plt.title("input's laplace")
plt.imshow(dst21)
plt.axis('off')
dst22 = cv.filter2D(dst1,-1,kernel = kernel)
plt.subplot(2,2,4)
plt.title("Gamma's laplace")
plt.imshow(dst22)
plt.axis('off')
plt.tight_layout()
src2 = cv.imread("./00023.png")
src1 = cv.imread("./00001.png")
mylaplace(src1)
mylaplace(src2)
可以看出在图像本身清晰度不高的情况下拉普拉斯算子提取出的轮廓部分并不是那么的多,但还是有的,对于最后的融合多少有一些帮助。
图像加权融合调整
单纯的应用Gamma矫正和边缘特征提取 均无法实现在调整对比度的同时有效增强可识别特征的目的,甚至Gamma矫正还可能对图像的细节造成弱化或改变,考虑到这一点,我们应该把两张图片加权融合在一起,这样才能达到双重目的,提高人脸识别准确率。经过多次试验之后,我采取的计算公式如下所示:
\[f(I)=f(I)+1.2 \cdot f(e)\]
其中,f ( I )表示原图像,f ( e )表示进行边缘提取后的细节特征。
由于细节特征的融合过程可能也会使图像的亮度信息发生改变,所以在融合完毕之后我尝试着对融合后的图片又进行了一次直方图均衡化,最终获得了处理后的人脸图像。
下面看一下总体代码:
import cv2 as cv
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False
def histequ(img):
ycrcb = cv.cvtColor(img, cv.COLOR_BGR2YCR_CB)
channels = cv.split(ycrcb)
equ = cv.equalizeHist(channels[0], channels[0])
equ = cv.merge(channels, ycrcb)
equ = cv.cvtColor(ycrcb, cv.COLOR_YCR_CB2BGR)
ax1 = plt.subplot2grid((2, 2), (1, 1))
ax1.imshow(equ)
ax1.set_title('直方图均衡')
plt.axis('off')
return
def modifiedgamma(img):
img2 = img[:,:,[2,1,0]]
plt.figure(figsize=(6, 4))
plt.title("输入")
plt.imshow(img2)
plt.axis('off')
img2 = np.array(img2)
mean = np.mean(img2)
if mean < 128:
gamma = (128-mean)/128
else :
gamma = 128/(mean-128)
dst1 = cv.cvtColor(img2,cv.COLOR_BGR2GRAY)
dst1 = np.power(img2/255.0,gamma)*255.0
dst1 = np.round(np.array(dst1)).astype(np.uint8)
plt.figure(figsize=(8, 6))
ax1 = plt.subplot2grid((2,2),(0, 0))
ax1.set_title("Gamma矫正")
ax1.imshow(dst1)
plt.axis('off')
kernel = np.array([[0,1,0],[1,-4,1],[0,1,0]], np.float32)
dst2 = cv.filter2D(dst1,-1,kernel = kernel)
ax1 = plt.subplot2grid((2,2),(0, 1))
ax1.set_title("Laplace算子提取边缘")
ax1.imshow(dst2)
plt.axis('off')
mixed = cv.addWeighted(dst1, 1, dst2, 0.02, 0)
ax1 = plt.subplot2grid((2,2),(1, 0))
ax1.set_title("改进Gamma矫正")
ax1.imshow(mixed)
plt.axis('off')
histequ(mixed)
plt.tight_layout()
src1 = cv.imread("./00001.png")
src2 = cv.imread("./00023.png")
src3 = cv.imread("./00002.png")
modifiedgamma(src1)
modifiedgamma(src2)
modifiedgamma(src3)
可以看出这一系列操作之后对于人脸图像的提升还是很明显的,虽然依然存在着很多的不足,但是我感觉目前用于人脸图像预处理一些小的图片已经足够,如果以后发现了更多更好的方法,会在后续进行补充。
另外的一些尝试
其实在完成这个方法之后我又改变了一下整体的处理流程的顺序,看看会不会有一些好转,原来的处理流程是:
读取图片 → 直方图均衡 → 拉普拉斯算子提取边缘 → Gamma矫正 → 融合
改变了一下处理顺序之后的流程:
读取图片 → Gamma矫正 → 拉普拉斯算子提取边缘 → 直方图均衡 → 融合
先看一下处理后的结果吧:
可以看出最后输出的结果远远比不上之前的处理流程的输出结果,所以这里就不放代码了,其实代码改动也不大,无非是调整了处理顺序。
虽然说这次尝试的结果不是很好,但是有新的想法还是必须要尝试的,后续如果有新的想法会继续补充,这里就先写到这吧。
参考文章
- Opencv-Python学习笔记七——图像直方图 calcHist,直方图均衡化equalizeHist: https://www.jianshu.com/p/bd12c4273d7d
- Gamma矫正: https://learnopengl-cn.readthedocs.io/zh/latest/05%20Advanced%20Lighting/02%20Gamma%20Correction/
- 差分近似图像导数算子之Laplace算子: https://blog.csdn.net/songzitea/article/details/12842825