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

如何检测答题卡中铅笔填涂选项的轮廓?

答题卡填涂选项轮廓检测问题

我需要检测答题卡中被铅笔填涂的圆形选项轮廓,但使用findContours()无法正常工作——要么检测到错误轮廓,要么完全检测不到。我也试过HoughCircles(),效果同样不理想。我对OpenCV不太熟悉,只了解这两种方法,附上我的代码,求可行的轮廓检测解决方案。

待处理的答题卡图片

待处理的答题卡图片

我的代码

void CorrectPerspective(const cv::Mat& InputArray, cv::Mat& OutputArray, const std::vector<cv::Point2f>& InputPoints) {
    std::vector<cv::Point2f> xSorted = InputPoints;
    std::sort(
        xSorted.begin(),
        xSorted.end(),
        [](const cv::Point2f& a, const cv::Point2f& b) {return a.x < b.x; }
    );
    std::vector<cv::Point2f> left(xSorted.begin(), xSorted.begin() + 2);
    std::vector<cv::Point2f> right(xSorted.begin() + 2, xSorted.end());
    std::sort(
        left.begin(),
        left.end(),
        [](const cv::Point2f& a, const cv::Point2f& b) {return a.y < b.y; }
    );
    cv::Point2f topLeft = left[0];
    cv::Point2f bottomLeft = left[1];
    std::vector<float> distance;
    for (const auto& point : right) {
        distance.push_back(cv::norm(topLeft - point));
    }
    cv::Point2f topRight = right[distance[0] < distance[1] ? 0 : 1];
    cv::Point2f bottomRight = right[distance[0] < distance[1] ? 1 : 0];
    std::vector<cv::Point2f> sourceVertex = {
        topLeft,
        topRight,
        bottomRight,
        bottomLeft
    };
    float widthA = std::sqrt(std::pow(bottomRight.x - bottomLeft.x, 2) + std::pow(bottomRight.y - bottomLeft.y, 2));
    float widthB = std::sqrt(std::pow(topRight.x - topLeft.x, 2) + std::pow(topRight.y - topLeft.y, 2));
    int width = static_cast<int>(std::max(widthA, widthB));
    float heightA = std::sqrt(std::pow(topRight.x - bottomRight.x, 2) + std::pow(topRight.y - bottomRight.y, 2));
    float heightB = std::sqrt(std::pow(topLeft.x - bottomLeft.x, 2) + std::pow(topLeft.y - bottomLeft.y, 2));
    int height = static_cast<int>(std::max(heightA, heightB));
    std::vector<cv::Point2f> targetVertex = {
        cv::Point2f(0, 0),
        cv::Point2f(width - 1, 0),
        cv::Point2f(width - 1, height - 1),
        cv::Point2f(0, height - 1)
    };
    cv::Mat transformMatrix = cv::getPerspectiveTransform(sourceVertex, targetVertex);
    cv::warpPerspective(InputArray, OutputArray, transformMatrix, cv::Size(width, height));
}

void PreProcess(const cv::Mat& InputArray, cv::Mat& OutputArray1, cv::Mat& OutputArray2) {
    cv::Mat gray;
    cv::cvtColor(
        InputArray,
        gray,
        cv::COLOR_BGR2GRAY
    );
    cv::Mat gaussianBlurred;
    cv::GaussianBlur(
        gray,
        gaussianBlurred,
        cv::Size(5, 5),
        0
    );
    cv::Mat edged;
    cv::Canny(
        gaussianBlurred,
        edged,
        50,
        200
    );
    OutputArray1 = edged.clone();
    cv::threshold(
        gaussianBlurred,
        OutputArray2,
        0,
        255,
        cv::THRESH_BINARY_INV | cv::THRESH_OTSU);
}

