File size: 11,561 Bytes
901a5f5 | 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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | """JPEG repair Task 1-2の契約テスト。"""
from __future__ import annotations
from io import BytesIO
import numpy as np
import pytest
from PIL import Image
from pixelmodel_robustness.jpeg_repair import (
CLIP_STRATEGIES,
CONDITION_IDS,
build_jpeg_registry,
clip_candidate,
decode_jpeg_rgb,
decode_protected,
diagnostics,
encode_jpeg_q100,
encode_protected,
materialize_fp16_png,
repair_nonfinite,
semantic_hash,
validate_layer_offsets,
)
def _image() -> Image.Image:
return Image.fromarray(
np.array([[[0, 1, 2], [20, 40, 60]], [[100, 120, 140], [240, 220, 200]]], dtype=np.uint8),
"RGB",
)
def test_registry_is_exact_ordered_and_semantically_hashed() -> None:
registry = build_jpeg_registry()
assert tuple(entry.condition_id for entry in registry) == CONDITION_IDS
assert len(registry) == 18
assert len({entry.parent_raw_sha256 for entry in registry[1:17]}) == 1
assert semantic_hash(registry) == semantic_hash(build_jpeg_registry())
assert tuple(entry.condition_id for entry in registry) == CONDITION_IDS
def test_jpeg_q100_source_is_one_reusable_decode() -> None:
raw = encode_jpeg_q100(_image())
first = decode_jpeg_rgb(raw)
second = decode_jpeg_rgb(raw)
assert raw.startswith(b"\xff\xd8")
np.testing.assert_array_equal(first, second)
assert first.shape == (2, 2, 3)
def test_fp16_flat_materialization_preserves_bits_and_png_contract() -> None:
values = np.array([0.0, -0.0, 1.5, -2.25, np.float16("inf")], dtype=np.float16)
png = materialize_fp16_png(values)
pixels = np.asarray(Image.open(BytesIO(png)).convert("RGB"), dtype=np.uint8).reshape(-1, 3)
np.testing.assert_array_equal((pixels[: len(values), 0].astype(np.uint16) << 8) | pixels[: len(values), 1], values.view(np.uint16))
assert pixels[:, 2].tolist() == [0] * len(values)
def test_finite_repair_materialize_decode_preserves_original_uint16_bits() -> None:
original = np.array([0.0, -0.0, 1.5, -2.25, 3.125, -4.5], dtype=np.float16)
repaired = repair_nonfinite(original, "layer_mean", {"layer": (0, len(original))}).values
png = materialize_fp16_png(repaired)
pixels = np.asarray(Image.open(BytesIO(png)).convert("RGB"), dtype=np.uint8).reshape(-1, 3)
decoded_bits = (pixels[: len(original), 0].astype(np.uint16) << 8) | pixels[: len(original), 1].astype(np.uint16)
np.testing.assert_array_equal(decoded_bits, original.view(np.uint16))
def test_protected_high_png_is_exact_low_jpeg_only_changes() -> None:
values = np.array([0.0, -0.0, 1.5, -2.25, 3.125, -4.5], dtype=np.float16)
artifact = encode_protected(values, (2, 3))
decoded, metadata = decode_protected(artifact.payload, (2, 3))
np.testing.assert_array_equal(decoded.view(np.uint16), values.view(np.uint16))
high = np.asarray(Image.open(BytesIO(artifact.high_png_bytes)).convert("L"), dtype=np.uint8)
np.testing.assert_array_equal(high.reshape(-1), (values.view(np.uint16) >> 8).astype(np.uint8))
assert metadata["payload_size_bytes"] == len(artifact.high_png_bytes) + len(artifact.low_jpeg_bytes)
assert metadata["high_mode"] == metadata["low_mode"] == "L"
def test_repair_replaces_only_nonfinite_and_reports_signed_zero_bits() -> None:
raw = np.array([0.0, -0.0, np.nan, np.inf, -np.inf, 2.0], dtype=np.float16)
result = repair_nonfinite(raw, "layer_mean", {"layer": (0, len(raw))})
assert result.repaired_value_count == 3
assert result.nan_count == result.posinf_count == result.neginf_count == 1
assert np.isfinite(result.values).all()
np.testing.assert_array_equal(result.values.view(np.uint16)[[0, 1, 5]], raw.view(np.uint16)[[0, 1, 5]])
def test_zero_repair_accepts_all_nonfinite_layer_and_preserves_finite_bits() -> None:
raw = np.array([np.nan, np.inf, -np.inf, 1.5, -0.0, np.nan], dtype=np.float16)
original_bits = raw.view(np.uint16).copy()
result = repair_nonfinite(raw, "zero", {"all_nonfinite": (0, 3), "mixed": (3, 6)})
assert np.isfinite(result.values).all()
assert result.repaired_value_count == 4
assert result.nan_count == 2
assert result.posinf_count == 1
assert result.neginf_count == 1
assert [record.repaired_count for record in result.layer_records] == [3, 1]
assert [record.nan_count for record in result.layer_records] == [1, 1]
assert [record.posinf_count for record in result.layer_records] == [1, 0]
assert [record.neginf_count for record in result.layer_records] == [1, 0]
finite = np.isfinite(raw)
np.testing.assert_array_equal(result.values.view(np.uint16)[finite], original_bits[finite])
def test_repair_rejects_empty_finite_layer() -> None:
raw = np.array([np.nan, np.inf], dtype=np.float16)
with pytest.raises(ValueError, match="finite values"):
repair_nonfinite(raw, "layer_median", {"empty": (0, 2)})
@pytest.mark.parametrize("method", ["layer_median", "layer_mean"])
def test_repair_rejects_empty_finite_layer_for_statistical_methods(method: str) -> None:
raw = np.array([np.nan, np.inf, -np.inf], dtype=np.float16)
with pytest.raises(ValueError, match="finite values"):
repair_nonfinite(raw, method, {"empty": (0, 3)})
@pytest.mark.parametrize("strategy", CLIP_STRATEGIES)
def test_clip_strategies_use_candidate_only_and_record_changes(strategy: str) -> None:
candidate = np.array([-10.0, -1.0, 0.0, 1.0, 10.0], dtype=np.float16)
result = clip_candidate(candidate, {"layer": (0, 5)}, strategy)
assert result.values.dtype == np.float16
assert result.layers[0].strategy == strategy
assert result.layers[0].changed_count == int(np.count_nonzero(result.values.view(np.uint16) != candidate.view(np.uint16)))
assert np.isfinite(result.values).all()
assert result.layers[0].lower <= result.layers[0].upper
def test_clip_uses_population_std_and_linear_percentile() -> None:
candidate = np.array([0.0, 1.0, 2.0, 100.0], dtype=np.float16)
result = clip_candidate(candidate, {"layer": (0, 4)}, "layer_mean_std_6")
expected = float(np.mean(candidate.astype(np.float64)) + 6 * np.std(candidate.astype(np.float64), ddof=0))
assert result.layers[0].upper == pytest.approx(expected)
percentile = clip_candidate(candidate, {"layer": (0, 4)}, "layer_pct_0.1_99.9")
expected_percentile = float(np.percentile(candidate.astype(np.float64), 99.9, method="linear"))
assert percentile.layers[0].upper == pytest.approx(expected_percentile)
def test_clip_degenerate_and_threshold_equal_are_unchanged() -> None:
candidate = np.array([2.0, 2.0], dtype=np.float16)
result = clip_candidate(candidate, {"one": (0, 2)}, "layer_mean_std_5")
np.testing.assert_array_equal(result.values.view(np.uint16), candidate.view(np.uint16))
assert result.layers[0].changed_count == 0
def test_fp16_bit_diagnostics_and_bounded_histogram() -> None:
baseline = np.array([1.0, -1.0, 0.0, 1.0], dtype=np.float16)
candidate = np.array([20.0, -1.0, np.float16("inf"), np.float16("nan")], dtype=np.float16)
result = diagnostics(baseline, candidate, {"layer": (0, 4)})
assert result["sign_mismatch_count"] == 0
assert result["exponent_mismatch_count"] == 3
assert result["mantissa_mismatch_count"] == 2
assert result["exponent_to31_count"] == 2
assert result["finite_extreme_outlier_count"] == 1
assert set(result["histograms"]) == {"baseline", "candidate", "delta"}
assert all(len(item["counts"]) <= 64 for item in result["histograms"].values())
def test_registry_parent_identity_is_only_shared_by_raw_jpeg_conditions() -> None:
raw = encode_jpeg_q100(_image())
registry = build_jpeg_registry(raw)
assert len({entry.parent_raw_sha256 for entry in registry[1:17]}) == 1
assert registry[0].parent_raw_sha256 != registry[1].parent_raw_sha256
assert registry[17].parent_raw_sha256 != registry[1].parent_raw_sha256
assert registry[0].parent_raw_sha256 != registry[17].parent_raw_sha256
def test_layer_offsets_are_contiguous_and_fail_closed() -> None:
assert validate_layer_offsets({"a": (0, 2), "b": (2, 4)}, 4) == {"a": (0, 2), "b": (2, 4)}
with pytest.raises(ValueError, match="contiguous"):
validate_layer_offsets({"a": (0, 2), "b": (3, 4)}, 4)
with pytest.raises(ValueError, match="overlap"):
validate_layer_offsets({"a": (0, 3), "b": (2, 4)}, 4)
def test_diagnostics_extreme_uses_layer_baseline_max_and_reports_finite_stats() -> None:
baseline = np.array([1.0, 2.0, 0.0, 0.0], dtype=np.float16)
candidate = np.array([25.0, 2.0, 1.0, 0.0], dtype=np.float16)
result = diagnostics(baseline, candidate, {"first": (0, 2), "zero": (2, 4)})
assert result["finite_extreme_outlier_count"] == 2
assert result["finite_count"] == 4
assert result["max_abs_weight"] == pytest.approx(25.0)
assert result["nan_count"] == result["posinf_count"] == result["neginf_count"] == 0
assert result["layers"][0]["finite_extreme_outlier_count"] == 1
assert result["layers"][1]["finite_extreme_outlier_count"] == 1
assert result["layers"][1]["max_abs_weight"] == pytest.approx(1.0)
def test_repair_has_layer_records_and_bounded_repaired_histogram() -> None:
raw = np.array([np.nan, np.inf, -np.inf, 1.0], dtype=np.float16)
result = repair_nonfinite(raw, "zero", {"layer": (0, 4)})
record = result.layer_records[0]
assert record.layer == "layer"
assert record.repaired_count == 3
assert record.nan_count == record.posinf_count == record.neginf_count == 1
assert sum(result.repaired_histogram["counts"]) == 3
assert len(result.repaired_histogram["counts"]) <= 64
def test_clip_has_total_count_and_changed_value_histograms() -> None:
candidate = np.array([-100.0, -1.0, 0.0, 1.0, 100.0], dtype=np.float16)
result = clip_candidate(candidate, {"layer": (0, 5)}, "layer_mean_std_5")
assert result.total_clipped_count == result.layers[0].changed_count
assert sum(result.changed_pre_histogram["counts"]) == result.total_clipped_count
assert sum(result.changed_post_histogram["counts"]) == result.total_clipped_count
assert sum(result.changed_delta_histogram["counts"]) == result.total_clipped_count
def test_protected_metadata_reports_decoded_planes_and_low_changes() -> None:
rng = np.random.default_rng(20260727)
values = rng.integers(0, 65536, size=32 * 32, dtype=np.uint16).view(np.float16)
artifact = encode_protected(values, (32, 32))
decoded, metadata = decode_protected(artifact.payload, (32, 32), source_values=values)
assert metadata["decoded_high_exact"] is True
assert metadata["decoded_high_sha256"]
assert metadata["decoded_low_sha256"]
assert metadata["decoded_low_changed_count"] > 0
assert 0.0 < metadata["decoded_low_changed_rate"] <= 1.0
assert metadata["pillow_version"]
assert metadata["combined_payload_exact"] is True
assert metadata["high_mode"] == metadata["low_mode"] == "L"
assert metadata["high_dimensions"] == metadata["low_dimensions"] == [32, 32]
assert metadata["combined_payload_sha256"]
assert metadata["combined_payload_bytes"] == metadata["high_bytes"] + metadata["low_bytes"]
assert "jpeg_codec_version" in metadata
assert decoded.shape == values.shape
def test_combined_payload_parser_rejects_invalid_png_chunk_bounds() -> None:
values = np.arange(16, dtype=np.float16)
artifact = encode_protected(values, (4, 4))
broken = artifact.payload[:16] + b"\xff"
with pytest.raises(ValueError, match="PNG"):
decode_protected(broken, (4, 4))
|