如何优化台球桌比赛区域的四边轮廓检测效果?
台球桌比赛区域四边形轮廓检测优化需求
我正在开展一项项目,需从视频帧中检测台球桌与台球,并重建2D游戏小地图,核心目标是获取台球桌比赛区域的边缘。目前采用的流程为:种子生长算法生成比赛区域mask → 对mask执行形态学操作 → 轮廓检测 → approxPolyDP四边形拟合,但检测效果无法满足需求。我需要检测出的轮廓尽可能覆盖图像中的白色区域,甚至在部分场景中可超出白色区域,以准确估计比赛区域边缘;同时输入视频中的台球桌无固定颜色,需适配该特性。
相关实现代码如下:
比赛区域mask生成代码
Mat src = imread(src_path); assert(!src.empty()); imshow("Source", src); Mat hsv; cvtColor(src, hsv, COLOR_BGR2HSV); imshow("HSV", hsv); /* 开始处理背景与比赛区域掩码 */ Mat sharp; Mat sharpening_kernel = (Mat_<double>(3, 3) << -1, -1, -1, -1, 9, -1, -1, -1, -1); filter2D(hsv, sharp, -1, sharpening_kernel); //imshow("Sharpening", sharp); double sigma=1; GaussianBlur(sharp, sharp, Size(), sigma); //imshow("Blurred Sharp", sharp); //waitKey(0); int min_region_area = int(min_region_area_factor * sharp.cols * sharp.rows); // 忽略小区域 int max_region_area = int(max_region_area_factor * sharp.cols * sharp.rows); // 忽略过大区域 // dest用不同数值标记所有区域:0-未确定,255-忽略,其他-已确定 uchar padding = 1; // dest中用于标记区域的数值 Mat dest = Mat::zeros(sharp.rows, sharp.cols, CV_8UC1); // mask记录当前区域,始终用1标记 Mat mask = Mat::zeros(sharp.rows, sharp.cols, CV_8UC1); // 最终保存的掩码 Mat final_mask; // 遍历整张图像,对未确定像素执行种子生长 for (int x=0; x<sharp.cols; ++x) { for (int y=0; y<sharp.rows; ++y) { if (dest.at<uchar>(Point(x, y)) == 0) { grow(sharp, dest, mask, Point(x, y), thresh); int mask_area = (int)sum(mask).val[0]; // 计算种子生长得到的区域面积 if (mask_area > min_region_area && mask_area < max_region_area) { dest = dest + mask * padding; // 将新区域记录到dest final_mask = mask*255; //waitKey(); if(++padding > max_region_num) { printf("run out of max_region_num..."); return -1; } } else { dest = dest + mask * 255; // 标记为忽略区域 } mask = mask - mask; // 重置mask,准备下一次种子生长 } } }
种子生长函数代码
void grow(Mat& src, Mat& dest, Mat& mask, Point seed, int thresh) { //thresh=200 /* 对给定种子执行种子生长 * 参数: * src: 源图像 * dest: 标记像素状态(已确定/未确定/忽略)的矩阵 * mask: 记录当前种子生长区域的矩阵 */ stack<Point> point_stack; point_stack.push(seed); while(!point_stack.empty()) { Point center = point_stack.top(); mask.at<uchar>(center) = 1; point_stack.pop(); for (int i=0; i<8; ++i) { Point estimating_point = center + PointShift2D[i]; if (estimating_point.x < 0 || estimating_point.x > src.cols-1 || estimating_point.y < 0 || estimating_point.y > src.rows-1) { // 估计点不能超出图像范围 continue; } else { // delta = (H-H')² + (S-S')² + (V-V')² int delta = int(pow(src.at<Vec3b>(center)[0] - src.at<Vec3b>(estimating_point)[0], 2) + pow(src.at<Vec3b>(center)[1] - src.at<Vec3b>(estimating_point)[1], 2) + pow(src.at<Vec3b>(center)[2] - src.at<Vec3b>(estimating_point)[2], 2)); if (dest.at<uchar>(estimating_point) == 0 && mask.at<uchar>(estimating_point) == 0 && delta < thresh) { mask.at<uchar>(estimating_point) = 1; point_stack.push(estimating_point); } } } } }
轮廓拟合代码
Mat im = imread(argv[1], 0); // 边缘图像 Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(11, 11)); Mat morph; morphologyEx(im, morph, MORPH_CLOSE, kernel); int rectIdx = 0; //vector<vector<Point>> contours; //vector<Vec4i> hierarchy; findContours(morph, contours, hierarchy, RETR_CCOMP, CHAIN_APPROX_SIMPLE, Point(0, 0)); for (size_t idx = 0; idx < contours.size(); idx++) { RotatedRect rect = minAreaRect(contours[idx]); double areaRatio = abs(contourArea(contours[idx])) / (rect.size.width * rect.size.height); if (areaRatio > .99) { rectIdx = idx; break; } } //cout << contours.size() << ", rectIdx: " <<rectIdx<<", cont[idx] = "<<contours[rectIdx]<<endl; // 获取轮廓的凸包 vector<Point> hull; convexHull(contours[rectIdx], hull, false, true); // 可视化 Mat rgb; Mat only_contours = Mat::zeros(im.rows, im.cols, CV_8UC1); cvtColor(im, rgb, COLOR_GRAY2RGB); //drawContours(rgb, contours, rectIdx, Scalar(0, 0, 255), 2); for(size_t i = 0; i < hull.size(); i++) { line(rgb, hull[i], hull[(i + 1)%hull.size()], Scalar(0, 255, 0), 2); line(only_contours, hull[i], hull[(i + 1)%hull.size()], Scalar(255), 2); } imshow("Result", rgb); imshow("Contour", only_contours); imwrite("./contour.png", only_contours); vector<Point> approx; double d=0; do { d=d+0.5; approxPolyDP(contours[0],approx,d,true); //cout << approx.size() << " " <<d<<endl; } while (approx.size()>4); cout << "Approx: " << approx << endl; contours.push_back(approx); drawContours(rgb,contours,contours.size()-1,Scalar(0, 0, 255),3); imshow("Ctr",rgb);
优化方案
1. 种子生长阶段修正与优化
- 颜色空间与相似度计算调整:放弃HSV全通道欧氏距离,改用Lab颜色空间(更贴合人眼感知,减少颜色偏差影响),或单独用V通道(亮度)结合S通道(饱和度)做判断——比赛区域通常是高亮度、低饱和度区域,可过滤掉彩色干扰。
- 动态阈值替代固定值:不要用固定
thresh=200,可根据种子点的局部颜色方差动态计算阈值,或分区域统计HSV分布设置自适应阈值,避免漏连有效区域或误连背景。 - 种子点精准筛选:无需遍历所有像素,先通过Canny边缘检测提取潜在桌边缘,在边缘附近的高亮度区域选种子;或先做全局亮度阈值分割,缩小候选区域后再执行种子生长,减少无效计算。
- 区域筛选逻辑修复:当前代码中
final_mask会被每次符合条件的区域覆盖,最终仅保留最后一个区域,这是错误的。应收集所有符合面积范围的区域,选择面积最大的区域(台球桌比赛区域是画面中面积最大的区域之一)作为最终mask。
2. Mask后处理优化
- 形态学操作动态适配:固定大小的椭圆核易导致过度膨胀或修复不足,可根据图像分辨率动态设置核大小(比如设为图像最短边的1/50);先做开运算去除小噪点,再做闭运算填补孔洞,避免边缘失真。
- Mask补全修复:若mask存在孔洞或边缘残缺,可先用
cv::convexHull生成凸包,再用cv::fillPoly补全;或用cv::inpaint修复缺失部分,确保mask尽可能覆盖完整比赛区域。
3. 轮廓拟合阶段优化
- 轮廓筛选逻辑放宽与精准化:当前
areaRatio > .99的阈值过于严格,台球桌mask不可能完全贴合最小外接矩形,建议将阈值降至0.8-0.9;同时优先筛选面积最大的轮廓,再结合轮廓凸包面积比、外接矩形宽高比(台球桌有标准比例,如斯诺克2:1、美式4:3)过滤无效轮廓。 approxPolyDP拟合优化:- 不要用
contours[0]拟合,应使用筛选后的目标轮廓(如面积最大的轮廓)。 - 放弃循环递增
d的方式,改用OpenCV官方推荐的方法:以轮廓周长的百分比作为epsilon(如epsilon = 0.02 * cv::arcLength(contour, true)),拟合更稳定。 - 若拟合后点数多于4,先对轮廓做凸包,再对凸包执行
approxPolyDP——台球桌是凸四边形,凸包可过滤掉轮廓上的小凸起。
- 不要用
- 强制四边形约束:若
approxPolyDP无法得到4个点,可先用cv::minAreaRect获取旋转矩形,提取四个顶点作为初始四边形,再结合mask区域向外扩展若干像素,确保轮廓覆盖甚至超出mask边缘。
4. 整体流程重构(可选)
- 先边缘检测再生成mask:先对原图做Canny边缘检测,再用霍夫直线检测提取潜在桌边缘直线,通过直线的交点、角度筛选出符合台球桌特征的四条边,再生成mask——这种方法对无固定颜色的场景鲁棒性更强。
- 多帧融合跟踪:针对视频帧,可结合前后帧的检测结果,用卡尔曼滤波跟踪台球桌轮廓,减少单帧检测的误差。
内容的提问来源于stack exchange,提问作者LordGrim
相关产品推荐
相关产品推荐

