fullseye

ライトフィールド(plenoptic 撮像・リフォーカス・深度) — 使い方ガイド

この族は何をする道具箱か

マイクロレンズアレイ(MLA)を撮像素子の前に置くと何が計算できるようになるか、の族です。各マイクロレンズが主レンズの射出瞳を自分の画素ブロックへ結像するので、1 回の露光が「どこに何 lx 届いたか」だけでなく「どの方向から届いたか」まで記録します。それが 4-D のライトフィールド L(v, u, y, x) で、画素を並べ替えるだけで少しずつ視点のずれた画像のグリッドが出てきます。そこから、単一センサ・単一ショットで: 2D 画像 / 後から合焦し直せる焦点スタック / 絞りを変えられる(そして遮蔽物を透かして撮れる)合成開口 / 密な深度マップ、が全部出ます。single-shot depth はマシンビジョンで活発な領域ですが、その基礎になる演算が fullseye には 1 つもありませんでした — light_field / plenoptic / refocus / sub_aperture / microlens / epi は全 op 名でヒット 0(2026-09-01 実測)。17 op / 5 カテゴリ(numpy + scipy のみ、台帳は opslightfield.py、実体は lightfield.py):

データ種は既存語彙の再利用が基本です: image2d(サブアパーチャ画像・中心視点・EPI・リフォーカス像・開口マスク・スロープ地図)、images(焦点スタック・視点リスト = 既存の多画像 op へ無変換で流れる)、depth(lf_disparity_to_depth の返りだけ)、table(lf_stats / lf_plenoptic_design の dict)。スロープ地図を depth と宣言していないのは意図的で、中身は px/視点であって距離ではないため、depth を名乗ると下流の距離 op に単位違いを渡せてしまいます。距離を名乗るのは metric 換算した後だけです。

新語は 1 つだけ、lightfield(4-D の (V, U, H, W) 実配列)。角度 2 軸 + 空間 2 軸で、角度がグリッドであることが全 op の前提です(u と v の両方向で視差を取る / 開口マスクが (V, U) の 2-D / EPI が角度 1 軸と空間 1 軸の断面)。images(2-D の list)へ潰すと「どの視点か」が消えて refocus も EPI も定義できなくなり(lf_views がまさにその潰す操作)、voxel は 3-D、pointmap(H, W, 3) 固定なので、4-D を表す既存語彙が台帳に 1 つも無く新設しました。

規約(ここを間違えると「もっともらしく間違った絵」が出ます)

既存 op との棲み分け(重複させていないもの)

やりたいこと 使う op 置き場所
レンズ・絞り・被写界深度の算術(薄レンズ結像、許容錯乱円ベースの深度) thin_lens / depth_of_field optics再実装せず呼んでいますlf_plenoptic_designdepth_of_field2 回呼び(錯乱円 = 画素ピッチ / MLA ピッチ)、その比がリフォーカスゲインです
2 眼の視差・SGM・LR 整合 disparity_map / disparity_census / disparity_sgm / depth_from_disparity / lr_consistency stereo。ライトフィールドは「2 台のカメラ」ではなく角度グリッド全体を同時に使う(それが遮蔽ロバスト性と subpixel 視差の出所)。2 視点しか無いなら stereo の方が適任
実カメラを物理的に N 回合焦し直して融合する focus_stack の一式 focus_stacklf_focal_stack は同じものを単一露光から計算で作り、返りが素の 2-D 画像 list なので focus_stack の融合機構がそのまま乗ります
深度マップ → 点群 → 3-D フィット・位置合わせ depth_to_points / ICP / RANSAC 一式 match3d / pointcloud / ransac_fit。この族は metric depth map で止めます
汎用の鮮鋭度・Laplacian・局所分散フィルタ ops / filters_freq の各 op ops / filters_freqlf_depth_from_focus の焦点尺度は意図的に private ヘルパにしてあり、新しい公開 sharpness op は増やしていません
FFT・複素画像・位相アンラップ cx_fft 系 / phase_unwrap complexops

