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

venerdì 20 marzo 2026

Spettri di Prisma

Sono ritornato a lavorare sui dati Prisma, stavolta in maniera piu' convinta

Il primo problema e' trovare il software giusto. Se si aprono le immagini con Esa Snap non ci sono particolari problemi tranne quando si arriva ad aprire Spectrum View e si scopre che le bande dell'immagini sono divise in due VNIR e SWIR e quindi non e' possibile avere uno spettro completo da 400 a 2500 nm. Si devono aprire le due immagini ed a seconda di dove e' il cursore Spectrum View si spostera' tra i dati VNIR e SWIR 

 


Per avere uno spettro completo l'unica soluzione e' stata passare da uno script Python (compilato via Claude AI)

 


"""
PRISMA Hyperspectral Spectrum Plotter (L2C)
=============================================
Reads a PRISMA L2C HE5 file and plots the full VNIR+SWIR spectrum
for a user-selected pixel.

Confirmed HDF5 layout:
Cubes : HDFEOS/SWATHS/PRS_L2C_HCO/Data Fields/{VNIR,SWIR}_Cube
shape = (rows, bands, cols)
Wavelengths : root attrs List_Cw_Vnir / List_Cw_Swir (nm, length=n_bands)
Band flags : root attrs List_Cw_Vnir_Flags / List_Cw_Swir_Flags (1=valid)
Scale : root attrs L2ScaleVnirMin/Max, L2ScaleSwirMin/Max
DN → reflectance = DN / 65535 * (ScaleMax - ScaleMin) + ScaleMin

Usage
-----
python prisma_spectrum_plot.py <file.he5> [row] [col] [-o out.png]

Dependencies
------------
pip install h5py numpy matplotlib
"""

import argparse
import numpy as np
import h5py
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
from pathlib import Path


CUBE_VNIR = "HDFEOS/SWATHS/PRS_L2C_HCO/Data Fields/VNIR_Cube"
CUBE_SWIR = "HDFEOS/SWATHS/PRS_L2C_HCO/Data Fields/SWIR_Cube"


def load_prisma(filepath: str, row: int | None = None, col: int | None = None):
"""
Extract a single-pixel VNIR+SWIR spectrum from a PRISMA L2C HE5 file.

Parameters
----------
filepath : path to the .he5 file
row, col : pixel coordinates (0-based). Default = image centre.

Returns
-------
wavelengths : np.ndarray – wavelengths in nm (valid bands only, sorted)
spectrum : np.ndarray – reflectance [0–1]
meta : dict
"""
filepath = Path(filepath)
if not filepath.exists():
raise FileNotFoundError(filepath)

with h5py.File(filepath, "r") as f:

# ── wavelengths and valid-band flags from root attributes ─────────────
vnir_wl = np.array(f.attrs["List_Cw_Vnir"], dtype=np.float32)
swir_wl = np.array(f.attrs["List_Cw_Swir"], dtype=np.float32)
vnir_flags = np.array(f.attrs["List_Cw_Vnir_Flags"], dtype=np.int8)
swir_flags = np.array(f.attrs["List_Cw_Swir_Flags"], dtype=np.int8)

vnir_valid = vnir_flags == 1 # boolean mask for valid VNIR bands
swir_valid = swir_flags == 1 # boolean mask for valid SWIR bands

# ── scale factors: uint16 DN → reflectance ────────────────────────────
vnir_scale_min = float(f.attrs["L2ScaleVnirMin"])
vnir_scale_max = float(f.attrs["L2ScaleVnirMax"])
swir_scale_min = float(f.attrs["L2ScaleSwirMin"])
swir_scale_max = float(f.attrs["L2ScaleSwirMax"])

# ── cube shapes: (rows, bands, cols) ──────────────────────────────────
vnir_ds = f[CUBE_VNIR]
swir_ds = f[CUBE_SWIR]
n_rows, n_vnir_bands, n_cols = vnir_ds.shape
_, n_swir_bands, _ = swir_ds.shape

if row is None: row = n_rows // 2
if col is None: col = n_cols // 2

