YOLO
사진·영상에서 물체를 찾아내는 AI 기술
사진·영상에서 물체를 찾아내는 AI 기술
분류 · 탐지 · 자세 감지 · 분할
YOLO 창시자 이야기
YOLO의 시작 — 조셉 레드먼 TED
10년 전 컴퓨터는 강아지·고양이도 구별 어려운 수준, 오늘은 99% 이상 정확도. 그 비결이 바로 YOLO
YOLO란?
• You Only Look Once — "한 번만 본다"
• 조셉 레드먼(Joseph Redmon)이 만든 오픈소스 객체 탐지 AI
• 이미지·영상 속 모든 사물을 번개처럼 빠르게 찾기
• 얼룩말, 정지 신호, 사람, 자동차 등 다 인식
어디에 쓰일까?
• 🚗 자율주행차 — 도로의 차·사람·신호 인식
• 🤖 로봇 공학 — 물건 집기, 길 찾기
• 🏥 의료 — 암 진단 보조
강연 영상 (영어, 자막 지원)
▶ YouTube에서 보기 — How computers learn to recognize objects instantly | Joseph Redmon (TED)
분류 (Classification)
분류 (Classification)
사진을 보여주면 "이게 뭐야?"를 맞추는 기능이에요. 강아지 사진을 넣으면 AI가 "강아지!"라고 대답하는 것처럼요. 한 사진에 하나의 정답을 출력합니다.
사용 모델
• yolo11n-cls.pt — 분류 전용 모델 (n = 가장 빠른 버전)
• 크기 순: n → s → m → l → x (작을수록 빠름, 클수록 정확)
전체 코드
• results[0].plot() — 예측 결과를 이미지에 그려줌
• cv2.imwrite() — 그 이미지를 파일로 저장
탐지 (Detection)
탐지 (Detection)
사진이나 영상에서 물체의 위치를 네모 박스(Bounding Box)로 표시해요. 사람이 몇 명인지, 차가 어디 있는지 찾아낼 수 있어요. 분류와 달리 여러 물체를 동시에 탐지합니다.
분류 vs 탐지 차이
• 분류(cls) — "이 사진은 강아지" (전체 사진 하나의 답)
• 탐지(det) — "여기 사람, 저기 자동차, 저기 의자" (위치까지 박스로 표시)
전체 코드
자세 감지 (Pose)
자세 감지 (Pose Estimation)
사람의 관절 17개 위치(코, 눈, 귀, 어깨, 팔꿈치, 손목, 엉덩이, 무릎, 발목)를 점으로 표시해요. 운동 자세 분석, 댄스 동작 인식, 낙상 감지 등에 활용할 수 있어요.
전체 코드
• yolo11n-pose.pt — 자세 감지 전용 모델
• results[0].plot() — 관절 17개 점·선을 이미지 위에 그려줌
• cv2.imwrite() — 결과 이미지 파일로 저장
분할 (Segmentation)
분할 (Segmentation)
탐지가 "네모 박스"라면, 분할은 물체의 정확한 외곽선을 따내요. 사람을 감지할 때 머리카락 한 올까지 정확히 구분할 수 있어요. 사진 배경 제거, 자율주행 도로 인식 등에 사용돼요.
탐지 vs 분할 차이
• 탐지(det) — 물체 주변에 네모 박스만 그림
• 분할(seg) — 물체 모양 그대로 색칠 (픽셀 단위)
전체 코드
코드 구조는 자세 감지와 동일해요. 달라지는 것은 모델 파일 이름(yolo11n-seg.pt)뿐이에요.
분류 모델 학습
YOLO 이미지 분류 모델 학습
미리 학습된 yolo11n-cls.pt를 토대로 새 클래스를 추가 학습. 정확히는 전이학습(Transfer Learning)의 한 기법인 Fine-tuning — pretrained 가중치를 출발점 삼아 새 데이터에 맞춰 전체 가중치를 미세 조정하는 방식 (분류 헤드는 새 클래스 수에 맞춰 자동 교체). train으로 학습, val로 성능 검증, test로 최종 확인하는 3단 구성
데이터셋 폴더 구조
• train/ — 모델이 학습할 이미지 (보통 전체의 70~80%)
• val/ — 학습 중 매 epoch마다 성능 측정용 (10~15%)
• test/ — 학습 후 최종 평가용 (10~15%)
• 클래스별 이미지 수량은 통일 (불균형 시 한쪽으로 치우친 모델이 됨)
val/ 검증셋 자세히
val(validation) = "모델이 새 이미지에도 잘 맞히는지" 학습 중간에 점검하는 모의고사 세트
• 역할 — 매 epoch가 끝날 때마다 val 이미지를 자동 추론, 정답률(val accuracy)·손실(val loss) 측정
• 학습엔 사용 안 함 — val 이미지는 가중치를 바꾸지 않음, 오직 채점용
• best.pt 선정 기준 — val 성능이 가장 좋았던 epoch의 가중치를 자동 저장
• train ↔ val 분리 필수 — 같은 이미지가 양쪽에 있으면 "시험 문제 유출" 상태, 점수가 부풀려짐
• val 이미지 선택 기준 — train과 비슷한 분포지만 다른 사진 (다양한 각도·조명·배경 포함)
• 수량 가이드 — 클래스당 최소 20~30장, 너무 적으면 점수가 우연에 휘둘림
전체 코드
핵심 파라미터 설명
• data — 데이터셋 폴더 경로 (안에 train/val 자동 인식)
• epochs — 전체 데이터를 몇 번 반복 학습할지 (많을수록 정확도↑, 시간↑, 너무 많으면 과적합)
• batch — 한 번에 GPU/CPU에 올릴 이미지 수 (메모리 허용 범위 내에서 클수록 빠름)
• imgsz — 입력 이미지 한 변 크기 (256/640 자주 사용, 클수록 정확↑ 속도↓)
과적합 (Overfitting) 이해
과적합 = 모델이 train 이미지를 "외워버린" 상태. 시험 본 문제는 100점인데 새 문제는 못 푸는 학생과 같음
• 비유 — 수학 문제집을 통째로 암기한 학생 → 같은 문제는 다 맞지만 살짝 바뀐 문제는 0점
• 증상 — train accuracy ↑↑ 인데 val accuracy ↓, 둘의 격차가 점점 벌어짐
• 원인
① epochs 너무 많음 → 같은 데이터 반복해서 디테일까지 외움
② 데이터 양 부족 → 다양성 없어 패턴 일반화 실패
③ 모델 용량 과다 → 작은 데이터에 너무 큰 모델
• 대응책
① epochs 줄이기 또는 EarlyStopping (val 성능 정체 시 자동 중단, YOLO는 patience 파라미터)
② 데이터 증강 (Augmentation — 회전·뒤집기·밝기 변경, YOLO 기본 활성)
③ 데이터 양 늘리기 (가장 확실)
④ best.pt 사용 (last.pt 대신 — val 최고점 시점 가중치)
• 반대 개념 — 과소적합(Underfitting) = 너무 덜 배움, train·val 둘 다 낮음. epochs 늘리거나 더 큰 모델 필요
과적합 그래프 — Loss 곡선으로 보기
아래는 train loss(파란선)와 val loss(분홍선)의 epoch별 추이. val loss가 최저점을 찍은 뒤 다시 오르기 시작하는 지점이 과적합 시작 신호 — 그 직전 epoch의 가중치(best.pt)를 사용하는 것이 정답
• 파란선 (train) — 계속 떨어짐. 모델이 train 데이터를 점점 더 잘 외움
• 분홍선 (val) — 처음엔 같이 떨어지다가 epoch 15 부근에서 반등
• 노란 별 — val loss 최저점 = best.pt로 저장되는 시점
• 그 이후 — train은 더 좋아지지만 새 이미지 성능은 나빠지는 과적합 구간
참고 자료 (Google 공식)
• Google ML Crash Course — Overfitting (training/validation loss 곡선 공식 설명)
• Google ML Glossary — Overfitting 용어 정의
• Google ML Crash Course — Generalization (일반화 능력)
트레이드오프 (Trade-off) 이해
트레이드오프 = "하나를 얻으면 다른 하나를 잃는" 양자택일 관계. 둘 다 최대화 불가능, 균형점 찾기가 핵심
• 일상 비유 — 라면 빨리 끓이려고 불 세게 → 면이 덜 익음. 속도 vs 품질
• AI 학습에서 자주 만나는 트레이드오프
① 정확도 ↔ 속도 — 큰 모델(yolo11x) 정확하지만 느림, 작은 모델(yolo11n) 빠르지만 덜 정확
② imgsz ↑ ↔ 학습 시간 ↑ — 큰 이미지 정밀하지만 메모리·시간 폭증
③ batch ↑ ↔ 메모리 ↑ — 큰 배치 빠르지만 GPU 메모리 부족 가능
④ epochs ↑ ↔ 과적합 위험 ↑ — 더 학습할수록 train 점수는 오르지만 일반화는 어느 순간부터 떨어짐
⑤ 모델 복잡도 ↔ 일반화 능력 — 복잡한 모델은 train은 잘 맞히지만 새 데이터엔 약함
• 실전 의미 — "가장 좋은 설정"은 없음. 하드웨어·시간·목표 정확도에 맞춰 균형점을 실험으로 찾는 과정이 학습의 본질
학습 결과물
학습이 끝나면 runs/classify/train/weights/ 폴더에 두 파일이 생겨요
• best.pt — val 성능이 가장 좋았던 시점의 가중치 (실전 사용 권장)
• last.pt — 마지막 epoch 가중치
학습 곡선·혼동행렬은 runs/classify/train/에서 PNG로 확인 가능
미션 — 표정 분류 챌린지
Happy · Sad · Normal 3가지 표정을 분류하는 나만의 모델 학습
• 이미지 크기 — 256 고정
• 배치 사이즈 — 자유
• epochs — 자유
• 클래스별 이미지 수량 통일 (예: 각 100장씩)
시상 — 성능 1등 모델 표창
• 가장 우수한 성능 모델 측정 후 시상
• 모델 완성 후 GitHub 업로드, 링크 공유
• 평가 지표 — val accuracy, confusion matrix, 일반화 능력
꿀팁 — 이미지 수집 도구
Chrome 웹 스토어 → Image Downloader 확장 프로그램으로 웹페이지의 모든 이미지를 한 번에 다운로드 가능. 수집 후 표정별로 폴더 분류해 dataset 구조에 맞춰 정리
추론 결과 저장
YOLO 추론 결과 저장
save=True 파라미터로 추론 결과를 자동 저장. model.names로 현재 모델의 클래스 목록 확인 가능
추론 파라미터 튜닝
YOLO 추론 파라미터 튜닝
추론 시 다양한 파라미터로 결과를 세밀하게 제어. model.names로 클래스 확인 후 원하는 클래스만 탐지 가능
주요 파라미터
save=True=결과 자동 저장 · conf=신뢰도 임계값(기본 0.25) · imgsz=추론 이미지 크기(기본 640) · max_det=최대 탐지 수 · save_crop=탐지 영역 저장 · save_txt=좌표 텍스트 저장 · save_conf=신뢰도 저장
원하는 객체 탐지
YOLO 원하는 객체만 탐지
퀴즈
Q. 한 장의 사진에서 사람과 자동차만 탐지하려면, 어떤 파라미터에 어떤 값을 넣어야 할까요?
힌트
• model.names로 클래스 ID ↔ 이름 매핑 확인 가능
• COCO 사전학습 모델은 80개 클래스 (사람·동물·차량·가구 등)
• 정답은 모자이크로 가려져 있음 — 직접 추론해서 작성
전체 코드
• 모자이크 영역 — 정답 비공개. 직접 작성해서 실행으로 확인
HTTP 스트림 (CCTV)
HTTP 실시간 CCTV 영상 받기
웹캠 대신 인터넷 m3u8 주소를 열어 실시간 CCTV 프레임 수신. 도시교통정보센터(UTIC)에서 공개된 CCTV 스트림 URL 사용
m3u8 이란?
m3u8 = HLS(HTTP Live Streaming) 재생 목록 파일. 영상을 수 초 단위 작은 조각으로 쪼개 순서대로 받아 재생 → 끊김 없이 실시간 송출 가능. 유튜브 라이브·CCTV·OTT 라이브가 같은 방식
스트림 URL 얻는 방법
1) utic.go.kr/map/map.do?menu=cctv 접속 · 2) 지도에서 원하는 CCTV 클릭 · 3) 영상 위에서 우클릭 → Copy Video Address · 4) 복사된 .m3u8 주소를 코드에 붙여넣기
• stream_url — CCTV의 m3u8 주소. UTIC에서 발급된 실시간 영상 경로
• cv2.VideoCapture(stream_url) — 웹캠 번호 대신 URL을 넣어 네트워크 영상 열기
• 나머지 흐름은 웹캠 코드와 동일 — q 키로 종료
CCTV 스트림 + YOLO 추론
CCTV 영상을 그대로 띄우기만 하던 코드에서, 프레임 한 장마다 YOLO 추론을 걸면 실시간 CCTV에서 차량·사람을 탐지하는 화면이 됨. 웹캠 코드와 비교해 늘어나는 건 딱 두 줄 — 추론하고, 결과 그려진 프레임을 대신 띄운다
전체 코드 (주석 포함)
• model(frame) — 프레임 한 장을 추론. 영상 파일 경로 대신 이미지 배열을 그대로 넣을 수 있음
• results[0].plot() — 탐지 박스·클래스명·확률까지 그려진 새 이미지를 돌려줌. 직접 사각형 그릴 필요 없음
• verbose=False — 없으면 초당 수십 줄씩 로그가 쏟아져 터미널이 안 보임
• 첫 실행 시 yolo11n.pt 가중치 파일을 자동 다운로드 (약 5MB)
cv2.waitKey(1) & 0xFF == ord('q') 요약 설명한 줄 요약 — "짧은 순간 키 눌렀나? 'q'면 종료"
• cv2.waitKey(1) — 1ms 동안 키 눌렀는지 확인. 안 눌렀으면 통과 → 다음 프레임 표시. 없으면 영상 안 보임
• ord('q') — 'q' 키의 번호(=113). 사용자가 누른 키 번호와 같으면 q 누른 것
• & 0xFF — 키 번호를 0~255 범위로 정리해주는 안전장치
0xFF 가 뭔가요? (아주 쉽게)
📞 할머니 전화번호 비유
친구한테 전화 걸려고 전화번호를 비교한다고 상상해봐.
• 너 수첩: 1234-5678
• 할머니 수첩: 2026년5월17일오후3시통화함1234-5678 😅
두 수첩을 그대로 비교하면 글자가 달라서 "다른 번호"로 보여. 그런데 사실 뒤 8자리만 보면 똑같은 번호!
👀 컴퓨터에서도 똑같은 일이 일어남
1단계 — 키보드의 모든 키는 번호가 있어. 'q'는 113번. (이름표 같은 거)
2단계 — 사용자가 'q'를 눌렀을 때:
• 어떤 컴퓨터: 113이라고 깔끔하게 말해 ✅
• 어떤 컴퓨터: 1114225라고 말해 😱 (할머니처럼 앞에 쓸데없는 정보 잔뜩 붙임)
3단계 — 우리 코드: if 키번호 == 113: 종료. 그런데 1114225 ≠ 113 이라 종료가 안 됨! 😭
✂️ 해결책 — 가위로 뒷부분만 잘라내기
& 0xFF 이게 바로 그 가위야. 앞에 붙은 쓸데없는 부분 싹뚝 자르고, 뒷부분만 남김.
가위질 결과:
• 1114225 → 가위질 → 113 ✂️
• 113 → 가위질 → 113 (이미 짧으니 그대로)
이제 둘 다 113! → 113 == 113 → 종료 성공 🎉
🔍 1114225가 어떻게 113이 되는지
잠깐, 컴퓨터의 비밀: 2진수
우리는 평소 0~9 (10가지 숫자)로 세지만, 컴퓨터는 0과 1 딱 두 가지로만 셈. 이걸 2진수라고 해.
예시:
• 우리: 5 · 컴퓨터: 101
• 우리: 113 · 컴퓨터: 1110001
• 우리: 1114225 · 컴퓨터: 100010000000000001110001 (엄청 길어짐!)
왜 이렇게 셀까? → 컴퓨터는 전구처럼 "켜짐(1) / 꺼짐(0)" 두 가지만 구분 가능
📐 마지막 8자리만 떼어내기
1114225를 2진수로 길게 쓰면:1114225 = 0001 0001 0000 0000 0111 0001
이 중 마지막 8자리(0111 0001)에만 진짜 키 번호가 들어있어. 앞쪽 0001 0001 0000 0000은 쓰레기.
& 0xFF는 "마지막 8자리만 가져와" 라는 컴퓨터 명령어. 그게 전부야. 가위로 잘라낸 뒷부분만 남는 것과 똑같음 ✂️
결과: 0111 0001 → 10진수로 바꾸면 113
📊 0111 0001이 왜 113인지
2진수 → 10진수 바꾸는 법. 각 칸마다 자릿값이 있음 (오른쪽부터 1, 2, 4, 8, 16, 32, 64, 128):자릿값: 128 64 32 16 8 4 2 1
2진수: 0 1 1 1 0 0 0 1
계산: 0 + 64 + 32 + 16 + 0 + 0 + 0 + 1 = 113
1이 켜진 칸의 자릿값만 더하면 끝! 64 + 32 + 16 + 1 = 113 ✅
🤔 왜 하필 "8비트"만 남길까?
비트(bit) = 컴퓨터가 정보를 저장하는 가장 작은 칸. 8칸 모으면 → 0부터 255까지 숫자 표현 가능.
아래 ASCII 코드 표를 보면 키보드 모든 글자가 0~127 안에 들어있음 (8비트 안에 여유롭게 수용):
📖 전체 표 보기 → 위키백과 ASCII 문서
| 글자 | 10진수 | 16진수 | 설명 |
|---|---|---|---|
| (공백) | 32 | 0x20 | 스페이스 바 |
| 0 | 48 | 0x30 | 숫자 0 시작 |
| 9 | 57 | 0x39 | 숫자 9 끝 |
| A | 65 | 0x41 | 대문자 A 시작 |
| Z | 90 | 0x5A | 대문자 Z 끝 |
| a | 97 | 0x61 | 소문자 a 시작 |
| q | 113 | 0x71 | ← 우리가 찾는 키! |
| z | 122 | 0x7A | 소문자 z 끝 |
| ~ | 126 | 0x7E | ASCII 마지막 글자 |
핵심 관찰
• 모든 키보드 글자가 0~127 사이 (7비트면 충분)
• 8비트는 0~255 → 키보드 글자 + 여유 공간까지 모두 OK
• 그래서 뒤 8칸만 남기면 어떤 컴퓨터에서든 키 번호가 똑같은 모양 → 비교 성공 ✅
한 줄 정리: & 0xFF = "쓸데없는 앞부분 자르고 키 번호만 깔끔하게 만들기"
영상에 글자 표시 (putText)
putText로 영상 위 글자 그리기
탐지 결과(객체 수·상태)를 실시간 프레임 좌측 상단에 출력 → CCTV 알람 UI·디버깅·시연 화면 구성. cv2.putText는 OpenCV에서 영상에 문자열을 그리는 함수
cv2.putText 매개변수 7개
① frame — 글자 그릴 영상(프레임 이미지) · ② text — 출력 문자열 · ③ org — 좌측 하단 시작 좌표 (x, y) · ④ fontFace — 폰트 스타일 (예 FONT_HERSHEY_SIMPLEX) · ⑤ fontScale — 폰트 크기(배율) · ⑥ color — 글자색 BGR 순서 · ⑦ thickness — 글자 두께(픽셀)
· (옵션) lineType — cv2.LINE_AA 안티앨리어싱 적용 시 글자 가장자리 매끈
색상은 BGR 순서 (RGB 아님)
OpenCV는 색상을 B(파랑) → G(초록) → R(빨강) 순으로 표기. (255, 0, 0) = 파랑 / (0, 255, 0) = 초록 / (0, 0, 255) = 빨강. 일반 RGB와 순서 반대라 헷갈리기 쉬움
전체 코드 (주석 포함)
• cv2.putText(frame, text, org, font, scale, color, thickness, lineType) — 영상 위에 문자열 그리기
• org=(10, 30) — 좌측 상단 기준점. y가 클수록 아래로 내려감
• color — BGR 순서이므로 (255, 0, 0)는 파랑
• cv2.LINE_AA — 글자 가장자리 부드럽게 처리하는 안티앨리어싱 옵션
count = len(results[0].boxes) — 실제 탐지 수 세기
results[0].boxes 안에 이 프레임에서 탐지된 박스가 전부 들어있음. 박스 1개 = 객체 1개이므로 len()으로 개수를 세면 그게 곧 탐지된 객체 수
• 아무것도 안 잡히면 0 — 리스트가 비어 에러가 아니라 0이 나옴
• conf=0.5 값에 따라 개수가 달라짐. 낮추면 많이 잡히고 늘리면 확실한 것만 잡힘
• 글자를 그릴 대상이 annotated_frame으로 바뀐 점에 주의 — 원본 frame에 그리면 탐지 박스가 안 보임
특정 클래스만 세고 싶다면
사람(0번)·자동차(2번)만 세려면 추론할 때 classes로 걸러내면 됨results = model(frame, conf=0.5, classes=[0, 2], verbose=False)
count = len(results[0].boxes) # 사람 + 자동차만 카운트
클래스 번호 확인: print(model.names)
추적 · 히트맵 · 보안 알람
추적 (Tracking)
추적 (Object Tracking)
탐지는 매 프레임마다 물체를 새로 찾지만, 추적은 물체마다 고유 번호(ID)를 붙여서 계속 따라가요. 사람 1번이 화면 밖으로 나갔다 다시 들어와도 같은 1번으로 인식해요.
탐지 vs 추적 차이
• 탐지(detect) — 매 프레임 독립적으로 탐지 (ID 없음)
• 추적(track) — 프레임 간 연결하여 같은 물체에 같은 ID 유지
CCTV 스트림 추적 코드
• model.track() — detect 대신 track 메서드 사용
• persist=True — 이전 프레임 정보를 기억해서 같은 ID 유지
• conf=0.6 — 60% 이상 확신할 때만 탐지
자주 묻는 질문
매 프레임마다 추적기가 초기화돼서 같은 사람에게 계속 새 ID가 붙습니다. 화면 번호가 1초에도 몇 번씩 바뀌는 게 보입니다. 영상 한 편을 이어서 처리할 때는 항상 persist=True.
정상 범위입니다. 사람이 기둥에 가려지거나(occlusion) 화면 밖으로 나갔다 들어오면 추적기가 다른 물체로 판단합니다. 재식별이 중요하면 tracker="botsort.yaml"이 기본 bytetrack보다 잘 버팁니다.
track()은 내부에서 탐지를 한 뒤 프레임 간 매칭까지 합니다. 그래서 ID가 생기고, 대신 조금 느립니다. ID가 필요 없고 위치만 알면 되는 경우엔 model(frame)으로 충분합니다.
CCTV 저화질 영상에서는 높습니다. 놓치는 객체가 많아지면 0.3~0.4로 낮춰보세요. 대신 오탐(엉뚱한 걸 사람으로 인식)이 늘어나는 트레이드오프가 있습니다.
히트맵 (Heatmap)
히트맵 (Heatmap)
물체가 자주 나타나는 곳을 열지도(Heatmap)로 시각화해요. 빨간색 = 자주 등장한 곳, 파란색 = 거의 없는 곳. 매장 진열대 동선 분석, 매대 인기 측정 등에 활용해요.
탐지 · 추적 vs 히트맵 차이
• 탐지/추적 — 매 프레임 위치 표시 (순간 정보)
• 히트맵(Heatmap) — 누적 위치 정보를 색상 강도로 시각화 (장기 패턴)
히트맵 생성 코드
• solutions.Heatmap() — ultralytics 제공 누적 시각화 솔루션
• model="yolo11n.pt" — 사용 모델 경로
• show=False — 솔루션 내부 창 끔 (직접 cv2.imshow로 표시)
• colormap=cv2.COLORMAP_MAGMA — 색상 지도 (대체: COLORMAP_JET, COLORMAP_HOT, COLORMAP_PARULA)
• heatmap(frame) — 인스턴스 직접 호출 (track 호출 아님), 반환 SolutionResults의 plot_im이 처리 프레임
• cv2.destroyAllWindows() — imshow로 연 창 정리
자주 묻는 질문
두 가지를 확인하세요. ① cv2.imshow로 results.plot_im을 그리는지 (show=False 기준) ② 루프 안에 cv2.waitKey가 있는지. OpenCV는 waitKey가 호출돼야 창을 다시 그립니다. 없으면 창이 회색으로 멈추거나 '응답 없음'이 됩니다.
heatmap(frame)의 반환값은 YOLO 추론 결과 리스트가 아니라 SolutionResults 객체입니다. 인덱싱이 안 됩니다. 화면에 그릴 때는 results.plot_im을 cv2.imshow로 표시하세요 (show=False 기준).
히트맵은 누적 방식이라 영상 초반에는 거의 안 보이는 게 정상입니다. 같은 자리에 물체가 반복해서 지나가야 색이 진해집니다. 짧은 영상보다 몇 분짜리 영상에서 패턴이 드러납니다.
생성할 때 classes=[0]을 추가하세요. 0이 COCO의 person입니다. 차량만 보려면 classes=[2].
보안 알람 (Alarm)
보안 알람 (Security Alarm)
CCTV에서 특정 클래스(자동차·사람 등)가 감지되면 자동으로 이메일 알림을 보내는 기능이에요. 무단 침입·주차장 이상 차량·매장 도난 감지 등에 활용해요.
탐지 + 추적 + 알림 통합 흐름
• YOLO 탐지 → 추적(ID 부여) → 누적 카운트(records 임계치) → SMTP 메일 발송
• 단일 프레임 오탐 방지 위해 동일 객체 N회 이상 감지 시에만 알림
준비 사항 (Gmail 앱 비밀번호)
• Gmail 계정 + 2단계 인증 활성화 필수
• Google 계정 → 보안 → 앱 비밀번호 → 16자리 비밀번호 발급
• ⚠ 일반 비밀번호로 SMTP 인증 불가 (보안 정책상 차단)
• classes=[2] — COCO 클래스 번호 (0=사람, 2=자동차)
보안 알람 코드
• solutions.SecurityAlarm() — 탐지+추적+SMTP 통합 솔루션
• records=2 — 2회 감지 시 메일 (1=즉시, 5=기본). 오탐 방지용 임계치
• classes=[0] 사람 / [2] 자동차 / [0,2] 둘 다
• .authenticate() — SMTP 로그인 (Gmail 587 포트 TLS 자동)
• google_alarm(frame) — 프레임 처리 (track 호출 아님, 인스턴스 직접 호출)
• assert cap.isOpened() — 스트림 연결 실패 조기 감지
• cv2.destroyAllWindows() — 모든 OpenCV 창 정리
os.environ.get("GMAIL_PW"))로 분리 권장자주 묻는 질문
2단계 인증이 켜진 Google 계정은 외부 프로그램의 SMTP 직접 로그인을 차단합니다. 대신 프로그램 전용으로 발급하는 16자리 문자열이 앱 비밀번호입니다. Google 계정 → 보안 → 앱 비밀번호에서 발급합니다.
순서대로 확인하세요. ① records 임계치에 아직 도달 안 했을 수 있음 ② 스팸함 ③ 앱 비밀번호 오타 ④ 회사·학교 네트워크가 587 포트를 막았는지. 인증 자체가 실패하면 authenticate()에서 바로 예외가 납니다.
records 값을 올리세요. '몇 번 감지되면 메일 1통'의 기준입니다. 실습에서는 빨리 확인하려고 2로 낮춘 것이고, 실제 운영에서는 5~10 정도가 현실적입니다.
안 됩니다. 코드를 GitHub에 올리면 비밀번호도 같이 올라가고 커밋 히스토리에 영구히 남습니다. os.environ.get("GMAIL_PW")로 환경변수에서 읽어오세요. 실수로 한 번 올렸다면 파일만 지우는 걸로는 부족하고 앱 비밀번호를 폐기하고 재발급해야 합니다.
블러 처리 (Blur)
블러 처리 (Object Blur)
방송에서 얼굴·번호판에 모자이크를 씌우듯, YOLO가 탐지한 물체 영역만 골라 자동으로 흐리게 처리하는 기능. CCTV 익명화·개인정보 보호·SNS 공개 영상 사전 검열 등에 활용
탐지 + 영역 마스킹 + 블러 통합 흐름
• YOLO 탐지 → Bounding Box 좌표 추출 → 해당 ROI에만 cv2.blur 적용 → 원본 프레임 합성
• 사람이 직접 좌표 지정할 필요 없이 탐지 영역 자동 마스킹 — 움직이는 객체도 실시간 추적 블러
블러 처리 코드 (v15_04_yolo_blurr.py)
• solutions.ObjectBlurrer() — 탐지+영역 블러 통합 솔루션 (별도 좌표 계산 불필요)
• model="yolo11n.pt" — 사전학습 모델 (n=가장 빠름, s/m/l/x로 갈수록 정확도↑ 속도↓)
• show=False — 솔루션 내부 창 끔 (직접 cv2.imshow로 results.plot_im 표시)
• blur_ratio=0.3 — 블러 강도. 0.1=살짝 흐림, 0.5=중간, 1.0=완전 모자이크
• blurrer(frame) — 프레임 직접 호출 (track/predict 별도 호출 아님)
• break — 웹캠 끊김·파일 끝 도달 시 루프 종료
conf=0.1) + 큰 모델(yolo11x.pt) 권장자주 묻는 질문
루프 안에 cv2.waitKey가 없으면 생기는 증상입니다. OpenCV는 waitKey가 호출될 때만 창을 갱신하고 키 입력을 받습니다. 위 코드의 3-2 블록이 그 처리입니다.
블러는 YOLO가 탐지한 박스 영역에만 적용됩니다. 탐지에 실패한 사람은 전혀 가려지지 않습니다. 확실히 가려야 하면 conf=0.1로 임계치를 낮추고 yolo11x.pt 같은 큰 모델을 쓰세요.
이 모델로는 안 됩니다. COCO 80개 클래스에 '얼굴'이 없어서 person을 잡으면 몸 전체가 흐려집니다. 얼굴만 하려면 얼굴 전용 탐지 모델이 따로 필요합니다.
solutions는 화면 표시만 담당합니다. 저장하려면 cv2.VideoWriter를 직접 만들어 프레임마다 write 해야 합니다.
거리 계산 (Distance)
거리 계산 (DistanceCalculation)
줄자 없이 두 사람 사이 거리를 눈으로 가늠하듯, YOLO가 탐지한 두 물체 중심점 사이의 픽셀 거리를 마우스 클릭 두 번으로 실시간 측정하는 기능. 사회적 거리 두기 모니터링·차량 간격 측정·작업장 안전 거리 감시·로봇 충돌 회피 등에 활용
동작 흐름 — 클릭 기반 측정
• YOLO 탐지 → 화면 위 박스 표시 → 마우스 좌클릭 2회로 대상 2개 선택 → 중심점 좌표 추출 → 유클리드 거리 계산 → 화면에 픽셀 값 오버레이
• 마우스 우클릭 시 선택 초기화 — 움직이는 객체를 계속 추적하며 거리 갱신
거리 계산 코드 (v15_05_yolo_distance.py)
• solutions.DistanceCalculation() — 탐지+추적+중심점 거리 계산 통합 솔루션 (별도 좌표 계산 불필요)
• model="yolo11n.pt" — 사전학습 모델 (n=가장 빠름, s/m/l/x로 갈수록 정확도↑ 속도↓)
• show=True — 처리 결과 창 실시간 표시 + 마우스 입력 활성화
• distance(frame) — 프레임 직접 호출 (track/predict 별도 호출 아님)
• cv2.waitKey(1) & 0xFF == ord("q") — 1ms 키 대기 + 하위 8비트 마스킹 + 'q' 문자 코드 비교
• break — 프레임 끝 도달·키 입력 시 루프 종료
yolo11m.pt) 사용자주 묻는 질문
좌클릭으로 객체 2개를 먼저 골라야 계산이 시작됩니다. 아무것도 안 고른 상태에서는 박스만 그려지고 거리는 표시되지 않습니다. 잘못 골랐으면 우클릭으로 초기화.
show=True여야 창이 뜨고 마우스 콜백도 함께 활성화됩니다. False면 창 자체가 없어서 클릭할 대상이 없습니다.
미터가 아니라 픽셀(px)입니다. 실제 거리로 바꾸려면 '1픽셀이 몇 미터인지'를 알아야 하고, 그 구하는 방법은 STEP 13에 정리돼 있습니다.
원근 때문입니다. 카메라에서 멀리 있을수록 같은 1m가 더 적은 픽셀로 보입니다. 화면 위쪽(먼 곳)과 아래쪽(가까운 곳)의 픽셀 거리는 애초에 같은 척도가 아닙니다.
거리 상태 분류
거리 기반 상태 분류 (v15_06_t_yolo_distance.py)
신호등처럼 거리가 멀면 안전, 가까우면 위험으로 자동 판정하는 로직. DistanceCalculation의 process() 호출 결과에서 pixels_distance 값을 꺼내 임계값과 비교하여 SAFE · Warning · DANGER 3단계 상태 분류. 사회적 거리두기 감시·차간 거리 경보·작업자 충돌 예방·로봇 안전 펜스에 활용
처리 흐름
• 비디오 프레임 읽기 → distance.process(frame) 호출 → results.pixels_distance 추출 → None/0 가드 → 임계값 비교 (150·100) → 상태 문자열 출력
• 결과 객체 기반 접근 — STEP 05의 distance(frame) 직접 호출과 달리 process()로 명시적 호출 후 속성 추출
거리 상태 분류 코드 (v15_06_t_yolo_distance.py)
• distance.process(frame) — 탐지+거리 계산 수행 후 SolutionResults 객체 반환 (ultralytics 8.3+ API)
• results.pixels_distance — 클릭으로 선택한 두 객체 중심점 간 픽셀 거리 (선택 전엔 0 또는 None)
• is None or == 0 가드 — 좌클릭 2회로 객체 미선택 시 발생하는 빈 결과 처리 (continue로 다음 프레임)
• 150 / 100 임계값 — SAFE(≥150) · Warning(≥100) · DANGER(<100) 3단계 분류 기준
• continue vs break — continue=현재 프레임 스킵, break=루프 완전 종료
• cv2.waitKey(1) & 0xFF == ord('q') — 1ms 키 입력 대기 후 'q' 코드 비교
winsound.Beep)·DB 로깅·Slack 웹훅 트리거 결합 시 실전 모니터링 시스템 완성자주 묻는 질문
정상입니다. 좌클릭으로 객체 2개를 고르기 전까지는 pixels_distance가 None 또는 0이라 그 분기로 빠집니다. 영상 창에서 두 대상을 클릭하면 숫자가 나옵니다.
continue가 waitKey보다 앞에 있으면 그렇게 됩니다. 객체 미선택 상태에서 continue로 건너뛰어 키 입력을 받는 줄까지 도달하지 못하기 때문입니다. 그래서 위 코드는 waitKey를 continue보다 먼저 배치했습니다.
정해진 값이 아니라 임의로 잡은 실습용 숫자입니다. 카메라 높이·줌 배율에 따라 같은 1m도 픽셀 값이 완전히 달라지므로, 실제로 쓰려면 현장 영상에서 안전/위험 상황을 찍어보고 직접 조정해야 합니다.
distance(frame)은 화면 표시까지 알아서 하고 끝, process(frame)는 SolutionResults 객체를 돌려줘서 계산 결과를 코드에서 꺼내 쓸 수 있게 합니다. 상태 분류처럼 값이 필요한 경우 process를 씁니다.
영역 카운팅 (Region)
영역 카운팅 (RegionCounter)
주차장 입구 바닥 사각형 안 차량 수를 자동 집계하듯, 다각형 영역을 좌표 4점으로 정의하고 YOLO 탐지 객체 중 해당 영역 내부에 들어온 것만 실시간 카운팅하는 기능. 매장 특정 구역 고객 수 집계·주차 구역 차량 수 측정·공장 위험 구역 진입 감시·전시장 인기 구역 분석 등에 활용
처리 흐름 — 영역 정의 + 내부 객체 집계
• CCTV·웹캠 영상 입력 → 다각형 좌표 dict 정의 (region-01: 4점) → cv2.resize 640x480 표준화 → YOLO 탐지 → 영역 내부 판정 (Point-in-Polygon) → 구역별 객체 수 화면 오버레이
• dict 키로 여러 영역 동시 관리 가능 — region-01, region-02… 형태로 확장 시 다중 구역 동시 집계
영역 카운팅 코드 (v15_07_yolo_region.py)
• solutions.RegionCounter() — 탐지+영역 판정+카운팅 통합 솔루션 (Point-in-Polygon 내부 처리)
• region_points dict 형식 — "region-01" 키 + 좌표 리스트 값 구조. 키 추가로 다중 영역 동시 집계 ("region-02": [...])
• 좌표 4점 (192,175) → (180,410) → (439,386) → (273,168) — 시계/반시계 방향으로 다각형 꼭짓점 순서 지정. STEP 08 마우스 클릭으로 추출 가능
• conf=0.1 — 신뢰도 임계값 낮춤. CCTV 저화질 영상 탐지율 보강용 (기본 0.25)
• region=region_points — RegionCounter에 영역 dict 주입. 내부에서 각 키별로 독립 카운팅 수행
• cv2.resize(frame, (640, 480)) — 영역 좌표 기준 해상도 통일. 원본 해상도와 region_points 좌표계 일치 필수
• yolo_region(re_frame) — 프레임 직접 호출 (track/predict 별도 호출 아님)
• cv2.waitKey(1) & 0xFF == ord('q') — 1ms 키 입력 대기 + 하위 8비트 마스킹 + 'q' 문자 코드 비교
"입구"·"계산대"·"매대" 등 구역별 동시 집계 가능 — 매장 동선 분석에 활용자주 묻는 질문
좌표계 불일치입니다. region_points는 resize 이후 해상도(640x480) 기준이어야 합니다. 원본 해상도에서 측정한 좌표를 그대로 넣거나 resize를 빠뜨리면 영역이 어긋납니다.
STEP 08의 마우스 클릭 도구를 실행해 화면을 4번 클릭하면 콘솔에 좌표가 찍힙니다. 그 값을 그대로 복사해 오면 됩니다. 두 코드 모두 640x480으로 resize하므로 좌표가 호환됩니다.
dict에 키를 추가하면 됩니다. {"입구": [...], "계산대": [...]} 처럼요. 키마다 독립적으로 카운팅됩니다.
많습니다. CCTV 저화질이라 탐지율을 끌어올리려고 낮춘 값이고, 그 대가로 그림자·간판 같은 걸 객체로 잡기도 합니다. 화질이 괜찮은 영상이면 기본값 0.25로 올리세요.
마우스 영역 설정
마우스 클릭으로 영역 좌표 추출 (v15_08_yolo_get_region.py)
CCTV 영상에서 마우스 좌클릭으로 4개 꼭짓점을 찍어 region_points 좌표를 직접 측정. 코드에 좌표 하드코딩 대신 클릭 한 번으로 화면 좌표 추출 → STEP 07 영역 카운팅의 region_points 값으로 그대로 사용
전체 코드
콜백 인자 5개 — flags · param 쉽게
OpenCV가 마우스 콜백을 호출할 때 항상 5개 인자를 순서대로 넘겨줌. 이름은 자유, 순서는 고정
• event — 무슨 일? (좌클릭/우클릭/이동 등) → cv2.EVENT_LBUTTONDOWN 같은 상수와 비교
• x, y — 어디서? → 클릭한 픽셀 좌표
• flags — 같이 누른 키 있었나? Ctrl·Shift·Alt 동시 눌림 상태. 안 쓰면 무시. 예) flags & cv2.EVENT_FLAG_CTRLKEY → Ctrl+클릭 감지
• param — 밖에서 넘긴 짐 보따리. setMouseCallback의 3번째 인자로 넘긴 값이 그대로 들어옴. 안 넘기면 None. 예) setMouseCallback("win", cb, points) → 콜백 안에서 param으로 받음
if flags & cv2.EVENT_FLAG_CTRLKEY: → Ctrl 누르면서 클릭한 경우만 처리 가능 (단축키 영역 추가/삭제 구현용)핵심 동작 6단계
• cv2.VideoCapture(m3u8 URL) — UTIC CCTV HLS 스트림 직접 오픈. 로컬 파일 대신 실시간 영상 좌표 측정
• mouse_callback(event, x, y, flags, param) — OpenCV 콜백 시그니처 고정. cv2.EVENT_LBUTTONDOWN 시 클릭 픽셀 좌표 (x, y)를 points 리스트에 append
• cv2.namedWindow("GET_X_Y", cv2.WINDOW_NORMAL) — 윈도우 미리 생성. WINDOW_NORMAL은 크기 조절 가능 모드 (기본 WINDOW_AUTOSIZE는 고정)
• cv2.setMouseCallback("GET_X_Y", mouse_callback) — 윈도우 이름과 콜백 함수 연결. 윈도우 생성 전 호출 시 실패 → namedWindow 먼저 필수
• cv2.resize(frame, (640, 480)) — 측정 좌표계 통일. STEP 07 영역 카운팅도 동일 640x480로 resize → 좌표 그대로 호환
• cv2.waitKey(20) & 0xFF == ord('q') — 20ms 키 입력 대기 + 8비트 마스킹 + 'q' 비교. waitKey(1)보다 CPU 부하 낮춤
points = []처럼 재할당 금지 → 전역 변수 가림 발생. append로만 추가 (현 코드 정상)자주 묻는 질문
① namedWindow를 setMouseCallback보다 먼저 호출했는지 ② 두 함수에 넘긴 창 이름 문자열이 완전히 같은지 확인하세요. 창 이름이 한 글자라도 다르면 콜백이 연결되지 않습니다.
OpenCV가 콜백을 부를 때 항상 5개 인자를 순서대로 넘기기 때문입니다. 받을 자리를 안 만들어 두면 인자 개수 불일치로 에러가 납니다. 받아만 두고 안 쓰는 건 문제없습니다.
양쪽 모두 cv2.resize(frame, (640, 480))를 하고 있는지 확인하세요. 한쪽만 resize하면 좌표 스케일이 달라집니다.
특별한 규칙은 없습니다. 대기 시간이 길수록 CPU를 덜 쓰고, 짧을수록 반응이 빠릅니다. 좌표만 찍는 용도라 20ms로 여유를 준 것입니다.
출입 카운팅 (In/Out)
출입 카운팅 (ObjectCounter) — 들어온 사람·나간 사람 자동 세기
화면에 줄을 하나 긋고, 그 줄을 넘어가는 사람·차를 자동으로 세는 기능. 예: 편의점 문 앞에 보이지 않는 줄을 그어 두면, 들어오면 IN+1, 나가면 OUT+1. 컴퓨터가 카메라 영상만 보고 알아서 셈
한눈에 보기 (3단계)
① 줄 긋기 — 화면 위에 점 2개 찍어서 선 정의
② 사람 추적 — YOLO가 각 사람에게 이름표(ID) 붙임
③ 방향 보기 — 그 사람이 줄을 어느 쪽에서 어느 쪽으로 넘었는지 확인 → IN 또는 OUT
IN/OUT 어떻게 구분? (공식 소스 기준)
• 줄 모양 먼저 판단 — 컴퓨터가 점 2개의 가로 길이 vs 세로 길이 비교해서 줄이 가로인지 세로인지 자동으로 정함
• 가로줄(−)이면 = 사람이 아래로 내려오면 IN, 위로 올라가면 OUT
• 세로줄(|)이면 = 사람이 오른쪽으로 가면 IN, 왼쪽으로 가면 OUT
• 한 사람 = 한 번만 카운트 — 같은 사람이 줄 앞에서 왔다갔다해도 처음 넘는 순간 1번만 셈. 이후 다시 넘어도 카운트 안 됨 (공식 소스: counted_ids 목록에 한 번 들어가면 영구 잠금)
• 현재 코드 좌표 (234,407)→(620,340) 예시 — 가로 길이 386 > 세로 길이 67 → 가로줄로 판정 → 사람이 아래로 내려오면 IN, 위로 올라가면 OUT
• 줄 방향 팁 — 사람 다니는 길과 직각으로 그어야 정확
• IN/OUT 거꾸로 표시되면 점 2개 순서 바꾸기
출입 카운팅 코드 (v15_09_yolo_inout.py)
코드 핵심만 쉽게
• count_points = [(234,407), (620,340)] — 점 2개 = 줄 1개. 줄 시작점·끝점 좌표
• ObjectCounter(...) — "사람 찾기 + 따라가기 + 줄 넘는지 보기 + 숫자 세기" 한꺼번에 해주는 도구
• region=count_points — 위에서 만든 줄을 도구에 알려주기
• cv2.resize(frame, (640,480)) — 영상 크기를 줄 좌표랑 같게 맞춤 (안 맞추면 줄이 엉뚱한 데 생김)
• counter(re_frame) — 프레임 1장 넣으면 화면에 "In: 3 Out: 2" 같은 글자 자동으로 표시
자주 묻는 질문
count_points의 점 2개 순서를 바꾸면 됩니다. 방향은 선의 시작점→끝점 기준으로 정해지기 때문입니다.
선이 사람이 다니는 방향과 나란히 그어져 있을 가능성이 큽니다. 아무도 그 선을 '넘지' 않으니 0으로 남습니다. 통로와 직각이 되게 그으세요.
아닙니다. 한 번 카운트된 ID는 내부 counted_ids 목록에 기록돼 다시 세지 않습니다. 다만 사람이 가려져서 ID가 새로 부여되면 다른 사람으로 보고 또 셉니다.
루프에 waitKey가 없으면 그렇습니다. 위 코드의 4-3 블록이 그 처리이고, 없으면 창이 '응답 없음'이 되어 강제 종료해야 합니다.
SAHI 데이터 준비
SAHI — 작은 물체도 놓치지 않는 비법 (데이터 준비)
멀리 있는 작은 차·사람은 일반 YOLO가 잘 못 봄. SAHI로 해결
왜 필요한가?
• 고해상도 사진 속 작은 물체 → 일반 추론 시 놓침
• 이번 단계는 SAHI 설치 + 테스트 이미지 다운로드만
설치 (터미널)
데모 이미지 2장 다운로드 (v15_10_yolo_sahi_img.py)
실행 결과
• demo_data/ 폴더 자동 생성
• 작은 차량·지형 이미지 2장 저장
• 다음 STEP 11~12에서 이 이미지로 일반 추론 vs 슬라이싱 추론 비교
자주 묻는 질문
파이썬을 실행한 위치(작업 디렉터리) 기준입니다. VS Code에서 실행하면 보통 프로젝트 루트가 되고, 터미널에서 cd한 폴더가 기준이 됩니다. 다음 STEP에서 파일을 못 찾으면 이 위치부터 확인하세요.
회사·학교 네트워크에서 GitHub raw 도메인이 막혀 있는 경우가 있습니다. 브라우저로 주소를 직접 열어 이미지를 받은 뒤 demo_data/ 폴더에 같은 파일명으로 넣어도 결과는 같습니다.
멀리서 찍어 작은 물체가 잔뜩 들어 있는 사진이라 SAHI의 효과가 극적으로 드러나기 때문입니다. 일반 사진으로는 슬라이싱을 해도 탐지 수가 크게 늘지 않습니다.
설치한 파이썬과 실행하는 파이썬이 다른 경우가 대부분입니다. 가상환경을 쓰고 있다면 활성화한 상태에서 설치했는지 확인하세요.
SAHI 기본 추론
SAHI 기본 YOLO 추론
사진 통째로 한 번만 보고 물체 찾기
쉽게 말하면
• 넓은 운동장 사진을 눈 한 번에 훑어보기
• 멀리 있는 작은 친구는 잘 안 보임
코드 한 줄씩 (v15_11_yolo_sahi_org.py)
핵심 단어 3개
• results[0].plot() — 탐지 박스가 그려진 이미지 배열을 돌려줌
• os.makedirs("sahi", exist_ok=True) — 저장 폴더 생성. exist_ok=True가 없으면 두 번째 실행에서 FileExistsError
• len(results[0].boxes) — 탐지된 박스 개수 = 탐지 수. 이 숫자가 STEP 12와의 비교 기준
실행 결과
• sahi/result_org.jpg — 박스 그려진 결과 이미지 저장
• 터미널에 탐지 수: N 출력
• 작은 물체는 대부분 놓침 → STEP 12와 숫자 비교
자주 묻는 질문
STEP 10을 먼저 실행해서 demo_data/small-vehicles1.jpeg를 받아야 합니다. 받았는데도 안 되면 실행 위치가 달라서 상대 경로가 어긋난 경우입니다.
없으면 폴더가 이미 있을 때 FileExistsError가 납니다. 즉 두 번째 실행부터 무조건 에러가 나는데, 이 옵션을 주면 '있으면 그냥 넘어가라'가 됩니다.
이미지 경로가 틀렸거나(엉뚱한 파일을 읽음), 이미지 속 물체가 너무 작아 기본 임계치에서 전부 걸러진 경우입니다. 후자라면 그게 바로 STEP 12에서 SAHI를 쓰는 이유입니다.
비교 기준(baseline)을 만드는 단계라서 그렇습니다. 여기서 나온 '탐지 수: N'을 기억해 두고 STEP 12의 숫자와 비교하면 SAHI가 얼마나 더 찾아내는지가 숫자로 보입니다.
SAHI 슬라이싱 추론
SAHI 슬라이싱 추론
사진을 200×200 타일로 잘라 각 조각마다 YOLO 실행 후 결과 합치기. 작은 물체 탐지 정확도 크게 향상
쉽게 말하면
• 큰 그림책을 한 페이지씩 돋보기로 보는 방식
• 멀리 있던 작은 차·사람도 또렷이 탐지
• STEP 11 탐지 수와 비교 → 숫자 확 늘어남
코드 한 줄씩
핵심 단어 4개
• get_sliced_prediction — 사진을 조각조각 잘라 추론
• slice_height / slice_width — 타일 크기 (작을수록 작은 물체 잘 봄)
• overlap_*_ratio — 타일 경계 10% 겹침 → 경계 물체 놓침 방지
• confidence_threshold=0.4 — 40% 이상 확신할 때만 탐지
실행 결과
• sahi/prediction_visual.png — 박스 그려진 결과 이미지 저장 (파일명은 SAHI가 정함)
• 터미널에 탐지 수: N 출력 → STEP 11의 숫자와 직접 비교
• STEP 11 결과 이미지와 나란히 보면 작은 차량 탐지 수 급증 확인
자주 묻는 질문
export_dir은 이름 그대로 폴더 경로를 받는 인자입니다. 파일명을 주면 그 이름으로 폴더를 만들어 버립니다. 저장 파일명은 SAHI가 prediction_visual.png로 정합니다.
정상입니다. 사진을 타일로 쪼갠 만큼 추론을 반복하기 때문입니다. 200x200 타일이면 큰 사진 한 장에 수십~수백 번 추론이 일어납니다. 정확도와 속도를 맞바꾸는 기법입니다.
찾으려는 물체 크기의 2~4배 정도가 무난합니다. 너무 작게 잡으면 물체가 타일 경계에서 잘리고 처리 시간만 늘어납니다.
타일 경계에 걸친 물체가 양쪽 모두에서 잘려 안 잡히는 걸 막습니다. 0.1~0.2(10~20%) 정도면 충분하고, 더 키우면 중복 탐지와 시간만 늘어납니다.
속도 추정 (Speed)
속도 추정 (SpeedEstimator)
실시간 CCTV 스트림에서 차량 이동 속도를 km/h 단위로 측정. 도로 속도위반 감지, 컨베이어 벨트 속도 측정 등에 활용
속도 추정 코드
픽셀 당 실제 거리를 얻는 방법 (meter_per_pixel 보정)
meter_per_pixel = 화면 속 1픽셀이 실제 몇 미터인지를 나타내는 환산 계수. 카메라 설치 높이·각도·렌즈에 따라 달라지므로 직접 측정 필요
⚠️ 한계 — 단일 환산값은 근사치. 화면 중앙은 가깝고 멀리 있는 객체는 같은 1m도 더 적은 픽셀로 보임(원근 왜곡). 정확한 속도 계산은 도로 표면 부근 고정 ROI에서만 신뢰 가능
• ① 기준물 측정법 — 화면에 길이를 아는 물체(점선 차선·횡단보도·차량 길이) 배치 → 픽셀 길이 측정 → 실제거리(m) ÷ 픽셀거리(px)
• ② 도로 표준 활용 (한국 도로교통법 시행규칙 별표6)
- 일반도로 백색 점선: 길이 3m · 간격 5m (1주기 8m)
- 자동차전용·고속도로 점선: 길이 10m · 간격 10m (1주기 20m)
- 차로 폭: 일반도로 3.0~3.25m · 고속도로 3.5m
- 횡단보도 폭: 보통 3m 이상
• ③ Homography 변환 (정확도 ↑ 권장) — cv2.findHomography()로 카메라 시점을 위에서 본 시점(Bird's Eye View)으로 변환 → 원근 왜곡 제거 → 화면 전체에서 균일한 m/px 적용 가능
• ④ 캘리브레이션 보드 (정밀) — 실측한 체커보드 패턴 촬영 → cv2.calibrateCamera()로 카메라 매트릭스·왜곡 계수 계산. 렌즈 왜곡까지 보정
meter_per_pixel = 3m ÷ 6px = 0.5SpeedEstimator(meter_per_pixel=0.5)🎨 코드 색상 의미
• URL 입력 소스 · SpeedEstimator 핵심 클래스 · max_speed 상한 · meter_per_pixel 보정 필수값 · classes 필터링 · line_width 시각 옵션 · yolo_speed(frame) 핵심 호출 · waitKey·& 0xFF·ord 종료 처리
자주 묻는 질문
meter_per_pixel을 현장에 맞게 보정하지 않아서입니다. 기본값 0.5는 예시일 뿐이고, 카메라 높이·각도·줌이 다르면 값이 완전히 달라집니다. 위의 보정 방법 4가지 중 하나로 직접 측정해야 합니다.
차량이 실제로 지나가는 도로면 근처에서 재야 합니다. 화면 위쪽 먼 곳과 아래쪽 가까운 곳은 같은 1m라도 픽셀 수가 다릅니다(원근 왜곡). 그래서 단일 환산값은 특정 구간에서만 신뢰할 수 있습니다.
표시되지 않습니다. 오측정으로 300km/h 같은 값이 튀는 걸 걸러내기 위한 상한선입니다.
classes=[2]가 승용차(car)만 걸러내기 때문입니다. COCO에서 버스는 5, 트럭은 7이므로 classes=[2, 5, 7]로 바꾸면 함께 잡힙니다.
SAM 세그먼테이션
SAM — Segment Anything Model
Meta AI 개발 SAM(Segment Anything Model). 이미지의 모든 물체를 별도 학습 없이 자동으로 픽셀 단위 세그먼트
SAM 추론 코드
• sam_b.pt(빠름) / sam_l.pt(정확) — 모델 크기 선택
• save=True — 결과 자동 저장 · time.time() — 추론 속도 측정
자주 묻는 질문
sam_b.pt 가중치를 자동으로 내려받기 때문입니다. 수백 MB라 네트워크에 따라 몇 분 걸릴 수 있고, 한 번 받으면 다음부터는 바로 실행됩니다.
runs/segment/predict/ 아래에 저장됩니다. 여러 번 실행하면 predict2, predict3… 로 번호가 붙습니다.
SAM은 기본적으로 보이는 걸 전부 자릅니다. 특정 대상만 원하면 좌표 프롬프트(points=, bboxes=)를 주거나, 다음 STEP의 FastSAM에서 텍스트로 지정하는 방식을 씁니다.
SAM은 ViT라는 무거운 인코더를 쓰기 때문입니다. 정밀도는 높지만 실시간 영상에는 적합하지 않고, 그래서 FastSAM 같은 경량 버전이 나왔습니다.
FastSAM
FastSAM — 텍스트로 골라잡는 고속 세그먼테이션
FastSAM = SAM의 다이어트 버전. 무거운 SAM을 YOLO 골격으로 갈아끼워 50배 빠르게 만들고, "dog" 같은 텍스트 한 단어로 원하는 물체만 콕 집어 잘라냄
FastSAM 텍스트 기반 추론 코드
• FastSAM-s.pt(소형·빠름) / FastSAM-x.pt(대형·정확) — 용도별 선택
• texts="dog" — 텍스트로 대상 지정 · .plot() — 마스크를 이미지에 합성
자주 묻는 질문
FastSAM의 텍스트 지정은 CLIP 유사도 기반이라 딱 떨어지는 판별이 아닙니다. 단어를 더 구체적으로 바꾸거나, 이미지에 비슷한 물체가 많으면 정확도가 떨어집니다.
results[0].plot()이 돌려주는 건 화면에 그려진 이미지 배열(numpy)입니다. 그대로 출력하면 픽셀 값이 전부 찍힙니다. 위 코드처럼 저장 경로를 출력하세요.
s는 가볍고 빠르며, x는 무겁고 정확합니다. 실습·실시간이면 s, 결과 품질이 중요하면 x.
무거운 ViT 인코더 대신 YOLO 세그먼테이션 골격으로 전체를 한 번에 자르고, 그 결과를 텍스트로 필터링하는 구조이기 때문입니다.
OpenVINO 변환
OpenVINO 모델 변환 — CPU 부스터 장착
OpenVINO = Intel이 만든 CPU 전용 부스터. PyTorch .pt 모델을 _openvino_model/ 폴더로 바꾸면, GPU 없이 CPU만으로도 추론이 빨라집니다. 단 이름 그대로 Intel CPU 전용 최적화라, AMD CPU에서는 효과가 없거나 오히려 느려질 수 있습니다 (STEP 17에서 직접 측정)
설치 (터미널) — 변환 전에 먼저
이 패키지가 없으면 model.export(format="openvino") 실행 시 변환 단계에서 에러가 납니다. ultralytics는 설치돼 있어도 OpenVINO 변환기는 별도 패키지입니다.
PyTorch → OpenVINO 변환 코드
• yolo11n.pt(원본) → yolo11n_openvino_model/(변환본) — 폴더로 출력
• format="openvino" — 외에 "onnx", "tensorrt" 등 다른 형식으로도 변환 가능
변환 가능한 형식 전체 목록
Ultralytics 공식 문서에 지원 형식과 각 형식의 인자가 표로 정리돼 있습니다. 어떤 환경에 어떤 형식을 써야 하는지 여기서 확인하세요.
📖 docs.ultralytics.com/modes/export#export-formats
• onnx — 어디서나 돌아가는 범용 형식 · openvino — Intel CPU · engine(TensorRT) — NVIDIA GPU · coreml — 아이폰·맥 · tflite — 안드로이드·라즈베리파이
• 각 형식마다 imgsz, half, int8, dynamic 등 지원 인자가 다릅니다 — 표의 Arguments 열 참고
(Get-CimInstance Win32_Processor).Name자주 묻는 질문
생성된 폴더 경로를 그대로 YOLO에 넘기면 됩니다. YOLO("yolo11n_openvino_model/") — 파일이 아니라 폴더라는 점만 주의하세요.
변환에 필요한 패키지가 없어서입니다. pip install openvino 후 다시 실행하세요.
OpenVINO는 Intel CPU 최적화가 목적입니다. NVIDIA GPU가 있다면 TensorRT(format="engine") 쪽이 더 효과적입니다.
기본(FP32) 변환은 결과가 거의 동일합니다. 속도를 더 얻으려고 int8 양자화까지 하면 정확도가 조금 내려갈 수 있습니다.
FPS 성능 비교
PT vs OpenVINO FPS 성능 비교
FPS = 게임 화면 부드러움 지수 (1초에 그림 몇 장 그리는가). 영상을 재생하면서 추론 시간을 재고 화면에 실시간 표시. .pt 모델로 한 번, OpenVINO 모델로 한 번 — 모델 경로 한 줄만 바꿔 두 번 돌려서 숫자를 비교
FPS 측정 + 화면 표시 코드
• 모델 줄만 바꿔서 두 번 실행 — 나머지 코드가 완전히 같아야 공정한 비교
• start_time / end_time — 추론 직전·직후 시각. 그 차이가 model_time(1장 걸린 시간)
• time.perf_counter() — 시간 간격을 잴 때 쓰는 시계. time.time()과 다른 이유는 아래 항목 참고
• fps = 1 / model_time — 아래 FPS 공식 항목 참고
• verbose=False — 없으면 추론마다 터미널 로그가 초당 수십 줄 쏟아져 화면 보기가 어렵고, 그 출력 시간까지 측정에 섞임
• results[0].plot() — 탐지 박스가 그려진 프레임. 여기에 FPS 글자를 얹음
• 숫자가 깜빡입니다 — 매 프레임 값을 그대로 찍기 때문. 눈대중 비교엔 지장 없고, 안정된 숫자를 원하면 최근 N개 평균을 내면 됨
FPS 공식 — 왜 1 ÷ 시간 인가
FPS = Frames Per Second = 초당 프레임 수. 그런데 우리가 잰 값은 그 반대인 프레임당 초입니다. 분자·분모가 뒤집힌 관계라 역수를 취하면 됩니다.1장에 0.05초 걸림 → 1 ÷ 0.05 = 20 FPS
1장에 0.05초니까 1초 동안 20장. 그게 전부입니다. 시간이 짧을수록 FPS는 커집니다(역수라 방향이 반대).
| 1장 처리 시간 | FPS | 체감 |
|---|---|---|
| 0.5초 | 2 | 뚝뚝 끊김 |
| 0.1초 | 10 | 버벅임 |
| 0.05초 | 20 | 쓸 만함 |
| 0.033초 | 30 | 부드러움 |
| 0.016초 | 60 | 아주 부드러움 |
측정 범위 — 무엇을 재고 무엇을 안 재는가
이 FPS는 추론 시간만 잽니다. 영상 디코딩(cap.read())과 화면 표시(imshow)는 측정 구간 밖입니다. 모델 성능만 비교하는 게 목적이라 이게 맞습니다.
단, 추론 시간 안에는 백엔드와 무관한 작업도 섞여 있습니다.
전체 = 전처리(numpy) + 추론(백엔드) + 후처리·NMS
앞뒤 두 조각은 PT든 OV든 완전히 같은 파이썬 코드라, 백엔드 차이가 그만큼 희석됩니다. 순수 추론 시간만 보려면 results[0].speed를 출력하세요 — {'preprocess': 2.9, 'inference': 30.1, 'postprocess': 1.0} 형태로 ms 단위 분해값이 나옵니다.
yolo11s 이상 또는 int8=True 양자화자주 묻는 질문
모델 초기화·메모리 할당이 첫 추론에 섞이기 때문입니다. 정상이고, 몇 프레임 지나면 안정됩니다. 비교할 때는 처음 1~2초는 무시하고 숫자가 자리 잡은 뒤에 읽으세요.
숫자로 정확히 재야 한다면 측정 전에 추론을 한 번 버리는 워밍업을 넣으면 됩니다.success, frame = cap.read()
model(frame, verbose=False) # 워밍업 1회
잘못한 게 아닐 가능성이 큽니다. 흔한 원인 3가지를 순서대로 확인하세요.
① 입력 크기가 다름 — 터미널 로그에 0: 640x640과 0: 640x384가 번갈아 찍히면 이 경우입니다. PyTorch는 동적 입력이라 16:9 영상을 640x384로 줄이는데, OpenVINO는 export 시 640x640으로 고정됩니다. 픽셀 수가 1.67배 차이나니 당연히 집니다. 양쪽에 imgsz=(640, 640)을 똑같이 주고 다시 재세요.
② AMD CPU — OpenVINO는 Intel 전용 명령어를 쓰도록 만들어졌습니다. AMD에서도 돌아가긴 하지만 가속 경로가 없어 이득이 사라집니다. 이건 고칠 수 없고, 정상 결과입니다.
③ 모델이 너무 작음 — yolo11n은 연산량이 적어 최적화 여지가 별로 없습니다. 게다가 PyTorch CPU도 이미 Intel oneDNN으로 최적화돼 있어서, '최적화 안 된 것 vs 된 것'의 대결이 아닙니다.
아닙니다. 성능 비교는 한 번에 하나씩, 순차로 재야 합니다.
• 이미 멀티스레드로 돌고 있습니다 — PyTorch도 OpenVINO도 추론 한 번 안에서 CPU 코어를 전부 씁니다. torch.get_num_threads()로 확인해 보세요. 실행 로그의 Using OpenVINO LATENCY mode도 코어를 다 쓰는 모드입니다.
• 동시에 돌리면 측정이 망가집니다 — 둘이 같은 코어를 서로 뺏어 둘 다 느려지고, 그 숫자는 '혼자 돌 때 얼마나 빠른가'와 무관해집니다.
멀티스레드가 의미 있는 건 실전 파이프라인입니다. 영상 디코딩과 추론을 각각 다른 스레드로 나누면(cap.read() ↔ 추론), 네트워크 스트림을 기다리는 동안 추론이 놀지 않아 체감 FPS가 오릅니다.
Frames Per Second — 1초에 몇 장을 처리하는가입니다. 1장 처리에 0.05초 걸렸다면 1÷0.05 = 20 FPS. 실시간이라고 하려면 보통 15~30 FPS 이상이 필요합니다.
대략적인 비교로는 충분합니다. 실행할 때마다 값이 크게 흔들린다면 횟수를 100회 정도로 늘리고 중앙값을 보세요.
자의 눈금이 다릅니다. 파이썬에서 직접 확인할 수 있습니다.import time
print(time.get_clock_info('time'))
print(time.get_clock_info('perf_counter'))
Windows 기준 결과:
• time.time() — resolution 0.015625초(15.6ms). 현재 시각(몇 시 몇 분)을 알려주는 시계
• time.perf_counter() — resolution 0.0000001초(0.1µs). 경과 시간 측정 전용 시계
왜 문제가 되나 — 추론이 50ms 걸리는데 15.6ms 눈금 자로 재면, 같은 50ms가 46.9ms나 62.5ms로 찍힙니다. 오차가 ±30%까지 벌어져 두 모델 비교가 무의미해집니다. 심하면 시작·종료가 같은 눈금에 걸려 차이가 정확히 0.0이 되고, 1 / 0.0에서 ZeroDivisionError로 죽습니다.
주의 — 실제로 재보면 time.time()이 예상보다 정밀하게 나오는 PC가 있습니다. 브라우저·미디어 플레이어 같은 프로그램이 시스템 타이머 해상도를 전역으로 올려놓은 경우인데, 그 프로그램을 끄면 다시 15.6ms로 돌아갑니다. 운에 기대는 셈이라 시간 간격 측정에는 항상 perf_counter를 쓰세요.
한 줄 정리 — "지금 몇 시?"는 time.time(), "얼마나 걸렸지?"는 time.perf_counter().
YOLO SearchApp
YOLO SearchApp — 이미지 구글
SearchApp = 내 사진첩 전용 구글. ultralytics solutions가 만들어 주는 웹 검색 사이트로, "dog"처럼 단어를 치면 폴더 안에서 비슷한 이미지를 골라 보여줌
SearchApp 실행 코드
• device="cpu" — GPU가 있다면 "cuda"로 바꿔 더 빠르게
• debug=True — 코드 수정 시 자동 재시작 · 배포 시 False
• 실행은 파일이 있는 폴더로 이동 후 python v15_18_yolo_search.py (Flask 앱이므로 streamlit run 사용 시 signal 오류)
자주 묻는 질문
SearchApp은 Streamlit이 아니라 Flask 앱입니다. python v15_18_yolo_search.py로 실행하세요. STEP 19의 Streamlit과 헷갈리기 쉬운 부분입니다.
주석 처리된 data="path/to/img/directory"를 풀고 폴더 경로를 넣으면 됩니다. 지정하지 않으면 기본 샘플로 동작합니다.
CLIP 모델을 내려받고, 폴더 안 이미지를 전부 벡터로 변환(임베딩)해서 색인을 만들기 때문입니다. 이미지가 많을수록 오래 걸리고, 이후 검색 자체는 빠릅니다.
다른 프로그램이 이미 그 포트를 쓰고 있는 경우입니다. 실행 중인 서버를 끄거나 포트를 바꿔 실행하세요.
Streamlit UI
Streamlit 웹 UI — 코드 3줄로 만드는 YOLO 대시보드
Streamlit = 코드 한 줄로 만드는 웹사이트. ultralytics의 Inference 솔루션이 모델 선택·웹캠 입력·결과 표시를 알아서 그려주는 YOLO 전용 대시보드를 즉시 띄워줌
Streamlit UI 실행 코드
• model="yolo11n.pt" — 다른 YOLO11/26 가중치로 교체 가능
• 실행은 python file.py가 아니라 streamlit run file.py
자주 묻는 질문
안 됩니다. Streamlit 앱은 반드시 streamlit run 파일명.py로 실행해야 합니다. python으로 실행하면 화면이 안 뜨거나 경고만 출력됩니다.
터미널에 표시되는 주소(보통 http://localhost:8501)를 직접 복사해서 브라우저에 붙여넣으면 됩니다.
브라우저의 카메라 권한을 허용했는지 확인하세요. 그리고 카메라는 서버가 돌아가는 컴퓨터의 장치를 씁니다.
solutions.Inference(model="yolo11s.pt")처럼 인자만 바꾸면 됩니다. 직접 학습한 best.pt도 그대로 넣을 수 있습니다.