brulee-1's picture
Add files using upload-large-folder tool
6debdcc verified
Raw
History Blame Contribute Delete
6.24 kB
"""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