venerdì 24 aprile 2026

RTMP Stream da drone DJI

Per prima cosa, per avere uno stream dal drone si crea un server RTMP tramite il docker di Mediamtx mettendo i seguenti files nella stessa cartella 

docker-compose.yml

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

version: "3.8"
services:
  mediamtx:
    image: bluenviron/mediamtx:latest
    container_name: mediamtx
    restart: unless-stopped
    ports:
      - "8554:8554"   # RTSP
      - "8888:8888"   # HTTP / WebRTC
      - "8889:8889"   # HTTPS
      - "1935:1935"   # RTMP
    volumes:
      - ./mediamtx.yml:/mediamtx.yml:ro

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

mediamtx.yml

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

# Porte
rtmpAddress: :1935
rtspAddress: :8554
hlsAddress: :8888
webrtcAddress: :8889
apiAddress: :9997

# Configurazione HLS (bassa latenza)
hlsSegmentDuration: 1s
hlsPartDuration: 200ms
hlsSegmentMaxSize: 50MB

# Path dello stream
paths:
  mystream:
    # Permessi semplificati (versioni recenti)
    source: publisher

    # Lancia ffmpeg automaticamente quando arriva uno stream
    runOnReady: >
      ffmpeg -i rtmp://localhost:1935/mystream
      -vf fps=1 -q:v 2 /tmp/frames/frame_%04d.jpg
    runOnReadyRestart: yes
 

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

e lanciando 

docker run -d --name mediamtx -p 0.0.0.0:1935:1935 -p 8554:8554 -p 8888:8888 bluenviron/mediamtx:latest 

Lo stream si puo' visualizzare con FFMpeg direttamente sulla macchina docker 

ffplay -fflags nobuffer \
       -flags low_delay \
       -framedrop \
       -sync ext \
       rtmp://localhost:1935/luca

e si possono salvare anche i frame ad intervalli prefissati

ffmpeg -i rtmp://localhost:1935/live/mystream \ -vf fps=1 \ -q:v 2 \ /tmp/frames/frame_%04d.jpg 

per semplificare lo sviluppo si puo' usare un cellulare al posto del drone usando la app RMTP Streamer

si va nel pulsante Server e si impostano i parametri del docker server

si torna indietro e si preme Go Stream in basso a destra
 

altrimenti da DJI Flight queste sono le schermate per impostare lo stream


 


 

a questo punto puo' essere interessante uno script che legga il flusso e salva un fotogramma quando c'e' una differenza compresa tra il 65% ed il 75% dal precedente salvato in modo da poi dare in pasto a Opendronemap per la ricostruzione 

 

import cv2
import numpy as np
import os

def main():
# Inserisci qui l'indirizzo del tuo server RTMP
rtmp_url = "rtmp://192.168.1.79:1935/luca"
# Inizializzazione cattura video
cap = cv2.VideoCapture(rtmp_url)

# Buffer setting: utile per stream di rete per ridurre la latenza
cap.set(cv2.CAP_PROP_BUFFERSIZE, 3)

if not cap.isOpened():
print(f"Errore: Impossibile connettersi al server RTMP: {rtmp_url}")
return

output_dir = "catture_rtmp"
if not os.path.exists(output_dir):
os.makedirs(output_dir)

print("Connessione stabilita. In attesa del primo frame...")

# 1. Leggi e salva il primo fotogramma come riferimento iniziale
ret, first_frame = cap.read()
if not ret:
print("Errore: Impossibile ricevere dati dallo stream.")
return

# Funzione di utility per preparare l'immagine al confronto
def prepare_frame(f):
gray = cv2.cvtColor(f, cv2.COLOR_BGR2GRAY)
return cv2.GaussianBlur(gray, (21, 21), 0)

ref_gray = prepare_frame(first_frame)
count = 0
# Salva il primo riferimento
cv2.imwrite(f"{output_dir}/ref_iniziale.jpg", first_frame)
print("Primo frame salvato. Monitoraggio differenze (65%-75%) avviato...")

try:
while True:
ret, frame = cap.read()
if not ret:
print("Perdita del segnale RTMP...")
break

# Preparazione frame corrente
current_gray = prepare_frame(frame)

