skip to content

Image I/O and Color

Reading and writing images and video, and the colour-space conversions that follow — starting with OpenCV's BGR channel order, which surprises nearly everyone once. Also covers HSV and LAB and when each is the better space to work in.

on this pageshow

questions

6

Why do images read with cv2.imread() look blue when shown by matplotlib?

level: juniorimportance: must knowfreq 88%

answer

  1. OpenCV's channel order is not the usual one
  2. first channel is blue, third is red
  3. matplotlib and PIL assume the opposite
  4. one cvtColor call at the boundary
  5. COLOR_BGR2RGB, and keep it contiguous

basics

~10 s

cv2.imread() returns pixels in BGR channel order, while matplotlib's imshow expects RGB. The first and third channels are interpreted swapped, so reds render as blues. Convert first with cv2.cvtColor(img, cv2.COLOR_BGR2RGB).

solid answer

~40 s

OpenCV's whole Python surface is BGR: `cv2.imread`, frames from `VideoCapture`, and what `cv2.imwrite`/`cv2.imshow` expect. Almost everything else in the Python ecosystem — matplotlib, Pillow, most model preprocessing — is RGB. Nothing raises when the two meet; the array shape and dtype are identical, so you only notice that faces look blue and skies look orange. The fix is one explicit conversion at the boundary: `rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)`, which returns a contiguous copy. The NumPy trick `img[:, :, ::-1]` swaps the channels too, but produces a reversed-stride view that some OpenCV calls reject, so wrap it in `np.ascontiguousarray` if you keep using it in cv2. The practical discipline is to keep BGR everywhere inside OpenCV code and convert exactly once, at the point where the array leaves for a non-OpenCV consumer.

code

python · 11 lines
python
import cv2
import numpy as np

bgr = cv2.imread("photo.png")            # (H, W, 3) uint8, BGR
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)

print(bgr[0, 0])   # e.g. [ 32  70 180 ] -> B=32, G=70, R=180
print(rgb[0, 0])   # e.g. [180  70  32 ] -> R=180, G=70, B=32

# cheap but non-contiguous; cv2 functions reject it as-is
flipped = np.ascontiguousarray(bgr[:, :, ::-1])

go deeper

for a junior

Just be able to say it out loud: cv2.imread gives BGR, matplotlib wants RGB, so call cv2.cvtColor with cv2.COLOR_BGR2RGB before plotting. Recognising the blue-face screenshot on sight is most of the credit here.

for a middle

Explain that the mismatch is silent because shape and dtype are identical, and that OpenCV is self-consistent end to end — imread, imshow and imwrite all speak BGR. Mention that img[:, :, ::-1] gives a non-contiguous view that cv2 functions reject.

for a senior

Show where you put the boundary in a real pipeline and how you keep it: convert once at the edge, name variables bgr/rgb, and pin the contract with a reference-image test. Be ready to explain how a missed swap degrades a pretrained model's accuracy without ever raising.

for a principal

Own the convention across services and teams — where in the architecture colour order is normalised, whether ingest or inference converts, and how training and serving preprocessing stay provably identical. Argue the cost of a per-call convert versus decoding directly with IMREAD_COLOR_RGB at the read.

