标注 JSON 结构
自动泊车停车场场景的图像标注通常以折线形式描述结构要素,典型 JSON 结构如下:
{
"code": 0,
"data": {
"markData": {
"width": 1920,
"height": 1536,
"marks": [
{
"type": "line",
"pselect": "墙",
"class": {
"pname": "wall"
},
"point": [
{"x": 117.9, "y": 890.8},
{"x": 164.4, "y": 824.3},
{"x": 210.9, "y": 777.4}
],
"attrs": {},
"markId": "y4u-C3GB3x"
},
{
"type": "line",
"pselect": "立柱",
"class": {
"pname": "pillar"
},
"attrs": {
"P": ["square"]
},
"point": [
{"x": 546.7, "y": 626.9},
{"x": 590.8, "y": 621.1}
],
"markId": "y4u-CBB2i3"
}
],
"totalNums": {
"line": 10,
"polygon": 0,
"rect": 0,
"point": 0
}
}
}
}
关键字段说明:
| 字段 | 说明 |
|---|---|
type: "line" | 折线标注(polyline) |
pselect | 中文类别名(墙、路沿、立柱、其他类) |
class.pname | 英文类别名(wall / kerb / pillar / other) |
point | 折线坐标数组,(x,y) 为像素坐标,原点左上角 |
attrs.P | 立柱形状(square=方形柱) |
objectId | 实例 ID,0 表示无实例关联 |
常见标注类别
| 类别 | 说明 | 用途 |
|---|---|---|
| wall(墙) | 停车场侧墙和隔断 | 边界检测 |
| kerb(路沿) | 车位和道路边缘 | 导航限制 |
| pillar(立柱) | 承重柱,通常为方形 | 障碍物避让 |
| other(其他类) | 无法分类的边缘或遮挡 | 训练时通常忽略 |
训练模型选择
这类折线标注本质是 Polyline Detection / Vector Map Learning,不是目标检测。
数据量 < 1 万张:SegFormer(推荐)
把折线标注膨胀成 Mask(每类一个像素值),用 SegFormer 做语义分割:
JSON 折线标注
↓
生成 Mask(墙=1, 路沿=2, 立柱=3, 背景=0)
↓
SegFormer 训练
↓
Wall/Pillar/Kerb 分割图
SegFormer 训练门槛低、开源实现成熟,几天内可出可用结果。
从 Mask 提取折线坐标(后处理):
import cv2
# 骨架提取
skeleton = cv2.ximgproc.thinning(mask)
# 轮廓提取
contours, _ = cv2.findContours(skeleton, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 折线拟合(减少点数)
for cnt in contours:
poly = cv2.approxPolyDP(cnt, epsilon=3, closed=False)
points = [(int(p[0][0]), int(p[0][1])) for p in poly]
数据量 > 10 万张:MapTR(生产级)
MapTR 直接学习向量化地图,输出的就是折线坐标序列,不需要 Mask 中间步骤:
输入图像
↓ MapTR
{"wall": [[x1,y1],[x2,y2],...], "pillar": [...]}
代价是训练难度高、数据格式转换复杂、对数据量要求更高。
方案对比
| 方案 | 输出坐标 | 难度 | 适合数据量 |
|---|---|---|---|
| SegFormer + 后处理 | 间接 | 低 | < 1 万 |
| LaneATT / LSTR | 直接 | 中 | 数万 |
| MapTR | 直接 | 高 | > 10 万 |
OpenCV 半自动辅助标注
标注工具里用 OpenCV 实现”沿已标线方向自动延伸”,可减少 50%~80% 的点击次数:
import cv2
import numpy as np
def predict_next_point(image, points):
if len(points) < 2:
return None
last = np.array(points[-1])
prev = np.array(points[-2])
direction = last - prev
direction = direction / (np.linalg.norm(direction) + 1e-6)
roi_size = 80
cx, cy = int(last[0]), int(last[1])
roi = image[max(0,cy-roi_size):cy+roi_size, max(0,cx-roi_size):cx+roi_size]
edges = cv2.Canny(roi, 50, 150)
lines = cv2.HoughLinesP(edges, 1, np.pi/180, threshold=15, minLineLength=10, maxLineGap=5)
if lines is None:
candidate = last + direction * 30
return tuple(candidate.astype(int))
best_line = None
best_score = -1
for line in lines:
x1, y1, x2, y2 = line[0]
lv = np.array([x2-x1, y2-y1], dtype=float)
lv = lv / (np.linalg.norm(lv) + 1e-6)
score = abs(np.dot(direction, lv))
if score > best_score:
best_score = score
best_line = line[0]
if best_line is not None and best_score > 0.7:
x1, y1, x2, y2 = best_line
mid = np.array([cx - roi_size + (x1+x2)//2, cy - roi_size + (y1+y2)//2])
return tuple(mid.astype(int))
return tuple((last + direction * 30).astype(int))
用户标完前两个点后,鼠标移动时实时预显示下一个预测点,按 Enter 确认。
