OpenCV图像处理基础(C++版)

目录

OpenCV环境搭建

加载 修改 保存图像

矩阵的掩膜操作

Mat对象

图像操作

图像混合

调整图像亮度与对比度

绘制形状与文字

模糊图像一

模糊图像二

膨胀与腐蚀

形态学操作

形态学操作应用-提取水平线和垂直线

图像金字塔-上采集与降采集

基本阈值操作

自定义线性滤波

处理边缘

Sobel算子

Laplance算子

Canny边缘检测

霍夫变换-直线

霍夫圆变换

像素重映射

直方图均衡化

直方图计算

直方图比较

直方图反向透射

模板匹配

轮廓发现

凸包

轮廓周围绘制矩形框和圆形框

圆形矩

点多边形测试

基于距离变换与分水岭的图像分割


OpenCV环境搭建

  • 配置环境变量

  •  新建项目

视图 - 其他窗口 - 属性管理器

 添加附加依赖项

 测试代码(这里应当注意,在进行测试的时候,注意相应位数)

#include<opencv2\opencv.hpp>
using namespace cv;

int main(int argc, char** argv)
{
	Mat src = imread("C:\\Users\\td\\Desktop\\he.jpeg");
	if (src.empty())
	{
		printf("could not load image ...\n");
		return -1;
	}
	namedWindow("test opencv setup ", CV_WINDOW_AUTOSIZE);
	imshow("test opencv setup", src);

	waitKey(0);
	
	return 0;
}

加载 修改 保存图像

  • imread函数

imread功能是加载图像文件成为一个Mat对象,其中第一个参数表示图像文件名称

第二个参数,表示加载的图像是什么类型,支持常见的三个参数值

IMREAD_UNCHANGED (<0) 表示加载原图,不做任何改变

IMREAD_GRAYSCALE (0)表示把原图作为灰度图像加载进来,如下这行代码就是加载灰度图像.

Mat src = imread("C:\\Users\\td\\Desktop\\he.jpeg", IMREAD_GRAYSCALE);

IMREAD_COLOR (>0) 表示把原图作为RGB图像加载进来

注意:OpenCV支持JPG、PNG、TIFF等常见格式图像文件加载

  • namedWindow函数

namedWindos功能是创建一个OpenCV窗口,它是由OpenCV自动创建与释放,你无需取销毁它。 常见用法namedWindow("Window Title", WINDOW_AUTOSIZE)

WINDOW_AUTOSIZE会自动根据图像大小,显示窗口大小,不能人为改变窗口大小 WINDOW_NORMAL,跟QT集成的时候会使用,允许修改窗口大小。

  • imshow函数

imshow根据窗口名称显示图像到指定的窗口上去,第一个参数是窗口名称,第二参数是Mat对象

  • cvtColor函数

cvtColor的功能是把图像从一个彩色空间转换到另外一个色彩空间,有三个参数,第一个参数表示源图像、第二参数表示色彩空间转换之后的图像、第三个参数表示源和目标色彩空间如:COLOR_BGR2HLS 、COLOR_BGR2GRAY 等 

  • imwrite函数

保存图像文件到指定目录路径

只有8位、16位的PNG、JPG、Tiff文件格式而且是单通道或者三通道的BGR的图像才可以通过这种方式保存

保存PNG格式的时候可以保存透明通道的图片

可以指定压缩参数

#include<opencv2\opencv.hpp>
using namespace cv;

int main(int argc, char** argv)
{
	Mat src = imread("C:\\Users\\td\\Desktop\\he.jpeg");
	if (src.empty())
	{
		printf("could not load image ...\n");
		return -1;
	}
	namedWindow("test opencv setup ", CV_WINDOW_AUTOSIZE);
	imshow("test opencv setup", src);
	waitKey(20);
	
	//转换相应的色彩空间
	namedWindow("output windows", CV_WINDOW_AUTOSIZE);
	Mat output;//存储转换之后的图像
	cvtColor(src, output,CV_BGR2BGR555);
	imshow("output windows", output);
	
	//保存图片的过程
	imwrite("D:/zhubajie.png", output);
	waitKey(0);
	
	return 0;
}

矩阵的掩膜操作

  •  掩膜操作:实现图像对比度调整(来重新计算每个像素的像素值)

        红色是中心像素,从上到下,从左到右对每个像素做同样的处理操作,得到最终结果就是对比度提高之后的输出图像Mat对象.如下图所示:

        计算公式是如下所示:

  •  图像的通道数

基本上,描述一个像素点,如果是灰度,那么只需要一个数值来描述它,就是单通道。
如果一个像素点,有RGB三种颜色来描述它,就是三通道.

  • 获取图像像素指针

CV_Assert(myImage.depth() == CV_8U);

Mat.ptr<uchar>(int i=0) 获取像素矩阵的指针,索引i表示第几行,从0开始计行数。

获得当前行指针const uchar*  current= myImage.ptr<uchar>(row );

获取当前像素点P(row, col)的像素值 p(row, col) =current[col]

  • 像素范围处理saturate_cast<uchar>

saturate_cast<uchar>(-100),返回 0。

saturate_cast<uchar>(288),返回255

saturate_cast<uchar>(100),返回100 这个函数的功能是确保RGB值得范围在0~255之间

  • 矩阵的掩膜操作的过程是如下代码所示:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;

int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop\\he.jpeg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input image", CV_WINDOW_AUTOSIZE);
	imshow("input image", src);

	/*
	int cols = (src.cols-1) * src.channels();//宽度上面提前打算
	int offsetx = src.channels();
	int rows = src.rows;

	dst = Mat::zeros(src.size(), src.type());//获取和src Mat类型
	for (int row = 1; row < (rows - 1); row++) {//第一行和最后一行是很难形成相应的图形,因此,进行一个省略的操作.
	const uchar* previous = src.ptr<uchar>(row - 1);//获取前一行
	const uchar* current = src.ptr<uchar>(row);//获取当前行
	const uchar* next = src.ptr<uchar>(row + 1);//获取下一行
	uchar* output = dst.ptr<uchar>(row);
	for (int col = offsetx; col < cols; col++) {
	output[col] = saturate_cast<uchar>(5 * current[col] - (current[col- offsetx] + current[col+ offsetx] + previous[col] + next[col]));
	}
	}
	namedWindow("contrast image demo", CV_WINDOW_AUTOSIZE);
	imshow("contrast image demo", dst);

	*/
	double t = getTickCount();
	Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);//定义一个掩膜
	filter2D(src, dst, src.depth(), kernel);//掩膜操作,将src之中的计算结果直接输出到dst之中
	double timeconsume = (getTickCount() - t) / getTickFrequency();
	printf("tim consume %.2f\n", timeconsume);

	namedWindow("contrast image demo", CV_WINDOW_AUTOSIZE);
	imshow("contrast image demo", dst);

	waitKey(0);
	return 0;
}

Mat对象

  • 什么叫做Mat对象??

        在人眼之中,这是一个一个对象,但是在计算机的眼中,这是一个个数据. 

  • Mat对象与IplImage对象的对比

        Mat对象OpenCV2.0之后引进的图像数据结构、自动分配内存、不存在内存泄漏的问题,是面向对象的数据结构。分了两个部分,头部与数据部分

        IplImage是从2001年OpenCV发布之后就一直存在,是C语言风格的数据结构,需要开发者自己分配与管理内存,对大的程序使用它容易导致内存泄漏问题(建议不进行使用)

  • 常用的构造函数

  • Mat对象的使用

部分复制:一般情况下只会复制Mat对象的头和指针部分,不会复制数据部分 Mat A= imread(imgFilePath); Mat B(A)  // 只复制

完全复制:如果想把Mat对象的头部和数据部分一起复制,可以通过如下两个API实现 Mat F = A.clone(); 或 Mat G; A.copyTo(G); 

  • Mat对象使用的四个要点

输出图像的内存是自动分配的

使用OpenCV的C++接口,不需要考虑内存分配问题

赋值操作和拷贝构造函数只会复制头部分

使用clone与copyTo两个函数实现数据完全复制

  • Mat对象创建

(1)cv::Mat::Mat 构造函数     

Mat M(2,2,CV_8UC3, Scalar(0,0,255))     其中前两个参数分别表示行(row)跟列(column)、第三个CV_8UC3中的8表示每个通道占8位、U表示无符号、C表示Char类型、3表示通道数目是3,第四个参数是向量表示初始化每个像素值是多少,向量长度对应通道数目一致

(2)cv::Mat::create 创建多维数组     

int sz[3] = {2,2,2};         Mat  L(3,sz, CV_8UC1, Scalar::all(0)); 一般我们是用不到这个的,进行了解一下就行.

  • 一些代码
    #include <opencv2/opencv.hpp>
    #include <iostream>
    
    using namespace std;
    using namespace cv;
    
    int main(int argc, char** argv) {
    	Mat src;
    	src = imread("C:\\Users\\td\\Desktop\\zhu.jpg");
    	if (src.empty()) {
    		cout << "could not load image..." << endl;
    		return -1;
    	}
    	namedWindow("input", CV_WINDOW_AUTOSIZE);
    	imshow("input", src);
    
    	/*Mat dst;
    	dst = Mat(src.size(), src.type());
    	dst = Scalar(127, 0, 255);
    	namedWindow("output", CV_WINDOW_AUTOSIZE);
    	imshow("output", dst);*/
    	Mat dst;
    	//src.copyTo(dst);
    	namedWindow("output", CV_WINDOW_AUTOSIZE);
    
    	cvtColor(src, dst, CV_BGR2GRAY);
    	printf("input image channels : %d\n", src.channels());
    	printf("output image channels : %d\n", dst.channels());
    
    	int cols = dst.cols;
    	int rows = dst.rows;
    
    	printf("rows : %d cols : %d\n", rows, cols);
    	const uchar* firstRow = dst.ptr<uchar>(0);
    	printf("fist pixel value : %d\n", *firstRow);
    
    	Mat M(100, 100, CV_8UC1, Scalar(127));
    	//cout << "M =" << endl << M << endl;
    
    	Mat m1;
    	m1.create(src.size(), src.type());
    	m1 = Scalar(0, 0, 255);
    
    	Mat csrc;
    	Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
    	filter2D(src, csrc, -1, kernel);
    
    	Mat m2 = Mat::eye(2, 2, CV_8UC1);
    	cout << "m2 =" << endl << m2 << endl;
    
    	Mat m2 = Mat::zeros(2, 2, CV_8UC1);//纯黑的图片
    
    	imshow("output", m2);
    	waitKey(0);
    	return 0;
    }

图像操作

  • 读取像素

读一个GRAY像素点的像素值(CV_8UC1)

Scalar intensity = img.at<uchar>(y, x); 或者 Scalar intensity = img.at<uchar>(Point(x, y));

读一个RGB像素点的像素值

Vec3f intensity = img.at<Vec3f>(y, x); float blue = intensity.val[0]; float green = intensity.val[1]; float red = intensity.val[2];

  • 修改像素值
  • 灰度图像

img.at<uchar>(y, x) = 128;

RGB三通道图像

img.at<Vec3b>(y,x)[0]=128; // blue

