Search…

How Images and Video Are Stored: Colour, JPEG, Frames

In this series (30 parts)
  1. Computer Vision Roadmap: From Basics to Real Projects
  2. What is Computer Vision? From Pixels to Decisions
  3. A Short History of Computer Vision: 1545 to Now
  4. Math for CV Beginners: Vectors, Matrices, Convolutions
  5. Image Fundamentals: Color, Histograms, Noise, Filtering
  6. How Images and Video Are Stored: Colour, JPEG, Frames
  7. OpenCV Setup + First 10 Tasks in Python
  8. Vision Metrics: Accuracy, Precision, Recall, mAP, IoU
  9. CV Project Workflow: Dataset, Baseline, Error Iteration
  10. Intensity Transforms and Frequency-Domain Filtering
  11. Edge Detection and Thresholding That Actually Work
  12. Morphology, Contours, and Shape Analysis for Real Images
  13. Feature Matching (SIFT/ORB) and Image Stitching
  14. Camera Calibration and Perspective Correction
  15. Epipolar Geometry, Stereo Vision, and Depth Estimation
  16. Optical Flow and Motion Tracking in Video
  17. HOG, HOF and MBH: Descriptors Before Deep Learning
  18. Your First Image Classifier (PyTorch + Transfer Learning)
  19. Data Pipelines and Augmentation for Vision Models
  20. CNN Architectures Explained: From LeNet to ResNet
  21. YOLO Detection Pipeline: Data to Inference
  22. Semantic and Instance Segmentation: U-Net to Mask R-CNN
  23. Vision Transformers (ViT) and When to Use Them
  24. CV Explainability: Grad-CAM, Failures, Bias Checks
  25. 3D Vision Basics: SfM, Point Clouds, and Pose Estimation
  26. Multimodal Vision: CLIP, Embeddings, and Retrieval Systems
  27. Video Understanding: Flow Networks, Interpolation, Stabilisation
  28. Real-Time CV Systems: Tracking, Latency, Streaming
  29. Deploying CV Models: ONNX, TensorRT, Edge, and APIs
  30. Responsible CV: Privacy, Fairness, Security, Governance

Every image your code loads has already been through a lossy transformation that you did not write and probably have not looked at. Most of the time that is fine. Sometimes it is the reason your model is 4% worse than it should be, and you will never find it by tuning hyperparameters.

Prerequisites: image fundamentals. You should know what a pixel and a histogram are.

A problem that is really a storage problem

A site has 12 cameras at 1080p, 25 frames a second, recording continuously. The team budgeted 30 days of retention. The disks fill in four days.

Someone suggests dropping to 12 fps. Someone else suggests fewer cameras. Both are wrong answers, because nobody has looked at what the encoder is doing. The actual fix — changing the keyframe interval and enabling B-frames on the archive stream while leaving the live analytics stream alone — costs nothing and gets them past 30 days.

To make that decision you need to know how images and video are actually stored. That is this post.

An image is a 2D discrete signal

discrete signal A signal measured at a finite set of positions, with each measurement recorded to a finite precision. An image is discrete in both: sampled on a pixel grid, and quantised to a fixed number of intensity levels.

Here is an 8×8 grayscale patch, exactly as the sensor hands it over. The upper rows are bright, and something dark occupies the bottom right.

8×8 patch (8×8)
111
115
113
111
112
111
112
111
135
138
137
139
145
146
149
147
163
168
188
196
206
202
206
207
180
184
206
219
202
200
195
193
189
193
214
216
104
79
83
77
191
201
217
220
103
59
60
68
195
205
216
222
113
68
69
83
199
203
223
228
108
68
71
77

Two independent decisions were made before you got this.

Decision 1: sampling — how many positions

The sensor divides the scene into a grid and records one number per cell. More cells means more spatial detail. This is resolution, and it is the one everybody already thinks about.

The thing worth knowing is that a feature smaller than one pixel is not blurred — it is gone. If a crack in a component is 0.3 mm wide and your camera resolves 0.8 mm per pixel, no algorithm recovers that crack. This is why the first question on any inspection project is millimetres per pixel, not which model to use.

Decision 2: quantisation — how many levels

Each sample is rounded to one of a fixed number of values. 8-bit gives 256 levels, which is what almost everything uses.

Watch what happens when you reduce it. Take the top-left 4×4 of that patch and requantise:

8-bit (256) (4×4)
111
115
113
111
135
138
137
139
163
168
188
196
180
184
206
219
4-bit (16) (4×4)
102
119
119
102
136
136
136
136
153
170
187
204
187
187
204
221
2-bit (4) (4×4)
85
85
85
85
128
128
128
128
170
170
170
213
170
213
213
213

