mercoledì 14 giugno 2023

Aruco calibration file format

 Ho provato a vedere se effettuare la calibrazione di tag Aruco con immagini in formato raw lossless od in formato jpg (lossy) poteva influenzare l'errore finale




Per fare cio' ho preso un telefono Android che permette di salvare la stessa immagine in formato DNG ed in formato JPG 

Lo script di calcolo e' il seguente

import numpy as np
import cv2
import cv2.aruco as aruco

# Aruco parameters
aruco_dict = aruco.Dictionary_get(aruco.DICT_4X4_50)
aruco_params = aruco.DetectorParameters_create()

# Chessboard parameters
num_chessboard_corners_horizontal = 9
num_chessboard_corners_vertical = 6
chessboard_square_size = 0.025  # in meters

# Image directory and file extension
image_directory = 'path_to_images_directory/'
image_extension = '.jpg'

# Arrays to store object points and image points
obj_points = []  # 3D points in real-world coordinates
img_points = []  # 2D points in image plane

# Generate chessboard object points
chessboard_size = (num_chessboard_corners_horizontal,
num_chessboard_corners_vertical)
objp = np.zeros((num_chessboard_corners_horizontal *
num_chessboard_corners_vertical, 3), np.float32)
objp[:, :2] = np.mgrid[0:num_chessboard_corners_horizontal,
0:num_chessboard_corners_vertical].T.reshape(-1, 2)
objp = objp * chessboard_square_size

# Iterate through images and find chessboard corners
image_count = 0
while True:
    # Load image
    image_path = image_directory + str(image_count) + image_extension
    image = cv2.imread(image_path)
    if image is None:
        break

    # Convert to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # Find chessboard corners
    ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)

    # If corners found, add object points and image points
    if ret == True:
        obj_points.append(objp)
        img_points.append(corners)

        # Draw and display corners
        cv2.drawChessboardCorners(image, chessboard_size, corners, ret)
        cv2.imshow('Chessboard Corners', image)
        cv2.waitKey(500)

    image_count += 1

cv2.destroyAllWindows()

# Perform Aruco calibration
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points,
img_points, gray.shape[::-1], None, None)

# Calculate reprojection error
mean_error = 0
for i in range(len(obj_points)):
    img_points_proj, _ = cv2.projectPoints(obj_points[i], rvecs[i],
tvecs[i], mtx, dist)
    error = cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2) /
len(img_points_proj)
    mean_error += error

calibration_error = mean_error / len(obj_points)
print("Calibration error: {}".format(calibration_error))

# Estimate additional error
reproj_error = 0
total_points = 0
for i in range(len(obj_points)):
    img_points_proj, _ = cv2.projectPoints(obj_points[i], rvecs[i],
tvecs[i], mtx, dist)
    error = cv2.norm(img_points[i], img_points_proj, cv2.NORM_L2)
    reproj_error += error
    total_points += len(obj_points[i])

additional_error = reproj_error / total_points
print("Additional error: {}".format(additional_error))

I risultati sono i seguenti (per i file DNG ho usato la libreria rawpy)

JPG

python calibration.py --dir ./jpg/ --square_size 0.015  -w 9 -t 6
Calibration error: 0.13602731796497697
Additional error: 0.13602731796497697

DNG

Calibration error: 0.13606447706803765

Additional error: 0.13606447706803765

In conclusione non si ha una significativa differenza di errore nell'usare JPG o DNG

giovedì 1 giugno 2023

Match Histogram

Un esempio per normalizzare l'istogramma tra due immagini riprese a differente orari con ovvie differenze di illuminazione mediante la funzione match_histogram di Scikit (attenzione che nelle versioni precedenti la funzione era definita in modo differente per i parametri)


Immagine corretta. Si usa la prima come reference e la seconda come immagine da correggere


 Questi gli istogrammi prima e dopo l'algoritmo


from skimage import exposure
import matplotlib.pyplot as plt
import argparse
import cv2
ap = argparse.ArgumentParser()
ap.add_argument("-s", "--source", required=True,
help="path to the input source image")
ap.add_argument("-r", "--reference", required=True,
help="path to the input reference image")
args = vars(ap.parse_args())

