YOLOv12로 공장 불량 자동 검사 시스템 구축하기

YOLOv12 파인튜닝으로 NG/OK 실시간 판별, RealSense 뎁스 카메라로 3D 좌표 추출 및 로봇 전송

2026.04.13

ObjectDetectionYOLOv12ComputerVisionRealSenseFinetuning

[미검증]

📌 0. 시리즈

응용편제목난이도핵심 기술
응용 1사진 10장으로 AI 캐릭터·프로필 이미지 만들기⭐⭐⭐FLUX.1 dev · LoRA · ComfyUI
응용 2내 목소리 AI 클론 — 유튜브 내레이터 자동화⭐⭐⭐F5-TTS · Kokoro · Sesame CSM-1B
응용 3상품 이미지 1장 → 15초 광고 영상 자동 생성⭐⭐⭐⭐Wan2.2 · HunyuanVideo 1.5 · LTX-2
응용 4공장 불량 자동 검사 — NG/OK 탐지 + 로봇 좌표 추출⭐⭐~⭐⭐⭐⭐⭐YOLOv12 · OpenCV · RealSense
응용 5영상 분위기 분석 → BGM 자동 생성 & 싱크⭐⭐⭐MusicGen · AudioCraft · Stable Audio Open
응용 6주제 한 줄 입력으로 유튜브 영상 완성 — AI 콘텐츠 자동화 파이프라인⭐⭐⭐⭐LangGraph · CrewAI · AutoGen 0.4
응용 7사진 보고 글 쓰는 AI — Vision LLM 상세페이지 자동 작성⭐⭐⭐⭐Qwen2.5-VL · InternVL3 · LLaVA-Next
응용 8내 PDF 문서를 AI가 읽는다 — 사내 지식 RAG 챗봇 구축⭐⭐⭐LlamaIndex · ChromaDB · Qdrant

📌 1. 들어가며

이 포스트에서 만들 것

이 포스트를 끝까지 따라하면 카메라 1대만으로 아래 결과물을 단계적으로 구축할 수 있습니다.

입력: 컨베이어 벨트 위 제품 실시간 영상
   ↓
[로직 1] YOLOv12 불량 탐지
   └─ 결과: NG/OK 실시간 판별 + 신호등/PLC 연동

[로직 2] RealSense 뎁스 카메라 추가
   └─ 결과: 불량 위치의 3D 좌표(X, Y, Z) 자동 추출

[로직 3] 로봇 팔 연동
   └─ 결과: 불량품 자동 집어서 불량 박스로 이동

3가지 로직 구조 설명

로직 1 — NG/OK 판별 (카메라 1대)
  난이도: ⭐⭐
  추가 장비: 없음 (웹캠 or USB 카메라)
  결과물: 불량 여부 판별 + 알람 출력

로직 2 — 3D 좌표 추출 (뎁스 카메라)
  난이도: ⭐⭐⭐
  추가 장비: Intel RealSense D435i
  결과물: 불량 위치 XYZ 좌표 → 로봇에 전달

로직 3 — Eye-in-Hand 로봇 제어
  난이도: ⭐⭐⭐⭐⭐
  추가 장비: 6축 로봇 팔 (UR, Fanuc 등)
  결과물: 불량품 자동 집기 → 폐기함 이동

💡 로직 1만으로도 현장에 바로 투입 가능합니다. 로직 2, 3은 로봇 자동화가 필요한 고급 환경에서 순차적으로 추가합니다.


📌 2. 환경 준비

2-1. 하드웨어

로직 1 — 일반 카메라:

권장 카메라:
  - 산업용 USB 카메라 (Basler, IDS) → 최고 품질
  - 웹캠 (Logitech C920, C1000e) → 입문용
  - IP 카메라 → RTSP 스트림으로 연결

카메라 설치 위치:
  - 컨베이어 벨트 정중앙 위 (Top-down 뷰)
  - 제품과 카메라 거리: 30~50cm 권장
  - 조명: 링라이트 or 면발광 조명 (그림자 최소화)

