TAI BUI
Back to curriculum
LearnPython

Image Fundamentals — Pixels, Channels, Color Spaces

An image is a tensor of light samples. Every vision model you'll ever use starts from this one fact.

TIME: ~45 minutesPREREQUISITES: Phase 1 Lesson 12 (Tensor Operations), Phase 3 Lesson 11 (Intro to PyTorch)

Learning Objectives

  • Explain how a continuous scene gets discretized into pixels and why sampling/quantization decisions set the ceiling on every downstream model
  • Read, slice, and inspect images as NumPy arrays and switch fluently between HWC and CHW layouts
  • Convert between RGB, grayscale, HSV, and YCbCr and justify why each color space exists
  • Apply pixel-level preprocessing (normalize, standardize, resize, channel-first) exactly as torchvision expects it

The Problem

Every paper you'll read, every pretrained weight you'll download, every vision API you'll call assumes a specific encoding of the input. Pass a uint8 image where the model wants float32 and it still runs, just silently producing garbage. Feed BGR to a network trained on RGB and accuracy collapses by ten points. Hand a model channels-last input when it expects channels-first, and the first conv layer treats height as a feature channel. None of this throws an error. It just ruins your metrics while you spend a week hunting for a bug that actually lives in how you loaded the file.

A convolution isn't complicated once you know what it's sliding over. The hard part is that "an image" means different things to a camera, a JPEG decoder, PIL, OpenCV, torchvision, and a CUDA kernel. Each stack has its own axis order, byte range, and channel convention. A vision engineer who can't keep them straight ships broken pipelines.

This lesson fixes the foundation so the rest of the phase can build on it. By the end you'll know what a pixel is, why there are three numbers per pixel instead of one, what "normalize with ImageNet stats" actually does, and how to move between the two or three layouts that every other lesson in this phase will assume.

Warm-up0/4
1. A digital photo stores each pixel's color as three numbers, one for red, one for green, one for blue. If a program swapped which number it treated as "red" and which it treated as "blue" before displaying the photo, what would you expect to see?Warm-up
A color swatch with its red, green and blue numbers labeled, and an arrow to a hidden swatch after a program swaps which number is called red and which is called blue.
Example values, made up to illustrate.
2. A spreadsheet lists rows by day and columns by product, so cell (day 3, product 5) has one specific meaning. If someone hands you the same numbers relabeled so rows are products and columns are days, does reading position (3, 5) still point at the same fact?Warm-up
A grid of numbers with one cell marked at row 3, column 5, shown with two different row and column labelings below it: one calling rows Day, the other calling rows Product.
3. A NumPy array `a` has shape (4, 5, 3). What is the shape of `a.transpose(2, 0, 1)`?Warm-up
A 3-axis NumPy array labeled axis 0 size 4, axis 1 size 5, axis 2 size 3, with the call a.transpose(2, 0, 1) and the resulting shape left blank.
4. An unsigned 8-bit integer can hold how many distinct values?Warm-up
Two rows of 8 bit cells, the all-zero pattern labeled smallest and the all-one pattern labeled largest, asking how many total patterns of 8 bits exist.
Check: The Problem0/2
1. Why is feeding BGR pixel order into a network trained on RGB such a costly bug?Explain
A model expecting channel order R G B next to a loader handing it channel order B G R, with an arrow between them and no indication of what happens as a result.
2. A training script runs without any error, but metrics are far below the model card. The loader returns arrays shaped (H, W, 3) and the model expects channels first. Which mechanism fits the symptoms?Analyze
A loader producing shape H, W, 3 feeding directly into a conv layer that expects shape C, H, W, with the result left as a question mark.

The Concept

The full preprocessing pipeline at a glance

Every production vision system is the same sequence of reversible transforms. Get one step wrong and the model sees a different input than it was trained on.

Mermaid diagram

The two red and blue boxes are where 80% of silent failures live: missing standardization and wrong layout.

A pixel is a sample, not a square

