이 폴더는
Harvest_Pipeline의 복사본에 RADIO-DPT-Pose 꼭지 백엔드를 추가한 버전이다 →PEDICEL_DPT_INTEGRATION.md.third_party/는 원본 폴더로의 symlink다.
ZED 2i 스테레오 기반 과일(구형 물체) 인지 파이프라인. 같은 프레임에서 깊이 추정 + 추적/세그멘테이션 + amodal + 3D 구 피팅을 락스텝으로 수행하고, 결과를 Rerun / ROS 2로 시각화·발행한다.
ZED 2i (HD1080 stereo)
├─ FoundationStereo ─────────────► depth
└─ EdgeTAM(track) ─ Ellipse R-CNN ─► visible mask + amodal ellipse
│
▼
SphereFitter (RANSAC 3D sphere
+ 정확한 conic 재투영 vs amodal)
│
┌─────────────────────┴─────────────────────┐
▼ ▼
3D bounding box / pointcloud [Pose 버튼 이벤트]
center + pedicel YOLO-Pose
→ tomato local +X axis
└─────────────────────┬─────────────────────┘
▼
Rerun · ROS 2
third_party/Fast-FoundationStereo) — 스테레오 깊이 (선택: TensorRT fp16, --ffs-trt, ~1.5배)third_party/EdgeTAM) — 프롬프트 1개로 시작하는 온라인 추적/세그멘테이션third_party/ellipse-rcnn) — 가려진 부분까지의 amodal 타원weights/tomato_pose/pedicel_pose.pt 상주 로드)weights/tomato_pose/radio_dpt_pose_v4_ih2_on_s0.pt)로 교체. base·tip을 FFS depth로 3D 승격해
base→tip 축을 Rerun/ROS(/tomato/pedicel_points, /tomato/pedicel_axis)로 실시간 확인하고, 그리퍼에
가려 중심을 못 구해도 꼭지 pose를 발행한다. 사용법·검증: PEDICEL_DPT_INTEGRATION.md.
원래 모델은 트래킹 모드 기본값(--tracking-pedicel-backend yolo).2026-09-22 변경: 로봇이 파지하기 전(트래킹 모드 Pose 버튼)은 원래 1-keypoint YOLO 모델(
--tracking-pedicel-backend yolo, 중심→꼭지 축)만 쓰고, RADIO-DPT(base+tip)는 그리퍼 파지 신호 후 grasp 모드에서만 실행된다./tomato/pedicel_points·/tomato/pedicel_axis도 grasp 모드에서만 발행.--pedicel-backend는 이제 grasp 모드 모델 (radio_dpt | yolo_pose). 트래킹에서도 RADIO-DPT를 쓰려면--tracking-pedicel-backend radio_dpt.
자세한 버전·패치는 requirements.txt 참고. 요약:
# 시스템: CUDA 12.6, ROS 2 Humble, ZED SDK 5.2.3 (pyzed)
source /opt/ros/humble/setup.bash
# PyTorch (CUDA 12.8 wheels)
pip install torch==2.10.0 torchvision==0.25.0 xformers==0.0.35 \
--index-url https://download.pytorch.org/whl/cu128
pip install -r requirements.txt
# vendored 모델 (requirements.txt의 주석대로 clone + 패치)
# Fast-FoundationStereo / EdgeTAM(pip install -e .) / ellipse-rcnn(PYTHONPATH)
.venv, 2026-09-14 구성·검증)시스템 python3.10 + ROS 2 Humble rclpy를 그대로 쓰고, ~/.local의 user-site(충돌하는 sam2 1.1.0 등)와는
격리된다 → PYTHONNOUSERSITE=1 불필요. third_party/는 symlink이므로 EdgeTAM editable 설치는 절대경로로.
cd Harvest_Pipeline_Pedicel
uv venv --python /usr/bin/python3.10 .venv
uv pip install --python .venv/bin/python torch==2.10.0 torchvision==0.25.0 xformers==0.0.35 \
--index-url https://download.pytorch.org/whl/cu128
uv pip install --python .venv/bin/python -r requirements.txt \
--extra-index-url https://download.pytorch.org/whl/cu128 --index-strategy unsafe-best-match
uv pip install --python .venv/bin/python setuptools wheel pytest lark
(cd third_party/EdgeTAM && SAM2_BUILD_CUDA=0 uv pip install --python "$OLDPWD/.venv/bin/python" -e . --no-build-isolation --no-deps)
# 실행 (매 셸)
source /opt/ros/humble/setup.bash
source .venv/bin/activate
HF_HUB_OFFLINE=1 python perception/pedicel_offline_check.py --out data/pedicel_check # 모델·환경 확인
python perception/web_gui.py --crop 230 --ros --tomato-pose
# 테스트: ROS의 launch_testing pytest 플러그인이 최신 pytest와 안 맞으므로 autoload 끔
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest perception --ignore=perception/test_real.py
pyzed(ZED SDK)는 venv에 없다. 라이브 카메라를 쓰려면 ZED SDK 설치 후
.venv/bin/python /usr/local/zed/get_python_api.py. 리플레이·오프라인 검증에는 불필요.weights/ellipse_rcnn/best.pt(159 MB), weights/ffs_trt/*.onnx(136 MB),
tomato-vlm safetensors는 원본 Harvest_Pipeline/에서 다시 받아 복사·검증 완료(Ellipse R-CNN 학습 가중치 로드 OK). 따라서
--ellipse-ckpt 더미 옵션은 더 이상 필요 없다. third_party/는 symlink라 VLM 가중치는 자동으로 공유된다.perception/live_pedicel_rerun.py: ZED → FoundationStereo depth → 고정 target zone(기본, 화면 중앙 정사각형) →
RADIO-DPT base/tip → Rerun. 그리퍼가 토마토를 zone 안에 두는 시나리오라 검출기가 필요 없다(tomato-vlm YOLO는
파지 상태에서 박스가 틀려 --detector yolo는 보조용). 세그멘테이션이 없으므로 중심은 unavailable, pose 위치는 3D base 점.
# 1) ZED SDK 5.2.3 (cu12.8) — 설치 파일은 ~/Downloads/zed_sdk/ 에 받아 둠. sudo 필요, 1회
sudo ~/Downloads/zed_sdk/ZED_SDK_Ubuntu22_cuda12.8_tensorrt10.9_v5.2.3.run -- silent skip_cuda skip_tools
sudo cp ~/Downloads/zed_sdk/99-slabs.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules && sudo udevadm trigger
sudo chmod -R o+rX /usr/local/zed && sudo chmod -R a+rwX /usr/local/zed/settings # silent 설치가 root:zed 770으로 만듦
# 2) pyzed를 venv에 (get_python_api.py가 python 버전에 맞는 wheel을 받아 pip 설치; venv에 pip 설치돼 있음)
.venv/bin/python /usr/local/zed/get_python_api.py
.venv/bin/python -c "import pyzed.sl as sl; print(sl.Camera.get_sdk_version())"
# 3) 실행 (카메라 연결: lsusb | grep 2b03 에 f880+f881 둘 다 보여야 함)
source .venv/bin/activate
python perception/live_pedicel_rerun.py --source zed --crop 230 --tta none # 중앙 zone
python perception/live_pedicel_rerun.py --source zed --crop 230 --box 380,120,680,420 --zone-depth 0.30,0.45
python perception/live_pedicel_rerun.py --source npz --save data/pedicel_live/x.rrd # 카메라 없이 npz 리플레이
화면: 초록/빨강 = target zone(안에 --zone-depth 범위의 depth 픽셀이 --zone-fill 이상이면 초록 "TOMATO IN ZONE",
아니면 빨강 "PLACE TOMATO IN GREEN ZONE"), 노랑 = 꼭지 모델이 실제로 보는 1.4× 정사각 crop. 3D 뷰에는 zone×깊이 범위의
frustum(world/target_zone)이 같은 색으로 그려진다. zone이 비어 있으면 꼭지 모델을 돌리지 않는다(--always-run으로 강제).
--zone-depth는 그리퍼 작업 거리 ±수 cm로 좁힐 것. 넓게 두면 책상·배경 depth가 zone을 채워 항상 초록이 된다.
crop 고해상도 depth(--refine-scale 2, 기본 켜짐): 가는 꼭지의 tip depth가 배경으로 새서 tip_on_base_plane
(정면 평행 가정 → 축이 누움)으로 떨어지는 문제 대책. 꼭지 모델 crop(1.4× 정사각 +--refine-pad)만 좌·우에서 잘라
--refine-scale배 업샘플해 FFS를 한 번 더 돌리고, 그 depth로 base/tip을 3D 승격한다. 우측 crop은 --zone-depth의
zmax에서의 시차만큼 왼쪽으로 옮겨 자르므로 zmax보다 가까운 물체는 crop 안에서 항상 양의 시차를 가진다(zmax보다 먼
배경은 무효). 전체 프레임 pass는 그대로 pointcloud·zone 판정에 쓴다. Rerun world/cam/refine, refine_depth에 crop 결과 표시.
--full-scale 0.5 --res HD1080로 전체 pass를 줄이고 crop은 실제 1080p 픽셀을 쓰는 조합도 가능. 합성 스테레오로 depth 복원 검증.
Rerun 레이아웃은 blueprint로 고정: 왼쪽 3D, 오른쪽 위 전체 이미지(파란 박스 = refine crop 영역), 오른쪽 아래
refine/image(업샘플 crop + 오버레이) / refine/depth. --dump DIR로 프레임별 main/refine/refine_depth PNG 저장.
실카메라(ZED 2i HD720, 2026-09-14)에서 refine ×2 crop 1184×932 포함 프레임당 약 600 ms 확인.
배율/zone 크기와 비용 (ZED 2i HD720 crop 230, zone 0.30–0.45 m, RTX 3090; 전체 pass + refine + 꼭지 모델 포함):
--center-frac | --refine-scale | refine crop | 프레임 시간 | FFS 피크 VRAM |
|---|---|---|---|---|
| 0.4 (288 px) | 2 | 1184×932 | ≈600 ms | 1.7 GB |
| 0.3 (216 px, 기본) | 3 (기본) | 1323×1098 | ≈690 ms | 2.5 GB |
| 0.3 | 4 | 1644×1464 | ≈1020 ms | 5.0 GB |
crop 안 시차 범위 (d(zmin) − d(zmax)) × scale이 체크포인트 한계(max_disp 416)를 넘으면 안 된다. 시작 로그에
in-crop range ... <= max_disp limit 416로 출력되며, 필요 시 해당 호출만 max_disp를 자동으로 올린다(기본 192).
넘으면 --zone-depth를 좁히거나 배율을 낮춘다. zone 깊이 범위를 좁힐수록 crop도 작아져 더 높은 배율이 가능하다.
VRAM (2026-09-14 실측, HD720, zone 0.3, refine ×3): 뷰어 프로세스 5.9 GB(스크립트가 PYTORCH_CUDA_ALLOC_CONF= expandable_segments:True를 설정; 없으면 7.8 GB) + Rerun 뷰어 0.7 GB. 내역: CUDA 컨텍스트 0.3, RADIO-DPT 가중치 1.8(fp16),
FFS 가중치 0.07, ZED SDK 0.1, FFS 전체 pass 피크 3.4, refine ×3 피크 5.2(일시적). 12 GB GPU에 여유 있음.
nvidia-smi 총합에는 데스크톱 등 다른 프로세스(이 PC ~1.3 GB)가 포함되니 프로세스별로 볼 것.
옵션: --refine-scale 3 --refine-pad 32 --full-scale 1 --dump DIR (뷰어는 전체 pass를 항상 돌림; grasp 모드는 crop만) --detector {center,box,yolo,stored} --center-frac 0.4 --box --zone-depth --zone-fill 0.25 --hold 10(yolo 박스 유지)
--tta --pedicel-device --hz --stride --zmax --save --no-spawn.
--tomato-pose로 띄우면 기존 버튼 줄 아래에 🤖 그리퍼 파지 신호 → 꼭지 모델 / 파지 해제 행이 생긴다. 실기에서는
--ros일 때 --grasp-topic(기본 /harvest/grasped, std_msgs/Bool)의 True/False가 같은 일을 한다.
파지 신호: EdgeTAM 추적 루프를 멈추고(/stop과 동일) perception/grasp_pedicel.py의 루프를 돌린다:
ZED → FFS 전체 pass(포인트클라우드·zone 판정) → crop 업샘플 FFS(refine) → RADIO-DPT base/tip → PoseSolution
→ Rerun(추적 모드와 같은 엔티티 world/tomato_pose/*, diagnostics/tomato_pose/image, world/target_zone)
/tomato/{pose,stem_direction,pedicel_points,pedicel_axis}(--pose-publish-rate로 반복), /tomato/pose_status(mode: grasp),
/harvest/{points,rgb}.모델 선택(2026-09-18): 행의 셀렉트 박스로 우리 모델 (RADIO-DPT-Pose) ↔ YOLO26s-pose (fine-tuned)를 고른다. 시작 시에는
--pedicel-backend(기본 radio_dpt) 하나만 로드하고, 다른 쪽은 그 모델을 고른 파지 신호가 처음 왔을 때 로드해(YOLO 0.2 s,
RADIO ~3 s) 이후 상주한다. grasp 상태줄에 model … (loaded: …)로 표시. yolo_pose = pedicel-2d runs/y26s-ih2-fx7/s0/best.pt
(in-hand 학습, 2 keypoint base/tip, 640², ~11 ms)를 weights/tomato_pose/yolo26s_pose_ih2_fx7_s0.pt로 복사한 것.
perception/pedicel_yolo.py가 RADIO 래퍼와 같은 입력 규약(1.4× 정사각 crop → 1024 letterbox → 640)으로 감싸 estimator의
radio_dpt 경로를 공유한다(TomatoPoseEstimator(backend='yolo_pose')). 오프라인 검증(18 npz, tta none): valid 14/18,
base 위치는 RADIO와 대체로 5 px 안, 미검출 1장·tip conf<0.5 3장(YOLO는 예측을 포기하는 경우가 있음; MODEL_SELECTION.md 참고).
python perception/pedicel_offline_check.py --backend yolo_pose --tta none --out data/pedicel_check_yolo
zone: 항상 화면 중앙 고정 정사각형(--grasp-zone-frac, 기본 0.3). 로봇 팔이 토마토를 카메라 앞으로 가져오는 시나리오라
캔버스/VLM/추적 박스는 zone에 쓰지 않는다(2026-09-18 수정; 이전에는 캔버스 1번 박스가 있으면 그 박스를 썼음). depth 범위·refine 배율·
"zone 비어도 실행"은 행의 입력칸으로 파지 시점마다 바꿀 수 있다. zone이 비면(depth 범위 안 픽셀 < --grasp-zone-fill) 모델을 돌리지 않고 이전 pose 반복 발행도 멈춘다.
브라우저에 오버레이(zone·crop·base/tip)가 실시간으로 뜨고, grasp 상태줄에 state·fill·conf·axis·길이·추론시간이 나온다.
파지 시 ZED 재오픈(2026-09-18): 파지 행의 해상도 셀렉트(2K / 1080p / 720p=전환 없음, 기본은 --grasp-res=HD2K)로
세션마다 고른다 — 파지 신호가 오면 카메라를 닫고 이 해상도로
다시 열어(약 2 s) grasp 루프를 돌리고, 해제 시 --res(HD720)로 복귀한다. 캔버스 zone과 --crop은 폭 비율(×1.725)로 자동
스케일(--grasp-crop으로 지정 가능). 720p 대비 crop에 실제 픽셀이 1.7배 들어간다.
재오픈한 소스는 이전 프레임 번호를 이어받는다(ZedLatestSource(idx_offset=…)) — 번호가 0부터 다시 시작하면 Rerun의
"frame" 타임라인이 과거로 돌아가 3D 뷰가 멈춘 것처럼 보였다(2026-09-18 수정, 라이브 검증: 720p 700 → 2K 725… → 720p 1138).
전제 조건 수정: ZED SDK가 연 /dev/video* fd를 이후 spawn되는 자식(Rerun 뷰어, VLM 워커)이 상속하면 close 후 재오픈이
CAMERA STREAM FAILED TO START로 실패한다 → orchestrator.ZedLatestSource가 open 직후 장치 fd에 FD_CLOEXEC를 건다.
스테레오 탐색 범위 ≠ zone 범위(2026-09-18 원인 규명): 같은 장면(모니터 0.95 m)으로 비교한 결과 crop 자체는 문제가 없다
(전체 프레임 0.954 m = 같은 좌표 crop 0.953 m = ×1.5 업샘플 crop 0.954 m). 문제는 우측 crop을 zone의 zmax(0.45 m) 시차만큼
옮긴 것으로, 그 범위 밖 배경이 0.40 m로 접혀 나왔다. 그래서 --grasp-stereo-far(기본 1.5 m)를 두어 shift는 이 거리의
시차로 잡고, 배율은 (d(zmin)−d(far))×scale ≤ 416에 맞춰 자동 결정한다(2K에서 ×1.1, 720p에서 ×2.2). 결과: 배경이 LR 검사
없이도 0.954 m로 제자리에 복원된다. 배율을 더 올리려면 zmin을 올리거나 far를 낮춘다.
좌우 일관성 검사(기본 켜짐, --grasp-no-lr-check로 해제): 우측 crop을 zmax 시차만큼 옮겨 자르므로 zmax보다 먼 배경은
crop 안에서 음의 시차여야 하는데 FFS는 양의 시차만 내서, 배경이 zone 깊이 범위 안으로 접혀 "앞으로 튀어나온" 점이 생겼다.
거울 쌍(flip(R), flip(L))을 같은 배치로 한 번 더 풀어 disp_R(x−d) ≈ d가 아닌 픽셀(+300 px 미만 연결 성분)을 무효 처리한다.
far=1.5 m 이후에는 1.5 m 밖 배경에만 관여한다. 비용은 refine pass 2배(2K ×1.1: 214 → 413 ms, 피크 2.6 → 5.1 GB;
프로세스 총 8.4 GB). 12 GB에 여유가 필요하면 끄고 zone 판정에 맡겨도 된다. 배경 위 keypoint는 depth가 없어 depth_unavailable로 실패하는 것이 정상.
crop만 복원(기본): grasp 모드는 전체 프레임 FFS pass를 돌리지 않고 crop만 스테레오 복원한다(--grasp-full-depth로 전체 pass
추가). zone 판정과 Rerun 포인트클라우드(crop 영역, dense, stride 2)가 모두 crop depth에서 나온다. --grasp-refine-iters 16
(crop pass GRU 반복; 전체 pass는 8). crop 시차 범위 (d(zmin)−d(zmax))×scale이 416을 넘으면 세션 배율을 자동으로 낮춘다.
실측(RTX 3090, 실제 HD2K 쌍, zone 0.30–0.45 m, crop 731×584@×1):
| 카메라 | refine | crop | FFS 시간 | 피크 VRAM |
|---|---|---|---|---|
| HD720 | ×3 (iters 16) | 1323×1098 | 394 ms | 3.5 GB |
| HD2K | ×1 | 731×584 | 101 ms | 0.9 GB |
| HD2K | ×1.5 (기본) | 1096×876 | 247 ms | 2.4 GB |
| HD2K | ×2 | 1462×1168 | 490 ms | 5.0 GB |
| HD2K | ×2.5 | 1828×1460 | 859 ms | 9.3 GB |
--grasp-refine-scale 기본값: HD1080/HD2K 재오픈 시 1.5, HD720이면 3. 라이브 검증: 파지→HD2K→유효 pose(0.84 s/프레임)→
해제→HD720 복귀를 2회 반복 성공.
모드 전환 시 Rerun 정리(2026-09-18): 추적 루프가 멈추면(Stop/파지) world/box, world/cam/image/{mask,tracked}를
마지막 프레임+1에 tombstone하고, 파지 세션 시작 프레임에서 world/tomato_pose·진단 이미지도 지운다. 파지 종료 시에는 grasp
엔티티(world/target_zone, world/cam/image/target, world/tomato_pose)를 같은 방식으로 지운다. (Clear는 스레드별
타임라인에 놓이므로 시각을 지정하지 않으면 적용되지 않던 문제 수정.) 이전 추적 결과나 720p 좌표의 옛 박스가 2K 파지 화면에
남던 현상이 이것이었다.
파지 해제 → idle. 다시 추적하려면 Start를 누른다. 세그멘테이션이 없으므로 pose 위치는 3D base 점(center_source=pedicel_base_point).
연속 실행이므로 --pedicel-tta none(0.15 s) 권장. --ffs-trt면 엔진이 고정 크기라 refine은 자동으로 꺼진다.
(2026-09-18 이후 best.pt가 복구되어 --ellipse-ckpt 더미는 불필요.) 검증(2026-09-17, replay 합성 스테레오): 버튼·ROS 토픽 시작/해제, /tomato/pedicel_points
(base,tip 2행), /tomato/pose_status 발행 확인.
source /opt/ros/humble/setup.bash && source .venv/bin/activate
python perception/web_gui.py # 2026-09-18부터 기본값: --crop 230 --ros(소스돼 있으면) --vlm --tomato-pose --pedicel-tta none
python perception/web_gui.py --source replay --seq data/pedicel_live/synth_seq # 카메라 없이 (crop 자동 0)
python perception/web_gui.py --no-vlm --no-ros # 끄기
ros2 topic pub --once /harvest/grasped std_msgs/msg/Bool "data: true" # 로봇 쪽 신호 시뮬레이션
모델과 카메라를 한 번만 로드해 유지하고, 브라우저에서 첫 프레임에 2D 박스를 그린 뒤 Start/Stop으로 3D 검출을 제어한다. 3D 결과(포인트클라우드 + 3D bbox + 카메라)는 Rerun 창에 표시.
source /opt/ros/humble/setup.bash
python3 perception/web_gui.py --crop 300 # 라이브 ZED (기본)
python3 perception/web_gui.py --crop 230 --vlm --ros --tomato-pose # 자동검출+Pose 통합
python3 perception/web_gui.py --source replay # 녹화 시퀀스 (기본 data/tomato_seq)
# 다른 녹화 + 프레임 범위 + 루프:
python3 perception/web_gui.py --source replay --seq data/line_5_r/seq --start 500 --end 610 --loop --crop 230
# 브라우저에서 http://localhost:8000 → 물체에 박스 드래그 → ▶ Start / ■ Stop
브라우저 = 첫 프레임에 박스 0~3개 드로잉(번호·색, "박스 지우기") + Start/Stop, Rerun 창 = 3D 결과. 박스 여러 개면 객체별 mask/2D·3D박스/Kalman이 각각, 0개면 검출 없이 장면만. 물체가 화면에서 벗어나 트래킹을 놓치면 그 객체 오버레이가 사라지고 장면(포인트클라우드+카메라)은 남는다.
replay 옵션 (--source replay):
--seq <dir> : 녹화 시퀀스 폴더 (left/, right/, K.npy, 있으면 pose/)--start N --end M : 사용할 프레임 범위 (inclusive)--loop : 끝나면 처음으로 되돌아가 반복 (루프마다 박스로 검출 재초기화)--crop N : 왼쪽 N px 잘라냄 (스테레오 invalid strip 제거; HD720이면 ~230, HD1080이면 ~300).
FFS는 full 쌍으로 depth를 만든 뒤 crop→speckle 제거(경계 보존), EdgeTAM/박스/intrinsics(cx)도 동일 crop.--ema N : depth 시간 EMA의 span(프레임, 기본 5). 실제 켜고/끄기는 GUI의 "Depth EMA" 버튼.
⚠️ 카메라 정지일 때만 켜세요 (움직이면 픽셀이 다른 3D점을 봐서 잔상). 정지 씬에서 depth 잔떨림 감소.orchestrator.KalmanBox의 q/r 노이즈로 강도 조절.--vlm : 토마토 VLM 자동검출 활성화 (YOLO+Qwen3-VL 4bit, 코드·weight third_party/tomato-vlm/).
브라우저 "VLM 자동검출" 버튼 → 현재 프레임에 YOLO→verifier→ripeness→scoring 실행 → 익은 토마토
박스(최대 3개) 자동 채움(수동 드로잉 대신, 0~3개). 프로세스 격리 방식: VLM은 vlm_worker.py라는
별도 자식 프로세스에서 돌아요(터미널은 그대로 1개). 첫 검출 때 워커 기동(VLM 로드 ~4GB), 이후 검출은
워커 재사용(빠름), Start/Stop 이후에도 유지되어 다음 자동검출에서 로드된 모델을 재사용한다.
왜 이렇게? 한 프로세스에서 4bit VLM과 FFS를 섞으면, VLM이 남긴 파편화로 트래킹 중 FFS의 큰 임시 블록이
재사용 안 돼 reserved VRAM이 ~2.8GB → ~11GB로 폭증(실사용은 0.5GB뿐). 별도 프로세스로 격리하면
트래킹 프로세스는 애초에 VLM을 안 봐서 깨끗한 ~2.8GB로 평탄하게 유지된다 (empty_cache 불필요).
(web_gui가 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True도 설정)
검출 시 GUI 하단에 파이프라인 4단계 시각화 이미지(1.YOLO / 2.필터링 / 3.scoring / 4.최종선정)가
뜨고 ◀▶ 버튼으로 단계 전환(데모 노트북 render_stage 이식). conf·NMS iou 슬라이더로 threshold 조절.--tomato-pose : GUI의 Tomato Pose 계산 버튼을 활성화한다.
obj_id에 보존한다. Pose 요청 시 그중 현재 visible
mask가 있는 가장 높은 초기 score 대상을 선택한다. 수동 박스는 번호 순서다. retry 도중에는 ID를 바꾸지 않는다.CycleResult를 사용한다. left/depth/mask/obj.box/K는
모두 동일 crop·동일 cycle 좌표다. 별도 ROS 토픽들을 다시 동기화해 조합하지 않는다.estimate_tomato_center; 실패하면 같은 cycle의 SphereFitter 중심을 사용한다.
--center-method sphere는 순서를 반대로 한다.stem_source=fallback_screen_up으로 표시한다. 중심 계산 두 방식이
모두 실패하면 다음 cycle 한 번 재시도하고, 다시 실패하면 pose를 발행하지 않는다.--pose-publish-rate)로 다음 요청/Stop까지 반복 발행한다. header stamp는 계산에
사용한 capture stamp로 고정된다.--calib은 4×4 .npy뿐 아니라 Hand-Eye 결과 YAML도 읽는다. --base-frame 기본값은 base_link이며
ur5e_base_link 등으로 바꿀 때 반드시 그 frame을 기준으로 구한 calibration을 함께 사용해야 한다.--ros : Rerun(world)에 더해 ROS2 토픽 동시 발행. eye-to-hand 캘리브 T_base_camera(--calib,
기본 calib/T_base_camera.npy, 현재 항등 placeholder)로 pointcloud·3D박스를 --base-frame 좌표로 변환해
발행(/harvest/points, /harvest/box_markers), RGB/mask는 --camera-frame(/harvest/rgb, /harvest/mask).
객체별 visible 마스크는 /harvest/masks(std_msgs/UInt8MultiArray, (N,H,W) 스택 —
각 레이어 foreground=obj_id, 겹침 보존)로도 발행(/harvest/mask은 한 장 라벨 이미지라 겹침 손실).
VLM 자동검출 시엔 익은 토마토 2D bbox를 /harvest/vlm_boxes_2d 로도 발행 —
std_msgs/Float32MultiArray, N×5 [x1,y1,x2,y2,score]를 score 내림차순 정렬(크롭된 좌표=/harvest/rgb와 동일).
일회성 검출이라 latched(TRANSIENT_LOCAL) QoS라 나중에 구독해도 마지막 결과를 받음
(ros2 topic echo /harvest/vlm_boxes_2d std_msgs/msg/Float32MultiArray --qos-durability transient_local).
캘리브 값 나오면 calib/T_base_camera.npy 교체. static TF --base-frame→--camera-frame(=T_base_camera) 도
발행하므로, rviz2에서 Fixed Frame = 설정한 base frame으로 두면 pointcloud·box·이미지가 정합돼 보인다
(기본 Fixed Frame=map이면 TF가 없어 안 보임).
--tomato-pose도 함께 주면 표준 메시지로 아래를 추가 발행한다(커스텀 메시지/colcon build 불필요).
/tomato/pose (geometry_msgs/PoseStamped) — position=토마토 중심, local +X=중심→꼭지, base frame/tomato/stem_direction (geometry_msgs/Vector3Stamped) — 위 +X 단위벡터, base frame/tomato/pose_status (std_msgs/String) — JSON 상태(requested/computing/retrying/valid/failed/stale),
target ID/초기 score/rank, center/stem source, retry 수, calibration/frame, pose_active--realtime 을 주면 latest-only(최신 프레임만, 스킵 = 라이브 시뮬레이션)로 동작.pose/가 있으면 world 좌표, 없으면 camera 좌표로 표시.--ffs-trt [engine] : FoundationStereo를 TensorRT fp16 엔진으로 실행 (PyTorch 대비 ~1.5배
빠름: 254ms→168ms @ 736×1280 RTX 4080 Laptop). 인자 생략 시 weights/ffs_trt/fast_foundationstereo.engine
사용. 엔진은 고정 해상도(736×1280 = HD720을 32의 배수로 패딩한 크기)로 빌드됨 — 다른 해상도면 재빌드 필요.
정확도: 토마토 작동거리(0.2–1.5m)에서 PyTorch 대비 depth median 차 ~9mm(sphere fitting의 median-band
±3cm 안), 경계/원거리 tail은 RANSAC+band가 흡수. 엔진 메모리는 TRT 자체 관리(torch 풀 외부).FoundationStereo TensorRT 엔진 빌드 (1회, --ffs-trt 용):
pip install onnx onnxscript # export 의존성 (tensorrt/trtexec는 이미 설치돼 있어야 함)
# 1) ONNX export — CPU에서 (GPU로 트레이싱하면 OOM: export는 fp32+비최적 cost-volume+그래프 전체 보존)
CUDA_VISIBLE_DEVICES="" python3 third_party/Fast-FoundationStereo/scripts/make_single_onnx_cpu.py \
--model_dir third_party/Fast-FoundationStereo/weights/weights/23-36-37/model_best_bp2_serialize.pth \
--save_path weights/ffs_trt --height 736 --width 1280 --valid_iters 8 --max_disp 192
# (make_single_onnx_cpu.py = 벤더 스크립트에 dynamo=False[레거시 exporter, adaptive_max_pool2d 지원] + CPU 트레이싱 추가한 사본)
# 2) 엔진 빌드 — GPU에서 (~16분; 빌드 중 .engine 파일 절대 건드리지 말 것)
trtexec --onnx=weights/ffs_trt/fast_foundationstereo.onnx \
--saveEngine=weights/ffs_trt/fast_foundationstereo.engine --fp16 --timingCacheFile=weights/ffs_trt/ffs_timing.cache
# 3) 사용
python3 perception/web_gui.py --crop 230 --vlm --ffs-trt
SVO(.svo2) → 시퀀스 추출 (rectified left/right + pose + K.npy):
python3 perception/extract_svo.py --svo data/line_5_r/line_5_r.svo2 --out data/line_5_r/seq
# VIEW.LEFT/RIGHT는 재생 시 SDK가 rectify → FoundationStereo에 바로 사용 가능
# positional tracking으로 프레임별 pose(world) 복원 → world 좌표 시각화 가능
모델을 먼저 모두 로드한 뒤 Rerun 뷰어를 띄우고 실시간 처리.
source /opt/ros/humble/setup.bash
python3 perception/live_rerun.py --source zed # 시작 시 물체를 화면 중앙에 두면 추적 시작
python3 perception/live_rerun.py --source replay # 카메라 없이 녹화 시퀀스로 재생
Rerun 창: 3D 포인트클라우드 + 노란 3D bbox + 이미지(초록 init박스 / 마젠타 추적박스). Ctrl-C 종료.
source /opt/ros/humble/setup.bash
python3 perception/precompute_results.py # 프레임별 구 결과 캐시 (1회)
python3 perception/ros2_rerun_publisher.py # /harvest/points, /harvest/box_markers 발행 + .rrd 저장
# 시각화: rerun data/tomato_seq/out/harvest.rrd
# 또는 rviz2 (Fixed Frame = camera_optical)
python3 perception/precompute_depth.py # 패스1: FFS 깊이 → data/tomato_seq/depth/
python3 perception/test_real.py # 패스2: 추적+amodal+구fit (프레임별 지표 출력)
python3 perception/make_viz.py # result.mp4 + montage.png
무거운 FFS를 별도 프로세스로 분리(2-패스)해 단일 프로세스 과부하를 피한다.
python3 perception/test_orchestrator.py # 최신 프레임만 처리(스킵) 동작 확인
data/tomato_seq/ — ZED 스테레오 시퀀스 (left/, right/, depth/, K.npy, 결과 out/)left/right + K.npy 저장 (실행 전 lsusb | grep 2b03:f880 로 카메라 확인).이 폴더는
Harvest_Pipeline의 복사본에 RADIO-DPT-Pose 꼭지 백엔드를 추가한 버전이다 →PEDICEL_DPT_INTEGRATION.md.third_party/는 원본 폴더로의 symlink다.
ZED 2i 스테레오 기반 과일(구형 물체) 인지 파이프라인. 같은 프레임에서 깊이 추정 + 추적/세그멘테이션 + amodal + 3D 구 피팅을 락스텝으로 수행하고, 결과를 Rerun / ROS 2로 시각화·발행한다.
ZED 2i (HD1080 stereo)
├─ FoundationStereo ─────────────► depth
└─ EdgeTAM(track) ─ Ellipse R-CNN ─► visible mask + amodal ellipse
│
▼
SphereFitter (RANSAC 3D sphere
+ 정확한 conic 재투영 vs amodal)
│
┌─────────────────────┴─────────────────────┐
▼ ▼
3D bounding box / pointcloud [Pose 버튼 이벤트]
center + pedicel YOLO-Pose
→ tomato local +X axis
└─────────────────────┬─────────────────────┘
▼
Rerun · ROS 2
third_party/Fast-FoundationStereo) — 스테레오 깊이 (선택: TensorRT fp16, --ffs-trt, ~1.5배)third_party/EdgeTAM) — 프롬프트 1개로 시작하는 온라인 추적/세그멘테이션third_party/ellipse-rcnn) — 가려진 부분까지의 amodal 타원weights/tomato_pose/pedicel_pose.pt 상주 로드)weights/tomato_pose/radio_dpt_pose_v4_ih2_on_s0.pt)로 교체. base·tip을 FFS depth로 3D 승격해
base→tip 축을 Rerun/ROS(/tomato/pedicel_points, /tomato/pedicel_axis)로 실시간 확인하고, 그리퍼에
가려 중심을 못 구해도 꼭지 pose를 발행한다. 사용법·검증: PEDICEL_DPT_INTEGRATION.md.
원래 모델은 트래킹 모드 기본값(--tracking-pedicel-backend yolo).2026-09-22 변경: 로봇이 파지하기 전(트래킹 모드 Pose 버튼)은 원래 1-keypoint YOLO 모델(
--tracking-pedicel-backend yolo, 중심→꼭지 축)만 쓰고, RADIO-DPT(base+tip)는 그리퍼 파지 신호 후 grasp 모드에서만 실행된다./tomato/pedicel_points·/tomato/pedicel_axis도 grasp 모드에서만 발행.--pedicel-backend는 이제 grasp 모드 모델 (radio_dpt | yolo_pose). 트래킹에서도 RADIO-DPT를 쓰려면--tracking-pedicel-backend radio_dpt.
자세한 버전·패치는 requirements.txt 참고. 요약:
# 시스템: CUDA 12.6, ROS 2 Humble, ZED SDK 5.2.3 (pyzed)
source /opt/ros/humble/setup.bash
# PyTorch (CUDA 12.8 wheels)
pip install torch==2.10.0 torchvision==0.25.0 xformers==0.0.35 \
--index-url https://download.pytorch.org/whl/cu128
pip install -r requirements.txt
# vendored 모델 (requirements.txt의 주석대로 clone + 패치)
# Fast-FoundationStereo / EdgeTAM(pip install -e .) / ellipse-rcnn(PYTHONPATH)
.venv, 2026-09-14 구성·검증)시스템 python3.10 + ROS 2 Humble rclpy를 그대로 쓰고, ~/.local의 user-site(충돌하는 sam2 1.1.0 등)와는
격리된다 → PYTHONNOUSERSITE=1 불필요. third_party/는 symlink이므로 EdgeTAM editable 설치는 절대경로로.
cd Harvest_Pipeline_Pedicel
uv venv --python /usr/bin/python3.10 .venv
uv pip install --python .venv/bin/python torch==2.10.0 torchvision==0.25.0 xformers==0.0.35 \
--index-url https://download.pytorch.org/whl/cu128
uv pip install --python .venv/bin/python -r requirements.txt \
--extra-index-url https://download.pytorch.org/whl/cu128 --index-strategy unsafe-best-match
uv pip install --python .venv/bin/python setuptools wheel pytest lark
(cd third_party/EdgeTAM && SAM2_BUILD_CUDA=0 uv pip install --python "$OLDPWD/.venv/bin/python" -e . --no-build-isolation --no-deps)
# 실행 (매 셸)
source /opt/ros/humble/setup.bash
source .venv/bin/activate
HF_HUB_OFFLINE=1 python perception/pedicel_offline_check.py --out data/pedicel_check # 모델·환경 확인
python perception/web_gui.py --crop 230 --ros --tomato-pose
# 테스트: ROS의 launch_testing pytest 플러그인이 최신 pytest와 안 맞으므로 autoload 끔
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest perception --ignore=perception/test_real.py
pyzed(ZED SDK)는 venv에 없다. 라이브 카메라를 쓰려면 ZED SDK 설치 후
.venv/bin/python /usr/local/zed/get_python_api.py. 리플레이·오프라인 검증에는 불필요.weights/ellipse_rcnn/best.pt(159 MB), weights/ffs_trt/*.onnx(136 MB),
tomato-vlm safetensors는 원본 Harvest_Pipeline/에서 다시 받아 복사·검증 완료(Ellipse R-CNN 학습 가중치 로드 OK). 따라서
--ellipse-ckpt 더미 옵션은 더 이상 필요 없다. third_party/는 symlink라 VLM 가중치는 자동으로 공유된다.perception/live_pedicel_rerun.py: ZED → FoundationStereo depth → 고정 target zone(기본, 화면 중앙 정사각형) →
RADIO-DPT base/tip → Rerun. 그리퍼가 토마토를 zone 안에 두는 시나리오라 검출기가 필요 없다(tomato-vlm YOLO는
파지 상태에서 박스가 틀려 --detector yolo는 보조용). 세그멘테이션이 없으므로 중심은 unavailable, pose 위치는 3D base 점.
# 1) ZED SDK 5.2.3 (cu12.8) — 설치 파일은 ~/Downloads/zed_sdk/ 에 받아 둠. sudo 필요, 1회
sudo ~/Downloads/zed_sdk/ZED_SDK_Ubuntu22_cuda12.8_tensorrt10.9_v5.2.3.run -- silent skip_cuda skip_tools
sudo cp ~/Downloads/zed_sdk/99-slabs.rules /etc/udev/rules.d/ && sudo udevadm control --reload-rules && sudo udevadm trigger
sudo chmod -R o+rX /usr/local/zed && sudo chmod -R a+rwX /usr/local/zed/settings # silent 설치가 root:zed 770으로 만듦
# 2) pyzed를 venv에 (get_python_api.py가 python 버전에 맞는 wheel을 받아 pip 설치; venv에 pip 설치돼 있음)
.venv/bin/python /usr/local/zed/get_python_api.py
.venv/bin/python -c "import pyzed.sl as sl; print(sl.Camera.get_sdk_version())"
# 3) 실행 (카메라 연결: lsusb | grep 2b03 에 f880+f881 둘 다 보여야 함)
source .venv/bin/activate
python perception/live_pedicel_rerun.py --source zed --crop 230 --tta none # 중앙 zone
python perception/live_pedicel_rerun.py --source zed --crop 230 --box 380,120,680,420 --zone-depth 0.30,0.45
python perception/live_pedicel_rerun.py --source npz --save data/pedicel_live/x.rrd # 카메라 없이 npz 리플레이
화면: 초록/빨강 = target zone(안에 --zone-depth 범위의 depth 픽셀이 --zone-fill 이상이면 초록 "TOMATO IN ZONE",
아니면 빨강 "PLACE TOMATO IN GREEN ZONE"), 노랑 = 꼭지 모델이 실제로 보는 1.4× 정사각 crop. 3D 뷰에는 zone×깊이 범위의
frustum(world/target_zone)이 같은 색으로 그려진다. zone이 비어 있으면 꼭지 모델을 돌리지 않는다(--always-run으로 강제).
--zone-depth는 그리퍼 작업 거리 ±수 cm로 좁힐 것. 넓게 두면 책상·배경 depth가 zone을 채워 항상 초록이 된다.
crop 고해상도 depth(--refine-scale 2, 기본 켜짐): 가는 꼭지의 tip depth가 배경으로 새서 tip_on_base_plane
(정면 평행 가정 → 축이 누움)으로 떨어지는 문제 대책. 꼭지 모델 crop(1.4× 정사각 +--refine-pad)만 좌·우에서 잘라
--refine-scale배 업샘플해 FFS를 한 번 더 돌리고, 그 depth로 base/tip을 3D 승격한다. 우측 crop은 --zone-depth의
zmax에서의 시차만큼 왼쪽으로 옮겨 자르므로 zmax보다 가까운 물체는 crop 안에서 항상 양의 시차를 가진다(zmax보다 먼
배경은 무효). 전체 프레임 pass는 그대로 pointcloud·zone 판정에 쓴다. Rerun world/cam/refine, refine_depth에 crop 결과 표시.
--full-scale 0.5 --res HD1080로 전체 pass를 줄이고 crop은 실제 1080p 픽셀을 쓰는 조합도 가능. 합성 스테레오로 depth 복원 검증.
Rerun 레이아웃은 blueprint로 고정: 왼쪽 3D, 오른쪽 위 전체 이미지(파란 박스 = refine crop 영역), 오른쪽 아래
refine/image(업샘플 crop + 오버레이) / refine/depth. --dump DIR로 프레임별 main/refine/refine_depth PNG 저장.
실카메라(ZED 2i HD720, 2026-09-14)에서 refine ×2 crop 1184×932 포함 프레임당 약 600 ms 확인.
배율/zone 크기와 비용 (ZED 2i HD720 crop 230, zone 0.30–0.45 m, RTX 3090; 전체 pass + refine + 꼭지 모델 포함):
--center-frac | --refine-scale | refine crop | 프레임 시간 | FFS 피크 VRAM |
|---|---|---|---|---|
| 0.4 (288 px) | 2 | 1184×932 | ≈600 ms | 1.7 GB |
| 0.3 (216 px, 기본) | 3 (기본) | 1323×1098 | ≈690 ms | 2.5 GB |
| 0.3 | 4 | 1644×1464 | ≈1020 ms | 5.0 GB |
crop 안 시차 범위 (d(zmin) − d(zmax)) × scale이 체크포인트 한계(max_disp 416)를 넘으면 안 된다. 시작 로그에
in-crop range ... <= max_disp limit 416로 출력되며, 필요 시 해당 호출만 max_disp를 자동으로 올린다(기본 192).
넘으면 --zone-depth를 좁히거나 배율을 낮춘다. zone 깊이 범위를 좁힐수록 crop도 작아져 더 높은 배율이 가능하다.
VRAM (2026-09-14 실측, HD720, zone 0.3, refine ×3): 뷰어 프로세스 5.9 GB(스크립트가 PYTORCH_CUDA_ALLOC_CONF= expandable_segments:True를 설정; 없으면 7.8 GB) + Rerun 뷰어 0.7 GB. 내역: CUDA 컨텍스트 0.3, RADIO-DPT 가중치 1.8(fp16),
FFS 가중치 0.07, ZED SDK 0.1, FFS 전체 pass 피크 3.4, refine ×3 피크 5.2(일시적). 12 GB GPU에 여유 있음.
nvidia-smi 총합에는 데스크톱 등 다른 프로세스(이 PC ~1.3 GB)가 포함되니 프로세스별로 볼 것.
옵션: --refine-scale 3 --refine-pad 32 --full-scale 1 --dump DIR (뷰어는 전체 pass를 항상 돌림; grasp 모드는 crop만) --detector {center,box,yolo,stored} --center-frac 0.4 --box --zone-depth --zone-fill 0.25 --hold 10(yolo 박스 유지)
--tta --pedicel-device --hz --stride --zmax --save --no-spawn.
--tomato-pose로 띄우면 기존 버튼 줄 아래에 🤖 그리퍼 파지 신호 → 꼭지 모델 / 파지 해제 행이 생긴다. 실기에서는
--ros일 때 --grasp-topic(기본 /harvest/grasped, std_msgs/Bool)의 True/False가 같은 일을 한다.
파지 신호: EdgeTAM 추적 루프를 멈추고(/stop과 동일) perception/grasp_pedicel.py의 루프를 돌린다:
ZED → FFS 전체 pass(포인트클라우드·zone 판정) → crop 업샘플 FFS(refine) → RADIO-DPT base/tip → PoseSolution
→ Rerun(추적 모드와 같은 엔티티 world/tomato_pose/*, diagnostics/tomato_pose/image, world/target_zone)
/tomato/{pose,stem_direction,pedicel_points,pedicel_axis}(--pose-publish-rate로 반복), /tomato/pose_status(mode: grasp),
/harvest/{points,rgb}.모델 선택(2026-09-18): 행의 셀렉트 박스로 우리 모델 (RADIO-DPT-Pose) ↔ YOLO26s-pose (fine-tuned)를 고른다. 시작 시에는
--pedicel-backend(기본 radio_dpt) 하나만 로드하고, 다른 쪽은 그 모델을 고른 파지 신호가 처음 왔을 때 로드해(YOLO 0.2 s,
RADIO ~3 s) 이후 상주한다. grasp 상태줄에 model … (loaded: …)로 표시. yolo_pose = pedicel-2d runs/y26s-ih2-fx7/s0/best.pt
(in-hand 학습, 2 keypoint base/tip, 640², ~11 ms)를 weights/tomato_pose/yolo26s_pose_ih2_fx7_s0.pt로 복사한 것.
perception/pedicel_yolo.py가 RADIO 래퍼와 같은 입력 규약(1.4× 정사각 crop → 1024 letterbox → 640)으로 감싸 estimator의
radio_dpt 경로를 공유한다(TomatoPoseEstimator(backend='yolo_pose')). 오프라인 검증(18 npz, tta none): valid 14/18,
base 위치는 RADIO와 대체로 5 px 안, 미검출 1장·tip conf<0.5 3장(YOLO는 예측을 포기하는 경우가 있음; MODEL_SELECTION.md 참고).
python perception/pedicel_offline_check.py --backend yolo_pose --tta none --out data/pedicel_check_yolo
zone: 항상 화면 중앙 고정 정사각형(--grasp-zone-frac, 기본 0.3). 로봇 팔이 토마토를 카메라 앞으로 가져오는 시나리오라
캔버스/VLM/추적 박스는 zone에 쓰지 않는다(2026-09-18 수정; 이전에는 캔버스 1번 박스가 있으면 그 박스를 썼음). depth 범위·refine 배율·
"zone 비어도 실행"은 행의 입력칸으로 파지 시점마다 바꿀 수 있다. zone이 비면(depth 범위 안 픽셀 < --grasp-zone-fill) 모델을 돌리지 않고 이전 pose 반복 발행도 멈춘다.
브라우저에 오버레이(zone·crop·base/tip)가 실시간으로 뜨고, grasp 상태줄에 state·fill·conf·axis·길이·추론시간이 나온다.
파지 시 ZED 재오픈(2026-09-18): 파지 행의 해상도 셀렉트(2K / 1080p / 720p=전환 없음, 기본은 --grasp-res=HD2K)로
세션마다 고른다 — 파지 신호가 오면 카메라를 닫고 이 해상도로
다시 열어(약 2 s) grasp 루프를 돌리고, 해제 시 --res(HD720)로 복귀한다. 캔버스 zone과 --crop은 폭 비율(×1.725)로 자동
스케일(--grasp-crop으로 지정 가능). 720p 대비 crop에 실제 픽셀이 1.7배 들어간다.
재오픈한 소스는 이전 프레임 번호를 이어받는다(ZedLatestSource(idx_offset=…)) — 번호가 0부터 다시 시작하면 Rerun의
"frame" 타임라인이 과거로 돌아가 3D 뷰가 멈춘 것처럼 보였다(2026-09-18 수정, 라이브 검증: 720p 700 → 2K 725… → 720p 1138).
전제 조건 수정: ZED SDK가 연 /dev/video* fd를 이후 spawn되는 자식(Rerun 뷰어, VLM 워커)이 상속하면 close 후 재오픈이
CAMERA STREAM FAILED TO START로 실패한다 → orchestrator.ZedLatestSource가 open 직후 장치 fd에 FD_CLOEXEC를 건다.
스테레오 탐색 범위 ≠ zone 범위(2026-09-18 원인 규명): 같은 장면(모니터 0.95 m)으로 비교한 결과 crop 자체는 문제가 없다
(전체 프레임 0.954 m = 같은 좌표 crop 0.953 m = ×1.5 업샘플 crop 0.954 m). 문제는 우측 crop을 zone의 zmax(0.45 m) 시차만큼
옮긴 것으로, 그 범위 밖 배경이 0.40 m로 접혀 나왔다. 그래서 --grasp-stereo-far(기본 1.5 m)를 두어 shift는 이 거리의
시차로 잡고, 배율은 (d(zmin)−d(far))×scale ≤ 416에 맞춰 자동 결정한다(2K에서 ×1.1, 720p에서 ×2.2). 결과: 배경이 LR 검사
없이도 0.954 m로 제자리에 복원된다. 배율을 더 올리려면 zmin을 올리거나 far를 낮춘다.
좌우 일관성 검사(기본 켜짐, --grasp-no-lr-check로 해제): 우측 crop을 zmax 시차만큼 옮겨 자르므로 zmax보다 먼 배경은
crop 안에서 음의 시차여야 하는데 FFS는 양의 시차만 내서, 배경이 zone 깊이 범위 안으로 접혀 "앞으로 튀어나온" 점이 생겼다.
거울 쌍(flip(R), flip(L))을 같은 배치로 한 번 더 풀어 disp_R(x−d) ≈ d가 아닌 픽셀(+300 px 미만 연결 성분)을 무효 처리한다.
far=1.5 m 이후에는 1.5 m 밖 배경에만 관여한다. 비용은 refine pass 2배(2K ×1.1: 214 → 413 ms, 피크 2.6 → 5.1 GB;
프로세스 총 8.4 GB). 12 GB에 여유가 필요하면 끄고 zone 판정에 맡겨도 된다. 배경 위 keypoint는 depth가 없어 depth_unavailable로 실패하는 것이 정상.
crop만 복원(기본): grasp 모드는 전체 프레임 FFS pass를 돌리지 않고 crop만 스테레오 복원한다(--grasp-full-depth로 전체 pass
추가). zone 판정과 Rerun 포인트클라우드(crop 영역, dense, stride 2)가 모두 crop depth에서 나온다. --grasp-refine-iters 16
(crop pass GRU 반복; 전체 pass는 8). crop 시차 범위 (d(zmin)−d(zmax))×scale이 416을 넘으면 세션 배율을 자동으로 낮춘다.
실측(RTX 3090, 실제 HD2K 쌍, zone 0.30–0.45 m, crop 731×584@×1):
| 카메라 | refine | crop | FFS 시간 | 피크 VRAM |
|---|---|---|---|---|
| HD720 | ×3 (iters 16) | 1323×1098 | 394 ms | 3.5 GB |
| HD2K | ×1 | 731×584 | 101 ms | 0.9 GB |
| HD2K | ×1.5 (기본) | 1096×876 | 247 ms | 2.4 GB |
| HD2K | ×2 | 1462×1168 | 490 ms | 5.0 GB |
| HD2K | ×2.5 | 1828×1460 | 859 ms | 9.3 GB |
--grasp-refine-scale 기본값: HD1080/HD2K 재오픈 시 1.5, HD720이면 3. 라이브 검증: 파지→HD2K→유효 pose(0.84 s/프레임)→
해제→HD720 복귀를 2회 반복 성공.
모드 전환 시 Rerun 정리(2026-09-18): 추적 루프가 멈추면(Stop/파지) world/box, world/cam/image/{mask,tracked}를
마지막 프레임+1에 tombstone하고, 파지 세션 시작 프레임에서 world/tomato_pose·진단 이미지도 지운다. 파지 종료 시에는 grasp
엔티티(world/target_zone, world/cam/image/target, world/tomato_pose)를 같은 방식으로 지운다. (Clear는 스레드별
타임라인에 놓이므로 시각을 지정하지 않으면 적용되지 않던 문제 수정.) 이전 추적 결과나 720p 좌표의 옛 박스가 2K 파지 화면에
남던 현상이 이것이었다.
파지 해제 → idle. 다시 추적하려면 Start를 누른다. 세그멘테이션이 없으므로 pose 위치는 3D base 점(center_source=pedicel_base_point).
연속 실행이므로 --pedicel-tta none(0.15 s) 권장. --ffs-trt면 엔진이 고정 크기라 refine은 자동으로 꺼진다.
(2026-09-18 이후 best.pt가 복구되어 --ellipse-ckpt 더미는 불필요.) 검증(2026-09-17, replay 합성 스테레오): 버튼·ROS 토픽 시작/해제, /tomato/pedicel_points
(base,tip 2행), /tomato/pose_status 발행 확인.
source /opt/ros/humble/setup.bash && source .venv/bin/activate
python perception/web_gui.py # 2026-09-18부터 기본값: --crop 230 --ros(소스돼 있으면) --vlm --tomato-pose --pedicel-tta none
python perception/web_gui.py --source replay --seq data/pedicel_live/synth_seq # 카메라 없이 (crop 자동 0)
python perception/web_gui.py --no-vlm --no-ros # 끄기
ros2 topic pub --once /harvest/grasped std_msgs/msg/Bool "data: true" # 로봇 쪽 신호 시뮬레이션
모델과 카메라를 한 번만 로드해 유지하고, 브라우저에서 첫 프레임에 2D 박스를 그린 뒤 Start/Stop으로 3D 검출을 제어한다. 3D 결과(포인트클라우드 + 3D bbox + 카메라)는 Rerun 창에 표시.
source /opt/ros/humble/setup.bash
python3 perception/web_gui.py --crop 300 # 라이브 ZED (기본)
python3 perception/web_gui.py --crop 230 --vlm --ros --tomato-pose # 자동검출+Pose 통합
python3 perception/web_gui.py --source replay # 녹화 시퀀스 (기본 data/tomato_seq)
# 다른 녹화 + 프레임 범위 + 루프:
python3 perception/web_gui.py --source replay --seq data/line_5_r/seq --start 500 --end 610 --loop --crop 230
# 브라우저에서 http://localhost:8000 → 물체에 박스 드래그 → ▶ Start / ■ Stop
브라우저 = 첫 프레임에 박스 0~3개 드로잉(번호·색, "박스 지우기") + Start/Stop, Rerun 창 = 3D 결과. 박스 여러 개면 객체별 mask/2D·3D박스/Kalman이 각각, 0개면 검출 없이 장면만. 물체가 화면에서 벗어나 트래킹을 놓치면 그 객체 오버레이가 사라지고 장면(포인트클라우드+카메라)은 남는다.
replay 옵션 (--source replay):
--seq <dir> : 녹화 시퀀스 폴더 (left/, right/, K.npy, 있으면 pose/)--start N --end M : 사용할 프레임 범위 (inclusive)--loop : 끝나면 처음으로 되돌아가 반복 (루프마다 박스로 검출 재초기화)--crop N : 왼쪽 N px 잘라냄 (스테레오 invalid strip 제거; HD720이면 ~230, HD1080이면 ~300).
FFS는 full 쌍으로 depth를 만든 뒤 crop→speckle 제거(경계 보존), EdgeTAM/박스/intrinsics(cx)도 동일 crop.--ema N : depth 시간 EMA의 span(프레임, 기본 5). 실제 켜고/끄기는 GUI의 "Depth EMA" 버튼.
⚠️ 카메라 정지일 때만 켜세요 (움직이면 픽셀이 다른 3D점을 봐서 잔상). 정지 씬에서 depth 잔떨림 감소.orchestrator.KalmanBox의 q/r 노이즈로 강도 조절.--vlm : 토마토 VLM 자동검출 활성화 (YOLO+Qwen3-VL 4bit, 코드·weight third_party/tomato-vlm/).
브라우저 "VLM 자동검출" 버튼 → 현재 프레임에 YOLO→verifier→ripeness→scoring 실행 → 익은 토마토
박스(최대 3개) 자동 채움(수동 드로잉 대신, 0~3개). 프로세스 격리 방식: VLM은 vlm_worker.py라는
별도 자식 프로세스에서 돌아요(터미널은 그대로 1개). 첫 검출 때 워커 기동(VLM 로드 ~4GB), 이후 검출은
워커 재사용(빠름), Start/Stop 이후에도 유지되어 다음 자동검출에서 로드된 모델을 재사용한다.
왜 이렇게? 한 프로세스에서 4bit VLM과 FFS를 섞으면, VLM이 남긴 파편화로 트래킹 중 FFS의 큰 임시 블록이
재사용 안 돼 reserved VRAM이 ~2.8GB → ~11GB로 폭증(실사용은 0.5GB뿐). 별도 프로세스로 격리하면
트래킹 프로세스는 애초에 VLM을 안 봐서 깨끗한 ~2.8GB로 평탄하게 유지된다 (empty_cache 불필요).
(web_gui가 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True도 설정)
검출 시 GUI 하단에 파이프라인 4단계 시각화 이미지(1.YOLO / 2.필터링 / 3.scoring / 4.최종선정)가
뜨고 ◀▶ 버튼으로 단계 전환(데모 노트북 render_stage 이식). conf·NMS iou 슬라이더로 threshold 조절.--tomato-pose : GUI의 Tomato Pose 계산 버튼을 활성화한다.
obj_id에 보존한다. Pose 요청 시 그중 현재 visible
mask가 있는 가장 높은 초기 score 대상을 선택한다. 수동 박스는 번호 순서다. retry 도중에는 ID를 바꾸지 않는다.CycleResult를 사용한다. left/depth/mask/obj.box/K는
모두 동일 crop·동일 cycle 좌표다. 별도 ROS 토픽들을 다시 동기화해 조합하지 않는다.estimate_tomato_center; 실패하면 같은 cycle의 SphereFitter 중심을 사용한다.
--center-method sphere는 순서를 반대로 한다.stem_source=fallback_screen_up으로 표시한다. 중심 계산 두 방식이
모두 실패하면 다음 cycle 한 번 재시도하고, 다시 실패하면 pose를 발행하지 않는다.--pose-publish-rate)로 다음 요청/Stop까지 반복 발행한다. header stamp는 계산에
사용한 capture stamp로 고정된다.--calib은 4×4 .npy뿐 아니라 Hand-Eye 결과 YAML도 읽는다. --base-frame 기본값은 base_link이며
ur5e_base_link 등으로 바꿀 때 반드시 그 frame을 기준으로 구한 calibration을 함께 사용해야 한다.--ros : Rerun(world)에 더해 ROS2 토픽 동시 발행. eye-to-hand 캘리브 T_base_camera(--calib,
기본 calib/T_base_camera.npy, 현재 항등 placeholder)로 pointcloud·3D박스를 --base-frame 좌표로 변환해
발행(/harvest/points, /harvest/box_markers), RGB/mask는 --camera-frame(/harvest/rgb, /harvest/mask).
객체별 visible 마스크는 /harvest/masks(std_msgs/UInt8MultiArray, (N,H,W) 스택 —
각 레이어 foreground=obj_id, 겹침 보존)로도 발행(/harvest/mask은 한 장 라벨 이미지라 겹침 손실).
VLM 자동검출 시엔 익은 토마토 2D bbox를 /harvest/vlm_boxes_2d 로도 발행 —
std_msgs/Float32MultiArray, N×5 [x1,y1,x2,y2,score]를 score 내림차순 정렬(크롭된 좌표=/harvest/rgb와 동일).
일회성 검출이라 latched(TRANSIENT_LOCAL) QoS라 나중에 구독해도 마지막 결과를 받음
(ros2 topic echo /harvest/vlm_boxes_2d std_msgs/msg/Float32MultiArray --qos-durability transient_local).
캘리브 값 나오면 calib/T_base_camera.npy 교체. static TF --base-frame→--camera-frame(=T_base_camera) 도
발행하므로, rviz2에서 Fixed Frame = 설정한 base frame으로 두면 pointcloud·box·이미지가 정합돼 보인다
(기본 Fixed Frame=map이면 TF가 없어 안 보임).
--tomato-pose도 함께 주면 표준 메시지로 아래를 추가 발행한다(커스텀 메시지/colcon build 불필요).
/tomato/pose (geometry_msgs/PoseStamped) — position=토마토 중심, local +X=중심→꼭지, base frame/tomato/stem_direction (geometry_msgs/Vector3Stamped) — 위 +X 단위벡터, base frame/tomato/pose_status (std_msgs/String) — JSON 상태(requested/computing/retrying/valid/failed/stale),
target ID/초기 score/rank, center/stem source, retry 수, calibration/frame, pose_active--realtime 을 주면 latest-only(최신 프레임만, 스킵 = 라이브 시뮬레이션)로 동작.pose/가 있으면 world 좌표, 없으면 camera 좌표로 표시.--ffs-trt [engine] : FoundationStereo를 TensorRT fp16 엔진으로 실행 (PyTorch 대비 ~1.5배
빠름: 254ms→168ms @ 736×1280 RTX 4080 Laptop). 인자 생략 시 weights/ffs_trt/fast_foundationstereo.engine
사용. 엔진은 고정 해상도(736×1280 = HD720을 32의 배수로 패딩한 크기)로 빌드됨 — 다른 해상도면 재빌드 필요.
정확도: 토마토 작동거리(0.2–1.5m)에서 PyTorch 대비 depth median 차 ~9mm(sphere fitting의 median-band
±3cm 안), 경계/원거리 tail은 RANSAC+band가 흡수. 엔진 메모리는 TRT 자체 관리(torch 풀 외부).FoundationStereo TensorRT 엔진 빌드 (1회, --ffs-trt 용):
pip install onnx onnxscript # export 의존성 (tensorrt/trtexec는 이미 설치돼 있어야 함)
# 1) ONNX export — CPU에서 (GPU로 트레이싱하면 OOM: export는 fp32+비최적 cost-volume+그래프 전체 보존)
CUDA_VISIBLE_DEVICES="" python3 third_party/Fast-FoundationStereo/scripts/make_single_onnx_cpu.py \
--model_dir third_party/Fast-FoundationStereo/weights/weights/23-36-37/model_best_bp2_serialize.pth \
--save_path weights/ffs_trt --height 736 --width 1280 --valid_iters 8 --max_disp 192
# (make_single_onnx_cpu.py = 벤더 스크립트에 dynamo=False[레거시 exporter, adaptive_max_pool2d 지원] + CPU 트레이싱 추가한 사본)
# 2) 엔진 빌드 — GPU에서 (~16분; 빌드 중 .engine 파일 절대 건드리지 말 것)
trtexec --onnx=weights/ffs_trt/fast_foundationstereo.onnx \
--saveEngine=weights/ffs_trt/fast_foundationstereo.engine --fp16 --timingCacheFile=weights/ffs_trt/ffs_timing.cache
# 3) 사용
python3 perception/web_gui.py --crop 230 --vlm --ffs-trt
SVO(.svo2) → 시퀀스 추출 (rectified left/right + pose + K.npy):
python3 perception/extract_svo.py --svo data/line_5_r/line_5_r.svo2 --out data/line_5_r/seq
# VIEW.LEFT/RIGHT는 재생 시 SDK가 rectify → FoundationStereo에 바로 사용 가능
# positional tracking으로 프레임별 pose(world) 복원 → world 좌표 시각화 가능
모델을 먼저 모두 로드한 뒤 Rerun 뷰어를 띄우고 실시간 처리.
source /opt/ros/humble/setup.bash
python3 perception/live_rerun.py --source zed # 시작 시 물체를 화면 중앙에 두면 추적 시작
python3 perception/live_rerun.py --source replay # 카메라 없이 녹화 시퀀스로 재생
Rerun 창: 3D 포인트클라우드 + 노란 3D bbox + 이미지(초록 init박스 / 마젠타 추적박스). Ctrl-C 종료.
source /opt/ros/humble/setup.bash
python3 perception/precompute_results.py # 프레임별 구 결과 캐시 (1회)
python3 perception/ros2_rerun_publisher.py # /harvest/points, /harvest/box_markers 발행 + .rrd 저장
# 시각화: rerun data/tomato_seq/out/harvest.rrd
# 또는 rviz2 (Fixed Frame = camera_optical)
python3 perception/precompute_depth.py # 패스1: FFS 깊이 → data/tomato_seq/depth/
python3 perception/test_real.py # 패스2: 추적+amodal+구fit (프레임별 지표 출력)
python3 perception/make_viz.py # result.mp4 + montage.png
무거운 FFS를 별도 프로세스로 분리(2-패스)해 단일 프로세스 과부하를 피한다.
python3 perception/test_orchestrator.py # 최신 프레임만 처리(스킵) 동작 확인
data/tomato_seq/ — ZED 스테레오 시퀀스 (left/, right/, depth/, K.npy, 결과 out/)left/right + K.npy 저장 (실행 전 lsusb | grep 2b03:f880 로 카메라 확인).