shuishen
14 hours ago 2ae460fc4a4c2419cf44329783d49a739e2a04ea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""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())