if not (0 <= row < n_rows and 0 <= col < n_cols):
raise ValueError(
f"Pixel ({row}, {col}) outside image bounds ({n_rows}x{n_cols})."
)

# ── read single pixel: cube[row, :, col] ─────────────────────────────
vnir_dn = vnir_ds[row, :, col].astype(np.float32)
swir_dn = swir_ds[row, :, col].astype(np.float32)

# ── DN → reflectance ──────────────────────────────────────────────────────
vnir_ref = vnir_dn / 65535.0 * (vnir_scale_max - vnir_scale_min) + vnir_scale_min
swir_ref = swir_dn / 65535.0 * (swir_scale_max - swir_scale_min) + swir_scale_min

# ── apply valid-band masks ────────────────────────────────────────────────
# Flags array length matches n_bands; trim to cube size just in case
vnir_mask = vnir_valid[:n_vnir_bands]
swir_mask = swir_valid[:n_swir_bands]

vnir_wl = vnir_wl[:n_vnir_bands][vnir_mask]
vnir_ref = vnir_ref[vnir_mask]
swir_wl = swir_wl[:n_swir_bands][swir_mask]
swir_ref = swir_ref[swir_mask]

# ── sort by wavelength (PRISMA stores bands longest-first) ───────────────
def sort_wl(wl, sp):
idx = np.argsort(wl)
return wl[idx], sp[idx]

vnir_wl, vnir_ref = sort_wl(vnir_wl, vnir_ref)
swir_wl, swir_ref = sort_wl(swir_wl, swir_ref)

# ── remove VNIR/SWIR overlap: keep VNIR below SWIR start ─────────────────
vnir_keep = vnir_wl < swir_wl[0]
wavelengths = np.concatenate([vnir_wl[vnir_keep], swir_wl])
spectrum = np.concatenate([vnir_ref[vnir_keep], swir_ref])

meta = dict(
row=row, col=col,
n_rows=n_rows, n_cols=n_cols,
n_vnir_valid=int(vnir_mask.sum()),
n_swir_valid=int(swir_mask.sum()),
filename=filepath.name,
)
return wavelengths, spectrum, meta


# ── plot ───────────────────────────────────────────────────────────────────────

COLOUR_REGIONS = [
(400, 700, "#e8f4e8", "VIS"),
(700, 1000, "#f5e8f5", "NIR"),
(1000, 1800, "#e8eef5", "SWIR-1"),
(1800, 2500, "#f5f0e8", "SWIR-2"),
]

ABSORPTION_BANDS = [
(1340, 1460, "H\u2082O"),
(1800, 1960, "H\u2082O"),
]


def plot_spectrum(wavelengths, spectrum, meta, output_path=None):
fig, ax = plt.subplots(figsize=(13, 5))

wl_min, wl_max = wavelengths[0], wavelengths[-1]
sp_max = np.nanmax(spectrum)

# Spectral region backgrounds
for lo, hi, colour, label in COLOUR_REGIONS:
lo_c, hi_c = max(lo, wl_min), min(hi, wl_max)
if lo_c < hi_c:
ax.axvspan(lo_c, hi_c, color=colour, alpha=0.45, zorder=0)
ax.text((lo_c + hi_c) / 2, sp_max * 1.02,
label, ha="center", va="bottom", fontsize=8,
color="#888888", zorder=2)

# Atmospheric absorption windows
for lo, hi, label in ABSORPTION_BANDS:
lo_c, hi_c = max(lo, wl_min), min(hi, wl_max)
if lo_c < hi_c:
ax.axvspan(lo_c, hi_c, color="#bbbbbb", alpha=0.55, zorder=1)
ax.text((lo_c + hi_c) / 2, sp_max * 0.96,
label, ha="center", va="top", fontsize=8,
color="#444444", zorder=3)

ax.plot(wavelengths, spectrum, color="#1a5276", linewidth=1.3, zorder=4)
ax.fill_between(wavelengths, spectrum, alpha=0.12, color="#1a5276", zorder=3)

