mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 13:30:55 +02:00
149 lines
5.3 KiB
Python
149 lines
5.3 KiB
Python
"""Pure-logic guards for the development-only re-watermarking study."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from PIL import Image
|
|
|
|
from remove_ai_watermarks import api
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
|
|
import rewatermarking_study as study
|
|
import rewatermarking_synthid_candidates as synthid_candidates
|
|
import rewatermarking_video_study as video_study
|
|
|
|
|
|
def test_bit_accuracy_requires_equal_nonempty_messages() -> None:
|
|
assert study.bit_accuracy((0, 1, 1, 0), (0, 0, 1, 0)) == pytest.approx(0.75)
|
|
with pytest.raises(ValueError, match="non-empty"):
|
|
study.bit_accuracy((), ())
|
|
with pytest.raises(ValueError, match="equal length"):
|
|
study.bit_accuracy((0,), (0, 1))
|
|
|
|
|
|
def test_image_fidelity_uses_decoded_8bit_pixels() -> None:
|
|
reference = np.zeros((2, 2, 3), dtype=np.uint8)
|
|
artifact = np.full((2, 2, 3), 10, dtype=np.uint8)
|
|
metrics = study.image_fidelity(reference, artifact)
|
|
assert metrics["mae_8bit"] == pytest.approx(10.0)
|
|
assert metrics["changed_fraction"] == pytest.approx(1.0)
|
|
assert metrics["psnr_db"] == pytest.approx(28.1308036087)
|
|
|
|
|
|
def test_summary_excludes_failed_positive_controls() -> None:
|
|
def row(*, scheme: str, control: bool, success: bool, psnr: float) -> dict[str, object]:
|
|
return {
|
|
"scheme": scheme,
|
|
"marked": {"detected_as_a": control, "bit_accuracy_vs_a": 1.0 if control else 0.5},
|
|
"rewatermarked": {
|
|
"bit_accuracy_vs_a": 0.5,
|
|
"bit_accuracy_vs_b": 1.0,
|
|
"fidelity_from_clean": {"psnr_db": psnr},
|
|
"fidelity_from_marked": {"psnr_db": psnr + 1},
|
|
},
|
|
"attack_success": success,
|
|
}
|
|
|
|
summary = study.summarize(
|
|
[
|
|
row(scheme="example", control=True, success=True, psnr=40.0),
|
|
row(scheme="example", control=False, success=True, psnr=10.0),
|
|
]
|
|
)["example"]
|
|
assert summary["cases"] == 2
|
|
assert summary["valid_positive_controls"] == 1
|
|
assert summary["attack_successes"] == 1
|
|
assert summary["rewatermarked_psnr_from_clean"]["median"] == pytest.approx(40.0)
|
|
|
|
|
|
def test_video_fidelity_uses_mean_per_frame_psnr() -> None:
|
|
reference = np.zeros((2, 2, 2, 3), dtype=np.float32)
|
|
artifact = np.stack(
|
|
(
|
|
np.full((2, 2, 3), 0.1, dtype=np.float32),
|
|
np.full((2, 2, 3), 0.2, dtype=np.float32),
|
|
)
|
|
)
|
|
assert video_study.mean_video_psnr(reference, artifact) == pytest.approx(16.9897, abs=1e-4)
|
|
assert video_study.mean_video_psnr(reference, reference) is None
|
|
with pytest.raises(ValueError, match="share"):
|
|
video_study.mean_video_psnr(reference, artifact[:1])
|
|
|
|
|
|
def test_synthid_candidate_sources_are_manifest_pinned_and_positive() -> None:
|
|
sources = synthid_candidates.selected_sources()
|
|
assert set(sources) == {"openai", "gemini"}
|
|
for provider, source in sources.items():
|
|
assert source.is_file()
|
|
assert synthid_candidates.sha256_file(source) == synthid_candidates.SELECTED[provider]
|
|
|
|
|
|
def test_synthid_candidate_bit_accuracy() -> None:
|
|
assert synthid_candidates.bit_accuracy((1, 0, 1), (1, 1, 1)) == pytest.approx(2 / 3)
|
|
with pytest.raises(ValueError, match="length"):
|
|
synthid_candidates.bit_accuracy((1,), (1, 0))
|
|
|
|
|
|
def test_synthid_control_removes_visible_mark_and_metadata_before_oracle(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = tmp_path / "source.png"
|
|
output = tmp_path / "control.png"
|
|
original = np.full((8, 9, 3), 80, dtype=np.uint8)
|
|
cleaned = original.copy()
|
|
cleaned[6:, 7:] = 70
|
|
Image.fromarray(original, "RGB").save(source)
|
|
|
|
def fake_remove(
|
|
actual_source: Path,
|
|
actual_output: Path,
|
|
**kwargs: object,
|
|
) -> SimpleNamespace:
|
|
assert actual_source == source
|
|
assert actual_output == output
|
|
assert kwargs == {
|
|
"sensitivity": "auto",
|
|
"backend": "cv2",
|
|
"strip_metadata": True,
|
|
"write_noop": True,
|
|
}
|
|
Image.fromarray(cleaned, "RGB").save(actual_output)
|
|
return SimpleNamespace(
|
|
status="cleaned",
|
|
marks=(SimpleNamespace(key="gemini"),),
|
|
labels=["Google Gemini visible watermark (sparkle)"],
|
|
)
|
|
|
|
monkeypatch.setattr(api, "remove_visible_detailed", fake_remove)
|
|
|
|
pixels, preflight = synthid_candidates.cleaned_control(source, output, required_visible_key="gemini")
|
|
|
|
assert np.array_equal(pixels, cleaned)
|
|
assert preflight["status"] == "cleaned"
|
|
assert preflight["removed_keys"] == ["gemini"]
|
|
assert preflight["changed_pixel_fraction"] == pytest.approx(4 / 72)
|
|
|
|
|
|
def test_synthid_control_rejects_unvalidated_visible_cleanup(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
source = tmp_path / "source.png"
|
|
output = tmp_path / "control.png"
|
|
Image.new("RGB", (8, 8), (80, 80, 80)).save(source)
|
|
monkeypatch.setattr(
|
|
api,
|
|
"remove_visible_detailed",
|
|
lambda *_args, **_kwargs: SimpleNamespace(status="partial", marks=(), labels=[]),
|
|
)
|
|
|
|
with pytest.raises(RuntimeError, match="not validated"):
|
|
synthid_candidates.cleaned_control(source, output, required_visible_key="gemini")
|