Cameras & sensorsTroubleshooting

Camera color wrong in saved images but right in the viewer

Color looks right in the camera viewer but wrong in saved images: how to test green tint, swapped channels, byte order and white balance you apply yourself.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01The preview is a rendering, not the data
  2. 02Four failure modes, four signatures
  3. 03Worked example: 1456 × 1088 frame, green when saved
  4. 04Byte order comes from the format name
  5. 05Apply the balance ratios in your own code
  6. 06Checks before you blame the camera
  7. 07Isolate the camera with a swap test
  8. 08Make the fault impossible to reintroduce
  9. 09Questions

You balance a color camera, the preview tool that came with the SDK shows believable grays, and the frame your code saves or streams arrives green, or with red and blue exchanged. The sensor is rarely at fault. The preview is a rendering: it reads the pixel format from the node map, demosaics raw data, converts to the display order and applies the camera's white balance ratios. The buffer handed to your code has none of that applied. This guide separates the four real causes — balance that never reached the bytes, channel order, a missing demosaic and a misread buffer — and gives a test, the arithmetic and the fix for each.

The preview is a rendering, not the data

A viewer earns its keep by making any camera look right on screen. To do that it reads the camera's PixelFormat node, demosaics when the value is a Bayer format, converts to whatever order the display surface wants, and multiplies in the BalanceRatio values for show. Your save and stream paths receive the buffer as it left the camera: raw, in wire order, with the balance attached only as numbers in the node map.

That split produces the classic complaint — colors right in the viewer, wrong in the file — and the file is byte-identical to the streamed buffer, so both go wrong together. It also produces stranger ones. In one field case the preview's green plane was byte-identical to its blue plane, so the "correct" colors on screen were a rendering accident, while the file carried the true, unbalanced green. The file was telling the truth; the preview was not.

The working rule follows: channel statistics measured on the buffer your code receives are the only ground truth. The preview is a convenience. The format node decides everything downstream, which is also why a GigE camera shows no color until the pixel format is fixed; for how a Bayer sensor builds color at all, see color filter arrays, monochrome versus Bayer.

Resampling is another silent viewer transform: why saved pixel dimensions can differ from the preview.

Four failure modes, four signatures

What you see Ten-minute test Fix that holds
Uniform green or magenta tint, one channel dominant Mean of each plane over a neutral gray target Apply the balance gains in your pipeline, or switch to a camera-converted RGB format
Red and blue exchanged Swap the two planes in code; the image snaps correct Convert using the order the format name states
Color mosaiced into stripes, detail wrong everywhere Compare received bytes with width × height × bytes per pixel Demosaic the raw Bayer data before anything else
Two channels exactly equal at every pixel Byte-compare the planes Fix format or stride interpretation; no color control can help

Order matters. Run the byte-count test first, because a misread buffer makes every other test meaningless. Then the plane-equality test, then the swap, and only then balance.

Worked example: 1456 × 1088 frame, green when saved

The inputs, each from a field case: the saved frame measures 1456 × 1088 pixels; the node map reports a 3-byte-per-pixel RGB-family format; the camera's manual gives BGR byte order at that output; the preview looked balanced; the saved file — byte-identical to the streamed buffer — carried a green cast with red still reading plausibly.

pixels = width × height = 1456 × 1088 = 1,584,128 expected_bytes = pixels × bytes_per_pixel 3 bytes per pixel (RGB8, BGR8) → 1,584,128 × 3 = 4,752,384 bytes 1 byte per pixel (BayerRG8, Mono8) → 1,584,128 bytes

Compare the buffer you actually receive. At 4,752,384 bytes the stream is three bytes per pixel, so a missing demosaic is off the table and byte order plus balance are the suspects. At 1,584,128 bytes you are holding single-byte Bayer data, and the first fix is a demosaic, not a color control.

Then byte-compare the planes. In the field case the preview's green plane matched its blue plane at all 1,584,128 positions — zero differences. That is a duplicated channel in the display path, so the balanced look on screen was an accident of rendering. The saved frame told the real story: green far above red and blue across a white patch, red plausible. The balance ratios had never reached the bytes.

The decision: with two channels identical in the preview and green dominant in the file, stop adjusting balance in the viewer and fix the pipeline — read the ratios, apply the gains from the balance section below in your code, and accept only when the neutral-target plane means land within 5 DN of each other. Because the file and the streamed buffer are identical, the fix belongs in your acquisition code, not in the save function.

Byte order comes from the format name

The name is the contract. In an RGB8 buffer the first byte of each pixel triplet is red; in BGR8 it is blue. The native pixel formats reference states the principle directly: the byte order within the stream is given by the pixel format's name. BayerRG8 names its tile the same way — the first row starts red-green, the second green-blue.

Cameras disagree with host toolkits more often than you would hope. Some camera manuals state BGR at the digital output while most host libraries default to RGB; one field case turned on exactly that line of the manual. Read the manual's pixel-format section before you assume, then read the PixelFormat node back after you set it.

Two related reads: why red and blue channels end up swapped covers the pure order fault, and BGR versus Bayer pixel formats on GigE shows the same node driving frame rate. When you record frames for OpenCV, converting them to BGR or RGB explicitly keeps the order visible in code instead of hidden inside a writer.

Apply the balance ratios in your own code

BalanceWhiteAuto writes per-channel ratios into the node map; it does not edit raw buffers. If you stream Bayer, the multiplication is yours.

gain_R = ratio_R / ratio_G gain_B = ratio_B / ratio_G R_out = R × gain_R B_out = B × gain_B G_out = G

