Phase 2-2까지 하고 로드맵을 다시 봤더니, Phase 2 카드에 "센서(RGB-D) 시뮬레이션 · ROS2 ↔ 시뮬레이터 브릿지"가 아직 안 끝난 채로 남아있었다. 굳이 되돌아가지 않고, 어차피 Phase 3(인지)를 시작하려면 제일 먼저 필요한 게 카메라라서 이번 세션 첫 단계로 자연스럽게 흡수시켰다. 오늘 한 세션에서 카메라 브릿지(3-1)랑 YOLO 인식(3-2)까지 이어서 했다.
① 손목에 카메라 달기 (eye-in-hand)
카메라를 세상에 고정시키는 대신, 팔 끝(hand_link)에 고정 조인트로 바로 붙였다. 실제 로봇팔이 물건을 집을 때 흔히 쓰는 방식이라고 한다 — 팔이 어디로 움직이든 "손이 보는 시야"를 카메라가 그대로 보게 된다.
Gazebo의 sensor type="depth" + libgazebo_ros_camera.so 플러그인 조합을 썼다. 예전 Gazebo9의 Kinect 스타일 플러그인(libgazebo_ros_openni_kinect.so) 역할을 ROS2 포트에서는 이 플러그인이 흡수했다고 한다. 컬러/깊이/포인트클라우드가 한 번에 나온다.
/hand_camera/image_raw 컬러 이미지
/hand_camera/depth/image_raw 깊이 이미지
/hand_camera/depth/points 포인트클라우드
camera_bridge_check라는 노드를 만들어서, 이 이미지가 진짜 계속 들어오는지(shape/평균 밝기)를 확인했다. 검정 화면도 랜덤 노이즈도 아니고 안정적인 값이 계속 찍혀서, 카메라 → ROS2 브릿지가 살아있다는 걸 확인했다.
② RViz2엔 왜 방/테이블이 안 보이나
처음엔 팔의 기본 자세(관절 0°, 0°)에서 카메라가 가까운 벽만 보고 있어서 화면이 회색 두 칸(위: 벽, 아래: 바닥)으로만 나왔다. 그 김에 헷갈렸던 걸 하나 정리했다 — RViz2 메인 3D 뷰에는 방 벽/테이블이 아예 안 보인다. room.world에 넣은 벽/테이블은 Gazebo 월드(SDF)에 직접 넣은 static 모델이라 /robot_description(URDF)에는 없고, RViz2는 ROS2로 발행되는 것만 그리지 .world 파일 자체는 읽지 않기 때문이다. 반대로 카메라 이미지엔 벽/테이블이 나오는데, 이건 그 이미지가 RViz가 그리는 게 아니라 Gazebo 렌더러가 실제 월드를 찍어서 토픽으로 보내주기 때문이다.
③ 팔 각도 계산해서 테이블 조준하기
room.world의 테이블이 정확히 (x=1.2, y=1.2)에 있어서, 원점(팔 회전축)에서 보면 딱 45° 대각선 방향이다. 두 관절(어깨/팔꿈치)이 전부 Z축 회전이라 팔꿈치를 0°로 펴두면, 어깨만 45°로 돌렸을 때 곧게 뻗은 팔+카메라가 정확히 그 대각선 위에 놓여서 테이블을 정면으로 보게 된다.
ros2 topic pub --once /joint_trajectory_controller/joint_trajectory \
trajectory_msgs/msg/JointTrajectory \
"{joint_names: ['shoulder_joint','elbow_joint'], points: [{positions: [0.785398, 0.0], time_from_start: {sec: 3}}]}"
계산대로 화면 정중앙에 테이블이 잡혔다.

