venerdì 14 agosto 2026

Da quota ellissoidica ad ortometrica

Per passare da quote ellissoidiche GPS a quote sul geoide ci sono varie opzioni in base al modello scelto

ITG2009

 

Se si usa EGM 2008 si ha un dato ogni 2.5 minuti di arco con una accuratezza dichiarata di 10 cm 

Per effettuare la correzione di deve scaricare il file egm2008-2.5.pgm

https://sourceforge.net/projects/geographiclib/files/geoids-distrib/ 

e lanciare lo script indicando le coordinate GPS 

from pygeodesy.geoids import GeoidPGM

geoid = GeoidPGM(r'.\geoids\egm2008-2_5.pgm', kind=3)

lat, lon = 43.7332442459068, 10.2760008626873

#lat, lon = 43.7337747, 10.2762272  # San Rossore
h_ell = 51.152


N = geoid.height(lat, lon)
H_ortho = h_ell - N

print(f"Differenza ellissoide ortometrica = {N:.3f} m")
print(f"H ortometrica = {H_ortho:.3f} m")

altrimenti si puo' usare ITG 2009 specifico per Italia e fornito da Polimi con spaziatura 1.5 minuti primi

https://www.isgeoid.polimi.it/Geoid/Europe/Italy/ITG2009_g.html 

"""
Parser per file .isg (International Service for the Geoid, Politecnico di Milano)
Formato griglia regolare ASCII: header + valori di ondulazione N riga per riga,
ordinamento default: nord -> sud, ogni riga ovest -> est.

Uso:
    geoid = IsgGeoid("ITG2009_20170606_isg.txt")
    N = geoid.height(lat=43.6833, lon=10.2833)
    H_ortho = h_ellissoidica - N
"""

import numpy as np
from scipy.interpolate import RegularGridInterpolator
import os


class IsgGeoid:
    def __init__(self, path):
        with open(path, encoding="utf-8", errors="replace") as f:
            lines = f.readlines()

        header = {}
        end_idx = None
        for i, line in enumerate(lines):
            if "end_of_head" in line:
                end_idx = i
                break
            if "=" in line and "begin_of_head" not in line:
                key, val = line.split("=", 1)
                header[key.strip()] = val.strip()

        if end_idx is None:
            raise ValueError("Non trovo 'end_of_head' nel file .isg")

        self.lat_min = float(header["lat min"])
        self.lat_max = float(header["lat max"])
        self.lon_min = float(header["lon min"])
        self.lon_max = float(header["lon max"])
        self.dlat = float(header["delta lat"])
        self.dlon = float(header["delta lon"])
        self.nrows = int(header["nrows"])
        self.ncols = int(header["ncols"])
        self.nodata = float(header.get("nodata", -9999.0))

        data_lines = lines[end_idx + 1:]
        vals = []
        for line in data_lines:
            line = line.strip()
            if line:
                vals.extend(float(x) for x in line.split())

        expected = self.nrows * self.ncols
        if len(vals) != expected:
            raise ValueError(
                f"Numero di valori letti ({len(vals)}) diverso da nrows*ncols "
                f"({expected}). File troncato o formato inatteso."
            )

        grid = np.array(vals, dtype=np.float64).reshape(self.nrows, self.ncols)
        grid[grid == self.nodata] = np.nan

        # riga 0 = lat_max, riga -1 = lat_min (ordinamento default ISG: nord->sud)
        # capovolgo per avere lat crescente, richiesto da RegularGridInterpolator
        grid = grid[::-1, :]
        lat_arr = np.linspace(self.lat_min, self.lat_max, self.nrows)
        lon_arr = np.linspace(self.lon_min, self.lon_max, self.ncols)

        self._interp = RegularGridInterpolator(
            (lat_arr, lon_arr), grid, method="linear", bounds_error=True
        )

    def height(self, lat, lon):
        """Ritorna l'ondulazione del geoide N (metri) al punto lat/lon (gradi decimali)."""
        val = self._interp([[lat, lon]])[0]
        if np.isnan(val):
            raise ValueError(f"Punto ({lat}, {lon}) cade in area nodata della griglia")
        return float(val)

    def height_array(self, lats, lons):
        """Versione vettorizzata: lats, lons array numpy della stessa lunghezza."""
        pts = np.column_stack([lats, lons])
        vals = self._interp(pts)
        if np.any(np.isnan(vals)):
            raise ValueError("Alcuni punti cadono in area nodata della griglia")
        return vals