# 2. Calcolo della differenza
# absdiff calcola la differenza assoluta pixel per pixel
diff_frame = cv2.absdiff(ref_gray, current_gray)
# Applichiamo una soglia: se la differenza di un pixel è > 25, diventa bianco (255)
_, thresh = cv2.threshold(diff_frame, 25, 255, cv2.THRESH_BINARY)
# 3. Calcolo percentuale di cambiamento
# Rapporto tra pixel bianchi e numero totale di pixel
change_pixels = np.count_nonzero(thresh)
total_pixels = thresh.size
diff_percentage = (change_pixels / total_pixels) * 100

# 4. Logica di salvataggio e aggiornamento riferimento
if 65.0 <= diff_percentage <= 75.0:
count += 1
filename = f"{output_dir}/cambiamento_{count}.jpg"
cv2.imwrite(filename, frame)
# Il frame attuale diventa il nuovo riferimento
ref_gray = current_gray
print(f"[{count}] Salvato! Differenza: {diff_percentage:.2f}%. Nuovo riferimento impostato.")

# Visualizzazione a schermo
cv2.putText(frame, f"Diff: {diff_percentage:.1f}%", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.imshow("RTMP Monitor - Press 'q' to stop", frame)

if cv2.waitKey(1) & 0xFF == ord('q'):
break

except KeyboardInterrupt:
print("\nChiusura forzata dall'utente.")
finally:
cap.release()
cv2.destroyAllWindows()
print("Risorse liberate correttamente.")

if __name__ == "__main__":
main()


Per procedere da linea di comando alla creazione di un modello 3d si puo' usare il docker di opendronemap (le impostazioni sono volutamente per la creazione di un modello speditivo da usare in campagna...per il perfezionamento si puo' fare in ufficio(

docker run -ti --rm \
  -v /home/luca/Downloads/immagini:/datasets/immagini \
  opendronemap/odm \
  --project-path /immagini \
  --pc-quality low \
  --pc-sample 0.01 \
  --resize-to 2000 \
  --feature-quality low \
  --skip-orthophoto
 

 

 

Convertire USGS Spectral LIbrary in formato Envi

 

 


import os
import pandas as pd
import numpy as np
import spectral.io.envi as envi

def create_envi_library(input_folder, output_filename):
spectra_data = []
names = []
wavelengths = None

files = sorted([f for f in os.listdir(input_folder) if f.endswith('.txt')])

if not files:
print("No .txt files found in the directory!")
return

for file in files:
file_path = os.path.join(input_folder, file)
try:
df = pd.read_csv(file_path, sep=r'\s+', comment='#', header=None, engine='python')
# Clean up: ensure we only have numeric rows
df = df[pd.to_numeric(df[0], errors='coerce').notnull()]
df = df.astype(float)
if wavelengths is None:
wavelengths = df[0].values
spectra_data.append(df[1].values)
names.append(os.path.splitext(file)[0])
print(f"Processed: {file}")
except Exception as e:
print(f"Skipping {file}: {e}")


lib_array = np.array(spectra_data).astype(np.float32)
lib_array = lib_array.reshape((lib_array.shape[0], 1, lib_array.shape[1]))
lib_array[lib_array < -100] = 0
metadata = {
'file type': 'ENVI Spectral Library',
'wavelength': list(wavelengths.astype(str)),
'wavelength units': 'Micrometers',
'spectra names': names,
'data type': 4, # Float32
'interleave': 'bsq'
}


output_path = output_filename if output_filename.endswith('.sli') else output_filename + '.sli'
envi.save_image(output_path + '.hdr', lib_array, metadata=metadata, force=True)
print(f"\nSuccess! Created {output_path} and associated header.")

# Usage
input_dir = '/home/luca/Downloads/ASCIIdata/ASCIIdata_splib07b_cvASD/ChapterM_Minerals'
output_name = 'USGS_splib07b_ASD_Library'
create_envi_library(input_dir, output_name)



Apire immagini Emit in Enmap Box

Per visualizzare immagini Emit in formato netcdf non si deve passare dal menu ma si deve aprire Processing Toolbox e cercare emit (vedi immagine_

  

Gli spettri Emit sono molto puliti




Raster Math iin Enmap-Box

Per utilizzare le formule con le bande in Enmap Box si deve prima aprire 

Processing Toolbox -> Raster Analysis -> Raster Math 

 




In parameter si scrive la formula . Quella di seguito calcola la profondita' di picco normalizzata per il crisotilo sull'assorbimento a 2325 nm (Banda 117) con spalla destra 2390 nm (Banda 126) e spalla sinistra a 2230 nm (Banda 106)

Attenzione : si deve inserire il raster in Raster layer mapped to R1. Qui il numero delle bande non inizia da 1 ma da zero quindi l'indice per la banda 117 sara' R1[116]

 

R_cont = R1[105] + (2325.0 - 2230.0) / (2390.0 - 2230.0) * (R1[125] - R1[105])
NBD_chrysotile = 1.0 - (R1[116] / R_cont) 

 


 

domenica 19 aprile 2026

Pleiadi

Pensavo non fosse possibile fotografare le Pleiaidi da Firenze, mentre scendevano all'orizzonte subito dopo il tramonto e con una reflex..ad occhio nudo non erano visibili..ed invece se ne vedono 9 (f/20, 8 sec, 3200 iso, focale 300 mm, Canon Eos 500D)

 



L'immagine successiva per riferimento (fonte Nasa) 

  


domenica 12 aprile 2026

Color fringing

In alcune immagini di Google Maps ci sono degli aloni in particolare sugli aerei (color fringing)

Questo e' dovuto al fatto che il sensore del satellite non acquisisce in contemporanea tutte le bande ma in successione con uno sfasamento di pochi centesimi di secondo.. 

Questo metodo puo' essere impiegato per la stima della velocita'


Maciej Adamiak, Yulia Grinblat, Julian Psotta, Nir Fulman, Himshikhar Mazumdar, Shiyu Tang, Alexander Zipf, Deep learning enhanced road traffic analysis: Scalable vehicle detection and velocity estimation using PlanetScope imagery, International Journal of Applied Earth Observation and Geoinformation, Volume 142,2025,104707,ISSN 1569-8432,
https://doi.org/10.1016/j.jag.2025.104707.

In questa immagine del dataset pubblico di WorlView (pansharpening a 30 cm di 4 bande multispettrali di Monaco OR2A_30cm_4-Band_Pansharpened _Munich-Germany le macchine non mostrano alone 

 
Indicativamente il delta di tempo tra due bande di WordVIew e' tra 1 e 2 centesimi di secondo ..per avere uno spostamento significativo si devono prendere in considerazione almeno 2 pixels quindi la velocita' del veicolo deve essere in movimento ad almeno 140 Km/h..in condizioni ottimali si puo' scendere a 110 Km/h

 

 

 

 

 

 

Lenovo IX2 Iomega Storcenter

Mi sono comprato usato un Iomega Storcenter (che si presenta sulla rete come un Lenovo IX2) come NAS da battaglia (1.8 Tb). Per usarlo su Linux queste sono le impostazione

sudo apt update
sudo apt install cifs-utils 

sudo mount -t cifs //192.168.1.100/backups /media/iomega/backups -o username=admin,vers=1.0

sudo mount -t cifs //192.168.1.100/documents /media/iomega/documents -o username=admin,vers=1.0



e cosi' via seguendo l'immagine sottostante

 


 

Bande spettrali Plastiche (Hyperspectral Reflectance Database of Plastic Debris for River Ecosystems)

Premesso che in laboratorio funziona sempre tutto mentre nel mondo reale non funziona piu' nessun modello in modo accettabile e' interessante il database Hyperspectral Reflectance Database of Plastic Debris for River Ecosystems che si trova all'indirizzo https://zenodo.org/records/13377060 mentre il codice si trova https://github.com/olyae001/Hyperspectral_reflectance_library

Olyaei, M., Ebtehaj, A., & Ellis, C. R. (2024). A Hyperspectral Reflectance Database of Plastic Debris for River Ecosystems [Data set]. Zenodo. https://doi.org/10.5281/zenodo.13377060 

Si tratta di misure di laboratorio di diversi materiali plastici (piu' o meno degradati) in fiumi simulati piu' o meno torbidi.. I dati sono in formato netcdf e dentro sono compresi spettri   

 

 



In github e' compreso lo script Statistical_analysis_Xgboost_Colab.ipynb che permette di calcolare il peso di ogni banda nel modello Xgboost

 Come era prevedibile la banda piu' significativa cade nello SWIR ma c'e' segnale anche nel visibile 

1) 1178 nm 
2) 611 nm
3) 676nnm
4) 559 nm 
5) 677 nm
6) 1206 nm 
[[1.17800000e+03 8.47618878e-02]
 [6.11000000e+02 4.13786322e-02]
 [6.76000000e+02 3.15730013e-02]
 [5.59000000e+02 2.42403690e-02]
 [6.77000000e+02 2.01107953e-02]
 [1.20600000e+03 1.70316305e-02]
 [4.44000000e+02 1.70264784e-02]
 [2.16600000e+03 1.45021593e-02]
 [2.41100000e+03 1.36355786e-02]
 [1.17300000e+03 1.23202708e-02]]

 

 