At 4 bits the numbers are snapping to multiples of 17. At 2 bits, whole regions have collapsed to one value — the entire second row is now identical, and the gentle gradient in row 1 has flattened completely.

That flattening is posterisation Visible banding caused by too few intensity levels, where a smooth gradient becomes a series of flat steps with hard edges between them. , and you can see it in a histogram as a comb: only a few values occur, with gaps between them.

Colour models: four ways to write down a colour

Post 4 covered RGB and HSV. Here is the complete set you will meet, and crucially why each one exists.

RGBCMYKHSL / HSVYCbCr
ChannelsRed, Green, BlueCyan, Magenta, Yellow, BlackHue, Saturation, Lightness/ValueLuma, Chroma-blue, Chroma-red
MixingAdditive — add lightSubtractive — add inkNeither, it is a re-coordinate of RGBNeither, a linear transform of RGB
All channels max givesWhiteBlack
Built forScreens, cameras, sensorsPrintingEditing and colour filteringCompression and broadcast
Use it in CV whenDefault; feeding a networkEssentially neverSelecting a colour under changing lightYou care about brightness only, or you are reading a codec
Four colour models and the job each was designed for

RGB and CMYK are opposites in a precise sense. RGB adds light: turn all three up and you get white. CMYK adds ink, and ink absorbs light: pile all of them on and you approach black. That is why your screen and your printer disagree, and why CMYK never appears in computer vision work — nothing you process came from a printer.

YCbCr, and why compression cares

YCbCr is the one that matters here. It splits an RGB pixel into:

  • Y — luma, the brightness
  • Cb — how blue it is relative to that brightness
  • Cr — how red it is relative to that brightness

The conversion is a fixed linear transform:

Y  =  0.299·R + 0.587·G + 0.114·B
Cb = -0.169·R - 0.331·G + 0.500·B + 128
Cr =  0.500·R - 0.419·G - 0.081·B + 128

Take a mid orange, R=200, G=120, B=60:

Y  = 0.299(200) + 0.587(120) + 0.114(60)
   = 59.8 + 70.4 + 6.8  = 137.0
Cb = -0.169(200) - 0.331(120) + 0.500(60) + 128
   = -33.8 - 39.7 + 30.0 + 128 = 84.5
Cr = 0.500(200) - 0.419(120) - 0.081(60) + 128
   = 100.0 - 50.3 - 4.9 + 128 = 172.8

Note the weights on Y: green contributes 0.587 and blue only 0.114. Human vision is far more sensitive to green light, so green carries most of the perceived brightness.

Now the payoff. Your eyes resolve fine detail in brightness much better than in colour. So a codec can store Y at full resolution and store Cb and Cr at half resolution in each direction, and you will not see it.

That is chroma subsampling Storing the colour channels at lower spatial resolution than the brightness channel. 4:2:0 halves the colour resolution both horizontally and vertically. and it is free compression before any clever maths happens:

# Scheme Y samples Cb/Cr samples Data vs 4:4:4 Where you see it
1 4:4:4 Full Full 100% Professional capture, some raw formats
2 4:2:2 Full Half horizontally 67% Broadcast, higher-end cameras
3 4:2:0 Full Half both directions 50% JPEG, H.264, H.265 — almost everything you touch

Chroma subsampling: half the data gone before compression even starts

JPEG: what actually happens to your image

Here is the whole pipeline. It is worth knowing because every step has a consequence you can observe.

Decoding runs the same chain backwards: entropy decode, de-quantise, inverse DCT, upsample chroma, convert back to RGB.

Only one step actually destroys information in a tunable way, and it is quantisation. Notice that the DCT itself is not lossy — it is a reversible change of representation. It exists to make the quantisation step effective.

The DCT, concretely

DCT Discrete Cosine Transform. It rewrites an 8×8 block of pixel values as a weighted sum of 64 fixed patterns, ranging from flat to rapidly alternating. The weights are called coefficients.

The 64 patterns are fixed and known to both encoder and decoder, so only the 64 weights need storing. The top-left pattern is flat (the block’s average). Moving right adds horizontal stripes of increasing frequency; moving down adds vertical stripes.

Here is why this matters. Take a real 8×8 block from a smooth region of a photo and look at its DCT coefficients:

Pixels (4×4 corner) (4×4)
111
115
113
111
135
138
137
139
163
168
188
196
180
184
206
219
DCT →
DCT coefficients (4×4)
612
-38
-6
2
-98
14
3
-1
12
-5
2
0
-3
1
0
0

Look at the size of the numbers. The top-left coefficient is 612. By the bottom-right corner they are 0, 1 and 2.

