ytcollector 1.1.7__py3-none-any.whl → 1.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ytcollector/__init__.py +1 -1
- ytcollector/analyzer.py +62 -25
- ytcollector/config.py +23 -21
- {ytcollector-1.1.7.dist-info → ytcollector-1.1.9.dist-info}/METADATA +12 -12
- ytcollector-1.1.9.dist-info/RECORD +12 -0
- ytcollector-1.1.7.dist-info/RECORD +0 -12
- {ytcollector-1.1.7.dist-info → ytcollector-1.1.9.dist-info}/WHEEL +0 -0
- {ytcollector-1.1.7.dist-info → ytcollector-1.1.9.dist-info}/entry_points.txt +0 -0
- {ytcollector-1.1.7.dist-info → ytcollector-1.1.9.dist-info}/top_level.txt +0 -0
ytcollector/__init__.py
CHANGED
ytcollector/analyzer.py
CHANGED
|
@@ -21,7 +21,7 @@ except ImportError:
|
|
|
21
21
|
EASYOCR_AVAILABLE = False
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
|
-
from ultralytics import
|
|
24
|
+
from ultralytics import YOLO
|
|
25
25
|
YOLO_AVAILABLE = True
|
|
26
26
|
except ImportError:
|
|
27
27
|
YOLO_AVAILABLE = False
|
|
@@ -32,7 +32,7 @@ try:
|
|
|
32
32
|
except ImportError:
|
|
33
33
|
USE_GPU = False
|
|
34
34
|
|
|
35
|
-
from .config import LICENSE_PLATE_PATTERNS, YOLO_MODEL_NAME, YOLO_CONFIDENCE
|
|
35
|
+
from .config import LICENSE_PLATE_PATTERNS, YOLO_MODEL_NAME, YOLO_CONFIDENCE
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class VideoAnalyzer:
|
|
@@ -59,16 +59,14 @@ class VideoAnalyzer:
|
|
|
59
59
|
self.ocr_reader = easyocr.Reader(['ko', 'en'], gpu=USE_GPU, verbose=False)
|
|
60
60
|
|
|
61
61
|
def _init_yolo(self):
|
|
62
|
-
"""YOLO
|
|
62
|
+
"""YOLO 모델 초기화 (필요할 때만, 스레드 안전, GPU 가속 체크)"""
|
|
63
63
|
if YOLO_AVAILABLE and self.yolo_model is None:
|
|
64
64
|
with self._ocr_lock:
|
|
65
65
|
if self.yolo_model is None:
|
|
66
66
|
device = "cuda" if USE_GPU else "cpu"
|
|
67
|
-
print(f" YOLO
|
|
68
|
-
self.yolo_model =
|
|
67
|
+
print(f" YOLO 모델 로딩 중... (Device: {device})")
|
|
68
|
+
self.yolo_model = YOLO(YOLO_MODEL_NAME)
|
|
69
69
|
self.yolo_model.to(device)
|
|
70
|
-
# 감지할 클래스(프롬프트) 설정
|
|
71
|
-
self.yolo_model.set_classes(YOLO_PROMPTS)
|
|
72
70
|
|
|
73
71
|
def extract_frames(self, video_path, num_frames=10):
|
|
74
72
|
"""영상에서 균등 간격으로 프레임 추출"""
|
|
@@ -106,7 +104,7 @@ class VideoAnalyzer:
|
|
|
106
104
|
gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
|
|
107
105
|
)
|
|
108
106
|
|
|
109
|
-
def detect_text(self, frame):
|
|
107
|
+
def detect_text(self, frame, return_positions=False):
|
|
110
108
|
"""EasyOCR로 텍스트 감지 (스레드 안전)"""
|
|
111
109
|
if not EASYOCR_AVAILABLE:
|
|
112
110
|
return []
|
|
@@ -114,7 +112,8 @@ class VideoAnalyzer:
|
|
|
114
112
|
self._init_ocr()
|
|
115
113
|
try:
|
|
116
114
|
h, w = frame.shape[:2]
|
|
117
|
-
|
|
115
|
+
scale = 1.0
|
|
116
|
+
|
|
118
117
|
# 가독성 개선을 위해 1080p 수준으로 리사이즈 (너무 작으면 인식률 저하)
|
|
119
118
|
if w > 1280:
|
|
120
119
|
scale = 1280 / w
|
|
@@ -124,22 +123,48 @@ class VideoAnalyzer:
|
|
|
124
123
|
scale = 960 / w
|
|
125
124
|
frame = cv2.resize(frame, (960, int(h * scale)), interpolation=cv2.INTER_CUBIC)
|
|
126
125
|
|
|
126
|
+
resized_h, resized_w = frame.shape[:2]
|
|
127
|
+
|
|
127
128
|
# 전처리: 그레이스케일 및 대비 강화 (옵션)
|
|
128
129
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
129
130
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
|
|
130
131
|
processed = clahe.apply(gray)
|
|
131
132
|
|
|
132
133
|
with self._ocr_lock:
|
|
133
|
-
# 원본(컬러)과 전처리(그레이) 중 선택 가능하나 보통 EasyOCR은 컬러에서 잘 작동함
|
|
134
|
-
# 대비 강화된 그레이스케일을 사용해봄
|
|
135
134
|
results = self.ocr_reader.readtext(processed)
|
|
136
|
-
|
|
135
|
+
|
|
136
|
+
if return_positions:
|
|
137
|
+
# 위치 정보와 함께 반환 (bbox, text, conf, y_ratio)
|
|
138
|
+
texts_with_pos = []
|
|
139
|
+
for r in results:
|
|
140
|
+
if r[2] > 0.25:
|
|
141
|
+
bbox = r[0]
|
|
142
|
+
# bbox의 y 중심점 계산 (0~1 비율)
|
|
143
|
+
y_center = (bbox[0][1] + bbox[2][1]) / 2
|
|
144
|
+
y_ratio = y_center / resized_h
|
|
145
|
+
texts_with_pos.append({
|
|
146
|
+
'text': r[1],
|
|
147
|
+
'conf': r[2],
|
|
148
|
+
'y_ratio': y_ratio
|
|
149
|
+
})
|
|
150
|
+
return texts_with_pos
|
|
151
|
+
|
|
137
152
|
# 신뢰도 임계값 0.25로 약간 하향 조정 (기존 0.3)
|
|
138
153
|
return [r[1] for r in results if r[2] > 0.25]
|
|
139
154
|
except Exception as e:
|
|
140
155
|
print(f" ⚠ OCR 에러: {e}")
|
|
141
156
|
return []
|
|
142
157
|
|
|
158
|
+
def detect_subtitle(self, frame):
|
|
159
|
+
"""자막 감지 - 화면 하단 60%~95% 영역의 텍스트만 필터링"""
|
|
160
|
+
texts_with_pos = self.detect_text(frame, return_positions=True)
|
|
161
|
+
if not texts_with_pos:
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
# 자막은 주로 화면 하단에 위치 (y_ratio 0.6 ~ 0.95)
|
|
165
|
+
subtitles = [t['text'] for t in texts_with_pos if 0.6 <= t['y_ratio'] <= 0.95]
|
|
166
|
+
return subtitles
|
|
167
|
+
|
|
143
168
|
def detect_license_plate(self, frame, texts=None):
|
|
144
169
|
"""
|
|
145
170
|
ROI 기반 번호판 감지 (최적화 버전)
|
|
@@ -153,19 +178,26 @@ class VideoAnalyzer:
|
|
|
153
178
|
self._init_yolo()
|
|
154
179
|
results = self.yolo_model(frame, verbose=False, conf=YOLO_CONFIDENCE)
|
|
155
180
|
|
|
156
|
-
|
|
181
|
+
yolo_detected = False
|
|
157
182
|
roi_ocr_matched = False
|
|
158
|
-
|
|
183
|
+
|
|
159
184
|
for r in results:
|
|
160
185
|
# 0: license plate
|
|
161
186
|
for box in r.boxes:
|
|
162
187
|
if box.cls == 0:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# ROI 크로핑 및 타겟 OCR
|
|
188
|
+
yolo_detected = True
|
|
189
|
+
|
|
190
|
+
# ROI 크기 필터링 (너무 작거나 큰 영역 제외)
|
|
168
191
|
x1, y1, x2, y2 = map(int, box.xyxy[0])
|
|
192
|
+
roi_w, roi_h = x2 - x1, y2 - y1
|
|
193
|
+
frame_h, frame_w = frame.shape[:2]
|
|
194
|
+
roi_area_ratio = (roi_w * roi_h) / (frame_w * frame_h)
|
|
195
|
+
|
|
196
|
+
# ROI가 전체 화면의 0.1% ~ 20% 사이여야 함
|
|
197
|
+
if roi_area_ratio < 0.001 or roi_area_ratio > 0.2:
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# ROI 크로핑 및 타겟 OCR
|
|
169
201
|
h, w = frame.shape[:2]
|
|
170
202
|
# 패딩 10% 추가
|
|
171
203
|
pad_w = int((x2 - x1) * 0.1)
|
|
@@ -192,8 +224,8 @@ class VideoAnalyzer:
|
|
|
192
224
|
if roi_ocr_matched: break
|
|
193
225
|
if roi_ocr_matched: break
|
|
194
226
|
|
|
195
|
-
# 최종
|
|
196
|
-
if
|
|
227
|
+
# 최종 판정: YOLO 감지 + OCR 패턴 매칭 둘 다 만족해야 함 (오탐지 방지)
|
|
228
|
+
if yolo_detected and roi_ocr_matched:
|
|
197
229
|
return True
|
|
198
230
|
|
|
199
231
|
except Exception as e:
|
|
@@ -305,14 +337,19 @@ class VideoAnalyzer:
|
|
|
305
337
|
results['license_plate'] = True
|
|
306
338
|
detected_now = True
|
|
307
339
|
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
texts = self.
|
|
340
|
+
# 텍스트/자막 감지
|
|
341
|
+
if target_category == 'text':
|
|
342
|
+
# 자막 영역 필터링 (화면 하단 60%~95%)
|
|
343
|
+
texts = self.detect_subtitle(frame)
|
|
312
344
|
if texts:
|
|
313
345
|
results['text'] = True
|
|
314
346
|
all_texts.extend(texts)
|
|
315
347
|
detected_now = True
|
|
348
|
+
elif detected_now and target_category != 'license_plate':
|
|
349
|
+
texts = self.detect_text(frame)
|
|
350
|
+
if texts:
|
|
351
|
+
results['text'] = True
|
|
352
|
+
all_texts.extend(texts)
|
|
316
353
|
elif target_category == 'license_plate' and not results['license_plate']:
|
|
317
354
|
# 번호판을 못 찾은 경우에만 전체 화면 OCR 한 번 더 시도 (보수적 접근)
|
|
318
355
|
texts = self.detect_text(frame)
|
ytcollector/config.py
CHANGED
|
@@ -11,21 +11,21 @@ USER_AGENTS = [
|
|
|
11
11
|
CATEGORY_QUERIES = {
|
|
12
12
|
'face': [
|
|
13
13
|
"SBS 인터뷰 클립",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
14
|
+
"MBC 뉴스 인터뷰",
|
|
15
|
+
"tvN 유퀴즈 일반인 인터뷰",
|
|
16
|
+
"JTBC 뉴스룸 초대석",
|
|
17
|
+
"기자회견 현장",
|
|
18
|
+
"연예대상 레드카펫 직캠",
|
|
19
|
+
"길거리 인터뷰 브이로그",
|
|
20
|
+
"무대인사 직캠 얼굴",
|
|
21
21
|
],
|
|
22
22
|
'license_plate': [
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
23
|
+
"SBS 드라마 자동차 추격 장면",
|
|
24
|
+
"SBS 드라마 주차장 씬",
|
|
25
|
+
"SBS 드라마 차량 씬",
|
|
26
|
+
"SBS 드라마 운전 장면",
|
|
27
|
+
"SBS 드라마 택시 장면",
|
|
28
|
+
"SBS 드라마 교통사고 장면",
|
|
29
29
|
],
|
|
30
30
|
'tattoo': [
|
|
31
31
|
"타투 시술 영상",
|
|
@@ -36,11 +36,14 @@ CATEGORY_QUERIES = {
|
|
|
36
36
|
"tattoo session",
|
|
37
37
|
],
|
|
38
38
|
'text': [
|
|
39
|
-
"SBS 런닝맨 레전드",
|
|
40
|
-
"SBS
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
39
|
+
"SBS 런닝맨 자막 레전드",
|
|
40
|
+
"SBS 미우새 자막 모음",
|
|
41
|
+
"tvN 놀라운토요일 자막",
|
|
42
|
+
"JTBC 아는형님 자막 레전드",
|
|
43
|
+
"MBC 나혼자산다 자막",
|
|
44
|
+
"유튜버 예능 자막 편집",
|
|
45
|
+
"브이로그 자막 효과",
|
|
46
|
+
"먹방 자막 편집",
|
|
44
47
|
],
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -69,9 +72,8 @@ BLACKLIST_KEYWORDS = {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
# YOLO 설정
|
|
72
|
-
YOLO_MODEL_NAME = '
|
|
73
|
-
YOLO_CONFIDENCE = 0.
|
|
74
|
-
YOLO_PROMPTS = ["license plate"]
|
|
75
|
+
YOLO_MODEL_NAME = 'license_plate_detector.pt' # 번호판 전용 학습 모델
|
|
76
|
+
YOLO_CONFIDENCE = 0.5 # 오탐지 방지를 위해 신뢰도 상향
|
|
75
77
|
|
|
76
78
|
# 번호판 정규식 패턴 (한국 자동차 번호판 중심)
|
|
77
79
|
LICENSE_PLATE_PATTERNS = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ytcollector
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
4
4
|
Summary: YouTube 콘텐츠 수집기 - 얼굴, 번호판, 타투, 텍스트 감지
|
|
5
5
|
Author: YTCollector Team
|
|
6
6
|
License: MIT
|
|
@@ -44,14 +44,12 @@ Requires-Dist: ytcollector[analysis,dev]; extra == "all"
|
|
|
44
44
|
pip install yt-dlp
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
### 분석 기능용 패키지 (권장 - v1.1.
|
|
47
|
+
### 분석 기능용 패키지 (권장 - v1.1.9+)
|
|
48
48
|
|
|
49
49
|
분석 기능을 원활하게 사용하려면 아래 패키지들이 필요합니다. GPU(CUDA)가 설치된 경우 자동으로 가속이 활성화됩니다.
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
pip install opencv-python easyocr numpy ultralytics
|
|
53
|
-
# YOLO-World 기능을 사용하려면 아래 CLIP 라이브러리 수동 설치가 필요합니다.
|
|
54
|
-
pip install "git+https://github.com/ultralytics/CLIP.git"
|
|
55
53
|
```
|
|
56
54
|
|
|
57
55
|
## 사용법
|
|
@@ -80,10 +78,10 @@ ytcollector
|
|
|
80
78
|
|
|
81
79
|
| 카테고리 | 설명 | 검색 소스 |
|
|
82
80
|
|----------|------|-----------|
|
|
83
|
-
| `face` | 얼굴/인물 | SBS 인터뷰,
|
|
84
|
-
| `license_plate` | 자동차 번호판 |
|
|
81
|
+
| `face` | 얼굴/인물 | SBS/MBC/tvN/JTBC 인터뷰, 기자회견 등 |
|
|
82
|
+
| `license_plate` | 자동차 번호판 | SBS 드라마 자동차/추격/주차장 씬 등 |
|
|
85
83
|
| `tattoo` | 타투/문신 | 타투 시술, 타투이스트 작업 영상 |
|
|
86
|
-
| `text` | 텍스트/자막 | SBS
|
|
84
|
+
| `text` | 텍스트/자막 | SBS/tvN/JTBC/MBC 예능, 유튜버 자막 편집 등 |
|
|
87
85
|
|
|
88
86
|
## 예시
|
|
89
87
|
|
|
@@ -190,14 +188,16 @@ https://www.youtube.com/watch?v=aqz-KE-bpKQ, 00:10, sample_task
|
|
|
190
188
|
| 감지 항목 | 사용 기술 | 설명 |
|
|
191
189
|
|-----------|-----------|------|
|
|
192
190
|
| 얼굴 | OpenCV Haar Cascade | 정면 얼굴 감지 |
|
|
193
|
-
| 텍스트 | EasyOCR | 한국어/영어 문자 인식
|
|
194
|
-
| 번호판 |
|
|
191
|
+
| 텍스트 | EasyOCR | 한국어/영어 문자 인식 |
|
|
192
|
+
| 번호판 | YOLOv8 전용 모델 + OCR | v1.1.9: 번호판 전용 학습 모델 + 한국 번호판 패턴 매칭 |
|
|
195
193
|
| 타투 | OpenCV HSV 분석 | 피부 영역 내 잉크 패턴 감지 |
|
|
196
194
|
|
|
197
|
-
### 주요 최적화 (v1.1.5~1.1.
|
|
198
|
-
-
|
|
195
|
+
### 주요 최적화 (v1.1.5~1.1.9)
|
|
196
|
+
- **번호판 전용 모델** (v1.1.9): YOLO-World → 번호판 전용 학습 모델로 교체 (감지율 대폭 향상, 모델 크기 27MB→6MB)
|
|
197
|
+
- **오탐지 방지** (v1.1.9): YOLO 감지 + OCR 패턴 매칭 둘 다 만족해야 번호판 판정, ROI 크기 필터링 추가
|
|
198
|
+
- **ROI 기반 감지**: 전체 화면이 아닌 YOLO가 지정한 영역만 OCR하여 속도와 정확도 향상
|
|
199
199
|
- **GPU 가속 지원**: CUDA 사용 가능 시 YOLO 및 OCR 자동 가속
|
|
200
|
-
- **로그 기반 중복 방지**:
|
|
200
|
+
- **로그 기반 중복 방지**: `youtube_url_*.txt` 기록을 참조하여 중복 분석 방지
|
|
201
201
|
|
|
202
202
|
## 주의사항
|
|
203
203
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ytcollector/__init__.py,sha256=EaxBP_0Fv0LEFdg067uZxBrQHwOKGX8u08Y4b5uF1-Q,1094
|
|
2
|
+
ytcollector/analyzer.py,sha256=7VJt4chc25HsEz8OwBDZhTz_8LnkpgSBM6mJKQpIUls,14391
|
|
3
|
+
ytcollector/cli.py,sha256=aHF4EuQRPLKh65lnkI_dZ0ResztlVjpHlS5iHfzmpig,5577
|
|
4
|
+
ytcollector/config.py,sha256=HmcFqMV1Z3kClnGKYi0q9cRZIXMRPAxIEI8cggatfrU,3199
|
|
5
|
+
ytcollector/dataset_builder.py,sha256=nfArEwszoCln48n3T0Eff_4OOaYv8FF0YH8cARBGMWQ,2608
|
|
6
|
+
ytcollector/downloader.py,sha256=TeC6agUmSPHZSZ9jdoc42i8i_NobzTEkoRtAIgW80kI,14544
|
|
7
|
+
ytcollector/utils.py,sha256=6XDif-e3GbMHmUvTsBT0YblxNxYnS-2I8HnmjMBZs-M,4254
|
|
8
|
+
ytcollector-1.1.9.dist-info/METADATA,sha256=WQIup61B02yB9ddtOaFGtlDYoV4nJVRD-vvVI_sVjc0,7208
|
|
9
|
+
ytcollector-1.1.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
ytcollector-1.1.9.dist-info/entry_points.txt,sha256=waiVuSJJYt-6_DAal-T4JkHgejo7wKYLdKrEI7tZ-ms,127
|
|
11
|
+
ytcollector-1.1.9.dist-info/top_level.txt,sha256=wozNyCUm0eMOm-9U81yTql6oGaM2O5rWVBXDb93zzyQ,12
|
|
12
|
+
ytcollector-1.1.9.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
ytcollector/__init__.py,sha256=uXLmOZ3da_7GwVej0hDAuiUJsw5XweBBUrUs4sJo7J4,1094
|
|
2
|
-
ytcollector/analyzer.py,sha256=raw7tcERbNG8cDTnKDr95VA3Nju9We4jW7XGheUUPWE,13097
|
|
3
|
-
ytcollector/cli.py,sha256=aHF4EuQRPLKh65lnkI_dZ0ResztlVjpHlS5iHfzmpig,5577
|
|
4
|
-
ytcollector/config.py,sha256=ZjyDWQg4haJPwUlP-eW0hXa_I2g9wyNaI8y5mxEU0vc,3040
|
|
5
|
-
ytcollector/dataset_builder.py,sha256=nfArEwszoCln48n3T0Eff_4OOaYv8FF0YH8cARBGMWQ,2608
|
|
6
|
-
ytcollector/downloader.py,sha256=TeC6agUmSPHZSZ9jdoc42i8i_NobzTEkoRtAIgW80kI,14544
|
|
7
|
-
ytcollector/utils.py,sha256=6XDif-e3GbMHmUvTsBT0YblxNxYnS-2I8HnmjMBZs-M,4254
|
|
8
|
-
ytcollector-1.1.7.dist-info/METADATA,sha256=Qyop1AVyK-BmWgrjsGUMiYbE55kmOZSwMKYWEADQzRk,7156
|
|
9
|
-
ytcollector-1.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
-
ytcollector-1.1.7.dist-info/entry_points.txt,sha256=waiVuSJJYt-6_DAal-T4JkHgejo7wKYLdKrEI7tZ-ms,127
|
|
11
|
-
ytcollector-1.1.7.dist-info/top_level.txt,sha256=wozNyCUm0eMOm-9U81yTql6oGaM2O5rWVBXDb93zzyQ,12
|
|
12
|
-
ytcollector-1.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|