(참고로 두 관절 다 Z축 회전이라, 카메라는 항상 바닥에서 약 0.1m 높이에서 순수 수평으로만 본다. 테이블이 마침 바닥~0.4m 높이라 그 안에 걸쳐 보인 거고, 카메라를 위아래로 기울이려면 팔에 다른 축의 회전 관절이 있어야 한다.)
④ YOLO 인식 노드 붙이기
여기서부터 3-2. camera_bridge_check의 콜백(이미지 받아서 shape만 로그로 찍던 부분)을 YOLOv8n(ultralytics, COCO 80종 사전학습) 추론으로 바꿨다. 결과는 두 가지로 발행한다.
- /hand_camera/detections: JSON 문자열로 [{"class": "cup", "confidence": 0.83, "bbox": [...]}] 형태
- /hand_camera/detections_image: 바운딩박스 그려진 디버그 이미지 (RViz2에 새 디스플레이로 추가)
표준 메시지 타입인 vision_msgs 대신 std_msgs/String(JSON)을 쓴 건, vision_msgs의 필드 구성이 버전(3.x/4.x)마다 꽤 달라서 지금 설치된 버전을 확인 없이 맞추면 빌드가 깨질 위험이 있었기 때문이다. 우선 확실히 동작하는 방식으로 가고, 나중에(Phase 4에서 좌표를 더 정교하게 다뤄야 할 때) 정식 타입으로 바꾸는 걸 고려하기로 했다.
⑤ numpy 버전 충돌 두 번 겪기
pip install ultralytics를 하고 나니 cv_bridge가 AttributeError: _ARRAY_API not found로 깨졌다. ultralytics가 numpy를 2.x로 업그레이드시켰는데, ROS2 Humble의 cv_bridge는 numpy 1.x C API에 맞춰 컴파일된 바이너리라서 생긴 문제였다.
pip install "numpy<2"
numpy를 1.26.4로 내려서 해결했는데, 이번엔 반대 방향 충돌이 났다. ultralytics가 같이 설치한 pip판 opencv-python이 numpy>=2를 요구한다는 경고가 뜬 것. 시스템에 이미 ROS(apt)용 python3-opencv가 cv_bridge와 궁합 맞게 깔려있어서, pip판을 지우고 시스템 버전을 쓰도록 정리했다.
pip uninstall -y opencv-python opencv-python-headless
교훈: 같은 파이썬 환경에 ROS(apt)가 설치한 라이브러리와 pip이 설치한 라이브러리가 섞이면 numpy 버전 기대치가 어긋나기 쉽다. "먼저 있었고 잘 동작하던 쪽"을 기준으로 잡고, 나중에 들어온 pip 패키지 쪽을 정리하는 방향이 안전했다.
⑥ Gazebo 상자는 왜 안 잡히나 — 모델 자체를 따로 검증
문제를 다 해결하고 실행했는데, room.world의 테이블(밋밋한 갈색 박스)엔 계속 "탐지된 물체 없음"만 떴다. 이게 아직 남은 설치 문제인지, 아니면 "상자가 실제 사진처럼 안 생겨서"인지 구분하려고, ROS2/Gazebo와 완전히 분리된 검증 스크립트(yolo_sanity_check.py, colcon build 불필요)를 만들어서 ultralytics 표준 샘플 사진(버스+사람들)에 같은 모델을 돌려봤다.

버스(0.87), 사람 4명(0.87/0.85/0.83/0.26), 정지 표지판(0.26)까지 정확히 잡아냈다 — 잘 보이는 건 확신 높게, 가려지거나 애매한 건 확신 낮게 잡는 전형적인 YOLO 패턴 그대로였다. 즉 모델/설치는 완전히 정상이고, Gazebo 상자가 안 잡히는 건 버그가 아니라 실제 사진으로 학습된 모델 입장에서 그 상자가 COCO의 어떤 물체와도 안 닮아서 생기는, 예상됐던 결과(sim-to-real 격차)였다.
덤으로 이 실행에서 CUDA initialization: driver too old 경고를 봤다. GPU 드라이버가 오래돼서 가속이 안 되고 CPU로 도는 중이고, 프레임당 추론에 10초 가까이 걸렸다. 지금 단계엔 문제없지만 나중에 실시간 반응이 필요해지면 짚어야 할 부분으로 남겨뒀다.
다음 편 (Phase 4) 예고
인지(카메라 + YOLO) 기본기는 여기까지. 다음은 MoveIt2로 실제 조작(그리핑) 단계로 넘어간다.
'로봇 > ROS2 공부' 카테고리의 다른 글
| [ROS2 공부 Phase 2-2] 방 world 만들고, RViz2+Gazebo 동시 확인하고, 계속 움직여보기 (0) | 2026.08.29 |
|---|---|
| [ROS2 공부 Phase 2-1] Gazebo 시뮬레이션 첫걸음 — 용어 정리 + 삽질 기록 (0) | 2026.08.25 |
| [ROS2 공부 Phase 1] 토픽(Topic) 통신으로 로봇 팔 자세 뿌려보기 (0) | 2026.08.25 |













