一、环境搭建

1、opencv4下载注意改名

可以关注opencv学堂公众号进行opencv4下载,但是注意下载下来的是zip压缩包格式要将其修改为.exe格式再运行就会生成include、build的。注意这里配置好的是x64位的,因此后面需配置release的64位的配置管理器下运行,以及相关路径配置。

opencv4书籍推荐 opencv4入门_opencv4书籍推荐

2、配置管理器路径包含

注意;配置的时候要注意自己dll所支持的是什么配置,当前是release的64位

2.1、头文件包含

D:\opencv4.4\opencv\build\include
 D:\opencv4.4\opencv\build\include\opencv2

2.2、lib路径包含

D:\opencv4.4\opencv\build\x64\vc14\lib
注意其中还有一个vc15  这个是给vs2019使用的,当前是vs2015则配置vc14即可

2.3、配置包含lib

opencv_world440.lib
注意vc14\lib下面还有一个叫opencv_world440d.lib多了一个d这个是再debug下使用的库,当前在release则使用不需要d的。

2.5、运行找不到opencv_world440.dll

opencv4书籍推荐 opencv4入门_opencv_02

将dll负责过来就可以运行正确了。

其实应该也是可以把当前bin目录配置当环境变量中,编译器运行时应该也是可以找到的(注意要开闭vs重新打开哈),可以尝试。但是工作中需要exe打包的则配置到环境变量这种方法不合适。

2.6、运行结果及源码

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_03

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace cv;//opencv的命名空间
using namespace std;

int main(int argc, char** argv)
{
	Mat src = imread("C:/Users/20531/Desktop/1/9.png");//读取图片存到mat  opencv中一切结识mat
	imshow("test1", src);//第一个参数是窗口的名字,如果后面接着又使用改名字则当前的图像会被冲掉
	waitKey(0);//等待操作  传入非0就是等待多少毫秒
	destroyAllWindows();//销毁windows所有窗口
	return 0;
}

二、读取和显示图片

1、图片读去显示imread、imshow

CV_EXPORTS_W Mat imread( const String& filename, int flags = IMREAD_COLOR );
读取一张图片、
第一个参数filename文件名路径、
第二个参数有默认值可以不写,
最后返回的是是将图片读取存到mat矩阵(二维数组)中,
注意opencv中一切皆是mat。
第二个参数是设置他的读取模式cv::ImreadModes的值
注意;
如果imread读取失败的话返回的mat则是NULL的,则一般操作之后是需要判断的,再进行其他操作。Mat::data==NULL,也可以使用Mat.empty()判断。
还有一些注意可以参考api定义介绍,如
—该功能根据图像的内容来确定图像的类型,而不是根据文件的扩展名。
-在彩色图像的情况下,解码图像将有通道存储在**B G R**顺序。
-当使用IMREAD_GRAYSCALE时,如果可用,将使用编解码器的内部灰度转换。结果可能与cvtColor()的输出不同

有图像的读取就有配套的显示图片的API的

CV_EXPORTS_W void imshow(const String& winname, InputArray mat);
参数;
winname表示窗口名称,注意使用相同的窗口名称会被冲掉之前的imshow的。
mat表示要显示图像的mat矩阵。
注意该函数是一显示有其他操作就消失的,因此一般显示图片会结合waitKey()这个api的.
waitKey传入0表示等待事件再继续,传入非0则表示等待传入数值的多少毫秒。

注意imshow只支持8位的或者浮点型的进行显示,有的其他图像类型是不显示的。但是imread的肯定是符合的。

2、涉及图片显示不全namedWindow设定WINDOW_FREERATIO

其实imshow也是会创建默认窗口,只是他设置的模式是WINDOW_AUTOSIZE 根据图片大小设定的,无法调整的,因此窗口可能无法展示完全。因此需要重新创建窗口并设置其他格式再由imshow显示上去。注意是根据窗口名匹配的。

namedWindow("输入窗口", WINDOW_FREERATIO);//创建一个窗口 并且设置为自由设置大小模式
imshow("输入窗口", src);//但是注意这个时候imshow就要指定显示到对应窗口上

3、如何以灰度图形式加载imread设置IMREAD_GRAYSCALE

Mat src = imread("C:/Users/20531/Desktop/1/9.png", IMREAD_GRAYSCALE);//显示灰度图 是编解码器内转换的
注意第二个参数还可以设置其他 
如有透明通道的需要保存则应该设置为IMREAD_UNCHANGED、不进行改变
如图片深度不是8位表示了则使用IMREAD_ANYDEPTH、任意深度
如图片是其他颜色格式的时候不是整数则使用IMREAD_ANYCOLOR、任意颜色

额外话;在opencv中自定义了很多枚举类型 如果图像深度bgr8位就是深度24.在读取深度的时候可能是返回枚举值为1来表示的

#include<opencv2/opencv.hpp>
#include <iostream>

using namespace cv;//opencv的命名空间
using namespace std;

int main(int argc, char** argv)
{
	Mat src = imread("C:/Users/20531/Desktop/1/9.png",IMREAD_GRAYSCALE);//显示灰度图 是编解码器内转换的
	if (src.empty())
	{
		cout << "imread faile" << endl;
		getchar();
		return -1;
	}
	cout << "Mat src  deep = " << src.depth() << endl;
	//Mat src = imread("C:/Users/20531/Desktop/1/9.png");  默认IMREAD_COLOR彩色的,3 channel BGR
	//其实是存在默认的窗口,只是他设置的模式是WINDOW_AUTOSIZE  根据图片大小设定的,因此窗口可能无法展示完全
	namedWindow("输入窗口", WINDOW_FREERATIO);//创建一个窗口 并且设置为自由设置大小模式
	imshow("输入窗口", src);//注意imshow的时候就要指定显示到对应窗口上
	waitKey(0);
	destroyAllWindows();
	return 0;
}

三、色彩转换和图像保存

1、色彩空间转换cvtColor、图像保存imwrite

opencv中主要是有三个大的色彩空间彩色BGR、灰度gray、还有hsv。 BRG是每位都是0-255有时候还要加入A透明通道 HSV的每位 H:0-180,S,V都是0-255.并且HS是表示颜色,V表示亮度通道。 因此有时候要对RGB图像做亮度处理则需要转换位HSV格式对V做处理再转换RGB格式。

CV_EXPORTS_W void cvtColor( InputArray src, OutputArray dst, int code, int dstCn = 0 );

opencv4书籍推荐 opencv4入门_opencv_04

四、图像对象Mat的创建与赋值

在opencv中图像都是存储在mat这个矩阵对象中的,因为图像以数据形式表示都是有一定规则的,而opencv中就是有mat来存放表示。并且mat有很多属性及相关操作,因此对mat对象的理解十分重要。

读取图像成mat我们怎么操作它? mat本身我们怎么去访问它的每个像素点? 怎么创建一个mat?多种方法

1、mat的基本结构

opencv当中mat分为两个部分,一个是head,一个是数据部分。 head;存放图像的属性,如大小、宽高,字节类型,通道数。 数据部分;就是我们所说的像素值的集合,有坐标有对应像素值。

注意;
mat进行赋值操作的时候数据区域的内存是没有变化的,只是进行指针的赋值,
但是在进行克隆或拷贝copyto方法的时候才会对数据区域的内存进行复制操作。
Mat m1 = src.clone();//克隆
Mat m2;src.copyto(m2);//拷贝
Mat m3 = src;//赋值,只有这个操作数据区域没有重新申请内存而是指针赋值

2、数据区的存放

opencv4书籍推荐 opencv4入门_赋值_05

3、创建图片

两个常见的初始化函数Mat::zeros、Mat::ones;注意ones只操作每个像素的第一个通道值。如果想修改像素的其他通道就要使用Scalar函数。

//创建空白mat  CV_8UC1表示8位 无符号类型 channel通道为1
Mat mEmpty = Mat::zeros(Size(8,8),CV_8UC1);//创建大小为8x8的单通道的空mat
//注意使用ones初始化为1的时候是只把每个像素的第一个通道初始化为1,其他还是0
Mat mOnes = Mat::ones(Size(8, 8), CV_8UC3);
//直接赋值
mEmptyC3 = 127;//这种也是只修改每个像素的第一个通道值
//如果想修改像素的其他通道就要使用Scalar
mEmptyC3 = Scalar(127, 127, 127);

4、基本操作源代码

