학습 목표
- 라이다의 측정 원리와 2D·3D 의 차이를 설명할 수 있다.
- LaserScan 메시지의 각 항목을 읽고 인덱스를 각도로 바꿀 수 있다.
- 라이다가 못 보는 물체와 상황을 나열하고 대비할 수 있다.
- 스캔 중 로봇이 움직여 생기는 왜곡을 설명할 수 있다.
- 점군 데이터의 크기 문제와 줄이는 방법을 안다.
- PointCloud2의 Field·Memory Layout과 Coordinate Frame을 검증할 수 있다.
- LiDAR–Robot Extrinsic과 Time Synchronization을 Calibration할 수 있다.
- QoS·Filtering·Deskewing·SLAM Pipeline을 실제 부하에서 설계할 수 있다.
- 침묵·오염·간섭·가짜 반사·시간 왜곡을 진단하고 안전하게 대응할 수 있다.
1. 빛을 쏘고 기다린다
라이다의 원리는 놀랄 만큼 단순합니다. 빛을 쏘고, 되돌아오는 데 걸린 시간을 잽니다. 빛의 속도는 알고 있으니 거리가 나옵니다.
앞 화면의 카메라와 정확히 반대입니다. 카메라는 색과 무늬는 잘 보지만 거리를 모릅니다. 라이다는 거리는 아주 정확히 재지만 그것이 무엇인지 모릅니다. 벽인지 사람인지 상자인지 구분하지 못합니다.
그래서 두 센서를 함께 쓰는 것이 표준입니다.
2D 라이다는 하나의 평면만 봅니다. 수평으로 한 바퀴 돌면서 그 높이의 단면만 잽니다. 값이 싸고, 데이터가 작고, 계산이 가볍습니다. 실내 이동 로봇의 지도 작성과 위치 추정에 압도적으로 많이 쓰입니다.
3D 라이다는 여러 층을 동시에 봅니다. 위아래로 여러 개의 빔이 배치되어 있어 공간을 통째로 얻습니다. 대신 비싸고 데이터가 수십 배 큽니다.
측정 자체는 매우 정확합니다. 보통 몇 센티미터 안쪽의 오차이고, 조명과 무관하며, 캄캄한 방에서도 똑같이 잘 봅니다. 이것이 카메라 대비 가장 큰 장점입니다.
하지만 이 정확함이 함정이 됩니다. 값이 정확하니까 다 믿어도 된다고 생각하게 됩니다. 뒤에서 보겠지만 라이다에는 아예 안 보이는 것들이 있고, 그것은 오차가 아니라 부재로 나타납니다.
| 2D 라이다 | 3D 라이다 |
|---|---|
| 한 평면만 본다 | 공간 전체를 본다 |
| 초당 수천 점 | 초당 수십만 점 |
| 데이터가 작다 | 무선으로 보내기 어렵다 |
| 평면 밖 물체를 못 본다 | 낮은 턱과 상판도 본다 |
| 실내 지도 작성에 충분 | 실외와 복잡한 지형에 |
두 종류의 라이다는 값과 용도가 다릅니다. 실내 이동 로봇이라면 2D 로 충분한 경우가 많습니다.

