Fork me on GitHub

OpenCV3编程入门(毛星云)读书笔记(二)

Core组件进阶

颜色空间缩减

颜色空间缩减:将现有的颜色空间值除以某个输入值,以获得较少的颜色数。(对于多通道图像而言颜色数太多了)。如以10为除数,10-19为10,20-29为20.

处理图像像素时,每个像素都进行一遍上述的操作。但其实只有0-255即256种情况,可以提前将256种计算好的结果存在表里,这样直接取结果即可。

int divideWith = 10;
uchar table[256];
for(int i = 0;i<256;i++){
    table[i] = divideWith * (i/divideWith);
}//table表中存放的是值为i像素减少颜色空间的结果

LUT函数

look up table。用来查表

批量进行图像查找、扫描与操作图像

Mat lookUpTable(1,256,CV_8U);
uchar *p = lookUpTable.data;
for(int i = 0;i<256;i++){
    p[i] = table[i];
}
for(int i = 0;i<times;i++){
    LUT(I,lookUpTable,J);//I输入,J输出
}

计时函数

getTickCount():返回cpu自某个事件以来走过的时钟周期

getTickFrequency():返回CPU1秒钟所走的时钟周期数

double time0 = static_cast<double>(getTickCount());
//处理事件
time0 = ((double)getTickCount()-time0)/getTickFrequency();
cout<<"花费时间"<<time0<<endl;

访问图像中的像素

对于颜色较多的情况,比如有256种颜色,我们希望有64种,那么只需要将原来的颜色除以4,以后再乘以4即可.

下面给出的各例子为对图像中的像素进行缩减。

#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <iostream>  
using namespace std;
using namespace cv;

void colorReduce(Mat& inputImage, Mat& outputImage, int div);

int main()
{
	//【1】创建原始图并显示
	Mat srcImage = imread("1.jpg");
	imshow("原始图像", srcImage);

	//【2】按原始图的参数规格来创建创建效果图
	Mat dstImage;
	dstImage.create(srcImage.rows, srcImage.cols, srcImage.type());//效果图的大小、类型与原图片相同 

	//【3】记录起始时间
	double time0 = static_cast<double>(getTickCount());

	//【4】调用颜色空间缩减函数
	colorReduce(srcImage, dstImage, 32);

	//【5】计算运行时间并输出
	time0 = ((double)getTickCount() - time0) / getTickFrequency();
	cout << "\t此方法运行时间为: " << time0 << "秒" << endl;  //输出运行时间

	//【6】显示效果图
	imshow("效果图", dstImage);
	waitKey(0);
}

用指针访问像素

//---------------------------------【colorReduce( )函数】---------------------------------
//          描述:使用【指针访问:C操作符[ ]】方法版的颜色空间缩减函数
//----------------------------------------------------------------------------------------------
void colorReduce(Mat& inputImage, Mat& outputImage, int div)//div=32
{
	//参数准备
	outputImage = inputImage.clone();  //拷贝实参到临时变量
	int rowNumber = outputImage.rows;  //行数
	int colNumber = outputImage.cols * outputImage.channels(); 
    //列数 x 通道数=每一行元素的个数

	//双重循环,遍历所有的像素值
	for (int i = 0; i < rowNumber; i++)  //行循环
	{
		uchar* data = outputImage.ptr<uchar>(i);  //获取第i行的首地址
		for (int j = 0; j < colNumber; j++)   //列循环
		{
			// ---------【开始处理每个像素】-------------     
			data[j] = data[j] / div * div + div / 2;
			// ----------【处理结束】---------------------
		}  //行处理结束
	}
}

Mat的公有变量cols和rows获取图像的宽和高,成员函数channels()返回图像的通道数。灰度图通道数为1,彩色图通道数为3

每行的像素 = 列数 x 通道数

Mat类中的ptr函数可以得到任意行的首地址。返回第i行的首地址

data[j] = data[j] / div * div + div / 2;
//等价于
*data++ = *data/div*div+div/2;

image-20211127113101540

用迭代器访问像素

void colorReduce(Mat& inputImage, Mat& outputImage, int div)  
{  
	//参数准备
	outputImage = inputImage.clone();  //拷贝实参到临时变量
    //clone()是将原本的矩阵复制一份,而不是信息头
    
	//获取迭代器
	Mat_<Vec3b>::iterator it = outputImage.begin<Vec3b>();  //初始位置的迭代器
	Mat_<Vec3b>::iterator itend = outputImage.end<Vec3b>();  //终止位置的迭代器

	//存取彩色图像像素
	for(;it != itend;++it)  
	{  
		// ------------------------【开始处理每个像素】--------------------
		(*it)[0] = (*it)[0]/div*div + div/2;  
		(*it)[1] = (*it)[1]/div*div + div/2;  
		(*it)[2] = (*it)[2]/div*div + div/2;  
		// ------------------------【处理结束】----------------------------
	}  //三通道
}  

