Overview
Nairobi National Park was gazetted in 1946 as Kenya's first national park, and it sits at the edge of a city of millions. Karura Forest, a few kilometres from the city centre, is where Wangari Maathai's 1998–99 campaign against forest allocation became a landmark of land-rights activism. Both are protected, and both are surrounded by growth.
The pressure is not unique to Nairobi. A global study found that 32.8% of protected land is under intense human pressure (Jones et al., Science, 2018). In Kenya, aerial surveys recorded an average 68% decline in wildlife between 1977 and 2016 (Ogutu et al., PLOS ONE, 2016). Protecting land needs monitoring it, and rangers cannot watch every boundary on foot.
Satellites already photograph all of it. The hard part is turning two photographs into one answer.
The goal: for a chosen protected area, measure how much land inside its boundary, and in a 100 m margin around it, became built-up between 2016 and 2025.
I built the whole pipeline: downloading imagery, segmenting it with a U-Net, comparing two years and reporting the area in square kilometres on an interactive map. Then I tested it on real Kenyan imagery and found that it does not yet work well enough to trust. This page covers both halves.
A model can be right about India and wrong about Nairobi.
The pipeline
The system has five stages. Each is a separate module in the app, so one can be replaced without touching the others.
| Stage | What happens | Built with |
|---|---|---|
| 1. Download | Fetch a true-colour Sentinel-2 composite for each year, at the native 10 m resolution | sentinelhub, Copernicus Data Space |
| 2. Segment | Classify every pixel into one of 7 land-cover classes, for both years | TensorFlow / Keras U-Net |
| 3. Compare | Find pixels that were not built-up in 2016 and are built-up in 2025 | NumPy, rasterio |
| 4. Measure | Intersect the change with protected areas plus a 100 m buffer; sum the area | geopandas, shapely |
| 5. Present | Show both years, the change layer and the headline figure on one map | Streamlit, folium |
Narrowing the question
The first version let a user pick any county and any dates. It was flexible and hard to trust: different dates and areas meant different image sizes, and larger areas had to be downloaded at a coarser resolution than the model was trained on.
So I narrowed it. The tool now answers one question, 2016 → 2025, with a full-year, least-cloud composite at each end. It covers two protected areas, each small enough to download in a single request at 10 m: Nairobi National Park (126 km²) and Karura Forest Reserve (10 km²). Larger areas such as the Mau complex and the Maasai Mara need tiled downloads, which are left for later. A smaller promise that holds is worth more than a bigger one that does not.
The data
| Layer | Source | Used for |
|---|---|---|
| Training chips | Sen-2 LULC (Sawant et al., Mendeley Data): 64×64 px RGB chips with 7-class masks | Training the model: 149,600 train, 32,079 validation, 32,079 test pairs |
| Satellite imagery | Copernicus Sentinel-2 L2A, via the Copernicus Data Space Ecosystem | Full-year least-cloud composites for 2016 and 2025 |
| Protected areas | UNEP-WCMC and IUCN, World Database on Protected Areas (WDPA) polygons | Deciding where a change counts as encroachment |
| Reference land cover | UNDP GeoHub 10 m annual land use / land cover, 9 classes, four UTM tiles | Independent check on the model in Kenya (see below) |
| Waterways | HOTOSM Kenya waterways (HDX) | Riparian buffers (gathered, not yet used) |
The training data is not from Kenya
No labelled chip dataset for Kenya with pixel-level masks was available, so the model was trained on the Sen-2 LULC chips, which cover India. That kept the training clean: no Kenyan chip was seen in training. It also set up the project's central question. A model that learned Indian fields and towns is being asked to read Kenyan ones.
The 100 m buffer
A building rarely appears exactly on a boundary line. I drew a 100 m buffer around each protected area, in a metric projection (UTM zone 37S) so the distance is in real metres. The app then reports land lost inside the boundary and in the buffer separately.

