基于OpenCV获取带圆角变形卡牌的精准近似轮廓
万智牌图像提取算法的轮廓拟合问题
我用OpenCV的FindContours和ApproxPolyDP写了一套万智牌图像提取算法:先对图像做模糊、自适应阈值预处理,找到所有轮廓后筛选出近似矩形的最大轮廓,再通过透视变换规整为固定尺寸。但目前ApproxPolyDP无法精准贴合卡牌边缘,导致透视变换时出现切边情况。
推测问题源于卡牌的圆角设计,试过调整ApproxPolyDP的epsilon值,也用过MinAreaRect(能较好对齐单边但解决不了透视问题),均未解决。
算法代码
private Mat ExtractCard(Mat image) { CvInvoke.Imwrite(Path.Join(Path.GetTempPath(), "output_orig.png"), image); Mat temp = image.Clone(); Mat gray = new(); CvInvoke.CvtColor(image, gray, ColorConversion.Bgr2Gray); Mat blurred = new(); CvInvoke.GaussianBlur(gray, blurred, new Size(5, 5), 0); Mat thresh = new(); CvInvoke.AdaptiveThreshold(blurred, thresh, 255, AdaptiveThresholdType.MeanC, ThresholdType.BinaryInv, 11, 2); Mat kernel = CvInvoke.GetStructuringElement(ElementShape.Rectangle, new Size(2, 2), new Point(-1, -1)); CvInvoke.Dilate(thresh, thresh, kernel, new Point(-1, -1), 1, BorderType.Reflect, default(MCvScalar)); CvInvoke.Erode(thresh, thresh, kernel, new Point(-1, -1), 1, BorderType.Reflect, default(MCvScalar)); CvInvoke.Imwrite(Path.Join(Path.GetTempPath(), "output_thresh.png"), thresh); var contours = new VectorOfVectorOfPoint(); Mat hierarchy = new(); CvInvoke.FindContours(thresh, contours, hierarchy, RetrType.List, ChainApproxMethod.ChainApproxSimple); VectorOfPoint? cardContour = null; double largestArea = 0; for (int i = 0; i < contours.Size; i++) { VectorOfPoint contour = contours[i]; double area = CvInvoke.ContourArea(contour); VectorOfPoint approxContour = new(); double epsilon = CvInvoke.ArcLength(contour, true) * 0.02; CvInvoke.ApproxPolyDP(contour, approxContour, epsilon, true); if (approxContour.Size == 4 && CvInvoke.IsContourConvex(approxContour) && area > largestArea) { largestArea = area; cardContour = approxContour; } } if (cardContour == null) { throw new Exception("No card found"); } Point[] sortedPoints = SortedPoints(cardContour.ToArray()); PointF[] src = { sortedPoints[0], sortedPoints[1], sortedPoints[2], sortedPoints[3] }; PointF[] dst = { new(0, 0), new(800, 0), new(0, 1120), new(800, 1120) }; var transform = CvInvoke.GetPerspectiveTransform(src, dst); var output = new Mat(); CvInvoke.WarpPerspective(image, output, transform, new System.Drawing.Size(800, 1120)); CvInvoke.DrawContours(temp, new VectorOfVectorOfPoint(cardContour), 0, new MCvScalar(0,255,0,255)); CvInvoke.Imwrite(Path.Join(Path.GetTempPath(), "output_contour.png"), temp); CvInvoke.Imwrite(Path.Join(Path.GetTempPath(), "output.png"), output); return output; }
调试图像说明
- 原图:卡牌带有轻微倾斜,放置在浅色平面上
- 阈值处理后:卡牌区域被完整二值化,但边缘因圆角出现不规则轮廓
- 轮廓图:绿色拟合轮廓明显偏离卡牌实际边缘,四个角向内收缩
- 输出结果:透视变换后卡牌边缘被裁切,部分图案缺失
解决方案思路
1. 用凸包预处理适配圆角轮廓
卡牌的圆角会让原始轮廓包含圆弧段,直接用ApproxPolyDP拟合会导致顶点内缩。可以先提取轮廓的凸包,再对凸包做多边形拟合:
VectorOfPoint convexHull = new(); CvInvoke.ConvexHull(contour, convexHull); // 调小epsilon提升拟合精度 double epsilon = CvInvoke.ArcLength(convexHull, true) * 0.01; CvInvoke.ApproxPolyDP(convexHull, approxContour, epsilon, true);
凸包会忽略圆角的凹陷,得到接近卡牌外接矩形的轮廓,再拟合多边形能更贴近实际边缘。
2. 替换预处理流程增强边缘连续性
当前的自适应阈值+开闭运算可能导致边缘断裂,尝试用Canny边缘检测替代:
Mat edges = new(); CvInvoke.Canny(blurred, edges, 50, 150); // 膨胀操作连接断裂的边缘 CvInvoke.Dilate(edges, edges, kernel, new Point(-1, -1), 2);
Canny能更精准地提取边缘轮廓,减少圆角带来的干扰。
3. 基于最小外接矩形优化透视变换
利用MinAreaRect得到贴合卡牌的外接矩形顶点,以此作为透视变换的源点:
// 找到目标轮廓后计算最小外接矩形 RotatedRect minRect = CvInvoke.MinAreaRect(cardContour); Point2f[] rectPoints = minRect.Points(); // 对顶点排序后用于透视变换 Point[] sortedRectPoints = SortedPoints(rectPoints.Select(p => new Point((int)p.X, (int)p.Y)).ToArray());
MinAreaRect的顶点贴合卡牌外接矩形,能避免圆角导致的内缩问题。
4. 手动补偿拟合误差
如果以上方法仍有轻微误差,可以对拟合的顶点做向外偏移(偏移量根据卡牌实际尺寸调整,比如5-10像素):
// 根据顶点位置(左上、右上、左下、右下)对应调整偏移方向 int offset = 8; Point[] offsetPoints = new Point[4]; // 左上顶点:X左移,Y上移 offsetPoints[0] = new Point(sortedPoints[0].X - offset, sortedPoints[0].Y - offset); // 右上顶点:X右移,Y上移 offsetPoints[1] = new Point(sortedPoints[1].X + offset, sortedPoints[1].Y - offset); // 左下顶点:X左移,Y下移 offsetPoints[2] = new Point(sortedPoints[2].X - offset, sortedPoints[2].Y + offset); // 右下顶点:X右移,Y下移 offsetPoints[3] = new Point(sortedPoints[3].X + offset, sortedPoints[3].Y + offset);
内容的提问来源于stack exchange,提问作者Brett
相关产品推荐
相关产品推荐

