드디어 문장이 로봇팔의 행동이 되는 순간
지금까지 우리는 로봇팔이 물체를 보는 방법, 좌표를 계산하는 방법, 경로를 계획하는 방법, 실패를 감지하고 다시 시도하는 방법을 하나씩 배웠다. 이번 장에서는 그 조각들을 하나의 프로젝트로 묶는다.
목표는 단순하게 들린다.
"빨간 컵을 집어 오른쪽에 놓아줘."
사람에게는 쉬운 말이다. 하지만 로봇에게는 이 짧은 문장 안에 여러 문제가 숨어 있다. 빨간 컵이 무엇인지 찾아야 하고, 그 컵이 어디에 있는지 알아야 하며, 손가락처럼 생긴 그리퍼로 어디를 잡을지 정해야 한다. 그리고 "오른쪽"이 어느 방향인지 정한 뒤, 충돌하지 않게 움직이고, 실제로 컵이 옮겨졌는지 확인해야 한다.
이 장의 프로젝트는 자연어 명령을 바로 모터 신호로 바꾸는 시스템이 아니다. 오히려 그 반대다. 자연어는 계획으로 바꾸고, 계획은 검증 가능한 스킬 호출로 바꾸며, 실제 로봇팔 제어는 MoveIt2와 안전 제어 계층이 담당한다. LLM은 똑똑한 지휘자이고, 로봇 제어기는 악기를 직접 연주하는 연주자다. 지휘자가 바이올린 현을 손으로 누르지 않는 것처럼, LLM도 관절 각도를 직접 내보내지 않는다.
이번 장에서 만들 시스템의 전체 흐름은 다음과 같다.
사용자의 자연어 명령에서 대상 물체, 속성, 놓을 위치를 추출한다.
카메라 이미지에서 조건에 맞는 물체를 찾고, 세그멘테이션으로 물체 영역을 분리한다.
깊이 정보와 좌표 변환을 이용해 물체의 3D 위치를 로봇 기준 좌표로 바꾼다.
집기 자세와 놓기 자세를 만들고 MoveIt2 목표 자세로 전달한다.
pick,move,place,verify스킬을 순서대로 호출한다.실패하면 원인을 구분하고, 가능한 경우 다시 계획한다.
이 프로젝트를 완성하면 "컵을 집어 오른쪽에 놓기"는 하나의 데모가 아니라, 자연어 로봇팔 에이전트의 핵심 뼈대가 된다. 컵을 블록으로 바꾸고, 오른쪽을 바구니 안으로 바꾸고, 빨간색을 파란색으로 바꾸어도 같은 구조가 유지된다.

