OpenCV算法学习笔记之对比度增强

发布于 26 天前  82 次阅读



此系列的其他文章:
OpenCV算法学习笔记之初识OpenCV
OpenCV算法学习笔记之几何变换
OpenCV算法学习笔记之平滑算法

对比度增强也叫做对比度拉伸,是图像增强技术的一种,主要解决由于图像的灰度级范围较小造成的对比度较低的问题,目的是将输出的图像的灰度级放大到指定的程度,使图像的细节看起来更清晰。常用的方法为线性变换、分段线性变换、伽马变化、直方图正规化、直方图均衡化、局部自适应直方图均衡化等,计算代价较小,但是可以产生较为理想的结果。

灰度直方图

什么是灰度直方图

灰度直方图是用来描述一幅图像信息的有效方式,灰度直方图的x轴的坐标代表灰度,对于8位深的单通道图,它的取值范围是[0,255]y轴代表数量,即一张图片中值等于x的像素数量。下面用C++实现计算灰度直方图的功能:

Mat calGrayHist(const Mat &image){
    // 存储256个灰度级的像素个数
    Mat histogram = Mat::zeros(Size(256, 1), CV_32SC1);
    // 图像的高和宽
    int rows = image.rows;
    int cols = image.cols;
    // 计算每个灰度级的个数
    for(int r = 0; r < rows; r++){
        for(int c = 0; c < cols; c++){
            int index = int(image.at<uchar>(r, c));
            histogram.at<int>(0, index) += 1;
        }
    }
    return histogram;
}

OpenCV提供函数calcHist()实现直方图的构建,但在计算8位图的灰度直方图时使用起来略显复杂。

灰度级范围越大代表对比度越高,反之对比度越低给人的感觉是看起来不够清晰。

线性变换

原理

线性变换是最简单的一种对比度增强方式。线性变换可以通过以下公式计算:
O(r,c)=a * I(r,c) + b
其中a控制的是图像的对比度,而b控制的是图像的亮度;对比度随着a增大而增大,亮度随着b增大而增大;显然a=1,b=0时图像不变;若a<1则对比度减弱,b<0则亮度减小。

实现

在Python中,实现线性变换的代码非常简单,仅仅利用numpy下的乘法运算符“*”即可:

import numpy as np
I = np.array([[200, 10], [0, 20]], np.uint8)
O = I * 2
print(O)

会有以下输出结果:

array([[144, 20], [0, 40]], dtype=uint8)

可以看到对于大于uint8范围的数字,numpy会进行取模运算,但是如果将2换为2.0,那么最后的结果会变成float64数据类型,范围也会增大。对于8位深的图来说,我们希望对比度增强后大于255的数直接截断而不是取模运算,所以不能简单的用“*”运算符对图像进行操作。我们可以用以下代码实现:

import cv2 as cv
import numpy as np

src = cv.imread("test.png")
a = 2
dst = float(a) * src
# 对大于255的进行截断
dst[dst>255] = 255
# 数据类型转换
dst = np.round(dst)
dst = dst.astype(np.uint8)
cv.imshow("dst", dst)
cv.waitKey()

在OpenCV中实现常数与矩阵相乘的方式有多种,可以通过Mat的成员函数convertTo()实现:

Mat::convertTo(OutputArray m, int rtype, double alpha=1, double beta=0)

示例代码:

Mat src = (Mat_<uchar>(2, 2) << 0, 200, 23, 4);
Mat dst;
src.convertTo(dst, CV_8UC1, 2.0, 0);

输入的数据类型为CV_8U时,输出的结果会对大于255的值进行截断处理;也可以利用乘法操作符“*”实现,同样也会对大于255的值自动截断为255,而且无论常数是什么类型,输出矩阵的数据类型总是和输入矩阵的类型相同。

OpenCV提供函数convertScaleAbs(InputArray sr, OutputArray dst, double alpha=1, double beta=0)实现线性变换,实现原理和我们之前所说的类似。

直方图正规化

原理

有时候直接用一个参数对整个图像进行操作可能结果不太理想,我们可以利用分段线性变换进行处理,即利用以下公式进行运算:
O(r,c)= \alpha_1 I(r,c)+b_1, 0 \leq{I(r,c)} < i \\ \alpha_2 I(r,c)+b_2, i \leq{I(r,c)} < j \\ \alpha_3 I(r,c)+b_3, j \leq{I(r,c)} \leq{255}