Green normalized to 1.0 keeps the image's overall level where the camera put it.

  1. Fill the center of the field of view with a neutral gray target under production lighting.
  2. Run the auto routine once, then read BalanceRatio for each setting of BalanceRatioSelector — Red, Green, Blue — and store the three numbers.
  3. Apply the gains after demosaic, before any compression or display.
  4. Accept when the three plane means over the target sit within 5 DN of each other, about 2 % of an 8-bit scale.
# Names in quotes are the standard GenICam nodes; read_node and write_node
# stand in for your SDK's node-map accessor.
fmt: str = read_node("PixelFormat")           # e.g. "BGR8" or "BayerRG8"
ratios: dict[str, float] = {}
for selector in ("Red", "Green", "Blue"):
    write_node("BalanceRatioSelector", selector)
    ratios[selector] = float(read_node("BalanceRatio"))
gain_r: float = ratios["Red"] / ratios["Green"]
gain_b: float = ratios["Blue"] / ratios["Green"]

The test that proves it: after the gains, abs(mean(R_out) - mean(G_out)) <= 5 and the same for blue, computed over the target region. Ratios drift while auto runs continuously — auto white balance drift between frames measures it — so run once and lock the values for repeatable color.

Checks before you blame the camera

  • Save a BMP first. It stores the bytes you hand it; a JPEG encoder assumes an order and subsamples chroma. If the BMP is right and the JPEG is wrong, the order you passed was wrong.
  • Two switches carry similar names. The acquisition driver's color-format option and the camera's PixelFormat node are separate settings; in the field case, changing the driver option alone changed nothing on screen or in the file. Set the camera node, read it back, then match the driver to it.
  • Check ExposureTimeSelector. Left pointing at a single color channel, it integrates that channel for a different time and the tint survives every balance attempt. Point it at the common entry your camera lists.
  • Mind bit depth. Packed 10- and 12-bit data read as 8-bit produce a clipped, tinted mess; 12-bit data in a 16-bit V4L2 frame walks through the same class of misread.
  • Mind stride. Padding lines and row padding shift every plane; when the buffer is taller than you set shows where they come from.

Isolate the camera with a swap test

When the fault survives every fix above, change one variable: the camera. Put a known-good color camera at the same working distance, aim it at the same gray target and run the balance test from the previous section. A pass clears your pipeline and points at the original camera's configuration; an identical failure convicts your code.

A compact color camera with a global shutter makes a good reference unit. The MV-CU020-90GC is a 2.5 MP color GigE camera on a 1/1.7" sensor with a C-mount.

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

For more resolution the MV-CS200-10GC pairs 20 MP with a C-mount, and on the vision-sensor side the MV-SC3050XC-16M-ATN-H is a 5 MP color unit on Fast Ethernet.

Area scan camerasHikrobot MV-CS200-10GC 20 MP Area Scan Camera, GigE, IMX183, Color, BasicMV-CS200-10GC
Sensor format
1"
Lens mount
C-mount
$739.00View part
Smart cameras & vision sensorsHikrobot MV-SC3050XC-16M-ATN-H 5 MP color vision sensorMV-SC3050XC-16M-ATN-H
Interface
Fast Ethernet
Resolution
5 MP
$1,809.00View part

Browse color area scan cameras or smart cameras and vision sensors, or filter directly in the camera finder and the camera step of the configurator.

Make the fault impossible to reintroduce

  • Log PixelFormat, Width and Height at session start; an unlogged format is an argument waiting to happen.
  • Assert that received bytes equal width × height × bytes per pixel before the first frame is processed.
  • Run auto balance once, lock the ratios, and store the three gains with the recipe.
  • Convert in exactly one place — camera-side RGB or host-side Bayer, never both.
  • Keep BMP as the forensic save format; compress only after conversion.
  • Add a startup self-test: neutral target, plane means inside the acceptance band from the balance section.
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

If a reference camera passes and your production camera still tints, collect PixelFormat, the three ratios and one BMP and send them through the contact page — that packet answers in one reply what a week of screenshots cannot. The cameras topic page collects the related guides.

Questions

Does the camera's auto white balance change the pixels it streams?

Only in formats the camera converts itself. If PixelFormat is an RGB or YUV output, the balance is already multiplied into the bytes before they leave the camera. If PixelFormat is a raw Bayer format, the streamed bytes are untouched and the ratios sit in the node map as BalanceRatio values for the host to apply. That split is why a preview can look balanced while your saved buffer does not.

Why save a BMP while debugging color?

Because a BMP stores the bytes you hand it with no chroma subsampling and no order assumption of its own, while a JPEG encoder assumes an order and subsamples chroma. If the BMP comes out right and the JPEG does not, the encoder received the wrong order. Save BMP while debugging, fix the order at the conversion, then return to your production format.

How do I tell a duplicated channel from a white balance error?

Compare two planes byte for byte. A white balance error multiplies a channel: every value shifts by a factor, so the planes stay different. A duplicated channel is exact equality at every pixel. In one field case the preview's 'correct' image had green identical to blue at all 1,584,128 positions of a 1456 × 1088 frame — a rendering fault, not a balance fault. No balance setting can turn two equal planes into correct color.

Which PixelFormat should I stream for color inspection?

Stream RGB8 when the camera must deliver finished color and the link has headroom: at 1456 × 1088 that is 4,752,384 bytes per frame. Stream raw BayerRG8 when you want to own the balance and demosaic on the host: 1,584,128 bytes per frame, one third the size, with the ratios applied in your code. Decide once, then log the format with every session.

Can a bad cable tint my colors?

Not as a uniform tint. A digital link either delivers bits or drops them: damage shows as torn frames, black lines or lost frames, not a green cast. A tint that holds frame after frame is an interpretation problem in software. Replace the cable when you see tearing; chase byte order and balance when you see stable color shifts.