Skip to content

JETSON_SENSOR_ACCESS

Taeyoung96 edited this page Sep 5, 2026 · 2 revisions

Jetson 센서 원격 접속 — 대역폭 제약과 데이터 반출

Jetson Orin에 붙은 LiDAR·카메라를 원격에서 보거나 데이터를 가져올 때 필요한 것. 접속 경로 자체는 JETSON_SSH_ACCESS를 먼저 본다. 이 문서는 접속이 된다는 전제에서 시작한다.


0. 먼저 알아야 할 것 — 링크가 0.30 MB/s다

이 문서의 거의 모든 내용이 이 숫자에서 나온다.

10 MB 전송에 33.2초  →  약 0.30 MB/s (2.4 Mbps)

Jetson은 ZER01NE_WIFI(2.4 GHz)에 붙어 있고, 외부에서는 Tailscale로만 닿는다. 신호 자체는 멀쩡하다:

freq 2457 (2.4GHz)   signal −36 dBm   tx bitrate 52 Mbit/s

신호 −36 dBm은 아주 좋은 값인데 실효 처리량은 PHY의 5%다. 전파 문제가 아니라 2.4 GHz 혼잡 또는 사무실 업링크 병목이다. 5 GHz 대역이나 유선으로 옮기지 않는 한 이 제약은 그대로다.

센서가 만들어내는 양과 비교하면:

토픽 대역폭 링크 대비
/camera/color/image_raw 13.84 MB/s 46배 초과
/livox/lidar 5.20 MB/s 17배 초과
/camera/color/image_raw/compressed 1.49 MB/s 5배 초과

즉 어떤 센서 토픽도 원격에서 온전한 레이트로는 못 받는다. 실시간으로 보려면 줄여야 하고, 전체 데이터가 필요하면 녹화 후 옮겨야 한다.


1. 현재 살아있는 토픽

Jetson에서 상시 도는 서비스가 발행한다 (livox-driver, realsense, foxglove-bridge, mosquitto).

토픽 타입 레이트 비고
/livox/lidar PointCloud2 10 Hz frame livox_frame, 19968 pts/msg
/livox/imu Imu —
/camera/color/image_raw Image 14.9 Hz 848×480 rgb8, fx 614.31
/camera/color/image_raw/compressed CompressedImage 14.9 Hz 848×480 JPEG q95
/camera/color/camera_info CameraInfo 14.9 Hz plumb_bob
/camera/aligned_depth_to_color/image_raw Image 13.8 Hz 848×480 16UC1 — 깊이는 이걸 써라
/camera/aligned_depth_to_color/camera_info CameraInfo 13.8 Hz color 와 동일 intrinsics
/camera/depth/image_rect_raw Image 14.1 Hz 640×480 16UC1, fx 383.14 — 정렬 안 됨, 쓰지 마라
/tf_static map·livox_frame·camera_* 전부 한 트리 (§1.1)

/livox/lidar의 point field는 x, y, z, intensity, tag, line, timestamp.

1.1 센서 외부파라미터 (2026-09-05 전면 개정)

이전 판 경고. 2026-09-05 오전에 배포한 dvlc 단독 값은 병진이 198 mm 틀렸다. 같은 날 오후 검증에서 드러나 수정했다. 그 이전 값으로 LiDAR–이미지 융합을 돌린 결과가 있으면 다시 봐야 한다 (오차: 1 m에서 90 px, 2 m에서 45 px).

정본 수치와 근거는 Jetson의 ~/spot_extrinsic.yaml. 백업은 ~/spot_extrinsic.yaml.bak-20260905.

body 기준 (정본)

body = x 전방, y 좌, z 상. 원점은 엉덩이 중심이고 기립 시 지면 위 0.5087 m다 (body_T_gpe 실측).

body -> livox_frame
  xyz   0.304668878  -0.000000116   0.230026415
  quat  0.0           0.104528463   0.0          0.994521895      # pitch down 12.0°

body -> camera_link                            # == camera_depth_optical_frame
  xyz   0.379725244   0.017510534   0.096776966
  quat  0.009310769   0.026752321   0.002583650  0.999595392

body -> camera_color_optical_frame
  xyz   0.379381950   0.032434884   0.097408095
  quat -0.508819740   0.516735369  -0.475408153  0.498070395
  # 광학규약 제거 시: roll +1.776  pitch +2.971  yaw +0.891 deg

CAD는 카메라 회전을 전부 0으로 놓았는데 틀렸다. roll·pitch가 실제로 +1.78 / +2.97°다. 병진은 CAD가 맞다.

