← All courses

Computer Vision (Developer) · Level 101

Computer Vision Developer 101 — Build Your First Land-Cover Classifier

Join PDLL as a junior developer — implement the CNN pipeline in code, from RGB arrays to a trained classifier on synthetic tiles.

6 modules Peak District Landscape Lab VS Code · Jupyter Notebook · Python · numpy · matplotlib · Pillow · TensorFlow

Computer Vision (Developer) · Level 101

Computer Vision Developer 101 — Build Your First Land-Cover Classifier

Join PDLL as a junior developer — implement the CNN pipeline in code, from RGB arrays to a trained classifier on synthetic tiles.

6 modules Peak District Landscape Lab VS Code · Jupyter Notebook · Python · numpy · matplotlib · Pillow · TensorFlow

Computer Vision Developer 101 — Build Your First Land-Cover Classifier

Prerequisite: Data Science 101.

This course assumes you can use VS Code, Jupyter Notebooks, Python and matplotlib. You will write Python code from the instructions below.

Prefer interpreting maps and writing briefs without building models? Take Computer Vision Analyst 101 instead — same PDLL setting, minimal implementation.

Who this course is for

Developers and coders training at the fictional Peak District Landscape Lab (PDLL) who want to implement the CNN pipeline — not only read the maps.

External learners: Use synthetic and public datasets (EuroSAT in 201). You do not need APGB aerial access.

What PDLL mirrors: The real Peak District CNN project (public details only — no real staff names in this course).

The published project PDLL is modelled on

Since 2023, Peak District National Park — with Cranfield University and the Alan Turing Institute — has published work on using CNNs to map the whole national park (1,439 km²) from Bluesky aerial photography at 12.5 cm ground resolution, via the Aerial Photography Great Britain (APGB) contract.

That published approach (press release, 2023):

  1. Split orthophotos into 64 m × 64 m patches (512 × 512 pixels).
  2. Hand-label 1,027 patches using a detailed UK habitat schema (Taylor et al., 1991).
  3. Train CNNs in multiple stages — broad classes first (moorland, woodland, grassland, ~95% accuracy), then finer subclasses (heather moor, deciduous woodland, wet grassland and rush pasture, typically 72–92%).
  4. Merge predictions with Ordnance Survey topographic data for a complete map.

That work supports Landscape Recovery, nature recovery monitoring, and species programmes such as water vole habitat assessment. Researchers are also exploring change detection between 2010 and 2020 imagery (Alan Turing Institute Data Study Group, 2023).

Public data and code: github.com/pdnpa/cnn-land-cover.

Your manager at PDLL is Andy Ipping (Senior Data Analyst — yes, the initials are deliberate). His job in this fictional programme is to make sure you understand the CNN pipeline — not just the GIS output — before you touch real operational maps anywhere.

How this course maps to that published work

Module What you practice in PDLL Step in the published project
1 Why high-resolution land cover matters Monitoring gap since the 1991 survey
2 Images as RGB number grids Bluesky APGB 12.5 cm tiles
3 Convolution filters Low-level pattern detection inside CNNs
4 Multi-stage CNN pipeline High-level pass, then subclass pass
5 Train a tiny classifier Simplified version of patch labelling
6 Reflection Ready for CNN 201 and public datasets

Over five modules you will:

  1. Understand why organisations invest in CNN mapping — and what rangers use the output for.
  2. Open an aerial image and see it as a grid of numbers (pixels).
  3. Run simple filters that highlight edges and textures — the same operation CNNs automate.
  4. Learn how multi-stage CNNs turn patterns into habitat labels.
  5. Train a tiny classifier on synthetic Peak District-style tiles (stand-in until EuroSAT in 201 or the public pdnpa/cnn-land-cover patches).

The mindset to keep throughout:

Bluesky photo → pixels → patterns → habitat label → 1,439 km² map


Before you start — create your project folder

Copy the developer starter kit (cnn-developer-101-starter.zip) or create folders manually:

# After downloading and extracting the zip into ~/coding/vision101
cd ~/coding/vision101

See also the glossary and worked example charts.

Or create manually: a folder called vision101 inside your coding folder:

vision101/
├── data/
├── notebooks/
├── outputs/
│ └── charts/
└── report/

Open VS Code. Choose File → Open Folder and open vision101.

You will save notebooks in notebooks/, charts in outputs/charts/, and short written answers in report/.

Install Python packages (one time)

Use Python 3.10 or 3.11 if you can — TensorFlow does not support every bleeding-edge Python version on all machines.

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -r requirements.txt    # or: pip install numpy matplotlib pillow jupyter ipykernel scikit-learn tensorflow

TensorFlow is a large download (~5–10 minutes on good Wi-Fi). Module 5 training takes about 1–2 minutes on a typical laptop.