This is energy compaction The tendency of the DCT to concentrate most of a natural image block's information into a few low-frequency coefficients, leaving the high-frequency ones near zero. , and it is the entire reason JPEG works. Natural images are mostly smooth, so the high-frequency coefficients are almost always tiny.

Quantisation divides each coefficient by a value from a quantisation table and rounds. The table uses large divisors for high frequencies — that is the whole trick. Divide a coefficient of 2 by 40 and round, and you get 0. Divide 612 by 16 and you get 38, which preserves it well.

Zeros compress to almost nothing, so the file shrinks. The JPEG quality slider is just a multiplier on that table.

Why artifacts are square

Because the image was cut into independent 8×8 blocks, and each block was quantised on its own. Neighbouring blocks make slightly different rounding errors, so their edges no longer line up. At low quality you see the grid.

Choosing a format

# Format Lossy? Good for Avoid when
1 JPEG Yes Photographs, delivery, storage at scale Repeated editing; text or line art; any masks
2 PNG No Masks, labels, screenshots, intermediates Large photo datasets — files get big
3 WebP Both modes Web delivery, ~25-30% smaller than JPEG Tooling that does not support it
4 TIFF Usually no Scientific and medical, 16-bit data Anything size-sensitive
5 GIF Yes (256 colours) Nothing in computer vision Always — the colour limit destroys images

Pick by whether the pixel values are data or presentation

The rule that matters: never store a segmentation mask or label image as JPEG. A mask has hard edges and exact integer class values. JPEG will smear class 3 and class 4 into a gradient of 3.2, 3.7, 3.9 along every boundary, and your labels become wrong.

Video: mostly storing what changed

A 1080p frame is 1920 × 1080 × 3 = 6.2 MB raw. At 25 fps that is 155 MB per second, or 13.4 TB per day for one camera. Nobody stores that.

The saving comes from a simple observation: consecutive frames are nearly identical. So store one frame properly, then store only the differences for the frames that follow.

The three frame types

# Type Full name Depends on Relative size Can you seek to it?
1 I Intra-coded (keyframe) Nothing — self-contained 1.0× Yes, directly
2 P Predicted An earlier frame ~0.3× No, needs the chain back to an I-frame
3 B Bi-directional predicted An earlier AND a later frame ~0.15× No

An I-frame is a JPEG in all but name. P and B frames store motion vectors and residuals.

A real stream looks something like I B B P B B P B B P B B I .... The gap between I-frames is the keyframe interval, often 1 to 4 seconds.

The B-frame problem, drawn

Here is the fact that decides live-video architecture.

To decode B₂, you need P₄ — a frame that comes after it in playback order. So the encoder must send P₄ before B₂, and the decoder must buffer.

The consequence: B-frames add latency proportional to how far ahead they reference. For a recorded file nobody notices. For a video call or a live analytics feed it is unacceptable, which is why real-time profiles disable B-frames entirely.

Back to the 12-camera problem

Now the fix from the opening is obvious. Run two streams:

  • Live analytics stream — no B-frames, short keyframe interval, moderate quality. Latency matters, size does not, because it is not retained.
  • Archive stream — B-frames on, keyframe interval at 4 seconds, higher compression. Latency is irrelevant, size is everything.

Neither the frame rate nor the camera count had to change.

Reading this from Python

import cv2

img = cv2.imread("photo.jpg")           # BGR uint8, already decoded
print(img.shape, img.dtype)             # (1080, 1920, 3) uint8

# Convert to YCbCr and look at the channels separately
ycc = cv2.cvtColor(img, cv2.COLOR_BGR2YCrCb)
y, cr, cb = cv2.split(ycc)
print("Y  range:", y.min(), y.max())
print("Cr range:", cr.min(), cr.max())