조명 선택 기준:
  반사성 제품 (금속, 유리) → 돔 조명 (Dome Light)
  일반 제품 (플라스틱, 고무) → 링라이트
  표면 흠집 탐지 → 저각 조명 (Grazing Light)

로직 2, 3 — Intel RealSense D435i:

RealSense D435i 스펙:
  RGB 해상도: 1920x1080 @ 30fps
  Depth 해상도: 1280x720 @ 30fps
  Depth 범위: 0.1m ~ 10m
  Depth 정확도: ±2% (1m 기준 ±2cm)
  인터페이스: USB 3.0 Type-C

구매처: 인텔 공식몰, 디지키, 아마존 (약 20~30만원)

2-2. 소프트웨어 설치

bash# Python 환경 생성
python -m venv venv
source venv/bin/activate       # Linux/Mac
venv\Scripts\activate          # Windows

# 핵심 라이브러리 설치
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
pip install ultralytics          # YOLOv12 포함
pip install opencv-python
pip install pyrealsense2         # Intel RealSense SDK

# 추가 라이브러리
pip install numpy pillow
pip install pyserial             # PLC 시리얼 통신
pip install requests             # 로봇 API 통신

RealSense SDK 별도 설치 (Linux):

bash# Intel RealSense SDK 2.0
sudo apt-key adv --keyserver keyserver.ubuntu.com \
  --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCD
sudo add-apt-repository \
  "deb https://librealsense.intel.com/Debian/apt-repo $(lsb_release -cs) main"
sudo apt update
sudo apt install librealsense2-dkms librealsense2-utils

📌 3. [로직 1] NG/OK 판별 시스템

3-1. 학습 데이터 수집

불량품 / 정상품 이미지 촬영 가이드:

수집 기준:
  OK (정상품): 200장 이상
  NG (불량품): 불량 유형별 100장 이상

  예시 클래스 구성:
  ├─ OK         → 정상
  ├─ Scratch    → 표면 긁힘
  ├─ Dent       → 찌그러짐
  ├─ Crack      → 균열
  └─ Missing    → 부품 누락

촬영 환경 통일 (매우 중요):
  ✅ 실제 검사 라인과 동일한 카메라 위치/거리
  ✅ 동일한 조명 조건
  ✅ 다양한 제품 방향 (회전 포함)
  ❌ 다른 환경에서 찍은 이미지 혼합 금지

데이터 증강 전략 (데이터 부족 시):

pythonfrom ultralytics.data.augment import Albumentations
import albumentations as A

# 증강 파이프라인
transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.VerticalFlip(p=0.3),
    A.Rotate(limit=30, p=0.5),
    A.RandomBrightnessContrast(p=0.3),
    A.GaussNoise(p=0.2),
    A.Blur(blur_limit=3, p=0.2),
    A.CLAHE(p=0.2),           # 조명 변화 대응
])

# 100장 → 500장으로 증강
import cv2, os, numpy as np

def augment_dataset(input_dir, output_dir, multiply=5):
    os.makedirs(output_dir, exist_ok=True)
    images = [f for f in os.listdir(input_dir) if f.endswith('.jpg')]

    for img_name in images:
        img = cv2.imread(os.path.join(input_dir, img_name))
        for i in range(multiply):
            augmented = transform(image=img)['image']
            save_name = f"{img_name[:-4]}_aug{i}.jpg"
            cv2.imwrite(os.path.join(output_dir, save_name), augmented)

augment_dataset("./dataset/ng/scratch", "./dataset/ng/scratch_aug", multiply=5)

Roboflow로 바운딩박스 라벨링:

1. https://roboflow.com 회원가입 (무료)
2. New Project → Object Detection 선택
3. 이미지 업로드 (드래그 앤 드롭)
4. 라벨링 도구로 불량 위치 바운딩박스 그리기
5. Export → YOLOv12 형식으로 다운로드
   └─ data.yaml + images/ + labels/ 폴더 구조 생성

자동 라벨링 기능 (Roboflow Auto-Label):
  → 첫 50장만 수동 라벨링 후
  → Auto-Label로 나머지 자동 처리
  → 검수 후 수정

data.yaml 구조:

yamlpath: ./dataset
train: images/train
val:   images/val
test:  images/test