if __name__ == "__main__":
    SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(SCRIPT_DIR, "ITG2009_20170606.isg.txt")
    geoid = IsgGeoid(path)

    punti = {
        "SanRossore": (43.7332442459068, 10.2760008626873),
    }


    # esempio conversione completa
    h_ell = 51.152  # quota ellissoidica misurata dal drone (esempio)
    lat, lon = punti["SanRossore"]
    N = geoid.height(lat, lon)
    print(N)
    H_ortho = h_ell - N
    print(f"\nEsempio: h_ell={h_ell} m -> H_ortho={H_ortho:.3f} m (N={N:.3f} m)")

 modelli piu' grezzi ASTER GDEM v3 con passo di campionamento ad 1 grado

 per confronto 

Punto: lat=43.7332442459068, lon=10.2760008626873

ASTER GDEM V3 (Open Topo Data)  : 7.00 m  (riferito a EGM96, ~quota ortometrica/DSM)
Ondulazione geoide EGM2008 (N)  : 46.5572 m
Ondulazione geoide EGM96  (N)   : 46.9583 m
Ondulazione geoide EGM84  (N)   : 47.5683 m

Quota ASTER ellissoidica WGS84 (via N_EGM96) : 53.96 m
Quota ASTER ellissoidica WGS84 (via N_EGM2008, non corretto) : 53.56 m

Differenza N(EGM96) - N(EGM2008) nel punto: +0.4011 m

 

 

 

DJI Log Parser e GPS FigSpec FS60-C

Uno dei problemi del FigSpec FS60-C su cui sto lavorando e' che i log files sono con un orario spostato di 6 ore in avanti rispetto al fuso italiano e che non sono valorizzati nel file figspec.gps i valori di yaw/pitch/roll 

E' quindi necessario appoggiarsi ai dati loggati dal drone stesso con il suo sistema RTK


 

 

I dati sono contenuti nel telecomando in  

Internal Storage / DJI / com.dji.industry.pilot / FlightRecord /

sia in formato DAT che in formato TXT entrambi formati binari codificati. 

ATTENZIONE : i dati contenuti nel file DAT sono piu' numerosi dei dati contenuti nel file TXT (per esempio nel file DAT sono contenute le colonne WEATHER con le indicazioni del vento che sono assenti nel file TXT  

Per estrarre i dati dal formato TXT si deve scaricare l'eseguibile da 

https://github.com/lvauvillier/dji-log-parser/releases 

ottenere una OpenAPI key tramite un account developer sul sito DJI e lanciare il comando

dji-log.exe  .\dji\DJIFlightRecord_2026-07-31_[10-36-33].txt --api-key XXXXXXXXXf100d529d2329caba3 --csv vv.csv 

altrimenti si puo' utilizzare il servizio on line

 https://www.phantomhelp.com/logviewer/upload/

ATTENZIONE : i dati contenuti nel file csv esportato dal servizio on line sono piu' numerosi dei dati contenuti nel file TXT (per esempio sono contenute le colonne WEATHER con le indicazioni del vento che sono assenti dall'esportazione con dji-log locale)

L'aspetto da sottolineare e' che la quota di volo e' considerata come AGL (above ground level) rispetto al punto di decollo. Non c'e' direttamente una informazione di quota sul livelllo del mare del drone. Si ha invece l'informazione di Home.height (quota ellissoidica GPS) che viene pero' ripresa dal GPS del telecomando che non e' fornito di RTK 


Le unità di misura possono essere in Sistema Internazionale od in Sistema Imperiale con le quote espresse in feet 

OSD — stato di volo/telemetria principale

