System designHow-to

Convert recorded camera frames to BGR or RGB for OpenCV

Convert recorded frames to BGR for OpenCV: request BGR8 at capture or convert saved RGB buffers with cv2.cvtColor, plus the byte layout and a swap test.

LumaSpec Vision9 min read Share on LinkedIn

On this page 9 sections
  1. 01OpenCV expects BGR: three bytes per pixel, rows packed
  2. 02Decide once: request BGR8 at capture, convert only what is saved
  3. 03What you need
  4. 04Convert a recorded frame: the steps
  5. 05Worked example: a 1920 × 1080, 30 fps RGB8 recording
  6. 06Formats that are not interleaved RGB
  7. 07Common mistakes and the checks that catch them
  8. 08Choosing a camera that never needs the swap
  9. 09Questions

A recording or a delivered buffer hands you bytes, not images. OpenCV wants them as BGR — blue first, eight bits per channel, rows packed with no gaps — and everything downstream, from cv2.imshow to cv2.VideoWriter to most detection code, assumes exactly that. Get the order wrong and the pipeline still runs, but red parts read as blue, and nobody catches it until a customer does. The fix takes one decision and one call. If you still control the capture, request an 8-bit packed BGR format in the stream-configuration call, and live frames need no conversion at all. If the frames are already recorded, convert each one with a single cv2.cvtColor swap. This guide gives you the byte layout behind the RGB8 and BGR8 names, six steps with a test that proves which order a saved buffer is in, and the per-frame arithmetic for a full-HD, 30 fps recording. The steps are identical on Windows and Linux.

OpenCV expects BGR: three bytes per pixel, rows packed

cv2.imread, cv2.imshow and cv2.VideoWriter treat a (height, width, 3) uint8 array as BGR: channel 0 is blue, channel 1 is green, channel 2 is red. Most annotation and training tools expect RGB instead. That mismatch is the entire problem — the data is identical, only the byte order differs, and nothing inside the array tells you which order it is.

The names you meet at capture time come from the GenICam pixel-format naming convention (PFNC), the name set GigE Vision cameras publish and many SDKs reuse. RGB8 means three interleaved bytes per pixel with red at the lowest address; BGR8 is the same layout with blue at the lowest address. Both are packed, so no padding sits between rows: the row stride is width × 3 bytes, and one frame is a single contiguous run of width × height × 3 bytes. Some SDKs add their own prefix to the standard name; match on the RGB8 or BGR8 part.

row stride = width px × 3 bytes per pixel (RGB8 / BGR8, packed) bytes per frame = width px × height px × 3

For the wider path from a delivered buffer to a processing-ready array, see the raw-buffer-to-image walk-through. This guide stays on the channel-order question.

Decide once: request BGR8 at capture, convert only what is saved

If you configure the stream yourself, make the conversion disappear at the source. The stream-configuration call in your SDK takes the stream, width, height, pixel format and frame rate; pass BGR8 for the colour stream and every delivered frame reshapes straight to (height, width, 3) with no swap. That is the answer for any camera you still control, including the colour stream that ships alongside depth data on most 3D sensors.

For a recording, a saved bag of frames, or a library that fixes RGB8, convert per frame instead:

# rgb: (height, width, 3) uint8 ndarray in RGB order
bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)   # channels 0 and 2 trade places

The format you request costs bandwidth, though: three bytes per pixel instead of the one an 8-bit Bayer or mono stream carries. On a GigE link that factor of three can be the difference between full frame rate and drops — the GigE BGR-versus-Bayer frame-rate guide shows when it bites.

What you need

  • The frames themselves: raw single-frame dumps from a recording, a saved bag of frames, or buffers your SDK has already delivered. A compressed video file is different — cv2.VideoCapture decodes it to BGR for you, so this guide is about raw frames.
  • The width and height in pixels you configured at capture, for example 1920 × 1080.
  • The format name from the capture session — RGB8, BGR8, a Bayer pattern or a YUV variant. If nothing recorded it, the one-pixel test in step 5 decides for you.
  • Python 3 with numpy and OpenCV: pip install numpy opencv-python.