항목 상태 근거
body_T_livox roll·pitch·tz ✅ CAD 맞음 (0.25~0.36°, 25 mm) 지면평면 3회
body_T_livox 병진 ✅ CAD 맞음 줄자
body_T_livox yaw ✅ CAD 0° 맞음 LiDAR→이미지 투영 육안검사
body_T_camera roll·pitch ✅ CAD 아님. +1.78 / +2.97° 지면평면 + dvlc 교차검증
body_T_camera 병진 ✅ CAD 맞음 줄자 + 피듀셜 잔차 (x 18 mm, z 8 mm)
body_T_camera yaw ✅ +0.891 ± 0.3° 피듀셜 3배치

/tf_static 에 발행 중인 것

spot-extrinsic.service가 두 개를 발행한다. enabled 상태라 재부팅해도 유지된다.

body -> livox_frame
xyz   0.304668878  -0.000000116   0.230026415
quat  0.0           0.104528463   0.0          0.994521895

livox_frame -> camera_link
xyz   0.101120323   0.017510650  -0.114732533
quat  0.008989699  -0.077880401   0.003542737  0.996915883
body -- livox_frame -- camera_link --+-- camera_color_frame -- camera_color_optical_frame
                                     +-- camera_depth_frame -- camera_depth_optical_frame

map -> livox_frame 자리표시자는 2026-09-05 제거했다 (~/svc_livox.sh). 전부 0이라 의미가 없었고, body를 넣으려면 livox_frame의 부모가 둘이 되면 안 됐다. map을 참조하는 노드·코드는 없다(전수 확인).

body는 지금 정적 루트일 뿐이다. 로봇이 움직일 때의 body 자세(odom -> body)를 발행하는 브리지가 아직 없다. 그래서 걷는 로봇의 bag에서는 body가 고정으로 보인다. 브리지가 생기면 odom -- body -- livox_frame -- ...로 자연스럽게 확장된다.

확인:

ros2 run tf2_ros tf2_echo body camera_color_optical_frame -p 6
#   xyz 0.379382 0.032435 0.097408
ros2 run tf2_ros tf2_echo livox_frame camera_color_optical_frame -p 6
#   xyz 0.100653 0.032435 -0.114187 / rpy -80.969 1.568 -88.849 deg

⚠️ livox_frame -> camera_color_optical_frame을 직접 발행하지 않는다. RealSense 드라이버가 이미 camera_color_frame -> camera_color_optical_frame을 발행하고 있어서, 저것을 또 쏘면 optical frame의 부모가 둘이 되고 TF 트리가 갈라진다.

무엇을 왜 고쳤나

배포 TF를 병진·회전 둘 다 교체했다. 이제 dvlc 값을 직접 쓰지 않고, body 기준 실측 두 개를 합성해 유도한다.

병진  old  0.205696150  -0.137685857  -0.032145274
      new  0.101120323   0.017510650  -0.114732533       (|Δ| = 198 mm)
회전  old  qxyzw 0.005522886 -0.077601178  0.029733257 0.996525709
      new  qxyzw 0.008989699 -0.077880401  0.003542737 0.996915883   (3.028°, 전부 yaw)

dvlc 판정 — tilt만 쓸 만하다:

성분 판정 어떻게 알았나
roll·pitch (tilt) ✅ 양호, 0.68° 지면평면 교차검증
yaw ❌ 3.028° 오류 LiDAR→이미지 투영 육안검사
병진 ❌ 198 mm 오류 줄자 + CAD

깊이 다양성이 부족하면 병진과 회전이 서로 상쇄되며 수렴하는 전형적 실패다. 재투영 잔차만 보면 이게 안 드러난다.

이전 문서에 적혀 있던 ±10 cm / ±2°는 tilt에만 해당했다. yaw와 병진은 그 범위를 넘었다.

고치기 전 LiDAR→이미지 투영 오차: 병진분이 1 m에서 90 px / 2 m 45 px / 5 m 18 px, 거기에 yaw 오류가 거리 무관 상수 32 px를 더했다.

어떻게 쟀나 (재현용)

지면평면 — roll·pitch·tz 3 DOF