img.at<Vec3b>(y,x)[1]=128; // green

img.at<Vec3b>(y,x)[2]=128; // red

空白图像赋值         img = Scalar(0);

ROI选择         Rect r(10, 10, 100, 100);         Mat smallImg = img(r);

  • 代码示例:
    #include <opencv2/core/core.hpp> 
    #include <opencv2/imgcodecs.hpp> 
    #include <opencv2/opencv.hpp>
    #include <opencv2/highgui/highgui.hpp>
    #include <iostream>
    
    using namespace cv;
    using namespace std;
    int main(int argc, char** args) {
    	Mat image = imread("C:/Users/td/Desktop/zhu.jpg", IMREAD_COLOR);
    	if (image.empty()) {
    		cout << "could not find the image resource..." << std::endl;
    		return -1;
    	}
    
    	Mat grayImg;
    	Mat dst;
    	cvtColor(image, grayImg, COLOR_BGR2GRAY);
    	 //单通道的一个像素提取过程
    	int height = image.rows;
    	int width = image.cols;
    	int channels = image.channels();
    	printf("height=%d width=%d channels=%d", height, width, channels);
    	for (int row = 0; row < height; row++) {
    	for (int col = 0; col < width; col++) {
    	int grey = image.at<uchar>(row,col);
    	}
    	}
    	
    	//三通道的像素提取过程
    	int height = image.rows;
    	int width = image.cols;
    	int channels = image.channels();
    	printf("height=%d width=%d channels=%d", height, width, channels);
    	for (int row = 0; row < height; row++) {
    	for (int col = 0; col < width; col++) {
    	if (channels == 3) {
    	int b = image.at<Vec3b>(row, col)[0]; // blue
    	int g = image.at<Vec3b>(row, col)[1]; // green
    	int r = image.at<Vec3b>(row, col)[2]; // red
    	}
    	}
    	}
    	bitwise_not(image,dst);//将255-现在的像素值得到相应的结果.
    }

图像混合

  • 理论-线性混合操作

上述表示是两幅图像进行一个合并的过程,其中a代表着相应的权重,上述是线型混合理论.

  •  API:addWeighted

参数1:输入图像Mat – src1

参数2:输入图像src1的alpha值 权重

参数3:输入图像Mat – src2

参数4:输入图像src2的beta值 权重

参数5:gamma值:校验值

参数6:输出混合图像

注意点:两张图像的大小和类型必须一致才可以 

调整图像亮度与对比度

  • 理论

 像素变换 – 点操作 (调整图像亮度和对比度属于像素变换-点操作)

 邻域操作 – 区域

  • API回顾

Mat new_image = Mat::zeros( image.size(), image.type() );  创建一张跟原图像大小和类型一致的空白图像、像素值初始化为0  

saturate_cast<uchar>(value)确保值大小范围为0~255之间

Mat.at<Vec3b>(y,x)[index]=value 给每个像素点每个通道赋值

  • 代码实现
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:/Users/td/Desktop/zhu.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	char input_win[] = "input image";
	cvtColor(src, src, CV_BGR2GRAY);
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	imshow(input_win, src);

	// contrast and brigthtness changes 
	int height = src.rows;
	int width = src.cols;
	dst = Mat::zeros(src.size(), src.type());
	float alpha = 1.2;//对比度
	float beta = 30;//调节亮度的关键参数

	Mat m1;
	src.convertTo(m1, CV_32F);
	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			if (src.channels() == 3) {
				float b = m1.at<Vec3f>(row, col)[0];// blue
				float g = m1.at<Vec3f>(row, col)[1]; // green
				float r = m1.at<Vec3f>(row, col)[2]; // red

													 // output
				dst.at<Vec3b>(row, col)[0] = saturate_cast<uchar>(b*alpha + beta);
				dst.at<Vec3b>(row, col)[1] = saturate_cast<uchar>(g*alpha + beta);
				dst.at<Vec3b>(row, col)[2] = saturate_cast<uchar>(r*alpha + beta);
			}
			else if (src.channels() == 1) {
				float v = src.at<uchar>(row, col);
				dst.at<uchar>(row, col) = saturate_cast<uchar>(v*alpha + beta);
			}
		}
	}

	char output_title[] = "contrast and brightness change demo";
	namedWindow(output_title, CV_WINDOW_AUTOSIZE);
	imshow(output_title, dst);

	waitKey(0);
	return 0;
}

绘制形状与文字

  • cv::Point与cv::Scalar

Point表示2D平面上一个点x,y     Point p;     p.x = 10;     p.y = 8;      or     p = Pont(10,8);

Scalar表示四个元素的向量     Scalar(a, b, c);// a = blue, b = green, c = red表示RGB三个通道

  • 测试代码
#include <opencv2/opencv.hpp>
#include <iostream>

	using namespace std;
	using namespace cv;
	Mat bgImage;
	const char* drawdemo_win = "draw shapes and text demo";
	void MyLines();
	void MyRectangle();
	void MyEllipse();
	void MyCircle();
	void MyPolygon();
	void RandomLineDemo();
	int main(int argc, char** argv) {
		bgImage = imread("C:\\Users\\td\\Desktop/li.jpg");
		if (!bgImage.data) {
			printf("could not load image...\n");
			return -1;
		}
	    MyLines();
		MyRectangle();
		MyEllipse();
		MyCircle();
		MyPolygon();
		//参数的含义                                      代表着不同的字体         放缩系数                 
		putText(bgImage, "Hello OpenCV", Point(300, 300), CV_FONT_HERSHEY_COMPLEX, 1.0, Scalar(12, 23, 200), 3, 8);
		namedWindow(drawdemo_win, CV_WINDOW_AUTOSIZE);
		imshow(drawdemo_win, bgImage);

		RandomLineDemo();
		waitKey(0);
		return 0;
	}

	void MyLines() {//执行一个划线的操作
		Point p1 = Point(20, 30);
		Point p2;
		p2.x = 400;
		p2.y = 400;
		Scalar color = Scalar(0, 0, 255);
		line(bgImage, p1, p2, color, 1, LINE_AA);//LINE_AA的含义是进行一个反锯齿操作,LINE_8是进行一个常规的划线操作
	}

	void MyRectangle() {//画出矩形
		Rect rect = Rect(200, 100, 300, 300);//参数1 参数2是起始位置,参数3 参数4是宽高
		Scalar color = Scalar(255, 0, 0);
		rectangle(bgImage, rect, color, 2, LINE_8);//参数4是线宽
	}

	void MyEllipse() {//椭圆
		Scalar color = Scalar(0, 255, 0);
		//下面参数的含义
		//     图像      中心点位置                                 长                    高                 角度  0-360  颜色  线宽  
		ellipse(bgImage, Point(bgImage.cols / 2, bgImage.rows / 2), Size(bgImage.cols / 4, bgImage.rows / 8), 90, 0, 360, color, 2, LINE_8);
	}

	void MyCircle() {//圆
		Scalar color = Scalar(0, 255, 255);
		Point center = Point(bgImage.cols / 2, bgImage.rows / 2);
		circle(bgImage, center, 150, color, 2, 8);//参数3是半径长度  
	}

	void MyPolygon() {//多边形
		Point pts[1][5];
		pts[0][0] = Point(100, 100);
		pts[0][1] = Point(100, 200);
		pts[0][2] = Point(200, 200);
		pts[0][3] = Point(200, 100);
		pts[0][4] = Point(100, 100);

		const Point* ppts[] = { pts[0] };
		int npt[] = { 5 };
		Scalar color = Scalar(255, 12, 255);

		fillPoly(bgImage, ppts, npt, 1, color, 8);//参数4是轮廓的含义
	}

	void RandomLineDemo() {
		RNG rng(12345);//随机的函数,给他一个种子
		Point pt1;
		Point pt2;
		Mat bg = Mat::zeros(bgImage.size(), bgImage.type());//生成一个纯黑色的图片,在这个纯黑色的图片之中进行相应的操作.
		namedWindow("random line demo", CV_WINDOW_AUTOSIZE);
		for (int i = 0; i < 100000; i++) {
			//生成随机数的过程
			pt1.x = rng.uniform(0, bgImage.cols);
			pt2.x = rng.uniform(0, bgImage.cols);
			pt1.y = rng.uniform(0, bgImage.rows);
			pt2.y = rng.uniform(0, bgImage.rows);

			Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));//给定一个随机的颜色
			if (waitKey(50) > 0) {
				break;
			}
			line(bg, pt1, pt2, color, 1, 8);
			imshow("random line demo", bg);
		}
	}


模糊图像一

  • 原理

Smooth/Blur 是图像处理中最简单和常用的操作之一

使用该操作的原因之一就为了给图像预处理时候减低噪声

使用Smooth/Blur操作其背后是数学的卷积计算

通常这些卷积算子计算都是线性操作,所以又叫线性滤波

  • 过程上面的边缘处理过程是需要进行使用插值处理
  •  模糊原理
  1. 归一化盒子滤波(均值滤波)

  2. 高斯滤波

  •  相关API

        均值模糊     - blur(Mat src, Mat dst, Size(xradius, yradius), Point(-1,-1));

        高斯模糊     - GaussianBlur(Mat src, Mat dst, Size(11, 11), sigmax, sigmay); 其中Size(x, y), x, y 必须是正数而且是奇数

  •  代码示例
#include <opencv2/opencv.hpp> 
#include <iostream> 
using namespace cv;

int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	char input_title[] = "input image";
	char output_title[] = "blur image";
	namedWindow(input_title, CV_WINDOW_AUTOSIZE);
	namedWindow(output_title, CV_WINDOW_AUTOSIZE);
	imshow(input_title, src);

	//均值模糊
	blur(src, dst, Size(11, 12), Point(-1, -1)); //这里的size(15,1)就是相当于电影之中的武打片,模糊
	imshow(output_title, dst);

	//高斯模糊
	Mat gblur;
	GaussianBlur(src, gblur, Size(11, 11), 11, 11);
	imshow("gaussian blur", gblur);

	waitKey(0);
	return 0;
}
  •  结果

模糊图像二

  • 中值滤波

统计排序滤波器

中值对椒盐噪声有很好的抑制作用(就像是一个图像上面撒了一些白点和黑点)

  •  双边滤波

均值模糊无法克服边缘像素信息丢失缺陷。原因是均值滤波是基于平均权重

高斯模糊部分克服了该缺陷,但是无法完全避免,因为没有考虑像素值的不同(只是考虑了相应的空间之间的不用,但是没有考虑像素的不同)

高斯双边模糊 – 是边缘保留的滤波方法,避免了边缘信息丢失,保留了图像轮廓不变(边缘保留的)

高斯滤波是高度中心对称的,相应的一个权重是对应的。是需要进行一个中心化的过程,这就是进行一个高斯的过程,比较容易进行理解。就比如说下面的

x=-2,w=0.05; x=-1,w=0.15; x=0,w=0.6; x=1,w=0.15; x=1,w=0.15; x=2,w=0.05;

  •  API的调用

中值模糊medianBlur(Mat src, Mat dest, ksize)

双边模糊bilateralFilter(src, dest, d=15, 150, 3);(有一个数学公式,一头雾水,就不看了)