nc: 5   # 클래스 수
names:
  0: OK
  1: Scratch
  2: Dent
  3: Crack
  4: Missing

3-2. YOLOv12 파인튜닝

클래스 설정 및 학습 실행:

pythonfrom ultralytics import YOLO

# YOLOv12 모델 로드 (사전학습 가중치)
# n=나노(가장 빠름), s=스몰, m=미디움, l=라지, x=엑스라지
model = YOLO("yolo12s.pt")   # 균형 잡힌 크기 권장

# 파인튜닝 실행
results = model.train(
    data    = "./dataset/data.yaml",
    epochs  = 100,
    imgsz   = 640,
    batch   = 16,
    device  = 0,           # GPU 0번 사용 (CPU: "cpu")
    workers = 4,
    project = "./runs",
    name    = "defect_v1",

    # 학습률 설정
    lr0    = 0.01,         # 초기 학습률
    lrf    = 0.01,         # 최종 학습률 비율 (lr0 * lrf)

    # 데이터 증강
    flipud = 0.3,          # 상하 반전 30%
    fliplr = 0.5,          # 좌우 반전 50%
    hsv_h  = 0.015,        # 색상 변화
    hsv_s  = 0.7,          # 채도 변화
    mosaic = 1.0,          # 모자이크 증강

    # 학습 전략
    patience  = 20,        # 20 epoch 개선 없으면 조기 종료
    save      = True,
    save_period = 10,      # 10 epoch마다 체크포인트 저장
)

print(f"최고 mAP50: {results.results_dict['metrics/mAP50(B)']:.4f}")

학습 파라미터 기준표:

epochs:
  50  epoch → 빠른 테스트 (정확도 낮음)
  100 epoch → 권장 (대부분의 경우 충분)
  200 epoch → 소량 데이터 or 고난도 불량 시

batch:
  VRAM 8GB  → batch=8
  VRAM 16GB → batch=16
  VRAM 24GB → batch=32

imgsz:
  640  → 표준, 권장
  1280 → 작은 불량(스크래치, 핀홀) 탐지 시

학습 결과 mAP 해석:

mAP50 (IoU 0.5):
  < 0.5   → 학습 부족 (데이터 추가 or 에포크 증가)
  0.5~0.7 → 기본 동작 가능
  0.7~0.9 → 실용적인 수준
  > 0.9   → 고품질 (현장 투입 권장 기준)

mAP50-95 (다양한 IoU 기준 평균):
  > 0.7   → 현장 투입 권장

Precision (정밀도): NG를 NG라고 맞춘 비율
Recall    (재현율): 실제 NG 중 잡아낸 비율
  → 제조업에서는 Recall 이 더 중요 (불량 놓치면 큰 일)
  → Recall > 0.95 목표

3-3. 실시간 카메라 추론 코드

pythonimport cv2
import serial
import time
from ultralytics import YOLO

# 학습된 모델 로드
model = YOLO("./runs/defect_v1/weights/best.pt")

# PLC 시리얼 통신 설정 (로직 1 연동)
# plc = serial.Serial('COM3', baudrate=9600, timeout=1)

# 판정 기준
CONFIDENCE_THRESHOLD = 0.65   # 65% 이상일 때만 NG 판정
NG_CLASSES = [1, 2, 3, 4]    # Scratch, Dent, Crack, Missing

def send_signal_to_plc(is_ng, plc=None):
    """PLC에 OK/NG 신호 전송"""
    signal = b'\x01' if is_ng else b'\x00'
    if plc:
        plc.write(signal)