A pixel is a single measurement, not a coloured square with area: one sample of light intensity at one location on a grid. Resize or rotate an image and what you're actually doing is resampling that measurement grid.

That measurement comes from a camera sensor counting photons on a grid of tiny detectors. Each detector integrates light for a fraction of a second and emits a voltage proportional to how many photons hit it; the sensor then discretizes that voltage into an integer. One detector becomes one pixel.

Continuous scene                 Sensor grid                     Digital image
(infinite detail)                (H x W detectors)               (H x W integers)

    ~~~~~                        +--+--+--+--+--+                 210 198 180 155 120
   ~   ~   ~                     |  |  |  |  |  |                 205 195 178 152 118
  ~ light ~      ---->           +--+--+--+--+--+     ---->       200 190 175 150 115
   ~~~~~                         |  |  |  |  |  |                 195 185 170 148 112
                                 +--+--+--+--+--+                 188 180 165 145 108

Two choices happen at this step and they fix the ceiling on everything downstream:

  • Spatial sampling decides how many detectors per degree of the scene. Too few, and edges become jagged (aliasing). Too many, and storage and compute explode.
  • Intensity quantization decides how finely the voltage is bucketed. 8 bits gives 256 levels and is standard for display. 10, 12, 16 bits give smoother gradients and matter for medical imaging, HDR, and raw sensor pipelines.

Why three channels

A single detector that counts photons across the whole visible spectrum gives you grayscale: one channel, no colour. Color needs more, so the sensor covers its grid with a mosaic of red, green, and blue filters. After demosaicing, every spatial location ends up with three integers, the response of the nearby red-filtered, green-filtered, and blue-filtered detectors. Those three integers are a pixel's RGB triplet.

One pixel in memory:

    (R, G, B) = (210, 140, 30)   <- reddish-orange

An H x W RGB image:

    shape (H, W, 3)     stored as   H rows of W pixels of 3 values
                                    each in [0, 255] for uint8

Three is not magic. Depth cameras add a Z channel. Satellites add infrared and ultraviolet bands. Medical scans often have one channel (X-ray, CT) or many (hyperspectral). The number of channels is the last axis; conv layers learn to mix across it.

Two layout conventions: HWC and CHW

Same tensor, two orderings. Every library picks one.

HWC (height, width, channels)           CHW (channels, height, width)

   W ->                                    H ->
  +-----+-----+-----+                     +-----+-----+
H |R G B|R G B|R G B|                   C |R R R R R R|
| +-----+-----+-----+                   | +-----+-----+
v |R G B|R G B|R G B|                   v |G G G G G G|
  +-----+-----+-----+                     +-----+-----+
                                          |B B B B B B|
                                          +-----+-----+

   PIL, OpenCV, matplotlib,              PyTorch, most deep learning
   almost every image file on disk       frameworks, cuDNN kernels

CHW exists because convolution kernels slide across H and W. Keeping the channel axis first means each kernel sees a contiguous 2D plane per channel, which vectorizes cleanly. Disk formats keep HWC because that matches how scanlines come out of a sensor.

The one-line conversion you'll type a thousand times:

img_chw = img_hwc.transpose(2, 0, 1)      # NumPy
img_chw = img_hwc.permute(2, 0, 1)        # PyTorch tensor

Memory layout, visualised:

Mermaid diagram

Byte ranges and dtype

Three conventions dominate:

ConventiondtypeRangeWhere you see it
Rawuint8[0, 255]Files on disk, PIL, OpenCV output
Normalizedfloat32[0.0, 1.0]After img.astype('float32') / 255
Standardizedfloat32roughly [-2, +2]After subtracting mean and dividing by std

Convolutional networks were trained on standardized inputs. ImageNet stats mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] are the arithmetic mean and standard deviation of the three channels over the full ImageNet training set, computed on [0, 1] normalized pixels. Feeding raw uint8 into a model that expects standardized float is the single most common silent failure in applied vision.

Color spaces and why they exist

