CHAPTER 6 3 / 10

비전 가이드 로봇

Eye-in-Hand, Eye-to-Hand 구성과 빈 피킹 AI 시스템

1비전 가이드 로봇의 개요

비전 가이드 로봇(Vision Guided Robot, VGR)은 카메라 기반 시각 정보를 활용하여 작업 대상의 위치와 자세를 인식하고, 이를 기반으로 로봇 동작을 수행하는 시스템입니다. 고정된 지그(jig) 없이도 다양한 제품을 핸들링할 수 있어 유연 생산에 필수적입니다.

2카메라 구성: Eye-in-Hand vs Eye-to-Hand

비전 가이드 로봇의 카메라 위치에 따라 시스템 특성이 크게 달라집니다.

구분 Eye-in-Hand Eye-to-Hand (Fixed)
카메라 위치 로봇 엔드 이펙터에 장착 작업 공간 고정 위치
장점 근접 촬영으로 고정밀, 좁은 공간 대응 넓은 시야, 빠른 인식, 캘리브 단순
단점 Hand-Eye 캘리브 필요, 모션 중 블러 원거리 해상도 한계, 차폐 문제
적용 예 정밀 조립, PCB 삽입 빈 피킹, 컨베이어 피킹

최신 시스템은 두 방식을 결합한 Hybrid 구성을 사용합니다. Fixed 카메라로 대략적 위치를 파악하고, Eye-in-Hand 카메라로 최종 정밀 보정을 수행합니다.

3Hand-Eye Calibration

Eye-in-Hand 시스템에서 카메라 좌표계와 로봇 좌표계의 관계를 정확히 알아내는 것이 Hand-Eye Calibration입니다. AX=XB 형태의 행렬 방정식을 풀어야 합니다.

import numpy as np from scipy.spatial.transform import Rotation class HandEyeCalibrator: """AX=XB 방정식 기반 Hand-Eye Calibration""" def __init__(self): self.A_matrices = [] # 로봇 그리퍼 포즈 변환 self.B_matrices = [] # 카메라에서 본 캘리브 패턴 변환 def add_measurement(self, robot_pose1, robot_pose2, pattern_pose1, pattern_pose2): """두 위치에서의 측정값 추가""" A = np.linalg.inv(robot_pose2) @ robot_pose1 B = pattern_pose2 @ np.linalg.inv(pattern_pose1) self.A_matrices.append(A) self.B_matrices.append(B) def solve_tsai_lenz(self): """Tsai-Lenz 알고리즘으로 X 계산""" # 회전 부분 먼저 해결 n = len(self.A_matrices) M = np.zeros((3 * n, 3)) b = np.zeros(3 * n) for i, (A, B) in enumerate(zip(self.A_matrices, self.B_matrices)): Ra, Rb = A[:3, :3], B[:3, :3] # Rodrigues 벡터로 변환 alpha = self._rotation_to_rodrigues(Ra) beta = self._rotation_to_rodrigues(Rb) # skew-symmetric 행렬 구성 M[3*i:3*(i+1), :] = self._skew(alpha + beta) b[3*i:3*(i+1)] = beta - alpha # 최소제곱 해 Pcg_prime = np.linalg.lstsq(M, b, rcond=None)[0] Pcg = 2 * Pcg_prime / np.sqrt(1 + np.dot(Pcg_prime, Pcg_prime)) Rcg = self._rodrigues_to_rotation(Pcg) # 이동 부분 해결 C = np.zeros((3 * n, 3)) d = np.zeros(3 * n) for i, (A, B) in enumerate(zip(self.A_matrices, self.B_matrices)): Ra, ta = A[:3, :3], A[:3, 3] tb = B[:3, 3] C[3*i:3*(i+1), :] = Ra - np.eye(3) d[3*i:3*(i+1)] = Rcg @ tb - ta tcg = np.linalg.lstsq(C, d, rcond=None)[0] # 4x4 변환 행렬 구성 X = np.eye(4) X[:3, :3] = Rcg X[:3, 3] = tcg return X def _skew(self, v): return np.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]]) def _rotation_to_rodrigues(self, R): r = Rotation.from_matrix(R) return r.as_rotvec() def _rodrigues_to_rotation(self, rvec): r = Rotation.from_rotvec(rvec) return r.as_matrix()