- 15 –计算的半径,半径之内的像数都会被纳入计算,如果提供-1 则根据sigma space参数取值     

- 150 – sigma color 决定多少差值之内的像素会被计算      

- 3 – sigma space 如果d的值大于0则声明无效,否则根据它来计算d值 中值模糊的ksize大小必须是大于1而且必须是奇数。

  • 代码
#include <opencv2/opencv.hpp> 
#include <iostream> 
		using namespace cv;

		int main(int argc, char** argv) {
			Mat src, dst;
			src = imread("C:\\Users\\td\\Desktop/li.jpg");
			if (!src.data) {
				printf("could not load image...\n");
				return -1;
			}
			namedWindow("input image", CV_WINDOW_AUTOSIZE);
			imshow("input image", src);

			//medianBlur(src, dst, 3);//中值滤波 3*3类型
			bilateralFilter(src, dst, 15, 100, 5);//轮廓是还在的,只是模糊了一下,如果要是使用高斯滤波的话GaussianBlur进行处理的话,会变成更加模糊.双边的会更加好一点
			namedWindow("BiBlur Filter Result", CV_WINDOW_AUTOSIZE);
			imshow("BiBlur Filter Result", dst);

			Mat resultImg;
			Mat kernel = (Mat_<int>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
			filter2D(dst, resultImg, -1, kernel, Point(-1, -1), 0);
			imshow("Final Result", resultImg);

			waitKey(0);
			return 0;

		}

膨胀与腐蚀

  • 形态学操作(morphology operators)-膨胀

图像形态学操作 – 基于形状的一系列图像处理操作的合集,主要是基于集合论基础上的形态学数学

形态学有四个基本操作:腐蚀、膨胀、开、闭

膨胀与腐蚀是图像处理中最常用的形态学操作手段

  • 膨胀

跟卷积操作类似,假设有图像A和结构元素B,结构元素B在A上面移动,其中B定义其中心为锚点,计算B覆盖下A的最大像素值用来替换锚点的像素,其中B作为结构体可以是任意形状

  • 腐蚀

腐蚀跟膨胀操作的过程类似,唯一不同的是以最小值替换锚点重叠下图像的像素值

  •  API

getStructuringElement(int shape, Size ksize, Point anchor)  

- 形状 (MORPH_RECT \MORPH_CROSS \MORPH_ELLIPSE)  

- 大小 必须是奇数  

- 锚点 默认是Point(-1, -1)意思就是中心像素

dilate(src, dst, kernel)

  •  调整结构元素的大小

TrackBar – createTrackbar(const String & trackbarname, const String winName,  int* value, int count, Trackbarcallback func, void* userdata=0)

其中最中要的是 callback 函数功能。如果设置为NULL就是说只有值update,但是不会调用callback的函数

  • 代码
#include <opencv2/opencv.hpp> 
#include <iostream> 
		using namespace cv;

		Mat src, dst;
		char OUTPUT_WIN[] = "output image";
		int element_size = 3;
		int max_size = 21;
		void CallBack_Demo(int, void*);
		int main(int argc, char** argv) {

			src = imread("C:\\Users\\td\\Desktop/li.jpg");
			if (!src.data) {
				printf("could not load image...\n");
				return -1;
			}
			namedWindow("input image", CV_WINDOW_AUTOSIZE);
			imshow("input image", src);

			namedWindow(OUTPUT_WIN, CV_WINDOW_AUTOSIZE);
			createTrackbar("Element Size :", OUTPUT_WIN, &element_size, max_size, CallBack_Demo);
			CallBack_Demo(0, 0);

			waitKey(0);
			return 0;
		}

		void CallBack_Demo(int, void*) {
			int s = element_size * 2 + 1;
			Mat structureElement = getStructuringElement(MORPH_RECT, Size(s, s), Point(-1, -1));
			dilate(src, dst, structureElement, Point(-1, -1), 1);
			//erode(src, dst, structureElement);
			imshow(OUTPUT_WIN, dst);
			return;
		}

通过代码的运行可以知道,膨胀是进行一个变白的过程,腐蚀是一个图片变黑的过程.

实际的项目之中可以通过中值滤波,消除椒盐噪声,在用一次腐蚀去除掉相应的干扰,最后,加上膨胀将轮廓进行一个放大的过程.

形态学操作

  • 开操作- open

先腐蚀后膨胀:可以去掉小的对象,假设对象是前景色,背景是黑色

文章来源地址https://uudwc.com/A/JeJ

  • 闭操作-close

先膨胀后腐蚀(bin2)  -  可以填充小的洞(fill hole),假设对象是前景色,背景是黑色 

  • 形态学梯度- Morphological Gradient

膨胀减去腐蚀又称为基本梯度(其它还包括-内部梯度、方向梯度) 

  •   顶帽 – top hat

顶帽 是原图像与开操作之间的差值图像

  • 黑帽

黑帽是闭操作图像与源图像的差值图像

  • API调用

morphologyEx(src, dest, CV_MOP_BLACKHAT, kernel);  

- Mat src – 输入图像  

- Mat dest – 输出结果  

- int OPT – CV_MOP_OPEN/ CV_MOP_CLOSE/ CV_MOP_GRADIENT / CV_MOP_TOPHAT/ CV_MOP_BLACKHAT 形态学操作类型

-Mat kernel 结构元素 int Iteration 迭代次数,默认是1

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
	}
	namedWindow("input image", CV_WINDOW_AUTOSIZE);
	imshow("input image", src);
	char output_title[] = "morphology demo";
	namedWindow(output_title, CV_WINDOW_AUTOSIZE);

	//结构元素的大小的选取会影响相应的最后获得的结果
	Mat kernel = getStructuringElement(MORPH_RECT, Size(11, 11), Point(-1, -1));
	
	//开操作:CV_MOP_OPEN	闭操作:CV_MOP_CLOSE		梯度:CV_MOP_GRADIENT	顶帽:CV_MOP_TOPHAT:原图像-开操作图像	黑帽:CV_MOP_BLACKHAT:闭操作与原图像之间的插值图像
	morphologyEx(src, dst, CV_MOP_BLACKHAT, kernel);
	imshow(output_title, dst);

	waitKey(0);
	return 0;
}
  • 结果:图片是贼好看的

形态学操作应用-提取水平线和垂直线

  • 形态学操作的原理

图像形态学操作时候,可以通过自定义的结构元素实现结构元素 对输入图像一些对象敏感、另外一些对象不敏感,这样就会让敏 感的对象改变而不敏感的对象保留输出。通过使用两个最基本的 形态学操作 – 膨胀与腐蚀,使用不同的结构元素实现对输入图像 的操作、得到想要的结果。  

- 膨胀,输出的像素值是结构元素覆盖下输入图像的最大像素值  

- 腐蚀,输出的像素值是结构元素覆盖下输入图像的最小像素值

  • 灰度图像的膨胀
  • 灰度图像的腐蚀
  • 结构元素

上述膨胀与腐蚀过程可以使用任意的结构元素

常见的形状:矩形、园、直线、磁盘形状、砖石形状等各种自定义形状。

  •  提取步骤
  1. 输入图像彩色图像 imread

  2. 转换为灰度图像 – cvtColor

  3. 转换为二值图像 – adaptiveThreshold

  4. 定义结构元素 开操作 (腐蚀+膨胀)

  • 提取 水平与垂直线

  • 转换为二值图像 – adaptiveThreshold      API

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("D:/vcprojects/images/chars.png");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char INPUT_WIN[] = "input image";
	char OUTPUT_WIN[] = "result image";
	namedWindow(INPUT_WIN, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_WIN, src);

	Mat gray_src;
	cvtColor(src, gray_src, CV_BGR2GRAY);
	imshow("gray image", gray_src);
	
	Mat binImg;
	adaptiveThreshold(~gray_src, binImg, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
	imshow("binary image", binImg);

	// 水平结构元素
	Mat hline = getStructuringElement(MORPH_RECT, Size(src.cols / 16, 1), Point(-1, -1));
	// 垂直结构元素
	Mat vline = getStructuringElement(MORPH_RECT, Size(1, src.rows / 16), Point(-1, -1));
	// 矩形结构
	Mat kernel = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));

	Mat temp;
	erode(binImg, temp, kernel);
	dilate(temp, dst, kernel);
	// morphologyEx(binImg, dst, CV_MOP_OPEN, vline);
	bitwise_not(dst, dst);
	//blur(dst, dst, Size(3, 3), Point(-1, -1));
	imshow("Final Result", dst);

	waitKey(0);
	return 0;
}

图像金字塔-上采集与降采集

  • 图像金字塔概念

1. 我们在图像处理中常常会调整图像大小,最常见的就是放大(zoom in)和缩小(zoom out),尽管几何变换也可以实现图像放大和缩小,但是这里我们介绍图像金字塔

2. 一个图像金字塔式一系列的图像组成,最底下一张是图像尺寸最大,最上方的图像尺寸最小,从空间上从上向下看就想一个古代的金字塔。

  •  图像金字塔概念

高斯金子塔 – 用来对图像进行降采样

拉普拉斯金字塔 – 用来重建一张图片根据它的上层降采样图片

  • 高斯金字塔

高斯金子塔是从底向上,逐层降采样得到。

降采样之后图像大小是原图像MxN的M/2 x N/2 ,就是对原图像删除偶数行与列,即得到降采样之后上一层的图片。(降采样的过程就是相当于是进行一个将上层之中的精英进行一个采样的过程)

高斯金子塔的生成过程分为两步:(逐层采样的过程)      

- 对当前层进行高斯模糊      

- 删除当前层的偶数行与列, 即可得到上一层的图像,这样上一层跟下一层相比,都只有它的1/4大小。

  •  高斯不同(Difference of Gaussian-DOG)

定义:就是把同一张图像在不同的参数下做高斯模糊之后的结果相减,得到的输出图像。称为高斯不同(DOG) 高斯不同是图像的内在特征,在灰度图像增强、角点检测中经常用到。

  •  采样相关API

上采样(cv::pyrUp) – zoom in 放大

降采样 (cv::pyrDown) – zoom out 缩小

pyrUp(Mat src, Mat dst, Size(src.cols*2, src.rows*2)) 生成的图像是原图在宽与高各放大两倍 pyrDown(Mat src, Mat dst, Size(src.cols/2, src.rows/2)) 生成的图像是原图在宽与高各缩小1/2

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include "math.h"

using namespace cv;
int main(int agrc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...");
		return -1;
	}

	char INPUT_WIN[] = "input image";
	char OUTPUT_WIN[] = "sample up";
	namedWindow(INPUT_WIN, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_WIN, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_WIN, src);

	// 上采样
	pyrUp(src, dst, Size(src.cols * 2, src.rows * 2));
	imshow(OUTPUT_WIN, dst);

	// 降采样
	Mat s_down;
	pyrDown(src, s_down, Size(src.cols / 2, src.rows / 2));
	imshow("sample down", s_down);

	// DOG
	Mat gray_src, g1, g2, dogImg;
	cvtColor(src, gray_src, CV_BGR2GRAY);
	GaussianBlur(gray_src, g1, Size(5, 5), 0, 0);
	GaussianBlur(g1, g2, Size(5, 5), 0, 0);
	subtract(g1, g2, dogImg, Mat());

	// 归一化显示:线性提亮的过程使用
	normalize(dogImg, dogImg, 255, 0, NORM_MINMAX);
	imshow("DOG Image", dogImg);

	waitKey(0);
	return 0;
}
  • 结果

