"""DNG 定量モードの輝度化パイプライン.
DNG(RAW_SENSOR)を読み込み,LSC(レンズシェーディング)補正を順適用して
PNG 経路と同じ portrait・linear・Rec.709 輝度画像へ変換する一連の処理をまとめる.
- 読み込みは load_image_dng(output_format="rgb_float32", linear, output_color=raw)
- DNG は RAW_SENSOR で未補正のため,meta.json の lscMap でレンズビネットを順適用(任意)
- DNG はセンサ native が landscape のため,PNG と同じ portrait へ rot90(-1) で向き合わせ
"""
import json
from pathlib import Path
import numpy as np
from src.analysis.uniformity import (
REC709_COEFF_B,
REC709_COEFF_G,
REC709_COEFF_R,
)
from src.io.dng_loader import load_image_dng
def _upsample(grid: np.ndarray, shape: tuple[int, int]) -> np.ndarray:
"""(rows, cols) グリッドを画像サイズ shape へバイリニア補間する.
Args:
grid: 補間元グリッドの NumPy 配列(rows x cols, float64).
shape: 補間先の画像サイズ (height, width).
Returns:
補間後の NumPy 配列(height x width, float64).
"""
rows, cols = grid.shape
h, w = shape
ys = np.linspace(0, rows - 1, h)
xs = np.linspace(0, cols - 1, w)
y0 = np.floor(ys).astype(int)
y1 = np.minimum(y0 + 1, rows - 1)
x0 = np.floor(xs).astype(int)
x1 = np.minimum(x0 + 1, cols - 1)
wy = (ys - y0)[:, None]
wx = (xs - x0)[None, :]
top = grid[y0][:, x0] * (1 - wx) + grid[y0][:, x1] * wx
bot = grid[y1][:, x0] * (1 - wx) + grid[y1][:, x1] * wx
return top * (1 - wy) + bot * wy
def lsc_gain_rgb(meta_path: Path, shape: tuple[int, int]) -> np.ndarray:
"""meta.json の lscMap (4, M, N) を (H, W, 3) の RGB ゲインへ補間する.
4ch = [R, G_even, G_odd, B].G は G_even/G_odd の平均を用いる.
Args:
meta_path: meta.json ファイルのパス.
shape: 補間先の画像サイズ (height, width).
Returns:
RGB ゲインの NumPy 配列(H x W x 3, float64).
"""
meta = json.loads(meta_path.read_text(encoding="utf-8"))
rows, cols = meta["lscMapRowCount"], meta["lscMapColumnCount"]
m = np.array(meta["lscMap"], dtype=np.float64).reshape(rows, cols, 4)
gr = _upsample(m[:, :, 0], shape)
gg = _upsample((m[:, :, 1] + m[:, :, 2]) / 2.0, shape)
gb = _upsample(m[:, :, 3], shape)
return np.stack([gr, gg, gb], axis=2)
def dng_luminance(image_path: str, apply_lsc: bool) -> np.ndarray:
"""DNG から portrait・linear・(任意で LSC 補正済)の輝度画像を返す.
- output_format="rgb_float32"(output_color=raw, linear)で読み込み
- apply_lsc=True なら lscMap を順適用してレンズビネットを除去
- Rec.709 で輝度化(uniformity.py と同じ係数)
- PNG と同じ portrait へ rot90(-1) で向き合わせ
Args:
image_path: DNG ファイルのパス.
apply_lsc: True の場合,同名 .meta.json から LSC ゲインを読み込んで補正する.
Returns:
portrait 向きの輝度画像(H x W, float64).値域は 0〜255 の linear スケール.
"""
rgb = load_image_dng(image_path, output_format="rgb_float32") # (H,W,3) linear landscape
if apply_lsc:
meta_path = Path(image_path).with_suffix("").with_suffix(".meta.json")
if not meta_path.exists():
# ".meta.json" は二重拡張子なので with_suffix では拾えない場合がある
meta_path = Path(str(Path(image_path).with_suffix("")) + ".meta.json")
if not meta_path.exists():
raise FileNotFoundError(f"meta.json が見つかりません(LSC 補正に必要): {meta_path}")
rgb = rgb * lsc_gain_rgb(meta_path, rgb.shape[:2])
luma = (
REC709_COEFF_R * rgb[:, :, 0]
+ REC709_COEFF_G * rgb[:, :, 1]
+ REC709_COEFF_B * rgb[:, :, 2]
)
# linear のまま 0〜255 スケールで表現する(PNG 図と軸を揃えて比較するため).
# CoV・max/min・中心周辺比はスケール不変なので不変,mean/std のみ ×255 となる.
# 既存 plot_histogram が range=(0,255) 固定であることにも整合する.
luma = luma * 255.0
return np.rot90(luma, -1) # portrait(PNG と同じ向き)