Convert a recorded frame: the steps

  1. Read the format name from the capture script or the recording metadata. RGB8 or BGR8: continue. A Bayer pattern or a YUV variant: jump to the format table below, because reshaping those to (height, width, 3) produces noise.

  2. Wrap one frame's bytes and check the size:

import numpy as np
import cv2

width, height = 1920, 1080                  # int, pixels: your capture settings
buf = open("frame_0001.bin", "rb").read()   # bytes: one raw frame from the recording

raw = np.frombuffer(buf, dtype=np.uint8)    # uint8 ndarray, read-only
assert raw.size == width * height * 3       # 6_220_800 at full HD: 8-bit interleaved colour
  1. Reshape to an image. Rows are packed, so the reshape needs no stride argument:
frame = raw.reshape(height, width, 3)       # (1080, 1920, 3) uint8, rows contiguous
  1. Swap the order if the file stores RGB:
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)   # new (1080, 1920, 3) array; input untouched
  1. Prove the order once per file format. Record a plain red target, or pick any object in the recording whose colour you know, and print one pixel from it:
print(frame[540, 960])   # uint8 triplet from the middle of the red target

A reading like [30 25 235] — highest value last — means channel 2 holds red: the recording is BGR8 and step 4 is unnecessary. A reading like [235 25 30] — highest value first — means channel 0 holds red: the recording is RGB8 and every frame needs the swap. Display works too: with cv2.imshow, a red target that shows blue is the RGB case.

  1. Hand frame_bgr to whatever runs downstream. cv2.imshow, cv2.VideoWriter, template matching and most cv2 functions assume BGR; keep the unswapped frame only for tools that genuinely want RGB.

Worked example: a 1920 × 1080, 30 fps RGB8 recording

Inputs: width 1920 px and height 1080 px at 30 fps — the stream configuration; format RGB8 at 8 bits per channel, interleaved and packed — the PFNC definition, which fixes 3 bytes per pixel.

The rule in words: a packed interleaved 8-bit frame carries three bytes per pixel with contiguous rows, so bytes per frame = width × height × 3.

Arithmetic: 1920 px × 1080 px = 2,073,600 px. At 3 bytes/px, one frame is 2,073,600 × 3 = 6,220,800 bytes, about 6.2 MB. Converting reads the input and writes the output: 6,220,800 × 2 = 12,441,600 bytes of memory traffic per frame. At 30 fps: 12,441,600 × 30 = 373,248,000 bytes/s ≈ 373 MB/s.

Result: every converted frame is a (1080, 1920, 3) uint8 array of exactly 6,220,800 bytes, and full-rate conversion adds about 373 MB/s of traffic — roughly 187 MB/s reading and 187 MB/s writing.

Decision: 373 MB/s is under 2% of a 20 GB/s desktop memory path, so on a PC, convert freely for debugging and data preparation. On an embedded controller that also runs detection, request BGR8 at capture and keep the conversion for saved files — at 30 fps that traffic competes with detection for the same memory, and that is the difference between frame budget and dropped frames. If saved-frame playback drops before any conversion enters the picture, the bag-playback recovery guide walks the offline path.

Formats that are not interleaved RGB

A size mismatch at step 2 means the recording is not 8-bit interleaved colour. Match the name to the layout before reshaping:

Format name Bytes per pixel Layout Handling
RGB8, BGR8 3 interleaved, packed rows reshape to (height, width, 3); swap channels if the order differs
Bayer RGGB, GRBG, BGGR, GBRG 1 colour mosaic, one sample per pixel demosaic before any colour math; the packed-Bayer conversion guide covers packed 12-bit data
YUV 4:2:2, for example YUYV 2 luma for every pixel, chroma shared by pixel pairs convert through the SDK's YUV path or the YUYV-to-RGB decode walk-through
Planar colour (the name says planar) 3 three separate planes, one per channel stack the planes into (height, width, 3), then swap with cvtColor
Mono8 1 grayscale use as-is; cv2.COLOR_GRAY2BGR replicates the channel for display only

