mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-19 20:17:12 +02:00
141 lines
4.6 KiB
Python
141 lines
4.6 KiB
Python
"""Build non-generative hybrid phase-projection and fragmentation candidates.
|
|
|
|
The matrix combines two independently measured mechanisms: sparse RGB/HSV
|
|
phase projection and spatially varying subpixel displacement. It is intended
|
|
for frozen provider-oracle batches with a visible-mark-removed source control.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from dataclasses import asdict
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import click
|
|
from PIL import Image
|
|
from synthid_ensemble_attack import alternating_projection
|
|
from synthid_ensemble_detector import detect_image, load_config, load_models
|
|
from synthid_fragment_attack import bounded_smooth_warp, color_nudge, jpeg_chain
|
|
from synthid_pixel_attack import load_rgb, measure, norm_matched_noise, resize_squeeze, smooth_warp
|
|
|
|
if TYPE_CHECKING:
|
|
import numpy as np
|
|
from synthid_color_space_probe import ColorPhaseModel
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def build_candidates(
|
|
source: np.ndarray,
|
|
rgb_model: ColorPhaseModel,
|
|
hsv_model: ColorPhaseModel,
|
|
) -> dict[str, np.ndarray]:
|
|
"""Return a frozen mechanism matrix derived from SOURCE."""
|
|
projected_075 = alternating_projection(
|
|
source,
|
|
rgb_model,
|
|
hsv_model,
|
|
strength=0.75,
|
|
iterations=1,
|
|
)
|
|
projected_100 = alternating_projection(
|
|
source,
|
|
rgb_model,
|
|
hsv_model,
|
|
strength=1.0,
|
|
iterations=1,
|
|
)
|
|
bounded_100 = bounded_smooth_warp(
|
|
source,
|
|
max_displacement=1.0,
|
|
sigma=56.0,
|
|
seed=20260824,
|
|
)
|
|
projected_bounded_100 = bounded_smooth_warp(
|
|
projected_075,
|
|
max_displacement=1.0,
|
|
sigma=56.0,
|
|
seed=20260824,
|
|
)
|
|
|
|
bounded_polish = resize_squeeze(projected_bounded_100, 0.98)
|
|
bounded_polish = color_nudge(
|
|
bounded_polish,
|
|
brightness=0.002,
|
|
contrast=0.003,
|
|
saturation=-0.003,
|
|
hue_degrees=0.1,
|
|
)
|
|
bounded_polish = jpeg_chain(bounded_polish, (96,))
|
|
|
|
elastic_combo = smooth_warp(projected_100, amplitude=0.75, sigma=56.0, seed=20260825)
|
|
elastic_combo = resize_squeeze(elastic_combo, 0.98)
|
|
elastic_combo = jpeg_chain(elastic_combo, (96,))
|
|
|
|
return {
|
|
"projection-075": projected_075,
|
|
"bounded-100": bounded_100,
|
|
"projection-075-bounded-100": projected_bounded_100,
|
|
"projection-075-bounded-polish": bounded_polish,
|
|
"projection-100-elastic-075": elastic_combo,
|
|
}
|
|
|
|
|
|
@click.command()
|
|
@click.argument("config_path", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
@click.argument("source", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
@click.argument("output_dir", type=click.Path(file_okay=False, path_type=Path))
|
|
def main(config_path: Path, source: Path, output_dir: Path) -> None:
|
|
"""Write a frozen non-generative hybrid attack matrix for SOURCE."""
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
config = load_config(config_path)
|
|
rgb_model, hsv_model = load_models(config)
|
|
reference = load_rgb(source)
|
|
if reference.shape != (config.height, config.width, 3):
|
|
raise click.BadParameter("source geometry does not match detector config")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
variants: list[dict[str, object]] = []
|
|
candidates = build_candidates(reference, rgb_model, hsv_model)
|
|
for name, pixels in candidates.items():
|
|
path = output_dir / f"{name}.png"
|
|
Image.fromarray(pixels, mode="RGB").save(path)
|
|
variants.append(
|
|
{
|
|
**asdict(measure(reference, pixels, name=name, path=path)),
|
|
**asdict(detect_image(path, config, rgb_model, hsv_model)),
|
|
}
|
|
)
|
|
|
|
selected = candidates["projection-075-bounded-100"]
|
|
sham = norm_matched_noise(reference, selected, seed=20260826)
|
|
sham_path = output_dir / "sham-projection-bounded-rms.png"
|
|
Image.fromarray(sham, mode="RGB").save(sham_path)
|
|
variants.append(
|
|
{
|
|
**asdict(measure(reference, sham, name="sham-projection-bounded-rms", path=sham_path)),
|
|
**asdict(detect_image(sham_path, config, rgb_model, hsv_model)),
|
|
}
|
|
)
|
|
|
|
report_path = output_dir / "report.json"
|
|
report_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"source": str(source),
|
|
"config": str(config_path),
|
|
"variants": variants,
|
|
},
|
|
indent=2,
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
log.info("Wrote %d frozen hybrid candidates: %s", len(variants), report_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|