基本阈值操作

  • 图像阈值(threshold)

阈值是什么?简单点说是把图像分割的标尺,这个标尺是根据什么产生的,阈值产生算法?阈值类型。(Binary segmentation)将像素值看成是苹果的大小, 大于某一个像素可以看成是一个部分,小于一个像素可以看成是另一个像素.这个像素标尺就是可以看成是相应的阈值.

  •  阈值类型一阈值二值化(threshold binary)

左下方的图表示图像像素点Src(x,y)值分布情况,蓝色水平线表示阈值

  •  阈值类型一阈值反二值化(threshold binary Inverted)

左下方的图表示图像像素点Src(x,y)值分布情况,蓝色水平线表示阈值

  • 阈值类型一截断 (truncate)

左下方的图表示图像像素点Src(x,y)值分布情况,蓝色水平线表示阈值

 

  • 阈值类型一阈值取零 (threshold to zero)

左下方的图表示图像像素点Src(x,y)值分布情况,蓝色水平线表示阈值

  • 阈值类型一阈值反取零 (threshold to zero inverted)

左下方的图表示图像像素点Src(x,y)值分布情况,蓝色水平线表示阈值

  •  描述

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
Mat src, gray_src, dst;
int threshold_value = 127;//阈值(这个值是如何知道的??自己设定的)
int threshold_max = 255;
int type_value = 2;
int type_max = 4;

const char* output_title = "binary image";
void Threshold_Demo(int, void*);

int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input image", CV_WINDOW_AUTOSIZE);
	namedWindow(output_title, CV_WINDOW_AUTOSIZE);
	imshow("input image", src);

	createTrackbar("Threshold Value:", output_title, &threshold_value, threshold_max, Threshold_Demo);
	createTrackbar("Type Value:", output_title, &type_value, type_max, Threshold_Demo);
	Threshold_Demo(0, 0);

	waitKey(0);
	return 0;
}

void Threshold_Demo(int, void*) {
	cvtColor(src, gray_src, CV_BGR2GRAY);

	threshold(src, dst, 0, 255, THRESH_TRIANGLE | type_value);//最后的一个参数是相应的阈值处理的方式,自动帮助我们计算阈值
	imshow(output_title, dst);
}

自定义线性滤波

  • 卷积概念

卷积是图像处理中一个操作,是kernel在图像的每个像素上的操作。 Kernel本质上一个固定大小的矩阵数组,其中心点称为锚点(anchor point)

 把kernel放到像素数组之上,求锚点周围覆盖的像素乘积之和(包括锚点),用来替换锚点覆盖下像素点值称为卷积处理。数学表达如下:

  • 卷积操作

Sum = 8x1+6x1+6x1+2x1+8x1+6x1+2x1+2x1+8x1

New pixel = sum / (m*n) 

 

  •  常见的算子

Robert算子

Sobel算子

拉普拉斯算子

  • 自定义卷积模糊

filter2D方法filter2D( Mat src, //输入图像

Mat dst, // 模糊图像

int depth, // 图像深度32/8

Mat kernel, // 卷积核/模板

Point anchor, // 锚点位置

double delta // 计算出来的像素+delta )

其中 kernel是可以自定义的卷积核 

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	int ksize = 0;

	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char INPUT_WIN[] = "input image";
	char OUTPUT_WIN[] = "Custom Blur Filter Result";
	namedWindow(INPUT_WIN, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_WIN, CV_WINDOW_AUTOSIZE);

	imshow(INPUT_WIN, src);

	// Sobel X 方向
	// Mat kernel_x = (Mat_<int>(3, 3) << -1, 0, 1, -2,0,2,-1,0,1);
	// filter2D(src, dst, -1, kernel_x, Point(-1, -1), 0.0);

	// Sobel Y 方向
	// Mat yimg;
	// Mat kernel_y = (Mat_<int>(3, 3) << -1, -2, -1, 0,0,0, 1,2,1);
	// filter2D(src, yimg, -1, kernel_y, Point(-1, -1), 0.0);

	// 拉普拉斯算子
	//Mat kernel_y = (Mat_<int>(3, 3) << 0, -1, 0, -1, 4, -1, 0, -1, 0);
	//filter2D(src, dst, -1, kernel_y, Point(-1, -1), 0.0);
	
	//自动卷积和进行宁一个操作的过程
	int c = 0;
	int index = 0;
	while (true) {
		c = waitKey(500);
		if ((char)c == 27) {// ESC 
			break;
		}
		ksize = 5 + (index % 8) * 2;
		
		//获取初始像素的平方分之一
		Mat kernel = Mat::ones(Size(ksize, ksize), CV_32F) / (float)(ksize * ksize);
		filter2D(src, dst, -1, kernel, Point(-1, -1));
		index++;
		imshow(OUTPUT_WIN, dst);
	}

	// imshow("Sobel Y", yimg);
	return 0;
}

处理边缘

  • 卷积边缘问题

如上所示,在橘色的小格子里面,相应的边缘部分的两行格子是不能够进行处理掉的,以此,卷积的边缘处理方式也是发挥着至关重要的作用的.

图像卷积的时候边界像素,不能被卷积操作,原因在于边界像素没有完全跟kernel重叠,所以当3x3滤波时候有1个像素的边缘没有被处理,5x5滤波的时候有2个像素的边缘没有被处理。

  • 处理边缘的方式

在卷积开始之前增加边缘像素,填充的像素值为0或者RGB黑色,比如3x3在 四周各填充1个像素的边缘,这样就确保图像的边缘被处理,在卷积处理之 后再去掉这些边缘。openCV中默认的处理方法是: BORDER_DEFAULT,此外 常用的还有如下几种:  

- BORDER_CONSTANT – 填充边缘用指定像素值  

- BORDER_REPLICATE – 填充边缘像素用已知的边缘像素值。  

- BORDER_WRAP – 用另外一边的像素来补偿填充

  •  给图像添加边缘API

copyMakeBorder(  

- Mat src, // 输入图像  

- Mat dst, // 添加边缘图像  

- int top, // 边缘长度,一般上下左右都取相同值,  

- int bottom,  

- int left,  

- int right,  

- int borderType // 边缘类型  

- Scalar value )

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	char INPUT_WIN[] = "input image";
	char OUTPUT_WIN[] = "Border Demo";
	namedWindow(INPUT_WIN, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_WIN, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_WIN, src);
	
	int top = (int)(0.05*src.rows);
	int bottom = (int)(0.05*src.rows);
	int left = (int)(0.05*src.cols);
	int right = (int)(0.05*src.cols);

	RNG rng(12345);//定义一个随机数

	int borderType = BORDER_DEFAULT;

	int c = 0;
	while (true) {
	c = waitKey(500);
	// ESC
	if ((char)c == 27) {
	break;
	}
	if ((char)c == 'r') {
	borderType = BORDER_REPLICATE;
	} else if((char)c == 'w') {
	borderType = BORDER_WRAP;
	} else if((char)c == 'c') {
	borderType = BORDER_CONSTANT;
	}
	Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
	copyMakeBorder(src, dst, top, bottom, left, right, borderType, color);
	imshow(OUTPUT_WIN, dst);
	}
	

	GaussianBlur(src, dst, Size(5, 5), 0, 0);
	imshow(OUTPUT_WIN, dst);

	waitKey(0);
	return 0;
}

Sobel算子

  • 卷积应用-图像边缘提取

上图之中就是皮肤和头发之间的像素会发生突然的变迁过程,中间的图是进行一个跃迁的过程,右图是进行一个求导的过程.中间的那一个点,也就是相应的一个拐点.

  • 卷积应用-图像边缘提取

边缘是什么 – 是像素值发生跃迁的地方,是图像的显著特征之一,在图像特征提取、对象检测、模式识别等方面都有重要的作用。

如何捕捉/提取边缘 – 对图像求它的一阶导数       

delta =  f(x) – f(x-1), delta越大,说明像素在X方向变化越大,边缘信号越强.

  • Sobel算子

是离散微分算子(discrete differentiation operator),用来计算图像灰度的近似梯度

Soble算子功能集合高斯平滑和微分求导.

又被称为一阶微分算子,求导算子,在水平和垂直两个方向上求导,得到图像X方法与Y方向梯度图像.

求取导数的近似值,kernel=3时不是很准确,OpenCV使用改进版本Scharr函数,算子如下:

  •  API说明cv::Sobel

cv::Sobel (

InputArray Src // 输入图像

OutputArray dst// 输出图像,大小与输入图像一致

int depth // 输出图像深度.

int dx.  // X方向,几阶导数

int dy // Y方向,几阶导数.

int ksize, SOBEL算子kernel大小,必须是1、3、5、7、

double scale  = 1,//放大或者进行缩小的过程

double delta = 0

int borderType = BORDER_DEFAULT )

 当我们输入的是一个灰度图像,会发现input depth是位于0-255之间,在这个范围之中的时候,选择进行输出的过程需要进行一个处理,我们将处理的结果进行一个更大的输出,就避免了精度的缺失.

  •  API说明cv::Scharr
cv::Scharr (
InputArray Src // 输入图像
OutputArray dst// 输出图像,大小与输入图像一致
int depth // 输出图像深度. 
Int dx.  // X方向,几阶导数
int dy // Y方向,几阶导数. 
double scale  = 1
double delta = 0
int borderType = BORDER_DEFAULT
)
  •  其它API

GaussianBlur( src, dst, Size(3,3), 0, 0, BORDER_DEFAULT );

cvtColor( src,  gray, COLOR_RGB2GRAY );

addWeighted( A, 0.5,B, 0.5, 0, AB);

convertScaleAbs(A, B)// 计算图像A的像素绝对值,输出到图像B

  • 代码过程讲解

高斯平滑处理 - 转灰度 - 求梯度X和Y - 振幅图像

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char INPUT_TITLE[] = "input image";
	char OUTPUT_TITLE[] = "sobel-demo";
	namedWindow(INPUT_TITLE, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_TITLE, src);

	Mat gray_src;
	GaussianBlur(src, dst, Size(3, 3), 0, 0);//高斯模糊
	cvtColor(dst, gray_src, CV_BGR2GRAY);//转化为一张灰度图像
	imshow("gray image", gray_src);

	Mat xgrad, ygrad;
	Scharr(gray_src, xgrad, CV_16S, 1, 0);//X方向的一阶导数
	Scharr(gray_src, ygrad, CV_16S, 0, 1);//Y方向的一阶导数

	// Sobel(gray_src, xgrad, CV_16S, 1, 0, 3);
	// Sobel(gray_src, ygrad, CV_16S, 0, 1, 3);
	convertScaleAbs(xgrad, xgrad);//变成正数
	convertScaleAbs(ygrad, ygrad);
	imshow("xgrad", xgrad);
	imshow("ygrad", ygrad);

	Mat xygrad = Mat(xgrad.size(), xgrad.type());

	//测试过程之中防止出现截断进行的操作  
	//需要将输入的类型与输出的类型是保持一致的
	printf("type : %d\n", xgrad.type());//0 uchar 类型
	int width = xgrad.cols;
	int height = ygrad.rows;
	for (int row = 0; row < height; row++) {
		for (int col = 0; col < width; col++) {
			int xg = xgrad.at<uchar>(row, col);
			int yg = ygrad.at<uchar>(row, col);
			int xy = xg + yg;
			xygrad.at<uchar>(row, col) = saturate_cast<uchar>(xy);
		}
	}
	//addWeighted(xgrad, 0.5, ygrad, 0.5, 0, xygrad);
	imshow(OUTPUT_TITLE, xygrad);

	waitKey(0);
	return 0;
}
  • 结果示意图

Laplance算子

  • 理论

解释:在二阶导数的时候,最大变化处的值为零即边缘是零值。通过二阶 导数计算,依据此理论我们可以计算图像二阶导数,提取边缘。

  • Laplance算子

拉普拉斯算子(Laplance operator) 

  •  处理流程

高斯模糊 – 去噪声GaussianBlur()

转换为灰度图像cvtColor()

拉普拉斯 – 二阶导数计算Laplacian()

取绝对值convertScaleAbs()

显示结果

  •  API使用cv::Laplacian

Laplacian( InputArray src,

OutputArray dst, int depth, //深度CV_16S

int kisze, // 3

double scale = 1,

double delta =0.0,

int borderType = 4 )

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image");
	}
	char input_title[] = "input image";
	char output_title[] = "Laplaiance Result";
	namedWindow(input_title, CV_WINDOW_AUTOSIZE);
	imshow(input_title, src);

	Mat gray_src, edge_image;
	GaussianBlur(src, dst, Size(3, 3), 0, 0);
	cvtColor(dst, gray_src, CV_BGR2GRAY);

	Laplacian(gray_src, edge_image, CV_16S, 3);
	convertScaleAbs(edge_image, edge_image);//变成8位的

	threshold(edge_image, edge_image, 0, 255, THRESH_OTSU | THRESH_BINARY);//对于边缘的图像进行相应的处理,进行一个二值化的过程,自动寻找相应的阈值.
	namedWindow(output_title, CV_WINDOW_AUTOSIZE);
	imshow(output_title, edge_image);

	waitKey(0);
	return 0;
}
  • 效果图

Canny边缘检测

  • Canny算法介绍

Canny是边缘检测算法,在1986年提出的,  是一个很好的边缘检测器,  很常用也很实用的图像处理方法.

  • Canny算法介绍 – 五步 in cv::Canny

高斯模糊 - GaussianBlur

灰度转换 - cvtColor

计算梯度 – Sobel/Scharr

非最大信号抑制

高低阈值输出二值图像

  • Canny算法介绍 - 非最大信号抑制

  •  Canny算法介绍-高低阈值输出二值图像

T1, T2为阈值,凡是高于T2的都保留,凡是小于T1都丢弃,从高于T2的像素出发,凡是大于T1而且相互连接的,都保留。最终得到一个输出二值图像。

推荐的高低阈值比值为 T2: T1 = 3:1/2:1其中T2为高阈值,T1为低阈值

  • API – cv::Canny

Canny( InputArray src, // 8-bit的输入图像

OutputArray edges,// 输出边缘图像, 一般都是二值图像,背景是黑色

double threshold1,// 低阈值,常取高阈值的1/2或者1/3

double threshold2,// 高阈值 int aptertureSize,//

Soble算子的size,通常3x3,取值3

bool L2gradient // 选择 true表示是L2来归一化,否则用L1归一化 )

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
Mat src, gray_src, dst;
int t1_value = 50;
int max_value = 255;
const char* OUTPUT_TITLE = "Canny Result";
void Canny_Demo(int, void*);
int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char INPUT_TITLE[] = "input image";
	namedWindow(INPUT_TITLE, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_TITLE, src);

	cvtColor(src, gray_src, CV_BGR2GRAY);
	createTrackbar("Threshold Value:", OUTPUT_TITLE, &t1_value, max_value, Canny_Demo);
	Canny_Demo(0, 0);

	waitKey(0);
	return 0;
}

void Canny_Demo(int, void*) {
	Mat edge_output;
	blur(gray_src, gray_src, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);
	Canny(gray_src, edge_output, t1_value, t1_value * 2, 3, false);

	//dst.create(src.size(), src.type());
	//src.copyTo(dst, edge_output);//参数少一些东西,不尽兴拷贝像素
	// (edge_output, edge_output);
	imshow(OUTPUT_TITLE, ~edge_output);
}
  • 效果

霍夫变换-直线(这一个项目之中可能会用到,需要特别注意)

  • 霍夫直线变换介绍

Hough Line Transform用来做直线检测

前提条件 – 边缘检测已经完成

平面空间到极坐标空间转换

  • 霍夫直线变换介绍

  •  霍夫直线变换介绍

对于任意一条直线上的所有点来说

变换到极坐标中,从[0~360]空间,可以得到r的大小

属于同一条直线上点在极坐标空(r, theta)必然在一个点上有最强的信号出现,根据此反算到平面坐标中就可以得到直线上各点的像素坐标, 从而得到直线.

  •  从平面坐标变换到霍夫空间(极坐标)

  • API学习

标准的霍夫变换 cv::HoughLines从平面坐标转换到霍夫空间,最终输出是

表示极坐标空间

霍夫变换直线概率 cv::HoughLinesP最终输出是直线的两个点

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;

int main(int argc, char** argv) {
	Mat src, src_gray, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char INPUT_TITLE[] = "input image";
	char OUTPUT_TITLE[] = "hough-line-detection";
	namedWindow(INPUT_TITLE, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_TITLE, src);

	// extract edge 边缘检测,直接提取边缘
	Canny(src, src_gray, 150, 200);//确定是8位的就可以
	cvtColor(src_gray, dst, CV_GRAY2BGR);
	imshow("edge image", src_gray);

	vector<Vec2f> lines;//定义一个霍夫变换的直线,存放直线的数组
	HoughLines(src_gray, lines, 1, CV_PI / 180, 150, 0, 0);
	for (size_t i = 0; i < lines.size(); i++) {
		float rho = lines[i][0]; // 极坐标中的r长度
		float theta = lines[i][1]; // 极坐标中的角度
		Point pt1, pt2;
		double a = cos(theta), b = sin(theta);
		double x0 = a*rho, y0 = b*rho;
		// 转换为平面坐标的四个点
		pt1.x = cvRound(x0 + 1000 * (-b));
		pt1.y = cvRound(y0 + 1000 * (a));
		pt2.x = cvRound(x0 - 1000 * (-b));
		pt2.y = cvRound(y0 - 1000 * (a));
		line(dst, pt1, pt2, Scalar(0, 0, 255), 1, CV_AA);
	}

	/*
	vector<Vec4f> plines;
	HoughLinesP(src_gray, plines, 1, CV_PI / 180.0, 10, 0, 10);
	Scalar color = Scalar(0, 0, 255);
	for (size_t i = 0; i < plines.size(); i++) {
	Vec4f hline = plines[i];
	line(dst, Point(hline[0], hline[1]), Point(hline[2], hline[3]), color, 3, LINE_AA);
	}*/
	imshow(OUTPUT_TITLE, dst);

	waitKey(0);
	return 0;
}
  • 结果示意图

霍夫圆变换

  • 霍夫圆检测原理

 

  • 霍夫圆变换原理

从平面坐标到极坐标转换三个参数

假设平面坐标的任意一个圆上的点,转换到极坐标中: 处有最大值,霍夫变换正是利用这个原理实现圆的检测。

  • 相关API cv::HoughCircles

因为霍夫圆检测对噪声比较敏感,所以首先要对图像做中值滤波。

基于效率考虑,Opencv中实现的霍夫变换圆检测是基于图像梯度的实现,分为两步:     

1. 检测边缘,发现可能的圆心     

2. 基于第一步的基础上从候选圆心开始计算最佳半径大小

  • HoughCircles参数说明
  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
using namespace std;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	char INPUT_TITLE[] = "input image";
	char OUTPUT_TITLE[] = "hough circle demo";
	namedWindow(INPUT_TITLE, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_TITLE, src);

	// 中值滤波:防止噪点的产生
	Mat moutput;
	medianBlur(src, moutput, 3);
	cvtColor(moutput, moutput, CV_BGR2GRAY);

	// 霍夫圆检测
	vector<Vec3f> pcircles;
	HoughCircles(moutput, pcircles, CV_HOUGH_GRADIENT, 1, 10, 100, 30, 5, 50);
	src.copyTo(dst);
	for (size_t i = 0; i < pcircles.size(); i++) {
		Vec3f cc = pcircles[i];
		circle(dst, Point(cc[0], cc[1]), cc[2], Scalar(0, 0, 255), 2, LINE_AA);
		circle(dst, Point(cc[0], cc[1]), 2, Scalar(198, 23, 155), 2, LINE_AA);
	}
	imshow(OUTPUT_TITLE, dst);

	waitKey(0);
	return 0;
}
  • 效果图

像素重映射

  • 什么是像素重映射

简单点说就是把输入图像中各个像素按照一定的规则映射到另外一张图像的对应位置上去,形成一张新的图像。

  •  什么是像素重映射

假设有映射函数

  • API介绍cv::remap

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
Mat src, dst, map_x, map_y;
const char* OUTPUT_TITLE = "remap demo";
int index = 0;
void update_map(void);

int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	char input_win[] = "input image";
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow(input_win, src);

	//32位的单通道的图像
	map_x.create(src.size(), CV_32FC1);//32位F类型
	map_y.create(src.size(), CV_32FC1);

	int c = 0;
	while (true) {
		c = waitKey(500);
		if ((char)c == 27) {
			break;
		}
		index = c % 4;
		update_map();
		remap(src, dst, map_x, map_y, INTER_LINEAR, BORDER_CONSTANT, Scalar(0, 255, 255));
		imshow(OUTPUT_TITLE, dst);
	}

	return 0;
}

void update_map(void) {
	
	//取得像素的过程
	for (int row = 0; row < src.rows; row++) {
		for (int col = 0; col < src.cols; col++) {
			switch (index) {
			case 0:
				if (col >= (src.cols * 0.25) && col <= (src.cols*0.75) && row >= (src.rows*0.25) && row <= (src.rows*0.75)) {
					map_x.at<float>(row, col) = 2 * (col - (src.cols*0.25));
					map_y.at<float>(row, col) = 2 * (row - (src.rows*0.25));
				}
				else {
					map_x.at<float>(row, col) = 0;
					map_y.at<float>(row, col) = 0;
				}
				break;
			case 1:
				map_x.at<float>(row, col) = (src.cols - col - 1);
				map_y.at<float>(row, col) = row;
				break;
			case 2:
				map_x.at<float>(row, col) = col;
				map_y.at<float>(row, col) = (src.rows - row - 1);
				break;
			case 3:
				map_x.at<float>(row, col) = (src.cols - col - 1);
				map_y.at<float>(row, col) = (src.rows - row - 1);
				break;
			}

		}
	}

}

直方图均衡化

  • 什么是直方图(Histogram)

  •  什么是直方图

图像直方图,是指对整个图像像在灰度范围内的像素值(0~255)统计出现频率次数,据此生成的直方图,称为图像直方图-直方图。直方图反映了图像灰度的分布情况, 是图像的统计学特征。

  • 图像直方图

由上面的直方图可见,我们是一般选用127作为图像的二值化的点.(原因是如上面所示)

  •  直方图均衡化

一种提高图像对比度的方法,拉伸图像灰度值范围。

拉刺的一个过程.

  •  直方图均衡化

如何实现,通过上一课中的remap我们知道可以将图像灰度分布从一个分布映射到另外一个分布,然后在得到映射后的像素值即可。

  • API说明cv::equalizeHist

equalizeHist(

InputArray src,//输入图像,必须是8-bit的单通道图像

OutputArray dst// 输出结果 )

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace cv;
int main(int argc, char** argv) {
	Mat src, dst;
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}

	cvtColor(src, src, CV_BGR2GRAY);

	//进行一个均衡化的过程
	equalizeHist(src, dst);
	char INPUT_T[] = "input image";
	char OUTPUT_T[] = "result image";
	namedWindow(INPUT_T, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_T, CV_WINDOW_AUTOSIZE);

	imshow(INPUT_T, src);
	imshow(OUTPUT_T, dst);

	waitKey(0);
	return 0;
}
  • 效果示意图

直方图计算

  • 直方图概念

  • 直方图概念 

上述直方图概念是基于图像像素值,其实对图像梯度、每个像素的角度、等一切图像的属性值,我们都可以建立直方图。这个才是直方图的概念真正意义,不过是基于图像像素灰度直方图是最常见的。

直方图最常见的几个属性:

 - dims 表示维度,对灰度图像来说只有一个通道值dims=1  

- bins 表示在维度中子区域大小划分,bins=256,划分为256个级别  

- range 表示值得范围,灰度值范围为[0~255]之间

  • API

  •  代码演示
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
	Mat src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	char INPUT_T[] = "input image";
	char OUTPUT_T[] = "histogram demo";
	namedWindow(INPUT_T, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_T, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_T, src);

	// 分通道显示
	vector<Mat> bgr_planes;
	split(src, bgr_planes);
	imshow("single channel demo", bgr_planes[0]);//这里是相应的一个单通道的输出

	// 计算直方图
	int histSize = 256;
	float range[] = { 0, 256 };
	const float *histRanges = { range };
	Mat b_hist, g_hist, r_hist;
	calcHist(&bgr_planes[0], 1, 0, Mat(), b_hist, 1, &histSize, &histRanges, true, false);
	calcHist(&bgr_planes[1], 1, 0, Mat(), g_hist, 1, &histSize, &histRanges, true, false);
	calcHist(&bgr_planes[2], 1, 0, Mat(), r_hist, 1, &histSize, &histRanges, true, false);

	// 归一化
	int hist_h = 400;
	int hist_w = 512;
	int bin_w = hist_w / histSize;
	Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));
	normalize(b_hist, b_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
	normalize(g_hist, g_hist, 0, hist_h, NORM_MINMAX, -1, Mat());
	normalize(r_hist, r_hist, 0, hist_h, NORM_MINMAX, -1, Mat());

	// render histogram chart
	for (int i = 1; i < histSize; i++) {
		line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(b_hist.at<float>(i - 1))),
			Point((i)*bin_w, hist_h - cvRound(b_hist.at<float>(i))), Scalar(255, 0, 0), 2, LINE_AA);

		line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(g_hist.at<float>(i - 1))),
			Point((i)*bin_w, hist_h - cvRound(g_hist.at<float>(i))), Scalar(0, 255, 0), 2, LINE_AA);

		line(histImage, Point((i - 1)*bin_w, hist_h - cvRound(r_hist.at<float>(i - 1))),
			Point((i)*bin_w, hist_h - cvRound(r_hist.at<float>(i))), Scalar(0, 0, 255), 2, LINE_AA);
	}
	imshow(OUTPUT_T, histImage);

	waitKey(0);
	return 0;
}

直方图比较

  • 直方图比较方法-概述

对输入的两张图像计算得到直方图H1与H2,归一化到相同的尺度空间 然后可以通过计算H1与H2的之间的距离得到两个直方图的相似程度进, 而比较图像本身的相似程度。Opencv提供的比较方法有四种:

Correlation 相关性比较

Chi-Square 卡方比较

Intersection 十字交叉性

Bhattacharyya distance 巴氏距离

  • 直方图比较方法-相关性计算(CV_COMP_CORREL)

  •  直方图比较方法-卡方计算(CV_COMP_CHISQR)

  • 直方图比较方法-十字计算(CV_COMP_INTERSECT)

  •  直方图比较方法-巴氏距离计算(CV_COMP_BHATTACHARYYA )

  • 相关API

  • 过程

首先把图像从RGB色彩空间转换到HSV色彩空间cvtColor(最为敏感,将三通道变为两个通道)

计算图像的直方图,然后归一化到[0~1]之间calcHist和normalize

使用上述四种比较方法之一进行比较compareHist

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

