venerdì 7 agosto 2026

Black and White reference FigSpec FS60-C

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
la zona colorata di giallo e' il riflesso diretto del Sole. La calibrazione dell'esposizione viene fatta estremamente vicina al sensore per cui non e' possibile avere una illuminazione omogenea
 

#!/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

Black and White reference FigSpec FS60-C

Per calibrare l'esposizione del FigSpec FS60-C prima del volo si deve acquisire la black and white reference (rispettivamente mettendo i...