Visualizzazione post con etichetta Emit. Mostra tutti i post
Visualizzazione post con etichetta Emit. Mostra tutti i post

martedì 20 ottobre 2026

Ricerca immagini Emit (2)

Versione aggiornata che crea anche una preview 

 

 


 

import csv
import json
import re
from pathlib import Path

import earthaccess
import geopandas as gpd
from shapely.geometry import Polygon, box
from shapely.ops import unary_union

# ---------- parametri ----------
VERSION = "002"
CLOUD_MAX = 10
TEMPORAL = ("2022-08-01", "2026-10-18")
MIN_LAND = 0.20
MAX_OVERLAP = 0.20
OUT_DIR = Path("./emit_iberia")
DRY_RUN = False

# Regioni di ricerca (lon_min, lat_min, lon_max, lat_max).
# La penisola iberica include anche le Baleari; le isole atlantiche sono opzionali.
REGIONI = {
"penisola_baleari": (-9.6, 35.95, 4.5, 43.9), # Spagna + Portogallo continentali + Baleari
}
INCLUDI_CANARIE = False # Spagna: Canarie
INCLUDI_MADEIRA = False # Portogallo: Madeira
INCLUDI_AZZORRE = False # Portogallo: Azzorre
if INCLUDI_CANARIE:
REGIONI["canarie"] = (-18.4, 27.5, -13.3, 29.5)
if INCLUDI_MADEIRA:
REGIONI["madeira"] = (-17.4, 32.35, -16.2, 33.2)
if INCLUDI_AZZORRE:
REGIONI["azzorre"] = (-31.4, 36.8, -24.9, 39.8)

GET_RFL = True # EMIT_L2A_RFL
GET_MASK = False # EMIT_L2A_MASK
GET_UNCERT = False # EMIT_L2A_RFLUNCERT
GET_OBS = False # EMIT_L1B_OBS (dal prodotto EMITL1BRAD)
GET_RAD = False # EMIT_L1B_RAD (file grande, di solito non serve)
GET_BROWSE = True # anteprima PNG
GET_META = True # JSON UMM-G + CSV riassuntivo

# ---------- contorni Spagna e Portogallo ----------
# Il poligono Natural Earth di ciascun paese comprende anche i territori lontani (Canarie, Azzorre,
# Madeira, Ceuta, Melilla): li teniamo solo se ricadono in una delle REGIONI scelte.
URL = "https://naturalearth.s3.amazonaws.com/10m_cultural/ne_10m_admin_0_countries.zip"
world = gpd.read_file(URL)
area = unary_union([box(*b) for b in REGIONI.values()])

PAESI = {"ESP": "spagna", "PRT": "portogallo"}
paesi = {}
for a3 in PAESI:
geom = (world[world["ADM0_A3"] == a3]
.to_crs(4326)
.union_all()
.intersection(area)
.buffer(0.05)) # margine per i granuli costieri
assert not geom.is_empty, f"nessuna geometria trovata per {a3}"
paesi[a3] = geom
iberia = unary_union(list(paesi.values()))

earthaccess.login(strategy="netrc") # oppure login() / login(persist=True)

# ---------- ricerca (una per regione, poi unione senza duplicati) ----------
def search_all(short_name, **kw):
found = {}
for nome, bbox in REGIONI.items():
res = earthaccess.search_data(
short_name=short_name,
version=VERSION,
bounding_box=bbox,
temporal=TEMPORAL,
count=-1,
**kw,
)
print(f" {short_name} [{nome}]: {len(res)} granuli")
for g in res:
found[g["umm"]["GranuleUR"]] = g
return list(found.values())

l2a = search_all("EMITL2ARFL", cloud_cover=(0, CLOUD_MAX))
print(f"{len(l2a)} granuli L2A nelle regioni scelte")

def browse_links(g):
d = g.dataviz_links
d = d() if callable(d) else d # proprietà o metodo, a seconda della versione
return [u for u in (d or []) if u.endswith(".png")]

def footprint(g):
pts = (g["umm"]["SpatialExtent"]["HorizontalSpatialDomain"]
["Geometry"]["GPolygons"][0]["Boundary"]["Points"])
return Polygon([(p["Longitude"], p["Latitude"]) for p in pts])

def cloud(g):
return float(g["umm"].get("CloudCover", 100))

def stamp(g):
"""Timestamp di acquisizione YYYYMMDDTHHMMSS dal GranuleUR."""
m = re.search(r"(\d{8}T\d{6})", g["umm"]["GranuleUR"])
return m.group(1) if m else None

# ---------- filtro terra + greedy ----------
cands = []
for g in l2a:
poly = footprint(g)
land = poly.intersection(iberia)
if land.area / poly.area >= MIN_LAND:
cands.append((g, land, land.area / poly.area))
print(f"{len(cands)} granuli con almeno {MIN_LAND:.0%} su terra spagnola o portoghese")

