OpenCV C++记录(三):颜色空间/椒盐/高斯噪声
图像基础
位图与矢量图
位图是以像素表示图像信息,因此能很容易控制色彩和细节信息,容易进行编辑,但最大的问题在于位图的清晰度束缚于像素分辨率,无限放大的图片必然带来图像的失真,清晰度下降;矢量图则是比较少接触的图像,它不以像素存储图像信息,而是使用各种数学化的几何元素、线段、曲线、多边形等描述,其算法不随放大而变化,因此无限放大下依然能够保持图像清晰,但因为设计数学公式运算,其编辑灵活性较低,矢量图格式包括swf、dwg、emf等;
jpeg、jpg和png
这是位图中常常看到的三种格式,其中jpeg
、jpg
是一种格式,命名差异在于早期Windows限定三个字符,因此将jpeg
称为jpg/jif
等,jpg
的特点是对图像进行有损压缩,体积较小,容易传播,一般的jpg
压缩率是95%,即质量值为95,质量值越低,清晰度越差;jpg
通常采用Progressive JPG(渐进式加载)
的保存方式,网络接收时先展示图像的模糊轮廓,再逐渐填充其他细节,在应用图片加载比较常用;另一种保存方式是Baseline JPG(线性加载)
,从上到下进行扫描和显示,jpg适用于风景图片(压缩效果不容易导致模糊)、网络图片传输等;
png
则是一种无损压缩,意味着无论编辑多少次、如何传输,均不会发生图像信息的丢失,在高对比度区域(颜色分明的图片、同色空间较多),jpg
的有损压缩会导致暗影和模糊,而png
压缩则仍然保持分明,因此效果更好,但png文件一般也大于jpg
;png
相较于jpg另一个大的优势是png
支持透明背景。png
适用于文件、文字的截图,以及Icon、商标制作等。
imwrite
imwrite提供了图像写入的方式,jpeg参数可调节压缩质量和渐进式加载等,png可调节压缩级别,从结果来看jpg从210kb压缩至17kb,清晰度肉眼上没有很大变化,而尽管压缩级别为9,png体积不降反升,可见jpg除了复杂图像的优势。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using namespace std;
using namespace cv;
int main(){
Mat jtest0 = imread("D:\\Documents\\Desktop\\note\\jLena.jpeg",0); //0:灰图
Mat jtest1 = imread("D:\\Documents\\Desktop\\note\\jLena.jpeg",1);
Mat ptest0 = imread("D:\\Documents\\Desktop\\note\\pLena.png",0); //1原图
Mat ptest1 = imread("D:\\Documents\\Desktop\\note\\pLena.png",1);
// imshow("jtest0_show",jtest0);
// imshow("jtest1_show",jtest1);
// imshow("ptest0_show",ptest0);
// imshow("ptest1_show",ptest1);
waitKey(0);
destroyAllWindows();
//IMWRITE_JPEG_LUMA_QUALITY:0-100,压缩质量,默认95;IMWRITE_JPEG_PROGRESSIVE:1使用渐进式加载
imwrite("D:\\Documents\\Desktop\\note\\jLena10.jpeg",jtest1,{IMWRITE_JPEG_LUMA_QUALITY,30,IMWRITE_JPEG_PROGRESSIVE, 1});
//IMWRITE_PNG_COMPRESSION:0-9,压缩级别,默认1
imwrite("D:\\Documents\\Desktop\\note\\pLena10.png",ptest1,{IMWRITE_PNG_COMPRESSION,9});
cout<<"Done";
return 0;
}
split/merge图像RGB分离&融合
注意:CV_8UC3,第一通道是B空间,第三通道才是R空间
将三通道分成三路单通道输出,单通道输出是灰度空间,得到三张灰度不一的灰度图;
1
2
3
4
5
6
7
8
9
10
11
12
13void splitMat(Mat& mat){
int row = mat.rows;
int col = mat.cols;
Mat b(row,col,CV_8UC1);
Mat g(row,col,CV_8UC1);
Mat r(row,col,CV_8UC1);
Mat res[3] = {b,g,r};
split(mat,res);
imshow("blue",b);
imshow("green",g);
imshow("red",r);
}
为了保留彩色空间特性,仍然按照三通道彩色空间输出,分离后merge回去零矩阵:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25//RGB颜色空间分离&融合
void splitMat(Mat& mat){
int row = mat.rows;
int col = mat.cols;
Mat b(row,col,CV_8UC1);
Mat g(row,col,CV_8UC1);
Mat r(row,col,CV_8UC1);
Mat res[3] = {b,g,r};
split(mat,res);
Mat zero_Mat = Mat::zeros(row,col,CV_8UC1);
vector<Mat>Mat_res_b = {b,zero_Mat,zero_Mat}; //merge需要vector
vector<Mat>Mat_res_g = {zero_Mat,g,zero_Mat};
vector<Mat>Mat_res_r = {zero_Mat,zero_Mat,r};
Mat bMat,gMat,rMat;
merge(Mat_res_b,bMat); //merge根据Mat_res_b填充三通道矩阵
imshow("bMat_show",bMat);
merge(Mat_res_g,gMat);
imshow("gMat_show",gMat);
merge(Mat_res_r,rMat);
imshow("rMat_show",rMat);
}
HSV颜色空间
RGB颜色空间通过红绿蓝的混合比例来定义颜色,导致一种颜色的像素数学比例并不准确,即使是纯蓝色的图片,也不能保证其B通道为255,其余两个通道为0,而HSV更符合肉眼对颜色的描述,在颜色定义上有着明显的优势。HSV(Hue, Saturation, Value)使用了三种通道定义来描述像素,分别是色度(Hue)、饱和度(Saturation)和明度(Value);HSV的色度空间是一个360°的圆状空间,0代表红色、60为黄色、120为绿色、180为青色、240为蓝色、300为品红色,特别的,完全的黑色、白色则由明度确定,明度为0为黑,255则为白,其他表示如图:
饱和度和明度(亮度)则用于形容颜色的鲜艳程度、图片光暗度,通过手机编辑软件就能够肉眼体会;
因此HSV空间下从一张图片提取特定颜色是比较容易的事情,因为颜色只有一个通道,从肉眼感觉上就能够确定粗糙的阈值范围,结合不同的饱和度、明度即可得到色块目标。
图像领域的HSV范围定义是色调0-360、饱和度、明度范围均为0-1,为了适应八位三通道的要求,OpenCV将色调除以二即0-180作为H通道范围,S、V通道均从0-1映射到0-255;
RGB to HSV
RGB到HSV转换的数学原理如下: 将RGB归一化:
计算明度V和饱和度S:
根据每个像素的明度,确认不同色调计算公式:
OpenCV提供了一个颜色转换接口:其中code表示转换方法,因为imread读入图像一般是BRG而不是RGB,所以使用COLOR_BGR2HSV
;dstCn
表示转换通道数,默认为0代表自动获取通道数;
1
void cv::cvtColor(InputArray src, OutputArray dst, int code, int dstCn = 0)
HSV提取色块
在HSV空间下,使用一定的颜色阈值可生成掩膜,所谓掩膜实际上是一个二值化图,在阈值范围内的为1(白色),超出阈值的为0(黑色),再与原图相与即可得到色块物体特征。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43void getColorObj(Mat& mRGB,Scalar& threshdown,Scalar& threshup){
Mat mHSV;
cvtColor(mRGB,mHSV,COLOR_BGR2HSV); //转换到HSV处理
if(!mHSV.data){
cout<<"mHSV fault"<<endl;
return;
}
Mat mask;
inRange(mHSV,threshdown,threshup,mask); //注意mask二值化是单通道,不能直接相与
imshow("test",mask); //查看掩图
for(int i=0; i<mHSV.rows; i++){
for(int j=0; j<mHSV.cols; j++){
uchar*elem = mHSV.data + mHSV.step[0]*i + mHSV.step[1]*j;
uchar*msk_elem = mask.data + mask.step[0]*i + mask.step[1]*j;
if((*msk_elem)==0){ //左边二值化位置为0
elem[0] = 0; //原图置黑
elem[1] = 0;
elem[2] = 0;
}
}
}
/*****不用循环置黑也可以用merge的方法
Mat mask_3ch;
merge(vector<Mat>(3,mask),mask_3ch); //将mask扩展三通道
bitwise_and(mHSV,mask_3ch,mHSV); //三通道,按位与
****/
Mat result;
cvtColor(mHSV,result,COLOR_HSV2BGR); //imshow需要BRG空间
if(!result.data){
cout<<"result fault"<<endl;
return;
}
imshow("ColorRes",result);
}
//main调用:
Mat mRGB = imread("D:\\Documents\\Desktop\\note\\bluef.jpg",1);
imshow("rawPic",mRGB);
Scalar threshdown(100,100,0);
Scalar threshup(150,255,255); //设定阈值
getColorObj(mRGB,threshdown,threshup); //执行
waitKey(0);
destroyAllWindows();inRange(mHSV,threshdown,threshup,mask)
函数;接受HSV对象mHSV和两个Scalar阈值,生成二值化掩图mask,注意二值化图输出是单通道值,且取值0或1;Scalar接受1-4通道的对象,和Vec类型类似,但Scalar的值都以double格式存储。
效果:
鼠标获取HSV
对于一些奇怪颜色,可能只有美术生才能肉眼确定颜色范围,因此这里提供一种使用鼠标点击的方法,点击时在控制台获取图像上鼠标位置的HSV,这样就能知道阈值大概分布了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//回调函数:event:点击事件 flags:拖拽事件,int x/y:鼠标坐标、param:观察对象指针
void getplotHSV(int event,int x,int y,int flags,void*param){
Mat* mRGB = reinterpret_cast<Mat*>(param);
Mat mHSV;
cvtColor(*mRGB,mHSV,COLOR_BGR2HSV);
if(!mHSV.data){
cout<<"mHSV fault"<<endl;
return;
}
if(event==EVENT_LBUTTONDOWN){ //左击输出HSV
cout<<mHSV.at<Vec3b>(x,y)<<endl;
}
if(event==EVENT_RBUTTONDOWN){ //右击输出坐标
cout<<"x:"<<x<<"\t"<<"y:"<<y<<endl;
}
}
//main调用:
if(mRGB.data)
setMouseCallback("rawPic",getplotHSV,reinterpret_cast<void*>(&mRGB));
使用这样的方法提取粉色花瓣:阈值定义为(5,105,120)
和(80,120,255)
:
从上图可见对于复杂、连通性较差物体轮廓,单靠HSV颜色空间去提取特征,仍然还是困难的。
添加噪声
椒盐噪声
随机数产生的随机分布噪声,将对于像素置白: 1
2
3
4
5
6
7
8
9
10
11//向图像m添加n份椒盐
void addSaltNoise(Mat& m,int n){
for(int i=0; i<n; i++){
int x = rand()%m.rows;
int y = rand()%m.cols;
Vec3b* elem = m.ptr<Vec3b>(x);
elem[y][0] = 255;
elem[y][1] = 255;
elem[y][2] = 255;
}
}
高斯噪声
一维高斯分布的概率密度是:
Box-Muller变换
Box-Muller变换给出了通过两个服从均匀分布的随机变量,如何得到服从正态分布的随机变量,结论是:假设U1、U2服从[0,1]区间的均匀分布且相互独立,那么有:
其中rand()
就是一种产生均匀分布随机变量的函数,然后对应使用cos或者sin均可生成对应的高斯分布。
因此代码: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//产生高斯变量
double getGaussianfunc(int mu,int sigma){ //均值mu、标准差sigma
double u1 = 1.0*rand()/RAND_MAX;
double u2 = 1.0*rand()/RAND_MAX;
double z = sqrt(-2*log(u1))*cos(2*CV_PI*u2);
return sigma*z+mu;
}
//图像加入高斯噪声,不想原图操作可clone并返回;
void addGaussianNoise(Mat& m,int mu,int sigma){//均值mu、标准差sigma
for(int i=0; i<m.rows; i++){
for(int j=0; j<m.cols; j++){
uchar* elem = m.data + i*m.step[0] + j*m.step[1];
int elem0 = elem[0] + getGaussianfunc(mu,sigma)*64; //高斯噪声,噪音水平64,可自调
int elem1 = elem[1] + getGaussianfunc(mu,sigma)*64;
int elem2 = elem[2] + getGaussianfunc(mu,sigma)*64;
elem[0] = elem0 > 0 ? (elem0<255?elem0:255):0; //确保像素
elem[1] = elem1 > 0 ? (elem1<255?elem1:255):0;
elem[2] = elem2 > 0 ? (elem2<255?elem2:255):0;
}
}
}
RNG类
RNG是OpenCV生成随机数的工具,可生成三种随机数:随机64bit数、服从某区间均匀分布的随机数、服从高斯分布的随机数;
64bit数
rng默认返回一个64bit数,而实际上这个数能被转换成任意基本数据类型,如double、int、float等;系统生成随机数时实际上已经生成了随机数组,使用next
可返回一个uint类型(32位)的整数,还可以通过operator指定下一个返回的随机数类型,注意不要使用其他类型接受next函数,operator更加安全。此外支持指定范围的返回;
1
2
3
4
5
6int rnum1 = rng; //任意基本数据类型
int rnum2 = rng.next();
double rnum3 = rng.operator double();
float rnum4 = rng.operator float();
int rnum = rng.operator ()(100); //返回[0,100)的随机数
均匀分布
返回区间[a,b)的均匀分布,a、b类型必须一致,int、float、double的一种,默认为int;
1
2RNG rng;
rng.uniform(50,100);
高斯分布
高斯分布接受标准差σ,返回服从1
2
3RNG rng;
rng.gaussian(2); //均值0,方差4
4+rng.gaussian(2); //均值4,方差4
RNG填充矩阵
RNG
可按均匀分布生成随机值并且填充矩阵,true
表示先确定范围(如[0,256)),再进行生成,因此值严格小于256;false
是先生成随机值,再向下截断为256,因此有可能等于256;
1
2
3
4
5
6RNG rng;
Mat m(20,15,CV_8UC3);
Mat m1 = m.clone();
rng.fill(m,RNG::UNIFORM,0,256,true); //严格小于256
rng.fill(m1,RNG::UNIFORM,0,256,false); //可能截断为256
cout<<m<<endl<<m1<<endl;
高斯分布也可进行填充: 1
rng.fill(m,RNG::NORMAL,1,3); //均值1,标准差3(方差9)
伪随机问题
计算机生成的随机数,实际上不是事实上的随机数,算法一定、种子一定、编译平台不变等情况下多次生成的随机数有可能是出现重复的,这种问题可以通过绑定种子解决,例如当前系统的时间,那么随机数的独立性就高很多了,其他随机生成方法同理:
1
2
RNG rng((unsigned)time(NULL));
RNG生成高斯噪声
1 | void addRngNmalNoise(Mat& m,int mu,int sigma){ //均值mu、标准差sigma |
C++11 库函数
C++11
引入了生成高斯变量的方法:绑定随机数生成器、生成高斯分布随机数即可:
1
2
3
4
5
6
7
8//头文件:#include <random>
//随机种子:从1970至今经过的纳秒数,32位截断
unsigned int seed = chrono::system_clock::now().time_since_epoch().count();
default_random_engine generator(seed); //定义生成器generator
normal_distribution<double> distribution(0.0, 1.0); //定义分布方式normal_distribution
for (int i = 0; i < 10; ++i) //可多次生成
cout << distribution(generator) << endl; //生成正态分布变量
代码如下: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void addGenNmalNoise(Mat& m,double mu,double sigma){
uint seed = 10086;
uint seed1 = 20000;
uint seed2 = 128;
default_random_engine generator1(seed); //三个随机种子
default_random_engine generator2(seed1);
default_random_engine generator3(seed2);
normal_distribution<double> distribution(mu, sigma);
for(int i=0; i<m.rows; i++){
for(int j=0; j<m.cols; j++){
uchar* elem = m.data+i*m.step[0]+j*m.step[1];
elem[0] += 32*round(distribution(generator1));//噪声叠加
elem[1] += 32*round(distribution(generator2));
elem[2] += 32*round(distribution(generator3));
//像素截断
elem[0] = elem[0] > 0 ? (elem[0]<255?elem[0]:255):0;
elem[1] = elem[1] > 0 ? (elem[1]<255?elem[1]:255):0;
elem[2] = elem[2] > 0 ? (elem[2]<255?elem[2]:255):0;
}
}
}
三种方法效果展示
效果几乎和磨砂滤镜差不多,不同方法的参数敏感度也不一样,和随机数生成结果有关、也和截断策略有关,但是还不清楚为什么库函数的方法产生了那么明显的绿色噪点...
参考链接: