OpenCV学习笔记(三)

仿射变换

对于平面区域,有两种方式的几何转换:一种是基于2×3矩阵进行的变换,叫仿射变换;另一种是基于3×3矩阵进行的变换,叫透视变换或者单应性映射。关于仿射变换和透射变换的矩阵变换,我们不做重点讨论,因为图像本质就是矩阵,对矩阵的变换就是对图像像素的操作,很简单的数学知识。

仿射变换可以将矩形转换成平行四边形,它可以将矩形的边压扁但必须保持边是平行的,也可以将矩形旋转或者按比例变化。透视变换提供了更大的灵活性,一个透视变换可以将矩阵转变成梯形。当然,平行四边形也是梯形,所以仿射变换是透视变换的子集。

opencv里实现仿射变换需要两步

第一步获得仿射矩阵,opencv提供了两个api,可以根据不同的需要来选择

Mat getRotationMatrix2D( Point2f center, double angle, double scale )
```- center,表示源图像旋转中心
- angle,旋转角度,正值表示逆时针旋转
- scale,缩放系数
  
  ```cpp
  Mat getAffineTransform( const Point2f src[], const Point2f dst[] )
  • src:原图的三个固定顶点
  • dst:目标图像的三个固定顶点

第二步进行仿射变换

void warpAffine( InputArray src, OutputArray dst,
                              InputArray M, Size dsize,
                              int flags = INTER_LINEAR,
                              int borderMode = BORDER_CONSTANT,
                              const Scalar& borderValue = Scalar());
  • src:输入变换前图像
  • dst:输出变换后图像,需要初始化一个空矩阵用来保存结果,不用设定矩阵尺寸
  • M:变换矩阵,一般用前面两个函数得到
  • dsize:设置输出图像大小
  • 后面三个一般默认

示例程序

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

int main()
{
	Mat src = imread("C:\\Users\\bluef\\Pictures\\lena.jpg");

	float angle = -10.0, scale = 0.8;
	Point2f center(src.cols * 0.5, src.rows * 0.5);

	Mat affine_m1 = getRotationMatrix2D(center, angle, scale);

	Point2f src_pt[] = { 
		Point2f(0,0),
		Point2f(0,src.rows),
		Point2f(src.cols,0) };
	Point2f dst_pt[] = {
		Point2f(0,src.rows * 0.3),
		Point2f(src.cols * 0.25,src.rows * 0.75),
		Point2f(src.cols * 0.75,src.rows * 0.25) };

	Mat affine_m2 = getAffineTransform(src_pt, dst_pt);

	Mat dst1, dst2;
	warpAffine(src, dst1, affine_m1, src.size());
	warpAffine(src, dst2, affine_m2, src.size());

	imshow("src", src);
	imshow("指定比例和角度", dst1);
	imshow("三点法", dst2);
	waitKey(0);
	return 0;
}

透视变换

透视变换(也叫投影变换)上面已经提到过了,就不多介绍了,和仿射变换差不多,也是两步,第一步求出透视矩阵

求透视矩阵只有一个api和上面求仿射矩阵的第二个函数用法一样,只是从三个点变成四个点

Mat getPerspectiveTransform( const Point2f src[], const Point2f dst[] );

第二步也和仿射变换差不多,参数跟warpAffine的也没什么区别,就是把仿射矩阵换成了透视矩阵

void warpPerspective( InputArray src, OutputArray dst,
                                   InputArray M, Size dsize,
                                   int flags = INTER_LINEAR,
                                   int borderMode = BORDER_CONSTANT,
                                   const Scalar& borderValue = Scalar());

示例程序

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

int main()
{
	Mat src = imread("C:\\Users\\bluef\\Pictures\\lena.jpg");

	Point2f src_pt[] = {
		Point2f(0,0),
		Point2f(0,src.rows),
		Point2f(src.cols,0),
		Point2f(src.cols, src.rows) };
	;
	Point2f dst_pt[] = {
		Point2f(src.cols * 0.1, src.rows * 0.1),
		Point2f(0, src.rows),
		Point2f(src.cols, 0),
		Point2f(src.cols * 0.7, src.rows * 0.8)};

	Mat m = getPerspectiveTransform(src_pt, dst_pt);

	Mat dst;
	warpPerspective(src, dst, m, src.size());

	imshow("src", src);
	imshow("dst", dst);
	waitKey(0);
	return 0;
}

倾斜图片校正

有一副这样的图片,我们如果要把它转正怎么办,最简单的方法就是,从四条边开始遍历,找到第一个非白色的点,就是图像的顶点,我们只需要利用仿射变换,把顶点移到正确的四个角即可