ColonnaSignificatoUnità / note
OSD.latitudeLatitudine GPS del dronegradi decimali (memorizzato in radianti nel file)
OSD.longitudeLongitudine GPS del dronegradi decimali (memorizzato in radianti nel file)
OSD.heightAltezza sul punto di decollo (relative altitude)metri
OSD.altitudeAltitudine assoluta (quando presente)metri
OSD.xSpeedVelocità orizzontale asse X (Nord/Est a seconda del riferimento)m/s
OSD.ySpeedVelocità orizzontale asse Ym/s
OSD.zSpeedVelocità verticalem/s (positivo = salita, a seconda della convenzione)
OSD.pitchAssetto pitch del velivologradi
OSD.rollAssetto roll del velivologradi
OSD.yawPrua/heading del velivologradi (0–360)
OSD.flycStateStato del "flight controller" (modo di volo interno, es. GPS, ATTI, Sport, RTH, Auto Takeoff/Landing)enum
OSD.flycCommandUltimo comando ricevuto dal FCenum
OSD.flightActionAzione automatica in corso (RTH, landing, ecc.)enum
OSD.groundOrSkySe il drone è a terra o in voloflag
OSD.isMotorOn / OSD.isMotorUpStato motoribool
OSD.isSwaveWorkStato "swave" (funzione interna)bool
OSD.goHomeStatusStato Return-to-Homeenum
OSD.isOnGroundRilevamento a terrabool
OSD.gpsLevel / OSD.gpsNumQualità/numero di satelliti GPS agganciatiintero (0–10+)
OSD.nonGpsCauseMotivo di eventuale assenza segnale GPSenum
OSD.flyTimeTempo di volo trascorsosecondi/decimi
OSD.voltageTensione batteria (talvolta duplicato da BATTERY)Volt
OSD.windSpeed / OSD.windDirectionVento stimatom/s / gradi
OSD.batteryTypeTipo batteria rilevataenum
OSD.vpsHeightAltezza da sensore visivo/ultrasuoni (VPS)metri
OSD.motorStartFailedCauseCausa fallito avviamento motori (se applicabile)enum

GIMBAL — stato del gimbal/camera

ColonnaSignificatoUnità
GIMBAL.pitchInclinazione gimbalgradi
GIMBAL.rollRollio gimbalgradi
GIMBAL.yawImbardata gimbalgradi
GIMBAL.modeModalità gimbal (follow, FPV, free)enum
GIMBAL.isPitchAtLimit / isRollAtLimit / isYawAtLimitFine corsa raggiuntobool
GIMBAL.isStuckBlocco meccanico rilevatobool

RC — stato del radiocomando

ColonnaSignificatoUnità
RC.aileron, RC.elevator, RC.throttle, RC.rudderPosizione stick (canali principali)valore raw (es. 364–1684, centro ~1024)
RC.modePosizione dello switch di modalità voloenum
RC.goHomeStato pulsante RTHbool
RC.gimbalPitchRotellina controllo gimbalvalore raw
RC.downlinkSignal / RC.uplinkSignalQualità collegamento RC↔dronepercentuale/dB

HOME — punto di ritorno / riferimenti

ColonnaSignificatoUnità
HOME.latitude / HOME.longitudeCoordinate del punto Home registratogradi
HOME.heightAltezza di riferimento Homemetri
HOME.isHomeRecordSe l'home point è stato acquisitobool
HOME.goHomeModeModalità RTH configurataenum
HOME.maxAllowedHeightLimite di altezza impostatometri
HOME.compassCalibrationStatusStato calibrazione bussolaenum

Batteria — CENTER_BATTERY (batteria singola) / SMART_BATTERY (pacco intelligente, multi-batteria come sul M400)

ColonnaSignificatoUnità
BATTERY.relativeCapacityPercentuale di carica residua%
BATTERY.voltageTensione totale paccoVolt (mV nel raw)
BATTERY.currentCorrente istantanea (assorbimento)mA/A
BATTERY.currentTemperature / BATTERY.temperatureTemperatura celle°C
BATTERY.cellVoltage[1..n]Tensione delle singole celleVolt
BATTERY.usefulTimeAutonomia residua stimatasecondi/minuti
BATTERY.goHomeTimeTempo stimato necessario per RTHsecondi
BATTERY.landTimeTempo stimato necessario per atterraggiosecondi
BATTERY.batteryOnChargeIn carica sì/nobool
BATTERY.numberOfDischargeNumero di cicli di scaricaintero
BATTERY.serialNumberNumero seriale pacco batteriastringa

APP_GPS — GPS lato app/RadioComando

ColonnaSignificatoUnità
APP_GPS.latitude / APP_GPS.longitudePosizione GPS del dispositivo mobile/RCgradi
APP_GPS.accuracyAccuratezza stimatametri

RECOVER — metadati del volo 

ColonnaSignificato
RECOVER.droneTypeModello del drone (es. Matrice 400)
RECOVER.appTypeApp usata (DJI GO, GO 4, Fly, Pilot 2)
RECOVER.appVersionVersione app (1/2/3 = major.minor.patch)
RECOVER.aircraftSn / RECOVER.aircraftNameNumero seriale e nome assegnato al drone
RECOVER.cameraSnSeriale della camera/payload
RECOVER.rcSnSeriale del radiocomando
RECOVER.batterySnSeriale batteria
RECOVER.activeTimestampTimestamp di attivazione registrazione

