台球桌边缘检测优化:区域生长、轮廓提取及自适应阈值
2025-03-04 05:34:37
如何优化检测台球桌边缘的流程?
这篇文章要解决的问题,简单说,就是如何从视频帧里更精准地找出台球桌的边缘。 你已经做了初步尝试,但结果还不太满意,想知道怎样改进。
问题原因分析
从你提供的代码和结果图来看,主要问题出在几个方面:
- 区域生长算法(
grow
函数)过于敏感: 容易受到颜色细微变化的影响, 将一些非台球桌区域(白色区域)包括进来。 - 轮廓提取后的处理不够完善: 获得的初始轮廓包含很多噪声,并且形状不规则。
approxPolyDP
尝试逼近四边形,但容易受到轮廓噪声影响, 造成找到的矩形偏差大. - 对于不同的图像,需要统一的处理逻辑: 给出的两个例子显示出很大的差异,对于图二甚至应该将外部作为有效区域进行检测, 而你提供的代码主要着眼于内部轮廓的检测与分析。
解决方案
针对以上问题,我们可以从以下几个方面入手改进:
1. 优化区域生长算法
核心思路是减少grow
函数的敏感性,提升鲁棒性。
- 原理: 不再直接比较原始像素值, 而是考虑颜色空间的差异,并增加对梯度的考量。
- 改进
grow
函数:
void grow_improved(Mat& src, Mat& dest, Mat& mask, Point seed, int colorThresh, int gradThresh) {
stack<Point> point_stack;
point_stack.push(seed);
Vec3b seedColor = src.at<Vec3b>(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
|| estimating_point.y < 0
|| estimating_point.y >= src.rows) {
continue;
}
if (dest.at<uchar>(estimating_point) == 0 && mask.at<uchar>(estimating_point) == 0) {
Vec3b currentColor = src.at<Vec3b>(estimating_point);
// 计算颜色差异(例如,在HSV空间中)
int colorDiff = colorDistance(seedColor, currentColor);
// 计算梯度 (可选, 如果图像有明显边缘)
int gradientMag = 0;
if (estimating_point.x > 0 && estimating_point.x < src.cols - 1) {
Vec3b colorLeft = src.at<Vec3b>(estimating_point - Point(1, 0));
Vec3b colorRight = src.at<Vec3b>(estimating_point + Point(1, 0));
gradientMag = colorDistance(colorLeft, colorRight);
}
// Mat grad_x, grad_y, abs_grad_x, abs_grad_y;
if (colorDiff < colorThresh && gradientMag < gradThresh) {
mask.at<uchar>(estimating_point) = 1;
point_stack.push(estimating_point);
dest.at<uchar>(estimating_point) = 1; // 标记已处理
} else{
dest.at<uchar>(estimating_point) = 255;
}
}
}
}
}
// 计算颜色距离 (示例, 可根据需要调整)
int colorDistance(Vec3b c1, Vec3b c2) {
// 可以考虑转换到 HSV 或 Lab 颜色空间进行计算
return sqrt(pow(c1[0] - c2[0], 2) + pow(c1[1] - c2[1], 2) + pow(c1[2] - c2[2], 2));
}
// 使用方法 (在原代码中替换 `grow` 调用)
// ...
grow_improved(sharp, dest, mask, Point(x, y), 30, 50); // 调整颜色和梯度阈值
// ...
-
颜色差异计算: 上面给的是 RGB 空间中的简单距离计算。建议尝试转到 HSV 或 Lab 颜色空间,,这样颜色差异的计算会更符合人眼感知. 可以自己编写 HSV/Lab 的颜色距离函数, 或直接用 OpenCV 的
cvtColor
转换色彩空间后计算。 -
梯度计算: 上述例子中用左右像素简单计算梯度。如果需要更精细的梯度信息,可以考虑使用 Sobel 算子 (
Sobel
函数).//计算梯度的参考实现: Mat gray; cvtColor(src, gray, COLOR_BGR2GRAY); // 先转成灰度图 Mat grad_x, grad_y; Sobel(gray, grad_x, CV_16S, 1, 0, 3); // X方向梯度 Sobel(gray, grad_y, CV_16S, 0, 1, 3); // Y方向梯度 // 计算梯度幅值 convertScaleAbs(grad_x, grad_x); convertScaleAbs(grad_y, grad_y); Mat gradientMag; addWeighted(grad_x, 0.5, grad_y, 0.5, 0, gradientMag); // 简单融合 // 在 grow_improved 中使用 gradientMag.at<uchar>(estimating_point) 进行比较 ```
-
安全建议:
colorThresh
和gradThresh
需要根据实际情况调整. 太小的值会导致区域生长过于严格,太大会导致区域生长过于宽松。 可以提供可配置的接口让用户调整,或者根据图像特征自动计算。- 为防止栈溢出 (如果处理的区域很大), 可以将递归的区域生长算法改为迭代版本. 上面的代码示例已经是迭代版本,因此这部分不用改动。
2. 优化轮廓提取和四边形逼近
这部分主要解决找到的轮廓不精确,以及 approxPolyDP
效果不佳的问题.
-
原理: 使用更 robust 的边缘检测和轮廓筛选方法,并优化
approxPolyDP
的使用。 -
具体步骤:
-
Canny 边缘检测 (替代形态学操作): Canny 边缘检测对噪声的抑制更好,也更能找到清晰的边缘。
Mat canny; Canny(morph, canny, 50, 150, 3); // 参数需要根据实际情况调整 imshow("Canny", canny);
-
轮廓筛选: 移除面积过小或过于细长的轮廓。
vector<vector<Point>> contours; vector<Vec4i> hierarchy; findContours(canny, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 只检测外轮廓 vector<vector<Point>> filteredContours; for (size_t i = 0; i < contours.size(); i++) { double area = contourArea(contours[i]); Rect boundingBox = boundingRect(contours[i]); double aspectRatio = (double)boundingBox.width / boundingBox.height; // 筛选条件 (根据实际情况调整) if (area > 1000 && aspectRatio > 0.5 && aspectRatio < 2) { // 面积和长宽比 filteredContours.push_back(contours[i]); } } // 可视化筛选后的轮廓 Mat contourImage = Mat::zeros(canny.size(), CV_8UC3); drawContours(contourImage, filteredContours, -1, Scalar(0, 255, 0), 2); imshow("Filtered Contours", contourImage);
-
改进四边形逼近:
vector<Point> bestApprox; double minPerimeter = 1e9; // 随便设置个很大的初始周长值。 //对筛选后的所有轮廓都进行一次循环, 寻找最合适的一个. for (const auto& cnt : filteredContours) { double perimeter = arcLength(cnt, true); vector<Point> approx; //逐步增加精度, 找最符合条件的四边形, 限制周长和边数 double d=0; do { d=d+0.5; approxPolyDP(cnt, approx, d, true); // 注意:这里用了原始的 cnt, 不是之前的 approx }while(approx.size()>4); if (approx.size() == 4 && perimeter < minPerimeter) { bestApprox = approx; minPerimeter = perimeter; } } if (!bestApprox.empty()) { // 绘制最终的四边形 vector<vector<Point>> finalContour; finalContour.push_back(bestApprox); drawContours(rgb, finalContour, 0, Scalar(255, 0, 0), 3); imshow("Final Rectangle", rgb); //输出找到的点坐标。 cout << "Best Approx: " << bestApprox << endl; } else { cout << "未找到合适的四边形。" << endl; }
- 主要改动是用
arcLength
计算周长作为约束,这样找到的四边形会更接近真实的台球桌。 - 上面是迭代对所有符合要求的轮廓进行遍历,这样可以找到更符合要求的四边形。
-
3. 统一处理不同图像:自适应阈值与参数
解决不同光照、不同颜色台球桌的适应性问题。
-
原理: 使用图像的统计信息,自动调整阈值等参数。
-
实现:
-
颜色阈值自适应: 可以根据种子点的颜色,动态调整
grow_improved
中的colorThresh
。 例如,将colorThresh
设置为种子点颜色在 HSV 空间中饱和度和亮度的某个比例。 -
获取自适应参数:
// Mat hsv; //cvtColor(src, hsv, COLOR_BGR2HSV); //将颜色转化到 HSV 空间进行操作会更加符合人眼对颜色的感知 // 获取种子像素的颜色。 Vec3b seedColor = sharp.at<Vec3b>(seed); // 可以在HSV空间中分别计算均值和方差, 更能反映出整体情况. //Scalar mean, stddev; // meanStdDev(hsv, mean, stddev); // 根据图像的统计信息,动态调整 colorThresh 和 gradThresh // double colorThresh = 0.1 * stddev.val[1]; // 例如,使用饱和度的标准差的一部分。 值需要调 // double gradThresh = ... ; double colorThresh=75;
-
梯度阈值调整 : 在有光照变化或阴影时特别有用, 可以使用统计直方图来完成自适应梯度调整, 让梯度的区分更加合理。
-
Canny 边缘检测阈值自适应: OpenCV 有提供
adaptiveThreshold
函数. 可以在调用 Canny 前,使用它来自适应调整。 但针对台球桌场景,使用大津法 (Otsu's method) 可能会更合适。
```c++ Mat gray; cvtColor(morph, gray, COLOR_BGR2GRAY); double otsuThreshold = threshold(gray, Mat(), 0, 255, THRESH_BINARY | THRESH_OTSU); // 获取大津法阈值。
Canny(morph, canny, otsuThreshold * 0.5, otsuThreshold, 3); // 用大津法得到的阈值来调整。
```
-
总结
通过上述的优化方案,我们可以得到一个比较通用,而且相对精确的台球桌边缘检测方案。 主要包括:
- 使用改进的区域生长算法,更加精确地找到台球桌所在的区域。
- 使用 Canny 边缘检测,并配合适当的轮廓筛选,来进一步处理,找到可靠轮廓。
- 使用改进的四边形逼近方法。 限制了边数为 4, 通过调整参数得到最优解.
- 使用一些简单的自适应策略,提高算法的鲁棒性。
把这些改动组合起来,你应该可以获得比之前好得多的结果. 如果对边缘的平滑度有更高要求,还可以做进一步的曲线拟合等优化。