"""sRGB OETF(光電変換関数)順方向適用モジュール.
linear radiance → sRGB 符号化空間への標準変換を提供する.
シミュの linear 値を実測 sRGB PNG と同じドメインへ揃えることで,
均一性指標の直接比較を正当化する(SPEC_03「手法訂正」参照).
無彩色シーン(LED・材料が RGB 等価)では luminance への直接 OETF 適用は
チャネル別適用後 Rec.709 luma と厳密一致する.これはシミュのシーンが
R=G=B の条件を満たすためであり,本モジュールはその前提を明記した上で
luminance への直接適用パスを提供する(参照実装: output/sweep_reflectance/apply_oetf.py).
将来カラー LED・色付き材料を扱う場合は,cli/evaluate.py での
「チャネル別 OETF → Rec.709 luma」順を使うこと(そちらが一般式).
"""
from __future__ import annotations
import numpy
def srgb_oetf(c: numpy.ndarray) -> numpy.ndarray:
"""sRGB OETF(IEC 61966-2-1).線形 [0, 1] → 符号化 [0, 1] へ変換する.
入力は [0, 1] にクリップしてから変換する.
トイ部(c ≤ 0.0031308)は線形 12.92c,それ以外は 1.055·c^(1/2.4) − 0.055.
Args:
c: 線形 radiance 配列.値域は任意(内部でクリップ).
Returns:
sRGB 符号化値(float64, 値域 [0, 1]).
References:
IEC 61966-2-1:1999 — sRGB
"""
c = numpy.clip(c, 0.0, 1.0)
lo = c <= 0.0031308
out = numpy.empty_like(c)
out[lo] = 12.92 * c[lo]
out[~lo] = 1.055 * numpy.power(c[~lo], 1.0 / 2.4) - 0.055
return out
def solve_exposure(
lin_roi: numpy.ndarray,
target_mean: float,
) -> tuple[float, float]:
"""OETF 後 ROI mean が target_mean になる線形スケールを対数二分で求める.
OETF は単調増加 → encode_with_exposure(lin_roi, scale).mean() も scale に
対して単調増加 → 二分法で一意解が存在する.
対数二分(初期区間 [1e-12, 1e6],200 回反復)で収束する.
実測 mean=146.71 は ROI 上の uint8 mean であるため,露出ソルバも
ROI 切り出し後の配列 lin_roi を渡すこと.
Args:
lin_roi: linear 輝度 ROI 配列(H x W, float64).
target_mean: 目標 ROI mean(uint8 スケール [0, 255]).
Returns:
(scale, clipped_fraction) のタプル.
scale: 線形ゲイン(lin_roi に掛ける値).
clipped_fraction: OETF 入力が 1.0 を超えるピクセルの割合 [0, 1].
"""
lo, hi = 1e-12, 1e6
for _ in range(200):
mid = (lo * hi) ** 0.5 # 対数二分(乗法的中点)
m = (srgb_oetf(lin_roi * mid) * 255.0).mean()
if m < target_mean:
lo = mid
else:
hi = mid
scale = (lo * hi) ** 0.5
clipped = float(numpy.mean(lin_roi * scale >= 1.0))
return scale, clipped
def apply_srgb_oetf(
lin: numpy.ndarray,
scale: float,
*,
quantize_uint8: bool = True,
) -> numpy.ndarray:
"""linear 配列に露出スケールを掛けて sRGB OETF を適用し,[0, 255] に変換する.
内部演算は float64 で行い,quantize_uint8=True のとき
``np.clip(np.round(x * 255), 0, 255)`` で uint8 量子化する(実測 PNG 同等).
Args:
lin: linear 輝度配列(任意次元 float64).
scale: 線形ゲイン(solve_exposure で取得した値を渡す).
quantize_uint8: True のとき uint8 量子化を適用し float64 で返す.
Returns:
OETF 適用後の配列(float64, quantize_uint8=True なら [0, 255] の整数値).
"""
encoded = srgb_oetf(lin * scale) * 255.0
if quantize_uint8:
encoded = numpy.clip(numpy.round(encoded), 0.0, 255.0)
return encoded