直方图概述
直方图是变量分布的统计图表示,它让我们能够理解数据的密度估计和概率分布。直方图是通过整个变量值范围划分为小的值范围,然后计算每个间隔落入多少个值来创建的
编码实现
//首先判断图像的类型
if (src.channels() == 1) {//灰度图像
// do something
}
else if (src.channels() == 3) {//BGR三通道图像
//1.将BGR三通道图像弄成vector
std::vector <Mat> bgr;
cv::split(src, bgr);
//2.定义数据范围并创建三个矩阵来存储每个直方图
float range[] = { 0,256 };
const float* histRange = { range };
Mat bHist, gHist, rHist;
int numBins = 256;
//3.用calcHist来计算直方图
//api说明(输入图像,用于计算直方图的输入图像数,用于计算直方图的数字通道尺寸,可选的掩码矩阵,用于存储得到的直方图的变量,直方图维度,要计算的区间数(256?),输入变量的范围)
cv::calcHist(&bgr[0], 1, 0, Mat(), bHist, 1, &numBins, &histRange);
cv::calcHist(&bgr[1], 1, 0, Mat(), gHist, 1, &numBins, &histRange);
cv::calcHist(&bgr[2], 1, 0, Mat(), rHist, 1, &numBins, &histRange);
//4.对直方图进行标准化
//在最小值0和最大值之间标准化直方图矩阵
//api说明:(输入图像,输出图像,直方图矩阵最低点,直方图矩阵最高点,前两个参数的枚举类型说明)
int w = 512, h = 300;
cv::normalize(bHist, bHist, 0, h, NORM_MINMAX);
cv::normalize(gHist, gHist, 0, h, NORM_MINMAX);
cv::normalize(rHist, rHist, 0, h, NORM_MINMAX);
//5.计算完毕之后在画布上画出来
Mat histImg(h, w, CV_8UC3, Scalar(20, 20, 20));
//6.画图线
//计算每一条直线的步长,计算有多少个像素在每个区间之间,从水平位置i-1到i绘制每条小线,垂直位置是相应i中的直方图值
int binStep = cvRound((float)w / (float)numBins);
for (int i = 1; i < numBins; i++) {
cv::line(histImg, Point(binStep * (i - 1), h - cvRound(bHist.at<float>(i - 1))),
Point(binStep * (i), h - cvRound(bHist.at<float>(i))), Scalar(255, 0, 0));//注意参数
cv::line(histImg, Point(binStep * (i - 1), h - cvRound(gHist.at<float>(i - 1))),
Point(binStep * (i), h - cvRound(gHist.at<float>(i))), Scalar(0, 255, 0));//注意参数
cv::line(histImg, Point(binStep * (i - 1), h - cvRound(rHist.at<float>(i - 1))),
Point(binStep * (i), h - cvRound(rHist.at<float>(i))), Scalar(0, 0, 255));//注意参数
}
//7.显示图像即可
imshow("hist", histImg);
代码剖析
对于代码中所使用的api,有些参数可能不是那么好理解,在这里进行解释
calcHist
参数为:
(输入图像,用于计算直方图的输入图像数,用于计算直方图的数字通道尺寸,可选的掩码矩阵,用于存储得到的直方图的变量,直方图维度,要计算的区间数(256?),输入变量的范围)
这个api的作用是计算出直方图的高度,注意,这个高度是相对的,需要画出来之前要先对直方图作一个归一化,
所谓的掩码矩阵就是,在绘制直方图的同时要不要对图像进行增强,增加一个卷积内核.
line
在本编码中,对于线的绘制比较复杂,但实际上非常简单,关键是要搞懂Point里面的参数,首先cvRound是一个四舍五入的函数,然后binstep是步长,设置这个参量可以使得绘制的线在整幅图像上都铺满,接着是 **h-cvRound(rHist.at < float >(i-1) )**这个参数,由于图像的坐标分布是从左上角开始的,因此对于线在图中的坐标应当是最大高度-直方图中所计算得到的高度.
那么在这里可以在直方图绘制的过程中,进行打擂, 计算出频度最高的灰度值,可以作为阈值参考使用
灰度分布直方图
以上是根据opencv的内置api来计算得到直方图,下面对一个灰度图像进行灰度分布直方图的计算,并且求取阈值
#include <stdio.h>
#include <math.h>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int srcRow=128;
const int srcCol=128;
void handle(int pic[][128],double &a,double &b){
//1.十字架不一定水平与垂直
//2.存在模糊
//3.明暗度不同
//阈值化分割:灰度图像阈值化分割
//1.设置好计算的参数
double nHistogram[256];//灰度分布直方图
double dVariance[256];//类间方差
int sumPic=srcRow*srcCol;
for(int i=0;i<256;i++){
nHistogram[i]=0.0;
dVariance[i]=0.0;
}
//2.对每一个像素点进行归类
for(int i=0;i<srcRow;i++){
for(int j=0;j<srcCol;j++){
nHistogram[pic[i][j]]++;//计算出频度
}
}
//3.根据频度计算出概率
double Pa=0.0;//背景出现的频率
double Pb=0.0;//十字出现的频率
double Wa=0.0;//背景平均灰度
double Wb=0.0;//目标平均灰度
double W0=0.0;//全局平均灰度
double dData1=0.0,dData2=0.0;
for(int i=0;i<256;i++){
nHistogram[i] /= sumPic;//计算出频率
W0 += i*nHistogram[i];
}
//4.对每个灰度值计算类间方差
for(int i=0;i<256;i++){
Pa += nHistogram[i];
Pb = 1-Pa;
dData1 += i*nHistogram[i];
dData2 = W0-dData1;
Wa = dData1/Pa;
Wb = dData2/Pb;
dVariance[i] = (Pa*Pb* pow((Wb-Wa), 2));
}
//5.遍历每个方差,打擂求取类间最大方差所对应的灰度值
double temp=0;
double nThreshold = 0;
for(int i=0;i<256;i++){
if(dVariance[i]>temp){
temp=dVariance[i];
nThreshold=i;
}
}
//根据阈值二值化图像,找到十字中心
//1.图像二值化
for(int i=0;i<srcRow;i++){
for(int j=0;j<srcCol;j++){
if(pic[i][j]>= (nThreshold-19) ){
pic[i][j]=255;
}else{
pic[i][j]=0;
}
}
}
//2.设置边缘点及其检测标志
double top=srcRow-1,down=0,left=srcCol-1,right=0;//假设上下左右的那几个端点
//3.检测最左侧的点和最右侧的点(打擂)
for(int i=0;i<srcRow;i++){
for(int j=0;j<srcCol;j++){//找最左侧的点
if(pic[i][j]==0 && left>j){
left=j;
}
}
for(int j=srcCol-1;j>=0;j--){//右
if(pic[i][j]==0 && right<j){
right=j;
}
}
}
//4.检测最上面的点和最下面的点
for(int i=0;i<srcCol;i++){
for(int j=0;j<srcRow;j++){//上
if(pic[j][i]==0 && top>j){
top=j;
}
}
for(int j=srcRow-1;j>=0;j--){//下
if(pic[j][i]==0 && down<j){
down=j;
}
}
}
//5.计算
b=(top+down)/2;
a=(right+left)/2;
//write it
/*
FILE *fp;
fp=fopen("E:/data/firsttest_output.txt","r+");
for(int i=0;i<srcRow;i++){
for(int j=0;j<srcCol;j++){
fprintf(fp,"%d ",pic[i][j]);
}
fprintf(fp,"\n");
}
*/
}
int main(){
int input[srcRow][srcCol];
FILE *fp;
fp=fopen("E:/data/83 80.txt","r+");
for(int i=0;i<srcRow;i++){
for(int j=0;j<srcCol;j++){
int x;
fscanf(fp,"%d",&x);
input[i][j]=x;
}
}
double ansx,ansy;
handle(input,ansx,ansy);
cout<<(int)ansy<<" "<<(int)ansx;
}
计算灰度分布直方图的方法
OTSU算法:
1.首先建立一定区间的数组,用来存储类间方差和灰度分布频度
2.分别计算前景、背景区域所占像素点的个数及其比例
3.计算前景、背景区域所占像素值均值
4.计算类间方差,公式为:前景区域像素点的频率 * (前景像素值均值-全图像素值均值)的平方 + 背景区域像素点的频率 (背景像素值均值-全图像素值均值)的平方
5.将阈值从0-255进行遍历,求取使得类间方差最大的阈值.
直方图的应用:图像颜色均衡
图像均衡(即直方图均衡化)试图获得具有均匀分布值的直方图。均衡的结果是导致图像的对比度增强。均衡能够使对比度较低的局部区域获得高对比度,从而分散最频繁的强度。当图像非常暗或者非常亮,并且背景和前景之间存在非常小的差异时,通过使用直方图均衡化,可以增加对比度,并提升暴露过度或者暴露不足的细节。
主要缺点:背景噪声会增加,有用信号的减少
具体算法步骤
为了均衡彩色图像,只需均衡亮度通道。可以用每个颜色通道执行该操作,但结果不能使用。
可以采用任何其他彩色图像格式(例如HSV或YCrCb)来分离单个通道中的亮度分量,因此,可以选择YCrCb并用Y通道(亮度)进行均衡
算法步骤在此处呈现在代码上
//1.用cvtcolor函数将BGR图像转换或者输入为YCrCb
Mat result, ycrcb;
cv::cvtColor(src, ycrcb, COLOR_BGR2YCrCb);
//2.将YCrCb图像的各个通道分离出来
vector <Mat> channels;
split(ycrcb, channels);
//3.用equalist函数均衡在Y通道中的直方图,该函数只有两个参数,输入和输出矩阵
cv::equalizeHist(channels[0], channels[0]);
//4.合并生成的通道并将其转换为BGR格式
cv::merge(channels, ycrcb);
cv::cvtColor(ycrcb, result, COLOR_YCrCb2BGR);
imshow("dst", result);
Lomography效果
简单地说,LOMO效果是一种渐变光晕的效果,不太好描述,请查阅百度上的相关图片
应用:在图像中产生一种光环的效果,在LOMO效果之后可以使用blur滤镜函数对光影图像应用大模糊,以获得平滑效果。
实现LOMO效果的步骤
1.通过使用查找表(LUTk)将一个曲线应用于(B/G/R)任一通道来实现颜色操作效果
2.通过对图像应用暗晕来实现复古效果
了解LUT技术
LUT是向量或表,它返回给定值的预处理值,以便在存储器中执行计算。LUT是一种常用的技术,用于通过避免重复执行耗时的计算来节省CPU周期。我们不对每个像素调用exponential/divide函数,而是只对每个可能的像素值执行一次(256)次,并将结果存储在表中。
Opencv提供LUT函数(输入图像,查找表的矩阵,输出图像)
代码实现
//1.计算lut变量
const double exponential_e = std::exp(1.0);
Mat lut(1, 256, CV_8UC1);
Mat res;
UCHAR* plut = lut.data;
for (int i = 0; i < 256; i++) {
double x = (double)(i / 256.0);
plut[i] = cvRound(256 * (1.0 / 1.0 + pow(exponential_e, -((x - 0.5)/0.1))));
}//类似于根据新的计算公式,把原像素值映射到新像素值上去
//2.分离通道
std::vector<Mat> bgr;
split(src, bgr);
//3.调用LUT函数处理红色通道
cv::LUT(bgr[2], lut, bgr[2]);
//4.合并通道
cv::merge(bgr, res);
//5.创建黑暗光环
Mat holo(src.size(), CV_32FC3, Scalar(0.3, 0.3, 0.3));
circle(holo, Point(src.cols / 2, src.rows / 2), src.cols / 3, Scalar(1, 1, 1));
//6.将光环放到原图像上去,将两个图像相乘
Mat resultf;
res.convertTo(resultf, CV_32FC3);
cv::multiply(resultf, holo, resultf);
resultf.convertTo(res, CV_8UC3);
imshow("res", res);
卡通效果
算法步骤
1.边缘检测
2.颜色过滤
这也是我们需要学习的技术
代码演示
//1.去噪声,根据噪声特点
cv::medianBlur(src, src, 3);
//2.消除噪声之后,用Canny检测算子检测强边缘
Mat imgCanny;
cv::Canny(src, imgCanny, 50, 150);
Mat kernel = getStructuringElement(MORPH_RECT, Size(2, 2));
cv::dilate(imgCanny, imgCanny,kernel);
//3.将边缘加到图像上去,同样的可以用相乘的方法
//当然直接相乘会直接溢出,达不到效果的因此要先整到0-1的区间
imgCanny = imgCanny / 255;
imgCanny = 1 - imgCanny;
Mat imgCannyf;
imgCanny.convertTo(imgCannyf, CV_32FC3);
//小操作
blur(imgCannyf, imgCannyf, Size(5, 5));
//接下来就是颜色过滤了
//4.为了得到卡通外观,使用bilaternal双边滤波滤镜
Mat imgBF;
cv::bilateralFilter(src, imgBF, 9, 150.0, 150.0);
//当直径大于5的时候,bilateral开始变慢
//当sigma的值大于150的时候,会出现卡通效果
//小技巧:为了获得更明显的卡通效果,通过乘和除将可能的颜色值截断为10
Mat result = imgBF / 25;
result = result * 25;
//5.合并边缘结果和颜色效果
Mat imgCanny3c;
Mat cannyChannel[] = { imgCannyf,imgCannyf,imgCannyf };
cv::merge(cannyChannel,3,imgCanny3c);
//同样的在乘于之前先转化图像类型
Mat resultf;
result.convertTo(resultf, CV_32FC3);
cv::multiply(resultf, imgCanny3c, resultf);
resultf.convertTo(result, CV_8UC3);
imshow("res", result);
总结
通过这个学习章节,应当学到求取直方图的方法,通过直方图,求取类间方差,通过遍历类间方差来找到阈值。
通过直方图均衡化来提高图像的对比度
实现LOMO效果的时候,应当学会使用LUT对将要计算像素值进行预处理
实现卡通效果的时候,注意merge函数内部的参数使用,以及实现图像合并的时候,做乘法先要转化数据类型8UC3->32FC3->8UC3这样的步骤来做.