System designHow-to

Simulate an illumination profile to test centroid accuracy

Simulate the illumination profile as a Gaussian whose centre moves in sub-pixel steps, then prove your centroid code reads each position back within 0.02 px.

LumaSpec Vision10 min read Share on LinkedIn

On this page 8 sections
  1. 01What synthetic image proves a centroid calculation?
  2. 02How do you place the centre at a sub-pixel position?
  3. 03How much do 8-bit quantization and a finite window move the answer?
  4. 04Which build steps turn the formula into a test harness?
  5. 05What does the error pattern tell you when the sweep fails?
  6. 06When does the desk test stop being enough and the cleanroom start?
  7. 07Which camera grabs the real illumination profile?
  8. 08Questions

Your centroid code is finished, but the light lives in a cleanroom and every debug run now costs a gowning procedure. The software does not need the hardware: simulate the illumination profile as a Gaussian image whose centre you set exactly, step that centre in sub-pixel increments, and compare it with what your code reports.

This guide builds the harness step by step. You get the render formula with declared constants, the bit-depth and window arithmetic behind the 0.02 px pass limit, the numbered build steps, the failure patterns a sweep reveals, and the single cleanroom session at the end that fits the real beam. Once fitted, the sweep doubles as a regression test that runs in seconds whenever the code changes.

What synthetic image proves a centroid calculation?

One Gaussian spot on an integer pixel grid proves the math. Render a 1280 × 960 frame with σ = 40 px, peak 240 DN over a 10 DN background, and set the centre at any coordinates you choose, to a tenth of a pixel or better.

The centroid is a geometric quantity. It depends on the profile's shape and position, not on wavelength, colour or absolute brightness, which is why a synthetic Gaussian can stand in for the real beam while you work on code. Keep the spot at least 3σ = 120 px from every frame edge so no tail is truncated; on a 1280 px wide frame that confines the centre to x = 120–1160.

Render in 64-bit floating point. The constants for this guide, all doubles in pixel and DN units, are: cx = 640.0, cy = 480.0, sigma = 40.0, I0 = 240.0, bg = 10.0.

I(x, y) = bg + I0 · exp( −((x − cx)² + (y − cy)²) / (2σ²) )

A synthetic spot is not a simulated camera. If you need to fake the acquisition itself for field-of-view work, the simulated camera resolution and pixel scale guide covers that separate tool.

How do you place the centre at a sub-pixel position?

Evaluate the formula at each integer pixel coordinate instead of drawing a shape. With cx = 640.25, the pixel at x = 640 keeps exp(−0.0625/3200) ≈ 0.99998 of the peak while x = 641 keeps exp(−0.5625/3200) ≈ 0.99982, and that tiny imbalance encodes the quarter pixel.

Drawing primitives snap to whole pixels and destroy exactly the information this test exists to check. Compute intensities arithmetically, then quantize once at the end: round and clip to 0–255 for 8-bit output. Quantizing twice, or rescaling and recompressing after quantization, re-rounds values and can smear a 0.25 px placement into an unrecoverable blur.

The estimator under test is the background-subtracted weighted mean, xc = Σ x·w / Σ w with w = intensity − bg over the analysis window. It is the same grayscale centroid used to locate a dark circle to sub-pixel precision, so the bright Gaussian exercises identical math without the inversion step.

How much do 8-bit quantization and a finite window move the answer?

Less than your pass limit if the window is generous. A 240 × 240 window (±3σ) keeps 1 − e^(−4.5) = 98.9% of the flux, and 8-bit rounding alone leaves an expected centroid jitter near 0.0002 px on the σ = 40 px geometry.

N = π(3σ)² = π · 120² ≈ 45,200 signal pixels σ_q = 1 DN / √12 = 0.29 DN → SNR = 240 / 0.29 ≈ 830 σ_x ≈ σ / (SNR · √N) = 40 / (830 · 212) ≈ 0.0002 px

