《Opencv3编程入门》学习笔记
记录一下在学习《Opencv3编程入门》这本书时遇到的问题或重要的知识点。
第五章 core组件进阶
一、访问图像中的像素
(一)图像在内存之中的存储方式
图像矩阵的大小取决于所用的颜色模型,确切地说,取决于所用通道数。
灰度图像
多通道图像
(二)颜色空间缩减
若矩阵元素存储的是单通道像素,使用C或C++的无符号字符类型,那么像素可有256个不同值。但若是三通道图像,这种存储格式的颜色数就有256256256,确切的说有一千六百多万种。
作用:可以大大降低运算复杂度
做法:将现有颜色空间值除以某个输入值,以获得较少的颜色数。也就是“做减法”,比如颜色值0到9可取为新值0,10到19可取为10,以此类推。
简单的颜色空间缩减算法由下面两步组成:
(1)遍历图像矩阵的每一个像素
(2)对像素应用上述公式
int deivideWith = 10;
uchar table[256];
for(int i = 0;i < 256;++i){
table[i] = deivideWith * (i/deivideWith );
p[j] = table[p[j]];
(三)LUT函数:Look up table操作(即滤镜)
作用:突出的有用信息,增强图像的光对比度的作用
cv2. LUT(src, lut, dst=None)有三个参数,分别为:
src:输入数据array,类型为8位整型(np.uin8)
lut:查找表,如果输入src是多通道的,例如是BGR三通到的图像,而查表是单通道的,则此时B、G、R三个通道使用的是同一个查找表
dst=None:输出数组,大小和通道数与src相同,而深度depth与lut相同
//首先我们建立一个mat型用于查表。
Mat lookUpTable(1,256,CV_8U);
uchar* p = lookUpTable.data;
//改变查找表的对应
for(int i = 0;i < 256;++i)
p[i] = table[i];
//然后我们调用函数(I是输入,J是输出):
for (int i = 0;i < times;++i)
LUT(I,lookUpTable,J);
(四)计时函数
- getTickCount():返回CPU自某个时间如(启动电脑)以来走过的时钟周期数。
- getTickFrequency()函数返回CPU一秒钟所走的时钟周期数。
double time0 = static_cast<double>(getTickCount()); //记录起始时间
//进行图像处理操作
time0 = ((double)getTickCount() - time0)/getTickFrequency();
cout<<"此方法运行时间为:"<<time0<<"秒"<<endl; //输出运行时间
(五)访问图像中像素的三类方法
示例代码:减少图像中颜色的数量
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
void colorReduce(Mat& inputImage, Mat& outputImage, int div);
int main() {
std::cout << "Hello, World!" << std::endl;
//【1】创建原始图并显示
Mat image = imread("D://lili/Desktop/jpg/opencv/1.jpg");
cout << image.size() << endl;
cout << image.rows << endl;
cout << image.cols << endl;
cout << image.channels() << endl;
cout << image.type() << endl;
//【2】按原始图的参数规格来创建效果图
Mat dstImage;
dstImage.create(image.rows, image.cols, image.type());
//【3】记录起始时间
double time0 = static_cast<double>(getTickCount());
//【4】调用颜色空间缩减函数
colorReduce(image, dstImage, 32);
//【5】计算运行时间并输出
time0 = ((double)getTickCount() - time0) / getTickFrequency();
cout << time0 << endl;
//【6】显示效果图
imshow("效果图", dstImage);
waitKey(0);
return 0;
}
【方法一】指针访问:C操作符[]
void colorReduce(Mat& inputImage, Mat& outputImage, int div){
outputImage = inputImage.clone(); // 复制实参到临时变量
int rowNum = outputImage.rows; // 图像的行数
int colNum = outputImage.cols * outputImage.channels(); // 图像的列数 * 通道数 = 每一行元素的个数
for (int i=0;i<rowNum;i++)
{
// 获取第i行的首地址
uchar* data = outputImage.ptr<uchar>(i);
for (int j=0;j<colNum;j++)
{
// 处理每个像素
data[j] = data[j]/div *div + div/2;
}
}
}
【方法二】迭代器iterator
void colorReduce(Mat& inputImage, Mat& outputImage, int div){
outputImage = inputImage.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 rowNum = outputImage.rows;
int colNum = outputImage.cols;
for (int i=0; i<rowNum;i++)
{
for (int j=0;j<colNum;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;
}
}
}
其中指针访问的速度最快。
(六)示例程序
配套的示例程序
【21】用指针访问像素
【22】用迭代器访问像素
【23】用动态地址计算配合at访问像素
【24】遍历图像像素的14种方法
二、ROI区域图像叠加&图像混合
(一)感兴趣区域:ROI
定义ROI区域的两种方法
1、使用表示矩形区域的Rect。它指定矩形的左上角坐标和矩形的长宽,以定义一个矩形区域。
Mat imgeROI;
imageROI = image(Rect(500,250,logo.cols,logo.rows));
2、指定感兴趣行或列的范围,Range是指从起始索引(不包括终止索引)的一连段连续序列。cRange可以用来定义Range。
imageROI = image(Range(250,250+logoImage.rows),Range(200,200+logoImage.cols));
示例代码
/*------------------------------------------------------
函数名:ROI_AddImage()
描述: 利用感兴趣区域ROI实现图像叠加(把图像叠加到ROI区域中,而不是俩张图的直接叠加)
-------------------------------------------------------*/
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
bool ROI_AddImage();
int main()
{
system("color 5E");
if (ROI_AddImage())
{
printf("运行成功");
}
waitKey(0);
return 0;
}
bool ROI_AddImage()
{
//【1】读入图像
Mat srcImage1 = imread("D:/lili/Desktop/jpg/opencv/1.jpg");
Mat logoImage = imread("D:/lili/Desktop/jpg/opencv/dotalogo.jpg");
if (!srcImage1.data)
{
printf("fuck, read the picture is wrong!!! \n");
return false;
}
if (!logoImage.data)
{
printf("fuck, read the picture is wrong!!! \n");
return false;
}
//【2】定义一个Mat类型并给其设定ROI区域
Mat imageROI = srcImage1(Rect(100,150,logoImage.cols,logoImage.rows));
//【3】加载掩膜
Mat mask = imread("D:/lili/Desktop/jpg/opencv/dotalogo.jpg",0);
//【4】将掩膜拷贝到ROI
logoImage.copyTo(imageROI,mask);
//【5】显示结果
namedWindow("1 利用ROI实现图像叠加示例窗口");
imshow("1 利用ROI实现图像叠加示例窗口",srcImage1);
return true;
}
运行效果
【拓展1】
image.copyTo(imageROI,mask);
作用是把mask和image重叠以后把mask中像素值为0(black)的点对应的image中的点变为透明,而保留其他点。(mask是掩膜)
(二)线性混合操作
(三)计算数组加权和:addWeighted()函数
void(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1);
1、参数1:InputArray类型的src1,表示需要加权的第一个数组,一般是一个Mat;
2、参数2:double类型的alpha,表示第一个数组的权重
3、参数3:InputArray类型的src2,表示需要加权的第二个数组,它需要和第一个数组拥有相同的尺寸和通道数;
4、参数4:double类型的beta,表示第二个数组的权重;
5、参数5:double类型的gamma,加到权重总和上的标量值(类似于偏置);
6、参数6:OutputArray类型的dst,输出的数组,它和输入的两个数组有相同的尺寸和通道数;
7、参数7:int类型的dtype,输出阵列的可选深度,默认值是-1。当两个输入数组有相同的深度时,设置为-1.
//addWeighted()函数的作用的矩阵表达式
dst = src1[I]*alpha + src2[I}*beta + gamma;
示例代码
//---------------------------------【LinearBlending()函数】-------------------------------------
// 函数名:LinearBlending()
// 描述:利用cv::addWeighted()函数实现图像线性混合
//--------------------------------------------------------------------------------------------
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
bool LinearBlending();
int main()
{
system("color 5E");
if (LinearBlending())
{
printf("运行成功");
}
waitKey(0);
return 0;
}
bool LinearBlending()
{
//【0】定义一些局部变量
double alphaValue = 0.7;
double betaValue;
Mat srcImage2, srcImage3, dstImage;
//【1】读取图像 ( 两幅图片需为同样的类型和尺寸 )
srcImage2= imread("D://lili/Desktop/jpg/pptjpg/frame/test.jpg");
srcImage3= imread("D://lili/Desktop/jpg/pptjpg/frame/kou.jpg");
if(!srcImage2.data ) { printf("读取srcImage2错误~! \n"); return false; }
if(!srcImage3.data ) { printf("读取srcImage3错误~! \n"); return false; }
//【2】做图像混合加权操作
betaValue= ( 1.0 - alphaValue );
addWeighted(srcImage2, alphaValue, srcImage3, betaValue, 0.0, dstImage);
//【3】创建并显示原图窗口
namedWindow("<2>线性混合示例窗口【原图】", 1);
imshow("<2>线性混合示例窗口【原图】", srcImage2 );
namedWindow("<3>线性混合示例窗口【效果图】", 1);
imshow("<3>线性混合示例窗口【效果图】", dstImage );
return true;
}
运行效果
(四)综合示例:初级图像混合
示例代码
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
//-----------------------------【全局函数声明部分】------------------------------
bool ROI_AddImage();
bool LinearBlending();
bool ROI_LinearBlending();
int main() {
system("color 5A");
if (ROI_AddImage() && LinearBlending() && ROI_LinearBlending())
cout << "运行成功!得出需要的图像了!:)" << endl;
waitKey(0);
return 0;
}
//----------------------【ROI_AddImage()函数】--------------------------
bool ROI_AddImage() {//利用感兴趣区域ROI实现图像叠加
//读入图像
Mat srcImage1 = imread("D://lili/Desktop/jpg/opencv/dota.jpg");
Mat logoImage = imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg");
if (!srcImage1.data) {
cout << "读取srcImage1失败!" << endl;
return false;
}
if (!logoImage.data) {
cout << "读取logoImage失败!" << endl;
return false;
}
//定义一个Mat类型并给其设定ROI区域
Mat imageROI = srcImage1(Rect(560, 240, logoImage.cols, logoImage.rows));
//加载掩膜(必须是灰度图)
Mat mask = imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg", 0);
//将掩膜复制到ROI区域
logoImage.copyTo(imageROI, mask);
//显示效果
namedWindow("<1>利用ROI实现图像叠加示例窗口");
imshow("<1>利用ROI实现图像叠加示例窗口", srcImage1);
return true;
}
//------------------------【LinearBlending()函数】----------------------------
bool LinearBlending() {//利用cv::addWeighted()函数实现图像线性混合
//定义一些局部变量
double alpha = 0.5;
double beta;
Mat srcImage2, srcImage3, dstImage;
//读取图像
srcImage2 = imread("D://lili/Desktop/jpg/pptjpg/frame/test.jpg");
srcImage3 = imread("D://lili/Desktop/jpg/pptjpg/frame/kou.jpg");
if (!srcImage2.data) {
cout << "读取srcImage2错误!" << endl;
return false;
}
if (!srcImage3.data) {
cout << "读取srcImage3错误!" << endl;
return false;
}
//进行图像混合加权操作
beta = (1.0 - alpha);
double gamma = 0;
addWeighted(srcImage2, alpha, srcImage3, beta, gamma, dstImage);
//创建并显示原图窗口
namedWindow("<2>线性混合示例窗口【原图】");
imshow("<2>线性混合示例窗口【原图】", srcImage2);
namedWindow("<3>线性混合示例窗口【效果图】");
imshow("<3>线性混合示例窗口【效果图】",dstImage);
return true;
}
//----------------------【ROI_LinearBlending()函数】线性混合直接输出到感兴趣区域---------------------------------
bool ROI_LinearBlending() {//指定区域线性图像混合
//读取图像
Mat srcImage4 = imread("D://lili/Desktop/jpg/opencv/dota.jpg", 1);//载入三通道图像
Mat logoImage = imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg");//默认flags=1,载入三通道图像
if (!srcImage4.data) {
cout << "读入srcImage4失败!" << endl;
return false;
}
if (!logoImage.data) {
cout << "读入logoImage失败!" << endl;
return false;
}
//定义一个Mat类型并给其设定ROI区域
Mat imageROI;
//imageROI = srcImage4(Rect(560, 240, logoImage.cols, logoImage.rows));
imageROI = srcImage4(Range(240, 240 + logoImage.rows), Range(560, 560 + logoImage.cols));//方法二
//注意:Range()方法是先给定上下范围,再给定左右范围!!
//定义一些局部变量
double alpha = 0.5, beta = 0.3, gamma = 0;
//将logo加到原图上
//将logo线性叠加到ROI区域并赋值给imageROI,而imageROI属于srcImage4,所以srcImage4为最终效果图
addWeighted(imageROI, alpha, logoImage, beta, gamma, imageROI);//注意参数一三六必须尺寸相同&通道数相同
//显示结果
namedWindow("<4>区域线性图像混合示例窗口");
imshow("<4>区域线性图像混合示例窗口", srcImage4);
return true;
}
运行效果
三、分离颜色通道、多通道图像混合
(一)通道分离:split()函数
C++: void split(const Mat& src, Mat*mvbegin);
C++: void split(InputArray m,OutputArrayOfArrays mv);
1、参数1:InputArray类型的m或者const Mat&类型的src,填我们需要进行分离的多通道数组。
2、参数2:OutputArrayOfArrays类型的mv,填函数的输出数组或者输出的vector容器。
示例代码
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void main(){
vector<Mat> channels;
Mat imageBlueChannel;
Mat imageGreenChannel;
Mat imageRedChannel;
Mat srcImage4 = imread("D://lili/Desktop/jpg/opencv/dota.jpg");
imshow("【原始图】", srcImage4);
//把一个3通道图像转换成3个单通道图像
split(srcImage4, channels);
imageBlueChannel = channels.at(0);
imageGreenChannel = channels.at(1);
imageRedChannel = channels.at(2);
//显示单通道图像
imshow("【BlueChannel】", imageBlueChannel);
imshow("【GreenChannel】", imageGreenChannel);
imshow("【RedChannel】", imageRedChannel);
waitKey();
}
运行效果
(二)通道合并:merge()函数
C++: void merge(const Mat* mv, size_tcount, OutputArray dst)
C++: void merge(InputArrayOfArrays mv,OutputArray dst)
1、参数1:mv,填需要被合并的输入矩阵或vector容器的阵列,这个mv参数中所有的矩阵必须有着一样的尺寸和深度。
2、参数2:count,当mv为一个空白的C数组时,代表输入矩阵的个数,这个参数显然必须大于1.
3、参数3:dst,即输出矩阵,和mv[0]拥有一样的尺寸和深度,并且通道的数量是矩阵阵列中的通道的总数。
示例代码
根据(一)中代码,将分离后的通道进行合并。
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace cv;
using namespace std;
void main(){
vector<Mat> channels;
Mat imageBlueChannel;
Mat imageGreenChannel;
Mat imageRedChannel;
Mat srcImage4 = imread("D://lili/Desktop/jpg/opencv/dota.jpg");
imshow("【原始图】", srcImage4);
//把一个3通道图像转换成3个单通道图像
split(srcImage4, channels);
imageBlueChannel = channels.at(0);
imageGreenChannel = channels.at(1);
imageRedChannel = channels.at(2);
//显示单通道图像
imshow("【BlueChannel】", imageBlueChannel);
imshow("【GreenChannel】", imageGreenChannel);
imshow("【RedChannel】", imageRedChannel);
Mat dstImage;
//合并
merge(channels,dstImage);
imshow("【合并效果图】", dstImage);
waitKey();
}
运行效果
(三)示例程序:多通道图像混合
示例代码
//-----------------------------------【程序说明】----------------------------------------------
// 程序名称::【OpenCV入门教程之四】分离颜色通道&多通道图像混合 配套源码
// VS2010版 OpenCV版本:2.4.8
// 2014年3月13 日 Create by 浅墨
// 图片素材出处:dota2原画 dota2logo
// 浅墨的微博:@浅墨_毛星云
//------------------------------------------------------------------------------------------------
//-----------------------------------【头文件包含部分】---------------------------------------
// 描述:包含程序所依赖的头文件
//----------------------------------------------------------------------------------------------
#include <cv.h>
#include <highgui.h>
#include <iostream>
//-----------------------------------【命名空间声明部分】---------------------------------------
// 描述:包含程序所使用的命名空间
//-----------------------------------------------------------------------------------------------
using namespace cv;
using namespace std;
//-----------------------------------【全局函数声明部分】--------------------------------------
// 描述:全局函数声明
//-----------------------------------------------------------------------------------------------
bool MultiChannelBlending();
//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
system("color5E");
if(MultiChannelBlending())
{
cout<<endl<<"获得混合值图像";
}
waitKey(0);
return 0;
}
//-----------------------------【MultiChannelBlending( )函数】--------------------------------
// 描述:多通道混合的实现函数
//-----------------------------------------------------------------------------------------------
bool MultiChannelBlending()
{
//【0】定义相关变量
Mat srcImage;
Mat logoImage;
vector<Mat>channels;
Mat imageBlueChannel;
//=================【蓝色通道部分】=================
// 描述:多通道混合-蓝色分量部分
//============================================
//【1】读入图片
logoImage=imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg",0);
srcImage=imread("D://lili/Desktop/jpg/opencv/dota.jpg");
if(!logoImage.data ) { printf("读取logoImage错误\n"); return false; }
if(!srcImage.data ) { printf("读取srcImage错误\n"); return false; }
//【2】把一个3通道图像转换成3个单通道图像
split(srcImage,channels);//分离色彩通道
//【3】将原图的蓝色通道引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
imageBlueChannel=channels.at(0);
//【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)));
//【5】将三个单通道重新合并成一个三通道
merge(channels,srcImage);
//【6】显示效果图
namedWindow("<1>游戏原画+logo蓝色通道");
imshow("<1>游戏原画+logo蓝色通道",srcImage);
//=================【绿色通道部分】=================
// 描述:多通道混合-绿色分量部分
//============================================
//【0】定义相关变量
Mat imageGreenChannel;
//【1】重新读入图片
logoImage=imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg",0);
srcImage=imread("D://lili/Desktop/jpg/opencv/dota.jpg");
if(!logoImage.data ) { printf("读取logoImage错误!\n"); return false; }
if(!srcImage.data ) { printf("读取srcImage错误!\n"); return false; }
//【2】将一个三通道图像转换成三个单通道图像
split(srcImage,channels);//分离色彩通道
//【3】将原图的绿色通道的引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
imageGreenChannel=channels.at(1);
//【4】将原图的绿色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageGreenChannel中
addWeighted(imageGreenChannel(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
logoImage,0.5,0.,imageGreenChannel(Rect(500,250,logoImage.cols,logoImage.rows)));
//【5】将三个独立的单通道重新合并成一个三通道
merge(channels,srcImage);
//【6】显示效果图
namedWindow("<2>游戏原画+logo绿色通道");
imshow("<2>游戏原画+logo绿色通道",srcImage);
//=================【红色通道部分】=================
// 描述:多通道混合-红色分量部分
//============================================
//【0】定义相关变量
Mat imageRedChannel;
//【1】重新读入图片
logoImage=imread("D://lili/Desktop/jpg/opencv/dotalogo.jpg",0);
srcImage=imread("D://lili/Desktop/jpg/opencv/dota.jpg");
if(!logoImage.data ) { printf("读取logoImage错误!\n"); return false; }
if(!srcImage.data ) { printf("读取srcImage错误!\n"); return false; }
//【2】将一个三通道图像转换成三个单通道图像
split(srcImage,channels);//分离色彩通道
//【3】将原图的红色通道引用返回给imageBlueChannel,注意是引用,相当于两者等价,修改其中一个另一个跟着变
imageRedChannel=channels.at(2);
//【4】将原图的红色通道的(500,250)坐标处右下方的一块区域和logo图进行加权操作,将得到的混合结果存到imageRedChannel中
addWeighted(imageRedChannel(Rect(500,250,logoImage.cols,logoImage.rows)),1.0,
logoImage,0.5,0.,imageRedChannel(Rect(500,250,logoImage.cols,logoImage.rows)));
//【5】将三个独立的单通道重新合并成一个三通道
merge(channels,srcImage);
//【6】显示效果图
namedWindow("<3>游戏原画+logo红色通道");
imshow("<3>游戏原画+logo红色通道",srcImage);
return true;
}
运行效果
四、图像对比度、亮度值调整
(一)理论依据
在opencv中控制图像的亮度和对比度的理论公式:
g(i,j)=a*f(i,j)+b
- i和j表示像素位于第i行和第j列
- f(x):表示源图像像素
- g(x):表示输出图像像素
- a:(满足a>0)被称为增益,常常被用来控制图像的对比度
- b:通常被称为偏置(bias),常常被用来控制图像的亮度
(二)访问图片中的像素
访问像素的代码片段,执行g(i,j)=a*f(i,j)+b
运算
//三个for循环,执行运算new_image(i,j) = a*image(i,j) + b
for(int y = 0;y < image.rows;y++){
for(int x = 0;x < image.cols;x++){
for(int c = 0;c < 3;c++){
new_image.at<Vec3b>(y,x)[c] = saturate_cast<uchar>((g_nContrastValue*0.01)*(image.at<Vec3b>(y,x)[c])+g_nBrightValue);
}
}
}
-
y是像素所在行,x是像素所在的列,c是R、G、B(对应0、1、2)其中之一。
-
为了访问图像的每一个像素,我们使用这样的语法:
image.at<Vec3b>(y,x)[c]
其中,y是像素所在的行, x是像素所在的列, c是R、G、B(对应0、1、2)其中之一。 -
因为我们的运算结果可能超出像素取值范围(溢出),还可能是非整数(如果是浮点数的话),所以我们要用
saturate_cast
对结果进行转换,以确保它为有效值。 -
这里的a也就是对比度,一般为了观察的效果,取值为0.0到3.0的浮点值,但是我们的轨迹条一般取值都会整数,所以在这里我们可以,将其代表对比度值的nContrastValue参数设为0到300之间的整型,在最后的式子中乘以一个0.01,这样就可以完成轨迹条中300个不同取值的变化。所以在式子中,我们会看到
saturate_cast<uchar>( (g_nContrastValue*0.01)*(image.at<Vec3b>(y,x)[c] ) + g_nBrightValue )
中的g_nContrastValue*0.01
。
(三)示例程序:图像对比度、亮度值调整
示例代码
//-----------------------------------【程序说明】----------------------------------------------
// 程序名称::【OpenCV入门教程之四】 创建Trackbar&图像对比度、亮度值调整 配套博文源码
//------------------------------------------------------------------------------------------------
#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;
//-----------------------------------【main( )函数】--------------------------------------------
// 描述:控制台应用程序的入口函数,我们的程序从这里开始
//-----------------------------------------------------------------------------------------------
int main( )
{
//改变控制台前景色和背景色
system("color5F");
//读入用户提供的图像
g_srcImage= imread( "D://lili/Desktop/jpg/opencv/1.jpg");
if(!g_srcImage.data ) { printf("读取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"
<<"\n\n\t\t\t\t";
//按下“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)
【?待学习】不知道傅里叶变换到底有什么作用
(一)离散傅里叶变换的原理
简单来说,对一张图像进行离散傅里叶变换就是将它分解成正弦和余弦两部分,也就是将图像从空间域转换到频域。这一转换的理论基础是:任一函数都可以表示成无数个正弦和余弦函数的和的形式。
二维图像的傅里叶变化用数学公式可以表示为:
式中的f是空间域值,F是频域值。
变换之后的频域值是复数,因此,显示傅里叶变换之后的结果需要使用实数图像(real image)加虚数图像(complex image),或者幅度图像(magitude image)加相位图像(phase image)形式。
(二)dft()函数详解
dft函数的作用是对一维或二维浮点数数组进行正向或反向离散傅里叶变换
C++: void dft(InputArray src, OutputArray dst, int flags=0, int nonzeroRows=0);
第一个参数,InputArray类型的src。输入矩阵,可以是实数或虚数。
第二个参数,OutputArray类型的dst,函数调用后的运算结果在这里,其尺寸和类型取决于标识符,也就是第三个参数flags
第三个参数,int类型的flags,转换的标识符,有默认值0,取值可以为下表。
第四个参数,int类型的nonzeroRows,默认值是0。
一个用dft函数计算两个二维实矩阵卷积的示例核心片段
(没看懂)
void consolveDFT(InputArray A, InputArray B, OutputArray C)
{
//【1】初始化输出矩阵
C.create(abs(A.rows - B.rows) + 1, abs(A.cols - B.cols) + 1, A.type());
Size dftSize;
//【2】计算DFT变换尺寸
dftSize.width = getOptimalDFTSize(A.cols + B.cols - 1);
dftSize.height = getOptimalDFTSize(A.rows + B.rows - 1);
//【3】分配临时缓冲区并初始化置零
Mat tempA(dftSize, A.type(), Scalar::all(0));
Mat tempB(dftSize, B.type(), Scalar::all(0));
//【4】分别复制A和B到tempA和tempB的左上角
Mat roiA(tempA, Rect(0, 0, A.cols, A.rows));
A.copyTo(roiA);
Mat roiB(tempB, Rect(0, 0, B.cols, B.rows));
B.copyTo(roiB);
//【5】就地操作(in_place),进行快速傅里叶变换,并将nonzeroRows参数置为非零,以进行更快速的处理
dft(tempA, tempA, 0, A.rows);
dft(tempB, tempB, 0, B.rows);
//【6】将得到的频谱相乘,结果存放于tempA中
mulSpectrums(tempA, tempB, tempA);//计算两个傅里叶频谱的每个元素的乘法
//【7】将结果变换为频域,且尽管结果行(result rows)都为非零,我们只需要其中的C.rows的第一行,所以采用nonzeroRows==C.rows
dft(tempA, tempA, DFT_INVERSE + DFT_SCALE, C.rows);
//【8】将结果复制到C中
tempA(Rect(0, 0, C.cols, C.rows)).copyTo(C);
//所有的临时缓冲区将被自动释放,所以无须收尾操作
}
(三)返回DFT最优尺寸大小:getOptimalDFTSize()函数
int getOptimalDFTSize(int vecsize)
vecsize:向量尺寸,即图片是我rows、cols
(四)扩充图像边界:copyMakeBorder()函数
作用:扩充图像边界
void copyMakeBorder(InputArray src, OutputArray dst, int top, int bottom, int left, int right, int borderType, const Scalar& value=Scalar() );
- InputArray src: 输入图像
- OutputArray dst: 输出图像,与src图像有相同的类型,其尺寸应为Size(src.cols+left+right, src.rows+top+bottom)
- int类型的top、bottom、left、right: 在图像的四个方向上扩充像素的值
- int borderType: 边界类型,由borderInterpolate()来定义,常见的取值为BORDER_CONSTANT
- Scalar:默认值为0,如果边界类型为BORDER_CONSTANT则表示为边界值
(五)计算二维矢量的幅值:magnitude()函数
void magnitude(InputArray x, InputArray y, OutputArray magnitude);
参数解释:
- InputArray x: 浮点型数组的x坐标矢量,也就是实部
- InputArray y: 浮点型数组的y坐标矢量,必须和x尺寸相同
- OutputArray magnitude: 与x类型和尺寸相同的输出数组
其计算公式如下:
(六)计算自然对数:log()函数
void log(InputArray src,OutputArray dst)
- 第一个参数:输入图像
- 第二个参数:得到的对数值
原理:
(七)矩阵归一化:normalize()函数
作用:将矩阵归一化
void normalize(InputArray src, OutputArray dst, double alpha=1, double beta=0, int norm_type=NORM_L2, int dtype=-1, InputArray mask=noArray() )
1、参数1:InputArray src: 输入图像
2、参数2:OutputArray dst: 输出图像,尺寸大小和src相同
3、参数3:double类型的alpha。归一化后的最大值,有默认值1
4、参数4:double类型的beta。归一化后的最小值,有默认值0
5、参数5:int norm_type = NORM_L2: 归一化的类型,主要有
- NORM_INF: 归一化数组的C-范数(绝对值的最大值)
- NORM_L1: 归一化数组的L1-范数(绝对值的和)
- NORM_L2: 归一化数组的L2-范数(欧几里得)
- NORM_MINMAX: 数组的数值被平移或缩放到一个指定的范围,线性归一化,一般较常用。
6、参数6:int dtype = -1: 当该参数为负数时,输出数组的类型与输入数组的类型相同,否则输出数组与输入数组只是通道数相同,而depth = CV_MAT_DEPTH(dtype)
7、参数7:InputArray mask = noArray(): 操作掩膜版,用于指示函数是否仅仅对指定的元素进行操作。
(八)示例程序:离散傅里叶变换
示例代码
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include<iostream>
using namespace std;
using namespace cv;
int main()
{
//[1]以灰度图读取原始图像并显示
Mat srcImage = imread("D://lili/Desktop/jpg/opencv/dota.jpg", 0);
if(!srcImage.data) {
printf("获取图像失败\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]将复数转换为幅度值
split(complexI, planes); //将多通道数组分离成几个单通道数组
// planes[0] = Re(DFT(I));
// planes[1] = Im(DFT(I));
magnitude(planes[0], planes[1], planes[0]);
Mat magnitudeImage = planes[0];
//[6]进行对数尺度缩放
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)); // 右下角
Mat q2(magnitudeImage, Rect(0,cy, cx, cy));
Mat q3(magnitudeImage, Rect(cx, cy, cx, cy));
//交换象限(左上角和右下角交换)
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);//opencv2
normalize(magnitudeImage, magnitudeImage, 0, 1, NORM_MINMAX);//opencv3
imshow("频谱图", magnitudeImage);
// waitKey(0);
while(char (waitKey(1)) != 'q'){}
return 0;
}
运行效果
六、输入输出XML和YAML文件
(一)XML和YAML文件简介
XML:可扩展标识语言。是一种语义/结构化语言,它描述了文档的结构和语义。
YAML:一个可读性高,用来表达资料序列的格式。
总之,YAML试图用一种比XML更敏捷的方式,来完成XML所完成的任务。
(二)FileStorage类操作文件的使用引导
写入或读取数据到XML或YAML文件中的过程:
(1)实例化一个FileStorage类的对象,用默认带参数的构造函數完成初始化,或者用FileStorage::open()成员函数辅助初始化。
(2)使用流操作符<<进行文件写入操作,或者>>进行文件读取操作,类似c++中的文件输入输出流。
(3)使用FileStorage::release()函数析构掉FileStorage类对象,同时关闭文件。
下面分别对这三个步骤进行实例讲解。
〖第一步〗XML、YAML文件的打开
(1)准备文件写操作
FileStorege是OpenCV中XML和YAML文件的存储类,封装了所有相关的信息。它是OpenCV从文件中读数据或向文件中写数据时必须要使用的一个类。
此类的构造函数为FileStorage::FileStorage,有两个重载,如下。
FiIeStorage::FileStorage
FiIeStorage::FileStorage(const string & source,int flags,const string& encoding=string())
构造函数在实际使用中,方法一般有两种。
- 对于第二种带参数的构造函数,进行写操作范例如下。
FileStorage fs;
fs.open("abc.xml",FileStorage::WRITE);
- 对于第一种不带参数的构造函数,可以使用其成员函数FileStorage::open进行数据的与操作,范例如下。
FileStorage fs;
fs.open("abc.xml",FileStorage::WRITE);
(2)准备文件读操作
上面讲到的都是以FileStorage::WRITE为标识符的写操作,而读操作,采用FileStronge::READ标识符即可,相关示例代码如下。
- 第一种方式
FileStorage fs("abc.xml",FileStorage::READ);
- 第二种方式
FileStorage fs;
fs.open("abc.xml",FileStroage::READ);
〖第二步〗进行文件的读取操作
(1)文本和数字的输入和输出
定义好FileStorage类对象之后,写入文件可以使用“<<”运算符,例如:
fs<<"iterationNr"<<100;
而读取文件,使用“>>”运算符,例如:
int itNr;
fs["iterationNr"]>>itNr;
itNr=(int)fs["iterationNr"];
(2)OpenCV数据结构的输入和输出
关于OpenCV数据结构的输入和输出,和基本的C++形式相同,范例如下。
//数据结构的初始化
Mat R = Mat_<uchar>::eye(3, 3);
Mat T = Mat_<double>::zeros(3, 1);
//向Mat中写入数据
fs << "R" << R;
fs << "T" << T;
//从Mat中读出数据
fs["R"] >> R;
fs["T"] >> T;
〖第三步〗vector(arrays)和maps的输入和输出
对于vector结构的输入和输出,要注意在第一个元素前加上"[",在最后一个元素前加上“]”。例如:
//开始读入string文本序列
fs << "strings" << "[";
fs << "image1.jpg" << "Awesomeness" << "baboon.jpg";
fs << "]";
而对于map结构的操作,使用的符号是“{”和“}”,例如:
fs << "Mapping";
fs << "{" << "One" << 1;
fs << "Two" << 2 << "}";
读取这些结构的时候,会用到FileNode和Fileodelterator数据结构。对Filestorage类的“[”、“]”操作符会返回FileNode数据类型;对于一连串的node,可以使用FileNodeIterator结构,例如:
FileNode n = fs["strings"];//读取字符串序列以得到节点
if (n.type()!=FileNode::SEQ)
{
cerr << "发生错误!字符串不是一个序列!" << endl;
return 1;
}
//遍历节点
FileNodeIterator it = n.begin(), it_end = n.end();
for (; it != it_end; it++)
{
cout << (string)*it << endl;
}
〖第四步〗关闭文件
需要注意的是,文件关闭操作会在FileStroage类销毁的同时自动进行,但我们也可显式调用其析构函数FileStroage::release()实现。FileStroage::release()函数会析构掉FileStroage类对象,同时关闭文件。
调用过程非常简单,如下。
fs.release();
(三)示例程序:XML和YAML文件的写入
示例代码
#include<opencv2\opencv.hpp>
#include<time.h>
using namespace cv;
int main(){
//初始化
FileStorage fs("D:/lili/Desktop/test.yaml", FileStorage::WRITE);
//开始文件写入
fs << "frameCount" << 5;
time_t rawtime;
time(&rawtime);
fs << "calibrationDate" << asctime(localtime(&rawtime));
Mat cameraMatrix = (Mat_<double>(3, 3) << 100, 0, 320, 0, 1000, 240, 0, 0, 1);
Mat distCoeffs = (Mat_<double>(5, 1) << 0.1, 0.01, -0.001, 0, 0);
fs << "cameraMatrix" << cameraMatrix << "distCoeffs" << distCoeffs;
fs << "features" << "[";
for (int i = 0; i < 3; i++)
{
int x = rand() % 640;
int y = rand() % 480;
uchar lbp = rand() % 256;
fs << "{:" << "x" << x << "y" << y << "lbp" << "[:";
for (int j = 0; j < 8; j++)
{
fs << ((lbp >> j) & 1);
}
fs << "]" << "}";
}
fs << "]";
fs.release();
printf("文件读写完毕!");
getchar();
return 0;
}
运行效果
(四)示例程序:XML和YAML文件的读取
示例代码
#include<opencv2\opencv.hpp>
#include<time.h>
using namespace cv;
using namespace std;
int main(){
//改变控制台字体颜色
system("color 6F");
//初始化
FileStorage fs2("D:/lili/Desktop/test.yaml", FileStorage::READ);
//第一种方法,对FileNode进行操作
int frameCount = (int)fs2["frameCount"];
std::string date;
//第二种方法,使用FileNode运算符
fs2["calibrationDate"] >> date;
Mat cameraMatrix2, distCoeffs2;
fs2["cameraMatrix"] >> cameraMatrix2;
fs2["distCoeffs"] >> distCoeffs2;
cout << "frameCount:" << frameCount << endl
<< "calibration date:" << date << endl
<< "camera matrix:" << cameraMatrix2 << endl
<< "dist coeffs2" << distCoeffs2 << endl;
FileNode features = fs2["features"];
FileNodeIterator it = features.begin(), it_end = features.end();
int idx = 0;
std::vector<uchar> lbpval;
//使用FileNodeIterator遍历序列
for (; it != it_end; it++)
{
cout << "feature #" << idx << ":";
cout << "x=" << (int)(*it)["x"] << ",y=" << (int)(*it)["y"] << ",lbp:(";
(*it)["lbp"] >> lbpval;
for (int i = 0; i < (int)lbpval.size(); i++)
{
cout << " " << (int)lbpval[i];
}
cout << ")" << endl;
}
fs2.release();
printf("\n文件读取完毕,请输入任意键结束程序!\n");
getchar();
return 0;
}
运行效果