Image Fundamentals — Pixels, Channels and Tensors
How computers see images. Pixel values, colour channels, image tensors, normalisation, and the preprocessing pipeline every vision model expects.
You see a photo of a Shopify kurta. A computer sees a 3D array of integers — height × width × channels, each value between 0 and 255. Everything in computer vision starts from this representation.
A digital image is a grid of pixels. Each pixel is a tiny square of colour. For a 224×224 RGB image there are 50,176 pixels. Each pixel has three values — one for red intensity, one for green, one for blue — each ranging from 0 (none) to 255 (full intensity). The full image is represented as a 3D array: 224 rows × 224 columns × 3 channels = 150,528 numbers.
Every computer vision operation — loading, resizing, cropping, normalising, augmenting — is a transformation of this array. Every vision model — CNN, ViT, CLIP — takes this array as input. Getting the array into exactly the right shape, dtype, and value range before the model sees it is the preprocessing pipeline. Most computer vision bugs are preprocessing bugs — wrong channel order, wrong value range, wrong normalisation statistics.
Think of a crossword puzzle grid. Each cell has a letter. The grid has rows, columns, and one layer of content. An image is like three crossword grids stacked on top of each other — one grid for red intensity, one for green, one for blue. Each cell has a number 0–255 instead of a letter. Read all three grids together and you reconstruct the colour.
PyTorch uses channels-first format: (channels, height, width). Pillow and OpenCV use channels-last: (height, width, channels). Mixing these two formats silently produces garbage predictions — this is the single most common computer vision bug.
Pixels, channels, and image tensors — from file to array
Colour spaces — RGB, greyscale, HSV, and when each matters
RGB is the default but not always the best representation. Greyscale (single channel) halves memory and speeds up models when colour is not informative — document OCR, X-ray analysis, fingerprint matching. HSV separates hue (colour type), saturation (colour intensity), and value (brightness) — useful for colour-based detection where you want to detect "red objects" regardless of lighting. LAB separates luminance from colour and is perceptually uniform — used in medical imaging.
Resize, crop, and pad — getting every image to the same shape
Every model expects a fixed input size — ResNet expects 224×224, EfficientNet expects 380×380, ViT-B/16 expects 224×224. Real-world images come in all sizes and aspect ratios. Transforming them to the required size without distorting the content requires understanding the trade-offs between resize, centre crop, random crop, and padding.
Normalisation — why ImageNet statistics are used everywhere and when to recompute them
After converting to float32 and dividing by 255 (values in 0–1), you must normalise each channel to zero mean and unit standard deviation. Without normalisation the network's first layer receives values in [0, 1] — a very different distribution from what the pretrained weights expect. The result: predictions that look random even from a perfectly fine model.
For any model pretrained on ImageNet, use the ImageNet statistics: mean = [0.485, 0.456, 0.406], std = [0.229, 0.224, 0.225]. These numbers were computed over the entire 1.2M ImageNet training set. For custom datasets (medical images, satellite imagery, product photos) compute your own statistics — ImageNet numbers may be far off.
Production image preprocessing pipeline — from file on disk to model input
Every common image preprocessing mistake — explained and fixed
The bug report you actually get: training and serving disagree on what a pixel means
Every concept in this module — channel order, colour space, normalisation statistics — sounds like a one-time setup detail you get right in a Jupyter notebook and never think about again. In production it is not one pipeline, it is two: the training pipeline, written in Python and owned by the ML team, and the serving pipeline, which frequently gets re-implemented in a different language by a different team — a mobile app decoding a photo on-device, or a backend service written in Go or Java handling uploads. Those two pipelines drift apart silently, and the model has no way to tell you when they have.
A realistic version of this: the training pipeline loads images with PIL, which decodes to RGB and quietly respects the EXIF orientation tag most phone cameras write into the file. The serving path was built later by a mobile team using a native image decoder that does not apply that same EXIF rotation. Every photo taken with the phone held sideways now arrives at the model rotated ninety degrees relative to how equivalent training images looked. Nothing crashes. The tensor shape is correct, the dtype is correct, the value range is correct — accuracy just quietly drops for a slice of real-world traffic, and it can take weeks to trace the drop back to a decoder library nobody on the ML team ever looked at.
The fix is not "be more careful" — it is a concrete, automatable test: run the exact same input image through the training-side preprocessing function and the serving-side preprocessing function, and assert the resulting tensors match within a tight numerical tolerance. This is normally added as a check in CI for the serving code, run against a small fixed set of real sample images every time either pipeline changes, precisely because a rotation, a channel swap, or a wrong normalisation constant all pass every existing test that only checks tensor shape and dtype.
Five things people get wrong about image representation and preprocessing
RGB is simply the encoding a camera sensor and most display hardware happen to use — it is not a more fundamental representation of the underlying scene than HSV or LAB. Each colour space packages the same information differently for a different purpose: HSV separates hue from brightness so you can find "red objects" regardless of lighting, LAB separates luminance from colour and is perceptually uniform for medical imaging and colour correction, greyscale discards colour entirely because tasks like document OCR or X-ray analysis do not need it. Choosing RGB by default is correct for most deep learning models because that is what they were trained on — not because RGB is a more truthful representation of the image.
Shape is only one of three things a model expects to match: shape, dtype, and value distribution. A (3, 224, 224) float32 tensor with values in [0, 1] has the correct shape but the wrong distribution for any ImageNet-pretrained model, which expects roughly zero mean and unit variance per channel. As this module's errors section shows, skipping normalisation produces a model that predicts the same class for every input — not a crash, which would be easy to catch, but silent, confident, wrong output. Resize gets the geometry right; normalisation gets the statistics right, and a pretrained model needs both.
Both orderings describe the identical pixel data — the danger is exactly that they are interchangeable in shape but not in meaning. A (3, 224, 224) tensor and a (224, 224, 3) array can both pass a "is this a valid input shape" check while representing completely different things: three 224×224 channel planes versus 224 rows of 224 pixels with 3 values each. Feed one where the other is expected and most code does not error — it silently reinterprets 3 rows of a 224-wide image as 3 colour channels, or vice versa, producing garbled input that still has a valid tensor shape. This is why the module calls channel-order mismatches the single most common computer vision bug: it fails silently rather than loudly.
A pretrained model's first layer only checks tensor shape — it has no way to verify that your resize strategy, normalisation statistics, or train/validation transform split match what the model was actually trained on. This module's errors section covers a real case: applying RandomResizedCrop to a validation set instead of a deterministic Resize plus CenterCrop still produces a correctly-shaped tensor, but validation accuracy becomes inconsistent between runs because the input distribution silently changed. Correct shape is necessary but nowhere near sufficient — the preprocessing pipeline has to reproduce the exact statistics the model expects, not just its input dimensions.
The 2D-plus-channels structure is not cosmetic — it is the specific property that lets convolutional layers work at all. A convolution slides the same small filter across every spatial location, detecting a feature like an edge or a curve regardless of where it appears in the image; this only makes sense because neighbouring entries in the (height, width) grid are actually neighbouring pixels in the real image. Flatten that same image into a 1D vector of 150,528 numbers and a convolutional filter has nothing meaningful to slide across — position information collapses, and two pixels that were adjacent in the image can end up far apart in the vector. The array shape covered in this module is not bookkeeping; it is the substrate that gives a CNN's translation invariance its meaning.
Image fundamentals — 5 questions interviewers actually ask
PyTorch's convolution kernels are implemented to expect the channel dimension first for memory-layout and performance reasons on GPU; PIL and OpenCV inherited channels-last from how image file formats and most C image libraries store pixel data, row by row with interleaved channel values. Neither is more "correct" — they are different conventions for the same information. Mixing them is dangerous specifically because a transposed shape like (224, 224, 3) can be reinterpreted as (224 channels, 224 height, 3 width) by code that only validates tensor rank, not axis meaning — it doesn't crash, it silently treats 224 rows as 224 channels. The fix is always an explicit .permute(2, 0, 1) or .transpose when converting between the two, and checking shape assumptions by name, not just by count.
The pretrained weights, especially in the first few layers, were learned assuming inputs with roughly zero mean and unit standard deviation per channel — that is what T.Normalize with the ImageNet statistics produces. An image left at [0, 1] has a mean around 0.5 and a much smaller spread, so it lands in a different region of the input space than the one the first convolutional filters were tuned to respond to. The activations that follow are not necessarily huge or NaN — often they are just systematically off — and because the shift compounds through every subsequent layer, the final predictions collapse toward whichever class the model defaults to, which is why the practical symptom is "the model predicts the same class for every image" rather than an obvious crash.
A fully connected layer treats every pixel as an independent input feature: it learns a separate weight connecting each pixel to each output neuron, with no assumption that pixel (10, 10) and pixel (10, 11) are spatially related. A convolutional layer instead learns one small filter — say 3×3 — and reuses those same few weights at every position in the image, which is only meaningful because the image is stored as a spatial grid where adjacent array entries really are adjacent pixels. That weight sharing is what gives CNNs translation invariance: a filter that learns to detect an edge detects that edge wherever it appears, using orders of magnitude fewer parameters than a fully connected layer would need to learn the same pattern separately at every location.
Normalisation statistics define the input distribution the model's weights were optimised against; changing them between training and inference — or between training and validation — shifts every activation downstream in a way the model was never trained to compensate for, even though nothing about the code raises an error. Use ImageNet's mean and std whenever you are fine-tuning an ImageNet-pretrained model, since that is the distribution its weights already expect. Compute your own statistics over your training set when the domain is different enough that ImageNet's numbers no longer describe your data well — medical scans, satellite imagery, or product photos shot under unusual lighting are the common cases where recomputing mean and std measurably helps.
First, pick the colour space — RGB by default, unless the task is colour-irrelevant (documents, X-rays) where greyscale saves memory and compute, or colour-based detection where HSV is more robust to lighting. Second, pick a resize strategy matched to the use case — deterministic Resize plus CenterCrop for validation and inference, so metrics are reproducible, versus RandomResizedCrop for training, so the model sees the object at varying scales and positions. Third, always convert to RGB explicitly on load to handle RGBA PNGs and greyscale images consistently. Fourth, normalise with statistics matched to the model — ImageNet stats for a pretrained backbone, custom-computed stats for a sufficiently different domain. And finally, keep the training and validation transform pipelines separate, differing only in which steps are random, so a train/validation mismatch never becomes the reason accuracy numbers cannot be trusted.
Images are tensors. Next: multiply your training data without collecting a single new image.
You now understand the complete representation of an image and how to preprocess it for any vision model. The next challenge is data — vision models need thousands of labelled images but collecting and labelling them is expensive. Data augmentation synthetically multiplies your dataset by applying random transformations that preserve the label. Module 56 covers every augmentation technique used in production and explains exactly what each one teaches the model.
Flips, crops, colour jitter, mixup, cutout — and how each one affects what the model learns.
🎯 Key Takeaways
- ✓A digital image is a 3D array: (height, width, channels). RGB images have 3 channels, each pixel value 0–255. PyTorch uses channels-first format (C, H, W). PIL and OpenCV use channels-last (H, W, C). Mixing these formats silently produces wrong results — always permute when converting.
- ✓The standard loading pipeline: PIL Image.open().convert("RGB") → T.ToTensor() divides by 255 and converts to (C, H, W) float32 → T.Normalize() applies per-channel mean/std normalisation. Never skip the convert("RGB") call — PNGs have RGBA (4 channels) and greyscale images have 1 channel.
- ✓For any ImageNet-pretrained model always use ImageNet normalisation statistics: mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]. Skipping normalisation is the most common reason a pretrained model predicts the same class for every image.
- ✓Training and validation transforms are different. Training: RandomResizedCrop + RandomHorizontalFlip + ColorJitter + ToTensor + Normalize. Validation: Resize(256) + CenterCrop(224) + ToTensor + Normalize. Never apply random operations to the validation set — it makes metrics inconsistent.
- ✓Memory scales quadratically with image size. A batch of 32 images at 224×224 float32 = 38MB. At 384×384 it is 113MB. At 512×512 it is 200MB. Always check memory requirements before choosing image size. pin_memory=True and num_workers=4 are standard DataLoader settings for GPU training.
- ✓OpenCV loads images as BGR not RGB. Always convert after loading: cv2.cvtColor(img, cv2.COLOR_BGR2RGB). Or use PIL exclusively: Image.open(path).convert("RGB") always gives RGB. Swapped channels degrade colour-sensitive model performance and are notoriously hard to debug because the image looks correct when displayed.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.