이 문서는 ROS2, 로봇 모델링, MoveIt, 컴퓨터비전이 하나의 로봇 작업 파이프라인으로 연결되는 과정을 정리한다. CV는 이미지에서 물체, 영역, 특징을 찾지만, 로봇이 행동하려면 그 결과가 depth, camera intrinsic, hand-eye calibration, TF를 거쳐 robot base 좌표계의 3D 위치로 바뀌어야 한다.
실습 코드는 camera color/depth/camera_info topic을 subscribe하고, pixel에서 depth를 읽고, camera 좌표를 base 좌표로 변환한 뒤 MoveIt pick-and-place로 넘기는 흐름을 보여 준다. Click-to-pick과 bar sorting은 image topic -> detection/click -> depth lookup -> 3D projection -> base transform -> planning -> gripper로 이어지는 대표 pipeline이다. 이 문서는 앞의 개념들을 실제 로봇 행동으로 묶는 통합 단계로 보면 된다.
키워드: Robot-CV pipeline, perception, RGB image, depth image, CameraInfo, intrinsic, pixel coordinate, depth lookup, point cloud, hand-eye calibration, TF, camera frame, base frame, detection, segmentation, click-to-pick, bar sorting, MoveIt, planning, gripper, pick-and-place, ROS2 topic
컴퓨터비전 수업은 개별 알고리즘을 따로 외우는 방식으로 이해하면 흐름이 끊긴다. 이미지 표현과 전처리에서 시작해 segmentation, morphology, feature matching, CNN, augmentation, object detection, semantic segmentation, foundation model로 이어지는 순서를 잡아야 각 개념이 왜 등장했는지 보인다.
초반 영상처리 개념은 픽셀과 필터, threshold, morphology처럼 사람이 설계한 규칙 기반 방법을 다룬다. 중반에는 feature extraction과 matching을 통해 이미지 사이의 대응 관계와 기하 변환을 이해한다. 후반에는 CNN과 딥러닝 모델을 통해 데이터로부터 feature를 학습하는 방식으로 넘어간다. 마지막으로 YOLO, U-Net, ViT, CLIP은 detection, segmentation, Transformer, multimodal foundation model로 확장된다.
고전 영상처리는 이미지의 픽셀 값과 지역 패턴을 사람이 설계한 연산으로 처리한다. Histogram equalization, thresholding, erosion, dilation, SIFT, ORB 같은 방법이 여기에 속한다. 이 방법들은 데이터가 적거나 문제 구조가 명확할 때 해석 가능성이 높다.
딥러닝 기반 CV는 feature를 사람이 직접 설계하지 않고 데이터로부터 학습한다. CNN은 convolution filter를 학습하고, YOLO는 detection을 end-to-end로 수행하며, U-Net은 픽셀 단위 segmentation을 학습한다. ViT와 CLIP은 이미지를 patch sequence나 이미지-텍스트 임베딩으로 확장한다.
CV는 로봇에게 주변 세계를 읽어 주는 perception 역할을 한다. Segmentation은 작업 영역과 물체 영역을 나누고, YOLO는 작업 대상 후보를 찾고, feature matching은 시점 변화나 기하 관계를 추정하며, CLIP은 언어 지시와 시각 정보를 연결하는 방향으로 확장된다.
이 결과들은 ROS2 topic으로 전달되고, TF와 depth 정보를 통해 로봇 좌표계로 변환되며, MoveIt planning scene이나 목표 pose로 연결된다. 따라서 CV 전체 흐름은 로봇 제어와 분리된 이미지 처리 과목이 아니라 지능형 로봇의 perception 기반으로 이해해야 한다.
컴퓨터비전 학습 흐름은 픽셀 기반 전처리에서 시작해 feature, CNN, detection, segmentation, multimodal 인식으로 확장되며, 로봇 perception의 기반이 된다.
로봇 수업과 CV 수업은 따로 떨어진 과목처럼 보일 수 있지만, 실제 지능형 로봇 시스템에서는 강하게 연결된다. CV는 로봇이 주변 세계를 인식하게 해 주고, ROS2는 그 인식 결과를 시스템 안에서 전달하며, MoveIt과 controller는 인식 결과를 실제 로봇 행동으로 바꾼다.
따라서 CV를 단순 이미지 처리로만 이해하거나, ROS2/MoveIt을 단순 로봇 제어로만 이해하면 전체 흐름이 보이지 않는다. 로봇은 “보고, 해석하고, 계획하고, 움직이는” 시스템이다.
로봇 perception의 입력은 카메라 이미지, depth image, point cloud, LiDAR, IMU, contact sensor 등이다. CV 수업에서 다룬 이미지 전처리, segmentation, morphology, feature matching, YOLO, U-Net, ViT, CLIP은 이 입력에서 의미 있는 정보를 추출하기 위한 방법들이다.
카메라 이미지는 2D 픽셀 좌표계에 있다. 로봇이 행동하려면 이 정보를 3D 공간과 로봇 좌표계로 연결해야 한다. 따라서 camera calibration, depth, TF 변환이 필요하다.
YOLO가 이미지에서 물체의 bounding box와 class를 예측하면, 로봇은 작업 대상 후보를 얻는다. 하지만 bounding box만으로 로봇이 물체를 잡을 수는 없다. Bounding box 중심은 이미지 좌표일 뿐이다. 이를 실제 물체 위치로 바꾸려면 depth 정보나 pose estimation이 필요하고, camera frame에서 robot base frame으로 변환해야 한다.
이렇게 얻은 물체 pose는 MoveIt의 목표 pose가 될 수 있다. 또는 물체가 장애물이라면 planning scene의 collision object로 추가될 수 있다. 이때 MoveIt은 collision checking을 수행하며 로봇이 안전하게 움직일 trajectory를 만든다.
Segmentation은 bounding box보다 더 정밀한 영역 정보를 제공한다. 물체의 실제 mask를 알면 물체 윤곽, 작업 가능 영역, 장애물 영역을 더 정확히 추정할 수 있다. Morphology는 segmentation mask의 작은 noise를 제거하고 구멍을 메워 더 안정적인 mask를 만든다.
이 mask 정보는 depth와 결합되어 3D obstacle이나 object region으로 변환될 수 있다. 이후 planning scene에 반영하면 MoveIt은 해당 영역을 피하거나 목표 대상으로 사용할 수 있다.
ROS2는 perception 결과를 시스템 안에서 전달한다. Camera node는 image topic을 publish하고, perception node는 image를 subscribe해 detection result나 segmentation mask를 publish한다. Planning node는 이 결과를 subscribe해 목표 pose나 collision object를 만든다.
Service는 특정 inference 요청이나 상태 조회에 사용할 수 있고, action은 perception 기반 작업 목표를 장기 수행으로 관리할 수 있다. 예를 들어 “서랍을 찾아 접근하고 열기” 같은 작업은 detection, pose estimation, planning, control이 모두 포함된 action 구조로 생각할 수 있다.
Gazebo는 로봇 모델과 controller가 물리 환경에서 동작하는지 검증하는 데 강하다. Isaac Sim은 카메라, LiDAR, IMU, contact sensor 같은 센서를 포함한 시뮬레이션과 pick-and-place 흐름을 검증하는 데 유용하다.
Isaac Sim 카메라 실습은 3D world point와 2D image coordinate의 관계를 보여 준다. 이 관계는 CV detection 결과를 로봇 좌표계로 연결하는 데 필요하다. Pick-and-place 실습은 물체 인식, 목표 위치 설정, gripper 제어, trajectory 실행이 하나로 이어져야 manipulation이 된다는 점을 보여 준다.
로봇과 CV의 연결은 다음 순서로 이해할 수 있다.
Robot-CV 연결 실습 정리
Depth viewer, calibration, YOLO/segmentation, MoveIt, pick-and-place 실습을 연결하면 Robot-CV pipeline의 전체 흐름이 보인다. CV는 image에서 객체 영역이나 bounding box를 찾는다. Depth camera는 해당 pixel의 depth를 제공한다. Camera intrinsic은 pixel과 depth를 camera 좌표계의 3D point로 바꾼다. Hand-eye calibration과 TF는 camera 좌표를 robot base 좌표로 바꾼다. MoveIt은 그 좌표를 목표 pose로 받아 접근/후퇴 경로를 계획한다.
Camera image와 depth image는 계속 들어오는 stream이므로 topic이다. 객체 검출 결과도 frame마다 publish되는 topic이 될 수 있다. 특정 물체의 3D pose를 계산해 달라는 요청은 service로 만들 수 있다. 로봇을 pick pose까지 움직이고 grasp sequence를 수행하는 작업은 시간이 걸리고 취소가 필요하므로 action 또는 task node가 적합하다.
Smart shop의 order_manager는 재고, 할인, 결제 service를 순서대로 호출하고 결과를 log topic으로 publish한다. 로봇에서도 이와 비슷하게 task manager가 perception service, calibration/TF 변환, MoveIt planning, gripper command, log/monitoring을 조합한다. 즉 ROS2 통신 실습은 쇼핑 예제에 머물지 않고, 복잡한 로봇 작업 orchestration을 이해하는 기초가 된다.
실제 pick-and-place pipeline은 보통 image topic -> detection/segmentation -> depth lookup -> camera coordinate -> robot base coordinate -> safe workspace clamp -> MoveIt planning -> controller execution -> gripper close/open -> result/log 순서로 이어진다. 각 단계가 분리되어야 어느 부분이 실패했는지 찾을 수 있다. Detection이 틀린 것인지, depth가 0인지, calibration이 틀린 것인지, planning scene collision인지, controller 실행 문제인지 구분해야 한다.
CV는 로봇에게 세계를 읽어 주고, ROS2와 TF는 그 정보를 전달하고 좌표계에 맞추며, MoveIt과 controller는 그 인식 결과를 실제 로봇 행동으로 바꾼다.
Robot-CV 연결은 하나의 거대한 함수가 아니라 topic, perception, depth projection, planning이 단계적으로 이어지는 구조다.
self.image_sub = self.create_subscription(Image, '/camera/color/image_raw', self.image_callback, 10)
self.depth_sub = self.create_subscription(Image, '/camera/depth/image_rect_raw', self.depth_callback, 10)
self.camera_info_sub = self.create_subscription(CameraInfo, '/camera/color/camera_info', self.info_callback, 10)
이 세 subscription은 perception pipeline의 입력 계층이다. Image만 있으면 class나 mask는 만들 수 있지만 실제 거리 정보가 부족하다. Depth와 camera info가 함께 있어야 image coordinate를 3D coordinate로 바꿀 수 있다.
results = self.detector(frame)
center_u = int((box.x1 + box.x2) / 2)
center_v = int((box.y1 + box.y2) / 2)
point_camera = self.project_depth_to_3d(center_u, center_v, depth)
Detection 결과의 box center는 robot pose가 아니다. project_depth_to_3d 같은 단계가 있어야 pixel과 depth가 camera frame의 point로 바뀐다. 그 다음 TF 변환을 거쳐 robot base frame의 목표 위치가 된다.
planning_component.set_goal_state(pose_stamped_msg=pose_goal, pose_link=EE_LINK)
plan_result = planning_component.plan(parameters=plan_parameters)
robot.execute(group_name=GROUP_NAME, robot_trajectory=plan_result.trajectory, blocking=True)
MoveIt 단계는 perception 결과를 실제 robot motion으로 바꾸는 곳이다. 여기서 실패하면 detection이 맞아도 로봇은 움직일 수 없다. 그래서 CV 학습 흐름은 image processing에서 끝나지 않고 depth, TF, MoveIt, controller까지 이어져야 로봇 과제로 완성된다.
Click-to-pick 코드: camera topic에서 MoveIt 실행까지 이어지는 파이프라인
click_pick_node.py는 사용자가 영상에서 한 점을 클릭했을 때, 그 pixel이 실제 로봇 좌표와 pick 동작으로 바뀌는 전체 흐름을 보여 준다. 입력은 세 topic에서 들어온다.
self.create_subscription(
CameraInfo, "/camera/camera/color/camera_info",
self._cam_info_cb, 10)
self.create_subscription(
Image, "/camera/camera/color/image_raw",
self._color_cb, 10)
self.create_subscription(
Image, "/camera/camera/aligned_depth_to_color/image_raw",
self._depth_cb, 10)
def _cam_info_cb(self, msg):
self.intrinsics = {
"fx": msg.k[0], "fy": msg.k[4],
"ppx": msg.k[2], "ppy": msg.k[5],
}
def _color_cb(self, msg):
self.color_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="bgr8")
def _depth_cb(self, msg):
self.depth_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="passthrough")
여기서 color image는 OpenCV 창에 표시되는 입력이고, depth image는 클릭한 pixel의 거리이며, camera info는 pixel을 3D ray로 복원하는 내부 파라미터다. rclpy.spin_once()가 반복문 안에서 돌기 때문에 이 세 callback이 계속 실행되고 최신 frame이 node 내부 변수에 저장된다.
while rclpy.ok():
rclpy.spin_once(self, timeout_sec=0.01)
if self.color_image is None:
continue
cv2.imshow(window, self.color_image)
사용자가 화면을 클릭하면 pixel 좌표 (x, y)에서 depth를 읽고, intrinsic을 이용해 camera frame의 3D point를 만든다.
z_raw = self.depth_image[y, x]
if z_raw == 0:
self.get_logger().warn("해당 픽셀 depth=0")
return
z_m = float(z_raw) / 1000.0 if self.depth_image.dtype == np.uint16 else float(z_raw)
fx, fy = self.intrinsics["fx"], self.intrinsics["fy"]
ppx, ppy = self.intrinsics["ppx"], self.intrinsics["ppy"]
cam_x = (x - ppx) * z_m / fx
cam_y = (y - ppy) * z_m / fy
cam_z = z_m
이 단계의 출력은 아직 robot base가 아니라 camera 좌표계의 점이다. 그래서 hand-eye calibration 결과인 gripper2cam과 현재 end-effector pose를 이용해 base 좌표로 바꾼다.
def transform_to_base(self, cam_xyz_m):
coord = np.append(np.array(cam_xyz_m, dtype=float), 1.0)
base2ee = get_ee_matrix(self.robot)
base2cam = base2ee @ self.gripper2cam
return (base2cam @ coord)[:3]
base = self.transform_to_base((cam_x, cam_y, cam_z))
bx, by, bz = float(base[0]), float(base[1]), float(base[2])
self.pick_and_place(bx, by, bz)
따라서 이 실습의 파이프라인은 camera topics -> callback 저장 -> OpenCV 클릭 -> depth lookup -> camera 3D point -> hand-eye transform -> base 좌표 -> pick_and_place()다. “이미지를 클릭했다”는 사용자 입력은 바로 robot command가 아니라, depth와 calibration을 거쳐 로봇이 이해할 수 있는 좌표로 바뀐 뒤 MoveIt 계획으로 넘어간다.
Bar sorting 코드: 검출, depth 분류, 좌표 변환, 순차 pick-and-place
bar_sort_node.py는 click-to-pick보다 한 단계 더 자동화된 흐름이다. 먼저 color image에서 막대 후보를 검출하고, 각 contour 중심 pixel에서 depth를 샘플링해 길이/거리 기준으로 분류한다.
bars, thresh = detect_bars(self.color_image, log)
if not bars:
log.error("contour 없음 — 중단")
return
for b in bars:
cx, cy = b["pixel"]
d_mm = self.sample_depth_mm(cx, cy)
if d_mm is None:
continue
name, rank = classify_depth_mm(d_mm)
Depth는 한 pixel만 읽으면 노이즈나 0 값에 흔들릴 수 있으므로, 주변 5x5 patch의 median을 사용한다.
x0, x1 = max(0, px - 2), min(w, px + 3)
y0, y1 = max(0, py - 2), min(h, py + 3)
patch = self.depth_image[y0:y1, x0:x1]
valid = patch[patch > 0]
if valid.size == 0:
return None
z = float(np.median(valid))
분류된 대상은 다시 pixel에서 base 좌표로 변환된다.
base = self.pixel_to_base(cx, cy)
if base is None:
log.error(f"{t['name']} 좌표 변환 실패 — 중단")
return
bx, by, bz = float(base[0]), float(base[1]), float(base[2])
ok = self.pick_and_place(bx, by, bz, PLACE_POSITIONS[rank])
이 흐름은 color/depth/camera_info topic -> contour 검출 -> depth median -> SHORT/MEDIUM/LONG 분류 -> pixel_to_base -> MoveIt pick_and_place -> gripper open/close로 이어진다. 여기서 CV는 막대 후보와 중심 pixel을 만들고, depth와 calibration은 그 pixel을 3D base 좌표로 바꾸며, MoveIt과 gripper는 그 좌표를 실제 작업으로 실행한다.
dsr_practice/setup.py에는 Robot-CV와 manipulation 실습을 실행하기 위한 console script가 등록되어 있다.
entry_points={
'console_scripts': [
'click_pick_node = dsr_practice.click_pick_node:main',
'bar_sort_node = dsr_practice.bar_sort_node:main',
'stt_node = dsr_practice.stt_node:main',
'stt_robot_control = dsr_practice.stt_robot_control:main',
'stt_pick_and_place = dsr_practice.stt_pick_and_place:main',
],
}
Cmd에서 click_pick_node를 실행하면 node는 camera topic을 subscribe하고 OpenCV 창을 띄운다. 사용자가 창에서 pixel을 클릭하면 callback이 depth를 읽고 base 좌표로 변환한 뒤 MoveIt pick sequence를 실행한다. bar_sort_node는 사용자가 한 점을 클릭하는 대신 color image에서 막대 contour를 찾고, depth로 분류한 뒤 순서대로 pick-and-place를 수행한다.
ros2 run dsr_practice click_pick_node
ros2 run dsr_practice bar_sort_node
ros2 topic echo /camera/camera/color/image_raw
ros2 topic echo /camera/camera/aligned_depth_to_color/image_raw
이때 topic echo는 실제 image message가 매우 크므로 장시간 보기보다 “topic이 들어오는지 확인하는 용도”로 이해하는 것이 좋다. 핵심은 cmd에서 node 실행을 확인하고, 코드에서는 subscription callback과 spin_once()가 최신 RGB/depth/camera info를 받아 pick pipeline으로 넘기는 구조를 읽는 것이다.
일반 RGB 카메라는 이미지의 색과 밝기 정보를 제공하지만, 로봇이 물체를 잡거나 장애물을 피하려면 3D 위치 정보가 필요하다. Depth 카메라는 각 픽셀이 카메라로부터 얼마나 떨어져 있는지 나타내는 깊이 정보를 제공한다.
Depth 정보가 있으면 2D 이미지에서 검출한 물체 위치를 3D 공간 위치로 바꿀 수 있다. 이 과정은 YOLO나 segmentation 결과를 로봇 motion planning으로 연결하는 데 핵심이다.
RGB 이미지는 물체의 색상과 외형을 보여 준다. Depth 이미지는 각 픽셀의 거리 값을 담는다. Depth와 camera intrinsic을 이용하면 각 픽셀을 3D point로 변환할 수 있고, 이 점들의 집합이 point cloud가 된다.
Point cloud는 로봇 주변의 3D 구조를 나타낸다. 물체 표면, 장애물, 작업대 높이, grasp 대상의 대략적인 위치를 추정하는 데 사용될 수 있다.
Intrinsic parameter는 카메라 자체의 특성을 나타낸다. 초점 거리, 주점, 렌즈 왜곡 계수 등이 포함된다. Intrinsic이 있어야 이미지 픽셀 좌표를 카메라 좌표계의 ray로 바꿀 수 있다.
렌즈 왜곡이 있으면 직선이 휘어 보이거나 위치 계산이 틀어질 수 있다. 따라서 카메라 모델에서는 pinhole 모델이나 fisheye 모델과 함께 distortion parameter를 다룬다.
Extrinsic parameter는 카메라가 로봇 기준 어디에 장착되어 있는지 나타낸다. 로봇이 카메라로 본 물체를 잡으려면 카메라 좌표계의 3D point를 로봇 base 좌표계로 변환해야 한다.
Hand-Eye Calibration은 카메라와 로봇 사이의 변환 관계를 구하는 과정이다. 카메라가 로봇 손목에 붙어 있거나 외부에 고정되어 있을 때, 체커보드 같은 기준 target을 여러 자세에서 관측해 변환 행렬을 추정한다.
캘리브레이션 실습에서는 체커보드를 준비하고, 로봇을 여러 자세로 이동시키며 카메라 이미지를 수집한다. 이때 로봇 pose와 카메라에서 본 target pose를 함께 기록해야 한다. 충분히 다양한 자세에서 데이터를 모아야 변환 행렬을 안정적으로 계산할 수 있다.
Depth 카메라를 사용할 때는 장치 번호, pixel format, colorspace도 확인해야 한다. 잘못된 device를 사용하면 이미지가 들어오지 않거나 depth/RGB stream이 맞지 않을 수 있다.
Depth 카메라와 캘리브레이션은 CV 결과를 로봇 행동으로 바꾸는 중간 다리다. YOLO가 bounding box를 찾고, depth가 해당 위치의 거리를 제공하며, calibration과 TF가 그 점을 robot base frame으로 변환한다. 그 결과가 MoveIt의 목표 pose나 planning scene object가 된다.
Depth 카메라와 캘리브레이션은 2D 이미지 인식 결과를 로봇이 사용할 수 있는 3D 위치 정보로 바꾸는 과정이다.
depth_pkg/depth_viewer.py, image_viewer.py, depth_click_viewer.py는 RealSense 같은 depth camera topic을 ROS2 node에서 받아 OpenCV로 확인하는 흐름을 보여 준다. Color image는 /camera/camera/color/image_raw, depth image는 /camera/camera/depth/image_rect_raw, camera info는 /camera/camera/depth/camera_info에서 들어온다.
depth_click_viewer.py는 클릭한 픽셀의 depth를 읽고 camera intrinsic으로 3D camera coordinate를 계산한다. CameraInfo.k에서 fx, fy, cx, cy를 꺼내고, depth raw 값을 m 단위 Z로 바꾼 뒤 X = (u - cx) * Z / fx, Y = (v - cy) * Z / fy를 계산한다. 이 식이 중요한 이유는 이미지 픽셀 좌표가 로봇이 사용할 수 있는 3D 위치로 바뀌는 첫 단계이기 때문이다.
Depth 값만 있으면 물체까지의 거리 Z는 알 수 있지만, 픽셀이 카메라 좌표계에서 어느 방향 ray에 해당하는지는 알 수 없다. fx, fy, cx, cy가 있어야 픽셀 (u, v)를 카메라 좌표계의 (X, Y, Z)로 복원할 수 있다. 따라서 depth topic과 camera info topic은 함께 해석해야 한다.
Calibration_Tutorial/data_recording.py는 로봇 현재 pose와 카메라 이미지를 함께 저장한다. 코드에서는 Doosan robot node를 초기화하고 get_current_posx()로 현재 로봇 pose를 얻은 뒤, OpenCV VideoCapture로 받은 frame을 파일로 저장한다. 동시에 이미지 파일 이름과 pose 목록을 calibrate_data.json에 기록한다.
이 구조가 필요한 이유는 hand-eye calibration이 “이미지 여러 장”만으로 풀리는 문제가 아니기 때문이다. 각 이미지가 촬영된 순간의 robot pose와, 이미지 안에서 보이는 calibration target pose가 함께 있어야 카메라 좌표계와 로봇 좌표계 사이의 extrinsic transform을 추정할 수 있다. 이미지와 robot pose가 서로 다른 시점에 저장되면 calibration 결과가 흔들린다.
handeye_calibration.py, eye2hand_calibration.py, realsense.py, test.py는 카메라와 로봇 사이의 변환을 계산하고 검증하는 흐름에 해당한다. Eye-in-hand라면 카메라가 robot end-effector에 붙어 있고, eye-to-hand라면 카메라가 외부 고정 위치에 있다. 두 경우 모두 목표는 카메라가 본 3D 점을 robot base 기준 좌표로 바꾸는 변환을 얻는 것이다.
Depth viewer에서 얻은 (X, Y, Z)는 아직 카메라 좌표계의 점이다. 로봇이 움직이려면 이 점을 camera frame에서 tool frame 또는 base frame으로 변환해야 한다. 이때 intrinsic calibration은 픽셀을 카메라 3D 좌표로 바꾸고, hand-eye calibration은 카메라 3D 좌표를 로봇 좌표로 바꾼다. 그래서 depth camera calibration은 CV와 MoveIt 사이의 연결부다.
Depth 실습의 핵심은 color image, depth image, camera info를 따로 보지 않고 같은 perception 문제의 세 입력으로 묶는 것이다.
self.color_sub = self.create_subscription(Image, COLOR_IMAGE_TOPIC, self.color_image_callback, 10)
self.depth_sub = self.create_subscription(Image, DEPTH_IMAGE_TOPIC, self.depth_image_callback, 10)
self.camera_info_sub = self.create_subscription(CameraInfo, CAMERA_INFO_TOPIC, self.camera_info_callback, 10)
Color image는 사람이 보는 장면이고, depth image는 각 pixel의 거리이며, CameraInfo는 pixel 좌표를 3D camera 좌표로 바꾸는 내부 파라미터다. 세 topic을 함께 받아야 “이미지에서 본 물체”를 “공간상의 점”으로 바꿀 수 있다.
self.fx = msg.k[0]
self.fy = msg.k[4]
self.cx = msg.k[2]
self.cy = msg.k[5]
fx, fy, cx, cy는 camera intrinsic matrix에서 나온다. cx, cy는 optical center이고, fx, fy는 pixel 단위 초점거리다. 이 값이 빠지면 같은 pixel 위치라도 실제 3D 방향을 계산할 수 없다.
Z = float(depth_raw) / 1000.0
X = (u_depth - self.cx) * Z / self.fx
Y = (v_depth - self.cy) * Z / self.fy
이 세 줄이 depth camera와 robot motion planning을 연결한다. YOLO의 bounding box 중심이나 segmentation mask 중심은 처음에는 (u, v) pixel일 뿐이다. Depth가 Z를 주고 intrinsic이 X, Y를 계산하면 camera frame의 3D point가 된다. 이후 TF와 hand-eye calibration으로 robot base frame으로 변환해야 MoveIt 목표 pose나 collision object 위치로 사용할 수 있다.
로봇에서 위치는 숫자 세 개로만 표현되지 않는다. 항상 “어떤 좌표계를 기준으로 한 위치인가”가 함께 필요하다. 카메라가 본 물체의 위치, 로봇 base에서 본 end-effector의 위치, world에서 본 로봇의 위치는 모두 서로 다른 frame을 기준으로 한다.
TF는 이 frame들 사이의 관계를 시간에 따라 관리하는 ROS의 좌표 변환 시스템이다.
Frame은 좌표계의 이름이다. 로봇에는 보통 base_link, link_1, link_2, tool0, camera_link, map, odom, world 같은 frame이 있다. 각 frame은 다른 frame에 대해 위치와 회전 관계를 가진다.
카메라가 로봇 팔 끝에 붙어 있다면 camera_link는 end-effector frame에 대해 고정된 transform을 가진다. 로봇 팔이 움직이면 end-effector frame이 바뀌고, 카메라 frame도 함께 움직인다.
TF를 이해해야 하는 이유는 perception과 control이 모두 좌표 변환에 의존하기 때문이다. 카메라 이미지에서 물체를 찾았다고 해서 로봇이 바로 잡을 수 있는 것은 아니다. 이미지 좌표를 카메라 좌표로, 카메라 좌표를 로봇 base 좌표로, 로봇 base 좌표를 planning에 사용할 목표 pose로 변환해야 한다.
이 과정에서 frame 관계가 틀리면 로봇은 엉뚱한 곳으로 움직인다. 따라서 TF는 단순 보조 개념이 아니라 로봇 perception과 motion planning을 연결하는 핵심이다.
고정된 센서 장착 위치처럼 변하지 않는 관계는 static transform으로 publish할 수 있다. 로봇 관절 움직임에 따라 계속 변하는 링크 관계는 robot state publisher와 joint state 정보를 통해 동적으로 계산된다.
URDF는 링크와 조인트 구조를 정의하고, TF는 그 구조가 현재 관절 상태에서 어떤 좌표 관계를 가지는지 알려 준다.
카메라로 물체를 검출하면 처음 얻는 정보는 이미지 좌표계에 있다. 이 좌표는 픽셀 위치이므로 로봇이 바로 사용할 수 없다. Depth 정보나 카메라 모델을 사용해 카메라 좌표계의 3D 위치로 바꾸고, TF를 통해 robot base frame이나 world frame으로 변환해야 한다.
이 변환이 맞아야 MoveIt이 목표 pose를 올바르게 해석할 수 있다. 예를 들어 카메라가 로봇 팔 끝에 붙어 있다면 카메라 frame은 end-effector frame과 연결되어야 하고, end-effector frame은 로봇의 link chain을 통해 base frame과 연결되어야 한다.
TF가 끊기면 RViz에서 sensor data가 표시되지 않거나, fixed frame 오류가 발생할 수 있다. Frame 이름이 잘못되면 데이터가 엉뚱한 위치에 나타난다. Transform 방향을 반대로 이해하면 물체 위치가 뒤집히거나 로봇이 목표와 반대 방향으로 움직일 수 있다.
그래서 TF는 로봇 시스템에서 조용하지만 매우 중요한 기반이다. perception, visualization, planning이 모두 TF 위에서 좌표를 공유한다.
RViz에서 TF tree를 확인하는 것은 매우 중요하다. TF tree가 끊겨 있거나 frame 이름이 잘못되어 있으면, 로봇 모델이 제대로 표시되지 않거나 sensor data가 올바른 위치에 나타나지 않는다.
TF 문제는 겉으로 보기에는 perception 오류나 planning 오류처럼 보일 수 있지만, 실제 원인은 좌표계 연결이 잘못된 경우가 많다.
TF는 로봇, 센서, 물체가 서로 어떤 좌표 관계에 있는지 연결해 주는 시스템이다.