PSF/Blurring. Turned out to be tough
This commit is contained in:
1 parent
4d070db85e
commit
d0cec6572e
22 files changed
+1250
-92
No files matched your search
+146
-8
@@ -17,7 +17,8 @@ Every pixel value comes from the same sequence:
|
||||
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.
|
||||
optics focus it onto the detector, losing a little to absorption. Each
|
||||
point of the scene lands as a small spot rather than a point.
|
||||
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.
|
||||
@@ -60,8 +61,8 @@ Some quantities that follow from that:
|
||||
IFOV.
|
||||
|
||||
Real lenses are not perfect pinholes. They bend straight lines slightly
|
||||
(distortion) and spread each point into a small blur (the point spread
|
||||
function); neither is modeled yet. The pinhole model is the starting point
|
||||
(distortion), not modeled yet, and spread each point into a small blur (the
|
||||
point spread function, below). The pinhole model is the starting point
|
||||
those effects are added to.
|
||||
|
||||
### Coordinate conventions
|
||||
@@ -425,11 +426,13 @@ fraction in quarters, so a pixel on the limb can be off by up to an eighth
|
||||
of the difference across the edge (measured 0.10 on Earth's limb from low
|
||||
orbit, against up to a half at C0). Patterns that stagger the rays fix
|
||||
aligned edges but do worse at other angles; the regular grid had the
|
||||
smallest error on average over all angles. The lens blur of the point
|
||||
spread function, once modeled, spreads every edge over a few pixels.
|
||||
smallest error on average over all angles. At C1 the point spread function
|
||||
(below) then spreads every edge over a few pixels, and C1 also renders each
|
||||
pixel as 2 x 2 finer ones by default, each sampled this way, which divides
|
||||
these errors further.
|
||||
|
||||
C1 costs 0.4 to 0.5 ms more per 1920 x 1080 frame on an RTX 3060, and 1.2
|
||||
to 1.4 ms at 3840 x 2160, measured on Landsat 8, ICESat-2 and the moonrise
|
||||
At one rendered pixel per image pixel, C1 costs 0.4 to 0.5 ms more per
|
||||
1920 x 1080 frame on an RTX 3060, and 1.2 to 1.4 ms at 3840 x 2160, measured on Landsat 8, ICESat-2 and the moonrise
|
||||
scene. On the CPU through lavapipe it costs much more, 2.6 to 8 times the
|
||||
mesh pass at C0 (ICESat-2's 752,000 triangles go from 74 to 580 ms per
|
||||
frame), because lavapipe appears to run the fragment shader for every
|
||||
@@ -437,6 +440,141 @@ sample rather than every pixel. The 4 samples of radiance and depth also
|
||||
take 80 bytes per pixel of GPU memory: 166 MB at 1920 x 1080 and 660 MB at
|
||||
3840 x 2160.
|
||||
|
||||
## Blur: the point spread function
|
||||
|
||||
Code: `camera.rs` (`Psf`); `gpu/blur.rs`, `shaders/blur.wgsl`.
|
||||
|
||||
No camera focuses a point to a point. Light from one point of the scene
|
||||
lands on the detector as a small spot, and the shape of that spot is the
|
||||
point spread function (PSF). Several things widen it:
|
||||
|
||||
- **Diffraction.** Light passing through a round aperture spreads into a
|
||||
pattern of rings (the Airy pattern) whose size depends only on the
|
||||
wavelength and the f-number. At f/4 and 550 nm the first dark ring is
|
||||
2.7 um from the center, half a pixel of the example camera.
|
||||
- **Aberrations and defocus.** Real lenses bring different parts of the
|
||||
aperture, different wavelengths and different parts of the field to
|
||||
slightly different points.
|
||||
- **The detector.** Electrons freed deep in a pixel can drift into its
|
||||
neighbors before they are collected (charge diffusion).
|
||||
- **Jitter.** The camera shakes a little during the exposure.
|
||||
|
||||
Each of these blurs the image of the one before it, and many such blurs
|
||||
together come out close to a Gaussian, a bell curve in 2D. So a common model
|
||||
of a camera's PSF is a Gaussian with one width, its standard deviation
|
||||
sigma, measured in pixels. A spot with sigma 0.5 px is 1.2 px wide at half
|
||||
its peak, and a point centered on a pixel puts about 47 % of its light in
|
||||
that pixel and the rest in its neighbors; at 0.7 px, 28 %. Longer
|
||||
wavelengths diffract more, so sigma is set per channel and usually grows
|
||||
toward the red.
|
||||
|
||||
How wide the spot should be depends on the camera. Published measurements,
|
||||
converted to a Gaussian's sigma with the pixel's own area taken out:
|
||||
|
||||
- Earth-observation imagers are built sharp: about 0.2 px for Landsat 8's
|
||||
OLI, the sharpest, and 0.35 to 0.55 px for WorldView-3, Pléiades Neo and
|
||||
Sentinel-2's specification. The Lunar Reconnaissance Orbiter's cameras are
|
||||
specified in the same range.
|
||||
- Navigation and small-satellite cameras are softer: about 0.6 to 0.9 px
|
||||
for DART's DRACO camera in flight, and 0.55 to 0.7 px for Satellogic's
|
||||
imagers.
|
||||
- Star trackers are defocused so each star covers several pixels. The one
|
||||
measured in flight here (on Lunar Flashlight) has a sharp core and wide
|
||||
wings that no single Gaussian matches: its spot is as wide at half its
|
||||
peak as a 0.4 px Gaussian, and spreads its light as widely as a 1.25 px
|
||||
one.
|
||||
|
||||
The imagers' numbers are for the whole system, including the atmosphere
|
||||
and image processing, so the optics alone are somewhat sharper. The example
|
||||
camera uses 0.5 px in every channel, a sharp camera, not derived from its
|
||||
optics; diffraction alone at f/4 would give about 0.17 px.
|
||||
|
||||
Without blur, a star or a distant spacecraft lands in one pixel or in none,
|
||||
and every edge is as sharp as the pixel grid allows. Algorithms that find
|
||||
the center of a star or a target to a fraction of a pixel (centroiding)
|
||||
need the spot spread over more than one pixel, so they cannot be tested on
|
||||
an unblurred image.
|
||||
|
||||
The blur is applied to radiance on the focal plane, before light becomes
|
||||
electrons. That order matters for anything bright enough to saturate. The
|
||||
Sun's image is tens of thousands of times brighter than a full well.
|
||||
Blurred first, its spot spreads that light into the neighboring pixels,
|
||||
which also fill, so the saturated region is wider than the Sun's disk, as
|
||||
in a real camera. Clipped first and then blurred, the saturated region
|
||||
would shrink back inside the disk and its edge would fade over a pixel or
|
||||
two, as if the Sun were barely brighter than the full well. The blur's
|
||||
weights sum to 1, so it moves light without adding or removing any.
|
||||
|
||||
### Blurring before the pixels average
|
||||
|
||||
The blur has to act on the light where it lands, before each pixel averages
|
||||
it. Blurring an image of pixel averages afterward is not the same once the
|
||||
spot is about as small as a pixel: a point of light that fell anywhere in a
|
||||
pixel has become that pixel's value, and where in the pixel it fell is
|
||||
gone. Its blurred spot should lean toward wherever it was, and no blur of
|
||||
the averages can tell which way. Measured with sigma 0.4 px, points of
|
||||
light blurred after averaging put up to 51 % of their light in the wrong
|
||||
pixel, and edges were off by up to 12 % of their step.
|
||||
|
||||
So from camera tier C1 the renderer draws the scene finer than the pixels,
|
||||
n x n rendered pixels to each image pixel, blurs that by a Gaussian PSF set
|
||||
in `CameraConfig::psf`, and only then averages each n x n block into one
|
||||
pixel. Each rendered pixel still collects light over its own area the way
|
||||
the previous section describes, so the finer image is itself made of
|
||||
small-pixel averages, and the same error remains at a smaller scale:
|
||||
|
||||
| Rendered pixels per pixel | Points, worst pixel | Edges, worst pixel |
|
||||
|---|---|---|
|
||||
| 1 x 1 | 51 % of the light | 12 % of the step |
|
||||
| 2 x 2 | 19 % | 2.2 % |
|
||||
| 4 x 4 | 4.5 % | 0.5 % |
|
||||
|
||||
(sigma 0.4 px, points at 16 and edges at 129 positions within a pixel; the
|
||||
worst edges lie on a pixel boundary.) C1 renders 2 x 2 by default;
|
||||
`RendererOptions::supersampling` sets 1 to 4. Point sources, stars in
|
||||
particular, need better than that, and will be drawn with the blurred spot
|
||||
computed exactly over each pixel instead.
|
||||
|
||||
A 2D Gaussian is the product of a 1D Gaussian across and one down, and
|
||||
averaging an n x n block is likewise an average across and one down. So the
|
||||
blur runs as two passes, one along rows and one along columns, each taking
|
||||
a weighted sum of the rendered pixels out to 4 sigma on either side
|
||||
(beyond which a Gaussian holds less than 1e-4 of its light) and averaging
|
||||
n of them into one pixel at once. Light from just outside the frame also
|
||||
blurs into it, so the renderer draws a margin of that width around the
|
||||
image, and the blur reads it and drops it. The weights are the Gaussian at
|
||||
each rendered pixel's offset, which keeps a spot's width (its variance)
|
||||
within 1 % of sigma squared once sigma is 0.65 rendered pixels or more:
|
||||
0.33 px in image pixels at 2 x 2.
|
||||
|
||||
Rendering finer costs about n^2 times the drawing, and the blur grows with
|
||||
it. To keep that down, the meshes are covered at 2 points per rendered pixel
|
||||
rather than 4 when rendering finer: at 2 x 2 that still puts samples at 4
|
||||
distinct offsets across and down each image pixel, as 4 samples did at
|
||||
1 x 1, so mesh edges still grade in quarters or better. (A device that
|
||||
cannot render 2 samples, such as lavapipe, uses 4.) GPU time per frame at
|
||||
C1, from the first pass to the last, on an RTX 3060 (Landsat 8, ICESat-2
|
||||
and the moonrise scene, at lighting L1, sigma 0.5 px):
|
||||
|
||||
| Rendered pixels per pixel | 1920 x 1080 | 3840 x 2160 |
|
||||
|---|---|---|
|
||||
| 1 x 1 | 1.9 to 2.5 ms | 6.1 to 6.9 ms |
|
||||
| 2 x 2 (default) | 3.7 to 4.4 ms | 12.6 to 14.6 ms |
|
||||
| 3 x 3 | 6.7 to 7.9 ms | 34 to 38 ms |
|
||||
| 4 x 4 | 10.9 to 13.0 ms | 59 to 66 ms |
|
||||
|
||||
Of the 3.7 to 4.4 ms at 2 x 2, the bodies take 1.3 to 1.4 ms, the meshes
|
||||
0.7 to 1.1 ms and the blur 1.0 ms. With 2 samples instead of 4 the meshes
|
||||
and bodies took 0.3 to 0.6 ms less in all; with 1 sample, 0.7 to 1.0 ms
|
||||
less, but a mesh edge then grades only in halves at 2 x 2, coarser than at
|
||||
1 x 1. The rendered images take 60 bytes per rendered pixel (100 at 1 x 1,
|
||||
with 4 samples), and the blur 16 bytes per image pixel per rendered row:
|
||||
about 0.27, 0.60, 1.25 and 2.15 GB at 1920 x 1080 for 1 x 1 to 4 x 4, and
|
||||
four times that at 3840 x 2160. The renderer reports running out of memory
|
||||
as an error. On the CPU through lavapipe, 2 x 2 adds 9 to 13 ms to
|
||||
1920 x 1080 frames of 116 to 680 ms, and 3 x 3 at 3840 x 2160 runs out of
|
||||
memory.
|
||||
|
||||
## Earth, Moon and Sun
|
||||
|
||||
Code: `bodies.rs`, `gpu/bodies.rs`, `shaders/bodies.wgsl`.
|
||||
@@ -1115,7 +1253,7 @@ velocity as an input, which the scene does not carry yet.
|
||||
|
||||
- Earthshine on the Moon's night side
|
||||
- Lens distortion
|
||||
- Blur from diffraction and lens imperfections (point spread function)
|
||||
- The diffraction pattern's rings, and a lens's measured point spread function
|
||||
- Noise: photon shot noise, read noise, dark current, pixel-to-pixel variation
|
||||
- Saturation spilling into neighbors (blooming)
|
||||
- Non-Lambertian materials (BRDFs), glints, and the full Moon's edge as bright
|
||||
|
||||
Reference in new issue
Block a user