평평한 바닥(반경 3 m 이상)에 세우고 12초 녹화. Spot이 주는 body_T_gpe(발바닥 접촉 기반 지면추정)의 법선·높이와 각 센서에서 피팅한 바닥 평면을 비교한다.

  • yaw·tx·ty는 원리상 안 나온다 — 평면은 자기 위로 미끄러지는 것과 법선축 회전에 불변이다.
  • 카메라는 정면 5 m 이상 열린 바닥이 필요하다. 벽을 보면 벽 밑동이 바닥으로 오분류돼 3~6°씩 틀린 답이 나온다 (2026-09-05에 실제로 겪었다).
  • 기립 흔들림은 무시 가능: 12초 p-p roll 0.011°, pitch 0.013°, z 0.0 mm.

피듀셜 — yaw

146 mm AprilTag 36h11을 세워서 전방 1.5~2.5 m, 지상 0.9 m 부근. Spot이 body_T_tag를, RealSense가 같은 태그를 본다.

  • 깊이를 쓰지 않는다. 태그 중앙이 검은색이라 D435 IR 깊이가 편향된다. 카메라 광선 방향과 Spot이 준 태그 방향만 맞춰 yaw 1 DOF를 푼다.
  • 태그 중심은 코너 평균이 아니라 대각선 교점을 쓴다 (사영 불변).
  • 노이즈: 카메라 방위 std 0.016° / Spot 방위 std 0.244°. Spot이 지배적이니 배치마다 20~25회 폴링해 평균낸다.
  • 배치의 방위를 넓게 흩어야 yaw와 ty가 분리된다.
  • 바닥에 누운 태그는 못 쓴다. 가까이 가면 앙각이 FOV를 벗어나고, 멀어지면 납작해져 디코딩이 안 된다. 성립 구간이 없다.

투영 육안검사 — LiDAR yaw

두 가설(dvlc 회전 / 피듀셜 유래 회전)로 LiDAR 점구름을 컬러 이미지에 각각 투영해 비교한다. 회전 오차는 거리에 무관한 상수 화각 오차라 3° = 32 px로 항상 보인다. 박스·케이스의 수직 에지에서 어느 쪽이 물체 경계와 맞는지 보면 된다.

  • 병진을 먼저 고쳐야 유효하다. 아니면 병진 오차에 묻혀 판별이 안 된다.
  • 스크립트 scratchpad/proj_test.py, 결과 이미지 ~/Desktop/spot-calib/proj_check/

2. Foxglove로 보기

Jetson에 foxglove-bridge가 0.0.0.0:8765로 상시 떠 있다. 다만 브릿지는 평문 ws://라 https://app.foxglove.dev에서 직접 연결하면 브라우저가 mixed content로 차단한다.

SSH 터널로 우회한다. 브라우저는 localhost를 secure로 취급하므로 차단되지 않는다.

# 터미널 1 — 터널 (작업 끝나면 Ctrl-C)
ssh -N -L 8765:127.0.0.1:8765 jetson-orin-spot

브라우저에서:

https://app.foxglove.dev/?ds=foxglove-websocket&ds.url=ws://localhost:8765

어떤 토픽을 볼 것인가

토픽 결과
/camera/color/image_raw ❌ 사실상 정지. 프레임이 0.92 MB라 3초에 한 장, 브릿지 송신 큐에 수 MB 적체 → 지연 폭증
/camera/color/image_raw/compressed ✅ 보인다. 15 Hz는 못 채우고 ~3 fps로 떨어지지만 화면은 나온다. 단 링크를 포화시킨다
/livox/lidar ✅ 보인다. 마찬가지로 대량 드랍 중이지만 포인트클라우드는 일부만 와도 그림이 그려져 티가 덜 난다

3D 패널 fixed frame은 map으로 둔다 (static TF가 그것뿐).

⚠️ 다른 작업과 겹칠 때

Foxglove가 켜져 있으면 업링크를 통째로 먹는다. 실측 사례 — bag 전송 중에 Foxglove가 raw 이미지를 구독하고 있었더니:

Foxglove 연결 중:  rsync 0.073 MB/s
Foxglove 끊은 후:  rsync 0.30 MB/s   (4배)

데이터 반출·rsync·bag 전송 중에는 Foxglove를 끄는 게 맞다. 850 MB 받을 때 10분 vs 40분 차이가 난다.

대역폭이 정말 아쉬우면 — 프리뷰 노드 패턴

토픽을 축소해서 새로 발행하는 작은 노드를 띄우는 방법이 있다. Jetson 안에서 resize + JPEG 하므로 링크에는 작은 것만 나간다.

/camera/color/image_raw  (로컬이라 공짜)
   → cv2 resize 424×240, imencode q50, 5 Hz로 간축
   → 12 KB/frame = 0.06 MB/s   (compressed 대비 25배 작음)