ax.set_xlim(wl_min, wl_max)
ax.set_ylim(bottom=0)
ax.set_xlabel("Wavelength (nm)", fontsize=11)
ax.set_ylabel("Reflectance", fontsize=11)
ax.set_title(
f"PRISMA L2C Spectrum - pixel ({meta['row']}, {meta['col']}) "
f"[{meta['n_rows']}x{meta['n_cols']}]\n{meta['filename']}",
fontsize=10, pad=8
)
ax.xaxis.set_minor_locator(ticker.AutoMinorLocator(5))
ax.yaxis.set_minor_locator(ticker.AutoMinorLocator(4))
ax.tick_params(which="both", direction="in")
ax.grid(True, which="major", linestyle="--", alpha=0.35)
ax.annotate(
f"VNIR: {meta['n_vnir_valid']} bands | SWIR: {meta['n_swir_valid']} bands",
xy=(0.99, 0.02), xycoords="axes fraction",
ha="right", va="bottom", fontsize=8, color="#666666"
)

plt.tight_layout()
if output_path:
fig.savefig(output_path, dpi=150, bbox_inches="tight")
print(f"[OK] Saved: {output_path}")
else:
plt.show()
plt.close(fig)


# ── CLI ────────────────────────────────────────────────────────────────────────

def main():
parser = argparse.ArgumentParser(
description="Plot a VNIR+SWIR pixel spectrum from a PRISMA L2C HE5 file."
)
parser.add_argument("he5_file")
parser.add_argument("row", nargs="?", type=int, default=None,
help="Pixel row (default: centre)")
parser.add_argument("col", nargs="?", type=int, default=None,
help="Pixel col (default: centre)")
parser.add_argument("-o", "--output", default=None,
help="Save figure to file (e.g. spectrum.png)")
args = parser.parse_args()

print(f"[...] Reading {args.he5_file}")
wavelengths, spectrum, meta = load_prisma(args.he5_file, args.row, args.col)

print(
f"[OK] {meta['n_vnir_valid']} VNIR + {meta['n_swir_valid']} SWIR valid bands\n"
f" Image : {meta['n_rows']} rows x {meta['n_cols']} cols\n"
f" Pixel : row={meta['row']}, col={meta['col']}\n"
f" Range : {wavelengths[0]:.1f} - {wavelengths[-1]:.1f} nm "
f"({len(wavelengths)} merged bands)"
)

plot_spectrum(wavelengths, spectrum, meta, output_path=args.output)


if __name__ == "__main__":
main()


 

 

 

 

 

 

 

 

 

 

 

sabato 14 marzo 2026

Red Edge con Sentinel 2

Volevo migliorare un po' quanto provato qui piu' che altro per avere una migliore risoluzione spaziale. Ho provato con Sentinel 2 (20 metri di risoluzione) pagando ovviamente dal punto di vista radiometrico

 


Riferimento bibliografico 

Guyot, G., & Baret, F. (1988). Utilisation de la haute résolution spectrale pour suivre l'état des couverts végétaux. In Proceedings of the 4th International Colloquium on Spectral Signatures of Objects in Remote Sensing, Aussois, France, 18–22 January 1988 (ESA SP-287), (pp. 279–286). European Space Agency. 

 

Grafico dell'articolo originale

 

La banda 5 e banda 7 sono utilizzate per calcolare la pendenza mentre la banda 4 e banda 7 sono utilizzate per la riflettanza di soglia 

In pratica  (B4 + B7) / 2) corrisponde al punto centrale tra il valore minimo del Red ed il massimo del NIR

Viene assunto un comportamento lineare tra B5 e B6. La formula di una retta tra due punti e' 

dove

è la lunghezza d'onda che cerchiamo (
è il valore di riflettanza target ()
sono le coordinate della Banda 5
sono le coordinate della Banda 6

risolvendo per isolare l'incognita

 

La formula finale da inserire in Band Math di Snap e' 

 (35 * ((((B4 + B7) / 2) - B5) / (B6 - B5)))+705

Attenzione la banda B4 ha una risoluzione di 10 m mentre le altre hanno una risoluzione di 20 m 
 
