"""Prepare unchanged real samples plus controlled anomaly validation inputs.""" from __future__ import annotations import argparse import hashlib import json import shutil from pathlib import Path import cv2 import numpy as np from PIL import Image def _require_empty(path: Path) -> None: if path.exists() and any(path.iterdir()): raise ValueError(f"Output directory is not empty: {path}") path.mkdir(parents=True, exist_ok=True) def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _bbox(width: int, height: int, fractions: tuple[float, float, float, float]) -> list[int]: x0, y0, x1, y1 = fractions return [int(width * x0), int(height * y0), int(width * x1), int(height * y1)] def generate(normal_source: Path, difficult_source: Path, raw_output: Path, processed_output: Path) -> dict: _require_empty(raw_output) _require_empty(processed_output) reference_dir = raw_output / "reference" raw_inputs = raw_output / "inputs" processed_inputs = processed_output / "inputs" truth_dir = processed_output / "truth" for directory in (reference_dir, raw_inputs, processed_inputs, truth_dir): directory.mkdir(parents=True, exist_ok=True) reference_path = reference_dir / "07-16-reference.jpg" normal_path = raw_inputs / "07-16-normal.jpg" difficult_path = raw_inputs / "07-19-difficult.jpg" shutil.copy2(normal_source, reference_path) shutil.copy2(normal_source, normal_path) shutil.copy2(difficult_source, difficult_path) shutil.copy2(normal_path, processed_inputs / normal_path.name) shutil.copy2(difficult_path, processed_inputs / difficult_path.name) with Image.open(normal_source) as image: rgb = np.asarray(image.convert("RGB")).copy() height, width = rgb.shape[:2] regions = [ {"name": "high_saturation_colour", "bbox": _bbox(width, height, (0.10, 0.16, 0.21, 0.29))}, {"name": "local_blur", "bbox": _bbox(width, height, (0.43, 0.12, 0.58, 0.30))}, {"name": "dark_occlusion", "bbox": _bbox(width, height, (0.68, 0.56, 0.81, 0.72))}, ] truth = np.zeros((height, width), dtype=np.uint8) x0, y0, x1, y1 = regions[0]["bbox"] rgb[y0:y1, x0:x1] = np.array([230, 20, 210], dtype=np.uint8) truth[y0:y1, x0:x1] = 255 x0, y0, x1, y1 = regions[1]["bbox"] rgb[y0:y1, x0:x1] = cv2.GaussianBlur(rgb[y0:y1, x0:x1], (151, 151), 0) truth[y0:y1, x0:x1] = 255 x0, y0, x1, y1 = regions[2]["bbox"] rgb[y0:y1, x0:x1] = np.array([8, 8, 8], dtype=np.uint8) truth[y0:y1, x0:x1] = 255 injected_name = "07-16-injected.png" Image.fromarray(rgb).save(processed_inputs / injected_name) truth_name = "07-16-injected.truth.png" Image.fromarray(truth).save(truth_dir / truth_name) manifest = { "images": { injected_name: { "mask": f"truth/{truth_name}", "regions": regions, } } } manifest_path = processed_output / "truth-manifest.json" manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") metadata = { "normal_source": normal_source.as_posix(), "difficult_source": difficult_source.as_posix(), "normal_source_sha256": _sha256(normal_source), "reference_copy_sha256": _sha256(reference_path), "difficult_source_sha256": _sha256(difficult_source), "difficult_copy_sha256": _sha256(difficult_path), "width": width, "height": height, "injected_regions": regions, "truth_manifest": manifest_path.as_posix(), "limitations": ["注入异常仅用于验证检测链路,不代表真实业务异常。"], } (processed_output / "generation_metadata.json").write_text( json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8" ) return metadata def main() -> int: parser = argparse.ArgumentParser(description="Generate anomaly-detection validation inputs.") parser.add_argument("--normal-source", type=Path, required=True) parser.add_argument("--difficult-source", type=Path, required=True) parser.add_argument("--raw-output", type=Path, required=True) parser.add_argument("--processed-output", type=Path, required=True) args = parser.parse_args() try: result = generate(args.normal_source, args.difficult_source, args.raw_output, args.processed_output) except (OSError, ValueError) as error: print(f"ERROR: {error}", file=__import__("sys").stderr) return 2 print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())