Questa la matrice di confusione che indica una ottima performance del modello

Il problema e' io ho in uso una camera iperspettrale da drone da 400 a 1000 nm (quindi al di fuori del range della feature spettrale ottimale(...vediamo cosa succede se si usano gli stessi spettri ma limitando tra 400 e 1000 nm la finestra

Tra 600 e 700 nm sono concentrate numerose bande diagnostiche

ma usando solo la parte VNIR il modello peggiora in modo sensibile

Se al posto di XGBoost gli stessi dati vengono processati con Random Forest viene confermato come la parte piu' significative delle plastiche sia concentrata tra 650 e 700 nm 

 Il problema e' che tra 650 e 700 ci sono concentrati molti segnali delle alghe (picco di fluorescenza a 685 nm, assorbimento dei cianobatteri a 620 nm. molto vicino l'inizio del Red Edge a 700 nm). In  condizioni reali questi potrebbe essere le maggiori cause di disturbo

Questo lo script per estrarre le immagini e gli spettri dai file netcdf

import os
import numpy as np
import pandas as pd
import glob
import matplotlib
matplotlib.use('Agg') # Non-interactive backend for batch saving
import matplotlib.pyplot as plt
import netCDF4
from tabulate import tabulate

# ── Paths ────────────────────────────────────────────────────────────────────
current_directory = os.getcwd()
print("Current directory:", current_directory)