RGB is the capture format but it is not always the most useful representation for a model.

 RGB               HSV                       YCbCr / YUV

 R red             H hue (angle 0-360)       Y luminance (brightness)
 G green           S saturation (0-1)        Cb chroma blue-yellow
 B blue            V value/brightness (0-1)  Cr chroma red-green

 Linear to         Separates color from      Separates brightness from
 sensor output     brightness. Useful for    color. JPEG and most video
                   color thresholding, UI    codecs compress the chroma
                   sliders, simple filters   channels harder because the
                                             human eye is less sensitive
                                             to chroma detail than to Y.

For most modern CNNs you feed RGB. You meet other spaces when:

  • HSV: classical CV code, color-based segmentation, white-balancing.
  • YCbCr: reading JPEG internals, video pipelines, super-resolution models that operate on Y only.
  • Grayscale: OCR, document models, any case where color is nuisance variable rather than signal.

Grayscale from RGB is a weighted sum, not an average, because the human eye is more sensitive to green than to red or blue:

Y = 0.299 R + 0.587 G + 0.114 B       (ITU-R BT.601, the classic weights)

Aspect ratio, resizing, and interpolation

Every model has a fixed input size (224x224 for most ImageNet classifiers, 384x384 or 512x512 for modern detectors). Your images rarely match. The three resize choices that matter:

  • Resize shorter side, then center crop: the standard ImageNet recipe. Preserves aspect ratio, throws away a strip of edge pixels.
  • Resize and pad: preserves aspect ratio and every pixel, adds black bars. Standard for detection and OCR.
  • Resize directly to target: stretches the image. Cheap, distorts geometry, fine for many classification tasks.

The interpolation method decides how intermediate pixels are computed when the new grid does not align with the old one:

Nearest neighbour     fastest, blocky, only choice for masks/labels
Bilinear              fast, smooth, default for most image resizing
Bicubic               slower, sharper on upscaling
Lanczos               slowest, best quality, used for final display

Rule of thumb: bilinear for training, bicubic or lanczos for assets you'll look at, nearest for anything containing integer class IDs.

