EGS 2403 · Jomo Kenyatta University of Agriculture and Technology
A course reader for fourth-year Geomatic Engineering and Geospatial Information Systems.
You have already done a principles unit: electromagnetic radiation, sensors, orbits, resolution. This unit is about getting a defensible answer out of an archive — choosing the sensor, masking the cloud, classifying honestly, validating with a probability sample, and knowing what the resulting map can and cannot support.
Each chapter covers one week with the same parts: learning outcomes, the reading, a two-tier practical, self-test questions, and further reading. The Core task is what everyone completes; the Extension is for those who finish early.
If you have taken Web Mapping, you have a head start — it is the same language. If you have not, the subset Earth Engine needs is small: variables, functions, objects, and one unfamiliar idea called method chaining. Appendix A is a self-check that will tell you privately where you stand.
What trips up everyone, regardless of background, is not the syntax. It is that your code does not run on your computer — it builds a description of a computation which Google's servers execute. Week 2 is entirely about that distinction, and until it clicks, nothing else will.
One habit to form now. Every map you produce in this unit must be able to answer: how accurate is it, and how do you know? A classified image without a validation sample is a coloured picture, not a map. Week 6 is the centre of this unit, and everything before it is preparation.
earthengine.google.com with a Google account, selecting non-commercial or academic use. Registration approval can take a few days — do this in week 1, not the night before practical 2.code.earthengine.google.com. Nothing to install. It runs in the browser and does the computation on Google's infrastructure, which is why this unit works on a modest laptop.Why Earth Engine, given the bandwidth here. A single Sentinel-2 tile is roughly 600 MB. A ten-year time series for one county, downloaded and processed locally, is hundreds of gigabytes of transfer and days of processing on a lab machine.
Earth Engine inverts this: the archive stays where it is, your code goes to it, and what comes back is a small answer — a chart, a statistic, a modest export. The transfer cost is measured in kilobytes. This is not a convenience; it is what makes continental-scale analysis possible from a university lab in Juja.
One caveat belongs here rather than at the end. Earth Engine is a product of a single company, offered under terms that company can change — and has changed before. Appendix D states that risk plainly, shows the same analysis written in the open Python stack, and sets the exercise that makes you independent of the platform you are about to learn. Read it in week 2, not week 11.
Have a fallback ready. Earth Engine needs a working connection, and the lab's is not guaranteed on any given afternoon. Keep pre-exported GeoTIFFs of the key datasets on local disk so that a practical can proceed in QGIS when the network does not cooperate. The unit's shared folder holds these.
Week one
You know the definitions. What matters now is that you cannot have all four at once, because they trade against each other through the physics of the instrument. A detector element collects a fixed amount of energy; make the ground footprint smaller and less energy arrives, so to keep the signal-to-noise ratio usable you must widen the spectral bands or lengthen the dwell time. Every satellite is one settled compromise, and choosing among them is the first professional decision in any project.
| If the application needs… | You are buying it with… |
|---|---|
| Fine spatial detail — individual buildings, field boundaries | Narrower swath, so less frequent revisit; and usually cost |
| Frequent revisit — crop phenology, flood response | Coarse pixels, because a wide swath means a large footprint |
| Many narrow spectral bands — mineralogy, water quality | Coarser pixels and a lower signal-to-noise ratio |
| Fine radiometric sensitivity — subtle water or thermal differences | Wider bands or larger pixels |
State the requirement before you choose the sensor. "Map crop types across Trans Nzoia" needs enough revisit to catch the phenological sequence and enough spatial detail to resolve fields — Sentinel-2 at 10 m every five days. "Map informal settlement structures" needs metres, not tens of metres, and revisit hardly matters — commercial very-high-resolution imagery. "Track a flood over three days under cloud" needs radar, and nothing else will do.
Read that figure as a set of practical rules. Vegetation is separated from soil in B8 far more strongly than in any visible band. Water is separated from everything in B8 and B11. Two vegetation types that look identical in the visible may differ in the SWIR because of moisture content. When a classification confuses two classes, the first question is which band would separate them? — and the answer usually comes from this figure.
It also explains the standard false-colour composite. Put B8 in the red channel, B4 in green, B3 in blue, and healthy vegetation — with its high NIR — renders bright red. This is the display convention you will use all semester, and once you have read the figure it is not arbitrary at all.
| Platform | Spatial | Revisit | What it is for |
|---|---|---|---|
| Landsat 8/9 OLI/TIRS | 30 m · 15 m pan 100 m thermal | 8 days (two satellites) | The long record — continuous since 1972. Any question about change over decades starts here. Also the accessible thermal band. |
| Sentinel-2 A/B/C MSI | 10 m · 20 m · 60 m | 5 days | The workhorse. Best free combination of resolution and revisit. Red-edge bands are genuinely useful for crops. |
| Sentinel-1 SAR | 10 m (IW GRD) | 6–12 days | Cloud-penetrating radar. Floods, crop structure, deformation. Week 8. |
| MODIS / VIIRS | 250–1000 m | daily | Regional and continental monitoring where daily coverage beats detail. Drought, fire, phenology. |
| Sentinel-3 OLCI/SLSTR | 300 m · 500–1000 m | 1–2 days | Ocean and land colour, sea and land surface temperature. |
| Sentinel-5P TROPOMI | ~5.5 × 3.5 km | daily | Atmospheric composition — NO₂, CO, SO₂, aerosol. Week 9. |
| PlanetScope | ~3 m | daily | Commercial. Free education tier available and worth applying for. |
Imagery is distributed at defined levels, and knowing which you have determines what you must do before analysis.
Level-2 products have already had substantial processing applied by someone who was not thinking about your question. The cloud mask was tuned to a global average and is systematically weaker over bright soils — which is most of northern Kenya. The atmospheric correction assumes an aerosol model that may not hold over the Rift during a dust event.
Use the products; they are excellent and you could not do better yourself. But know what has been done, and check the result over your own area rather than trusting it. Week 3 is about exactly this.
Until recently, remote sensing meant downloading scenes and processing them locally. That model imposes hard limits: a single Sentinel-2 tile is around 600 MB, so a decade of imagery for one county is hundreds of gigabytes before you have computed anything. On a university connection this is not a delay; it is a wall.
Earth Engine, and platforms like it, invert the arrangement. The archive stays in place, your algorithm is sent to it, and only the result returns. The consequences are worth stating plainly because they reshape what questions are askable:
Reading. Jensen, Remote Sensing of the Environment, ch. 1–2 for the applications framing. Lillesand, Kiefer & Chipman, ch. 1 and 5. Gorelick et al. (2017), "Google Earth Engine: Planetary-scale geospatial analysis for everyone", RSE 202 — the platform's own statement of what it is for.
Week two
map() and the reducers instead of loopsYour script does not process imagery. It builds a description of a computation, sends that description to Google's servers, and receives a result. The objects in your code — ee.Image, ee.Number, ee.List — are handles to things that live on the server. They are not JavaScript values, and treating them as though they were is the source of nearly every problem students have in this unit.
// WRONG — collection.size() is an ee.Number, not a JavaScript number.
// The comparison is meaningless and this loop never runs correctly.
var total = 0;
for (var i = 0; i < collection.size(); i++) {
total += collection.get(i);
}
// RIGHT — describe the whole operation and let the server do it.
var total = collection.size();
print(total); // print() is one of the few calls that fetches to the client
// WRONG — a JavaScript conditional on a server-side value
if (image.get('CLOUD_COVER') < 10) { ... }
// RIGHT — express the condition server-side, as a filter
var clear = collection.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 10));
The rule. If a value came from an ee. object, it is on the server. You cannot compare it with <, you cannot loop over it with for, and you cannot print it with anything but print().
Anything you want done to many images is done with .map(); anything you want reduced across images or pixels is done with a reducer. Once you stop trying to write loops, Earth Engine becomes straightforward — and the code becomes shorter than the local-processing equivalent by an order of magnitude.
Almost every Earth Engine script has the same three-stage skeleton, and once you can write it from memory you can write most of this unit.
JavaScript · Earth Enginevar aoi = ee.FeatureCollection('FAO/GAUL/2015/level1')
.filter(ee.Filter.eq('ADM1_NAME', 'Kiambu'));
// ---- 1. FILTER: narrow the archive to what you need -------------------
var s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(aoi)
.filterDate('2025-01-01', '2025-12-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40));
print('scenes found:', s2.size());
// ---- 2. MASK: remove cloud, shadow and cirrus, pixel by pixel ---------
function maskS2(image) {
var scl = image.select('SCL'); // scene classification layer
var clear = scl.neq(3) // 3 cloud shadow
.and(scl.neq(8)) // 8 cloud, medium probability
.and(scl.neq(9)) // 9 cloud, high probability
.and(scl.neq(10)) // 10 thin cirrus
.and(scl.neq(11)); // 11 snow / ice
return image.updateMask(clear)
.divide(10000) // SCALE FACTOR — see the warning below
.copyProperties(image, ['system:time_start']);
}
var masked = s2.map(maskS2); // map(), never a for loop
// ---- 3. COMPOSITE: reduce many images to one --------------------------
var composite = masked.median().clip(aoi);
Map.centerObject(aoi, 10);
Map.addLayer(composite, {bands: ['B8', 'B4', 'B3'], min: 0, max: 0.4},
'false colour — vegetation is red');
Three notes on that script. median() is the standard compositing reducer because it is robust to the cloud that survived masking — a residual bright cloud is an outlier and the median ignores it, where mean() would not. copyProperties preserves the timestamp, without which any later time-series operation silently fails. And clip() is for display: it does not make the computation faster, and clipping early in a long chain can actually slow things down.
Satellite products store reflectance as integers to save space, with a documented scale and offset. Forget them and everything downstream is wrong by a constant factor, with no error message.
Sentinel-2 L2A: reflectance = DN / 10000.
Landsat Collection 2 Level-2 surface reflectance: reflectance = DN × 0.0000275 − 0.2. Note the offset — this one is not a simple division, and applying only the multiplier gives values that look plausible and are wrong.
Landsat Collection 2 surface temperature: kelvin = DN × 0.00341802 + 149.0.
The diagnostic is simple: surface reflectance must lie between 0 and 1. If your NDVI comes out as 3000, or your reflectance histogram peaks at 2400, you have skipped a scale factor. Check this on every new dataset, every time.
A reducer collapses many values into fewer. The same objects work in three contexts, which is what makes them worth learning properly:
collection.reduce(ee.Reducer.median()), or the shorthand collection.median(). Many images become one.image.reduceRegion({reducer: ee.Reducer.mean(), geometry: aoi, scale: 10}). Many pixels become one statistic. Always pass scale explicitly, or Earth Engine picks one for you and it is rarely what you intended.image.reduceRegions() with a FeatureCollection. One statistic per polygon: mean NDVI per ward, in one call.Nothing leaves Earth Engine until you export, and exports run as background tasks you must start from the Tasks tab.
Export.image.toDrive — a GeoTIFF to Google Drive. Specify scale, region, crs and maxPixels. This is the route for QGIS work.Export.image.toAsset — keeps the result inside Earth Engine. Use this for an intermediate you will reuse; it is far faster than recomputing a long chain each session.Export.table.toDrive — a CSV of statistics or sample points. This is what you will export most often, and it is tiny.Export the smallest thing that answers your question. A CSV of per-ward means is a few kilobytes; the raster it came from is gigabytes. Students routinely export a full-resolution image when they needed a table of ten numbers — and then wait an hour on a lab connection for it.
reduceRegion to print the mean of B8 over the composite. If it is not between 0 and 1, find your error before going further.reduceRegions and export as CSV.qualityMosaic('NDVI') and compare it with the median composite. When would each be the right choice?for (var i = 0; i < collection.size(); i++) not work?median() preferred over mean() for compositing?scale in a reduceRegion call?Reading. Cardille et al., Cloud-Based Remote Sensing with Google Earth Engine (2023, open access), parts F1–F3. The Earth Engine "Client vs Server" guide — short, and it is the concept the whole platform rests on.
Week three
Level-2 surface reflectance has had the dominant atmospheric effects removed: Rayleigh scattering, aerosol scattering and absorption, and water vapour absorption, using an atmospheric model driven by ancillary data. This is a substantial and well-validated piece of work, and you should use it.
Three things it does not handle, all of which matter in Kenya:
This matters as soon as your study area has relief — the Aberdares, Mount Kenya, the Rift escarpment, the Taita Hills. The illumination condition for a sloped pixel is
θs is solar zenith, φs solar azimuth, θn terrain slope and φn terrain aspect, all obtainable from the image metadata and a DEM. The result, cos i, is the illumination — 1 for a surface facing the sun directly, falling toward 0 as the surface turns away.
The simplest correction divides by cos i, which over-corrects steep shaded slopes badly. The C-correction adds an empirical term fitted per band from the regression of reflectance on cos i, and is the standard practical choice. SCS+C is better again for forestry, because it preserves the geotropic orientation of tree canopies rather than assuming a Lambertian surface.
Diagnose the need by displaying a false-colour composite over a mountain: if the sunlit and shaded flanks of the same forest look like different cover types, you need correction. Diagnose success by plotting reflectance against cos i before and after — a good correction flattens the slope of that relationship toward zero.
Landsat 8/9 and Sentinel-2 have similar but not identical bands: different centre wavelengths, different bandwidths, different spatial resolutions. Combining them roughly triples your usable observations, which for a cloudy tropical site is often the difference between a workable time series and no time series at all. But you cannot simply concatenate them.
A per-pixel NDVI series from real imagery is ragged: gaps where cloud was masked, dips where thin cloud or shadow survived the mask, and noise. Three responses, in increasing sophistication:
Every smoother imposes an assumption about how the underlying series behaves. If the truth contains an abrupt event — a fire, a clearance, a flood — the smoother will round it off, and you will report a gradual decline where a sudden loss occurred. Worse, an aggressive smoother across a long cloud gap will draw a confident curve through a period when you had no observations at all.
Always keep and plot the raw observations alongside the smoothed curve, and mark which points are real. If a feature exists only in the smoothed line, it does not exist.
Reading. Roy et al. (2016), "Characterization of Landsat-7 to Landsat-8 reflective wavelength and normalized difference vegetation index continuity", RSE 185, and the companion Sentinel-2 harmonisation paper. Cardille et al., part F4. Riaño et al. (2003) on topographic correction methods, IEEE TGRS 41(5).
Week four
An index is a band ratio designed to amplify one signal and suppress others. There are hundreds; six will cover almost everything you do.
| Index | Sentinel-2 form | For, and where it fails |
|---|---|---|
| NDVI | (B8−B4)/(B8+B4) | General greenness. Saturates in dense canopy — above about LAI 3 it stops responding. Also sensitive to soil background when cover is sparse. |
| EVI | 2.5(B8−B4)/ (B8+6B4−7.5B2+1) | Dense vegetation, where NDVI has saturated. The blue term corrects for residual aerosol. |
| SAVI | 1.5(B8−B4)/ (B8+B4+0.5) | Sparse cover over bright soil — rangelands, ASALs. The soil adjustment factor is what NDVI lacks. |
| NDMI | (B8−B11)/(B8+B11) | Canopy water content. Detects water stress before it shows as browning, which makes it valuable for early warning. |
| MNDWI | (B3−B11)/(B3+B11) | Open water. Outperforms NDWI in built-up areas, where concrete otherwise reads as water. |
| NBR | (B8−B12)/(B8+B12) | Burn severity, via the difference between pre- and post-fire values. |
McFeeters (1996) NDWI = (Green − NIR)/(Green + NIR) maps open water bodies.
Gao (1996) NDWI = (NIR − SWIR)/(NIR + SWIR) maps vegetation water content, and is usually called NDMI to avoid exactly this collision.
They share a name, use different bands, and answer different questions. Papers are frequently ambiguous about which they used. Always write the band formula in your methods rather than the name alone — and when reading someone else's work, check which one they meant before believing their result.
A single date tells you little. The shape of the curve through a season tells you a great deal: when growth started, how fast, how high it peaked, when it senesced, and how much total greenness accumulated. From a smoothed series you can extract:
Crop types differ in phenology even when their spectra on any single date are identical. This is why a time-series feature stack classifies crops far better than a single image, and it is the basis of practically all operational crop mapping. Week 5 uses it.
NDVI is not comparable between places — a value of 0.4 is poor for Kericho and remarkable for Marsabit. To ask "is this area unusually stressed?", you must compare a location against its own history in the same season. That is what VCI does.
where min and max are taken over the historical record for the same calendar window at the same location. VCI is a percentile-like position within local history: 0 means the worst on record, 100 the best.
For one ward, the record of mean March NDVI over the past twenty years runs from a minimum of 0.18 to a maximum of 0.52. March 2026 comes in at 0.24.
VCI = 100 × (0.24 − 0.18) / (0.52 − 0.18) = 100 × 0.06 / 0.34 = 17.6
Against the categories used in Kenya's drought early-warning bulletins:
| VCI | Vegetation condition |
|---|---|
| ≥ 50 | Normal or above |
| 35 – 50 | Moderate vegetation deficit |
| 21 – 34 | Severe vegetation deficit |
| ≤ 20 | Extreme vegetation deficit |
17.6 falls in the extreme band. Note what has happened: an NDVI of 0.24 is unremarkable in absolute terms and would mean nothing on its own. Placed against twenty years of the same ward in the same month, it is a warning that feeds into resource allocation decisions.
Two ways to get this wrong. If you build the baseline from annual maxima rather than the same month, you are comparing March against the historical peak of the growing season, and every March looks like a drought. If you build it from a short record — five years, say — a single unusual year sets the min or the max and distorts every subsequent value. The baseline is the whole method; treat its construction with more care than the arithmetic.
// NDVI over a long baseline, restricted to the same calendar window
var withNDVI = function(img) {
return img.addBands(img.normalizedDifference(['B8', 'B4']).rename('NDVI'));
};
var march = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(ward)
.filter(ee.Filter.calendarRange(3, 3, 'month')) // same window, every year
.map(maskS2).map(withNDVI).select('NDVI');
var ndviMin = march.min();
var ndviMax = march.max();
var current = march.filterDate('2026-03-01', '2026-04-01').median();
var vci = current.subtract(ndviMin)
.divide(ndviMax.subtract(ndviMin))
.multiply(100)
.rename('VCI');
Map.addLayer(vci, {min: 0, max: 100, palette: ['8B0000', 'FF8C00', 'FFFF00', 'ADFF2F', '006400']},
'VCI, March 2026');
Reading. Huete et al. (2002) on MODIS vegetation indices, RSE 83. Kogan (1995) on VCI, Advances in Space Research 15(11). Kenya's NDMA drought bulletins — read three consecutive months and see how the remote sensing feeds an operational decision.
Week five · CAT 1 this week
Before any code, decide what you are mapping. Two constraints bind, and beginners routinely violate both.
Classes must be spectrally separable. "Maize" and "sorghum" may be indistinguishable on any single date. "Smallholder farm" and "commercial farm" differ by ownership, not reflectance, and no classifier can see ownership. If two classes cannot be told apart by the sensor, merge them or find a feature that does separate them — usually a temporal one.
Classes must be exhaustive and mutually exclusive. Every pixel must belong to exactly one class. A scheme with "forest", "cropland" and "wetland" has nowhere to put a road. The classifier will assign it somewhere regardless, silently, and your accuracy assessment will not tell you which of your classes absorbed it.
Start from an established scheme — the FAO Land Cover Classification System, or Kenya's national land-cover classes — rather than inventing one. It costs nothing and makes your product comparable with everyone else's.
This is where classification projects succeed or fail, and it takes longer than everything else combined.
It is tempting to draw training polygons, classify, look at the result, and then add more polygons where it looks wrong. That is legitimate iterative refinement of the training set — but the moment you validate against points chosen the same way, your accuracy is meaningless, because you have measured how well the classifier reproduces your own opinion.
Validation data must be an independent probability sample drawn after the map exists, by a design that does not depend on the map's appearance. This is not a formality; it is the difference between a reported accuracy that means something and one that does not. Week 6 is entirely about doing it properly.
You are not limited to the spectral bands of one date. A classifier can use any co-registered layer, and adding the right ones usually helps more than changing the algorithm:
// ---- build a feature stack: two seasons, indices, terrain -------------
var dry = buildComposite('2025-01-01', '2025-03-31');
var wet = buildComposite('2025-04-01', '2025-06-30');
var dem = ee.Image('USGS/SRTMGL1_003');
var stack = dry.select(['B2','B3','B4','B8','B11','B12'])
.rename(['d_B2','d_B3','d_B4','d_B8','d_B11','d_B12'])
.addBands(wet.select(['B2','B3','B4','B8','B11','B12'])
.rename(['w_B2','w_B3','w_B4','w_B8','w_B11','w_B12']))
.addBands(dry.normalizedDifference(['B8','B4']).rename('d_NDVI'))
.addBands(wet.normalizedDifference(['B8','B4']).rename('w_NDVI'))
.addBands(dem.rename('elev'))
.addBands(ee.Terrain.slope(dem).rename('slope'));
// ---- sample the stack at the training polygons ------------------------
var training = stack.sampleRegions({
collection: trainingPolygons, // must carry a 'class' property
properties: ['class'],
scale: 10,
tileScale: 4 // raise this if you hit memory limits
});
// ---- train, and hold back nothing here — validation is a SEPARATE sample
var classifier = ee.Classifier.smileRandomForest({
numberOfTrees: 300,
minLeafPopulation: 3
}).train({
features: training,
classProperty: 'class',
inputProperties: stack.bandNames()
});
var classified = stack.classify(classifier);
// ---- which features are actually doing the work? ----------------------
print('variable importance:', classifier.explain().get('importance'));
| Classifier | Character |
|---|---|
| CART | A single decision tree. Interpretable — you can read the rules — but unstable: a small change in training data gives a different tree. Use it to understand, not to publish. |
| Random forest | Hundreds of trees on bootstrapped samples, voting. Robust, handles many correlated features, needs almost no tuning, gives variable importance. The default choice, and it is hard to beat. |
| Gradient boosting | Often marginally better than RF, at the cost of real tuning sensitivity. Worth trying once you have a working RF baseline. |
| SVM | Strong with limited training data and high-dimensional features. Slow on large areas, and kernel and cost parameters matter a great deal. |
| Minimum distance, maximum likelihood | The classical parametric methods. Still worth knowing — they appear throughout the older literature and in ENVI-based workflows you may inherit. |
Do not spend the semester comparing classifiers. On most real problems the difference between random forest and gradient boosting is a percentage point or two, while the difference between poor and good training data is ten or twenty. Spend your effort where the leverage is.
A classifier that has memorised its training data will report near-perfect accuracy on that data and fail on anything else. Three signals, available before you run a formal assessment:
Remedies: more and better-distributed training data; fewer, less correlated features; a larger minLeafPopulation; and — the one that actually settles the question — an honest validation sample, next week.
minLeafPopulation of 1 and of 10. Describe the difference in map texture.Reading. Cardille et al., part F2 on classification. Belgiu & Drăguţ (2016), "Random forest in remote sensing: A review", ISPRS Journal 114. Foody (2002) on classification accuracy, RSE 80 — read it before week 6.
Week six · the centre of this unit
Random forest reports an accuracy on its training data. It is typically above 95% and it is worthless — the model has seen those pixels. Splitting your training polygons into train and test is better but still inadequate, for a reason specific to this field: your training polygons were not a probability sample. You drew them where you were confident, which is where the classes are pure and obvious. The map's errors live at boundaries, in mixed pixels, and in the ambiguous cases you avoided digitising. A holdout from your training set contains none of them.
An accuracy figure is only meaningful if the validation points were selected by a probability sampling design — every pixel in the map having a known, non-zero chance of selection. That is the requirement, and everything in this chapter follows from it.
Simple random is valid but inefficient: rare classes get few points. If built-up land is 3% of your area, a 200-point random sample gives you about six built-up points, from which you can say almost nothing about that class.
Stratified random — strata being the map classes — is the standard choice. Allocate points to each class deliberately, ensuring even rare classes get enough (a common floor is 50 per class), and weight the results by class area afterwards. That weighting is the step that students omit, and it is what makes the arithmetic below different from a plain confusion matrix.
For sample size, Cochran's formula for stratified estimation of overall accuracy:
Wi is the area proportion of map class i, Ui your guess at that class's user's accuracy, and S(Ô) the standard error you are willing to accept on overall accuracy — 0.01 is a common target. In practice this lands most projects between 200 and 800 points.
Then interpret each sample point against the best reference data you can obtain — high-resolution imagery, field visits, or both — without looking at the map's classification of that point. If you know what the map said, you will agree with it more often, and that bias is not recoverable afterwards.
A three-class map of a 900 km² area. Mapped areas: forest 270 km² (30%), cropland 540 km² (60%), built-up 90 km² (10%). A stratified random sample of 200 points is allocated 60 / 100 / 40 and interpreted against high-resolution imagery.
The confusion matrix, in sample counts. Rows are what the map said; columns are what the reference says.
| Ref forest | Ref crop | Ref built | Total | |
|---|---|---|---|---|
| Map forest | 50 | 8 | 2 | 60 |
| Map cropland | 12 | 82 | 6 | 100 |
| Map built | 2 | 8 | 30 | 40 |
| Total | 64 | 98 | 38 | 200 |
The naive overall accuracy is (50 + 82 + 30) / 200 = 81.0%. This figure is wrong, because the sample was stratified: built-up land is 10% of the map but 20% of the sample, so it is over-represented. We must weight by area.
Step 1 — convert counts to area proportions. For each cell, p̂ij = Wi × nij / ni·, with W = 0.30, 0.60, 0.10:
| Forest | Crop | Built | Wi | |
|---|---|---|---|---|
| Map forest | 0.250 | 0.040 | 0.010 | 0.300 |
| Map cropland | 0.072 | 0.492 | 0.036 | 0.600 |
| Map built | 0.005 | 0.020 | 0.075 | 0.100 |
| Reference | 0.327 | 0.552 | 0.121 | 1.000 |
Step 2 — the accuracies. Overall accuracy is the sum of the diagonal: 0.250 + 0.492 + 0.075 = 81.7%.
| Class | User's accuracy | Producer's accuracy |
|---|---|---|
| Forest | 0.250/0.300 = 83.3% | 0.250/0.327 = 76.5% |
| Cropland | 0.492/0.600 = 82.0% | 0.492/0.552 = 89.1% |
| Built-up | 0.075/0.100 = 75.0% | 0.075/0.121 = 62.0% |
Read those two columns as answers to different questions. User's accuracy answers "the map says built-up here — how often is that right?" — 75%, the reliability a user experiences. Producer's accuracy answers "of the land that really is built-up, how much did the map find?" — only 62%. The map is missing more than a third of the built-up land. That asymmetry is invisible in the overall figure.
Step 3 — the bias-adjusted area. The reference column totals estimate the true area proportions, so:
| Class | Mapped area | Adjusted estimate | Difference |
|---|---|---|---|
| Forest | 270 km² | 294.3 km² | +9% |
| Cropland | 540 km² | 496.8 km² | −8% |
| Built-up | 90 km² | 108.9 km² | +21% |
Step 4 — the confidence interval. The standard error of an estimated area proportion under stratified sampling is
For built-up: the three strata contribute 0.30²×(2/60)(58/60)/59 = 0.0000492, 0.60²×(6/100)(94/100)/99 = 0.0002051, and 0.10²×(30/40)(10/40)/39 = 0.0000481. The sum is 0.0003023, so S = 0.0174. At 95% confidence that is ±1.96 × 0.0174 = ±0.0341 in proportion, or ±30.7 km².
The reportable result: built-up area = 109 ± 31 km² (95% CI).
Compare that with what a student would otherwise have written: "built-up area is 90 km², overall accuracy 81%." Both numbers in that sentence are wrong — the area by 21%, and the accuracy by a bias whose direction you could not have guessed. And the honest version carries an interval nearly a third as wide as the estimate, which is itself the most important thing the reader needs to know.
Kappa purports to correct overall accuracy for agreement "expected by chance". Pontius & Millones (2011) showed the correction is not useful: kappa is very highly correlated with overall accuracy, so it adds no information, and its chance-agreement baseline — that a classifier assigns classes at random in proportion to their marginals — describes no classifier anyone builds.
It persists because reviewers expect it. Report overall, producer's and user's accuracy with confidence intervals, plus quantity and allocation disagreement if you want a summary of error structure. If a reviewer insists on kappa, give it, and cite Pontius & Millones in the same sentence.
stratifiedSample.Reading. Olofsson et al. (2014), "Good practices for estimating area and assessing accuracy of land change", RSE 148 — read this one properly; it is the standard the field works to. Pontius & Millones (2011), IJRS 32(15). Stehman & Foody (2019), "Key issues in rigorous accuracy assessment", RSE 231.
Week seven
The naive approach — classify 2000, classify 2025, compare — is the most common method in the literature and the most error-prone. Before anything else, three things must be true of your two images, and if they are not, everything you find is artefact:
A 1,000 km² area. Suppose 5% of it — 50 km² — genuinely changed. You have two land-cover maps, each with 85% overall accuracy, which is a respectable figure.
A pixel is flagged as changed when the two maps disagree. For a pixel to be classified correctly on both dates: 0.85 × 0.85 = 0.7225. So roughly 28% of pixels have at least one wrong label.
Of the 950 km² that did not change, most of those disagreements show up as spurious change — on the order of 250 km² falsely flagged.
Of the 50 km² that did change, only about 72% is caught on both dates — roughly 36 km² correctly flagged.
So the change map flags around 285 km², of which about 36 km² is real. Roughly one flagged pixel in eight is genuine change.
Two caveats keep this honest. It assumes the two maps' errors are independent, and in practice they correlate — same classifier, same training data, same confusions — which reduces the spurious rate somewhat. And the arithmetic is illustrative, not a substitute for measuring your own maps. But the structural point survives every refinement: when real change is rare, classification error dominates the change signal. This is why direct methods, which compare the imagery rather than the classifications, are usually preferable — and why any change map must be validated as a change map, with its own sample, not inferred from the accuracies of its inputs.
| Method | How, and when |
|---|---|
| Image differencing | Subtract one date's index from the other and threshold. Simple, robust, and it avoids the compounding problem entirely because no classification is involved. Best when the change has a clear spectral direction — deforestation, burn, flood. |
| Image ratioing | Divide rather than subtract. Less sensitive to multiplicative differences in illumination between dates. |
| Change vector analysis | Treat the multi-band difference as a vector: its magnitude says how much changed, its direction hints at what kind of change. More informative than a single index difference. |
| Post-classification comparison | The only method that names both the from-class and the to-class, which is what a land-cover transition matrix needs. Use it when you need that, and validate the change map independently. |
| Time-series algorithms | Use every available observation rather than two. Far more robust. See below. |
Differencing gives you a continuous surface; you must decide what counts as change. Three approaches, in increasing defensibility:
Whichever you use, state it and show the histogram with the threshold marked. And recognise the choice is not purely technical: a low threshold catches more real change and more false alarms. Which error is worse depends on what happens next — a forest-loss alert that triggers a ranger patrol tolerates false alarms far better than one that triggers a prosecution.
Using two dates throws away everything in between. The modern approach fits a model to a pixel's whole history and looks for departures.
All three share one advantage: because they use the seasonal cycle as their baseline, a dry year does not read as deforestation. Bi-temporal methods have no way to make that distinction.
Your algorithm found a break. It does not know whether the cause was logging, fire, charcoal burning, drought, disease, or a cloud your mask missed. Attribution requires additional evidence — the shape and edge geometry of the patch, the spectral direction of the change, the season, the recovery trajectory afterwards, ancillary data, and often a field visit or a conversation with someone local.
Reporting "500 ha of deforestation" when you have detected 500 ha of negative NBR change is a claim your data does not support, and in a context where such claims affect land-use policy and individual liability, the distinction matters a great deal.
Reading. Kennedy et al. (2010) on LandTrendr, RSE 114. Zhu & Woodcock (2014) on CCDC, RSE 144. Verbesselt et al. (2010) on BFAST, RSE 114. Olofsson et al. (2014) again — its area-estimation guidance applies with particular force to change.
Week eight
Optical remote sensing fails under cloud, and much of Kenya is cloudy exactly when the interesting things happen. During the long rains, a whole month can pass without a usable Sentinel-2 observation over the highlands — and floods, planting, and rapid vegetation change all occur in precisely that window. Optical sensing is systematically blind to the events it is most needed for.
Synthetic Aperture Radar supplies its own illumination at microwave wavelengths that pass through cloud unimpeded. Sentinel-1 operates in C-band at 5.405 GHz — a wavelength of about 5.6 cm — and images regardless of cloud, day or night, every six to twelve days.
SAR does not measure reflected sunlight. It transmits a pulse and measures how much energy returns, reported as the backscatter coefficient σ⁰ in decibels. Three surface properties dominate:
The practical consequence: in mountainous terrain — the Aberdares, Mount Kenya, the Rift escarpments — parts of every SAR scene are unusable, and a shadow region is not water even though it is as dark as water. Terrain-corrected products handle the geometry, but they cannot invent data where none was collected. Mask layover and shadow explicitly; Earth Engine's Sentinel-1 collection is terrain-corrected but does not mask them for you.
Sentinel-1 in its standard mode transmits vertically and receives both vertically (VV) and horizontally (VH). The pair carries structural information:
SAR images have a grainy salt-and-pepper texture that is not noise in the ordinary sense. It arises from coherent interference among the many scatterers within a resolution cell, and it is deterministic — image the same scene twice under identical geometry and you get the same speckle pattern.
It still has to be reduced before most analysis. Multi-looking averages several independent looks, trading resolution for radiometric quality. Adaptive filters — Lee, refined Lee, Frost — smooth homogeneous areas while preserving edges. For a threshold-based flood map, a modest speckle filter makes the difference between a clean water mask and a shredded one. Do not over-filter: aggressive smoothing removes small water bodies along with the speckle.
JavaScript · Earth Engine// Sentinel-1 GRD in Earth Engine arrives terrain-corrected and in dB.
var s1 = ee.ImageCollection('COPERNICUS/S1_GRD')
.filterBounds(aoi)
.filter(ee.Filter.eq('instrumentMode', 'IW'))
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV'))
.filter(ee.Filter.eq('orbitProperties_pass', 'DESCENDING')) // keep geometry constant
.select('VV');
var before = s1.filterDate('2026-03-01', '2026-03-25').median();
var after = s1.filterDate('2026-04-20', '2026-04-30').median();
// speckle reduction — enough to stabilise the threshold, not enough to erase ponds
var smooth = function(img) {
return img.focalMedian(50, 'circle', 'meters');
};
// water is specular: very low backscatter. Threshold, then remove pre-existing water.
var THRESH = -16; // dB — CHECK THIS against your own histogram
var waterAfter = smooth(after ).lt(THRESH);
var waterBefore = smooth(before).lt(THRESH);
var flooded = waterAfter.and(waterBefore.not());
// exclude permanent water and steep terrain, where shadow mimics water
var jrc = ee.Image('JRC/GSW1_4/GlobalSurfaceWater').select('seasonality');
var slope = ee.Terrain.slope(ee.Image('USGS/SRTMGL1_003'));
flooded = flooded.updateMask(jrc.lt(5)).updateMask(slope.lt(5));
var areaKm2 = flooded.multiply(ee.Image.pixelArea()).divide(1e6)
.reduceRegion({reducer: ee.Reducer.sum(), geometry: aoi, scale: 20, maxPixels: 1e10});
print('flooded area, km²:', areaKm2);
The −16 dB above is a starting point, not a constant. The right threshold depends on the incidence angle, the wind — a roughened water surface backscatters far more than a calm one and can disappear from your mask entirely — the season, and your speckle filtering. Plot the histogram of your own scene: open water should appear as a distinct low mode, and the threshold belongs in the trough.
And mask the confusers explicitly, as the code above does. Radar shadow, dry smooth sand, and asphalt all produce low backscatter and will otherwise be mapped as flood. A flood map that includes the runway at Wilson Airport has not been checked.
Reading. Richards, Remote Sensing with Imaging Radar (2009), ch. 1–3. The ESA Sentinel-1 user guide. Cardille et al., part F3 on SAR in Earth Engine. Moreira et al. (2013), "A tutorial on synthetic aperture radar", IEEE GRSM 1(1) — the best single introduction available.
Week nine
A thermal sensor measures emitted radiance, from which brightness temperature follows by inverting the Planck function. Converting that to the actual surface temperature requires emissivity — how efficiently a surface radiates compared with a perfect black body. Water is near 0.99, dense vegetation about 0.98, bare soil 0.92–0.96, and concrete and metal roofing lower still and highly variable.
Get emissivity wrong and your temperature is wrong: an error of 0.01 in emissivity translates to roughly 0.5–0.7 K. Most operational approaches estimate emissivity from NDVI via a vegetation-cover fraction, which works reasonably over vegetated land and poorly over the mixed urban surfaces where you most want the answer.
Note also what LST is and is not. It is the radiometric temperature of the surface — the roof, the road, the leaf canopy — not the air temperature a person experiences. On a sunny afternoon a tarmac surface can read 20 K above the air temperature two metres above it. Reporting LST as "temperature" without qualification is a real misrepresentation, and it appears in published work.
Landsat Collection 2 Level-2 provides a surface temperature band, ST_B10, as scaled integers. The documented conversion is kelvin = DN × 0.00341802 + 149.0.
A pixel over the Nairobi CBD reads DN = 44,000.
44,000 × 0.00341802 = 150.39
150.39 + 149.0 = 299.39 K
299.39 − 273.15 = 26.2 °C
Now the two ways students get this wrong. Forget the scale entirely and you report 44,000 — obviously absurd, and therefore harmless. Subtract 273.15 from the raw DN and you report 43,727 °C, equally absurd. But apply the multiplier and forget the 149.0 offset and you report 150.4 K, or −122.8 °C — a number that is wrong by 150 degrees and yet looks like a temperature. That third error is the dangerous one, because it survives a careless glance.
Always sanity-check against physical reality. Land surface temperature in Kenya should fall roughly between 10 °C and 60 °C. Anything outside that means a scaling error, not a discovery.
Cities are hotter than their surroundings: dark impervious surfaces absorb more shortwave radiation, there is little vegetation to cool by transpiration, and the urban geometry traps longwave radiation. The measurement is straightforward and the design decisions are where the difficulty lies.
ET is the largest term in the water balance of most Kenyan catchments and the one hardest to measure directly. Satellite methods exploit the surface energy balance: net radiation is partitioned between ground heat flux, sensible heat and latent heat, and latent heat is evapotranspiration. Because a well-watered surface stays cool through transpiration while a water-stressed one heats up, surface temperature carries the signal.
The applications are concrete and Kenyan: irrigation scheduling in the Rift Valley horticulture schemes, water allocation in the Ewaso Ng'iro basin, and crop water productivity assessment. FAO's WaPOR portal publishes ET for Africa and is worth knowing.
The JRC Global Surface Water dataset is a finished product built from 40 years of Landsat, and it saves you an enormous amount of work. It provides, at 30 m: occurrence (the percentage of observations in which each pixel was water), change intensity, seasonality, recurrence, and transitions between permanent, seasonal and no water.
Kenya offers unusually good material here. The Rift Valley lakes — Baringo, Nakuru, Naivasha, Turkana — have undergone dramatic and well-documented level changes, with substantial rises since about 2010 that displaced settlements and flooded infrastructure. The occurrence layer distinguishes the historically permanent lake from the recently inundated margin immediately, and that distinction is exactly what a planning question needs.
Sentinel-5P's TROPOMI instrument measures NO₂, CO, SO₂, CH₄, O₃ and aerosol index daily at roughly 5.5 × 3.5 km. Three limitations to state whenever you use it:
Within those limits it is genuinely useful: NO₂ over Nairobi shows a clear weekday–weekend cycle that tracks traffic, and the aerosol index picks up biomass burning across the region during the dry season.
Reading. The USGS Landsat Collection 2 Level-2 Science Product guide, for the scale factors and the surface temperature algorithm. Pekel et al. (2016), "High-resolution mapping of global surface water and its long-term changes", Nature 540. Senay et al. (2013) on SSEBop, JAWRA 49(3).
Week ten · CAT 2 this week
Field data is not truth. It is another observation, with its own errors: the GPS fix, the observer's judgement, the date it was collected, the definition of the class in the observer's head. Two competent field workers will disagree on perhaps 5–15% of land-cover calls at boundaries and in mixed cover — and that disagreement rate is a ceiling on any accuracy you can demonstrate. If your interpreters agree only 90% of the time, you cannot demonstrate 95% map accuracy, because you have no instrument capable of measuring it.
The honest term is reference data, and the honest practice is to report how it was collected, by whom, when, and with what agreement between interpreters.
| Method | Accuracy | Adequate for |
|---|---|---|
| Smartphone GNSS | 3–5 m | 30 m Landsat pixels, and 10 m Sentinel-2 in homogeneous cover |
| Mapping-grade receiver | 0.5–2 m | 10 m pixels near boundaries; small features |
| RTK / PPK | ~2 cm | Very-high-resolution validation; individual trees; structures |
Match the method to the pixel. A 3 m smartphone error on a 30 m pixel is a tenth of a pixel and irrelevant; the same error on 3 m PlanetScope imagery puts you in the wrong pixel entirely. And record the accuracy the device reports for each point — under canopy or between tall buildings it degrades sharply, and knowing which points are weak lets you weight or exclude them later.
A Landsat pixel is 30 × 30 m — 900 m². A typical field plot might be 10 × 10 m, about one ninth of that. You are comparing a measurement of part of a pixel against a value integrated over all of it, and in a smallholder landscape with fields smaller than a pixel, most pixels are mixtures.
This is not a solved problem, but it is a manageable one. Sample in patches large enough to contain a full pixel with margin; record the fractional composition of a plot rather than a single label where you can; or move to a finer sensor when the landscape demands it. Above all, state the mismatch in your methods — a reader needs to know that your 30 m map was validated with 10 m plots in a landscape of half-hectare fields.
A county planner does not want a confusion matrix. They want to know whether they can act on the map. Translate:
Earth observation watches people who did not consent and mostly do not know. That is worth sitting with rather than passing over, and four questions arise directly in the projects you will build.
Mapping informal settlements can direct services to people who need them, and it can support eviction. The imagery is identical; the difference is who commissioned the map and what happens next. You are entitled to ask that question, and in professional practice you should.
At 30 m you map settlement extent. At 30 cm you map individual structures and can count them. The finer product is more useful and more dangerous, and choosing the resolution is choosing what becomes possible. Default to the coarsest resolution that answers the question.
A crop map that under-detects smallholder plots may exclude those farmers from an input subsidy. A forest-loss map with high commission error may implicate the wrong landholder. When errors fall unevenly on people with different power to contest them, the class-level accuracies in week 6 are not a technical annex — they are the fairness analysis.
Publishing your map openly is usually right: it enables verification, reuse, and accountability. But a map of water points during a resource conflict, or of a wildlife population under poaching pressure, is a targeting aid. Kenya has open data commitments and a Data Protection Act; know the framework, and know that legal compliance is a floor rather than the whole of the question.
Reading. Stehman & Foody (2019), RSE 231, on reference data quality. Kenya's Data Protection Act (2019). Kwan (2016) on algorithmic geographies and the ethics of geospatial data, Annals of the AAG 106(2).
Week eleven
Agriculture and food security. Crop type and area, condition monitoring, yield forecasting, drought early warning. In Kenya this runs through NDMA's bulletins and FEWS NET, and it is directly operational — it triggers resource allocation. The hard parts are smallholder field sizes below the pixel and persistent cloud during the growing season, which is why week 8 exists.
Forestry and carbon. Deforestation and degradation monitoring, REDD+ measurement, reporting and verification. Carbon finance requires accuracy assessments that meet published standards — which is why week 6 is the centre of this unit rather than an afterthought. A forest-change map without a probability-sample accuracy assessment and bias-adjusted areas has no financial value at all.
Urban planning. Growth monitoring, informal settlement mapping, heat islands, green space, infrastructure planning. Kenyan cities are growing faster than surveys can track, which makes EO the only practical instrument, and raises every question in week 10.
Disaster response. Flood extent, landslides, fire, drought. The International Charter "Space and Major Disasters" provides free tasking after a declared disaster; response timelines are hours to days, which shapes every methodological choice toward the robust and the fast.
Water resources. Surface water extent, lake levels, evapotranspiration, irrigation monitoring. Kenya's Rift Valley lake rises since 2010 are a well-documented case with real displacement consequences.
Most student projects produce a map. Operational use needs several things a map does not have, and knowing this is part of what distinguishes a graduate:
Ten minutes of presentation, five minutes of questions, in pairs. The reasoning behind your decisions is what is examined — not the appearance of your maps.
A report of no more than eight pages: the question, the data and why, the preprocessing, the method, the validation design and full accuracy assessment with bias-adjusted areas and confidence intervals, the limitations, and the ethics statement. Submit your Earth Engine script — it must run from a clean account. Include the AI-use declaration: which tools, which prompts, what they produced, what you changed and why.
The platforms will change. Earth Engine is roughly a decade old; the sensors you have used will be superseded; the classifiers will be replaced by things that work better and explain themselves less. What does not change is the structure of the problem: an instrument measuring reflected or emitted radiation, an atmosphere in the way, a pixel that averages a piece of a world that does not respect pixel boundaries, and a person who will act on the map.
And one habit, which is the reason week 6 sits at the centre of this reader. A classified image is not a map until it has been validated with a probability sample. The classification is the easy part, and it now takes fifteen lines of code that a language model will write for you in seconds. The validation is the part that makes the result mean something — and it is the part nobody can do for you, because it requires going and looking.
Reading. Cardille et al., parts A1–A3 on applications. Olofsson et al. (2014) once more. The GFOI Methods and Guidance Documentation for the REDD+ reporting context.
Appendix A
The Code Editor is JavaScript. If you have taken Web Mapping you already have the language; if not, the subset you need is small. Work through these before week 2, without help of any kind — no notes, no search, no AI assistant. The point is to find out what you have.
"Kiambu" and print it to the console.name, zoom and bands (an array of three strings). Print the second band.collection.filterDate(a, b).filterBounds(g).median(), and in what order the operations apply."Found 42 scenes" from a variable n.var x = 5; and var x = ee.Number(5);.if (image.get('CLOUD_COVER') < 20) { print('clear'); }for (var i = 0; i < collection.size(); i++) { ... }map(): "add an NDVI band to every image in a collection".image.reduceRegion({reducer: ee.Reducer.mean(), geometry: aoi}) and get an unexpected number. What did you omit?Tasks 1–5 are ordinary JavaScript. Tasks 6–10 are the Earth Engine concept, and getting them wrong now is completely normal — but getting them wrong in week 5 will cost you a practical.
ee.Number with <, loop over an ee.List with for, or print one with console.log. Use ee.Filter, map(), and print().print() is expensive and limited. It fetches to the client and will fail on large objects. Never call it inside a map().scale to reducers. Omit it and Earth Engine chooses one — usually the projection of the first band, which is rarely what you meant, and the number will be wrong without complaint.copyProperties preserves the timestamp. Forget it inside a mapped function and every later time-series operation fails, often silently.They are permitted, and declared. They are genuinely good at Earth Engine boilerplate — a cloud mask, a composite, an export block — and using them for that is sensible.
Know how they fail here, because the failures are specific and they all produce output that looks completely normal. Generated Earth Engine code routinely: omits or misapplies scale factors, so NDVI comes out in the thousands; reports accuracy from training data; quotes kappa as the headline statistic; compares NDVI across years with no cloud masking; uses a fixed backscatter threshold copied from a tutorial written for a different climate; and computes an area from a classified map with no bias adjustment and no confidence interval.
Every one of those is an error this unit trains you to catch, and every one produces a convincing map. The tool is an asset only if you can catch them — and the way you learn to catch them is by doing week 6 by hand, once, properly.
Appendix B
This sheet is supplied in the CATs and the final examination.
| Band | Centre | Res. | Use |
|---|---|---|---|
| B2 | 490 nm | 10 m | Blue — aerosol correction in EVI, true colour |
| B3 | 560 nm | 10 m | Green — water indices, true colour |
| B4 | 665 nm | 10 m | Red — chlorophyll absorption |
| B5–B7 | 705–783 nm | 20 m | Red edge — crop stress, species discrimination |
| B8 | 842 nm | 10 m | NIR — the vegetation workhorse |
| B8A | 865 nm | 20 m | Narrow NIR |
| B11 | 1610 nm | 20 m | SWIR — moisture, built-up, water |
| B12 | 2190 nm | 20 m | SWIR — burn severity, minerals |
| SCL | — | 20 m | Scene classification: 3 shadow, 8/9 cloud, 10 cirrus, 11 snow |
Landsat 8/9 equivalents: B2 blue, B3 green, B4 red, B5 NIR, B6 SWIR1, B7 SWIR2, B10 thermal. Note that NIR is B5 on Landsat and B8 on Sentinel-2 — the most common band-mixing error there is.
| Product | Conversion |
|---|---|
| Sentinel-2 L2A reflectance | DN / 10000 |
| Landsat C2 L2 reflectance | DN × 0.0000275 − 0.2 |
| Landsat C2 L2 surface temperature | DN × 0.00341802 + 149.0 → K |
| Sentinel-1 GRD in Earth Engine | already dB — do not rescale |
Sanity checks: surface reflectance ∈ [0, 1]; NDVI ∈ [−1, 1]; LST in Kenya ∈ roughly [10, 60] °C; Sentinel-1 VV σ⁰ ∈ roughly [−25, 0] dB.
ee.ImageCollection(id).filterBounds(g).filterDate(a, b)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 30))
collection.map(fn) · collection.median() · collection.qualityMosaic('NDVI')
image.select(bands).rename(names) · image.updateMask(mask)
image.normalizedDifference(['B8','B4']) // any normalised difference
image.addBands(other) · image.clip(geom)
image.reduceRegion({reducer, geometry, scale, maxPixels}) // scale is REQUIRED
image.reduceRegions({collection, reducer, scale})
image.sampleRegions({collection, properties, scale})
image.stratifiedSample({numPoints, classBand, region, scale})
ee.Classifier.smileRandomForest(n).train({features, classProperty, inputProperties})
classified.errorMatrix('reference', 'classification')
ee.Image.pixelArea() // for area sums
Export.image.toDrive({image, region, scale, crs, maxPixels})
Export.table.toDrive({collection, fileFormat: 'CSV'})
Appendix C
| Collection | Earth Engine ID | Used in |
|---|---|---|
| Sentinel-2 L2A, harmonised | COPERNICUS/S2_SR_HARMONIZED | 1–7, 9 |
| Sentinel-2 cloud probability | COPERNICUS/S2_CLOUD_PROBABILITY | 2 |
| Landsat 9 C2 L2 | LANDSAT/LC09/C02/T1_L2 | 3, 7, 9 |
| Landsat 8 C2 L2 | LANDSAT/LC08/C02/T1_L2 | 3, 7, 9 |
| Landsat 5 TM C2 L2 | LANDSAT/LT05/C02/T1_L2 | 7 |
| Sentinel-1 GRD | COPERNICUS/S1_GRD | 8 |
| Sentinel-5P NO₂ | COPERNICUS/S5P/OFFL/L3_NO2 | 9 |
| SRTM 30 m DEM | USGS/SRTMGL1_003 | 3, 5, 8, 9 |
| JRC Global Surface Water | JRC/GSW1_4/GlobalSurfaceWater | 8, 9 |
| CHIRPS daily rainfall | UCSB-CHG/CHIRPS/DAILY | 4 |
| MODIS vegetation indices | MODIS/061/MOD13Q1 | 4 |
| Administrative boundaries | FAO/GAUL/2015/level2 | all |
| WorldPop population | WorldPop/GP/100m/pop | 8, 9 |
Earth Engine collections carry the licence terms of their originating agency — Copernicus data is free and open with attribution; Landsat is public domain; CHIRPS and WorldPop have their own citation requirements. Cite the dataset, not just the platform, and cite Gorelick et al. (2017) for Earth Engine itself.
EGS 2403 Remote Sensing Applications · Course reader, edition 1.0 · Jomo Kenyatta University of Agriculture and Technology · September 2026. Corrections and suggestions are welcome and will be credited in the next edition.
Appendix D
This unit teaches Earth Engine. Earth Engine is a product, owned by one company, offered under terms that company sets and can change. It is reasonable to ask what happens to your skills if it does — and the honest answer is worth more to you than the platform is.
Google has changed the terms of geospatial products before, with real consequences for people who had built on them. In 2018 the Google Maps Platform moved to a pay-as-you-go model with a monthly credit; a great deal of academic, NGO and small-business mapping that had run free for years suddenly carried a bill, and some of it simply stopped. Earth Engine itself moved to a commercial footing in 2023, with paid tiers for commercial users alongside continued free access for research, education and non-profit work. Free academic access remains, and there is no signal that it is going away — but it is a policy, not a guarantee, and policies are revised.
Set against that: the value is real. In a lab where the connection is unreliable and the machines are modest, a platform that transfers no imagery at all and does the computation elsewhere is not a convenience, it is the difference between doing this work and not doing it. That is why this unit teaches it.
Nearly everything. Strip away the API and the workflow in every chapter of this reader is the same five operations:
None of that is Google's. The sensor physics of week 1, the index choices of week 4, the training design of week 5, the whole of week 6, the change-detection logic of week 7, the radar geometry of week 8 — all of it is platform-independent, and all of it is the part that takes a semester to learn. What is Google-specific is the syntax, and syntax is the cheap part. A competent analyst moves between these environments in a week.
The cloud-native geospatial stack does the same job with open standards and open tools:
ee.ImageCollection(...).filter(...). Public endpoints include Element 84's Earth Search over the AWS Sentinel-2 archive (no account needed) and Microsoft's Planetary Computer.xarray cube.Week 2's central script, in both environments. Read them together — the operations line up one for one.
JavaScript · Earth Enginevar s2 = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
.filterBounds(aoi)
.filterDate('2025-01-01', '2025-12-31')
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 40));
var masked = s2.map(function(img) {
var scl = img.select('SCL');
var clear = scl.neq(3).and(scl.neq(8)).and(scl.neq(9))
.and(scl.neq(10)).and(scl.neq(11));
return img.updateMask(clear).divide(10000);
});
var composite = masked.median();
var ndvi = composite.normalizedDifference(['B8', 'B4']);
Export.image.toDrive({image: ndvi, region: aoi, scale: 10});
Python · STAC + COG
import pystac_client, odc.stac, rioxarray
bbox = [36.6, -1.3, 37.2, -0.9] # Kiambu
# 1. SEARCH — STAC replaces ee.ImageCollection().filter()
catalog = pystac_client.Client.open("https://earth-search.aws.element84.com/v1")
items = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime="2025-01-01/2025-12-31",
query={"eo:cloud_cover": {"lt": 40}},
).item_collection()
print(f"{len(items)} scenes found")
# 2. LOAD — lazily, into an xarray cube backed by dask.
# Nothing is downloaded yet: COG range requests happen on demand.
ds = odc.stac.load(
items,
bands=["red", "nir", "scl"],
resolution=10,
bbox=bbox,
chunks={"x": 2048, "y": 2048},
)
# 3. MASK — the same SCL classes as the Earth Engine version
clear = ~ds.scl.isin([3, 8, 9, 10, 11])
ds = ds.where(clear) / 10000 # the scale factor does not go away
# 4. COMPOSITE and 5. BAND MATH
composite = ds.median(dim="time")
ndvi = (composite.nir - composite.red) / (composite.nir + composite.red)
# only NOW does anything actually transfer or compute
ndvi.compute().rio.to_raster("ndvi_kiambu_2025.tif")
Two things to notice. The shape is identical — search, mask, composite, band maths, write — and both environments defer execution until you ask for a result, so the mental model transfers directly. And the scale factor is still there in the Python version, because it is a property of the Sentinel-2 product, not of the platform. Every trap in this reader is a trap in both stacks.
| Earth Engine | STAC + COG + Python | |
|---|---|---|
| Install | None — a browser | A conda environment, and it will fight you once |
| Compute | Google's, free at academic scale | Yours. A laptop handles a county; a country needs a cluster or a cloud bill |
| Data transfer | Effectively zero | Range requests — tens of MB for a county-year, not the gigabytes of full-scene download |
| Ceiling | Continental, casually | Bounded by your hardware or your budget |
| Reproducible in 2036 | If Google still offers it on these terms | Yes, if you pin your versions — the data and the tools are public |
| Who can revoke it | Nobody, in practice — though the hosting is still someone's cloud | |
| Learning curve | Shallow, then the client/server wall in week 2 | Steeper at the start, no wall afterwards |
| Employability | Widely used in NGOs, agencies, research | Increasingly the industry standard for production pipelines |
Note the row that people forget: the open stack does not remove the dependency on someone's cloud, it moves it. Earth Search runs on AWS; the Planetary Computer is Microsoft's. What it removes is lock-in — your code is not written against a proprietary API, so switching provider is a changed URL rather than a rewrite.
The recommendation this unit follows. Learn Earth Engine, because in this environment it is what makes the work possible, and because a semester spent fighting installations is a semester not spent on accuracy assessment.
Then port one complete workflow out of it, deliberately, before you graduate. The port is what converts a platform skill into a transferable one — and having done it once, you will know that the answer to "what if Google changes the terms" is a week of work, not a lost career.
geemap. It drives Earth Engine from Python, which puts your work in the same language and notebook environment as the open stack — a half-step that makes the eventual port much shorter.Do this once, at any point after week 4. It is the most valuable two hours in the unit for your career, and it is a legitimate project component.
conda install -c conda-forge pystac-client odc-stac xarray rioxarray dask matplotlib.Reading. The STAC specification and the pystac-client documentation. The Cloud-Native Geospatial Foundation's introductory material. The openEO API documentation and the Copernicus Data Space Ecosystem. Gorelick et al. (2017) for what Earth Engine set out to be, read alongside Google's current Earth Engine terms of service — the comparison is itself instructive.