DETAILS — riepilogo del volo (calcolato a fine registrazione)

ColonnaSignificatoUnità
DETAILS.latitude / DETAILS.longitudePosizione di decollogradi
DETAILS.city / DETAILS.area / DETAILS.streetGeocoding inverso della zona di volotesto
DETAILS.totalDistanceDistanza totale percorsametri
DETAILS.totalTimeDurata totale volosecondi
DETAILS.maxHeightAltezza massima raggiuntametri
DETAILS.maxHorizontalSpeedVelocità orizzontale massimam/s
DETAILS.maxVerticalSpeedVelocità verticale massimam/s
DETAILS.photoNum / DETAILS.videoTimeNumero foto scattate / tempo video registratointero / secondi

Altri tipi di record

Tipo recordContenuto
CUSTOMMetadati custom, es. CUSTOM.updateTime (timestamp locale della riga)
DEFORMStato apertura/chiusura bracci (drone pieghevole)
APP_TIP / APP_WARN / APP_SER_WARNMessaggi di avviso/errore mostrati in app durante il volo (testo)
FIRMWAREVersioni firmware dei vari componenti (FC, ESC, RC, camera, batteria...)
COMPONENTElenco componenti rilevati/collegati (per aeromobili modulari come il M400)
RC_GPS, RC_DEBUG, OFDM_DEBUG, VISION_GROUP, VISION_WARN, MC_PARAM, APP_OPERATIONFormato non documentato pubblicamente — noti solo per nome, non per struttura
JPEGMiniature/fotogrammi JPEG incorporati nel log (non telemetria)

 questi invece sono i campi completi 