cands.sort(key=lambda x: cloud(x[0]))
selected, covered = [], None
for g, land, frac in cands:
overlap = 0.0 if covered is None else land.intersection(covered).area / land.area
if overlap <= MAX_OVERLAP:
selected.append((g, frac))
covered = land if covered is None else unary_union([covered, land])

print(f"{len(selected)} granuli selezionati")
if covered is not None:
print(f"copertura Spagna + Portogallo: {covered.intersection(iberia).area / iberia.area:.1%}")
for a3, nome in PAESI.items():
print(f" {nome}: {covered.intersection(paesi[a3]).area / paesi[a3].area:.1%}")

# ---------- ricerca L1B (RAD + OBS) e abbinamento per timestamp ----------
l1b_by_stamp = {}
if GET_OBS or GET_RAD:
l1b = search_all("EMITL1BRAD")
print(f"{len(l1b)} granuli L1B nelle regioni scelte")
l1b_by_stamp = {stamp(g): g for g in l1b if stamp(g)}

# ---------- costruzione lista file ----------
def pick(links, key):
return [u for u in links if key in u.split("/")[-1] and u.endswith(".nc")]

links, rows, missing_l1b = [], [], []
for g, frac in selected:
ts = stamp(g)
fp = footprint(g)
l2a_links = g.data_links()

if GET_RFL: links += pick(l2a_links, "_RFL_")
if GET_MASK: links += pick(l2a_links, "_MASK_")
if GET_UNCERT: links += pick(l2a_links, "RFLUNCERT")
if GET_BROWSE: links += browse_links(g)

g1 = l1b_by_stamp.get(ts)
if g1 is None:
if GET_OBS or GET_RAD:
missing_l1b.append(g["umm"]["GranuleUR"])
else:
l1b_links = g1.data_links()
if GET_OBS: links += pick(l1b_links, "_OBS_")
if GET_RAD: links += pick(l1b_links, "_RAD_")

rows.append({
"granule_ur": g["umm"]["GranuleUR"],
"timestamp": ts,
"begin": g["umm"]["TemporalExtent"]["RangeDateTime"]["BeginningDateTime"],
"end": g["umm"]["TemporalExtent"]["RangeDateTime"].get("EndingDateTime", ""),
"cloud_pct": cloud(g),
"land_fraction": round(frac, 3),
"land_spagna": round(fp.intersection(paesi["ESP"]).area / fp.area, 3),
"land_portogallo": round(fp.intersection(paesi["PRT"]).area / fp.area, 3),
"l1b_ur": g1["umm"]["GranuleUR"] if g1 else "",
"footprint_wkt": fp.wkt,
})

links = list(dict.fromkeys(links)) # elimina duplicati mantenendo l'ordine

print(f"\n{len(links)} file da scaricare")
for r in rows:
print(r["granule_ur"], f"cloud={r['cloud_pct']:.1f}%",
f"ES={r['land_spagna']:.0%} PT={r['land_portogallo']:.0%}",
"| L1B:", r["l1b_ur"] or "NON TROVATO")
if missing_l1b:
print(f"\nATTENZIONE: nessun L1B abbinato per {len(missing_l1b)} granuli")

try:
tot = sum(g.size() for g, _ in selected)
print(f"\nDimensione L2A (indicativa, tutti i file): {tot/1024:.1f} GB")
except Exception:
pass

# ---------- metadati ----------
if GET_META and not DRY_RUN and rows:
meta_dir = OUT_DIR / "metadata"
meta_dir.mkdir(parents=True, exist_ok=True)

for g, _ in selected:
ur = g["umm"]["GranuleUR"]
(meta_dir / f"{ur}.umm.json").write_text(
json.dumps({"umm": g["umm"], "meta": g["meta"]}, indent=2))
g1 = l1b_by_stamp.get(stamp(g))
if g1 is not None:
(meta_dir / f"{g1['umm']['GranuleUR']}.umm.json").write_text(
json.dumps({"umm": g1["umm"], "meta": g1["meta"]}, indent=2))

