You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用OpenCV填充重叠边缘缺口并保留形状?棋盘g1方格检测失败

棋盘方格检测问题:g1方格无法识别的解决方案

问题说明

灰度图
这张图中,g1方格因字母g与棋盘边缘重叠,导致无法被检测。对应的Canny边缘图如下:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 21:30:56