动态地址计算

void colorReduce(Mat& inputImage, Mat& outputImage, int div)
{
	//参数准备
	outputImage = inputImage.clone();  //拷贝实参到临时变量
	int rowNumber = outputImage.rows;  //行数
	int colNumber = outputImage.cols;  //列数

	//存取彩色图像像素
	for (int i = 0; i < rowNumber; i++)
	{
		for (int j = 0; j < colNumber; j++)
		{
			// ------------------------【开始处理每个像素】--------------------
			outputImage.at<Vec3b>(i, j)[0] = outputImage.at<Vec3b>(i, j)[0] / div * div + div / 2;  //蓝色通道
			outputImage.at<Vec3b>(i, j)[1] = outputImage.at<Vec3b>(i, j)[1] / div * div + div / 2;  //绿色通道
			outputImage.at<Vec3b>(i, j)[2] = outputImage.at<Vec3b>(i, j)[2] / div * div + div / 2;  //红是通道
			// -------------------------【处理结束】----------------------------
		}  // 行处理结束     
	}
}

at(int y,int x)可以用来存取图像元素,但必须在编译器知道图像的数据类型。要确保指定的数据类型和矩阵中的数据类型相同,at不会对任何数据类型转换。

注意上述代码中对于各通道的写法。

彩图由三个部分组成:蓝色通道、绿色通道、红色通道(BGR)。所以包含彩图的Mat会返回一个由三个8位数组成的向量。此类型向量为Vec3b。

所以使用下面的写法

outputImage.at<Vec3b>(i, j)[channel] = value;
//channel为通道号 

注意是以BGR的顺序存放

感兴趣区域ROI

从图像中选择一个区域,这个区域是图像处理的重点。

定义ROI区域:

  1. 使用表示矩形区域的Rect。指定矩形的左上角坐标和矩形的长宽
Mat imageROI;
imageROI = image(Rect(500,250,logo.cols,logo.rows));//logo是一个Mat类型的图像
  1. 指定感兴趣的行或列的范围。
imageROI = image(Range(250,250+logo.rows),Range(200,200+logo.cols));

对ROI实现图像叠加

#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
bool ROI_AddImage();
int main()
{
	ROI_AddImage();
	return 0;
}
bool ROI_AddImage()
{
	Mat srcImage1 = imread("dota_pa.jpg");
	Mat logoimage = imread("1.1.jpg");
	if(!srcImage1.data)
	{
		cout << "读入srcimage1错误" << endl;
	}
	if (!logoimage.data)
	{
		cout << "读入logoimage错误" << endl;
	}
	Mat imageROI = srcImage1(Rect(20, 25, logoimage.cols, logoimage.rows));
	//获取logo图的大小为ROI区域
	Mat mask = imread("1.1.jpg",0);//灰度图.掩膜应该为灰度图,为了查找
	logoimage.copyTo(imageROI, mask);//掩膜复制到ROI区域
	//这里是将掩膜的logo联系起来,将符合掩膜的logo那部分复制到ROI区域
	namedWindow("success");
	imshow("success", srcImage1);
	imshow("logo", logoimage);
	waitKey(0);
	return true;
}

线性混合操作

产生画面叠化的效果。利用addWeighted()函数

通过在0-1之间改变alpha值,对两幅图像画面叠化

void addWeighted(InputArray src1,double alpha,InputArray src2,double beta,double gamma,OutputArray dst,int dtype = -1)

  1. src1:需要加权的第一个数组,一个Mat
  2. alpha:第一个数组的权重
  3. src2:第二个数组,需要与第一个数组拥有相同的尺寸和通道数
  4. beta:第二个数组的权重
  5. gamma:一个加到权重总和上的标量值
  6. dst:输出的数组,与一、二个数组相同的尺寸和通道数
  7. 输出阵列的深度。默认为-1,表示等同于src1的深度

运算过程:dst = src1[I] * alpha + src[I] * beta + gamma;

I表示多维数组的索引值。若输出数组的深度为CV_32S则该函数不适用。