4빈 피킹 AI 시스템

빈 피킹(Bin Picking)은 무작위로 쌓인 부품들 중에서 하나를 인식하고 충돌 없이 집어내는 가장 도전적인 로봇 응용입니다. 3D 비전과 AI가 결합되어야 합니다.

Bin Picking AI Pipeline
RGB-D
Camera
Point
Cloud
Instance
Segmentation
6D Pose
Estimation
Grasp Pose Generation
Grasp Evaluation Matrix
Force Closure
40%
Accessibility
25%
Collision
Pass/Fail
Stability
20%
Total Score
0~100
Optimal Grasp Execution
Motion Planning + Pick
import torch import torch.nn as nn from typing import List, Tuple class BinPickingPipeline: """빈 피킹 AI 파이프라인""" def __init__(self, point_cloud_model, pose_estimator, grasp_planner, collision_checker): self.segmentation = point_cloud_model self.pose_estimator = pose_estimator self.grasp_planner = grasp_planner self.collision_checker = collision_checker def process(self, depth_image, rgb_image=None) -> List[GraspCandidate]: """ 전체 빈 피킹 프로세스 Returns: 충돌 없는 그래스프 후보 리스트 (점수 순) """ # 1. 포인트 클라우드 생성 point_cloud = self._depth_to_pointcloud(depth_image) # 2. 인스턴스 세그멘테이션 (개별 물체 분리) instances = self.segmentation.segment_instances(point_cloud) grasp_candidates = [] for instance in instances: # 3. 6D 포즈 추정 pose = self.pose_estimator.estimate(instance.points) # 4. 그래스프 포즈 생성 grasps = self.grasp_planner.plan_grasps( instance.points, object_pose=pose ) # 5. 충돌 검사 및 필터링 for grasp in grasps: if self._check_feasibility(grasp, point_cloud): grasp_candidates.append(GraspCandidate( pose=grasp, score=self._compute_grasp_score(grasp, instance), object_id=instance.id )) # 점수 기준 정렬 return sorted(grasp_candidates, key=lambda x: x.score, reverse=True) def _check_feasibility(self, grasp, scene_pointcloud) -> bool: """그래스프 실행 가능성 검사""" # 접근 경로 충돌 검사 approach_path = self._generate_approach_path(grasp) for waypoint in approach_path: if self.collision_checker.check_collision(waypoint, scene_pointcloud): return False return True def _compute_grasp_score(self, grasp, instance) -> float: """그래스프 품질 점수 계산""" # Force closure 점수 force_closure = self._analyze_force_closure(grasp, instance) # 접근 용이성 점수 (위에서 집는 것이 유리) approach_score = grasp.approach_vector[2] # Z 방향 성분 # 안정성 점수 stability = self._estimate_stability(grasp, instance) return 0.4 * force_closure + 0.3 * approach_score + 0.3 * stability class GraspPoseGenerator(nn.Module): """딥러닝 기반 그래스프 포즈 생성기 (6-DoF GraspNet 변형)""" def __init__(self, num_points=1024): super().__init__() self.num_points = num_points # PointNet++ 백본 self.sa1 = PointNetSetAbstraction( npoint=512, radius=0.02, nsample=32, in_channel=3, mlp=[64, 64, 128] ) self.sa2 = PointNetSetAbstraction( npoint=128, radius=0.04, nsample=64, in_channel=128, mlp=[128, 128, 256] ) self.sa3 = PointNetSetAbstraction( npoint=None, radius=None, nsample=None, in_channel=256, mlp=[256, 512, 1024] ) # 그래스프 예측 헤드 self.grasp_head = nn.Sequential( nn.Linear(1024, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, 7 + 1) # 쿼터니언(4) + 위치(3) + 성공확률(1) ) def forward(self, xyz): """ Args: xyz: (B, N, 3) 포인트 클라우드 Returns: grasps: (B, num_grasps, 8) 그래스프 파라미터 """ B, N, _ = xyz.shape # 포인트 클라우드 인코딩 l1_xyz, l1_points = self.sa1(xyz, None) l2_xyz, l2_points = self.sa2(l1_xyz, l1_points) l3_xyz, l3_points = self.sa3(l2_xyz, l2_points) # 글로벌 피처 global_feat = l3_points.squeeze(-1) # (B, 1024) # 그래스프 예측 grasp_params = self.grasp_head(global_feat) return grasp_params

