Files
Titano/Titano/Core/LocalRegression.cs
T
Alby96andClaude Opus 5 a815a91006 Sei moduli avanzati per la produzione cinematografica
Estende il motore con stabilizzazione sub-pixel, deflicker per regioni,
transizioni giorno-notte, movimento di macchina virtuale, rimappatura non
lineare del tempo e accumulo temporale. Tutto in-house, nessuna dipendenza
aggiunta: il progetto continua a non contenere un solo PackageReference.

Perche' questa forma. Il rendering non percorre piu' la sequenza sorgente ma
un piano di fotogrammi d'uscita, ognuno con posizione anche frazionaria e
durata propria. Le quattro modalita' temporali producono tutte quella stessa
forma, quindi il ciclo di rendering e' uno solo e non ha un ramo per ciascun
caso; da li' discende anche la sfocatura, perche' un fotogramma che copre v
scatti ha angolo di otturatore diviso v.

Gli spostamenti si misurano con la correlazione di fase, che ignora per
costruzione le differenze di luminosita' fra scatti - in un time-lapse ci sono
sempre - e reagisce alla sola geometria. Il picco intero non basta: la
superficie di correlazione viene ricostruita a passo fine valutando la somma
di Fourier sulle posizioni intermedie invece di interpolare con una parabola
tre campioni di una cresta che parabola non e'. L'errore misurato scende da
0,14 a 0,08 px.

I gradini di esposizione non sono rumore da mediare: l'ampiezza si legge
esatta nei metadati e viene ridistribuita su una transizione a derivata nulla
agli estremi. Il deflicker lavora poi su una serie gia' priva di gradini,
invece di trasformare lo scalino in una rampa con due spigoli.

La maschera delle regioni nasce dalla mediana temporale di un campione di
fotogrammi, che toglie di mezzo proprio le nuvole di passaggio, e la linea
d'orizzonte viene agganciata al massimo del gradiente verticale. Sulla scena
di prova il terreno passa da 0,062 a 0,026 stop di oscillazione.

Ritaglio virtuale e correzione di stabilizzazione sono entrambi affini e
vengono composti in una sola trasformazione: due ricampionamenti in fila
costerebbero il doppio di nitidezza senza dare nulla in cambio.

Sulla memoria: i moduli avanzati hanno rotto l'assunto che bastassero due
fotogrammi vivi alla volta, quindi il disco entra ora in gioco - come annotato
nel commit precedente, e' questa la porta che si apriva. La finestra attiva
resta sempre in memoria perche' la mediana ha bisogno di tutti i suoi
fotogrammi insieme; solo la lettura in anticipo viene parcheggiata su disco
oltre il tetto, e ripresa una volta sola. Non esiste un caso in cui lo stesso
fotogramma vada e torni piu' volte. Il file di parcheggio si cancella da se'.

Verifica: da 26 a 51 controlli. Nessuna soglia scelta a posteriori - il
tremolio ha un percorso noto, il gradino un'ampiezza dichiarata nei metadati e
visibile nei pixel, la nuvola attraversa il solo cielo. Il controllo
conclusivo rende una sequenza con tutti i moduli attivi insieme e tetto di
memoria volutamente stretto, poi la rilegge con il lettore di sistema.
Verificato anche sui DNG GoPro reali: 580 file, render di prova conforme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 16:55:27 +02:00

121 lines
4.1 KiB
C#

namespace Titano.Core;
/// <summary>
/// Regressione lineare locale pesata, con seconda passata robusta agli scarti anomali.
///
/// È il nucleo numerico condiviso da tutto ciò che, nel motore, deve distinguere una
/// variazione reale e lenta da un disturbo veloce: l'esposizione che sfarfalla sopra una
/// rampa di tramonto, la deriva del bilanciamento del bianco, il tremolio del treppiede
/// sopra una panoramica voluta. In tutti questi casi il segnale utile è la componente
/// liscia e il disturbo è quella ad alta frequenza.
///
/// Rispetto a una media mobile, la componente lineare della regressione segue senza ritardo
/// le rampe: una media mobile su una salita costante restituisce sempre un valore vecchio di
/// mezza finestra, e la correzione risultante spegnerebbe proprio il fenomeno da preservare.
/// </summary>
public static class LocalRegression
{
/// <summary>Costante del peso di Tukey: oltre 4.685 deviazioni il campione non pesa più nulla.</summary>
private const double TukeyConstant = 4.685;
/// <summary>
/// Lisciatura della serie su una finestra mobile di <paramref name="windowFrames"/> campioni.
/// Con <paramref name="rejectOutliers"/> si esegue una seconda passata in cui i campioni
/// lontani dalla prima stima vengono neutralizzati.
/// </summary>
public static double[] Smooth(double[] series, int windowFrames, bool rejectOutliers)
{
int n = series.Length;
var result = new double[n];
if (n == 0) return result;
if (n <= 2)
{
Array.Copy(series, result, n);
return result;
}
int radius = Math.Clamp((Math.Max(3, windowFrames) - 1) / 2, 1, Math.Max(1, n - 1));
double sigma = Math.Max(radius / 2.0, 0.5);
var robust = new double[n];
Array.Fill(robust, 1.0);
FitAll(series, result, radius, sigma, robust);
if (rejectOutliers)
{
UpdateRobustWeights(series, result, robust);
FitAll(series, result, radius, sigma, robust);
}
return result;
}
private static void FitAll(double[] y, double[] output, int radius, double sigma, double[] robust)
{
int n = y.Length;
double twoSigmaSq = 2.0 * sigma * sigma;
for (int i = 0; i < n; i++)
{
int from = Math.Max(0, i - radius);
int to = Math.Min(n - 1, i + radius);
// Sistema normale della retta pesata y = a + b·t, con t = j - i.
double sw = 0, swt = 0, swt2 = 0, swy = 0, swty = 0;
for (int j = from; j <= to; j++)
{
double t = j - i;
double w = Math.Exp(-(t * t) / twoSigmaSq) * robust[j];
if (w <= 1e-9) continue;
sw += w;
swt += w * t;
swt2 += w * t * t;
swy += w * y[j];
swty += w * t * y[j];
}
if (sw <= 1e-9)
{
output[i] = y[i];
continue;
}
double det = sw * swt2 - swt * swt;
if (Math.Abs(det) < 1e-12)
{
output[i] = swy / sw; // finestra degenere: media pesata
continue;
}
double a = (swt2 * swy - swt * swty) / det; // intercetta = valore stimato in t = 0
output[i] = a;
}
}
private static void UpdateRobustWeights(double[] y, double[] fit, double[] robust)
{
int n = y.Length;
var residuals = new double[n];
for (int i = 0; i < n; i++) residuals[i] = Math.Abs(y[i] - fit[i]);
var sorted = (double[])residuals.Clone();
Array.Sort(sorted);
double mad = sorted[n / 2];
double scale = 1.4826 * mad;
// Se la sequenza è già pulita non si scarta nulla: evita di amplificare il rumore numerico.
if (scale < 1e-4)
{
Array.Fill(robust, 1.0);
return;
}
for (int i = 0; i < n; i++)
{
double u = residuals[i] / (TukeyConstant * scale);
robust[i] = u >= 1.0 ? 0.0 : Math.Pow(1.0 - u * u, 2.0);
}
}
}