Check: The Concept0/26
1. What does each number in a uint8 image array of shape (224, 224, 3) represent?Define
A box labeled shape 224 by 224 by 3 with one cell highlighted, and a question mark asking what kind of quantity the highlighted number is.
2. What is a channel in an image tensor?Define
Three small grids of real pixel numbers side by side, labeled R, G and B, taken from the same corner of one image.
3. What do the names HWC and CHW refer to?Define
Two boxes holding the same image, one labeled HWC with shape 128 by 192 by 3 and one labeled CHW with shape 3 by 128 by 192, connected by a transpose arrow.
4. In this lesson's vocabulary, what does normalizing an image do?Define
A four-stage pipeline, raw bytes, normalize, further steps, model, with the normalize stage highlighted and no description of what any stage does.
5. What does standardizing an image do?Define
A four-stage pipeline, raw bytes, earlier steps, standardize, model, with the standardize stage highlighted and no description of what any stage does.
6. What is grayscale conversion from RGB?Define
One real RGB pixel from the lesson's synthetic image feeding into a grayscale conversion box, with the single output value hidden.
7. What is interpolation in the context of resizing an image?Define
A row of old grid points and a denser row of new grid points below it, with one new point that does not line up with any old point and its value left blank.
8. What is aspect ratio, and why does it matter when fitting an image to a model's input size?Define
A wide rectangle labeled photo 400 by 300 next to a square labeled model input 224 by 224, with no resize strategy applied yet.
Photo size is illustrative. The 224 x 224 input size is the lesson's.
9. Why does resizing or rotating an image count as resampling the data rather than moving coloured squares around?Explain
An original grid of measurement points with a rotated grid of new points drawn over it, one new point highlighted that falls between the old points and whose value is left blank.
10. What happens if a sensor uses too few detectors per degree of the scene during spatial sampling?Explain
The same straight edge drawn across two sensor grids of equal area, one split into few detector cells and one into many, with no sampled result shown.
11. When would a pipeline use 10, 12 or 16-bit intensity quantization instead of the standard 8-bit?Explain
An 8-bit box labeled 256 levels next to an unlabeled higher bit depth box labeled more levels, asking when the extra levels would be needed.
12. Why does an RGB image end up with three integers per pixel instead of one?Explain
A plain grid of sensor detectors on the left, a hidden step in the middle, and on the right one pixel carrying the lesson's example RGB triple.
13. Why do deep learning frameworks keep the channel axis first (CHW)?Explain
The same 12 color values laid out two ways in memory: R G B repeating in HWC order, and the four R values then the four G values then the four B values in CHW order.
14. Why do JPEG and most video codecs compress the chroma channels of YCbCr harder than Y?Explain
Three grids labeled Y, Cb and Cr, with Y divided into many small cells and Cb and Cr each divided into fewer, larger cells over the same area.
15. What does HSV give you that raw RGB does not?Explain
One real pixel shown as a swatch, with its three RGB numbers on the left and its three computed HSV numbers on the right, each component named.
16. When would you reach for YCbCr instead of RGB?Explain
One RGB swatch feeding into the three named YCbCr components, one luminance and two chroma, with no use case shown.
17. RGB grayscale conversion uses weights 0.299 R + 0.587 G + 0.114 B rather than an equal one-third each. Why?Explain
A bar chart of the three grayscale weights, 0.299 for red, 0.587 for green and 0.114 for blue, with a dashed line marking an equal one-third share.
18. Why do ImageNet pretrained models expect inputs standardized with mean=[0.485, 0.456, 0.406] and std=[0.229, 0.224, 0.225]?Explain
A table of the ImageNet mean and std constants, three numbers per statistic, one for each colour channel.
19. Which of the three resize strategies preserves aspect ratio and every original pixel by adding black bars?Explain
A wide photo next to a dashed square target frame with a question mark, no fitting strategy chosen yet.
20. Why is bilinear the usual interpolation for training, while bicubic or Lanczos are used for assets you will look at?Explain
A bar chart of measured roughness for nearest, bilinear and bicubic resizing of the same image.
Real run of the lesson's Step 5 resize comparison.
21. You resize a segmentation mask (integer class IDs 0 to 20) from 500x500 to 224x224. Which interpolation method is correct?Apply
A small patch of integer class ID labels with an arrow to a blank resized version, asking which interpolation keeps the values valid class IDs.
22. A teammate feeds raw uint8 images straight into a pretrained ImageNet network. It runs and gives poor predictions. What is the most likely cause?Analyze
An array of uint8 pixels in the range 0 to 255 going straight into a pretrained ImageNet model, with poor output and nothing shown in between.
23. A teammate divides images by 255 and calls preprocessing done. The ImageNet model still underperforms. Which step is missing?Analyze
A two-item preprocessing checklist with divide by 255 ticked and the second item left blank, leading to a model that still underperforms.
24. A detector trained on padded images misses objects near the image border after you switch to Resize(shorter side) plus center crop. What is the most likely reason?Analyze
An object near the right edge of a full frame, and the same frame with a centre crop boundary drawn near it, with a question mark over whether the object stays inside.
25. You build an OCR pipeline for tall scanned pages fed to a model with a fixed square input. Which resize approach fits best?Design
A tall page covered in text lines next to a dashed square target frame and a question mark, with no resize strategy chosen.
26. You train a model from scratch on your own photo dataset. How should you pick the standardization constants for training and serving?Design
Two boxes, one showing ImageNet's published per-channel mean, the other your own photo dataset with its mean left blank.

Build It

Step 1: Load an image and inspect its shape

Use Pillow to load any JPEG or PNG, convert to NumPy, and print what you got. For a deterministic example that runs offline, synthesize one.

python
import numpy as np
from PIL import Image
 