parent_directory = os.path.dirname(current_directory)

subfolder = "openaire/data_NETCDF"
datapath_file = os.path.join(parent_directory, subfolder)
print("Path of data files:", datapath_file)

# ── Output directories ────────────────────────────────────────────────────────
output_root = os.path.join(parent_directory, "openaire", "exported_data")

POLYMERS = ['PET', 'HDPE', 'LDPE', 'PP', 'EPSF', 'Mix', 'Weathered']
BACKGROUNDS = {'C': 'Clear', 'T': 'Turbid', 'F': 'Foamy'}

# Pre-create all output subdirectories polymer / background
for polymer in POLYMERS:
for bg_key, bg_name in BACKGROUNDS.items():
folder = os.path.join(output_root, polymer, bg_name)
os.makedirs(folder, exist_ok=True)

# Fallback folder for files that don't match known categories
unknown_folder = os.path.join(output_root, "Unknown")
os.makedirs(unknown_folder, exist_ok=True)

# ── Discover & sort files ─────────────────────────────────────────────────────
netcdf_files = glob.glob(os.path.join(datapath_file, '*.nc4'))

files_number = [int(os.path.basename(f).split('_')[0][1:]) for f in netcdf_files]
sortedlist = [os.path.basename(f)
for _, f in sorted(zip(files_number, netcdf_files))]

print(f"\nFound {len(sortedlist)} measurement files.\n")

# ── Summary table (same as original) ─────────────────────────────────────────
polymer_counts = {}
background_counts = {}

for polymer in POLYMERS:
polymer_counts[polymer] = len([x for x in sortedlist if polymer in x])

for bg_key in BACKGROUNDS:
background_counts[bg_key] = len(
[x for x in sortedlist if bg_key == x.split('_')[2][0]]
)

table_data = []
for polymer in POLYMERS:
row = [polymer]
for bg_key in ['C', 'T', 'F']:
combo = [x for x in sortedlist
if polymer in x and bg_key == x.split('_')[2][0]]
row.append(len(combo))
row.append(polymer_counts[polymer])
table_data.append(row)

table_data.append(["-----"] * 5)
table_data.append(
['Sum'] + [background_counts[bg] for bg in ['C', 'T', 'F']]
+ [sum(background_counts.values())]
)

headers = ['Polymer'] + [BACKGROUNDS[bg] for bg in ['C', 'T', 'F']] + ['Sum']
print(tabulate(table_data, headers, tablefmt='pretty'))
print()

# ── Wavelength axis (shared across all files) ─────────────────────────────────
# Will be computed from the first file's reflectance length; reused for all.
wavelengths = None

# ── Main export loop ──────────────────────────────────────────────────────────
plt.rcParams["font.family"] = "Times New Roman"
plt.rcParams["font.size"] = 14

skipped = []
exported = 0