print("[INFO] loading source and reference images...")
src = cv2.imread(args["source"])
ref = cv2.imread(args["reference"])
print("[INFO] performing histogram matching...")
multi = True if src.shape[-1] > 1 else False
matched = exposure.match_histograms(src, ref, channel_axis=-1)
cv2.imshow("Source", src)
cv2.imshow("Reference", ref)
cv2.imshow("Matched", matched)
cv2.imwrite("matched.jpg",matched)
cv2.waitKey(0)

(fig, axs) = plt.subplots(nrows=3, ncols=3, figsize=(8, 8))
for (i, image) in enumerate((src, ref, matched)):
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
for (j, color) in enumerate(("red", "green", "blue")):
(hist, bins) = exposure.histogram(image[..., j],
source_range="dtype")
axs[j, i].plot(bins, hist / hist.max())
(cdf, bins) = exposure.cumulative_distribution(image[..., j])
axs[j, i].plot(bins, cdf)
axs[j, 0].set_ylabel(color)

axs[0, 0].set_title("Source")
axs[0, 1].set_title("Reference")
axs[0, 2].set_title("Matched")
plt.tight_layout()
plt.savefig("grafico.png")

 

mercoledì 31 maggio 2023

Batimetria Venezia da Landsat con Decision Tree

Visto lo scarso successo del metodo analitico visto qui ho provato un metodo alternativo citato spesso il letteratura che prevede la determinazione della batimetria da dati ottici usando la rete neurale decision tree

Per cambiare ho preso i dati Landsat sulla Laguna di Venezia (la batimetria della Laguna di Venezia e' molto piu' variabile rispetto a quella della lagunan di Orbetello) usando come dato di verita' a terra questo geotiff http://cigno.ve.ismar.cnr.it/layers/geonode%3Alag02_okart_export

Su Qgis sono stati importate le bande RGB,NIR di Landsat8  e dopo avere creato una griglia regolare di punti spaziati di 30 m e' stata estratta una tabella con i valori di riflettanza per ogni banda e la profondita'


 Una volta ottenuto il file CSV i dati di batimetria sono stati divisi in classi di 0.5 m per rendere possibile la successiva elaborazione tramite un semplice script in GO 


package main

import (
    "encoding/csv"
    "fmt"
    "io"
    "log"
    "math"
    "os"
    "strconv"
)

func main() {
    f, err := os.Open("tree.csv")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    csvReader := csv.NewReader(f)
    for {
        rec, err := csvReader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatal(err)
        }
        tt, _ := strconv.ParseFloat(rec[0], 'f')
        t := math.Abs(tt)
        fmt.Print(math.Floor(t / 0.5))
        fmt.Println("," + rec[1] + "," + rec[2] + "," + rec[3] + "," + rec[4])

    }
}


il file finale ha un formato del tipo

 classe,red,nir,gren,blue
3,6707,5566,8241,9397
3,6714,5575,8221,9375
3,6696,5573,8184,9369
3,6665,5577,8144,9331
3,6638,5584,8089,9287
3,6636,5568,8080,9281

In totale sono emerse 23 classi per un totale di 16000 punti


Come si vede la distribuzione e' fondamentalmentre asimmetrica con le prima 4 classi che rappresentano la gran parte dei dati. E' stato effettuato un taglio dei dati alle prime 6 classi

 Il file CSV e' stato utilizzato  all'interno dello script seguente in Colab (si tratta di un semplice adattamento del programma di esempio di Tensorflow per la rete  decision tree)


# -*- coding: utf-8 -*-
"""decisiontree_venezia.ipynb

Automatically generated by Colaboratory.

Original file is located at
    https://colab.research.google.com/drive/1f8HLdjHiDNvpplKzNOVrXuIXrKTay0TS
"""

!pip install tensorflow_decision_forests

import numpy as np
import pandas as pd
import tensorflow_decision_forests as tfdf

path = "/content/classi_nobat_ridotto.csv"
pandas_dataset = pd.read_csv(path)

# Display the first 3 examples.
pandas_dataset.head(3)

np.random.seed(1)
# Use the ~10% of the examples as the testing set
# and the remaining ~90% of the examples as the training set.
test_indices = np.random.rand(len(pandas_dataset)) < 0.1
pandas_train_dataset = pandas_dataset[~test_indices]
pandas_test_dataset = pandas_dataset[test_indices]

print("Training examples: ", len(pandas_train_dataset))

print("Testing examples: ", len(pandas_test_dataset))

