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