但是调整\alpha_k,b_k的值是一件很繁琐的事,这时候我们就可以通过直方图正规化的方式“自动”调整对应的值。

假设输入图像I高为h,宽为w,将I中出现的最小灰度值记为I_{min},出现的最大值记为I_{max},为了使输出图像O的灰度范围为[O_{min}, O_{max}],我们利用以下公式计算:
O(r,c)=\frac{O_{max} - O_{min}} {I_{max} - I_{min}}(I(r,c) - I_{min}) + O_{min}
其中0\leq{r} < h,0\leq{c} < w。上述过程称为直方图正规化,因为0 \leq{\frac{I(r,c) - I_{min}}{I_{max} - I_{min}}} \leq{1},所以O(r,c)\in{[O_{min},O_{max}]},一般令O_{min}=0, O_{max}=255。直方图正规化是一种自动选取\alpha,b的线性变换方法,其中:
\alpha = \frac{O_{max} - O_{min}}{I_{max} - I_{min}},b=O_{min}-\frac{O_{max} - O_{min}}{I_{max} - I_{min}} * I_{min}

实现

下面我们采用C++实现此算法,OpenCV提供函数minMaxLoc(src, double* minVal, double* maxVal=0, Point* minLoc=0, Point* maxLoc=0, InputArray mask=noArray())计算矩阵中的最大值和最小值,其中参数解释如下表所示:

参数 解释
src 输入矩阵
minVal 最小值,double类型指针
maxVal 最大值,double类型指针
minLoc 最小值的位置索引,Point类型指针
maxLoc 最大值的位置索引,Point类型指针

如果只想得出最大值和最小值,将位置索引设置为空即可:minMaxLoc(src, &minVal, &maxVal, NULL, NULL)

实现代码如下:

Mat src = imread("test.png", IMREAD_COLOR);
// 输入图像的最大最小值
double inMaxVal, inMinVal;
minMaxLoc(src, &inMinVal, &inMaxVal, NULL, NULL);
// 输出图像的最大最小值
double outMaxVal = 255, outMinVal = 0;
// 计算 alpha 和 b
double alpha = (outMaxVal - outMinVal)/(inMaxVal - inMinVal);
double b = outMinVal - a*inMinVal;
// 线性变换
Mat dst;
// 这里也可以用前面讲的Mat::convertTo()函数
convertScaleAbs(src, dst, alpha, b);
// 显示效果
imshow("dst", dst);
waitKey();

OpenCV提供函数normlize(src, dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray())实现了多种正规化操作,其中norm_type是正规化类型,常用的有NORM_L1,NORM_L2,NORM_MAX三种类型,对应了三种范数:

  1. 1-范数——计算矩阵中值的绝对值的和:||src||_1=\sum^M_{r=1}\sum^N_{c=1}|src(r,c)|
  2. 2-范数——计算矩阵中值的平方和的开方:||src||_2=\sqrt{\sum^M_{r=1}\sum^N_{c=1}|src(r,c)|^2}
  3. \infin-范数——计算矩阵中值的绝对值的最大值:||src||_{\infin}=max|src(r,c)|

在使用此函数时,通常令norm_type=NORM_MAX,原理和上面所说的是一样的,其中alpha相当于O_{max}beta相当于O_{min}。另外normlize函数可以处理多通道图片,是分别对每个通道进行正规化操作。

伽马变换

原理

假设输入图像为I,首先将其灰度值归一化[0, 1]区间上,对于8位深的图片也就是除以255。用I'(r,c)代表归一化后的第r行第c列的灰度值,则伽马变换后的输出图像O为:O(r,c)=I'(r,c)^\gamma。伽马变换的本质是对每个像素进行幂运算。

\gamma=1时,图像不变,如果图像整体或感兴趣局域较暗,可以令0<\gamma < 1提高对比度;\gamma > 1则会降低对比度。

实现

对于Python来说,numpy提供函数power()可以实现对矩阵的幂运算:

import numpy as np
I = np.array([[1, 2], [3, 4]])
O = np.power(I, 2)  # 对I中每个像素求平方
# O = ([[1, 4], [9, 16]])