The model
The segmentation model is a U-Net: an encoder that compresses the image into features, a decoder that rebuilds a full-resolution map, and skip connections that carry fine detail across. Skip connections matter here because edges, such as where a road ends and a field begins, are what the model has to get right.
I scaled the standard design down to fit 64×64 px chips. The first draft was a 5-level, 1024-filter network sized for 256×256 images, far too large for chips this small.
32 → 256 filters
Four encoder levels, three decoder levels with skip connections, about 1.9 million parameters
7 classes
Water, dense forest, sparse forest, barren land, built up, agriculture, fallow land
149,600 chips
The full Sen-2 LULC training split: 64×64 px RGB, 7 classes
0.49
On 32,079 held-out chips. A first CPU baseline on 1,200 chips scored 0.24
Class imbalance
The classes are badly unbalanced. Agriculture and built-up dominate the pixel counts, and water and dense forest are rare. Plain cross-entropy would learn to favour the big classes, so the loss combines two corrections: inverse-frequency class weights estimated from 5,000 training masks, and a Dice loss term that optimises overlap directly.
| Class | Loss weight |
|---|---|
| Water | 2.82 |
| Dense forest | 2.48 |
| Fallow land | 0.71 |
| Sparse forest | 0.53 |
| Barren land | 0.28 |
| Built up | 0.11 |
| Agriculture | 0.06 |
Why mean IoU, not accuracy
Pixel accuracy flatters an unbalanced problem. A model that predicts "agriculture" everywhere scores 49% accuracy on the test split, but only 0.07 mean Intersection over Union (IoU). Mean IoU averages the overlap score across all classes, so a class the model ignores drags the number down. It was the training metric and the early-stopping signal.
From a CPU baseline to the full dataset
The first model trained on 1,200 chips on a CPU, because TensorFlow on native Windows cannot use the GPU. It reached 0.24. The second run used all 149,600 training chips on a GPU, and doubled that.
Getting the GPU busy took one fix. Decoding each TIFF mask inside the training loop left the GPU idle for 60–100 ms per batch while the actual computation took 5–15 ms. Converting every mask to a NumPy array once, up front, made reads about 100× faster and removed the bottleneck. Training ran for 38 epochs, with early stopping restoring the best weights from epoch 34.

Results on the held-out test set
The test mean IoU is 0.493, with a pixel accuracy of 0.80. The built-up class, which the whole alert depends on, reaches 0.623.


On the data it was trained for, the model works. The next question is whether that carries over to Kenya.
From chips to a whole scene
The model reads 64×64 px chips. A Nairobi National Park scene is about 2,260 × 1,480 px, so it cannot be fed in whole. The app tiles it with a sliding window:
- Pad the image so its size divides evenly by the stride.
- Cut overlapping 64×64 chips with a stride of 48 px, so neighbouring chips share a 16 px margin.
- Run the model on batches of 32 chips.
- Add each chip's class probabilities into an array the size of the full scene.
- Average where chips overlap, then take the most likely class at every pixel.
The overlap is there to hide seams. Averaging with neighbours smooths the joins, though it does not remove them entirely, as the evaluation below shows. The output is a single-band GeoTIFF where each pixel holds a class index from 0 to 6. On a CPU, a full park takes about 30 seconds to segment.
Detecting change
The change rule is deliberately simple: a pixel counts as new built-up if it is built-up in 2025 and was not built-up in 2016.
The two masks are first aligned: the second is warped onto the first's exact pixel grid with nearest-neighbour sampling, because class labels are categories and must not be blended. The changed pixels become polygons, which are intersected with each protected area grown by its 100 m buffer. The app splits the result into land lost inside the boundary and in the buffer, and reports what the changed pixels used to be.
The app
The interface is a Streamlit app with one job: let someone run the analysis without writing code.
- Choose a protected area.
- Press Run analysis. There is no date picker, by design.
A strip of four metrics sits above an interactive map: protected land lost, how much of it is inside the boundary, how much is in the buffer, and the total new built-up land in the scene. The map has toggleable layers for both years' imagery and segmentation, the change in red and the protected-area outline. Expandable tables show what the changed pixels used to be.
Two decisions shaped how it feels to use:
- Everything is cached. Downloads and masks are stored under a hash of the area, dates and layer, so a repeat run is instant.
- A credential-free demo. Pre-downloaded imagery is copied into the cache on launch, so both protected areas work without a Copernicus account. Any other imagery needs free Copernicus credentials, supplied as environment variables or Streamlit secrets.
What it reports
Run on the two protected areas, the app reports the following.
2.03 km²
Protected land lost, 2016 → 2025: 1.92 km² inside the boundary, 0.11 km² in the 100 m buffer
0.19 km²
Protected land lost, 2016 → 2025: 0.13 km² inside the boundary, 0.06 km² in the 100 m buffer
0.62
How well the model finds built-up land on data like its training set
0.06
The same model against an independent map of the two parks. The figures above are estimates, not findings.
Does it work in Kenya?
The test score of 0.49 describes how well the model reproduces the data it was trained on. It says nothing about the question that matters: how well does it read real Kenyan imagery?
There is no hand-labelled Kenyan ground truth. So I used an independent map as a stand-in: UNDP's 10 m annual land-cover product for 2025. It comes from the same satellite at the same resolution, but from a completely different model. Agreement with it is a fair external check, though not a perfect one, because the two products use different class definitions.
| UNDP class | Mapped to model class | Why |
|---|---|---|
| Water | Water | Direct match |
| Trees | Dense forest | Closest forest class |
| Flooded vegetation | Water | Rare; a wet surface reads closest to water |
| Crops | Agriculture | Direct match |
| Built area | Built up | Direct 1:1 match: the class the tool depends on |
| Bare ground | Barren land | Direct match |
| Rangeland | Fallow land | No grassland class exists; fallow is nearest |
| Snow, ice, clouds | Ignored | Not a land-cover target |
Because two rows are imperfect matches, the mean IoU is held down by definitions as well as by model error. So the honest headline is built-up, which maps one-to-one.
| Class | India test split | Nairobi National Park | Karura Forest Reserve |
|---|---|---|---|
| Water | 0.243 | 0.001 | 0.012 |
| Dense forest | 0.337 | 0.115 | 0.582 |
| Sparse forest | 0.418 | 0.000 | 0.000 |
| Barren land | 0.485 | 0.000 | 0.000 |
| Built up | 0.623 | 0.060 | 0.060 |
| Agriculture | 0.846 | 0.001 | 0.000 |
| Fallow land | 0.499 | 0.388 | 0.038 |
| Mean (classes present) | 0.493 | 0.081 | 0.099 |
Built-up IoU falls from 0.62 to 0.06. That is the domain-shift penalty, in the one number every encroachment figure rests on.
The maps show where. Land-cover colours follow this legend.

