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),