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 자동 동작 확인