La scala di colore e' dal viola (vegetazione piu'sana) verso il blu chiaro (vegetazione sotto stress)...il bianco e' sostanzialmente tutto cio' che non e' vegetazione 
 
Mappa
 
Facendo un transetto come fatto nella prova precedente
 
Transetto 

In questo caso sembra che la vegetazione sia maggiormente in sofferenza in corrispodenza del sito di interesse 


 

lunedì 2 marzo 2026

C2RCC Sentinel 2 Esa Snap

Per processare le immagini Sentinel 2 su acqua per la rimozione atmosferica si puo' usare, oltre a sen2water, anche C2RCC (Case 2 Regional CoastColour)


 

Si parte da un prodotto L1C ed in modo preliminare si deve otterenere la stessa risoluzione spaziale tra tutte le bande con Raster/Geometric/Resampling

Successivamente si usa Optical/Thematic Water Processing/C2RCC Processor/S2-MSI 

La risposta del modello sono numerose bande

RTOARadiometricoRiflettanza top-of-atmosphere
RHOWRadiometricoRiflettanza emergente dall’acqua
RHOWNRadiometricoRiflettanza normalizzata
CONCBiochimicoConcentrazioni (clorofilla, TSM materia sospesa totale, CDOM materia organica disciolta)
IOPFisicoProprietà ottiche intrinseche
KDFisicoAttenuazione verticale della luce
UNCQualitàIncertezza del modello

 Per gli indici spettrali e' conveniente usare RHOWN

mercoledì 10 aprile 2024

Decision Tree Tensorflow su dati Sentinel 2

 Ho ripreso il filone di questa prova per provare i Decision Tree con le firme spettrali di Sentinel 2

Per la prova ho preso l'immagine S2B_MSIL2A_20230826T100559_N0509_R022_T32TPP_20230826T134424.SAFE

del periodo estivo del Mugello (Toscana) in modo da avere suolo nudo disponibile.


Visto che volevo estrarre tutte le bande tramite SNAP ho campionato tutte le bande a 10 m (altrimenti come visto nel precedente post lo Spectral Viewer estrae solo le bande native a 10 m)...e qui c'e' un problema...ho dovuto fare il resampling spaziale di tutta l'immagine e dopo fare il subset della mia sola area di interesse altrimenti, invertendo le due operazioni, SNAP entrava continuamente in errore

Usando gli OpenData della Regione Toscana

https://dati.toscana.it/dataset/dbped

ho usato il dataset per selezionare il parametro contenuto in argilla del suolo categorizzandolo a passi del 5%. Usando i Pin e la vista sincronizzata tra la mappe del suolo e l'immagine telerilevata sono selezionati 153 punti appartenenti a 4 classi di contenuto in argilla

Classe/Nr spettri 3 44 2 41 0 34 1 34

Il contenuto in argille delle classi e'

classe 1 (amaranto) : 45-50%

classe 2 (arancione) : 30-35%

classe 3 (rosso) :40-45%

classe 4 (verde) : 20-25%




il file dati in CSV e' formato nel seguente modo

443,490,560,665,705,740,783,842,865,945,1610,2190,Classe
0.0711,0.1056,0.1548,0.2046,0.2335,0.2442,0.2617,0.2624,0.2801,0.2813,0.3556,0.2868,1
0.0777,0.1052,0.157,0.2032,0.2311,0.2415,0.2611,0.2562,0.2741,0.2841,0.3473,0.2829,1

Come si vede dall'esempio la variabilita' spettrale in ogni classe e' molto elevata (in rosso lo spettro medio)



Lo spettro medio per ogni classe di contenuto in argilla e' visualizzato negl grafico sottostante


Usando una rete neurale tradizionale l'accuratezza e ' prossima al 77%

import pandas as pd
import numpy as np
import tensorflow as tf
import seaborn as sns
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense,Dropout
from sklearn.preprocessing import LabelEncoder , StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix

df = pd.read_csv('/content/totale2.csv')
print(df.head())

