详解利用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面向对象编程中的类和对象学习教程
Mar 30 Python
Python脚本暴力破解栅栏密码
Oct 19 Python
深入讲解Python中的迭代器和生成器
Oct 26 Python
Python探索之pLSA实现代码
Oct 25 Python
对Python 语音识别框架详解
Dec 24 Python
解决使用PyCharm时无法启动控制台的问题
Jan 19 Python
python f-string式格式化听语音流程讲解
Jun 18 Python
Python字典底层实现原理详解
Dec 18 Python
为什么黑客都用python(123个黑客必备的Python工具)
Jan 31 Python
python实现UDP协议下的文件传输
Mar 20 Python
python画环形图的方法
Mar 25 Python
在jupyter notebook中调用.ipynb文件方式
Apr 14 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
安健A254立体声随身听的分析与打磨
2021/03/02 无线电
一个数据采集类
2007/02/14 PHP
PHP 5.0对象模型深度探索之类的静态成员
2008/03/27 PHP
yii实现级联下拉菜单的方法
2014/07/31 PHP
Laravel 5.1 on SAE环境开发教程【附项目demo源码】
2016/10/09 PHP
php实现连接access数据库并转txt写入的方法
2017/02/08 PHP
PHP迭代器接口Iterator用法分析
2017/12/28 PHP
ExtJs使用IFrame的实现代码
2010/03/24 Javascript
js下用eval生成JSON对象
2010/09/17 Javascript
js 第二代身份证号码的验证机制代码
2011/05/12 Javascript
JQuery判断HTML元素是否存在的两种解决方法
2013/12/26 Javascript
函数window.open实现关闭所有的子窗口
2015/08/03 Javascript
Bootstrap每天必学之简单入门
2015/11/19 Javascript
JavaScript实现简单的拖动效果
2016/07/02 Javascript
javascript的函数劫持浅析
2016/09/26 Javascript
原生javascript实现图片放大镜效果
2017/01/18 Javascript
js实现滑动到页面底部自动加载更多功能
2017/02/15 Javascript
vue2.0之多页面的开发的示例
2018/01/30 Javascript
深入理解JavaScript 箭头函数
2019/05/30 Javascript
[52:15]2014 DOTA2国际邀请赛中国区预选赛5.21 HGT VS LGD-GAMING
2014/05/23 DOTA
python多线程操作实例
2014/11/21 Python
Python基于win32ui模块创建弹出式菜单示例
2018/05/09 Python
对Python3中列表乘以某一个数的示例详解
2019/07/20 Python
python单例模式原理与创建方法实例分析
2019/10/26 Python
python多线程高级锁condition简单用法示例
2019/11/07 Python
戴尔新加坡官网:Dell Singapore
2020/12/13 全球购物
将"引用"作为函数参数有哪些特点
2013/04/05 面试题
高中的自我鉴定
2013/12/16 职场文书
三年级评语大全
2014/04/23 职场文书
水利局群众路线专题民主生活会发言材料
2014/09/21 职场文书
党员对十八届四中全会的期盼思想汇报范文
2014/10/17 职场文书
义诊活动通知
2015/04/24 职场文书
自信主题班会
2015/08/14 职场文书
子女赡养老人协议书
2016/03/23 职场文书
类和原型的设计模式之复制与委托差异
2022/07/07 Javascript
vue本地构建热更新卡顿的问题“75 advanced module optimization”完美解决方案
2022/08/05 Vue.js