Skip to content
learn
  1. Learn
  2. Machine Learning
  3. Computer Vision

Machine Learning ML-09.01

Images as tensors

To a network, an image is a grid of numbers: pixels, channels, the (N, C, H, W) layout and why pixel values are normalized before training.

Beginner 8 min Last reviewed

The big idea

You see a handwritten zero. A computer sees a table of numbers. Each small square of the image, a pixelPicture element: one cell of the grid that makes up a digital image, holding a brightness value (grayscale) or several colour values., holds a brightness value, and the image is nothing more than the grid of those values. Everything a vision model does, from recognizing digits to finding tumours, starts from that grid.

Think of a mosaic made of tiles. From across the room you see a face; up close, each tile is a single flat colour. The model only ever gets the list of tile colours in order, and must learn which arrangements mean "face".

A grid of handwritten digits from the MNIST dataset: ten rows, one per digit from 0 to 9, each row showing many different handwritten versions of that digit.
Handwritten digits from MNIST, a classic dataset of 70,000 such images, each 28 by 28 pixels in grayscale. Every one is stored as 784 numbers. Image: Suvanjanprasai, CC BY-SA 4.0 via Wikimedia Commons, CC BY-SA 4.0, via Wikimedia Commons

How it works

Step 1: A grayscale image is a 2D grid

A digit as a grid of numbers An 8 by 8 grid of cells showing a handwritten zero. Each cell holds a number from 0 to 16 and is shaded violet in proportion to it: cells with 0 are dark and empty, cells with values like 13, 14 and 15 glow bright, and together the bright cells trace the ring of the zero. 0 0 5 13 9 1 0 0 0 0 13 15 10 15 5 0 0 3 15 2 0 11 8 0 0 4 12 0 0 8 8 0 0 5 8 0 0 9 8 0 0 4 11 0 1 12 7 0 0 2 14 5 10 12 0 0 0 0 6 13 10 0 0 0 Figure 1. A handwritten zero from the code's dataset, 8 by 8 pixels with values 0 to 16. Shading shows the value; the numbers are all the model ever sees.

A grayscale image of height H and width W is an H by W array. The digits in the code are 8 by 8 with values from 0 to 16; MNIST digits are 28 by 28 with values from 0 to 255. Each value is one input number to the model, so a 28 by 28 image is 784 inputs.

Step 2: Colour adds channels

A colour image stores three numbers per pixel: red, green and blue intensity, usually 0 to 255 each. Each of those is a channelOne layer of values in an image tensor, such as the red, green or blue intensities, or one feature map inside a network., and the image becomes a 3D array: a 224 by 224 photo is 224 x 224 x 3 = 150,528 numbers. Inside a network, the number of channels grows: a layer might output 64 or 512 channels, each a map of where some learned pattern appears.

Step 3: The (N, C, H, W) layout

Networks process images in batches, so the data becomes a 4D tensorA multi-dimensional array of numbers with a shape, such as (N, C, H, W) for a batch of images.. PyTorch uses (N, C, H, W): batch size, channels, height, width. A batch of 32 RGB photos at 224 by 224 has shape (32, 3, 224, 224).

Image files decode into channels-last order, (H, W, C), so a transpose is usually part of loading. TensorFlow defaults to channels-last (N, H, W, C). Neither is right or wrong, but mixing them up is one of the most common vision bugs: the model happily treats image rows as colour channels. In the code, a (224, 224, 3) decoded image becomes (3, 224, 224) with one transpose and (1, 3, 224, 224) with an added batch dimension.

Step 4: Normalize pixel values

Raw pixels are integers from 0 to 255. Networks train better on small, centred inputs ( Feature engineering ), so two steps are standard:

  1. Convert to floats and divide by 255, giving values from 0 to 1.
  2. Standardize each channel: subtract the channel mean and divide by its standard deviation, computed on the training set.

Pretrained models expect the exact statistics they were trained with: the widely used ImageNet values are means (0.485, 0.456, 0.406) and standard deviations (0.229, 0.224, 0.225) for red, green and blue. Feeding a pretrained model differently normalized inputs silently costs accuracy.

Going deeper

Code: digits, layouts and normalization

image-tensors.pyruns with python3 + numpy + scikit-learn
import numpy as np
from sklearn.datasets import load_digits

# A grayscale image is a 2D grid of numbers: here 8x8 pixels with values 0 (no ink) to 16 (full ink)
digits = load_digits()
img = digits.images[0]
print("label:", digits.target[0], " shape:", img.shape, " dtype:", img.dtype)
print(img.astype(int))

# A batch of grayscale images in (N, C, H, W) layout: N images, 1 channel, 8x8
batch = digits.images[:32][:, None, :, :]
print("\nbatch shape (N, C, H, W):", batch.shape)

# A colour image is H x W x 3 when loaded from a file (channels last) ...
rng = np.random.default_rng(0)
rgb = rng.integers(0, 256, size=(224, 224, 3), dtype=np.uint8)   # stand-in for a decoded photo
print("decoded photo shape (H, W, C):", rgb.shape, rgb.dtype, "values", rgb.min(), "to", rgb.max())
# ... and most frameworks want channels first: (C, H, W), then a batch dimension
chw = rgb.transpose(2, 0, 1)
print("channels first (C, H, W):", chw.shape, "  as a batch:", chw[None].shape)
print(f"numbers in one 224x224 RGB image: {chw.size:,}  ({chw.size * 4 / 1e6:.2f} MB as float32)")

