Files
remove-ai-watermarks/tests/test_text_restoration.py

129 lines
4.1 KiB
Python

"""Verified-text manifest and compositor tests without model downloads."""
from __future__ import annotations
import json
import numpy as np
import pytest
from PIL import Image, PngImagePlugin
from remove_ai_watermarks._internal.text_restoration import (
FIDELITY_BLEND_ALPHA,
VerifiedTextLine,
blend_fidelity_anchor,
load_verified_text_manifest,
restore_verified_text,
source_pixel_sha256,
)
def _manifest(image: Image.Image) -> dict[str, object]:
return {
"schema_version": 1,
"verified": True,
"source_pixel_sha256": source_pixel_sha256(image),
"width": image.width,
"height": image.height,
"lines": [
{
"box": [8, 8, 40, 24],
"text": "Exact text",
"script": "alphabetic",
"angle": 0.0,
}
],
}
def test_pixel_hash_ignores_container_metadata(tmp_path) -> None:
image = Image.new("RGB", (48, 32), (10, 20, 30))
plain = tmp_path / "plain.png"
tagged = tmp_path / "tagged.png"
image.save(plain)
metadata = PngImagePlugin.PngInfo()
metadata.add_text("note", "different container bytes")
image.save(tagged, pnginfo=metadata)
with Image.open(plain) as left, Image.open(tagged) as right:
assert plain.read_bytes() != tagged.read_bytes()
assert source_pixel_sha256(left) == source_pixel_sha256(right)
def test_verified_manifest_is_bound_to_source_pixels(tmp_path) -> None:
source = Image.new("RGB", (48, 32), (10, 20, 30))
path = tmp_path / "lines.json"
path.write_text(json.dumps(_manifest(source)), encoding="utf-8")
loaded = load_verified_text_manifest(path, source)
assert loaded.width == 48
assert loaded.height == 32
assert loaded.lines == (VerifiedTextLine((8, 8, 40, 24), "Exact text", "alphabetic", 0.0),)
@pytest.mark.parametrize(
("mutation", "message"),
[
({"verified": False}, "verified=true"),
({"source_pixel_sha256": "0" * 64}, "does not match"),
({"width": 49}, "dimensions"),
({"lines": []}, "non-empty"),
],
)
def test_manifest_rejects_unverified_or_unbound_input(tmp_path, mutation, message) -> None:
source = Image.new("RGB", (48, 32), (10, 20, 30))
payload = _manifest(source)
payload.update(mutation)
path = tmp_path / "lines.json"
path.write_text(json.dumps(payload), encoding="utf-8")
with pytest.raises(ValueError, match=message):
load_verified_text_manifest(path, source)
def test_fidelity_anchor_uses_the_calibrated_rounding() -> None:
clean = Image.fromarray(np.array([[[1, 2, 3], [100, 150, 200]]], dtype=np.uint8))
donor = Image.fromarray(np.array([[[255, 254, 253], [200, 100, 50]]], dtype=np.uint8))
result = np.asarray(blend_fidelity_anchor(clean, donor))
expected = np.rint(
np.asarray(clean, dtype=np.float32) * (1.0 - FIDELITY_BLEND_ALPHA)
+ np.asarray(donor, dtype=np.float32) * FIDELITY_BLEND_ALPHA
).astype(np.uint8)
assert np.array_equal(result, expected)
def test_restoration_uses_lama_and_qwen_vae_core(monkeypatch) -> None:
from remove_ai_watermarks import region_eraser
source = np.full((40, 64, 3), 20, dtype=np.uint8)
source[12:24, 12:44] = 235
candidate = np.full_like(source, 30)
candidate[12:24, 12:44] = 150
donor = np.full_like(source, 40)
donor[12:24, 12:44] = (210, 220, 230)
calls: list[np.ndarray] = []
def fake_erase(image_bgr, mask):
calls.append(mask.copy())
output = image_bgr.copy()
output[mask > 0] = (30, 30, 30)
return output
monkeypatch.setattr(region_eraser, "lama_available", lambda: True)
monkeypatch.setattr(region_eraser, "erase_lama", fake_erase)
result = restore_verified_text(
Image.fromarray(source),
Image.fromarray(candidate),
Image.fromarray(donor),
(VerifiedTextLine((8, 8, 48, 28), "Exact text", "alphabetic"),),
)
restored = np.asarray(result)
assert calls
assert np.all(restored[16, 20] == donor[16, 20])
assert np.all(restored[0, 0] == candidate[0, 0])