| """Weight image codec conditions used by the study.""" |
|
|
| from __future__ import annotations |
|
|
| from collections.abc import Callable, Mapping |
| from dataclasses import dataclass |
| from io import BytesIO |
|
|
| import numpy as np |
| from PIL import Image |
|
|
|
|
| @dataclass(frozen=True) |
| class Condition: |
| """一つの画像変換条件とその capability を表す。""" |
|
|
| name: str |
| family: str |
| level: float | int | str | None |
| capability: bool |
| transform: Callable[[Image.Image, np.random.Generator], bytes] |
| description: str |
| metadata: Mapping[str, object] | None = None |
|
|
| def apply(self, image: Image.Image, seed: int = 0) -> bytes: |
| """固定 seed で条件を適用し、PNG/JPEG等の bytes を返す。""" |
| return self.transform(image.copy(), np.random.default_rng(seed)) |
|
|
|
|
| def _encode(image: Image.Image, fmt: str, **kwargs: object) -> bytes: |
| stream = BytesIO() |
| image.convert("RGB").save(stream, format=fmt, **kwargs) |
| return stream.getvalue() |
|
|
|
|
| def _pixel_png(array: np.ndarray) -> bytes: |
| """RGB arrayを有効なPNG streamとして保存する。""" |
| return _encode(Image.fromarray(np.asarray(array, dtype=np.uint8), "RGB"), "PNG") |
|
|
|
|
| def _noise(image: Image.Image, rng: np.random.Generator, kind: str, level: float) -> bytes: |
| array = np.asarray(image.convert("RGB"), dtype=np.int16).copy() |
| if kind == "gaussian": |
| array += rng.normal(0, level, array.shape).round().astype(np.int16) |
| elif kind == "salt_pepper": |
| mask = rng.random(array.shape[:2]) < level |
| salt = rng.random(mask.sum()) < 0.5 |
| pixels = array[mask] |
| pixels[:] = np.where(salt[:, None], 255, 0) |
| array[mask] = pixels |
| elif kind == "pixel_dropout": |
| mask = rng.random(array.shape[:2]) < level |
| array[mask] = 0 |
| elif kind == "bit_flip": |
| mask = rng.random(array.shape[:2] + (2,)) < level |
| bits = rng.integers(0, 8, mask.shape, dtype=np.int16) |
| channels = array[..., :2] |
| channels[mask] ^= (1 << bits[mask]) |
| return _encode(Image.fromarray(np.clip(array, 0, 255).astype(np.uint8)), "PNG") |
|
|
|
|
| def _bit_flip(image: Image.Image, rng: np.random.Generator, level: float) -> bytes: |
| """R/G channelだけをbit flipし、Bを不変にしたPNGを返す。""" |
| return _noise(image, rng, "bit_flip", level) |
|
|
|
|
| def _byte_corrupt(image: Image.Image, rng: np.random.Generator, level: float, byte: str = "low") -> bytes: |
| """PNG streamを壊さず、RGB arrayの指定byteのみ非重複indexで変更する。""" |
| array = np.asarray(image.convert("RGB"), dtype=np.uint8).copy() |
| channel = 0 if byte == "high" else 1 if byte == "low" else None |
| if channel is None: |
| raise ValueError("byte must be high or low") |
| flat = array[..., channel].reshape(-1) |
| count = max(1, min(len(flat), round(len(flat) * level))) |
| indices = rng.choice(len(flat), size=count, replace=False) |
| old = flat[indices].copy() |
| replacement = rng.integers(0, 256, size=count, dtype=np.uint8) |
| replacement[replacement == old] ^= np.uint8(1) |
| flat[indices] = replacement |
| return _pixel_png(array) |
|
|
|
|
| def _jpeg(image: Image.Image, quality: int, subsampling: int | str = 0) -> bytes: |
| return _encode(image, "JPEG", quality=quality, subsampling=subsampling, optimize=False) |
|
|
|
|
| def build_registry(profile: str = "full") -> list[Condition]: |
| """仕様で固定された codec/corruption registry を返す。""" |
| qualities = [100, 95, 90, 85, 80, 70, 60, 50, 40, 30, 20, 10] |
| registry: list[Condition] = [Condition("png_baseline", "png", None, True, lambda i, r: _encode(i, "PNG"), "lossless PNG", {})] |
| if profile == "quick": |
| qualities = [100, 95, 80, 50, 10] |
| gaussian = [2.0] |
| salt_pepper = [0.01] |
| bit_flips = [1e-4] |
| dropouts = [0.01] |
| bytes_levels = [1e-4] |
| else: |
| gaussian = [1.0, 2.0, 5.0, 10.0] |
| salt_pepper = [0.001, 0.01, 0.05, 0.1] |
| bit_flips = [1e-6, 1e-5, 1e-4, 1e-3] |
| dropouts = [0.001, 0.01, 0.05, 0.1] |
| bytes_levels = [1e-5, 1e-4, 1e-3] |
| for quality in qualities: |
| registry.append(Condition(f"jpeg_q{quality}", "jpeg", quality, True, lambda i, r, q=quality: _jpeg(i, q), f"JPEG quality {quality}")) |
| for subsampling, label in [(0, "444"), (1, "422"), (2, "420")]: |
| registry.append(Condition(f"jpeg_q90_{label}", "jpeg_subsampling", label, True, lambda i, r, s=subsampling: _jpeg(i, 90, s), f"JPEG 90 {label}")) |
| try: |
| Image.registered_extensions() |
| webp_capable = "WEBP" in Image.SAVE |
| except (AttributeError, KeyError): |
| webp_capable = False |
| registry.extend([ |
| Condition("webp_lossless", "webp", "lossless", webp_capable, lambda i, r: _encode(i, "WEBP", lossless=True), "WebP lossless"), |
| Condition("webp_lossy_q80", "webp", 80, webp_capable, lambda i, r: _encode(i, "WEBP", quality=80), "WebP lossy quality 80"), |
| ]) |
| for value in gaussian: |
| registry.append(Condition(f"gaussian_{value:g}", "gaussian", value, True, lambda i, r, v=value: _noise(i, r, "gaussian", v), f"Gaussian sigma {value}")) |
| for value in salt_pepper: |
| registry.append(Condition(f"salt_pepper_{value:g}", "salt_pepper", value, True, lambda i, r, v=value: _noise(i, r, "salt_pepper", v), f"salt-pepper rate {value}")) |
| for value in bit_flips: |
| registry.append(Condition(f"random_bit_flip_{value:g}", "bit_flip", value, True, lambda i, r, v=value: _noise(i, r, "bit_flip", v), f"bit flip probability {value}")) |
| for value in dropouts: |
| registry.append(Condition(f"pixel_dropout_{value:g}", "pixel_dropout", value, True, lambda i, r, v=value: _noise(i, r, "pixel_dropout", v), f"pixel dropout rate {value}")) |
| for value in bytes_levels: |
| for level_name in ("low", "high"): |
| scale = value * (0.25 if level_name == "low" else 4.0) |
| registry.append(Condition(f"random_byte_{level_name}_{value:g}", "byte_corruption", value, True, lambda i, r, v=scale, b=level_name: _byte_corrupt(i, r, v, b), f"random byte corruption {level_name}", {"nominal_rate": value, "effective_rate": scale, "channel": "R" if level_name == "high" else "G", "rate_multiplier": 4.0 if level_name == "high" else 0.25})) |
| return registry |
|
|