ytcollector 1.0.8__py3-none-any.whl → 1.1.1__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 +36 -11
- ytcollector/analyzer.py +239 -0
- ytcollector/cli.py +186 -218
- ytcollector/config.py +79 -61
- ytcollector/dataset_builder.py +71 -0
- ytcollector/downloader.py +315 -470
- ytcollector/utils.py +116 -134
- ytcollector-1.1.1.dist-info/METADATA +207 -0
- ytcollector-1.1.1.dist-info/RECORD +12 -0
- ytcollector-1.1.1.dist-info/entry_points.txt +4 -0
- {ytcollector-1.0.8.dist-info → ytcollector-1.1.1.dist-info}/top_level.txt +0 -1
- config/settings.py +0 -39
- ytcollector/verifier.py +0 -187
- ytcollector-1.0.8.dist-info/METADATA +0 -105
- ytcollector-1.0.8.dist-info/RECORD +0 -12
- ytcollector-1.0.8.dist-info/entry_points.txt +0 -2
- {ytcollector-1.0.8.dist-info → ytcollector-1.1.1.dist-info}/WHEEL +0 -0
ytcollector/__init__.py
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
|
-
"""
|
|
2
|
-
SBS Dataset Collector - YouTube 영상 수집 및 YOLO-World 검증 파이프라인
|
|
3
|
-
"""
|
|
4
|
-
from pathlib import Path
|
|
1
|
+
"""YouTube 콘텐츠 수집기 라이브러리
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
외부에서 라이브러리로 사용하거나 CLI로 실행할 수 있습니다.
|
|
4
|
+
|
|
5
|
+
라이브러리 사용 예시:
|
|
6
|
+
from ytcollector import YouTubeDownloader, run
|
|
7
|
+
|
|
8
|
+
# 방법 1: YouTubeDownloader 직접 사용
|
|
9
|
+
downloader = YouTubeDownloader(output_path="./videos")
|
|
10
|
+
count = downloader.collect("face", max_videos=5)
|
|
11
|
+
|
|
12
|
+
# 방법 2: run() 함수 사용 (간단한 방법)
|
|
13
|
+
results = run(categories=["face", "text"], count=3)
|
|
14
|
+
|
|
15
|
+
CLI 사용 예시:
|
|
16
|
+
ytcollector -c face -n 5
|
|
17
|
+
ytc -c face text --fast
|
|
18
|
+
"""
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
from .config import CATEGORY_NAMES, CATEGORY_QUERIES, USER_AGENTS, LICENSE_PLATE_PATTERNS
|
|
21
|
+
from .analyzer import VideoAnalyzer, check_dependencies
|
|
22
|
+
from .downloader import YouTubeDownloader
|
|
23
|
+
from .cli import run, main as cli_main
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
__all__ = [
|
|
27
|
+
# 주요 클래스
|
|
28
|
+
"VideoAnalyzer",
|
|
29
|
+
"YouTubeDownloader",
|
|
30
|
+
# 설정
|
|
31
|
+
"CATEGORY_NAMES",
|
|
32
|
+
"CATEGORY_QUERIES",
|
|
33
|
+
"USER_AGENTS",
|
|
34
|
+
"LICENSE_PLATE_PATTERNS",
|
|
35
|
+
# 유틸리티
|
|
36
|
+
"check_dependencies",
|
|
37
|
+
"run",
|
|
38
|
+
"cli_main",
|
|
39
|
+
]
|
ytcollector/analyzer.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import threading
|
|
3
|
+
from .config import LICENSE_PLATE_PATTERNS
|
|
4
|
+
|
|
5
|
+
# 선택적 import
|
|
6
|
+
try:
|
|
7
|
+
import cv2
|
|
8
|
+
CV2_AVAILABLE = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
CV2_AVAILABLE = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import easyocr
|
|
14
|
+
EASYOCR_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
EASYOCR_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import numpy as np
|
|
20
|
+
NUMPY_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
NUMPY_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VideoAnalyzer:
|
|
26
|
+
"""영상 분석 클래스 - 얼굴, 텍스트, 번호판, 타투 감지"""
|
|
27
|
+
|
|
28
|
+
_ocr_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.ocr_reader = None
|
|
32
|
+
self.face_cascade = None
|
|
33
|
+
|
|
34
|
+
if CV2_AVAILABLE:
|
|
35
|
+
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
|
36
|
+
self.face_cascade = cv2.CascadeClassifier(cascade_path)
|
|
37
|
+
|
|
38
|
+
def _init_ocr(self):
|
|
39
|
+
"""OCR 리더 초기화 (필요할 때만, 스레드 안전)"""
|
|
40
|
+
if EASYOCR_AVAILABLE and self.ocr_reader is None:
|
|
41
|
+
with self._ocr_lock:
|
|
42
|
+
if self.ocr_reader is None:
|
|
43
|
+
print(" OCR 엔진 초기화 중...")
|
|
44
|
+
self.ocr_reader = easyocr.Reader(['ko', 'en'], gpu=False, verbose=False)
|
|
45
|
+
|
|
46
|
+
def extract_frames(self, video_path, num_frames=10):
|
|
47
|
+
"""영상에서 균등 간격으로 프레임 추출"""
|
|
48
|
+
if not CV2_AVAILABLE:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
cap = cv2.VideoCapture(video_path)
|
|
52
|
+
if not cap.isOpened():
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
56
|
+
if total_frames <= 0:
|
|
57
|
+
cap.release()
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
frame_indices = [int(i * total_frames / (num_frames + 1)) for i in range(1, num_frames + 1)]
|
|
61
|
+
frames = []
|
|
62
|
+
|
|
63
|
+
for idx in frame_indices:
|
|
64
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
|
65
|
+
ret, frame = cap.read()
|
|
66
|
+
if ret:
|
|
67
|
+
frames.append(frame)
|
|
68
|
+
|
|
69
|
+
cap.release()
|
|
70
|
+
return frames
|
|
71
|
+
|
|
72
|
+
def detect_faces(self, frame):
|
|
73
|
+
"""Haar Cascade로 얼굴 감지"""
|
|
74
|
+
if not CV2_AVAILABLE or self.face_cascade is None:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
78
|
+
return self.face_cascade.detectMultiScale(
|
|
79
|
+
gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def detect_text(self, frame):
|
|
83
|
+
"""EasyOCR로 텍스트 감지 (스레드 안전)"""
|
|
84
|
+
if not EASYOCR_AVAILABLE:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
self._init_ocr()
|
|
88
|
+
try:
|
|
89
|
+
h, w = frame.shape[:2]
|
|
90
|
+
if w > 640:
|
|
91
|
+
scale = 640 / w
|
|
92
|
+
frame = cv2.resize(frame, (640, int(h * scale)))
|
|
93
|
+
|
|
94
|
+
with self._ocr_lock:
|
|
95
|
+
results = self.ocr_reader.readtext(frame)
|
|
96
|
+
return [r[1] for r in results if r[2] > 0.3]
|
|
97
|
+
except:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
def detect_license_plate(self, texts):
|
|
101
|
+
"""텍스트에서 번호판 패턴 감지"""
|
|
102
|
+
for text in texts:
|
|
103
|
+
text_clean = text.replace(' ', '').upper()
|
|
104
|
+
for pattern in LICENSE_PLATE_PATTERNS:
|
|
105
|
+
if re.search(pattern, text_clean):
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def detect_tattoo(self, frame):
|
|
110
|
+
"""피부 영역에서 타투(어두운 잉크 패턴) 감지"""
|
|
111
|
+
if not CV2_AVAILABLE or not NUMPY_AVAILABLE:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
|
116
|
+
|
|
117
|
+
# 피부색 범위
|
|
118
|
+
lower_skin = np.array([0, 30, 80], dtype=np.uint8)
|
|
119
|
+
upper_skin = np.array([17, 170, 255], dtype=np.uint8)
|
|
120
|
+
skin_mask = cv2.inRange(hsv, lower_skin, upper_skin)
|
|
121
|
+
|
|
122
|
+
# 노이즈 제거
|
|
123
|
+
kernel = np.ones((5, 5), np.uint8)
|
|
124
|
+
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)
|
|
125
|
+
skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
|
|
126
|
+
|
|
127
|
+
skin_pixels = cv2.countNonZero(skin_mask)
|
|
128
|
+
total_pixels = frame.shape[0] * frame.shape[1]
|
|
129
|
+
|
|
130
|
+
# 피부 영역 최소 10% 필요
|
|
131
|
+
if skin_pixels < total_pixels * 0.10:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
# 피부 영역 내 어두운 픽셀(타투) 감지
|
|
135
|
+
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
136
|
+
skin_gray = cv2.bitwise_and(gray, gray, mask=skin_mask)
|
|
137
|
+
dark_mask = cv2.inRange(skin_gray, 1, 80)
|
|
138
|
+
|
|
139
|
+
dark_pixels = cv2.countNonZero(dark_mask)
|
|
140
|
+
dark_ratio = dark_pixels / max(skin_pixels, 1)
|
|
141
|
+
|
|
142
|
+
# 어두운 영역이 3~35%일 때 타투로 판정
|
|
143
|
+
if 0.03 < dark_ratio < 0.35:
|
|
144
|
+
contours, _ = cv2.findContours(dark_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
145
|
+
significant = [c for c in contours if cv2.contourArea(c) > 100]
|
|
146
|
+
return len(significant) >= 1
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
except:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def analyze(self, video_path):
|
|
153
|
+
"""영상 전체 분석"""
|
|
154
|
+
results = {
|
|
155
|
+
'face': False,
|
|
156
|
+
'text': False,
|
|
157
|
+
'license_plate': False,
|
|
158
|
+
'tattoo': False,
|
|
159
|
+
'face_count': 0,
|
|
160
|
+
'detected_texts': [],
|
|
161
|
+
'first_detection_sec': None,
|
|
162
|
+
'first_detection_ts': None
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if not CV2_AVAILABLE:
|
|
166
|
+
print(" ⚠ OpenCV 미설치")
|
|
167
|
+
return results
|
|
168
|
+
|
|
169
|
+
# 영상 정보 가져오기
|
|
170
|
+
cap = cv2.VideoCapture(video_path)
|
|
171
|
+
if not cap.isOpened():
|
|
172
|
+
return results
|
|
173
|
+
|
|
174
|
+
fps = cap.get(cv2.CAP_PROP_FPS) or 30
|
|
175
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
176
|
+
cap.release()
|
|
177
|
+
|
|
178
|
+
num_analysis_frames = 10
|
|
179
|
+
frame_indices = [int(i * total_frames / (num_analysis_frames + 1)) for i in range(1, num_analysis_frames + 1)]
|
|
180
|
+
|
|
181
|
+
all_texts = []
|
|
182
|
+
total_faces = 0
|
|
183
|
+
|
|
184
|
+
cap = cv2.VideoCapture(video_path)
|
|
185
|
+
for idx in frame_indices:
|
|
186
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
|
187
|
+
ret, frame = cap.read()
|
|
188
|
+
if not ret: continue
|
|
189
|
+
|
|
190
|
+
# 현재 프레임의 시간(초)
|
|
191
|
+
current_sec = idx / fps
|
|
192
|
+
detected_now = False
|
|
193
|
+
|
|
194
|
+
# 얼굴
|
|
195
|
+
faces = self.detect_faces(frame)
|
|
196
|
+
if len(faces) > 0:
|
|
197
|
+
results['face'] = True
|
|
198
|
+
total_faces += len(faces)
|
|
199
|
+
detected_now = True
|
|
200
|
+
|
|
201
|
+
# 텍스트
|
|
202
|
+
texts = self.detect_text(frame)
|
|
203
|
+
if texts:
|
|
204
|
+
results['text'] = True
|
|
205
|
+
all_texts.extend(texts)
|
|
206
|
+
detected_now = True
|
|
207
|
+
|
|
208
|
+
# 타투
|
|
209
|
+
if self.detect_tattoo(frame):
|
|
210
|
+
results['tattoo'] = True
|
|
211
|
+
detected_now = True
|
|
212
|
+
|
|
213
|
+
# 첫 감지 시점 기록
|
|
214
|
+
if detected_now and results['first_detection_sec'] is None:
|
|
215
|
+
results['first_detection_sec'] = current_sec
|
|
216
|
+
m, s = int(current_sec // 60), int(current_sec % 60)
|
|
217
|
+
results['first_detection_ts'] = f"{m:02d}:{s:02d}"
|
|
218
|
+
|
|
219
|
+
cap.release()
|
|
220
|
+
|
|
221
|
+
# 번호판 (텍스트에서)
|
|
222
|
+
if all_texts:
|
|
223
|
+
results['license_plate'] = self.detect_license_plate(all_texts)
|
|
224
|
+
results['detected_texts'] = list(set(all_texts))[:10]
|
|
225
|
+
|
|
226
|
+
results['face_count'] = total_faces
|
|
227
|
+
return results
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def check_dependencies():
|
|
231
|
+
"""의존성 체크"""
|
|
232
|
+
missing = []
|
|
233
|
+
if not CV2_AVAILABLE:
|
|
234
|
+
missing.append("opencv-python")
|
|
235
|
+
if not EASYOCR_AVAILABLE:
|
|
236
|
+
missing.append("easyocr")
|
|
237
|
+
if not NUMPY_AVAILABLE:
|
|
238
|
+
missing.append("numpy")
|
|
239
|
+
return missing
|