本文已参与「新人创作礼」活动,一起开启掘金创作之路。
大纲
1.如何进行图像混合 2. 如何进行图像拼接 3. 如何进行多通道图像分离和混合 4. 如何调整图像对比度和亮度 5. 如何对图像进行傅立叶变换
一、图像混合 图像混合的核心在于圈出感兴趣区域和addWeighted()函数的使用,当两张图像大小不一致时,就需要先在较大的图上圈出待混合区域,再通过addWeighted()函数与小图进行叠加。 圈出感兴趣区域的方法有两种,一种是通过在通过在原图上截取Rect类的区域,如:
Roi=srcImg(Rect(x,y,width,height));
另一种方法是通过Range函数直接在原图上截取:
Roi=srcImg(Range(y,y+height),Range(x,x+width));
addWeighted()函数用于计算两个矩阵的加权和,其原型为:
addWeighted(InputArray src1, double alpha, InputArray src2,double beta, double gamma, OutputArray dst, int dtype = -1);
第一,三个参数为要进行加权的矩阵; 第二、四个参数为相应矩阵的权重; 第五个参数为线性加权的偏置值; 第六个参数为加权结果的输出矩阵即; 第七个参数为输出矩阵的可选深度,当两个加权矩阵深度相同时,取默认值1-输出矩阵即为该深度; 核心介绍完毕给出图像混合使用范例:
int main()
{
Mat mat_1 = imread("E:\\material\\assassin.jpeg"); //原代码此处第三个参数取199会影响图片大小,估计是opencv版本的锅,现取默认1
Mat mat_2 = imread("E:\\material\\symbol.png");
imshow("assassin", mat_1);
imshow("symbol", mat_2);
Mat imgROI;
imgROI = mat_1(Rect(200, 300, mat_2.cols, mat_2.rows)); //此处imgROI与原图像共享内存
addWeighted(imgROI, 0.5, mat_2, 0.3, 0., imgROI); //共享内存的缘故,操作也对源图像造成了影响
imshow("mixture", mat_1);
imwrite("mixture.png", mat_1);
waitKey(0);
return 0;
}
二、图像拼接 图像拼接和图像混合类似,一般情况下其实可以看作是图像混合时将小图的权重取1,大图的权重取0;但通常我们也会使用掩模使得拼接更加合理,从而与图像混合区别开来。图像拼接的核心在于感兴趣区域的选取和copyTo函数的使用,前者已经介绍过,矩阵的copyTo成员函数用于矩阵的真复制,我们通过将小图真复制到感兴趣区域从而完成图像拼接,copyTo函数原型如下:
copyTo(OutputArray dst, InputArray mask)
第一个参数为复制去向目标矩阵; 第二个参数为掩模矩阵,需要为灰度图,将复制来源矩阵和此掩模取与之后再给到目标矩阵,如果不输入此参数的话则直接将复制来源矩阵给到目标矩阵 图像拼接示例如下:
int main()
{
Mat assassin = imread("E:\\material\\assassin.jpeg");
Mat logo = imread("E:\\material\\symbol.png");
Mat mask = imread("E:\\material\\symbol.png", 0);
imshow("assassin", assassin);
imshow("symbol", logo);
Mat imgROI = assassin(Rect(0, 0, logo.cols, logo.rows));
logo.copyTo(imgROI, mask);
//这种类型的copy函数指将logo与mask重叠后,将mask中为像素值为0位置的logo位置像素变透明,其他像素值位置保持不变,再复制到ROI位置
//相当于logo和mask取与再复制到ROI,如果不用mask的话,结果就直接是logo代替原区域,用了就是logo为零的地方为原区域,不为零的地方为logo
//要求mask与logo的size相同,且通道数为1,或者与ROI的通道数相同
imshow("mixture", assassin);
waitKey(0);
return 0;
}
三、多通道图像的分离与混合 通道分离通过split()函数实现,其原型如下:
void split(InputArray m, OutputArrayOfArrays mv);
第一个参数为待分离的图像,应该是一个多通道矩阵; 第二个参数为储存分离后矩阵的数组或者Vector; 通道混合通过merge()函数实现,其原型如下:
void merge(InputArrayOfArrays mv, OutputArray dst);
参数含义和split()函数正好对偶,不再赘述 通过对多通道图像的分离与混合,结合前述图像混合的知识,我们可以将某张灰度图与另一图片的某一通道混合,给出示例如下:
int main()
{
//读取图像
Mat assassin = imread("E:\\material\\assassin.jpeg");
Mat symbol = imread("E:\\material\\symbol.png",0);//因为要进行的是单通道的混合,这里要读取灰度图片
imshow("原图", assassin);
if (!assassin.data || !symbol.data)
{
cout << "完了,图片路径写错了" << endl;
return false;
}
//创建接收分离通道的vector
vector<Mat> channels;
Mat BlueChannel;
//分离
split(assassin, channels);
//引用蓝色通道
BlueChannel = channels.at(0); //vector类型用at访问可以有效避免越界,但也可使用[]访问,只是不安全
//勾选矩形
Mat ROI = BlueChannel(Rect(0, 0, symbol.cols, symbol.rows));
//混合图像
addWeighted(ROI, 0.8, symbol, 0.2, 0., ROI);
//三通道合并
merge(channels, assassin);
imshow("混合之后的图", assassin);
return 0;
}
四、调整图像的对比度和亮度 图像的对比度和亮度实际指的是相对于原图的增益及偏置,数学公式为,其中即为调整后的图像;为原图;就分别是对比度和亮度了,可以看出a的存在使得原图中像素的极值差增大,b使得所有像素增大或减小,也就不难理解“对比”和“亮”的含义了。 给出对比度亮度调整示例代码如下:
void Contrast_Bright(int, void*);
int Contrast_value=100; //对比度定义
int Bright_value=100; //亮度定义
Mat srcImg = imread("E:\\material\\assassin.jpeg");
int main()
{
namedWindow("原始图像");
imshow("原始图像", srcImg);
namedWindow("处理后图像");
createTrackbar("对比度", "处理后图像", &Contrast_value, 300, Contrast_Bright);
createTrackbar("亮 度", "处理后图像", &Bright_value, 200, Contrast_Bright);
//设置初值
Contrast_Bright(Contrast_value, 0);
Contrast_Bright(Bright_value, 0);
//图像一直显示
while ((char)waitKey(0) != 'q');
return 0;
}
void Contrast_Bright(int, void*)
{
Mat dst_Img;
dst_Img.create(srcImg.rows, srcImg.cols, srcImg.type());
for (int i = 0;i < srcImg.rows;i++)
{
Vec3b* it1 = srcImg.ptr<Vec3b>(i);
Vec3b* it2 = dst_Img.ptr<Vec3b>(i);
for (int j = 0;j < srcImg.cols;j++)
{
for (int k = 0;k <3;k++)
{
it2[j][k] = saturate_cast<uchar>((Contrast_value *it1[j][k]* 0.01) + (Bright_value-100));
//由算子得调整方案,原图*对比+亮度,satutate防止溢出
//×0.01是因为滑动条取值一般为整数,而对比度为0~3.0浮点
}
}
}
imshow("处理后图像", dst_Img);
}
五、图像傅立叶变换 图像傅立叶变换所需数学知识为DFT,参见另一篇博客FFT介绍及python源码编写,此处仅介绍在Opencv中的API及使用方法,Opencv中df函数原型如下:
void dft(InputArray src, OutputArray dst, int flags = 0, int nonzeroRows = 0);
第一个参数为待变换的时域图像;
第二个参数为储存变换结果的频域矩阵;
(注意通常情况下上述两参数都应该为双通道的矩阵,两通道分别保存实部和虚部 ,即使我们的图像都是实数也要分成复数的形式;因为如果src值只用单通道矩阵的,输出结果会采用CCS(复共轭对称)的压缩格式输出为单通道,即:
详情查看dft官方文档:opencv官方文档)
第三个参数为变换标识符,默认取零为正向变换,取其他值时:
第四个值不为0时,函数会默认只有输入矩阵的前nonzeroRows行(未设置DFT_INVERSE)是非零行,或者只有输出矩阵的前nonzeroRows(设置了DFT_INVERSE)行是非零行,因此,函数在处理剩余行是可以节省一些时间,这项技术在采用DFT计算矩阵卷积时尤为明显,通常我们会去这个值为src.rows;
除了最基本的dft函数之外,我们还需要了解其他辅助函数以帮助我们更快速的进行傅立叶变换,如下:
返回DFT最优尺寸
int getOptimalDFTSize(int vecsize);
在FFT相关学习中,我们知道当计算序列为2的幂时更加方便运算,同样对于dft()函数而言,当矩阵各维度长度均为2,3,5的倍数(原理尚未理解)时更方便计算,我们通过getOptimalDFTSize()函数算出这一较好值,其唯一参数就是矩阵某维度的长度,如src.cows. 扩充图像
copyMakeBorder(InputArray src, OutputArray dst,int top, int bottom, int left, int right,int borderType, const Scalar& value = Scalar() );
通过getOptimalDFTSize()函数得到图像的最优尺寸后,就需要使用copyMakeBorder()函数对图像进行填充以使得计算更加迅速,参数信息如下: 第一个参数为填充前图像; 第二个参数为填充后图像; 第三、四、五、六个参数分别为在图像相应方向上添加像素行\列的数目; 第六个参数为填充类型,常见取值为BORDER_CONSTANT表示用指定值填充,而后第七个参数即为此指定边界值Scalar取0,其余具体标识符见下源码:
enum BorderTypes {
BORDER_CONSTANT = 0, //!< `iiiiii|abcdefgh|iiiiiii` with some specified `i`
BORDER_REPLICATE = 1, //!< `aaaaaa|abcdefgh|hhhhhhh`
BORDER_REFLECT = 2, //!< `fedcba|abcdefgh|hgfedcb`
BORDER_WRAP = 3, //!< `cdefgh|abcdefgh|abcdefg`
BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba`
BORDER_TRANSPARENT = 5, //!< `uvwxyz|abcdefgh|ijklmno`
BORDER_REFLECT101 = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_DEFAULT = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_ISOLATED = 16 //!< do not look outside of ROI
};
enum | 符号说明 | |||
---|---|---|---|---|
BORDER_CONSTANT | `iiiiii | abcdefgh | iiiiiii` | 用指定值i填充 |
BORDER_REPLICATE | `aaaaaa | abcdefgh | hhhhhhh` | 用边界值填充 |
BORDER_REFLECT | `fedcba | abcdefgh | hgfedcb` | 以边界(含边界)镜像对称填充 |
BORDER_WRAP | `cdefgh | abcdefgh | abcdefg` | 以边界重复填充 |
BORDER_REFLECT_101 | `gfedcb | abcdefgh | gfedcba` | 以边界(不含边界)镜像对称填充 |
BORDER_TRANSPARENT | `uvwxyz | abcdefgh | ijklmno` | 尚未理解 |
BORDER_ISOLATED | 无 | 尚未理解 | ||
对数尺度缩放 |
log(InputArray src, OutputArray dst);
进行傅立叶变换之后,频域图像幅度值插值过大,高值在屏幕上显示为白点,低值在屏幕上显示为黑点,彼此相互不连续,需要进行对数尺度缩放以使得图像更加连贯。使用常用对数函数,参数即为输入输出矩阵。 归一化
normalize( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0, int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());
对数尺度缩放之后,频域图像极值差虽然减下了,但是并没有到float型图像显示的0~1范围之内,所以我们还需要将图像映射到此范围之内,用到了归一化函数:
第一、二个参数为输入输出矩阵,大小通道相同;
第三个参数确定了选择归一化数学公式中参数的下限;
第四个参数确定了选择归一化数学公式中参数的上限;
第五个参数确定归一化的数学公式,通常选用NORM_MINMAX,即线性归一,其他标识符:
第六个参数决定了输出矩阵的深度,当为负,输出在大小深度通道数都等于输入,当为正,输出只在深度与输入不同,不同的地方由dtype决定;
第七个参数决定了进行归一化的范围,同样是灰度图取与操作。
经由上述介绍给出图像傅立叶变换实例:
int main()
{
//导入图片
Mat srcImg = imread("E:\\material\\assassin.jpeg",0);
if (srcImg.empty())
{
cout << "图片导入失败" << endl;
return -1;
}
//确定输入图像最佳尺寸
Size srcsize;
srcsize.width = getOptimalDFTSize(srcImg.cols);
srcsize.height = getOptimalDFTSize(srcImg.rows);
//扩充图像
Mat padded;
copyMakeBorder(srcImg, padded, 0, srcsize.height - srcImg.rows, 0, srcsize.width - srcImg.cols, BORDER_CONSTANT, Scalar::all(0));
//为傅立叶变化后虚部实部预留空间
Mat planes[] = { Mat_<float>{padded},Mat::zeros(padded.size(),CV_32F )};
//将planes数组合并成一个多通道的矩阵
Mat complexI;
merge(planes, 2, complexI); //2表示待合成的数组个数为2
//傅立叶变换
dft(complexI, complexI);;
//提取实部虚部
split(complexI, planes);
//计算幅值,并储存在magnitudeImg中
Mat magnitudeImg;
magnitude(planes[0], planes[1],magnitudeImg);
//进行对数尺度缩放
magnitudeImg += Scalar::all(1);
log(magnitudeImg, magnitudeImg);
//剪切并重新排布图像
magnitudeImg = magnitudeImg(Rect(0, 0, magnitudeImg.cols & -2, magnitudeImg.rows & -2)); //这里通过位运算的方式判断奇偶性,并使得奇数减一
int cx = magnitudeImg.cols / 2;
int cy = magnitudeImg.rows / 2;
//图像分为四个象限
Mat q0 = magnitudeImg(Rect(0, 0, cx, cy)); //左上
Mat q1= magnitudeImg(Rect(cx, 0, cx, cy));//右上
Mat q2 = magnitudeImg(Rect(0, cy, cx, cy));//左下
Mat q3 = magnitudeImg(Rect(cx, cy, cx, cy));//右下
//交换象限
Mat temp;
q0.copyTo(temp);
q3.copyTo(q0);
temp.copyTo(q3);
q1.copyTo(temp);
q2.copyTo(q1);
temp.copyTo(q2);
//归一化,以显示图片
normalize(magnitudeImg, magnitudeImg, 0, 1, NORM_MINMAX);//最后一个参数指归一化的方式,这里选择线性归一化
//显示图片
imshow("频谱图", magnitudeImg);
imshow("原图", srcImg);
while ((char)waitKey(0) != 'q');
return 0;
}