CUSTOM.date [local],
CUSTOM.updateTime [local],
OSD.flyTime,OSD.flyTime [s],7
OSD.latitude,OSD.
longitude,OSD.height [ft],
OSD.heightMax [ft],
OSD.vpsHeight [ft],
OSD.altitude [ft],
OSD.mileage [ft],
OSD.hSpeed [MPH],
OSD.hSpeedMax [MPH],
OSD.xSpeed [MPH],
OSD.xSpeedMax [MPH],
OSD.ySpeed [MPH],
OSD.ySpeedMax [MPH],
OSD.zSpeed [MPH],
OSD.zSpeedMax [MPH],
OSD.pitch,OSD.roll,
OSD.yaw,OSD.yaw [360],
OSD.directionOfTravel,
OSD.flycState,
OSD.flycCommand,
OSD.flightAction,
OSD.gpsNum,
OSD.gpsLevel,
OSD.isGPSUsed,
OSD.nonGPSCause,
OSD.droneType,
OSD.isSwaveWork,
OSD.waveError,
OSD.goHomeStatus,
OSD.batteryType,
OSD.ctrlDevice,
OSD.isOnGround,
OSD.isMotorOn,
OSD.isMotorBlocked,
OSD.motorStartFailedCause,
OSD.motorFailReason,
OSD.isImuPreheated,
OSD.imuInitFailReason,
OSD.isAcceletorOverRange,
OSD.isBarometerDeadInAir,
OSD.isCompassError,
OSD.isGoHomeHeightModified,
OSD.canIOCWork,
OSD.isNotEnoughForce,
OSD.isOutOfLimit,
OSD.isPropellerCatapult,
OSD.isVibrating,
OSD.isVisionUsed,
OSD.voltageWarning,
GIMBAL.mode,
GIMBAL.pitch,
GIMBAL.roll,
GIMBAL.yaw,
GIMBAL.yaw [360],
GIMBAL.isPitchAtLimit,
GIMBAL.isRollAtLimit,
GIMBAL.isYawAtLimit,
GIMBAL.isStuck,
CAMERA.isPhoto,
CAMERA.isVideo,
CAMERA.filename,
CAMERA.sdCardIsInserted,
CAMERA.sdCardState,
RC.downlinkSignal,
RC.uplinkSignal,
RC.aileron,
RC.elevator,
RC.throttle,
RC.rudder,
RC.mode,
RC.goHomeDepressed,
RC.recordDepressed,
RC.shutterDepressed,
RC.playbackDepressed,
RC.wheelDepressed,
RC.wheelOffset,
RC.custom1Depressed,
RC.custom2Depressed,
RC.custom3Depressed,
RC.custom4Depressed,
BATTERY.chargeLevel,
BATTERY.currentPV [V],
BATTERY.currentCapacity [mAh],
BATTERY.fullCapacity [mAh],
BATTERY.voltage [V],
BATTERY.isCellVoltageEstimated,
BATTERY.cellVoltage1 [V],
BATTERY.cellVoltage2 [V],
BATTERY.cellVoltage3 [V],
BATTERY.cellVoltage4 [V],
BATTERY.cellVoltage5 [V],
BATTERY.cellVoltage6 [V],
BATTERY.cellVoltage7 [V],
BATTERY.cellVoltage8 [V],
BATTERY.cellVoltage9 [V],
BATTERY.cellVoltage10 [V],
BATTERY.cellVoltage11 [V],
BATTERY.cellVoltage12 [V],
BATTERY.maxCellVoltageDeviation,
BATTERY.isCellVoltageDeviationHigh,
BATTERY.isVoltageLow,
BATTERY.current [A],
BATTERY.temperature [F],
BATTERY.minTemperature [F],
BATTERY.maxTemperature [F],
BATTERY.usefulTime [s],
BATTERY.goHomeTime [s],
BATTERY.landTime [s],
BATTERY.goHomeBattery,
BATTERY.landBattery,
BATTERY.safeFlyRadius,
BATTERY.volumeConsume,
BATTERY.status,
BATTERY.goHomeStatus,
BATTERY.goHomeCountdown,
BATTERY.lowWarning,
BATTERY.lowWarningGoHome,
BATTERY.seriousLowWarning,
BATTERY.seriousLowWarningLanding,
BATTERY.timesCharged,
MC.failSafeAction,
MC.isObstacleAvoidanceEnabled,4
MC.isCollisionAvoidanceEnabled,
MC.isRthObstacleAvoidanceEnabled,
MC.isBraking,MC.isAvoidingObstacle,
MC.isAvoidingActiveObstacle,
MC.isAscentLimitedByObstacle,
MC.isLandingConfirmationNeeded,
MC.atLowAltitudeLimit,
MC.atDistanceLimit,
MC.atAirportAltitudeLimit,
MC.atAirportBoundary,
HOME.latitude,
HOME.longitude,
HOME.distance [ft],
HOME.height [ft],
HOME.heightLimit [ft],
HOME.isHomeRecord,
HOME.goHomeMode,
HOME.aircraftHeadDirection,
HOME.isDynamicHomePointEnabled,
HOME.isReachedLimitDistance,
HOME.isReachedLimitHeight,
HOME.isCompassCalibrating,
HOME.compassCalibrationState,
HOME.isMultipleFlightModeEnabled,
HOME.isBeginnerMode,
HOME.isIOCEnabled,
HOME.iocMode,
HOME.goHomeHeight [ft],
HOME.courseLockAngle,
HOME.forceLandingHeight [ft],
HOME.dataRecorderFileIndex,
WEATHER.windDirection,
WEATHER.windRelativeDirection,
WEATHER.windSpeed [MPH],
WEATHER.maxWindSpeed [MPH],
WEATHER.windStrength,
WEATHER.isFacingWind,
WEATHER.isFlyingIntoWind,
RECOVER.appType,
RECOVER.appVersion,
RECOVER.aircraftName,
RECOVER.aircraftSerial,
RECOVER.cameraSerial,
RECOVER.rcSerial,
RECOVER.batterySerial,
DETAILS.totalTime [s],
DETAILS.totalDistance [ft],
DETAILS.maxHeight [ft],
DETAILS.maxHorizontalSpeed [MPH],
DETAILS.maxVerticalSpeed [MPH],
DETAILS.photoNum,
DETAILS.videoTime [s],
DETAILS.aircraftName,
DETAILS.aircraftSerial,
DETAILS.cameraSerial,
DETAILS.rcSerial,
DETAILS.batterySerial,
DETAILS.appName,
DETAILS.appType,
DETAILS.appVersion,
DETAILS.guid,
SERIAL.flightController,
SERIAL.camera,
SERIAL.gimbal,
SERIAL.rc,
SERIAL.battery,
SERIAL.battery2,
APPGPS.latitude,
APPGPS.longitude,
APPGPS.accuracy,
APP.tip,
APP.warning
 

martedì 11 agosto 2026

Star Trails

 Niente stelle cadenti (orario e data sbagliata, troppo presto) ma almeno un po' di star trails. Si vede Cassiopea e la Stella Polare

 



 

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()


 

Da quota ellissoidica ad ortometrica

Per passare da quote ellissoidiche GPS a quote sul geoide ci sono varie opzioni in base al modello scelto ITG2009   Se si usa EGM 2008 si ha...