void QuickDemo::mat_creat_Demo(Mat &image)
{
	Mat mClone;
	Mat mCopy;
	mClone = image.clone();//克隆
	image.copyTo(mCopy);//复制

	//创建空白mat  CV_8UC1表示8位 无符号类型 channel通道为1
	Mat mEmpty = Mat::zeros(Size(8,8),CV_8UC1);//创建大小为8x8的单通道的空mat
	//注意使用ones初始化为1的时候是只把每个像素的第一个通道初始化为1,其他还是0
	Mat mOnes = Mat::ones(Size(8, 8), CV_8UC3);
	std::cout << "ones ;Size(8,8)  CV_8UC3:" << mOnes << std::endl;
	std::cout << "Size(8,8)  CV_8UC1:" <<  mEmpty << std::endl;
	std::cout << "width=" << mEmpty.cols << ",height=" << mEmpty.rows << ",channele=" << mEmpty.channels() << std::endl;
	std::cout << "====================" << std::endl;
	Mat mEmptyC3 = Mat::zeros(Size(8, 8), CV_8UC3);//创建大小为8x8的3通道的空mat
	std::cout << "Size(8,8)  CV_8UC3:" << mEmptyC3 << std::endl;
	std::cout << "width=" << mEmptyC3.cols << ",height=" << mEmptyC3.rows << ",channele=" << mEmptyC3.channels() << std::endl;

	//赋值操作
	mEmptyC3 = 127;//这种赋值方法也同样只是会对每个像素的第一个通道赋值为127其他的不变
	std::cout << "mEmptyC3 = 127;:" << mEmptyC3 << std::endl;

	//创建底色画布的
	mEmptyC3 = Scalar(127, 127, 127);//如果想要操作每个通道则要使用Scalar,
	std::cout << "mEmptyC3 = Scalar(127, 127, 127);:" << mEmptyC3 << std::endl;

	imshow("创建窗口", mEmptyC3);//注意imshow支持8位或浮点数类型 有限制的

	//验证赋值操作 其实数据区复制,只是指针赋值,因此=操作会改变原来mat
	//但是clone和copyto是产生新的数据区,不会影响原来的。
	Mat mEqualOpr = mEmptyC3;
	mEqualOpr = Scalar(0, 255, 255);
	imshow("创建窗口1", mEmptyC3);
	
	return;
}

五、图像像素的遍历读写

在opencv当中一切图像皆mat。 遍历一张图片mat的每个像素点的值并对其进行读写操作。

1、数组方式访问

使用at方法进行访问像素点,根据at<参数类型>来进行对像素数据的读取,决定读取几个如uchar适合单通道会读取一个数据、Vec3b适合三通道会读取三个数据。

2、指针方式访问

使用ptr方法进行访问,这里就直接指定类型,因为指针是按字节访问的即可,不需要读取多个。

//数组访问方式
void QuickDemo::pixel_visit_demo(Mat &image)
{
	int w = image.cols;
	int h = image.rows;
	int channel = image.channels();
	for (int row = 0; row < h; row++)
	{
		for (int  cols = 0; cols < w; cols++)
		{
			if (channel == 1)//单通道 灰度图
			{
				int p = image.at<uchar>(row, cols);//读操作 uchar无符号字节类型 转换为int
				image.at<uchar>(row, cols) = 255 - p;//保证数据在255之间,数据不越出。
			}
			else if (channel == 3)//三通道
			{
				Vec3b bgr = image.at<Vec3b>(row, cols);//有Vec3b这样的数据类型 一次读取三个值,重定义Vec<uchar, 3>
				//写
				image.at<Vec3b>(row, cols)[0] = 255 - bgr[0];
				image.at<Vec3b>(row, cols)[1] = 255 - bgr[1];
				image.at<Vec3b>(row, cols)[2] = 255 - bgr[2];
			}
		}
	}
	imshow("像素读写显示",image);
}
//指针访问方式
for (int row = 0; row < h; row++)
{
	uchar* current_row = image.ptr<uchar>(row);
	for (int cols = 0; cols < w; cols++)
	{
		if (channel == 1)//单通道 灰度图
		{
			*current_row++ = 255 - *current_row;//保证数据在255之间,数据不越出。
		}
		else if (channel == 3)//三通道
		{
			//每次访问一个字节,并且进行++移动
			*(current_row++) = 255 - *current_row;
			*(current_row++) = 255 - *current_row;
			*(current_row++) = 255 - *current_row;
		}
	}
}

opencv4书籍推荐 opencv4入门_opencv_06

六、图像像素的算术操作

就是mat支持所有的加减乘除的所有算术操作,都可以进行实践。 并且同时opencv其实自己也提供了相对应的函数接口如add、subract,divide、multiply。 可以和Scalar标量本身也可以和Mat本身进行加减乘除。

Mat dst;
	Scalar表示标量
	dst = image + Scalar(50, 50, 50);//也可以使用opencv自带api;add()方法
	imshow("加法操作", dst);
	dst = image - Scalar(50, 50, 50);
	imshow("减法操作", dst);
	dst = image / Scalar(4, 4, 4);//也可以使用divide()方法
	imshow("除法操作", dst);
	/*dst = image * Scalar(2, 2, 2);直接乘法会存在数据溢出报错,opencv中提供了专门的乘法函数
	imshow("乘法法操作", dst);*/
	//opencv提供的乘法函数 他会将超出范围的数据自动截取 
	//使用saturate_cast<uchar>函数 小于0则是0 大于255则是255

	//注意需创建大小与image相同的mat进行乘法  可以这样 直接Scalar也可以
	//Mat m = Mat::zeros(image.size(), image.type());
	//m = Scalar(2, 2, 2);
	multiply(image, Scalar(2, 2, 2), dst);
	imshow("乘法操作", dst);

	//自定义加法操作
	Mat m = Mat::zeros(image.size(), image.type());
	dst = Mat::zeros(image.size(), image.type());
	m = Scalar(50, 50, 50);
	int w = image.cols;
	int h = image.rows;
	int channel = image.channels();
	for (int row = 0; row < h; row++)
	{
		uchar* current_row = image.ptr<uchar>(row);
		uchar* current_m = m.ptr<uchar>(row);
		uchar* current_dst = dst.ptr<uchar>(row);
		for (int cols = 0; cols < w; cols++)
		{
			//每次访问一个字节,并且进行++移动
			//关键 saturate_cast  保证数据类型在范围内
			*(current_dst++) = saturate_cast<uchar>(*current_row++ + *current_m++);
			*(current_dst++) = saturate_cast<uchar>(*current_row++ + *current_m++);
			*(current_dst++) = saturate_cast<uchar>(*current_row++ + *current_m++);
		}
	}
	imshow("自定义加法操作", dst);

	return;

对应效果图

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_07

七、TrackBar滚动条操作实现亮度和对比度比较createTrackbar

创建TrackBar滚动条的
CV_EXPORTS int createTrackbar(const String& trackbarname, const String& winname,
                              int* value, int count,
                              TrackbarCallback onChange = 0,
                              void* userdata = 0);
const String& trackbarname,进度条名字
const String& winname,需绑定到的目标窗口名
int* value, int count,当前值 最大值
TrackbarCallback onChange = 0回调函数,进度条触发
void* userdata = 0  传参可以任何类型但是要注意类型转换


CV_EXPORTS_W void addWeighted(InputArray src1, double alpha, InputArray src2,
                              double beta, double gamma, OutputArray dst, int dtype = -1);
用于两张图片的混合,第一张图占比多少,第二张占比多少,
 double alpha,图片1权重
 double beta  图片2权重
 double gamma  图片1和图片2求和后偏移量
 
最后结果输出的公示就是
 dst = src1*alpha + src2*beta + gamma;

源代码

//这个int b就是亮度调整传过来的值
//void *userdata  任何类型的指针 只是要类型转换
static void on_track(int b, void *userdata)
{
	Mat image = *((Mat *)userdata);
	Mat mTemp = Mat::zeros(image.size(), image.type());
	Mat dst = Mat::zeros(image.size(), image.type());
	addWeighted(image, 1.0, mTemp, 0, b, dst);//图像混合 根据各种权重
	imshow("亮度调整", dst);
}
static void on_constrast(int b, void *userdata)
{
	Mat image = *((Mat *)userdata);
	Mat mTemp = Mat::zeros(image.size(), image.type());
	Mat dst = Mat::zeros(image.size(), image.type());
	double constrat = b / 100.0;
	addWeighted(image, constrat, mTemp, 0.0, 0, dst);
	imshow("亮度调整", dst);
}
void QuickDemo::tracking_bar_demo(Mat &image)
{
	int lightness = 50;//亮度值
	namedWindow("亮度调整", WINDOW_AUTOSIZE);
	int max_value = 100;
	int constract_value = 100;
	//on_track  回调函数
	createTrackbar("Value Bar:", "亮度调整", &lightness, max_value, on_track, (void *)(&image));
	createTrackbar("constrat Bar:", "亮度调整", &constract_value, 200, on_constrast, (void *)(&image));
	//首先要调用一次,否则刚开始是没有图像的,要点击才有,第二个参数没有就传0
	on_track(50, &image);
	return;
}

