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 CHANGED
@@ -1,14 +1,39 @@
1
- """
2
- SBS Dataset Collector - YouTube 영상 수집 및 YOLO-World 검증 파이프라인
3
- """
4
- from pathlib import Path
1
+ """YouTube 콘텐츠 수집기 라이브러리
5
2
 
6
- __version__ = "1.0.8"
7
- __author__ = "SBS Dataset Team"
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
- # Package root directory
10
- PACKAGE_DIR = Path(__file__).parent
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
- # Default data directories (can be overridden)
13
- DEFAULT_DATA_DIR = Path.cwd() / "data"
14
- DEFAULT_OUTPUT_DIR = Path.cwd() / "outputs"
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
+ ]
@@ -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