获取灰度直方图的函数如下:
void cv::calcHist(const cv::Mat *images, int nimages, const int *channels, cv::InputArray mask, cv::OutputArray hist, int dims, const int *histSize, const float **ranges, bool uniform = true, bool accumulate = false)
用法如下:
Mat srcImage;
srcImage = imread("../orange.jpg");
//分割成三通道图像
vector<Mat> channels;
split(srcImage, channels);
//设定bin数目
int histBinNum = 256;
//设定取值范围
float range[] = {0, 255};
const float* histRange = range;
//设置参数
bool uniform = true;
bool accumulate = false;
//声明三个通道的hist数组
Mat red_hist, green_hist, blue_hist;
//计算直方图
calcHist(&channels[0], 1, 0, Mat(), red_hist, 1, &histBinNum, &histRange, uniform, accumulate);
calcHist(&channels[1], 1, 0, Mat(), green_hist, 1, &histBinNum, &histRange, uniform, accumulate);
calcHist(&channels[2], 1, 0, Mat(), blue_hist, 1, &histBinNum, &histRange, uniform, accumulate);
输出是Size为[1 x 256]的Mat类型。red_hist
代表红色直方图,green_hist
代表绿色直方图,blue_hist
代表蓝色直方图。
以下是绘制直方图曲线的方式(使用频率低):
//创建直方图窗口
int hist_w = 800;
int hist_h = 300;
int bin_w = cvRound((double)hist_w/histBinNum);
Mat histImage(hist_h, hist_w, CV_8UC3, Scalar(0, 0, 0));
//将直方图归一化到范围[0, hist_h]
normalize(red_hist, red_hist, 0, hist_h, NORM_MINMAX);
normalize(green_hist, green_hist, 0, hist_h, NORM_MINMAX);
normalize(blue_hist, blue_hist, 0, hist_h, NORM_MINMAX);
//循环绘制直方图
for(int i = 1; i < histBinNum; ipp)
{
line(histImage, Point(bin_w*(i-1), hist_h - cvRound(red_hist.at<float>(i-1))),
Point(bin_w*(i), hist_h - cvRound(red_hist.at<float>(i))), Scalar(0, 0, 255), 2, 8, 0);
line(histImage, Point(bin_w*(i-1), hist_h - cvRound(green_hist.at<float>(i-1))),
Point(bin_w*(i), hist_h - cvRound(green_hist.at<float>(i))), Scalar(0, 255, 0), 2, 8, 0);
line(histImage, Point(bin_w*(i-1), hist_h - cvRound(blue_hist.at<float>(i-1))),
Point(bin_w*(i), hist_h - cvRound(blue_hist.at<float>(i))), Scalar(255, 0, 0), 2, 8, 0);
}
//绘制图像
imshow("原图像", srcImage);
imshow("图像直方图", histImage);
效果大概如下:
图像对比度是通过灰度级范围来度量的,灰度级范围可通过观察灰度直方图得到。灰度级范围越大代表对比度越高;反之,对比度越低,图像在视觉上给人的感觉是看起来不够清晰的,所以需要通过算法调整图像的灰度值。
图像的线性变化可以利用以下公式定义:
O ( r , c ) = a ⋅ I ( r , c ) + b , 0 ≤ r < H , 0 ≤ c < W \mathbf{O}(r,c)=a\cdot \mathbf{I}(r,c)+b,0\le r
当 a = 1 , b = 0 a=1,b=0 a=1,b=0时, O \mathbf{O} O为 I \mathbf{I} I的一个副本
当 a > 1 a>1 a>1时,输出图像 O \mathbf{O} O的对比度比 I \mathbf{I} I有所增大
当 a < 1 a<1 a<1时,输出图像 O \mathbf{O} O的对比度比 I \mathbf{I} I有所减小
当 b > 0 b>0 b>0时,亮度增加
当 b < 0 b<0 b<0时,亮度减小
OpenCV实现有以下方式:
convertTo
void cv::Mat::convertTo(cv::OutputArray m, int rtype, double alpha = (1.0), double beta = (0.0))
参数alpha
和beta
可以理解为线性变换中的 a a a和 b b b。
Mat I = (Mat_<uchar>(2, 2) << 0, 200, 23, 4);
Mat O;
I.convertTo(O, CV_8UC1, 2.0, 0);
注意:当输出矩阵的数据类型是CV_8U时,大于255的值会自动截断为255。
Mat O = 3.5 * I;
注意:输出矩阵的数据类型和输入矩阵的数据类型相同,若为CV_8U,大于255的值会自动截断为255。
convertScaleAbs
void cv::convertScaleAbs(cv::InputArray src, cv::OutputArray dst, double alpha = (1.0), double beta = (0.0))
参数alpha
和beta
可以理解为线性变换中的 a a a和 b b b。
Mat I = (Mat_<uchar>(2, 2) << 0, 200, 23, 4);
Mat O;
convertScaleAbs(I, O, 2.0, 0);
注意:输出矩阵的数据类型和输入矩阵的数据类型相同,若为CV_8U,大于255的值会自动截断为255。
以上线性变换是对整个灰度级范围使用了相同的参数。有的时候也需要针对不同的灰度级范围进行不同的线性变换,即分段线性变换。
例如:
KaTeX parse error: Unknown column alignment: * at position 40: … \begin{array}{*̲{35}{l}} 0.5…
但是,选择合适的参数是很麻烦的。需要有一种方式可以基于当前图像的情况自动选取 a a a和 b b b的值,即直方图正规化。
O ( r , c ) = O max − O min I max − I min [ I ( r , c ) − I min ] + O min \mathbf{O}(r,c)=\frac{{{\mathbf{O}}_{\max }}-{{\mathbf{O}}_{\min }}}{{{\mathbf{I}}_{\max }}-{{\mathbf{I}}_{\min }}}[\mathbf{I}(r,c)-{{\mathbf{I}}_{\min }}]+{{\mathbf{O}}_{\min }} O(r,c)=Imax−IminOmax−Omin[I(r,c)−Imin]+Omin
显然,直方图正规化就是一种自动选取 a a a和 b b b的值的线性变换方法,其中:
a = O max − O min I max − I min a=\frac{{{\mathbf{O}}_{\max }}-{{\mathbf{O}}_{\min }}}{{{\mathbf{I}}_{\max }}-{{\mathbf{I}}_{\min }}} a=Imax−IminOmax−Omin
b = O min − O max − O min I max − I min ⋅ I min b={{\mathbf{O}}_{\min }}-\frac{{{\mathbf{O}}_{\max }}-{{\mathbf{O}}_{\min }}}{{{\mathbf{I}}_{\max }}-{{\mathbf{I}}_{\min }}}\cdot {{\mathbf{I}}_{\min }} b=Omin−Imax−IminOmax−Omin⋅Imin
直方图正规化最核心的步骤之一是计算原图中出现的最小灰度级和最大灰度级,OpenCV提供的函数:
void cv::minMaxLoc(cv::InputArray src, double *minVal, double *maxVal = (double *)0, cv::Point *minLoc = (cv::Point *)0, cv::Point *maxLoc = (cv::Point *)0, cv::InputArray mask = noArray())
src:输入矩阵
minVal:最小值,double类型指针
maxVal:最大值,double类型指针
minLoc:最小值的位置索引,Point类型指针
maxLoc:最大值的位置索引,Point类型指针
利用minMaxLoc
函数不仅可以计算出矩阵中的最大值和最小值,而且可以求出最大值和最小值的位置。在使用过程中如果只想得到最大值和最小值,则将其他的变量值设为NULL即可。
minMaxLoc(src, &minVal, &maxValue, NULL, NULL)
直方图正规化的C++实现如下所示:
//输入图像矩阵
Mat I = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
//找到I的最大值和最小值
double Imax, Imin;
minMaxLoc(I, &Imin, &Imax, NULL, NULL);
//设置Omin和Omax
double Omin = 0, Omax = 255;
//计算a和b
double a = (Omax - Omin) / (Imax - Imin);
double b = Omin - a * Imin;
//线性变换
Mat O;
convertScaleAbs(I, O, a, b);
//显示原图和直方图正规化的效果
imshow("I", I);
imshow("O", O);
也可以调用normalize
函数实现:
void cv::normalize(cv::InputArray src, cv::InputOutputArray dst, double alpha = (1.0), double beta = (0.0), int norm_type = 4, int dtype = -1, cv::InputArray mask = noArray())
src:输入矩阵
dst:结构元
alpha:结构元的锚点
beta:腐蚀操作的次数
norm_type:边界扩充类型
dtype:边界扩充值
当参数norm_type=NORM_L1时,计算src的1-范数(绝对值的和);
当参数norm_type=NORM_L2时,计算src的2-范数(平方和的开方);
当参数norm_type=NORM_INF时,计算src的∞-范数(绝对值的最大值);
d s t ( r , c ) = alpha ⋅ s r c ( r , c ) ∥ s r c ∥ + beta \mathbf{dst}(r,c)=\text{alpha}\cdot \frac{\mathbf{src}(r,c)}{\left\| \mathbf{src} \right\|}+\text{beta} dst(r,c)=alpha⋅∥src∥src(r,c)+beta
d s t ( r , c ) = alpha ⋅ s r c ( r , c ) − s r c min s r c max − s r c min + beta \mathbf{dst}(r,c)=\text{alpha}\cdot \frac{\mathbf{src}(r,c)-\mathbf{sr}{{\mathbf{c}}_{\min }}}{\mathbf{sr}{{\mathbf{c}}_{\max }}-\mathbf{sr}{{\mathbf{c}}_{\min }}}+\text{beta} dst(r,c)=alpha⋅srcmax−srcminsrc(r,c)−srcmin+beta
使用函数normalize
对图像进行对比度增强时,经常令参数norm_type = NORM_MINMAX,仔细观察会发现和直方图正规化原理详解中提到的计算方法是相同的,参数alpha
相当于 O max − O min {{\mathbf{O}}_{\max }}-{{\mathbf{O}}_{\min }} Omax−Omin,参数beta
相当于 O min {\mathbf{O}}_{\min } Omin。注意,使用normalize
可以处理多通道矩阵,分别对每一个通道进行正规化操作。
Mat src = imread(argv[1], CV_LOAD_IMAGE_ANYCOLOR);
Mat dst;
//alpha = Omax - Omin = 255 - 0 = 255;
//beta = Omin = 0;
normalize(src, dst, 255, 0, NORM_MINMAX, CV_8U);
首先将输入图像的灰度值归一化到 [ 0 , 1 ] [0,1] [0,1]的范围。对于8位图来说,除以255即可。
伽马变换就是令:
O ( r , c ) = I ( r , c ) γ \mathbf{O}(r,c)=\mathbf{I}{{(r,c)}^{\gamma }} O(r,c)=I(r,c)γ
//输入图像矩阵
Mat I = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
//灰度值归一化
Mat fI;
I.convertTo(fI, CV_64F, 1.0/255, 0);
//伽马变换
double gamma = 0.5;
Mat O;
pow(fI, gamma, O); //注意O和fI有相同的数据类型
//显示伽马变换后的效果
imshow("O", O);
注意:如果想本地保存O,则还需要将灰度值变为在[0,255]之间且转换为CV_8U类型。
O.convertTo(0, CV_8U, 255, 0);
imwrite("O.jpg", O);
假设 h i s t I ( k ) \mathbf{his}{{\mathbf{t}}_{\mathbf{I}}}(k) histI(k)代表灰度值等于 k k k的像素点的个数,其中 k ∈ [ 0 , 255 ] k\in [0,255] k∈[0,255]。我们期望 h i s t O ( k ) ≈ H ⋅ W 256 \mathbf{his}{{\mathbf{t}}_{\mathbf{O}}}(k)\approx\frac{H·W}{256} histO(k)≈256H⋅W(图像不可能出现每一个灰度级的像素点的个数都是严格相等的)。那么对应任意的灰度级 p p p, 0 ≤ p ≤ 255 0\le p\le255 0≤p≤255,总能找到 q q q, 0 ≤ q ≤ 255 0\le q\le255 0≤q≤255,使得:
∑ k = 0 p h i s t I ( k ) = ∑ k = 0 q h i s t O ( k ) ≈ ( q + 1 ) H ⋅ W 256 \sum\limits_{k=0}^{p}{\mathbf{his}{{\mathbf{t}}_{\mathbf{I}}}(k)=}\sum\limits_{k=0}^{q}{\mathbf{his}{{\mathbf{t}}_{\mathbf{O}}}(k)\approx (q+1)\frac{H\cdot W}{256}} k=0∑phistI(k)=k=0∑qhistO(k)≈(q+1)256H⋅W
化简可得:
q ≈ 256 ∑ k = 0 p h i s t I ( k ) H ⋅ W − 1 q\approx256\frac{\sum\limits_{k=0}^{p}{\mathbf{his}{{\mathbf{t}}_{\mathbf{I}}}(k)}}{H\cdot W}-1 q≈256H⋅Wk=0∑phistI(k)−1
由此可以得到:
O ( r , c ) = 256 ∑ k = 0 I ( r , c ) h i s t I ( k ) H ⋅ W − 1 \mathbf{O}(r,c)=256\frac{\sum\limits_{k=0}^{\mathbf{I}(r,c)}{\mathbf{his}{{\mathbf{t}}_{\mathbf{I}}}(k)}}{H\cdot W}-1 O(r,c)=256H⋅Wk=0∑I(r,c)histI(k)−1
OpenCV实现的直方图均衡化函数:
void cv::equalizeHist(cv::InputArray src, cv::OutputArray dst)
**背景:**全局直方图均衡化处理以后暗区域的噪声可能会被放大,变得清晰可见;亮区域可能会损失信息。为了解决该问题,提出了自适应直方图均衡化。
**思想:**首先将图像分为不重叠的区域块,然后对每一个块分别进行直方图均衡化。如果直方图的bin超过了提前预设好的”限制对比度“,那么会被裁剪,然后将裁剪的部分均匀分布到其他的bin。
默认设置“限制对比度”为40(以下设置为2),块的大小为8×8:
//输入图像
Mat src=imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
//构建CLAHE对象
Ptr<CLAHE> clahe = createCLAHE(2.0, Size(8, 8));
Mat dst;
//限制对比度的自适应直方图均衡化
clahe->apply(src,dst);