八、键盘响应waitKey()

int c = waitKey(100);//Waits for a pressed key. 等待100ms //永远要记得一点在做视频分析的时候waitKey一定要传入1、waitKey(1)

void QuickDemo::key_demo(Mat &image)
{
	Mat dst = Mat::zeros(image.size(), image.type());
	while (true)
	{
		int c = waitKey(100);//Waits for a pressed key. 等待100ms
		//std::cout << c << std::endl;  暂时先测试按下什么按键在opencv中对应的值
		if (c == 27)//esc 退出27
		{
			break;
		}
		else if (c == 49)//点击图片按数字1进行灰度转换
		{
			std::cout << "you enter key #1" << std::endl;
			cvtColor(image, dst, COLOR_BGR2GRAY);
		}
		else if (c == 50)//点击图片按数字2进行hsv转换
		{
			std::cout << "you enter key #2" << std::endl;
			cvtColor(image, dst, COLOR_BGR2HSV);
		}
		else if (c == 51)//点击图片按数字3进行亮度增加转换
		{
			std::cout << "you enter key #3" << std::endl;
			add(image, Scalar(50, 50, 50), dst);
		}
		imshow("显示操作结果图片", dst);
	}

	return;
}

九、opencv自带的颜色表applyColorMap;查找表LUT

Look Up Table(LUT) 查找表

查找表在我们的图像基础或简单的颜色匹配或伪色彩增强等当中是十分重要的,有时候我们可以看到红外的照片或各种各样风格的照片是怎么样做到的呢?这就涉及到具体细节的知识点就是颜色查找表,就是要把原来的某种颜色匹配到另外一种颜色上面去,就是把低对比度的图转换到高对比度的图上去。

opencv4书籍推荐 opencv4入门_opencv_08

这个查找表可以是一个单调的增函数或者单调的减函数或者直接值匹配的这种也行,在图像像素这个层次而言为什么需要查找表,就是一副图像大概几万个像素而每个像素都要进行转换都要去进行函数转换的话那么十分耗资源,而查找表就是预先做一次转换把0-255都做一次匹配,再图像转换的时候就需要去查找匹配的值就可以不需要每次都去调用函数进行转换了,节约了大量的资源,这才是我们使用LUT查找表的真正原因。LUT查找表在实践很多数据处理场景当中只要你知道你需要处理的数据范围是什么,变化公式是什么,最后的结果是什么,那么对单个元素的变化,很多时候我们都通过LUT查找表来做,这样来节省资源时间,将计算步骤得到很多的减少。

作用;伪色彩加强和灰度或RGB图像的二值图像,加快计算速度

伪色彩函数applyColorMap

CV_EXPORTS_W void applyColorMap(InputArray src, OutputArray dst, int colormap);
src;源图像
dst;目标图像
colormap;提供的色彩图代码值。(参见:ColormapTypes 枚举数据类型)也可以自定义。

可以实现彩色图片颜色变化,以及灰度图颜色增强。

opencv4书籍推荐 opencv4入门_赋值_09

//色彩表
void QuickDemo::color_style_demo(Mat &image)
{
	//enum ColormapTypes  支持的20种色彩空间
	int colorMap[] = 
	{
		COLORMAP_AUTUMN,
		COLORMAP_BONE,
		COLORMAP_JET,
		COLORMAP_WINTER,
		COLORMAP_RAINBOW,
		COLORMAP_OCEAN,
		COLORMAP_SUMMER,
		COLORMAP_SPRING,
		COLORMAP_COOL,
		COLORMAP_HSV ,
		COLORMAP_PINK ,
		COLORMAP_HOT,
		COLORMAP_PARULA,
		COLORMAP_MAGMA,
		COLORMAP_INFERNO,
		COLORMAP_PLASMA,
		COLORMAP_VIRIDIS,
		COLORMAP_CIVIDIS,
		COLORMAP_TWILIGHT,
		COLORMAP_TWILIGHT_SHIFTED
	};
	Mat dst;
	int index = 0;
	while (true)
	{
		int c = waitKey(2000);//Waits for a pressed key. 等待100ms
		if (c == 27)//esc 退出27
		{
			break;
		}
		//伪色彩函数
		applyColorMap(image, dst, colorMap[index%19]);
		index++;
		imshow("颜色风格", dst);
	}
}

十、图像像素的逻辑操作(位操作)bite oprocted

位操作对应与或非操作 而opencv中是哪些API与之对应的。

CV_EXPORTS_W void rectangle(InputOutputArray img, Rect rec,
                          const Scalar& color, int thickness = 1,
                          int lineType = LINE_8, int shift = 0);
Rect_<_Tp>::Rect_(_Tp _x, _Tp _y, _Tp _width, _Tp _height)
    : x(_x), y(_y), width(_width), height(_height) {}

InputOutputArray img传入输出
Rect rec,矩形 传入xy宽高
const Scalar& color,填充颜色
int thickness = 1,1表示一个像素去绘制,相当于描边
传入<0表示填充这个矩形的意思.
opencv当中绘制和填充矩形都是这一个函数,用这个大于0 小于0来进行区分
int lineType = LINE_8  图形绘制方面的知识,与像素排列锯齿有关系。
//对Mat实现像素的位操作
void QuickDemo::bitwise_demo(Mat &image)
{
	Mat m1 = Mat::zeros(Size(256, 256), CV_8UC3);
	Mat m2 = Mat::zeros(Size(256, 256), CV_8UC3);
	Mat m3 = Mat::zeros(Size(256, 256), CV_8UC3);
	rectangle(m1,Rect(100, 100, 80, 80), Scalar(255, 255, 0), -1, LINE_8, 0);//-1表示填充矩形,LINE_8表示利用周围8个像素
	rectangle(m2, Rect(150, 150, 80, 80), Scalar(0, 255, 255), -1, LINE_8, 0);
	rectangle(m3, Rect(0, 0, 80, 80), Scalar(255, 0, 255),5, LINE_8, 0);//绘制 相当于描边
	imshow("m1", m1);
	imshow("m2", m2);
	imshow("m3", m3);

	Mat dst;
	bitwise_and(m1, m2, dst);//相当于交集
	imshow("像素位操作与", dst);
	bitwise_or(m1, m2, dst);//全集  
	imshow("像素位操作或", dst);
	bitwise_not(image, dst);
	//dst = ~image;//也是可以的
	imshow("像素位操作非", dst);
	bitwise_xor(m1, m2, dst);
	imshow("像素位操作异或", dst);
	return;
}

opencv4书籍推荐 opencv4入门_赋值_10

十一、图像通道的分离合并混合

分离split

CV_EXPORTS_W void split(InputArray m, OutputArrayOfArrays mv);
OutputArrayOfArrays mv 输出的通道数组  可以用std::vector<Mat> mv;来接收
实践
//图像分离split函数
std::vector<Mat> mv;
//opencv顺序是BGR的
split(image, mv); //分离成三个通道对应三种颜色
imshow("蓝色", mv[0]);
imshow("绿色", mv[1]);
imshow("红色", mv[2]);

合并merge

CV_EXPORTS_W void merge(InputArrayOfArrays mv, OutputArray dst);
InputArrayOfArrays mv  多通道的集合
使用
//合并通道到目标mat
Mat dst;
mv[1] = 0;//只保留蓝色
mv[2] = 0;
merge(mv, dst);
imshow("只显示蓝色", dst);

混合mixChannels

CV_EXPORTS void mixChannels(const Mat* src, size_t nsrcs, Mat* dst, size_t ndsts,
                            const int* fromTo, size_t npairs);
const Mat* src  原图数组
size_t nsrcs    操作多少张图像
Mat* dst  目标图数组
size_t ndsts  操作多少张图像
const int* fromTo  转换从通道几到通道几  如{0,2, 1,1, 2,0}第一张0通道到第二张2通道
size_t npairs	   转换的数据有几对  3对

实践
//混合交互通道
int from_to[] = {0,2, 1,1, 2,0};//通道交互从哪里到哪里  第一张0通道到第二张2通道
mixChannels(&image, 1, &dst, 1, from_to, 3);//支持多张图片  1表示多少张图片  3表示有几对通道交换数据
imshow("通道混合", dst);

opencv4书籍推荐 opencv4入门_opencv_11

