영상 중복 탐지 완성 + 간단 편집(회전·자르기) + 동시 재생 + 별점·태그 세트
중복 탐지 - 이미지 근접쌍 밴드 색인 + 스트리밍 클러스터, 후보 검증(타일 다수결·크롭 해시) - 색 지문으로 색만 다른 오탐 제거, 회전·반전본 탐지(8변형 질의, 선택 옵션) - 오디오 지문(chromaprint)을 같은 디코드 패스에서 뽑아 레터박스 사본을 잡는다. 배경음만 같은 남남은 시각 확인 패스(트림 축 프레임 거리)로 걸러낸다 — 실측 9 vs 29. - 비교 재생(CompareView): 여러 사본을 offset 정렬해 나란히 재생 - 지문 생성 속도 조절(느림·보통·빠름) — 재시작 없이 반영 간단 편집 (원본을 직접 고침, Ctrl+Z로 되돌림) - 회전·반전: JPEG은 EXIF 방향 태그 2바이트만, MP4는 회전 행렬만 — 화질 손실 0. 방향값 전이표는 군의 성질(90도x4=제자리)로 검산하고, 실제 화소를 transpose=1과 비교해 "시계 방향"이 정말 시계인지 확인한다. - 자르기: 화면 좌표 기준(회전 메타가 붙어 있어도 어긋나지 않는다) - 구간 자르기: 스트림 복사라 무손실. 필름스트립 타임라인 + 손잡이 끌기 + 구간 미리보기. - 되돌릴 수 없는 편집(자르기·반전)은 원본을 .archive_trash/edits로 옮겨 보관 - 편집이 수정시각을 바꾸지 않는다 — 기본 정렬이 촬영일이라 항목이 튀면 다시 찾아야 한다 동시 재생 - 고른 영상을 최대 9개 격자에 놓고 함께 재생. 소리는 한 칸만. - 실시간 변환이 필요한 코덱은 2개까지만 — 그 이상은 ffmpeg가 기계를 멈춘다 그 외 - 별점(스냅샷 flags 남는 비트에 얹어 항목당 바이트 증가 0) - 태그: 검색 없이 클릭으로 붙이기, 세트+단축키, 이름 수정 시 붙은 항목에 전파 - 파일 로깅(tracing-appender) — 릴리스는 콘솔이 없어 stdout만으로는 원인을 못 찾았다 - ARM64 크로스 컴파일 스크립트(관리자 권한 없이 VS 카탈로그에서 조립) - DB 마이그레이션 v7~v10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
67d2ddc59e
commit
8aa74c1a8d
@@ -0,0 +1,102 @@
|
||||
//! 진단용 — `wmbig` 오탐의 실제 근거 수치를 찍는다.
|
||||
//! `cargo test -p archive-indexer --test wmbig_evidence -- --nocapture`
|
||||
|
||||
use archive_indexer::sigmatch::{self, FrameSig};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn repo_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..").canonicalize().unwrap()
|
||||
}
|
||||
|
||||
fn sig_of(p: &Path) -> Option<FrameSig> {
|
||||
let img = image::ImageReader::open(p).ok()?.with_guessed_format().ok()?.decode().ok()?;
|
||||
let gray = img
|
||||
.resize_exact(
|
||||
sigmatch::FRAME as u32,
|
||||
sigmatch::FRAME as u32,
|
||||
image::imageops::FilterType::Triangle,
|
||||
)
|
||||
.to_luma8()
|
||||
.into_raw();
|
||||
sigmatch::derive(&gray)
|
||||
}
|
||||
|
||||
fn color_of(p: &Path) -> Option<[i8; 32]> {
|
||||
let img = image::ImageReader::open(p).ok()?.with_guessed_format().ok()?.decode().ok()?;
|
||||
let small = img.resize_exact(4, 4, image::imageops::FilterType::Triangle).to_rgb8().into_raw();
|
||||
sigmatch::color_of(&small)
|
||||
}
|
||||
|
||||
/// 임계값을 다시 손볼 때 근거표를 뽑는 용도. 단정이 없으므로 기본 실행에서는 뺀다.
|
||||
/// `sigmatch`의 VETO_AGREE_RATIO·CROP_MAX_DIST 주석에 적힌 수치가 여기서 나온 것이다.
|
||||
#[test]
|
||||
#[ignore = "근거 측정용 — cargo test --test wmbig_evidence -- --ignored --nocapture"]
|
||||
fn print_wmbig_evidence() {
|
||||
let dir = repo_root().join(".fixtures/images");
|
||||
if !dir.is_dir() {
|
||||
eprintln!("픽스처 없음 — 건너뜀");
|
||||
return;
|
||||
}
|
||||
let pairs = [
|
||||
("pos_still3_wmbig.jpg", "pos_still4_wmbig.jpg", "오탐: 서로 다른 원본 + 같은 큰 가림"),
|
||||
("pos_still3_orig.jpg", "pos_still4_orig.jpg", "그 두 원본 자체"),
|
||||
("pos_still3_orig.jpg", "pos_still3_wmbig.jpg", "정탐: 원본 ↔ 자기 가림본"),
|
||||
("pos_still3_orig.jpg", "pos_still3_rot90.jpg", "회전본"),
|
||||
("pos_still3_orig.jpg", "pos_still3_crop95.jpg", "5% 크롭"),
|
||||
("pos_still3_orig.jpg", "pos_still3_q12.jpg", "저품질 재인코딩 (거부권에 걸리면 안 된다)"),
|
||||
("pos_still3_orig.jpg", "pos_still3_half.jpg", "50% 축소"),
|
||||
("pos_still3_orig.jpg", "pos_still3_bright.jpg", "밝기·대비 변경"),
|
||||
("pos_still3_orig.jpg", "pos_still3_wm.jpg", "하단 자막 바"),
|
||||
("neg_bars.jpg", "neg_hdbars.jpg", "무관: 컬러바 두 종"),
|
||||
];
|
||||
for (a, b, label) in pairs {
|
||||
let (pa, pb) = (dir.join(a), dir.join(b));
|
||||
let (Some(sa), Some(sb)) = (sig_of(&pa), sig_of(&pb)) else {
|
||||
eprintln!("{label}: 파일 없음");
|
||||
continue;
|
||||
};
|
||||
let ra = sigmatch::RichSig {
|
||||
dhash: sa.dhash,
|
||||
orient: sa.orient,
|
||||
tiles: sa.tiles,
|
||||
scales: sa.scales,
|
||||
tile_flat: sa.tile_flat,
|
||||
color: color_of(&pa),
|
||||
};
|
||||
let rb = sigmatch::RichSig {
|
||||
dhash: sb.dhash,
|
||||
orient: sb.orient,
|
||||
tiles: sb.tiles,
|
||||
scales: sb.scales,
|
||||
tile_flat: sb.tile_flat,
|
||||
color: color_of(&pb),
|
||||
};
|
||||
let whole = sigmatch::hamming(ra.dhash, rb.dhash);
|
||||
let (agree_loose, seen) = sigmatch::informative_agreement(&ra, &rb, sigmatch::TILE_PER_TILE);
|
||||
let (agree_strict, _) =
|
||||
sigmatch::informative_agreement(&ra, &rb, sigmatch::TILE_STRICT_PER_TILE);
|
||||
let cdist = match (&ra.color, &rb.color) {
|
||||
(Some(x), Some(y)) => sigmatch::color_distance(x, y) as i64,
|
||||
_ => -1,
|
||||
};
|
||||
let verdict = sigmatch::verify_pair(&ra, &rb, 5, true);
|
||||
println!("── {label}");
|
||||
println!(" 전체 dHash 거리 {whole} · 색차 {cdist} · 판정 {verdict:?}");
|
||||
println!(
|
||||
" 정보타일 {seen}개 중 근접 {agree_loose}(느슨 ≤{})/{agree_strict}(엄격 ≤{})",
|
||||
sigmatch::TILE_PER_TILE,
|
||||
sigmatch::TILE_STRICT_PER_TILE
|
||||
);
|
||||
// 타일별로 거리와 '정보 없음' 여부를 같이 본다 (X = 판정에서 제외)
|
||||
let cells: Vec<String> = (0..16)
|
||||
.map(|i| {
|
||||
let d = sigmatch::hamming(ra.tiles[i], rb.tiles[i]);
|
||||
let skip = (ra.tile_flat >> i) & 1 == 1 || (rb.tile_flat >> i) & 1 == 1;
|
||||
format!("{}{d:>2}", if skip { "X" } else { " " })
|
||||
})
|
||||
.collect();
|
||||
for row in 0..4 {
|
||||
println!(" {}", cells[row * 4..row * 4 + 4].join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user