bool LinearBlending()
{
	double alphaValue = 0.5;
	double detaValue;
	Mat srcImage2, srcImage3, dstImage;
	srcImage2 = imread("mogu.jpg");
	srcImage3 = imread("rain.jpg");
	if (!srcImage2.data)
	{
		cout << "读入srcImage2错误" << endl;
	}
	if (!srcImage3.data)
	{
		cout << "读入srcImage3错误" << endl;
	}
	detaValue = (1.0 - alphaValue);
	addWeighted(srcImage2, alphaValue, srcImage3, detaValue, 0.0, dstImage);
	imshow("原图", srcImage2);
	imshow("效果图", dstImage);
	waitKey(0);
	return true;
}
image-20211128113453697 image-20211128112753146

ROI区域线性混合

bool ROI_AddImage()
{
	double alphaValue = 0.5;
	double betaValue;
	Mat dstImage;
	Mat srcImage1 = imread("dota_pa.jpg");
	Mat logoimage = imread("dota_logo.jpg");
	if(!srcImage1.data)
	{
		cout << "读入srcimage1错误" << endl;
	}
	if (!logoimage.data)
	{
		cout << "读入logoimage错误" << endl;
	}
	betaValue = 1.0 - alphaValue;
	Mat imageROI = srcImage1(Rect(200, 250, logoimage.cols, logoimage.rows));

	addWeighted(imageROI, alphaValue, logoimage, betaValue, 0.0, imageROI);
/*这里输出的图片是imageROI,而这一部分是在srcImage1上选取的感兴趣部分。所以此时srcimage1已经被改变了。**所以输出的是srcimage1,而不是imageROI。输出imageROI时显示的是线性混合和那一部分区域的图像
*/
	imshow("logo", srcImage1);
	waitKey(0);
	return true;
}
image-20211128143037523

logo是单独的一张图片,结果线性混合后,就像是一张图。

分离颜色通道

通道分离:splite()函数

void split(const Mat& src,Mat* mvbegin);

void split(InputArray m,OutputArrayOfArrays mv);

src或者m填需要分离的多通道的数组

mv填函数的输出数字或者输出的Vector容器

通道合并

split()的逆操作

void merge(const Mat* mv,size_t count,OutputArray dst)

void merge(InputArrayOfArrays mv,OutputArray dst)

mv为要被合并的输入矩阵或vector容器的阵列。mv参数中的所有矩阵必须有着一样的尺寸和深度

count代表输入矩阵的个数

dst为输出的矩阵

merge将一些数组合并称一个多通道的数组。

bool  MultiChannelBlending()
{
	//【0】定义相关变量
	Mat srcImage;
	Mat logoImage;
	vector<Mat> channels;//通道,它用来存储split后的三个单通道的图像
	Mat  imageBlueChannel;

	// 【1】读入图片
	logoImage = imread("dota_logo.jpg", 0);//灰度图的形式
	srcImage = imread("dota.jpg");

	if (!logoImage.data) { printf("Oh,no,读取logoImage错误~! \n"); return false; }
	if (!srcImage.data) { printf("Oh,no,读取srcImage错误~! \n"); return false; }

	//【2】把一个3通道图像转换成3个单通道图像
	split(srcImage, channels);//分离色彩通道

	//【3】将原图的蓝色通道引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
	imageBlueChannel = channels.at(0);
	//0蓝 1绿 2红

	//【4】将原图的蓝色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageBlueChannel中
	addWeighted(imageBlueChannel(Rect(500, 250, logoImage.cols, logoImage.rows)), 1.0,
		logoImage, 0.5, 0, imageBlueChannel(Rect(500, 250, logoImage.cols, logoImage.rows)));
	//这里的imageBlueChannel的用法和先前作ROI的方法一样

	//【5】将三个单通道重新合并成一个三通道
	merge(channels, srcImage);

	//【6】显示效果图
	namedWindow(" <1>游戏原画+logo蓝色通道");
	imshow(" <1>游戏原画+logo蓝色通道", srcImage);

	return true;
}

效果就是logo是以蓝色的形式贴在原图上,修改channels.at(i)中的i,可以改变不同的通道。1为蓝,2为绿,3为红

图像对比度、亮度值

通过点操作来实现。

点操作特点:只根据输入像素值来计算相应的输出像素值。

最常用的点操作是乘上一个常数(对比度的调节)以及加上一个常数(亮度值的调节)

g(x) = a*f(x)+b

  1. f(x)为源图像像素

  2. g(x)为输出图像像素

  3. a(a>0)被称为增益,控制对比度

  4. b被称为偏置,控制图像的亮度

或者上式可以写为:g(i,j) = a*f(i,j)+b (i和j表示像素位于第i行,第j列)