十二、色彩空间转换-提取图像替换背景

色彩空间转换cvtColor

提取指定色彩空间范围区域imRange

hsv色彩空间的红橙黄绿蓝顶紫与rgb比较明显的区别界限,

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_12

因为hsv相对于rgb的颜色类型要少一些,只有hs两个表示并且只有180*255个颜色,从而就有hsv相对grb有一些纯颜色就有一定的界限,可以使用imRange提取出来的,利用上面那个表的最大最小值。

inRange可实现二值化功能,主要是将在两个阈值内的像素值设置为白色(255),而不在阈值区间内的像素值设置为黑色(0),该功能类似于之间所讲的双阈值化操作。
CV_EXPORTS_W void inRange(InputArray src, InputArray lowerb,
                          InputArray upperb, OutputArray dst);
InputArray lowerb;下限值
InputArray upperb; 上限值
OutputArray dst输出的图像 只有01的,再结合copyto可以实现图像叠加
实践
Mat hsv;
cvtColor(image, hsv, COLOR_BGR2HSV);
Mat mask;
inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), mask);//输入hsv图像、 根据绿色作为分界则传入hsv绿色的hsv最大最下值
imshow("inRange mask", mask);

首先要提取像素值在rgb中是很难的,因为rgb255255255颜色值太多太广了而hsv当中只有180*255的色彩空间值,就比较容易对颜色进行提取。 所以当看到一个单一颜色的时候要想去提取他,那么你的第一个反应就应该是要转换为一个色彩比较度高的色彩空间去,如hsv当中去。处理完成之后再返回rgb。

图像叠加copyto函数;源于可以根据mask进行有选择性的拷贝,结合inrange就可以完成不规则图像的iro图像提取。不规则图像的读取就是将不规则图像生成一个闭合区域,生成一个mask再利用copyto完成任意提取。

inline
void GpuMat::copyTo(OutputArray dst, InputArray mask) const
{
    copyTo(dst, mask, Stream::Null());
}
关键在于mask,
image.copyTo(reaback, mask);
/*
//mask的意思就是image在copy的时候只有像素不为0的像素点部分才会copy到reaback上去。
只是把mask中像素值不为0的像素点从image复制到reaback上去。
*/

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_13

//色彩空间的转换
void QuickDemo::inrange_demo(Mat &image)
{
	Mat hsv;
	cvtColor(image, hsv, COLOR_BGR2HSV);
	Mat mask;
	/*
	inRange可实现二值化功能,
	主要是将在两个阈值内的像素值设置为白色(255),而不在阈值区间内的像素值设置为黑色(0),
	该功能类似于之间所讲的双阈值化操作。
	*/
	inRange(hsv, Scalar(35, 43, 46), Scalar(77, 255, 255), mask);//输入hsv图像、 根据绿色作为分界则传入hsv绿色的hsv最大最下值
	imshow("inRange mask", mask);//只有黑白  把绿色都变成1白色 其他变成0黑色

	Mat reaback = Mat::zeros(image.size(), image.type());
	reaback = Scalar(40, 40, 200);//红底
	bitwise_not(mask, mask);//取反 黑变白 白变黑   就是相当于把绿色部分变成0黑色、人部分变成1白色
	imshow("bitwise_not mask", mask);
	/*
	//mask的意思就是image在copy的时候只有像素不为0的点才会copy到reaback上去
	相当于只人部分变成1白色部分从image上叠加上去,其他不变
	(就是把image人所在区域拷贝到reaback上来)
	*/
	image.copyTo(reaback, mask);
	imshow("roi区域提取", reaback);
}

十三、像素值统计信息

像素值统计一般就是最小值 最大值 均值mean 标准方差standard deviationn 涉及的API就是 最大最小值;minMaxLoc 只用于单通道 使用前要split分割 计算均值与标准方差;meanStdDev 方差低表示图片差异度低从而在图像处理中可以过滤掉一些信息如何学习opencvAPI以及在合适的场景选择他们完成功能这就是学习opencv的目标。

CV_EXPORTS_W void minMaxLoc(InputArray src, CV_OUT double* minVal,
                            CV_OUT double* maxVal = 0, CV_OUT Point* minLoc = 0,
                            CV_OUT Point* maxLoc = 0, InputArray mask = noArray());
值和坐标点都是传入指针,最后的mask就是用于iro非规则范围读取使用的
一定要记得只使用与单通道 要进行split分割的

CV_EXPORTS_W void meanStdDev(InputArray src, OutputArray mean, OutputArray stddev,
                             InputArray mask=noArray());
OutputArray mean,均值输出
OutputArray stddev  方差输出 
InputArray mask=noArray() iro非规则图像范围类读取
使用
Mat mean, stddev;
meanStdDev(image,mean,stddev);
输出是每个通道一个值 mean.at<double>(0)表示通道1的均值

实践

opencv4书籍推荐 opencv4入门_赋值_14

//进行像素值统计
void QuickDemo::pixel_statistic_demmo(Mat &image)
{
	double minv, maxv;//注意是double类型
	Point minLoc, maxLoc;//最大最小的地址
	std::vector<Mat> mv;
	split(image, mv);
	for (int i = 0; i < mv.size(); i++)
	{
		minMaxLoc(mv[i], &minv, &maxv, &minLoc, &maxLoc, Mat());//传入参数必须是单通道的、得先split
		std::cout << "minvalue:" << minv << "  maxvalue:" << maxv << std::endl;
	}
	
	Mat mean, stddev;
	meanStdDev(image,mean,stddev);//第四个参数mask就是iro不规则区域计算
	std::cout << "mean:" << mean << std::endl;
	std::cout << "mean[0]:" << mean.at<double>(0) << std::endl;
	//方差小就是图像的对比度差异度低、有效信息低
	//因此在图像分析当中,可以根据方差值小表示周围差异性小 从而是可以过滤掉一些东西的
	std::cout << "stddev:" << stddev << std::endl;
}

十四、图像几何形状绘制

分为各种形状的绘制和填充绘制, 如rectangle 中的int thickness参数 -1表示填充、其他的表示绘制宽度。

1、常规图形绘制

Rect 矩形对象类
rectangle 绘制矩形的方法
Point 表示点坐标
circle 绘制园
line  绘制线
RotatedRect  椭圆对象
ellipse绘制椭圆

可以使用addWeighted根据与图像的占比不同实现在图像上关键部位进行叠加几何形状

实际

opencv4书籍推荐 opencv4入门_赋值_15

//几何形状绘制
void QuickDemo::drawing_demo(Mat &image)
{
	//坐标的原点是从左上角为0 0
	//绘制矩形是可以有类型定义的
	Rect rect;
	rect.x = 150;
	rect.y = 20;
	rect.width = 70;
	rect.height = 100;
	
	Mat bg = Mat::zeros(image.size(), image.type());
	//rectangle(bg, rect, Scalar(0, 0, 255), 5, 0);
	rectangle(bg, rect, Scalar(0, 0, 255), -1, 0);//thickness参数改成小于0则表示填充
	circle(bg, Point(200, 200), 50, Scalar(255, 0, 0), -1, 0);//画圆
	line(bg, Point(100, 100), Point(255, 255), Scalar(0,0,0),8, 8, 0);//lineType图像锯齿问题  shift参数0 是相对左上角位置偏移亮的意思
	RotatedRect rrt;//椭圆
	rrt.center = Point(300,300);
	rrt.size = Size(50,100);
	rrt.angle = 0;
	ellipse(bg, rrt, Scalar(0, 255, 255), 2, 8);
	Mat dst;
	addWeighted(image, 0.7, bg, 0.3, 0, dst);//image占比0.7,bg占比0.3 叠加

	imshow("绘制几何", dst);
}

2、随机数产生绘制

RNG 产生随机数对象 传入种子一般为时间

RNG::uniform(int a, int b) 产生a-b范围内的随机值

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_16

实践

//随机数和随即绘制
void QuickDemo::randomdrawing_demo(Mat &image)
{
	Mat canvas = Mat::zeros(Size(500, 500), CV_8UC3);
	int w = canvas.cols;
	int h = canvas.rows;
	RNG rng(12345);//产生随机数 传入种子 一般传入时间
	while (true)
	{
		int c = waitKey(10);
		if (c == 27)
		{
			break;
		}
		int x1 = rng.uniform(0, w);//在0 - w范围内产生一个随机数
		int y1 = rng.uniform(0, h);
		int x2 = rng.uniform(0, w);//在0 - w范围内产生一个随机数
		int y2 = rng.uniform(0, h);
		int b = rng.uniform(0, 255);
		int g = rng.uniform(0, 255);
		int r = rng.uniform(0, 255);
		//canvas = Scalar(0, 0, 0);每次画布更新 达到每次图像只有一条线
		line(canvas, Point(x1, y1), Point(x2, y2), Scalar(b, g, r), 1, LINE_AA, 0);//lineType图像锯齿问题
		imshow("随机绘制", canvas);
	}
	return;
}