5컨베이어 트래킹

이동 중인 컨베이어 위의 물체를 추적하면서 피킹하는 기술입니다. 비전 인식 결과와 컨베이어 엔코더 값을 실시간으로 융합해야 합니다.

import time from dataclasses import dataclass from collections import deque @dataclass class TrackedObject: """추적 중인 물체 정보""" id: int initial_position: np.ndarray # 최초 감지 위치 detection_time: float # 감지 시각 encoder_at_detection: float # 감지 시점 엔코더 값 pose: np.ndarray # 6D 포즈 confidence: float class ConveyorTracker: """컨베이어 트래킹 시스템""" def __init__(self, conveyor_speed: float, # mm/s conveyor_direction: np.ndarray, # 이동 방향 벡터 pick_zone_start: float, # 피킹 영역 시작점 pick_zone_end: float): # 피킹 영역 끝점 self.speed = conveyor_speed self.direction = conveyor_direction / np.linalg.norm(conveyor_direction) self.pick_zone = (pick_zone_start, pick_zone_end) self.tracked_objects = {} self.encoder_position = 0.0 self.last_update_time = time.time() def update_encoder(self, encoder_value: float): """엔코더 값 업데이트""" self.encoder_position = encoder_value self.last_update_time = time.time() def add_detection(self, object_id: int, position: np.ndarray, pose: np.ndarray, confidence: float): """새로운 물체 감지 추가""" self.tracked_objects[object_id] = TrackedObject( id=object_id, initial_position=position.copy(), detection_time=time.time(), encoder_at_detection=self.encoder_position, pose=pose, confidence=confidence ) def get_current_position(self, object_id: int) -> np.ndarray: """특정 물체의 현재 위치 계산""" if object_id not in self.tracked_objects: return None obj = self.tracked_objects[object_id] # 엔코더 기반 이동 거리 계산 encoder_delta = self.encoder_position - obj.encoder_at_detection # 현재 위치 = 초기 위치 + 이동 벡터 current_pos = obj.initial_position + encoder_delta * self.direction return current_pos def get_pickable_objects(self) -> List[Tuple[int, np.ndarray, float]]: """피킹 가능 영역 내의 물체 목록""" pickable = [] for obj_id, obj in self.tracked_objects.items(): current_pos = self.get_current_position(obj_id) if current_pos is None: continue # 컨베이어 방향 좌표 conveyor_coord = np.dot(current_pos, self.direction) # 피킹 영역 체크 if self.pick_zone[0] <= conveyor_coord <= self.pick_zone[1]: # 피킹 우선순위: 영역 끝에 가까울수록 높음 priority = (conveyor_coord - self.pick_zone[0]) / \ (self.pick_zone[1] - self.pick_zone[0]) pickable.append((obj_id, current_pos, priority)) # 우선순위 순 정렬 return sorted(pickable, key=lambda x: x[2], reverse=True) def predict_pick_timing(self, object_id: int, robot_reach_time: float) -> np.ndarray: """로봇 도달 시점의 물체 위치 예측""" current_pos = self.get_current_position(object_id) if current_pos is None: return None # 로봇이 도달할 때까지 물체 이동 거리 travel_distance = self.speed * robot_reach_time / 1000.0 # 예측 위치 predicted_pos = current_pos + travel_distance * self.direction return predicted_pos

컨베이어 트래킹에서 지연(Latency) 보상이 핵심입니다. 비전 처리 시간, 통신 지연, 로봇 반응 시간을 모두 고려하여 예측 위치를 계산해야 합니다.

6비전 가이드 로봇 시스템 통합

전체 비전 가이드 로봇 시스템의 아키텍처와 통합 방법입니다.

