"""放射状輝度プロファイルモジュール.
ROI の中心からの正規化楕円距離を等分割したビンごとに平均輝度を集計し,
中心-周辺方向の輝度変化を連続的に把握するためのプロファイルを生成する.
References:
[1] SmTIAS-Evaluation SPEC_02 「放射状輝度プロファイル」節
https://github.com/rintoHasegawa/SmTIAS-Evaluation
"""
import numpy
from sm_tias_lightsim.evaluation.zones import compute_normalized_distance_map
def calc_radial_profile(
luminance: numpy.ndarray,
n_bins: int = 20,
) -> numpy.ndarray:
"""輝度画像から放射状輝度プロファイルを算出する.
正規化楕円距離 $d \\in [0, 1]$ を `n_bins` 等分のビンに分割し,各ビン内の
平均輝度を集計する [1].最外ビンは $d = 1.0$ の画素を欠損させないよう上端を含める.
Args:
luminance: 輝度画像(shape: (H, W), dtype: float64).
n_bins: ビン数.デフォルトは 20.
Returns:
放射状プロファイル配列(shape: (n_bins, 2), dtype: numpy.float64).
列 0: ビン中心距離(ビン両端の中点).
列 1: そのビンの平均輝度.空ビンは numpy.nan.
"""
height, width = luminance.shape
distance_map = compute_normalized_distance_map(height, width)
# [0, 1] を n_bins 等分するビン境界を生成する
bin_edges = numpy.linspace(0.0, 1.0, n_bins + 1)
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2.0
# numpy.digitize でビンインデックスを割り当てる(1 始まり, 右端開区間)
# 最外ビン(d == 1.0)が n_bins + 1 に割り振られないよう right=True を使用する
bin_indices = numpy.digitize(distance_map.ravel(), bin_edges, right=True)
flat_luminance = luminance.ravel()
mean_luminance = numpy.full(n_bins, numpy.nan, dtype=numpy.float64)
for i in range(1, n_bins + 1):
mask = bin_indices == i
if numpy.any(mask):
mean_luminance[i - 1] = float(numpy.mean(flat_luminance[mask]))
# 空ビンは初期値 nan のまま維持する
profile = numpy.column_stack([bin_centers, mean_luminance])
return profile
def calc_radial_min_max_ratio(radial_profile: numpy.ndarray) -> dict:
"""動径プロファイルの最小/最大輝度比を算出する.
SmTIAS-Evaluation の ``src/analysis/spatial.py:calc_radial_min_max_ratio`` を
SmTIAS-LightSim にマージしたもの.
``calc_radial_profile`` で得た N 点の輪帯平均輝度のうち,最小値を最大値で割った比
(0〜1)を返す.1 に近いほど均一で,中心明・周辺暗の単調減衰では
「最外輪帯 / 最内輪帯」に一致する.全 N 点の min/max を使うため,
非単調プロファイル(例: 中央暗・周辺明)にもロバストに対応する.
``nan`` ビン(空ビン)は無視する.
References:
SmTIAS-Evaluation ``src/analysis/spatial.py:calc_radial_min_max_ratio``
[TEST_05](docs/06_TEST/TEST_05_PNG_DNG評価比較.md) — 新指標の物理的意味
Args:
radial_profile: 放射状プロファイル配列(shape: (N, 2), dtype: numpy.float64).
列 0 = ビン中心距離, 列 1 = 平均輝度.``calc_radial_profile`` の戻り値を渡す.
空ビン(nan)は算出から除外される.
Returns:
動径 min/max 比を格納した辞書.キーは:
- ``"radial_min_max_ratio"`` (float): 最小輝度 / 最大輝度(0〜1).
最大輝度が 0 の場合は ``float("inf")``.
- ``"radial_min_distance"`` (float): 最小輝度ビンの正規化距離.
- ``"radial_min"`` (float): プロファイルの最小輝度値(nan 除外後).
- ``"radial_max"`` (float): プロファイルの最大輝度値(nan 除外後).
"""
luminances = radial_profile[:, 1]
distances = radial_profile[:, 0]
# 空ビン(nan)を除外して集計する
valid_mask = ~numpy.isnan(luminances)
valid_luminances = luminances[valid_mask]
valid_distances = distances[valid_mask]
if valid_luminances.size == 0:
# 有効ビンがない場合は nan で返す
return {
"radial_min_max_ratio": float("nan"),
"radial_min_distance": float("nan"),
"radial_min": float("nan"),
"radial_max": float("nan"),
}
radial_min = float(numpy.min(valid_luminances))
radial_max = float(numpy.max(valid_luminances))
# max が 0 のときはゼロ除算を回避して inf を返す(zones.py の既存パターンに倣う)
radial_min_max_ratio = radial_min / radial_max if radial_max != 0.0 else float("inf")
# 最小輝度をとるビンの正規化距離(補助情報)
min_idx = int(numpy.argmin(valid_luminances))
radial_min_distance = float(valid_distances[min_idx])
return {
"radial_min_max_ratio": radial_min_max_ratio,
"radial_min_distance": radial_min_distance,
"radial_min": radial_min,
"radial_max": radial_max,
}