COMP90086 · Computer Vision · Workshop 4

Colour, light
and learned filters

How a scene becomes three numbers per pixel —
and what happens when we stop choosing the kernels ourselves.
colour  ·  light & surfaces  ·  learned filters

QR code linking to the Workshop 4 slides
Scan to open the interactive slides on your device hesamasad.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?

SpaceWhat it is best atWhat 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.
2

Light and surfaces

Where a pixel value actually comes from.

Diffuse (Lambertian) reflectance

\[ {\color{#7fd6a8} I_D(x)} \;=\; {\color{#f7d774} I_L} \;\cdot\; {\color{#f2a65a} R} \;\cdot\; \bigl(\mathbf{N}(x) \cdot \mathbf{L}\bigr) \]
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

\( \text{Luminance} \;=\; {\color{#f2a65a}\text{Reflectance}} \;\times\; {\color{#f7d774}\text{Illumination}} \)

WHAT YOU SEE

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 3Workshop 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.

The MLP, counted

Input28×28×1
Flatten7840
Dense 161612,560
Dense 1010170
softmaxp(0…9)
\( \underbrace{784 \times 16 + 16}_{12{,}560} \;+\; \underbrace{16 \times 10 + 10}_{170} \;=\; \mathbf{12{,}730} \)
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 size F filter size P padding S stride
LayerWFPSOutput
Conv2D(8, 5×5)28501 (28−5)/1 + 1 = 24
MaxPooling2D(2×2)24202 (24−2)/2 + 1 = 12
Conv2D(16, 5×5)12501 (12−5)/1 + 1 = 8
MaxPooling2D(2×2)8202 (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×.

The worksheet's CNN, layer by layer

Input28×28×1
Conv 8@5×524×24×8208
MaxPool 2×212×12×80
Conv 16@5×58×8×163,216
MaxPool 2×24×4×160
Flatten2560
Dense 10102,570
blue = output shape orange = trainable parameters
\( \underbrace{(5\!\times\!5\!\times\!1)\!\times\!8 + 8}_{208} \;+\; \underbrace{(5\!\times\!5\!\times\!8)\!\times\!16 + 16}_{3{,}216} \;+\; \underbrace{256\!\times\!10 + 10}_{2{,}570} \;=\; \mathbf{5{,}994} \)
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

MLPCNN
Trainable parameters12,730 5,994
Test accuracy≈ 95.4% ≈ 98.0%
Errors per 10,000 test digits≈ 460 ≈ 200
Where the parameters sit99% 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)secondsminutes
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.

Cheat sheet


# --- colour ----------------------------------------------------------
img  = cv2.imread(path)                      # BGR, not RGB
rgb  = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # ...before plt.imshow
lab  = cv2.cvtColor(img, cv2.COLOR_BGR2Lab)  # uint8: L*255/100, a/b +128
lab[:, :, 1] = 255 - lab[:, :, 1]            # invert a*: reflect through grey
out  = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR)
hsv  = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)  # uint8: H is 0-179, NOT 0-359

# --- networks --------------------------------------------------------
x = x.astype(float) / 255                    # scale before anything else
x = np.expand_dims(x, axis=-1)               # (N, 28, 28) -> (N, 28, 28, 1)

model = keras.Sequential([
    layers.Input((28, 28, 1)),
    layers.Conv2D(8, (5, 5), activation='relu'),   # -> 24x24x8,   208 params
    layers.MaxPooling2D((2, 2)),                   # -> 12x12x8,     0 params
    layers.Conv2D(16, (5, 5), activation='relu'),  # ->  8x8x16,  3216 params
    layers.MaxPooling2D((2, 2)),                   # ->  4x4x16,     0 params
    layers.Flatten(),                              # ->     256
    layers.Dense(10, activation='softmax')])       # ->      10,  2570 params
model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
              metrics=['accuracy'])                # "sparse" = labels are ints
model.summary()                                    # call it BEFORE fit()
  
Output size (W − F + 2P)/S + 1 · conv params (F·F·Cin)·Cout + Cout · dense params in·out + out.

Play with these afterwards

  • CNN Explainer (Georgia Tech) — click any neuron, see the convolution that made it poloclub.github.io/cnn-explainer/
  • Adelson's checker-shadow illusion (MIT) — the original, with the proof image persci.mit.edu/gallery/checkershadow
  • Foundations of Computer Vision (MIT Press) — ch. 8 Colour, ch. 24 ConvNets visionbook.mit.edu/color.html
  • CS231n notes: Convolutional Networks (Stanford) — shapes, parameter sharing, the conv demo cs231n.github.io/convolutional-networks/
  • EECS 442 lecture 4, Light + Color (Michigan) — the same colour-space comparison, told again web.eecs.umich.edu/~justincj/slides/eecs442/winter2020/
  • A guide to convolution arithmetic for deep learning — Dumoulin & Visin's animations github.com/vdumoulin/conv_arithmetic
  • Metamers demo (Brown) — sketch two spectra, match their colour cs.brown.edu/courses/cs123/archive/2020/demos/metamers/
  • OpenCV colour conversion reference — the exact formulas, including the ranges docs.opencv.org/4.x/de/d25/imgproc_color_conversions.html
Next week: what those learned filters look like deeper in a real network (VGG16), and how to make convolution cheaper.
End of workshop 4

Questions?

Press S for speaker notes · O for the slide overview · F for fullscreen