ファミリ共通の入力契約(fail-closed)

全 op が入力を検証してから計算します。以下は 2026-09-01 の敵対監査で実際に見つかったバグか、それを塞ぐために書いた罠です。

この契約は連鎖ファザーでも確認済みで、1200 連鎖 × 長さ 6(seed 7001)で lightfield 族 17/17 が到達し、走行全体の 364 件の発見はすべて CONTRACT(= docstring に書いた ValueError 契約どおりの拒否)、クラッシュ・型不整合はゼロでした。

代表的なパイプライン(op の繋がり)

センサ生データから深度まで通す主筋(検証済み examples/lightfield_depth.py そのもの)。データ種が image2d → lightfield → images/image2d → depth で繋がります。

flowchart LR
    A[画素ピッチ・MLA ピッチ・f 値] --> B[lf_plenoptic_design 角度/空間分解能・基線]
    B -.optics.thin_lens / depth_of_field を呼ぶ.-> B
    C[MLA 生画像 image2d] --> D[lf_from_mla 並べ替え]
    D -->|lightfield| E[lf_stats 形状・測れる最大スロープ]
    D --> F[lf_center_view 2D 画像 image2d]
    D --> G[lf_focal_stack スロープ掃引 images]
    G --> H[lf_depth_from_focus 鮮鋭度ピーク image2d]
    D --> I[lf_epi_slope EPI 傾き 1 パス image2d]
    H --> J[lf_disparity_to_depth Z=f_px*b/|s| depth]
    I --> J
    H --> K[lf_all_in_focus 全焦点 image2d]
    G --> K
    B -->|focal_px・baseline| J

合成開口の筋。開口マスクで被写界深度を制御し、縮約を median に替えると遮蔽物の裏が出ます。lf_synthesize は同じ光場を既知の答えつきで作れるので、上の筋の検算に使えます。

flowchart LR
    S[lf_synthesize 既知スロープ + 遮蔽] -->|lightfield| T[lf_refocus 素の shift-and-add]
    M[lf_aperture_mask circle/square/gaussian/annulus] -->|image2d V,U| U
    S -->|lightfield| U[lf_synthetic_aperture 開口整形 + 縮約]
    U -->|reduce=mean| V[絞るほど深度が伸びる]
    U -->|reduce=median| W[遮蔽の裏の背景を復元]
    S -.スロープ地図 image2d.-> X{推定と突き合わせ}
    T --> X
    S -->|lightfield| Y[lf_epi 角度x空間の断面 image2d]
    Y --> Z[直線の傾き = 視差 の目視確認]

使い方(最小の 1 本)

import numpy as np
import lightfield as L

# 0) カメラを設計する(optics を再実装せず呼ぶ)
d = L.lf_plenoptic_design(focal_mm=50.0, f_number=8.0, object_mm=300.0,
                          pixel_um=3.45, mla_pitch_um=27.6,
                          sensor_px=(2048, 2448))
print(d["angular_u"], d["refocus_gain"])        # 8, 8.0038 = 角度分解能

# 1) 既知スロープ 2.0 px/視点 の場を合成 → MLA 生画像 → 復号(往復はビット一致)
lf, truth = L.lf_synthesize((2.0,), angular=(9, 9), shape=(64, 64),
                            occlusion=False, texture_sigma=3.0,
                            edge="wrap", seed=0)
assert np.array_equal(L.lf_from_mla(L.lf_to_mla(lf), (9, 9)), lf)

# 2) 掃引して鮮鋭度ピークから深度を出す(argmax は真値ちょうど)
sweep = np.round(np.linspace(-4.0, 4.0, 81), 6)
slope, conf = L.lf_depth_from_focus(lf, sweep, edge="wrap", subpixel=False)
print(float(np.median(slope)))                  # 2.0

