Why do images read with cv2.imread() look blue when shown by matplotlib?
answer
- OpenCV's channel order is not the usual one
- first channel is blue, third is red
- matplotlib and PIL assume the opposite
- one cvtColor call at the boundary
- COLOR_BGR2RGB, and keep it contiguous
basics
~10 scv2.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 sOpenCV'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 linesimport 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
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.
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.
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.
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