随着近年移动设备上的面部识别和AI美颜等功能的火热,OpenCV作为最流行的计算机图形处理库开始大放异彩。
OpenCV(Open Source Computer Vision Library)是一个开源的跨平台计算机视觉库,底层基于C和C++实现,它提供了很多函数,这些函数非常高效地实现了计算机视觉算法, 大大简化了图像处理和深度学习应用的开发过程。
OpenCV 提供了多种语言的接口,OpenCV.js则为JavaScript开发者提供了高效便捷的图像处理算法,原理是借助一款 LLVM-to-Javascript 的编译器 —— Emscripten 将库底层 C++ 代码编译为可在浏览器运行的 asm.js 或者 WebAssembly。本文通过介绍 OpenCV.js 在纸张识别上的一个例子,帮助大家了解前端中的图像处理领域。
开发者可以下载 OpenCV 库,然后安装 Emscripten,然后自行编译为 OpenCV.js 文件,但是编译过程比较繁琐,建议还是在 OpenCV.js 官网中下载编译好的 js 文件。然后在自己的页面中通过 <script>
标签引入,引入后会把一个 cv 对象挂到全局对象上面,cv对象中包含了所有能用到的函数和常量。
OpenCV.js 中操作图像都是通过矩阵进行操作的,提供了一个矩阵类型 Mat,新建一个矩阵:
let img = new cv.Mat();
在 OpenCV.js 中通过 imread 和 imshow 这两个函数进行读取和输出图像。读取图像是将<img>和<canvas>元素或id值将图像转换为矩阵进行操作;输出图像也同样,但是只能输出到<canvas>元素或者id。
// 读取图像转换为矩阵
let img = cv.imread(new Image() || id);
// 输出图像到canvas
cv.imshow(document.createElement('canvas') || id, img);
在做项目的开发时,接到了一个识别纸张并校正的需求,背景是学生需要通过上传自己的板书到虚拟小黑板供老师进行批改,学生们拍的角度、形状不一,所以需要纸张自动校正功能。这就促使我去了解计算机图像处理和OpenCV,以下就是基于 OpenCV.js 识别和校正纸张时的主要流程。
对图像进行resize操作到预定大小,缩小图像能够对图像进行统一化参数处理并且减少识别时间。
const dsize = new cv.Size(this.width, this.height);
let dst = new cv.Mat();
// 重置图像大小
cv.resize(src, dst, dsize, 0, 0, cv.INTER_AREA);
为了对图像进行边缘提取,先对图像进行预处理,减少图像噪声。通常预处理包括把图像转为灰度图像,并用滤波去除图像噪声,这里选用的是高斯滤波。高斯滤波是一种线性平滑滤波,适用于消除噪声,保证性能的同时效果也不错。
// 转灰度图像
cv.cvtColor(img, img, cv.COLOR_RGB2GRAY);
// 高斯滤波
cv.GaussianBlur(img, img, new cv.Size(5, 5), 0, 0);
通过Canny边缘识别函数识别得到图像的全部轮廓信息。
cv.Canny(img, img, 20, 50, 3); //边缘提取
拍摄纸张时,纸张通常在图像里所占面积是最大的。通过筛选轮廓,选择面积最大的闭合轮廓,并且其面积不能过小。
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
// 找出所有轮廓,并遍历筛选
cv.findContours(img, contours, hierarchy, cv.RETR_CCOMP, cv.CHAIN_APPROX_NONE);
let index = 0, maxArea = 0;
if (!contours.size()) return;
const area = this.width * this.height;
for (let i = 0; i < contours.size(); ++i) {
let tempArea = Math.abs(cv.contourArea(contours.get(i)));
if (tempArea > maxArea && tempArea > 0.3 * area) {
index = i;
maxArea = tempArea;
}
}
对轮廓进行多边形拟合,得到四个顶点,不是4个角点则识别失败。
if (maxArea === 0) return;
const foundCours = contours.get(index);
const arcL = cv.arcLength(foundCours, true);
let tmp = new cv.Mat();
// 逼近多边形
cv.approxPolyDP(foundCours, tmp, 0.01 * arcL, true);
if (tmp.total() === 4) {
let points = [];
const data32S = tmp.data32S;
for (let i = 0, len = data32S.length / 2; i < len; i++) {
points[i] = { x: data32S[i * 2] - this.edge, y: data32S[i * 2 + 1] - this.edge };
}
return points;
}
对四个角点坐标进行顺时针或者逆时针排序,并计算原图像对应的四个坐标,根据原图像四点坐标对原图像进行透射变换,校正图片。
// 投影变换
cv.warpPerspective(this.src, target, transmtx, target.size());
图像噪声较多时,轮廓逼近多边形后无法识别为四边形,得到不是4个角点
解决办法:寻找其他识别角点的方案
识别直线:利用霍夫直线检测,原理是变换笛卡尔坐标系到霍夫坐标系中,识别直线的问题变成求解最多直线公共交点的问题,求解得到一系列的直线
筛选直线:排除过于接近的直线,如果筛选完检测出来的直线不是4条,重新调整霍夫参数,重复上一个步骤
求解四条直线的交点
交点在图像所在坐标范围外、过于接近、构成不了四边形则排除,排除得到的角点不是4个则调整霍夫参数,重复上一个步骤
如果重复检测到了预设值,还没有找到角点则失败
此外搜索霍夫直线检测的过程比较耗时,所以要保留多边形逼近,检查不到时再调用霍夫直线检测方法找角点
霍夫直线检测基本原理
</div>
Canny边缘算法无法识别纸张不完全在图像里的情况
Canny边缘算法识别纸张不完全在图像里的情况时,识别出来的轮廓无法闭合,面积为0,这时可以人为填充边界,往图像矩阵外层填充rgba值,使得Canny边缘算法识别出来的轮廓时闭合的,填充的rgba值最起码得有最常见的黑色和白色,由于通常识别的是白色纸张,则优先填充黑色边界,避免不必要的操作执行。
当纸张与背景颜色较为接近时无法识别纸张轮廓
可以通过调整Canny参数获取到颜色较为相近时的轮廓,但是由于降低了阀值,引入了更多的噪声,无法得到闭合的轮廓。通过搜索资料加入边缘检测神经网络HED的效果要明显优于Canny算子边缘检测,由于未实现这个步骤,将不再展开论述,详细可以查阅参考文献。
本文简单介绍了计算机图形处理库 OpenCV 的 JavaScript 版本的一些基本概念和在纸张识别与校正上一个的应用,OpenCV.js的功能十分强大,探索过程十分有趣。OpenCV.js 还加入了深度神经网络的支持,期望 OpenCV.js 在更多实际场景上得到应用。
OpenCV.js 官网:https://docs.opencv.org/3.4/d5/d10/tutorial_js_root.html
OpenCV.js 下载:https://docs.opencv.org/3.4.14/opencv.js
初识 OpenCV.js:https://zhuanlan.zhihu.com/p/50428738
OpenCV Java 实现票据、纸张的四边形边缘检测与提取、摆正:https://www.cnblogs.com/josephkim/p/8319069.html
手机端运行卷积神经网络的一次实践 – 基于 TensorFlow 和 OpenCV 实现文档检测功能:http://fengjian0106.github.io/2017/05/08/Document-Scanning-With-TensorFlow-And-OpenCV/