mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-19 12:07:13 +02:00
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
|
|
import synthid_v3_codebook_probe as probe
|
|
|
|
|
|
def _write_codebook(path: Path, *, height: int, width: int, phase: float) -> None:
|
|
half_width = width // 2 + 1
|
|
rows = np.asarray([7, 11, 13, 17], dtype=np.uint32)
|
|
columns = np.asarray([5, 9, 12, 15], dtype=np.uint32)
|
|
indices = np.ravel_multi_index((rows, columns), (height, half_width)).astype(np.uint32)
|
|
payload: dict[str, np.ndarray] = {
|
|
"format_version": np.asarray(2),
|
|
f"{height}x{width}/sparse": np.asarray(1),
|
|
}
|
|
for channel in range(3):
|
|
payload[f"{height}x{width}/idx_{channel}"] = indices
|
|
magnitudes = np.asarray([1000.0, 10.0, 10.0, 10.0])
|
|
payload[f"{height}x{width}/mag_{channel}"] = np.log2(1.0 + magnitudes).astype(np.float16)
|
|
payload[f"{height}x{width}/phase_{channel}"] = np.full(4, phase, dtype=np.float16)
|
|
payload[f"{height}x{width}/cons_{channel}"] = np.full(4, 255, dtype=np.uint8)
|
|
np.savez(path, **payload)
|
|
|
|
|
|
def _write_dense_codebook(path: Path, *, height: int, width: int, phase: float) -> None:
|
|
half_width = width // 2 + 1
|
|
magnitudes = np.zeros((height, half_width, 3), dtype=np.float16)
|
|
phases = np.zeros_like(magnitudes)
|
|
coherence = np.zeros_like(magnitudes, dtype=np.uint8)
|
|
rows = np.asarray([7, 11, 13, 17])
|
|
columns = np.asarray([5, 9, 12, 15])
|
|
for channel in range(3):
|
|
magnitudes[rows, columns, channel] = np.log2(1.0 + np.asarray([1000.0, 10.0, 10.0, 10.0]))
|
|
phases[rows, columns, channel] = phase
|
|
coherence[rows, columns, channel] = 255
|
|
np.savez(
|
|
path,
|
|
format_version=np.asarray(2),
|
|
**{
|
|
f"{height}x{width}/sparse": np.asarray(0),
|
|
f"{height}x{width}/mag": magnitudes,
|
|
f"{height}x{width}/phase": phases,
|
|
f"{height}x{width}/cons": coherence,
|
|
},
|
|
)
|
|
|
|
|
|
def _write_carrier(path: Path, *, height: int, width: int, phase: float) -> None:
|
|
yy, xx = np.mgrid[:height, :width]
|
|
carrier = 80.0 + 20.0 * np.cos(2.0 * np.pi * (7.0 * yy / height + 5.0 * xx / width) + phase)
|
|
pixels = np.repeat(carrier[:, :, None], 3, axis=2)
|
|
Image.fromarray(np.clip(np.rint(pixels), 0, 255).astype(np.uint8), mode="RGB").save(path)
|
|
|
|
|
|
def test_numeric_codebook_scores_matching_phase(tmp_path: Path) -> None:
|
|
height = width = 64
|
|
codebook = tmp_path / "codebook.npz"
|
|
image = tmp_path / "image.png"
|
|
_write_codebook(codebook, height=height, width=width, phase=0.4)
|
|
_write_carrier(image, height=height, width=width, phase=0.4)
|
|
|
|
model = probe.load_v3_model(codebook, height=height, width=width, peak_count=4, min_radius=1.0)
|
|
score = probe.score_image(image, model)
|
|
|
|
assert score.peak_count == 4
|
|
assert score.phase_score > 0.0
|
|
|
|
|
|
def test_dense_numeric_codebook_scores_matching_phase(tmp_path: Path) -> None:
|
|
height = width = 64
|
|
codebook = tmp_path / "dense-codebook.npz"
|
|
image = tmp_path / "image.png"
|
|
_write_dense_codebook(codebook, height=height, width=width, phase=0.4)
|
|
_write_carrier(image, height=height, width=width, phase=0.4)
|
|
|
|
model = probe.load_v3_model(codebook, height=height, width=width, peak_count=4, min_radius=1.0)
|
|
score = probe.score_image(image, model)
|
|
|
|
assert score.peak_count == 4
|
|
assert score.phase_score > 0.0
|
|
|
|
|
|
def test_translation_search_recovers_shifted_carrier(tmp_path: Path) -> None:
|
|
height = width = 64
|
|
codebook = tmp_path / "codebook.npz"
|
|
image = tmp_path / "image.png"
|
|
shifted = tmp_path / "shifted.png"
|
|
_write_codebook(codebook, height=height, width=width, phase=0.4)
|
|
_write_carrier(image, height=height, width=width, phase=0.4)
|
|
with Image.open(image) as source:
|
|
pixels = np.asarray(source).copy()
|
|
Image.fromarray(np.roll(pixels, shift=(1, 1), axis=(0, 1)), mode="RGB").save(shifted)
|
|
model = probe.load_v3_model(codebook, height=height, width=width, peak_count=4, min_radius=1.0)
|
|
|
|
fixed = probe.score_image(shifted, model)
|
|
registered = probe.score_translations(shifted, model, max_shift=2)
|
|
|
|
assert registered.phase_score > fixed.phase_score
|
|
assert abs(registered.row_shift) <= 2
|
|
assert abs(registered.column_shift) <= 2
|
|
|
|
|
|
def test_rejects_wrong_format(tmp_path: Path) -> None:
|
|
artifact = tmp_path / "bad.npz"
|
|
np.savez(artifact, format_version=np.asarray(1))
|
|
|
|
with pytest.raises(ValueError, match="version 2"):
|
|
probe.load_v3_model(artifact, height=64, width=64)
|