If install fails — try this order

  1. Google Colab — upload notebooks; TensorFlow is pre-installed. Skip local pip install and save outputs to Google Drive or download charts manually.
  2. Python 3.11 venvpython3.11 -m venv .venv then install again.
  3. School IT — ask whether TensorFlow is allowed on managed laptops.
  4. Teacher / mentor — share the exact error message, not just “it didn’t work.”

Expected outputs (check your work)

Sample student outputs: Worked examples. Your charts should look broadly similar (three-band tile, greenness line high at top, edge boundaries glowing, prediction map matching three stripes).

Viewing images and charts you save

Whenever code writes a .png file:

  1. In the notebook — keep plt.show() (or display(...)) in the same cell so the image appears inline below the code.
  2. In VS Code — click the file in Explorer (e.g. data/peak_tile.png or outputs/charts/module2_green_profile.png). VS Code opens a preview so you see the actual picture, not just a file path.
  3. Check both — the notebook view and the saved file should match. If they do not, re-run the cell before continuing.

Tools

  • VS Code
  • Jupyter Notebook
  • Python
  • numpy
  • matplotlib
  • Pillow (for reading images)
  • TensorFlow (for the tiny neural network in Module 5)

Module 1 — Why Map From Above?

Manager email

From: Andy Ipping
Subject: Welcome — understand the map before you trust it

Welcome to the Peak District Landscape Lab — a fictional training team. Before I point you at our teaching materials or the public pdnpa/cnn-land-cover repository, I need you to understand why real national parks invest in CNN mapping and what rangers do with the output.

The last time land cover was mapped in this detail across UK national parks was 1991. Manual interpretation cannot keep pace with Landscape Recovery, nature recovery targets, or questions like "where is wet grassland fragmenting?" Published work from the Peak District — with Cranfield and the Turing Institute — shows how Bluesky APGB photos and two-pass CNNs can help.

Your first task is research, not coding. Read that published work (start with the 2023 press release and the Turing impact story). Then send me a one-page note answering:

  1. Why can't a park rely on walking every field and moor anymore?
  2. Name one documented use of the published land-cover map.
  3. What would you ask before showing a CNN map to a ranger?

No Python today.

— Andy

Why this matters

The published CNN maps are not an academic exercise. They feed decisions about where to restore habitats, nature recovery tracking, and field work — for example, where to survey for water voles beyond obvious stream edges.

A junior analyst who only clicks layers in QGIS will eventually misread a map. Someone who understands pixels, filters and staged classification can ask the right questions before a grazing or planting decision.

What you'll learn

  • What the published Peak District CNN project delivers and who uses it
  • How Bluesky APGB photography differs from coarse satellite summaries
  • The roles of Cranfield University and the Alan Turing Institute in the research
  • How to read a habitat map critically before briefing colleagues

Who was involved in the published project

Role Organisation Contribution
National park Peak District NPA Local habitat knowledge, labels, operational use
Remote sensing Cranfield University Method design, earth observation expertise
AI research Alan Turing Institute Multi-stage CNN development, change detection
Imagery supply Bluesky (APGB contract) 12.5 cm RGB orthophotos across 1,439 km²

(PDLL is fictional; the table describes real published partners.)

Your tasks

Create a new file:

report/module1_land_mapping_research.md

Answer the following in clear prose (roughly 300–500 words total).

Task 1 — Why CNN mapping for a national park?

In your own words, explain at least two reasons a park might map from aerial photos instead of field survey alone. Use real context from the published project: 1,439 km², mixed ownership, upland access, Landscape Recovery, repeat monitoring every few years.

Task 2 — One documented use

From the press release or Turing impact page, describe one way the published map is used in practice (not a hypothetical). Quote or paraphrase accurately.

Task 3 — Questions for rangers

List two questions you would ask a ranger or ecologist before they rely on a CNN-derived habitat boundary.

Reflection questions

  • Would you rather visit the Peak District in person or analyse it from a laptop?
  • How comfortable are you with answers that are "probably grassland" rather than perfectly certain?
  • Do you like the idea of your code affecting real conservation decisions?

Manager feedback

From: Andy Ipping

Good first step. The clients who pay us do not care about "AI" as a buzzword. They care whether the grazing map helps them avoid over-grazing Kinder Scout or whether the woodland layer matches what rangers see on the ground.

Tomorrow we open an image file and stop treating it as a picture. We treat it as a table of numbers.

— Andy

AQA Mathematics links

  • Scale and area — mapping km² of land cover
  • Sampling — checking a few ground-truth fields to validate a map

Beyond A-Level

Look up the public repository github.com/pdnpa/cnn-land-cover and the Remote Sensing paper on multi-stage semantic segmentation across the PDNP (Plas et al., 2023).


Module 2 — Images as Numbers

Manager email

From: Andy Ipping
Subject: Your first aerial tile — same idea as Bluesky, smaller file

Production maps use Bluesky APGB orthophotos at 12.5 cm — each 64 m × 64 m patch is 512 × 512 pixels. Today you will open a synthetic teaching tile with the same RGB structure: improved grassland, heather moor / rough grazing, and woodland blocks inspired by Kinder Scout and the White Peak.