//访问像素
for(int y = 0;y < image.rows;y++)
{
    for(int x = 0;x<image.cols;x++)
    {
        for(int c = 0;c<3;c++)//c代表GBR的三个通道
        {
            new image.at<Vec3b>(y,x)[c] = saturate_cast<unchar>((g_nContrastValue*0.01)*(image.at<Vec3b>(y,x)[c]) + g_nBeightValue);
            //saturate_cast对结果进行转换,保证它是有效值。因为运算结果可能是非整数或者溢出
        }
    }
}

示例程序

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>

using namespace std;
using namespace cv;

static void ContrastAndBright(int, void*);

int g_nContrastValue; //对比度值
int g_nBrightValue;  //亮度值
Mat g_srcImage, g_dstImage;

int main()
{
	// 读入用户提供的图像
	g_srcImage = imread("flower.png");
	if (!g_srcImage.data) { printf("Oh,no,读取g_srcImage图片错误~! \n"); return false; }
	g_dstImage = Mat::zeros(g_srcImage.size(), g_srcImage.type());

	//设定对比度和亮度的初值
	g_nContrastValue = 80;
	g_nBrightValue = 80;

	//创建窗口
	namedWindow("【效果图窗口】", 1);

	//创建轨迹条
	createTrackbar("对比度:", "【效果图窗口】", &g_nContrastValue, 300, ContrastAndBright);
	createTrackbar("亮   度:", "【效果图窗口】", &g_nBrightValue, 200, ContrastAndBright);

	//调用回调函数
	ContrastAndBright(g_nContrastValue, 0);
	ContrastAndBright(g_nBrightValue, 0);

	//输出一些帮助信息
	cout << endl << "\t运行成功,请调整滚动条观察图像效果\n\n"
		<< "\t按下“q”键时,程序退出\n";

	//按下“q”键时,程序退出
	while (char(waitKey(1)) != 'q') {}
	return 0;
}

//-----------------------------【ContrastAndBright( )函数】------------------------------------
//	描述:改变图像对比度和亮度值的回调函数
//-----------------------------------------------------------------------------------------------
static void ContrastAndBright(int, void*)
{

	// 创建窗口
	namedWindow("【原始图窗口】", 1);
	// 三个for循环,执行运算 g_dstImage(i,j) = a*g_srcImage(i,j) + b
	for (int y = 0; y < g_srcImage.rows; y++)
	{
		for (int x = 0; x < g_srcImage.cols; x++)
		{
			for (int c = 0; c < 3; c++)
			{
				g_dstImage.at<Vec3b>(y, x)[c] = saturate_cast<uchar>((g_nContrastValue * 0.01) * (g_srcImage.at<Vec3b>(y, x)[c]) + g_nBrightValue);
			}
		}
	}
	// 显示图像
	imshow("【原始图窗口】", g_srcImage);
	imshow("【效果图窗口】", g_dstImage);
}

使用之前提到过的轨迹条的写法和点操作结合即可。

离散傅里叶变换

离散傅里叶变换(DFT):指傅里叶变换在时域和频域上都呈现出离散的形式,将时域信号的采样变换为在离散时间傅里叶变换频域的采样。

在形式上,变换两端(时域、频域)的序列是有限长的,而实际上这两组序列列都应当被认为是离散周期信号的主值序列。即使对有限长的离散信号做DFT,也要对其结果周期延拓称为周期信号再进行变换。

实际中常用快速傅里叶变换来高效计算DFT

简单来说对一张图傅里叶变换就是将它分解为正弦和余弦两部分,即将图像从空间域转换到频域

转换后的频域是复数,因此,显示傅里叶变换后的结果需要使用实数图像加虚数图像,或者幅度图像加相位图像的形式。但是在实际的图像处理过程中,仅仅用了幅度图像,因为幅度图像包含了原图像几乎所有需要的几何信息。但是如果想要通过修改幅度图像或相位图像的方法来间接修改原图像,需要使用逆傅里叶变换来得到修改后的空间图像,这样就必须同时保存幅度图像和相位图像了

频域中,高频部分代表图像的细节、纹理部分;低频表示图像的轮廓信息。

傅里叶变换在图像处理中可以做到图像的增强与图像去噪、图像分割之边缘检测、图像特征提取、图片压缩等。

dft()函数yi

对一维或二维浮点数数组进行正向或者反向傅里叶转换