The last line is the standard small-noise expression for a symmetric spot; use it to predict, then let the sweep measure. With added noise at SNR 20 per pixel the same line gives 40/(20 · 212) ≈ 0.009 px, so noise, not bit depth, is the limiter once the synthetic case gets realistic.

Set the pass gate at 0.02 px. It sits far above the 0.0002 px quantization floor, so rounding inside your pipeline cannot false-fail. It also sits far below the 0.1 px-plus signature of real defects such as origin mismatches or asymmetric truncation.

Two window habits keep that floor low. Grow to ±4σ, a 320 × 320 window keeping 1 − e^(−8) = 99.97%, whenever the centre can sit more than a pixel or two from the window centre, because a fixed window with an off-centre spot loses tails asymmetrically. And subtract the background before weighting: on this geometry a background ramp of 1 DN per 100 px shifts the reading by roughly 0.004 px, and steeper ramps shift it in proportion.

To predict the jitter of a real camera instead of guessing it, measure its noise with the SNR calculation for 8-bit images guide and drop that SNR into the σ_x line.

Which build steps turn the formula into a test harness?

Seven steps take the formula to a running harness. Step 4 is the one that makes it honest: simulated frames enter the same queue, in the same image container, as live camera frames, so the code under test cannot tell which producer sent them.

  1. Fix the geometry as named constants: width = 1280, height = 960, sigma = 40.0 px, I0 = 240.0 DN, bg = 10.0 DN, start centre (cx, cy) = (640.0, 480.0).
  2. Render the Gaussian in double precision over the full frame; if that is slow, render a ±5σ = ±200 px region around the centre and place it on a bg-pedestal frame.
  3. Quantize once: round to integers and clip to 0–255 for 8-bit output. Never quantize, rescale or recompress a second time.
  4. Wrap the output in the same container the camera callback delivers — same resolution, same grey scale, same metadata fields — and enqueue it where camera frames are enqueued. One boolean at the enqueue point selects live or simulated.
  5. Drive the centre from a loop variable: step cx by 0.25 px per iteration from 639.00 to 641.00, nine frames, holding cy = 480.00.
  6. Log set centre, measured centre, window position and frame index for every iteration, so a failure carries its own evidence.
  7. Gate the run: pass if |measured − set| ≤ 0.02 px on all nine frames; otherwise stop and print the frame index with both values.

When you want the simulator to present itself as a camera to the whole program, so viewers, logging and configuration run unchanged, the test adaptor for image acquisition shows the wrapping pattern.

What does the error pattern tell you when the sweep fails?

The shape of the error across the sweep names the bug before you open the code. A constant offset means a coordinate-convention mismatch, error that grows with distance from the window centre means truncation, and a jump near the frame border means the spot is clipped.

Error pattern across the sweep Likely cause Fix
Constant +0.5 px or −0.5 px Pixel-origin convention: simulator and estimator disagree on whether pixel n sits at coordinate n or n + 0.5 Fix one convention on both sides; the half-pixel offset explainer works through the coordinate-origin issue
Constant +1 px Off-by-one in window bounds or array indexing Check the loop bounds, not the math
Grows as the centre moves away from the window centre Tails truncated asymmetrically Widen to ±4σ or re-centre the window on the measured value
Jumps near the frame edge Spot clipped by the border Enforce the 3σ = 120 px edge margin in the generator
Random scatter above 0.02 px Background not subtracted, or noise too strong Check subtraction; raise SNR or widen the gate per the σ_x line
Bias only in y Sigma or window size mismatch between the axes Log x and y errors separately and set sigma_x and sigma_y independently

When does the desk test stop being enough and the cleanroom start?

The desk proves the math and the cleanroom proves the beam, so book exactly one session. Grab the real profile, fit its actual σ, ellipticity and tails, then simulate from the fitted numbers and return only when hardware changes.

