Per calibrare l'esposizione del FigSpec FS60-C prima del volo si deve acquisire la black and white reference (rispettivamente mettendo i tappi sul sensore e ponendo il sensore di fronte al telo bianco di riferimento)
Questa fase crea due file figspec.figspecblack e figspec.figspecwhite
Il contenuto dei file e' una matrice di 300 linee per la risoluzione spaziale impostate. Non ho informazioni dirette ma credo alla fine vengono mediati i valori di tutte le linee
![]() |
| Black Reference |
![]() |
| White Reference |
#!/usr/bin/env python3
"""
decode_figspec_blackwhite.py
------------------------------
Decodifica i file di riferimento radiometrico (dark/black e white reference)
generati dal software del sensore iperspettrale pushbroom CHNSpec FigSpec
FS-60CL (estensioni .figspecblack / .figspecwhite).
FORMATO FILE (binario, little-endian, TLV proprietario):
Ogni record ha uno dei due layout seguenti:
A) Valore numerico semplice (8 byte totali):
uint16 id
uint16 0x0000 <- flag "valore diretto"
int32 value
B) Blocco con payload esplicito (10 + N byte):
uint16 id
uint32 length (N, in byte)
uint32 reserved (sempre 0x00000000 osservato)
byte[N] payload (stringa ASCII oppure array di float32 LE)
Il file e' una sequenza di questi record fino a un certo punto, dopo il quale
inizia un blocco binario finale non piu' "taggato": un array continuo di
float32 di lunghezza Samples x Bands (480 x 300 = 144000 valori = 576000
byte), che rappresenta l'immagine di riferimento vera e propria (nero o
bianco), in ordine BIL (banda dopo banda, ciascuna larga "Samples" pixel).
Record identificati (id -> significato):
6 stringa -> codice seriale/timestamp di acquisizione
7 stringa -> "bil" (interleave del dato)
8 stringa -> "BlackCorrection" / "WhiteCorrection" (tipo file)
9 stringa -> "Line"
10 float32 -> valore scalare (~515), probabile numero di linee mediate
11 float32 -> valore scalare (spesso 0.0)
12 float32[300] -> lista di lunghezze d'onda (nm), una per banda
13 float32[300] -> (solo file white) fattori/tempi di integrazione per banda
14 blob grande -> dati intermedi (probabile media parziale multi-linea)
15 float32 -> valore scalare addizionale
16 float32[2] -> coppia di valori addizionali (~0)
Dopo questi record, il resto del file e' l'immagine di riferimento raw
(float32, Samples x Bands, BIL).
Uso:
python3 decode_figspec_blackwhite.py file.figspecblack
python3 decode_figspec_blackwhite.py file.figspecblack --plot
python3 decode_figspec_blackwhite.py file.figspecblack --export-npy out.npy
"""
import struct
import argparse
import sys
SAMPLES_DEFAULT = 480 # pixel spaziali across-track del sensore FS-60CL
BANDS_DEFAULT = 300 # bande spettrali del sensore FS-60CL
def parse_header(data):
"""
Analizza i record TLV all'inizio del file.
Ritorna (offset_fine_header, lista_record).
"""
pos = 0
n = len(data)
records = []
while pos + 4 <= n:
rec_id, tag = struct.unpack_from("<HH", data, pos)
if tag == 0:
if pos + 8 > n:
break
val = struct.unpack_from("<i", data, pos + 4)[0]
records.append({"pos": pos, "id": rec_id, "kind": "int32", "value": val})
pos += 8
else:
if pos + 10 > n:
break
length = struct.unpack_from("<I", data, pos + 2)[0]
reserved = data[pos + 6:pos + 10]
if any(reserved) or pos + 10 + length > n:
break
payload = data[pos + 10:pos + 10 + length]
records.append({"pos": pos, "id": rec_id, "kind": "payload", "value": payload})
pos += 10 + length
return pos, records
def summarize(records, tail_len, samples, bands):
print(f"Record di header trovati: {len(records)}")
print("-" * 60)
for r in records:
if r["kind"] == "int32":
print(f" id={r['id']:3d} INT32 = {r['value']}")
else:
payload = r["value"]
is_ascii = len(payload) > 0 and all(32 <= b < 127 for b in payload)
if is_ascii:
print(f" id={r['id']:3d} STRING = {payload.decode()!r}")
elif len(payload) == 4:
v = struct.unpack("<f", payload)[0]
print(f" id={r['id']:3d} FLOAT32 = {v:.4f}")
elif len(payload) % 4 == 0 and len(payload) <= 4000:
nfloats = len(payload) // 4
arr = struct.unpack(f"<{nfloats}f", payload)
print(f" id={r['id']:3d} FLOAT32[{nfloats}] min={min(arr):.4f} "
f"max={max(arr):.4f} primi 3={[round(x,3) for x in arr[:3]]}")
else:
nfloats = len(payload) // 4
print(f" id={r['id']:3d} BLOB = {len(payload)} byte "
f"({nfloats} float32 se interpretato come tale)")
print("-" * 60)
expected = samples * bands * 4
print(f"Byte residui dopo l'header (immagine di riferimento): {tail_len}")
print(f"Attesi per {samples} campioni x {bands} bande (float32): {expected}")
if tail_len == expected:
print(" -> corrispondenza ESATTA: il blob finale e' l'immagine di riferimento.")
else:
print(" -> ATTENZIONE: la dimensione non corrisponde esattamente; "
"verificare Samples/Bands o la presenza di ulteriori record.")
def load_reference_image(path, samples=SAMPLES_DEFAULT, bands=BANDS_DEFAULT):
with open(path, "rb") as f:
data = f.read()
header_end, records = parse_header(data)
tail = data[header_end:]
image = None
expected = samples * bands * 4
if len(tail) == expected:
flat = struct.unpack(f"<{samples * bands}f", tail)
image = [flat[b * samples:(b + 1) * samples] for b in range(bands)]
return records, image, tail
def plot_image(image, out_png="reference_image.png", title="Immagine di riferimento"):
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
except ImportError:
print("matplotlib/numpy non disponibili: salto il grafico.")
return
arr = np.array(image)
plt.figure(figsize=(9, 5))
plt.imshow(arr, aspect="auto", cmap="viridis")
plt.colorbar(label="DN")
plt.xlabel("Pixel spaziale (across-track)")
plt.ylabel("Banda spettrale")
plt.title(title)
plt.tight_layout()
plt.savefig(out_png, dpi=150)
print(f"Grafico salvato in: {out_png}")
def export_npy(image, out_path):
try:
import numpy as np
except ImportError:
print("numpy non disponibile: impossibile esportare .npy")
return
arr = np.array(image)
np.save(out_path, arr)
print(f"Esportato array numpy {arr.shape} in: {out_path}")
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("file", help="Percorso al file .figspecblack o .figspecwhite")
parser.add_argument("--samples", type=int, default=SAMPLES_DEFAULT,
help=f"Numero di pixel spaziali (default {SAMPLES_DEFAULT})")
parser.add_argument("--bands", type=int, default=BANDS_DEFAULT,
help=f"Numero di bande spettrali (default {BANDS_DEFAULT})")
parser.add_argument("--plot", action="store_true",
help="Genera un'immagine PNG del riferimento (bande x campioni)")
parser.add_argument("--export-npy", metavar="OUT.npy",
help="Esporta l'immagine di riferimento come array numpy")
args = parser.parse_args()
records, image, tail = load_reference_image(args.file, args.samples, args.bands)
summarize(records, len(tail), args.samples, args.bands)
if image is None:
print("\nImpossibile interpretare il blocco finale con le dimensioni date.")
sys.exit(1)
if args.plot:
title = "Riferimento " + args.file.split(".")[-1]
plot_image(image, title=title)
if args.export_npy:
export_npy(image, args.export_npy)
if __name__ == "__main__":
main()


Nessun commento:
Posta un commento