Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

TP Python 4 : Opérations Raster

Open in Colab Open in Kaggle Launch on Renku

Exécution dans le cloud : ce notebook peut aussi tourner dans le cloud (Colab, Kaggle, Renku). Avant de lancer les cellules, télécharge les rasters (dossiers rasters/ et dhm25_p/) depuis le dossier OneDrive du cours et dépose-le dans le répertoire de travail du notebook (Colab : panneau 📁 → Upload ; Kaggle : File → Upload ; Renku : déjà dans le projet). Procédure complète : docs/cloud-badges.md.

TP Python 4 : Opérations Raster

Ce TP Python est le pendant numérique du TP QGIS 4. Tu vas reproduire les mêmes opérations raster en Python avec rasterio et numpy.

Info QGISAttribut rasterio
Système de référencesrc.crs
Dimensionssrc.height, src.width
Emprisesrc.bounds
Résolution pixelsrc.res
Nombre de bandessrc.count
Valeur NoDatasrc.nodata
=== Métadonnées — Bande 4 (NIR) ===
CRS          : EPSG:32632
Dimensions   : 7292 lignes × 8212 colonnes
Résolution   : 31.1 m × 31.3 m
Emprise      : BoundingBox(left=366798.8360244284, bottom=4983476.32585051, right=621856.6517301749, top=5212035.991148069)
Type données : uint8
NoData       : None

1.2 Fusion des bandes séparées en image multibandes

Dans QGIS tu utilisais Raster > Miscellaneous > Merge avec l’option “Place each input file in a separate band”. En Python : on empile les tableaux NumPy des 7 bandes avec np.stack() et on écrit le résultat dans un GeoTIFF à 7 bandes.

Image multibandes : (7, 7292, 8212)
  7 bandes × 7292 lignes × 8212 colonnes

GeoTIFF multibandes sauvegardé : rasters/output/Landsat4_1990_multibande.tif

Partie 2 — Compositions colorées

Dans QGIS tu choisissais les bandes R/G/B dans la symbologie. En Python : on sélectionne les indices des bandes, on les normalise et on les empile.

CompositionBande RBande GBande BUsage
Vraies couleursB3B2B1Vision naturelle
Fausses couleurs (végétation)B4B3B2Végétation en rouge vif
AgricultureB5B4B1Cultures, humidité
<Figure size 1800x600 with 3 Axes>

Partie 3 — Calcul du NDVI

Le NDVI est calculé depuis la Calculatrice raster dans QGIS. En Python : opération pixel-à-pixel NumPy sur les bandes B3 et B4.

NDVI=NIR−RougeNIR+Rouge=B4−B3B4+B3\text{NDVI} = \frac{\text{NIR} - \text{Rouge}}{{\text{NIR} + \text{Rouge}}} = \frac{B4 - B3}{B4 + B3}
Plage NDVIInterprétation
-1 à 0Eau, neige, surfaces imperméables
0 à 0.3Sol nu, rochers
0.3 à 1Végétation dense (forêt, prairies)
NDVI — min=-1.000, max=1.000, moy=0.357
<Figure size 1400x500 with 3 Axes>
NDVI sauvegardé : rasters/output/NDVI_Ticino1990.tif

Partie 4 — Reclassification de l’occupation du sol

4.1 Chargement et visualisation

Le fichier MapTicino1990_8classes.tif contient 8 classes :

