6、相机标定中的特殊棋盘格检测方法(真是啥需求都会有)

1. 需求

我目前在做自动驾驶车辆上的各类传感器的标定问题。很容易理解,各类传感器就相当于自动驾驶汽车的眼睛,眼睛近视了,我们还能指望他安全吗?所以各类传感器的标定也是一个非常重要的方面。
最近组里有一个我认为有点奇葩的需求,利用棋盘格进行广角相机的内参标定,相机模型用的是之前我们介绍过的OCAM模型。也许你会说,这不都很成熟的算法了,棋盘格检测,opencv就能轻松实现啊。是啊,但是他给的场景是这样的。(原图我不好放,放了一个中间处理的图像)
在这里插入图片描述
这就是创新!我心想你用一个opencv要求的那种棋盘格不行吗,非得整一个这俄罗斯方块式的棋盘格来做。就这几个点能标的好?(后来打脸了,精度还真不错!还是得不断进取啊,对不起,是我肤浅了。)

2. 算法的步骤

废话不多说,直接上解决的步骤吧。
其实,棋盘格的角点检测还是相对较容易地,思路也很简单,就是找到一个个黑色方块,然后取上面的点就好了。步骤大概如下:
①基于灰度直方图分析进行智能图像阈值分割(参考opencv源码)
②设置mask,并直接进入角点检测流程(设置mask是为了去掉一些干扰,前提是你知道“俄罗斯方块”的位置,我们一眼就能看到嘛。直接进入角点检测流程是为了保证一些光线较亮的场景也能检测出来。
③如果第②步没有检测成功,则对阈值分割后的图像做膨胀的形态学操作(一次不行可做多次,这个操作可以使黑色块更好的呈现原本的形状。)
④进入角点检测流程
(1)找到黑色块的轮廓(使用opencv中的检测轮廓的接口,但是我们要用很多限制条件,筛选出黑色方块,因为黑色方块是正方形嘛)
(2)寻找每个黑块的最近邻居,并统计最近邻居个数(去掉重复角点的一个思路,因为上面棋盘格拥有两个最近邻居的角点发生重复。)
在这里插入图片描述
(3)将所有检测到的角点存到一个vector中,并按行进行排序(排序这个应该都会,我就是先按行排了一下,又按列排了一下
(4)到这表示校测成功,进行亚像素检测。

3. 实现的效果

角点是一行一行的画出来的。(支持一个或者多个“俄罗斯方块”角点检测。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. 部分代码

考虑到一些公司的利益,这里不会贴出全部的代码(只贴出整个流程的代码,其中重要函数的实现我相信可以自己实现。),如果有小伙伴对这个项目感兴趣,可以博客交流,互相学习。
.h文件

// 黑色块的四个角点
struct QuadContour
{
    // four corners info (clockwise)
    CvPoint2f pt[4];

    // parent contour ID
    uint8_t parent_contour;

    QuadContour(const CvPoint2f point[4], uint8_t parent_contour_) : parent_contour(parent_contour_)
    {
        pt[0] = point[0];
        pt[1] = point[1];
        pt[2] = point[2];
        pt[3] = point[3];
    }
};

// 角点
struct ChessBoardCorner
{
    CvPoint2f pt;
    bool_t    traverse;  // whether it traversed

    // Its adjacent corner information
    struct ChessBoardCorner* neighbors[4];

    ChessBoardCorner(const CvPoint2f& point = CvPoint2f()) : pt(point), traverse(0)
    {
        neighbors[0] = neighbors[1] = neighbors[2] = neighbors[3] = NULL;
    }
};

// 黑色块
struct ChessBoardQuad
{
    // Number of nearest quadrilateral
    uint8_t count;
    
    // Minimum side length of quadrilateral (square)--给角点排序会用到。
    Scalar edge_len;

    // Quadrilateral four corner information
    ChessBoardCorner* corners[4];

    // nearest quadrilateral information
    struct ChessBoardQuad* neighbors[4];

    ChessBoardQuad() : count(0), edge_len(0)
    {
        corners[0] = corners[1] = corners[2] = corners[3] = NULL;
        neighbors[0] = neighbors[1] = neighbors[2] = neighbors[3] = NULL;
    }
};

// 角点检测器
class ChessBoardDetector
{
public:
    // Binary figure
    CvMat binarized_image_;

    // An array of information to store all quadrilaterals
    cv::AutoBuffer<ChessBoardQuad> all_quads_;

    // An array that stores all quadrilateral corner information
    cv::AutoBuffer<ChessBoardCorner> all_corners_;

    // Stores the total number of quadrilaterals
    uint8_t all_quads_count_;

    ChessBoardDetector() : all_quads_count_(0)
    {
    }

    bool_t Detect(CvMat image, std::vector<CvPoint2f>& corners);

protected:
    void reset()
    {
        all_quads_.deallocate();
        all_corners_.deallocate();
        all_quads_count_ = 0;
    }

    // Extracting quadrilateral information
    void generateQuads(const CvMat& image);

    // Total flow (extract checkerboard corner information and return)
    bool_t processQuads(std::vector<CvPoint2f>& out_corners);

    // Get the nearest neighbor quadrilateral of a quadrilateral
    void findQuadNeighbors();
    
    // Group of detection Angle points
    uint16_t checkQuadGroup(std::vector<ChessBoardQuad*>& quad_group, std::vector<ChessBoardCorner*>& out_corners);

    // Sorts an array of quadrilaterals in the specified order
    void sortForCorners(std::vector<ChessBoardCorner*>& corners, Scalar threshold);
};

.cpp文件

bool_t ChessBoardDetector::Detect(cv::Mat image, std::vector<cv::Point2f>& corners)
{
    bool_t found = false;
    if (image.empty())
    {
        return false;
    }

    int8_t type  = image.type();
    int8_t cn    = CV_MAT_CN(type);
    int8_t depth = CV_MAT_DEPTH(type);
    if (!(cn == 1 || cn == 3 || cn == 4) && !(depth == CV_8U || depth == CV_16U || depth == CV_32F))
    {
        return false;
    }

    CvMat img = image.clone();
    if (img.channels() != 1)
    {
        cvtColor(img, img, cv::COLOR_BGR2GRAY);
    }

    std::vector<CvPoint2f> out_corners;
    CvMat                  threshImgNew = img.clone();

    ///@brief Threshold segmentation of intelligent image based on gray histogram analysis
    icvBinarizationHistogramBased(threshImgNew);  // < OpenCV source
    cv::imwrite("xxx/threshImgNew.jpg", threshImgNew);

    this->reset();

    CvMat binarizedImg = threshImgNew;
    CvMat mask         = CvMat::zeros(CvSize(binarizedImg.cols, binarizedImg.rows), CV_8UC1);
    LOG(INFO) << "Create mask....";
    cv::rectangle(mask, cv::Rect(10, 10, binarizedImg.cols, binarizedImg.rows), cv::Scalar(255), -1);
    // cv::rectangle(mask, cv::Rect(10, binarizedImg.rows / 7, binarizedImg.cols - 10, binarizedImg.rows / 2),
    //               cv::Scalar(255), -1);
    cv::imwrite("xxx/mask.jpg", mask);

    CvMat dst;
    binarizedImg.copyTo(dst, mask);
    cv::imwrite("xxx/dst.jpg", dst);

    generateQuads(dst);

    if (processQuads(out_corners))
    {
        found = true;
    }

    if (!found)
    {
        LOG(INFO) << "The image only after threshold segmentation is not enough to detect corner points, so the image "
                     "needs Morphological manipulations..."
                  << std::endl;
        const uint8_t min_dilations = 0;
        const uint8_t max_dilations = 7;
        for (uint8_t dilations = min_dilations; dilations <= max_dilations; dilations++)
        {
            LOG(INFO) << "Image dilate....";
            cv::dilate(threshImgNew, threshImgNew,
                       cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3), cv::Point(-1, -1)), cv::Point(-1, -1),
                       1);
            cv::imwrite("xxx/dilate.jpg", threshImgNew);

            this->reset();
            binarizedImg = threshImgNew;
            binarizedImg.copyTo(dst, mask);
            cv::imwrite("xxx/dst.jpg", dst);

            generateQuads(dst);

            if (processQuads(out_corners))
            {
                found = true;
                break;
            }
        }
    }

    if (found)
    {
        cv::cornerSubPix(img, out_corners, CvSize(2, 2), CvSize(-1, -1),
                         cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::MAX_ITER, 15, 0.1));

        CvMat drawImg;
        cv::cvtColor(img, drawImg, cv::COLOR_GRAY2BGR);
        for (uint8_t i = 0; i < out_corners.size(); ++i)
        {
            cv::circle(drawImg, out_corners[i], 1, cv::Scalar(0, 0, 255), 3);
            cv::putText(drawImg, std::to_string(i), cv::Point(out_corners[i].x + 1, out_corners[i].y - 1),
                        cv::FONT_HERSHEY_COMPLEX, 0.5, cv::Scalar(0, 0, 255));
        }

        LOG(INFO) << "save corners img....";
        cv::imwrite("xxx/corners_img.jpg", drawImg);

        // save corners
        corners.resize(out_corners.size());
        std::copy(out_corners.begin(), out_corners.end(), corners.begin());
    }

    return found;
}

5. 非常感谢您的阅读!

6 期待您加入

也非常期待您能关注我的微信公众号–“过千帆”,里面不仅有技术文章还有我的读书分享,希望您在那里也有收获。我们一起进步。

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宛如新生

转发即鼓励,打赏价更高!哈哈。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值