3D visionTroubleshooting

Linear RGB vs sRGB: image brightness in script vs viewer

Why the same capture looks darker in your script than in the viewer: linear RGB vs sRGB gamma, and the conversion that restores image brightness.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01The diagnosis: same capture, different colour space
  2. 02Linear RGB and sRGB in one minute
  3. 03What the gamma curve does to real pixel values
  4. 04Rank the causes, then test each one
  5. 05The fix: request sRGB output where you present the image
  6. 06Keep linear for measurement, sRGB for eyes
  7. 07Prove the fix numerically
  8. 08Prevent it: pin the colour space once
  9. 09Questions

You exported the capture settings from the viewer, ran them unchanged in your script, and the script's image came out noticeably darker. The capture is not the problem. This is the most common colour-space mismatch in 3D camera work, and it has a one-line fix: the viewer converts the camera's linear RGB to sRGB before it draws, while your script displays the raw linear buffer and skips that step. Gamma lifts mid-tones by roughly 2.5x, which is exactly the gap you are seeing. This guide shows you how to confirm the diagnosis from two pixel values, rank the remaining causes, and request the sRGB conversion so both presentations match.

The diagnosis: same capture, different colour space

Both images contain the same numbers. The camera delivered one linear RGB frame; the viewer and your script only present it differently. Viewer interfaces convert to sRGB for display because that is what monitors expect, and the conversion brightens mid-tones. Most display and file-writing functions in scripts draw the numbers you hand them and assume the buffer is already display-ready, so the same frame renders dark.

You can confirm this without touching the camera. Sample the same flat surface in both presentations. If the viewer shows a code near 118 where your script shows 46, you are looking at the same linear value of 0.18 before and after the sRGB curve, and the case is closed. If the ratio differs, or the colours also shift hue, work through the causes below; colour space is still the first suspect, but not the only one.

For the underlying colour science from the display side, see the guide to sRGB colour space and image brightness.

Linear RGB and sRGB in one minute

A camera sensor counts photons: double the light, double the value, until the pixel saturates. That linear signal is what the SDK hands your script. Monitors and human vision are not linear, so display standards encode images with a gamma curve before storage. sRGB, defined in IEC 61966-2-1, is the display standard, and the sRGB reference lists the exact transfer function your viewer is applying:

V_srgb = 1.055 × V_linear^(1/2.4) − 0.055 for V_linear > 0.0031308 V_srgb = 12.92 × V_linear for V_linear ≤ 0.0031308

Two consequences matter here. First, the curve brightens mid-tones strongly and deep shadow even more strongly, which is why a linear render looks dark rather than slightly off. Second, the curve is invertible, so no information is destroyed beyond 8-bit rounding; you can always convert back to linear with the inverse function.

Most machine-vision cameras and 3D sensors deliver linear colour by default unless an on-camera image processor applies its own gamma. That default is exactly why the viewer-versus-script gap keeps surprising people.

What the gamma curve does to real pixel values

Put numbers on it. The table converts linear 8-bit codes through the exact function above:

Linear code Linear value sRGB code Display change
1 0.004 13 ×13
5 0.020 38 ×7.6
46 0.180 118 ×2.6
64 0.251 137 ×2.1
128 0.502 188 ×1.5
255 1.000 255 fixed at white

Worked example at middle grey, linear 0.18, roughly what a matte grey card gives under even illumination:

raw code: 0.18 × 255 = 45.9 → 46 0.18^(1/2.4) = 0.4895 1.055 × 0.4895 = 0.5164 0.5164 − 0.055 = 0.4614 0.4614 × 255 = 117.7 → 118

The same pixel leaves your script as code 46 and appears in the viewer as code 118. That is the 2.6× brightness difference, and no capture setting can remove it, because the change happens after capture. Note the toe of the curve as well: values at or below 0.0031308 scale by 12.92, so linear 0.002 becomes code 7 instead of code 1. Near-black moves least, deep shadow moves most: code 5 becomes 38, a 7.6× lift.

Rank the causes, then test each one

Work down this list. The first cause explains nearly every instance of this symptom.

  1. Colour space. The viewer shows sRGB, the script shows linear. Test: apply the formula above to your script image and compare against the viewer. A mean gap under about 3 counts per channel confirms it; a gap of 30-70 counts on a mid-tone scene says you are still comparing linear with sRGB.
  2. The settings never reached the camera, or an auto function overrode them. Export files go stale, parameter names change between SDK versions, and auto-exposure re-aims at its own brightness target in each application. Test: read the applied exposure and gain back from the device immediately after setting them, then capture with both fixed. The guides on manual exposure versus auto exposure and on consistent multi-exposure settings for HDR capture cover the failure modes.
  3. Bit-depth or scaling mismatch. A 10- or 12-bit buffer dropped into an 8-bit container, or never normalised into a 16-bit one, renders dark and posterized. Test: print the buffer's data type, minimum and maximum; values that top out far below the container's full scale point at a shift that never happened. The 12-bit data in a 16-bit preview guide shows the pattern.
  4. Channel order or alpha. A red-blue swap or a stride error shifts hue and paints colour casts; it does not darken every channel uniformly. Test: view each channel on its own.

One signature settles most cases: a uniform, scene-wide darkening with correct hue is colour space, hue distortion is channels, posterized darkness is bit depth.

