How a scene becomes three numbers per pixel —
and what happens when we stop choosing the kernels ourselves. colour
·
light & surfaces
·
learned filters
Scan to open the interactive slides on your devicehesamasad.github.io/week-4-colour-and-cnns-workshop
Where we're going
1 — COLOUR
Why three numbers
RGB · HSV · XYZ · Lab
Manipulating colour
OpenCV's traps
2 — LIGHT & SURFACES
Diffuse reflectance
What sets a pixel value
Why vision is hard
Why edges survive
3 — LEARNED FILTERS
MNIST & preprocessing
MLP vs CNN
Counting parameters
A network, running live
The thread: parts 1 and 2 are about what a pixel value means.
Part 3 is about letting a machine discover which combinations of pixels are worth measuring —
the same convolution as last week, with the numbers learned instead of chosen.
1
Colour
An entire spectrum, collapsed into three numbers.
Three numbers from a whole spectrum
Each number is the area of the overlap between the light and one sensor's sensitivity.
\( I_X = \int I(\lambda)\,S_X(\lambda)\,d\lambda \) —
the integral sign just says "multiply the two curves and measure the area under the
result". Do it three times and the entire spectrum is gone, replaced by three numbers.
Two different lights, one colour
If only the three areas survive, then any two spectra with the same three areas are
indistinguishable.
Spectra that differ but look identical are metamers. This is not a curiosity —
it is the only reason screens work: a projector has just three narrow primaries, yet it can
forge almost any colour by matching the three areas.
One photo, four ways to split it into three numbers
Same information, different axes — each is an invertible function of the others.
What changes is which question becomes easy: RGB smears brightness through all three
channels; HSV and Lab put it in one and leave the other two for colour alone.
The same colours, three shapes
All three are built from the same set of colours — only the axes they are plotted
against change.
RGB and Lab are both rectangular — three perpendicular axes; only the meaning of the
axes changes. HSV is the polar one: hue is an angle, so it wraps.
Lab has a polar form too, LCh, where chroma is the radius and hue the angle —
that is Lab's cylinder, and it is what "how colourful, and which colour" really means.
So why are there so many?
Space
What it is best at
What it costs you
RGB
what sensors capture and screens emit — universal, and free
channels are correlated, and distance does not match perceived difference
HSV
picking and thresholding by colour; cheap to convert
hue wraps at 360° and is undefined at grey; still not perceptual
YCbCr
compression and transmission — JPEG keeps Y sharp and throws away
most of Cb and Cr, because your eye barely notices
a transmission format, not a working space
L*a*b*
perceptual distance, and edits that leave lightness alone
needs a white point; the most arithmetic of the four
Pick the space the task needs: RGB to store it, HSV to select it,
YCbCr to ship it, Lab to measure or edit it.
There and back again
Pick a space, read off the real channel ranges, then convert straight back to RGB.
The picture on the right is the round trip, and it looks perfect. It is not bit-identical:
each hop re-quantises to 8 bits. Note the ranges under each channel too —
H is 0–360° here, but OpenCV has to squeeze it into a byte.
Pick a pixel, read it four ways
Drag from the red macaw to the blue-and-gold one: a* swings from strongly positive
to negative while L* barely moves. That single number is "how red versus how
green" — which is why the exercise inverts it.
What the a* and b* axes actually are
Neutral grey sits at a* = b* = 0, in the centre.
"Invert the a* axis" means reflect this plane left-to-right: every red becomes its
matching green, and greys are left untouched.
Two ways to "swap the colours" — and why they differ
Swapping R and G also moves brightness around, because both channels carry
luminance. Inverting a* changes only the green–red opponent axis:
L* is untouched, so the shading and the shadows survive intact.
Four things OpenCV will do to you
1 · IMREAD GIVES YOU BGR
plt.imshow expects RGB, so a raw imshow(img)
shows the birds with red and blue traded.
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
2 · uint8 RANGES ARE SQUASHED
Hue is 0–360° but a byte only holds 0–255, so OpenCV stores H/2 → 0–179.
Lab stores L×255/100 and a*, b* offset by +128, so neutral grey is 128, not 0.
3 · BGR2XYZ SKIPS THE GAMMA
It applies the XYZ matrix to the gamma-encoded values. BGR2Lab
linearises first; BGR2XYZ does not — so the two are not consistent
with each other.
For mid-red (128,0,0):
OpenCV gives X = 53, the gamma-correct answer is X ≈ 23.
4 · ROUND TRIPS ARE NOT LOSSLESS
Every conversion re-quantises to 8 bits. On kodim23.png,
converting there and back changes ~75–80% of pixels:
max |error| — HSV 5, Lab 8,
XYZ 24. Convert to float first if you plan several hops.
Part 1 in one line each
Colour is three integrals of a spectrum, not three properties of a surface.
Because it is three integrals, different spectra can look identical — metamerism
is what makes displays possible.
Colour spaces are re-parameterisations: same information, different axes.
Pick the space where your edit is one operation on one axis.
Every conversion is lossy in 8 bits. Measure, don't eyeball.
Next: that spectrum did not come from nowhere. It is a light source, bounced off a surface,
at an angle — and all three of those are baked into every pixel you have.
A matte surface scatters light equally in all directions, so where you stand doesn't
matter — only how the surface is tilted relative to the light. That single dot product
is the entire shading model.
So what sets the R, G, B of one pixel?
\( I_L \) — THE LIGHT
Its intensity and its colour. A red surface under blue light is
not red in the image.
\( R \) — THE SURFACE
Reflectance, or albedo: the fraction of each wavelength the material
sends back. This is the thing we usually mean by "the object's colour".
\( \mathbf{N}\cdot\mathbf{L} \) — THE GEOMETRY
The angle between the surface normal and the direction to the light.
Pure shape, no colour.
This is part (i) of the worksheet exercise. Three things — light, material, orientation —
and the image only ever shows you their product.
The problem: only the product reaches the camera
A bright surface in dim light and a dark surface in bright light produce
the identical image. Recovering the world from one picture is
underconstrained — there are more unknowns than measurements.
Your visual system solves it anyway — by guessing well
In Adelson's checker-shadow figure, two squares with identical pixel values look
obviously different — because your visual system has already decided one of them is in
shadow and is reporting reflectance, not brightness.
WHAT THAT COSTS
The split is a guess built on assumptions: illumination varies smoothly, shadows have soft
edges, surfaces are locally uniform. Break the assumptions and the guess breaks —
which is exactly what a visual illusion is.
"Lightness constancy" is the visual system doing an inverse problem with a prior.
The illusion is not a bug — it is the price of the prior being usually right.
Your turn: which parameter is changing?
All surfaces Lambertian, one light source. At the circle — what is changing, and what is constant?
Which is why vision leans so hard on edges
THE ARGUMENT
We cannot recover \(R\) or \(\mathbf{N}\) from one image. But a change in
\(I_D\) reliably signals a change in something real: an occlusion boundary,
a corner, a change of material, or a shadow.
Which is why last week's derivative
filters are not a side topic — they are the most robust measurement we have.
AND THEY ARE INVARIANT
∂I/∂x is invariant to an intensity shift
\(I + b\) and tolerant to a contrast change \(aI\).
Turn the room lights up and the edges stay put.
Invariant to X = does not vary with X.
Tolerant to X = mostly insensitive to X. The lecture's distinction, and it is
examinable.
Almost every vision system starts by measuring local change — and, as we are about to see,
a CNN rediscovers exactly that in its first layer without being told to.
3
Learned filters
The same convolution — with the numbers chosen by data.
Last week we chose the numbers. This week we don't.
Workshop 3
Workshop 4
you write np.array([[1,0,−1],[2,0,−2],[1,0,−1]])
you write layers.Conv2D(8, (5, 5))
one kernel, chosen because you know what it detects
8 kernels, initialised at random
cv2.filter2D slides it over the image
the layer slides all 8 over the image — identically
the output is "the vertical edges"
the output is "8 activation maps" — you find out later what they mean
you tune it by thinking
gradient descent tunes it by measuring the loss
The convolution operation does not change at all. What changes is where the 25
numbers in each kernel come from.
The dataset: MNIST
60,000 training and 10,000 test images, 28×28, one 8-bit channel, labels 0–9 —
and roughly 6,000 examples per class, so accuracy is a fair metric here.
On an unbalanced dataset it would not be.
Why divide by 255?
Gradient descent takes one step size in every direction at once.
If features live on wildly different scales, the loss surface is a stretched valley and
no single learning rate suits it.
An MLP starts by throwing the geometry away
The shuffled digit is unreadable to you — but that is not the point.
Train an MLP on shuffled images and it scores exactly the same, because
Flatten() already threw away which pixels were neighbours.
The CNN is the one that breaks. Its advantage is a restriction, not extra power.
A dense layer's weights are (inputs × outputs), plus one bias per output.
Nearly every parameter is in the first layer — because it has to look at all 784 pixels at once.
The two non-linearities in this model
ReLU sits after each hidden layer: without something non-linear between them,
stacked linear layers collapse into a single linear layer.
Softmax sits at the very end, turning 10 unconstrained scores into a distribution.
A conv layer is last week's sliding window
One kernel sweeps the whole image and produces one activation map(lectures 6–8's term; worksheet 5 calls the same thing a feature map).
The same 25 weights are reused at every position — that reuse is called
parameter sharing, and it is where the savings come from.
Where every shape comes from
\[ O \;=\; \left\lfloor \frac{W - F + 2P}{S} \right\rfloor + 1 \]
W input sizeF filter sizeP paddingS stride
Layer
W
F
P
S
Output
Conv2D(8, 5×5)
28
5
0
1
(28−5)/1 + 1 = 24
MaxPooling2D(2×2)
24
2
0
2
(24−2)/2 + 1 = 12
Conv2D(16, 5×5)
12
5
0
1
(12−5)/1 + 1 = 8
MaxPooling2D(2×2)
8
2
0
2
(8−2)/2 + 1 = 4
Keras defaults to padding='valid' (P = 0), so every 5×5 conv costs
you 4 pixels. Use padding='same' if you want the size preserved.
Lecture 7 writes the no-padding case as
\( \lceil (W - F + 1)/S \rceil \) and the padded case as \( \lceil W/S \rceil \).
The first is algebraically the same formula as the one above — use whichever you
find easier to remember.
Max pooling: keep the strongest response, forget where exactly
It has no parameters. It buys a little tolerance to small shifts, and it shrinks
every later layer's work by 4×.
A conv layer's cost is (filter area × input channels) × filters, plus one bias per
filter — and crucially it does not depend on the image size.
What it actually learned
Several are clear oriented light-to-dark ramps — last week's derivative filters,
discovered rather than designed. But look again: unlike Sobel, most of these
sum well above zero, so they measure "how much ink is here" and "which way it runs"
at the same time.
The whole network, running right now
Sharp, digit-shaped responses early; small, abstract, hard-to-read maps late.
That progression — from where the strokes are, to what the strokes mean —
is the whole idea of a deep network.
MLP vs CNN — the answer to the worksheet's last question
MLP
CNN
Trainable parameters
12,730
5,994
Test accuracy
≈ 95.4%
≈ 98.0%
Errors per 10,000 test digits
≈ 460
≈ 200
Where the parameters sit
99% in the first dense layer
54% in conv 2, 43% in the final dense
If the image doubled to 56×56
≈ 4× the parameters
conv layers unchanged; only the layer after Flatten grows
Training time (CPU)
seconds
minutes
Fewer than half the parameters, and less than half the errors. Not because the CNN
is bigger, but because parameter sharing and locality are true of images — so the
constraint costs nothing and saves a great deal.
4
What to carry out of this room
Habits that outlast the worksheet.
Take-homes — colour and light
1 · CHOOSE THE SPACE, THEN THE OPERATION
Before writing a colour manipulation, ask which axis of which space your intent is a
single move along. If the answer is "three coupled changes in RGB", you are in the wrong
space.
"Keep the lighting, change the hue" is
one subtraction in Lab and a mess in RGB.
2 · A CONVERSION IS A COMPUTATION, NOT A RELABELLING
Every hop through cvtColor quantises, clips, and may or may not
linearise. Convert to float once, do all your work, convert back once.
And check the ranges — the one that bites
is hue at 0–179.
3 · A PIXEL IS A PRODUCT, NOT A PROPERTY
\(I_D = I_L \cdot R \cdot (\mathbf{N}\cdot\mathbf{L})\). When something in an image
changes, the useful question is not "what colour is that" but
which of the three moved?
Light, material, orientation. Getting into
the habit of listing all three is most of the marks on this kind of question.
4 · UNDERCONSTRAINED MEANS YOU MUST ADD SOMETHING
One image, three unknowns. Every method that recovers shape or reflectance is smuggling in
an assumption — smooth lighting, constant albedo, a learned prior.
Name the assumption out loud and you will
predict where the method fails.
Take-homes — learning
5 · COUNT THE PARAMETERS BEFORE YOU TRAIN
(F·F·Cin)·Cout + Cout for conv,
in·out + out for dense. It takes ten seconds and tells you where
your model's capacity actually is.
Do the sum rather than guessing: in today's
CNN it is the second conv layer that dominates, not the dense one — and that only
flips as the input grows.
6 · AN ARCHITECTURE IS A SET OF ASSUMPTIONS
Convolution assumes nearby pixels are related and that a useful pattern is worth finding
everywhere. Those are claims about images, and they are why the CNN wins with
fewer parameters.
When an architecture underperforms, ask
which of its assumptions your data violates.
7 · ACCURACY IS NOT A RESULT ON ITS OWN
Report it against a baseline, on a balanced set, from more than one run — and prefer error
counts when the numbers are close to 100%.
95% vs 98% sounds marginal.
460 errors vs 200 does not. Same data.
8 · TRAIN, VALIDATION AND TEST ARE THREE DIFFERENT THINGS
validation_split=0.2 carves the validation set out of the
training data. It guides your choices; it does not measure them.
The moment you tune anything against the
test set, it stops being a test set.
5
Your turn
worksheet04.ipynb
The five exercises
1 · RGB → XYZ AND RGB → Lab
Convert, show each channel, convert back.
Show channels with
cmap='gray' — they are single-channel, not colour.
2 · INVERT THE a* AXIS
Convert to Lab, flip a, convert back.
Neutral is stored at 128, not 0 — so the
flip is 255 − a, not −a on a raw
uint8. Compare against the R↔G swap.
3 · WHICH PARAMETER IS CHANGING?
The three chessboard images: name the parameters, then say which move and which hold.
Part (i) wants all three named. Part (ii)
wants a decision per image — and image 3 has two answers.
4 · RESHAPE FOR KERAS
Make the input 4-D: (N, 28, 28, 1).
np.expand_dims(x, axis=-1).
Do this before the visualisation cell — it indexes
[s,:,:,0] and will fail otherwise.
5 · BUILD THE CNN
Fill in the four missing layers, train it, then answer: how does it compare with the MLP
on parameter count and test accuracy?
Read the table in the worksheet literally —
8 then 16 filters, 5×5, stride 1, ReLU, with 2×2 pooling after each. Then check your
hand-computed parameter counts against summary().
Heads-up on the notebook: the exercises restart their numbering halfway through
(there are two "Exercise 1"s and two "Exercise 2"s), and the shading question is labelled
Exercise 5 in your copy but Exercise 3 in the solutions. Go by the section
heading, not the number.