3、多边形的填充绘制fillPoly、polylines、drawContours

fillPoly 只能填充 polylines 只能绘制 如果需要表现出来既有填充又会绘制需要注意两者顺序 drawContours 可以绘制多个多边形

实践

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_17

//绘制多边形
void QuickDemo::polylinedrawing_demo(Mat &image)
{
	Mat canvas = Mat::zeros(Size(500, 500), CV_8UC3);
	Point p1(100, 100);
	Point p2(350, 100);
	Point p3(450, 280);
	Point p4(320, 450);
	Point p5(80, 400);
	std::vector<Point> vc_pts;
	vc_pts.push_back(p1);
	vc_pts.push_back(p2);
	vc_pts.push_back(p3);
	vc_pts.push_back(p4);
	vc_pts.push_back(p5);
	//要实现边框加填充就要注意fillPoly、polylines的顺序
	fillPoly(canvas, vc_pts, Scalar(255, 0, 255), 8, 0);//只能填充
	polylines(canvas, vc_pts, true, Scalar(0, 0, 255), 1, LINE_AA, 0);//这里的thickness只能传大于0 不能填充只能绘制
	imshow("多边形绘制", canvas);

	//显示多个多边形函数  绘制填充都可以选择
	std::vector<std::vector<Point>> contous;
	contous.push_back(vc_pts);
	//contourIdx索引显示第几个,-1表示都显示出来  
	//thickness  可以传入-1表示填充
	drawContours(canvas, contous, -1, Scalar(255, 0, 0), -1);
	imshow("多边形绘制1", canvas);

	return;
}

十五、鼠标操作及响应 在图形上提取鼠标框选区域setMouseCallback

绑定鼠标事件setMouseCallback

void setMouseCallback(const String& winname, MouseCallback onMouse, void* userdata = 0);
const String& winname;窗口名字
MouseCallback onMouse 鼠标响应后调用的回调函数、注意其原型需要是
typedef void(* cv::mouseCallBack)(int event, int x, int y, int flags, void *userdata)
void* userdata = 0  用于传给回调函数参数的
int event 表示当前发生的鼠标事件是什么
int x, int y 当前鼠标的坐标位置
int flags,   当前鼠标事件的flags值

鼠标event事件的官方截图

opencv官方文档链接

opencv4书籍推荐 opencv4入门_opencv_18

实践 还实现了ROI区域的读取、注意几个细节

每次都在最新的image上绘制采用一个临时mat先clone再每次绘制前进行copyto最初的到image上

注意鼠标回调事件的传参,如当前demo传入Mat image 在该图像上操作

opencv4书籍推荐 opencv4入门_赋值_19

代码

//鼠标操作及相应
Point sp(-1, -1);
Point ep(-1, -1);
Mat tempImage;
static void on_draw(int event, int x, int y, int flags, void *usrdata)
{
	Mat image = *((Mat *)usrdata);
	if (event == EVENT_LBUTTONDOWN)//鼠标左键按下事件
	{
		sp.x = x;
		sp.y = y;
		std::cout << "start Point: " << sp << std::endl;
	}
	else if (event == EVENT_LBUTTONUP)//左键松开
	{
		ep.x = x;
		ep.y = y;
		int dx = ep.x - sp.x;
		int dy = ep.y - sp.y;
		if (dx > 0 && dy > 0)
		{
			Rect box(sp.x, sp.y, dx, dy);
			//注意顺序 不然提取区域会有红色矩形框 并且要保证矩形范围在图形内否则会崩溃
			tempImage.copyTo(image);
			imshow("ROI区域", image(box));//截取鼠标款选的区域
			rectangle(image, box, Scalar(0, 0 , 255), 2);
			imshow("鼠标绘制", image);
			
			//给下次绘制初始化
			sp.x = -1;
			sp.y = -1;
		}
		std::cout << "end Point: " << ep << std::endl;
	}
	else if (event == EVENT_MOUSEMOVE)
	{
		if (sp.x >0 && sp.y > 0)
		{
			ep.x = x;
			ep.y = y;
			int dx = ep.x - sp.x;
			int dy = ep.y - sp.y;
			if (dx > 0 && dy > 0)
			{
				Rect box(sp.x, sp.y, dx, dy);
				tempImage.copyTo(image);//保证每次 绘制image都是最初的
				rectangle(image, box, Scalar(0, 0, 255), 2);
				imshow("鼠标绘制", image);
			}
		}
		
	}
	return;
}
void QuickDemo::setMouseCallback_demo(Mat &image)
{
	namedWindow("鼠标绘制", WINDOW_AUTOSIZE);
	setMouseCallback("鼠标绘制", on_draw, (void *)&image);
	tempImage = image.clone();
	imshow("鼠标绘制", image);
}

十六、图像像素数据类型转换及归一化convertTo、normalize

//归一化处理就是处理浮点数,到0-1之间,因此执行归一化处理首先要将图像数据转换为浮点数类型再进行归一化。并且浮点数类型的图片显示必须经过归一化处理把数据变成0-1之间才能正常显示出来,之后如何转换回来,就是归一化处理之后的像素值乘以255,再converto转换会int类型 数据类型转换converto 归一化处理normalize

inline
void GpuMat::convertTo(OutputArray dst, int rtype, double alpha, double beta) const
{
    convertTo(dst, rtype, alpha, beta, Stream::Null());
}
int rtype  数据类型  CV_32F 32位浮点数类型
double alpha;每个像素值乘以alpha
double beta;每个像素值加上beta
输出的时候每个像素值是否需要乘以alpha加上beta值

归一化处理
CV_EXPORTS_W void normalize( InputArray src, InputOutputArray dst, double alpha = 1, double beta = 0,
                             int norm_type = NORM_L2, int dtype = -1, InputArray mask = noArray());
double alpha = 1, double beta = 0,归一化处理值的上下限
int norm_type = NORM_L2  归一化处理的算法
int dtype = -1,   通道数变化 -1表示与原图像一致
InputArray mask = noArray()  ROI图像提取  也就是只归一化mat为非0的区域、可以使用inRange函数提取mask

图像归一化处理主要用在深度学习的建立模型的时候使用

实践

opencv4书籍推荐 opencv4入门_opencv_20

//像素类型转换及归一化处理
void QuickDemo::norm_demo(Mat &image)
{
	Mat dst;
	std::cout << image.type() << std::endl;
	image.convertTo(image, CV_32F);//转化为32位的浮点数
	std::cout << image.type() << std::endl;//type()输出的值是opencv重定义的枚举类型值
	normalize(image, dst, 1.0, 0, NORM_MINMAX);
	std::cout << dst.type() << std::endl;
	imshow("图像数据归一化", dst);
}

十七、图像放缩及 插值算法 resize

图像的resize大小放大缩小实现中就使用了插值算法、其中插值算法有很多种如双立方插值、双线性内插值、Lanczos采用放缩算法等,其实只要涉及图像的像素提取移动都是用到插值算法的(几何变化、透视变化、插值计算像素resize等系列情况)

CV_EXPORTS_W void resize( InputArray src, OutputArray dst,
                          Size dsize, double fx = 0, double fy = 0,
                          int interpolation = INTER_LINEAR );
Size dsize,  目标图像的大小
double fx = 0, double fy = 0  如果size写入的是0,0 则安装fx,fy进行水平 竖直方向的放缩
int interpolation = INTER_LINEAR   选择的插值方法

opencv4书籍推荐 opencv4入门_赋值_21

//图像放缩及插值
void QuickDemo::resize_demo(Mat &image)
{
	Mat zoomin;
	Mat zoomax;
	int w = image.cols;
	int h = image.rows;
	resize(image, zoomin, Size(w/2, h/2), 0, 0, INTER_LINEAR);
	imshow("zoomin", zoomin);
	resize(image, zoomax, Size(w * 1.5, h * 1.5), 0, 0, INTER_LINEAR);
	imshow("zoomax", zoomax);
}

十八、图像的翻转 平移

1、图像翻转flip

图像的翻转其实就是获取他的镜像、

CV_EXPORTS_W void flip(InputArray src, OutputArray dst, int flipCode);
int flipCode指定如何翻转数组的标志;
0表示绕x轴翻转,就是上下翻转
正值(例如1)表示绕y轴翻转。就是左右翻转
负值(例如-1)意味着在两个轴上翻转。上下左右都翻转

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_22