def draw_result(frame, result, is_ng):
    """결과 시각화"""
    color  = (0, 0, 255) if is_ng else (0, 255, 0)
    label  = "NG" if is_ng else "OK"

    # 판정 박스
    cv2.rectangle(frame, (10, 10), (200, 60), color, -1)
    cv2.putText(frame, label, (30, 50),
                cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 3)

    # 탐지 바운딩박스
    for box in result.boxes:
        if float(box.conf) < CONFIDENCE_THRESHOLD:
            continue
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        cls_name = model.names[int(box.cls)]
        conf     = float(box.conf)
        cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2)
        cv2.putText(frame, f"{cls_name} {conf:.2f}",
                    (x1, y1 - 8),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
    return frame

# 실시간 추론 루프
cap = cv2.VideoCapture(0)   # 0: 기본 웹캠 / "rtsp://...": IP 카메라

while cap.isOpened():
    ret, frame = cap.read()
    if not ret:
        break

    # YOLOv12 추론
    results = model(frame, conf=CONFIDENCE_THRESHOLD, verbose=False)
    result  = results[0]

    # NG 여부 판단
    detected_classes = [int(c) for c in result.boxes.cls]
    is_ng = any(c in NG_CLASSES for c in detected_classes)

    # PLC 신호 전송
    send_signal_to_plc(is_ng)

    # 화면 표시
    frame = draw_result(frame, result, is_ng)
    cv2.imshow("Defect Detection", frame)

    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

📌 4. [로직 2] 3D 좌표 추출

4-1. RealSense 뎁스 카메라 개념

RGB 픽셀 → Depth → 3D(XYZ) 변환 원리:

일반 카메라: 픽셀(u, v)만 알 수 있음 → 거리 정보 없음

RealSense D435i:
  ├─ RGB 카메라 → 색상 픽셀(u, v) 제공
  ├─ IR 스테레오 카메라 → 각 픽셀의 깊이값 D(u,v) 제공
  └─ 두 값을 조합 → 3D 좌표 (X, Y, Z) 계산

변환 수식 (Deprojection):
  X = (u - cx) * Z / fx
  Y = (v - cy) * Z / fy
  Z = D(u, v)               ← 뎁스 카메라에서 직접 획득

  fx, fy: 초점 거리 (카메라 Intrinsic 파라미터)
  cx, cy: 주점 (이미지 중심)
  → RealSense SDK가 자동으로 제공
시각적 이해:

카메라                   제품
  O ─────────────────── [불량위치]
  │  픽셀(u=320, v=240)
  │  깊이 Z = 0.45m
  │
  → X = (320-640/2) * 0.45 / 600 = 0.0m   (중앙)
  → Y = (240-360/2) * 0.45 / 600 = -0.09m (위쪽)
  → Z = 0.45m                               (거리)

4-2. 카메라 캘리브레이션

pythonimport pyrealsense2 as rs
import numpy as np

def init_realsense():
    """RealSense 카메라 초기화 및 Intrinsic 파라미터 획득"""
    pipeline = rs.pipeline()
    config   = rs.config()

    # 스트림 설정
    config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30)
    config.enable_stream(rs.stream.depth, 1280, 720, rs.format.z16,  30)

    profile  = pipeline.start(config)

    # Intrinsic 파라미터 자동 획득
    depth_sensor    = profile.get_device().first_depth_sensor()
    depth_scale     = depth_sensor.get_depth_scale()   # 단위 변환 계수

    color_stream    = profile.get_stream(rs.stream.color)
    intrinsics      = color_stream.as_video_stream_profile().get_intrinsics()

    print(f"초점거리: fx={intrinsics.fx:.2f}, fy={intrinsics.fy:.2f}")
    print(f"주점:     cx={intrinsics.ppx:.2f}, cy={intrinsics.ppy:.2f}")
    print(f"Depth Scale: {depth_scale}")

    # Depth-Color 정렬 (픽셀 좌표 일치화)
    align = rs.align(rs.stream.color)

    return pipeline, align, intrinsics, depth_scale

pipeline, align, intrinsics, depth_scale = init_realsense()

4-3. 불량 위치 좌표 → 로봇 TCP 전송 코드

pythonimport pyrealsense2 as rs
import numpy as np
import cv2
import socket
from ultralytics import YOLO

model = YOLO("./runs/defect_v1/weights/best.pt")

def pixel_to_3d(depth_frame, intrinsics, u, v):
    """
    픽셀 좌표 (u, v) → 3D 좌표 (X, Y, Z) 변환
    단위: meter
    """
    depth = depth_frame.get_distance(u, v)   # 해당 픽셀의 깊이값 (m)

    if depth == 0:
        return None   # 뎁스 값 없음 (반사체, 투명체 등)

    point_3d = rs.rs2_deproject_pixel_to_point(
        intrinsics, [u, v], depth
    )
    return point_3d   # [X, Y, Z] in meters

