实验一
实验内容:
1、利用opencv读取图片
在opencv中读取一张图片使用imread()函数,比如:
Mat src = imread("D:/images/dog.jpg", IMREAD_COLOR)。
imread()一般有两个参数,前一个是图片地址,另外一个是读取图片的格式。
这里采用的IMREAD_COLOR只的读取为默认的BGR的格式,常见的有读取为灰度的格式,就可以使用
IMREAD_GRAYSCALE。当然这个参数也可以是数字,比如“-1”,代表读取默认格式。
当我们读取一张图片后可以用empty()函数,判断读取是否成功。比如:
if (src.empty()){
cout << "can`t open this ph!" << endl;
exit(0);
}
读取一张图片后,进行展示需要用到imshow()函数,imshow()一般有两个参数,第一个是窗口名称,第二个参数是一个Mat对象。比如这样:
imshow("input", origin)。
单独使用imshow()给出的窗口的大小和Mat对象的大小有关,且不能调整。这时候就需要设置窗口,要用到namedWindow()函数,这个函数一般也是有两个参数,第一个是窗口名字,第二个是窗口类型,比如
常用的比如WINDOW_FREERATION,这种窗口是可以拖动大小的,WINDOW_AUTOSIZE是默认大小。具体使用如下:
namedWindow("input",WINDOW_FREERATIO);
imshow("input", origin);
通过imshow()展示只能展示三通道或者单通道的图片,有些图片有其他通道,比如png格式的透明通道是不能展示出来的。要看到效果就需要保存下来,这时候会用到imwrite()函数,这个函数也有两个参数第一个是保存的路径以及名称,第二参数是需要保存的Mat对象。举例:
imwrite("D:/images/green_channels.jpg", src_mat[1]);
只有前面几个步骤,运行的时候展示图片会一闪而过,这时候就要用到waitKey(),常用的waitKey(0),waitKey(27),这里的27是ascll码表中的esc。
做完所有的工作后,记得要销毁所有的窗口,这时候就需要destroyAllWindows()。
比如这部分代码:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
void show_photo(const Mat& origin)
{
namedWindow("input",WINDOW_FREERATIO);
imshow("input", origin);
}
int main(int argc, char* argv)
{
Mat src = imread("D:/images/dog.jpg", IMREAD_COLOR);
if (src.empty()) {
cout << "can`t open this ph!" << endl;
exit(0);
}
show_photo(src);
waitKey(0);
destroyAllWindows();
return 0;
}
2、灰度图像二值化处理
具体内容:设置并调整阀值对图像二值化处理。
实验是调整阀值对图片的进行二值化处理,这种代码实现也是很简单的。虽然要造车轮子,但是我们也可以先了解现在有哪些二值化的方法。一般对灰度图二值化的方法是调用threshold()方法(后面我们实现下THRESH_BINARY这种方式),常见的有以下几种类型:
threshold(gray,dst,127,255,THRESH_BINARY);
threshold(gray, dst, 127, 255,THRESH_BINARY_INV);
threshold(gray, dst, 127, 255,THRESH_OTSU);
threshold(gray, dst, 127, 255, THRESH_TRUNC);
threshold(gray, dst, 127, 255, THRESH_TOZERO);
如果使用最大最小值这种就可采用THRESH_BINARY和THRESH_BINARY_INV,这里设置127是不合理的,可以通过全局阀值来寻找这个thershold, 比如常见的均值 ,OTSU和Triangle 的方法,还有自适应阈值
THRESH_BINARY
这种方式大概就是实验中要我们实验的方式,这种方式,
If(像素值 >threshold):
像素值 = 255;
else:
像素值 = 0;
THRESH_BINARY_INV
这种方式恰好相反,
If(像素值 >threshold):
像素值 = 0;
else:
像素值 = 255;
THRESH_TRUNC
这种方式,又叫截断,即是
If(像素值 >threshold):
像素值 = threshold;
else:
像素值不变;
THRESH_TOZERO
这种方式,大于阀值原值,小于threshold变为0;
If(像素值 >threshold):
像素值不变;
else:
像素值 = 0;
全局阈值
均值法
这里可以统计每个像素点计算均值,但是没必要。我们可以调用mean()来获取均值
Scalar m = mean(src),这里的src是一个单通道的Mat对象, m[0]就是图片的均值,我们可以选择把均值作为阀值进行二值化。
THRESH_OSTU
这种方式,这种方式是通过全局统计像素点,利用直方图,找到合适的threshold,比如说将一个灰度图分为几个灰度级别,然后在每个灰度级别选择一个灰度级别作为阈值,然后计算方差,对每个级别的方差乘以自身在整个像素分布的权重,得到总共的方法。然后换下一个灰度级别作为阀值再计算,最后选择产生方差最小的那个灰度级别作为阀值。然后在使用上面几种二值化的方法。常见调用方式为
threshold(gray, dst, 0, 255, THRESH_BINARY | THRESH_OTSU);
三角阈值方法
寻找最大d对应的h,α和β一般为45°,h与横坐标交点一般就是我们要寻找的阈值,一般还会再偏移0.2,有时候如果直方图的峰值大于127,还会做一个对称的映射,然后再变换回去。
自适应阈值方法
举例调用:adaptiveThreshold(gray, dst, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, 11, 3),这种主要用于像素分布不均的图片二值化 ,这种情况下单独设置唯一阈值,有点不合适。 这种方式的方法一般先对原图进行模糊,模糊的方式可以是多种,比如说盒子模糊或者高斯模糊,模糊后的图片我们成为图片D。然后原图S加上一个偏置常量C,我们记为S_C,
然后S_C 减去D,得到差异部分。这里 S_C - D会大于-C, S_C-D = S+C-D ,如果S_C - D>0, 就让他等于255,小于0就让他等于0。这里依赖于模糊的方法,我们可以有自适应均值分割,自适应高斯分割。
实现THRESH_BINARY
我们用代码是实现下THRESH_BINARY这种方式,我设置一个滑动条来动态改变阈值。
实验代码
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
int th = 0;
Mat src;
void my_thresholdTwoValued(int,void*) {
int height = src.rows;
int width = src.cols;
Mat TwoValued = Mat::zeros(src.size(), CV_8UC1);
for (size_t row = 0; row < height; row++) {
for (size_t col = 0; col < width; col++)
{
int pv = src.at<uchar>(row, col);
if (pv >= th) {
TwoValued.at<uchar>(row,col) = 255;
}
else {
TwoValued.at<uchar>(row, col) = 0;
}
}
}
namedWindow("二值化后的图像", WINDOW_FREERATIO);
imshow("二值化后的图像", TwoValued);
}
int main(int argc, char* argv)
{
src = imread("D:/images/dog.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
exit(0);
}
namedWindow("input", WINDOW_FREERATIO);
imshow("input", src);
createTrackbar("阀值", "input", &th, 255, my_thresholdTwoValued);
my_thresholdTwoValued(127,0);
waitKey(0);
destroyAllWindows();
return 0;
}
实验效果:
当阈值拖动为127的时候:
当阈值拖动到107的时候:
当阈值拖动到90的时候:
阈值越小,白色点越多。
当阈值为145的时候:
阈值越高,黑点越多。
3、灰度图像的对数变换
具体内容:设置并调整r值对图像进行对数变换。对数变换常用的公式为:
s = c*log(1 + r)
其中r, s代表处理前后的像素点的值,c为常数。该变换范围较窄的低灰度映射为输出范围较宽的灰度值。这种类型用来拓展暗像素值,压缩更高灰度级别的像素。反对数变得作用与此相反。
实验代码
将输入的灰度图由CV_8UC1成CV_32FC1
src.convertTo(logSrc, CV_32FC1, 1.0);
将像素值都加上1,这样是避免出现log的结果为负
logSrc += Scalar::all(1);
进行对数变换,并且在结果乘以常数c_
log(logSrc, logSrc);
logSrc = logSrc * c_;
转换为CV_8UC1,并变为8位整数
normalize(logSrc,logSrc, 0, 255,NORM_MINMAX);
Mat dst;
logSrc.convertTo(dst, CV_8UC1, 1.0);
convertScaleAbs(dst,dst);
实验效果
这里我将c的值动态变换了一下 ,1到5之内,效果并不明显。
这段的完整代码:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
Mat src;
int c_ = 1;
void Logarithmic_transformation(int, void*)
{
Mat logSrc;
src.convertTo(logSrc, CV_32FC1, 1.0);
logSrc += Scalar::all(1);
log(logSrc, logSrc);
logSrc = logSrc * c_;
normalize(logSrc,logSrc, 0, 255,NORM_MINMAX);
Mat dst;
logSrc.convertTo(dst, CV_8UC1, 1.0);
convertScaleAbs(dst,dst);
namedWindow("对数变换结果", WINDOW_FREERATIO);
imshow("对数变换结果", dst);
}
int main(int argc, char* argv)
{
src = imread("D:/images/nightwalk.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
exit(0);
}
namedWindow("input", WINDOW_FREERATIO);
imshow("input", src);
createTrackbar("c值", "input", &c_, 5, Logarithmic_transformation);
Logarithmic_transformation(1,0);
waitKey(0);
destroyAllWindows();
return 0;
}
4、灰度图像的伽马变换
具体内容:设置并调整γ对图像进行伽马变换。伽马变换的基本形式为:
s = c·r^{γ}
其中c,γ为正常数,当c=γ=1时就是恒等变换,γ>1 与γ<1的效果相反。
常见的应用:
输入监视器的图像经过伽马变换产生外观上接近于原图像的输出。
使用伽马变换增强对比度。
处理图像的“冲淡”现象,进行灰度的压缩,这时候γ>1。
实验代码
这里我做了个灰度压缩的情况。
实验步骤:
1、将图片由CV_8UC1转换成CV_32FC1
src.convertTo(gammaSrc, CV_32FC1, 1.0);
2、设置c值和gamma值,进行伽马变换。这里代码很简单,我没有逐像素点操作,那个很简单。包括按照数组下标访问,还是指针访问,我都做过,我觉得没必要造这个轮子了。
pow(gammaSrc, gamma,gammaSrc);
int c_ = 1;
gammaSrc = gammaSrc * c_;
3、转化为8位整数格式
normalize(gammaSrc, gammaSrc, 0, 255, NORM_MINMAX);
Mat dst;
gammaSrc.convertTo(dst, CV_8UC1, 1.0);
convertScaleAbs(dst, dst);
实验效果
当c=1,γ=1时,就是原图。
当c=1, 伽马=3时,亮的背景部分变暗,人物轮廓变清晰。
当c=1 ,伽马 = 5时,人物突出,背景亮光部分基本变成黑色。
当c=1,伽马=14时:
实验代码:
#include<opencv2/opencv.hpp>
#include<iostream>
using namespace std;
using namespace cv;
Mat src;
int gamma = 2;
void gammaTransformation(int, void*)
{
Mat gammaSrc;
src.convertTo(gammaSrc, CV_32FC1, 1.0);
pow(gammaSrc, gamma,gammaSrc);
int c_ = 1;
gammaSrc = gammaSrc * c_;
normalize(gammaSrc, gammaSrc, 0, 255, NORM_MINMAX);
Mat dst;
gammaSrc.convertTo(dst, CV_8UC1, 1.0);
convertScaleAbs(dst, dst);
namedWindow("伽马变换结果", WINDOW_FREERATIO);
imshow("伽马变换结果", dst);
}
int main(int argc, char** argv)
{
src = imread("D:/images/gamma.jpg", IMREAD_GRAYSCALE);
if (src.empty()) {
exit(0);
}
namedWindow("input",WINDOW_FREERATIO);
imshow("input", src);
createTrackbar("gamma值", "input", &gamma, 35, gammaTransformation);
gammaTransformation(2,0);
waitKey(0);
destroyAllWindows();
return 0;
}
5、彩色图像的补色变换
具体内容:对彩色图像进行补色变换。
代码实现
for (int i = 0; i < row; ++i) {
for (int j = 0; j < col; ++j) {
int b = src.at<Vec3b>(i, j)[0];
int g = src.at<Vec3b>(i, j)[1];
int r = src.at<Vec3b>(i, j)[2];
int maxrgb = max(max(r, g), b);
int minrgb = min(min(r, g), b);
dst.at<Vec3b>(i, j)[0] = maxrgb + minrgb - b;
dst.at<Vec3b>(i, j)[1] = maxrgb + minrgb - g;
dst.at<Vec3b>(i, j)[2] = maxrgb + minrgb - r;
}
}
对于一个RGB图像像素点来说,就是找到当前三个通道上最大最小值,然后将最大最小值相加,再减去原像素点。
对于RGB的补色有点像转换成CMY色彩空间。
实验结果
对于其他的色彩空间的补色变换。就是对该像素点在色环上找到180度对应的值,色环如下图:
这个我看了下其他博客实现的代码:
void Complementary_Color(C3 *src,C3 *dst,int width,int height,int color_space_type){
switch (color_space_type) {
case COLOR_SPACE_RGB:
RGB2CMY(src, dst, width, height);
break;
case COLOR_SPACE_CMY:
CMY2RGB(src, dst, width, height);
break;
case COLOR_SPACE_HSI:{
for(int i=0;i<width*height;i++){
double h=src[i].c1;
if(h>=M_PI)
dst[i].c1=h-M_PI;
else
dst[i].c1=M_PI+h;
dst[i].c2=src[i].c2;
dst[i].c3=255.-src[i].c3;
}
break;
}
case COLOR_SPACE_HSV:{
for(int i=0;i<width*height;i++){
double h=src[i].c1;
if(h>=180.0)
dst[i].c1=h-180.0;
else
dst[i].c1=180.0+h;
dst[i].c2=src[i].c2;
dst[i].c3=1.0-src[i].c3;
}
break;
}
default:
break;
}
}
实验遇到问题
1、 对于彩色图片的补色操作,对于不同的色彩空间有不同的操作,在实验中我只选取了RGB色彩空间的补色。其他色彩空间的补色,我参考了别人的博客:https://www.cnblogs.com/bhlsheji/p/5215171.html
2、在二值化实验中,我实现的是基于阀值T的情况,即是大于T为255,小于T为0.其他的几种情况原理我都列举出来了,实现不难。全局阈值的情况,我介绍了均值法,OSTU以及三角法的简单原理。自适应分割的两种方法,我简单的介绍了原理。
3、在对数变换和伽马变换中,我没有逐像素点去计算,其实过程也很简单。只要会像素点访问方式就明白,一种是基于数组下标的方式,一种是基于指针的操作。我将这两种方式放在我的git上https://github.com/cyssmile/openCV_learning_notes/blob/master/opencv_test/opencv_004_point/main.cpp