기존 서비스를 건드리지 않고 토픽만 추가하는 방식이라 안전하다. 상시 필요하지는 않아서 지금은 안 띄워놨다. 필요하면 만들어 쓰면 된다.


3. 데이터 반출 — 녹화 후 전송

실시간이 안 되면 녹화해서 옮긴다. 이건 시간만 쓰면 되므로 확실하다.

전송은 rsync -az를 쓴다. rgb8 raw 이미지는 압축이 잘 먹어서 체감 차이가 크다 — 188 MB bag이 약 2분에 들어왔다 (비압축 추정 12분).

rsync -az --info=progress2 \
  jetson-orin-spot:/home/zer01ne-spot/calib_bags/<이름> ~/원하는경로/

4. LiDAR–카메라 캘리브레이션용 bag 녹화

direct_visual_lidar_calibration (koide3, MIT) 을 쓰기 위한 수집 절차다.

한 번 돌려서 결과를 이미 배포했다 (§1.1). 여기부터는 값을 다시 뽑아야 할 때만 필요하다 — 센서를 다시 마운트했거나, 더 정확한 값이 필요할 때. 그냥 쓰려는 것뿐이면 /tf_static에 이미 나오고 있으니 이 절은 건너뛴다.

⚠️ 이 절차의 병진 출력은 믿지 마라. 2026-09-05 검증에서 dvlc 병진이 198 mm 틀린 것으로 드러났다(§1.1). 회전은 0.68° 안에서 정확했다. 깊이 다양성이 부족하면 병진과 회전이 서로 상쇄되며 수렴하는데, 재투영 오차만 보면 이게 안 보인다.

다시 돌린다면 반드시:

  1. 병진은 CAD·줄자로 독립 교차검증한다 (두 센서 하우징 중심 사이 거리를 직접 잰다)
  2. 수집 자세의 깊이 다양성을 늘린다 — 근·중·원거리를 고루 섞는다
  3. 회전은 지면평면 교차검증으로 확인한다 (§1.1 "어떻게 쟀나")

왜 스로틀이 필요한가

레퍼런스 데이터셋은 이미지를 2 Hz로 기록한다 (15.65초 bag에 points 157개, image 30개). 15 Hz로 뜨면 용량만 7배가 되고 이득이 없다. topic_tools가 Jetson에 설치되어 있지 않아 스로틀 노드를 따로 띄운다.

준비된 스크립트

Jetson에 이미 올려둔 것:

~/calib_throttle.py    /camera/color/image_raw 15Hz → /calib/image 2Hz
                       + /calib/camera_info 를 이미지와 같은 스탬프로 동기 발행
~/record_calib.sh      스로틀 켜고 → 녹화 → 끄고 → 결과 출력

사용법

ssh jetson-orin-spot '~/record_calib.sh <지점명> 30'
rsync -az jetson-orin-spot:/home/zer01ne-spot/calib_bags/<지점명> ~/원하는경로/

한 지점 30초 기준 약 190 MB, 전송 약 2분이다.

기록되는 토픽은 셋뿐이다. preprocess -a(자동 토픽 선택)가 bag마다 image/camera_info/points 각 1개를 요구하기 때문이다.

/calib/image        sensor_msgs/Image        ~2 Hz
/calib/camera_info  sensor_msgs/CameraInfo   ~2 Hz
/livox/lidar        PointCloud2              10 Hz

수집 조건

  • Spot 정지 상태로 5~8지점. MID-360은 non-repetitive scan이라 정지 누적이 정답이다 (preprocess -d 불필요)
  • 지점마다 yaw 30~40°씩 변화를 준다. 회전 관측성이 없으면 해가 국소 최적에 빠진다
  • 유리·모니터·광택 바닥을 피한다. 텍스처 있는 벽과 직교 엣지(문틀, 기둥, 박스)가 있는 곳
  • 최소 1지점만으로도 계산은 되지만(single-shot 지원), 정확도를 위해 여러 지점을 권장한다

IMU는 넣지 않는다

direct_visual_lidar_calibration의 preprocess는 IMU를 읽지 않는다. /camera/imu는 롤링셔터 분석용으로 목적이 다르니 별도 bag에 담는다.

초기값은 수동으로

자동 초기값 추정은 SuperGlue를 쓰는데 상용 이용이 금지되어 있다. initial_guess_manual을 써야 하고, 이건 GUI(Iridescence)가 필요하므로 headless인 Jetson이 아니라 작업자 PC에서 돌린다. 대응점을 3점 이상 클릭하는 방식이다.

