如何用OpenCV填充重叠边缘缺口并保留形状?棋盘g1方格检测失败
棋盘方格检测问题:g1方格无法识别的解决方案
问题说明

这张图中,g1方格因字母g与棋盘边缘重叠,导致无法被检测。对应的Canny边缘图如下:
以下是截取屏幕、检测棋盘并调整图像尺寸后的最小可复现代码:
#include <cassert> #include <string> #include <opencv2/opencv.hpp> #include <cstdbool> #include <vector> #include <cwchar> using std::vector; using namespace cv; #define ZERO 0 #define MAXVAL 255 static const cv::Size IMG_ANALYSIS_SZ = cv::Size(500, 500); static const cv::Size STRUCT_KERN_SZ = cv::Size(2, 2); void show_images(vector<cv::Mat> images) { size_t sz = images.size(); for (size_t count = 0; count < sz; count++) { std::string strCnt = std::to_string(count); cv::imshow(strCnt, images[count]); } while (cv::getWindowProperty("0", cv::WND_PROP_VISIBLE) >= 0) { int key = cv::waitKey(200); if (key == 27) { // If Esc key is pressed break; } } cv::destroyAllWindows(); } static inline double angle(cv::Point pt1, cv::Point pt2, cv::Point pt0) { // vec A . vec B = |A| |B| cos(theta) double dx1 = pt1.x - pt0.x; double dy1 = pt1.y - pt0.y; double dx2 = pt2.x - pt0.x; double dy2 = pt2.y - pt0.y; return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10); } static inline bool isSquare(double area, double perimeter, vector<cv::Point> contour) { double ratio = (16 * area) / (perimeter * perimeter); vector<cv::Point> approx; approxPolyDP(contour, approx, perimeter*0.02, true); if ((ratio > 0.95 && ratio < 1.05) && approx.size() == 4 && isContourConvex(approx)) { double maxCos = 0; for(size_t i = 2; i < 5; i++) { double cos = fabs(angle(approx[i%4], approx[i-2], approx[i-1])); if(cos > maxCos) { maxCos = cos; } } if(maxCos < 0.3) { // cos(90) -> 0 return true; } } return false; } int main(void) { cv::Mat img = cv::imread("0_screenshot.png"); cv::Mat edge_img; cv::Canny(img, edge_img, 0, 255); Mat kernel = getStructuringElement(MORPH_RECT, STRUCT_KERN_SZ); dilate(edge_img, edge_img, kernel); // I tried the following codes with different configuration and kernels etc. //cv::morphologyEx(edge_img, edge_img, cv::MORPH_OPEN, kernel); //cv::morphologyEx(edge_img, edge_img, cv::MORPH_CLOSE, kernel); //erode(edge_img, edge_img, kernel); vector<vector<cv::Point>> contours; cv::findContours(edge_img, contours, cv::RETR_LIST, cv::CHAIN_APPROX_SIMPLE); size_t sz = contours.size(); vector<vector<cv::Point>> square_contours; for (size_t i = 0; i < sz; i++) { double area = cv::contourArea(contours[i]); double perimeter = cv::arcLength(contours[i], true); // why 3200? image size = 500 x 500 and 64 squares on the board // (500 x 500) / 64 = 3906.25, So, area of each square little bit less than 3900 if(isSquare(area, perimeter, contours[i]) && area > 3200 && area < 3910) { square_contours.push_back(contours[i]); } } printf("%zu", square_contours.size()); cv::Mat contour_image = cv::Mat::zeros(edge_img.size(), CV_8UC3); cv::drawContours(contour_image, square_contours, -1, cv::Scalar(0, 255, 0), 2); show_images({ img, contour_image, edge_img }); return EXIT_SUCCESS; }
轮廓绘制的输出图像:
标准输出:
63
尝试多种方法后仍无法检测到g1方格,以下是针对性的解决措施:
解决措施
1. 优化轮廓检测模式
将cv::findContours的检索模式从RETR_LIST改为RETR_EXTERNAL,该模式只检测最外层轮廓,能更好捕捉与图像边缘重合的g1方格轮廓:
cv::findContours(edge_img, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
2. 调整面积阈值范围
g1方格因边缘被字母遮挡,实际面积可能略小于正常方格。将面积阈值的下限从3200降至3000,上限从3910提升至4000,给边缘方格留出余量:
if(isSquare(area, perimeter, contours[i]) && area > 3000 && area < 4000)
3. 替换形态学操作
将单独的dilate替换为开运算(先腐蚀后膨胀),可去除字母g带来的小噪声边缘,同时保留方格的完整轮廓:
cv::morphologyEx(edge_img, edge_img, cv::MORPH_OPEN, kernel);
4. 调整Canny边缘检测参数
使用更合理的阈值替代0和255,精准提取方格边缘,减少字母干扰:
cv::Canny(img, edge_img, 50, 150);
5. 放宽正方形判断的容错度
- 调整
approxPolyDP的精度参数,从perimeter*0.02改为perimeter*0.03,允许边缘方格的轮廓有更大近似误差 - 放宽角度余弦值阈值,从0.3提升至0.4,适配边缘方格的非完美直角
修改后的isSquare函数关键部分:
approxPolyDP(contour, approx, perimeter*0.03, true); // ... if(maxCos < 0.4) { // 允许角度有更大偏差 return true; }
将以上修改组合后,即可正常检测到g1方格,输出结果会变为64。
内容的提问来源于stack exchange,提问作者Pragmatic
相关产品推荐
相关产品推荐