def synthetic_rgb(h=128, w=192, seed=0):
    rng = np.random.default_rng(seed)
    yy, xx = np.meshgrid(np.linspace(0, 1, h), np.linspace(0, 1, w), indexing="ij")
    r = (np.sin(xx * 6) * 0.5 + 0.5) * 255
    g = yy * 255
    b = (1 - yy) * xx * 255
    rgb = np.stack([r, g, b], axis=-1) + rng.normal(0, 6, (h, w, 3))
    return np.clip(rgb, 0, 255).astype(np.uint8)
 
arr = synthetic_rgb()
# Or load from disk:
# arr = np.asarray(Image.open("your_image.jpg").convert("RGB"))
 
print(f"type:   {type(arr).__name__}")
print(f"dtype:  {arr.dtype}")
print(f"shape:  {arr.shape}     # (H, W, C)")
print(f"min:    {arr.min()}")
print(f"max:    {arr.max()}")
print(f"pixel at (0, 0): {arr[0, 0]}")

Expected output: shape: (H, W, 3), dtype: uint8, range [0, 255]. That is the canonical on-disk representation whether the bytes came from a camera, a JPEG decoder, or a synthetic generator.

Step 2: Split channels and re-order layout

Pull out R, G, B separately, then convert from HWC to CHW for PyTorch.

python
R = arr[:, :, 0]
G = arr[:, :, 1]
B = arr[:, :, 2]
print(f"R shape: {R.shape}, mean: {R.mean():.1f}")
print(f"G shape: {G.shape}, mean: {G.mean():.1f}")
print(f"B shape: {B.shape}, mean: {B.mean():.1f}")
 
arr_chw = arr.transpose(2, 0, 1)
print(f"\nHWC shape: {arr.shape}")
print(f"CHW shape: {arr_chw.shape}")

Three grayscale planes, one per channel. CHW just reorders the axes; no data copy is strictly required when the memory layout allows it.

Step 3: Grayscale and HSV conversions

Weighted-sum grayscale, then a manual RGB-to-HSV.

python
def rgb_to_grayscale(rgb):
    weights = np.array([0.299, 0.587, 0.114], dtype=np.float32)
    return (rgb.astype(np.float32) @ weights).astype(np.uint8)
 
def rgb_to_hsv(rgb):
    rgb_f = rgb.astype(np.float32) / 255.0
    r, g, b = rgb_f[..., 0], rgb_f[..., 1], rgb_f[..., 2]
    cmax = np.max(rgb_f, axis=-1)
    cmin = np.min(rgb_f, axis=-1)
    delta = cmax - cmin
 
    h = np.zeros_like(cmax)
    mask = delta > 0
    rmax = mask & (cmax == r)
    gmax = mask & (cmax == g)
    bmax = mask & (cmax == b)
    h[rmax] = ((g[rmax] - b[rmax]) / delta[rmax]) % 6
    h[gmax] = ((b[gmax] - r[gmax]) / delta[gmax]) + 2
    h[bmax] = ((r[bmax] - g[bmax]) / delta[bmax]) + 4
    h = h * 60.0
 
    s = np.where(cmax > 0, delta / cmax, 0)
    v = cmax
    return np.stack([h, s, v], axis=-1)
 
gray = rgb_to_grayscale(arr)
hsv = rgb_to_hsv(arr)
print(f"gray shape: {gray.shape}, range: [{gray.min()}, {gray.max()}]")
print(f"hsv   shape: {hsv.shape}")
print(f"hue range: [{hsv[..., 0].min():.1f}, {hsv[..., 0].max():.1f}] degrees")
print(f"sat range: [{hsv[..., 1].min():.2f}, {hsv[..., 1].max():.2f}]")
print(f"val range: [{hsv[..., 2].min():.2f}, {hsv[..., 2].max():.2f}]")

Hue comes out in degrees, saturation and value in [0, 1]. That matches the OpenCV hsv_full convention.

Step 4: Normalize, standardize, and reverse it

Go from raw bytes to the exact tensor a pretrained ImageNet model expects, then back.

python
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
 
