"""3 ゾーン空間分析モジュール.
ROI を中心・中間・周辺の 3 同心楕円ゾーンに分割し,各ゾーンの輝度統計を算出する.
References:
[1] MiniTias-Evaluation SPEC_02 「空間分析」節
https://github.com/rintoHasegawa/MiniTias-Evaluation
"""
import numpy
def compute_normalized_distance_map(height: int, width: int) -> numpy.ndarray:
"""ROI の各ピクセルについて,中心からの正規化楕円距離マップを算出する.
正規化楕円距離 $d(x, y)$ を以下の式で定義する [1]:
$$d(x, y) = \\sqrt{
\\left(\\frac{x - c_x}{c_x}\\right)^2 + \\left(\\frac{y - c_y}{c_y}\\right)^2
}$$
ここで $c_x = W/2$, $c_y = H/2$ は ROI の中心座標.
楕円距離を用いることで,縦横比が異なる ROI でも等方的な評価が可能となる.
算出後,値域を $[0, 1]$ に再正規化する($d \\leftarrow d / d_{\\max}$).
Args:
height: ROI の高さ(ピクセル数).
width: ROI の幅(ピクセル数).
Returns:
正規化距離マップ(shape: (height, width), dtype: numpy.float64).
値域は [0, 1].
"""
cx = width / 2.0
cy = height / 2.0
# 各ピクセル座標をグリッドで生成する(ブロードキャストのため列ベクトル・行ベクトル)
ys = numpy.arange(height, dtype=numpy.float64).reshape(-1, 1)
xs = numpy.arange(width, dtype=numpy.float64).reshape(1, -1)
distance_map = numpy.sqrt(((xs - cx) / cx) ** 2 + ((ys - cy) / cy) ** 2)
# d_max で除算して [0, 1] に正規化する
d_max = float(numpy.max(distance_map))
if d_max > 0.0:
distance_map = distance_map / d_max
return distance_map
def classify_zones(
distance_map: numpy.ndarray,
boundaries: tuple[float, float] = (0.33, 0.66),
) -> numpy.ndarray:
"""正規化距離マップを 3 ゾーンのラベルマップに変換する.
SPEC_02 の 3 ゾーン分類に従い,以下の閾値でラベルを割り当てる [1]:
| ゾーン | ラベル | 距離条件 |
|------------|--------|---------------------------------------|
| Center | 0 | $d < \\text{boundaries}[0]$ |
| Middle | 1 | $\\text{boundaries}[0] \\leq d < \\text{boundaries}[1]$ |
| Periphery | 2 | $d \\geq \\text{boundaries}[1]$ |
Args:
distance_map: 正規化距離マップ(shape: (H, W), dtype: float).
boundaries: ゾーン境界の閾値ペア (lower, upper).デフォルトは (0.33, 0.66).
Returns:
ゾーンラベルマップ(shape: (H, W), dtype: numpy.int8).
値は 0(center), 1(middle), 2(periphery)のいずれか.
"""
lower, upper = boundaries
zone_map = numpy.full(distance_map.shape, 2, dtype=numpy.int8)
zone_map[distance_map < lower] = 0
zone_map[(distance_map >= lower) & (distance_map < upper)] = 1
return zone_map
def calc_zone_stats(
luminance: numpy.ndarray,
zone_map: numpy.ndarray,
) -> dict:
"""各ゾーンの輝度統計と空間分析指標を算出する.
ゾーン別に平均輝度・標準偏差を算出し,中心/周辺比と勾配量 (%) を導出する [1]:
$$\\text{C/P Ratio} = \\frac{\\mu_{center}}{\\mu_{periphery}}$$
$$\\text{Gradient} = \\frac{\\mu_{center} - \\mu_{periphery}}{\\mu_{center}} \\times 100$$
Args:
luminance: 輝度画像(shape: (H, W), dtype: float64).
zone_map: ゾーンラベルマップ(shape: (H, W), dtype: int8).
0=center, 1=middle, 2=periphery.
Returns:
空間分析指標を格納した辞書.以下の 8 キーを含む:
- center_mean: 中心ゾーンの平均輝度
- center_std: 中心ゾーンの標準偏差
- middle_mean: 中間ゾーンの平均輝度
- middle_std: 中間ゾーンの標準偏差
- periphery_mean: 周辺ゾーンの平均輝度
- periphery_std: 周辺ゾーンの標準偏差
- center_periphery_ratio: 中心/周辺比 (μ_center / μ_periphery)
- gradient_magnitude: 中心-周辺勾配量 (%)
Raises:
ValueError: 周辺ゾーンの平均輝度が 0 の場合(ROI 設定の確認を促す).
"""
center_pixels = luminance[zone_map == 0]
middle_pixels = luminance[zone_map == 1]
periphery_pixels = luminance[zone_map == 2]
center_mean = float(numpy.mean(center_pixels))
center_std = float(numpy.std(center_pixels))
middle_mean = float(numpy.mean(middle_pixels))
middle_std = float(numpy.std(middle_pixels))
periphery_mean = float(numpy.mean(periphery_pixels))
periphery_std = float(numpy.std(periphery_pixels))
# 周辺輝度が 0 の場合は中心/周辺比が計算不能(ROI 設定の誤りを示す)
if periphery_mean == 0.0:
raise ValueError(
"周辺ゾーンの平均輝度が 0 のため中心/周辺比を計算できません."
"ROI の設定を確認してください."
)
center_periphery_ratio = center_mean / periphery_mean
gradient_magnitude = (center_mean - periphery_mean) / center_mean * 100.0
return {
"center_mean": center_mean,
"center_std": center_std,
"middle_mean": middle_mean,
"middle_std": middle_std,
"periphery_mean": periphery_mean,
"periphery_std": periphery_std,
"center_periphery_ratio": center_periphery_ratio,
"gradient_magnitude": gradient_magnitude,
}