Eye-in-Hand, Eye-to-Hand 구성과 빈 피킹 AI 시스템
비전 가이드 로봇(Vision Guided Robot, VGR)은 카메라 기반 시각 정보를 활용하여 작업 대상의 위치와 자세를 인식하고, 이를 기반으로 로봇 동작을 수행하는 시스템입니다. 고정된 지그(jig) 없이도 다양한 제품을 핸들링할 수 있어 유연 생산에 필수적입니다.
비전 가이드 로봇의 카메라 위치에 따라 시스템 특성이 크게 달라집니다.
| 구분 | Eye-in-Hand | Eye-to-Hand (Fixed) |
|---|---|---|
| 카메라 위치 | 로봇 엔드 이펙터에 장착 | 작업 공간 고정 위치 |
| 장점 | 근접 촬영으로 고정밀, 좁은 공간 대응 | 넓은 시야, 빠른 인식, 캘리브 단순 |
| 단점 | Hand-Eye 캘리브 필요, 모션 중 블러 | 원거리 해상도 한계, 차폐 문제 |
| 적용 예 | 정밀 조립, PCB 삽입 | 빈 피킹, 컨베이어 피킹 |
최신 시스템은 두 방식을 결합한 Hybrid 구성을 사용합니다. Fixed 카메라로 대략적 위치를 파악하고, Eye-in-Hand 카메라로 최종 정밀 보정을 수행합니다.
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()빈 피킹(Bin Picking)은 무작위로 쌓인 부품들 중에서 하나를 인식하고 충돌 없이 집어내는 가장 도전적인 로봇 응용입니다. 3D 비전과 AI가 결합되어야 합니다.
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이동 중인 컨베이어 위의 물체를 추적하면서 피킹하는 기술입니다. 비전 인식 결과와 컨베이어 엔코더 값을 실시간으로 융합해야 합니다.
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) 보상이 핵심입니다. 비전 처리 시간, 통신 지연, 로봇 반응 시간을 모두 고려하여 예측 위치를 계산해야 합니다.
전체 비전 가이드 로봇 시스템의 아키텍처와 통합 방법입니다.
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
}비전 가이드 로봇의 대표적인 산업 적용 사례들입니다.
| 응용 분야 | 비전 구성 | 핵심 기술 | 정밀도 |
|---|---|---|---|
| 전자 부품 빈 피킹 | 3D 스테레오 + Eye-to-Hand | 포인트 클라우드 세그멘테이션 | ±0.5mm |
| 자동차 부품 로딩 | 2.5D 구조광 | CAD 매칭, 홀 검출 | ±0.3mm |
| 식품 피킹 | RGB + AI 분류 | 품질 분류, 색상 검사 | ±2mm |
| 의료기기 조립 | Hybrid (Fixed + Eye-in-Hand) | 마이크로 정밀 보정 | ±0.05mm |
비전 가이드 로봇 도입 시 조명 환경이 가장 중요합니다. 일정한 조명을 유지하거나, 조명 변화에 강건한 딥러닝 모델을 사용하세요.