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