mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 06:28:36 +02:00
Remove assume_ai, add tophat front-end and rival margin, fix two CLI defects
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a8f3536d3e
commit
cfefd9d819
@@ -0,0 +1,101 @@
|
||||
"""Build BLIND contact sheets for hand-labelling relaxation additions.
|
||||
|
||||
Crops are centered on the DETECTED REGION (not the corner), padded by ~0.9x the mark
|
||||
size, and resized to 240px with INTER_NEAREST -- a downscaled preview destroys a faint
|
||||
mark, so nothing here may smooth. The manifest is written to a separate file that must
|
||||
NOT be read until labelling is finished.
|
||||
|
||||
Each sheet mixes three strata in shuffled order:
|
||||
add - the relaxation additions whose precision we are measuring
|
||||
pos - strict-consistent detections (a mark is really there): labeller sensitivity
|
||||
clean - verified-clean negatives (no mark can be there): labeller specificity
|
||||
The two control strata are what make a low measured precision trustworthy.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from numpy.typing import NDArray
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||
from remove_ai_watermarks import watermark_registry as wr
|
||||
from remove_ai_watermarks.image_io import imread
|
||||
|
||||
CELL = 240
|
||||
COLS, ROWS = 6, 3
|
||||
PER = COLS * ROWS
|
||||
|
||||
|
||||
def region_for(path: str, key: str) -> tuple[int, int, int, int] | None:
|
||||
"""Re-detect to recover the mark bbox (Candidate carries no region)."""
|
||||
img = imread(path)
|
||||
if img is None:
|
||||
return None
|
||||
mark = next(m for m in wr._REGISTRY if m.key == key)
|
||||
d = mark.detect(img, provenance=True)
|
||||
return d.region if d.region else None
|
||||
|
||||
|
||||
def crop(path: str, region: tuple[int, int, int, int] | None, pad_factor: float = 0.9) -> NDArray[Any] | None:
|
||||
img = imread(path)
|
||||
if img is None:
|
||||
return None
|
||||
h, w = img.shape[:2]
|
||||
if region:
|
||||
x, y, rw, rh = region
|
||||
else:
|
||||
return None
|
||||
px, py = int(rw * pad_factor), int(rh * pad_factor)
|
||||
x0, y0 = max(0, x - px), max(0, y - py)
|
||||
x1, y1 = min(w, x + rw + px), min(h, y + rh + py)
|
||||
c = img[y0:y1, x0:x1]
|
||||
if c.size == 0:
|
||||
return None
|
||||
s = CELL / max(c.shape[0], c.shape[1])
|
||||
return cv2.resize(c, (max(1, int(c.shape[1] * s)), max(1, int(c.shape[0] * s))), interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with open(sys.argv[1]) as fh:
|
||||
items = json.load(fh) # [{uid,path,key,stratum,conf}]
|
||||
outdir = Path(sys.argv[2])
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
random.Random(1234).shuffle(items) # noqa: S311 -- sheet ordering, not cryptography
|
||||
|
||||
manifest = []
|
||||
cells = []
|
||||
for it in items:
|
||||
reg = region_for(it["path"], it["key"])
|
||||
c = crop(it["path"], reg)
|
||||
if c is None:
|
||||
continue
|
||||
cells.append((it, c))
|
||||
|
||||
for si in range(0, len(cells), PER):
|
||||
chunk = cells[si : si + PER]
|
||||
sheet = np.full((ROWS * (CELL + 26), COLS * (CELL + 8), 3), 40, np.uint8)
|
||||
for i, (it, c) in enumerate(chunk):
|
||||
r, col = divmod(i, COLS)
|
||||
y0 = r * (CELL + 26) + 22
|
||||
x0 = col * (CELL + 8) + 4
|
||||
sheet[y0 : y0 + c.shape[0], x0 : x0 + c.shape[1]] = c
|
||||
label = f"{si + i:04d}" # index ONLY -- no stratum, no confidence
|
||||
cv2.putText(sheet, label, (x0, y0 - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1)
|
||||
manifest.append({"idx": si + i, **it})
|
||||
cv2.imwrite(str(outdir / f"sheet_{si // PER:03d}.png"), sheet)
|
||||
|
||||
with open(outdir / "MANIFEST_DO_NOT_OPEN.csv", "w", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=list(manifest[0]))
|
||||
w.writeheader()
|
||||
w.writerows(manifest)
|
||||
print(f"sheets={(len(cells) + PER - 1) // PER} cells={len(cells)} -> {outdir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user