(PDLL staff with APGB access: compare colours to a real orthophoto tile.)

  1. Display the tile in Python.
  2. Print RGB values at three points.
  3. Chart average greenness by row.

Save outputs/charts/module2_green_profile.png.

— Andy

Why this matters

Every CNN, no matter how sophisticated, starts here: a colour image is a 3D array of height × width × 3 (red, green, blue). Each number is usually 0–255.

Once you internalise that, "the computer recognised trees" becomes "the computer found a pattern in the numbers that often appears where trees are." Less magic, more mechanism.

What you'll learn

  • How a digital image is stored (height, width, channels)
  • How to read and display images with Pillow and matplotlib
  • Why grass looks green in RGB space
  • How to index into a numpy array to inspect individual pixels

Concept boxes (amber cards with a light-bulb icon in the web view) explain one idea in plain language before you code. Read them even if you skip the formulas — they are the bit that makes the code make sense.

Step 1 — Create the notebook

notebooks/module2_pixels.ipynb

After the greenness chart, try changing one grassland RGB value in Section 1 and predict how the profile line will shift before re-running.


Section 1 — Generate a Peak District-style tile

We use a synthetic image so everyone has the same file and you can re-run the code years from now without hunting for satellite downloads.

Question

Can we draw a simple aerial scene with three land types?

Code — build and save

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
h, w = 128, 128
img = np.zeros((h, w, 3), dtype=np.uint8)