The fix: request sRGB output where you present the image

Fix it at the presentation boundary and leave the capture settings alone.

  1. Look for a colour-space option in your SDK's image conversion or save call. 3D camera SDKs commonly expose two accessors side by side — for example image_rgba() for the linear values and image_srgb() for the display-ready conversion. Call the sRGB variant for anything a person will look at.
  2. If your SDK offers no conversion, apply the formula per channel on float values scaled 0-1, then multiply by 255 for 8-bit output or 65535 for 16-bit.
  3. Convert a copy. The linear buffer stays your measurement data; the sRGB copy is for screens, reports and saved images.
  4. Re-run the viewer comparison. With identical capture settings and one sRGB conversion, the two presentations should now be numerically near-identical.

Nothing in this fix touches depth, exposure or projector power. The point cloud and the measurements stay as they were; only the picture you look at moves onto the display curve.

Keep linear for measurement, sRGB for eyes

Use linear wherever a number will be computed, sRGB wherever a human will look. Any ratio-based measurement breaks under gamma: double the illumination on that grey card and the linear code moves from 46 to 92, a clean factor of 2, while the sRGB code moves from 118 to 162. A brightness ratio computed on sRGB codes reports 1.37 instead of 2.0. Reflectance comparison, HDR blending and photometric measurement all need the linear buffer.

Point clouds inherit the question. Exporters often write the camera's linear RGB straight into the file, and viewers paint it as if it were display-ready. The companion guide Linear RGB vs sRGB: fix dark images and point cloud colors walks through converting point-cloud colour, and why exported point clouds differ from the viewer render covers the wider viewer-versus-file gap.

The same logic holds for plain area scan cameras: raw colour from the sensor is linear, and a viewer that looks brighter is showing you a converted image, not a different capture.

Prove the fix numerically

Eyeballing that two images now look the same is how a double conversion slips through, so compare numbers. Save both presentations as 8-bit images and compute the mean absolute difference per channel: MAD = (1/N) × Σ |A − B| over all pixels. Subtract in a signed or floating type, because 8-bit unsigned subtraction wraps around — the same trap that corrupts SNR calculation from 8-bit camera images.

Read the result like this. Matched presentations land within about 3 counts per channel, which is rounding and viewer rendering. A residual of 30-70 counts on a mid-tone scene means one side is still linear. A residual near 50-80 counts with an over-bright, low-contrast look means gamma got applied twice.

Then spot-check one pixel: the grey card should read 46 in the linear copy and 118 in the sRGB copy, within a count. If both checks pass, the capture path and the presentation path are both under control.

Prevent it: pin the colour space once

Make the colour space an explicit contract in your pipeline instead of an accident of whichever function drew last. Keep linear float values for every computation inside the pipeline; convert to sRGB exactly once at the display and report boundary; record the space in the file name or metadata, where a suffix such as _lin or _srgb prevents the next person from re-converting.

Add one unit test with a synthetic ramp: feed linear code 46 into your display converter and assert code 118 within ±1. The test takes a few lines and catches regressions, because SDK updates have shifted default output colour spaces between releases before, and your pipeline should survive that quietly.

When you specify new hardware, favour 3D sensors and cameras whose SDK documents its colour output and conversion options. The camera finder filters by the specs that drive the rest of the chain, and more capture-side fixes live on the 3D vision topic page.

Questions

The capture settings are identical, so how can the images differ?

Because the settings control capture, not presentation. Both images hold the same linear pixel values; the viewer applies an sRGB conversion when it draws to the screen, while a script typically writes or displays the raw buffer untouched. Identical exposure, gain and processing settings give identical linear data, so the brightness gap appears only at the display step, and that is where you fix it.

Does converting linear RGB to sRGB change exposure or the captured data?

No. The conversion runs on values the camera has already delivered; it is a per-pixel output transform, not a new capture. Exposure time, gain and projector or laser power stay exactly as set. Keep the linear buffer for measurements and point-cloud colour, and make a converted copy only for images a person will see or a report will show.

How can I tell whether a saved image is linear or sRGB?

Sample a mid-tone pixel that sits near 18% of full scale, or light a matte grey target to that level. A stored 8-bit code near 46 means the file is linear; a code near 118 means it is sRGB. The pair comes straight from the sRGB curve at 0.18, and any image editor's pixel readout shows it in seconds.

Should training images for a vision classifier be linear or sRGB?

Match the training data. Public image datasets are almost all sRGB-encoded, so a classifier trained on them expects sRGB inputs; convert your captures the same way before inference. If you train on your own photometric measurements such as reflectance ratios or HDR stacking, keep everything linear from sensor to network and never mix the two spaces in one dataset.

Is the gamma 2.2 approximation good enough instead of the exact sRGB curve?

For display copies, yes: the simple V^(1/2.2) curve lands within about one 8-bit count of the exact piecewise function through the mid-tones (117 versus 118 at linear 0.18). Use the exact formula when values must survive a round trip back to linear, for example when you undo the gamma to compute a reflectance ratio.

Why are the colours on my exported point cloud dark too?

Same cause, one step later: many exporters write the camera's linear RGB straight into the file, and viewers paint those values as if they were already sRGB. Run the colour channel through the sRGB curve before writing the file, and the point cloud will match the on-screen image you validated.