def preprocess_imagenet(rgb_uint8):
    x = rgb_uint8.astype(np.float32) / 255.0
    x = (x - mean) / std
    x = x.transpose(2, 0, 1)
    return x
 
def deprocess_imagenet(chw_float32):
    x = chw_float32.transpose(1, 2, 0)
    x = x * std + mean
    x = np.clip(x * 255.0, 0, 255).astype(np.uint8)
    return x
 
x = preprocess_imagenet(arr)
print(f"preprocessed shape: {x.shape}     # (C, H, W)")
print(f"preprocessed dtype: {x.dtype}")
print(f"preprocessed mean per channel:  {x.mean(axis=(1, 2)).round(3)}")
print(f"preprocessed std  per channel:  {x.std(axis=(1, 2)).round(3)}")
 
roundtrip = deprocess_imagenet(x)
max_diff = np.abs(roundtrip.astype(int) - arr.astype(int)).max()
print(f"roundtrip max pixel diff: {max_diff}    # should be 0 or 1")

Per-channel mean should be close to zero, std close to one. The preprocess/deprocess pair is exactly what every torchvision transforms.Normalize call is doing under the hood.

Step 5: Resize with three interpolation methods

Compare nearest, bilinear, and bicubic on an upscale so the difference is visible.

python
target = (arr.shape[0] * 3, arr.shape[1] * 3)
 
nearest = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.NEAREST))
bilinear = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.BILINEAR))
bicubic = np.asarray(Image.fromarray(arr).resize(target[::-1], Image.BICUBIC))
 
def local_roughness(x):
    gy = np.diff(x.astype(float), axis=0)
    gx = np.diff(x.astype(float), axis=1)
    return float(np.abs(gy).mean() + np.abs(gx).mean())
 
for name, out in [("nearest", nearest), ("bilinear", bilinear), ("bicubic", bicubic)]:
    print(f"{name:>8}  shape={out.shape}  roughness={local_roughness(out):6.2f}")

Nearest scores highest on roughness because it keeps hard edges. Bilinear is the smoothest. Bicubic sits in between, preserving perceived sharpness without the stair-step artifacts.

Check: Build It0/5
1. In Step 2, `R = arr[:, :, 0]` is taken from the array returned by `synthetic_rgb()` with its default arguments. What does `R.shape` print?Apply
Code showing arr.shape equals 128, 192, 3, then R assigned from arr sliced on the last axis at index 0, with print(R.shape) left unanswered.
2. In Step 3, `rgb_to_grayscale` is applied to a one-pixel image holding (210, 140, 30). What value does it return?Apply
One pixel with RGB values 210, 140, 30 feeding into rgb_to_grayscale, with the single output value hidden.
3. In Step 3, `rgb_to_hsv` is applied to a pure red pixel (255, 0, 0). What (h, s, v) does it return?Apply
A pure red pixel, RGB 255, 0, 0, feeding into rgb_to_hsv, with the resulting hue, saturation, value triple hidden.
4. In Step 4, `preprocess_imagenet` runs on a pixel whose red value is 255. Roughly what does the red channel hold afterwards?Apply
The preprocess_imagenet steps with the ImageNet red-channel mean 0.485 and std 0.229, applied to a pixel whose red value is 255, with the resulting value hidden.
5. Step 5 upscales the default 128x192 array by 3 and passes `target[::-1]` to PIL's resize. What is the shape of `nearest`?Apply
The Step 5 upscaling code with arr.shape 128, 192, 3, computing target from arr.shape times 3 and calling PIL resize with target reversed, with the resulting array shape hidden.

Use It

torchvision.transforms bundles everything above into a single composable pipeline. The code below reproduces exactly what preprocess_imagenet does, plus resize and crop.

python
import torch
from torchvision import transforms
from PIL import Image
 
img = Image.fromarray(synthetic_rgb(256, 256))
 
pipeline = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
 