# 3) metric 深度へ。視差ゼロ(無限遠)は無言の inf ではなく ValueError
f_px = 50.0 / 27.6e-3                           # サブアパーチャ画素 = MLA ピッチ
z = L.lf_disparity_to_depth(slope, focal_px=f_px, baseline=d["baseline_mm"])
print(float(np.median(z)))                      # 808.747 mm = f_px*b/2.0

# 4) 中心視点(= 普通の 2D 画像)と、その面に合焦した合成開口像
plain = L.lf_center_view(lf)
sharp = L.lf_refocus(lf, 2.0, edge="wrap")      # plain と 2.0e-15 以内で一致

実測値(このガイドの主張の根拠)

すべて 2026-09-01 に本モジュールで測った値です。再現は tests/test_lightfield.py(125 件)と examples/lightfield_depth.py(通しで PASS / 終了コード 0)。

主張 実測
MLA 生画像 ⇄ 光場の往復 (1,1) / (2,3) / (5,5) / (3,7) / (9,9) のすべてで np.array_equal が True(ビット一致)
整数スロープ + edge="wrap" のリフォーカスは元テクスチャに戻る 5x5(25 視点)で最大差 5.6e-16、9x9(81 視点)で 2.0e-15(視点数ぶんの加算誤差)
EPI の端視点間ラグ = s·(U-1) 9x9 で s=1.0 → 実測 8 px / 理論 8 px、s=2.0 → 実測 16 px / 理論 16 px
lf_depth_from_focus の argmax は真値ちょうど 121 点掃引 × (真値 0.0 / +0.5 / +1.0 / +1.5 / +2.0 / -1.0)×(テクスチャ σ = 1.5 / 3.0 / 5.0)= 18/18 で厳密一致。サブピクセル補正もこの 18 件を動かさない
lf_epi_slope は速いが \|s\| > 1 で過小評価 σ=1.5: +1.0→+1.0004 / +0.5→+0.5285 / +1.5→+1.3018 / +2.0→+1.4614(26.9% 過小)
σ=3.0: +1.0→+1.0001 / +2.0→+1.8716
σ=5.0: +1.0→+1.0003 / +2.0→+1.9482(2.6% 過小)。s=0 は全 σ で厳密 0、符号は常に正しい
median 合成開口は遮蔽の裏を厳密に復元する 9x9、遮蔽が中心視点の 25% を覆い、隠れ画素で塞がる視点が最大 46%(過半未満)のとき、隠れた真背景との RMS は median 0.0000 / mean 0.1590 / 中心視点 0.5607
ただし過半を超えると保証は消える 被覆 35%(最大 60% の視点が塞がる)で median 0.1329 / mean 0.2128。median が mean より良いという話ではなく、多数決が覆るかどうかの話です
リフォーカスゲイン = 角度分解能(教科書結果) f=50 mm, f/8, 物体 300 mm で MLA 20.7 µm → 角度 6・ゲイン 6.0016、27.6 µm → 8・8.0038、34.5 µm → 10・10.0075
角度分解能は空間分解能で買う 2048x2448 センサが、角度 6 で 341x408 / 角度 8 で 256x306 / 角度 10 で 204x244 に。被写界深度は 1.656 mm → それぞれ 9.939 / 13.254 / 16.573 mm
絞ると非合焦面のボケが減る 合焦面から外れた面での残存コントラスト(分散)が 全開 0.0049 → 半径 1 に絞って 0.0264
全焦点合成はどの単一スライスより鋭い 2 層シーン(手前が 40% を遮蔽)で 勾配エネルギー 0.00574 対 最良スライス 0.00490(1.17 倍、最悪スライスは 0.00031)
metric 換算は閉形式 f_px=1811.6 px, b=0.8929 mm, s=2.0 → 808.747 mm(f_px·b/\|s\| と機械精度で一致)
連鎖ファザーで全 op が到達可能 1200 連鎖 × 長さ 6(seed 7001)で lightfield 17/17 到達、走行全体の発見 364 件はすべて CONTRACT

アルゴリズムの正典(著者・年)

正直な限界


© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.