le = LabelEncoder()
df['Classe'] = le.fit_transform(df['Classe'])

X = df.drop(columns=['Classe'])
y = df['Classe']
print(y.head())
print(y.value_counts())
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.25,shuffle=True,random_state=7)

sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)

print(X_train.size)
print(y_train)
y_train = tf.keras.utils.to_categorical(y_train,num_classes=4)
def get_models():
    model = Sequential([
        Dense(units=32,input_shape=(12,),activation='relu'),
        Dense(units=32,activation='relu'),
        Dropout(0.5),
        Dense(units=4,activation='softmax')
    ])
    return model

model = get_models()
model.compile(optimizer='Adam',loss='categorical_crossentropy',metrics=['accuracy'])
model.summary()

model.fit(X_train,y_train,epochs=100, verbose=2)

prediction = model.predict(X_test)
prediction = np.argmax(prediction,axis=-1)
acury = accuracy_score(y_test,prediction)
print(acury)
cm = confusion_matrix(y_test,prediction)
print(cm)

0.7692307692307693

Con la seguente matrice di confusione

[[ 4 1 0 0] [ 4 7 0 0] [ 0 0 9 0] [ 1 2 1 10]]


Utilizzando i decision tree

!pip install -q -U tensorflow_decision_forests
!pip install -q -U dtreeviz
import tensorflow_decision_forests as tfdf

import tensorflow as tf

import os
import numpy as np
import pandas as pd
import tensorflow as tf
import math

import dtreeviz

from matplotlib import pyplot as plt
from IPython import display

import logging
logging.getLogger('matplotlib.font_manager').setLevel(level=logging.CRITICAL)

display.set_matplotlib_formats('retina') # generate hires plots

np.random.seed(1234)
def split_dataset(dataset, test_ratio=0.30, seed=1234):
  np.random.seed(seed)
  test_indices = np.random.rand(len(dataset)) < test_ratio
  return dataset[~test_indices], dataset[test_indices]
df_spettri = pd.read_csv("/content/totale2.csv")
df_spettri.head(3)
spettri_label = "Classe"   # Name of the classification target label
classes = list(df_spettri[spettri_label].unique())
df_spettri[spettri_label] = df_spettri[spettri_label].map(classes.index)

print(f"Target '{spettri_label}'' classes: {classes}")
df_spettri.head(3)
# Split into training and test sets
train_ds_pd, test_ds_pd = split_dataset(df_spettri)
print(f"{len(train_ds_pd)} examples in training, {len(test_ds_pd)} examples for testing.")

# Convert to tensorflow data sets
train_ds = tfdf.keras.pd_dataframe_to_tf_dataset(train_ds_pd, label=spettri_label)
test_ds = tfdf.keras.pd_dataframe_to_tf_dataset(test_ds_pd, label=spettri_label)
cmodel = tfdf.keras.RandomForestModel(verbose=0, random_seed=1234)
cmodel.fit(train_ds)
cmodel.compile(metrics=["accuracy"])
cmodel.evaluate(test_ds, return_dict=True, verbose=0)
# Tell dtreeviz about training data and model
spettri_features = [f.name for f in cmodel.make_inspector().features()]
viz_cmodel = dtreeviz.model(cmodel,
                           tree_index=3,
                           X_train=train_ds_pd[spettri_features],
                           y_train=train_ds_pd[spettri_label],
                           feature_names=spettri_features,
                           target_name=spettri_label,
                           class_names=classes)
viz_cmodel.view(scale=1.75)

si ha una accuracy di circa 0.7




venerdì 18 novembre 2022

Elaborazione batch con SNAP GPT headless

 E' possibile utilizzare SNAP in modalita' headless installando da remoto su un server il normale pacchetto di SNAP. A seconda del fatto che sia presente o meno una sessione di interfaccia grafica o meno viene eseguito un tipo diverso di installer

Per eseguire una catena di comandi in SNAP si puo' in maniera interattiva creare un grafico con Graph Builder e salvare il grafico in formato xml



questo e' un esempio di grafico salvato da SNAP per il calcolo NDVI tramite BandMath