#include 
#include
using namespace cv;
using namespace std;

int main()
{
	Mat src = imread("C:\\Users\\bluef\\Pictures\\lena_rot.jpg");

	Point2f dst_pt[] = {
		Point2f(0,0),
		Point2f(0,src.rows),
		Point2f(src.cols,0) };
	Point2f p1, p2, p3;
	Mat gray;
	cvtColor(src, gray, CV_RGB2GRAY);
	for (int i = 0; i < src.rows; i++)
	{
		bool flag = false;
		for (int j = 0; j < src.cols; j++)
			if (gray.at(i, j) < 250)
			{
				p1 = Point2f(j, i);
				flag = true;
				break;
			}
		if (flag)
			break;
	}
	for (int i = 0; i < src.cols; i++)
	{
		bool flag = false;
		for (int j = 0; j < src.rows; j++)
			if (gray.at(j, i) < 250)
			{
				p2 = Point2f(i, j);
				flag = true;
				break;

			}
		if (flag)
			break;
	}
	for (int i = src.cols - 1; i >= 0; i--)
	{
		bool flag = false;
		for (int j = 0; j < src.rows; j++)
			if (gray.at(j, i) < 250)
			{
				p3 = Point2f(i, j);
				flag = true;
				break;
			}
		if (flag)
			break;
	}
	Point2f src_pt[] = { p1,p2,p3 };

	Mat affine_m = getAffineTransform(src_pt, dst_pt);

	Mat dst;
	warpAffine(src, dst, affine_m, src.size());
	imshow("gray", gray);
	imshow("src", src);
	imshow("dst", dst);
	waitKey(0);
	return 0;
}

图像旋转不裁剪

正常图像旋转后,如果图像大小不变,就会有一部分图像在画面外面,为了避免这张情况,我们需要程序能计算出旋转后图像的大小

#include 
#include
using namespace cv;
using namespace std;

int main()
{
	Mat src = imread("C:\\Users\\bluef\\Pictures\\lena.jpg");

	float angle = -20.0;
	Point2f center(src.cols * 0.5, src.rows * 0.5);//原图像的中心

	int rotated_w = ceil(src.rows * fabs(sin(angle * CV_PI / 180)) + src.cols * fabs(cos(angle * CV_PI / 180)));//旋转后图像的宽
	int rotated_h = ceil(src.cols * fabs(sin(angle * CV_PI / 180)) + src.rows * fabs(cos(angle * CV_PI / 180)));//旋转后图像的高

	Mat affine_m = getRotationMatrix2D(center, angle, 1.0);
	//由于图像变大了,所以的图像中心也要移动到新图像的中心,通过修改仿射矩阵的平移部分
	affine_m.at(0, 2) += (rotated_w - src.cols) / 2;
	affine_m.at(1, 2) += (rotated_h - src.rows) / 2;
	Mat dst;
	warpAffine(src, dst, affine_m, Size(rotated_w, rotated_h));

	imshow("src", src);
	imshow("dst", dst);
	waitKey(0);
	return 0;
}

霍夫变换

原理可以参考这篇《『OpenCV3』霍夫变换原理及实现

我的理解就是把普通坐标系里的直线对应到霍夫空间里的点,而普通坐标系里的点对应到霍夫空间就是条直线。这样如果图像中有许多点是在一条直线上的,那么对应到霍夫空间就是许多直线交于一个点,通过这个办法我们就可以检测出图像中的直线

void HoughLines( InputArray image, OutputArray lines,
                              double rho, double theta, int threshold,
                              double srn = 0, double stn = 0,
                              double min_theta = 0, double max_theta = CV_PI );
  • src, 输入图像,必须8-bit的灰度图像
  • lines, 输出的极坐标来表示直线
  • rho, 生成极坐标时候的像素扫描步长
  • theta,生成极坐标时候的角度步长,一般取CV_PI/180
  • threshold, 阈值,只有获得足够交点的极坐标点才被看成是直线
  • 后面几个一般默认

使用霍夫变换前我们一般进行一次边缘检测,比如用canny

生成的直线是极坐标表示,需要转换成直角坐标才可以绘图,且需要算出两个端点(因为我们只能画线段,所以还是需要端点的)

#include 
#include
#include
using namespace cv;
using namespace std;