with open(OUT_DIR / "granules_summary.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)

# ---------- download ----------
if not DRY_RUN:
OUT_DIR.mkdir(parents=True, exist_ok=True)
earthaccess.download(links, str(OUT_DIR))


 

lunedì 19 ottobre 2026

Ricerca immagini Emit

Un programma per scaricare in automatico immagini Emit L2A da EarthData minizzando l'overlap, selezionando almeno una percentuale minima di terra ed una copertura nuvola 


Da notare che Emit vola su ISS e quindi non e' eliosincrona

 

import earthaccess
import geopandas as gpd
from shapely.geometry import Polygon
from shapely.ops import unary_union

# ---------- parametri ----------
SHORT_NAME = "EMITL2ARFL"
VERSION = "002"
CLOUD_MAX = 5 # % nuvolosità massima del granulo
TEMPORAL = ("2022-08-01", "2026-10-18")
MIN_LAND = 0.20 # frazione minima del granulo su terra italiana
MAX_OVERLAP = 0.10 # max sovrapposizione con l'area già coperta
OUT_DIR = "./emit_italia"
DRY_RUN = False # True = solo elenco, senza scaricare, False scaricare le immagini

# ---------- contorno Italia ----------
URL = "https://naturalearth.s3.amazonaws.com/10m_cultural/ne_10m_admin_0_countries.zip"
world = gpd.read_file(URL)
italia = (world[world["ADM0_A3"] == "ITA"]
.to_crs(4326)
.union_all()
.buffer(0.05))

# ---------- ricerca ----------
earthaccess.login(persist="True")
granules = earthaccess.search_data(
short_name=SHORT_NAME,
version=VERSION,
bounding_box=(6.6, 35.5, 18.6, 47.1),
cloud_cover=(0, CLOUD_MAX),
temporal=TEMPORAL,
count=-1,
)
print(f"{len(granules)} granuli trovati nel box (versione {VERSION})")

def footprint(g):
pts = (g["umm"]["SpatialExtent"]["HorizontalSpatialDomain"]
["Geometry"]["GPolygons"][0]["Boundary"]["Points"])
return Polygon([(p["Longitude"], p["Latitude"]) for p in pts])

def cloud(g):
return float(g["umm"].get("CloudCover", 100))

# ---------- filtro: abbastanza terra italiana ----------
cands = []
for g in granules:
poly = footprint(g)
land = poly.intersection(italia)
if land.area / poly.area >= MIN_LAND:
cands.append((g, land))
print(f"{len(cands)} granuli con almeno {MIN_LAND:.0%} su terra italiana")

# ---------- greedy anti-sovrapposizione (solo parte terrestre) ----------
cands.sort(key=lambda x: cloud(x[0]))
selected, covered = [], None
for g, land in cands:
overlap = 0.0 if covered is None else land.intersection(covered).area / land.area
if overlap <= MAX_OVERLAP:
selected.append(g)
covered = land if covered is None else unary_union([covered, land])

print(f"{len(selected)} granuli selezionati")
if covered is not None:
print(f"copertura dell'Italia: {covered.intersection(italia).area / italia.area:.1%}")

for g in selected:
print(g["umm"]["GranuleUR"], f"cloud={cloud(g):.1f}%")

# ---------- link da scaricare (senza RFLUNCERT) ----------
links = []
for g in selected:
links += [u for u in g.data_links() if "RFLUNCERT" not in u]

print(f"{len(links)} file da scaricare")
for u in links[:6]:
print(" ", u.split("/")[-1])

# ---------- download ----------
if not DRY_RUN:
earthaccess.download(links, OUT_DIR)


 

 

 

165 granuli trovati nel box (versione 002)
52 granuli con almeno 20% su terra italiana
22 granuli selezionati
copertura dell'Italia: 31.7%
EMIT_L2A_RFL_002_20230204T101735 cloud=0.0%
 

lunedì 21 settembre 2026

USGS Tetracorder Docker

Non esistono metodi banali per installare Tetracorder (versione moderna) ma una versione comoda e' costituita da un docker non ufficiale

 


git clone https://github.com/jl-0/tetracorder-lite.git

cd tetracorder-lite

 prima di fare la build si deve modificare il file Containerfile aggiungendo la riga evidenziata in giallo

RUN cd tetracorder &&\
    sed -i "398,416 s/^/#/" AAA.INSTALL.spectroscopy-os-setup-linux.sh &&\
    sed -i "231,254 s/^/#/" AAA.INSTALL.spectroscopy-os-setup-linux.sh &&\
    yes "y" | bash AAA.INSTALL.spectroscopy-os-setup-linux.sh install &&\
    cd tetracorder &&\
    chmod +x checkmultmapcompilemode &&\
    make install &&\
    sed -i "137,140 s/^/#/" multmap.h &&\
    sed -i "144,147 s/^#//" multmap.h &&\
    make installsingle 

a questo punto si fa la build

docker build --platform linux/amd64 -f Containerfile -t tetracorder-lite . 

e si creano dei folder di lavoro

mkdir -p ~/tetracorder/input ~/tetracorder/output 

per controllare che tutto sia a posto si puo' lanciare 

docker run --rm tetracorder-lite tetrapy --help
docker run --rm tetracorder-lite tetrapy run --help

a questo punto siamo pronti a lanciare una prima elaborazione.Il problema e' che  Tetracorder accetta immagini in formato Envi mentre le immagini Emit sono distribuite in formato NetCDF (si devono scarivare sia i dati RFL di riflettanza che RFLUNCERT dell'incertezza della riflettanza

Per la conversione si possono le emit utilities 

git clone https://github.com/nasa/emit-utils.git
cd emit-utils
python emit_utils/reformat.py /path/to/EMIT_L2A_RFL_xxx.nc /path/to/output_envi/ --orthorectify 

funzionano ma non sono riuscito ad installare su Debian pip install xarray netCDF4 rioxarray earthaccess ovvero i modulo per orthorectify...si puo' fare a meno per questa prova

cd ~/emit-utils
python -m emit_utils.reformat ./emit_utils/EMIT_L2A_RFL_001_20260825T085423_2623706_045.nc /home/luca/tetracorder/input/ --orthorectify

arrivati a questo punto si puo' lanciare il processing  

docker run --rm \
  -v ~/tetracorder/input:/data \
  -v ~/tetracorder/output:/output \
  tetracorder-lite \
  tetrapy run config.yml \
    --data.rfl /data/EMIT_L2A_RFL_001_20260825T085423_2623706_045_reflectance \
    --data.rfluncert /data/EMIT_L2A_RFLUNCERT_001_20260825T085423_2623706_045_reflectance_uncertainty 

 

al termine del processing avremo una serie di file tipo la seguente dove

  • group.1um, group.1.5um-broad, group.2um, group.2um-broad — minerali (argille, carbonati, solfati, ecc., a seconda della banda di assorbimento)
  • group.ree, group.ree_g21, group.ree_samar — terre rare (rare earth elements)
  • group.ch4gas-2.3um, group.co2gas-2um — gas (metano, CO2)
  • group.veg — vegetazione
  • case.ep-cal-chl — epidoto/calcite/clorite
  • case.red-edge — red edge della vegetazione
  • case.veg.type — tipologia di vegetazione
  •   

    .
    ./test
    ./test/aggregate
    ./test/aggregate/agg.nc
    ./test/aggregate/config.yml
    ./test/aggregate/agg-uncert.nc
    ./test/aggregate/tetrapy.log
    ./test/tetracorder
    ./test/tetracorder/AAAA.license.txt
    ./test/tetracorder/group.1.5um-broad
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_vbroad_br20.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.d.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm+0.3water-273k.depth.gz
    ./test/tetracorder/group.1.5um-broad/vegetation1.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.2water-273k.depth.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll2.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo18_ki3377.fd.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.1water-273k.fit.gz
    ./test/tetracorder/group.1.5um-broad/water+mont300gpl.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.2water-273k.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo60_ki3189.fit.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-300cm-path+chlrph-g.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont0.5gpl.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.melting16+0.5veg.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/anorthite_hs201.3b.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-10cm-path-model.depth.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path-model.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.8.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm-273k.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow.slush.9.depth.gz
    ./test/tetracorder/group.1.5um-broad/water+mont0.5gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo66_ki3054.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo60_ki3189.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br22c_actinolite.fit.gz
    ./test/tetracorder/group.1.5um-broad/vegetation1.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl+0.5veg.fit.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-30cm-path-model.fit.gz
    ./test/tetracorder/group.1.5um-broad/anorthite_hs201.3b.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.3water-273k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.1water-273k.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/rutile_hs126.3b.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.1water-273k.fd.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-30cm-path-model.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting16+0.5veg.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_nrw.cummingtonite.fd.gz
    ./test/tetracorder/group.1.5um-broad/vegetation.dry+green.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-300cm-path+chlrph-g.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo66_ki3054.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-1mm-273k.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont16.5gpl.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlorite.clinochlor.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm-273k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_vbroad_br20.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-g.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.3water-273k.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/microcline_hs103.3b.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-1mm-273k.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.slush.9.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/bytownite_hs106.3b.depth.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-10cm-path+chlrph-g.depth.gz
    ./test/tetracorder/group.1.5um-broad/organic_dry_long_grass-thick.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-30cm-path-model.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl+0.5veg.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/vegetation.dry+green.fd.gz
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.fd.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlor+muscphy.fit.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll2.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.1water-273k.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/bytownite_hs106.3b.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/sphalerite_s26-341_1.5um.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/anorthite_hs201.3b.fit.gz
    ./test/tetracorder/group.1.5um-broad/water+mont5.01gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.1a.depth.gz
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm+0.3water-273k.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.8.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo18_ki3377.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.slush.9.fit.gz
    ./test/tetracorder/group.1.5um-broad/rutile_hs126.3b.depth.gz
    ./test/tetracorder/group.1.5um-broad/vegetation.dry+green.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/sphalerite_s26-341_1.5um.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.a.fit.gz
    ./test/tetracorder/group.1.5um-broad/anorthite_hs201.3b.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.melting.3.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.1a.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont16.5gpl.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.3.fit.gz
    ./test/tetracorder/group.1.5um-broad/water+mont5.01gpl.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/organic_dry_long_grass-thick.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.3water-273k.fd.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br5a_actinolite.fit.gz
    ./test/tetracorder/group.1.5um-broad/rutile_hs126.3b.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll-blue1.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.3water-273k.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/organic_white_pvc_pipe.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlor+muscphy.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo91_gds71.b.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow.slush.16.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.d.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow.and.ice-170k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-g.fd.gz
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll-blue1.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow.slush.16.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.slush.16.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting9+0.5veg.fd.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo66_ki3054.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl+0.5veg.depth.gz
    ./test/tetracorder/group.1.5um-broad/sphalerite_s26-341_1.5um.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.melting9+0.5veg.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/organic_green_plastic_tarp.fit.gz
    ./test/tetracorder/group.1.5um-broad/vegetation.dry+green.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm+0.3water-273k.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm-273k.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.and.ice-248k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br5a_actinolite.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br5a_actinolite.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont16.5gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_vbroad_br20.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-30cm-path-model.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo18_ki3377.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/rutile_hs126.3b.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_nrw.cummingtonite.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_vbroad_br20.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.8.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo80_hs285.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.c.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.melting9+0.5veg.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont1.67gpl.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont5.01gpl.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.melting16+0.5veg.fit.gz
    ./test/tetracorder/group.1.5um-broad/water+mont1.67gpl.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.red.algae.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont1.67gpl.depth.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-g.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.2water-273k.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo66_ki3054.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont0.5gpl.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/magnetite_hs195.3b.or.olivine.fd.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-300cm-path+chlrph-g.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.1a.fd.gz
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll-blue1.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo18_ki3377.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo60_ki3189.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/magnetite_hs195.3b.or.olivine.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo18_ki3377.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl+0.5veg.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlorite.clinochlor.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_nrw.cummingtonite.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.slush.16.fd.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm+0.3water-273k.fd.gz
    ./test/tetracorder/group.1.5um-broad/organic_green_plastic_tarp.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont0.5gpl.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo91_gds71.b.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.and.ice-248k.depth.gz
    ./test/tetracorder/group.1.5um-broad/organic_green_plastic_tarp.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/bytownite_hs106.3b.fd.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo80_hs285.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll2.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-300cm-path+chlrph-g.depth.gz
    ./test/tetracorder/group.1.5um-broad/vegetation1.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_nrw.cummingtonite.fit.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo80_hs285.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.red.algae.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.slush.9.fd.gz
    ./test/tetracorder/group.1.5um-broad/water+mont1.67gpl.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.1water-273k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow.and.ice-170k.depth.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlor+muscphy.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/vegetation.dry+green.fit.gz
    ./test/tetracorder/group.1.5um-broad/water+mont300gpl.fit.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.2water-273k.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/sphalerite_s26-341_1.5um.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.8.depth.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlor+muscphy.fd.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br22c_actinolite.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont50gpl.fit.gz
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_chlor+muscphy.depth.gz
    ./test/tetracorder/group.1.5um-broad/bytownite_hs106.3b.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll.fd.gz
    ./test/tetracorder/group.1.5um-broad/magnetite_hs195.3b.or.olivine.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm-273k.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-10cm-path-model.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path-model.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm-273k.fit.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path-model.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.low.chlorophyll-turquoise.depth.gz
    ./test/tetracorder/group.1.5um-broad/water.red.algae.fd.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.d.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/snow-ice-3mm+0.3water-273k.depth.gz
    ./test/tetracorder/group.1.5um-broad/water+mont100gpl+0.5veg.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water.red.algae.fit.gz
    ./test/tetracorder/group.1.5um-broad/water.high.chlorophyll.depth.gz
    ./test/tetracorder/group.1.5um-broad/organic_white_pvc_pipe.fd.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.a.fd.gz
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.fd.gz
    ./test/tetracorder/group.1.5um-broad/snow-ice-10mm-273k.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_vbroad_br20.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.c.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.d.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/anorthite_hs201.3b.depth.gz
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.melting.1a.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br5a_actinolite.fd.gz
    ./test/tetracorder/group.1.5um-broad/olivine_fo91_gds71.b.depth.gz
    ./test/tetracorder/group.1.5um-broad/microcline_hs103.3b.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/organic_white_pvc_pipe.depth.gz
    ./test/tetracorder/group.1.5um-broad/rutile_hs126.3b.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water-20C-100cm-path+chlrph-a.depth.gz
    ./test/tetracorder/group.1.5um-broad/snow.and.ice-170k.fd.gz
    ./test/tetracorder/group.1.5um-broad/water.turquoise-shallow.fd.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.d.fit.gz.hdr
    ./test/tetracorder/group.1.5um-broad/g4-fe2+generic_brd.br22c_actinolite.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/olivine_fo89_gds70.c.depth.gz
    ./test/tetracorder/group.1.5um-broad/water+mont300gpl.depth.gz.hdr
    ./test/tetracorder/group.1.5um-broad/water+mont0.5gpl.depth.gz

     

    rimane il problema della visualizzazione  

    mkdir -p ~/tetracorder_out
    python tetracorder_iron_oxides.py \
        --group-dir group.2um \
        --filter "kaolgrp_|kaol|smectite_|micagrp_illite|micagrp_vermiculite|chlorite|palygorskite|sepiolite|saponite|hectorite|dickite|nacrite|halloysite|pyroph" \
        --output ~/tetracorder_out/argille_map.png \
        --geotiff ~/tetracorder_out/argille_map.tif \
        --title "Minerali argillosi" 

     

    #!/usr/bin/env python3
    """
    tetracorder_visualize.py
    =========================

    Visualizza una mappa di classificazione prodotta da USGS Tetracorder
    (es. results.cluster, results.cluster.sorted, results.group.X, ...)
    colorando ogni classe secondo il file color.keys prodotto dallo stesso run,
    e generando una legenda con i nomi dei materiali.

    USO
    ---
    python tetracorder_visualize.py \
    --cluster /path/results.cluster.sorted \
    --colorkeys /path/color.keys \
    --output mappa_materiali.png

    Se il file .hdr (ENVI) ha lo stesso nome + ".hdr" o si chiama
    "<cluster>.hdr", viene individuato automaticamente. In alternativa
    si può passare esplicitamente con --hdr.

    DIPENDENZE
    ----------
    pip install numpy matplotlib
    # opzionale ma consigliato (lettura ENVI con georeferenziazione):
    pip install rasterio

    NOTE SUL PARSING DI color.keys
    -------------------------------
    Il formato del file color.keys di Tetracorder non è uno standard fisso:
    varia leggermente tra versioni. Questo script prova diversi pattern
    comuni (vedi funzione `parse_color_keys`). Se il parsing fallisce o i
    colori/nomi non tornano, lancia lo script con --debug-colorkeys per
    vedere riga per riga cosa viene interpretato, e poi mandami un estratto
    delle prime ~10 righe del tuo file color.keys così affino il parser.
    """

    import argparse
    import re
    import sys
    from pathlib import Path

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.colors import ListedColormap, BoundaryNorm
    from matplotlib.patches import Patch


    # --------------------------------------------------------------------------
    # Lettura immagine ENVI (results.cluster, results.group.X, ecc.)
    # --------------------------------------------------------------------------

    def find_hdr(cluster_path: Path) -> Path:
    """Cerca l'header ENVI associato al file dati."""
    candidates = [
    cluster_path.with_suffix(cluster_path.suffix + ".hdr"),
    cluster_path.with_suffix(".hdr"),
    Path(str(cluster_path) + ".hdr"),
    ]
    for c in candidates:
    if c.exists():
    return c
    raise FileNotFoundError(
    f"Nessun file .hdr trovato per {cluster_path}. "
    f"Passalo esplicitamente con --hdr."
    )


    def parse_envi_hdr(hdr_path: Path) -> dict:
    """Parser minimale di un header ENVI (testo chiave=valore)."""
    text = hdr_path.read_text(errors="ignore")
    info = {}
    for key in ("samples", "lines", "bands", "data type",
    "interleave", "byte order", "header offset"):
    m = re.search(rf"{key}\s*=\s*(\S+)", text, re.IGNORECASE)
    if m:
    info[key.replace(" ", "_")] = m.group(1)
    required = ("samples", "lines", "data_type")
    missing = [k for k in required if k not in info]
    if missing:
    raise ValueError(f"Header ENVI incompleto, mancano: {missing}")
    return info


    ENVI_DTYPES = {
    "1": np.uint8, "2": np.int16, "3": np.int32, "4": np.float32,
    "5": np.float64, "6": np.complex64, "9": np.complex128,
    "12": np.uint16, "13": np.uint32, "14": np.int64, "15": np.uint64,
    }


    def read_envi_raw(data_path: Path, hdr: dict) -> np.ndarray:
    """Legge un raster ENVI senza dipendenze esterne (fallback)."""
    samples = int(hdr["samples"])
    lines = int(hdr["lines"])
    bands = int(hdr.get("bands", 1))
    dtype = ENVI_DTYPES.get(hdr.get("data_type", "2"), np.int16)
    offset = int(hdr.get("header_offset", 0))
    byte_order = hdr.get("byte_order", "0")
    interleave = hdr.get("interleave", "bsq").lower()

    arr = np.fromfile(data_path, dtype=dtype, offset=offset)
    expected = samples * lines * bands
    arr = arr[:expected]
    if byte_order == "1": # big-endian
    arr = arr.byteswap().newbyteorder()

    if interleave == "bsq":
    arr = arr.reshape(bands, lines, samples)
    elif interleave == "bil":
    arr = arr.reshape(lines, bands, samples).transpose(1, 0, 2)
    elif interleave == "bip":
    arr = arr.reshape(lines, samples, bands).transpose(2, 0, 1)
    else:
    raise ValueError(f"Interleave ENVI non riconosciuto: {interleave}")

    return arr[0] if bands == 1 else arr


    def load_cluster_map(cluster_path: Path, hdr_path: Path | None) -> np.ndarray:
    """Carica la mappa raster, usando rasterio se disponibile."""
    try:
    import rasterio
    with rasterio.open(cluster_path) as src:
    return src.read(1)
    except Exception:
    pass # fallback manuale

    if hdr_path is None:
    hdr_path = find_hdr(cluster_path)
    hdr = parse_envi_hdr(hdr_path)
    band = read_envi_raw(cluster_path, hdr)
    if band.ndim == 3:
    band = band[0]
    return band


    # --------------------------------------------------------------------------
    # Metadati di classificazione incorporati nell'header ENVI
    # (class names / class lookup) - spesso è qui, non in un color.keys esterno
    # --------------------------------------------------------------------------

    def parse_envi_classification(hdr_path: Path) -> dict | None:
    """
    Cerca nel .hdr i campi ENVI standard:
    class names = {Unclassified, Kaolinite, Goethite, ...}
    class lookup = {0,0,0, 255,0,0, 200,120,0, ...}
    e ritorna {id: (r,g,b,nome)} se presenti, altrimenti None.
    """
    text = hdr_path.read_text(errors="ignore")

    def extract_braced(field):
    m = re.search(rf"{field}\s*=\s*\{{([^}}]*)\}}", text,
    re.IGNORECASE | re.DOTALL)
    return m.group(1) if m else None

    names_raw = extract_braced("class names")
    lookup_raw = extract_braced("class lookup")
    if not names_raw or not lookup_raw:
    return None

    names = [n.strip() for n in names_raw.split(",") if n.strip()]
    nums = [int(x) for x in re.findall(r"-?\d+", lookup_raw)]
    if len(nums) < len(names) * 3:
    return None

    result = {}
    for i, name in enumerate(names):
    r, g, b = nums[i * 3], nums[i * 3 + 1], nums[i * 3 + 2]
    result[i] = (r, g, b, name)
    return result


    # --------------------------------------------------------------------------
    # Parsing color.keys (file di testo esterno, se lo trovi separatamente)
    # --------------------------------------------------------------------------

    def parse_color_keys(path: Path, debug: bool = False) -> dict:
    """
    Ritorna {id_materiale: (r, g, b, nome)}.

    Prova più pattern, perché il formato varia tra run Tetracorder:
    1) " 12 255 128 0 Kaolinite_CM9.spc"
    2) "12 Kaolinite_CM9 R=255 G=128 B=0"
    3. CSV-like: "12,255,128,0,Kaolinite"
    """
    patterns = [
    re.compile(r"^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S.*)$"),
    re.compile(r"^\s*(\d+)\s+(\S+).*?R\s*=?\s*(\d+).*?G\s*=?\s*(\d+).*?B\s*=?\s*(\d+)", re.IGNORECASE),
    re.compile(r"^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\S.*)$"),
    ]

    result = {}
    unmatched = []
    for raw_line in path.read_text(errors="ignore").splitlines():
    line = raw_line.strip()
    if not line or line.startswith("#") or line.startswith(";"):
    continue

    m = patterns[0].match(line)
    if m:
    idx, r, g, b, name = m.groups()
    result[int(idx)] = (int(r), int(g), int(b), name.strip())
    continue

    m = patterns[1].match(line)
    if m:
    idx, name, r, g, b = m.groups()
    result[int(idx)] = (int(r), int(g), int(b), name.strip())
    continue

    m = patterns[2].match(line)
    if m:
    idx, r, g, b, name = m.groups()
    result[int(idx)] = (int(r), int(g), int(b), name.strip())
    continue

    unmatched.append(raw_line)

    if debug:
    print(f"[debug] {len(result)} voci interpretate, "
    f"{len(unmatched)} righe non riconosciute.")
    for u in unmatched[:15]:
    print(" ??", u)

    if not result:
    raise ValueError(
    "Nessuna riga di color.keys è stata interpretata. "
    "Rilancia con --debug-colorkeys e mandami le prime righe del file "
    "cosi' adatto il parser al formato esatto."
    )
    return result


    # --------------------------------------------------------------------------
    # Plot
    # --------------------------------------------------------------------------

    def plot_classification(class_map: np.ndarray, color_keys: dict,
    output: Path, title: str, max_legend_items: int = 40):
    present_ids = sorted(i for i in np.unique(class_map) if i != 0 and i in color_keys)

    if not present_ids:
    print("Attenzione: nessun ID presente nella mappa ha una corrispondenza "
    "in color.keys. Controlla che i due file provengano dallo stesso run.")

    # id 0 = nessun match -> sfondo grigio chiaro
    ids = [0] + present_ids
    colors = [(0.92, 0.92, 0.92)]
    labels = ["(nessun match)"]
    for i in present_ids:
    r, g, b, name = color_keys[i]
    colors.append((r / 255, g / 255, b / 255))
    labels.append(f"{i}: {name}")

    cmap = ListedColormap(colors)
    bounds = ids + [ids[-1] + 1]
    norm = BoundaryNorm(bounds, cmap.N)

    remap = np.zeros_like(class_map, dtype=np.int32)
    for new_i, orig_i in enumerate(ids):
    remap[class_map == orig_i] = new_i

    fig, ax = plt.subplots(figsize=(10, 8))
    ax.imshow(remap, cmap=ListedColormap(colors), interpolation="nearest")
    ax.set_title(title)
    ax.set_xticks([])
    ax.set_yticks([])

    legend_ids = present_ids[:max_legend_items]
    handles = [Patch(facecolor=(color_keys[i][0] / 255,
    color_keys[i][1] / 255,
    color_keys[i][2] / 255),
    label=f"{color_keys[i][3]}")
    for i in legend_ids]
    if len(present_ids) > max_legend_items:
    handles.append(Patch(facecolor="white",
    label=f"... +{len(present_ids) - max_legend_items} altri"))

    ax.legend(handles=handles, bbox_to_anchor=(1.02, 1), loc="upper left",
    fontsize=7, frameon=False, ncol=1)

    fig.tight_layout()
    fig.savefig(output, dpi=200, bbox_inches="tight")
    print(f"Salvato: {output} ({len(present_ids)} materiali distinti trovati)")


    # --------------------------------------------------------------------------
    # Main
    # --------------------------------------------------------------------------

    def main():
    p = argparse.ArgumentParser(description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("--cluster", required=True, type=Path,
    help="File risultati (es. results.cluster.sorted)")
    p.add_argument("--hdr", type=Path, default=None,
    help="Header ENVI (.hdr), se non individuabile automaticamente")
    p.add_argument("--colorkeys", required=False, type=Path, default=None,
    help="File color.keys testuale (opzionale: se assente, "
    "lo script cerca 'class names'/'class lookup' "
    "direttamente nell'header ENVI del raster)")
    p.add_argument("--output", type=Path, default=Path("tetracorder_map.png"),
    help="File immagine di output (default: tetracorder_map.png)")
    p.add_argument("--title", default="Classificazione Tetracorder")
    p.add_argument("--debug-colorkeys", action="store_true",
    help="Stampa il dettaglio del parsing di color.keys")
    args = p.parse_args()

    class_map = load_cluster_map(args.cluster, args.hdr)

    color_keys = None
    hdr_path = args.hdr or find_hdr(args.cluster)
    if hdr_path.exists():
    color_keys = parse_envi_classification(hdr_path)
    if color_keys:
    print(f"Trovate {len(color_keys)} classi nei metadati ENVI "
    f"dell'header ({hdr_path.name}).")

    if color_keys is None:
    if args.colorkeys is None:
    print("Nessuna classificazione trovata nell'header ENVI e "
    "nessun --colorkeys fornito. Passa un file color.keys "
    "testuale con --colorkeys, oppure mandami un estratto "
    "del .hdr per capire dove sta l'informazione colore/nome.",
    file=sys.stderr)
    sys.exit(1)
    color_keys = parse_color_keys(args.colorkeys, debug=args.debug_colorkeys)

    plot_classification(class_map, color_keys, args.output, args.title)


    if __name__ == "__main__":
    sys.exit(main())


     

     

     

    Uso di Ecostress per la temperatura di discariche

    Ho provato a vedere se si possono i dati di Ecostress per misurare la temperatura di discariche. Per avere un confronto e' stato creato ...