## What actually happens `cv2.imread("photo.png")` returns a NumPy array of shape `(height, width, 3)` and dtype `uint8`. The three values at `img[y, x]` are **blue, green, red** — in that order. Matplotlib's `imshow`, Pillow, torchvision and virtually every deep-learning preprocessing pipeline assume that the same three values mean red, green, blue. Because the array's shape, dtype and value range are identical either way, no library can detect the mismatch. The channels are simply reinterpreted: a warm skin tone (high red) is read as high blue and comes out looking like a blue-tinted corpse; an orange sunset turns teal. ## Why OpenCV is BGR The order is historical, not technical. OpenCV's C API dates from the late 1990s, when Windows BMP files and many frame grabbers stored pixels blue-first in memory. The library adopted that layout, and by the time the rest of the ecosystem standardised on RGB, changing it would have broken every existing program. So BGR remains the internal convention, and OpenCV's own display and file functions are self-consistent: `cv2.imshow(win, img)` on a BGR array shows correct colours, and `cv2.imwrite("out.png", img)` on the same array writes a correct file. The bug only appears at the seam with a non-OpenCV consumer. ## Where the seam is Everything that comes **out of** OpenCV is BGR: `imread`, `imdecode`, frames from `VideoCapture.read()`, the output of `cvtColor(..., COLOR_HSV2BGR)`. Everything that goes **into** OpenCV's writers is expected to be BGR: `imwrite`, `imshow`, `VideoWriter.write()`. So if you load with Pillow (RGB) and save with `cv2.imwrite`, you get the same bug in the opposite direction — a correct-looking array written to a wrong-looking file. ## The conversions - `cv2.cvtColor(img, cv2.COLOR_BGR2RGB)` — the canonical fix. It allocates a new contiguous array with the channels reordered. `cv2.COLOR_RGB2BGR` performs the identical byte swap; the two names document intent rather than doing different work. - `img[:, :, ::-1]` — a NumPy view with a negative stride on the last axis. It costs nothing, but it is **not contiguous**, and OpenCV's bindings require the last-dimension step to equal the element size. Passing such a view into a cv2 function raises a layout error. `np.ascontiguousarray(img[:, :, ::-1])` makes it safe, at the same cost as `cvtColor`. - `cv2.imread(path, cv2.IMREAD_COLOR_RGB)` — available in OpenCV 4.10 and later, including OpenCV 5. It decodes straight to RGB, which removes the conversion step entirely when the image is only ever going to a non-OpenCV consumer. ## Why this matters beyond screenshots A blue-looking plot is embarrassing but harmless. The dangerous version is a model pipeline. If a classifier was trained on RGB tensors (the normal case for torchvision or Keras pretrained weights) and your serving code loads frames with OpenCV and forgets the conversion, the network receives systematically wrong inputs. It will not crash and it will not produce garbage — it produces *slightly worse* predictions, which is far harder to notice in a metrics dashboard than a crash. The same applies to the mean/std normalisation constants published with pretrained models: they are per-channel and ordered RGB. Channel order also silently changes grayscale conversion. `cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)` uses luminance weights of roughly 0.299 for red, 0.587 for green and 0.114 for blue. Feed it an RGB array and red and blue get each other's weights, so the resulting gray image is subtly but genuinely different — enough to move a threshold or an edge map. ## The discipline Pick a boundary and state it in the code. Inside OpenCV processing, everything is BGR and no conversion happens. At the edge — plotting, feeding a model, handing the array to PIL — convert exactly once, and name the variable accordingly (`bgr`, `rgb`). Converting twice is as wrong as converting zero times, and a variable called `img` tells the next reader nothing.

  • Is cv2.COLOR_RGB2BGR different from cv2.COLOR_BGR2RGB?
    No — both perform the same swap of the first and third channels, and OpenCV implements them identically. The two names exist so the code documents which direction you believe you are going. Choosing the wrong one has no effect on the output; the real bug is applying the conversion twice, or not at all.
  • What breaks if I use img[:, :, ::-1] instead of cvtColor?
    Nothing visually — the channels really are reversed. But the result is a view with a negative stride on the last axis, and OpenCV's Python bindings require the innermost step to equal the element size, so passing it back into a cv2 function raises a layout error. Wrap it in np.ascontiguousarray, or just use cvtColor, which returns a contiguous copy.
  • How would you catch a BGR/RGB mismatch in a serving pipeline before users do?
    Assert the contract at the boundary rather than eyeballing images: name variables bgr/rgb, and add a test that runs one fixed reference image through the full preprocessing path and compares the resulting tensor against a stored expected tensor. A channel swap changes those numbers immediately, whereas an accuracy dashboard may only sag by a point or two.

saying these in an interview costs you the question

  • Claiming cv2.imread returns RGB like every other library
  • Thinking OpenCV raises or warns on a channel-order mismatch
  • Converting to RGB and then saving with cv2.imwrite anyway
  • Believing COLOR_BGR2RGB and COLOR_RGB2BGR do different things
  • Assuming BGR only affects display, not model accuracy

context

open as a page

What range does the hue channel use after cv2.cvtColor(img, cv2.COLOR_BGR2HSV)?

level: middleimportance: must knowfreq 60%

basics

~20 s

For an 8-bit image, OpenCV stores hue as 0-179, halving the usual 0-360 degrees so it fits in a byte; saturation and value use 0-255. Copying hue values from a colour picker that reports 0-360 selects the wrong colour band.

open as a page

What does cv2.imread() return when it cannot read the file, and why?

level: juniorimportance: should knowfreq 56%

basics

~20 s

cv2.imread() returns None on any failure — missing path, unreadable permissions, corrupt data, or an unsupported format. It never raises. The error surfaces later, usually as 'NoneType' object has no attribute 'shape', so check for None immediately after the read.

open as a page

Which image data does cv2.imread() discard by default, and how do you keep it?

level: middleimportance: should knowfreq 44%

basics

~10 s

The default flag cv2.IMREAD_COLOR always yields a 3-channel 8-bit BGR array: it drops any alpha channel and truncates 16-bit files to 8 bits. cv2.IMREAD_UNCHANGED returns the data as stored, alpha and bit depth included.

open as a page

When should you use cv2.split() instead of NumPy channel indexing on an image?

level: middleimportance: should knowfreq 38%

basics

~20 s

cv2.split() allocates a separate copy of every channel, so use it only when you need standalone single-channel arrays. NumPy indexing like img[:, :, 0] returns a view with no copy — cheaper for reading, and writes through to the original image.

open as a page

When would you convert to LAB rather than HSV in an OpenCV pipeline?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Use LAB when the task is about perceived difference or about changing brightness without shifting colour: its L axis is lightness and a/b are colour-opponent axes, and distances approximate perceived difference. HSV is better for carving out a band of the colour wheel with cheap thresholds.

open as a page