详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)
作者:才才才 发布时间:2022-06-23 16:42:41
前言
最近参加了大创项目,题目涉及到计算机视觉,学姐发了个修正图像的博客链接,于是打算用这个题目入门OpenCV。
分析问题
照片中的PPT区域总是沿着x,y,z三个轴都有倾斜(如下图),要想把照片翻转到平行位置,需要进行透视变换,而透视变换需要同一像素点变换前后的坐标。由此可以想到,提取矩形区域四个角的坐标作为变换前的坐标,变换后的坐标可以设为照片的四个角落,经过投影变换,矩形区域将会翻转并充满图像。
因此我们要解决的问题变为:提取矩形的四个角落、进行透视变换。
提取矩形角落坐标
矩形的检测主要是提取边缘,PPT显示部分的亮度通常高于周围环境,我们可以将图片阈值化,将PPT部分与周围环境明显的分别开来,这对后边的边缘检测非常有帮助。
检测矩形并提取坐标需要对图像进行预处理、边缘检测、提取轮廓、检测凸包、角点检测。
预处理
由于手机拍摄的照片像素可能会很高,为了加快处理速度,我们首先缩小图片,这里缩小了4倍。
pyrDown(srcPic, shrinkedPic); //减小尺寸 加快运算速度
pyrDown(shrinkedPic, shrinkedPic);
转化为灰度图
cvtColor(shrinkedPic, greyPic, COLOR_BGR2GRAY); //转化为灰度图
中值滤波
medianBlur(greyPic, greyPic, 7); //中值滤波
转为二值图片
threshold(greyPic, binPic, 80, 255, THRESH_BINARY); //阈值化为二值图片
此时图片已经变成了这个样子:
可见PPT部分已经与环境分离开来。
边缘检测与轮廓处理
进行Canny边缘检测
Canny(binPic, cannyPic, cannyThr, cannyThr*FACTOR); //Canny边缘检测
这里 cannyThr = 200, FACTOR = 2.5
可能由于边缘特征过于明显,系数在100-600范围(具体数字可能有出入,反正范围非常大)内产生的效果几乎相同。
提取轮廓
vector<vector<Point>> contours; //储存轮廓
vector<Vec4i> hierarchy;
findContours(cannyPic, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE); //获取轮廓
findContour
函数原型如下:
CV_EXPORTS_W void findContours( InputOutputArray image, OutputArrayOfArrays contours,
OutputArray hierarchy, int mode,
int method, Point offset = Point());
检测到的轮廓都存在contours
里,每个轮廓保存为一个vector<Point>
hierarchy
为可选的输出向量,包括图像的拓扑信息,这里可以选择不用。
我们可以反复调用drawContours
函数将轮廓画出
linePic = Mat::zeros(cannyPic.rows, cannyPic.cols, CV_8UC3);
for (int index = 0; index < contours.size(); index++){
drawContours(linePic, contours, index, Scalar(rand() & 255, rand() & 255, rand() & 255), 1, 8/*, hierarchy*/);
}
drawContours
函数原型:
CV_EXPORTS_W void drawContours( InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point() );
作用是将contours
中的第contourIdx
条轮廓用color
颜色绘制到image
中,thickness
为线条的粗细, contourIdx
为负数时画出所有轮廓
这里要注意的是在绘制轮廓前要提前为输出矩阵分配空间,否则会出现以下错误
OpenCV(3.4.1) Error: Assertion failed (size.width>0 && size.height>0) in cv::imshow, file C:\build\master_winpack-build-win64-vc15\opencv\modules\highgui\src\window.cpp, line 356
提取面积最大的轮廓并用多边形将轮廓包围
从上面的轮廓图中看出,PPT的矩形已经成为了图片的主要部分,接下来的思路是提取面积最大的轮廓,得到矩形轮廓。
vector<vector<Point>> polyContours(contours.size());
int maxArea = 0;
for (int index = 0; index < contours.size(); index++){
if (contourArea(contours[index]) > contourArea(contours[maxArea]))
maxArea = index;
approxPolyDP(contours[index], polyContours[index], 10, true);
}
contourArea
用来计算轮廓的面积approxPolyDP
的作用是用多边形包围轮廓,可以得到严格的矩形,有助于找到角点
画出矩形,同样注意要提前为Mat
分配空间
Mat polyPic = Mat::zeros(shrinkedPic.size(), CV_8UC3);
drawContours(polyPic, polyContours, maxArea, Scalar(0,0,255/*rand() & 255, rand() & 255, rand() & 255*/), 2);
如图,接下来我们只需提取到四个角的坐标
寻找凸包
vector<int> hull;
convexHull(polyContours[maxArea], hull, false); //检测该轮廓的凸包
convexHull
函数原型
CV_EXPORTS_W void convexHull( InputArray points, OutputArray hull,
bool clockwise = false, bool returnPoints = true );
hull
为输出参数, clockwise
决定凸包顺逆时针方向, returnPoints
为真时返回凸包的各个点,否则返回各点的指数 hull
可以为vector<int>
类型,此时返回的是凸包点在原图中的下标索引
我们可以把点和多边形添加到原图中查看效果
for (int i = 0; i < hull.size(); ++i){
circle(polyPic, polyContours[maxArea][i], 10, Scalar(rand() & 255, rand() & 255, rand() & 255), 3);
}
addWeighted(polyPic, 0.5, shrinkedPic, 0.5, 0, shrinkedPic);
现在我们已经比较准确地获得了需要的点,下面就要利用这些点进行坐标映射。
投影变换
投影变换需要像素在两个坐标系中的坐标一一对应,虽然我们已经有了四个坐标,但还没有区分它们的位置。
新建两个数组
Point2f srcPoints[4], dstPoints[4];
dstPoints[0] = Point2f(0, 0);
dstPoints[1] = Point2f(srcPic.cols, 0);
dstPoints[2] = Point2f(srcPic.cols, srcPic.rows);
dstPoints[3] = Point2f(0, srcPic.rows);
dstPoints
储存的是变换后各点的坐标,依次为左上,右上,右下, 左下
srcPoints
储存的是上面得到的四个角的坐标
下面对得到的四个点进行处理
for (int i = 0; i < 4; i++){
polyContours[maxArea][i] = Point2f(polyContours[maxArea][i].x * 4, polyContours[maxArea][i].y * 4); //恢复坐标到原图
}
//对四个点进行排序 分出左上 右上 右下 左下
bool sorted = false;
int n = 4;
while (!sorted){
for (int i = 1; i < n; i++){
sorted = true;
if (polyContours[maxArea][i-1].x > polyContours[maxArea][i].x){
swap(polyContours[maxArea][i-1], polyContours[maxArea][i]);
sorted = false;
}
}
n--;
}
if (polyContours[maxArea][0].y < polyContours[maxArea][1].y){
srcPoints[0] = polyContours[maxArea][0];
srcPoints[3] = polyContours[maxArea][1];
}
else{
srcPoints[0] = polyContours[maxArea][1];
srcPoints[3] = polyContours[maxArea][0];
}
if (polyContours[maxArea][9].y < polyContours[maxArea][10].y){
srcPoints[1] = polyContours[maxArea][2];
srcPoints[2] = polyContours[maxArea][3];
}
else{
srcPoints[1] = polyContours[maxArea][3];
srcPoints[2] = polyContours[maxArea][2];
}
即先对四个点的x坐标进行冒泡排序分出左右,再根据两对坐标的y值比较分出上下
(笔者试图通过凸包的顺逆时针顺序以及凸包点与原点的距离来活得位置信息,却均以失败告终)
坐标变换需要矩阵运算,OpenCV中给我们提供了getPerspectiveTransform
函数用来得到矩阵
Mat transMat = getPerspectiveTransform(srcPoints, dstPoints); //得到变换矩阵
接下来进行坐标变换,网上查到的步骤都是通过perspectiveTransform
函数变换,但尝试多次都出现了报错,Google了好长时间才知道原来这个函数的传入输入输出参数均为点集,我们这个场景用起来比较麻烦。
而warpPerspective
函数可以直接传入输入Mat
类型数据,比较方便
warpPerspective(srcPic, outPic, transMat, srcPic.size()); //进行坐标变换
参数分别为输入输出图像、变换矩阵、大小。
坐标变换后就得到了我们要的最终图像。
总结
我们利用了屏幕亮度较高的特点,通过二值化突出轮廓提取坐标,进行透视变换。
但局限性在于,如果矩形的亮度与背景相差不大,就很难用这种方法检测到轮廓。
来源:https://segmentfault.com/a/1190000013925648


