diff --git "a/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" "b/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" index e99276e..7f65f1b 100644 --- "a/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" +++ "b/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" @@ -85,7 +85,7 @@ - raw: 10-bit(整数 0〜1023) - 黒レベル: 64(`SENSOR_BLACK_LEVEL_PATTERN`)→ 除去後 0〜959 - 飽和警告のめやす: 値 > 900 -- デモザイク既定: バイリニア(`rawpy.DemosaicAlgorithm.LINEAR`).`demosaic` 引数で AHD/DHT に切替可 +- デモザイク既定: バイリニア(`rawpy.DemosaicAlgorithm.LINEAR`).`demosaic` 引数で `nearest`(LINEAR 代替)・`ahd`(AHD)に切替可.ただし AHD は非線形で量的評価では非推奨(`dng_loader.py` 参照) - カラーマトリクスは DNG Exif(`ColorMatrix1/2`)に埋め込み済み(rawpy が参照).ただし linear 評価経路では `output_color=raw`(非適用)で読む - linear ドメイン(TONEMAP 線形・sRGB OETF 未適用).輝度化の詳細は [SPEC_02](SPEC_02_照明均一性評価アルゴリズム.md) 「輝度変換」を参照 diff --git a/src/analysis/reproducibility.py b/src/analysis/reproducibility.py index c6a1d03..bbae12e 100644 --- a/src/analysis/reproducibility.py +++ b/src/analysis/reproducibility.py @@ -7,6 +7,43 @@ from src.analysis.registration import apply_shift, compute_valid_mask, phase_correlate +# Rec.709 輝度(uint8 RGB → float64)の取りうる範囲は 0.0〜255.0 +_DATA_RANGE = 255.0 + + +def _validate_luminance_maps(luminance_maps: list[np.ndarray]) -> None: + """輝度マップが SSIM 算出に必要な枚数(2 枚以上)あるか検証する. + + Args: + luminance_maps: 輝度マップのリスト. + + Raises: + ValueError: 輝度マップが 2 枚未満の場合. + """ + if len(luminance_maps) < 2: + raise ValueError( + f"輝度マップは 2 枚以上必要です(現在: {len(luminance_maps)} 枚)." + ) + + +def _aggregate_scores(scores: np.ndarray, prefix: str) -> dict: + """スコア配列から mean/min/max/std を float で集約した辞書を返す. + + Args: + scores: 集約対象のスコア配列. + prefix: 辞書キーの接尾辞(例 "ssim" → "mean_ssim" 等). + + Returns: + {f"mean_{prefix}": ..., f"min_{prefix}": ..., + f"max_{prefix}": ..., f"std_{prefix}": ...} 形式の辞書. + """ + return { + f"mean_{prefix}": float(np.mean(scores)), + f"min_{prefix}": float(np.min(scores)), + f"max_{prefix}": float(np.max(scores)), + f"std_{prefix}": float(np.std(scores, ddof=0)), + } + def calc_pairwise_ssim(luminance_maps: list[np.ndarray]) -> dict: """複数の輝度マップ間のペアワイズ SSIM を算出する. @@ -28,13 +65,7 @@ Raises: ValueError: 輝度マップが 2 枚未満の場合. """ - if len(luminance_maps) < 2: - raise ValueError( - f"輝度マップは 2 枚以上必要です(現在: {len(luminance_maps)} 枚)." - ) - - # Rec.709 輝度(uint8 RGB → float64)の取りうる範囲は 0.0〜255.0 - data_range = 255.0 + _validate_luminance_maps(luminance_maps) pairwise_scores: list[dict] = [] for idx_a, idx_b in combinations(range(len(luminance_maps)), 2): @@ -42,7 +73,7 @@ ssim( luminance_maps[idx_a], luminance_maps[idx_b], - data_range=data_range, + data_range=_DATA_RANGE, ) ) pairwise_scores.append({"image_a": idx_a, "image_b": idx_b, "ssim": score}) @@ -51,10 +82,7 @@ return { "pairwise_scores": pairwise_scores, - "mean_ssim": float(np.mean(scores)), - "min_ssim": float(np.min(scores)), - "max_ssim": float(np.max(scores)), - "std_ssim": float(np.std(scores, ddof=0)), + **_aggregate_scores(scores, "ssim"), "n_pairs": len(pairwise_scores), } @@ -92,13 +120,7 @@ Raises: ValueError: 輝度マップが 2 枚未満の場合. """ - if len(luminance_maps) < 2: - raise ValueError( - f"輝度マップは 2 枚以上必要です(現在: {len(luminance_maps)} 枚)." - ) - - # Rec.709 輝度(uint8 RGB → float64)の取りうる範囲は 0.0〜255.0 - data_range = 255.0 + _validate_luminance_maps(luminance_maps) pairwise_scores: list[dict] = [] for idx_a, idx_b in combinations(range(len(luminance_maps)), 2): @@ -106,7 +128,7 @@ img_b = luminance_maps[idx_b] # 1. 位置合わせなし SSIM - score_raw = float(ssim(img_a, img_b, data_range=data_range)) + score_raw = float(ssim(img_a, img_b, data_range=_DATA_RANGE)) # 2. 位相相関でシフト推定(img_b を img_a に合わせる) dy, dx = phase_correlate(img_a, img_b) @@ -129,7 +151,7 @@ rows[0] : rows[-1] + 1, cols[0] : cols[-1] + 1 ] score_registered = float( - ssim(img_a_cropped, img_b_cropped, data_range=data_range) + ssim(img_a_cropped, img_b_cropped, data_range=_DATA_RANGE) ) else: score_registered = score_raw @@ -152,14 +174,8 @@ return { "pairwise_scores": pairwise_scores, - "mean_ssim": float(np.mean(scores)), - "min_ssim": float(np.min(scores)), - "max_ssim": float(np.max(scores)), - "std_ssim": float(np.std(scores, ddof=0)), - "mean_ssim_registered": float(np.mean(scores_reg)), - "min_ssim_registered": float(np.min(scores_reg)), - "max_ssim_registered": float(np.max(scores_reg)), - "std_ssim_registered": float(np.std(scores_reg, ddof=0)), + **_aggregate_scores(scores, "ssim"), + **_aggregate_scores(scores_reg, "ssim_registered"), "mean_shift_magnitude": float(np.mean(shifts)), "max_shift_magnitude": float(np.max(shifts)), "n_pairs": len(pairwise_scores), diff --git a/src/io/dng_loader.py b/src/io/dng_loader.py index 1a20f69..cd5b6d4 100644 --- a/src/io/dng_loader.py +++ b/src/io/dng_loader.py @@ -163,7 +163,7 @@ **_postprocess_common, output_color=rawpy.ColorSpace.sRGB, ) - rgb_float = rgb16.astype(numpy.float32) / 65535.0 + rgb_float = rgb16.astype(numpy.float32) / _UINT16_MAX raw.close() rgb_srgb = _apply_srgb_oetf(rgb_float)