根据原理对图像进行伽马变换:

src = cv2.imread('test.png')
# 归一化
after_src = src/255.0
# 伽马变化
gamma = 0.5
dst = np.power(after_src, gamma)

OpenCV提供函数pow(Input src, double power, Output dst)实现幂运算,其中输出的dst数据类型和输入矩阵数据类型相同。实现过程和Python类似,这里不再赘述。

全局直方图均衡化

原理

假设输入图像为I,高为h,宽为whist_I代表输入图像的直方图,hist_I (k)代表灰度值等于k的像素的数量。那么全局直方图均衡化就是对图像进行改变,使得输出的图像O的灰度直方图每一个灰度值的像素数量差不多,即hist_O (k)\approx \frac{h * w}{256},且对\forall p, \exists q,有\sum^p_{k=0}hist_I(k)=\sum^q_{k=0}hist_O(k)成立,其中p、q分别代表图像I、O的灰度值且都属于[0, 255]\sum^p_{k=0}hist_I (k)是图像的累加直方图。由上面式子可得
\sum^p_{k=0}hist_I (k) \approx (q+1) \frac{h * w}{256}
化简得
q \approx \frac{\sum^p_{k=0}hist_I(k)}{h * w}_256 - 1
由此我们就得出了全局直方图均衡化公式:
O(r,c) = \frac{\sum^{I(r,c)}_{k=0}hist_I(k)}{h * w}_256 - 1

实现

实现主要有四步:

  1. 计算图像灰度直方图
  2. 计算灰度直方图的累加直方图
  3. 利用公式得到输入灰度值与输出灰度值的关系
  4. 循环运算直到得到所有像素的灰度值

我们下面利用Python语言实现:

def equal_hist(image):
    # 图像的高和宽
    rows, cols = image.shape
    # 1. 计算灰度直方图
    gray_hist = cv2.calcGrayHist(image)
    # 2. 计算累加直方图
    zero_cumu_moment = np.zeros([256], np.uint32)
    for p in range(256):
        if p == 0:
            zero_cumu_moment[p] = gray_hist[0]
        else:
             zero_cumu_moment[p] = zero_comu_moent[p-1] + gray_hist[p]
    # 3. 利用公式得到输入灰度值与输出灰度值的关系
    output_q = np.zeros([256], np.uint8)
    confficient = 256.0/(rows * cols)
    for p in range(256):
        q = cofficient * float(zero_comu_moment[p]) - 1
        if q >= 0:
            output_q[p] = math.floor(q)
        else:
            output_q[p] = 0
    # 4. 计算结果
    equal_hist_image = np.zeros(image.shape, np.uint8)
    for r in range(rows):
        for c in range(cols):
            equal_hist_image[r][c] = output_q[image[r][c]]
    return equal_hist_image

实际上直方图均衡化后的结果可能并不表现为每个灰度值像素数量大约相同,这是由于在一些灰度级处可能没有像素,而在另外一些灰度级处像素很多造成的。全局直方图均衡化的结果容易受噪音、阴影、光照等因素的影响。

OpenCV提供函数equalizeHist()实现直方图均衡化,只支持8位图的处理。均衡化处理后暗区域的噪音可能会被放大,而亮区域可能损失信息,由此我们提出自适应直方图均衡化。

自适应直方图均衡化

自适应直方图均衡化首先会将图像划分为几个小区域,对每个局域分别进行直方图均衡化。为了解决某些小区域有噪音,均衡化后噪音会被放大的情况,用以下方法解决:如果直方图的某个灰度值的像素数量大于了提前预设好的限制值,那么多出来的部分会被裁剪并将其平均分布到其他灰度值上。这叫做“限制对比度”(Contrast Limiting)。OpenCV提供函数createCLAHE构建指向CLAHE的指针,默认的限制对比度是40。

#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
using namesapce cv;

int main(){
    Mat src = imread("test.png");
    // 构建CLAHE对象
    Ptr<CLAHE> clahe = createCLAHE(2.0, Size(6, 6));
    Mat dst;
    chahe->apply(src, dst);
    return 0;
}

参考

《OpenCV算法精解——基于Python和C++》(张平)第四章


一沙一世界,一花一天堂。君掌盛无边,刹那成永恒。