fix(pipeline): 스티칭 버그 3종 수정 + AI 마디번호 스탬프 제거
- [BUG1] _merge_scroll_candidates: 씬전환 가속도 조건 제거 (9→1 세그먼트) - [BUG2] merge_panoramas_list: 매칭 임계치 0.60→0.50 (파노라마 3→1 병합) - [BUG3] _detect_measure_bars: 마디선 최소간격 100px 필터 추가 (17px 오탐 제거) - remove: _stamp_measure_number 호출 제거 (AI 임의 [1][2][3] 스탬프 삭제) - add: sim_stitch.py, simulate_ocr_pipeline.py, verify_fixes.py (진단/검증 스크립트)
This commit is contained in:
@@ -27,18 +27,17 @@ Raw Frames → HSV Strip 검출 → Median Crop → MSE 1차 → 파노라마
|
||||
|
||||
| 날짜 | 변경 내용 |
|
||||
|------|-----------|
|
||||
| 2026-03-27 | **[BUG1]** `_merge_scroll_candidates` 씬전환 가속도 조건 제거 → 씬전환 오탐 9→1 |
|
||||
| 2026-03-27 | **[BUG2]** `merge_panoramas_list` 매칭 임계치 0.60→0.50 → 파노라마 분리 3→1 |
|
||||
| 2026-03-27 | **[BUG3]** `_detect_measure_bars` 마디선 최소간격 필터 100px 추가 → 오탐(17px) 제거 |
|
||||
| 2026-03-25 | 마디번호 기반 중복 검색 기능(OCR) 파이프라인 적용 |
|
||||
| 2026-03-25 | 1080p 에러 방지용 720p 폴백(다운스케일링 부하 원천 차단) 도입 |
|
||||
| 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 트림: 연속된 흰색 행 영역 찾기 (검은색 끊김 허용) 개선 |
|
||||
| 2026-03-25 | overlay 프레임 수 최적화: 858→51프레임 (OVERLAY_SIMILARITY_THRESHOLD=0.55) |
|
||||
| 2026-03-24 | 패턴 감지 고도화: overlay→split→scroll 우선순위 |
|
||||
| 2026-03-24 | 히스토그램 비교 → MSE 픽셀 비교로 전환 |
|
||||
|
||||
## 알려진 제한사항
|
||||
|
||||
- 1080p 처리 시 여전히 중복 프레임 존재 가능 (마디번호 기반 추가 검증 필요)
|
||||
- 프레임 하단 기타리스트 영상이 탭 행 아래에 소량 노출됨 (`_trim_to_content` 개선 필요)
|
||||
- 순차 영상 처리 시 메모리 누적 주의 (gc.collect 필수)
|
||||
- test_pipeline.py 아직 메인 코드와 완전 통합 안 됨
|
||||
|
||||
@@ -85,8 +85,27 @@
|
||||
- **해결**: 스크롤 방식에서는 오직 물리적 마디 구분선(|)을 기준으로 분절 후 재배치하는 타일 엔진 도입
|
||||
- **주의**: Scroll 파노라마 같은 선형 시계열 구조에서는 지엽적 Hash 기반 중복제거를 적용하면 안 됨
|
||||
|
||||
### [2026-03-27] Crop 마진 — 마디 번호 소실 (Decapitation)
|
||||
### [2026-03-28] Crop 마진 — 마디 번호 소실 (Decapitation)
|
||||
- **증상**: 입력 영상 최상단에 존재하는 숫자(마디 번호)가 출력물에서 무차별적으로 잘려나감
|
||||
- **원인**: 밀도 필터(row_dark > 0.02)에서 숫자 픽셀이 차지하는 비율이 가로폭 대비 0.4%에 불과하여 여백으로 간주됨
|
||||
- **해결**: 필터링 임계점을 0.002로 대폭 완화 및 Top Clearance Margin을 120px로 확장
|
||||
- **주의**: 심볼 밀도가 극히 낮은 구역 통과 시 % 기반의 밀도 크롭 알고리즘은 치명적으로 작용함
|
||||
|
||||
### [2026-03-28] 씬전환 오탐 — 스크롤 가속도 조건
|
||||
- **증상**: 스크롤 속도가 급격히 변하는 구간(예: 빠른 이동)에서 씬전환으로 오탐 → 세그먼트 9개로 과다 분리
|
||||
- **원인**: `_merge_scroll_candidates`의 `abs(s_px - prev_s_px) > 100` 조건이 스크롤 가속을 씬전환으로 오인
|
||||
- **해결**: 해당 조건 제거. confidence 기반 조건(`conf <= 0.15`, `prev_conf - conf > 0.4`)만 유지
|
||||
- **주의**: 스크롤 속도는 영상마다 다르므로 절댓값 기반 조건은 신뢰할 수 없음
|
||||
|
||||
### [2026-03-28] 파노라마 병합 임계치 — 반복 구간 분리
|
||||
- **증상**: 코러스처럼 유사한 구간에서 `merge_panoramas_list` 매칭 스코어가 0.56~0.59에 머물러 파노라마가 3개로 분리됨
|
||||
- **원인**: `max_val > 0.60` 임계치가 반복성 악보의 픽셀 유사도를 커버하지 못함
|
||||
- **해결**: 임계치 0.60 → 0.50으로 완화
|
||||
- **주의**: 임계치를 낮추면 오매핑 위험이 있으므로 0.50 이하로는 내리지 말 것
|
||||
|
||||
### [2026-03-28] OCR-First 방식 — 스크롤 영상 적용 불가
|
||||
- **증상**: 각 프레임에서 마디번호 OCR 시도 시 인식률 4.8% (126프레임 중 6개만 인식)
|
||||
- **원인**: 스크롤 영상에서 마디번호는 화면 왼쪽 고정이 아닌 악보와 함께 이동 → 프레임마다 위치 다름. 오인식 결과(7→10→71→101→710)도 다수
|
||||
- **해결**: OCR-First 방식 폐기. 파노라마 스티칭 후 물리적 마디구분선(|) 탐지 방식 유지
|
||||
- **주의**: 스크롤 영상에서 마디번호 기반 중복제거는 파노라마를 완성한 뒤 적용해야 의미가 있음
|
||||
|
||||
|
||||
6
docs/devlog/2026-03-28.md
Normal file
6
docs/devlog/2026-03-28.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Devlog — 2026-03-28
|
||||
|
||||
| # | 시간 | 작업 설명 | 커밋 | 상태 |
|
||||
|---|------|-----------|------|------|
|
||||
| 1 | 23:10~23:45 | 스티칭 버그 3종 수정: 씬전환 오탐(9→1) + 파노라마 병합 임계치(0.60→0.50) + 마디선 최소간격(17→136px) | `` | ✅ |
|
||||
| 2 | 23:45~00:00 | AI 임의 마디번호 스탬프([1][2][3]...) 제거 — 원본 악보 번호 그대로 출력 | `` | ✅ |
|
||||
296
sim_stitch.py
Normal file
296
sim_stitch.py
Normal file
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
파노라마 스티칭 정밀 진단 스크립트
|
||||
-----------------------------------
|
||||
실제 캐시 프레임(temp_frames/f_XXXX.png)을 이용해
|
||||
파노라마 스티칭의 각 단계를 정밀 추적합니다.
|
||||
|
||||
목적:
|
||||
1. 스크롤 오프셋이 제대로 감지되는가?
|
||||
2. `_merge_scroll_candidates` 씬 전환 감지가 정확한가?
|
||||
3. `merge_panoramas_list` 템플릿 매칭이 반복 구간을 제대로 이어붙이는가?
|
||||
4. 최종 파노라마에 실제로 누락된 마디가 있는가?
|
||||
|
||||
실행:
|
||||
C:\\ProgramData\\miniforge3\\envs\\score\\python.exe sim_stitch.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
FRAME_DIR = Path("output/temp_frames")
|
||||
OUT_DIR = Path("output/sim_stitch")
|
||||
OUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# ─── 기존 코드와 동일한 함수들 ────────────────────────────────────────────
|
||||
|
||||
def _find_white_tab_strip(frame, min_strip_ratio=0.10):
|
||||
h, w = frame.shape[:2]
|
||||
margin_x = int(w * 0.1)
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||
_, s_ch, v_ch = cv2.split(hsv)
|
||||
roi_v = v_ch[:, margin_x:w - margin_x]
|
||||
roi_s = s_ch[:, margin_x:w - margin_x]
|
||||
pure_white = (roi_v > 180) & (roi_s < 40)
|
||||
bright_pastel = (roi_v > 200) & (roi_s < 100)
|
||||
tab_mask = pure_white | bright_pastel
|
||||
row_tab_ratio = np.mean(tab_mask, axis=1)
|
||||
bright_mask = row_tab_ratio > 0.5
|
||||
max_gap = int(h * 0.02)
|
||||
regions, start, gap_count = [], None, 0
|
||||
for i in range(h):
|
||||
if bright_mask[i]:
|
||||
if start is None: start = i
|
||||
gap_count = 0
|
||||
else:
|
||||
if start is not None:
|
||||
gap_count += 1
|
||||
if gap_count > max_gap:
|
||||
length = (i - gap_count) - start
|
||||
if length >= h * min_strip_ratio:
|
||||
regions.append((start, i - gap_count))
|
||||
start = None
|
||||
if start is not None:
|
||||
length = (h - gap_count) - start
|
||||
if length >= h * min_strip_ratio:
|
||||
regions.append((start, h - gap_count))
|
||||
if not regions: return None
|
||||
best = max(regions, key=lambda r: r[1] - r[0])
|
||||
pad = int(h * 0.03)
|
||||
return (max(0, best[0] - pad), min(h, best[1] + pad))
|
||||
|
||||
def _has_tab_content(region):
|
||||
if region is None or region.size == 0: return False
|
||||
gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) if len(region.shape) == 3 else region
|
||||
h, w = gray.shape
|
||||
if h < 15 or w < 50: return False
|
||||
dark_ratio = np.sum(gray < 180) / gray.size
|
||||
return 0.02 < dark_ratio < 0.30
|
||||
|
||||
def compare_frames(f1, f2):
|
||||
g1 = cv2.cvtColor(f1, cv2.COLOR_BGR2GRAY) if len(f1.shape)==3 else f1
|
||||
g2 = cv2.cvtColor(f2, cv2.COLOR_BGR2GRAY) if len(f2.shape)==3 else f2
|
||||
if g1.shape != g2.shape:
|
||||
g2 = cv2.resize(g2, (g1.shape[1], g1.shape[0]))
|
||||
target_w = 480
|
||||
if g1.shape[1] > target_w:
|
||||
scale = target_w / g1.shape[1]
|
||||
sz = (target_w, int(g1.shape[0] * scale))
|
||||
g1 = cv2.resize(g1, sz)
|
||||
g2 = cv2.resize(g2, sz)
|
||||
mse = np.mean(((g1.astype(np.float32) - g2.astype(np.float32)) / 255.0)**2)
|
||||
return max(0.0, 1.0 - min(mse * 8.0, 1.0))
|
||||
|
||||
def _extract_tracking_channel(frame):
|
||||
if len(frame.shape) != 3: return frame
|
||||
return frame[:, :, 0] # Blue channel
|
||||
|
||||
def _detect_scroll_offset(frame_a, frame_b, min_confidence=0.1):
|
||||
h, w = frame_a.shape[:2]
|
||||
ga = _extract_tracking_channel(frame_a)
|
||||
gb = _extract_tracking_channel(frame_b)
|
||||
tmpl_w = int(w * 0.5)
|
||||
template = ga[:, w - tmpl_w:]
|
||||
result = cv2.matchTemplate(gb, template, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(result)
|
||||
scroll_px = (w - tmpl_w) - max_loc[0]
|
||||
if max_val < min_confidence or scroll_px <= 0:
|
||||
return (0, max_val)
|
||||
return (scroll_px, max_val)
|
||||
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
paths = sorted(FRAME_DIR.glob("f_0*.png"))
|
||||
if not paths:
|
||||
print("❌ 프레임 없음:", FRAME_DIR); return
|
||||
|
||||
print(f"[STITCH-SIM] {len(paths)}개 프레임")
|
||||
|
||||
# 스트립 Y범위 계산
|
||||
strip_tops, strip_bottoms = [], []
|
||||
for p in paths[:30]:
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
s = _find_white_tab_strip(f)
|
||||
if s: strip_tops.append(s[0]); strip_bottoms.append(s[1])
|
||||
med_top = int(np.median(strip_tops))
|
||||
med_bottom = int(np.median(strip_bottoms))
|
||||
print(f" 스트립 Y: {med_top} ~ {med_bottom}")
|
||||
|
||||
# 탭 크롭 추출 + MSE 중복 제거 (기존 로직)
|
||||
SIMILARITY_THRESHOLD = 0.95
|
||||
candidates, all_compared = [], []
|
||||
for p in paths:
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
h = f.shape[0]
|
||||
crop = f[max(0, med_top):min(h, med_bottom), :]
|
||||
if not _has_tab_content(crop): continue
|
||||
compare_img = cv2.resize(crop, (480, 120), interpolation=cv2.INTER_AREA)
|
||||
if any(compare_frames(compare_img, ref) >= SIMILARITY_THRESHOLD for ref in all_compared):
|
||||
continue
|
||||
candidates.append(crop)
|
||||
all_compared.append(compare_img)
|
||||
|
||||
print(f"\n[1단계] MSE 중복제거 후 후보: {len(candidates)}개 프레임")
|
||||
|
||||
# 스크롤 오프셋 분석 — 연속 프레임 간 이동량 측정
|
||||
print(f"\n[2단계] 연속 프레임 스크롤 오프셋 분석:")
|
||||
print(f" {'idx':>4} {'scroll_px':>10} {'conf':>6} {'씬전환':>8}")
|
||||
print(f" {'-'*40}")
|
||||
|
||||
scroll_data = []
|
||||
prev_s = 0
|
||||
prev_conf = 1.0
|
||||
for i in range(1, len(candidates)):
|
||||
s, conf = _detect_scroll_offset(candidates[i-1], candidates[i])
|
||||
is_cut = (conf <= 0.15) or (abs(s - prev_s) > 100) or (prev_conf - conf > 0.4)
|
||||
scroll_data.append((i, s, conf, is_cut))
|
||||
mark = "✂ CUT" if is_cut else ""
|
||||
print(f" {i:>4} {s:>10}px {conf:>6.3f} {mark}")
|
||||
prev_s = s
|
||||
prev_conf = conf
|
||||
|
||||
n_cuts = sum(1 for _, _, _, cut in scroll_data if cut)
|
||||
print(f"\n → 씬 전환 감지 횟수: {n_cuts}개 (예상: 1~3개)")
|
||||
print(f" → 분절 세그먼트: {n_cuts+1}개")
|
||||
|
||||
# 세그먼트별 파노라마 스티칭
|
||||
print(f"\n[3단계] 세그먼트 파노라마 스티칭:")
|
||||
segments = []
|
||||
current_seg = [candidates[0]]
|
||||
for i, (idx, s, conf, is_cut) in enumerate(scroll_data):
|
||||
if is_cut:
|
||||
segments.append(current_seg)
|
||||
current_seg = [candidates[idx]]
|
||||
else:
|
||||
current_seg.append(candidates[idx])
|
||||
segments.append(current_seg)
|
||||
|
||||
panos = []
|
||||
for seg_i, seg in enumerate(segments):
|
||||
if len(seg) == 1:
|
||||
panos.append(seg[0])
|
||||
print(f" 세그먼트 {seg_i}: 1프레임 → 스티칭 불필요 ({seg[0].shape[1]}px)")
|
||||
continue
|
||||
|
||||
min_h = min(f.shape[0] for f in seg)
|
||||
panorama = seg[0][:min_h, :]
|
||||
for i in range(1, len(seg)):
|
||||
curr = seg[i][:min_h, :]
|
||||
scroll_px, conf = _detect_scroll_offset(seg[i-1][:min_h, :], curr)
|
||||
if scroll_px > 0 and conf > 0.15:
|
||||
new_strip = curr[:, curr.shape[1] - scroll_px:]
|
||||
panorama = np.hstack([panorama, new_strip])
|
||||
else:
|
||||
panorama = np.hstack([panorama, curr])
|
||||
|
||||
panos.append(panorama)
|
||||
print(f" 세그먼트 {seg_i}: {len(seg)}프레임 → 파노라마 {panorama.shape[1]}px")
|
||||
cv2.imwrite(str(OUT_DIR / f"raw_pano_{seg_i:02d}.png"), panorama)
|
||||
|
||||
# merge_panoramas_list 단계 진단
|
||||
print(f"\n[4단계] 파노라마 병합 (merge_panoramas_list):")
|
||||
print(f" 병합 전: {len(panos)}개 파노라마")
|
||||
|
||||
if len(panos) > 1:
|
||||
merged_list = []
|
||||
current_master = panos[0].copy()
|
||||
for i in range(1, len(panos)):
|
||||
next_pano = panos[i].copy()
|
||||
head_w = min(800, next_pano.shape[1])
|
||||
head = next_pano[:, :head_w]
|
||||
search_w = min(1500, current_master.shape[1])
|
||||
search_region = current_master[:, -search_w:]
|
||||
h_gray = _extract_tracking_channel(head)
|
||||
s_gray = _extract_tracking_channel(search_region)
|
||||
matched = False
|
||||
if h_gray.shape[1] <= s_gray.shape[1] and h_gray.shape[0] == s_gray.shape[0]:
|
||||
res = cv2.matchTemplate(s_gray, h_gray, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
||||
print(f" 파노라마 {i}: 템플릿 매칭 max_val={max_val:.3f}", end="")
|
||||
if max_val > 0.60:
|
||||
match_x = max_loc[0]
|
||||
abs_x = current_master.shape[1] - search_w + match_x
|
||||
skip = current_master.shape[1] - abs_x
|
||||
append_part = next_pano[:, skip:]
|
||||
if append_part.shape[1] > 0:
|
||||
current_master = np.hstack([current_master, append_part])
|
||||
matched = True
|
||||
print(f" → ✅ 매칭 성공 (이어붙임, skip={skip}px)")
|
||||
else:
|
||||
print(f" → ❌ 매칭 실패 (score 낮음, 새 파노라마로 분리)")
|
||||
else:
|
||||
print(f" 파노라마 {i}: 크기 불일치로 매칭 불가")
|
||||
|
||||
if not matched:
|
||||
merged_list.append(current_master)
|
||||
current_master = next_pano
|
||||
merged_list.append(current_master)
|
||||
else:
|
||||
merged_list = panos
|
||||
|
||||
print(f"\n 병합 후: {len(merged_list)}개 파노라마")
|
||||
for i, m in enumerate(merged_list):
|
||||
print(f" 최종 파노라마 {i}: {m.shape[1]}x{m.shape[0]}px")
|
||||
cv2.imwrite(str(OUT_DIR / f"final_pano_{i:02d}.png"), m)
|
||||
|
||||
# 마디 구분선 탐지 결과 진단
|
||||
print(f"\n[5단계] 마디 구분선(|) 탐지:")
|
||||
def _detect_measure_bars(gray_pano):
|
||||
_, thresh = cv2.threshold(gray_pano, 200, 255, cv2.THRESH_BINARY_INV)
|
||||
h, w = thresh.shape
|
||||
row_sums = np.sum(thresh, axis=1) / 255
|
||||
staff_rows = np.where(row_sums > w * 0.5)[0]
|
||||
if len(staff_rows) < 2: return []
|
||||
top_line = staff_rows[0]
|
||||
bottom_line = top_line
|
||||
for r in staff_rows:
|
||||
if r - top_line > 100: break
|
||||
bottom_line = r
|
||||
staff_region = thresh[top_line:bottom_line+1, :]
|
||||
expected_h = bottom_line - top_line + 1
|
||||
if expected_h < 10: return []
|
||||
col_sums = np.sum(staff_region, axis=0) / 255
|
||||
bar_cols = np.where(col_sums >= expected_h * 0.8)[0]
|
||||
measures = []
|
||||
curr = []
|
||||
for c in bar_cols:
|
||||
if not curr: curr.append(c)
|
||||
else:
|
||||
if c - curr[-1] < 10: curr.append(c)
|
||||
else:
|
||||
measures.append(int(np.mean(curr)))
|
||||
curr = [c]
|
||||
if curr: measures.append(int(np.mean(curr)))
|
||||
return measures
|
||||
|
||||
for i, m in enumerate(merged_list):
|
||||
# Red 채널 (출력용)
|
||||
gray = m[:, :, 2]
|
||||
bars = _detect_measure_bars(gray)
|
||||
print(f" 파노라마 {i} ({m.shape[1]}px): {len(bars)}개 마디 구분선 탐지")
|
||||
if bars:
|
||||
intervals = [bars[j+1]-bars[j] for j in range(len(bars)-1)]
|
||||
if intervals:
|
||||
print(f" 마디 간격: min={min(intervals)}, max={max(intervals)}, mean={np.mean(intervals):.0f}px")
|
||||
print(f" 처음 5개 좌표: {bars[:5]}")
|
||||
|
||||
print(f"\n[STITCH-SIM 완료]")
|
||||
print(f" 결과 저장: {OUT_DIR}")
|
||||
print(f" 핵심 체크포인트:")
|
||||
print(f" - 씬 전환 {n_cuts}회 → {n_cuts+1}개 세그먼트 분리")
|
||||
print(f" - 최종 병합 파노라마: {len(merged_list)}개")
|
||||
total_bars = sum(len(_detect_measure_bars(m[:,:,2])) for m in merged_list)
|
||||
print(f" - 총 탐지 마디 구분선: {total_bars}개")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
315
simulate_ocr_pipeline.py
Normal file
315
simulate_ocr_pipeline.py
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OCR-First 파이프라인 시뮬레이션 스크립트
|
||||
--------------------------------------------
|
||||
실제 캐시된 프레임 이미지(temp_frames/f_XXXX.png)를 읽어서
|
||||
새 파이프라인의 각 단계를 시뮬레이션하고 결과를 검증합니다.
|
||||
|
||||
단계:
|
||||
[A] HSV Tab Strip 추출 (기존 로직 재사용)
|
||||
[B] 각 프레임에서 마디번호 OCR
|
||||
[C] 마디번호 기반 그룹핑 + 최고선명도 프레임 선택
|
||||
[D] 마디번호 순서 정렬
|
||||
[E] 파노라마 이어붙이기 (단순 hstack)
|
||||
[F] 결과 리포트 출력
|
||||
|
||||
실행:
|
||||
C:\\ProgramData\\miniforge3\\envs\\score\\python.exe simulate_ocr_pipeline.py
|
||||
"""
|
||||
|
||||
import sys, os
|
||||
from pathlib import Path
|
||||
import cv2
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
FRAME_DIR = Path("output/temp_frames")
|
||||
OUT_DIR = Path("output/sim_result")
|
||||
OUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# ─── [A] HSV Tab Strip 검출 (기존 코드와 동일) ─────────────────────────────
|
||||
|
||||
def _find_white_tab_strip(frame, min_strip_ratio=0.10):
|
||||
h, w = frame.shape[:2]
|
||||
margin_x = int(w * 0.1)
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||
_, s_ch, v_ch = cv2.split(hsv)
|
||||
roi_v = v_ch[:, margin_x:w - margin_x]
|
||||
roi_s = s_ch[:, margin_x:w - margin_x]
|
||||
pure_white = (roi_v > 180) & (roi_s < 40)
|
||||
bright_pastel= (roi_v > 200) & (roi_s < 100)
|
||||
tab_mask = pure_white | bright_pastel
|
||||
row_tab_ratio = np.mean(tab_mask, axis=1)
|
||||
bright_mask = row_tab_ratio > 0.5
|
||||
max_gap = int(h * 0.02)
|
||||
regions, start, gap_count = [], None, 0
|
||||
for i in range(h):
|
||||
if bright_mask[i]:
|
||||
if start is None: start = i
|
||||
gap_count = 0
|
||||
else:
|
||||
if start is not None:
|
||||
gap_count += 1
|
||||
if gap_count > max_gap:
|
||||
length = (i - gap_count) - start
|
||||
if length >= h * min_strip_ratio:
|
||||
regions.append((start, i - gap_count))
|
||||
start = None
|
||||
if start is not None:
|
||||
length = (h - gap_count) - start
|
||||
if length >= h * min_strip_ratio:
|
||||
regions.append((start, h - gap_count))
|
||||
if not regions: return None
|
||||
best = max(regions, key=lambda r: r[1] - r[0])
|
||||
pad = int(h * 0.03)
|
||||
return (max(0, best[0] - pad), min(h, best[1] + pad))
|
||||
|
||||
def _trim_tab_crop(crop, margin_px=6):
|
||||
"""Tab 크롭에서 상단/하단 여백 + 하단 기타리스트 영상 제거"""
|
||||
h, w = crop.shape[:2]
|
||||
if h < 15 or w < 50: return crop
|
||||
hsv = cv2.cvtColor(crop, cv2.COLOR_BGR2HSV)
|
||||
_, s_ch, v_ch = cv2.split(hsv)
|
||||
white_mask = ((v_ch > 180) & (s_ch < 40)) | ((v_ch > 200) & (s_ch < 100))
|
||||
row_white = np.mean(white_mask, axis=1)
|
||||
tab_rows = (row_white > 0.30) & (row_white < 0.97)
|
||||
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
|
||||
row_dark = np.mean(gray < 180, axis=1)
|
||||
content_rows = row_dark > 0.002
|
||||
valid_rows = tab_rows | content_rows
|
||||
|
||||
# 상단: 첫 유효 행 기준 -120px
|
||||
top = 0
|
||||
for i in range(h):
|
||||
if valid_rows[i] and row_white[i] > 0.20:
|
||||
top = max(0, i - 120)
|
||||
break
|
||||
|
||||
# 하단: 마지막 유효 행 (흰색비율 > 0.20 조건 유지, 여유 +margin_px)
|
||||
bottom = h
|
||||
for i in range(h - 1, -1, -1):
|
||||
if valid_rows[i] and row_white[i] > 0.20:
|
||||
bottom = min(h, i + margin_px)
|
||||
break
|
||||
|
||||
if bottom - top < 15: return crop
|
||||
return crop[top:bottom, :]
|
||||
|
||||
|
||||
# ─── [B] OCR 마디번호 읽기 ─────────────────────────────────────────────────
|
||||
|
||||
_ocr_reader = None
|
||||
|
||||
def _get_ocr():
|
||||
global _ocr_reader
|
||||
if _ocr_reader is None:
|
||||
import easyocr
|
||||
print(" → EasyOCR 로딩 중...")
|
||||
_ocr_reader = easyocr.Reader(['en'])
|
||||
return _ocr_reader
|
||||
|
||||
def _ocr_measure_number(crop):
|
||||
"""Tab 크롭 이미지에서 마디번호(상단 숫자)를 읽어 int 또는 None 반환"""
|
||||
if crop is None or crop.size == 0: return None
|
||||
h, w = crop.shape[:2]
|
||||
gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) if len(crop.shape) == 3 else crop
|
||||
_, thresh = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV)
|
||||
row_sums = np.sum(thresh, axis=1) / 255
|
||||
staff_rows = np.where(row_sums > w * 0.5)[0]
|
||||
|
||||
if len(staff_rows) > 0:
|
||||
first_line_y = staff_rows[0]
|
||||
y0 = max(0, first_line_y - 50)
|
||||
y1 = max(10, first_line_y - 2)
|
||||
# 좌측 10% 영역 (마디번호가 각 행 맨 왼쪽에 인쇄됨)
|
||||
roi = gray[y0:y1, :int(w * 0.10)]
|
||||
else:
|
||||
roi = gray[:int(h * 0.25), :int(w * 0.10)]
|
||||
|
||||
if roi.size == 0: return None
|
||||
|
||||
# 3x 업스케일 + 이진화 → OCR 인식률 향상
|
||||
up = cv2.resize(roi, (0, 0), fx=3.0, fy=3.0, interpolation=cv2.INTER_CUBIC)
|
||||
_, up_bin = cv2.threshold(up, 150, 255, cv2.THRESH_BINARY_INV)
|
||||
|
||||
reader = _get_ocr()
|
||||
results = reader.readtext(up_bin, allowlist='0123456789')
|
||||
for (_, text, conf) in results:
|
||||
if conf > 0.3 and text.isdigit() and 1 <= len(text) <= 3:
|
||||
return int(text)
|
||||
return None
|
||||
|
||||
|
||||
# ─── [C] 선명도 기준 최고 프레임 선택 ────────────────────────────────────
|
||||
|
||||
def _pick_best(frames):
|
||||
if len(frames) == 1: return frames[0]
|
||||
def sharpness(f):
|
||||
g = cv2.cvtColor(f, cv2.COLOR_BGR2GRAY) if len(f.shape)==3 else f
|
||||
return cv2.Laplacian(g, cv2.CV_64F).var()
|
||||
return max(frames, key=sharpness)
|
||||
|
||||
|
||||
# ─── Main 시뮬레이션 ────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
# 프레임 로드 (f_0025 ~ f_0149 순서 정렬)
|
||||
paths = sorted(FRAME_DIR.glob("f_0*.png"))
|
||||
if not paths:
|
||||
print("❌ 프레임 파일 없음:", FRAME_DIR)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"[SIM] {len(paths)}개 프레임 로드 — {FRAME_DIR}")
|
||||
|
||||
# ── 스트립 위치 중앙값 계산 ─────────────────────────────────────────
|
||||
strip_tops, strip_bottoms = [], []
|
||||
for p in paths[:30]: # 첫 30장으로 샘플링
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
s = _find_white_tab_strip(f)
|
||||
if s: strip_tops.append(s[0]); strip_bottoms.append(s[1])
|
||||
|
||||
if not strip_tops:
|
||||
print("❌ Tab 스트립 감지 실패")
|
||||
sys.exit(1)
|
||||
|
||||
med_top = int(np.median(strip_tops))
|
||||
med_bottom = int(np.median(strip_bottoms))
|
||||
print(f"[SIM-A] 스트립 Y범위 중앙값: {med_top} ~ {med_bottom}px")
|
||||
|
||||
# ── [B+C] 각 프레임 OCR + 그룹핑 ───────────────────────────────────
|
||||
measure_groups = defaultdict(list) # {measure_num: [crop, ...]}
|
||||
no_ocr_frames = [] # OCR 실패 프레임
|
||||
ocr_log = []
|
||||
|
||||
total = len(paths)
|
||||
for idx, p in enumerate(paths):
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
h = f.shape[0]
|
||||
crop = f[max(0, med_top):min(h, med_bottom), :]
|
||||
crop = _trim_tab_crop(crop)
|
||||
|
||||
num = _ocr_measure_number(crop)
|
||||
status = f"[{num:3d}]" if num is not None else "[ ? ]"
|
||||
print(f" 프레임 {idx+1:3d}/{total} ({p.name}) → {status}")
|
||||
ocr_log.append((p.name, num))
|
||||
|
||||
if num is not None:
|
||||
measure_groups[num].append(crop)
|
||||
else:
|
||||
no_ocr_frames.append((idx, crop))
|
||||
|
||||
print(f"\n[SIM-B] OCR 결과:")
|
||||
print(f" 마디번호 감지 성공: {len(measure_groups)}개 고유 마디")
|
||||
print(f" OCR 실패 프레임: {len(no_ocr_frames)}개")
|
||||
|
||||
if not measure_groups:
|
||||
print("❌ 마디번호 하나도 감지 못함 → OCR 방식 불가")
|
||||
sys.exit(1)
|
||||
|
||||
# 마디번호 분포 출력
|
||||
nums = sorted(measure_groups.keys())
|
||||
print(f" 감지된 마디번호 범위: {nums[0]} ~ {nums[-1]}")
|
||||
print(f" 감지된 마디번호: {nums}")
|
||||
|
||||
# 연속성 체크: 빠진 마디번호
|
||||
expected = set(range(nums[0], nums[-1]+1))
|
||||
missing = expected - set(nums)
|
||||
if missing:
|
||||
print(f" ⚠ 누락된 마디번호: {sorted(missing)} ({len(missing)}개) — OCR 실패로 인한 것")
|
||||
else:
|
||||
print(f" ✅ 마디번호 연속성 완전 (누락 없음)")
|
||||
|
||||
# 중복 프레임 수
|
||||
total_dup = sum(len(v)-1 for v in measure_groups.values() if len(v) > 1)
|
||||
print(f" 제거될 중복 프레임 수: {total_dup}개")
|
||||
|
||||
# ── [C] 최고선명도 프레임 선택 ───────────────────────────────────────
|
||||
print(f"\n[SIM-C] 각 마디별 최고선명도 프레임 선택...")
|
||||
best_frames = {}
|
||||
for num in sorted(measure_groups.keys()):
|
||||
best_frames[num] = _pick_best(measure_groups[num])
|
||||
|
||||
# ── [D] 정렬 + [E] 파노라마 조립 시뮬레이션 ─────────────────────────
|
||||
print(f"\n[SIM-D/E] 마디번호 순 정렬 + 파노라마 조립...")
|
||||
|
||||
sorted_measures = sorted(best_frames.keys())
|
||||
|
||||
# 결과 이미지 저장: 마디별 크롭 저장 (검증용)
|
||||
frame_widths = []
|
||||
frame_heights = []
|
||||
for i, num in enumerate(sorted_measures[:10]): # 처음 10개만 저장
|
||||
img = best_frames[num]
|
||||
save_path = OUT_DIR / f"sim_measure_{num:03d}.png"
|
||||
cv2.imwrite(str(save_path), img)
|
||||
frame_widths.append(img.shape[1])
|
||||
frame_heights.append(img.shape[0])
|
||||
|
||||
# 전체 파노라마 너비 계산
|
||||
chunk_w = sorted(frame_widths)[len(frame_widths)//2] if frame_widths else 1280 # 중앙값
|
||||
|
||||
# 파노라마 조립: 연속된 마디 이어붙이기
|
||||
all_frames_sorted = [best_frames[n] for n in sorted_measures]
|
||||
target_h = int(np.median([f.shape[0] for f in all_frames_sorted]))
|
||||
|
||||
print(f" 기준 높이: {target_h}px, 기준 폭: {chunk_w}px")
|
||||
|
||||
# 행 단위 조립 (chunk_width 초과 시 새 행)
|
||||
rows = []
|
||||
current_row_imgs = []
|
||||
current_w = 0
|
||||
for img in all_frames_sorted:
|
||||
# 높이 정규화
|
||||
if img.shape[0] != target_h:
|
||||
img = cv2.resize(img, (int(img.shape[1] * target_h / img.shape[0]), target_h))
|
||||
if current_w + img.shape[1] > chunk_w and current_row_imgs:
|
||||
row = np.hstack(current_row_imgs)
|
||||
rows.append(row)
|
||||
current_row_imgs = [img]
|
||||
current_w = img.shape[1]
|
||||
else:
|
||||
current_row_imgs.append(img)
|
||||
current_w += img.shape[1]
|
||||
if current_row_imgs:
|
||||
rows.append(np.hstack(current_row_imgs))
|
||||
|
||||
print(f" → 총 {len(rows)}개 행 생성 (각 행 폭 ≤ {chunk_w}px)")
|
||||
|
||||
# 결과 이미지 저장
|
||||
for i, row in enumerate(rows):
|
||||
save_path = OUT_DIR / f"sim_row_{i:03d}.png"
|
||||
cv2.imwrite(str(save_path), row)
|
||||
|
||||
# ── 최종 리포트 ─────────────────────────────────────────────────────
|
||||
print(f"\n{'='*60}")
|
||||
print("[SIM 결과 리포트]")
|
||||
print(f" 입력 프레임: {len(paths)}개")
|
||||
print(f" OCR 성공: {len(sorted_measures)}개 고유 마디")
|
||||
print(f" OCR 실패: {len(no_ocr_frames)}개 ({len(no_ocr_frames)/len(paths)*100:.1f}%)")
|
||||
print(f" 제거된 중복 프레임: {total_dup}개")
|
||||
print(f" 누락 마디번호: {sorted(missing) if missing else '없음'}")
|
||||
print(f" 출력 행 수: {len(rows)}개")
|
||||
print(f" 결과 저장: {OUT_DIR}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# ── 핵심 판정 ────────────────────────────────────────────────────────
|
||||
ocr_rate = len(sorted_measures) / len(paths) * 100
|
||||
print(f"\n[판정]")
|
||||
if ocr_rate >= 40:
|
||||
print(f" ✅ OCR-First 방식 유효 (인식률 {ocr_rate:.1f}%)")
|
||||
print(f" → 누락 마디는 인접 마디 프레임으로 보완 가능")
|
||||
elif ocr_rate >= 20:
|
||||
print(f" ⚠ OCR 인식률 낮음 ({ocr_rate:.1f}%) → 파라미터 조정 필요")
|
||||
print(f" → 업스케일 배율 증가, 신뢰도 임계치 하향 검토")
|
||||
else:
|
||||
print(f" ❌ OCR 인식률 너무 낮음 ({ocr_rate:.1f}%) → 이 영상에서 OCR 방식 불가")
|
||||
print(f" → 마디번호가 없는 영상이거나, 크롭 영역 조정 필요")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
116
verify_fixes.py
Normal file
116
verify_fixes.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
수정된 버그 3개가 실제로 동작하는지 검증하는 재실행 시뮬레이션.
|
||||
youtube_tab_to_pdf.py의 수정된 함수들을 직접 임포트하여 사용합니다.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
# 메인 모듈 임포트 (수정된 코드 사용)
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from youtube_tab_to_pdf import (
|
||||
_find_white_tab_strip, _has_tab_content,
|
||||
_detect_scroll_offset, _extract_tracking_channel,
|
||||
_merge_scroll_candidates, merge_panoramas_list,
|
||||
_detect_measure_bars, compare_frames
|
||||
)
|
||||
|
||||
FRAME_DIR = Path("output/temp_frames")
|
||||
OUT_DIR = Path("output/sim_verify")
|
||||
OUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
def main():
|
||||
paths = sorted(FRAME_DIR.glob("f_0*.png"))
|
||||
if not paths:
|
||||
print("❌ 프레임 없음"); return
|
||||
|
||||
print(f"[VERIFY] {len(paths)}개 프레임 — 수정된 코드로 재검증")
|
||||
|
||||
# 스트립 Y범위
|
||||
tops, bots = [], []
|
||||
for p in paths[:30]:
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
s = _find_white_tab_strip(f)
|
||||
if s: tops.append(s[0]); bots.append(s[1])
|
||||
med_top = int(np.median(tops))
|
||||
med_bot = int(np.median(bots))
|
||||
print(f" 스트립 Y: {med_top}~{med_bot}")
|
||||
|
||||
# MSE 중복제거
|
||||
THRESHOLD = 0.95
|
||||
candidates, compared = [], []
|
||||
for p in paths:
|
||||
f = cv2.imread(str(p))
|
||||
if f is None: continue
|
||||
h = f.shape[0]
|
||||
crop = f[max(0, med_top):min(h, med_bot), :]
|
||||
if not _has_tab_content(crop): continue
|
||||
cmp_img = cv2.resize(crop, (480, 120), interpolation=cv2.INTER_AREA)
|
||||
if any(compare_frames(cmp_img, ref) >= THRESHOLD for ref in compared):
|
||||
continue
|
||||
candidates.append(crop)
|
||||
compared.append(cmp_img)
|
||||
|
||||
print(f"\n[1] MSE 중복제거 후: {len(candidates)}개 후보")
|
||||
|
||||
# ── BUG1 검증: 씬전환 감지 횟수 ─────────────────────────────────────
|
||||
print(f"\n[2] BUG1 검증 — 씬전환 감지 횟수 (기대: 1~3)")
|
||||
stitched = _merge_scroll_candidates(candidates)
|
||||
print(f" _merge_scroll_candidates 결과: {len(stitched)}개 세그먼트 → 파노라마")
|
||||
for i, s in enumerate(stitched):
|
||||
print(f" 세그먼트 파노라마 {i}: {s.shape[1]}px")
|
||||
cv2.imwrite(str(OUT_DIR / f"seg_pano_{i:02d}.png"), s)
|
||||
|
||||
# ── BUG2 검증: 파노라마 병합 ────────────────────────────────────────
|
||||
print(f"\n[3] BUG2 검증 — 파노라마 병합 (기대: 1~2개)")
|
||||
merged = merge_panoramas_list(stitched)
|
||||
print(f" merge_panoramas_list 결과: {len(merged)}개 최종 파노라마")
|
||||
for i, m in enumerate(merged):
|
||||
print(f" 최종 파노라마 {i}: {m.shape[1]}x{m.shape[0]}px")
|
||||
cv2.imwrite(str(OUT_DIR / f"final_pano_{i:02d}.png"), m)
|
||||
|
||||
# ── BUG3 검증: 마디 구분선 탐지 ────────────────────────────────────
|
||||
print(f"\n[4] BUG3 검증 — 마디 구분선 탐지 (기대: 간격 모두 ≥100px)")
|
||||
total_measures = 0
|
||||
all_ok = True
|
||||
for i, m in enumerate(merged):
|
||||
gray = m[:, :, 2] # Red 채널
|
||||
bars = _detect_measure_bars(gray)
|
||||
total_measures += max(0, len(bars) - 1) # 구분선 사이가 마디 수
|
||||
print(f" 파노라마 {i}: {len(bars)}개 구분선 탐지", end="")
|
||||
if bars:
|
||||
gaps = [bars[j+1]-bars[j] for j in range(len(bars)-1)]
|
||||
min_gap = min(gaps) if gaps else 0
|
||||
ok = min_gap >= 100
|
||||
if not ok: all_ok = False
|
||||
print(f" | 최소간격: {min_gap}px {'✅' if ok else '❌ (오탐 여전히 존재)'}")
|
||||
print(f" 첫5개 좌표: {bars[:5]}")
|
||||
else:
|
||||
print()
|
||||
|
||||
# ── 최종 판정 ───────────────────────────────────────────────────────
|
||||
print(f"\n{'='*60}")
|
||||
print("[검증 결과]")
|
||||
seg_ok = len(stitched) <= 5 # 씬전환 5회 이하 (이전 8회 → 개선)
|
||||
merge_ok = len(merged) <= 2 # 파노라마 2개 이하 (이전 3개 → 개선)
|
||||
bar_ok = all_ok # 모든 마디선 간격 ≥100px
|
||||
print(f" BUG1 씬전환 오탐: {'✅ 개선됨' if seg_ok else '❌ 여전히 과다'} ({len(stitched)}개 세그먼트, 이전 9개)")
|
||||
print(f" BUG2 파노라마 분리: {'✅ 개선됨' if merge_ok else '❌ 여전히 분리'} ({len(merged)}개, 이전 3개)")
|
||||
print(f" BUG3 마디선 오탐: {'✅ 개선됨' if bar_ok else '❌ 여전히 오탐'}")
|
||||
print(f" 탐지된 총 마디 수: {total_measures}개")
|
||||
print(f"{'='*60}")
|
||||
|
||||
if seg_ok and merge_ok and bar_ok:
|
||||
print("\n✅ 모든 버그 수정 확인 — 실제 파이프라인 실행 가능")
|
||||
else:
|
||||
print("\n⚠ 일부 문제 잔존 — 추가 파라미터 조정 필요")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -548,11 +548,15 @@ def _detect_measure_bars(gray_pano: np.ndarray) -> List[int]:
|
||||
for c in bar_cols:
|
||||
if not curr: curr.append(c)
|
||||
else:
|
||||
if c - curr[-1] < 10: curr.append(c)
|
||||
# [BUG3 FIX] 클러스터 허용폭 10→30px (마디선은 보통 2~5px 폭 클러스터)
|
||||
if c - curr[-1] < 30: curr.append(c)
|
||||
else:
|
||||
measures.append(int(np.mean(curr)))
|
||||
curr = [c]
|
||||
if curr: measures.append(int(np.mean(curr)))
|
||||
# [BUG3 FIX] 100px 미만 간격 마디선 제거 (음표 기둥 오탐 방지)
|
||||
measures = [x for i, x in enumerate(measures)
|
||||
if i == 0 or x - measures[i-1] >= 100]
|
||||
return measures
|
||||
|
||||
def _stamp_measure_number(measure_bgr: np.ndarray, num: int) -> np.ndarray:
|
||||
@@ -591,8 +595,9 @@ def _merge_scroll_candidates(candidates: List[np.ndarray], min_scroll: int = 5,
|
||||
curr_frame = candidates[i]
|
||||
s_px, conf = _detect_scroll_offset(prev_frame, curr_frame, min_confidence=0.1)
|
||||
|
||||
# 씬 전환 조건: conf 폭락, 가속도>100, 노란마커 증발(>0.4)
|
||||
is_cut = (conf <= 0.15) or (abs(s_px - prev_s_px) > 100) or (prev_conf - conf > 0.4)
|
||||
# [BUG1 FIX] 씬 전환 조건: conf 기반만 사용
|
||||
# abs(s_px - prev_s_px) > 100 제거 — 스크롤 가속도를 씬전환으로 오탐하던 원인
|
||||
is_cut = (conf <= 0.15) or (prev_conf - conf > 0.4)
|
||||
|
||||
if not is_cut:
|
||||
current_segment.append(curr_frame)
|
||||
@@ -635,7 +640,8 @@ def merge_panoramas_list(panoramas):
|
||||
res = cv2.matchTemplate(s_gray, h_gray, cv2.TM_CCOEFF_NORMED)
|
||||
_, max_val, _, max_loc = cv2.minMaxLoc(res)
|
||||
|
||||
if max_val > 0.60:
|
||||
# [BUG2 FIX] 매칭 임계치 0.60 → 0.50 (반복 코러스 구간에서 0.56~0.59 스코어로 분리되던 버그)
|
||||
if max_val > 0.50:
|
||||
match_x_in_search = max_loc[0]
|
||||
absolute_match_x = current_master.shape[1] - search_w + match_x_in_search
|
||||
next_start_idx = current_master.shape[1] - absolute_match_x
|
||||
@@ -717,9 +723,6 @@ def extract_unique_scroll(frames: List[np.ndarray], threshold: float = SIMILARIT
|
||||
gray_m = _extract_print_channel(measure_img)
|
||||
bgr_m = cv2.cvtColor(gray_m, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
bgr_m = _stamp_measure_number(bgr_m, global_measure_counter)
|
||||
global_measure_counter += 1
|
||||
|
||||
if current_row is None:
|
||||
current_row = bgr_m
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user