SamGIS - Segment Anything adattato al GIS
Tags: python, machinelearning, maps, vue
Published: 2024-05-24 12:07:16

## SamGIS - Segment Anything adattato ai sistemi geografici informativi (GIS)

Questo progetto, composto da frontend [SPA](https://en.wikipedia.org/wiki/Single-page_application) e da una parte backend ([samgis-be](https://github.com/trincadev/samgis-be/)), è un tentativo di effettuare riconoscimento d'immagine basato sul machine learning su immagini derivanti da dati geo-spaziali anche senza l'utilizzo di capacità computazionali particolari. Di recente ho aggiunto la possibilità di partire da [prompt testuali in linguaggio naturale](https://trinca.tornidor.com/it/projects/lisa-adapted-for-samgis).

La prima versione del backend era modificata da [SAM Exporter](https://github.com/vietanhdev/samexporter) semplificando l'utilizzo di [segment anything](https://segment-anything.com/) al fine di migliorare il riconoscimento di poligoni in applicazioni web GIS.

Dalla versione 1.5.1 il backend integra modifiche mutuate da [sam_onnx_full_export](https://github.com/AndreyGermanov/sam_onnx_full_export), in modo da supportare OnnxRuntime 1.17.x e versioni successive. Si noti che su MacOS l'esecuzione diretta da linea di comando soffre di [memory leak](https://github.com/microsoft/onnxruntime/issues/14455), rendendo le operazioni di inferenza più lente di quanto sarebbe normale. Si consiglia quindi l'esecuzione del progetto dentro ad un container docker, a meno di voler fare attività di sviluppo e debug.

Questi sono i link alle risorse del progetto:

- una [demo di HuggingFace Space](https://huggingface.co/spaces/aletrn/samgis) pubblica
- una [pagina demo](https://ml-trinca.tornidor.com/prediction-map) sotto autenticazione
- la [documentazione e le specifiche AWS lambda OpenAPI](https://docs.ml-trinca.tornidor.com)

Si tenga presente che la demo dello spazio HuggingFace ha i suoi file frontend statici nella cartella /static.

Chiunque desideri una spiegazione dettagliata sul funzionamento di questo progetto oppure voglia testare la demo sotto autenticazione può contattarmi tramite [linkedin](https://www.linkedin.com/in/trincatornidor/).

### LISA e SamGIS

Ho anche aggiunto il supporto alla segmentazione d'immagine tramite prompt testuali in linguaggio naturale: qui i dettagli a proposito dell'[integrazione di SamGIS con LISA](/it/projects/lisa-adapted-for-samgis).

## SamGIS rimpiazza anche SurferDTM

[SurferDTM](https://trinca.tornidor.com/it/projects/surferdtm-docs) è un tentativo di trovare [stratovulcani](https://volcano.oregonstate.edu/stratovolcanoes) all'interno di modelli digitali del terreno (DEM/DTM). Come nuova funzionalità, SamGIS ora può utilizzare anche DEM di input (come la prima immagine in questa galleria):

![samgis-dem-mapzen](https://res.cloudinary.com/aletrinok/image/upload/v1701206785/samgis/samgis-dem-1.png)

![samgis-dem-bw](https://res.cloudinary.com/aletrinok/image/upload/v1701206785/samgis/samgis-dem-2.png)

Si tenga conto che il provider dei dati "terrarium" codifica le immagini come RGB, quindi il backend le elabora per ottenere le quote di terreno effettive (come nella seconda immagine in questa galleria) utilizzando questa formula (dalla <a href="https:// www.mapzen.com/blog/terrain-tile-service/">pagina del provider, mapzen/nextzen</a>):

> (red * 256) + green + (blue / 256) - 32768

I also added a custom elaboration step to create an intermediate RGB image composed by channels:

- ROSSO - immagine DEM normalizzata
- VERDE - immagine 2d normalizzata composta dalla risultante di
  - immagine DEM normalizzata
  - slope
  - curvatura
- BLU - curvatura

![samgis-dem-rgb](https://res.cloudinary.com/aletrinok/image/upload/v1701206785/samgis/samgis-rgb2-1.png)

Quest'immagine RGB infine sarà sottoposta a Segment Anything per l'inferenza. La request sarà fatta usando questo payload in formato json:

```json
{
   "bbox": {
         "ne": {"lat": 46.203706648934954, "lng": 9.401906739170105},
         "sw": {"lat": 46.12464369105346, "lng": 9.231103669101747}},
   "prompt": [
         {"id": 238, "type": "point", "data": {"lat": 46.158760519552146, "lng": 9.306795638666486}, "label": 1},
         {"id": 250, "type": "point", "data": {"lat": 46.152693546080975, "lng": 9.289288652508679}, "label": 0},
         {"id": 264, "type": "point", "data": {"lat": 46.16613515509541, "lng": 9.319840059725284}, "label": 0},
         {"id": 272, "type": "point", "data": {"lat": 46.161377438862836, "lng": 9.288087192674299}, "label": 0},
         {"id": 285, "type": "point", "data": {"lat": 46.174103408003454, "lng": 9.308340372739261}, "label": 0},
         {"id": 293, "type": "point", "data": {"lat": 46.17005996124035, "lng": 9.298042145587583}, "label": 0}
   ],
   "zoom": 13,
   "source_type": "nextzen.terrarium"
}
```

## Architettura del progetto (vecchia demo su AWS)

![project-architecture](https://res.cloudinary.com/aletrinok/image/upload/v1701206785/samgis/photo1.png)

1. L'utente interagisce con una WebMap (nel mio esempio è una mappa leaflet che utilizza i dati cartografici di OpenStreetMap).
2. L'utente invia una POST ad un'[API](https://docs.ml-trinca.tornidor.com/openapi) utilizzando
    1. le coordinate estreme della vista corrente della mappa
    2. lo zoom corrente della mappa
    3. un prompt contenente
      - un marker "inclusivo", marcando quindi la posizione come inclusa (a couple of float - latitude and longitude)
      - un marker "escludente", marcando quindi la posizione come esclusa (a couple of float - latitude and longitude)
      - a "include" rectangle (a couple of marker positions - north-east and south-west)
3. a causa del vincoli posti dai CORS è difficile inviare direttamente richieste a risorse remote su domini diversi da quello di origine: per evitare questo problema ho preparato una semplice funzione proxy su Cloudflare (questo sito funziona con le Cloudflare Pages) che inoltra la richiesta dell'utente all'API remota
4. Il API Gateway inoltra la richiesta al backend serverless
5. La API Lambda serverless elabora la richiesta:
    1. analizza la richiesta e in particolare traduce il prompt con le coordinate di latitudine/longitudine in uno di coordinate x/y mappate sull'origine dell'immagine
    2. scarica le _tiles_ georeferenziate per poi unirle e ritagliarle
    3. crea un'istanza del modello [MobileSam](https://github.com/ChaoningZhang/MobileSAM) se non esistente utilizzando OnnxRuntime come runtime
    4. prepara il [_embedding_ delle immagini](https://datasciencedojo.com/blog/embeddings-and-llm/#) e...
    5. ...avvia il processo di inferenza
    6. trasforma il array di maschere di oggetti riconosciuti in un'unica immagine binaria quindi [vettorializza](https://gisgeography.com/rasterization-vettorializzazione/) la maschera binaria riconosciuta:
       1. innanzitutto converte la maschera in un GeoDataframe GeoPandas utilizzando [`rasterio.features.shapes`](https://rasterio.readthedocs.io/en/stable/api/rasterio.features.html#rasterio.features.shapes) e [`GeoDataFrame.from_features`](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.from_features.html)
       2. quindi esporta il nuovo Geodataframe in un geojson utilizzando [`GeoDataFrame.to_json`](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.to_json.html)
6. la funzione proxy Cloudflare elabora la risposta e la inoltra al frontend
7. il frontend estrae dalla risposta il geojson che sarà poi caricato all'interno della WebMap. Sono anche aggiunte alcune informazioni come la [durata non relativistica della risposta](https://xkcd.com/2867/)

   ![datetime-diff](https://imgs.xkcd.com/comics/datetime.png)

Per evitare abusi la [webmap dedicata a questo progetto sul mio sito personale](https://ml-trinca.tornidor.com/prediction-map) è sotto autenticazione (utilizzo [auth0.com](https://auth0.com)).

## Architettura della demo basata su Fastapi

Questa variante del progetto funziona in maniera abbastanza simile a [questa](https://trinca.tornidor.com/it/projects/samgis-segment-anything-applied-to-GIS#architettura-della-demo-aws). Queste le differenze principali:

- `opencv-python` non è più fra le dipendenze di SamGIS dalla versione 1.5.1
- il backend salva e riutilizza gli _embedding_ delle immagini per evitare sprechi di potenza di calcolo e velocizzare il processo di inferenza
- il frontend SPA (Single Page Application) è esposto direttamente dall'applicazione Fastapi

Frontend e backend quindi risiedono sullo stesso dominio. Non servirà più quindi un proxy (la demo su AWS usava una [Page function di Cloudflare](https://developers.cloudflare.com/pages/functions/)) per fare richieste ad API su un dominio diverso.

Inoltre, posso mantenere [online questa demo](https://huggingface.co/spaces/aletrn/samgis) usando [HuggingFace gratuitamente](https://huggingface.co/pricing#hub), rimuovendo la necessità di us sistema di autenticazione.

## AWS lambda, una soluzione serverless: alcuni pro

- Può funzionare anche con immagini docker, molto utile nel caso di progetti complessi
- Integrazione abbastanza semplice con altri servizi AWS e sistemi di autenticazione di terze parti
- Servizio a consumo

## ...e alcuni svantaggi

- Configurazione non sempre facile (ho utilizzato [Powertools for AWS Lambda (Python)](https://docs.powertools.aws.dev/lambda/python/latest/) per migliorare la configurazione del mio logger)
- Il modello serverless suggerito da AWS [non sempre è la soluzione migliore](https://offbynone.io/issues/233/)
- Le istanze Arm graviton EC2 che eseguono AWS lambda sono notevolmente limitate ([OnnxRuntime al momento non può essere eseguito su AWS lambda Arm e probabilmente non verrà mai eseguito](https://github.com/microsoft/onnxruntime/issues/10038)).