mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-24 14:32:28 +02:00
Evaluate selective text restoration
This commit is contained in:
@@ -20,7 +20,7 @@ import pytest
|
||||
_SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
|
||||
|
||||
|
||||
def _load_assign():
|
||||
def _load_module():
|
||||
# fidelity_metrics is a standalone PEP723 script, not an installed module; load it by
|
||||
# path with scripts/ on sys.path so its `_plain_console` shim import resolves.
|
||||
sys.path.insert(0, str(_SCRIPTS))
|
||||
@@ -35,7 +35,15 @@ def _load_assign():
|
||||
pytest.skip(f"fidelity_metrics import deps missing: {exc}")
|
||||
finally:
|
||||
sys.path.remove(str(_SCRIPTS))
|
||||
return mod.assign_faces_one_to_one
|
||||
return mod
|
||||
|
||||
|
||||
def _load_assign():
|
||||
return _load_module().assign_faces_one_to_one
|
||||
|
||||
|
||||
def test_cer_remains_case_sensitive() -> None:
|
||||
assert _load_module()._cer("A", "a") == 1.0
|
||||
|
||||
|
||||
def test_distinct_faces_match_nearest() -> None:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts/infer_text_lines.py"
|
||||
SPEC = importlib.util.spec_from_file_location("infer_text_lines", SCRIPT)
|
||||
assert SPEC is not None
|
||||
assert SPEC.loader is not None
|
||||
module = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = module
|
||||
SPEC.loader.exec_module(module)
|
||||
|
||||
|
||||
def test_choose_language_prefers_observed_unicode_script() -> None:
|
||||
probes = {"en": ("gibberish", 0.9), "ru": ("пример", 0.9), "ch": ("example", 0.9)}
|
||||
assert module.choose_language(probes) == "ru"
|
||||
|
||||
probes["ch"] = ("示例", 0.9)
|
||||
assert module.choose_language(probes) == "ch"
|
||||
|
||||
|
||||
def test_stable_recognition_requires_agreement_and_confidence() -> None:
|
||||
assert module.stable_recognition([("Sample text", 0.9), ("sample text", 0.95)]) == "Sample text"
|
||||
assert module.stable_recognition([("Sample", 0.9), ("Simple", 0.95)]) is None
|
||||
assert module.stable_recognition([("Sample", 0.8), ("Sample", 0.95)]) is None
|
||||
@@ -0,0 +1,177 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
SCRIPT = Path(__file__).parents[1] / "scripts" / "selective_text_restoration.py"
|
||||
SPEC = importlib.util.spec_from_file_location("selective_text_restoration", SCRIPT)
|
||||
assert SPEC is not None
|
||||
assert SPEC.loader is not None
|
||||
module = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = module
|
||||
SPEC.loader.exec_module(module)
|
||||
|
||||
|
||||
def test_normalized_edit_distance_ignores_case_and_whitespace() -> None:
|
||||
assert module.normalized_edit_distance(" Sample text ", "sample\ntext") == 0.0
|
||||
|
||||
|
||||
def test_preserve_requires_source_candidate_agreement() -> None:
|
||||
assert module.should_preserve_line("clean text", "clean text", 0.9, "clean text", 0.9)
|
||||
assert not module.should_preserve_line("clean text", "clean text", 0.9, "damaged", 0.9)
|
||||
|
||||
|
||||
def test_preserve_rejects_unreliable_source_recognition() -> None:
|
||||
assert not module.should_preserve_line("expected", "unrelated", 0.9, "unrelated", 0.9)
|
||||
assert not module.should_preserve_line("expected", "expected", 0.7, "expected", 0.9)
|
||||
|
||||
|
||||
def test_cjk_recognition_box_excludes_overlapping_neighbor_lines() -> None:
|
||||
line = module.TextLine((1281, 650, 2357, 818), "每天都是一个新的机会。", "cjk")
|
||||
|
||||
assert module._recognition_box(line, 2816, 1536) == (1247, 650, 2458, 818)
|
||||
|
||||
|
||||
def test_latin_recognition_box_keeps_context_padding() -> None:
|
||||
line = module.TextLine((100, 200, 300, 260), "Sample text", "latin")
|
||||
|
||||
assert module._recognition_box(line, 1000, 1000) == (40, 180, 360, 280)
|
||||
assert module._recognition_box(line, 1000, 1000, 0.1) == (40, 192, 360, 268)
|
||||
|
||||
|
||||
def test_verified_lines_cover_each_ground_truth_string() -> None:
|
||||
root = Path(__file__).parents[1]
|
||||
lines = json.loads((root / "data/evaluations/fidelity/text-lines.json").read_text(encoding="utf-8"))
|
||||
ground_truth = json.loads((root / "data/evaluations/fidelity/ground-truth.json").read_text(encoding="utf-8"))
|
||||
|
||||
assert lines.keys() == ground_truth.keys()
|
||||
for source, expected in ground_truth.items():
|
||||
observed = " ".join(line["text"] for line in lines[source])
|
||||
assert module.normalize_text(observed) == module.normalize_text(expected)
|
||||
|
||||
|
||||
def test_group_word_boxes_merges_words_but_not_neighboring_lines() -> None:
|
||||
boxes = [(10, 10, 30, 30), (32, 12, 60, 29), (10, 35, 50, 55)]
|
||||
|
||||
assert module.group_word_boxes(boxes) == [(10, 10, 60, 30), (10, 35, 50, 55)]
|
||||
|
||||
|
||||
def test_group_word_boxes_does_not_merge_distant_columns() -> None:
|
||||
boxes = [(10, 10, 60, 30), (500, 11, 560, 31)]
|
||||
|
||||
assert module.group_word_boxes(boxes) == boxes
|
||||
|
||||
|
||||
def test_source_glyph_composite_keeps_masked_pixels_exact() -> None:
|
||||
source = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
source[:, :] = (220, 180, 40)
|
||||
background = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
background[:, :] = (10, 20, 30)
|
||||
mask = np.zeros((9, 9), dtype=np.uint8)
|
||||
mask[3:6, 3:6] = 255
|
||||
|
||||
result = module.composite_source_glyphs(source, background, mask, feather=0.7)
|
||||
|
||||
np.testing.assert_array_equal(result[3:6, 3:6], source[3:6, 3:6])
|
||||
np.testing.assert_array_equal(result[0, 0], background[0, 0])
|
||||
|
||||
|
||||
def test_fresh_silhouette_uses_new_color_instead_of_source_pixels() -> None:
|
||||
background = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
background[:, :] = (10, 20, 30)
|
||||
mask = np.zeros((9, 9), dtype=np.uint8)
|
||||
mask[3:6, 3:6] = 255
|
||||
|
||||
result = module.composite_fresh_silhouette(background, mask, (220, 180, 40), feather=0)
|
||||
|
||||
assert np.all(result[3:6, 3:6] == (220, 180, 40))
|
||||
np.testing.assert_array_equal(result[0, 0], background[0, 0])
|
||||
|
||||
|
||||
def test_fresh_silhouette_antialiasing_softens_binary_edges() -> None:
|
||||
background = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
background[:, :] = (10, 20, 30)
|
||||
mask = np.zeros((9, 9), dtype=np.uint8)
|
||||
mask[3:6, 3:6] = 255
|
||||
|
||||
result = module.composite_fresh_silhouette(background, mask, (220, 180, 40), feather=1.0)
|
||||
|
||||
assert np.all(result[3, 3] > background[3, 3])
|
||||
assert np.all(result[3, 3] < (220, 180, 40))
|
||||
|
||||
|
||||
def test_reconstructed_glyphs_keep_exact_donor_core_and_fresh_edge() -> None:
|
||||
donor = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
donor[:, :] = (180, 140, 60)
|
||||
background = np.zeros((9, 9, 3), dtype=np.uint8)
|
||||
background[:, :] = (10, 20, 30)
|
||||
mask = np.zeros((9, 9), dtype=np.uint8)
|
||||
mask[3:6, 3:6] = 255
|
||||
|
||||
fresh_edge = module.composite_fresh_silhouette(background, mask, (220, 180, 40))
|
||||
result = module.composite_reconstructed_glyphs(donor, fresh_edge, mask, feather=0.5)
|
||||
|
||||
np.testing.assert_array_equal(result[3:6, 3:6], donor[3:6, 3:6])
|
||||
assert np.any(result[2, 3] != fresh_edge[2, 3])
|
||||
np.testing.assert_array_equal(result[0, 0], background[0, 0])
|
||||
|
||||
|
||||
def test_source_silhouette_discards_foreground_amplitudes() -> None:
|
||||
source = np.full((15, 15, 3), 20, dtype=np.uint8)
|
||||
source[5:10, 6:9] = 230
|
||||
source[6:9, 7] = 180
|
||||
|
||||
mask = module.source_silhouette_mask(source, (4, 4, 11, 11))
|
||||
|
||||
assert mask.dtype == np.uint8
|
||||
assert set(np.unique(mask)) <= {0, 255}
|
||||
assert mask[7, 7] == 255
|
||||
assert mask[4, 4] == 0
|
||||
|
||||
|
||||
def test_rotated_source_silhouette_excludes_axis_aligned_corners() -> None:
|
||||
source = np.full((80, 160, 3), 20, dtype=np.uint8)
|
||||
source[10:70, 10:150] = 230
|
||||
|
||||
mask = module.source_silhouette_mask(source, (0, 0, 160, 80), angle=12)
|
||||
|
||||
assert mask[0, 0] == 0
|
||||
assert mask[79, 159] == 0
|
||||
|
||||
|
||||
def test_source_box_mask_pads_and_clips_boxes() -> None:
|
||||
mask = module.source_box_mask((20, 30), [(1, 2, 11, 10), (25, 15, 30, 20)])
|
||||
|
||||
assert mask.shape == (20, 30)
|
||||
assert mask[0, 0] == 255
|
||||
assert mask[19, 29] == 255
|
||||
assert mask[0, 22] == 0
|
||||
|
||||
|
||||
def test_detect_line_boxes_fails_closed_on_count_mismatch() -> None:
|
||||
class Engine:
|
||||
def predict(self, _image):
|
||||
return [{"rec_scores": [0.9], "rec_boxes": [[10, 10, 30, 30]]}]
|
||||
|
||||
with pytest.raises(module.click.ClickException, match="detected 1 source lines; expected exactly 2"):
|
||||
module.detect_line_boxes(Engine(), np.zeros((50, 50, 3), dtype=np.uint8), expected_count=2)
|
||||
|
||||
|
||||
def test_residual_mask_is_limited_to_original_glyph_positions(monkeypatch) -> None:
|
||||
background = np.zeros((8, 8, 3), dtype=np.uint8)
|
||||
original = np.zeros((8, 8), dtype=np.uint8)
|
||||
original[3, 3] = 255
|
||||
detected = np.zeros((8, 8), dtype=np.uint8)
|
||||
detected[3, 3] = 255
|
||||
detected[6, 6] = 255
|
||||
monkeypatch.setattr(module, "foreground_mask", lambda _image, _box: detected)
|
||||
|
||||
residual = module.residual_glyph_mask(background, original, (0, 0, 8, 8))
|
||||
|
||||
assert residual[3, 3] == 255
|
||||
assert residual[6, 6] == 0
|
||||
Reference in New Issue
Block a user