void dft(InputArray src,Outstream dst,int flags = 0,int nonzeroRows = 0)

  1. src:输入矩阵,可为实数或虚数
  2. dst:调用后的运算结果。其尺寸与类型取决于flags
  3. flags:转换的标识符。默认为0
  4. nonzeroRows:默认为0.若设为非0值,则为最想要处理的那一行

getOptimalDFTSize()

返回给定向量尺寸大的傅里叶最优尺寸大小

int getOptimalDFTSize(int vecsize)

vecsize为向量尺寸,即图片的rows和cols

copyMakeBorder()

扩充图像边界

copyMakeBorder(InputArray src,OutputArray,int top,int bottm,int left,int right,int borderType,const Scalar& value = Scalar() )

top、bottom、left、right为源图像的四个方向上扩充多少像素

bordeType为边界类型,常取BORDER_CONSTANT

value默认为0.边界类型取BORDER_CONSTANT时,该值表示边界值

magnitude()

计算二维矢量的幅值

代码示例

//---------------------------------【头文件、命名空间包含部分】-----------------------------
//		描述:包含程序所使用的头文件和命名空间
//-------------------------------------------------------------------------------------------------
#include "opencv2/core/core.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;



//--------------------------------------【main( )函数】-----------------------------------------
//          描述:控制台应用程序的入口函数,我们的程序从这里开始执行
//-------------------------------------------------------------------------------------------------
int main()
{

	//【1】以灰度模式读取原始图像并显示
	Mat srcImage = imread("flower.png", 0);//以灰度图的形式读取
	if (!srcImage.data) { printf("读取图片错误,请确定目录下是否有imread函数指定图片存在~! \n"); return false; }
	imshow("原始图像", srcImage);


	//【2】将输入图像延扩到最佳的尺寸,边界用0补充
	int m = getOptimalDFTSize(srcImage.rows);
	int n = getOptimalDFTSize(srcImage.cols);
	//将添加的像素初始化为0.
	Mat padded;
	copyMakeBorder(srcImage, padded, 0, m - srcImage.rows, 0, n - srcImage.cols, BORDER_CONSTANT, Scalar::all(0));

	//【3】为傅立叶变换的结果(实部和虚部)分配存储空间。
	//将planes数组组合合并成一个多通道的数组complexI
	Mat planes[] = { Mat_<float>(padded), Mat::zeros(padded.size(), CV_32F) };
	Mat complexI;
	merge(planes, 2, complexI);

	//【4】进行就地离散傅里叶变换
	dft(complexI, complexI);

	//【5】将复数转换为幅值,即=> log(1 + sqrt(Re(DFT(I))^2 + Im(DFT(I))^2))
	split(complexI, planes); // 将多通道数组complexI分离成几个单通道数组,planes[0] = Re(DFT(I), planes[1] = Im(DFT(I))
	magnitude(planes[0], planes[1], planes[0]);// planes[0] = magnitude  
	Mat magnitudeImage = planes[0];

	//【6】进行对数尺度(logarithmic scale)缩放
	magnitudeImage += Scalar::all(1);
	log(magnitudeImage, magnitudeImage);//求自然对数

	//【7】剪切和重分布幅度图象限
	//若有奇数行或奇数列,进行频谱裁剪      
	magnitudeImage = magnitudeImage(Rect(0, 0, magnitudeImage.cols & -2, magnitudeImage.rows & -2));
	//重新排列傅立叶图像中的象限,使得原点位于图像中心  
	int cx = magnitudeImage.cols / 2;
	int cy = magnitudeImage.rows / 2;
	Mat q0(magnitudeImage, Rect(0, 0, cx, cy));   // ROI区域的左上
	Mat q1(magnitudeImage, Rect(cx, 0, cx, cy));  // ROI区域的右上
	Mat q2(magnitudeImage, Rect(0, cy, cx, cy));  // ROI区域的左下
	Mat q3(magnitudeImage, Rect(cx, cy, cx, cy)); // ROI区域的右下
	//交换象限(左上与右下进行交换)
	Mat tmp;
	q0.copyTo(tmp);
	q3.copyTo(q0);
	tmp.copyTo(q3);
	//交换象限(右上与左下进行交换)
	q1.copyTo(tmp);
	q2.copyTo(q1);
	tmp.copyTo(q2);

	//【8】归一化,用0到1之间的浮点值将矩阵变换为可视的图像格式
	normalize(magnitudeImage, magnitudeImage, 0, 1, CV_MINMAX);

	//【9】显示效果图
	imshow("频谱幅值", magnitudeImage);
	waitKey();

	return 0;
}
image-20211202113019434
posted @ 2021-12-02 11:39  kki_m  阅读(147)  评论(0)    收藏  举报