tf_train_dataset = tfdf.keras.pd_dataframe_to_tf_dataset(pandas_train_dataset, label='classe')
model = tfdf.keras.CartModel()
model.fit(tf_train_dataset)

tfdf.model_plotter.plot_model_in_colab(model, max_depth=10)

model.compile("accuracy")
print("Train evaluation: ", model.evaluate(tf_train_dataset, return_dict=True))

tf_test_dataset = tfdf.keras.pd_dataframe_to_tf_dataset(pandas_test_dataset, label='classe')
print("Test evaluation: ", model.evaluate(tf_test_dataset, return_dict=True))

from keras.utils import plot_model

plot_model(
    model,
    to_file='model.png',
    show_shapes=False,
    show_dtype=False,
    show_layer_names=True,
    rankdir='TB',
    expand_nested=False,
    dpi=96,
    layer_range=None,
    show_layer_activations=False,
    show_trainable=False
)

!pip install keras-tuner

import keras_tuner as kt

def build_model(hp):
  model = tfdf.keras.CartModel(
      min_examples=hp.Choice("min_examples",
          # Try four possible values for "min_examples" hyperparameter.
          # min_examples=10 would limit the growth of the decision tree,
          # while min_examples=1 would lead to deeper decision trees.
         [1, 2, 5, 10]),
      validation_ratio=hp.Choice("validation_ratio",
         # Three possible values for the "validation_ratio" hyperparameter.
         [0.0, 0.05, 0.10]),
      )
  model.compile("accuracy")
  return model

tuner = kt.RandomSearch(
    build_model,
    objective="val_accuracy",
    max_trials=10,
    directory="/tmp/tuner",
    project_name="tune_cart")

tuner.search(x=tf_train_dataset, validation_data=tf_test_dataset)
best_model = tuner.get_best_models()[0]

print("Best hyperparameters: ", tuner.get_best_hyperparameters()[0].values)
# >> Best hyperparameters:  {'min_examples': 2, 'validation_ratio': 0.0}

model = tfdf.keras.CartModel(min_examples=2, validation_ratio=0.0)
model.fit(tf_train_dataset)

model.compile("accuracy")
print("Test evaluation: ", model.evaluate(tf_test_dataset, return_dict=True))

tfdf.model_plotter.plot_model_in_colab(model, max_depth=10)

Training examples: 13463 Testing examples: 1520


 



al termine si ha una accuracy della porzione di dati di test superiore al 70%

14/14 [==============================] - 1s 8ms/step - loss: 0.0000e+00 - accuracy: 0.7761 Train evaluation: {'loss': 0.0, 'accuracy': 0.7761271595954895} 2/2 [==============================] - 0s 19ms/step - loss: 0.0000e+00 - accuracy: 0.7388 Test evaluation: {'loss': 0.0, 'accuracy': 0.7388157844543457} 



In generale la rete nel caso di incertezza di una classe seleziona sempre classi contingue

 

giovedì 25 maggio 2023

Batimetria Sentinel 2 Laguna Orbetello

 Aggiornamento : forse ho trovato il motivo per il quale non riuscivo a trovare correlazione tra batimetria e dato di controllo. Il grafico sottostante e' stato ricavato da dati Landsat ma con la stessa metodologia che ho provato. Come si vede la correlazione si sviluppa a partire da 4 m di profondita, per profondita' inferiore l'indice non e' utilizzabile. La laguna di Orbetello ha profondita' praticamente ovunque inferiore ai 3 m

 


 

BathymetryMapping Using Landsat 8 Satellite Imagery Jagalingam P1, Akshaya B J1and Arkal Vittal Hegde doi: 10.1016/j.proeng.2015.08.326

 

Ho provato a seguire il tutorial https://www.youtube.com/watch?v=xD1VQbnfasw&t=1923s per vedere se riuscito a trovare una correlazione tra il dato telerilevato ed i dati Lidar di batimetria della Laguna di Orbetello 

Sono partito dall'immagine S2A_MSIL2A_20220806T100611_N0400_R022_T32TPN_20220806T162658, ho effettuato un subset solo sull'area di interesse ed effettuato un resampling per avere le bande 2,3,4 ed 8 tutte a 10 m (Raster/Geometric/Resampling)


 

A questo punto si maschera il terreno mediante Raster/Band Math e creando una formula con

if  B8 > 0.05 then NaN else 1


