Aller au contenu

Publication cloud native - raster

Cette publication convient à un indicateur raster produit par une chaîne de traitement : une température de surface, une classification, ou tout autre indicateur raster. Vous livrez deux choses :

  • des fichiers COG sur S3 : les pixels, lus par TiTiler pour générer les tuiles à la volée
  • une description STAC de ce fichier : où il se trouve, comment le colorier, où le ranger dans le géoportail.

La plateforme fait le reste. Le géoportail lit le catalogue STAC et demande les tuiles à TiTiler, qui les calcule à la volée à partir de votre COG. Vous n'avez rien à préparer en plus : pas de projet QGIS, pas de style .qml, pas de tuiles pré-calculées.

graph LR
    T[Votre traitement] -->|1-2. COG| S[(S3)]
    T -->|3-4. Collection + Item| C[Catalogue STAC]
    C --> G[Géoportail]
    G -->|tuiles, valeur au clic| TT[TiTiler]
    TT -->|lit le COG| S

Chaque morceau de la description a un rôle précis :

Vous fournissez À quoi ça sert
le COG sur S3 les valeurs des pixels, lues par TiTiler
l'Item STAC (une date) où est le COG, comment l'afficher, sa ville, son indicateur, sa date, sa description
la Collection STAC (indicateur × ville × producteur) le producteur (qui donne les droits d'accès), la licence, les libellés lisibles

Les quatre étapes sont détaillées ci-dessous. Un exemple complet et une check-list terminent la page.

1. Produire un COG

Un Cloud Optimized GeoTIFF est un GeoTIFF organisé pour être lu par morceaux. TiTiler n'en lit que la portion utile à chaque tuile : c'est ce qui rend l'affichage rapide, même sur un gros fichier.

Pour produire vos COG, la plateforme met à votre disposition AntFlow : vous y exécutez votre chaîne de traitement (packagée en CWL), qui exécute votre workflow ou vourrez produire vos COG et faire le stockage programmatiquement dans le depot S3. Le guide Processing explique comment construire et lancer vos chaînes. sur S3. Le guide Processing explique comment construire et lancer cette chaîne.

2. Déposer le COG sur S3

Déposez le fichier dans un bucket de la plateforme (voir Stocker mes données). Le chemin est libre, mais suivez de préférence la convention commune :

s3://<votre-bucket>/votre-arborescence/<ville>/<indicateur>/<date>/<fichier>.tif
s3://value-adder/map-products/metropole-grand-nancy/ndvi/20260627/ndvi_20260627.tif

TiTiler lit ce fichier directement, avec son compte de service. Vérifiez qu'il y arrive en ouvrant cette URL : elle doit renvoyer l'emprise et le type du raster.

https://titiler.urbiverde.com/cog/info?url=s3://<votre-bucket>/<chemin>/<fichier>.tif

Si TiTiler ne peut pas lire votre bucket, demandez l'accès à l'IF Operator.

3. Décrire le COG en STAC

Les extensions à déclarer

Une extension STAC est un ensemble de champs normalisés. Chacune doit être déclarée dans le tableau stac_extensions de l'Item, par l'URL de son schéma.

Extension Statut Ce qu'elle apporte
UrbiVerde v1.0.0 obligatoire ville, indicateur, date, producteur : le rangement dans le géoportail
Render v2.0.0 obligatoire comment afficher le raster : une couche du géoportail par render
Alternate Assets v1.2.0 obligatoire l'adresse s3:// du COG, celle que lit TiTiler
Classification v2.0.0 recommandée pour des classes le nom de chaque classe, repris dans la légende
Projection v2.0.0, Raster v2.0.0 recommandées CRS, résolution, statistiques des bandes
"stac_extensions": [
  "https://stac-extensions.github.io/projection/v2.0.0/schema.json",
  "https://stac-extensions.github.io/raster/v2.0.0/schema.json",
  "https://stac-extensions.github.io/render/v2.0.0/schema.json",
  "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json",
  "https://stac-extensions.github.io/classification/v2.0.0/schema.json",
  "https://stac.urbiverde.com/extensions/urbiverde/v1.0.0/schema.json"
]

La Collection : indicateur × ville × producteur

Une Collection regroupe toutes les dates d'un indicateur, sur une ville, pour un producteur. Vous la créez une fois, à la première date publiée.

Champ Valeur Exemple
id <indicateur>-<ville>-<producteur>, en minuscules, sans accent, mots séparés par - ndvi-metropole-grand-nancy-cs-group
description ce que représente l'indicateur ; affichée si l'Item n'a pas la sienne NDVI Sentinel-2 sur…
license identifiant SPDX (etalab-2.0, CC-BY-4.0…), sinon other avec un lien rel: license other
providers au moins {"name": …, "roles": ["producer"]} : c'est lui qui donne les droits d'accès CS Group
keywords affichés dans la fiche de la couche ["ndvi", "sentinel-2"]
urbiverde:city, urbiverde:indicator, urbiverde:producer les mêmes slugs que dans les Items metropole-grand-nancy, ndvi, cs-group
urbiverde:city_label, urbiverde:indicator_label les noms lisibles Métropole du Grand Nancy, NDVI
urbiverde:show_in_geoportal facultatif : false masque toute la Collection du géoportail true

Le nom du producteur ne change jamais

L'accès au géoportail est accordé par ville et par producteur. Le producteur est le name de providers de rôle producer. Écrivez-le exactement de la même façon dans toutes vos Collections, et demandez à l'équipe UrbiVerde le groupe qui ouvre l'accès à vos utilisateurs.

L'Item : une date

Champ Valeur Pourquoi
id <id de la Collection>-<date> ndvi-metropole-grand-nancy-cs-group-20260627
collection l'id de la Collection rattache l'Item à son producteur, donc à ses droits
geometry, bbox emprise en WGS 84 (EPSG:4326) la couche est découpée sur la bbox dans le géoportail
datetime, ou start_datetime + end_datetime période observée : datetime seul pour une acquisition, start_datetime + end_datetime pour une année ou un mois recherche temporelle et 3e niveau du géoportail : 2026-06-27, 2022-06 ou 2022
properties.description ce que montre cette date : source, millésime, méthode texte affiché sur la couche
properties.urbiverde:city la ville, en slug 1er niveau du géoportail
properties.urbiverde:indicator l'indicateur, en slug 2e niveau du géoportail
properties.urbiverde:producer le producteur, en slug recherche par producteur
properties.renders voir plus bas les couches affichées
assets voir ci-dessous le COG

La date affichée vient de la période : une année entière (2022-01-01 au 2022-12-31) s'affiche 2022, un mois entier 2022-06, sinon le jour (2026-06-27). Les dates sont triées par début de période.

L'asset : où se trouve le COG

L'asset décrit le fichier. TiTiler le lit à l'adresse donnée par alternate.s3.href. Le href principal peut rester relatif : il pointe sur le fichier posé à côté de l'Item.

"assets": {
  "ndvi": {
    "href": "./ndvi_20260627.tif",
    "alternate": {
      "s3": { "href": "s3://cs-group/map-products/metropole-grand-nancy/ndvi/20260627/ndvi_20260627.tif" }
    },
    "type": "image/tiff; application=geotiff; profile=cloud-optimized",
    "roles": ["data"],
    "bands": [{ "data_type": "float32", "nodata": "nan" }]
  }
}

Sans alternate.s3.href en s3://, aucune couche n'est créée pour cet asset.

Les renders : comment afficher

Un render est une recette d'affichage. Chaque render devient une couche du géoportail, nommée <indicateur> · <ville> · <date> · <title du render>. Un même COG peut donc donner plusieurs couches : le NDVI en dégradé, et le NDVI en classes.

Champ Rôle
assets exactement un asset (le COG à lire)
title nom de la couche
colormap_name + rescale dégradé de couleurs et ses bornes, pour une valeur continue
colormap une couleur par valeur entière, pour des classes : un objet {"0": [r, g, b, a], …}
expression calcul appliqué aux pixels avant affichage, en texte (b1 = 1re bande)
nodata, resampling, bidx valeur transparente, rééchantillonnage, bandes à lire
minmax_zoom facultatif : niveaux de zoom d'affichage ; par défaut, à partir du zoom 8

Deux recettes couvrent la plupart des cas :

"ndvi": {
  "title": "NDVI (RdYlGn)",
  "assets": ["ndvi"],
  "rescale": [[-1, 1]],
  "colormap_name": "rdylgn",
  "resampling": "nearest"
}

Dans le géoportail :

  • la légende est une barre de couleurs, avec 1 en haut et -1 en bas (les bornes de rescale) ;
  • un clic sur la carte affiche la valeur du pixel : NDVI (RdYlGn) : 0.066.

Les noms de dégradés disponibles sont listés sur https://titiler.urbiverde.com/colorMaps.

"ndvi_classes": {
  "title": "NDVI classes",
  "assets": ["ndvi"],
  "expression": "where(b1<0,0,where(b1<0.2,1,where(b1<0.4,2,where(b1<0.6,3,4))))",
  "colormap": {
    "0": [165, 0, 38, 255],
    "1": [244, 109, 67, 255],
    "2": [255, 255, 191, 255],
    "3": [134, 188, 106, 255],
    "4": [0, 104, 55, 255]
  },
  "resampling": "nearest"
}

L'expression range chaque pixel dans une classe : « si b1 < 0 alors 0, sinon si b1 < 0.2 alors 1… ». La colormap donne la couleur de chaque classe.

Pour nommer les classes, ajoutez sur la bande de l'asset l'extension Classification :

"bands": [{
  "data_type": "float32",
  "nodata": "nan",
  "classification:classes": [
    { "value": 0, "name": "water", "title": "Eau / sans végétation", "color_hint": "A50026" },
    { "value": 4, "name": "dense", "title": "Végétation dense", "color_hint": "006837" }
  ]
}]

Le géoportail combine les deux sources dans la légende et au clic :

Classe title (Classification) Seuils (expression) Libellé affiché
0 Eau / sans végétation b1 < 0 Eau / sans végétation (< 0)
1 - 0 ≤ b1 < 0.2 0 – 0.2
4 Végétation dense b1 ≥ 0.6 Végétation dense (≥ 0.6)

Sans l'un ni l'autre, le libellé est la valeur de la classe (1). Les seuils ne sont lus que sous la forme where(b1<seuil, classe, …) imbriquée, avec des seuils croissants.

Si votre COG contient déjà des classes (des entiers), il suffit d'une colormap, sans expression.

Les erreurs qui font disparaître une couche

  • colormap en liste ([[[-1, 0], [r, g, b, a]], …]) : TiTiler l'accepte, mais l'Item n'est plus valide pour l'extension Render. Utilisez un objet, et une expression pour former les classes.
  • Plusieurs assets dans un render : la couche est ignorée. Un render lit un seul COG.
  • expression qui n'est pas du texte : la couche est ignorée.

Une couche ignorée ne bloque jamais le reste du catalogue : les autres couches s'affichent normalement.

4. Enregistrer dans le catalogue

Le catalogue https://stac.urbiverde.com accepte l'écriture (extension Transactions). Créez la Collection une seule fois, puis ajoutez ou remplacez chaque Item :

STAC=https://stac.urbiverde.com

# Une fois : créer la Collection (409 si elle existe déjà)
curl -X POST "$STAC/collections" -H "Content-Type: application/json" -d @collection.json

# À chaque date : ajouter ou remplacer l'Item
curl -X POST "$STAC/collections/ndvi-metropole-grand-nancy-cs-group/bulk_items" \
  -H "Content-Type: application/json" \
  -d '{"method": "upsert", "items": {"ndvi-metropole-grand-nancy-cs-group-20260627": '"$(cat item.json)"'}}'

Republier le même id d'Item remplace la version précédente. Le catalogue est aujourd'hui ouvert : aucune authentification n'est nécessaire. Dans sa version sécurisée, ajoutez un jeton OIDC Keycloak à chaque requête, avec -H "Authorization: Bearer <jeton>" (voir Le catalogue STAC).

Le projet ndvi-demo applique toute cette publication dans une chaîne CWL : stac/build_stac.py construit la Collection et l'Item, publish/publish.py dépose les fichiers sur S3 et les enregistre dans le catalogue. Partez de lui pour un nouvel indicateur.

5. Vérifier

Dans le géoportail, votre indicateur apparaît sous ville → indicateur, avec une couche par date et par render. Ouvrez une couche : la description, la légende et le producteur doivent être ceux de votre Item et de votre Collection. Cliquez sur la carte : la valeur ou la classe du pixel s'affiche.

Ce que vous voyez Cause probable
L'indicateur n'apparaît pas un champ urbiverde:* manquant ou vide ; pas de datetime ni de start_datetime ; urbiverde:show_in_geoportal: false ; votre compte n'a pas de droit sur cette ville ou ce producteur
La date apparaît, mais pas une couche pas de alternate.s3.href en s3://, render multi-assets ou expression non textuelle
La couche est vide TiTiler ne lit pas le fichier : testez l'URL /cog/info de l'étape 2
La légende affiche 0, 1, 2… ni classification:classes ni expression à seuils
Une bande colorée apparaît loin de la ville en vue éloignée la bbox de l'Item est absente

Exemple complet

Collection
{
  "type": "Collection",
  "stac_version": "1.1.0",
  "stac_extensions": ["https://stac.urbiverde.com/extensions/urbiverde/v1.0.0/schema.json"],
  "id": "ndvi-metropole-grand-nancy-cs-group",
  "title": "NDVI · Métropole du Grand Nancy · CS Group",
  "description": "Sentinel-2 L2A NDVI (SCL cloud masked) over Métropole du Grand Nancy, produced by CS Group.",
  "license": "other",
  "providers": [{ "name": "CS Group", "roles": ["producer"] }],
  "keywords": ["ndvi", "sentinel-2", "metropole-grand-nancy"],
  "extent": {
    "spatial": { "bbox": [[6.0653, 48.5984, 6.3026, 48.7480]] },
    "temporal": { "interval": [["2026-06-27T10:40:41Z", null]] }
  },
  "urbiverde:city": "metropole-grand-nancy",
  "urbiverde:city_label": "Métropole du Grand Nancy",
  "urbiverde:indicator": "ndvi",
  "urbiverde:indicator_label": "NDVI",
  "urbiverde:producer": "cs-group",
  "urbiverde:show_in_geoportal": true,
  "links": [{ "rel": "license", "href": "https://www.urbiverde.com", "title": "UrbiVerde terms of use" }]
}
Item
{
  "type": "Feature",
  "stac_version": "1.1.0",
  "stac_extensions": [
    "https://stac-extensions.github.io/projection/v2.0.0/schema.json",
    "https://stac-extensions.github.io/raster/v2.0.0/schema.json",
    "https://stac-extensions.github.io/render/v2.0.0/schema.json",
    "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json",
    "https://stac-extensions.github.io/classification/v2.0.0/schema.json",
    "https://stac.urbiverde.com/extensions/urbiverde/v1.0.0/schema.json"
  ],
  "id": "ndvi-metropole-grand-nancy-cs-group-20260627",
  "collection": "ndvi-metropole-grand-nancy-cs-group",
  "geometry": { "type": "Polygon", "coordinates": [[[6.0653, 48.5984], [6.3026, 48.5984], [6.3026, 48.7480], [6.0653, 48.7480], [6.0653, 48.5984]]] },
  "bbox": [6.0653, 48.5984, 6.3026, 48.7480],
  "properties": {
    "datetime": "2026-06-27T10:40:41Z",
    "description": "Sentinel-2 L2A NDVI (SCL cloud masked) over Métropole du Grand Nancy on 27/06/2026, produced by CS Group.",
    "license": "other",
    "proj:code": "EPSG:32631",
    "urbiverde:city": "metropole-grand-nancy",
    "urbiverde:indicator": "ndvi",
    "urbiverde:producer": "cs-group",
    "renders": {
      "ndvi": {
        "title": "NDVI (RdYlGn)",
        "assets": ["ndvi"],
        "rescale": [[-1, 1]],
        "colormap_name": "rdylgn",
        "resampling": "nearest"
      },
      "ndvi_classes": {
        "title": "NDVI classes",
        "assets": ["ndvi"],
        "expression": "where(b1<0,0,where(b1<0.2,1,where(b1<0.4,2,where(b1<0.6,3,4))))",
        "colormap": {
          "0": [165, 0, 38, 255], "1": [244, 109, 67, 255], "2": [255, 255, 191, 255],
          "3": [134, 188, 106, 255], "4": [0, 104, 55, 255]
        },
        "resampling": "nearest"
      }
    }
  },
  "assets": {
    "ndvi": {
      "href": "./ndvi_20260627.tif",
      "alternate": {
        "s3": { "href": "s3://cs-group/map-products/metropole-grand-nancy/ndvi/20260627/ndvi_20260627.tif" }
      },
      "type": "image/tiff; application=geotiff; profile=cloud-optimized",
      "title": "NDVI (SCL + AOI masked)",
      "roles": ["data"],
      "bands": [{
        "data_type": "float32",
        "nodata": "nan",
        "raster:spatial_resolution": 10.0,
        "classification:classes": [
          { "value": 0, "name": "water-no-vegetation", "title": "Water / no vegetation", "color_hint": "A50026" },
          { "value": 1, "name": "bare-soil-built-up", "title": "Bare soil / built-up", "color_hint": "F46D43" },
          { "value": 2, "name": "sparse-vegetation", "title": "Sparse vegetation", "color_hint": "FFFFBF" },
          { "value": 3, "name": "moderate-vegetation", "title": "Moderate vegetation", "color_hint": "86BC6A" },
          { "value": 4, "name": "dense-vegetation", "title": "Dense vegetation", "color_hint": "006837" }
        ]
      }]
    }
  },
  "links": [{ "rel": "license", "href": "https://www.urbiverde.com", "title": "UrbiVerde terms of use" }]
}

Check-list

  • [ ] Le raster est un COG valide (rio cogeo validate), avec CRS et nodata.
  • [ ] TiTiler le lit : l'URL /cog/info?url=s3://… répond.
  • [ ] Les six extensions utiles sont déclarées dans stac_extensions.
  • [ ] Collection : id en <indicateur>-<ville>-<producteur>, license, providers de rôle producer, urbiverde:* et libellés.
  • [ ] Nom du producteur identique dans toutes vos Collections.
  • [ ] Item : id en <collection>-<date>, bbox, description, datetime (ou start_datetime + end_datetime), et les champs urbiverde:* non vides.
  • [ ] Chaque asset affiché a un alternate.s3.href en s3://.
  • [ ] Chaque render lit un seul asset ; colormap en objet ; expression en texte.
  • [ ] Pour des classes : classification:classes sur la bande, ou une expression à seuils.