猜你喜欢
- mark标记在实际工作中,我们要写的自动化用例会比较多,也不会都放在一个py文件中,如果有几十个py文件,上百个方法,而我们只想运行当中部分
- 本文实例讲述了java实现的连接oracle mysql数据库功能。分享给大家供大家参考,具体如下:package com.nuo.test
- “'验证码'等于'流氓软件'”这句话本身存在逻辑问题,因为“验证码”并不是一个软件,而是软件里的一个功能。这
- IT界的每个人都应该知道终端(Terminal)的基本知识,数据科学家也不例外。有时,终端是你的全部,尤其是在将模型和数据管道部署到远程机器
- PHP session用法其实很简单它可以把用户提交的数据以全局变量形式保存在一个session中并且会生成一个唯一的session_id,
- 前面已经介绍过几种基本语句(print,import,赋值语句),下面我们来介绍条件语句,循环语句。一. print和import的更多信息
- 1.准备工作下载mysql的最新免安装版本mysql-noinstall-5.1.53-win32.zip,解压缩到相关目录,如:d:\\
- 通过使用bootstrap框架,并配合Django自带的Paginator分页组件即可实现简单的分页效果。1.创建MyWeb项目python
- 什么是接口测试接口测试主要用于检测外部系统与内部系统之间,以及系统内部各 个子系统之间的交互点。其测试的重点是,检查数据的交换、传递和控 制
- 一、在vs中新建一个工程,然后新建一个源文件二、右击上述图片中的mysql_test工程名,然后在最下方找到属性,并点击三、点击VC++目录
- 在标准的dgango项目中,自动生成的目录结构会包括models.py和views.py两个文件,分别在里面写model的代码和contro
- 傅立叶级数的介绍我就不说了,自己也是应用为主,之前一直觉得很难懂,但最近通过自己编程实现了一些函数的傅立叶级数展开之后对傅立叶 级数展开的概
- JavaScript游戏开发之键盘控制层的移动截图:<html> <head> <meta http-equi
- interfaceGo语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便,当你看完这一章,你就会被inter
- 下面先说说window.showModalDialog的基本用法showModalDialog() (IE 4+ 支持)showModele
- 如题,解决Python中用PyQt时中文乱码问题的解决方法:在中文字符串前面加上u,如u'你好,世界',其他网上的方法没有多
- python简单实现最大似然估计1、scipy库的安装wim+R输入cmd,然后cd到python的pip路径,即安装:pip instal
- 一、安装pip2pi工具:pip install pip2pi或:git clone https://github.com/wolever/
- python提供了json包来进行json处理,json与python中数据类型对应关系如下:一个python object无法直接与jso
- 应用场景:1、授权(Authorization)装饰器能有助于检查某个人是否被授权去使用一个web应用的端点(endpoint)。它们被大量