프로젝트의 큰 그림
먼저 시스템을 사람의 작업 방식과 비교해 보자. 사람이 "빨간 컵을 집어 오른쪽에 놓아줘"라는 말을 들으면 머릿속에서 대략 이런 일이 일어난다.
"빨간 컵"이라는 대상을 찾고, 그것이 손이 닿는 위치에 있는지 확인한다. 컵을 잡을 만한 부분을 고른다. 컵을 들어 올린다. 오른쪽의 빈 공간을 확인한다. 그곳에 컵을 내려놓는다. 마지막으로 컵이 제대로 놓였는지 본다.
로봇도 같은 구조를 가진다. 다만 사람의 직관을 여러 모듈로 나누어 명시적으로 구현한다.
| 단계 | 모듈 | 역할 |
|---|---|---|
| 명령 이해 | LLM 또는 명령 파서 | 대상, 색상, 동작, 위치 조건 추출 |
| 장면 인식 | 객체 검출, 세그멘테이션 | 컵 후보와 물체 영역 찾기 |
| 위치 계산 | RGB-D, 좌표 변환 | 픽셀 위치를 로봇 기준 3D 좌표로 변환 |
| 자세 생성 | Grasp Planner | 접근 자세, 집기 자세, 들어올리기 자세 생성 |
| 실행 | MoveIt2, 그리퍼 제어 | 충돌 없는 경로 계획과 동작 수행 |
| 검증 | 비전 검증기, 상태 확인 | 목표 위치에 물체가 있는지 확인 |
이 표에서 중요한 점은 각 모듈이 서로 약속된 형식으로 데이터를 주고받는다는 것이다. 자연어 처리 결과가 아무 문장이나 반환되면 안 된다. 비전 모듈은 "컵이 보입니다"라고 말하는 대신, 컵의 위치와 신뢰도, 마스크, 깊이 값을 구조화해 전달해야 한다. 실행 모듈도 "안 됐어요"라고만 말하면 부족하다. 어느 단계에서 왜 실패했는지 알려 주어야 다음 행동을 정할 수 있다.
그래서 이 프로젝트에서는 모든 단계가 작고 명확한 계약을 가진다. 좋은 로봇 에이전트는 거대한 하나의 모델이 아니라, 서로 잘 맞물린 약속들의 집합이다.
자연어 명령에서 작업 의도 추출하기
사용자의 문장은 짧지만, 로봇에게 필요한 정보는 꽤 많다.
"빨간 컵을 집어 오른쪽에 놓아줘"에서 뽑아야 할 핵심은 다음과 같다.
| 항목 | 추출 결과 |
|---|---|
| 동작 | pick_and_place |
| 대상 물체 | cup |
| 대상 조건 | color = red |
| 놓을 위치 | right_side |
| 주의 조건 | 명시 없음 |
| 기준 물체 | 명시 없음 |
여기서 "오른쪽"은 생각보다 위험한 표현이다. 사용자가 보는 화면의 오른쪽일 수도 있고, 로봇 기준 오른쪽일 수도 있고, 작업대 위의 오른쪽 영역일 수도 있다. 프로젝트에서는 처음부터 기준을 정해 두어야 한다.
이 장에서는 가장 단순한 규칙을 사용하겠다. "오른쪽"은 로봇 베이스 좌표계 기준 오른쪽 영역을 뜻한다. 예를 들어 로봇 베이스 좌표계에서 x는 앞쪽, y는 왼쪽, z는 위쪽이라고 정하면, 오른쪽은 -y 방향이다. 실제 로봇마다 좌표축 정의가 다를 수 있으므로, 이 값은 코드에 직접 박아 넣지 말고 설정 파일로 분리하는 것이 좋다.
명령 해석 결과는 다음과 같은 구조로 만들 수 있다.
task_type: pick_and_place
target:
category: cup
attributes:
color: red
place:
type: workspace_region
region: right
constraints:
speed: normal
safety: defaultLLM을 사용한다면 자유로운 문장을 이 구조로 바꾸게 할 수 있다. 하지만 중요한 규칙이 있다. LLM이 마음대로 새로운 스킬 이름을 만들게 해서는 안 된다. 사용할 수 있는 스킬 목록은 미리 고정해야 한다. 예를 들어 이 프로젝트의 허용 스킬은 detect_object, pick, place, verify, stop뿐이라고 정한다.
LLM이 만들어야 하는 계획은 이런 형태다.
plan:
- skill: detect_object
args:
category: cup
color: red
- skill: pick
args:
target_id: from_previous_result
- skill: place
args:
region: right
- skill: verify
args:
condition: target_in_right_region이 계획은 아직 실행 명령이 아니다. 실행 전에 디스패처가 검사한다. 존재하지 않는 스킬이 있는지, 인자가 빠졌는지, 작업 영역 밖으로 이동하려는지, 안전 정책에 어긋나는지 확인한다. 지난 장에서 말한 안전한 에이전트 설계는 바로 여기에서 힘을 발휘한다.
"빨간 컵"을 찾는 비전 파이프라인
명령을 이해했으면 이제 카메라를 본다. 로봇 입장에서 "빨간 컵"은 이미지 속 픽셀 묶음이다. 비전 모듈의 임무는 그 픽셀 묶음을 찾아 실제 위치로 바꿀 준비를 하는 것이다.
일반적인 흐름은 객체 검출과 세그멘테이션을 함께 쓰는 방식이다. 객체 검출은 "이 근처에 컵이 있다"는 바운딩 박스를 준다. 세그멘테이션은 "이 픽셀들이 컵이다"라는 마스크를 준다. 컵을 집을 때는 바운딩 박스 중심만으로 부족할 때가 많다. 손잡이, 그림자, 배경이 박스 안에 섞이기 때문이다. 그래서 최종 집기 후보는 가능하면 마스크 기반으로 계산한다.
비전 파이프라인은 다음 순서로 구성할 수 있다.
RGB 이미지와 Depth 이미지를 동시에 가져온다.
객체 검출 모델로
cup후보를 찾는다.각 컵 후보에 대해 세그멘테이션 마스크를 얻는다.
마스크 내부 픽셀의 색상 분포를 확인해 빨간색 조건을 검사한다.
조건을 만족하는 후보 중 신뢰도와 위치 안정성이 가장 좋은 컵을 선택한다.
선택된 마스크에서 중심점과 집기 후보 영역을 계산한다.
빨간색 판정은 두 가지 방식으로 할 수 있다. 첫 번째는 모델이 색상 속성까지 함께 예측하게 하는 방식이다. 두 번째는 컵 마스크 내부의 픽셀 색상을 직접 분석하는 방식이다. 실습 프로젝트에서는 두 번째가 이해하기 쉽다. 객체 검출 모델은 컵을 찾고, 색상 조건은 OpenCV의 HSV 색상 공간에서 검사한다.
HSV는 색을 색상, 채도, 밝기로 나누어 표현하는 방식이다. RGB보다 "빨간색 계열"을 잡아내기 편하다. 다만 조명에 따라 색이 흔들리므로, 빨간색 범위를 너무 좁게 잡으면 실제 빨간 컵을 놓칠 수 있다. 반대로 너무 넓게 잡으면 갈색 책상이나 주황색 물체가 빨간색으로 오인될 수 있다.
간단한 판정 로직은 다음과 같이 생각할 수 있다.
def is_red_object(rgb_image, mask):
hsv = convert_rgb_to_hsv(rgb_image)
pixels = hsv[mask > 0]
red_pixels = pixels_in_red_range(pixels)
ratio = len(red_pixels) / max(len(pixels), 1)
return ratio > 0.35여기서 0.35는 정답이 아니다. 작업 환경의 조명, 컵의 재질, 카메라 자동 노출에 따라 바뀐다. 좋은 프로젝트는 이 값을 설정 파일로 빼고, 실험 로그를 보며 조정한다.
객체 후보를 선택할 때는 단순히 검출 신뢰도만 보지 않는 것이 좋다. 로봇팔이 실제로 집을 수 있어야 하기 때문이다. 예를 들어 이미지 가장자리에 걸린 컵, 깊이 값이 불안정한 컵, 다른 물체와 너무 가까운 컵은 좋은 후보가 아니다.
후보 점수는 다음처럼 만들 수 있다.
score = detection_confidence
+ color_score
+ depth_stability_score
+ graspability_score
- edge_penalty
- collision_risk_penalty처음부터 완벽한 점수 함수를 만들 필요는 없다. 중요한 것은 로봇이 왜 그 컵을 선택했는지 나중에 확인할 수 있어야 한다는 점이다. "빨간 컵 후보 3개 중 2번 컵을 선택했고, 이유는 색상 점수와 깊이 안정성이 가장 높았기 때문"이라고 로그에 남기면 디버깅이 쉬워진다.
마스크에서 집기 후보 만들기
컵을 찾았다고 바로 집을 수 있는 것은 아니다. 로봇팔은 컵 전체를 잡는 것이 아니라, 그리퍼가 닿을 몇 센티미터의 영역을 잡는다. 따라서 비전 결과에서 집기 후보를 만들어야 한다.
간단한 탁상 환경에서는 컵의 윗면 또는 옆면 중심을 기준으로 집기 자세를 만들 수 있다. 그리퍼가 위에서 내려오는 구조라면, 마스크의 중심점과 깊이 값을 이용해 컵 중심 근처를 목표로 삼는다. 하지만 실제 컵은 비어 있을 수도 있고, 손잡이가 있을 수도 있고, 표면이 미끄러울 수도 있다. 그래서 처음 프로젝트에서는 "성공 가능성이 높은 제한된 상황"을 정하는 것이 좋다.
이 장의 기본 가정은 다음과 같다.
컵은 작업대 위에 똑바로 서 있다.
그리퍼는 위에서 아래로 접근한다.
컵의 윗부분 또는 몸통 상단을 잡을 수 있다.
작업대에는 컵을 놓을 수 있는 오른쪽 빈 영역이 있다.
집기 후보를 만들 때 가장 먼저 계산할 값은 마스크 중심이다. 마스크에 포함된 픽셀들의 평균을 구하면 대략적인 중심 픽셀을 얻을 수 있다.
def mask_center(mask):
ys, xs = where(mask > 0)
u = mean(xs)
v = mean(ys)
return u, v하지만 평균 중심은 이상치에 약하다. 컵 그림자나 세그멘테이션 오류가 마스크에 섞이면 중심이 흔들린다. 조금 더 안정적으로 만들려면 마스크에서 가장 큰 연결 영역만 사용하고, 깊이 값도 중심 한 픽셀만 보지 말고 주변 작은 영역의 중앙값을 사용한다. 평균보다 중앙값이 튀는 값에 강하다.
def robust_depth(depth_image, u, v, window=5):
patch = depth_image[v-window:v+window+1, u-window:u+window+1]
valid = patch[(patch > 0.1) & (patch < 2.0)]
return median(valid)이렇게 얻은 픽셀 좌표와 깊이 값은 아직 카메라 기준 정보다. 로봇팔에게는 "이미지의 420번 픽셀, 260번 픽셀"이 아무 의미가 없다. 로봇팔은 자신의 베이스 좌표계에서 목표 위치를 알아야 한다.
픽셀 좌표를 로봇 좌표로 바꾸기
카메라 내부 파라미터를 알고 있으면 픽셀 좌표와 깊이 값으로 카메라 기준 3D 좌표를 계산할 수 있다. 이전 장에서 다룬 식을 다시 사용한다.
X = (u - cx) * Z / fx
Y = (v - cy) * Z / fy
Z = depth(u, v)여기서 u, v는 픽셀 좌표이고, fx, fy, cx, cy는 카메라 캘리브레이션으로 얻은 내부 파라미터다. 이렇게 계산한 점은 카메라 좌표계의 점이다.
다음으로 카메라 좌표를 로봇 베이스 좌표로 변환한다.
p_base = T_base_camera * p_cameraT_base_camera는 카메라 좌표계에서 로봇 베이스 좌표계로 가는 변환 행렬이다. ROS2 환경에서는 보통 TF를 통해 이 변환을 얻는다. 중요한 것은 이 변환이 정확해야 한다는 것이다. 카메라가 조금만 흔들려도 로봇팔 끝에서는 몇 센티미터의 오차가 생길 수 있다. 컵을 집는 작업에서 몇 센티미터는 성공과 실패를 가르는 큰 차이다.
컵 중심의 로봇 기준 좌표를 얻었다면, 곧바로 그 위치로 내려가면 안 된다. 먼저 컵 위쪽의 안전한 접근 위치로 이동한 다음, 천천히 내려가야 한다. 그래서 보통 세 가지 자세를 만든다.
| 자세 | 의미 |
|---|---|
pre_grasp_pose | 컵 위쪽의 접근 준비 자세 |
grasp_pose | 그리퍼가 컵을 잡는 자세 |
post_grasp_pose | 컵을 잡은 뒤 들어 올린 자세 |
예를 들어 위에서 내려잡는 방식이라면 pre_grasp_pose는 컵 중심보다 z 방향으로 10cm 높은 위치가 될 수 있다. grasp_pose는 컵 중심보다 약간 위쪽 또는 잡기 좋은 높이로 정한다. post_grasp_pose는 컵을 잡은 뒤 다시 위로 들어 올린 위치다.
def make_grasp_poses(cup_position):
grasp = Pose()
grasp.position = cup_position
grasp.position.z += 0.03
grasp.orientation = top_down_gripper_orientation()
pre_grasp = copy_pose(grasp)
pre_grasp.position.z += 0.10
post_grasp = copy_pose(grasp)
post_grasp.position.z += 0.15
return pre_grasp, grasp, post_grasp이 값들은 로봇마다 달라진다. 작은 그리퍼는 컵 몸통을 잡아야 할 수 있고, 흡착 그리퍼는 윗면 중심을 노릴 수 있다. 그래서 집기 자세 생성기는 반드시 로봇 하드웨어와 함께 조정해야 한다.
"오른쪽에 놓기"를 좌표로 정의하기
이제 컵을 어디에 놓을지 정해야 한다. "오른쪽"은 자연어지만, MoveIt2가 필요한 것은 목표 자세다. 따라서 오른쪽 영역을 명확한 좌표로 바꾸어야 한다.
가장 간단한 방법은 작업대 위에 미리 놓기 영역을 정의하는 것이다.
workspace_regions:
right:
center: [0.45, -0.25, 0.12]
size: [0.20, 0.20, 0.05]이 예에서 center는 로봇 베이스 좌표계 기준 오른쪽 놓기 영역의 중심이다. size는 해당 영역의 크기다. 로봇은 이 영역 안에서 충돌 위험이 낮고, 현재 비어 있는 지점을 찾아 컵을 내려놓는다.
조금 더 똑똑하게 만들고 싶다면, "오른쪽"을 현재 컵 위치 기준으로 해석할 수도 있다.
place_position = cup_position + right_direction * offset로봇 베이스 좌표계에서 오른쪽이 -y 방향이고, offset이 0.25m라면 컵의 현재 위치에서 오른쪽으로 25cm 이동한 지점이 놓기 후보가 된다. 다만 이 방식은 작업대 밖으로 나갈 위험이 있다. 반드시 작업 영역 경계 검사를 해야 한다.
실전에서는 두 방식을 섞는 것이 좋다. 사용자가 "오른쪽에 놓아줘"라고 하면 미리 정의된 오른쪽 영역을 사용한다. 사용자가 "노트북 오른쪽에 놓아줘"라고 하면 노트북을 기준 물체로 인식하고, 그 물체의 오른쪽에 상대 위치를 만든다. 이번 장에서는 첫 번째 방식, 즉 작업대 오른쪽 영역을 사용한다.
놓기 자세도 집기 자세처럼 세 단계로 나눈다.
| 자세 | 의미 |
|---|---|
pre_place_pose | 놓기 지점 위쪽의 접근 자세 |
place_pose | 컵을 내려놓는 자세 |
post_place_pose | 그리퍼를 연 뒤 위로 빠지는 자세 |
컵을 내려놓을 때는 집을 때보다 더 조심해야 한다. 너무 높은 곳에서 그리퍼를 열면 컵이 떨어지고, 너무 낮게 내려가면 컵이 작업대와 충돌하거나 기울어진다. 처음에는 실제 컵보다 가벼운 플라스틱 컵이나 블록으로 실험하는 것이 좋다.
스킬 인터페이스 설계
이 프로젝트의 핵심 스킬은 네 가지다.
| 스킬 | 입력 | 출력 |
|---|---|---|
detect_object | category, color | target_id, mask, pose 후보 |
pick | target_id 또는 grasp poses | 성공 여부, 실패 단계, 이유 |
place | region 또는 place pose | 성공 여부, 실패 단계, 이유 |
verify | 조건 | 성공 여부, 관측 결과 |
스킬은 내부적으로 복잡할 수 있지만, 외부 인터페이스는 단순해야 한다. 그래야 LLM 계획기와 디스패처가 안정적으로 사용할 수 있다.
예를 들어 pick 스킬은 내부적으로 그리퍼 열기, 접근 자세 이동, 집기 자세 이동, 그리퍼 닫기, 들어올리기, 물체 파지 확인을 수행한다. 하지만 상위 에이전트에는 다음처럼 요약된 결과를 반환한다.
success: false
stage: grasp
reason: empty_gripper
recoverable: true
suggestion: try_next_grasp_candidate이런 반환값이 있으면 상위 에이전트는 당황하지 않는다. "집기 단계에서 빈 그리퍼가 감지되었고, 회복 가능하니 다른 집기 후보로 다시 시도하자"라고 판단할 수 있다.
반대로 다음과 같은 실패는 즉시 정지해야 한다.
success: false
stage: approach
reason: collision_risk
recoverable: false
suggestion: stop_and_request_human_check실패를 단순한 실패로 보지 않고, 단계와 이유가 있는 사건으로 다루는 것이 로봇 에이전트를 안전하게 만든다.
실행 디스패처 만들기
디스패처는 계획과 실제 스킬 사이의 중간 관리자다. LLM이 만든 계획을 받아 검사하고, 순서대로 스킬을 호출하며, 각 스킬의 결과를 기록한다. 로봇 프로젝트에서는 이 디스패처가 매우 중요하다. LLM이 실수해도 디스패처가 막아야 하고, 스킬이 실패해도 디스패처가 재시도 여부를 판단해야 한다.
전체 흐름을 의사코드로 쓰면 다음과 같다.
def run_command(command_text):
task = parse_command(command_text)
plan = make_plan(task)
validate_plan(plan)
context = {}
for step in plan:
result = call_skill(step, context)
log_step(step, result)
if not result.success:
recovery = handle_failure(step, result, context)
if recovery == 'retry':
result = retry_skill(step, context)
elif recovery == 'replan':
plan = replan(task, context, result)
return run_plan(plan, context)
else:
call_skill({'skill': 'stop'}, context)
return result
context.update(result.outputs)
return context여기서 context는 작업 중간 정보를 담는 공간이다. detect_object가 찾은 컵의 target_id, 마스크, 3D 위치, 집기 후보는 context에 저장된다. 다음 단계인 pick은 이 정보를 사용한다. place는 현재 로봇이 물체를 들고 있다는 상태와 놓기 영역 정보를 사용한다. verify는 최종 이미지와 상태를 확인한다.
디스패처가 관리해야 할 상태는 적어도 다음과 같다.
현재 작업 ID와 원문 명령
선택된 대상 물체의 ID, 카테고리, 속성, 위치
사용한 집기 후보와 실패한 집기 후보 목록
로봇팔과 그리퍼의 현재 상태
각 스킬의 시작 시간, 종료 시간, 성공 여부, 실패 이유
최종 검증 결과
이 정보는 단순한 로그가 아니다. 로봇이 실패했을 때 다시 시도하기 위한 기억이다.
전체 실행 예시
이제 실제로 "빨간 컵을 집어 오른쪽에 놓아줘"라는 명령이 들어왔다고 해 보자.
첫 번째 단계에서 명령 파서는 작업을 구조화한다.
task_type: pick_and_place
target:
category: cup
color: red
place:
region: right두 번째 단계에서 비전 모듈이 장면을 본다. 카메라 이미지 안에는 빨간 컵 하나, 파란 컵 하나, 흰색 상자 하나가 있다. 객체 검출은 컵 후보 두 개를 찾는다. 세그멘테이션은 각 컵의 마스크를 만든다. 색상 검사 결과 첫 번째 컵의 빨간색 비율이 높다. 디스패처는 이 컵을 target_01로 선택한다.
selected_target:
id: target_01
category: cup
color_score: 0.82
detection_confidence: 0.91
position_base: [0.42, 0.08, 0.12]세 번째 단계에서 집기 자세 생성기가 target_01의 위치를 이용해 접근 자세, 집기 자세, 들어올리기 자세를 만든다. 이 자세들은 바로 실행되지 않고, 먼저 MoveIt2의 계획 대상이 된다. MoveIt2는 로봇팔이 해당 자세까지 충돌 없이 갈 수 있는 경로를 계산한다.
네 번째 단계에서 pick 스킬이 실행된다. 그리퍼를 열고, pre_grasp_pose로 이동한 뒤, grasp_pose로 천천히 내려간다. 그리퍼를 닫고, 힘 또는 위치 변화를 통해 물체가 잡혔는지 확인한다. 이후 post_grasp_pose로 들어 올린다.
다섯 번째 단계에서 place 스킬이 오른쪽 영역의 놓기 지점을 선택한다. 이때 놓기 지점이 비어 있는지 간단히 확인한다. 깊이 이미지에서 해당 영역에 예상보다 높은 물체가 있으면 다른 지점을 고르거나 재계획한다. 놓기 지점이 정해지면 pre_place_pose, place_pose, post_place_pose를 만들고 MoveIt2로 실행한다.
마지막 단계에서 verify 스킬이 카메라를 다시 본다. 빨간 컵이 오른쪽 영역 안에 있는지, 그리퍼가 비어 있는지 확인한다. 둘 다 참이면 작업은 성공이다.
final_result:
success: true
task: pick_and_place
target: red cup
placed_region: right
verification:
object_in_region: true
gripper_empty: true이 순간 사용자의 문장은 로봇팔의 실제 행동으로 완성된다.
검증은 선택이 아니라 필수다
로봇팔 프로젝트에서 초보자가 자주 하는 실수는 "명령을 보냈으니 성공했을 것"이라고 가정하는 것이다. 현실은 그렇지 않다. 로봇팔이 계획대로 움직였더라도 컵을 놓쳤을 수 있다. 컵을 들었지만 이동 중에 미끄러졌을 수 있다. 놓기 위치에 내려놓았지만 컵이 넘어졌을 수도 있다.
그래서 verify는 마지막 장식이 아니라 필수 단계다. 검증은 최소한 두 가지를 확인해야 한다. 첫째, 대상 물체가 목표 영역에 있는가. 둘째, 로봇의 내부 상태가 작업 완료와 일치하는가. 예를 들어 컵이 오른쪽 영역에 보이는데 그리퍼도 아직 닫혀 있다면, 로봇이 컵을 제대로 놓지 못했을 가능성이 있다.
비전 기반 검증은 다음처럼 구현할 수 있다.
def verify_red_cup_in_right_region(rgb, depth):
detections = detect_objects(rgb)
cups = filter_category(detections, 'cup')
red_cups = filter_red_by_mask(rgb, cups)
for cup in red_cups:
p_base = estimate_position_in_base(cup, depth)
if is_inside_region(p_base, 'right'):
return True
return False검증 결과가 실패하면 바로 같은 행동을 반복하면 안 된다. 실패 원인을 먼저 나누어야 한다.
| 검증 실패 상황 | 가능한 원인 | 대응 |
|---|---|---|
| 오른쪽 영역에 컵이 없음 | 집기 실패, 이동 중 낙하, 인식 실패 | 현재 장면 재인식 후 재계획 |
| 컵은 있지만 목표 영역 밖 | 놓기 좌표 오류, 좌표 변환 오차 | 보정 후 짧은 이동 계획 |
| 그리퍼가 비어 있지 않음 | 놓기 실패, 컵 끼임 | 안전하게 다시 놓기 시도 |
| 컵이 넘어짐 | 낮은 놓기 자세, 빠른 그리퍼 열림 | 사람 확인 요청 또는 복구 동작 |
검증이 있기 때문에 로봇은 "했다고 주장하는 기계"가 아니라 "했는지 확인하는 기계"가 된다. 이것이 자연어 로봇팔 에이전트와 단순 데모의 차이다.
실패했을 때 다시 생각하기
실패 처리는 최종 프로젝트의 완성도를 좌우한다. 한 번에 성공하는 로봇보다, 실패를 알아차리고 안전하게 다시 시도하는 로봇이 훨씬 믿음직하다.
예를 들어 pick 스킬이 empty_gripper를 반환했다고 하자. 이 말은 그리퍼가 닫혔지만 실제로 컵을 잡지 못했다는 뜻이다. 이때는 같은 위치를 그대로 반복하기보다, 다음 집기 후보를 선택해야 한다. 마스크에서 조금 다른 지점을 고르거나, 접근 높이를 낮추거나, 그리퍼 방향을 바꿀 수 있다.
failure:
skill: pick
stage: grasp
reason: empty_gripper
recoverable: true
next_action:
type: retry_pick
change:
grasp_candidate: next
approach_offset_z: -0.01반면 collision_risk가 발생했다면 이야기가 다르다. 충돌 위험은 재시도보다 정지가 먼저다. 특히 사람과 가까운 환경에서는 LLM이 "다른 방향으로 한번 가 보자"라고 말해도 디스패처가 막아야 한다. 안전 정책은 계획보다 우선한다.
재계획은 보통 다음 순서로 진행한다. 먼저 장면을 다시 인식한다. 물체 위치가 바뀌었는지 확인한다. 실패한 후보를 제외한다. 새로운 후보가 있으면 다시 시도한다. 그래도 실패하면 사람에게 도움을 요청한다. 로봇이 스스로 모든 문제를 해결해야 한다고 생각하지 않아도 된다. 좋은 로봇은 자신의 한계를 알고 멈출 줄 안다.
구현할 때의 패키지 구조
프로젝트가 커지면 파일을 잘 나누는 것이 중요하다. 모든 코드를 하나의 노드에 넣으면 처음에는 편하지만, 나중에는 디버깅하기 어려워진다. 다음과 같은 구조로 시작하면 각 모듈의 책임이 비교적 분명해진다.
ai_robot_arm_agent/
config/
workspace.yaml
skills.yaml
safety.yaml
perception/
object_detector.py
segmenter.py
color_filter.py
pose_estimator.py
planning/
command_parser.py
task_planner.py
dispatcher.py
manipulation/
grasp_generator.py
pick_skill.py
place_skill.py
moveit_client.py
gripper_client.py
verification/
visual_verifier.py
state_verifier.py
logs/
episodes/perception은 카메라에서 의미 있는 물체 정보를 만든다. planning은 자연어와 스킬 순서를 다룬다. manipulation은 실제 로봇팔 동작을 담당한다. verification은 결과를 확인한다. config는 환경마다 달라지는 값을 보관한다. 특히 작업 영역, 오른쪽 영역 좌표, 그리퍼 폭, 접근 높이, 속도 제한은 코드 안에 직접 넣지 않는 것이 좋다.
로그는 반드시 남기자. 성공한 데모 영상도 중요하지만, 실패한 로그가 프로젝트를 성장시킨다. 어떤 조명에서 빨간색 판정이 흔들렸는지, 어느 좌표에서 집기 오차가 컸는지, MoveIt2 계획이 자주 실패하는 자세가 무엇인지 기록해야 다음 개선이 가능하다.
최소 성공 버전부터 만들기
최종 프로젝트라고 해서 처음부터 모든 것을 자동화하려고 하면 오히려 완성하기 어렵다. 좋은 방법은 최소 성공 버전을 먼저 만드는 것이다.
첫 번째 버전에서는 컵 하나만 작업대에 올려 둔다. 색상 판정도 단순하게 하고, 오른쪽 놓기 영역도 고정 좌표로 둔다. 목표는 전체 파이프라인이 한 번 끝까지 도는 것이다. 즉, 자연어 입력에서 시작해 검증 성공까지 도달하는 것이 첫 번째 목표다.
두 번째 버전에서는 컵을 여러 개 놓고 빨간 컵만 고르게 한다. 이때 비전 후보 선택과 색상 판정 로그를 확인한다. 세 번째 버전에서는 놓기 영역에 장애물을 추가하고, 빈 공간을 찾게 한다. 네 번째 버전에서는 실패를 일부러 만든다. 컵을 살짝 멀리 두거나, 조명을 바꾸거나, 그리퍼가 잡기 어려운 방향으로 컵을 놓는다. 로봇이 실패를 어떻게 보고하는지 확인한다.
이렇게 단계적으로 확장하면 프로젝트가 무너지지 않는다. 로봇 시스템은 한 번에 완벽해지지 않는다. 작은 성공을 쌓고, 실패를 기록하고, 다시 고치는 과정에서 강해진다.
이 장의 완성 기준
이번 장의 프로젝트가 제대로 동작한다고 말하려면 단순히 로봇팔이 움직이는 것만으로는 부족하다. 다음 조건을 만족해야 한다.
자연어 명령에서 대상 물체, 색상 조건, 놓기 위치를 구조화된 작업으로 변환한다.
객체 검출과 세그멘테이션을 이용해 조건에 맞는 물체를 선택한다.
픽셀 좌표와 깊이 값을 로봇 베이스 좌표로 변환한다.
집기 자세와 놓기 자세를 만들고 MoveIt2로 충돌 없는 경로를 계획한다.
pick,place,verify스킬을 순서대로 실행한다.실패 단계와 이유를 기록하고, 회복 가능한 실패는 제한된 범위에서 재시도한다.
최종적으로 빨간 컵이 오른쪽 영역에 있는지 확인한다.
이 조건을 만족하면 우리는 자연어 로봇팔 에이전트의 핵심 기능을 완성한 것이다. 물론 아직 갈 길은 남아 있다. 이동 로봇과 결합하면 "회의실로 가서 컵을 가져와" 같은 명령을 생각할 수 있고, 음성 인터페이스를 붙이면 사용자는 키보드 없이 로봇에게 말할 수 있다. 하지만 핵심은 이미 여기에 있다.
자연어는 목표를 말한다. 비전은 세상을 본다. 좌표 변환은 본 것을 로봇의 세계로 옮긴다. MoveIt2는 안전한 움직임을 만든다. 스킬 디스패처는 실행을 관리한다. 검증기는 결과를 확인한다.
이 모든 것이 연결될 때, 로봇팔은 더 이상 정해진 버튼만 누르는 장치가 아니다. 사람의 말을 듣고, 주변을 보고, 계획하고, 행동한 뒤, 스스로 결과를 확인하는 AI 로봇 에이전트가 된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.