详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)


Posted in Python onJuly 01, 2019

前言

最近参加了大创项目,题目涉及到计算机视觉,学姐发了个修正图像的博客链接,于是打算用这个题目入门OpenCV。

分析问题

照片中的PPT区域总是沿着x,y,z三个轴都有倾斜(如下图),要想把照片翻转到平行位置,需要进行透视变换,而透视变换需要同一像素点变换前后的坐标。由此可以想到,提取矩形区域四个角的坐标作为变换前的坐标,变换后的坐标可以设为照片的四个角落,经过投影变换,矩形区域将会翻转并充满图像。

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

因此我们要解决的问题变为:提取矩形的四个角落、进行透视变换。

提取矩形角落坐标

矩形的检测主要是提取边缘,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); //阈值化为二值图片

此时图片已经变成了这个样子:

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

可见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

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

提取面积最大的轮廓并用多边形将轮廓包围

从上面的轮廓图中看出,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);

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

如图,接下来我们只需提取到四个角的坐标

寻找凸包

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);

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

现在我们已经比较准确地获得了需要的点,下面就要利用这些点进行坐标映射。

投影变换

投影变换需要像素在两个坐标系中的坐标一一对应,虽然我们已经有了四个坐标,但还没有区分它们的位置。

新建两个数组

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()); //进行坐标变换

参数分别为输入输出图像、变换矩阵、大小。

坐标变换后就得到了我们要的最终图像。

详解利用OpenCV提取图像中的矩形区域(PPT屏幕等)

总结

我们利用了屏幕亮度较高的特点,通过二值化突出轮廓提取坐标,进行透视变换。

但局限性在于,如果矩形的亮度与背景相差不大,就很难用这种方法检测到轮廓。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python类的定义、继承及类对象使用方法简明教程
May 08 Python
Python框架Flask的基本数据库操作方法分析
Jul 13 Python
对python GUI实现完美进度条的示例详解
Dec 13 Python
python将控制台输出保存至文件的方法
Jan 07 Python
Python使用百度api做人脸对比的方法
Aug 28 Python
Python 实现一个手机号码获取妹子名字的功能
Sep 25 Python
python的scipy实现插值的示例代码
Nov 12 Python
django在保存图像的同时压缩图像示例代码详解
Feb 11 Python
python+selenium+chrome批量文件下载并自动创建文件夹实例
Apr 27 Python
python一些性能分析的技巧
Aug 30 Python
在Windows下安装配置CPU版的PyTorch的方法
Apr 02 Python
Python3.8官网文档之类的基础语法阅读
Sep 04 Python
用python打印1~20的整数实例讲解
Jul 01 #Python
python sklearn库实现简单逻辑回归的实例代码
Jul 01 #Python
python实现列表的排序方法分享
Jul 01 #Python
Apache,wsgi,django 程序部署配置方法详解
Jul 01 #Python
Python中字符串List按照长度排序
Jul 01 #Python
python opencv minAreaRect 生成最小外接矩形的方法
Jul 01 #Python
VPS CENTOS 上配置python,mysql,nginx,uwsgi,django的方法详解
Jul 01 #Python
You might like
PHP $_SERVER详解
2009/01/16 PHP
PHP循环遍历数组的3种方法list()、each()和while总结
2014/11/19 PHP
微信支付开发发货通知实例
2016/07/12 PHP
YII框架学习笔记之命名空间、操作响应与视图操作示例
2019/04/30 PHP
(currentStyle)javascript为何有时用style得不到已设定的CSS的属性
2007/08/15 Javascript
Javascript学习笔记之 对象篇(四) : for in 循环
2014/06/24 Javascript
jQuery Ajax使用实例
2015/04/16 Javascript
jQuery实现文本展开收缩特效
2015/06/03 Javascript
javascript性能优化之事件委托实例详解
2015/12/12 Javascript
JS Array创建及concat()split()slice()的使用方法
2016/06/03 Javascript
JavaScript里 ==与===区别详解
2016/08/16 Javascript
Angular路由简单学习
2016/12/26 Javascript
vue组件Prop传递数据的实现示例
2017/08/17 Javascript
小试SVG之新手小白入门教程
2019/01/08 Javascript
Vue使用watch监听一个对象中的属性的实现方法
2019/05/10 Javascript
详解js创建对象的几种方式和对象方法
2021/03/01 Javascript
[43:51]2018DOTA2亚洲邀请赛3月30日 小组赛B组 EG VS Secret
2018/03/31 DOTA
python启动办公软件进程(word、excel、ppt、以及wps的et、wps、wpp)
2009/04/09 Python
python urllib爬取百度云连接的实例代码
2017/06/19 Python
浅谈pyqt5中信号与槽的认识
2019/02/17 Python
详解Python Qt的窗体开发的基本操作
2019/07/14 Python
浅析使用Python搭建http服务器
2019/10/27 Python
pytorch 常用线性函数详解
2020/01/15 Python
用Python做一个久坐提醒小助手的示例代码
2020/02/10 Python
Hunkemöller西班牙:欧洲最大的内衣连锁店
2018/08/15 全球购物
上课迟到检讨书
2014/02/19 职场文书
经典洗发水广告词
2014/03/13 职场文书
企业宗旨标语
2014/06/10 职场文书
党员评议表自我评价范文
2014/10/20 职场文书
员工工作能力评语
2014/12/31 职场文书
迎新生欢迎词
2015/01/23 职场文书
数学教师个人工作总结
2015/02/06 职场文书
卢旺达饭店观后感
2015/06/05 职场文书
我的生日感言
2015/08/03 职场文书
小学三年级班主任工作经验交流材料
2015/11/02 职场文书
使用RedisTemplat实现简单的分布式锁
2021/11/20 Redis