ValeurClasse
1Forêt
2Prés
3Eau
4Neige
5Sols nus
6Aires urbaines
7Nuages
8Ombres
Occupation du sol : (3456, 2707)
MNT reprojeté     : (3456, 2707)  (même grille que l'occupation du sol)

Distribution des classes (avant reclassification) :
  Classe 1 (Forêt          ) :  4200598 pixels  (44.9 %)
  Classe 2 (Prés           ) :  1513422 pixels  (16.2 %)
  Classe 3 (Eau            ) :   470355 pixels  (5.0 %)
  Classe 4 (Neige          ) :   159060 pixels  (1.7 %)
  Classe 5 (Sols nus       ) :  1458456 pixels  (15.6 %)
  Classe 6 (Aires urbaines ) :   320292 pixels  (3.4 %)
  Classe 7 (Nuages         ) :   380119 pixels  (4.1 %)
  Classe 8 (Ombres         ) :   126523 pixels  (1.4 %)
<Figure size 1400x600 with 3 Axes>

4.2 Reclassification conditionnelle

Dans QGIS tu utilisais la Calculatrice Raster :

if( (MNT > 1400) AND (classes = 6), 5, classes )

En Python : np.where() avec deux conditions combinées par &.

But : corriger les pixels classés Aires urbaines (6) en altitude > 1 400 m → les reclasser en Sols nus (5) car la confusion vient de signatures spectrales similaires entre béton et rochers.

Pixels reclassifiés (Urbain→Sol nu, altitude >1400 m) : 9985

Distribution après reclassification :
  Classe 1 (Forêt          ) :  4200598 pixels
  Classe 2 (Prés           ) :  1513422 pixels
  Classe 3 (Eau            ) :   470355 pixels
  Classe 4 (Neige          ) :   159060 pixels
  Classe 5 (Sols nus       ) :  1468441 pixels  (+9985)
  Classe 6 (Aires urbaines ) :   310307 pixels  (-9985)
  Classe 7 (Nuages         ) :   380119 pixels
  Classe 8 (Ombres         ) :   126523 pixels
<Figure size 1600x700 with 2 Axes>
Reclassifié sauvegardé : rasters/output/ReClass_MapTicino1990_8classes.tif

Partie 5 — Interpolation IDW

L’interpolation IDW prédit la valeur en tout point depuis des mesures ponctuelles. Dans QGIS : outil IDW sur les points DHM25 de Swisstopo (résolution 900 m). En Python : scipy.interpolate.griddata.

z^(x)=∑izi⋅di−p∑idi−p\hat{z}(x) = \frac{\sum_i z_i \cdot d_i^{-p}}{\sum_i d_i^{-p}}
Points chargés : 209505
Colonnes       : ['OBJECTID', 'OBJECTVAL', 'OBJECTORIG', 'YEAROFCHAN', 'geometry']
CRS            : EPSG:21781
   OBJECTID   OBJECTVAL OBJECTORIG  YEAROFCHAN  \
0   8561055  Hoehenkote       LK25        1987   
1   8561059  Hoehenkote       LK25        1987   
2   8561134  Hoehenkote       LK25        1981   

                          geometry  
0  POINT Z (559145.3 242445.3 715)  
1  POINT Z (560184.4 242995.3 725)  
2  POINT Z (579737.5 253873.4 523)  
MNT interpolé : (254, 428)  —  résolution 900 m
Altitude : min=-128 m, max=4515 m
<Figure size 1000x800 with 2 Axes>
MNT IDW sauvegardé : rasters/output/MNT_IDW_900m.tif

Partie 6 — Ombrage (Hillshade)

Dans QGIS : Raster > Analysis > Ombrage (azimut 315°, élévation 45°). En Python : on calcule le gradient du MNT, puis la pente, l’exposition et le produit scalaire avec la direction solaire.

Hillshade=cos⁡(zs)cos⁡(slope)+sin⁡(zs)sin⁡(slope)cos⁡(as−aspect)\text{Hillshade} = \cos(z_s)\cos(\text{slope}) + \sin(z_s)\sin(\text{slope})\cos(a_s - \text{aspect})
/tmp/ipykernel_107251/90092553.py:10: RuntimeWarning: overflow encountered in square
  slope  = np.arctan(np.sqrt(dx**2 + dy**2))
<Figure size 1400x600 with 2 Axes>
Hillshade sauvegardé : rasters/output/Hillshade_Ticino.tif

Partie 7 — Classification de l’occupation du sol avec RF, XGBoost et MLP

Contexte et objectif

Les parties précédentes ont produit plusieurs couches raster alignées. On peut maintenant traiter chaque pixel comme une observation et entraîner un modèle de classification — l’une des tâches les plus courantes en télédétection.

RôleVariableSource
Cible yyClasse d’occupation du sol (1–6)MapTicino1990_8classes.tif
Prédicteurs x1..7x_{1..7}Réflectances Landsat (7 bandes)Parties 1–3
Prédicteur x8x_8Altitude (m)MNT reprojeté (Partie 4)

Progression par rapport aux TP précédents :

TPModèlesNouveauté
TP02Régression OLSR²/r, IC vs IP, surapprentissage, split entraînement/test
TP03Polynomiale, GLM, GAMNon-linéarité, lisseur spatial, K-fold CV, incertitude du R² CV
TP04RF, XGBoost, MLPModèles non-paramétriques, jeu de validation, prédiction conforme

Les modèles précédents (TP02–TP03) sont tous paramétriques : ils reposent sur une forme mathématique fixée à l’avance (une droite, un polynôme, une somme de splines) dont on estime les paramètres. Random Forest, XGBoost et le MLP (Multi-Layer Perceptron, un petit réseau de neurones) sont non-paramétriques — leur complexité s’adapte aux données plutôt que d’être fixée par une formule.

Ce qu’on va faire, dans l’ordre :

  1. Préparer les données (§7.1) — une matrice de features par pixel (bandes + altitude).

  2. Entraîner trois modèles candidats (RF, XGBoost, MLP) et choisir le meilleur sur un jeu de validation dédié, sans jamais regarder le jeu de test avant la toute fin (§7.2).

  3. Comparer leurs importances de variables (§7.3).

  4. Quantifier la fiabilité du modèle retenu par prédiction conforme (§7.4) : au lieu d’une seule classe prédite par pixel, on obtiendra un ensemble de classes plausibles, avec une garantie statistique du type « la vraie classe est dans l’ensemble au moins 90 % du temps ».

Les étapes 2 et 4 demandent chacune un jeu de données à part — d’où la séparation en quatre jeux dès le début de la §7.2.

ℹ️ Cible utilisée : la classification ci-dessous prend comme vérité terrain la carte d’occupation du sol originale (landcover, classes 1–6). La reclassification de la §4.2 (landcover_reclass) était une démonstration d’édition raster (Calculatrice Raster), pas la cible du modèle.

7.1 Préparation des données — matrice de features par pixel

On reprojette les 7 bandes Landsat sur la grille de l’occupation du sol, puis on construit, pour un sous-échantillon de 20 000 pixels valides, une matrice de 8 features (7 réflectances + altitude) et le vecteur cible (classe 1–6).

Rééchantillonnage des 7 bandes Landsat sur la grille de l'occupation du sol...
  B1-Bleu      rééchantillonné
  B2-Vert      rééchantillonné
  B3-Rouge     rééchantillonné
  B4-PIR       rééchantillonné
  B5-SWIR1     rééchantillonné
  B6-Therm     rééchantillonné
  B7-SWIR2     rééchantillonné

Image rééchantillonnée : (7, 3456, 2707)  (bandes × lignes × colonnes)
Pixels valides : 2,696,468 / 9,355,392 (28.8 %)

Features (8) : ['B1-Bleu', 'B2-Vert', 'B3-Rouge', 'B4-PIR', 'B5-SWIR1', 'B6-Therm', 'B7-SWIR2', 'MNT']
Taille du jeu de données : 20,000 pixels

Distribution des classes :
  1 – Forêt           : 11461 pixels (57.3 %)
  2 – Prés            :  3275 pixels (16.4 %)
  3 – Eau             :   537 pixels (2.7 %)
  4 – Neige           :    83 pixels (0.4 %)
  5 – Sols nus        :  3936 pixels (19.7 %)
  6 – Urbain          :   708 pixels (3.5 %)

7.2 Séparation en quatre jeux — entraînement / validation / calibration / test

Jusqu’ici (TP02–TP03), un seul modèle était ajusté à la fois : un split entraînement/test (ou une CV) suffisait à estimer sa généralisation. Ici, on compare trois modèles candidats (RF, XGBoost, MLP) et on veut, en plus, mesurer la fiabilité du modèle retenu. Cela demande quatre jeux disjoints, chacun avec un rôle strict :

JeuRôlePart
EntraînementAjuster les paramètres de chaque modèle50 %
ValidationChoisir lequel des trois modèles garder (jamais vu pendant l’entraînement)15 %
CalibrationMesurer, après coup, à quel point les probabilités du modèle retenu sont fiables (§7.4)15 %
TestÉvaluation finale, indépendante de toutes les décisions précédentes20 %

⚠️ Utiliser le jeu de test pour choisir un modèle (« je regarde l’accuracy de test des 3 modèles et je garde le meilleur ») invalide l’évaluation finale : le test n’est alors plus vraiment « jamais vu ». C’est exactement pour éviter ça que le jeu de validation existe.

Pourquoi un quatrième jeu, la calibration ? Un modèle de classification ne donne pas seulement une classe prédite, mais une probabilité pour chaque classe (predict_proba). Ces probabilités ne sont pas parfaitement fiables : un modèle peut se dire « sûr à 95 % » et se tromper bien plus souvent que 5 % du temps. La prédiction conforme (§7.4) corrige cela en observant, sur le jeu de calibration, à quel point le modèle se trompe réellement — puis s’en sert pour construire, pour chaque nouveau pixel, un ensemble de classes plausibles plutôt qu’une seule réponse, avec une garantie de couverture. La calibration doit rester distincte de la validation : la validation sert à choisir le modèle, la calibration sert seulement à mesurer sa fiabilité une fois ce choix fait.

Le MLP (MLPClassifier), contrairement aux arbres (RF, XGBoost), est sensible à l’échelle des variables : les réflectances Landsat (≈ 0–1) et l’altitude MNT (≈ 0–3000 m) ont des échelles très différentes, ce qui déséquilibre l’apprentissage d’un réseau de neurones. On standardise donc les features (StandardScaler) avant le MLP, dans un Pipeline — étape inutile, et donc omise, pour RF et XGBoost.

Entraînement : 10,000   Validation : 3,000   Calibration : 2,999   Test : 4,001

Sélection de modèle — précision sur le jeu de validation :
  Random Forest   : 0.895
  XGBoost         : 0.896
  MLP             : 0.898

→ Modèle retenu (validation la plus élevée) : MLP
<Figure size 1900x500 with 3 Axes>

7.3 Importances des variables

RF et XGBoost exposent nativement .feature_importances_ (basé sur la réduction d’impureté apportée par chaque variable dans les arbres). Le MLP n’a pas d’équivalent direct : ses poids sont distribués sur des couches cachées et ne s’interprètent pas variable par variable de la même façon — c’est un des compromis des réseaux de neurones (performance vs interprétabilité directe).

<Figure size 1400x500 with 2 Axes>
Classement RF — de la plus à la moins importante :
  B6-Therm     : 0.2041
  B3-Rouge     : 0.1627
  B2-Vert      : 0.1389
  B7-SWIR2     : 0.1184
  MNT          : 0.1003
  B1-Bleu      : 0.0984
  B4-PIR       : 0.0887
  B5-SWIR1     : 0.0886

7.4 Prédiction conforme (classification)

On met en œuvre ici le principe présenté en §7.2 : mesurer la fiabilité du modèle retenu (best_name) sur le jeu de calibration, puis construire des ensembles de classes avec une couverture garantie.

Modèle utilisé : MLP
n_cal = 2,999
q̂    = 0.5281  →  on retient toutes les classes avec P(classe|x) ≥ 0.4719

Couverture empirique  : 0.899  (cible ≥ 0.90)
Taille moy. ensemble  : 1.00 classes / pixel

Distribution des tailles d'ensemble :
  0 classe(s) :    30 pixels (0.7 %)
  1 classe(s) :  3940 pixels (98.5 %)
  2 classe(s) :    31 pixels (0.8 %)
<Figure size 1600x600 with 2 Axes>

Récapitulatif

Opération QGISCode Python clé
Propriétés de la coucherasterio.open(f) → .crs, .bounds, .res, .meta
Merge (fusion de bandes)np.stack(bands) + rasterio.open('w', count=N)
Composition coloréenp.dstack([r, g, b]) + normalisation percentile
Calculatrice raster (NDVI)(nir - rouge) / (nir + rouge)
Calculatrice raster (if/and)np.where((cond1) & (cond2), val_vrai, val_faux)
Reprojection rasterrasterio.warp.reproject(src, dst, src_crs=..., dst_crs=...)
Interpolation IDWscipy.interpolate.griddata(xy, z, (XX,YY), method='linear')
Ombrage (Hillshade)np.gradient(dem) → calcul slope / aspect / hillshade
Écrire un GeoTIFFrasterio.open(path, 'w', **meta) → .write(array, band)

Partie 7 — Classification ML sur pixels raster

ÉtapeCode clé
Reprojection bandes Landsatrasterio.warp.reproject(src, dst, dst_transform=meta_lc['transform'], ...)
Masque pixels validesnp.isin(lc, [1..6]) & np.all(ls > 0, axis=0) & (mnt > 0)
Sous-échantillonnagenp.random.default_rng(42).choice(n_valid, 20_000)
Matrice de featuresnp.column_stack([ls_on_lc[:, rows, cols].T, mnt[rows, cols]])
Séparation 50/15/15/20train_test_split deux fois (train / val / cal / test)
Random ForestRandomForestClassifier(n_estimators=200).fit(X_tr, y_tr)
XGBoost (0-indexé)LabelEncoder() + XGBClassifier().fit(X_tr, y_tr_enc)
MLP (standardisé)make_pipeline(StandardScaler(), MLPClassifier(...)).fit(X_tr, y_tr)
Sélection de modèlecomparer l’accuracy des 3 modèles sur X_val, jamais sur X_te
Matrice de confusionConfusionMatrixDisplay.from_predictions(y_te, y_pred, ...)
Importances RF / XGBoost.feature_importances_ (MLP : pas d’équivalent direct)
Score conforme (LAC)`scores = 1 – P(vraie_classe
Quantile conformeq̂ = np.quantile(scores, ceil((n+1)(1–α)) / n)
Ensemble de prédiction`pred_set = {classes k : P(k
Couverture empirique(pred_set[i, true_class[i]]).mean() sur le test

Concepts clés :

IndicateurSignification
Précision (accuracy)Part des pixels correctement classés ∈ [0, 1]
Jeu de validationSert à choisir un modèle — jamais à l’entraîner ni à l’évaluer finalement
Jeu de calibrationDistinct de la validation — sert uniquement à calculer les scores conformes
Matrice de confusionTableau croisé classes réelles × prédites — révèle les confusions fréquentes
Importance des variablesContribution de chaque bande/MNT à la qualité de classification (RF, XGBoost)
Score LAC`1 – P(vraie classe
q̂ conformeSeuil de probabilité : classe incluse si P(classe) ≥ 1 – q̂
Taille de l’ensemble1 = pixel certain ; >1 = zones de transition (écotones, urbain/roc)
Couverture garantieP(vraie classe ∈ ensemble) ≥ 1–α sans hypothèse distributionnelle

🎯 Bonnes pratiques ML — niveau 3 : sélectionner et garantir

(rappel niveaux 1–2 : TP02 posait les fondamentaux — jamais évaluer sur l’entraînement, toujours benchmarker, choisir la bonne métrique, questionner la composition du split ; TP03 a ajouté comparer plusieurs modèles par une procédure commune (CV) et quantifier l’incertitude d’une estimation de performance (CV répétée, bootstrap).)

PrincipeOù on l’a vu
Avec plusieurs modèles candidats, un jeu de validation dédié — distinct du test et de la calibration — sert à choisir lequel garder§7.2
Ne pas choisir un modèle en regardant le jeu de test : cela invalide l’évaluation finale, même si la tentation est grande§7.2
Une probabilité prédite (predict_proba) n’est pas automatiquement fiable ; la prédiction conforme donne une garantie de couverture mesurable, sans hypothèse sur sa distribution§7.4
L’importance des variables aide à interpréter un modèle (quelle bande contribue le plus), mais ne prouve pas une relation causale§7.3

Avec ces trois niveaux, construits progressivement TP après TP, on a les réponses à trois questions que tout projet de modélisation pose — pas seulement la régression ou la classification :

QuestionOutils vus dans ce fil rouge
Quel modèle choisir ?Comparer plusieurs candidats sur les mêmes données, avec la même métrique : tableau CV multi-modèles (TP03 §4.7), accuracy de validation (TP04 §7.2). Ne jamais choisir un modèle en regardant le jeu de test.
Comment le valider ?Train/test simple (TP02 §6.5) → K-fold CV (TP03 §4.7) → jeu de validation dédié quand plusieurs modèles sont en compétition (TP04 §7.2). Toujours évaluer — et sélectionner — sur des données jamais vues.
Comment quantifier l’incertitude ?IC vs IP (TP02 §6.3) → CV répétée et bootstrap (TP03 §4.8–4.9) → prédiction conforme (TP04 §7.4). Un seul chiffre de performance ne dit jamais tout : il faut aussi savoir à quel point il pourrait varier.

Un point commun à toutes ces réponses : elles demandent de réserver des données qu’on s’interdit d’utiliser avant le bon moment (test, validation, calibration). C’est contraignant, mais c’est le prix d’une évaluation qu’on peut réellement croire.

Bilan de la progression TP02 → TP04 : d’un modèle unique évalué par split entraînement/test (TP02), à plusieurs modèles non-linéaires comparés par K-fold CV avec attention à l’incertitude d’échantillonnage (TP03), jusqu’à plusieurs modèles non-paramétriques sélectionnés sur un jeu de validation dédié et accompagnés d’une garantie de couverture par prédiction conforme (TP04) — la rigueur de l’évaluation augmente avec la flexibilité du modèle.


✅ Validation du TP

Ton notebook est terminé ! Il te reste à valider ce volet Python sur Moodle.

ActivitéOù ?
Questions CodeRunner du TP4 (à soumettre dans Moodle)CodeRunner_TP4 (accès réservé aux étudiant·e·s UNIL)

La page d’accueil du TP indique la section Évaluation et rendus (quiz théorique, questions CodeRunner, dépôts) : retour au TP4.


📝 Quiz Moodle

Une fois ce TP terminé, teste tes connaissances avec le quiz Moodle du TP4 :

👉 Ouvrir le quiz du TP4 sur Moodle (accès réservé aux étudiant·e·s UNIL)