Real beams differ from the textbook Gaussian in three ways worth testing deliberately. Homogenized flat-top beams have sharp edges, so the window must contain the whole top hat plus a 2σ margin, not just ±3σ around a waist. Elliptical spots are free to simulate: set sigma_x = 40.0 and sigma_y = 25.0 and confirm the estimator handles both axes. Ghost reflections are the dangerous case — add a second Gaussian carrying 5% of the flux 150 px away and watch the bias: a lobe pulls the centroid by f·d/(1 + f) = (0.05 × 150)/1.05 ≈ 7 px, which tells you immediately whether the estimator should reject lobes or average them.

During the grabs, keep the peak between 60% and 80% of full scale, roughly 155–205 DN at 8 bits, so the core never saturates. Clipped pixels corrupt the weights, and blooming spreads charge into neighbouring pixels.

If the rig is still on your bench, the light finder filters spot lights by wavelength, colour and size before you commit. With one good real grab on disk, the exposure variant simulation guide extends it across exposure settings without booking the cleanroom again.

Which camera grabs the real illumination profile?

Use a mono sensor with a linear response and no in-camera sharpening, exposed so the peak lands near 70% of full scale. Colour cameras interpolate across the Bayer pattern and distort the weights; shutter type is irrelevant because neither spot nor camera moves.

One boundary on the numbers: 8 bits is enough for the centroid itself, and 12 bits pays off only when the fit needs the faint tails of the real beam.

Two mono C-mount options cover both scales. A 20 MP sensor on a 1" format resolves the faint tails:

Area scan camerasHikrobot MV-CS200-10UM 20 MP Area Scan Camera, USB3.0, IMX183, 2nd Gen., MonoMV-CS200-10UM
Sensor format
1"
Lens mount
C-mount
Interface
USB3
Resolution
16-25 MP
$759.00View partBuild a system with it

A 6 MP sensor on a 1/1.8" format keeps the GigE pipeline simple:

Area scan camerasHikrobot MV-CT0MG0060/R/NN/NN 6 MP Area Scan Camera, 3rd Gen., GigE, Mono, BASEMV-CT0MG0060/R/NN/NN
Sensor format
1/1.8"
Lens mount
C-mount
Interface
GigE
Resolution
6-10 MP
$909.00View partBuild a system with it

If neither footprint fits the rig, the camera finder filters the area scan cameras by mono, resolution and interface.

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

Run the nine-frame sweep today, read the error pattern, and fix what it names. The cleanroom visit then becomes a one-hour fitting exercise instead of a debugging marathon. Every later code change is guarded by a regression test that never needs a gown. More desk-side test patterns sit on the systems topic page.

Questions

How many sweep positions does a centroid regression test need?

Fourteen frames cover both bug families: nine positions across one pixel, 639.00 to 641.00 in 0.25 px steps, expose origin and rounding errors, and five positions spread over ±50 px expose window truncation. The whole set renders in under a second, so run it on every build.

Does the synthetic image need the real light's wavelength or spectrum?

No. The weighted centroid uses only geometry: shape, size and noise move the reading, while colour, spectrum and absolute brightness cancel out. A white LED and an 850 nm emitter with the same profile produce the same centroid, so simulate the shape and ignore the spectrum entirely.

What sigma should the simulated spot use?

Derive it from the beam's full width at half maximum: σ = FWHM / 2.355. A 3 mm spot imaged at 20 px/mm has FWHM = 60 px, so σ ≈ 25 px. For coverage, sweep σ from 10 to 80 px; small spots raise jitter roughly in proportion to σ, and large ones need wider windows.

How do I add realistic noise to the synthetic frames?

After rendering and before quantizing, add zero-mean Gaussian noise with standard deviation = peak signal / SNR. At SNR 20 on a 240 DN peak that is 240/20 = 12 DN per pixel, then quantize once. The measured sweep jitter should land near the σ_x expression, which validates both the noise model and the estimator.

Do the pass limits change at 12-bit or with a different resolution?

Recompute rather than guess. Quantization noise falls 16-fold at 12 bits, 0.29/16 ≈ 0.018 DN, so the floor drops far below 0.001 px and the 0.02 px gate still stands. Halving σ to 20 px roughly halves the jitter at the same SNR, so keep the gate and widen it only when SNR drops.