int main() {
    cv::Mat img;
    img = cv::imread("I:\\ProjectArtemis\\OpenCVTest1\\testimg.png");

    std::vector<std::vector<cv::Point>> contours;
    std::vector<cv::Vec4i> hierarchy;
    cv::Mat edged;
    cv::Mat thresh;
    PreProcess(img, edged, thresh);
    cv::findContours(
        edged.clone(),
        contours,
        hierarchy,
        cv::RETR_EXTERNAL,
        cv::CHAIN_APPROX_SIMPLE
    );

    cv::Mat warped;
    cv::Mat warped_thresh;
    for (const auto& c : contours) {
        double peri = 0.01 * cv::arcLength(c, true);
        std::vector<cv::Point> approx;
        cv::approxPolyDP(c, approx, peri, true);

        if (approx.size() == 4) {
            std::vector<cv::Point2f> points;
            for (const auto& point : approx) {
                points.push_back(cv::Point2f(point.x, point.y));
            }
            CorrectPerspective(img, warped, points);
            CorrectPerspective(thresh, warped_thresh, points);
        }
    }

    cv::Mat opt;
    cv::Mat opt_thresh;
    PreProcess(warped, opt, opt_thresh);    //能跑起来就先别动
    std::vector<std::vector<cv::Point>> optionContours;
    std::vector<cv::Vec4i> optionHierarchy;
    cv::findContours(
        opt.clone(),
        optionContours,
        optionHierarchy,
        cv::RETR_EXTERNAL,
        cv::CHAIN_APPROX_SIMPLE
    );

    cv::cvtColor(warped_thresh, warped_thresh, cv::COLOR_GRAY2BGR);

    std::vector<std::vector<cv::Point>> options;
    std::vector<cv::Rect> boundingRects;
    for (size_t i = 0; i < optionContours.size(); i++) {
        cv::Rect bounding = cv::boundingRect(optionContours[i]);
        int x = bounding.x;
        int y = bounding.y;
        int w = bounding.width;
        int h = bounding.height;

        float ar = w / static_cast<float>(h);

        if (w >= 25 && h >= 25 && ar >= 0.6 && ar <= 1.3) {
            options.push_back(optionContours[i]);
            boundingRects.push_back(bounding);
            cv::putText(warped, std::to_string(i), cv::Point(x - 1, y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255), 2);
            cv::putText(warped_thresh, std::to_string(i), cv::Point(x - 1, y - 5), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255), 2);
        }
    }
}

解决方案

问题分析

你的代码在透视校正后重复调用了PreProcess,但校正后的warped_thresh已经是二值化图,重复处理会引入额外噪声;另外原预处理的全局阈值和边缘检测参数不适合答题卡光照不均的场景,导致轮廓检测失效。

改进步骤

  1. 优化预处理逻辑:改用自适应阈值代替全局阈值,更好应对局部光照差异;调整Canny边缘检测阈值贴合填涂区域特征。
  2. 直接使用校正后的二值图:避免重复预处理,减少噪声干扰。
  3. 优化轮廓筛选条件:结合轮廓面积、圆形度(而非仅宽高比)精准筛选填涂圆形。

改进后的关键代码

替换原PreProcess函数

void PreProcess(const cv::Mat& InputArray, cv::Mat& OutputArray1, cv::Mat& OutputArray2) {
    cv::Mat gray;
    cv::cvtColor(InputArray, gray, cv::COLOR_BGR2GRAY);
    // 增强对比度,适配浅填涂
    cv::equalizeHist(gray, gray);
    cv::Mat gaussianBlurred;
    cv::GaussianBlur(gray, gaussianBlurred, cv::Size(5, 5), 0);
    // 自适应阈值处理光照不均
    cv::adaptiveThreshold(gaussianBlurred, OutputArray2, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY_INV, 11, 2);
    // 调整Canny阈值,更贴合填涂边缘
    cv::Canny(gaussianBlurred, OutputArray1, 30, 150);
}

调整轮廓检测部分

在main函数中,透视校正后直接使用warped_thresh检测轮廓:

// 替换原opt和opt_thresh相关代码
std::vector<std::vector<cv::Point>> optionContours;
std::vector<cv::Vec4i> optionHierarchy;
// 直接用校正后的二值图检测轮廓
cv::findContours(warped_thresh.clone(), optionContours, optionHierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

cv::cvtColor(warped, warped, cv::COLOR_BGR2GRAY);
cv::cvtColor(warped, warped, cv::COLOR_GRAY2BGR);

std::vector<std::vector<cv::Point>> options;
std::vector<cv::Rect> boundingRects;
for (size_t i = 0; i < optionContours.size(); i++) {
    double area = cv::contourArea(optionContours[i]);
    cv::Rect bounding = cv::boundingRect(optionContours[i]);
    int w = bounding.width;
    int h = bounding.height;
    float ar = w / static_cast<float>(h);
    // 计算圆形度:值越接近1,轮廓越接近圆形
    double perimeter = cv::arcLength(optionContours[i], true);
    double circularity = (4 * M_PI * area) / (perimeter * perimeter);

    // 精准筛选填涂区域
    if (area > 500 && area < 2000 && ar >= 0.8 && ar <= 1.2 && circularity > 0.7) {
        options.push_back(optionContours[i]);
        boundingRects.push_back(bounding);
        cv::rectangle(warped, bounding, cv::Scalar(0, 255, 0), 2);
    }
}

// 显示检测结果
cv::imshow("Detected Options", warped);
cv::waitKey(0);

额外提示

  • 确保imread的路径正确,Windows系统下路径需用双反斜杠\\或正斜杠/。
  • 如果填涂痕迹极浅,可在预处理前增加cv::GaussianBlur的模糊程度,或调整自适应阈值的参数。

内容的提问来源于stack exchange,提问作者ZZhangC

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:05:56