int main()
{
	Mat src = imread("C:/Users/bluef/Pictures/metal-part-distorted-03.png");
	Mat canny;
	Canny(src, canny, 100, 250);
	imshow("canny", canny);

	vector lines;
	HoughLines(canny, lines, 1, CV_PI / 180, 100);
	for (auto& it : lines)
	{
		float rho = it[0], theta = it[1];
		Point p1, p2;
		double a = cos(theta);
		double b = sin(theta);
		double x0 = a * rho;
		double y0 = b * rho;
		p1.x = saturate_cast(x0 + 1000 * (-b));
		p1.y = saturate_cast(y0 + 1000 * a);
		p2.x = saturate_cast(x0 - 1000 * (-b));
		p2.y = saturate_cast(y0 - 1000 * a);
		line(src, p1, p2, Scalar(0, 0, 255));
	}
	imshow("dst", src);
	waitKey(0);
	return 0;
}


这里只有两条直线是因为我只需要这两条,如果需要其他的直线,只要稍微调整一下参数即可

上面这个函数只能检测出直线,而下面这个函数可以检测线段,只是在原来的基础上加上了概率,输出是每个线段的两个端点

void HoughLinesP( InputArray image, OutputArray lines,
                               double rho, double theta, int threshold,
                               double minLineLength = 0, double maxLineGap = 0 );
  • 前面几个参数和之前一样
  • minLineLength,线段的最小长度,比这个设定参数短的线段就不能被显现出来
  • maxLineGap, 线段上最近两点之间的阈值
#include 
#include
#include
using namespace cv;
using namespace std;


int main()
{
	Mat src = imread("C:/Users/bluef/Pictures/metal-part-distorted-03.png");
	Mat canny;
	Canny(src, canny, 100, 250);
	imshow("canny", canny);

	vector lines;
	HoughLinesP(canny, lines, 1, CV_PI / 180, 40, 50, 10);
	for (auto& it : lines)
		line(src, Point(it[0], it[1]), Point(it[2], it[3]), Scalar(0, 0, 255),2);
	imshow("dst", src);
	waitKey(0);
	return 0;
}

图像筛选

有时候我们需要从图片中找出需要的东西,这个时候我们就需要用到一些之前学到过的方法,比如二值化、图像形态学,连通域分析等等。

下面这张是提取出车轮中间的孔,只需要进行合适的二值化后,再进行连通域分析即可,不过要把不是孔的特征去掉,精确点的话,应该用面积和圆形度来筛选出我们需要的孔,不过这里只用面积就够了。

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/rim.png");
	imshow("src", srcMat);
	Mat grayMat, dstMat, labels, stats, centroids;
	cvtColor(srcMat, grayMat, CV_RGB2GRAY);
	threshold(grayMat, dstMat, 125, 255, THRESH_BINARY_INV);

	int num = connectedComponentsWithStats(dstMat, labels, stats, centroids);

	vector colors(num);
	colors[0] = false;
	for (int i = 1; i < num; i++)
	{
		if (stats.at(i, 4) > 1000 && stats.at(i, 4) < 5500)//只标记面积符合条件的
			colors[i] = true;
		else
			colors[i] = false;
	}

	for (int y = 0; y < srcMat.rows; y++)
		for (int x = 0; x < srcMat.cols; x++)
			if (colors[labels.at(y, x)])
				srcMat.at(y, x) = Vec3b(0, 255, 255);

	imshow("res", srcMat);
	waitKey(0);
	return 0;
}

下面这个是定位芯片的位置,也很简单,二值化加连通域分析即可,精确点的话,除了面积这个特征,再算一下矩形度或者长宽比什么的,这张图特征也比较明显,也是筛选一下面积就行了

#include 
#include
using namespace cv;
using namespace std;


int main()
{
	Mat srcMat = imread("C:/Users/bluef/Pictures/die_on_chip.png");
	imshow("src", srcMat);
	Mat grayMat,dstMat, labels, stats, centroids;
	cvtColor(srcMat, grayMat, CV_RGB2GRAY);
	threshold(grayMat, dstMat, 150, 255, THRESH_BINARY);
	int num = connectedComponentsWithStats(dstMat, labels, stats, centroids);

	for (int i = 1; i < num; i++)
		if (stats.at(i, 4) > 500)
			rectangle(srcMat, Rect(stats.at(i, 0), stats.at(i, 1), stats.at(i, 2), stats.at(i, 3)), Scalar(0, 0, 255), 2);
	imshow("res", srcMat);
	waitKey(0);
	return 0;
}

下面这个稍微复杂一点点,框出中间这个不知道什么东西(好像水杯的盖子?),其实就是通过hsv通道的二值化,提取出画面中的红色部分,同时为了更精准一点,我做了一次开运算,然后就是连通域分析,把干扰的鼠标滚轮之类的删掉。

这里红色的h范围为(0-8)∪(156-180),s为43-255,v为46-255