=============================================================
<graph id="Graph">
  <version>1.0</version>
  <node id="Read">
    <operator>Read</operator>
    <sources/>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <useAdvancedOptions>false</useAdvancedOptions>
      <file>/home/luca/transi/S2B_MSIL2A_20220811T100559_N0400_R022_T32TPP_20220811T162101.zip</file>
      <copyMetadata>true</copyMetadata>
      <bandNames/>
      <pixelRegion>0,0,10980,10980</pixelRegion>
      <maskNames/>
    </parameters>
  </node>
  <node id="BandMaths">
    <operator>BandMaths</operator>
    <sources>
      <sourceProduct refid="Read"/>
    </sources>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <targetBands>
        <targetBand>
          <name>NDVI</name>
          <type>float32</type>
          <expression>(B8 - B4) / (B8 + B4)</expression>
          <description/>
          <unit/>
          <noDataValue>0.0</noDataValue>
        </targetBand>
      </targetBands>
      <variables/>
    </parameters>
  </node>
  <node id="Write">
    <operator>Write</operator>
    <sources>
      <sourceProduct refid="BandMaths"/>
    </sources>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <file>/home/luca/transi/BandMath.tif</file>
      <formatName>GeoTIFF</formatName>
    </parameters>
  </node>
  <applicationData id="Presentation">
    <Description/>
    <node id="Read">
            <displayPosition x="37.0" y="134.0"/>
    </node>
    <node id="BandMaths">
      <displayPosition x="184.0" y="136.0"/>
    </node>
    <node id="Write">
            <displayPosition x="455.0" y="135.0"/>
    </node>
  </applicationData>
</graph>

=============================================================

per renderlo utilizzabile in modalita' batch si devono modificarlo per inserire delle variabili
Le variabili sono nel formato ${nome_variabile}
Nel codice sottostante e' stato reso variabile il file di input
=============================================================
<graph id="Graph">
  <version>1.0</version>
  <node id="Read">
    <operator>Read</operator>
    <sources/>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <useAdvancedOptions>false</useAdvancedOptions>
      <file>${ingresso}</file>
      <copyMetadata>true</copyMetadata>
      <bandNames/>
      <pixelRegion>0,0,10980,10980</pixelRegion>
      <maskNames/>
    </parameters>
  </node>
  <node id="BandMaths">
    <operator>BandMaths</operator>
    <sources>
      <sourceProduct refid="Read"/>
    </sources>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <targetBands>
        <targetBand>
          <name>NDVI</name>
          <type>float32</type>
          <expression>(B8 - B4) / (B8 + B4)</expression>
          <description/>
          <unit/>
          <noDataValue>0.0</noDataValue>
        </targetBand>
      </targetBands>
      <variables/>
    </parameters>
  </node>
  <node id="Write">
    <operator>Write</operator>
    <sources>
      <sourceProduct refid="BandMaths"/>
    </sources>
    <parameters class="com.bc.ceres.binding.dom.XppDomElement">
      <file>/home/luca/transi/ndvi.tif</file>
      <formatName>GeoTIFF</formatName>
    </parameters>
  </node>
  <applicationData id="Presentation">
    <Description/>
    <node id="Read">
            <displayPosition x="37.0" y="134.0"/>
    </node>
    <node id="BandMaths">
      <displayPosition x="184.0" y="136.0"/>
    </node>
    <node id="Write">
            <displayPosition x="455.0" y="135.0"/>
    </node>
  </applicationData>
</graph>
=============================================================

da linea di comando si puo' usare lo switch -P per popolare la variabile

-Pnome_variabile=valore_variabile

per lanciare il processo per esempio si puo' usare

/home/luca/snap/bin/gpt ndvi2.xml -Pingresso=/home/luca/transi/S2B_MSIL2A_20220811T100559_N0400_R022_T32TPP_20220811T162101.zip

Hyperview competition

Ho provato a cimentarmi (anche se e' una gara terminata) con https://platform.ai4eo.eu/seeing-beyond-the-visible/data  J. Nalepa, B. Le ...