在此图像中,由于 g 与板边缘重叠,无法检测到正方形 g1。这是一个精明的边缘图像来显示我的意思:
这是截取屏幕截图并检测板并调整图像大小后的最小可重现代码:
#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。我可以做什么来解决这个问题?
我找到了一个非常hacky的方法来解决它。我对这个解决方案并不满意,因为它使用了很多魔法值。魔法值对于我的图像源来说是正确的,但可能不能解决整个问题。我只是不知道 openCV 解决这个问题的方法,因为我是 openCV 的新手。
这是我编写的一个函数,它找到 y 像素范围内的边缘,然后删除所有可能在膨胀过程中导致问题的白色,并用白色填充边缘。
如果有人可以建议 openCV 方式,请告诉我! 但现在我用它来完成棋盘,以便我可以移动棋子检测:
static inline void cure_board_edge_x(int startY, int endY, Mat& img) {
typedef struct {
size_t count;
size_t first_x;
size_t last_x;
}RowData;
size_t len = endY - startY;
RowData *data = (RowData *)malloc(sizeof(RowData)*(len));
ASSERT_EXIT(data, "Failed to allocated memory");
size_t idx = 0;
size_t max_idx = 0;
for(int y = startY; y < endY; y++) {
bool first255found = false;
for(int x = 0; x < img.cols; x++) {
if(img.at<uchar>(y, x) == 255) {
(data[idx].count)++;
if(!first255found) {
first255found = true;
data[idx].first_x = x;
}
data[idx].last_x = x;
}
}
if(data[idx].count > data[max_idx].count) {
max_idx = idx;
}
idx++;
}
startY = startY + (int)max_idx; //-> location of edge
int mY = startY - 1;
int mmY = startY - 2;
int mmmY = startY - 3;
int rangeStart = startY - 50;
int rangeEnd = startY - 30;
int y;
for(int x = data[max_idx].first_x + 5; x < data[max_idx].last_x - 5; x++) {
img.at<uchar>(startY, x) = 255;
for(int i = 0; i < 3; i++) {
y = rangeStart + (i * (rangeEnd - rangeStart) / 3);
if (img.at<uchar>(y, x) != 255) {
img.at<uchar>(mY, x) = img.at<uchar>(mmY, x) = img.at<uchar>(mmmY, x) = 0;
break;
}
}
}
free(data);
}
在主函数膨胀前添加:
int main() {
// more code above ...............
cure_board_edge_x(490, 495, edge_img);
dilate(edge_img, edge_img, kernel, cv::Point(-1,-1), 1);
// more code below ................
}