Added spectrum and camera specs

This commit is contained in:
connor committed 2026-10-14 15:23:09 -04:00
1 parent 114b03740a
commit 3eb7ec4808
9 files changed
+714 -1

No files matched your search

+267
View File
@@ -0,0 +1,267 @@
# Optics and radiometry guide
This explains the physics the renderer models, for a reader who knows
orbital mechanics but not optics. Each section says which code implements
it. When a feature adds a new optical effect, it adds or updates a section
here in the same change.
Numbers in the examples come from `CameraConfig::example()` unless stated
otherwise: 1920 x 1080 pixels, 5.5 um pitch, 50 mm focal length, f/4, three
color channels, 12-bit output.
## The chain from Sun to number
Every pixel value comes from the same sequence:
1. The Sun emits power across a range of wavelengths.
2. Some of it lands on a surface (Earth, the Moon, the target), which
reflects part of it in various directions.
3. The fraction reflected toward the camera enters the lens aperture. The
optics focus it onto one pixel, losing a little to absorption.
4. The pixel converts some of the arriving photons into electrons. The
fraction depends on wavelength and on the color filter over that pixel.
5. The electrons pile up during the exposure, up to a limit.
6. The readout electronics turn the electron count into an integer, the
digital number (DN).
The renderer follows this chain with physical units at every step. Image
brightness therefore comes from the scene and the camera settings, not from
a tuned scale factor. Change the exposure time, and the image changes
exactly as a real camera's would.
## Geometry: the pinhole camera
Code: `camera.rs` (`Intrinsics`), `scene.rs`.
A pinhole camera maps each direction in space to one point on the focal
plane. Put the pinhole at the origin, looking along +Z. A point at camera
coordinates (x, y, z) lands on the focal plane at (f x / z, f y / z), where
f is the focal length. Dividing by the pixel pitch p converts focal-plane
distance to pixels:
```text
u = (f / p) * x / z + cx
v = (f / p) * y / z + cy
```
`f / p` is the focal length in pixels. (cx, cy) is the principal point,
where the boresight hits the image, normally the image center. For the
example camera, f / p = 50 mm / 5.5 um = 9091 pixels.
Some quantities that follow from that:
- **IFOV, the instantaneous field of view.** The angle one pixel covers:
p / f = 110 microradians. At 10 m that is 1.1 mm per pixel; at 7000 km
it is 770 m per pixel. This sets the smallest detail the camera can
resolve.
- **FOV, the field of view.** The angle the whole image covers:
2 atan(width / 2 / (f / p)), about 12.1 x 6.8 degrees here. A longer
focal length or smaller sensor gives a narrower field of view and finer
IFOV.
Real lenses are not perfect pinholes. They bend straight lines slightly
(distortion, planned in M3) and spread each point into a small blur (the
point spread function, also M3). The pinhole model is the starting point
those effects are added to.
### Coordinate conventions
The camera frame has +Z along the boresight, +X toward increasing column u,
and +Y toward increasing row v. This is the computer-vision convention: row
0 is the top of the image, so +Y points down in the picture. Pixel (i, j)
covers u in [i, i+1) and v in [j, j+1), so its center is at (i + 0.5,
j + 0.5).
### Why the GPU sees camera-relative coordinates
GPUs compute in 32-bit floats, which carry about 7 significant digits. A
spacecraft 7000 km from Earth's center, stored in Earth-centered f32
coordinates, can only be placed to the nearest 0.5 m. A target 10 m away
would jitter by several pixels. So all positions stay in 64-bit floats on
the CPU. They are made relative to the camera there, and converted to
32-bit only after that. A target 10 m from the camera is then placed to
about a micrometer.
### Depth
When two things overlap in the image, the nearer one must win. The GPU
keeps a depth value per pixel for this. The renderer stores near / z,
which is 1 at the near clip distance and falls toward 0 with distance.
Combined with how floating-point numbers are spaced, this gives roughly the
same relative precision at every distance. That matters in a scene that
contains a 10 m target, Earth at thousands of kilometers, and the Sun at
150 million kilometers. The scheme is called reversed-Z with an infinite
far plane.
## Radiometric quantities
Code: `spectrum.rs` now; `radiometry.rs` in #4.
Light has a few distinct measures, and the camera measures a specific one.
- **Irradiance, E, in W/m^2.** Power arriving per unit area of a surface.
Sunlight at 1 AU delivers about 1361 W/m^2 to a surface facing the Sun.
It falls off with the square of the distance from the Sun. A surface
tilted by angle theta from the Sun direction receives E cos(theta).
- **Solid angle, in steradians (sr).** The 2D version of an angle: the area
a cone cuts out of a unit sphere. The full sphere is 4 pi sr. One pixel
of the example camera covers (110 urad)^2, about 1.2e-8 sr.
- **Radiance, L, in W/m^2/sr.** Power per unit area per unit solid angle,
traveling in a particular direction. It is what a camera pixel responds
to: how bright a surface looks from a given direction.
Radiance has a property that explains a lot of what a camera sees: it does
not change along a ray through empty space. Move a camera twice as far from
a wall, and each pixel sees four times as much wall area. But each square
meter now contributes a quarter of the power, so the pixel value is
unchanged. An object that covers many pixels has the same surface
brightness at any distance; it just covers fewer pixels farther away.
That stops being true once the object is smaller than a pixel. The pixel
then also sees dark space around the object, so the signal falls as one
over distance squared. That is the regime of a distant satellite seen as a
point (space situational awareness), as opposed to the resolved imagery
this renderer is mostly for.
### Reflection: albedo and Lambertian surfaces
A Lambertian surface reflects light equally in every direction. Matte
paint, the Moon (roughly) and cloud tops behave close to this. If such a
surface receives irradiance E and reflects a fraction rho (its albedo), its
radiance in every direction is:
```text
L = rho * E / pi
```
The pi comes from integrating the reflected radiance over the hemisphere
above the surface. A white Lambertian sheet facing the Sun at 1 AU, with
rho = 1, has a radiance of about 1361 / pi = 433 W/m^2/sr, summed over all
wavelengths. Earth's average albedo is about 0.3.
Real spacecraft surfaces are not Lambertian. Multilayer insulation (MLI)
and solar cells are partly mirror-like and produce bright glints at
particular angles. These are modeled by a BRDF, a function giving reflected
radiance for every pair of incoming and outgoing directions. That arrives
in M4.
## Wavelength: spectra, channels and photons
Code: `spectrum.rs` (`Spectrum`), `camera.rs` (`Channel`).
Everything above varies with wavelength. Sunlight peaks around 500 nm and
extends from the ultraviolet into the infrared. Surfaces reflect different
wavelengths differently; that is what color is. Optics transmit some
wavelengths better than others. The sensor's sensitivity also depends on
wavelength.
The renderer describes each of these as a `Spectrum`: a value as a function
of wavelength, in meters.
- **Quantum efficiency (QE).** The fraction of arriving photons that become
electrons in a pixel. A color camera puts red, green and blue filters
over different pixels, so each channel has its own QE curve. The example
camera uses idealized Gaussian curves centered at 620, 540 and 460 nm,
80 nm wide at half height, peaking at 60 %.
- **Transmission (tau).** The fraction of light the lenses pass, about 0.9
for the example camera.
Detectors count photons, not energy. A photon of wavelength lambda carries
energy h c / lambda (h is Planck's constant, c the speed of light). So one
watt of red light delivers more photons than one watt of blue. The
renderer converts power to photons per wavelength before applying QE.
### Folding spectra into per-channel constants
Doing all of this wavelength by wavelength for every pixel would be slow.
Most factors do not depend on the pixel: the solar spectrum, the optics,
and the QE curves. The renderer multiplies and integrates those over
wavelength once, when the camera is set up. That yields a few numbers per
channel, and the GPU only multiplies by them.
The cost is a small error where two spectra both vary inside one channel's
band. An example is a surface whose reflectance changes steeply across
green. Rendering more, narrower channels reduces that error, and the code
keeps the channel count generic for that reason. (#4)
## From radiance to electrons: the camera equation
Code: the sensor pass, #9.
A lens with focal length f and aperture diameter D has f-number
N = f / D. The example camera is f/4: a 50 mm lens with a 12.5 mm aperture.
For a scene radiance L, the irradiance on the focal plane at the image
center is:
```text
E_image = pi * L * tau / (4 N^2)
```
The aperture collects light over a solid angle proportional to (D / f)^2 =
1 / N^2. So opening the aperture one "stop", which divides N by the square
root of 2, doubles the light. Image irradiance is independent of distance
for the same reason radiance is.
Away from the center, image irradiance falls off as cos^4(theta), where
theta is the angle from the boresight. Two factors come from the longer
path to the off-axis pixel. One comes from the aperture appearing
foreshortened. One comes from light reaching the pixel at a slant. At the
corner of the example camera, theta is 6.9 degrees, and the falloff is
3 %. Real lenses add more falloff of their own, called vignetting.
The electrons a pixel collects in one channel are then:
```text
electrons = E_image * (pixel area) * (exposure time) * (photons per joule) * QE
```
The last two factors are wavelength-dependent and are folded together per
channel, as described above.
## From electrons to DN: the detector
Code: `camera.rs` (`Detector`); the sensor pass, #9.
- **Full well.** The maximum number of electrons a pixel holds. Beyond it
the pixel saturates and reads its maximum value whatever the light.
Brighter scenes do not look brighter. In real sensors, excess charge can
also spill into neighboring pixels (blooming, M3).
- **Gain.** Electrons per DN. With a gain of 5, every 5 electrons add 1 to
the output.
- **Bias.** A fixed offset added to every pixel, so that noise around zero
signal does not get clipped at 0. The example uses 64 DN.
- **ADC bits.** The output resolution. 12 bits gives values 0 to 4095.
So DN = bias + electrons / gain, rounded and clipped to the ADC range.
### Worked example
Take a white Lambertian surface facing the Sun at 1 AU, seen by the example
camera's green channel. Integrating the solar spectrum through the green QE
curve, f/4 optics, 5.5 um pixels and a 0.1 ms exposure gives about 10,300
electrons. That figure came from a blackbody approximation of the Sun; #4
replaces it with the measured solar spectrum.
- DN = 64 + 10,300 / 5, about 2125, roughly half the 12-bit range.
- The full well is 20,000 electrons, so this surface is at about half of
saturation.
- Earth, with albedo near 0.3, reads about 64 + 3,100 / 5, about 682 DN.
Picking the exposure time is the same trade a photographer makes. A longer
exposure saturates the sunlit target. A shorter one leaves the side of the
target lit only by Earthshine lost in noise once noise is modeled (M3).
## Not modeled yet
| Effect | Where |
|---|---|
| Light from Earth and the Moon illuminating the target (Earthshine, Moonshine) | M2 |
| Shadows the target casts on itself | M2 |
| The Sun as a disk that is darker toward its edge (limb darkening) | M2 |
| Lens distortion | M3 |
| Blur from diffraction and lens imperfections (point spread function) | M3 |
| Noise: photon shot noise, read noise, dark current, pixel-to-pixel variation | M3 |
| Saturation spilling into neighbors (blooming) | M3 |
| Non-Lambertian materials (BRDFs), glints | M4 |
| Earth's atmosphere: scattering, the bright limb | M4 |
| Motion blur, rolling shutter, stray light | M6 |