for idx, filename in enumerate(sortedlist, start=1):
filepath = os.path.join(datapath_file, filename)

try:
nc_file = netCDF4.Dataset(filepath)

# ── Read metadata ─────────────────────────────────────────────────────
polymer = getattr(nc_file, 'Debris Polymer', 'Unknown')
background = getattr(nc_file, 'Background flow status', 'Unknown')
plastic_frac = getattr(nc_file, 'Plastic fraction(%)', 'N/A')

# ── Read variables (force load into numpy arrays) ─────────────────────
reflectance_data = nc_file.variables['Reflectacne'][:] # note: typo preserved to match file
rgb_var = nc_file.variables['RgbImage'][:]
# label_var = nc_file.variables['LabeledImage'][:] # uncomment if needed

nc_file.close()

# ── Wavelength axis ───────────────────────────────────────────────────
if wavelengths is None or len(wavelengths) != len(reflectance_data):
wavelengths = np.linspace(350, 2500, len(reflectance_data))

# ── Determine output subfolder ────────────────────────────────────────
matched_polymer = next((p for p in POLYMERS if p in filename), None)
matched_bg_key = next(
(bg for bg in ['C', 'T', 'F']
if bg == filename.split('_')[2][0]),
None
)

if matched_polymer and matched_bg_key:
out_folder = os.path.join(
output_root,
matched_polymer,
BACKGROUNDS[matched_bg_key]
)
else:
out_folder = unknown_folder

# ── Shared base filename (strip extension) ────────────────────────────
base_name = os.path.splitext(filename)[0] # e.g. "O001_PET_C_..."

# ── Save reflectance CSV ──────────────────────────────────────────────
csv_path = os.path.join(out_folder, base_name + '.csv')
df = pd.DataFrame({
'wavelength_nm': wavelengths,
'reflectance': reflectance_data.flatten(),
})
# Prepend metadata columns so the CSV is self-describing
df.insert(0, 'observation', idx)
df.insert(1, 'filename', filename)
df.insert(2, 'polymer', polymer)
df.insert(3, 'background', background)
df.insert(4, 'plastic_fraction', plastic_frac)
df.to_csv(csv_path, index=False)

# ── Save RGB image ────────────────────────────────────────────────────
rgb_image_data = np.transpose(rgb_var, (2, 1, 0)) # match MATLAB order

img_path = os.path.join(out_folder, base_name + '_rgb.png')

fig, ax = plt.subplots(figsize=(8, 6))
ax.imshow(rgb_image_data)
ax.set_xticks([])
ax.set_yticks([])
ax.set_title(
f"O#{idx} | Polymer: {polymer} | Background: {background}"
f" | Plastic fraction: {plastic_frac}%",
fontsize=11
)
fig.tight_layout()
fig.savefig(img_path, dpi=150, bbox_inches='tight')
plt.close(fig)

# ── Save reflectance spectrum ─────────────────────────────────────────
spectrum_path = os.path.join(out_folder, base_name + '_spectrum.png')

fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(wavelengths, reflectance_data.flatten(), linewidth=2.0,
color='steelblue', label='Reflectance')
ax.set_xlabel('Wavelength (nm)')
ax.set_ylabel('Reflectance')
ax.set_xlim(350, 2500)
ax.set_title(
f"O#{idx} | Polymer: {polymer} | Background: {background}"
f" | Plastic fraction: {plastic_frac}%",
fontsize=11
)
ax.grid(True, which='major', linestyle='--', color='gray', alpha=0.7)
ax.legend(fontsize=12)
fig.tight_layout()
fig.savefig(spectrum_path, dpi=150, bbox_inches='tight')
plt.close(fig)

exported += 1
print(f"[{idx:>4}/{len(sortedlist)}] Saved: {base_name}_rgb.png | _spectrum.png | .csv")

except Exception as e:
print(f"[{idx:>4}/{len(sortedlist)}] SKIPPED {filename}: {e}")
skipped.append((filename, str(e)))

# ── Final report ──────────────────────────────────────────────────────────────
print(f"\n{'='*60}")
print(f"Export complete: {exported} files saved, {len(skipped)} skipped.")
if skipped:
print("\nSkipped files:")
for fname, reason in skipped:
print(f" {fname}: {reason}")
print(f"\nOutput root: {output_root}")


 

RTMP Stream da drone DJI

Per prima cosa, per avere uno stream dal drone si crea un server RTMP tramite il docker di Mediamtx mettendo i seguenti files nella stessa c...