# Top third: bright grassland (lime green)
img[: h // 3, :, 0] = 60
img[: h // 3, :, 1] = 140 + rng.integers(-15, 15, size=(h // 3, w))
img[: h // 3, :, 2] = 50

# Middle third: heather moor / grazing (purple-brown patches)
for _ in range(40):
    cx, cy = rng.integers(0, w), rng.integers(h // 3, 2 * h // 3)
    radius = rng.integers(4, 12)
    y, x = np.ogrid[-cy : h - cy, -cx : w - cx]
    mask = x * x + y * y <= radius * radius
    img[mask & (np.arange(h)[:, None] >= h // 3) & (np.arange(h)[:, None] < 2 * h // 3)] = [
        110, 85, 70,
    ]
img[h // 3 : 2 * h // 3, :, 1] = np.minimum(
    img[h // 3 : 2 * h // 3, :, 1].astype(int)
    + rng.integers(-10, 10, size=(img[h // 3 : 2 * h // 3].shape[0], w)),
    255,
).astype(np.uint8)

# Bottom third: woodland (dark green blobs)
for _ in range(25):
    cx, cy = rng.integers(0, w), rng.integers(2 * h // 3, h)
    radius = rng.integers(6, 18)
    y, x = np.ogrid[-cy : h - cy, -cx : w - cx]
    mask = x * x + y * y <= radius * radius
    img[mask & (np.arange(h)[:, None] >= 2 * h // 3)] = [25, 70, 35]

Image.fromarray(img).save("../data/peak_tile.png")
print("Saved ../data/peak_tile.png")

Run that cell. You have not seen the picture yet — only created the file.

Expected result: three horizontal bands — green top, patchy middle, dark woodland blobs bottom. Compare with the sample peak tile if unsure.


View the tile as an image

Stop here. Before you inspect pixel numbers, look at the tile with your eyes.

In the notebook — new cell:

img = np.array(Image.open("../data/peak_tile.png"))

plt.figure(figsize=(5, 5))
plt.imshow(img)
plt.title("Synthetic Peak District aerial tile")
plt.axis("off")
plt.show()

You should see three horizontal bands: green grassland (top), patchy moor (middle), dark woodland blobs (bottom). If the cell shows a blank square or an error, check that ../data/peak_tile.png exists and re-run the save cell above.

In VS Code — click data/peak_tile.png in Explorer. The editor shows the same image as a preview. You are looking at the same file the CNN will read — not a separate copy.

Do not continue until you can see the tile both ways.

What the code does

We build a 128×128 colour image as a numpy array and save it. The view cell loads that file back and displays it — the same path a CNN uses.

Each pixel has three channels: R, G, B (0 = none, 255 = full brightness).

  • The top band is bright green — improved grassland.
  • The middle band is patchy purple-brown — heather moor and rough grazing.
  • The bottom band is dark green blobs — scattered trees and woodland.

This is simplified, but it captures the idea: different land covers look different in colour and texture.


Section 2 — Inspect pixel values

Look with your eyes first

Open peak_tile.png in Explorer (or use the view cell from Section 1). Without code, answer in your head — or jot in report/module2_notes.md:

  1. Which horizontal band looks brightest green — top, middle, or bottom?
  2. Which looks darkest overall?
  3. Tap or imagine one point in each band. Would you expect the middle number of RGB (green) to be highest in the top band?

You are making predictions. The code below checks what you already saw.

Question

What numbers sit at three points Andy marked on the map?

Code — confirm your predictions

img = np.array(Image.open("../data/peak_tile.png"))
print("Shape (height, width, channels):", img.shape)

points = {
    "grassland (top)": (20, 64),
    "moorland (middle)": (64, 64),
    "woodland (bottom)": (100, 64),
}

for name, (row, col) in points.items():
    r, g, b = img[row, col]
    print(f"{name:22s} row={row:3d} col={col:3d}  RGB=({r:3d}, {g:3d}, {b:3d})")

What the code does

img[row, col] returns [R, G, B]. Compare the printed numbers to your predictions from above:

  • Grassland (top) should show a high G (green) value.
  • Woodland (bottom) should show low R and low G — darker overall.
  • Moorland (middle) sits in between, often more red-brown.

If the numbers surprise you, look at the tile again at those row/column positions. The code is confirming what is already in the picture.

A CNN eventually learns these relationships automatically. Today you are reading them by hand.


Section 3 — Greenness profile

Look with your eyes first

Look at peak_tile.png again — full image, not zoomed to one pixel.

  1. Does the top third look more green overall than the bottom third?
  2. Do you expect a line graph of "greenness by row" to be higher at the top and lower at the bottom?

Sketch a rough answer (even a wavy line on paper). Then run the code — the chart should match what you already saw.

Question

Does the average green channel drop as we move down the image?

Code — draw the greenness chart

green_by_row = img[:, :, 1].mean(axis=1)

plt.figure(figsize=(8, 4))
plt.plot(green_by_row, color="green")
plt.xlabel("Row (top = sky side of photo)")
plt.ylabel("Average green channel")
plt.title("Greenness profile — synthetic Peak District tile")
plt.tight_layout()
plt.savefig("../outputs/charts/module2_green_profile.png", dpi=150)
plt.show()
print("Saved — also open ../outputs/charts/module2_green_profile.png in Explorer")

What the code does

For each row of the image we average the green channel across all columns. That produces one number per row — a simple summary of "how green is this strip of the photo?"

The line should be higher where you saw bright grass and lower where you saw dark woodland. If the chart contradicts your eyes, re-open the tile and find out why (mixed bands, one dark blob, etc.) before moving on.

This is a very simple feature — one number summarising part of the image. CNNs learn richer features, but the idea starts here: look first, then measure.


Reflection questions

  • If you showed only the RGB triple (25, 70, 35) to a friend, could they guess woodland? What about a whole 32×32 patch?
  • Why might averaging one row be too crude to map the whole Peak District?
  • Did manipulating arrays feel similar to working with pandas tables?

Manager feedback

From: Andy Ipping

Good. You now see what I see when I open a GeoTIFF from a drone flight: a big numeric array.

Colour alone is not enough — moor and woodland can both look dark. Tomorrow we look at texture: the small patterns that distinguish rough heather from a smooth grass field.

— Andy

AQA Mathematics links

  • Arrays and matrices — images as 2D and 3D structures
  • Averages — summarising rows and regions

Beyond A-Level

Search for RGB vs multispectral satellite bands. Professional land mapping often uses wavelengths the human eye cannot see (e.g. near-infrared), which makes separating vegetation types easier.


Module 3 — Filters & Textures

Manager email

From: Andy Ipping
Subject: Edge filters — how computers "feel" texture

Two fields can both look green. One is smooth pasture, one is rough tussock grass. The difference is texture — how much the brightness changes from pixel to pixel.

CNNs use small filters (also called kernels) that slide across the image and highlight patterns. Today you'll run one by hand with numpy — no neural network yet.

Apply a simple edge-detection filter to peak_tile.png. Save the result as outputs/charts/module3_edges.png and write three sentences on what got highlighted.

— Andy

Why this matters

The core operation inside a CNN is called convolution: multiply a small grid of weights by a patch of the image and sum the result. Repeat everywhere.

You are about to do exactly that manually. Once you have felt it with your own code, CNNs become "many filters, learned from data" instead of a black box.

What you'll learn

  • What a convolution filter does, in plain language
  • Why edges and texture matter for land-cover mapping
  • How to apply a 3×3 kernel with numpy (no ML library required)

Step 1 — Create the notebook

notebooks/module3_filters.ipynb

You will implement convolution with a pixel loop — the same operation a Conv2D layer automates in Module 5.


Section 1 — Greyscale simplifies the story

Question

Can we focus on brightness only?

Code

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

img = np.array(Image.open("../data/peak_tile.png"))
# Standard luminance weights for RGB → grey
grey = (0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2])

plt.imshow(grey, cmap="gray")
plt.title("Greyscale aerial tile")
plt.axis("off")
plt.show()

What the code does

We combine R, G and B into one greyscale value per pixel. Many edge filters work on greyscale first. Colour comes back later when we need to distinguish heather from woodland.


Section 2 — A simple edge filter

Look with your eyes first

Before any edge code, open peak_tile.png and find boundaries yourself:

  1. Where does the bright green top meet the middle band?
  2. Where does the middle meet the dark woodland bottom?
  3. Inside the middle band, do you see spotty patches (busy texture) compared to the smoother top?

Mark them mentally or trace on paper. These are edges your eye already sees. The filter below should light up those same places — if it does not, something went wrong.

Question

Does the edge filter highlight the boundaries you already spotted?

Code — run the filter and compare

kernel = np.array([
    [-1, 0, 1],
    [-2, 0, 2],
    [-1, 0, 1],
], dtype=float)

h, w = grey.shape
edges = np.zeros((h - 2, w - 2))

for row in range(1, h - 1):
    for col in range(1, w - 1):
        patch = grey[row - 1 : row + 2, col - 1 : col + 2]
        edges[row - 1, col - 1] = np.sum(patch * kernel)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(grey, cmap="gray")
axes[0].set_title("Original greyscale")
axes[0].axis("off")
axes[1].imshow(np.abs(edges), cmap="inferno")
axes[1].set_title("Vertical edges (absolute values)")
axes[1].axis("off")
plt.tight_layout()
plt.savefig("../outputs/charts/module3_edges.png", dpi=150)
plt.show()
print("Saved — open module3_edges.png in Explorer and compare to peak_tile.png")

Expected result: bright horizontal lines at the three band boundaries; busier texture in the middle (moor) band. See the sample edge chart.

What the code does

Left panel: greyscale tile — same as your eyes saw, minus colour.

Right panel: filter output. Bright pixels = strong vertical edges (big left–right brightness change). Dark = smooth areas.

Check the horizontal boundaries between the three bands — they should glow. Check the patchy moor — often brighter than smooth grass because texture creates many small edges.

If the bright lines match the boundaries you marked before coding, the matrix did its job: it measured what you already saw.

This sliding-window multiply-and-sum is convolution. CNNs learn the kernel values automatically instead of us writing [-1, 0, 1, ...].


Section 3 — Compare land bands

Look with your eyes first

Look at the three horizontal bands in peak_tile.png again:

  1. Which band looks smoothest?
  2. Which looks busiest (most spots, blobs, or contrast)?

Guess which will score highest on "edge strength" before you run the code.

Question

Does the busiest-looking band match the highest edge numbers?

Code — confirm with averages

regions = {
    "grassland": grey[: grey.shape[0] // 3, :],
    "moorland": grey[grey.shape[0] // 3 : 2 * grey.shape[0] // 3, :],
    "woodland": grey[2 * grey.shape[0] // 3 :, :],
}

for name, band in regions.items():
    eh, ew = band.shape
    e = np.zeros((eh - 2, ew - 2))
    for row in range(1, eh - 1):
        for col in range(1, ew - 1):
            patch = band[row - 1 : row + 2, col - 1 : col + 2]
            e[row - 1, col - 1] = abs(np.sum(patch * kernel))
    print(f"{name:12s}  mean edge strength: {e.mean():.2f}")

What the code does

We measure average edge strength in each horizontal band. The ranking should match your eyeball guess from above — usually moorland and woodland show more texture than uniform grass.

If the numbers disagree with your eyes, trust your eyes first and debug (wrong band split, filter direction, etc.).


Your written task

Add report/module3_filter_note.md with three sentences:

  1. What did the edge filter highlight — and did it match what you saw before running the code?
  2. Which land type had the strongest average edges — and did that match your eyeball guess?
  3. Why might edge strength alone not be enough to map the whole Peak District?

Reflection questions

  • The loop over pixels is slow. Production code uses optimised libraries — but the maths is identical. Does knowing the maths make you trust the tool more?
  • Could you design a filter that detects "blobbiness" instead of edges?

Manager feedback

From: Andy Ipping

You just implemented the heart of a CNN layer. Professional models use dozens of filters at once — some find edges, some find spots, some find stripes — and later layers combine those patterns into "this looks like heather."

Next module: the full CNN story, still keeping the jargon light.

— Andy

AQA Mathematics links

  • Matrix multiplication — each 3×3 patch times a 3×3 kernel
  • Gradients and rates of change — edges as sharp brightness change

Beyond A-Level

Look up 2D convolution in image processing. The notation is more formal, but you already ran a working example.


Module 4 — How CNNs Think

Manager email

From: Andy Ipping
Subject: From filters to the two-pass CNNs in the published pipeline

You can spot grass, heather moor and woodland because your brain builds patterns. Our production system does something similar — in two passes:

  1. High-level classes — moorland, woodland, grassland (~95% accuracy).
  2. Subclasses — heather moor, deciduous woodland, wet grassland and rush pasture, and others (typically 72–92%).

Thijs van der Plas (Turing Institute) and Cranfield designed this multi-stage semantic segmentation approach because rare habitats are easily drowned out in a single model. Today you explain that pipeline back to me before we train anything in Module 5.

Write report/module4_cnn_explainer.md (400–600 words) and sketch a diagram. Reference our MDPI Remote Sensing paper (2023) if helpful — you do not need every class code.

— Andy

Why this matters

Most introductions either drown you in layer names or wave their hands and say "it's like the brain." Neither helps. The useful mental model is a pipeline:

  1. Small filters scan the image and fire on local patterns (edges, blobs, colour patches).
  2. Pooling shrinks the map — "was there strong edge activity here?" without remembering every exact pixel.
  3. Deeper layers combine earlier patterns — edges + dark green blobs → "tree canopy texture."
  4. Final layer outputs scores for each class: 70% grassland, 20% moorland, 10% woodland.

None of this requires you to label pixels by hand at the end. You label whole example patches during training; the network learns which filters help.

What you'll learn

  • The two-pass CNN story without heavy maths
  • Why labelled training data matters
  • How a Peak District patch becomes a land-cover label
  • Limits: when CNNs fail (clouds, shadows, rare classes)

The pipeline — read this carefully

Imagine a 64 m × 64 m patch from a Bluesky tile over the Dark Peak — the same patch size used in the published project.

Stage 1 — High-level segmentation (Pass 1)

Dozens of learned filters slide across the patch — like your Module 3 loop, but trained on 1,027 hand-labelled patches sampled across the park. The network predicts broad classes: moorland, woodland, grassland, arable, and others from the Taylor schema.

Output: a coarse habitat mask. Accuracy here is typically around 95% — similar to human interpreters working at this scale.

Stage 2 — Subclass segmentation (Pass 2)

The high-level mask restricts where subclasses are predicted — heather moor only inside moorland, deciduous woodland only inside woodland. This is why rare classes like wet grassland and rush pasture (F3d) survive training instead of being ignored.

Subclass accuracy is lower (72–92%) because categories look similar from above and because some habitats are naturally fragmented.

Stage 3 — Merge with Ordnance Survey

CNN output does not include buildings and roads reliably. The published project merges predictions with OS topographic layers for a complete map rangers can navigate.

Stage 4 — Use in monitoring

The finished map supports Landscape Recovery, tracks nature recovery indicators, and directs field work — e.g. where to survey for water voles beyond obvious stream edges.

Training in one paragraph

You show the network thousands of labelled patches: "this 32×32 tile is grassland," "this one is woodland." It guesses, compares to your labels, and nudges filter weights to reduce mistakes — backpropagation and gradient descent do the nudging. You do not need the calculus this week; you need to know labels teach the filters.

Where CNNs struggle

Be honest in your report about limitations:

Problem Example in the Peak District
Different flight dates Seven APGB capture dates — shadows and colour vary
Mixed pixels One 64 m patch half grass, half path
Rare subclasses Wet grassland fragments — few training patches
Seasonal change Heather purple in autumn, brown in winter
Change over time 2010 vs 2020 imagery — active research (Turing DSG)

Professional teams combine CNNs with ground truth visits, multispectral bands, and time series (photos across seasons). Your tiny model is the first step in that world, not the final product.

Your tasks

Create report/module4_cnn_explainer.md covering:

  1. The scenario — one paragraph on mapping the Peak District from above.
  2. The four stages — convolution, pooling, deeper layers, classification — in your own words, no copy-paste from this page.
  3. A diagram — boxes and arrows from "aerial photo patch" to "label: grassland / moorland / woodland."
  4. One limitation — when would you not trust the model alone?

Reflection questions

  • Does the filter idea from Module 3 make the CNN feel less mysterious?
  • Would you rather design filters by hand or train them from data?
  • Who should decide what happens when the model is 60% confident?

Manager feedback

From: Andy Ipping

If you can explain this to a ranger without saying "tensor," you're already ahead of many graduate applicants.

Tomorrow you train a miniature version on synthetic tiles. It will not change national policy — but it will prove you understand the loop: data → model → prediction → map.

— Andy

AQA Mathematics links

  • Functions and composition — layers feeding layers
  • Probability — softmax outputs as percentages

Beyond A-Level

Search for CNN feature visualisation (e.g. "what did layer 1 learn?"). You'll see edge detectors that look remarkably like the Sobel filter you coded.


Module 5 — Train a Tiny Classifier

Manager email

From: Andy Ipping
Subject: Smallest useful CNN — not production, but same logic

The PDLL teaching pipeline uses deep semantic segmentation on 512 × 512 patches in the published project. Your exercise uses smaller synthetic 32 × 32 tiles — three teaching classes standing in for grassland, moorland and woodland — so you feel the training loop before EuroSAT in 201 or the public pdnpa/cnn-land-cover patches.

Train a tiny CNN with TensorFlow. Report accuracy on a held-out test set. Then classify a new "mystery" mosaic and save outputs/charts/module5_land_map.png.

Deliverables:

  1. notebooks/module5_tiny_cnn.ipynb
  2. outputs/charts/module5_land_map.png
  3. Short note in report/module5_results.md (150–250 words)

— Andy

Why this matters

This is the full loop in miniature — the same loop used for national land-cover products, just with toy data you can train in a few minutes on a laptop.

You will feel how much data labels matter and how accuracy numbers can look good on synthetic tiles but mean little until ground-checked.

What you'll learn

  • How to generate labelled image patches for training
  • How to build and train a minimal CNN in Keras
  • How to read a confusion matrix in plain language
  • How predictions on a grid become a simple land-cover map

Step 1 — Create the notebook

notebooks/module5_tiny_cnn.ipynb

If test accuracy stays below 85% after 12 epochs, try epochs=20.


Section 1 — Generate labelled patches

Question

Can we create many small examples of each land type?

Code

import numpy as np
from PIL import Image
import matplotlib.pyplot as plt

CLASSES = ["grassland", "moorland", "woodland"]
rng = np.random.default_rng(7)

def random_tile(class_id):
    """Return a 32x32 RGB patch for one land class."""
    patch = np.zeros((32, 32, 3), dtype=np.uint8)
    if class_id == 0:  # grassland
        patch[:, :, 1] = rng.integers(120, 160, size=(32, 32))
        patch[:, :, 0] = rng.integers(40, 70, size=(32, 32))
        patch[:, :, 2] = rng.integers(30, 60, size=(32, 32))
    elif class_id == 1:  # moorland
        patch[:, :, 0] = rng.integers(90, 130, size=(32, 32))
        patch[:, :, 1] = rng.integers(60, 100, size=(32, 32))
        patch[:, :, 2] = rng.integers(50, 80, size=(32, 32))
        for _ in range(8):
            cx, cy = rng.integers(0, 32, size=2)
            r = rng.integers(2, 6)
            y, x = np.ogrid[-cy : 32 - cy, -cx : 32 - cx]
            mask = x * x + y * y <= r * r
            patch[mask] = [130, 90, 75]
    else:  # woodland
        patch[:, :, :] = np.array([30, 55, 30])
        for _ in range(6):
            cx, cy = rng.integers(0, 32, size=2)
            r = rng.integers(3, 10)
            y, x = np.ogrid[-cy : 32 - cy, -cx : 32 - cx]
            mask = x * x + y * y <= r * r
            patch[mask] = [20, rng.integers(50, 90), 25]
    return patch

n_per_class = 300
X_list, y_list = [], []
for class_id in range(3):
    for _ in range(n_per_class):
        X_list.append(random_tile(class_id))
        y_list.append(class_id)

X = np.stack(X_list).astype("float32") / 255.0
y = np.array(y_list)

fig, axes = plt.subplots(1, 3, figsize=(8, 3))
for i, name in enumerate(CLASSES):
    axes[i].imshow(X[y == i][0])
    axes[i].set_title(name)
    axes[i].axis("off")
plt.tight_layout()
plt.show()

Look with your eyes first

The cell above shows one example of each class. Before training, describe each in plain English (colour, texture, blobs). Could you tell them apart? If yes, the CNN has something to learn. If no, the labels are too ambiguous.

What the code does

We create 900 patches (300 per class) with random noise so the model cannot memorise one perfect image. Values are scaled to 0–1 for neural networks.


Section 2 — Train / test split

Code

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=0
)
X_train.shape, X_test.shape

Install scikit-learn if needed: pip install scikit-learn


Section 3 — Build a tiny CNN

Question

Can three layers learn our synthetic patterns?

Code

import tensorflow as tf
from tensorflow import keras

model = keras.Sequential([
    keras.layers.Conv2D(16, 3, activation="relu", input_shape=(32, 32, 3)),
    keras.layers.MaxPooling2D(2),
    keras.layers.Conv2D(32, 3, activation="relu"),
    keras.layers.MaxPooling2D(2),
    keras.layers.Flatten(),
    keras.layers.Dense(64, activation="relu"),
    keras.layers.Dense(3, activation="softmax"),
])

model.compile(
    optimizer="adam",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"],
)
model.summary()

What the code does

This stack is the same story as Modules 3–4, packaged for Keras:

  • Conv2D = sliding filters you coded by hand — here the computer learns the matrix values.
  • MaxPooling = "any strong pattern in this neighbourhood?"
  • Dense + softmax = final vote: e.g. 62% grassland, 28% moorland, 10% woodland.

This is a real CNN — just very small.


Section 4 — Train

Code

history = model.fit(
    X_train, y_train,
    validation_split=0.15,
    epochs=12,
    batch_size=32,
    verbose=1,
)

test_loss, test_acc = model.evaluate(X_test, y_test, verbose=0)
print(f"Test accuracy: {test_acc:.1%}")

Training should reach high accuracy on this synthetic task (>90%). If not, run a few more epochs — the data are separable.


Section 5 — Confusion matrix

Look with your eyes first

Before opening the matrix: which two classes looked most similar in Section 1? Guess which pair the model will confuse. Then run the code and see if you were right.

Code

from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

y_pred = model.predict(X_test, verbose=0).argmax(axis=1)
cm = confusion_matrix(y_test, y_pred)

disp = ConfusionMatrixDisplay(cm, display_labels=CLASSES)
disp.plot(cmap="Blues", xticks_rotation=45)
plt.title("Where does the tiny CNN get confused?")
plt.tight_layout()
plt.show()

Read the matrix: rows are true class, columns are predicted. Off-diagonal cells are mistakes — which pair confuses the model?


Section 6 — Map a mosaic

Build a larger synthetic strip and classify each 32×32 window.

Look with your eyes first

Mark which third should be grass, moor and woodland before the model runs.

Code — build and view the mosaic

rng = np.random.default_rng(99)
H, W = 96, 192
mosaic = np.zeros((H, W, 3), dtype=np.uint8)

def fill_band(mosaic, row_start, row_end, class_id):
    """Stamp 32×32 tiles across each row (works for any width divisible by 32)."""
    for row in range(row_start, row_end):
        tile = random_tile(class_id)
        for c0 in range(0, W, 32):
            mosaic[row, c0 : c0 + 32] = tile[row % 32]

fill_band(mosaic, 0, H // 3, 0)
fill_band(mosaic, H // 3, 2 * H // 3, 1)
fill_band(mosaic, 2 * H // 3, H, 2)

plt.figure(figsize=(8, 4))
plt.imshow(mosaic)
plt.title("Input mosaic — look before you classify")
plt.axis("off")
plt.show()

Code — classify each window and compare

pred_map = np.zeros((H // 32, W // 32), dtype=int)
for i in range(H // 32):
    for j in range(W // 32):
        patch = mosaic[i * 32 : (i + 1) * 32, j * 32 : (j + 1) * 32]
        x = patch.astype("float32") / 255.0
        x = x.reshape(1, 32, 32, 3)
        pred_map[i, j] = model.predict(x, verbose=0).argmax()

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].imshow(mosaic)
axes[0].set_title("Input mosaic")
axes[0].axis("off")
axes[1].imshow(pred_map, cmap="viridis", vmin=0, vmax=2)
axes[1].set_title("CNN prediction map")
axes[1].set_xticks([])
axes[1].set_yticks([])
plt.tight_layout()
plt.savefig("../outputs/charts/module5_land_map.png", dpi=150)
plt.show()

Does the right-hand map match the three bands you saw on the left?

Expected result: three horizontal colour bands on the left; three matching stripes in the prediction map. See the sample prediction map.

What the code does

Each window is classified independently — a naive but clear version of how operational systems tile huge aerial surveys. Professional pipelines add overlap and smoothing so windows do not disagree at boundaries.


Your written task

report/module5_results.md should include:

  1. Your test accuracy (one sentence).
  2. One confusion you noticed — e.g. "moorland sometimes classified as woodland."
  3. One limitation of applying this toy model to real Peak District imagery.

Reflection questions

  • Did training feel like "statistics with pictures"?
  • Would you trust this map for a legal subsidy form without ground checks?
  • Which did you enjoy more — building the model or explaining results?

Manager feedback

From: Andy Ipping

Well done. You trained a CNN that beats guessing by a mile on synthetic tiles. Real work adds: georeferencing (each pixel has a map coordinate), class imbalance, and rangers who argue with your labels.

The skill you practiced — turning photos into decisions — is the same skill whether the client is a national park or a crop-insurance firm.

— Andy

AQA Mathematics links

  • Probability — softmax outputs
  • Optimisation — training minimises loss (gradient descent at A-Level Further)

Beyond A-Level

Look up U-Net and semantic segmentation — the step beyond patch classification where every pixel gets a label, not just the centre of a window.


Module 6 — What You Have Learned

By the end of CNN 101, you will understand:

  1. why organisations map 1,439 km² of national park from Bluesky APGB photography;
  2. how orthophotos are stored as RGB number grids;
  3. how convolution filters relate to CNN layers;
  4. how two-pass (high-level then subclass) segmentation works in production;
  5. how a simplified training loop relates to the full published pipeline.

If you enjoyed writing the pipeline in code, continue to Computer Vision Developer 201.

If you prefer interpreting maps and briefing rangers, see Computer Vision Analyst 101 instead.

Final reflection

Answer in report/module6_reflection.md (200–300 words):

  • Would you enjoy this as a career — coding, maps, and client context together?
  • What felt most surprising about CNNs once you saw pixels and filters first?
  • What would you want to learn next — satellite data, bigger models, or the conservation policy side?

Manager feedback

From: Andy Ipping

You are not ready to retrain the production two-pass model. But you now know the story behind every map layer we publish: Bluesky pixels → staged CNNs → OS merge → monitoring use.

Computer Vision Developer 201 uses public EuroSAT tiles to practice evaluation and transfer learning. Compare your results to the public github.com/pdnpa/cnn-land-cover dataset when you are ready.

Thank you for your work this week. — Andy


Next steps: Computer Vision Developer 201 for real EuroSAT imagery and transfer learning. For a broader ML path, see Machine Learning 101.