x = pipeline(img)
print(f"tensor type:  {type(x).__name__}")
print(f"tensor dtype: {x.dtype}")
print(f"tensor shape: {tuple(x.shape)}      # (C, H, W)")
print(f"per-channel mean: {x.mean(dim=(1, 2)).tolist()}")
print(f"per-channel std:  {x.std(dim=(1, 2)).tolist()}")
 
batch = x.unsqueeze(0)
print(f"\nbatched shape: {tuple(batch.shape)}   # (N, C, H, W) — ready for a model")

Four steps, in this exact order: Resize(256) scales the shorter side to 256; CenterCrop(224) takes a 224x224 patch from the middle; ToTensor() divides by 255 and swaps HWC to CHW; Normalize subtracts the ImageNet mean and divides by std. Reversing that order silently changes what reaches the model.

Check: Use It0/3
1. The Use It pipeline is Resize(256), CenterCrop(224), ToTensor(), Normalize(...). Why does the order matter?Explain
The four Use It transforms, Resize 256, CenterCrop 224, ToTensor, Normalize, drawn in their stated order with a question mark about reordering them.
2. A 480 x 640 PIL image goes through Resize(256), CenterCrop(224), ToTensor() and Normalize(...) from Use It. What is `x.shape`?Apply
A 480 by 640 image flowing through Resize 256, CenterCrop 224, ToTensor and Normalize, with the final tensor shape hidden.
3. In Use It, `x` has shape (3, 224, 224). What is the shape of `batch = x.unsqueeze(0)`?Apply
A tensor x of shape 3, 224, 224 feeding into unsqueeze(0), with the resulting batch shape hidden.

Ship It

This lesson produces:

  • outputs/prompt-vision-preprocessing-audit.md: a prompt that turns any model card or dataset card into a checklist of the exact preprocessing invariants a team must honour.
  • outputs/skill-image-tensor-inspector.md: a skill that, given any image-shaped tensor or array, reports dtype, layout, range, and whether it looks raw, normalized, or standardized.
Check: Ship It0/1
1. You are writing a skill that labels an array as raw, normalized or standardized. Which check separates the three cases?Design
One image-shaped array going into a skill, which must return exactly one of three labels: raw, normalized or standardized.

Exercises

  1. (Easy) Load a JPEG with OpenCV (cv2.imread) and with Pillow. Print both shapes and the pixel at (0, 0). Explain the channel-order difference, then write a one-line conversion that makes the OpenCV array identical to the Pillow one.
  2. (Medium) Write standardize(img, mean, std) and its inverse that together pass a roundtrip_max_diff <= 1 test on any uint8 image. Your functions must work on a single image in HWC and on a batch in NCHW with the same call.
  3. (Hard) Take a 3-channel ImageNet-standardized tensor and run it through a 1x1 conv that learns a weighted mixture of RGB into a single grayscale channel. Initialize the weights to [0.299, 0.587, 0.114], freeze them, and verify the output matches your manual rgb_to_grayscale to within floating-point error. What other classical color-space transforms can be written as 1x1 convolutions?

Key Terms

TermWhat people sayWhat it actually means
Pixel"A coloured square"One sample of light intensity at one grid location: three numbers for colour, one for grayscale
Channel"The colour"One of the parallel spatial grids stacked into an image tensor; last axis in HWC, first in CHW
HWC / CHW"The shape"Axis orderings for an image tensor; disk and PIL use HWC, PyTorch and cuDNN use CHW
Normalize"Scale the image"Divide by 255 so pixels live in [0, 1]: necessary but not sufficient
Standardize"Zero-center"Subtract mean and divide by std per channel so the input distribution matches what the model was trained on
Grayscale conversion"Average the channels"A weighted sum with coefficients 0.299/0.587/0.114 that matches human luminance perception
Interpolation"How resize picks pixels"The rule that decides output values when the new grid does not align with the old one: nearest for labels, bilinear for training, bicubic for display
Aspect ratio"Width over height"The ratio that distinguishes "resize and pad" from "resize and stretch"

Further Reading