una volta ottenuta questa maschera si moltiplicano le bande B2,B3,B4,B8 (sempre tramite band math) per il valore della maschera


A questo punto si deve passare alla Sun Glint Correction (questa e' la procedura manuale..nel Plugin Sen2Coral di SNAP in Optical/Thematic Water Processing/Sen2Coral/Processing Modules/Deglint Processor)

Si creano dei poligoni (o si importa uno shapefile poligonale) 

Si crea quindi uno scatterplot (mediante il tool di Snap) indicando come ROI Mask il file poligonale, come X la banda 8 e come Y le altre bande (2,3,4) e si clicca Refresh


si osserva una correlazione (molto blanda nel mio caso, nel tutorial e' molto migliore). Si clicca sul grafico  e si seleziona Copy Data to Clipboard)

Ho provato con una immagine della Laguna di Venezia ma per assurdo i risultati sono stati ancora peggiori


 

 

Si copiano i dati in Libreoffice od in R e si calcola retta di correlazione tra le bande

si ripete la correlazione tra B8 e B2,B3, B4 estraendo le rette di calibrazione

Si riapre il Raster Calculator e si rimuove il Sun Glint tramite

B2_Land_Mask -  m*( B8_Land_Mask-c)

m = coefficiente angolare della retta di correlazione

c = valore minimo  della banda B2_Land_Mask che si puo' ricavare dall'istogramma


 
 Le nuove bande saranno B2_Deglint, B3_Deglint e B4_Deglint. Il composit sara' l'immagine sottostante


Ultimo passo. Dal Deglint si passa alla DOS sottraendo al Deglint il valore minimo sempre mediante l'istogramma

B3DOS = B3_Deglint - 0.003

Alla fine si puo' applicare la correlazione di Stumph Linear Ratio Model (Stumpf, R. P., Holderied, K., & Sinclair, M. (2003). Determination of water depth with high-resolution satellite imagery over variable bottom
types. Limnology and Oceanography, 48(1), 547–556.)

log(1000 *B3DOS)/log(1000 *B2DOS)

si esporta la mappa in Geotiff e la si importa in QGis insieme allo shape della batimetria misurata tramite Lidar per verificare una eventuale correlazione tra dato telerilevato ottico  e dato reale

 


Tramite il plugin Point Sampling Tool si crea una tabella in cui sono riportate alla stessa posizione geografica il valore dell'indice telerilevato ed il dato di batimetria Lidar (i due layers devono essere nello stesso sistema di riferimento). Effettuando uno scatterplot (sono circa 230.000 punti e sono stati elaborati in R per difficolta' di Calc)

rm()
dati <- read.csv("C:\\Users\\l.innocenti\\Desktop\\orbetello_L2A\\confronto\\confronto.csv")


y <- dati$field_3
x <- dati$step1_resampled_extractor

plot(x,y, main="Batimetria vs Sentinel", xlab="Indice SDB", ylab="Batimetria (m)",pch=20, xlim=c(0.85,1.0))

model <- lm(y ~ x, data = dati)
summary(model)


il grafico del risultato finale ... sostanzialmente un insuccesso





mercoledì 17 maggio 2023

Plugin OrfeoToolbox in QGis Windows

 Per utilizzare Orfeo Toolbox direttamente dentro QGis si spacchetta lo zip di Orfeo Toolbox in una directory (con il nome senza caratteri speciali). Poi si seleziona strumenti di processing/Options /Programmi e si popolano i campi 

Cartella OTBC:/OTB-7.4.1-Win64/OTB-7.4.1-Win64

Cartella delle applicazioni OTB C:/OTB-7.4.1-Win64/OTB-7.4.1-Win64/bin;C:/OTB-7.4.1-Win64/OTB-7.4.1-Win64/lib/otb/applications




Pansharpening Landsat con Qgis

 

Si caricano i raster delle bande 2,3,4 (BGR) e la banda 8 pancromatica

Si apre il menu Raster/Miscellanea/Crea Raster Virtuale selezionando come input le bande ottiche e spuntando il flag Place each input file in a separate band

A questo punto dal toolbox si seleziona pansharpening indicando come Dataset spettrale il raster virtuale creato in precedenza e come dataset pancromatico la banda 8




Debugger integrato ESP32S3

Aggiornamento In realta' il Jtag USB funziona anche sui moduli cinesi Il problema risiede  nell'ID USB della porta Jtag. Nel modulo...