卡尺算法流程详解
对应 detection – EdgeDetection 模块的 RulerLinePoints / RulerCirclePoints 两个函数。
1. 卡尺算法是什么
卡尺算法(Caliper/Ruler Algorithm)是基于 1D 采样的边缘检测方法,沿指定方向(直线或圆周)定位边缘点。做法是:
用得多的场合:工业测量的零件尺寸检测、边缘位置偏差类的缺陷检测、基于边缘点的图像配准、直线/圆形的几何参数估计。
2. RulerLinePoints 流程
2.1 完整流程
开始
│
├─ 参数检查 ── 失败 → 返回错误
│ 成功 → 初始化变量
│
├─ 需要显示结果?
│ 是 → 先绘制主直线
│ 否 → 跳过
│
├─ 创建滤波器内核
│
├─ 外层循环:n = 1 … ruler_nums,每个卡尺一轮
│ │
│ ├─ 计算卡尺中心位置
│ │
│ ├─ 采样,双重循环:
│ │ 外层 m = 0 … ruler_width
│ │ 内层 ir = -ruler_height … ruler_height
│ │ 坐标在图像内?
│ │ 否 → 采样值置 -1
│ │ 是 → 双线性插值
│ │ 存储采样值
│ │ ir++ 扫完 ruler_height 后 m++,直到扫完 ruler_width
│ │
│ ├─ 计算投影平均值
│ ├─ 应用滤波器
│ ├─ 峰值检测
│ │ 无峰值 → 直接进入下一个 n
│ │ 有峰值 → 根据选择模式筛选峰值
│ ├─ 选择峰值点
│ ├─ 亚像素定位
│ ├─ 计算边缘坐标,存储边缘点
│ └─ 需要显示结果 → 绘制该卡尺及边缘
│
│ n++;n > ruler_nums 时退出循环
│
└─ 返回 SUCCESS
2.2 核心步骤详解
步骤 1: 参数检查与初始化
// 参数验证
CHECK_MAT_NOT_EMPTY(image); // 图像非空
CHECK_MAT_TYPE(image, CV_8UC1); // 灰度图像
CHECK_POINT_NOT_EMPTY(line.pt1); // 起点有效
CHECK_POINT_NOT_EMPTY(line.pt2); // 终点有效
// 初始化参数
bool show_result = !result_image.empty(); // 是否显示结果
ruler_width = ruler_width <= 0 ? 10 : ruler_width – 1;
ruler_height = ruler_height <= 0 ? 8 : ruler_height;
ruler_nums = ruler_nums <= 0 ? 5 : ruler_nums;
步骤 2: 计算直线参数
// 转换坐标
cv::Point line_st = cv2d::PointToCVPoint(line.pt1);
cv::Point line_ed = cv2d::PointToCVPoint(line.pt2);
// 计算直线角度
double t = atan2(line_ed.y – line_st.y, line_ed.x – line_st.x);
// 计算直线长度
float L = (float)cv::norm(cv::Point2d(line_st.x, line_st.y) –
cv::Point2d(line_ed.x, line_ed.y));
// 计算卡尺间距
float plus_step = (L – ruler_nums * ruler_width) / (ruler_nums + 1.0f);
步骤 3: 创建滤波器
// 差分滤波器(边缘检测)
std::vector<float> filter_kernal(2 * filter_size + 1, 0.0f);
for (int n = 0; n < filter_size; n++) {
filter_kernal[n] = 1; // 正系数(左侧)
filter_kernal[2 * filter_size – n] = –1; // 负系数(右侧)
}
滤波器就是 [1, 1, …, -1, -1] 形式的差分核,用来检测灰度梯度变化——边缘处(灰度突变)会输出峰值。
步骤 4: 循环处理每个卡尺
for (int n = 1; n <= ruler_nums; n++) {
// 4.1 计算卡尺中心位置
float offset = n * (plus_step + ruler_width) – ruler_width + ruler_width / 2.0f;
cv::Point2f center(
line_st.x + offset * cos(t),
line_st.y + offset * sin(t)
);
// 4.2 沿垂直方向采样
std::vector<float> samples;
for (float m = –ruler_width / 2.0f; m <= ruler_width / 2.0f; m += 1.0f) {
for (int ir = –ruler_height; ir <= ruler_height; ir++) {
// 计算采样坐标
float x = center.x + ir * cos(t + PI/2) + m * cos(t);
float y = center.y + ir * sin(t + PI/2) + m * sin(t);
// 双线性插值
float value = BilinearInterpolation(image, x, y);
samples.push_back(value);
}
}
// 4.3 计算投影(沿卡尺宽度方向平均)
std::vector<float> projection;
for (int i = 0; i < 2 * ruler_height + 1; i++) {
float sum = 0;
for (int j = 0; j <= ruler_width; j++) {
sum += samples[i * (ruler_width + 1) + j];
}
projection.push_back(sum / (ruler_width + 1));
}
// 4.4 应用滤波器
std::vector<float> filtered = ApplyFilter(projection, filter_kernal);
// 4.5 峰值检测
std::vector<int> peeks = FindPeaks(filtered, threshold);
// 4.6 选择最佳峰值
int selected = SelectBestPeak(peeks, selection_mode);
// 4.7 亚像素定位
float subpixel_pos = SubpixelRefinement(filtered, selected);
// 4.8 计算边缘坐标
cv::Point2f edge_point(
center.x + subpixel_pos * cos(t + PI/2),
center.y + subpixel_pos * sin(t + PI/2)
);
edge_points.push_back(edge_point);
}
步骤 5: 亚像素定位
对峰值点及其左右邻域做三点抛物线拟合:
// 三点抛物线拟合
int m = peak_index; // 峰值点
int l = m – 1; // 左邻域
int r = m + 1; // 右邻域
float a = filtered[l]; // 左点梯度
float b = filtered[m]; // 峰值点梯度
float c = filtered[r]; // 右点梯度
// 亚像素偏移公式
float u = 0.5f * (a – c) / (a – 2*b + c);
// 最终位置(范围:m-0.5 到 m+0.5)
float subpixel_pos = m + u;
3. RulerCirclePoints 流程
3.1 完整流程
开始
│
├─ 参数检查 ── 失败 → 返回错误
│ 成功 → 初始化变量
│
├─ 需要显示结果?
│ 是 → 先绘制主圆
│ 否 → 跳过
│
├─ 创建滤波器
│
├─ 计算角度步长 plus_step = 2π / ruler_nums
│
├─ 循环:t = -π … π,步长 plus_step
│ │
│ ├─ 计算卡尺中心 cx, cy = center + radius·(cos t, sin t)
│ ├─ 采样圆周方向(径向)
│ ├─ 计算投影平均值
│ ├─ 应用滤波器
│ ├─ 峰值检测
│ │ 无峰值 → t += plus_step
│ │ 有峰值 → 根据选择模式筛选峰值
│ ├─ 亚像素定位
│ ├─ 计算边缘坐标,存储边缘点
│ └─ 需要显示结果 → 绘制该卡尺及边缘
│
│ t += plus_step;t ≥ π 时退出循环
│
└─ 返回 SUCCESS
3.2 与直线卡尺的区别
| 检测路径 | 直线 | 圆周 |
| 坐标计算 | 线性插值 | 极坐标转换 |
| 角度计算 | 固定角度 t = atan2(dy, dx) | 可变角度 t = -π to π |
| 卡尺间距 | 线性间距 | 角度间距 plus_step = 2π/ruler_nums |
| 卡尺方向 | 垂直于直线 | 径向(圆心向外) |
4. 卡尺生成过程可视化
4.1 卡尺结构示意
卡尺(Caliper)结构:
↑ 搜索方向
|
| ruler_height (高度)
|
↓
┌─────────────────────────────────────┐
│ │ ruler_width (宽度)
│ ──────────── 采样线 ────────── │
│ │
│ ──────────── 采样线 ────────── │
│ │
│ ──────────── 采样线 ────────── │
│ │
│ ──────────── 采样线 ────────── │
│ │
│ ──────────── 采样线 ────────── │
│ │
└─────────────────────────────────────┘
↑
|
中心点
(在检测线上)
4.2 直线卡尺生成过程
示例配置:图像 800 × 600,直线 (100, 300) → (700, 300),ruler_nums = 5,ruler_width = 10,ruler_height = 20。
生成过程:
步骤 1: 绘制主直线
(100,300) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (700,300)
步骤 2: 计算卡尺间距
直线长度: 600 像素
ruler_nums: 5
ruler_width: 10
卡尺总宽度: 5 × 10 = 50
可用长度: 600 – 50 = 550
卡尺间距: 550 / 6 ≈ 91.67 像素
步骤 3: 生成卡尺位置
卡尺1: x = 100 + 91.67 + 5 = 196.67
卡尺2: x = 100 + 2×91.67 + 15 = 298.34
卡尺3: x = 100 + 3×91.67 + 25 = 400.00
卡尺4: x = 100 + 4×91.67 + 35 = 501.67
卡尺5: x = 100 + 5×91.67 + 45 = 603.34
步骤 4: 沿垂直方向采样
(100,300) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (700,300)
│ │ │ │ │
│ │ │ │ └─ 卡尺5 (603.34, 300)
│ │ │ └────── 卡尺4 (501.67, 300)
│ │ └────────── 卡尺3 (400.00, 300)
│ └────────────── 卡尺2 (298.34, 300)
└────────────────── 卡尺1 (196.67, 300)
每个卡尺的采样区域 (ruler_height = 20, ruler_width = 10):
↑
20px │ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
│ ● ● ● ● ● ● ● ● ● ●
-20px ● ● ● ● ● ● ● ● ● ● ●
└─────────────
10px 宽度
4.3 圆形卡尺生成过程
示例配置:圆心 (400, 300),半径 150,ruler_nums = 8(每 45° 一个卡尺),ruler_width = 10,ruler_height = 20。
生成过程:
步骤 1: 绘制主圆
┌─────────────────┐
┌───┤ ├───┐
┌─┤ │ │ ├─┐
┌─┤ │ ●(400,300) │ ├─┐
│ │ │ │ │ │
└─┤ │ │ ├─┘
└─┤ │ │ ├─┘
└───┤ ├───┘
└─────────────────┘
步骤 2: 计算卡尺位置(圆周上8个点)
角度步长: plus_step = 2π / 8 = 45°
卡尺1: t = -180° → (-150, 300)
卡尺2: t = -135° → (294, 194)
卡尺3: t = -90° → (400, 150)
卡尺4: t = -45° → (506, 194)
卡尺5: t = 0° → (550, 300)
卡尺6: t = 45° → (506, 406)
卡尺7: t = 90° → (400, 450)
卡尺8: t = 135° → (294, 406)
步骤 3: 径向采样(沿圆心向外)
卡尺3 (400,150)
↑
│
│
────────┼────────●(400,300)────────┼────────
│
│
↓
卡尺7 (400,450)
4.4 采样数据流
原始图像 (灰度)
↓
采样 (沿卡尺方向)
↓
采样数据矩阵 (height × width)
↓
投影 (沿宽度方向平均)
[v1, v2, v3, …, v(2*height+1)]
↓
滤波 (差分滤波器)
[g1, g2, g3, …, g(2*height+1)]
↓
峰值检测
找到梯度最大的位置
↓
亚像素定位
抛物线拟合得到亚像素精度
↓
边缘点坐标
转换回图像坐标系
5. 关键算法详解
5.1 双线性插值
采样坐标大多不是整数,用周围 4 个像素加权取值:
对于坐标 (x, y),设:
– x0 = floor(x), x1 = x0 + 1
– y0 = floor(y), y1 = y0 + 1
– dx = x – x0, dy = y – y0
插值值 = (1-dx)(1-dy)I(x0,y0) + dx(1-dy)I(x1,y0) +
(1-dx)dy I(x0,y1) + dx dy I(x1,y1)
5.2 峰值检测
思路是相邻点做差分,检测符号变化(正→负),再筛掉不超过阈值的:
void FindPeak(int size, float data[], float threshold, std::vector<int>& peaks) {
std::vector<int> sign;
for (int i = 1; i < size; i++) {
float diff = data[i] – data[i–1];
if (diff < 0.0f) sign.push_back(–1);
else if (diff > 0.0f) sign.push_back(1);
else sign.push_back(0);
}
for (int j = 1; j < sign.size(); j++) {
int diff = sign[j] – sign[j–1];
if (diff != 0 && abs(data[j]) > threshold) {
peaks.push_back(j);
}
}
}
5.3 亚像素定位
用峰值点及其左右邻点三点拟合抛物线 y = Ax² + Bx + C,顶点在 x = -B/(2A),化简后就是 u = 0.5 × (a – c) / (a – 2b + c),精度能到 0.01 像素级。
6. 性能
采样数组改用 std::vector(分配次数减少约 95%),RAII 自动管理生命周期消除泄漏风险,容器用 reserve() 预分配减少扩容开销。
7. 适用与局限
精度到亚像素级,投影平均天然抗噪,峰值选择模式可扩展。代价是计算量与卡尺数量成正比,对图像质量有一定要求,参数(宽度、高度、数量、阈值)得按场景调。
边缘清晰、形状规则、需要亚像素精度的检测用它合适;高度模糊或低对比度的图像、极度不规则的边缘,不要硬上。
网硕互联帮助中心




评论前必须登录!
注册