로컬 설치는 GTSAM·Ceres·Iridescence 빌드가 필요해서 오래 걸린다. 공식 도커 이미지(koide3/direct_visual_lidar_calibration)를 쓰는 편이 빠르다.


5. 알려진 문제

compressed_image_transport OpenCV 충돌 (2026-08-29 해결)

증상 — /camera/color/image_raw/compressed가 빈 메시지(data: [])만 발행하고, 구독자가 붙을 때마다 realsense 로그에 에러가 쏟아졌다.

[CompressedPublisher]: OpenCV(4.8.0) alloc.cpp:73:
(-4:Insufficient memory) Failed to allocate 323193268964880 bytes

원인 — 한 프로세스에 OpenCV 두 버전이 동시에 로드되고 있었다.

librealsense2_camera.so (ros2_ws 로컬 빌드)  → libopencv_core.so.408   (JetPack 4.8.0)
cv_bridge, compressed_image_transport (apt)  → libopencv_core.so.4.5d  (Ubuntu 4.5.4)

realsense가 먼저 로드되므로 cv::imencode 심볼이 4.8.0으로 해석되는데, 플러그인은 4.5.4 헤더 기준으로 컴파일되어 있다. cv::Mat 레이아웃이 달라 쓰레기 크기를 읽고 할당에 실패한다.

조치 — cv_bridge와 compressed_image_transport를 설치된 apt 버전과 동일한 태그로 소스를 받아 OpenCV 4.8.0으로 재빌드하고 오버레이에 올렸다. /opt/ros 원본은 건드리지 않았다.

cd ~/ros2_ws/src
git clone --branch humble https://github.com/ros-perception/vision_opencv.git
git clone --branch humble https://github.com/ros-perception/image_transport_plugins.git
cd vision_opencv && git checkout 3.2.1              # apt cv_bridge 3.2.1 과 일치
cd ../image_transport_plugins && git checkout 2.5.5  # apt 2.5.5 와 일치

cd ~/ros2_ws
colcon build --packages-select cv_bridge compressed_image_transport \
  --cmake-args -DCMAKE_BUILD_TYPE=Release -DOpenCV_DIR=/usr/lib/cmake/opencv4
sudo systemctl restart realsense

svc_realsense.sh가 이미 ~/ros2_ws/install/setup.bash를 source하므로 재시작만으로 적용된다.

원복이 필요하면 ~/ros2_ws/install/{cv_bridge,compressed_image_transport}를 지우고 realsense를 재시작한다.

ros2_ws에서 다른 패키지를 새로 빌드할 때 같은 함정에 빠질 수 있다. apt로 깔린 ROS 바이너리는 Ubuntu OpenCV 4.5.4 기준이고, 이 장비의 dev 헤더는 JetPack 4.8.0이다. OpenCV를 쓰는 노드를 로컬 빌드하면 둘이 섞인다.

jpeg_quality 파라미터가 반영되지 않음 (미해결)

camera.color.image_raw.jpeg_quality를 바꿔도 출력 크기가 그대로다.

param 95 → 99465 B
param 40 → 97989 B
param 30 → 100386 B    (구독을 끊었다 다시 붙여도 동일)

같은 프레임을 Jetson에서 cv2로 직접 인코딩하면 q95 95.6 KB / q50 22.6 KB / q10 9.6 KB가 나온다. 즉 플러그인이 항상 q95로 인코딩하고 파라미터는 선언만 되어 있다. compressed_image_transport 2.5.5 자체 문제로 보인다.

원격 대역폭을 줄이려면 이 파라미터에 기대지 말고 §2의 프리뷰 노드 방식을 쓴다.


6. 참고 — SPOT 본체와 센서는 별개다

LiDAR와 카메라는 Jetson에 직결되어 있어서 SPOT 로봇 본체가 꺼져 있어도 정상 동작한다. 실제로 로봇이 18시간 넘게 응답 없는 상태에서도 두 센서는 정상 발행 중이었다.

로봇 본체 연결이 끊기면 이렇게 보인다:

spot-state-ro 로그:  ProxyConnectionError: The proxy on the robot could not be reached.
ip neigh dev eno1:   192.168.77.72  INCOMPLETE      ← ARP 응답 없음

INCOMPLETE는 로봇이 L2에서 아예 안 보인다는 뜻이다. 소프트웨어가 아니라 전원 또는 이더넷 연결을 확인해야 한다. 서브넷은 192.168.77.x이고 Jetson이 eno1에서 DHCP 서버(spot-dhcp) 역할을 한다.

Clone this wiki locally