牙叔教程 简单易懂
原图
效果
思路
提取中间扑克的矩形轮廓--> 仿射变换
步骤
1) 高斯滤波平滑图像, 图像基本没变化
Imgproc.GaussianBlur(img.mat, gaussianBlurMat, Size(5, 5), 0);
2) 转灰度图
Imgproc.cvtColor(gaussianBlurMat, grayMat, Imgproc.COLOR_RGBA2GRAY);
3) Canny 边缘检测
Imgproc.Canny(grayMat, cannyMat, lowThreshold, lowThreshold * ratio, kernel_size, false);
4) 开运算
我们要提取扑克的轮廓, 上一步的边缘检测, 图片中间最大的矩形四条边是断开的, 我们要把矩形四条边闭合
Imgproc.morphologyEx( cannyMat, closeMat, Imgproc.MORPH_CLOSE, Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(7, 7)) );
5) 画轮廓
我们只检测 最外围轮廓, 并且画的时候填充轮廓, 可以看到一个大概的矩形
Imgproc.findContours(closeMat, contours, hierarchy, Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE, Point()); Imgproc.drawContours(img.mat, contours, -1, Scalar(0, 255, 0, 255), -1, 8);
6) 在灰度图上填充轮廓
Imgproc.drawContours(closeMat, contours, -1, Scalar(255), -1, 8);
7) 开运算, 只剩下最大的轮廓了
Imgproc.morphologyEx( closeMat, closeMat, Imgproc.MORPH_OPEN, Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(55, 55)) );
8) 最小外接矩形
他这个最小外接矩形, 我是不认同的
for (let j = 0; j < 4; j++) { Imgproc.line(img.mat, rect[j], rect[(j + 1) % 4], Scalar(0, 0, 255, 255), 5, 8); }
9) 我决定倒退至 6)在灰度图上填充轮廓
10)除了最大的轮廓, 其他小轮廓都填充为黑色
// 先排序轮廓 contours.sort( new Comparator({ compare: function (a1, a2) { if (Imgproc.contourArea(a1) > Imgproc.contourArea(a2)) { return 1; } if (Imgproc.contourArea(a1) < Imgproc.contourArea(a2)) { return -1; } return 0; }, }) ); // 再更改颜色 Imgproc.drawContours(closeMat, contours, -1, Scalar(0), -1, 8); Imgproc.drawContours(closeMat, contours, contours.length - 1, Scalar(255), -1, 8);
11) Canny 边缘检测
12) 绘制四条边
// 霍夫变换直线检测 Imgproc.HoughLinesP(mat, lines, 1, Math.PI / 180, threshold, minLineSize, lineGap); // 绘制直线 Imgproc.line(mat, startPoint, endPoint, new Scalar(blue, green, red, 255), 33);
上图中一共有5条直线, 直线数据如下:
[ { angle: 31.947479458708028, distance: 209.77368757782756, x1: 284, y1: 235, x2: 462, y2: 124 }, { angle: 32.005383208083494, distance: 311.3213773578679, x1: 285, y1: 234, x2: 549, y2: 69 }, { angle: 34.07719528013074, distance: 328.3900120283807, x1: 428, y1: 332, x2: 700, y2: 148 }, { angle: 327.89875110416443, distance: 190.057885919001, x1: 292, y1: 245, x2: 453, y2: 346 }, { angle: 333.96422503312334, distance: 193.65174928205528, x1: 527, y1: 59, x2: 701, y2: 144 } ]
分为两组
- 角度小于90度为一组, 有3个
- 角度大于90度左右为一组, 有2个
第一组直线需要去掉一个, 去掉那个直线长度短的
13) 通过四条直线, 计算四个顶点
直线未相交, 要通过计算, 确认交点坐标
已知: 四条直线, 八个坐标,
求: 四条直线的交点
两直线的交点公式
function getFocusCoordinatesOfTwoLines(line1, line2) { var x1 = line1.x1; var y1 = line1.y1; var x2 = line1.x2; var y2 = line1.y2; var x3 = line2.x1; var y3 = line2.y1; var x4 = line2.x2; var y4 = line2.y2; var x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)); var y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / ((x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)); return { x: x, y: y }; }
[ { x: 279.72287662740234, y: 237.29820210787352 }, { x: 548.3290322580646, y: 69.41935483870968 }, { x: 429.2910186352809, y: 331.12666386436877 }, { x: 703.8528728961114, y: 145.39364480557168 }, ];
14) 确认四个顶点的上下左右
任意取一个点, 计算他和其他三个点的距离,
最短的是短边,
最长的是对角线
中间的是长边
两点距离公式
function getDistance(pointA, pointB) { return Math.sqrt(Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)); }
[ { x: 279.72287662740234, y: 237.29820210787352, num: 0, distance: 0 }, { x: 429.2910186352809, y: 331.12666386436877, num: 2, distance: 176.5627631729834 }, { x: 548.3290322580646, y: 69.41935483870968, num: 1, distance: 316.75317552174084 }, { x: 703.8528728961114, y: 145.39364480557168, num: 3, distance: 433.973157450812 } ]
上面按照distance排序, 前两个为一组, 后两个为一组
假设四个点是p1, p2, p3, p4, 观察可知
p1在p2的左侧, 那么扑克是对称的, 现在p3也在p4的左侧,
那么就可以对四个点排序了
假设p1是左上角,
那么p2是右上角
p3是左下角, p4是右下角
{ "topLeft": { "x": 279.72287662740234, "y": 237.29820210787352, "num": 0, "distance": 0 }, "topRight": { "x": 429.2910186352809, "y": 331.12666386436877, "num": 2, "distance": 176.5627631729834 }, "bottomLeft": { "x": 548.3290322580646, "y": 69.41935483870968, "num": 1, "distance": 316.75317552174084 }, "bottomRight": { "x": 703.8528728961114, "y": 145.39364480557168, "num": 3, "distance": 433.973157450812 } }
15) 设计将要显示的扑克尺寸
百度得知中国扑克尺寸是57:87的比例, 我们放大6倍
let poker = { width: 57, height: 87, scale: 6, }; poker.width *= poker.scale; poker.height *= poker.scale;
16) 透视变换
知道了扑克在图片中的四个顶点,
也有了将要显示的扑克尺寸,
开始做透视变换
points = { topLeft: new Point(0, 0), topRight: new Point(poker.width, 0), bottomLeft: new Point(0, poker.height), bottomRight: new Point(poker.width, poker.height), }; let endM = service.getEndM(points); let perspectiveTransform = Imgproc.getPerspectiveTransform(startM, endM); Imgproc.warpPerspective(img.mat, pokerAfterStraightening, perspectiveTransform, new Size(poker.width, poker.height));
显示的颜色不对, 我们转换一下颜色
Imgproc.cvtColor(pokerAfterStraightening, pokerAfterStraightening, Imgproc.COLOR_BGR2RGB);
17) 现在只显示了扑克内部的矩形, 我们把四个顶点向外扩张一点, 显示完整的扑克;
扩张长度大概是扑克内部矩形短边的三分一;
四个顶点都延长同样的长度, 导致变形严重,
所以四个顶点不能延长同样的长度
function extendEndPoint(startPoint, endPoint, distance) { var A = [startPoint.x, startPoint.y]; var B = [endPoint.x, endPoint.y]; var aToB = [B[0] - A[0], B[1] - A[1]]; var aToBDistance = Math.sqrt(aToB[0] * aToB[0] + aToB[1] * aToB[1]); var atoBNormalization = [aToB[0] / aToBDistance, aToB[1] / aToBDistance]; var newEndPoint = { x: atoBNormalization[0] * distance + B[0], y: atoBNormalization[1] * distance + B[1], }; return newEndPoint; }
18) 按比例扩张区域
四个顶点按比例扩张, 结果如下:
图片有一点变形, 基本达到预期
名人名言
思路是最重要的, 其他的百度, bing, stackoverflow, github, 安卓文档, autojs文档, 最后才是群里问问 --- 牙叔教程
声明
部分内容来自网络 本教程仅用于学习, 禁止用于其他用途