从0-255之间每个像素的取值都只能是其中一个,那么0-255的值出现再图像各个像素的频率也就会不一样。
Bins其实就是统计的单元:如果是一单元那么0-255(256)个频率值,如果是16那么相当于0-15,16-31这样 每16个bins作为一组值来计算频率(就是将16个的频率之和)。
意义;如果都是一副图像的值都是255那么直方图就在255那里有一个很高的值,那么他就是白色了。这样的话那么其实我们可以根据直方图来反推图像像素的值频率得到图像大致颜色。
图像直方图不会受到图像的旋转,放大缩小,平移的影响。对于灰度图像来说可以使用直方图来进行分割(两个高峰值之间的低估值作为分割),便于分割成二值图像,而不是之间才有127来分割灰度,根据图像的统计学来进行分割更具说服性。
对于颜色的图像我们也可也基于颜色的图像检索以及图像分类、反向投影跟踪和图像均衡化对于图像质量的提升很有帮助的。
直方图实践源码;
//获取直方图
void QuickDemo::histogram(Mat &image)
{
//分割通道
std::vector<Mat> bgr_plane;
split(image, bgr_plane);
//定义参数变量
const int channels[1] = { 0 };
const int bins[1] = { 256 };//直方图的取值个数
float hranges[2] = { 0, 255 };//直方图的取值范围
const float* ranges[1] = {hranges};//因为接口可以支持多维的 多张图像
Mat b_hist;
Mat g_hist;
Mat r_hist;
//计算直方图
calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges);
calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges);
calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges);
//显示直方图
int hist_w = 512;
int hist_h = 400;
int bin_w = cvRound((double)hist_w/bins[0]);//每个bin是256个,总宽度除以它得到每个灰度值在图像中占几个像素点,最终是要画图的
Mat hisImage = Mat::zeros(hist_h, hist_w, CV_8UC3);//绘制直方图的底布
//归一化处理 因为三个通道出现的相同灰度值次数差值太大了因此需要归一化到一段范围内 (底布高度范围这么大)来显示在一张图片上
normalize(b_hist, b_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
normalize(g_hist, g_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
normalize(r_hist, r_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
//绘制直方图曲线
for (int i = 1; i < bins[0]; i++)
{
//点的坐标都是基于屏幕坐标的 因此要做转换 hist_h -cvRound(b_hist.at(i-1)))
line(hisImage, Point(bin_w*(i-1), hist_h -cvRound(b_hist.at<float>(i-1))),
Point(bin_w*(i), hist_h-cvRound(b_hist.at<float>(i))), Scalar(255,0,0),2,8,0);
line(hisImage, Point(bin_w*(i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, 8, 0);
line(hisImage, Point(bin_w*(i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, 8, 0);
}
//显示直方图
namedWindow("Histogram Deemo", WINDOW_AUTOSIZE);
imshow("Histogram Deemo", hisImage);
}
一维直方图是统计灰度图中0-255像素值出现在整个图像中的频率,而二维的就是将图像转换为HSV色彩空间之后对HS色彩度和饱和度两个维度进行统计,统计对应H与对应V组合值出现在整个图像中的频率,因为要结合HV两个维度因此是二维的直方图。
实践代码
//获取二维直方图
void QuickDemo::histogram_2d_demo(Mat &image)
{
//2D直方图
imshow("原图", image);
Mat hsv, hsv_hist;
cvtColor(image, hsv, COLOR_BGR2HSV);
int hbins = 30;//h的灰度只是0-179,一共180个灰度值,30个灰度值为一个单位统计在该范围内的像素频率
int sbins = 32;//s的灰度值实时0-255
int hist_bins[] = {hbins, sbins};//2d将两组bins设置进去
float h_range[] = {0, 180};//灰度值的取值范围
float s_range[] = { 0, 256 };
const float* hs_range[] = {h_range, s_range};
int hs_channels[] = { 0, 1 };
//hsv 只有一张图像 第0个和第一个通道、mask就是与之前的一样 只对非0的做处理 产生的mat 2维的 第1、2个通道多少个bins 第1、2个通道多少个取值范围
//这个就是在得出每个h与v对应的像素点的个数
calcHist(&hsv, 1, hs_channels, Mat(), hsv_hist, 2, hist_bins, hs_range, true, false);
//2维的一图图像里面 则首先要进行归一化处理
double maxVal = 0;
minMaxLoc(hsv_hist, 0, &maxVal, 0, 0);//最大值
int scale = 10;
Mat hist2d_image = Mat::zeros(sbins*scale, hbins*scale, CV_8UC3);//创建空白图像 将数据往上面填入即可
for (int h = 0; h < hbins; h++)
{
for (int s = 0; s < sbins; s++)
{
float binVal = hsv_hist.at<float>(h, s);
int intensity = cvRound(binVal * 255 / maxVal);//取整
rectangle(hist2d_image, Point(h*scale, s*scale),
Point((h+1)*scale -1, (s+1)*scale -1),
Scalar::all(intensity), -1);
}
}
imshow("黑白二维直方图", hist2d_image);
applyColorMap(hist2d_image, hist2d_image, COLORMAP_JET);//变成色彩图
imshow("彩色二维直方图", hist2d_image);
}
图像直方图均衡化可以用于图像增强、对输入图像进行直方图均衡化处理,提升后续对象检测的准确率、在医学图像和卫星摇杆图像是很有增强的。在人脸检测的官方代码中就是摄像头获取每帧图像然后对每帧图像进行的操作就是将图像进行灰度化,灰度化之后进行一个直方图均衡化,然后再送给人脸检测器从而达到提升图像质量从而提供人脸检测的准确率。
该算法对图像的亮度进行归一化,提高了图像的对比度。
图像直方图均衡化原理
实践代码
//对彩色图像进行直方图的均衡化
void QuickDemo::histogram_eqRGB_demo(Mat &image)
{
namedWindow("RGB图像", WINDOW_FREERATIO);
namedWindow("直方图均衡化", WINDOW_FREERATIO);//图片太大了
Mat ycrcb;
cvtColor(image, ycrcb, COLOR_BGR2YCrCb);
std::vector<Mat> vcChannels;
split(ycrcb, vcChannels);
Mat dst;
/*
均衡化灰度图像的直方图。
该算法对图像的亮度进行归一化,提高了图像的对比度。
输入为8位单通道图像。输出于输入相同大小类型
*/
equalizeHist(vcChannels[0], vcChannels[0]);//只能处理灰度图
merge(vcChannels, ycrcb);
cvtColor(ycrcb, dst, COLOR_YCrCb2BGR);
imshow("RGB图像", image);
imshow("直方图均衡化", dst);
}
一次计算多个通道的直方图并且还合并到一个图像进行输出,还支持这种方式。因此我们的直方图计算还可以一次计算多个通道的直方图。
图像直方图比较,就是计算两幅图像的直方图数据,比较两组数据的相似性,从而得到两幅图像之间的相似程度。这个在早期的CBIR(基于内容的图像检索)中就有使用,特别是在边缘处理、图像特征和词袋技术(词袋模型是一种用机器学习算法对文本进行建模时表示文本数据的方法)一起使用。
API介绍
CV_EXPORTS_W double compareHist( InputArray H1, InputArray H2, int method );
返回值double类型的数据表示我们用第三个参数比较两幅直方图相似性之后得出来的结论,
参数H1,H2就是两幅直方图的数据,对于数据而言,比较他们的相似性相关性计算。
我要比较两个两幅直方图,就是有两堆数据在哪里你要进行比较,那么你最先想到的就是数据相似关的计算。
则第三个参数就是数据相关性计算的方法类别。
HistCompMethods{ Correlation、 Chi-Square、 Intersection 、Bhattacharyya distance }
在opencv当中我们比较直方图数据的相关性程度的最常用的几种方法就是;相关性,卡方,交叉,巴士。
correlation相关性方法: 相似性程度越高那么最后的结果就越趋近于正1,而如果相关的程度非常非常低那么他的结果也就趋近于-1。-1就表示这两个东西压根就没有相关性的存在。+1就表示两幅图像非常相似。这个取值范围是在0-1之间的,就是实际上他的值是需要0-1之间,但他的相关性计算并没有做归一化处理的,因此我们会在直方图比较之前先做归一化处理到0-1之间,这样出来的值才会满足条件。
卡方计算:卡方就是这两组数据的方差,这个是比较常规的计算欧几里距离(欧几里得距离来计算两个向量之间的相似度),我们之前是两个点之间就是两维的欧几里得距离,三个点三维的,如这个方差就相当于是N维的欧几里得距离。卡方计算出来的值是可能很大也可能很小,同样也是没有做归一化处理的。他计算的值越小就是相似度越高的,值越大的就相似度越低的。
交叉;就是取这两个当中较小的那个。如果这两个min,我们一计算他们的mins加起来就是我们的交叉性验证。最终我们交叉性验证得出来的他们的和,如果他是一个完完整整的值的话,那么我们交叉性验证的结果他就应该是在 如果图像完全相同的话那么他就是所有的和加起来,如果不相同的话那么他们两个最小的和加起来是都比较大或者小,越小那么相似的可能性大一点,反之可能性小,交叉的方式十分不准的。
巴氏距离;这个而是比较准的,,我们在做直方图比较最好是选择第一个相关性或巴氏距离这个是比较准确的。在数学上本来是巴氏距离越靠近1的相似相关性越高,但是opencv中做了一个1减,那么也就变成了越靠近0的相似度越高,0就是没有距离的就非常相似,而1是有距离的则不相似。
实践
常规经验性的处理都是要转换到HSV的类型,H S分量就是颜色。
如果是int类型的数归一化到0-1之间那么就需要dtip,还有提前把1转换为浮点数才行。
直方图比较效果图
直方图比较源代码
//对直方图进行比较
void QuickDemo::histogram_compare_demo()
{
//读取图片
Mat src1 = imread("D:/images/m1.png");
Mat src2 = imread("D:/images/m2.png");
Mat src3 = imread("D:/images/pedestrian.png");
Mat src4 = imread("D:/images/persons.png");
namedWindow("input1", WINDOW_FREERATIO);
namedWindow("input2", WINDOW_FREERATIO);
namedWindow("input3", WINDOW_FREERATIO);
namedWindow("input4", WINDOW_FREERATIO);
imshow("input1", src1);
imshow("input2", src2);
imshow("input3", src3);
imshow("input4", src4);
//转换hsv
Mat hsv1, hsv2, hsv3, hsv4;
cvtColor(src1, hsv1, COLOR_BGR2HSV);
cvtColor(src2, hsv2, COLOR_BGR2HSV);
cvtColor(src3, hsv3, COLOR_BGR2HSV);
cvtColor(src4, hsv4, COLOR_BGR2HSV);
//输出直方图
int h_bins = 60;
int sbins = 64;
int histSize[] = {h_bins, sbins};
float h_ranges[] = { 0, 180 };
float s_ranges[] = { 0, 256 };
const float* ranges[] = { h_ranges , s_ranges };
int channels[] = { 0, 1 };//处理两个通道
Mat hist1, hist2, hist3, hist4;
calcHist(&hsv1, 1, channels, Mat(), hist1, 2, histSize, ranges, true, false);
calcHist(&hsv2, 1, channels, Mat(), hist2, 2, histSize, ranges, true, false);
calcHist(&hsv3, 1, channels, Mat(), hist3, 2, histSize, ranges, true, false);
calcHist(&hsv4, 1, channels, Mat(), hist4, 2, histSize, ranges, true, false);
//归一化处理
normalize(hist1, hist1, 0, 1, NORM_MINMAX, -1, Mat());
normalize(hist2, hist2, 0, 1, NORM_MINMAX, -1, Mat());
normalize(hist3, hist3, 0, 1, NORM_MINMAX, -1, Mat());
normalize(hist4, hist4, 0, 1, NORM_MINMAX, -1, Mat());
//直方图比较
for (int i = 0; i < 4; i++)
{
int compare_method = i;//四种算法
/*
CV_EXPORTS_W double compareHist( InputArray H1, InputArray H2, int method );
返回值double类型的数据表示我们用第三个参数比较两幅直方图相似性之后得出来的结论,
参数H1,H2就是两幅直方图的数据,对于数据而言,比较他们的相似性相关性计算。
我要比较两个两幅直方图,就是有两堆数据在哪里你要进行比较,那么你最先想到的就是数据相似关的计算。
则第三个参数就是数据相关性计算的方法类别。HistCompMethods{ Correlation、 Chi-Square、 Intersection 、Bhattacharyya distance }
*/
double src1_src2 = compareHist(hist1, hist2, compare_method);
double src3_src4 = compareHist(hist3, hist4, compare_method);
printf(" Method [%d] : src1_src2 : %f, src3_src4: %f, \n", i, src1_src2, src3_src4);
}
}
图像直方图的反向投影其实就是通过构建指定模板图像的二维直方图空间于目标的直方图空间,进行直方图归一化之后,进行比率操作得到的非零数值,生成查找表对原图像进行像素映射之后,再进行图像模糊输出的结果。
举例来说就是现在所有人都在用手机,那么手机就有基站,手机有一种定位技术就是通过基站来进行定位的,其实手机底层是有一个发射层来进行信号的发射于接收,并且手机不是于一个基站进行通信的,而是多个,在通信的时候去连接匹配选择最强的那个进行通信。而基站也是可以通过这个信号的传输进行反向定位手机的,并且手机连接的基站还会把手机于其他基站的查找也找到,从而通过几个基站的于手机反射的信号线那么就会有一个交点从而定位到手机。通过这个例子从而在图像中就是通过图像直方图这个特征来反向投影找到自己感兴趣的ROI区域。
1、 对指定模板图像进行直方图数据解析(其实就是将图像转换为hsv,解析出二维直方图)
2、 对要查找的目标图像进行直方图数据解析,同样也是生成hsv的二维直方图
3、 目标图像直方图中数据分布中存在与模板图像直方图数据向叠合的地方,那么可能目标图像中存在模板图像的画面。
4、 从而需要将两幅直方图数据进行&操作,从而得出的直方图就是两幅图像的交集(就是hs像素频率相同的部分)
5、 再最后对得出的直方图中的范围值设置LUT查找表去遍历原图像(在范围类则为,不再则为0)从而可以绘制出模型
直方图;白色区域就是表示这个像素值信号很强,出现的频率很高,而黑色灰色的区域就表示该像素值信号弱,出现频率低。
中间还可以进行一下卷积模糊,就是将像素值尽量集中一些,便于反向投影操作
首先计算模板图像的直方图,然后进行归一化,直接拿过去进行LUT查找表进行反向投影查找,opencv就做了进行节省从而提高效率。
代码实践:
后续还可以对视频进行实时目标匹配查找的,但是注意实现的时候要注意视频差帧的区别。
//直方图反向投影
void QuickDemo::histogram_backProjection(Mat &image, Mat &model)
{
/*
images 原图像 一个数值
nimages 原图像个数 支持多张图片
channels 图像通道
hist 输入模板的直方图
backProject 反向输出结果
ranges 像素范围 如h为0-180 s0-255
scale 是否放缩
CV_EXPORTS void calcBackProject(const Mat* images, int nimages,
const int* channels, InputArray hist,
OutputArray backProject, const float** ranges,
double scale = 1, bool uniform = true);
*/
imshow("模板图像", model);
imshow("目标图像", image);
//转换为hsv色彩空间
Mat model_hsv, image_hsv;
cvtColor(model, model_hsv, COLOR_BGR2HSV);
cvtColor(image, image_hsv, COLOR_BGR2HSV);
//对模板图像进行直方图 归一化处理
int h_bins = 32;
int s_bins = 32;
int histSize[] = { h_bins , s_bins };
float h_ranges[] = { 0, 180 };
float s_ranges[] = {0, 256};
const float* ranges[] = { h_ranges ,s_ranges };
int channels[] = {0,1};
Mat roiHist;
calcHist(&model_hsv, 1, channels, Mat(), roiHist, 2, histSize, ranges, true, false);
normalize(roiHist, roiHist, 0, 255, NORM_MINMAX, -1, Mat());
//直接反向查找
Mat backproj;
calcBackProject(&image_hsv, 1, channels, roiHist, backproj, ranges, 1.0);
imshow("BackProj", backproj);
}
这是均值卷积,每个像素的卷积核都是相同的,
线性卷积;就是卷积和与图像对应位置进行点乘累加得到的结果除以卷积和个数,得到的结果替换到之前进行操作的图像区域的中心位置。这个位置在opencv中有一个名称叫锚点anchor(默认是中心输出)。
这种处理在原图像的边缘是与锚点没有关系的,因此对待这些边缘有两种做法一是扔掉,二是做填充边缘化处理。
实践:
//计算卷积
void QuickDemo::blur_demo(Mat &image)
{
Mat dst;
namedWindow("卷积", WINDOW_FREERATIO);
//卷积核越大图像越模糊 根据Size的传入还可以进行水平方向卷积Size(15, 1),竖直方向卷积Size(1,15
blur(image, dst, Size(5, 5), Point(-1, -1));)
imshow("卷积", dst);
}
非均值卷积,之前的卷积是每个像素的卷积核是相同的,而高斯模糊就是可以达到不同的卷积核。
高斯模糊的卷积核就是中心位置最大,离中心位置越远的就越小。
高斯核函数
中心化效应,考虑了图形中心像素对图像的一个贡献,而均值卷积就没有考虑到,中心位置比例最大。
高斯模糊和均值卷积的区别就是他们的卷积核不一样,一个是均值都是1一个是通过高斯核函数计算的,是根据中心占比的得到的。
实践
//计算高斯模糊
void QuickDemo::gaussian_blur_demo(Mat &image)
{
Mat dst;
//卷积核的大小,注意一定要是基数 偶数就是错误的(违反了高斯中心化的原则)
//sigma 当窗口设置Size(0,0)的时候opencv就会从sigma反算窗口大小,
//当窗口大小已经设定一个值之后那么sigma这边无论设什么都没有效果,他会从窗口计算得到sigma的
//并且窗口或者sigma都是值越大图形越模糊,sigma对图形的模糊效果更加明显所以很多时候都会设置size而是直接设计sigma的来查看模糊效果
GaussianBlur(image, dst, Size(5, 5), 15);
imshow("高斯模糊", dst);
}
中值滤波本质上是统计排序滤波器的一种,中值滤波对图像特定噪声类型(椒盐噪声)会取得比较好的去噪效果,也是常见的图像去噪声与增强的方法之一。常见的统计排序滤波器还有最小值滤波和最大值滤波。
中值滤波的工作方式与卷积的非常相似:也是类似移动的操作,但是不会去做卷积那种点乘操作而是对覆盖区域的ROI区域的像素值进行排序取中值作为其ROI区域中心像素值的输出。
最终的效果其实就是使得局部的像素区域减少波动,让局部区域的像素更加平滑,减少噪声。
实践
实践代码
//中值滤波
void QuickDemo::medianblur_demo(Mat &image)
{
Mat dst;
imshow("原图像", image);
//注意 ksize参数必须是奇数并且大于1的,只有奇数才有中值
medianBlur(image, dst, 3);
imshow("中值滤波", dst);
}
前面介绍的三种卷积方法(均值卷积,高斯卷积,中值卷积)都是在图像预处理当中都是可以对图像进行去噪的。
图像的噪声:就是本来拍摄出来的图像应该是这样的,但是经过设备拍摄的干扰,传输时干扰导致的成像出来的图像质量有问题,反应到图像上就是图像的亮度与颜色呈现某种程度的不一致性。
根据图像噪声的分类可以分为
是一种随机在图像中出现的稀疏分布的黑白像素点, 对椒盐噪声一种有效的去噪手段就是图像中值滤波(因为均值滤波就是取中值,而椒盐噪声又是黑白像素点,因此中值滤波去椒盐噪声的效果是比较好的)。它的本质其实就是在图像中部分像素是0或255的杂像素点。
代码实践:
//产生椒盐噪声
void QuickDemo::add_saltPepperNoise(Mat &image)
{
imshow("原图", image);
RNG rng(12345);//分配随机数种子
int h = image.rows;
int w = image.cols;
int nums = 10000;
for (int i = 0; i < nums; i++)
{
int x = rng.uniform(0, w);//0到w之间参数一个随机数
int y = rng.uniform(0, h);
if (i % 2 == 1) {
image.at<Vec3b>(y, x) = Vec3b(255, 255, 255);//椒盐噪声的核心就是产生黑白像素点
}
else
{
image.at<Vec3b>(y, x) = Vec3b(0, 0, 0);
}
}
imshow("salt pepper", image);
}
一般会在数码相机的图像采集(acquisition)阶段发生的一些错误,这个时候它的物理/电/光等各种信号都可能导致产生高斯分布噪声。
代码实践:
//产生高斯噪声
void QuickDemo::add_gaussianNoise(Mat &image)
{
imshow("原图", image);
Mat nosi = Mat::zeros(image.size(), image.type());
randn(nosi, (15, 15, 15), (30, 30, 30));//用正态分布的随机数填充数组。 产生均值15, 方差30的高斯图像
Mat dst;
add(image, nosi, dst);//叠加
imshow("gaussian Noise", dst);
}
这种噪声都是由于某种规律性的错误导致的,这样消除在硬件上成本就比较高一点一般会考虑在软件上进行消除的。
图像去噪对图像二值化与二值分析是很有作用的,opencv当中常见的去噪方法有。
均值去噪声:也就是我们的卷积模糊的作用,
高斯模糊去噪;非均值卷积模糊的作用
非局部均值去噪声:opencv中提供了专门的API;fastNlMeansDenoisingColored
双边滤波去噪;
形态学去噪:主要是针对二值图像的灰度图像效果相对明显一点。
实践
卷积我们最好是选择33的,最多选择55,再选择大了的话就会影响原本图像了。
//去除噪声
void QuickDemo::pictureNoise_wipe(Mat &image)
{
Mat ret1, ret2, ret3, ret4;
blur(image, ret1, Size(3,3));//最好选择3*3的卷积,最多选择5*5 再大就会影响原图像数据了
imshow("卷积模糊去噪", ret1);
GaussianBlur(image, ret2, Size(3, 3), 0);
imshow("高斯模糊去噪", ret2);
medianBlur(image, ret3, 3);
imshow("中值卷积去噪", ret3);
/*
非局部均值去噪声:他是有两个版本的一个是针对灰度图,一个是有颜色的图像
第3、4个参数分别是表示我们的颜色分量和亮度分量,取一个阈值分量,这个分量值越大去噪的程度越大那么图像原本的细节也会被去除掉。通常不用超过10或15,默认3
第5,6个参数,搜索窗口大小,和模板窗口的大小 ; 注意模板窗口一个不能大过搜索窗口,因为模板窗口是在搜索窗口里面进行搜索的
非局部均值去噪;实际上是一个大的窗口上有一个小的搜索窗口和模糊窗口,通过这两个窗口进行搜索或移动来达到非局部均值去噪
*/
fastNlMeansDenoisingColored(image, ret4, 15,15,10, 30);
imshow("非局部均值去噪声", ret4);
}
边缘保留滤波算法(EPF):之前介绍的滤波去噪都是模糊卷积(均值卷积或高斯卷积),他们都是模糊之后图像的边缘信息不复存在,受到了破坏。而边缘保留滤波算法就是在通过卷积处理实现图像模糊的同时对图像边缘不会造成破坏,滤波之后的输出完整的保存了图像整体边缘(轮廓)信息。常见的边缘保留滤波算法有以下几种:
高斯双边模糊、PS的磨皮算法一般用这个精度高只是耗时需要一定条件。
MeanShitf均值迁移模糊、实际当中是基于金字塔的
局部均方差模糊、基于积分图思想的快速计算的边缘保留滤波算法,在深度学习没有大热起来之前美图pp他是最常用的。
opencv当中专门提供的一个边缘保留滤波的API函数;对我们去噪是很有作用的
高斯模糊是考虑图像空间位置对权重的影响(离输出中心点越近的那么他所占的权重也就越大,考虑的输出位置对我们卷积的影响),但是它没有考虑图像像素分布对图像卷积输出的影响(其实正确的方式应该只有周围像素值相差不大的才进行卷积计算的,说明他们有相同的像素分布,如果他们像素值相差比较大那么他们就应该从计算当中剥离出来不参与本个像素点值的卷积计算),双边模糊考虑了像素值分布的影响,对像素值空间分布差异较大的进行保留从而完整的保留了图像的边缘信息。
不是整个图形都进行模糊,这样把图像原有的信息丢掉了。高斯双边模糊就是处理这个问题的,他模糊的同时保留了较大区别的边缘(如亮暗的边缘)而模糊掉了一些细节处理。
双边是指的空间的和色彩的。
CV_EXPORTS_W void bilateralFilter( InputArray src, OutputArray dst, int d,
double sigmaColor, double sigmaSpace,
int borderType = BORDER_DEFAULT );
InputArray src, OutputArray dst, 原图像和目标图像
int d,窗口大小,之前说了可以填0 由后面的sigma来反推计算
double sigmaColor, 这个值要大一点,用于色彩的卷积核
double sigmaSpace, 空间的卷积核
int borderType = BORDER_DEFAULT 边缘的处理方式
图19;图片来自于贾志刚老师的知识星球内容
//计算双边高斯卷积
void QuickDemo::Bilateralgaussian_blur_demo(Mat &image)
{
/*namedWindow("原图", WINDOW_FREERATIO);
imshow("原图", image);*/
Mat dst;
//100表示色彩的卷积核 要大一点
//10表示空间距离的卷积核
//d表示我们输出的维度是否放大缩小,一般我们都是与输入保持一致的
// sigmaColor, 表示色彩空间色彩相差多大范围之内的他才参与计算,这个可以填大一点
// sigmaSpace,表示高斯模糊里面的 sigmaX,sigmaY,空间的的sigma,这个值要选择小一点 5,10,15
bilateralFilter(image, dst, 0, 100, 10);
Mat combine1, combine;
hconcat(image, dst, combine1);//水平拼接函数
vconcat(image, dst, combine);//垂直拼接函数
namedWindow("高斯模糊1", WINDOW_FREERATIO);
imshow("高斯模糊1", combine);
namedWindow("高斯模糊", WINDOW_FREERATIO);
imshow("高斯模糊", combine1);
}
概念:是边缘保留滤波算法中的一种,经常用来对图像分水岭分割之前对图像去噪,并且均值迁移模糊不仅可以对图像进行模糊还可以实现对图像的目标跟踪,图像模式识别 视频分析对比等多环节都可以进行应用是很经典的图像处理算法。
基本原理
前面卷积就是要在图片上开窗点乘,这是空间域卷积上要做的事情。而我们均值迁移模糊就是在图像进行开窗的时候同样,不仅考虑像素值空间范围分布(窗口位置中心点与窗口位置的空间关系),而且还要考虑空间关系中像素值的分布, 只有符合像素值分布的像素点才参与计算,他们参与计算之后得到像素均值与空间位置xy也有均值(对于彩色图像而言我们就是要找到均值迁移当中的五个向量RGB三通道和空间的xy分别在这些方向上移动了多少,再对比原来的中心位置的差那么就可以得到新的均值位置),使用新的均值位置作为窗口中心位置继续基于给定像素值空间分布计算均值与均值位置,如此不断迁移中心位置直到不再变化位置(dx=dy=0)(就是向密度高的方向不断迁移直到来到密度最高的中心位置,因为这个时候得到的均值与所有像素去得到的均值也是一致的了,当一致之后就不会再有data数据移动了,如果有一个像素点没有参数计算那么他的均值不可能是0,就是因为他周围的像素点分布已经与中心点已经很相似了,符合我们要求,因此这个算法是基于均值的因此会不断迁移),但是在实际情况中数据可能不会那么理想化,因此我们会人为设置一个停止条件比如迁移几次,这样就可以把最后的RGB均值赋值给中心位置。
实际
代码
//均值迁移滤波
void QuickDemo::MeanShift(Mat &image)
{
/*
sp 空间大小就是我们的空间域
sr颜色大小就是我们色彩空间分布值
在什么情况停止:5是迁移5次,或者两次迁移从差值小于1的时候停止
void pyrMeanShiftFiltering( InputArray src, OutputArray dst,
double sp, double sr, int maxLevel = 1,
TermCriteria termcrit=TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS,5,1) );
*/
Mat ret1, combine1;
pyrMeanShiftFiltering(image, ret1, 15, 50, 1, TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 5, 1));
hconcat(image, ret1, combine1);//水平拼接函数
imshow("均值迁移滤波", combine1);
}
概念:为了在多尺度透视投影中提高渲染速度,是一种快速计算图像区域和与平方和的算法。他可以很快速度的计算一幅图像的任意区域的像素和或者平方和,也就是我们卷积窗口下对应区域的像素和和平方和他都可以很快获取到。
**其核心思想:**对每个图像建立自己的积分图查找表(如果是计算和那么就要积分和图查找表、如果是计算平方和那么就是建立自己的 积分图平方和查找表),在图像积分处理计算阶段根据预先建立的积分图查找表,直接查找从而实现对均值卷积线性时间计算(我们进行卷积的时候就可以实现对图像线性时间的查找计算),做到了卷积执行的时间与半径窗口大小的无关联。图像积分图在图像特征提取HAAR/SURF、二值图像分析、图像相似相关性NCC计算、图像卷积快速计算等方面均有应用,是图像处理中的经典算法之一。
图像积分图建立与查找 :
在积分图像(Integral Image - ii)上任意位置(x, y)处的ii(x, y)表示该点左上角所有像素之和, 其中(x,y)是图像像素点坐标。
基于图像平方和表来做一个图像均值模糊,也就是基于图像积分图
代码
//图像积分图算法
int getblockSum(Mat &sum, int x1, int y1, int x2, int y2, int i) {//获取指定区域的和
int tl = sum.at<Vec3i>(y1, x1)[i];
int tr = sum.at<Vec3i>(y2, x1)[i];
int bl = sum.at<Vec3i>(y1, x2)[i];
int br = sum.at<Vec3i>(y2, x2)[i];
int s = (br - bl - tr + tl);
return s;
}
void blurintegral_demo(Mat &image, Mat &sum) {//图像积分图算法
int w = image.cols;
int h = image.rows;
Mat result = Mat::zeros(image.size(), image.type());
int x2 = 0, y2 = 0;
int x1 = 0, y1 = 0;
int ksize = 5;//图像直径
int radius = ksize / 2;
int ch = image.channels();
int cx = 0, cy = 0;
//遍历像素点
for (int row = 0; row < h + radius; row++) {
y2 = (row + 1)>h ? h : (row + 1);
y1 = (row - ksize) < 0 ? 0 : (row - ksize);
for (int col = 0; col < w + radius; col++) {
//然后要找到区域四个点的位置传过去得到区域积分和
x2 = (col + 1)>w ? w : (col + 1);
x1 = (col - ksize) < 0 ? 0 : (col - ksize);
cx = (col - radius) < 0 ? 0 : col - radius;
cy = (row - radius) < 0 ? 0 : row - radius;
int num = (x2 - x1)*(y2 - y1);//有多少个像素参与计算
for (int i = 0; i < ch; i++) {
// 积分图查找和表,计算卷积
int s = getblockSum(sum, x1, y1, x2, y2, i);
//和表除以个数就是得到中心像素点的值 均值滤波的一个输出 也就是这块区域积分和表均值
result.at<Vec3b>(cy, cx)[i] = saturate_cast<uchar>(s / num);
}
}
}
Mat combine1;
hconcat(image, result, combine1);//水平拼接函数
imshow("output", combine1);
//imwrite("D:/result.png", result);
}
void QuickDemo::integral_demo()
{
/*
sum 和表opencv中有API可以输出和表的
sqsum 平方和表
tilted 瓦块和表(就是倾斜45度的和表) 重载了该参数可以没有
sdepth 和表数据深度常见:CV_32S //保证数据不会溢出
sqdepth 平方和表数据深度常见:CV_32F //保证数据不会溢出
void integral(InputArray src, OutputArray sum,
OutputArray sqsum, OutputArray tilted,
int sdepth = -1, int sqdepth = -1);
*/
//需要区域的四个点坐标 num有多少个像素点参与计算,cx/cy中心点的坐标 就可以通过getblockSum和表
Mat src = imread("D:/images/yuan_test.png");
if (src.empty()) {
printf("could not load image...\n");
return ;
}
// 计算积分图
Mat sum, sqrsum;
integral(src, sum, sqrsum, CV_32S, CV_32F);
blurintegral_demo(src, sum);
}
高斯双边模糊与mean shift均值模糊两种边缘保留滤波算法,都因为计算量比较大,无法实时实现图像边缘保留滤波,限制了它们的使用场景,OpenCV中还实现了一种快速的边缘保留滤波算法。高斯双边与mean shift均值在计算时候使用五维向量是其计算量大速度慢的根本原因,该算法通过等价变换到低纬维度空间,实现了数据降维与快速计算。
void QuickDemo::edgePreserving_demo()
{
Mat src = imread("D:/images/example.png");
Mat dst;
/*
flag 1 就表示我们选择的边缘保留算法其中的一种 还有其他的边缘保留滤波降维算法,这只是i其中的一种。
sigma_s 空间域的窗口值,跟我们开窗模糊都有关系。
sigma_r 表示颜色空间域的特征值。
其中sigma_s的取值范围为0~200, sigma_r的取值范围为0~1
注意sigma_s 和sigma_r 在取某些值的时候会影响最后的结果输出。
sigma_s 和sigma_r都很小的时候(sigma_s 很大,sigma_r很小)那个边缘保留滤波基本很少的影响。
只有当两者都取到合适的值的时候,其中一个不变,另外一个变大的话那么图像的模糊效果就会越明显。
CV_EXPORTS_W void edgePreservingFilter(InputArray src, OutputArray dst, int flags = 1,
float sigma_s = 60, float sigma_r = 0.4f);
*/
edgePreservingFilter(src, dst, 1, 60, 0.44);
Mat combine1;
hconcat(src, dst, combine1);//水平拼接函数
imshow("output", combine1);
return;
}
图像卷积最主要功能有图像模糊、锐化、梯度边缘等,前面已经分享图像卷积模糊的相关知识点,OpenCV除了支持上述的卷积模糊(均值与边缘保留)还支持自定义卷积核,实现自定义的滤波操作。自定义卷积核常见的主要是均值、锐化、梯度等算子。下面的三个自定义卷积核分别可以实现卷积的均值模糊、锐化、梯度功能。
代码
void QuickDemo::filter2D_demo()
{
Mat src = imread("D:/images/test.png");
Mat kernel1 = Mat::ones(5, 5, CV_32F) / (float)(25); //均值算子
//锐化算子
Mat kernel2 = (Mat_<char>(3, 3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
//梯度算子
Mat kernel3 = (Mat_<int>(2, 2) << 1, 0, 0, -1);
/*
ddepth 输出图像的深度,默认-1表示输入输出是一致的
InputArray kernel;// 卷积核或者卷积窗口大小
Point anchor;就是卷积的铆点,就是最后卷积结果赋值的地方。
CV_EXPORTS_W void filter2D(InputArray src, OutputArray dst, int ddepth,
InputArray kernel, Point anchor = Point(-1, -1),
double delta = 0, int borderType = BORDER_DEFAULT);
*/
Mat dst1, dst2, dst3;
filter2D(src, dst1, -1, kernel1);
filter2D(src, dst2, -1, kernel2);
filter2D(src, dst3, CV_32F, kernel3);
convertScaleAbs(dst3, dst3);
imshow("blur=5x5", dst1);
imshow("shape=3x3", dst2);
imshow("gradient=2x2", dst3);
return;
}
卷积的作用除了实现图像模糊或者去噪,还可以寻找一张图像上所有梯度信息,这些梯度信息是图像的最原始特征数据,进一步处理之后就可以生成一些比较高级抽象的特征信息用来表示一张图像实现基于图像特征的匹配,图像分类等应用。
Sobel算子是一种很经典的图像梯度提取算子,其本质是基于图像空间域卷积,背后的思想是图像一阶导数算子的理论支持。OpenCV实现了基于Sobel算子提取图像梯度的API,表示与解释如下:
API介绍
void cv::Sobel(
InputArray src, // 输入图像
OutputArray dst, // 输出结果
int ddepth, // 图像深度CV_32F
int dx,// 1,X方向 一阶导数
int dy, // 1,Y方向 一阶导数
int ksize = 3, // 窗口大小
double scale = 1, // 放缩比率,1 表示不变
double delta = 0, // 对输出结果图像加上常量值
int borderType = BORDER_DEFAULT )
ddepth表示图像的深度,-1表示于输入图像一致的深度,但是这个时候会有应该麻烦,-1出来的sobel梯度图像效果很差,原因是sobel算子的范围已经超过了0-255,所以当我们用字节类型去存储的时候就会被截取从而导致x有些信息丢失不全了从而以sobel算子计算出来d的梯度信息也就不准了,
int dx,// 1,X方向 一阶导数
int dy, // 1,Y方向 一阶导数
其中s可以dx=1,dy=0那么就表示是x方向的一阶导数,反之同意是y方向的一阶导数。
int borderType = BORDER_DEFAULT ;就是在卷积的时候怎么c去处理图像边缘的类型,给定一个方法。
代码
void QuickDemo::Sobel_demo()
{
Mat src = imread("D:/images/hist_02.jpg");
Mat grad_x, grad_y;
Mat dst;
/*
CV_EXPORTS_W void Sobel(
InputArray src,
OutputArray dst,
int ddepth, // 图像深度CV_32F
int dx,X方向 一阶导数
int dy, Y方向 一阶导数
int ksize = 3,// 窗口大小
double scale = 1,// 放缩比率,1 表示不变
double delta = 0,// 对输出结果图像加上常
int borderType = BORDER_DEFAULT);就是在卷积的时候怎么c去处理图像边缘的类型,给定一个方法。
注意:
其中s可以dx=1,dy=0那么就表示是x方向的一阶导数,反之同意是y方向的一阶导数。
ddepth表示图像的深度,-1表示于输入图像一致的深度,但是这个时候会有应该麻烦,-1出来的sobel梯度图像效果很差,
原因是sobel算子的范围已经超过了0-255,所以当我们用字节类型去存储的时候就会被截取从而导致x有些信息丢失不全了
从而以sobel算子计算出来d的梯度信息也就不准了,
*/
Sobel(src, grad_x, CV_32F, 1, 0, 3, 1, 0, BORDER_DEFAULT);//计算得到图像的X方向的梯度,并且是CV_32F的
Sobel(src, grad_y, CV_32F, 0, 1, 3, 1, 0, BORDER_DEFAULT);//计算得到图像的Y方向的梯度
//就会得到一个8位的放缩之后的0-255的图像,就是字节大小的图像了,就是吧float类型的图像转换位255的图像说明我们的信息就没有损失
//因为Sobel的出来的结果有可能是正值也有可能是负值,因此要经过这个转换到0-255之间的
convertScaleAbs(grad_x, grad_x);
convertScaleAbs(grad_y, grad_y);
//在将X,Y方向梯度的梯度值转换为8位之后再进行叠加
add(grad_x, grad_y, dst, Mat(), CV_16S);//因为叠加之后的结果又会超出0-255,因此需要使用CV_16S接收
convertScaleAbs(dst, dst);//最后再次进行转换
Mat combine1;
hconcat(src, dst, combine1);//水平拼接函数
imshow("output", combine1);
}
图像的一阶导数算子除了sobel算子之外,常见的还有robert算子与prewitt算子,它们也都是非常好的可以检测图像的梯度边缘信息,通过OpenCV中自定义滤波器,使用自定义创建的robert与prewitt算子就可以实现图像的rober与prewitt梯度边缘检测。
实践
代码
void QuickDemo::robertAndprewitt_demo()
{
Mat src = imread("D:/images/hist_02.jpg");
//定义两种梯度的算子
Mat robert_x = (Mat_<int>(2, 2) << 1, 0, 0, -1);
Mat robert_y = (Mat_<int>(2, 2) << 0, -1, 1, 0);
Mat prewitt_x = (Mat_<char>(3, 3) << -1, 0, 1,
-1, 0, 1,
-1, 0, 1);
Mat prewitt_y = (Mat_<char>(3, 3) << -1, -1, -1,
0, 0, 0,
1, 1, 1);
/*
通过自定义的卷积核然后再通过filter2D来实现我们的robert算子与prewitt算子的一阶导数计算和效果演示。
ddepth,这里不能用 - 1,因为算子不一样那么取值范围也不一样,那么久不能于输入图像一致了,而是要与输出的结果相匹配。
*/
Mat dst;
Mat robert_grad_x, robert_grad_y, prewitt_grad_x, prewitt_grad_y;
filter2D(src, robert_grad_x, CV_16S, robert_x);
filter2D(src, robert_grad_y, CV_16S, robert_y);
convertScaleAbs(robert_grad_x, robert_grad_x);//转换位8位的0-255之间的
convertScaleAbs(robert_grad_y, robert_grad_y);
add(robert_grad_x, robert_grad_y, dst, Mat(), CV_16S);//因为叠加之后的结果又会超出0-255,因此需要使用CV_16S接收
convertScaleAbs(dst, dst);//最后再次进行转换
Mat combine1;
hconcat(src, dst, combine1);//水平拼接函数
imshow("robert", combine1);
filter2D(src, prewitt_grad_x, CV_32F, prewitt_x);
filter2D(src, prewitt_grad_y, CV_32F, prewitt_y);
convertScaleAbs(prewitt_grad_x, prewitt_grad_x);
convertScaleAbs(prewitt_grad_y, prewitt_grad_y);
add(prewitt_grad_x, prewitt_grad_y, dst, Mat(), CV_16S);//因为叠加之后的结果又会超出0-255,因此需要使用CV_16S接收
convertScaleAbs(dst, dst);//最后再次进行转换
hconcat(src, dst, combine1);//水平拼接函数
imshow("prewitt", combine1);
}
图像的一阶导数算子可以得到图像梯度局部梯度相应值,二阶导数可以通过快速的图像像素值强度的变化来检测图像边缘,其检测图像边缘的原理跟图像的一阶导数有点类似,只是在二阶导数是求X、Y方向的二阶偏导数,对图像来说:
一阶导数对图像求导,能突出图像中的对象边缘,且能对x、y方向分别提取边缘,也就是具有方向性;二阶导数对图像的导数求导,对图像中灰度值的剧烈变化敏感,能突出图像的纹理结构,而且不具有方向性。
拉普拉斯算子是一种特别容易受到噪声干扰的边缘发现算子,所以经常对要处理的图像首先进行一个高斯模糊,然后再进行拉普拉斯算子的边缘提取,因此高斯模糊和拉普拉斯算子边缘提取一般在一起,而且在一些场景中会把这两步合并成为一步,就是我们经常听说的LOG算子。
API
OpenCV中Laplacian滤波函数就是二阶导数发现边缘的函数: void cv::Laplacian(
InputArray src,
OutputArray dst,
int ddepth, // 深度默认是-1表示输入与输出图像相同,但是这里一般不使用而是使用CV_32F。
int ksize = 1,// 必须是奇数, 等于1是四邻域算子,大于1改用八邻域算子 double scale = 1,
double delta = 0, // 对输出图像加上常量值
int borderType = BORDER_DEFAULT )
void QuickDemo::Laplacian_demo()
{
Mat image = imread("D:/images/yuan_test.png");
Mat blured, dst;
GaussianBlur(image, blured, Size(3, 3), 0);//3*3的高斯模糊
//cv::Laplacian(
// InputArray src,
// OutputArray dst,
// int ddepth, // 深度默认是-1表示输入与输出图像相同,但是这里一般不使用而是使用CV_32F。
// int ksize = 1,// 必须是奇数, 等于1是四邻域算子,大于1改用八邻域算子梯度选择越明显
//double scale = 1,
// double delta = 0, // 对输出图像加上常量值
// int borderType = BORDER_DEFAULT)
Laplacian(blured, dst, CV_32F, 3, 1.0, 127.0, BORDER_DEFAULT);//拉普拉斯算子的边缘梯度提取
convertScaleAbs(dst, dst);//将结果转换位8位的图像
Mat combine1;
hconcat(image, dst, combine1);//水平拼接函数
imshow("Laplacian_demo四邻域", combine1);
}
图像卷积的主要有三功能分别是图像的模糊/去噪、图像梯度/边缘发现、图像锐化/增强。图像锐化的本质是图像拉普拉斯滤波加原图权重像素叠加的输出,目的是锐化图像,增强图形细节 :
锐化算子
-1 -1 -1
-1 C -1
-1 -1 -1
当C值大于8时候表示图像锐化、越接近8表示锐化效果越好
当C值等于8时候图像的高通滤波 (高对比度的区域)
当C值越大,图像锐化效果在减弱、中心像素的作用在提升(就是中心像素的权重越大,而差异权重加上去作用越小了)
一般常见取9。
还有四邻域那种就是
0 -1 0
-1 5 -1
0 -1 0
实践
代码
void QuickDemo::ruihua_demo()
{
Mat image = imread("D:/images/yuan_test.png");
//定义锐化算子 就是强化细节
Mat sharpen_op = (Mat_<char>(3, 3) << 0, -1, 0,
-1, 5, -1,
0, -1, 0);
Mat result;
filter2D(image, result, CV_32F, sharpen_op);//自定义卷积核,传入锐化算子
convertScaleAbs(result, result);//结果转换8字节
Mat combine1;
hconcat(image, result, combine1);//水平拼接函数
//在图像上加字符
//第一个参数为要加字符的目标函数
//第二个参数为要加的字符
//第三个参数为字体
//第四个参数为子的粗细
//第五个参数为字符的颜色
cv::putText(combine1, "original image", cv::Point(10,30), cv::FONT_HERSHEY_TRIPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
cv::putText(combine1, "suihua image", cv::Point(image.rows+10, 30), cv::FONT_HERSHEY_TRIPLEX, 0.8, cv::Scalar(0, 0, 255), 2);
imshow("ruihua_demo四邻域", combine1);
}
图像卷积处理实现锐化有一种常用的算法叫做Unsharpen Mask方法,这种锐化的方法就是对原图像先做一个高斯模糊,然后用原来的图像减去一个系数乘以高斯模糊之后的图像,然后再把值Scale到0~255的RGB像素值范围之内。
基于USM锐化的方法可以去除一些细小的干扰细节和噪声,比一般直接使用卷积锐化算子得到的图像锐化结果更加真实可信。
USM锐化公式表示如下: (源图像– w*高斯模糊)/(1-w);其中w表示权重(0.1~0.9),默认为0.6
OpenCV中的代码实现步骤
void QuickDemo::USM_demo()
{
Mat image = imread("D:/images/yuan_test.png");
Mat blur_img, usm;
GaussianBlur(image, blur_img, Size(0, 0), 25);//高斯模糊
addWeighted(image, 1.5, blur_img, -0.5, 0, usm);//权重增加
Mat combine1;
hconcat(image, usm, combine1);//水平拼接函数
imshow("USM锐化", combine1);
}
Canny边缘检测器是一种经典的图像边缘检测与提取算法,应用广泛,
主要是因为Canny边缘检测具备以下特点: 1. 有效的噪声抑制 2. 更强的完整边缘提取能力
Canny算法是如何做到精准的边缘提取的,主要是靠下面五个步
void cv::Canny
( InputArray image,
OutputArray edges,
double threshold1, //threshold1 是Canny边缘检测算法第四步中高低阈值链接中低阈值
double threshold2, // threshold2 是Canny边缘检测算法第四步中高低阈值链接中高阈值、高低阈值之比在2:1~3:1之间
int apertureSize = 3,
bool L2gradient = false )// 最后一个参数是计算gradient的方法L1或者L2
void QuickDemo::Canny_demo()
{
Mat src = imread("D:/images/lena.png");
imshow("src image", src);
Mat edges,dst;
/*
void cv::Canny
( InputArray image,
OutputArray edges,
double threshold1, //threshold1 是Canny边缘检测算法第四步中高低阈值链接中低阈值
double threshold2, // threshold2 是Canny边缘检测算法第四步中高低阈值链接中高阈值、高低阈值之比在2:1~3:1之间
int apertureSize = 3,
bool L2gradient = false )// 最后一个参数是计算gradient的方法L1计算量小或者L2计算量大
*/
Canny(src, edges, 100, 300, 3, false);//结果就是边缘,则可以当初mask进行&运算则会只显示边缘颜色了。
src.copyTo(dst, edges);//就是直接mask非0的部分拷贝过去
///Canny(src, edges, 100, 300, 3, true);
imshow("Canny image", dst);
}
学习笔记来自贾志刚老师的opencv知识星期内容