//翻转
void QuickDemo::flip_demo(Mat &image)
{
	Mat dst;
	flip(image, dst, 0);//上下翻转
	imshow("上下翻转", dst);
	flip(image, dst, 1);//大于0 左右翻转
	imshow("左右翻转", dst);
	flip(image, dst, -1);//小于0  上下左右都翻转 就是180度旋转
	imshow("180度旋转", dst);
}

2、图像旋转getRotationMatrix2D、warpAffine

图像的旋转 opencv也是提供了api的是warpAffine()函数, 注意图像的旋转是需要借助矩阵的,因为图像旋转其实每个像素点都是有变化的。矩形其实opencv中提供了api,getRotationMatrix2D()方法传入旋转角度就会返回矩阵的

inline
Mat getRotationMatrix2D(Point2f center, double angle, double scale)
{
    return Mat(getRotationMatrix2D_(center, angle, scale), true);
}
Point2f center 图像的中心位置
double angle  旋转角度
 double scale  这个还可以进行图像的放缩,输入目标图像的放缩比例

opencv4书籍推荐 opencv4入门_opencv_23

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_24

计算旋转后图形的宽高几何公式来源图

opencv4书籍推荐 opencv4入门_赋值_25

实现旋转代码

//旋转
void QuickDemo::rotate_demo(Mat &image)
{
	Mat dst, M;
	int w = image.cols;
	int h = image.rows;
	M = getRotationMatrix2D(Point2f(w/2, h/2), 45, 1.0);//得到旋转45度的矩阵
	//计算旋转后的图片大小  这个是得到旋转Mat可以知道的对应的cos、sin
	double cos = abs(M.at<double>(0, 0));
	double sin = abs(M.at<double>(0, 1));
	//计算旋转后的新宽高  根据几何知识 
	int nh = cos*w + sin*h;
	int nw = sin*w + cos*h;
	//计算偏移量 修改中心的偏移量  因为warpAffine需要传入的mat的中心坐标
	M.at<double>(0, 2) += (nw/2 - w/2);
	M.at<double>(1, 2) += (nh / 2 - h / 2);
	warpAffine(image, dst, M, Size(nw, nh), INTER_LINEAR, 0, Scalar(255, 0, 0));
	imshow("旋转演示", dst);//没有遮挡的
}

十九、摄像头及视频处理

opencv保存视频是只处理视频不处理音频的,如果要处理音视频那么就只能使用ffmpeg了,但是在使用ffmpeg的时候可以合理利用opencv处理视频图像的手段,更好的完成功能。 注意opencv保存视频也是有大小限制的,最大2G。

视频的访问及相关属性

VideoCapture类 用于抽象化视频文件的对象
VideoCapture captureCamear(0);  
传入0表示获取默认摄像头设备 打开摄像头流之后captureCamear对象就抽象为摄像头设备进行读取视频流等操作

VideoCapture captureCamear("C:/Users/20531/Desktop/1.mp4");
也可以传入视频文件,但是注意读取视频文件的时候可能读取失败 读取出来的宽高为0 ,这需要添加一个库
/*
//如果读取视频文件的帧宽高失败则需要
将opencv安装目录D:\opencv\build\x64\vc14\bin中的opencv_videoio_ffmpeg440_64.dll复制
到生成项目的.exe所在的文件(Debug/Release)中。
(因为我用OpenCV版本是4.4,所以ffmpeg440)
*/

VideoCapture的系列API
如captureCamear.get(属性)//有很多宏控制,可以返回对应的视频参数,
当然也可以set,但是注意如果是摄像头的话 set的参数摄像头硬件是否支持
captureCamear.read(Mat)//读取视频帧图像 
将视频读取成一帧帧的mat图像之后就可以对其每个mat做处理就达到对视频流做处理的效果。

注意VideoCapture对象占用资源 需要在不再使用之前进行realease()

视频的写入

VideoWriter类对象完成对视频的写入
其中一种构造函数
CV_WRAP VideoWriter(const String& filename, int fourcc, double fps,
                Size frameSize, bool isColor = true);

const String& filename写入视频的文件全路径
int fourcc,  采用的编解码类型,这个可以从源视频流对象那里获取、captureCamear.get(CAP_PROP_FOURCC)
double fps,帧率
 Size frameSize,宽高 最好也从原视频获取,但是如果就是要对视频进行放缩的话 要与后面的mat写入对应
如
VideoWriter writer("D:/test.mp4", captureCamear.get(CAP_PROP_FOURCC), fps, Size(frame_width, frame_height), true);

//写入API  write即可
writer.write(frameCamear);//写入文件

注意视频写入的时候可能会报一个错误 提示

could not open codec “libopenh264” : Unspecified error错误,

则需要根据提示下载对应版本的dll库,放到.exe的运行目录下即可

参考博客openh264库下载网站

opencv4书籍推荐 opencv4入门_赋值_26

缺少一个从一个视频扣ROI图到另外一个视频上叠加的案例 实践

//视频读取
void QuickDemo::video_demo(Mat &image)
{
	//VideoCapture captureCamear(0);//传入0表示读取摄像头 还可以传入视频文件的全路径字符串
	/*
	//如果读取视频文件的帧宽高失败则需要
	将opencv安装目录D:\opencv\build\x64\vc14\bin中的opencv_videoio_ffmpeg440_64.dll复制
	到生成项目的.exe所在的文件(Debug/Release)中。
	(因为我用OpenCV版本是4.4,所以ffmpeg440)
	*/
	VideoCapture captureCamear("C:/Users/20531/Desktop/1.mp4");
	//get视频属性 注意也是可以set设置的 但是注意如果是摄像头的话他硬件是否支持set的参数
	int frame_width = captureCamear.get(CAP_PROP_FRAME_WIDTH);
	int frame_height = captureCamear.get(CAP_PROP_FRAME_HEIGHT);
	int frame_counts = captureCamear.get(CAP_PROP_FRAME_COUNT);
	int fps = captureCamear.get(CAP_PROP_FPS);
	std::cout << "frame_width = " << frame_width << std::endl;
	std::cout << "frame_height = " << frame_height << std::endl;
	std::cout << "frame_counts = " << frame_counts << std::endl;
	std::cout << "fps = " << fps << std::endl;

	//CAP_PROP_FOURCC  获取原来的编码类型   注意size的大小不能变
	VideoWriter writer("D:/test.mp4", captureCamear.get(CAP_PROP_FOURCC), fps, Size(frame_width, frame_height), true);

	Mat frameCamear;
	Mat frameVedio;
	while (true)
	{
		captureCamear.read(frameCamear);//读取视频 每一帧 从而操作视频就是操作图像了
		//captureVedio.read(frameVedio);
		flip(frameCamear, frameCamear, 1);//左右翻转一下 与实际保持一致
		writer.write(frameCamear);//写入文件
		if (frameCamear.empty())
		{
			break;
		}
		imshow("frame", frameCamear);
		int c = waitKey(10);
		if (c == 27)
			break;
	}
	//captureVedio.release();
	captureCamear.release();//摄像头资源需要释放
	writer.release();//视频写入变量也需要释放资源
}

二十、图像直方图

1、直方图概念

在计算机当中图像的保存也都是只有数字0和1,其中如rgb这样的存储模式中每位数字也都是有范围的0-255,因此那么我们就应该可以统计出一副图像当中RGB中那个数字出现的频率最高,展示出来这也就是直方图了。直方图反应的就是0-255个灰度等级在一幅图像这么多像素点当中他们各种出现的次数是多少。最后得出的直方图也就很直观的可以得到某个灰度等级在这张图片的所有像素当中出现的次数。

直方图的作用;是图像像素值的统计学特征,在于对图像进行众多处理如翻转平移放缩等手段的时候直方图始终是可以保持他的不变性的。广泛应用与图像处理的各个领域,特别是灰度图像的阈值分割,基于颜色的图像检索及图像分类,反向投影跟踪。

相同的直方图不一定是同一张图像,因为直方图只是保留了图像的像素值特征,而丢失了图像的空间坐标等特征。因此直方图只是图像的特征,但是真正能够唯一表示某种图像的特征只有图像特征本身,opencv中也支持很多种。

直方图分为 灰度直方图 彩色直方图

2、一维直方图

CV_EXPORTS void calcHist( const Mat* images, int nimages,
                          const int* channels, InputArray mask,
                          OutputArray hist, int dims, const int* histSize,
                          const float** ranges, bool uniform = true, bool accumulate = false );