from enum import Enum from abc import ABC, abstractmethod class VGRState(Enum): IDLE = "idle" SEARCHING = "searching" APPROACHING = "approaching" GRASPING = "grasping" RETREATING = "retreating" PLACING = "placing" ERROR = "error" class VisionGuidedRobot: """비전 가이드 로봇 통합 시스템""" def __init__(self, robot_controller, vision_system, gripper, config: dict): self.robot = robot_controller self.vision = vision_system self.gripper = gripper self.config = config self.state = VGRState.IDLE self.current_target = None self.cycle_count = 0 self.success_count = 0 async def run_pick_cycle(self, source_bin, target_position) -> bool: """ 단일 피킹 사이클 실행 Returns: 성공 여부 """ try: # 1. 비전 위치로 이동 self.state = VGRState.SEARCHING await self.robot.move_to(self.config['vision_position']) # 2. 물체 인식 및 그래스프 계획 grasp = await self._find_best_grasp(source_bin) if grasp is None: print("No graspable object found") return False self.current_target = grasp # 3. 접근 self.state = VGRState.APPROACHING approach_pose = self._compute_approach_pose(grasp.pose) await self.robot.move_to(approach_pose) # 4. 그리퍼 열기 및 최종 접근 await self.gripper.open() await self.robot.move_linear(grasp.pose) # 5. 그래스핑 self.state = VGRState.GRASPING await self.gripper.close() await asyncio.sleep(0.1) # 안정화 대기 # 그래스프 확인 if not await self.gripper.has_object(): print("Grasp failed - no object detected") await self.gripper.open() return False # 6. 후퇴 self.state = VGRState.RETREATING await self.robot.move_linear(approach_pose) # 7. 배치 위치로 이동 self.state = VGRState.PLACING await self.robot.move_to(target_position) # 8. 배치 await self.gripper.open() # 통계 업데이트 self.cycle_count += 1 self.success_count += 1 self.state = VGRState.IDLE return True except Exception as e: self.state = VGRState.ERROR print(f"Pick cycle error: {e}") return False async def _find_best_grasp(self, bin_id) -> GraspCandidate: """최적 그래스프 찾기""" # 이미지 획득 rgb, depth = await self.vision.capture() # 빈 피킹 처리 candidates = self.vision.bin_picking_pipeline.process(depth, rgb) # 실행 가능한 최상위 후보 반환 for candidate in candidates: if await self._verify_reachability(candidate): return candidate return None async def _verify_reachability(self, grasp: GraspCandidate) -> bool: """로봇 도달 가능성 검증""" # 역기구학 해 존재 확인 ik_solution = self.robot.compute_ik(grasp.pose) if ik_solution is None: return False # 관절 한계 검사 if not self.robot.check_joint_limits(ik_solution): return False # 충돌 검사 if self.robot.check_collision(ik_solution): return False return True def _compute_approach_pose(self, grasp_pose: np.ndarray, distance: float = 50.0) -> np.ndarray: """그래스프 전 접근 포즈 계산""" approach_pose = grasp_pose.copy() # Z 방향으로 후퇴 approach_pose[:3, 3] -= distance * grasp_pose[:3, 2] return approach_pose def get_statistics(self) -> dict: """성능 통계""" return { 'total_cycles': self.cycle_count, 'successful_picks': self.success_count, 'success_rate': self.success_count / max(1, self.cycle_count), 'current_state': self.state.value }

7실전 적용 사례

비전 가이드 로봇의 대표적인 산업 적용 사례들입니다.

응용 분야 비전 구성 핵심 기술 정밀도
전자 부품 빈 피킹 3D 스테레오 + Eye-to-Hand 포인트 클라우드 세그멘테이션 ±0.5mm
자동차 부품 로딩 2.5D 구조광 CAD 매칭, 홀 검출 ±0.3mm
식품 피킹 RGB + AI 분류 품질 분류, 색상 검사 ±2mm
의료기기 조립 Hybrid (Fixed + Eye-in-Hand) 마이크로 정밀 보정 ±0.05mm

비전 가이드 로봇 도입 시 조명 환경이 가장 중요합니다. 일정한 조명을 유지하거나, 조명 변화에 강건한 딥러닝 모델을 사용하세요.