왼쪽의 2D LiDAR는 센서 높이의 한 수평면만 비추므로 그 평면과 만나는 벽·의자 다리·사람은 감지하지만, Scan Plane보다 낮은 문턱과 높은 Table 상판은 놓칠 수 있습니다. 오른쪽의 3D LiDAR는 여러 수직 Channel로 바닥부터 벽·사람·가구의 높이 구조까지 Point Cloud로 측정합니다. 실제 검출 범위와 Point 밀도는 LiDAR의 수직 FOV와 Channel 수에 따라 달라집니다.
주의 라이다의 정확도는 반사면에 따라 달라집니다. 밝고 거친 표면은 잘 되돌아오지만 어둡고 매끄러운 면은 신호가 약합니다. 사양서의 정확도는 표준 반사판에서 잰 값이므로, 실제 환경의 검은 물체나 젖은 바닥에서는 더 나쁩니다.
2. LaserScan 을 정확히 읽는 법
2D 라이다의 표준 메시지는 거리 배열 하나와 그 배열을 해석하는 규칙으로 이루어져 있습니다. 이 규칙을 정확히 아는 것이 라이다를 쓰는 첫걸음입니다.
ranges 는 그냥 숫자 배열입니다. 각도 정보가 없습니다. 몇 번째 값이 어느 방향인지는 계산해서 알아내야 합니다.
계산은 간단합니다. angle_min 에서 시작해 angle_increment 씩 더해 나가면 됩니다. i 번째 값의 각도는 angle_min + i * angle_increment 입니다.
여기서 자주 하는 실수가 있습니다. "정면은 배열의 가운데겠지"라고 짐작하는 것입니다. 대부분 맞지만 항상 그렇지는 않습니다. 어떤 라이다는 뒤쪽부터 시작하고, 어떤 것은 시계 반대 방향으로 돕니다. 반드시 계산해서 확인하십시오.
range_min 과 range_max 를 반드시 확인해야 합니다. 이 범위 밖의 값은 의미가 없습니다. 앞 화면에서 배운 대로 무한대는 "아무것도 안 맞았다", NaN 은 "측정 실패"입니다. 하지만 0 이나 아주 작은 값이 나오는 라이다도 있습니다. 그러면 로봇은 바로 앞에 벽이 있다고 믿고 급정지합니다.
intensities 는 되돌아온 빛의 세기입니다. 비어 있는 라이다도 많지만, 있다면 유용합니다. 세기가 유난히 낮은 점은 검은 물체이거나 비스듬히 맞은 것이므로 덜 믿는 것이 안전합니다.
scan_time 과 time_increment 는 뒤에서 다룰 왜곡 보정에 쓰입니다. 한 바퀴 도는 데 걸린 시간과 점 하나 사이의 시간 간격입니다.
| 항목 | 뜻 | 주의할 점 |
|---|---|---|
| angle_min | 첫 점의 각도 (rad) | 정면이 0 이 아닐 수 있음 |
| angle_increment | 점 사이의 각도 간격 | 음수면 반대 방향으로 돎 |
| ranges | 거리 배열 (m) | 각도 정보가 없음 |
| range_min · range_max | 유효한 거리 구간 | 밖의 값은 반드시 버릴 것 |
| intensities | 되돌아온 빛의 세기 | 비어 있을 수 있음 |
| scan_time | 한 바퀴에 걸린 시간 | 왜곡 보정에 필요 |
| time_increment | 점 사이의 시간 간격 | 왜곡 보정에 필요 |
새 라이다를 만나면 첫 메시지의 제원을 반드시 한 번 찍어 보십시오. 점 개수와 시작 각도가 예상과 다른 경우가 자주 있습니다.
# LaserScan 을 바르게 읽는 최소한의 코드
# 각도 계산과 유효값 걸러 내기를 모두 담았습니다.
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
import math
class ScanReader(Node):
def __init__(self):
super().__init__('scan_reader')
self.create_subscription(
LaserScan, 'scan', self.on_scan, qos_profile_sensor_data)
self.printed = False
def on_scan(self, msg):
if not self.printed:
# 이 라이다가 어떻게 생겼는지 한 번은 반드시 찍어 본다
self.get_logger().info(
f'점 {len(msg.ranges)}개, '
f'{math.degrees(msg.angle_min):.1f}도 ~ '
f'{math.degrees(msg.angle_max):.1f}도, '
f'간격 {math.degrees(msg.angle_increment):.3f}도, '
f'유효 거리 {msg.range_min:.2f}~{msg.range_max:.2f} m')
self.printed = True
# ① 정면 ±15도 안의 점만 고른다.
# "가운데가 정면"이라고 짐작하지 않고 각도를 계산한다.
front = []
for i, r in enumerate(msg.ranges):
angle = msg.angle_min + i * msg.angle_increment
if abs(angle) > math.radians(15):
continue
# ② 무의미한 값을 걸러 낸다. 이것을 빠뜨리면 안 된다.
if math.isnan(r) or math.isinf(r):
continue
if r < msg.range_min or r > msg.range_max:
continue
front.append((angle, r))
if not front:
self.get_logger().warn('정면에 유효한 점이 없습니다')
return
nearest_angle, nearest = min(front, key=lambda p: p[1])
self.get_logger().info(
f'정면 유효 {len(front)}점, '
f'가장 가까운 것 {nearest:.2f} m '
f'({math.degrees(nearest_angle):+.1f}도)')
def main():
rclpy.init()
rclpy.spin(ScanReader())
# 실행 결과 예
# 점 360개, -180.0도 ~ 179.0도, 간격 1.000도, 유효 거리 0.15~12.00 m
# 정면 유효 29점, 가장 가까운 것 1.34 m (-3.0도)
주의 정면이 배열의 몇 번째인지 하드코딩하지 마십시오. 라이다를 바꾸거나 드라이버 설정을 바꾸면 점 개수가 달라집니다. 항상 각도로 계산해서 고르십시오. 이것을 지키면 라이다를 바꿔도 코드가 그대로 돕니다.
3. 라이다가 못 보는 것들
이 절이 이 화면에서 가장 중요합니다. 라이다의 값은 정확하지만, 안 보이는 것은 오차가 아니라 부재로 나타납니다. 로봇은 "거기 아무것도 없다"고 믿습니다.
① 유리와 거울 — 가장 위험합니다.
빛이 통과하거나 엉뚱한 곳으로 반사됩니다. 유리문 앞에서 라이다는 문 너머의 벽을 보고, 로봇은 그대로 돌진합니다. 거울은 더 나빠서 거울 속 공간이 진짜 공간처럼 지도에 그려집니다.
② 검은 물체 — 빛을 흡수합니다.
검은 옷을 입은 사람, 검은 가방, 검은 케이블 트레이. 되돌아오는 빛이 약해 거리가 부정확해지거나 아예 안 잡힙니다.
③ 얇은 것 — 빔 사이로 빠져나갑니다.
의자 다리, 테이블 다리, 봉, 얇은 기둥. 멀수록 빔 간격이 벌어져 더 잘 놓칩니다. 5 m 앞에서는 1도 간격이 약 9 cm 이므로 가느다란 다리는 통째로 사라집니다.
④ 스캔 평면 밖 — 2D 라이다의 근본 한계입니다.
이것이 실제로 가장 많은 사고를 냅니다. 라이다를 20 cm 높이에 달았다면, 높이 5 cm 의 문턱도, 높이 70 cm 의 테이블 상판도 보이지 않습니다. 로봇은 테이블 아래 빈 공간만 보고 "지나갈 수 있다"고 판단한 뒤 상판에 부딪힙니다.
⑤ 젖은 바닥과 반짝이는 면 — 빛이 미끄러져 반사됩니다.
⑥ 직사광선 — 강한 햇빛은 잡음을 늘리고 범위를 줄입니다.
대응은 결국 하나입니다. 다른 센서를 함께 쓰는 것. 초음파는 유리를 봅니다. 카메라는 무엇인지 압니다. 범퍼와 절벽 센서는 최후의 방어선입니다.
| 안 보이는 것 | 왜 | 어떻게 나타나는가 | 대비 |
|---|---|---|---|
| 유리문 | 빛이 통과 | 너머의 벽이 보임 | 초음파, 표시 테이프 |
| 거울 | 빛이 반사 | 가짜 공간이 지도에 | 지도에서 수동 차단 |
| 검은 물체 | 빛을 흡수 | 값이 없거나 부정확 | 카메라 병용 |
| 의자 다리 | 빔 사이로 빠짐 | 아예 없는 것처럼 | 느린 속도, 범퍼 |
| 낮은 문턱 | 스캔 평면 아래 | 평평한 바닥으로 보임 | 3D 또는 기울인 라이다 |
| 테이블 상판 | 스캔 평면 위 | 지나갈 수 있다고 판단 | 3D 라이다, 높이 센서 |
| 젖은 바닥 | 빛이 미끄러짐 | 거리가 튐 | 필터, 다른 센서 |
주의 로봇을 처음 놓는 공간에서는 반드시 한 바퀴 돌며 라이다 화면을 보십시오. 유리 칸막이, 거울, 검은 가구, 낮은 턱이 어디 있는지 눈으로 확인해야 합니다. 지도에 안 나타나는 것을 찾는 것이 이 작업의 목적입니다.
4. 3D 라이다와 점군
3D 라이다는 여러 층의 빔을 동시에 쏘아 공간을 통째로 얻습니다. 2D 의 근본 한계인 "평면 밖을 못 본다"가 사라집니다.
대신 데이터가 폭발합니다. 16층짜리가 초당 30만 점, 64층짜리는 초당 백만 점을 넘습니다. 점 하나에 좌표 세 개와 부가 정보를 담으면 초당 수십 MB 가 됩니다. 앞 화면의 영상과 마찬가지로 무선으로 보내는 것은 무리입니다.
점군 메시지는 자유로운 형식입니다. 어떤 항목을 담을지 스스로 정의하고, 그 정의를 메시지 안에 함께 넣습니다. 보통 좌표 세 개에 더해 되돌아온 세기, 몇 번째 층인지, 언제 찍혔는지가 들어갑니다.
마지막 두 개가 중요합니다. 층 번호가 있으면 특정 높이만 골라내 2D 처럼 쓸 수 있고, 점마다의 시각이 있으면 뒤에서 다룰 왜곡 보정을 할 수 있습니다.
점군은 두 가지 형태로 옵니다. 격자 모양으로 정리되어 나오는 것과, 그냥 점들의 목록인 것입니다. 정리된 쪽은 이웃 점을 바로 찾을 수 있어 처리가 빠릅니다.
그리고 반드시 줄여서 씁니다. 백만 점을 그대로 처리하려는 것은 대개 잘못된 접근입니다.
공간을 격자로 나누고 칸마다 점 하나만 남기는 것이 가장 흔한 방법입니다. 5 cm 격자면 점이 수십 분의 일로 줄어드는데 모양은 거의 그대로입니다. 여기에 관심 없는 범위를 잘라 내는 것을 더하면 계산이 극적으로 가벼워집니다.
| 줄이는 방법 | 무엇을 하는가 | 언제 |
|---|---|---|
| 범위 자르기 | 관심 밖 영역 제거 | 가장 먼저, 가장 쌈 |
| 격자로 묶기 | 칸마다 점 하나만 | 모양을 유지하며 대폭 감소 |
| 층 고르기 | 특정 높이만 사용 | 2D 처럼 쓰고 싶을 때 |
| 바닥 분리 | 땅과 장애물을 나눔 | 주행 판단 직전 |
| 튀는 점 제거 | 이웃이 없는 점 삭제 | 잡음이 많을 때 |
원본 그대로
가장 싼 연산을 먼저
수십 분의 일로
용도별로 분리
점군은 받자마자 줄이는 것이 원칙입니다. 줄이는 순서를 바꾸면 같은 결과라도 계산량이 크게 달라집니다.
주의 점군을 그대로 무선으로 보내려 하지 마십시오. 앞 화면의 영상과 같은 문제가 더 크게 일어납니다. 줄이는 노드를 로봇 위에서 돌리고, 줄인 결과만 보내십시오. 이것이 앞에서 배운 "계산을 데이터 가까이 둔다"는 원칙입니다.
5. 한 스캔은 한순간이 아니다
여기서 초보자가 거의 반드시 걸리는 문제가 나옵니다.
라이다가 한 바퀴 도는 데 시간이 걸립니다. 10 Hz 라이다라면 한 바퀴에 0.1 초입니다. 그 0.1 초 동안 로봇은 가만히 있지 않습니다.
초속 1 m 로 달리는 로봇은 한 스캔 동안 10 cm 를 갑니다. 그러면 스캔의 첫 점과 마지막 점은 10 cm 다른 위치에서 찍은 것입니다. 그런데 우리는 그것을 한 좌표계의 한 순간으로 취급합니다.
결과는 이렇습니다. 곧은 벽이 휘어 보입니다. 회전하고 있다면 더 심합니다. 제자리에서 빠르게 도는 로봇은 벽이 나선처럼 보입니다.
이것이 왜 문제인가. 지도 작성과 위치 추정은 스캔의 모양을 맞춰 보는 방식입니다. 모양이 왜곡되면 맞춰지지 않거나 엉뚱한 곳에 맞습니다. 천천히 움직이면 괜찮다가 속도를 올리면 갑자기 위치를 잃는 증상이 여기서 나옵니다.
해결은 점마다 시각을 고려하는 것입니다. 각 점이 찍힌 시각에 로봇이 어디 있었는지를 앞 화면에서 배운 TF 로 알아내, 모든 점을 같은 시각의 좌표계로 옮깁니다. 이 과정을 왜곡 보정이라고 하며, 좋은 드라이버와 SLAM 패키지는 이것을 자동으로 해 줍니다.
그러니 확인해야 할 것은 이것입니다. 내가 쓰는 도구가 왜곡 보정을 하는가. 하지 않는다면 로봇 속도를 낮추는 것이 가장 현실적인 대응입니다.
회전이 직진보다 훨씬 큰 왜곡을 만듭니다. 제자리 회전 중에 지도가 흐트러진다면 이것이 원인입니다.
# 라이다 왜곡이 실제로 얼마나 되는지 계산해 봅니다
# python3 skew.py
SCAN_HZ = 10.0 # 라이다 회전 속도
LIDAR_POINTS = 360 # 한 바퀴의 점 개수
def report(linear_mps, angular_dps):
scan_time = 1.0 / SCAN_HZ
# 한 스캔이 도는 동안 로봇이 이동한 거리와 회전한 각도
moved_m = linear_mps * scan_time
turned_deg = angular_dps * scan_time
# 마지막 점은 첫 점보다 이만큼 다른 곳에서 찍혔다
print(f'직진 {linear_mps:.1f} m/s, 회전 {angular_dps:.0f} 도/s')
print(f' 한 스캔 시간 : {scan_time*1000:.0f} ms')
print(f' 그 사이 이동 : {moved_m*100:.1f} cm')
print(f' 그 사이 회전 : {turned_deg:.1f} 도')
# 회전 왜곡은 거리에 비례해 커진다
for dist in (2.0, 5.0):
import math
arc_cm = dist * math.radians(turned_deg) * 100
print(f' {dist:.0f} m 거리의 벽이 어긋나는 폭 : {arc_cm:.1f} cm')
print()
def main():
report(0.2, 10) # 천천히
report(0.5, 30) # 보통
report(1.0, 60) # 빠르게
if __name__ == '__main__':
main()
# 실행 결과
# 직진 0.2 m/s, 회전 10 도/s
# 한 스캔 시간 : 100 ms
# 그 사이 이동 : 2.0 cm
# 그 사이 회전 : 1.0 도
# 2 m 거리의 벽이 어긋나는 폭 : 3.5 cm
# 5 m 거리의 벽이 어긋나는 폭 : 8.7 cm
#
# 직진 1.0 m/s, 회전 60 도/s
# 한 스캔 시간 : 100 ms
# 그 사이 이동 : 10.0 cm
# 그 사이 회전 : 6.0 도
# 2 m 거리의 벽이 어긋나는 폭 : 20.9 cm
# 5 m 거리의 벽이 어긋나는 폭 : 52.4 cm
#
# 5 m 거리에서 52 cm 어긋나면 지도 정합이 실패합니다.
| Robot 속도 조건 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 한 Scan 중 이동 거리(m) | 0.02 | 0.05 | 0.10 | 0.15 | 0.20 | 0.30 | 0.40 |
| 지도 정합 실패 위험(상대값) | 0.05 | 0.08 | 0.15 | 0.30 | 0.50 | 0.75 | 0.95 |
로봇이 빨라질수록 한 스캔 동안 이동한 거리가 커지고, 보정하지 않으면 그만큼 스캔이 일그러집니다.
주의 왜곡 보정을 하려면 스캔 시각과 TF 가 정확해야 합니다. 앞 화면에서 배운
stamp문제,use_sim_time문제가 여기서 그대로 발목을 잡습니다. 보정을 켰는데 오히려 나빠졌다면 시각을 먼저 의심하십시오.
6. 라이다를 검증하는 순서
앞 화면의 센서 점검 순서를 라이다에 맞춰 구체화한 것입니다. 대부분의 문제가 처음 세 단계에서 잡힙니다.
① 벽 시험 — 가장 강력합니다.
평평한 벽 앞에 로봇을 정면으로 세우고 RViz2 로 봅니다.
벽이 곧은 직선인가. 휘어 있다면 왜곡 보정 문제이거나 라이다 결함입니다.
벽까지의 거리가 자로 잰 값과 같은가. 다르면 라이다 장착 위치가 틀렸습니다.
벽이 로봇 정면과 나란한가. 비스듬하면 장착 각도나 yaw 가 틀린 것입니다. 앞 화면에서 배운 그 시험입니다.
② 무효값 비율 — 환경을 진단합니다.
전체 점 중 무한대와 NaN 이 몇 퍼센트인지 셉니다. 넓은 공간에서는 원래 많고, 좁은 방인데 많다면 문제입니다. 유리나 검은 물체가 많은 것입니다.
③ 못 보는 물체 찾기 — 반드시 하십시오.
의자, 유리 칸막이, 검은 가방을 놓고 화면에 나타나는지 하나씩 확인합니다. 나타나지 않는 물체의 목록을 만드는 것이 이 작업의 결과물입니다.
④ 속도별 시험 — 왜곡을 확인합니다.
천천히 돌 때와 빠르게 돌 때의 지도를 비교합니다. 빠를 때만 흐트러진다면 왜곡 보정 문제입니다.
| 증상 | 원인 | 확인·대응 |
|---|---|---|
| 점이 하나도 없음 | 드라이버·전원·QoS 불일치 | hz, BEST_EFFORT 확인 |
| 벽이 휘어 보임 | 왜곡 보정 안 함 | 속도를 낮춰 재시험 |
| 벽이 비스듬함 | 장착 각도 또는 yaw | lidar_joint 의 rpy |
| 거리가 일정하게 틀림 | 장착 위치 | lidar_joint 의 xyz |
| 점군이 로봇 안쪽에 | frame_id 또는 TF 오류 | tf2_echo 로 확인 |
| 무효값이 지나치게 많음 | 유리·검은 물체·범위 밖 | 환경 점검, 다른 센서 |
| 갑자기 급정지 | 0 이나 작은 값을 안 걸러 냄 | range_min 미만을 버릴 것 |
| 빠를 때만 지도가 깨짐 | 스캔 왜곡 | 보정 켜기 또는 감속 |
| 특정 물체만 안 보임 | 검은색·유리·얇음 | 목록화하고 다른 센서로 |
무효값 비율은 환경마다 크게 다릅니다. 정상 환경에서의 값을 미리 재 두면 나중에 이상을 바로 알아챌 수 있습니다.
# ── 기본 확인 ─────────────────────────────────────────
ros2 topic hz /scan
ros2 topic echo /scan --field header.frame_id --once
ros2 run tf2_ros tf2_echo base_link laser
# ── 제원을 한 번 찍어 본다 ────────────────────────────
ros2 topic echo /scan --once --field angle_min
ros2 topic echo /scan --once --field angle_max
ros2 topic echo /scan --once --field angle_increment
ros2 topic echo /scan --once --field range_min
ros2 topic echo /scan --once --field range_max
# ── 무효값 비율을 센다 ────────────────────────────────
python3 - <<EOF
import rclpy, math
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
class Stat(Node):
def __init__(self):
super().__init__('scan_stat')
self.create_subscription(LaserScan, '/scan',
self.cb, qos_profile_sensor_data)
def cb(self, m):
total = len(m.ranges)
nan = sum(1 for r in m.ranges if math.isnan(r))
inf = sum(1 for r in m.ranges if math.isinf(r))
out = sum(1 for r in m.ranges
if not math.isnan(r) and not math.isinf(r)
and (r < m.range_min or r > m.range_max))
good = total - nan - inf - out
self.get_logger().info(
f'전체 {total} 유효 {good} ({good*100//total}%) '
f'NaN {nan} 무한대 {inf} 범위밖 {out}')
rclpy.init(); rclpy.spin(Stat())
EOF
# ── 벽 시험 ───────────────────────────────────────────
# 1) 평평한 벽에서 정확히 1.000 m 떨어져 정면으로 세운다
# 2) RViz2 에서 Fixed Frame 을 base_link 로 두고 LaserScan 추가
# 3) 벽이 곧은가, 1 m 인가, 정면과 나란한가를 본다
# 이 세 가지가 라이다 장착 검증의 전부입니다.
주의 라이다는 사람 눈높이보다 낮게 다는 경우가 많아 안전하다고 생각하기 쉽습니다. 하지만 회전부에 머리카락이나 먼지가 감기면 성능이 급격히 떨어집니다. 주기적으로 창을 닦고 회전부를 확인하는 것이 정비의 기본입니다.
7. LiDAR 측정 방식과 사양 읽기
LiDAR는 Laser를 발사하고 반사 신호로 거리를 추정하지만 구현은 하나가 아닙니다.
| 방식 | 원리 | 장점 | 주의점 |
|---|---|---|---|
| Pulsed ToF | Pulse 왕복 시간 측정 | 긴 거리·직관적 거리 | 빠른 Timing 회로 필요 |
| Phase Shift | 변조광 위상 차 측정 | 높은 반복 정밀도 | Ambiguity·다중 반사 |
| FMCW | 주파수 Chirp Beat 분석 | 거리와 상대 속도 가능 | 복잡도·비용·간섭 관리 |
| Triangulation | 발광점의 Image 위치로 거리 | 근거리 정밀 | Range와 외란광 한계 |
사양서의 Range와 Accuracy는 Reflectivity, 입사각, 햇빛, 비·안개와 Target 크기 조건을 확인합니다. ±2 cm가 모든 물체·거리·날씨에서 보장된다는 뜻이 아닙니다. Eye Safety 등급, Wavelength, Scan Rate, Angular Resolution, Return 수, 최소 거리, IP 등급과 작동 온도도 함께 비교합니다.
8. LaserScan Geometry를 검증한다
LaserScan 배열의 예상 개수는 대략 (angle_max-angle_min)/angle_increment + 1이지만 Floating-point 반올림과 Driver 계약이 있으므로 실제 len(ranges)를 기준으로 반복합니다.
def iter_valid_scan(msg):
for index, distance in enumerate(msg.ranges):
angle = msg.angle_min + index * msg.angle_increment
if not math.isfinite(distance):
continue
if not msg.range_min <= distance <= msg.range_max:
continue
x = distance * math.cos(angle)
y = distance * math.sin(angle)
yield index, angle, distance, x, y
angle_increment의 부호, Angle 범위, frame_id, Stamp, Scan Rate와 Point 수를 Driver 시작 때 Diagnostic으로 남깁니다. Intensities의 단위·Scale은 표준화되어 있다고 가정하지 말고 제조사 문서를 확인합니다.
ros2 interface show sensor_msgs/msg/LaserScan
ros2 topic echo /scan --once
ros2 topic hz /scan
ros2 topic delay /scan
9. PointCloud2 Memory Layout
PointCloud2는 자유로운 Binary Record 배열입니다. fields가 각 Point의 이름·Offset·Datatype·Count를 설명하고 point_step은 Point 하나, row_step은 한 행의 Byte 크기입니다.
| Field | 의미 |
|---|---|
height, width |
Organized Cloud의 행·열 또는 1×N |
fields |
x·y·z·intensity·ring·time 등 Layout |
is_bigendian |
Multi-byte 값의 Byte Order |
point_step |
Point Record 하나의 Byte 수 |
row_step |
한 행의 Byte 수, Padding 가능 |
is_dense |
유효하지 않은 Point 포함 여부에 대한 표시 |
ros2 interface show sensor_msgs/msg/PointCloud2
ros2 topic echo /points --field fields --once
ros2 topic bw /points
Field 이름과 Time 단위는 Driver마다 다를 수 있습니다. Offset을 하드코딩하지 말고 Iterator·Point Cloud Library·sensor_msgs_py 같은 검증된 API로 읽습니다.
from sensor_msgs_py import point_cloud2
for x, y, z, intensity in point_cloud2.read_points(
msg, field_names=('x', 'y', 'z', 'intensity'), skip_nans=True):
if math.isfinite(x) and math.isfinite(y) and math.isfinite(z):
process(x, y, z, intensity)
10. LiDAR Frame과 Extrinsic Calibration
LiDAR Point는 Sensor Frame에서 표현됩니다. Robot 판단에 사용하려면 측정 시각의 TF로 base_link, odom 또는 map에 변환합니다.
Extrinsic 오류는 평평한 벽이 기울거나, 바닥이 경사져 보이거나, Robot 자세를 바꿀 때 고정 물체가 움직이는 현상으로 나타납니다. xyz·roll·pitch·yaw를 눈대중으로 맞추지 말고 Target Plane·Checkerboard·Camera–LiDAR Calibration 절차로 추정하고 독립 장면에서 검증합니다.
ros2 topic echo /points --field header --once
ros2 run tf2_ros tf2_echo base_link lidar
ros2 run tf2_tools view_frames
11. Timestamp와 Deskewing
Deskewing에는 Scan 시작·끝 기준 Stamp의 의미, Point별 Relative Time, Robot Motion 추정과 Clock 동기화가 필요합니다. Driver마다 Header Stamp가 첫 Point·마지막 Point·Packet 수신 시각 중 무엇인지 다를 수 있습니다.
- Sensor Packet의 Hardware Timestamp 정책을 확인합니다.
- LiDAR Clock과 IMU·Host Clock을 PTP·GNSS·PPS 등으로 동기화합니다.
- Point별 Time 또는 Azimuth로 측정 시각을 계산합니다.
- IMU·Odometry의 연속 Pose를 Interpolation합니다.
- 모든 Point를 하나의 기준 시각 Frame으로 변환합니다.
시간 Offset 10 ms는 2 m/s에서 2 cm Translation 오차를 만들고, 100°/s 회전에서는 1° 방향 오차를 만듭니다. Deskew를 켰는데 벽이 더 휘면 Timestamp 기준·Time Field 단위·TF Clock을 먼저 확인합니다.
12. Point Cloud Filtering Pipeline
가장 싼 제거 작업부터 수행해 뒤 단계의 계산량을 줄입니다.
| Filter | 목적 | 잘못 설정하면 |
|---|---|---|
| Crop Box | 관심 영역만 유지 | 실제 장애물 삭제 |
| Self Filter | Robot Body 반사 제거 | 가까운 장애물까지 제거 |
| Voxel Grid | 공간 해상도에 맞게 감소 | 얇은 물체 소실 |
| Statistical Outlier | 고립 Noise 제거 | Sparse Target 소실 |
| Radius Outlier | 이웃 수 기준 제거 | 먼 거리 Point 과도 제거 |
| Ground Plane | 주행면과 장애물 분리 | 경사로·턱을 Ground로 오판 |
Filter 전후 Point 수, 처리 시간, 얇은 기둥 Recall과 최소 장애물 크기를 측정합니다.
13. QoS와 Network·Compute Budget
LiDAR Stream은 최신성이 중요하므로 Sensor Data QoS를 많이 사용하지만, 환경·Topology와 Safety 요구에 따라 실측해야 합니다.
from rclpy.qos import qos_profile_sensor_data
self.sub = self.create_subscription(
LaserScan, '/scan', self.on_scan, qos_profile_sensor_data)
sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
"/points", rclcpp::SensorDataQoS(), callback);
ros2 topic info /points --verbose
ros2 topic hz /points
ros2 topic bw /points
top -H -p <PID>
3D Point Cloud를 Wi-Fi로 그대로 보내기보다 Robot에서 ROI·Voxel·Obstacle 결과로 줄입니다. QoS Reliability만 바꾸는 것이 아니라 Message 크기, Packet Fragmentation, CPU Serialization, Queue Age와 Frame Drop을 함께 측정합니다.
14. 다중 Return·Intensity·Ring 활용
고급 LiDAR는 한 Laser 발사에서 여러 Return을 제공할 수 있습니다. First·Strongest·Last Return은 나뭇잎, 비·안개, Fence와 단단한 물체에서 서로 다른 정보를 줍니다.
- Return Mode와 Point Rate의 Trade-off를 확인합니다.
- Intensity는 거리·입사각·재질·Receiver Gain에 영향받으므로 단순 Reflectivity로 간주하지 않습니다.
- Ring은 Vertical Beam 번호로 Scanline 분석과 Calibration에 유용합니다.
- Ambient·Reflectivity·Confidence Field가 있으면 제조사 의미와 Scale을 확인합니다.
- Field가 없는 Driver에 임의 Default를 넣어 실제 측정처럼 사용하지 않습니다.
Lane Marker나 Retroreflector 검출은 Intensity를 쓸 수 있지만 거리·각도별 Normalization과 실제 환경 Calibration이 필요합니다.
15. LiDAR와 SLAM·Navigation
LiDAR 기반 System은 Sensor 하나가 아니라 연속 Pipeline입니다.
| 단계 | 입력 품질 요구 | 실패 증상 |
|---|---|---|
| Scan Matching | 충분한 Geometry·정확한 Time | Corridor에서 방향 불확실 |
| Odometry | 짧은 구간 Motion 추정 | 빠른 회전에서 Drift |
| Loop Closure | 재방문 특징·Outlier 억제 | 잘못된 장소 연결 |
| Costmap | Frame·Height·Clearing 정책 | 장애물 잔류 또는 삭제 |
| Localization | Map과 현재 Scan 일치 | 유리·동적 사람에서 Jump |
긴 평행 Corridor, 넓은 빈 공간, 반복 기둥과 움직이는 군중은 Geometry가 부족하거나 Ambiguous합니다. IMU·Wheel Odometry·Camera와 결합하고 SLAM Output이 매끄럽다는 이유만으로 Ground Truth 없이 정확하다고 판단하지 않습니다.
Bag에는 /scan 또는 /points, /imu, /odom, /tf, /tf_static, Diagnostics와 Clock을 함께 기록해 재현합니다.
16. LiDAR Health Monitor 예제
#!/usr/bin/env python3
import math
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus, KeyValue
class LidarMonitor(Node):
def __init__(self):
super().__init__('lidar_monitor')
self.last_rx_ns = None
self.last_stamp_ns = None
self.frames = 0
self.valid_ratio_sum = 0.0
self.non_monotonic = 0
self.diag_pub = self.create_publisher(DiagnosticArray, '/diagnostics', 10)
self.create_subscription(LaserScan, '/scan', self.on_scan,
qos_profile_sensor_data)
self.create_timer(1.0, self.report)
def on_scan(self, msg):
self.last_rx_ns = self.get_clock().now().nanoseconds
stamp_ns = msg.header.stamp.sec * 1_000_000_000 + msg.header.stamp.nanosec
if self.last_stamp_ns is not None and stamp_ns <= self.last_stamp_ns:
self.non_monotonic += 1
self.last_stamp_ns = stamp_ns
valid = sum(1 for r in msg.ranges
if math.isfinite(r) and msg.range_min <= r <= msg.range_max)
self.valid_ratio_sum += valid / max(1, len(msg.ranges))
self.frames += 1
def report(self):
age_ms = float('inf') if self.last_rx_ns is None else (
self.get_clock().now().nanoseconds - self.last_rx_ns) / 1e6
ratio = self.valid_ratio_sum / max(1, self.frames)
status = DiagnosticStatus()
status.name = 'front_lidar_monitor'
status.hardware_id = 'front_lidar'
if age_ms > 500.0:
status.level, status.message = DiagnosticStatus.STALE, 'scan timeout'
elif ratio < 0.2 or self.non_monotonic:
status.level, status.message = DiagnosticStatus.WARN, 'scan quality warning'
else:
status.level, status.message = DiagnosticStatus.OK, 'lidar healthy'
status.values = [
KeyValue(key='frames_last_sec', value=str(self.frames)),
KeyValue(key='valid_ratio', value=f'{ratio:.3f}'),
KeyValue(key='age_ms', value=f'{age_ms:.1f}'),
KeyValue(key='non_monotonic_stamps', value=str(self.non_monotonic)),
]
array = DiagnosticArray()
array.header.stamp = self.get_clock().now().to_msg()
array.status = [status]
self.diag_pub.publish(array)
self.frames = 0
self.valid_ratio_sum = 0.0
def main():
rclpy.init()
node = LidarMonitor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
유효 비율은 넓은 Outdoor와 좁은 실내에서 정상 범위가 다릅니다. 환경별 Baseline과 회전 속도·Motor 상태·Temperature·Packet Loss를 함께 Monitoring합니다.
17. 현장 고장과 안전 대책
| 증상 | 가능한 원인 | 대응 |
|---|---|---|
| Scan 완전 중단 | 전원·Cable·Driver·Network | Watchdog·안전 정지·재연결 제한 |
| 주기는 정상, 값 고정 | Driver Freeze·Motor Stop | Stamp·Sequence·Value 변화 확인 |
| 원형 Noise·Ghost | 다른 LiDAR 간섭·다중 반사 | 위치·동기·Return·Filter 시험 |
| 한 구간만 Point 없음 | Window 오염·가림 | 각도별 유효율과 청소 점검 |
| 비·안개에서 가짜 근접점 | 공중 입자 반사 | Weather Filter·감속·다른 Sensor |
| 지도가 이중으로 보임 | Time Offset·Deskew 실패 | Clock·Point Time·IMU 확인 |
| 바닥이 기울어짐 | Extrinsic 오류·Mount 변형 | Calibration 재검증 |
| 유리 통과 | 투과·Specular 반사 | Ultrasonic·Camera·Bumper·금지구역 |
LiDAR는 Safety-rated Sensor가 아닐 수 있습니다. 사람 보호가 필요한 Machine은 인증된 Safety Scanner, 물리 Bumper, 속도·구역 제한과 E-stop을 별도 설계합니다.
18. 선정부터 운영까지 표준 절차
운영 전 Checklist
- Model·Serial·Firmware·Driver·Return Mode를 기록했다.
- LaserScan 또는 PointCloud2 Field와 Time 단위를 문서화했다.
frame_id, Extrinsic과 Point Timestamp를 검증했다.- 유리·거울·검은 물체·얇은 봉·낮은 턱을 시험했다.
- 최고 속도·회전에서 Deskew와 Map 품질을 확인했다.
- Filter 후 최소 장애물 Recall을 검증했다.
- QoS·대역폭·CPU·Worst-case Age를 측정했다.
- 침묵·Freeze·오염·간섭·비·안개를 주입했다.
- LiDAR 실패 시 감속·정지·다른 Sensor 대체를 검증했다.
- Window 청소와 Mount·Calibration 점검 주기를 정했다.
19. 확인 퀴즈 15문항
답을 선택한 뒤 정답 확인을 누르세요. 정답과 해설은 제출 후에 표시됩니다.
1. 라이다와 카메라의 관계를 가장 잘 설명한 것은?
잃어버리는 것이 정확히 반대이기 때문에 둘을 합치면 서로의 약점을 덮게 됩니다. 그래서 실무에서는 두 센서를 함께 쓰는 것이 사실상 표준적인 구성으로 자리 잡았습니다.
2. 2D 라이다가 3D 라이다보다 나은 점은?
한 평면만 보므로 초당 수천 점이면 충분하고 무선으로도 무리 없이 보낼 수 있습니다. 실내 이동 로봇의 지도 작성과 위치 추정에는 이것으로 충분한 경우가 매우 많습니다.
3. LaserScan 의 i 번째 값이 가리키는 각도는?
ranges 는 각도 정보가 없는 숫자 배열이므로 계산해서 알아내야 합니다. 정면이 배열 가운데라고 짐작하면 안 되며 라이다마다 시작 각도와 회전 방향이 다를 수 있습니다.
4. 정면 방향의 인덱스를 하드코딩하면 안 되는 이유는?
각도로 계산해서 고르면 라이다를 바꿔도 코드가 그대로 돕니다. 어떤 라이다는 뒤쪽부터 시작하고 어떤 것은 시계 반대 방향으로 돌기 때문에, 짐작으로 정하면 조용히 틀리게 됩니다.
5. LaserScan 에서 range_min 보다 작은 값이 나왔을 때 걸러 내지 않으면?
무한대와 NaN 외에 0 이나 아주 작은 값을 내보내는 라이다도 있습니다. 유효 범위 밖의 값은 의미가 없으므로 수신 즉시 반드시 걸러 내야 합니다.
6. intensities 값이 유난히 낮은 점은 어떻게 다루는 것이 안전한가?
되돌아온 빛이 약하다는 뜻이므로 거리 값도 덜 정확합니다. intensities 가 비어 있는 라이다도 많지만 있다면 신뢰도 판단에 유용하게 쓸 수 있습니다.
7. 라이다에서 안 보이는 물체가 위험한 이유는?
틀린 값이라면 이상하다고 알아챌 수 있지만 아예 없으면 알아챌 방법이 없습니다. 라이다의 값이 정확하다는 사실이 오히려 다 믿어도 된다는 착각을 낳아 이 함정을 키웁니다.
8. 유리문 앞에서 라이다가 보는 것은?
거울은 더 나빠서 거울 속 공간이 진짜 공간처럼 지도에 그려집니다. 초음파를 함께 쓰거나 유리에 표시 테이프를 붙이는 것이 실무에서 흔히 쓰는 대응 방법입니다.
9. 20 cm 높이에 단 2D 라이다가 못 보는 것은?
스캔 평면 밖은 아예 보이지 않습니다. 로봇은 테이블 아래 빈 공간만 보고 지나갈 수 있다고 판단한 뒤 상판에 부딪히며, 이것이 실제로 가장 많은 사고를 냅니다.
10. 멀리 있는 의자 다리를 더 잘 놓치는 이유는?
5 m 앞에서 1도 간격이면 점 사이가 약 9 cm 이므로 가느다란 다리는 통째로 사라집니다. 속도를 낮추고 범퍼 같은 최후의 방어선을 두는 것이 대응입니다.
11. 3D 라이다 점군을 처리할 때 가장 먼저 해야 할 일은?
초당 30만 점을 그대로 처리하려는 것은 대개 잘못된 접근입니다. 범위 자르기 다음에 격자로 묶으면 모양은 거의 그대로 두면서 점이 수십 분의 일로 줄어듭니다.
12. 점군을 격자로 묶어 칸마다 점 하나만 남기면?
5 cm 격자면 대부분의 판단에 충분한 형태가 유지됩니다. 범위 자르기와 함께 쓰면 계산이 극적으로 가벼워지므로, 점군을 다루는 실무에서 거의 언제나 첫 단계로 쓰입니다.
13. 10 Hz 라이다를 단 로봇이 초속 1 m 로 달릴 때, 한 스캔의 첫 점과 마지막 점의 위치 차이는?
한 바퀴에 0.1 초가 걸리고 그 사이에 로봇이 10 cm 를 갑니다. 그런데 우리는 그것을 한 좌표계의 한 순간으로 취급하므로 곧은 벽이 휘어 보이게 됩니다.
14. 천천히 움직일 때는 괜찮다가 속도를 올리면 갑자기 위치를 잃는 증상의 원인은?
지도 작성과 위치 추정은 스캔의 모양을 맞춰 보는 방식이므로 모양이 왜곡되면 맞춰지지 않습니다. 회전이 직진보다 훨씬 큰 왜곡을 만든다는 점도 함께 기억해야 합니다.
15. 라이다 장착을 검증하는 벽 시험에서 확인하는 세 가지는?
곧지 않으면 왜곡 보정 문제, 거리가 다르면 장착 위치, 비스듬하면 장착 각도 문제입니다. 앞 화면의 이동 로봇 모델링에서 배운 그 시험이 여기서 그대로 쓰입니다.
- LiDAR라이다
- Laser를 발사하고 반사 신호를 분석해 주변 물체까지의 거리와 공간 구조를 측정하는 Sensor입니다.
- Time of Flight비행시간 방식
- 빛의 왕복 시간을 측정하고 광속을 이용해 거리를 계산하는 방식입니다.
- LaserScan2차원 거리 스캔
- 각도 범위·증분과 거리 배열로 한 평면의 LiDAR 측정을 표현하는 ROS Message입니다.
- PointCloud2점군 메시지
- 3차원 Point와 Intensity·Ring·Time 같은 Field를 자유로운 Binary Record로 전달하는 ROS Message입니다.
- Angular Resolution각도 해상도
- 인접한 Laser 측정 방향 사이의 각도 간격으로 먼 거리의 공간 Point 간격을 결정합니다.
- Intensity반사 세기
- LiDAR Receiver에 돌아온 Signal 크기로 거리·입사각·재질과 Sensor 설정의 영향을 받습니다.
- Return반사 반환
- 한 번의 Laser 발사에서 검출한 반사 신호이며 First·Strongest·Last 등 Mode가 있을 수 있습니다.
- Ring레이저 채널 번호
- 다층 LiDAR에서 Point를 측정한 Vertical Laser Beam 또는 Scanline 식별자입니다.
- Motion Distortion운동 왜곡
- 한 Scan을 얻는 동안 Robot이 움직여 각 Point가 서로 다른 Pose에서 측정되어 생기는 형상 왜곡입니다.
- Deskewing운동 왜곡 보정
- Point별 측정 시각과 Robot Motion을 이용해 모든 Point를 하나의 기준 시각 좌표로 변환하는 과정입니다.
- Voxel Grid복셀 격자 필터
- 3차원 공간을 일정 크기 Cell로 나누고 대표 Point만 남겨 Point Cloud를 줄이는 Filter입니다.
- Ground Segmentation지면 분리
- Point Cloud에서 주행면과 장애물 Point를 분류하는 과정입니다.
- Self Filter로봇 자체점 제거
- Robot Body에서 반사되어 Point Cloud에 나타나는 Point를 Robot Model로 제거하는 Filter입니다.
- Extrinsic Calibration외부 좌표 보정
- LiDAR와 Robot 또는 다른 Sensor Frame 사이의 위치와 방향을 추정하는 과정입니다.
- Scan Matching스캔 정합
- 연속 LiDAR Scan이나 Map과 현재 Scan의 Geometry가 가장 잘 겹치는 Pose를 찾는 과정입니다.
- Ghost Point유령점
- 다중 반사·간섭·날씨 등으로 실제 물체 위치와 무관하게 나타나는 가짜 Point입니다.
- Organized Cloud구조화 점군
- Image처럼 행과 열의 이웃 관계가 유지된 Point Cloud입니다.
- ROI관심 영역
- Algorithm이 실제로 사용할 공간 범위만 남기도록 정의한 Region of Interest입니다.
- Reflectivity반사율
- 표면이 입사한 Laser Energy를 얼마나 되돌려 보내는지를 나타내는 물리 특성입니다.
- Safety Scanner안전 레이저 스캐너
- 사람 보호용 안전 기능과 규격에 맞게 인증된 Laser Scanner로 일반 Mapping LiDAR와 구분됩니다.
연습 문제
- 사용 중인 LiDAR의 측정 방식·Wavelength·Scan Rate·각도 해상도·Return Mode를 조사하세요.
- LaserScan의 모든 유효 Point를 XY로 바꾸고 정면 Sector의 최소 거리를 계산하세요.
- angle_increment가 음수인 가상 Scan에서도 동작하는 Test를 만드세요.
- 유리·거울·검은 천·얇은 봉의 유효율과 거리 오차를 측정하세요.
- PointCloud2의 fields·point_step·row_step을 검사하는 Program을 작성하세요.
- LiDAR–base_link Extrinsic을 벽·바닥 Target으로 검증하세요.
- 최고 속도에서 한 Scan 중 이동·회전 오차를 계산하세요.
- Deskew 전후 평면 잔차와 Scan Matching Score를 비교하세요.
- Crop·Voxel·Outlier Filter별 Point 수·시간·장애물 Recall을 측정하세요.
- Raw PointCloud와 Downsample 결과의 대역폭·CPU·지연을 비교하세요.
- Intensity를 거리·입사각별로 측정해 Threshold의 한계를 설명하세요.
- Corridor·넓은 빈 공간·군중에서 Localization 품질을 비교하세요.
- Scan 중단·Freeze·Stamp 역행·Window 가림을 Monitor에 주입하세요.
- 비·안개·먼지·다른 LiDAR 간섭 시험 계획을 작성하세요.
- LiDAR 실패 시 감속·정지·Sensor 대체와 E-stop 절차를 검증하세요.
COMMUNITY
강의 댓글
질문과 학습 경험을 함께 나눠보세요.댓글을 불러오는 중입니다.