# Normalization: scale to [0, 1], then standardize each channel with statistics of the training set
x = chw.astype(np.float32) / 255.0
mean, std = x.mean((1, 2), keepdims=True), x.std((1, 2), keepdims=True)  # here: from this one image
xn = (x - mean) / std   # pretrained ImageNet models instead use fixed means (0.485, 0.456, 0.406)
print("\nafter /255:        min %.2f max %.2f" % (x.min(), x.max()))
print("channel means", np.round(mean.ravel(), 3), " stds", np.round(std.ravel(), 3))
print("after standardize: per-channel mean", np.round(xn.mean((1, 2)), 2) + 0.0, " std", np.round(xn.std((1, 2)), 2))

Output

label: 0  shape: (8, 8)  dtype: float64
[[ 0  0  5 13  9  1  0  0]
 [ 0  0 13 15 10 15  5  0]
 [ 0  3 15  2  0 11  8  0]
 [ 0  4 12  0  0  8  8  0]
 [ 0  5  8  0  0  9  8  0]
 [ 0  4 11  0  1 12  7  0]
 [ 0  2 14  5 10 12  0  0]
 [ 0  0  6 13 10  0  0  0]]

batch shape (N, C, H, W): (32, 1, 8, 8)
decoded photo shape (H, W, C): (224, 224, 3) uint8 values 0 to 255
channels first (C, H, W): (3, 224, 224)   as a batch: (1, 3, 224, 224)
numbers in one 224x224 RGB image: 150,528  (0.60 MB as float32)

after /255:        min 0.00 max 1.00
channel means [0.497 0.5   0.499]  stds [0.29  0.291 0.29 ]
after standardize: per-channel mean [0. 0. 0.]  std [1. 1. 1.]

The printed 8 by 8 array is the zero from Figure 1. The colour part uses random pixels as a stand-in for a decoded photo, so its channel means sit near 0.5; after standardizing with its own statistics, each channel has mean 0 and standard deviation 1. One 224 by 224 RGB image is 150,528 numbers, 0.60 MB as 32-bit floats, so a batch of 256 is about 154 MB before the network computes anything.

Why not just flatten?

You could flatten an image into one long vector and feed it to the multi-layer networks of Multi-layer networks . That works for tiny digits, but it throws away the grid: the network no longer knows which pixels are neighbours, and a 224 by 224 RGB input connected to 1,000 hidden units needs 150 million weights in the first layer alone. Convolutions ( Convolutions ) keep the grid and reuse a small set of weights everywhere.

Resolution and resizing

Models expect a fixed input size, so images are resized (and often cropped) during loading. Higher resolution keeps detail but costs compute that grows with H x W. Resizing can also distort: squashing a wide photo into a square changes shapes, which is why "resize the short side, then centre-crop" is common.

Trade-offs

OptionWins whenLoses when
Channels first (N, C, H, W) PyTorch and many GPU kernels Needs a transpose from decoded files
Channels last (N, H, W, C) Matches image files; TensorFlow default; some hardware prefers it Must convert when mixing frameworks
Low resolution Fast training and inference, small memory Fine details and small objects are lost
High resolution Small objects, medical and satellite images Compute and memory grow with H x W

Common mistakes

  • Feeding (H, W, C) arrays to a model that expects (C, H, W).
  • Forgetting to divide by 255, or dividing twice.
  • Using different normalization at inference than in training, or than a pretrained model expects.
  • Loading images as BGR (some libraries' default) when the model was trained on RGB.
  • Computing normalization statistics on the test set instead of the training set.

In the wild

Every vision pipeline starts with this decoding and tensor conversion step: phone camera apps, medical imaging systems, document scanners and self-driving perception stacks. Pretrained model libraries ship each model with its expected input size and normalization constants, precisely because mismatches here are so common and so silent.

Check yourself

  1. What is the shape of a batch of 16 RGB images of 64 by 64 pixels in PyTorch's layout?

    Show answer

    B. (16, 3, 64, 64) PyTorch uses (N, C, H, W): 16 images, 3 channels, 64 by 64.

  2. How many numbers are in one 32 by 32 RGB image?

    Show answer

    B. 3,072 32 x 32 pixels x 3 channels = 3,072 values.

  3. A pixel's red value is 128. With ImageNet normalization (mean 0.485, std 0.229 for red), what does it become?

    Show answer

    B. About 0.07 128 / 255 = 0.502, minus 0.485 = 0.017, divided by 0.229 is about 0.074.

  4. A model loads fine but its accuracy is near chance on images that look correct. Which bug fits best?

    Show answer

    B. Channels and height swapped by a wrong layout, or wrong normalization Layout and normalization bugs do not crash; they just feed the model scrambled or mis-scaled numbers.

Recap

  • An image is a grid of pixel values; grayscale has one channel, colour has three.
  • Batches are 4D tensors; PyTorch uses (N, C, H, W), files decode as (H, W, C).
  • Scale pixels to 0 to 1 and standardize each channel with training-set (or pretrained-model) statistics.
  • Flattening loses the grid and needs huge weight matrices; convolutions keep the spatial structure.
  • Layout and normalization mismatches fail silently, so check them first.

Get new lessons by email

Get new posts on backend and AI internals. No spam, unsubscribe anytime.

Your email is only used to send new posts. Privacy