feat(pipeline): v3-v4 dedup + panorama stitching + 1080p support
- HSV-aware _trim_to_content (white ratio 30-97%) - pHash cluster dedup: dHash 32x32(1024bit), max_hamming=20 - Panoramic stitching: template matching scroll offset detection - 4-stage pipeline: MSE -> Panorama -> pHash - 1080p download priority + MAX_FRAME_WIDTH=1280 cap - test_pipeline.py with YouTube URLs and --download mode - 3 new known-issues documented - devlog + STATUS.md updated
This commit is contained in:
@@ -4,26 +4,38 @@
|
|||||||
|
|
||||||
| 기능 | 상태 | 비고 |
|
| 기능 | 상태 | 비고 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| YouTube 다운로드 | ✅ 완료 | yt-dlp + 쿠키 인증 |
|
| YouTube 다운로드 | ✅ 완료 | yt-dlp, 1080p 우선 다운로드 |
|
||||||
| 프레임 추출 | ✅ 완료 | fps=2 기본값 |
|
| 프레임 추출 | ✅ 완료 | fps=2, MAX_FRAME_WIDTH=1280 캡 |
|
||||||
| 패턴 감지 (overlay) | ✅ 완료 | Tab 라인 검증 포함 |
|
| 패턴 감지 (overlay) | ✅ 완료 | Tab 라인 검증 포함 |
|
||||||
| 패턴 감지 (split) | ✅ 완료 | 밝기 기준 엄격화 |
|
| 패턴 감지 (split) | ✅ 완료 | 밝기 기준 엄격화 |
|
||||||
| 패턴 감지 (scroll) | ✅ 완료 | 기본 폴백 |
|
| 패턴 감지 (scroll) | ✅ 완료 | 기본 폴백 |
|
||||||
| MSE 기반 중복 제거 | ✅ 완료 | 히스토그램 → MSE 전환 |
|
| HSV 기반 Tab 검출 | ✅ 완료 | 2-tier HSV 마스크, 960px 업스케일 |
|
||||||
| 오버레이 정규화 비교 | ✅ 완료 | 320×120 정규화 + 슬라이딩 윈도우 |
|
| MSE 기반 중복 제거 | ✅ 완료 | 480px 정규화 비교 |
|
||||||
|
| pHash 클러스터 중복제거 | ✅ 완료 | dHash 32×32(1024bit), max_hamming=20 |
|
||||||
|
| 파노라마 스티칭 | ✅ 완료 | 템플릿 매칭 수평 스크롤 합성 |
|
||||||
|
| 오버레이 정규화 비교 | ✅ 완료 | 480×180 정규화 + 전체 히스토리 MSE 비교 |
|
||||||
| PDF/PNG 생성 | ✅ 완료 | A4 + 롱 이미지 |
|
| PDF/PNG 생성 | ✅ 완료 | A4 + 롱 이미지 |
|
||||||
|
|
||||||
|
## 처리 파이프라인 (scroll)
|
||||||
|
|
||||||
|
```
|
||||||
|
Raw Frames → HSV Strip 검출 → Median Crop → MSE 1차 → 파노라마 스티칭 → pHash 2차 → PDF
|
||||||
|
```
|
||||||
|
|
||||||
## 최근 변경
|
## 최근 변경
|
||||||
|
|
||||||
| 날짜 | 변경 내용 |
|
| 날짜 | 변경 내용 |
|
||||||
|------|-----------|
|
|------|-----------|
|
||||||
|
| 2026-03-25 | 1080p 우선 다운로드 + MAX_FRAME_WIDTH=1280 캡 (OOM 방지) |
|
||||||
|
| 2026-03-25 | dHash 32×32 + max_hamming=20으로 pHash 정밀도 향상 |
|
||||||
|
| 2026-03-25 | 파노라마 스티칭: 템플릿 매칭 스크롤 오프셋 검출 + 연속 프레임 합성 |
|
||||||
|
| 2026-03-25 | HSV 트림: 흰색비율 30~97% 기반 정밀 크롭 |
|
||||||
|
| 2026-03-25 | overlay 프레임 수 최적화: 858→51프레임 (OVERLAY_SIMILARITY_THRESHOLD=0.55) |
|
||||||
| 2026-03-24 | 패턴 감지 고도화: overlay→split→scroll 우선순위 |
|
| 2026-03-24 | 패턴 감지 고도화: overlay→split→scroll 우선순위 |
|
||||||
| 2026-03-24 | 히스토그램 비교 → MSE 픽셀 비교로 전환 |
|
| 2026-03-24 | 히스토그램 비교 → MSE 픽셀 비교로 전환 |
|
||||||
| 2026-03-24 | split 모드: 42% 크롭 + 밝기 필터 + Tab 라인 검증 |
|
|
||||||
| 2026-03-24 | overlay 모드: 정규화 + 슬라이딩 윈도우 중복 제거 |
|
|
||||||
| 2026-03-24 | split 감지 조건 엄격화 (top>180, bottom<100) |
|
|
||||||
|
|
||||||
## 알려진 제한사항
|
## 알려진 제한사항
|
||||||
|
|
||||||
- 오버레이형 영상(空奏列車)에서 추출 프레임 수가 아직 많을 수 있음 (MSE 임계값 추가 튜닝 필요)
|
- 1080p 처리 시 여전히 중복 프레임 존재 가능 (마디번호 기반 추가 검증 필요)
|
||||||
- 영상 내 Tab이 반복되는 곡은 실제 고유 프레임 수가 적음 (正常 동작)
|
- 순차 영상 처리 시 메모리 누적 주의 (gc.collect 필수)
|
||||||
|
- test_pipeline.py 아직 메인 코드와 완전 통합 안 됨
|
||||||
|
|||||||
@@ -55,3 +55,21 @@
|
|||||||
- **원인**: _detect_tab_overlay가 프레임마다 다른 크기의 바운딩박스 반환 (69~360px)
|
- **원인**: _detect_tab_overlay가 프레임마다 다른 크기의 바운딩박스 반환 (69~360px)
|
||||||
- **해결**: 320×120 흰색 캔버스에 정규화 후 비교 + 슬라이딩 윈도우(5프레임)
|
- **해결**: 320×120 흰색 캔버스에 정규화 후 비교 + 슬라이딩 윈도우(5프레임)
|
||||||
- **주의**: overlay 프레임 수 최적화는 아직 진행 중 (추가 튜닝 필요)
|
- **주의**: overlay 프레임 수 최적화는 아직 진행 중 (추가 튜닝 필요)
|
||||||
|
|
||||||
|
### [2026-03-25] pHash 16×16 — Tab 프레임 과도합병
|
||||||
|
- **증상**: 서로 다른 Tab 페이지가 pHash 클러스터링에서 동일 그룹으로 합병 (20→9 프레임)
|
||||||
|
- **원인**: 16×16 dHash(256비트)는 Tab 구조(6선 + 숫자)를 구분하기엔 해상도 부족. 모든 Tab이 유사한 hash 생성
|
||||||
|
- **해결**: dHash 32×32(1024비트)로 확대 + max_hamming 50→20 조정
|
||||||
|
- **주의**: hash_size와 max_hamming은 항상 쌍으로 조정해야 함 (비트수 대비 비율)
|
||||||
|
|
||||||
|
### [2026-03-25] 1080p 프레임 — 메모리 부족/프로세스 행
|
||||||
|
- **증상**: 1920×1080 프레임 500+개 로딩 시 프로세스 무한 대기 (3.5GB+ RAM)
|
||||||
|
- **원인**: extract_frames가 모든 프레임을 list에 보관, 1080p는 프레임당 ~6MB
|
||||||
|
- **해결**: MAX_FRAME_WIDTH=1280 캡 + gc.collect() 추가. 4K→1280px 다운스케일
|
||||||
|
- **주의**: 영상 3개 순차 처리 시 GC 없으면 누적 메모리로 swap thrashing 발생
|
||||||
|
|
||||||
|
### [2026-03-25] yt-dlp 다운로드 — 360p 폴백
|
||||||
|
- **증상**: `bestvideo[height>=720]` 포맷으로 요청했으나 640×360 파일 다운로드
|
||||||
|
- **원인**: format string의 `/best` 폴백이 720p 없을 때 360p 선택. 또는 mp4 전용 필터가 해상도 제한
|
||||||
|
- **해결**: 명시적 1080p 우선 + 720p 폴백 체인 분리 (`bestvideo[height>=1080]/.../best[height>=720]/best`)
|
||||||
|
- **주의**: 캐시된 파일이 있으면 재다운로드 안 함 — 해상도 변경 시 기존 파일 삭제 필요
|
||||||
@@ -87,9 +87,9 @@ git log --oneline -20
|
|||||||
|
|
||||||
| 커밋 유형 | Vikunja 액션 |
|
| 커밋 유형 | Vikunja 액션 |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| 기존 태스크 해당 작업 **완료** | `C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py done {ID}` |
|
| 기존 태스크 해당 작업 **완료** | `C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py done {ID}` |
|
||||||
| 신규 작업 완료 (기존 태스크 없음) | `C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --done --labels Backend,Priority:High` |
|
| 신규 작업 완료 (기존 태스크 없음) | `C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --done --labels Backend,Priority:High` |
|
||||||
| 작업 중 발견된 **미완료 TODO** | `C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:Mid` |
|
| 작업 중 발견된 **미완료 TODO** | `C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:Mid` |
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> 모든 커밋이 기존 또는 신규 태스크에 매핑되었는지 확인.
|
> 모든 커밋이 기존 또는 신규 태스크에 매핑되었는지 확인.
|
||||||
@@ -97,13 +97,13 @@ git log --oneline -20
|
|||||||
### 2-2. 완료 처리
|
### 2-2. 완료 처리
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py done {TASK_ID}
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py done {TASK_ID}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2-3. 신규 태스크 생성
|
### 2-3. 신규 태스크 생성
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py create "제목" "설명" --labels Backend,Priority:High
|
||||||
```
|
```
|
||||||
|
|
||||||
### 라벨 규칙
|
### 라벨 규칙
|
||||||
@@ -138,11 +138,11 @@ git diff --name-only .agent/references/
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# STATUS.md가 변경된 경우
|
# STATUS.md가 변경된 경우
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\wiki_helper.py update "Status" .agent\references\STATUS.md
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\wiki_helper.py update "Status" .agent\references\STATUS.md
|
||||||
```
|
```
|
||||||
```powershell
|
```powershell
|
||||||
# architecture.md가 변경된 경우
|
# architecture.md가 변경된 경우
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\wiki_helper.py update "Architecture" .agent\references\architecture.md
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\wiki_helper.py update "Architecture" .agent\references\architecture.md
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ if sys.stdout.encoding != "utf-8":
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
API_BASE = "https://plan.variet.net/api/v1"
|
API_BASE = "https://plan.variet.net/api/v1"
|
||||||
TOKEN = "tk_070f8e0b715e818bb7178c3815ed5389040eddca"
|
TOKEN = "tk_070f8e0b715e818bb7178c3815ed5389040eddca"
|
||||||
PROJECT_ID = 7 # Variet Agent 프로젝트
|
PROJECT_ID = 12 # guitar_score 프로젝트
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
|||||||
# ============================================================
|
# ============================================================
|
||||||
GITEA_BASE_URL = "https://git.variet.net"
|
GITEA_BASE_URL = "https://git.variet.net"
|
||||||
GITEA_OWNER = "Variet"
|
GITEA_OWNER = "Variet"
|
||||||
GITEA_REPO = "variet-agent" # Variet Agent 프로젝트
|
GITEA_REPO = "guitar_score" # guitar_score 프로젝트
|
||||||
GITEA_TOKEN = "3a01b4b15a39921572e64c413353e870d4d2161b"
|
GITEA_TOKEN = "3a01b4b15a39921572e64c413353e870d4d2161b"
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ description: 프로젝트 서비스 연동 정보 + 작업 프로토콜 (서비
|
|||||||
> 직접 API 호출 대신 반드시 helper 스크립트를 사용하세요.
|
> 직접 API 호출 대신 반드시 helper 스크립트를 사용하세요.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py list todo
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py list todo
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vikunja 라벨 체계
|
### Vikunja 라벨 체계
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ git log --oneline -5
|
|||||||
### 3. Vikunja TODO 태스크
|
### 3. Vikunja TODO 태스크
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
C:\ProgramData\miniforge3\envs\variet-agent\python.exe .agent\workflows\helpers\vikunja_helper.py list todo
|
C:\ProgramData\miniforge3\envs\score\python.exe .agent\workflows\helpers\vikunja_helper.py list todo
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 종합 보고
|
### 4. 종합 보고
|
||||||
|
|||||||
75
diag_v2.py
Normal file
75
diag_v2.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"""video_2 진단: 왜 0 프레임인지 각 단계별 확인"""
|
||||||
|
import sys
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location("p", "youtube_tab_to_pdf.py")
|
||||||
|
p = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(p)
|
||||||
|
|
||||||
|
mp4 = Path("output") / "サカナクション/新宝島(エレキギターTAB) 難易度★★★ sakanaction shintakarajima.mp4"
|
||||||
|
cap = cv2.VideoCapture(str(mp4))
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
|
||||||
|
# 10개 프레임 샘플
|
||||||
|
indices = np.linspace(total * 0.1, total * 0.8, 10, dtype=int)
|
||||||
|
for idx in indices:
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
continue
|
||||||
|
|
||||||
|
h, w = frame.shape[:2]
|
||||||
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
# 행별 밝기
|
||||||
|
margin_x = int(w * 0.1)
|
||||||
|
row_br = np.mean(gray[:, margin_x:w-margin_x], axis=1)
|
||||||
|
|
||||||
|
strip = p._find_white_tab_strip(frame)
|
||||||
|
has_tab = False
|
||||||
|
if strip:
|
||||||
|
top, bottom = strip
|
||||||
|
crop = frame[top:bottom, :]
|
||||||
|
has_tab = p._has_tab_content(crop)
|
||||||
|
|
||||||
|
print(f"Frame {idx:5d}: strip={strip}, has_tab={has_tab}, "
|
||||||
|
f"top_br={np.mean(row_br[:h//3]):.0f}, "
|
||||||
|
f"mid_br={np.mean(row_br[h//3:2*h//3]):.0f}, "
|
||||||
|
f"bot_br={np.mean(row_br[2*h//3:]):.0f}")
|
||||||
|
|
||||||
|
# strip이 있지만 has_tab=False인 경우 상세 진단
|
||||||
|
if strip and not has_tab:
|
||||||
|
top, bottom = strip
|
||||||
|
crop = frame[top:bottom, :]
|
||||||
|
g = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
|
||||||
|
ch, cw = g.shape
|
||||||
|
_, binary = cv2.threshold(g, 180, 255, cv2.THRESH_BINARY_INV)
|
||||||
|
horiz_k = cv2.getStructuringElement(cv2.MORPH_RECT, (max(cw//3, 30), 1))
|
||||||
|
horiz = cv2.morphologyEx(binary, cv2.MORPH_OPEN, horiz_k)
|
||||||
|
lines = cv2.HoughLinesP(horiz, 1, np.pi/180, threshold=40,
|
||||||
|
minLineLength=cw//4, maxLineGap=20)
|
||||||
|
nlines = 0 if lines is None else len(lines)
|
||||||
|
ys = []
|
||||||
|
if lines is not None:
|
||||||
|
for l in lines:
|
||||||
|
x1,y1,x2,y2 = l[0]
|
||||||
|
if abs(y2-y1) < max(5, abs(x2-x1)*0.05):
|
||||||
|
ys.append((y1+y2)/2)
|
||||||
|
ys.sort()
|
||||||
|
clusters = []
|
||||||
|
for y in ys:
|
||||||
|
if not clusters or y - clusters[-1] > ch * 0.02:
|
||||||
|
clusters.append(y)
|
||||||
|
print(f" → 크롭크기: {cw}x{ch}, 라인수: {nlines}, "
|
||||||
|
f"수평ys: {len(ys)}, 클러스터: {len(clusters)}")
|
||||||
|
|
||||||
|
# 디버그: 크롭 저장
|
||||||
|
cv2.imwrite(f"output/raw_dump/v2_diag_{idx}.png", crop)
|
||||||
|
|
||||||
|
cap.release()
|
||||||
7
docs/devlog/2026-03-25.md
Normal file
7
docs/devlog/2026-03-25.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Devlog — 2026-03-25
|
||||||
|
|
||||||
|
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||||
|
|---|------|-----------|------|------|
|
||||||
|
| 1 | 00:00~01:00 | HSV 트림 + pHash 클러스터 중복 제거 (v3 고도화) | `pending` | ✅ |
|
||||||
|
| 2 | 01:00~01:30 | 파노라마 스티칭: 템플릿 매칭 스크롤 오프셋 + 연속 프레임 합성 | `pending` | ✅ |
|
||||||
|
| 3 | 12:00~21:50 | 1080p 다운로드 + dHash 32×32 + OOM 방지 (MAX_FRAME_WIDTH=1280) | `pending` | 🔧 |
|
||||||
34
docs/devlog/entries/20260325-001.md
Normal file
34
docs/devlog/entries/20260325-001.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Pipeline v3→v4: Dedup + 파노라마 + 1080p 고도화
|
||||||
|
|
||||||
|
- **시간**: 2026-03-25 00:00~21:50
|
||||||
|
- **Commit**: `pending`
|
||||||
|
- **Vikunja**: 신규 태스크 생성 예정
|
||||||
|
|
||||||
|
## 작업 내용
|
||||||
|
|
||||||
|
### v3 고도화 (HSV + 트림)
|
||||||
|
- `_trim_to_content`: HSV 색공간 기반 흰색비율(30~97%) 분석으로 Tab 영역 정밀 트림
|
||||||
|
- Two-tier HSV 마스크 (pure white + bright pastel) → 노란 하이라이트/컬러 배경 정확 배제
|
||||||
|
- 검출용 업스케일 960px width로 Tab 라인 인식률 향상
|
||||||
|
|
||||||
|
### v4 중복 제거 (pHash + 파노라마)
|
||||||
|
- `_dhash` + `_dedup_by_hash`: pHash 클러스터링으로 반복 연습 구간 제거
|
||||||
|
- `_detect_scroll_offset`: 템플릿 매칭(오른쪽 60%)으로 수평 스크롤 오프셋 측정
|
||||||
|
- `_stitch_scroll_segment` + `_merge_scroll_candidates`: 연속 스크롤 프레임 파노라마 합성
|
||||||
|
- 4단계 파이프라인: MSE → 파노라마 → pHash
|
||||||
|
|
||||||
|
### 1080p 전환
|
||||||
|
- yt-dlp 포맷을 1080p 우선으로 변경 (기존 720p fallback to 360p 문제 해결)
|
||||||
|
- MAX_FRAME_WIDTH=1280 캡으로 4K OOM 방지
|
||||||
|
- dHash 16×16→32×32 (256→1024bit), max_hamming 50→20
|
||||||
|
- test_pipeline.py에 YouTube URL 추가 + --download 모드 + gc.collect()
|
||||||
|
|
||||||
|
## 결정 사항
|
||||||
|
- **MSE → 파노라마 → pHash 순서**: MSE가 완전 동일 제거, 파노라마가 겹침 합성, pHash가 반복 구간 제거. 각 단계가 다른 특성의 중복 처리
|
||||||
|
- **dHash 32×32 선택**: 16×16은 Tab의 6선 구조를 구분 못함(모두 유사 hash). 32×32는 마디번호/음표 위치까지 차별화
|
||||||
|
- **MAX_FRAME_WIDTH=1280**: 1920은 OOM 유발, 960은 360p와 큰 차이 없음. 1280은 1080p 소스에서 충분한 품질 + RAM 절약
|
||||||
|
|
||||||
|
## 미완료
|
||||||
|
- **1080p 파이프라인 전체 테스트 미완료**: 3개 영상 순차 실행 시 메모리 행으로 결과 미확인
|
||||||
|
- 사용자 피드백: "여전히 중복 있음" → 마디번호 기반 추가 검증 로직 필요
|
||||||
|
- test_pipeline.py ↔ youtube_tab_to_pdf.py 완전 통합 미정
|
||||||
33
dump_frames.py
Normal file
33
dump_frames.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""원본 프레임 덤프 — 각 영상에서 5개 프레임을 랜덤 추출"""
|
||||||
|
import sys
|
||||||
|
if sys.platform == "win32":
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
output = Path("output")
|
||||||
|
dump_dir = output / "raw_dump"
|
||||||
|
dump_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
mp4s = sorted(output.glob("*.mp4"))
|
||||||
|
for vi, mp4 in enumerate(mp4s):
|
||||||
|
cap = cv2.VideoCapture(str(mp4))
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||||
|
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
print(f"Video {vi+1}: {mp4.name[:30]}... ({w}x{h}, {fps:.0f}fps, {total} frames)")
|
||||||
|
|
||||||
|
# 균등 간격으로 5개 프레임
|
||||||
|
indices = np.linspace(total * 0.1, total * 0.9, 5, dtype=int)
|
||||||
|
for i, idx in enumerate(indices):
|
||||||
|
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if ret:
|
||||||
|
path = dump_dir / f"v{vi+1}_raw_{i}.png"
|
||||||
|
cv2.imwrite(str(path), frame)
|
||||||
|
print(f" frame {idx} → {path.name} ({frame.shape})")
|
||||||
|
cap.release()
|
||||||
|
|
||||||
|
print(f"\n덤프 완료: {dump_dir}")
|
||||||
110
test_pipeline.py
Normal file
110
test_pipeline.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""로컬 캐시된 mp4 파일로 파이프라인 테스트 (다운로드 스킵)
|
||||||
|
1080p 다운로드 모드: python test_pipeline.py --download
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import gc
|
||||||
|
|
||||||
|
# youtube_tab_to_pdf 모듈 임포트
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"pipeline", str(Path(__file__).parent / "youtube_tab_to_pdf.py"))
|
||||||
|
pipeline = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(pipeline)
|
||||||
|
|
||||||
|
# 테스트용 YouTube URLs
|
||||||
|
TEST_URLS = {
|
||||||
|
"video_1": "https://www.youtube.com/watch?v=x76IMSvWR0o", # 晴る
|
||||||
|
"video_2": "https://www.youtube.com/watch?v=90BWvJY6KbE", # 新宝島
|
||||||
|
"video_3": "https://www.youtube.com/watch?v=Ri9g4lwnrJQ", # 空奏列車
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_video(mp4_path: Path, label: str):
|
||||||
|
"""단일 영상 테스트 — 다운로드 없이 로컬 파일 직접 사용"""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"테스트: {label}")
|
||||||
|
print(f"파일: {mp4_path.name}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
output_dir = Path("output")
|
||||||
|
debug_dir = output_dir / "debug_frames" / label
|
||||||
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Step 2: 프레임 추출
|
||||||
|
frames = pipeline.extract_frames(mp4_path)
|
||||||
|
|
||||||
|
# Step 3: 패턴 감지
|
||||||
|
pattern = pipeline.detect_pattern(frames)
|
||||||
|
|
||||||
|
# Step 4: 고유 프레임 추출
|
||||||
|
if pattern == "scroll":
|
||||||
|
unique = pipeline.extract_unique_scroll(frames)
|
||||||
|
elif pattern == "split":
|
||||||
|
unique = pipeline.extract_unique_split(frames)
|
||||||
|
else:
|
||||||
|
unique = pipeline.extract_unique_overlay(frames)
|
||||||
|
|
||||||
|
# Step 5: PDF 생성
|
||||||
|
pdf_path = output_dir / f"test_{label}.pdf"
|
||||||
|
pipeline.generate_pdf(unique, pdf_path, debug_dir=debug_dir)
|
||||||
|
|
||||||
|
print(f"\n결과: {pattern} / {len(unique)}개 고유 프레임")
|
||||||
|
return pattern, len(unique)
|
||||||
|
|
||||||
|
|
||||||
|
def download_test_videos():
|
||||||
|
"""1080p로 테스트 영상 다운로드"""
|
||||||
|
output_dir = Path("output")
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
for label, url in TEST_URLS.items():
|
||||||
|
print(f"\n--- {label} 다운로드 ---")
|
||||||
|
try:
|
||||||
|
video_path, title = pipeline.download_video(url, output_dir)
|
||||||
|
print(f" → 완료: {video_path.name}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" → 실패: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--download", action="store_true",
|
||||||
|
help="1080p로 테스트 영상 다운로드")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.download:
|
||||||
|
download_test_videos()
|
||||||
|
return
|
||||||
|
|
||||||
|
output_dir = Path("output")
|
||||||
|
mp4_files = sorted(output_dir.glob("*.mp4"))
|
||||||
|
|
||||||
|
if not mp4_files:
|
||||||
|
print("output/ 폴더에 mp4 파일이 없습니다!")
|
||||||
|
print(" → python test_pipeline.py --download 로 영상 다운로드")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"캐시된 영상 {len(mp4_files)}개 발견:")
|
||||||
|
for f in mp4_files:
|
||||||
|
print(f" - {f.name} ({f.stat().st_size / 1024 / 1024:.1f} MB)")
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for i, mp4 in enumerate(mp4_files):
|
||||||
|
label = f"video_{i+1}"
|
||||||
|
pattern, count = test_video(mp4, label)
|
||||||
|
results[label] = (mp4.name, pattern, count)
|
||||||
|
gc.collect() # 1080p 프레임 메모리 해제
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("전체 결과 요약:")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
for label, (name, pattern, count) in results.items():
|
||||||
|
print(f" {label}: {pattern:8s} → {count:4d}개 프레임 | {name[:40]}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user