const Mat* images; 传入的指针是可以接收多张图片的 同时显示他们的直方图解析
int nimages    传入的图片个数
const int* channels  传入的通道数
InputArray mask  掩码 与之前一致 只处理mask非0的区域
OutputArray hist   直方图输出
int dims  几维的  
const int* histSize,  取值范围个数
const float** ranges  直方图的取值范围
案例
//分割通道
std::vector<Mat> bgr_plane;
split(image, bgr_plane);
//定义参数变量
const int channels[1] = { 0 };
const int bins[1] = { 256 };//直方图的取值个数
float hranges[2] = { 0, 255 };//直方图的取值范围
const float* ranges[1] = {hranges};//因为接口可以支持多维的 多张图像
Mat b_hist;
Mat g_hist;
Mat r_hist;

//计算直方图
calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges);
calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges);
calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges);

实践

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_27

//获取直方图
void QuickDemo::histogram(Mat &image)
{
	//分割通道
	std::vector<Mat> bgr_plane;
	split(image, bgr_plane);
	//定义参数变量
	const int channels[1] = { 0 };
	const int bins[1] = { 256 };//直方图的取值个数
	float hranges[2] = { 0, 255 };//直方图的取值范围
	const float* ranges[1] = {hranges};//因为接口可以支持多维的 多张图像
	Mat b_hist;
	Mat g_hist;
	Mat r_hist;

	//计算直方图
	calcHist(&bgr_plane[0], 1, 0, Mat(), b_hist, 1, bins, ranges);
	calcHist(&bgr_plane[1], 1, 0, Mat(), g_hist, 1, bins, ranges);
	calcHist(&bgr_plane[2], 1, 0, Mat(), r_hist, 1, bins, ranges);

	//显示直方图
	int hist_w = 512;
	int hist_h = 400;
	int bin_w = cvRound((double)hist_w/bins[0]);//每个bin是256个,总宽度除以它得到每个灰度值在图像中占几个像素点,最终是要画图的
	Mat hisImage = Mat::zeros(hist_h, hist_w, CV_8UC3);//绘制直方图的底布

	//归一化处理 因为三个通道出现的相同灰度值次数差值太大了因此需要归一化到一段范围内 (底布高度范围这么大)来显示在一张图片上
	normalize(b_hist, b_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
	normalize(g_hist, g_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
	normalize(r_hist, r_hist, 0, hisImage.rows, NORM_MINMAX, -1, Mat());
	//绘制直方图曲线
	for (int i = 1; i < bins[0]; i++)
	{
		//点的坐标都是基于屏幕坐标的 因此要做转换 hist_h -cvRound(b_hist.at<float>(i-1)))
		line(hisImage, Point(bin_w*(i-1), hist_h -cvRound(b_hist.at<float>(i-1))),
		Point(bin_w*(i), hist_h-cvRound(b_hist.at<float>(i))), Scalar(255,0,0),2,8,0);
		line(hisImage, Point(bin_w*(i - 1), hist_h - cvRound(g_hist.at<float>(i - 1))),
			Point(bin_w*(i), hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, 8, 0);
		line(hisImage, Point(bin_w*(i - 1), hist_h - cvRound(r_hist.at<float>(i - 1))),
			Point(bin_w*(i), hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, 8, 0);
	}
	//显示直方图
	namedWindow("Histogram Deemo", WINDOW_AUTOSIZE);
	imshow("Histogram Deemo", hisImage);
}

3、二维直方图

之前的一维直方图是我们将rgb图片split分割成三个通道了,然后进行显示,现在说的二维直方图是讲的hsv图像,将rgb图像转换为hsv图像,因为hsv格式只有hs是表示颜色的,v表示的是亮度。(注意h的取值范围是0-180,s的取值范围是0-255)

opencv4书籍推荐 opencv4入门_赋值_28

实践

这部分没有看太懂,直方图与原图如何对应解析的

opencv4书籍推荐 opencv4入门_赋值_29

//获取二维直方图
void QuickDemo::histogram_2d_demo(Mat &image)
{
	//2D直方图
	Mat hsv, hsv_hist;
	cvtColor(image, hsv, COLOR_BGR2HSV);
	int hbins = 30;//h的灰度只是0-179,一共180个灰度值,30个灰度值为一个单位统计在该范围内的像素频率
	int sbins = 32;//s的灰度值实时0-255
	int hist_bins[] = {hbins, sbins};//2d将两组bins设置进去
	float h_range[] = {0, 180};//灰度值的取值范围
	float s_range[] = { 0, 256 };
	const float* hs_range[] = {h_range, s_range};
	int hs_channels[] = { 0, 1 };
	//hsv 只有一张图像  第0个和第一个通道、mask就是与之前的一样 只对非0的做处理  产生的mat  2维的 第1、2个通道多少个bins  第1、2个通道多少个取值范围
	这个就是在得出每个灰度值对应的像素点的个数
	calcHist(&hsv, 1, hs_channels, Mat(), hsv_hist, 2, hist_bins, hs_range, true, false);
	//2维的一图图像里面 则首先要进行归一化处理
	double maxVal = 0;
	minMaxLoc(hsv_hist, 0, &maxVal, 0, 0);//最大值
	int scale = 10;
	Mat hist2d_image = Mat::zeros(sbins*scale, hbins*scale, CV_8UC3);//创建空白图像 将数据往上面填入即可
	for (int h = 0; h < hbins; h++)
	{
		for (int s = 0; s < sbins; s++)
		{
			float binVal = hsv_hist.at<float>(h, s);
			int intensity = cvRound(binVal * 255 / maxVal);//取整
			rectangle(hist2d_image, Point(h*scale, s*scale),
				Point((h+1)*scale -1, (s+1)*scale -1),
				Scalar::all(intensity), -1);
		}
	}
	applyColorMap(hist2d_image, hist2d_image, COLORMAP_JET);//变成色彩图
	imshow("H-S Histogram", hist2d_image);
}

4、直方图均衡化

图像直方图均衡化主要是可以用于图像增强,对图像直方图均衡处理之后会提升后续对象检测的准确率,提升图像质量。 该opencv提供的均衡化API(equalizeHist)只支持单通道。

void equalizeHist( InputArray src, OutputArray dst );
原图像和目标图像,但是注意该API只支持单通道,则使用前需要把输入图像转换为灰度图cvtColor(image, gray, COLOR_BGR2GRAY);//灰度图像

实践;可以看出均衡化大致就是把图像亮暗更加明显一点

opencv4书籍推荐 opencv4入门_opencv_30

//获取直方图的均衡化
void QuickDemo::histogram_eq_demo(Mat &image)
{
	namedWindow("灰度图像", WINDOW_FREERATIO);
	namedWindow("直方图均衡化", WINDOW_FREERATIO);//图片太大了
	Mat gray;
	cvtColor(image, gray, COLOR_BGR2GRAY);//灰度图像
	imshow("灰度图像", gray);
	Mat dst;
	equalizeHist(gray, dst);
	imshow("直方图均衡化", dst);
}

问题;后期再补 这里只支持单通道那就是灰度图像,那么彩色图像如何均衡化 大致就是将BGR转为hsv通道 对v通道进行均衡化处理完成之后在合并到hsv,最后再转换为BGR完成彩色图像的直方图均衡化。

如何局部范围直方图均衡化

二十一、卷积操作

1、图像卷积操作

这是均值卷积,每个像素的卷积核都是相同的, 线性卷积;就是卷积和与图像对应位置进行点乘累加得到的结果除以卷积和个数,得到的结果替换到之前进行操作的图像区域的中心位置。这个位置在opencv中有一个名称叫锚点anchor(默认是中心输出)

这种处理在原图像的边缘是与锚点没有关系的,因此对待这些边缘有两种做法一是扔掉,二是做填充边缘化处理

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_31

CV_EXPORTS_W void blur( InputArray src, OutputArray dst,
                        Size ksize, Point anchor = Point(-1,-1),
                        int borderType = BORDER_DEFAULT );
InputArray src, OutputArray dst原图像 目标图像
Size ksize  卷积核的大小 默认值都是1的  Size(3,3)就是3X3的卷积核
注意;卷积核越大图像越模糊  根据Size的传入还可以进行水平方向卷积Size(15, 1),竖直方向卷积Size(1,15)
Point anchor = Point(-1,-1) 锚点 默认-1就是卷积核的中心位置
int borderType = BORDER_DEFAULT opencv对边缘化的处理方式

实践

opencv4书籍推荐 opencv4入门_opencv_32

//计算卷积
void QuickDemo::blur_demo(Mat &image)
{
	Mat dst;
	namedWindow("卷积", WINDOW_FREERATIO); 
	//卷积核越大图像越模糊  根据Size的传入还可以进行水平方向卷积Size(15, 1),竖直方向卷积Size(1,15
	blur(image, dst, Size(5, 5), Point(-1, -1));)
	imshow("卷积", dst);
}

2、高斯模糊

非均值卷积,之前的卷积是每个像素的卷积核是相同的,而高斯模糊就是可以达到不同的卷积核。

高斯模糊的卷积核就是中心位置最大,离中心位置越远的就越小。

高斯核函数

中心化效应,考虑了图形中心像素对图像的一个贡献,而均值卷积就没有考虑到,中心位置比例最大。

高斯模糊和均值卷积的区别就是他们的卷积核不一样,一个是均值都是1一个是通过高斯核函数计算的,是根据中心占比的得到的。

opencv4书籍推荐 opencv4入门_opencv_33

//计算高斯模糊
void QuickDemo::gaussian_blur_demo(Mat &image)
{
	Mat dst;
	//卷积核的大小,注意一定要是基数 偶数就是错误的(违反了高斯中心化的原则)
	//sigma  当窗口设置Size(0,0)的时候opencv就会从sigma反算窗口大小,
	//当窗口大小已经设定一个值之后那么sigma这边无论设什么都没有效果,他会从窗口计算得到sigma的
	//并且窗口或者sigma都是值越大图形越模糊,sigma对图形的模糊效果更加明显所以很多时候都会设置size而是直接设计sigma的来查看模糊效果
	GaussianBlur(image, dst, Size(5, 5), 15);
	imshow("高斯模糊", dst);
}

3、高斯双边模糊

不是整个图形都进行模糊,这样把图像原有的信息丢掉了。高斯双边模糊就是处理这个问题的,他模糊的同时保留了较大区别的边缘(如亮暗的边缘)而模糊掉了一些细节处理。

双边是指的空间的和色彩的。

opencv4书籍推荐 opencv4入门_opencv_34

CV_EXPORTS_W void bilateralFilter( InputArray src, OutputArray dst, int d,
                                   double sigmaColor, double sigmaSpace,
                                   int borderType = BORDER_DEFAULT );
InputArray src, OutputArray dst, 原图像和目标图像
int d,窗口大小,之前说了可以填0 由后面的sigma来反推计算
 double sigmaColor, 这个值要大一点,用于色彩的卷积核 
double sigmaSpace,    空间的卷积核
int borderType = BORDER_DEFAULT  边缘的处理方式

实践

opencv4书籍推荐 opencv4入门_opencv_35

//计算双边高斯卷积
void QuickDemo::Bilateralgaussian_blur_demo(Mat &image)
{
	Mat dst;
	//100表示色彩的卷积核 要大一点
	//10表示空间距离的卷积核
	bilateralFilter(image, dst, 0, 100, 10);
	imshow("高斯模糊", dst);
}

二十二、人脸识别 基于dnn的Tensorflow模型

dnn深度神经网络模块; opencv中有一个dnn模块就是深度神经网络模块,就是opencv4新增的用来支持各种深度学习训练出来的 模型,但是它只支持推理不支持训练的。包括tensorflow,caffe,pytorch中一些主流的算法。

安装模型;可以执行python执行

D:\opencv4.4\opencv\sources\samples\dnn\face_detector

下面的download_weights.py安装dnn人脸识别相关的模型,但是下载很难成功的,这里直接给出模型文件地址

opencv学堂贾志刚快速入门opencv30讲课件地址

opencv4书籍推荐 opencv4入门_opencv_36

基于深度学习的方法如何构建程序

1、首先 读取dnn里面的Tensorflow模型 传入学习模型和配置文件  返回一个网络文件
cv::dnn::Net net =  cv::dnn::readNetFromTensorflow(rootdir+"opencv_face_detector_uint8.pb", rootdir + "opencv_face_detector.pbtxt");
2、读取模型实例  具体参数可以查看dnn下的models.yml文件
  CV_EXPORTS_W Mat blobFromImage(InputArray image, double scalefactor=1.0, const Size& size = Size(),
                                   const Scalar& mean = Scalar(), bool swapRB=false, bool crop=false,
                                   int ddepth=CV_32F);
InputArray image  输入图像
scalefactor=1.0 图像值的乘数。
const Size& size = Size() 输出图像的大小 根据models.yml获取
const Scalar& mean = Scalar(), 均值 根据models.yml获取
bool swapRB=false   RB通道是否交互根据models.yml获取
实例
Mat blob = cv::dnn::blobFromImage(frameVedio, 1.0, Size(300, 300), Scalar(104, 177, 123), false, false);//读取模型
这个函数的目的就是将BGR格式的MAT转换为NCWH也就是模型推理所需要的格式。

opencv4书籍推荐 opencv4入门_赋值_37

3、准备数据
net.setInput(blob);//准备数据   blob就是NCWH (多少个,通道数,宽高)
4、Mat probs = net.forward();//完成推理
5、进行一下转换
Mat detectionMat(probs.size[2], probs.size[3], CV_32F, probs.ptr<float>());//宽,高,数据类型,数据地址
6、判断是否是人脸 进行显示
参数是
取值七个值
0类型 1自己的index  
2自己的得分得分越高越可能是人脸  
34 左上角点的坐标  56右下角点的坐标
float confidence = detectionMat.at<float>(i, 2); 
绘制
if (confidence > 0.5)//大于0.5就可能是人脸概率
{
	//因为预测出来的数是0-1之间的,要乘以实践宽高 才是真正的坐标值
	int x1 = static_cast<int>(detectionMat.at<float>(i, 3)*frameVedio.cols);
	int y1 = static_cast<int>(detectionMat.at<float>(i, 4)*frameVedio.rows);
	int x2 = static_cast<int>(detectionMat.at<float>(i, 5)*frameVedio.cols);
	int y2 = static_cast<int>(detectionMat.at<float>(i, 6)*frameVedio.rows);
	Rect rect(x1, y1, x2-x1, y2-y1);
	rectangle(frameVedio, rect, Scalar(0, 0, 255), 2, 8, 0);
}

实践

opencv4书籍推荐 opencv4入门_opencv4书籍推荐_38

//人脸识别的demo
void QuickDemo::face_detection_demo()
{
	std::string rootdir = "D:/opencv4.4/opencv/sources/samples/dnn/face_detector/";
	//读取dnn里面的Tensorflow模型  返回一个网络文件
	cv::dnn::Net net = cv::dnn::readNetFromTensorflow(rootdir+"opencv_face_detector_uint8.pb", rootdir + "opencv_face_detector.pbtxt");
	VideoCapture capture(0);
	Mat frameVedio;
	while (true)
	{
		capture.read(frameVedio);
		if (frameVedio.empty())
			break;
		//模型需要的大小 均值 参数 可以查看dnn下的models.yml文件 
		Mat blob = cv::dnn::blobFromImage(frameVedio, 1.0, Size(300, 300), Scalar(104, 177, 123), false, false);//读取模型
		net.setInput(blob);//准备数据   blob就是NCWH (多少个,通道数,宽高)
		Mat probs = net.forward();//完成推理    出来的结果是多少张图像有个编号imageindex、第几个批次的,有多少个框,每个框有七列。
		Mat detectionMat(probs.size[2], probs.size[3], CV_32F, probs.ptr<float>());//宽,高,数据类型,数据地址
		//解析结果  
		for (int i = 0; i < detectionMat.rows; i++)
		{
			//取值七个值 0类型 1自己的index  2自己的得分得分越高越可能是人脸  34 左上角点的坐标  56右下角点的坐标
			float confidence = detectionMat.at<float>(i, 2);
			if (confidence > 0.5)//大于0.5就可能是人脸概率
			{
				//因为预测出来的数是0-1之间的,要乘以实践宽高 才是真正的坐标值
				int x1 = static_cast<int>(detectionMat.at<float>(i, 3)*frameVedio.cols);
				int y1 = static_cast<int>(detectionMat.at<float>(i, 4)*frameVedio.rows);
				int x2 = static_cast<int>(detectionMat.at<float>(i, 5)*frameVedio.cols);
				int y2 = static_cast<int>(detectionMat.at<float>(i, 6)*frameVedio.rows);
				Rect rect(x1, y1, x2-x1, y2-y1);
				rectangle(frameVedio, rect, Scalar(0, 0, 255), 2, 8, 0);
			}
		}
		imshow("人脸检测演示", frameVedio);
		int c = waitKey(10);
		if (c == 27)
			break;
	}

	capture.release();//摄像头资源需要释放
}