Karura Forest Reserve


The model finds the forest, with a dense-forest IoU of 0.58. What it misses is the city around it: the reference shows built-up land on every side, and the model calls much of it something else.

Nairobi National Park



A faint grid of 64 px tiles is visible in the prediction: the chip-by-chip approach leaves marks even with overlap.
What this means for the numbers
Two things follow.
- The square-kilometre figures are estimates with large, unmeasured error. In both parks the blue misses outweigh the red false alarms, so the model mostly under-calls built-up land. But change is the difference of two noisy maps, and differencing amplifies noise, so errors can run in either direction.
- This is the strongest argument for fine-tuning on Kenyan imagery. Closing the built-up gap is worth more than any other change to the project.
What I would do next
- Fine-tune on Kenyan labels. The UNDP tiles are already reprojected, and one function generates a chip-and-label pair for any downloaded image. The 9 UNDP classes need mapping to the model's 7, as above.
- Check the model's input. The downloaded composites are saved as raw reflectance scaled to 0–255 with no brightness gain, and the 2025 images in the evaluation look nearly black. Comparing their brightness with the training chips is a cheap test, and may explain part of the gap.
- Mask clouds using Sentinel-2's own scene classification band, so cloud is never read as bare ground or built-up land.
- Add the near-infrared band. Vegetation and built-up land separate far better with it, but the model has to be retrained on 4-band input.
- Report confidence. Compare class probabilities instead of hard labels, flag a pixel only when the model is confident in both years, and show a score with every alert.
- Validate against something real, such as high-resolution imagery or field reports, before treating any figure as a finding.
- Add the time dimension. Feed the U-Net's features into an LSTM or GRU to read several dates instead of two, and add tiled downloads so larger areas such as the Mau complex and the Maasai Mara fit.
Limitations
- Domain shift is the dominant risk. Trained on Indian RGB chips, the model's built-up IoU drops from 0.62 to about 0.06 in Kenya.
- No true ground truth. The Kenyan check uses another model's map, which has errors of its own and different class definitions.
- Differencing amplifies noise. Two independent segmentations compared pixel by pixel turn small errors into false change.
- Cloud cover. There is no cloud mask yet, so cloud can be read as built-up or bare land.
- A narrow definition of change. The rule only catches land turning built-up. It misses clearing for farming, logging and degradation that never becomes a building.
- Small scope. Two protected areas, one pair of years, RGB bands only.
References and acknowledgements
Data
- Sawant, S., Garg, R. D., Meshram, V., Mistry, S. (2023). Sen-2 LULC. Mendeley Data, V3. doi:10.17632/f4ky6ks248.3 (opens in a new tab)
- UNDP GeoHub. 10 m Annual Land Use Land Cover (9-class).
- UNEP-WCMC and IUCN. World Database on Protected Areas (WDPA).
- HOTOSM. Kenya waterways, via the Humanitarian Data Exchange.
- Contains modified Copernicus Sentinel data (2016, 2025), accessed through the Copernicus Data Space Ecosystem.
Context
- Jones, K. R., Venter, O., Fuller, R. A., Allan, J. R., Maxwell, S. L., Negret, P. J., Watson, J. E. M. (2018). One-third of global protected land is under intense human pressure. Science, 360(6390), 788–791. doi:10.1126/science.aap9565 (opens in a new tab)
- Ogutu, J. O., Piepho, H.-P., Said, M. Y., et al. (2016). Extreme wildlife declines and concurrent increase in livestock numbers in Kenya: what are the causes? PLOS ONE, 11(9). doi:10.1371/journal.pone.0163249 (opens in a new tab)
Method
- Ronneberger, O., Fischer, P., Brox, T. (2015). U-Net: Convolutional Networks for Biomedical Image Segmentation. MICCAI 2015.
Libraries
TensorFlow / Keras · rasterio · geopandas · shapely · sentinelhub · NumPy · Pillow · folium · Streamlit
Source code
github.com/AzadiGathui/environmental-degradation-monitor (opens in a new tab)
Acknowledgements
This project was completed as a capstone at Zindua School (opens in a new tab), Nairobi.