def send_to_robot(x, y, z, robot_ip="192.168.1.100", port=30002):
    """
    로봇 컨트롤러에 TCP 좌표 전송 (UR 로봇 기준)
    단위: mm 변환 후 전송
    """
    x_mm, y_mm, z_mm = x * 1000, y * 1000, z * 1000
    command = f"movej(p[{x_mm:.2f},{y_mm:.2f},{z_mm:.2f},0,3.14,0], a=1.2, v=0.25)\n"

    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((robot_ip, port))
            s.sendall(command.encode())
        print(f"로봇 이동 명령 전송: X={x_mm:.1f}mm, Y={y_mm:.1f}mm, Z={z_mm:.1f}mm")
    except ConnectionRefusedError:
        print("로봇 연결 실패 — IP/포트 확인")

# 실시간 루프
pipeline, align, intrinsics, depth_scale = init_realsense()

while True:
    frames         = pipeline.wait_for_frames()
    aligned_frames = align.process(frames)

    color_frame = aligned_frames.get_color_frame()
    depth_frame = aligned_frames.get_depth_frame()
    if not color_frame or not depth_frame:
        continue

    color_image = np.asanyarray(color_frame.get_data())

    # YOLOv12 불량 탐지
    results = model(color_image, conf=0.65, verbose=False)
    result  = results[0]

    for box in result.boxes:
        cls = int(box.cls)
        if cls == 0:            # OK → 건너뜀
            continue

        # 바운딩박스 중심 픽셀 계산
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        center_u = (x1 + x2) // 2
        center_v = (y1 + y2) // 2

        # 픽셀 → 3D 좌표 변환
        point_3d = pixel_to_3d(depth_frame, intrinsics, center_u, center_v)

        if point_3d:
            X, Y, Z = point_3d
            defect_name = model.names[cls]
            print(f"불량 [{defect_name}] 위치: X={X:.3f}m, Y={Y:.3f}m, Z={Z:.3f}m")

            # 로봇에 좌표 전송
            send_to_robot(X, Y, Z)

            # 시각화
            cv2.circle(color_image, (center_u, center_v), 8, (0,0,255), -1)
            cv2.putText(color_image,
                        f"{defect_name} ({X:.2f},{Y:.2f},{Z:.2f})m",
                        (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 2)

    cv2.imshow("Defect + 3D Coord", color_image)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

pipeline.stop()
cv2.destroyAllWindows()

📌 5. [로직 3] Eye-in-Hand 로봇 제어

5-1. Eye-in-Hand vs Eye-to-Hand 차이

Eye-to-Hand (카메라 고정):
  카메라
    │  (천장 or 프레임에 고정)
    ↓
  [컨베이어] ─── 제품 ───────────────
                    ↑
                  로봇 팔

  장점: 카메라 캘리브레이션 1회로 끝
  단점: 로봇이 제품을 집을 때 카메라 시야 가려짐

Eye-in-Hand (카메라 로봇에 부착):
  카메라
    │  (로봇 끝단 Flange에 부착)
    ↓
  로봇 팔 ─── 카메라가 같이 이동
    ↓
  제품 바로 위에서 촬영 후 집기

  장점: 집기 직전 정밀 위치 보정 가능
  단점: 로봇 움직임마다 카메라 좌표가 바뀜 → 캘리브레이션 필요

5-2. Hand-Eye 캘리브레이션 절차

pythonimport numpy as np
import cv2

def hand_eye_calibration(robot_poses, camera_poses):
    """
    Hand-Eye 캘리브레이션
    - robot_poses:  로봇 base → flange 변환 행렬 목록 (N개)
    - camera_poses: 카메라 → 체커보드 변환 행렬 목록 (N개)
    - 반환: 카메라 → 로봇 flange 변환 행렬 (R_cam2gripper, t_cam2gripper)
    """
    R_gripper2base = []
    t_gripper2base = []
    R_target2cam   = []
    t_target2cam   = []

    for rp, cp in zip(robot_poses, camera_poses):
        R_gripper2base.append(rp[:3, :3])
        t_gripper2base.append(rp[:3, 3])
        R_target2cam.append(cp[:3, :3])
        t_target2cam.append(cp[:3, 3])

    R_cam2gripper, t_cam2gripper = cv2.calibrateHandEye(
        R_gripper2base, t_gripper2base,
        R_target2cam,   t_target2cam,
        method=cv2.CALIB_HAND_EYE_TSAI   # 가장 안정적인 방법
    )
    return R_cam2gripper, t_cam2gripper

# 사용 예시 (최소 15개 자세 권장)
# 체커보드를 다양한 각도에서 15~30회 촬영하며
# 각 자세에서의 로봇 TCP 좌표와 카메라 검출 좌표를 수집

캘리브레이션 절차 순서:

1. 체커보드 (7x5, 20mm) 준비 → 작업 테이블에 고정
2. 로봇을 15~30개 다양한 자세로 이동
3. 각 자세에서:
   ├─ 로봇 TCP 변환행렬 기록 (로봇 컨트롤러에서 획득)
   └─ 카메라로 체커보드 코너 검출
4. calibrateHandEye() 실행 → R, t 행렬 획득
5. 결과 저장 → 운영 시 불러서 사용

5-3. 실시간 로봇 제어 루프 구성

pythonimport numpy as np
import pyrealsense2 as rs
import cv2
from ultralytics import YOLO

# 캘리브레이션 결과 로드
R_cam2gripper = np.load("./calibration/R_cam2gripper.npy")
t_cam2gripper = np.load("./calibration/t_cam2gripper.npy")

model = YOLO("./runs/defect_v1/weights/best.pt")

def camera_to_robot_frame(point_camera, R, t):
    """
    카메라 좌표 → 로봇 Flange 좌표 변환
    """
    p_cam    = np.array(point_camera).reshape(3, 1)
    p_robot  = R @ p_cam + t
    return p_robot.flatten()   # [X, Y, Z]

def robot_pick_and_place(robot_x, robot_y, robot_z,
                          drop_x=0.3, drop_y=0.5, drop_z=0.1):
    """
    집기(Pick) → 옮기기(Place) 동작 시퀀스
    실제 로봇 SDK에 맞게 수정 필요
    """
    # 1. 불량품 위 이동 (Z + 0.1m 위에서 접근)
    move_robot(robot_x, robot_y, robot_z + 0.1, speed=0.3)
    # 2. 천천히 하강
    move_robot(robot_x, robot_y, robot_z, speed=0.05)
    # 3. 그리퍼 닫기
    gripper_close()
    # 4. 위로 들기
    move_robot(robot_x, robot_y, robot_z + 0.15, speed=0.2)
    # 5. 불량 박스로 이동
    move_robot(drop_x, drop_y, drop_z + 0.1, speed=0.5)
    # 6. 그리퍼 열기 (폐기)
    gripper_open()
    print("불량품 제거 완료")

# 실시간 제어 루프
pipeline, align, intrinsics, depth_scale = init_realsense()

while True:
    frames         = pipeline.wait_for_frames()
    aligned_frames = align.process(frames)
    color_frame    = aligned_frames.get_color_frame()
    depth_frame    = aligned_frames.get_depth_frame()
    if not color_frame or not depth_frame:
        continue

    color_image = np.asanyarray(color_frame.get_data())
    results     = model(color_image, conf=0.7, verbose=False)

    for box in results[0].boxes:
        if int(box.cls) == 0:
            continue   # OK → 건너뜀

        # 픽셀 → 카메라 3D 좌표
        x1, y1, x2, y2 = map(int, box.xyxy[0])
        cu = (x1 + x2) // 2
        cv_coord = (y1 + y2) // 2

        point_cam = pixel_to_3d(depth_frame, intrinsics, cu, cv_coord)
        if point_cam is None:
            continue

        # 카메라 좌표 → 로봇 좌표
        point_robot = camera_to_robot_frame(point_cam, R_cam2gripper, t_cam2gripper)
        X_r, Y_r, Z_r = point_robot

        print(f"로봇 좌표: X={X_r:.3f}, Y={Y_r:.3f}, Z={Z_r:.3f} [m]")

        # 로봇 Pick & Place 실행
        robot_pick_and_place(X_r, Y_r, Z_r)

    cv2.imshow("Eye-in-Hand Control", color_image)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

pipeline.stop()
cv2.destroyAllWindows()

📌 6. 결과 확인 & 트러블슈팅

탐지 오검출(False Positive) 줄이는 법

문제: 정상품을 불량으로 판단하는 경우

원인 1: Confidence Threshold 너무 낮음
  → conf=0.5 → conf=0.7로 높이기
  → 신뢰도 낮은 탐지는 무시

원인 2: 학습 데이터에 OK 샘플 부족
  → OK 클래스 이미지를 더 수집
  → 현장 조명 변화 상황에서 촬영

원인 3: 조명이 불균일하여 그림자가 스크래치로 인식
  → 면발광 조명(Diffuse Light) 으로 교체
  → 조명 방향 조정 (상단 직하방)

원인 4: 모델 과적합
  → 검증 데이터(val) mAP와 학습(train) mAP 차이 확인
  → dropout, weight_decay 추가
  → 학습 데이터 다양성 강화

좌표 오차 보정

python# 뎁스 값 평균화 (노이즈 감소)
def get_stable_depth(depth_frame, u, v, window=5):
    """
    단일 픽셀 대신 window x window 영역의 중앙값 사용
    → 뎁스 노이즈 50% 이상 감소
    """
    depths = []
    for du in range(-window//2, window//2 + 1):
        for dv in range(-window//2, window//2 + 1):
            d = depth_frame.get_distance(u + du, v + dv)
            if d > 0:
                depths.append(d)

    if not depths:
        return 0
    return float(np.median(depths))   # 중앙값으로 노이즈 제거

# 좌표 오프셋 보정 (현장 테스트 후 조정)
OFFSET_X = 0.005    # X 방향 5mm 보정
OFFSET_Y = -0.003   # Y 방향 -3mm 보정
OFFSET_Z = 0.010    # Z 방향 10mm 보정

def apply_offset(x, y, z):
    return x + OFFSET_X, y + OFFSET_Y, z + OFFSET_Z

실시간 처리 속도 최적화

python# 목표: 30fps 이상 (컨베이어 속도에 맞게 조정)

# 1. 모델 크기 선택
#    yolo12n → 가장 빠름, 정확도 낮음
#    yolo12s → 균형 (권장)
#    yolo12m → 정확도 높음, 속도 낮음

# 2. TensorRT 변환 (추론 속도 3~5배 향상)
model = YOLO("./runs/defect_v1/weights/best.pt")
model.export(format="engine", device=0, half=True)
# → best.engine 생성

# TensorRT 모델로 추론
model_trt = YOLO("./runs/defect_v1/weights/best.engine")

# 3. 입력 해상도 낮추기
results = model(frame, imgsz=320, verbose=False)   # 640 → 320

# 4. FPS 측정
import time
fps_history = []

while True:
    start = time.time()
    # ... 추론 코드 ...
    elapsed = time.time() - start
    fps     = 1 / elapsed
    fps_history.append(fps)

    if len(fps_history) > 30:
        print(f"평균 FPS: {sum(fps_history[-30:]) / 30:.1f}")
        fps_history = fps_history[-30:]

완성 체크리스트

  • [로직 1] Roboflow 라벨링 완료 (클래스별 100장 이상)
  • [로직 1] YOLOv12 학습 mAP50 > 0.80 달성
  • [로직 1] 실시간 카메라 추론 + NG 신호 출력 확인
  • [로직 2] RealSense D435i 연결 및 뎁스 스트림 확인
  • [로직 2] 픽셀 → 3D 좌표 변환 오차 ±5mm 이내
  • [로직 3] Hand-Eye 캘리브레이션 15자세 이상 수집 완료
  • [로직 3] Pick & Place 자동 동작 확인