# Control JPEG quality explicitly. Default is 95; do not go below 90 for data.
cv2.imwrite("out.jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 95])

# Masks must be lossless
cv2.imwrite("mask.png", mask)           # correct
# cv2.imwrite("mask.jpg", mask)         # wrong: smears class boundaries

Checking whether an image has been heavily compressed:

import numpy as np

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
levels = np.unique(gray).size
print(f"{levels} distinct levels of 256")
# Well under 200 on a normal photo suggests requantisation somewhere upstream.

Reading video without silently landing on keyframes:

cap = cv2.VideoCapture("clip.mp4")
fps = cap.get(cv2.CAP_PROP_FPS)

frames, idx = [], 0
while True:
    ok, frame = cap.read()      # sequential read: no keyframe snapping
    if not ok:
        break
    if idx % int(fps) == 0:     # one frame per second
        frames.append(frame)
    idx += 1
cap.release()
print(f"{len(frames)} frames from {idx} total at {fps} fps")

Reading sequentially and discarding is slower than seeking, and it is correct. Seeking with CAP_PROP_POS_FRAMES is where the keyframe-snapping bias comes from.

Practice task

About forty minutes, and it will change how you handle data.

  1. Take one photo you own. Save it at JPEG quality 95, 75, 50 and 20. Record the four file sizes.
  2. Load the quality-20 version, zoom into a smooth area such as sky or a plain wall, and find the 8×8 grid.
  3. Count distinct grey levels in the original and in the quality-20 version using the snippet above.
  4. Now the important one. Take the quality-75 file, load it, save it again at 75, and repeat ten times. Compare round 1 against round 10 side by side.
  5. Finally, take a short video. Extract frames sequentially, then extract them again using CAP_PROP_POS_FRAMES seeking. Compare how many unique frames each method actually produced.

Step 4 is the one worth doing. The degradation from repeated encoding is much larger than most people expect, and it is exactly what a careless dataset pipeline does.

Summary

An image is sampled in space and quantised in value, and both choices throw information away permanently. Colour models exist for different jobs: RGB for screens, CMYK for ink, HSV for selecting colours under changing light, and YCbCr for compression, because separating brightness from colour lets a codec keep brightness sharp and blur the colour.

JPEG converts to YCbCr, halves the colour resolution, splits into 8×8 blocks, transforms each to frequencies, and rounds the high frequencies to zero. That is where both the compression and the square artifacts come from. Video goes further and stores differences: I-frames stand alone, P-frames reference the past, and B-frames reference both past and future — which is why they compress best and why they cannot be used live.

The practical consequences are worth more than the theory. Never JPEG a mask. Never re-encode a dataset repeatedly. Check for comb histograms. And read video sequentially unless you have a reason not to.

What comes next

Now that you know what has already been done to your pixels, OpenCV setup and first 10 tasks gets you loading and manipulating them in code.

If you want to keep going on the image-processing side, intensity transforms and frequency-domain filtering picks up the frequency idea introduced here and uses it to actually filter images — the same domain the DCT works in, applied to enhancement rather than compression.

Test your understanding
You are building a live intrusion-detection system on eight cameras. An operator must see an alert within half a second. To save disk you enable B-frames with a 4-second keyframe interval on the same stream your detector consumes. What goes wrong?
Test your understanding
Your segmentation model trains to 0.91 IoU on your validation set but only 0.84 on production images. Investigating, you find the training pipeline resizes and saves as JPEG, then augments and saves as JPEG again, then packages and saves a third time. Production images come straight from the camera at quality 95. What is the most likely cause?

Frequently asked questions

Why does JPEG produce square blocks in the image?
JPEG splits the image into 8×8 pixel blocks and compresses each one independently. Every block goes through its own DCT and its own quantisation, so each makes slightly different rounding errors. Because neighbouring blocks were never compared, their edges no longer line up after decoding, and at low quality that mismatch becomes a visible 8×8 grid. It is most obvious in smooth regions like sky, where the eye has nothing else to look at.
Should I convert images to YCbCr before feeding a neural network?
Generally no. Standard pretrained backbones expect RGB with specific normalisation statistics, and deviating from that throws away the benefit of the pretrained weights. YCbCr is worth using when you specifically want brightness independent of colour — some classical algorithms, or a preprocessing step where you equalise only the Y channel to avoid shifting the colours. Know that it exists and why codecs use it, but keep RGB as your default for models.
What is the difference between an I-frame, a P-frame and a B-frame?
An I-frame is self-contained and decodes on its own, essentially a JPEG inside the video. A P-frame stores only the difference from an earlier frame, typically about 30% of an I-frame's size. A B-frame stores the difference from both an earlier and a later frame, about 15%, which makes it the smallest but also means it cannot be decoded until that later frame has arrived. That dependency on the future is why B-frames add latency and are disabled in real-time profiles.
Is it safe to train a model on JPEG images?
Yes, provided the compression is applied once at reasonable quality — 90 or above — and, importantly, provided your production images have gone through similar processing. The failures come from mismatch and from repetition. Training on triple-encoded quality-75 images and running inference on clean camera output creates a domain gap the model never saw. Keep a lossless master, do augmentation in memory rather than through the filesystem, and encode at most once.
How do I tell whether an image has already been heavily compressed?
Count the distinct intensity values with numpy's unique on the grayscale version. A normal 8-bit photograph uses most of the 256 levels; substantially fewer suggests requantisation upstream. Then look at the histogram for a comb pattern of isolated spikes with gaps, and zoom into a smooth region at 400% to check for an 8×8 grid. All three signs together mean the file has been through a lossy step, and you should find the original before using it as training data.
Start typing to search across all content
navigate Enter open Esc close