학습 목표
- 센서 측정값이 참값과 어떻게 다른지 항목별로 설명할 수 있다.
- 정확도와 정밀도를 구분하고 각각의 대응책을 말할 수 있다.
- 측정 시각과 도착 시각의 차이가 왜 중요한지 설명할 수 있다.
- 센서 메시지의 frame_id 와 단위 규약을 바르게 다룰 수 있다.
- 센서가 고장 나는 여섯 가지 방식을 알고 각각을 탐지할 수 있다.
- Sampling·Aliasing·Filter·Calibration과 Covariance를 구분해 적용할 수 있다.
- Camera·LiDAR·IMU·Encoder·GNSS 센서의 장단점과 실제 Robot 적용법을 설명할 수 있다.
- 여러 센서의 Timestamp를 동기화하고 Sensor Fusion 입력 품질을 검증할 수 있다.
1. 센서는 진실을 주지 않는다
초보자가 가장 오래 붙들고 있는 오해가 하나 있습니다. "센서가 2.4 m 라고 했으니 2.4 m 다."
아닙니다. 센서가 주는 것은 참값이 아니라 참값에 여러 가지가 더해진 숫자입니다.
측정값 = 참값 + 편향 + 잡음 + (지연 때문에 생긴 오차)
그리고 이 식 바깥에도 한계가 있습니다. 너무 멀거나 가까우면 아예 못 재고, 아무리 정밀해도 최소 단위보다 잘게는 못 잽니다.
이것을 인정하는 것에서 로봇 소프트웨어가 시작됩니다. 센서를 믿고 짠 코드는 실험실에서는 잘 돌다가 현장에서 무너집니다. 반대로 "이 값은 얼마나 틀릴 수 있는가"를 항상 함께 생각하는 코드는 튼튼합니다.
그래서 로봇에서는 값 하나만 보지 않습니다. 같은 것을 여러 번 재고, 여러 센서로 재고, 얼마나 확신하는지를 값과 함께 들고 다닙니다. ROS 2 의 여러 센서 메시지에 분산 배열이 들어 있는 이유가 이것입니다. "이 값은 이 정도 흔들릴 수 있다"를 함께 전달하는 것입니다.
뒤에 나올 센서 융합은 이 생각의 결론입니다. 틀린 센서 여러 개를 합쳐 각각보다 나은 추정을 만드는 일이며, 이것이 가능한 이유는 각 센서가 서로 다른 방식으로 틀리기 때문입니다.
| 시간 표본 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| 참값 | 0.30 | 0.36 | 0.42 | 0.48 | 0.54 | 0.60 | 0.66 |
| 측정값 | 0.38 | 0.41 | 0.52 | 0.51 | 0.65 | 0.64 | 0.75 |
참값은 매끄럽게 변하지만 측정값은 잡음으로 떨립니다. 게다가 전체가 조금 위로 치우쳐 있는데, 이 치우침이 편향입니다.
주의 센서 사양서의 숫자는 실험실 조건의 값입니다. 온도, 진동, 조명, 전원 잡음, 그리고 로봇 자신의 움직임이 실제 성능을 크게 바꿉니다. 여러분의 로봇에서 직접 재 본 값만이 여러분의 값입니다.
2. 모든 센서에 공통인 여섯 가지 성질
센서가 무엇이든 이 여섯 가지로 설명할 수 있습니다. 새 센서를 받았을 때 이 여섯 개를 채워 보는 것이 첫 번째 할 일입니다.
① 편향 — 일관되게 치우친 정도.
항상 3 cm 씩 크게 나오는 자와 같습니다. 여러 번 재서 평균을 내도 사라지지 않습니다. 대신 알아내면 빼 버릴 수 있습니다. 이것을 보정이라고 합니다.
② 잡음 — 잴 때마다 달라지는 정도.
가만히 있는데도 값이 떨립니다. 여러 번 재서 평균을 내면 줄어듭니다. 이것이 필터링의 근거입니다.
③ 분해능 — 구분할 수 있는 최소 단위.
1 mm 단위로만 나오는 센서에 0.1 mm 의 변화를 기대할 수 없습니다. 아무리 오래 재도 넘을 수 없는 벽입니다.
④ 범위 — 잴 수 있는 구간.
범위 밖에서는 틀린 값이 아니라 무의미한 값이 나옵니다. 그리고 많은 센서가 범위 경계에서 슬며시 부정확해집니다.
⑤ 갱신 주기 — 얼마나 자주 주는가.
10 Hz 센서로는 0.1 초보다 빠른 변화를 볼 수 없습니다. 빠르게 움직이는 로봇에서는 이것이 곧 위치 오차가 됩니다.
⑥ 지연 — 실제 사건과 데이터가 손에 들어오는 시각의 차이.
가장 자주 무시되고 가장 자주 사고를 냅니다. 50 ms 지연이 있는데 로봇이 초당 1 m 로 달린다면, 내가 보고 있는 세상은 5 cm 전의 세상입니다.
정확도와 정밀도를 꼭 구분하십시오. 정밀한데 부정확한 센서는 값이 안 떨리는데 꾸준히 틀린 것이고, 정확한데 부정밀한 센서는 평균은 맞는데 매번 크게 흔들리는 것입니다. 앞의 것은 보정으로 고치고, 뒤의 것은 여러 번 재서 고칩니다. 이 둘을 헷갈리면 엉뚱한 처방을 하게 됩니다.
| 성질 | 뜻 | 줄이는 법 | 무시하면 |
|---|---|---|---|
| 편향 | 일관된 치우침 | 보정해서 빼기 | 평균 내도 안 사라짐 |
| 잡음 | 잴 때마다의 흔들림 | 여러 번 재서 평균 | 제어가 떨림 |
| 분해능 | 최소 구분 단위 | 더 좋은 센서 | 없는 정밀도를 기대 |
| 범위 | 유효한 구간 | 배치를 바꿈 | 경계에서 조용히 틀림 |
| 갱신 주기 | 초당 몇 번 | 빠른 센서 | 빠른 변화를 놓침 |
| 지연 | 사건과 도착의 시차 | 경로 단축, 보상 | 과거를 현재로 착각 |
| 정밀한데 부정확 (편향) | 정확한데 부정밀 (잡음) |
|---|---|
| 값이 거의 안 떨린다 | 값이 크게 흔들린다 |
| 평균이 참값에서 벗어나 있다 | 평균은 참값에 가깝다 |
| 여러 번 재도 소용없다 | 여러 번 재면 좋아진다 |
| 보정으로 고친다 | 필터로 고친다 |
| 눈치채기 어렵다 | 한눈에 보인다 |
정확도와 정밀도는 다른 문제이고 처방도 다릅니다. 어느 쪽인지 먼저 가르고 나서 대응해야 합니다.
3. 시각이 데이터의 절반이다
앞 화면에서 TF 를 다룰 때 이미 만난 문제가 여기서 본격적으로 나옵니다.
센서 데이터가 손에 들어왔습니다. 이 값은 언제의 값입니까.
지금이 아닙니다. 빛이 물체에서 반사되고, 소자가 그것을 받고, 드라이버가 읽고, 메시지로 만들고, 네트워크를 건너오는 동안 시간이 흘렀습니다. 그래서 모든 센서 메시지에는 header.stamp 가 있습니다.
여기에 함정이 있습니다. stamp 에 무엇을 넣느냐는 드라이버가 정합니다. 좋은 드라이버는 "측정한 순간"을 넣고, 나쁜 드라이버는 "메시지를 만든 순간"을 넣습니다. 후자라면 이미 지연이 섞여 들어간 것입니다.
두 종류의 지연을 구분해야 합니다.
측정 지연 — 실제 사건과 stamp 사이. 사양서를 보거나 실험해야 압니다.
전송 지연 — stamp 와 내 손에 들어온 시각 사이. 이것은 명령 하나로 잴 수 있습니다.
왜 이것이 중요한가. 앞 화면에서 배운 대로 센서 데이터를 다른 좌표계로 옮기려면 그 데이터를 찍던 순간의 TF 가 필요합니다. stamp 가 틀리면 엉뚱한 시점의 로봇 위치로 계산하게 되고, 로봇이 빨리 움직일수록 오차가 커집니다.
stamp 를 0 으로 두고 발행하는 드라이버를 조심하십시오. TF 조회가 "항상 최신"으로 처리되어 로봇이 느릴 때는 멀쩡하다가 빨라지면 무너집니다. 이런 문제는 원인을 찾기가 매우 어렵습니다.
hz 와 delay 와 echo 세 개를 함께 봐야 합니다. 하나만 보면 얼어붙은 센서나 시각 없는 센서를 놓칩니다.
# ── 데이터가 실제로 흐르는가, 얼마나 자주 오는가 ────────
ros2 topic hz /scan
# average rate: 9.987
# min: 0.089s max: 0.114s std dev: 0.00512s window: 100
#
# 평균만 보지 마십시오. max 가 크면 가끔 크게 늦는다는 뜻이고,
# 그 순간에 제어가 흔들립니다.
# ── 전송 지연을 잰다 (header 가 있는 메시지만 가능) ──────
ros2 topic delay /scan
# average delay: 0.0231
# min: 0.018s max: 0.061s window: 100
#
# 평균 23 ms. 로봇이 1 m/s 로 달린다면 2.3 cm 전의 세상입니다.
# ── 대역폭을 잰다 (영상·점군에서 특히 중요) ─────────────
ros2 topic bw /camera/image_raw
# ── 값이 실제로 변하는지 본다 (아주 중요) ───────────────
ros2 topic echo /scan --field ranges[0]
# hz 가 정상인데 값이 계속 같다면 센서가 얼어붙은 것입니다.
# hz 만 보고 정상이라고 판단하면 이것을 놓칩니다.
# ── 시각이 제대로 붙는지 본다 ─────────────────────────
ros2 topic echo /scan --field header.stamp --once
# sec: 0 이 나오면 드라이버가 시각을 채우지 않는 것입니다.
# ── 재생·시뮬레이션 중이라면 반드시 확인 ────────────────
ros2 param get /내노드이름 use_sim_time
주의
ros2 topic delay는 두 시계가 같을 때만 뜻이 있습니다. 센서가 별도 컴퓨터에 있고 시계가 맞지 않으면 음수 지연이나 말도 안 되는 값이 나옵니다. 이런 값이 보이면 지연 문제가 아니라 시계 동기 문제입니다.
4. frame_id 와 단위 — 값에 뜻을 붙이는 것
앞 화면에서 "숫자 세 개는 아무 뜻이 없다"고 했습니다. 센서 데이터에서 이 원칙이 그대로 적용됩니다.
모든 센서 메시지는 header.frame_id 를 가집니다. "이 측정값은 어느 좌표계 기준인가"입니다. 라이다면 라이다의 회전 중심, 카메라면 광학 프레임입니다. 앞 화면에서 배운 optical frame 이 여기서 쓰입니다.
frame_id 가 틀리면 오류가 나지 않고 조용히 틀립니다. 점군이 로봇 몸통 안쪽에 그려지거나, 검출한 물체가 엉뚱한 곳에 찍힙니다. RViz2 에 띄워 보는 것이 가장 빠른 확인법입니다.
단위 규약도 지켜야 합니다. 길이는 미터, 각도는 라디안, 시간은 초입니다. 센서가 밀리미터로 주더라도 메시지에 넣을 때는 미터로 바꿔야 합니다. 이것을 안 바꾸면 로봇이 천 배 먼 곳에 물체가 있다고 믿습니다.
표준 메시지 타입을 쓰는 것이 중요합니다. 앞 화면에서 커스텀 인터페이스를 배울 때 말한 것과 같은 이유입니다. 표준을 쓰면 RViz2 가 그려 주고, 기존 알고리즘이 그대로 붙습니다.
그리고 확신의 정도를 함께 실어 보내십시오. IMU 나 위치 메시지에는 분산 배열이 있습니다. 이것을 채우지 않으면 뒤의 융합 단계에서 모든 센서를 똑같이 믿어 버리는 결과가 됩니다. 값을 모르면 관례대로 첫 항목을 음수로 두어 "모른다"고 표시할 수 있습니다.
| 재는 것 | 표준 타입 | 단위 | frame_id 는 |
|---|---|---|---|
| 거리 스캔 | sensor_msgs/LaserScan | m, rad | 라이다 회전 중심 |
| 점군 | sensor_msgs/PointCloud2 | m | 센서 원점 |
| 영상 | sensor_msgs/Image | 픽셀 값 | 카메라 광학 프레임 |
| 카메라 제원 | sensor_msgs/CameraInfo | 픽셀 | 영상과 같아야 함 |
| 가속·각속도 | sensor_msgs/Imu | m/s², rad/s | IMU 몸체 |
| 단일 거리 | sensor_msgs/Range | m | 센서 원점 |
| 관절 각도 | sensor_msgs/JointState | rad, m | 비움 (이름으로 식별) |
| 위성 위치 | sensor_msgs/NavSatFix | 도, m | 안테나 위치 |
주의 센서 데이터는 대개 BEST_EFFORT 로 발행됩니다. 앞 화면에서 배운 대로 구독자가 RELIABLE 이면 아무것도 못 받습니다.
ros2 topic hz는 데이터가 흐른다고 하는데 내 노드만 못 받는다면 가장 먼저 이것을 의심하십시오.
5. 센서가 고장 나는 여섯 가지 방식
센서는 깔끔하게 죽지 않습니다. 여섯 가지 방식으로 망가지고, 뒤로 갈수록 알아채기 어렵습니다.
① 완전 침묵 — 가장 다루기 쉽습니다.
토픽이 아예 안 나옵니다. ros2 topic hz 로 즉시 알 수 있고, 코드에서도 "일정 시간 데이터가 없으면 정지"로 대응하기 쉽습니다.
② 얼어붙음 — 여기서부터 위험합니다.
정상 주기로 계속 발행하는데 값이 안 변합니다. hz 는 완벽하게 정상으로 보입니다. 로봇은 "장애물이 계속 저기 있다"고 믿으며 주행합니다. 탐지법은 값의 변화를 감시하는 것뿐입니다.
③ 무의미한 값 — NaN 과 무한대.
라이다에서 아무것도 안 맞으면 무한대가 나오고, 측정이 실패하면 NaN 이 나옵니다. 이것을 그냥 평균에 넣으면 결과 전체가 NaN 이 되어 로봇이 멈춥니다. 반드시 걸러야 하며, 범위 밖 값도 함께 걸러야 합니다.
④ 포화 — 범위 끝에 붙어 버립니다.
너무 밝거나 너무 가까워서 값이 최대치에 눌어붙습니다. 값은 그럴듯한데 변화가 없습니다.
⑤ 간헐적 — 가장 짜증나는 종류입니다.
대부분 정상인데 가끔 이상합니다. 케이블 접촉 불량, 전원 부족, CPU 포화로 인한 드라이버 지연이 흔한 원인입니다. hz 의 평균은 정상인데 max 가 유난히 큽니다.
⑥ 조용히 틀림 — 가장 무섭습니다.
값이 그럴듯하게 나오는데 틀립니다. 센서가 비뚤게 달렸거나, 보정값이 어긋났거나, frame_id 가 잘못됐거나, 단위를 안 바꿨습니다. 이것은 자동으로 탐지할 수 없습니다. 아는 답과 대조하는 수밖에 없습니다. 앞 화면의 벽 시험이 바로 이 방법이었습니다.
| 고장 방식 | 겉보기 | 탐지법 |
|---|---|---|
| 완전 침묵 | 토픽이 없음 | hz, 수신 시각 감시 |
| 얼어붙음 | hz 는 정상 | 값의 변화 감시 |
| NaN·무한대 | 값이 이상함 | 수신 즉시 걸러 내기 |
| 포화 | 값이 최대치 고정 | 범위 경계 값 감시 |
| 간헐적 | 가끔 이상 | hz 의 max, 로그 상관 분석 |
| 조용히 틀림 | 완전히 정상으로 보임 | 아는 답과 대조 (벽 시험) |
침묵은 타이머로, 얼어붙음은 값 비교로, 무의미한 값은 걸러 내기로 각각 다르게 잡아야 합니다.
# 센서 값을 그대로 믿지 않는 구독자
# 무의미한 값을 걸러 내고, 얼어붙음과 침묵을 함께 감시합니다.
import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import LaserScan
import math
class ScanGuard(Node):
def __init__(self):
super().__init__('scan_guard')
# 센서 데이터는 BEST_EFFORT 로 오므로 맞춰 준다
self.create_subscription(
LaserScan, 'scan', self.on_scan, qos_profile_sensor_data)
self.last_stamp = None
self.last_values = None
self.frozen_count = 0
self.create_timer(0.5, self.check_silence)
def on_scan(self, msg):
self.last_stamp = self.get_clock().now()
# ① 무의미한 값을 먼저 걸러 낸다.
# NaN, 무한대, 범위 밖을 그냥 두면 평균이 통째로 망가진다.
good = [r for r in msg.ranges
if not math.isnan(r) and not math.isinf(r)
and msg.range_min <= r <= msg.range_max]
if len(good) < len(msg.ranges) * 0.3:
self.get_logger().warn(
f'유효한 점이 {len(good)}/{len(msg.ranges)} 뿐입니다')
return
# ② 값이 정말 변하는지 본다. hz 만으로는 알 수 없다.
sample = tuple(round(r, 4) for r in good[::40])
if sample == self.last_values:
self.frozen_count += 1
if self.frozen_count >= 5:
self.get_logger().error('센서가 얼어붙은 것 같습니다')
else:
self.frozen_count = 0
self.last_values = sample
# ③ 여기서부터 실제 사용
self.get_logger().info(f'유효 {len(good)}점, 최소 {min(good):.2f} m')
def check_silence(self):
if self.last_stamp is None:
return
gap = (self.get_clock().now() - self.last_stamp).nanoseconds / 1e9
if gap > 1.0:
self.get_logger().error(f'{gap:.1f}초째 데이터가 없습니다')
def main():
rclpy.init()
rclpy.spin(ScanGuard())
6. 새 센서를 받았을 때의 순서
센서를 처음 연결할 때 순서를 정해 두면 하루가 한 시간이 됩니다. 앞 화면들에서 배운 것들이 여기서 하나로 모입니다.
① 나오는가. ros2 topic list 와 hz. 안 나오면 드라이버, 권한, 케이블, 전원 순으로 봅니다.
② 변하는가. 센서 앞에서 손을 흔들며 echo 로 값을 봅니다. 이 단계에서 얼어붙음이 걸러집니다.
③ 시각이 붙는가. stamp 가 0 이 아니고 delay 가 합리적인가.
④ 좌표계가 맞는가. RViz2 에 띄웁니다. 앞뒤가 뒤집혔거나 90도 돌아가 있으면 여기서 보입니다.
⑤ 값이 맞는가. 아는 거리를 재 봅니다. 벽에서 정확히 1 m 떨어뜨려 놓고 1 m 라고 하는지 봅니다. 이것이 조용히 틀림을 잡는 유일한 방법입니다.
⑥ 얼마나 흔들리는가. 가만히 둔 채 백 번쯤 재서 평균과 흔들림 폭을 봅니다. 이 숫자가 나중에 융합 단계의 분산값이 됩니다.
⑦ 한계에서 어떻게 되는가. 너무 가까이, 너무 멀리, 어두운 물체, 유리, 반짝이는 금속. 언제 실패하는지 아는 것이 값 자체보다 중요할 때가 많습니다.
요점
- 토픽이 나오는가 — hz 로 확인
- 값이 변하는가 — 손을 흔들며 echo
- 시각이 붙는가 — stamp 와 delay
- 좌표계가 맞는가 — RViz2 로 눈 확인
- 아는 거리를 맞게 재는가 — 자로 대조
- 가만히 둘 때 얼마나 흔들리는가 — 분산 측정
- 언제 실패하는가 — 어두운 물체, 유리, 금속, 역광
- 실패했을 때 무엇을 내보내는가 — NaN 인가 0 인가 옛 값인가
평균과 참값의 차이가 편향, 표준편차가 잡음입니다. 이 두 숫자를 적어 두면 나중에 필터를 맞출 때 그대로 씁니다.
# ── 새 센서 점검 한 판 ────────────────────────────────
# 1) 나오는가
ros2 topic list | grep -i scan
ros2 topic hz /scan
# 2) 시각과 좌표계
ros2 topic echo /scan --field header --once
# stamp 가 0 이 아닌지, frame_id 가 기대한 이름인지
# 3) TF 가 이어져 있는지 (앞 화면 내용)
ros2 run tf2_ros tf2_echo base_link laser
# 4) 가만히 둔 채 흔들림을 잰다
ros2 topic echo /scan --field ranges[180] > /tmp/still.txt
# 30초쯤 모은 뒤 중단하고 평균과 표준편차를 계산한다
python3 - <<EOF
import statistics
xs = [float(l) for l in open('/tmp/still.txt')
if l.strip().replace('.','',1).replace('-','',1).isdigit()]
print(f'표본 {len(xs)}개')
print(f'평균 {statistics.mean(xs):.4f} m')
print(f'표준편차 {statistics.pstdev(xs):.4f} m')
EOF
# 5) 아는 거리와 대조한다
# 벽에서 정확히 1.000 m 떨어뜨리고 위 평균과 비교한다.
# 차이가 편향이고, 표준편차가 잡음입니다.
# 이 두 숫자가 뒤의 융합 단계에서 그대로 쓰입니다.
주의 센서를 로봇에 달기 전에 책상에서 먼저 이 순서를 밟으십시오. 로봇에 달고 나면 진동, 전원 잡음, 다른 센서의 간섭이 섞여 무엇이 원인인지 가릴 수 없게 됩니다. 책상에서 잰 값이 나중에 비교 기준이 됩니다.
7. 센서 선택은 물리 원리에서 시작한다
센서 이름보다 “무엇과 상호작용해 어떤 물리량을 전기 신호로 바꾸는가”를 먼저 봅니다. 같은 거리 센서라도 빛, 초음파와 전파는 실패 조건이 다릅니다.
| 센서 | 주로 얻는 값 | 강점 | 대표적인 약점 | 실제 적용 |
|---|---|---|---|---|
| 2D LiDAR | 평면 거리 Scan | 넓은 시야, 정밀한 형상 | 유리·검은 물체·비·먼지 | 실내 Navigation·Obstacle |
| 3D LiDAR | 3차원 Point Cloud | 거리와 구조 직접 측정 | 가격·대역폭·진동·악천후 | Mapping·자율주행 |
| RGB Camera | 색상 Image | 의미·Texture 정보 풍부 | 조명·역광·Blur·거리 불명확 | Detection·Inspection |
| Depth Camera | Depth Image·Point Cloud | 가까운 3D 구조 | 햇빛·반사·최소 거리 | Manipulation·실내 인식 |
| Ultrasonic | 단일 또는 Cone 거리 | 저가, 투명체 대응 가능 | 넓은 Beam·다중 반사·느린 갱신 | 근거리 보조 감지 |
| IMU | 가속도·각속도 | 빠른 갱신, 자가 운동 감지 | Bias·온도 Drift·진동 | 자세·Odometry 보조 |
| Wheel Encoder | 회전 위치·속도 | 고속, 단순, 저가 | Slip·Backlash·바퀴 반경 오차 | Wheel Odometry |
| GNSS | 위도·경도·고도 | 넓은 Outdoor 절대 위치 | 실내 불가·Multipath·차폐 | Outdoor Localization |
| Force/Torque | 힘·Torque | 접촉을 직접 측정 | 과부하·열 Drift·Mounting 영향 | Manipulation·충돌 감지 |
센서 하나의 장점만 보고 선택하지 말고 Range, Field of View, Rate, Latency, Interface, 전력, IP 등급, 작동 온도, Mechanical Mount, Calibration 유지 비용까지 System 요구사항과 대조합니다.
8. Sampling Rate와 Aliasing
Sampling은 연속적인 세계를 일정한 시각의 숫자로 바꾸는 과정입니다. 목표 신호의 최고 주파수보다 Sampling이 충분히 빠르지 않으면 높은 주파수가 낮은 주파수처럼 보이는 Aliasing이 생깁니다. 이상적인 이론의 Nyquist 조건은 최고 성분의 두 배보다 큰 Sampling이지만, 실제 Sensor에는 Transition Band와 Jitter가 있으므로 여유와 Analog Anti-alias Filter가 필요합니다.
실제 진동: 빠르게 위아래로 반복
Sampling: 드문드문 측정
관측 결과: 천천히 흔들리거나 반대 방향으로 보임
- Control Loop 100 Hz라고 Sensor 100 Hz가 항상 충분한 것은 아닙니다.
- Driver의 Publish Rate와 Sensor 내부 Sampling Rate는 다를 수 있습니다.
- 같은 Timestamp가 반복되거나 Burst로 도착하면 평균 Hz만으로 판단하지 않습니다.
- Downsampling 전에는 Low-pass Filter로 고주파 성분을 줄입니다.
- Camera Exposure가 길면 Sampling Rate가 높아도 Motion Blur가 생깁니다.
ros2 topic hz /imu/data
ros2 topic echo /imu/data --field header.stamp
ros2 topic bw /camera/image_raw
9. Filtering: 잡음을 줄이되 지연을 만든다
Filter는 없는 정보를 만들지 못합니다. 잡음을 줄이는 대신 반응을 늦추거나 실제 Peak를 깎을 수 있습니다.
| Filter | 특징 | 좋은 사용처 | 주의점 |
|---|---|---|---|
| Moving Average | 최근 N개 평균 | 정적인 거리·온도 | Window만큼 지연, Peak 손실 |
| Exponential Moving Average | 새 값에 가중치 | Memory가 작은 Online 처리 | α가 작을수록 느림 |
| Median | 중앙값 선택 | 순간 Spike 제거 | 연속 Noise에는 제한적 |
| Low-pass | 고주파 억제 | 진동·전기 Noise | Cutoff와 Sampling Rate 필요 |
| Complementary | 빠른 센서와 느린 기준 결합 | IMU 자세 | 단순하지만 Model 제한 |
| Kalman 계열 | Model과 Covariance로 추정 | Localization·Tracking | 잘못된 Model·Covariance에 취약 |
class EmaFilter:
def __init__(self, alpha: float):
if not 0.0 < alpha <= 1.0:
raise ValueError('alpha must be in (0, 1]')
self.alpha = alpha
self.value = None
def update(self, measurement: float) -> float:
if not math.isfinite(measurement):
raise ValueError('measurement must be finite')
if self.value is None:
self.value = measurement
else:
self.value = (
self.alpha * measurement
+ (1.0 - self.alpha) * self.value)
return self.value
Step Input을 넣어 Filter 전후의 Rise Time과 Delay를 함께 측정합니다. Control 안전 한계에 쓰는 원본 장애물 거리를 지나치게 평활화하면 정지가 늦어질 수 있습니다.
10. Calibration은 편향과 기하를 추정하는 과정이다
Calibration은 Sensor 값을 임의로 “맞추는” 작업이 아닙니다. 알려진 기준과 여러 자세·거리·온도 조건의 관측을 이용해 Parameter를 추정하고 별도 검증 Data로 확인하는 절차입니다.
대표적인 Calibration
- IMU: Gyro·Accelerometer Bias, Scale, 축 정렬, 온도 영향
- Camera Intrinsic: Focal Length, Principal Point, Lens Distortion
- Stereo: 두 Camera 사이 Extrinsic과 Rectification
- Camera–LiDAR: 두 Sensor Frame 사이 6-DoF Extrinsic
- Wheel: 바퀴 반경, Wheel Base, Encoder Scale
- Force/Torque: Zero Offset, 방향, Cross-axis 영향
Calibration Data와 검증 Data를 분리합니다. 같은 Data로 맞추고 성능까지 평가하면 Overfitting을 놓칩니다. 결과에는 Sensor Serial, Firmware, 날짜, 온도, Mounting 상태, Tool Version과 Residual을 기록합니다.
sensor_id: imu_front_001
calibrated_at: '2026-09-02T10:00:00+09:00'
temperature_c: 24.8
gyro_bias_rad_s: [0.0012, -0.0008, 0.0021]
validation_rms_rad_s: 0.0035
Sensor를 분리·재장착하거나 충돌·온도 변화가 있었으면 Extrinsic과 Bias를 다시 검증합니다.
11. Covariance는 확신의 숫자다
Covariance는 단순히 “Noise 크기” 하나가 아니라 각 축의 분산과 축 사이 상관관계를 나타냅니다. 3×3 배열은 Row-major 순서로 해석되는 경우가 많지만 반드시 해당 Message 정의를 확인합니다.
[ var(x), cov(x,y), cov(x,z),
cov(y,x), var(y), cov(y,z),
cov(z,x), cov(z,y), var(z) ]
- 대각 원소: 각 축 분산, 표준편차의 제곱
- 비대각 원소: 두 축 오차의 상관
- 단위: 원래 물리량 단위의 제곱
- 0: 무조건 “모름”이라는 공통 의미가 아님
- -1 관례:
sensor_msgs/Imu에서 제공되지 않는 측정의 첫 Covariance 원소 등에 Message별 규칙이 있음
모든 Sensor Message가 같은 “-1이면 모름” 규칙을 쓰는 것은 아닙니다. ros2 interface show sensor_msgs/msg/Imu처럼 실제 정의와 주석을 확인합니다. Calibration 없이 임의로 작은 Covariance를 넣으면 Fusion Filter가 나쁜 센서를 지나치게 신뢰합니다.
ros2 interface show sensor_msgs/msg/Imu
ros2 interface show sensor_msgs/msg/NavSatFix
ros2 topic echo /imu/data --field angular_velocity_covariance --once
12. ROS 2 표준 Sensor Message를 정확히 쓴다
표준 Message는 Field 의미와 단위를 공유해 RViz2, Navigation, Perception과 Fusion Package가 바로 사용할 수 있게 합니다.
| Message | 핵심 검증 항목 |
|---|---|
LaserScan |
angle_min/max/increment, time_increment, scan_time, range_min/max |
PointCloud2 |
fields, point_step, row_step, endian, dense, frame_id |
Image |
encoding, step, width·height, CameraInfo와 Stamp·Frame |
Imu |
Quaternion 정규화, rad/s, m/s², 세 Covariance |
Range |
radiation_type, field_of_view, min/max_range |
JointState |
name과 position·velocity·effort 배열 길이 대응 |
NavSatFix |
Status, Covariance Type, 위도·경도·고도 의미 |
ros2 interface show sensor_msgs/msg/LaserScan
ros2 topic type /scan
ros2 topic info /scan --verbose
ros2 topic echo /scan --once
LaserScan의 Inf가 유효하게 “Return 없음”을 표현할 수 있으므로 무조건 0으로 바꾸지 않습니다. Consumer가 요구하는 정책에 따라 유효값, No-return과 오류를 구분합니다.
13. QoS는 Sensor의 신선도와 전달 보장을 조절한다
ROS 2의 Sensor Data Profile은 일반적으로 Keep Last, 작은 Depth, Best Effort와 Volatile을 사용합니다. 오래된 모든 Frame을 재전송하느라 최신 Data가 늦어지는 것보다 일부를 놓쳐도 새 값을 받는 선택입니다. 하지만 Safety Event, 저속 상태값과 유선 환경까지 무조건 같은 Profile을 써야 한다는 뜻은 아닙니다.
from rclpy.qos import qos_profile_sensor_data
self.sub = self.create_subscription(
LaserScan, '/scan', self.on_scan, qos_profile_sensor_data)
scan_sub_ = create_subscription<sensor_msgs::msg::LaserScan>(
"/scan", rclcpp::SensorDataQoS(), callback);
QoS는 Publisher와 Subscriber의 호환성을 확인하고 실제 Wi-Fi·부하·Message 크기에서 Loss, Latency와 CPU를 측정해 결정합니다.
ros2 topic info /scan --verbose
ros2 topic echo /scan --qos-reliability best_effort
14. 여러 센서는 Timestamp로 맞춘다
Camera Image와 CameraInfo, RGB와 Depth, LiDAR와 IMU를 융합하려면 “비슷한 도착 시각”이 아니라 측정 Timestamp를 기준으로 맞춰야 합니다.
- Exact Time: Stamp가 정확히 같은 Set만 전달
- Approximate Time:
slop이내의 가까운 Stamp를 Pairing - Queue가 작으면 Pair 전 Drop, 너무 크면 Memory·Latency 증가
- 모든 Input Subscriber의 QoS 호환성을 맞춤
- Header가 없다고 Arrival Time을 대신 쓰면 불확실한 지연이 섞이므로 가능한 피함
from message_filters import Subscriber, ApproximateTimeSynchronizer
from sensor_msgs.msg import Image, CameraInfo
image_sub = Subscriber(self, Image, '/camera/image', qos_profile=qos)
info_sub = Subscriber(self, CameraInfo, '/camera/camera_info', qos_profile=qos)
sync = ApproximateTimeSynchronizer(
[image_sub, info_sub], queue_size=20, slop=0.03)
sync.registerCallback(self.on_pair)
self.sync = sync # 객체 수명 유지
slop을 크게 하면 Pair 수는 늘지만 서로 다른 장면을 합칠 수 있습니다. Robot 속도와 허용 공간 오차로 시간 허용값을 계산합니다.
15. Sensor Fusion은 나쁜 입력을 자동 치료하지 않는다
Fusion은 서로 보완적인 Sensor와 올바른 Model·Frame·Time·Covariance가 있을 때 효과가 있습니다.
| Sensor | 짧은 시간 | 긴 시간 | 대표 역할 |
|---|---|---|---|
| IMU Gyro | 빠르고 매끄러움 | Bias로 Drift | 빠른 회전 변화 |
| Encoder | 상대 이동에 강함 | Slip로 누적 오차 | Wheel Odometry |
| LiDAR Localization | 계산 지연 가능 | 환경이 맞으면 Drift 교정 | Map 기준 Pose |
| GNSS | Noise·Multipath | Outdoor 절대 위치 | Global Drift 교정 |
Fusion 전에 다음을 검증합니다.
- 모든 단위와 축 방향이 ROS Convention에 맞는가?
- Timestamp가 실제 측정 시각인가?
- TF Extrinsic이 정확한가?
- Covariance가 실측과 일치하는가?
- 중복 정보의 상관을 무시하고 두 번 믿고 있지 않은가?
- Outlier와 Sensor 단절 시 Filter가 어떻게 동작하는가?
Filter Output이 매끄럽다는 이유만으로 정확하다고 판단하지 않습니다. Ground Truth나 독립 기준과 Trajectory Error·Innovation·Residual을 비교합니다.
16. 실제 Robot용 Sensor Guard 전체 예제
#!/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 SensorGuard(Node):
def __init__(self):
super().__init__('sensor_guard')
self.last_rx_ns = None
self.last_signature = None
self.frozen = 0
self.status_pub = self.create_publisher(
DiagnosticArray, '/diagnostics', 10)
self.create_subscription(
LaserScan, '/scan', self.on_scan, qos_profile_sensor_data)
self.create_timer(0.2, self.check_timeout)
def publish_status(self, level, message, **values):
array = DiagnosticArray()
array.header.stamp = self.get_clock().now().to_msg()
status = DiagnosticStatus()
status.name = 'front_lidar_guard'
status.hardware_id = 'front_lidar'
status.level = level
status.message = message
status.values = [KeyValue(key=k, value=str(v))
for k, v in values.items()]
array.status = [status]
self.status_pub.publish(array)
def on_scan(self, msg):
now_ns = self.get_clock().now().nanoseconds
self.last_rx_ns = now_ns
valid = [r for r in msg.ranges
if math.isfinite(r) and msg.range_min <= r <= msg.range_max]
ratio = len(valid) / max(1, len(msg.ranges))
signature = tuple(round(x, 3) for x in valid[::max(1, len(valid)//16)])
self.frozen = self.frozen + 1 if signature == self.last_signature else 0
self.last_signature = signature
if ratio < 0.3:
self.publish_status(DiagnosticStatus.ERROR, 'too few valid rays', ratio=ratio)
elif self.frozen >= 10:
self.publish_status(DiagnosticStatus.WARN, 'scan may be frozen', frozen=self.frozen)
else:
self.publish_status(DiagnosticStatus.OK, 'sensor healthy', ratio=ratio)
def check_timeout(self):
if self.last_rx_ns is None:
self.publish_status(DiagnosticStatus.STALE, 'no scan received')
return
age_ms = (self.get_clock().now().nanoseconds - self.last_rx_ns) / 1e6
if age_ms > 500.0:
self.publish_status(DiagnosticStatus.STALE, 'scan timeout', age_ms=age_ms)
def main():
rclpy.init()
node = SensorGuard()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Frozen 판정은 정지된 환경에서도 같은 값이 나올 수 있어 단독 Safety 판정으로 쓰면 안 됩니다. Robot Motion, 다른 Sensor와 Driver Sequence Number 등을 함께 사용하고 Warning과 Hard Fault 기준을 구분합니다.
17. 전원·배선·장착도 Sensor Software의 일부다
현장 Sensor 문제의 상당수는 Algorithm보다 Power와 Mechanical 조건에서 시작합니다.
- 전압 강하와 Inrush Current를 측정한다.
- Motor Power와 Sensor Power Ground·Noise 경로를 검토한다.
- USB Cable 길이·Connector Lock·EMI·차폐를 확인한다.
- LiDAR와 Camera 시야를 Robot Body·Cable이 가리지 않게 한다.
- IMU를 진동원과 열원에서 떨어뜨리고 축 방향을 표시한다.
- Camera Focus·Exposure·Lens 오염과 결로를 운영 점검에 넣는다.
- Sensor 간 간섭을 한 개씩 켜고 끄며 A/B Test한다.
- Mount 변형과 충돌 뒤 Extrinsic을 재검증한다.
Driver Log, Kernel Log와 전원 계측 시각을 ROS Bag과 맞추면 간헐 장애의 원인을 찾기 쉬워집니다.
dmesg --follow
ros2 topic hz /scan
ros2 bag record /scan /imu/data /diagnostics /tf /tf_static
18. Sensor 선정부터 운영까지 표준 절차
운영 전 Checklist
- Sensor Serial·Firmware·Driver Version을 기록했다.
- 단위,
frame_id, Timestamp 의미를 문서화했다. - 유효 범위와 NaN·Inf·No-return 정책을 정했다.
- Bias·표준편차·Latency·최악 간격을 실측했다.
- QoS Endpoint 호환성과 Network 부하를 확인했다.
- Calibration File과 Sensor ID가 일치한다.
- 침묵·Freeze·Outlier·포화·Clock 이상을 시험했다.
- Watchdog가 오래된 Data를 사용하지 못하게 한다.
- Sensor 단절 시 Robot의 안전 상태를 검증했다.
- Bag·Log·Diagnostics로 재현 가능한 증거를 남긴다.
19. 확인 퀴즈 15문항
답을 선택한 뒤 정답 확인을 누르세요. 정답과 해설은 제출 후에 표시됩니다.
1. 센서 측정값을 가장 정확히 표현한 것은?
센서를 그대로 믿고 짠 코드는 실험실에서는 잘 돌다가 현장에서 무너집니다. 이 값이 얼마나 틀릴 수 있는지를 항상 함께 생각하는 코드라야 튼튼하며, 센서 융합도 이 생각에서 출발합니다.
2. 여러 번 재서 평균을 내도 사라지지 않는 오차는?
편향은 항상 같은 방향으로 치우친 오차이므로 평균으로는 없앨 수 없습니다. 대신 아는 값과 대조해 알아내면 빼 버릴 수 있으며, 이것을 보정이라고 합니다.
3. 값이 거의 안 떨리는데 꾸준히 3 cm 크게 나오는 센서의 문제는?
값이 안 떨리므로 정밀하고, 평균이 참값에서 벗어났으므로 부정확합니다. 이 경우 여러 번 재도 소용이 없고 아는 값과 대조해 편향을 알아내 빼야 합니다.
4. 1 mm 분해능 센서에서 0.1 mm 변화를 보려면?
분해능은 구분할 수 있는 최소 단위이므로, 아무리 오래 재도 없는 정보를 만들어 낼 수는 없습니다. 잡음과 달리 평균으로 개선되지 않는 근본적인 한계이므로 처방이 완전히 다릅니다.
5. 지연이 50 ms 인 센서로 초당 1 m 로 달리는 로봇이 보는 세상은?
속도에 지연을 곱한 만큼 뒤처진 정보를 보고 있는 셈입니다. 지연은 가장 자주 무시되면서 가장 자주 사고를 내는 성질이며, 로봇이 빠를수록 오차가 커집니다.
6. header.stamp 에 좋은 드라이버가 넣는 값은?
메시지를 만든 순간을 넣으면 이미 지연이 섞여 들어간 것입니다. 측정 순간이 들어가야 TF 조회 시 그 데이터를 찍던 순간의 로봇 위치로 계산할 수 있습니다.
7. 전송 지연을 재는 명령은?
header 가 있는 메시지에서 stamp 와 수신 시각의 차이를 재 줍니다. 다만 두 컴퓨터의 시계가 맞지 않으면 음수나 말이 안 되는 값이 나오므로 그때는 시계 동기 문제입니다.
8. frame_id 가 잘못되면 어떻게 되는가?
숫자 세 개는 아무 뜻이 없다는 원칙이 그대로 적용됩니다. RViz2 에 띄워 보면 점군이 로봇 몸통 안쪽에 그려지는 식으로 드러나므로 눈으로 확인하는 것이 가장 빠릅니다.
9. 센서가 밀리미터로 값을 줄 때 메시지에 넣어야 할 단위는?
ROS 2 는 길이를 미터, 각도를 라디안, 시간을 초로 씁니다. 바꾸지 않으면 로봇이 천 배 먼 곳에 물체가 있다고 믿게 되므로 드라이버에서 반드시 변환해야 합니다.
10. 메시지의 분산 배열을 채우지 않으면 뒤의 융합 단계에서 생기는 일은?
융합은 각 센서를 얼마나 믿을지에 따라 가중치를 정하므로, 분산이 비어 있으면 판단 근거가 사라집니다. 값을 모른다면 관례대로 첫 항목을 음수로 두어 모른다고 표시할 수 있습니다.
11. ros2 topic hz 는 정상인데 내 노드만 데이터를 못 받는다. 가장 먼저 의심할 것은?
센서 데이터는 대개 BEST_EFFORT 로 발행됩니다. 앞 화면에서 배운 QoS 호환 규칙에 따라 구독자가 더 강한 보장을 요구하면 연결 자체가 성립하지 않습니다.
12. 얼어붙은 센서를 탐지할 수 없는 방법은?
얼어붙은 센서는 정상 주기로 계속 발행하므로 hz 는 완벽하게 정상으로 보입니다. 값이 실제로 변하는지 감시하는 것이 유일한 탐지법이며, 로봇은 그 사이 옛 장애물을 믿으며 주행합니다.
13. LaserScan 의 NaN 과 무한대를 그대로 평균에 넣으면?
무한대는 아무것도 안 맞았다는 뜻이고 NaN 은 측정 실패입니다. 수신 즉시 걸러야 하며 range_min 과 range_max 를 벗어난 값도 함께 걸러야 합니다.
14. 조용히 틀리는 센서를 잡는 유일한 방법은?
값이 그럴듯하게 나오므로 자동으로 탐지할 수 없습니다. 센서가 비뚤게 달렸거나 단위를 안 바꿨거나 frame_id 가 틀린 경우가 대표적이며, 앞 화면의 벽 시험이 이 방법이었습니다.
15. 새 센서를 처음 점검할 때 로봇에 달기 전에 책상에서 하는 이유는?
책상에서 잰 값이 나중에 비교 기준이 됩니다. 로봇에 단 뒤 값이 나빠졌다면 그 차이가 곧 로봇 환경이 만들어 낸 영향이므로, 무엇을 고쳐야 하는지 원인을 좁힐 수 있게 됩니다.
- Sensor센서
- 빛·힘·거리·회전 같은 물리량을 Robot이 처리할 수 있는 전기 신호와 Data로 변환하는 장치입니다.
- Accuracy정확도
- 측정값 또는 그 평균이 참값에 얼마나 가까운지를 나타내는 성질입니다.
- Precision정밀도
- 같은 조건의 반복 측정값이 서로 얼마나 가깝게 모이는지를 나타냅니다.
- Bias편향
- 측정값을 한 방향으로 지속해서 치우치게 만드는 체계적인 오차입니다.
- Noise잡음
- 같은 조건에서도 측정마다 불규칙하게 달라지는 성분입니다.
- Resolution분해능
- Sensor가 출력에서 구분할 수 있는 물리량 변화의 최소 단위입니다.
- Latency지연
- 실제 측정 Event부터 Data가 Consumer에서 사용 가능해질 때까지 걸리는 시간입니다.
- Sampling Rate표본화 주파수
- 연속적인 물리 신호를 초당 몇 번 측정해 숫자로 만드는지를 나타냅니다.
- Aliasing앨리어싱
- Sampling이 부족해 높은 주파수 변화가 더 낮거나 다른 주파수처럼 관측되는 현상입니다.
- Calibration보정
- 알려진 기준 관측으로 Bias·Scale·Intrinsic·Extrinsic Parameter를 추정하는 절차입니다.
- Covariance공분산
- 각 축의 오차 분산과 축 사이 오차 상관관계를 함께 나타내는 불확실성 표현입니다.
- Extrinsic외부 기하 보정
- 두 Sensor 또는 Sensor와 Robot Base Frame 사이의 위치와 방향 변환입니다.
- Intrinsic내부 보정값
- Camera Focal Length·Principal Point·Lens Distortion처럼 Sensor 내부 측정 Model의 Parameter입니다.
- Field of View시야각
- Sensor가 한 번에 관측할 수 있는 공간의 각도 범위입니다.
- Timestamp측정 시각
- Sensor Data가 어느 시점의 세계를 나타내는지 기록한 시간 정보입니다.
- frame_id센서 좌표계 식별자
- 측정값이 표현된 Coordinate Frame의 이름을 담는 Header Field입니다.
- SensorDataQoS센서 데이터 통신 프로필
- 낮은 지연과 최신 Data를 우선하도록 주로 Best Effort·작은 Queue를 사용하는 ROS 2 QoS Profile입니다.
- Time Synchronization시간 동기화
- 여러 Sensor Data를 Timestamp 기준으로 같은 측정 시점에 맞추는 과정입니다.
- Sensor Fusion센서 융합
- 서로 다른 Sensor의 Model과 불확실성을 이용해 단일 Sensor보다 나은 상태를 추정하는 과정입니다.
- Watchdog감시 타이머
- Sensor Data의 침묵·노후화 등 비정상 상태를 감지해 안전 동작을 Trigger하는 장치나 Software입니다.
연습 문제
- LiDAR·Camera·Ultrasonic 중 하나를 골라 물리 원리와 실패 조건 표를 작성하세요.
- 정지 상태에서 1,000개 표본의 평균·표준편차·min·max를 계산하세요.
- 알려진 세 거리에서 편향과 선형성 오차를 측정하세요.
- Sensor Publish Rate, Timestamp 간격과 수신 간격을 비교하세요.
- Robot 최고 속도와 Sensor 지연으로 최대 공간 오차를 계산하세요.
- Moving Average와 EMA의 Noise 감소·Step Response 지연을 비교하세요.
- Downsampling 전후 Aliasing이 나타나는 Signal 실험을 설계하세요.
- LaserScan의 NaN·Inf·범위 밖·No-return 처리 정책을 구현하세요.
frame_id와 TF Extrinsic 오류를 RViz2에서 찾아 수정하세요.- Sensor Data QoS와 Reliable QoS를 Wi-Fi에서 A/B Test하세요.
- Image와 CameraInfo를 ApproximateTime으로 Pairing하고 Stamp 차를 기록하세요.
- IMU Covariance를 정지 Data에서 추정하고 단위를 설명하세요.
- Silence·Freeze·포화·Spike를 주입해 SensorGuard를 시험하세요.
- Sensor 전원·Cable·진동·온도 점검표를 작성하세요.
- 실물 Robot에서 Sensor 단절 시 안전 정지와 복구 절차를 검증하세요.
COMMUNITY
강의 댓글
질문과 학습 경험을 함께 나눠보세요.댓글을 불러오는 중입니다.