맵 이미지에서 Centerline과 트랙 경계 추출하기
SLAM으로 얻은 점유격자 맵(/map)에서 트랙의 Centerline과 Wall boundary를 추출합니다. gb_optimizer 패키지 global_planner_node의 앞부분이며, Global Planning 파이프라인의 첫 단추이자 Global Trajectory Optimization: IQP/SP의 입력을 만드는 단계입니다.
세 가지를 추출합니다 — ① Centerline(트랙 중앙 폐곡선, 좌표계 $s$ 의 기준) ② Track width(각 점에서 좌/우 벽까지 거리) ③ Wall boundary(실제 벽 — 안전검증·회피 클램핑용)
파이프라인 다이어그램
점유격자 → 1px 골격 → 미터 좌표 + 폭 순으로 정보를 정제하는 과정입니다. 이진 이미지가 골격(Centerline)과 watershed 경계 두 갈래로 갈라졌다가 폭 계산에서 다시 만납니다. 이후 계산된 폭은 IQP & SP 라인을 최적화할 때 alpha 박스 제약의 입력으로 사용됩니다.
단계별 설명
1. 맵 로드 + 이진화
map_server가 발행하는 점유격자(또는 저장된 map.png)를 읽습니다. 점유격자 값이 occupancy_grid_threshold보다 작은(자유공간) 픽셀을 흰색(255)으로 이진화합니다.
map_server가 불러오는 맵은 이미지([맵 이름].pgm 또는 [맵 이름].png)와 메타데이터 yaml([맵 이름].yaml) 파일로 구성되어 있습니다. 이 yaml이 픽셀을 실좌표로 해석하는 데 활용되는 해상도와 원점 정보를 담고 있으며, 3단계의 픽셀→미터 변환에 활용됩니다.
1
2
3
4
5
6
7
# stack_master/maps/[맵 이름]/[맵 이름].yaml
image: [맵 이름].pgm
resolution: 0.05 # m/pixel — 한 픽셀의 실제 크기
origin: [-8.21, -12.4, 0] # 맵 좌하단 픽셀의 실좌표 [x, y, yaw]
occupied_thresh: 0.65 # 해당 값 이상이면 점유(벽)
free_thresh: 0.196 # 해당 값 이하면 자유공간
negate: 0 # 흑백 반전 여부
resolution과 origin이 아래 변환식($x_m = \text{col}\cdot r + o_x$)의 $r$ 과 $(o_x, o_y)$ 입니다. 즉, 해당 yaml 파일이 맵의 픽셀 좌표를 실제 미터 좌표로 바꿀 때 활용됩니다.
2. Skeletonization
채워진 주행 영역을 1px 중심선으로 thinning하여, 닫힌 루프에서 끊김 없는 골격을 추출합니다.
3. Centerline 추출
골격 이미지에서 닫힌 곡선(contour)들을 찾아, 그중 트랙 루프에 해당하는 폐곡선을 센터라인으로 삼습니다. 후보 폐곡선이 여럿일 경우에는 가장 짧은 것을 선택해 안쪽 루프를 잡습니다. 이렇게 고른 센터라인은 아직 픽셀 좌표이므로, 앞서 yaml에서 읽은 resolution과 origin을 이용해 실제 미터 좌표로 변환합니다.
여기서 $r$ 은 resolution, $(o_x, o_y)$ 는 origin입니다.
4. 방향 정렬 (CCW/CW)
추출된 센터라인은 점들의 순서로 방향을 가지는데, 이 방향이 차량의 실제 진행 방향과 반대일 수 있습니다. 그래서 차량의 초기 진행 방향과 센터라인의 진행 방향을 비교하여, 서로 어긋나면 점의 순서를 뒤집어 방향을 맞춥니다. 이는 이후 주행 좌표계($s$)의 진행 방향을 실제 주행 방향과 일치시키기 위한 과정이며, reverse_mapping 옵션이 켜져 있으면 여기서 한 번 더 방향을 반전합니다.
5. 벽 경계 추출: Watershed
Watershed 알고리즘으로 트랙의 좌/우 벽을 분리합니다. 여기서 두 가지 개념이 쓰입니다.
- Distance transform: 원본 맵과 크기가 같은 이미지로, 주행영역(흰색)의 각 픽셀 값을 그 픽셀에서 가장 가까운 벽까지의 거리로 바꾼 것입니다. 벽에 붙은 픽셀은 0에 가깝고 트랙 폭 한가운데로 갈수록 값이 커지므로, 각 지점이 벽에서 얼마나 떨어졌는지를 밝기로 나타낸 이미지가 됩니다.
- Watershed: 값이 낮은 지점부터 서로 다른 시작 영역을 확장해 나가다가, 인접한 두 영역이 맞닿는 경계선을 찾아 영역을 분할하는 알고리즘입니다.
Distance transform 값의 부호를 뒤집어, 벽에서 먼 트랙 중앙일수록 값이 작아지도록 만듭니다. 센터라인을 시작 영역으로 지정한 뒤 watershed를 적용하면, 센터라인을 기준으로 좌·우 영역이 확장되어 두 영역이 맞닿는 경계선이 좌/우 벽으로 검출됩니다.
6. 트랙 폭 결합 + 센터라인 발행
추출된 센터라인의 각 점에, 앞 단계에서 찾은 좌/우 벽까지의 거리를 폭 정보로 붙입니다. 곡률($\kappa$)과 heading($\psi$)은 폭과는 별개로, 센터라인을 매끄러운 곡선(spline)으로 보고 수치 미분해서 계산한 뒤 각 점에 함께 기록합니다.
| 정보 | 출처 | 용도 |
|---|---|---|
d_left, d_right | watershed 경계 | IQP/SP의 alpha 박스 제약 |
| boundary (좌/우 벽) | watershed | 안전검증 및 회피 클램핑 (실제 벽) |
psi, kappa | Centerline spline | 곡률·heading |
이렇게 만들어진 Centerline과 Wall boundary 값은 global_waypoints.json에 저장됩니다.
입출력
파라미터는 두 경로로 들어옵니다. 값 자체는 stack_master/config/global_planner_params.yaml에서 오고, 실행할 때마다 바뀌는 스위치(맵·방향·시뮬레이션 여부 등)는 raceline_generator.launch.xml의 인자로 넘깁니다.
config에서 오는 값 (global_planner_params.yaml)
| 파라미터 | 값 | 설명 |
|---|---|---|
occupancy_grid_threshold | 10 | 점유격자 자유공간 판정 임계값 (노드 기본값은 50이지만 config에서 10으로 사용) |
required_laps | 1 | 주행 모드에서 최적화 전 주행 랩수 |
launch 인자로 넘기는 값 (raceline_generator.launch.xml)
| 인자 | 전달되는 파라미터 | 설명 |
|---|---|---|
map | map / map_dir | 사용할 맵 이름 (stack_master/maps/ 하위 폴더) |
sim | racecar_version | true면 SIM, false면 CAR config 폴더 선택 |
reverse | reverse_mapping | raceline 진행 방향 반전 |
show_plots | show_plots | 추출·최적화 디버그 플롯 표시 |
발행 토픽: /centerline_waypoints(+/markers), /trackbounds/markers. RViz에서 Centerline(파란 구체)과 Wall boundary를 확인할 수 있습니다.
UNICORN Racing Stack에서 직접 실행하기
이 스택은 UNICORN Racing Stack 기반입니다. 시스템 ROS 설치 없이 conda 환경 하나로 ROS 2 Jazzy가 돌아갑니다. 환경 진입은
unicornalias로 합니다.
1. 환경 진입 + 빌드
1
2
unicorn # conda 환경 진입
cbuild # 빌드
이미 빌드가 되어 있다면 건너뛰어도 됩니다.
2. raceline_generator 런치파일 실행
Centerline 및 wall boundary 추출 단계는 단독 노드가 아니라 global_planner_node의 일부로 실행됩니다. raceline_generator.launch.xml을 통해 한 번에 실행합니다.
1
2
3
4
# 저장된 맵에서 raceline 생성 (Centerline·Wall boundary 추출 포함)
ros2 launch stack_master raceline_generator.launch.xml map:=f sim:=true
# map:=맵 이름
# 실차면 sim:=false (CAR config) / 방향 반전 reverse:=true / 결과 플롯 show_plots:=true
결과 확인: RViz에서
/centerline_waypoints/markers(파란 구체)·/trackbounds/markers(초록/노랑) 구독. 디버그 플롯은show_plots:=true를 통해 출력할 수 있습니다.
실행 결과
raceline_generator.launch.xml map:=f sim:=true를 실행했을 때 출력되는 결과입니다.
1) 실행 로그 — 맵을 불러오고 Centerline, wall boundary를 추출하는 과정이 로그로 출력됩니다.
1
2
3
4
5
6
[map_io]: Read map .../maps/f/f.png: 545 X 322 map @ 0.05 m/cell
[global_planner]: resolved source map dir -> .../maps/f
[global_planner_node] (1680, 2)
[global_planner_node] Direction of the centerline ...
[global_planner]: Using watershed for track bound extraction...
[global_planner]: Start Global Trajectory optimization with iterative minimum curvature...
추출된 Centerline이 1680점이고, watershed로 트랙 경계를 추출했다는 로그가 확인됩니다.
2) 디버그 시각화 (show_plots:=true) — 노드가 실행 중 직접 출력하는 플롯들로, 추출이 제대로 완료됐는지 시각화하는 용도입니다. ① 이진맵+골격 ② 미터 좌표 Centerline+Left/Right boundary(빨강=Centerline, 파랑=Right boundary, 초록=Left boundary) ③ 방향 정렬 확인(시작 두 점):
가운데 그림에서 Centerline(빨강)이 좌/우 벽 한가운데를 지나면 정상입니다. 오른쪽 그림의 시작 두 점(빨강→파랑) 방향이 의도와 반대면 reverse:=true로 뒤집습니다.
3) RViz 확인 — map_server가 띄운 점유격자 맵(/map) 위에 Centerline 마커가 올라옵니다. 흰 주행영역(F자 트랙) 중앙을 파란 선(Centerline)이 따라갑니다:
마커 토픽은 일반 QoS로 발행되므로(노드는 spin 상태로 계속 살아 있습니다), 노드가 떠 있는 동안 RViz를 함께 띄워 두면 /centerline_waypoints/markers·/trackbounds/markers가 실시간으로 갱신됩니다. (참고로 /map만 map_server의 latched 설정에 맞춰 TRANSIENT_LOCAL로 구독합니다.)
4) 생성물 — 추출·최적화 결과는 모두 stack_master/maps/f/에 저장됩니다. Centerline, Wall boundary는 global_waypoints.json에 함께 담깁니다.
위 그림·로그는 UNICORN Racing Stack에서 map
f(F자 트랙)를 실제로 실행했을 때의 출력입니다.
마무리
이 글에서는 SLAM 점유격자 맵에서 Centerline, Track width, Wall boundary를 추출하는 과정을 다뤘습니다. 맵 이진화 → 골격화 → Centerline 추출·방향 정렬 → watershed 벽 분리 → 폭 결합 순으로 정제된 트랙 데이터는 global_waypoints.json에 저장되어 Global Planning 파이프라인의 기반이 됩니다.






