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

此系列的其他文章:
OpenCV算法学习笔记之初识OpenCV
OpenCV算法学习笔记之几何变换
OpenCV算法学习笔记之平滑算法
OpenCV算法学习笔记之阈值分割
OpenCV算法学习笔记之形态学处理
OpenCV算法学习笔记之边缘检测(一)
OpenCV算法学习笔记之边缘处理(二)
OpenCV算法学习笔记之形状检测

更多文章可以访问我的博客Aengus | Blog

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

灰度直方图

什么是灰度直方图

灰度直方图是用来描述一幅图像信息的有效方式,灰度直方图的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)= \left\{\begin{array}\alpha_1 I(r,c)+b_1,&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} \end{array}\right.
但是调整\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. \infty-范数——计算矩阵中值的绝对值的最大值:||src||_{\infty}=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++》(张平)第四章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容