string convertToString(double d);
int main(int argc, char** argv) {
	Mat base, test1, test2;
	Mat hsvbase, hsvtest1, hsvtest2;
	base = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!base.data) {
		printf("could not load image...\n");
		return -1;
	}
	test1 = imread("C:\\Users\\td\\Desktop/li.jpg");
	test2 = imread("C:\\Users\\td\\Desktop/li.jpg");

	cvtColor(base, hsvbase, CV_BGR2HSV);
	cvtColor(test1, hsvtest1, CV_BGR2HSV);
	cvtColor(test2, hsvtest2, CV_BGR2HSV);

	int h_bins = 50; int s_bins = 60;
	int histSize[] = { h_bins, s_bins };
	// hue varies from 0 to 179, saturation from 0 to 255     
	float h_ranges[] = { 0, 180 };
	float s_ranges[] = { 0, 256 };
	const float* ranges[] = { h_ranges, s_ranges };
	// Use the o-th and 1-st channels     
	int channels[] = { 0, 1 };
	MatND hist_base;
	MatND hist_test1;
	MatND hist_test2;

	calcHist(&hsvbase, 1, channels, Mat(), hist_base, 2, histSize, ranges, true, false);
	normalize(hist_base, hist_base, 0, 1, NORM_MINMAX, -1, Mat());

	calcHist(&hsvtest1, 1, channels, Mat(), hist_test1, 2, histSize, ranges, true, false);
	normalize(hist_test1, hist_test1, 0, 1, NORM_MINMAX, -1, Mat());

	calcHist(&hsvtest2, 1, channels, Mat(), hist_test2, 2, histSize, ranges, true, false);
	normalize(hist_test2, hist_test2, 0, 1, NORM_MINMAX, -1, Mat());

	double basebase = compareHist(hist_base, hist_base, CV_COMP_INTERSECT);
	double basetest1 = compareHist(hist_base, hist_test1, CV_COMP_INTERSECT);
	double basetest2 = compareHist(hist_base, hist_test2, CV_COMP_INTERSECT);
	double tes1test2 = compareHist(hist_test1, hist_test2, CV_COMP_INTERSECT);
	printf("test1 compare with test2 correlation value :%f", tes1test2);

	Mat test12;
	test2.copyTo(test12);
	putText(base, convertToString(basebase), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
	putText(test1, convertToString(basetest1), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
	putText(test2, convertToString(basetest2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);
	putText(test12, convertToString(tes1test2), Point(50, 50), CV_FONT_HERSHEY_COMPLEX, 1, Scalar(0, 0, 255), 2, LINE_AA);

	namedWindow("base", CV_WINDOW_AUTOSIZE);
	namedWindow("test1", CV_WINDOW_AUTOSIZE);
	namedWindow("test2", CV_WINDOW_AUTOSIZE);

	imshow("base", base);
	imshow("test1", test1);
	imshow("test2", test2);
	imshow("test12", test12);

	waitKey(0);
	return 0;
}

string convertToString(double d) {
	ostringstream os;
	if (os << d)
		return os.str();
	return "invalid conversion";
}

直方图反向投射

  • 反向投影

反向投影是反映直方图模型在目标图像中的分布情况 简单点说就是用直方图模型去目标图像中寻找是否有相似的对象。

通常用HSV色彩空间的HS两个通道直方图模型

  • 实现步骤

加载图片imread

将图像从RGB色彩空间转换到HSV色彩空间cvtColor

计算直方图和归一化calcHist与normalize

Mat与MatND其中Mat表示二维数组,MatND表示三维或者多维数据,此处均可以用Mat表示。

计算反向投影图像 - calcBackProject

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

Mat src; Mat hsv; Mat hue; 
int bins = 12;
void Hist_And_Backprojection(int, void*);
int main(int argc, char** argv) {
	src = imread("D:/vcprojects/images/t1.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	const char*  window_image = "input image";
	namedWindow(window_image, CV_WINDOW_NORMAL);
	namedWindow("BackProj", CV_WINDOW_NORMAL);
	namedWindow("Histogram", CV_WINDOW_NORMAL);

	cvtColor(src, hsv, CV_BGR2HSV);
	hue.create(hsv.size(), hsv.depth());
	int nchannels[] = { 0, 0 };
	mixChannels(&hsv, 1, &hue, 1, nchannels, 1);

	createTrackbar("Histogram Bins:", window_image, &bins, 180, Hist_And_Backprojection);
	Hist_And_Backprojection(0, 0);

	imshow(window_image, src);
	waitKey(0);
	return 0;
}

void Hist_And_Backprojection(int, void*) {
	float range[] = { 0, 180 };
	const float *histRanges = { range };
	Mat h_hist;
	calcHist(&hue, 1, 0, Mat(), h_hist, 1, &bins, &histRanges, true, false);
	normalize(h_hist, h_hist, 0, 255, NORM_MINMAX, -1, Mat());

	Mat backPrjImage;
	calcBackProject(&hue, 1, 0, h_hist, backPrjImage, &histRanges, 1, true);
	imshow("BackProj", backPrjImage);

	int hist_h = 400;
	int hist_w = 400;
	Mat histImage(hist_w, hist_h, CV_8UC3, Scalar(0, 0, 0));
	int bin_w = (hist_w / bins);
	for (int i = 1; i < bins; i++) {
		rectangle(histImage, 
			Point((i - 1)*bin_w, (hist_h - cvRound(h_hist.at<float>(i - 1) * (400 / 255)))),
			//Point(i*bin_w, (hist_h - cvRound(h_hist.at<float>(i) * (400 / 255)))),
			Point(i*bin_w, hist_h),
			Scalar(0, 0, 255), -1);
	}
	imshow("Histogram", histImage);

	return;
}

模板匹配

  • 模板匹配介绍

  •  模板匹配介绍
  1. 模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。
  2. 所以模板匹配首先需要一个模板图像T(给定的子图像)
  3. 另外需要一个待检测的图像-源图像S
  4. 工作方法,在带检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
  • 模板匹配介绍 – 匹配算法介绍

OpenCV有六种常见的匹配方法,分别是计算平方不同 计算相关性 计算相关系数 计算归一化平方不同 计算归一化相关性 计算归一化相关系数

  • 相关API介绍cv::matchTemplate

  • 相关API介绍cv::matchTemplate

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>

#include <math.h>
using namespace std;
using namespace cv;

Mat src, temp, dst;
int match_method = TM_SQDIFF;
int max_track = 5;
const char* INPUT_T = "input image";
const char* OUTPUT_T = "result image";
const char* match_t = "template match-demo";
void Match_Demo(int, void*);
int main(int argc, char** argv) {
	// 待检测图像
	src = imread("D:/vcprojects/images/flower.png");
	// 模板图像
	temp = imread("D:/vcprojects/images/t2.png");
	if (src.empty() || temp.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow(INPUT_T, CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_T, CV_WINDOW_NORMAL);
	namedWindow(match_t, CV_WINDOW_AUTOSIZE);
	imshow(INPUT_T, temp);
	const char* trackbar_title = "Match Algo Type:";
	createTrackbar(trackbar_title, OUTPUT_T, &match_method, max_track, Match_Demo);
	Match_Demo(0, 0);

	waitKey(0);
	return 0;
}

void Match_Demo(int, void*) {
	//输出的结果,首先定义相应的输出结果的矩阵
	//下面的公式是已经给出的
	int width = src.cols - temp.cols + 1;
	int height = src.rows - temp.rows + 1;
	Mat result(width, height, CV_32FC1);//输出是32位的浮点型

	//进行相应的模板匹配的过程
	matchTemplate(src, temp, result, match_method, Mat());
	//归一化处理 变成是0-1之间的
	normalize(result, result, 0, 1, NORM_MINMAX, -1, Mat());

	//找出相应的位置,找出最小最大值的匹配
	Point minLoc;
	Point maxLoc;
	double min, max;
	src.copyTo(dst);
	Point temLoc;

	minMaxLoc(result, &min, &max, &minLoc, &maxLoc, Mat());
	if (match_method == TM_SQDIFF || match_method == TM_SQDIFF_NORMED) {//这句代码是个什么?????????????
		temLoc = minLoc;
	}
	else {
		temLoc = maxLoc;
	}

	// 绘制矩形
	//目标图像上面进行绘制
	rectangle(dst, Rect(temLoc.x, temLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2, 8);
	rectangle(result, Rect(temLoc.x, temLoc.y, temp.cols, temp.rows), Scalar(0, 0, 255), 2, 8);

	imshow(OUTPUT_T, result);
	imshow(match_t, dst);
}

轮廓发现

  • 轮廓发现(find contour)

轮廓发现是基于图像边缘提取的基础寻找对象轮廓的方法。 所以边缘提取的阈值选定会影响最终轮廓发现结果

  • API 轮廓发现(find contour) 

  • API 轮廓绘制(draw contour) 

  •  代码过程

输入图像转为灰度图像cvtColor

使用Canny进行边缘提取,得到二值图像

使用findContours寻找轮廓

使用drawContours绘制轮廓

  • 代码测试
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

Mat src, dst;
const char* output_win = "findcontours-demo";
int threshold_value = 100;
int threshold_max = 255;
RNG rng;
void Demo_Contours(int, void*);
int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow("input-image", CV_WINDOW_AUTOSIZE);
	namedWindow(output_win, CV_WINDOW_AUTOSIZE);
	imshow("input-image", src);
	cvtColor(src, src, CV_BGR2GRAY);

	const char* trackbar_title = "Threshold Value:";
	//创建一个trackbar的过程,前面已经用了好多了.注意trackbar的使用可以应用到项目之中.
	createTrackbar(trackbar_title, output_win, &threshold_value, threshold_max, Demo_Contours);
	Demo_Contours(0, 0);

	waitKey(0);
	return 0;
}

void Demo_Contours(int, void*) {
	Mat canny_output;
	vector<vector<Point>> contours;
	vector<Vec4i> hierachy;
	//最后的一个参数false是代表着之直接使用绝对值的方式进行相应的求解,但是如果要是使用true,代表着使用开根号的方式进行相应的求解,这种方式进行相应的求解的时候,会发生计算量大的问题
	Canny(src, canny_output, threshold_value, threshold_value * 2, 3, false);
	
	findContours(canny_output, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

	dst = Mat::zeros(src.size(), CV_8UC3);
	RNG rng(12345);
	for (size_t i = 0; i < contours.size(); i++) {
		Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
		drawContours(dst, contours, i, color, 2, 8, hierachy, 0, Point(0, 0));
	}
	imshow(output_win, dst);
}
  • 效果图

凸包

  • 概念介绍

什么是凸包(Convex Hull)?        

在一个多变形边缘或者内部任意两个点的连线都包含在多边形边界或者内部。

正式定义: 包含点集合S中所有点的最小凸多边形称为凸包

上图之中,左边的图是定义为相应的凸包.

  • 概念介绍-Graham扫描算法

首先选择Y方向最低的点作为起始点p0

从p0开始极坐标扫描,依次添加p1….pn(排序顺序是根据极坐标的角度大小,逆时针方向)

对每个点pi来说,如果添加pi点到凸包中导致一个左转向(逆时针方法)

则添加该点到凸包, 反之如果导致一个右转向(顺时针方向)删除该点从凸包中

  • API说明cv::convexHull

  •  代码过程

首先把图像从RGB转为灰度

然后再转为二值图像

在通过发现轮廓得到候选点

凸包API调用

绘制显示。

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;
Mat src, src_gray, dst;
int threshold_value = 100;
int threshold_max = 255;
const char* output_win = "convex hull demo";
void Threshold_Callback(int, void*);
RNG rng(12345);
int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	const char* input_win = "input image";
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	namedWindow(output_win, CV_WINDOW_NORMAL);
	const char* trackbar_label = "Threshold : ";

	cvtColor(src, src_gray, CV_BGR2GRAY);
	blur(src_gray, src_gray, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);
	imshow(input_win, src_gray);

	createTrackbar(trackbar_label, output_win, &threshold_value, threshold_max, Threshold_Callback);
	Threshold_Callback(0, 0);
	waitKey(0);
	return 0;
}

void Threshold_Callback(int, void*) {
	Mat bin_output;
	vector<vector<Point>> contours;
	vector<Vec4i> hierachy;

	threshold(src_gray, bin_output, threshold_value, threshold_max, THRESH_BINARY);
	findContours(bin_output, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

	vector<vector<Point>> convexs(contours.size());
	for (size_t i = 0; i < contours.size(); i++) {
		convexHull(contours[i], convexs[i], false, true);
	}

	// 绘制
	dst = Mat::zeros(src.size(), CV_8UC3);
	vector<Vec4i> empty(0);
	for (size_t k = 0; k < contours.size(); k++) {
		Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
		drawContours(dst, contours, k, color, 2, LINE_8, hierachy, 0, Point(0, 0));
		drawContours(dst, convexs, k, color, 2, LINE_8, empty, 0, Point(0, 0));
	}
	imshow(output_win, dst);

	return;
}

轮廓周围绘制矩形框和圆形框

  •  轮廓周围绘制矩形 -API

基于RDP算法实现,目的是减少多边形轮廓点数

cv::boundingRect(InputArray points)得到轮廓周围最小矩形左上交点坐标和右下角点坐标,绘制一个矩形

cv::minAreaRect(InputArray  points)得到一个旋转的矩形,返回旋转矩形

 cv::minEnclosingCircle(InputArray points, //得到最小区域圆形     

Point2f& center, // 圆心位置     

float& radius)// 圆的半径

cv::fitEllipse(InputArray  points)得到最小椭圆

  • 步骤

首先将图像变为二值图像

发现轮廓,找到图像轮廓

通过相关API在轮廓点上找到最小包含矩形和圆,旋转矩形与椭圆。

绘制它们。

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;
Mat src, gray_src, drawImg;
int threshold_v = 170;
int threshold_max = 255;
const char* output_win = "rectangle-demo";
RNG rng(12345);
void Contours_Callback(int, void*);
int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	cvtColor(src, gray_src, CV_BGR2GRAY);
	blur(gray_src, gray_src, Size(3, 3), Point(-1, -1));

	const char* source_win = "input image";
	namedWindow(source_win, CV_WINDOW_AUTOSIZE);
	namedWindow(output_win, CV_WINDOW_AUTOSIZE);
	imshow(source_win, src);

	createTrackbar("Threshold Value:", output_win, &threshold_v, threshold_max, Contours_Callback);
	Contours_Callback(0, 0);

	waitKey(0);
	return 0;
}

void Contours_Callback(int, void*) {
	Mat binary_output;
	vector<vector<Point>> contours;
	vector<Vec4i> hierachy;
	threshold(gray_src, binary_output, threshold_v, threshold_max, THRESH_BINARY);
	//imshow("binary image", binary_output);
	findContours(binary_output, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));

	vector<vector<Point>> contours_ploy(contours.size());
	vector<Rect> ploy_rects(contours.size());
	vector<Point2f> ccs(contours.size());
	vector<float> radius(contours.size());

	vector<RotatedRect> minRects(contours.size());
	vector<RotatedRect> myellipse(contours.size());

	for (size_t i = 0; i < contours.size(); i++) {
		approxPolyDP(Mat(contours[i]), contours_ploy[i], 3, true);
		ploy_rects[i] = boundingRect(contours_ploy[i]);
		minEnclosingCircle(contours_ploy[i], ccs[i], radius[i]);
		if (contours_ploy[i].size() > 5) {
			myellipse[i] = fitEllipse(contours_ploy[i]);
			minRects[i] = minAreaRect(contours_ploy[i]);
		}
	}

	// draw it
	drawImg = Mat::zeros(src.size(), src.type());
	Point2f pts[4];
	for (size_t t = 0; t < contours.size(); t++) {
		Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
		//rectangle(drawImg, ploy_rects[t], color, 2, 8);
		//circle(drawImg, ccs[t], radius[t], color, 2, 8);
		if (contours_ploy[t].size() > 5) {
			ellipse(drawImg, myellipse[t], color, 1, 8);
			minRects[t].points(pts);
			for (int r = 0; r < 4; r++) {
				line(drawImg, pts[r], pts[(r + 1) % 4], color, 1, 8);
			}
		}
	}

	imshow(output_win, drawImg);
	return;
}

圆形矩

  • 几何矩

 图像中心Center(x0, y0)------质量的中心

  •  API介绍与使用-计算矩cv::moments

  • 步骤

提取图像边缘

发现轮廓

计算每个轮廓对象的矩

计算每个对象的中心、弧长、面积 

  •  代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

Mat src, gray_src;
int threshold_value = 80;
int threshold_max = 255;
const char* output_win = "image moents demo";
RNG rng(12345);
void Demo_Moments(int, void*);
int main(int argc, char** argv) {
	src = imread("C:\\Users\\td\\Desktop/li.jpg");
	if (!src.data) {
		printf("could not load image...\n");
		return -1;
	}
	cvtColor(src, gray_src, CV_BGR2GRAY);
	GaussianBlur(gray_src, gray_src, Size(3, 3), 0, 0);

	char input_win[] = "input image";
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	namedWindow(output_win, CV_WINDOW_AUTOSIZE);
	imshow(input_win, src);

	createTrackbar("Threshold Value : ", output_win, &threshold_value, threshold_max, Demo_Moments);
	Demo_Moments(0, 0);

	waitKey(0);
	return 0;
}

void Demo_Moments(int, void*) {
	Mat canny_output;
	vector<vector<Point>> contours;
	vector<Vec4i> hierachy;

	//提取图像边缘
	Canny(gray_src, canny_output, threshold_value, threshold_value * 2, 3, false);
	//发现轮廓
	findContours(canny_output, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

	vector<Moments> contours_moments(contours.size());
	vector<Point2f> ccs(contours.size());//中心位置
	for (size_t i = 0; i < contours.size(); i++) {
		contours_moments[i] = moments(contours[i]);
		
		//中心位置
		ccs[i] = Point(static_cast<float>(contours_moments[i].m10 / contours_moments[i].m00), static_cast<float>(contours_moments[i].m01 / contours_moments[i].m00));
	}

	Mat drawImg;// = Mat::zeros(src.size(), CV_8UC3);
	src.copyTo(drawImg);
	for (size_t i = 0; i < contours.size(); i++) {
		if (contours[i].size() < 100) {
			continue;
		}
		Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));//给定一个颜色
		printf("center point x : %.2f y : %.2f\n", ccs[i].x, ccs[i].y);
		printf("contours %d area : %.2f   arc length : %.2f\n", i, contourArea(contours[i]), arcLength(contours[i], true));
		drawContours(drawImg, contours, i, color, 2, 8, hierachy, 0, Point(0, 0));
		circle(drawImg, ccs[i], 2, color, 2, 8);
	}

	imshow(output_win, drawImg);
	return;
}

点多边形测试

  • 概念介绍 - 点多边形测试

测试一个点是否在给定的多边形内部,边缘或者外部

  •  API介绍 cv::pointPolygonTest

  •  代码-步骤

构建一张400x400大小的图片, Mat::Zero(400, 400, CV_8UC1)

画上一个六边形的闭合区域line

发现轮廓

对图像中所有像素点做点与多边形测试,得到距离

归一化后显示。

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;
int main(int argc, char** argv) {
	const int r = 100;
	Mat src = Mat::zeros(r * 4, r * 4, CV_8UC1);

	//画出六个点
	vector<Point2f> vert(6);
	vert[0] = Point(3 * r / 2, static_cast<int>(1.34*r));
	vert[1] = Point(1 * r, 2 * r);
	vert[2] = Point(3 * r / 2, static_cast<int>(2.866*r));
	vert[3] = Point(5 * r / 2, static_cast<int>(2.866*r));
	vert[4] = Point(3 * r, 2 * r);
	vert[5] = Point(5 * r / 2, static_cast<int>(1.34*r));

	//连接六个点的线
	for (int i = 0; i < 6; i++) {
		line(src, vert[i], vert[(i + 1) % 6], Scalar(255), 3, 8, 0);
	}

	
	vector<vector<Point>> contours;
	vector<Vec4i> hierachy;
	Mat csrc;
	src.copyTo(csrc);

	//找这个轮廓
	findContours(csrc, contours, hierachy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
	Mat raw_dist = Mat::zeros(csrc.size(), CV_32FC1);
	//对于每个像素进行相应的处理
	for (int row = 0; row < raw_dist.rows; row++) {
		for (int col = 0; col < raw_dist.cols; col++) {
			//返回距离
			double dist = pointPolygonTest(contours[0], Point2f(static_cast<float>(col), static_cast<float>(row)), true);
			raw_dist.at<float>(row, col) = static_cast<float>(dist);
		}
	}

	double minValue, maxValue;
	minMaxLoc(raw_dist, &minValue, &maxValue, 0, 0, Mat());
	Mat drawImg = Mat::zeros(src.size(), CV_8UC3);
	for (int row = 0; row < drawImg.rows; row++) {
		for (int col = 0; col < drawImg.cols; col++) {
			float dist = raw_dist.at<float>(row, col);
			if (dist > 0) {//在内部
				drawImg.at<Vec3b>(row, col)[0] = (uchar)(abs(1.0 - (dist / maxValue)) * 255);
			}
			else if (dist < 0) {//在外部
				drawImg.at<Vec3b>(row, col)[2] = (uchar)(abs(1.0 - (dist / minValue)) * 255);
			}
			else {
				drawImg.at<Vec3b>(row, col)[0] = (uchar)(abs(255 - dist));
				drawImg.at<Vec3b>(row, col)[1] = (uchar)(abs(255 - dist));
				drawImg.at<Vec3b>(row, col)[2] = (uchar)(abs(255 - dist));
			}
		}
	}

	const char* output_win = "point polygon test demo";
	char input_win[] = "input image";
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	namedWindow(output_win, CV_WINDOW_AUTOSIZE);

	imshow(input_win, src);
	imshow(output_win, drawImg);

	waitKey(0);
	return 0;
}
  • 效果图

基于距离变换与分水岭的图像分割

  • 什么是图像分割(Image Segmentation)

图像分割(Image Segmentation)是图像处理最重要的处理手段之一

图像分割的目标是将图像中像素根据一定的规则分为若干(N)个cluster集合,每个集合包含一类像素。

根据算法分为监督学习方法和无监督学习方法,图像分割的算法多数都是无监督学习方法 - KMeans 

  • 距离变换与分水岭介绍

 

距离变换常见算法有两种 - 不断膨胀/ 腐蚀得到  - 基于倒角距离

 分水岭变换常见的算法 - 基于浸泡理论实现

  • API

  • 处理流程

1.将白色背景变成黑色-目的是为后面的变换做准备

2. 使用filter2D与拉普拉斯算子实现图像对比度提高,sharp

3. 转为二值图像通过threshold

4. 距离变换

5. 对距离变换结果进行归一化到[0~1]之间

6. 使用阈值,再次二值化,得到标记

7. 腐蚀得到每个Peak - erode

8.发现轮廓 – findContours

9. 绘制轮廓- drawContours

10.分水岭变换 watershed

11. 对每个分割区域着色输出结果 

  • 代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>

using namespace std;
using namespace cv;

int main(int argc, char** argv) {
	char input_win[] = "input image";
	char watershed_win[] = "watershed segmentation demo";
	Mat src = imread("C:\\Users\\td\\Desktop/1.png");
	// Mat src = imread("D:/kuaidi.jpg");
	if (src.empty()) {
		printf("could not load image...\n");
		return -1;
	}
	namedWindow(input_win, CV_WINDOW_AUTOSIZE);
	imshow(input_win, src);


	// 1. change background : 将原来的背景变成黑色
	for (int row = 0; row < src.rows; row++) {
		for (int col = 0; col < src.cols; col++) {
			if (src.at<Vec3b>(row, col) == Vec3b(255, 255, 255)) {
				src.at<Vec3b>(row, col)[0] = 0;
				src.at<Vec3b>(row, col)[1] = 0;
				src.at<Vec3b>(row, col)[2] = 0;
			}
		}
	}
	namedWindow("black background", CV_WINDOW_AUTOSIZE);
	imshow("black background", src);

	// 2. sharpen:提高图像的对比度,为下一步进行二值化进行准备
	Mat kernel = (Mat_<float>(3, 3) << 1, 1, 1, 1, -8, 1, 1, 1, 1);
	Mat imgLaplance;
	Mat sharpenImg = src;
	//进行均值处理的时候.是不会出现负数的,一旦要是存在相应的负数,就是需要将相应的位数变成32位
	filter2D(src, imgLaplance, CV_32F, kernel, Point(-1, -1), 0, BORDER_DEFAULT);
	src.convertTo(sharpenImg, CV_32F);
	Mat resultImg = sharpenImg - imgLaplance;

	resultImg.convertTo(resultImg, CV_8UC3);
	imgLaplance.convertTo(imgLaplance, CV_8UC3);
	imshow("sharpen image", resultImg);
	// src = resultImg; // copy back

	// 3. convert to binary:进行二值变换
	Mat binaryImg;
	cvtColor(src, resultImg, CV_BGR2GRAY);
	threshold(resultImg, binaryImg, 40, 255, THRESH_BINARY | THRESH_OTSU);
	imshow("binary image", binaryImg);

	//4. 进行距离变换
	Mat distImg;
	distanceTransform(binaryImg, distImg, DIST_L1, 3, 5);
	normalize(distImg, distImg, 0, 1, NORM_MINMAX);
	imshow("distance result", distImg);

	//5. binary again:距离变换之后的二值化处理---得到相应的孤立的点
	threshold(distImg, distImg, .4, 1, THRESH_BINARY);
	Mat k1 = Mat::ones(13, 13, CV_8UC1);
	erode(distImg, distImg, k1, Point(-1, -1));
	imshow("distance binary image", distImg);

	//6. markers 发现轮廓 
	Mat dist_8u;
	distImg.convertTo(dist_8u, CV_8U);
	vector<vector<Point>> contours;
	findContours(dist_8u, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE, Point(0, 0));

	// create makers 创建轮廓
	Mat markers = Mat::zeros(src.size(), CV_32SC1);
	for (size_t i = 0; i < contours.size(); i++) {
		drawContours(markers, contours, static_cast<int>(i), Scalar::all(static_cast<int>(i) + 1), -1);
	}
	circle(markers, Point(5, 5), 3, Scalar(255, 255, 255), -1);
	imshow("my markers", markers * 1000);

	// perform watershed: 创建一个分水岭
	watershed(src, markers);
	Mat mark = Mat::zeros(markers.size(), CV_8UC1);
	markers.convertTo(mark, CV_8UC1);
	bitwise_not(mark, mark, Mat());
	imshow("watershed image", mark);

	// generate random color
	vector<Vec3b> colors;
	for (size_t i = 0; i < contours.size(); i++) {
		int r = theRNG().uniform(0, 255);
		int g = theRNG().uniform(0, 255);
		int b = theRNG().uniform(0, 255);
		colors.push_back(Vec3b((uchar)b, (uchar)g, (uchar)r));
	}

	// fill with color and display final result
	Mat dst = Mat::zeros(markers.size(), CV_8UC3);
	for (int row = 0; row < markers.rows; row++) {
		for (int col = 0; col < markers.cols; col++) {
			int index = markers.at<int>(row, col);
			if (index > 0 && index <= static_cast<int>(contours.size())) {
				dst.at<Vec3b>(row, col) = colors[index - 1];
			}
			else {
				dst.at<Vec3b>(row, col) = Vec3b(0, 0, 0);
			}
		}
	}
	imshow("Final Result", dst);

	waitKey(0);
	return 0;
}
  • 结果示意图

原文地址:https://blog.csdn.net/m0_47489229/article/details/126207719

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

h
上一篇 2023年06月12日 01:17
STM32F103驱动LD3320语音识别模块
下一篇 2023年06月12日 01:19