Closed edigiacomo closed 1 year ago
Se si può fare con chiamate a gdal_*
si dovrebbe poter fare anche con python-gdal senza chiamare programmi esterni.
Abbiamo comunque come precondizione l'avere delle ricette che creano immagini georeferenziabili, senza bordini o legende all'interno. Immagino questa issue debba seguire #45
Di seguito, un esempio di come è possibile fare il clip di un PNG usando uno shapefile senza usare file intermedi.
from osgeo import gdal, osr
# bounding box del PNG
bbox = [7, 43, 14, 46.5]
# dimensioni in pixel
size = [600, 422]
# Apro il file PNG e lo georeferenzio
ds_png = gdal.Open("input.png")
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
ds_png.SetProjection(srs.ExportToWkt())
ds_png.SetGeoTransform([
bbox[0],
(bbox[2]-bbox[0])/size[0],
0,
bbox[3],
0,
(bbox[1]-bbox[3])/size[1],
])
# Faccio il clip usando uno shapefile
gdal.Warp(
"clip.png",
ds_png,
cutlineDSName="er.geojson",
format="png"
)
Questa soluzione può essere applicata anche a #91 - che si era detto di considerare un caso particolare di questa issue.
Dimenticavo i file con cui far girare l'esempio files.zip
Ho iniziato il lavoro nel branch issue74
. Apro alcuni elementi di discussione, intanto che continuo a sperimentare:
Lo script di esempio ha hardcoded la georeferenziazione del PNG in input, perché i file PNG generati da Magics non sono georeferenziati.
Dai parametri di add_basemap possiamo tirar fuori subpage_map_projection
e il bounding box dell'immagine, e sperare che siano sufficienti. Noto che lo script di esempio usa EPSG:4326
(WGS84), mentre emro_web e gli output tiled usano EPSG:3857
(web mercator), e intuisco che ci sarà da fare abbastanza attenzione a questo passaggio.
Caricare il geojson una volta per ogni immagine generata mi sembra inefficiente, e mi piacerebbe poterlo caricare, convertire nel sistema di coordinate dell'immagine da ritagliare, e riusare per tutte le immagini di un batch.
Non mi sono ancora chiari tutti i passaggi per farlo, ma ci si può arrivare.
Una volta ottenuta la geometria buona per il ritaglio, c'è da capire come passarla a gdal.Warp
. In alternativa, cosa che forse può essere piú efficiente, si può sare gdal.Rasterize, una volta capito come.
Alcuni link:
Gli step attuali sono tutti step di Magics, e l'immagine PNG vera e propria viene creata dopo che tutti gli step sono stati eseguiti da Magics. Come specifichiamo gli step di ritaglio?
Alcune opzioni:
Il seguente codice permette di fare il clipping del PNG "georeferenziabile" generato da arkimaps usando uno shapefile qualsiasi caricato in memoria e senza appoggiarsi a file temporanei salvati su disco.
Allego lo zip che contiene codice e file di input (PNG e GeoJSON) prototipo_issue74.zip, ma incollo anche qui sotto lo script nel caso in cui si volessero fare eventuali commenti a qualche riga.
Rispondo anche a qualche considerazione fatta:
Dai parametri di add_basemap possiamo tirar fuori
subpage_map_projection
e il bounding box dell'immagine, e sperare che siano sufficienti. Noto che lo script di esempio usaEPSG:4326
(WGS84), mentre emro_web e gli output tiled usanoEPSG:3857
(web mercator), e intuisco che ci sarà da fare abbastanza attenzione a questo passaggio.
Nello script ho messo dei parametri di input che dovrebbero essere passati dalla ricetta. Nel caso della proiezione, si dovrà fare probabilmente un mapping tra alcune proiezioni "prosaiche" e l'EPSG corrispondente: sono comunque un numero limitato (26, si veda https://confluence.ecmwf.int/display/MAGP/Subpage+-+Projection) e non è detto che serva supportare tutte. Nel caso di 3857, abbiamo subpage_map_projection = EPSG:3857
, quindi è facile :)
Caricare il geojson una volta per ogni immagine generata mi sembra inefficiente, e mi piacerebbe poterlo caricare, convertire nel sistema di coordinate dell'immagine da ritagliare, e riusare per tutte le immagini di un batch.
Nello script ho usato gdal.Rasterize
(come suggerito da @spanezz) che prende un dataset vettoriale, che quindi non deve essere riletto tutte le volte da file. Non serve nemmeno convertirlo nel CRS dell'immagine da ritagliare - anche se forse potrebbe aver senso farlo per non delegarlo a ogni chiamata di gdal.Rasterize
- ma lo terrei come ottimizzazione futura.
PNG input:
PNG output:
#!/usr/bin/python3
import osgeo
from osgeo import gdal, osr
# PARAMETRI FORNITI DALLA RICETTA
# bounding box del PNG in EPSG:4326 nel formato [LONMIN, LATMIN, LONMAX, LATMAX]
BBOX = [9.1951463, 43.7133281, 12.8283720, 45.1425750]
# dimensioni in pixel del PNF
SIZE = [1280, 705]
# epsg del PNG
EPSG = 3857
# FINE PARAMETRI FORNITI DALLA RICETTA
def convert_magics_bbox_to_epsg(bbox: tuple[float, float, float], epsg_out: int) -> tuple[float, float, float, float]:
"""Metodo per la conversione del bounding box di Magics (in EPSG:4326 e nella forma [LONMIN, LATMIN, LONMAX,
LATMAX])."""
srs_src = osr.SpatialReference()
srs_src.ImportFromEPSG(4326)
srs_dst = osr.SpatialReference()
srs_dst.ImportFromEPSG(epsg_out)
transform = osr.CoordinateTransformation(srs_src, srs_dst)
# NOTA: in GDAL < 3 non viene rispettato l'ordine degli assi e viene sempre usato l'ordine lon,lat. Vedi
# https://gdal.org/tutorials/osr_api_tut.html#crs-and-axis-order EPSG:4326 usa invece l'ordine lat,lon Se tutte le
# operazioni successive con il bbox risultante sono delegate a GDAL (e.g. SetGeoTransform) non importa più l'ordine
# in quanto è consistente con la versione di GDAL stessa.
if osgeo.gdal_version[0] < 3:
lr = transform.TransformPoint(bbox[0], bbox[1])
ul = transform.TransformPoint(bbox[2], bbox[3])
else:
lr = transform.TransformPoint(bbox[1], bbox[0])
ul = transform.TransformPoint(bbox[3], bbox[2])
return [lr[0], lr[1], ul[0], ul[1]]
# Converto il bounding box della ricetta nella proiezione del raster
bbox = convert_magics_bbox_to_epsg(BBOX, EPSG)
# Apro il file PNG e lo georeferenzio
ds_png = gdal.Open("input.png")
srs = osr.SpatialReference()
srs.ImportFromEPSG(EPSG)
ds_png.SetProjection(srs.ExportToWkt())
ds_png.SetGeoTransform([
bbox[0],
(bbox[2]-bbox[0])/SIZE[0],
0,
bbox[3],
0,
(bbox[1]-bbox[3])/SIZE[1],
])
# Converto il PNG in un GeoTIFF in memoria usando il virtual FS di GDAL
# https://gdal.org/user/virtual_file_systems.html#vsimem-in-memory-files
# NOTA: si potrebbe anche usare il driver MEM e copiare a mano tutti i metadati e dati, ma non offre vantaggi se non
# quello di non dover fare l'unlink del file virtuale associato (vedi in fondo).
# NOTA: il file è visibile all'interno del processo, quindi in caso di multiprocesso non è un problema, ma nel caso di
# multithreading si deve generare un nome univoco (e.g. con threading.get_ident())
tif_path = "/vsimem/input.tif"
ds_tif = gdal.Translate(tif_path, ds_png, format="GTiff")
# Apro il file vettoriale
# NOTA: ogr.Open non va bene perché gdal.Rasterize vuole un tipo GDALDatasetShadow
# NOTA: Non serve riproiettare il vettoriale (che è in EPSG:32632, diversa dal raster)
shape = gdal.OpenEx("er.geojson", gdal.OF_VECTOR)
# Ritaglio il GeoTIFF, scrivendo su tutte le bande
gdal.Rasterize(
ds_tif,
shape,
inverse=True,
bands=list(range(1, ds_tif.RasterCount + 1)),
burnValues=(0,) * ds_tif.RasterCount,
)
# Salvo il GeoTiff (alla fine di tutti i postprocessing)
gdal.Translate("output.png", ds_tif)
# NOTA: il file un memoria va chiuso manualmente. Si potrebbe fare una classe RAII
gdal.Unlink(tif_path)
Il seguente codice permette di fare il clipping del PNG "georeferenziabile" generato da arkimaps usando uno shapefile qualsiasi caricato in memoria e senza appoggiarsi a file temporanei salvati su disco.
Dimenticavo: il ritaglio ha senso solo se il PNG è "georeferenziabile", quindi nel caso di tile e con flavour come emro_web
(https://github.com/ARPA-SIMC/arkimaps/blob/v1.4-2/recipes/flavours/emro.yaml#L41). Negli altri casi non ha senso.
È facile identificare il flavour "tile" perché c'è il campo tile
nella ricetta, ma questo non è vero nel caso di una singola immagine PNG georeferenziabile (come nel caso del flavour emro_web
), perché il risultato è dato da una particolare configurazione dei parametri Magics. Si potrebbe far diventare questo flavour un "first class citizen" come i tile, estraendo la parte opportuna dalla ricetta e cablandola nel codice di arkimaps, analogamente ai tile.
Intanto ho implementato una feature non richiesta come proof of concept di postprocessatori:
class Watermark(Postprocessor):
"""
Write a string on the image.
Parameters:
* ``message``: text string to write
* ``font``: name of a .ttf file to use as a font. The .ttf file needs to be
found inside the static data directory
* ``x``: horizontal coordinates (in pixel) of the beginning of the text. A
negative value is the number of pixels from the right margin of the image
* ``y``: vertical coordinates (in pixel) of the beginning of the text. A
negative value is the number of pixels from the bottom margin of the image
* ``anchor``: The text anchor alignment. See
https://pillow.readthedocs.io/en/stable/handbook/text-anchors.html#text-anchors
* ``size``: font size in pixels (default: 10)
* ``color``: color name as defined in
https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#color-names
Default: "#fff0"
"""
Ho fatto push della prima implementazione del postprocessing cutshape (branch issue74):
- name: emro_web
postprocess:
- type: cutshape
shapefile: "er.geojson"
Non è un gran ché ottimizzata, però sembra funzionare.
Alcune note:
er.geojson
nel repository perché è enorme. Può essere forse piú efficiente convertirlo in shapefile e aggiungere quello?E la nota piú importante di tutte: complimenti per il codice del prototipo di ritaglio :heart: Ho notato e apprezzato tante bellissime accortezze per renderlo, generico, succinto, e facile da integrare. È stato un piacere lavorarci!
Grazie @spanezz, è un piacere sapere che è stato un piacere :)
- non ho fatto push di
er.geojson
nel repository perché è enorme. Può essere forse piú efficiente convertirlo in shapefile e aggiungere quello?
Ho convertito il file da GeoJSON a Shapefile, lo trovi qui: er.zip. La dimensione passa da ~1.2MB a ~460K
- funziona solo quando in add_basemap si usano proiezioni epsg, altrimenti logga un warning e non fa nulla
Per me va benissimo, se poi dobbiamo supportare altre proiezioni apriremo issue.
Grazie ancora!
Perfetto, non solo prende meno spazio ma fa anche molto prima a caricarlo.
Ho fatto push del caricamento degli shape solo una volta per render script.
In recipes/flavours/emro.yaml
ci sono postprocessing di esempio definiti: va bene finché il branch è un prototipo, ma vanno probabilmente tolti/sistemati con quello che effettivamente serve prima di un merge in main.
In particolare il flavour emro
al momento non funziona perché richiede di mettere manualmente LiberationSans-Regular.ttf
in recipes/static
(non volevo aggiungere un ttf alla storia del repository solo per fare delle prove)
Passo a voi per verifica
Ho fatto delle prove con emro_web
(commentando il postprocessing di emro
) e direi che il ritaglio funziona perfettamente!
Per me è ok, puoi rimuovere il postprocess da emro
e lasciare quello di emro_web
.
Ho sistemato i postprocessatori nei due flavour e il nome dello shapefile. Considero la issue risolta e faccio merge nel master.
Alcuni output richiedono il ritaglio della mappa mediante shapefile, ad esempio:
È una operazione che, a partire dal PNG generato da Magics, può essere effettuata con i seguenti comandi:
Volendo, si può valutare di non implementare questa funzionalità in arkimaps ma di usare i suddetti comandi lato client.
Questa operazione ha senso (o quantomeno per come l'ho implementata io) solo con output georeferenziato (issue collegata #45).