Two recording-time traps sit behind most wrong-format surprises. A stream configured for a colour format can deliver mono8 anyway when the encoder falls back — the mono8 fallback fix shows how to catch it and hold the format you asked for. And a 12-bit camera stores packed data at 1.5 bytes per pixel (three bytes per two pixels), a size that fits neither width × height × 3 nor width × height × 1.

Common mistakes and the checks that catch them

  • Trusting the format label instead of running the one-pixel test. Names get copied between capture scripts; one red-target test per file format settles the order for good.
  • Deleting the size assertion once things work. When a camera is swapped or a format changes upstream, raw.size is the first thing that tells you — keep the check in production paths.
  • Converting twice from different modules. One swap per frame, in one helper function; pass the result everywhere else.
  • Feeding RGB straight to cv2.imshow to save a call. Display and writing functions assume BGR, so the colours invert and the inspection keeps running on wrong data.

More capture-and-format fixes sit on the systems topic page.

Choosing a camera that never needs the swap

If you are specifying hardware rather than rescuing a recording, the colour models in the area scan cameras range publish the standard PFNC names, so BGR8 is a stream-configuration choice rather than a post-processing step. The camera configurator filters by interface, resolution and shutter for a shortlist, and the contact page reaches the team for format questions before you order. Keep the one-line conversion anyway: recordings from other sites and tools will not all share your byte order.

Questions

Do I still need cvtColor if I read the recording with cv2.VideoCapture?

No. cv2.VideoCapture decodes video files and streams into BGR arrays by default, so every frame from .read() is already in OpenCV's order. The conversion in this guide is for raw recorded buffers — single frames dumped as bytes — where no decoder has made the choice for you. Check frame.shape first: (height, width, 3) uint8 means you can skip the swap and start processing.

How many bytes is one 1920 × 1080 BGR frame?

1920 px × 1080 px × 3 bytes per pixel = 6,220,800 bytes, about 6.2 MB, because 8-bit interleaved colour stores three bytes per pixel. The same pixel count is 2,073,600 bytes in mono8 and about 4,147,200 in YUV 4:2:2, so a size mismatch in your reshape is the fastest clue that you wrapped the wrong format.

Can I swap channels with numpy slicing instead of cv2.cvtColor?

You can write frame[:, :, ::-1], but that returns a view with negative strides rather than a copy, and many cv2 calls reject non-contiguous input. If you slice, wrap the result in np.ascontiguousarray before it reaches OpenCV. cvtColor is one call, costs the same read-and-write as any conversion, and states the intent in its name.

Why does my recording play back blue where it should be red?

Red appears blue when the file stores RGB order: the red channel sits in byte 0, which a BGR-assuming player renders as blue. Run the one-pixel test on a frame that shows an object you know is red. A high first value means RGB storage, and one cvtColor swap per frame fixes the whole file; a high last value means it is already BGR.

Does conversion cost scale with sensor resolution?

Linearly with pixel count. A full-HD frame is 6,220,800 bytes; the same three-bytes-per-pixel arithmetic at a 5 MP format of 2448 × 2048 gives 15,040,512 bytes per frame, about 2.4 times as much to read, write and store. Budget recording disk and memory from that ratio rather than from the megapixel figure alone.

What if my SDK has no BGR8 format at all?

Request RGB8 and convert with the one-line swap, or use whatever interleaved 8-bit colour format the SDK offers — the reshape, the size check and the one-pixel test all work unchanged. Skip planar colour formats for OpenCV work: stacking the three planes back together adds a step and a full extra frame copy to every image.