원뿔 조명(Cone Illumination)
실제 카메라 시스템에서 각 픽셀에 도달하는 빛은 단일 방향이 아닌 렌즈 사출 동공(Exit Pupil)의 전체 영역에서 옵니다. ConeIllumination 클래스는 조명 원뿔을 가중 평면파(Planewave)로 분해하고 결과를 적분하여 이를 모델링합니다. 이 가이드는 실용적·코드 중심의 워크스루입니다.
원뿔 파라미터
인터랙티브 콘 조명 뷰어
CRA(주광선 각도)와 콘 반각이 픽셀 조명에 미치는 영향을 시각화합니다. 콘은 방향별 평행 광선 묶음들로 표현되며, 각 묶음은 마이크로렌즈와 고굴절률 스택을 통과하면서 스넬 법칙에 따라 굴절되어 방향별 초점으로 수렴합니다.
원뿔은 세 가지 제어값으로 기술됩니다: CRA(픽셀까지의 주광선각), F 넘버(
컬러 필터 관점에서 cone illumination이 중요한 이유
얇은 막 컬러 필터(또는 스택 내부의 Fabry-Pérot 공진기)는 경사 입사 시 투과 피크가 더 짧은 파장 쪽으로 이동합니다 — 대략
인터랙티브: Fabry-Perot 원뿔 적분
아래 시뮬레이터는 Goossens 외 (2018)의 핵심 결과를 페이지 안에서 직접 재현합니다. CRA와 F 넘버를 조정해 보면, 적분된 투과 곡선이 평면파 곡선에 비해 단파장 쪽으로 이동하고 동시에 broadening되는 모습을 볼 수 있습니다. 파장 윈도와 샘플 수는 코드에서 ConeIllumination(cra_deg=..., f_number=..., n_points=...)를 통해 그대로 제어할 수 있습니다.
파브리-페로 원뿔 조명 시뮬레이터
Goossens et al. (2018), Appl. Opt. 57(26):7539의 핵심 결과를 재현합니다. 이미지 센서 위 단일 공진 Fabry-Perot 필터에 초점 광선이 입사할 때, 주광선 각도(CRA)와 F 넘버로 정의되는 원뿔에 대해 Airy 투과율을 적분하고 평면파 결과와 비교합니다.
원뿔 샘플 (위에서 본 모습, 센서 좌표계)
모델 설명
단일 공진 Fabry-Perot 필터의 Airy 투과율은 다음과 같습니다
T(λ, θ) = 1 / [1 + F · sin²(δ(λ, θ) / 2)], 여기서 δ = (4π neff d cosθint) / λ, F = 4R / (1 − R)², neff sinθint = sinθ.
공진층 두께 d는 수직 입사에서 λ₀가 1차 피크에 오도록 d = λ₀ / (2 n_eff)로 고정됩니다. 각 원뿔 샘플에서 주광선 각도만큼 y-축으로 회전시켜 필터 법선에 대한 실제 입사각 θ를 구하고, 반원뿔 θ_h = arcsin(1 / 2F) 위에서 피보나치 나선으로 샘플링한 후 cosθ 가중치(aplanatic pupil)로 평균합니다.
원뿔 적분 투과율은 다음과 같습니다
Tcone(λ) = ∑i wi T(λ, θi) / ∑i wi.
이 두 가지 결과 — centroid의 blue-shift(원뿔 평균 cosθ_int가 주광선 값보다 작기 때문)와 투과 피크의 broadening(각 샘플이 서로 다른 파장에서 피크를 가지기 때문) — 가 Goossens et al. (2018)에서 강조한 핵심 효과입니다.
참고문헌: T. Goossens et al., 집적 박막 Fabry-Perot 필터 분광 카메라의 유한 조리개 보정, Appl. Opt. 57(26):7539 (2018). DOI: 10.1364/AO.57.007539.
상면도: 픽셀 어레이 위의 풋프린트
콘 조명 – 상면도
2×2 베이어 픽셀 배열에 대한 콘 조명의 상면도입니다. CRA, f-넘버, 샘플링을 조정하여 조명 풋프린트가 픽셀을 어떻게 덮는지 확인하세요.
권장 기본값: 황금각 기반으로 ring artifact 없이 준균일하게 덮습니다.
위의 측면도는 원뿔 기하학을 단면으로 보여줍니다. **상면도(Top View)**는 보완적인 관점을 제공합니다: 픽셀 어레이를 위에서 내려다보면, 조명 원뿔이 2x2 Bayer 패턴에 어떻게 투영되는지 확인할 수 있습니다.
상면도에서의 핵심 관찰 사항:
- 풋프린트 직경: 초점면에서의 원뿔 풋프린트 직경은
이며, 여기서 는 원뿔 확산에 사용하는 유효 전파 높이입니다. F 넘버가 낮을수록 더 넓은 풋프린트가 생성됩니다. - CRA 시프트: CRA가 0이 아닌 경우 풋프린트 중심이 픽셀 중심에서 벗어납니다. 실제 BSI 스택에서는 단순한 공기 중 경로 값
를 그대로 쓰지 않습니다. 컬러 필터, BARL, 실리콘의 굴절 때문에 주광선이 법선 방향으로 꺾이므로 유효 시프트는 더 작습니다. - 샘플링 커버리지: 위의 인터랙티브 뷰어에서 피보나치, 동일 면적 ring, Halton/Hammersley low-discrepancy set, Gauss-Legendre, legacy polar grid 샘플링을 비교할 수 있습니다.
grid는 기준 비교에는 유용하지만, 보통 production 선택지로는 적합하지 않습니다. - 렌즈 면적: 풋프린트 면적
(여기서 )는 인접 픽셀이 원뿔로부터 얼마나 많은 빛을 받는지를 결정하며, 이는 크로스토크에 직접적으로 영향을 미칩니다.
ConeIllumination 인스턴스 생성
from compass.sources.cone_illumination import ConeIllumination
cone = ConeIllumination(
cra_deg=15.0, # Chief Ray Angle in degrees
f_number=2.0, # F-number of the lens
n_points=37, # Number of angular sample points
sampling="fibonacci", # "fibonacci", "rings", "halton", "hammersley", "gauss", or "grid"
weighting="cosine", # "uniform", "cosine", "cos4", or "gaussian"
)
print(f"Half-cone angle: {cone.half_cone_rad * 180 / 3.14159:.1f} degrees")샘플링 방법
get_sampling_points() 메서드는 (theta_deg, phi_deg, weight) 튜플의 리스트를 반환합니다. 각 튜플은 평면파 방향과 관련 적분 가중치를 나타냅니다.
피보나치 샘플링(Fibonacci Sampling)
피보나치(황금각 나선) 샘플링은 원뿔 영역에 점을 준균일하게 분포시킵니다. 비교적 적은 점으로 좋은 커버리지를 제공합니다.
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=37, sampling="fibonacci"
)
points = cone.get_sampling_points()
print(f"Number of sample points: {len(points)}")
for i, (theta, phi, w) in enumerate(points[:5]):
print(f" Point {i}: theta={theta:.2f} deg, phi={phi:.2f} deg, weight={w:.4f}")피보나치 샘플링은 대부분의 경우에 권장됩니다. 빠른 추정에는 19-37점, 프로덕션 결과에는 61-91점을 사용하십시오.
동일 면적 ring 샘플링(Equal-area rings)
동심 ring 샘플링은 cone cap을 동일 면적 radial band로 나누고, 바깥 ring처럼 둘레가 긴 영역에 더 많은 방위각 샘플을 배치합니다. 결정론적이고 눈으로 검토하기 쉬워, regular polar grid가 너무 구조적으로 보일 때 좋은 대안입니다.
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=37, sampling="rings"
)
points = cone.get_sampling_points()
print(f"Ring sampling: {len(points)} points")Halton 샘플링
Halton 샘플링은 cone cap 위의 low-discrepancy sequence를 사용합니다. 눈에 띄는 ring 또는 spoke 대칭을 피하므로, deterministic quasi-random 패턴으로 수렴성을 확인할 때 유용합니다.
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=37, sampling="halton"
)Halton은 incremental sequence입니다. 샘플 수를 늘려도 앞쪽 sequence가 유지되므로, progressive convergence check에 편리합니다.
Hammersley 샘플링
Hammersley 샘플링도 low-discrepancy point set이지만, 전체 샘플 수를 미리 알고 있다는 가정에서 점을 배치합니다. 고정된 sample budget으로 한 번 계산할 때는 같은 n_points에서 Halton보다 더 깔끔한 분포가 나오는 경우가 많습니다.
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=37, sampling="hammersley"
)샘플 수 하나를 정해서 실행할 때는 Hammersley, 샘플 수를 점진적으로 키우며 수렴성을 볼 때는 Halton을 선택하는 것이 실용적입니다.
Gauss-Legendre 샘플링
Gauss-Legendre 샘플링은 radial angle 방향으로 quadrature node를 사용하고, 방위각은 균일하게 나눕니다. 정확히 n_points개를 맞추는 것보다 각도 적분 정확도가 더 중요할 때 가장 적합합니다.
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=36, sampling="gauss"
)YAML 호환성을 위해 sampling="gaussian_quadrature"도 alias로 허용됩니다.
Legacy 격자 샘플링(Grid Sampling)
격자 샘플링은 균일한
cone = ConeIllumination(
cra_deg=10.0, f_number=2.8, n_points=36, sampling="grid"
)
points = cone.get_sampling_points()
print(f"Grid sampling: {len(points)} points")격자 샘플링은 n_theta x n_phi개의 점을 생성하며, n_theta = sqrt(n_points), n_phi = n_points / n_theta입니다.
grid는 주로 진단용 기준선으로 쓰는 것이 좋습니다. cone 중심을 과샘플링하고 강한 radial/azimuthal 구조를 만들기 때문에, 실제 수렴성이 좋거나 나쁜 것처럼 보이게 만들 수 있습니다.
가중 함수
가중 함수는 동공 전체에 걸친 강도 분포를 모델링합니다:
| 가중 | 수식 | 물리적 모델 |
|---|---|---|
uniform | 균일 동공 조명 | |
cosine | 람베르트 소스 / 아플라나틱 렌즈 | |
cos4 | 이미지 평면에서의 cos-fourth 감쇠 | |
gaussian | 아포다이즈된 동공 |
기본값은 cosine이며, 대부분의 카메라 렌즈 시스템에 적합합니다.
# Compare different weighting functions
for wf in ["uniform", "cosine", "cos4", "gaussian"]:
cone = ConeIllumination(
cra_deg=0.0, f_number=2.0, n_points=37, weighting=wf
)
points = cone.get_sampling_points()
weights = [p[2] for p in points]
print(f"{wf:10s}: max_w={max(weights):.4f}, min_w={min(weights):.4f}")Ray file 기반 cone averaging
ConeIllumination은 cone을 CRA, F-number, analytic pupil weighting으로 표현할 수 있을 때 간결한 모델입니다. Lens-design workflow에서는 더 명시적인 입력이 나오는 경우가 많습니다. 여러 sensor position에 대해 ray file을 만들고, 각 ray가 보통 다음 정보를 가집니다:
| Field | 의미 |
|---|---|
image_x, image_y | ray bundle이 도달하는 sensor position |
pupil_x, pupil_y | pupil coordinate. chief ray는 pupil center |
theta_deg, phi_deg | sensor에서의 입사각 |
intensity | lens transmission / Fresnel / vignetting factor |
weight | pupil area 또는 quadrature weight |
한 sensor position의 ray bundle에 대한 cone-averaged QE는 다음과 같습니다:
여기서
각도 응답 값을 얻는 방법은 두 가지입니다:
| 전략 | 쓰는 경우 | trade-off |
|---|---|---|
| Direct ray simulation | sensor position 또는 ray sample이 적음 | 정확한 ray에서 계산하지만 sensor 전체로 반복하면 비쌈 |
| Angular-grid interpolation | position과 lens ray가 많음 | 구조적인 |
Camera-level characterization에서는 angular-grid interpolation이 보통 더 확장성이 좋습니다. Angular grid는 lens의 전체 CRA/MRA 범위를 덮어야 하며,
def cone_average_from_ray_bundle(qe_lookup, rays, wavelength):
numerator = 0.0
denominator = 0.0
for ray in rays:
qe = qe_lookup.interpolate(
wavelength=wavelength,
theta_deg=ray["theta_deg"],
phi_deg=ray["phi_deg"],
)
ray_weight = ray["intensity"] * ray["weight"]
numerator += qe * ray_weight
denominator += ray_weight
return numerator / max(denominator, 1e-12)INFO
Ray-file workflow는 Zemax/OpticStudio, custom Python ray tracer, measured CRA/MRA map과 모두 개념적으로 호환됩니다. COMPASS는 특정 lens-design tool에 의존하기보다 ray angle과 weight를 받는 optical interface로 다루는 것이 좋습니다.
평면파 솔버와의 통합
원뿔 조명 QE를 계산하려면, 각 샘플링된 각도에서 평면파 시뮬레이션을 실행하고 가중합을 계산합니다:
import numpy as np
from compass.sources.cone_illumination import ConeIllumination
from compass.solvers.base import SolverFactory
# Set up cone
cone = ConeIllumination(cra_deg=15.0, f_number=2.0, n_points=37, weighting="cosine")
points = cone.get_sampling_points()
# Create solver
solver = SolverFactory.create("torcwa", solver_config, device="cuda")
solver.setup_geometry(pixel_stack)
# Run planewave at each sample point
wavelength = 0.55
weighted_qe = {}
for theta_deg, phi_deg, weight in points:
solver.setup_source({
"wavelength": wavelength,
"theta": float(theta_deg),
"phi": float(phi_deg),
"polarization": "unpolarized",
})
result = solver.run()
for pixel_name, qe in result.qe_per_pixel.items():
if pixel_name not in weighted_qe:
weighted_qe[pixel_name] = 0.0
weighted_qe[pixel_name] += weight * float(np.mean(qe))
print("Cone-illuminated QE at 550 nm:")
for pixel_name, qe in weighted_qe.items():
print(f" {pixel_name}: QE = {qe:.3f}")원뿔 조명과 파장 스위프 결합
원뿔 조명에서의 전체 스펙트럼 스위프를 위해, 파장과 각도 샘플을 반복합니다:
wavelengths = np.arange(0.40, 0.701, 0.01)
cone = ConeIllumination(cra_deg=15.0, f_number=2.0, n_points=37)
points = cone.get_sampling_points()
# Initialize QE storage
pixel_names = None
cone_qe = {}
for wl in wavelengths:
wl_qe = {}
for theta_deg, phi_deg, weight in points:
solver.setup_source({
"wavelength": float(wl),
"theta": float(theta_deg),
"phi": float(phi_deg),
"polarization": "unpolarized",
})
result = solver.run()
if pixel_names is None:
pixel_names = list(result.qe_per_pixel.keys())
for pn in pixel_names:
cone_qe[pn] = []
for pn in pixel_names:
if pn not in wl_qe:
wl_qe[pn] = 0.0
wl_qe[pn] += weight * float(np.mean(result.qe_per_pixel[pn]))
for pn in pixel_names:
cone_qe[pn].append(wl_qe[pn])
# Plot results
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
wl_nm = wavelengths * 1000
for pn in pixel_names:
ax.plot(wl_nm, cone_qe[pn], label=pn)
ax.set_xlabel("Wavelength (nm)")
ax.set_ylabel("QE (cone illumination)")
ax.set_title(f"Cone Illumination QE (CRA={cone.cra_deg} deg, F/{cone.f_number})")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()샘플링 수렴
n_points를 증가시키며 결과를 비교하여 샘플 점 수가 충분한지 확인합니다:
for n in [7, 19, 37, 61, 91]:
cone = ConeIllumination(cra_deg=15.0, f_number=2.0, n_points=n)
points = cone.get_sampling_points()
# ... run weighted sum, record QE
print(f"n_points={n}: avg QE = ...")일반적으로 F/2.0 이하에서 37점이면 완전히 수렴된 적분 대비 1% 이내의 결과를 제공합니다.
렌즈 면적 스위프: F 넘버 vs QE 및 크로스토크
조명 원뿔의 풋프린트 면적은 F 넘버에 따라 변합니다. F 넘버를 스위프하면 렌즈 속도가 QE와 광학적 크로스토크에 미치는 영향을 파악할 수 있으며, 이는 CIS 설계에서 핵심적인 트레이드오프입니다.
풋프린트 면적 vs F 넘버
풋프린트 반경
| F 넘버 | 풋프린트 반경 (um) | 면적 (um²) | |
|---|---|---|---|
| F/1.4 | 20.9 | 1.91 | 11.5 |
| F/2.0 | 14.5 | 1.29 | 5.3 |
| F/2.8 | 10.3 | 0.91 | 2.6 |
| F/4.0 | 7.2 | 0.63 | 1.2 |
| F/5.6 | 5.1 | 0.45 | 0.63 |
(스택 높이 h = 5.0 um 기준)
F 넘버 스위프 실행
import numpy as np
from compass.sources.cone_illumination import ConeIllumination
from compass.solvers.base import SolverFactory
f_numbers = [1.4, 2.0, 2.8, 4.0, 5.6, 8.0]
wavelength = 0.55
cra_deg = 15.0
solver = SolverFactory.create("torcwa", solver_config, device="cuda")
solver.setup_geometry(pixel_stack)
results = {}
for fn in f_numbers:
cone = ConeIllumination(cra_deg=cra_deg, f_number=fn, n_points=37)
points = cone.get_sampling_points()
weighted_qe = {}
for theta_deg, phi_deg, weight in points:
solver.setup_source({
"wavelength": wavelength,
"theta": float(theta_deg),
"phi": float(phi_deg),
"polarization": "unpolarized",
})
result = solver.run()
for pixel_name, qe in result.qe_per_pixel.items():
if pixel_name not in weighted_qe:
weighted_qe[pixel_name] = 0.0
weighted_qe[pixel_name] += weight * float(np.mean(qe))
results[fn] = weighted_qe
print(f"F/{fn}: {weighted_qe}")트레이드오프 분석
빠른 렌즈(낮은 F 넘버)는 더 많은 빛을 수집하여 신호를 개선합니다. 그러나 넓어진 원뿔은 인접 픽셀 간의 각도 확산과 크로스토크도 증가시킵니다:
import matplotlib.pyplot as plt
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# 녹색 픽셀의 QE vs F 넘버
green_qe = [results[fn].get("green_tl", 0) for fn in f_numbers]
ax1.plot(f_numbers, green_qe, "go-", linewidth=2, markersize=8)
ax1.set_xlabel("F-number")
ax1.set_ylabel("QE (green pixel)")
ax1.set_title("QE vs F-number (550 nm)")
ax1.grid(True, alpha=0.3)
ax1.invert_xaxis()
# 크로스토크: 비대상 픽셀 QE / 대상 픽셀 QE 비율
crosstalk = []
for fn in f_numbers:
green = results[fn].get("green_tl", 1e-9)
red = results[fn].get("red_tr", 0)
xtalk = red / green * 100 # 백분율
crosstalk.append(xtalk)
ax2.plot(f_numbers, crosstalk, "rs-", linewidth=2, markersize=8)
ax2.set_xlabel("F-number")
ax2.set_ylabel("Crosstalk (%)")
ax2.set_title("Green→Red crosstalk vs F-number")
ax2.grid(True, alpha=0.3)
ax2.invert_xaxis()
plt.tight_layout()CRA + F 넘버 결합 스위프
완전한 렌즈 면적 감도 분석을 위해, CRA와 F 넘버를 모두 스위프하여 2D 맵을 구축합니다:
cra_values = [0, 5, 10, 15, 20, 25, 30]
f_numbers = [1.4, 2.0, 2.8, 4.0]
wavelength = 0.55
qe_map = np.zeros((len(cra_values), len(f_numbers)))
for i, cra in enumerate(cra_values):
for j, fn in enumerate(f_numbers):
cone = ConeIllumination(cra_deg=cra, f_number=fn, n_points=37)
points = cone.get_sampling_points()
weighted_qe = 0.0
for theta_deg, phi_deg, weight in points:
solver.setup_source({
"wavelength": wavelength,
"theta": float(theta_deg),
"phi": float(phi_deg),
"polarization": "unpolarized",
})
result = solver.run()
weighted_qe += weight * float(
np.mean(result.qe_per_pixel.get("green_tl", [0]))
)
qe_map[i, j] = weighted_qe
# 히트맵으로 시각화
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(qe_map, aspect="auto", origin="lower",
extent=[f_numbers[0], f_numbers[-1],
cra_values[0], cra_values[-1]])
ax.set_xlabel("F-number")
ax.set_ylabel("CRA (degrees)")
ax.set_title("Green pixel QE: CRA vs F-number")
plt.colorbar(im, ax=ax, label="QE")
plt.tight_layout()이 2D 스위프는 목표 QE 임계값에 대한 CRA 및 F 넘버 한계를 식별하는 데 도움이 되며, 이는 마이크로렌즈 설계 최적화에 필수적인 정보입니다.
YAML을 통한 설정
원뿔 조명은 소스 설정에서 지정할 수 있습니다:
source:
type: "cone"
cone:
cra_deg: 15.0
f_number: 2.0
n_points: 37
sampling: "fibonacci"
weighting: "cosine"
wavelength:
mode: "sweep"
sweep: {start: 0.40, stop: 0.70, step: 0.01}
polarization: "unpolarized"다음 단계
- ROI 스위프 -- 센서 전체에 걸친 원뿔 조명 스위프
- 마이크로렌즈 & CRA 최적화 쿡북 -- CRA vs QE 연구
- 첫 번째 시뮬레이션 -- 기본 평면파 설정