Correzioni emerse provando i moduli avanzati sui DNG reali
I sei moduli passavano cinquantuno controlli su scene sintetiche. Le scene sintetiche pero' sono costruite perche' la risposta sia nota, e per questo non possono sorprendere. Provati su quattro sequenze vere - da 554 a 1005 scatti, riprese notturne di comete e aurore con una GoPro - hanno mostrato cinque difetti, nessuno dei quali visibile prima. Uno sta nel nucleo del deflicker e c'era da molto. La curva obiettivo poteva essere piu' a scatti del segnale che lisciava. La robustezza di Tukey presuppone anomalie sparse; dove invece un tratto contiguo si discosta - il crollo di luce del crepuscolo - azzera l'intera finestra, e la stima commutava fra l'usare i soli sopravvissuti e l'usare tutto. Nel punto di commutazione si apriva uno scalino di 2,3 stop, piu' grande di qualunque salto presente nel segnale che doveva lisciare. Il rimedio non e' scegliere meglio fra le due stime ma non scegliere affatto: ora si mescolano con continuita' secondo quanta finestra e' sopravvissuta, e dove sopravvive per intero il risultato resta identico a prima. Sulla ripresa di sedici ore la riduzione dello sfarfallio passa dal 17% al 67% e il salto massimo da 1,73 a 0,21 stop. La correlazione di fase inseguiva spostamenti inventati. Una ripresa attraversa il giorno con pose da trenta secondi, quindi esce bruciata: su un riquadro uniforme la normalizzazione al modulo unitario amplifica il solo rumore numerico e l'antitrasformata da' un picco qualunque. Con un controllo di tessitura il tremolio misurato scende da 9,8 a 0,07 px e il ritaglio richiesto dal 15% allo 0,1%. La ricerca dell'orizzonte presupponeva un cielo chiaro e sgombro sopra la testa. Sotto un pergolato agganciava il bordo del tetto e chiamava cielo le travi. Ora la linea e' il gradino piu' marcato del profilo di luminanza per riga, che del verso non si cura: cielo dal 29% al 91% dell'inquadratura, con 1,9 EV di separazione dove prima erano zero, e sfarfallio trasmesso al paesaggio ridotto del 79% invece che del 57%. I gradini dichiarati nei metadati non sempre si vedono. Se il fotogramma e' gia' saturo dimezzare la sensibilita' non lo scurisce, e se l'esposizione automatica insegue l'alba il salto e' compensato dalla scena. In entrambi i casi sottrarlo introduceva il gradino invece di toglierlo. Ora ogni cambio viene ridotto alla quota che la luminanza ha davvero recepito. La temperatura di colore inventava numeri. Su un cielo stellato il colore medio non somiglia a nessun corpo nero, McCamy diverge, e uscivano decine di migliaia di kelvin troncate a un estremo. Ora fuori dall'intorno del luogo di Planck la misura si dichiara inapplicabile - e il controllo che lo verifica ha trovato subito un buco nel primo filtro, perche' un riquadro sulle coordinate cromatiche sa dire in quale zona si e' ma non quanto si e' vicini a una curva. La diagnostica impara a fare queste domande: --diagnose accetta ora parole chiave che accendono i moduli e riporta cosa ciascuno ha trovato sul materiale vero. Le scie stellari si verificano sull'uscita con la proprieta' che le definisce - la luminanza non puo' calare, perche' ogni pixel trattiene il valore piu' alto incontrato - e su 150 fotogrammi risulta non decrescente sul 99,3% dei passi. Verifica: da 51 a 54 controlli. I tre nuovi coprono proprio cio' che era sfuggito, a partire dalla garanzia che una curva lisciata non sia mai piu' a scatti dell'originale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,20 +34,46 @@ public static class ColorScience
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Temperatura di colore correlata con l'approssimazione di McCamy: il punto di
|
/// Temperatura di colore correlata con l'approssimazione di McCamy: il punto di
|
||||||
/// convergenza delle rette isotermiche sta molto vicino a (0,3320; 0,1858) e l'angolo
|
/// convergenza delle rette isotermiche sta molto vicino a (0,3320; 0,1858) e l'angolo
|
||||||
/// visto da lì determina la temperatura. Vale con buona precisione fra 2000 e 25000 K,
|
/// visto da lì determina la temperatura.
|
||||||
/// che copre tutto ciò che si incontra fra una candela e un cielo di mezzogiorno.
|
///
|
||||||
|
/// Restituisce NaN quando il colore medio non sta nell'intorno del luogo di Planck in cui
|
||||||
|
/// la formula è stata costruita. Non è prudenza teorica: su una ripresa notturna la
|
||||||
|
/// cromaticità media cade lontanissimo dal luogo dei corpi neri, il polinomio diverge e
|
||||||
|
/// produce valori di decine di migliaia di kelvin. Troncarli a un estremo darebbe un
|
||||||
|
/// numero dall'aria plausibile che non descrive nulla, ed è peggio che non darne uno.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double CorrelatedColorTemperature(double r, double g, double b)
|
public static double CorrelatedColorTemperature(double r, double g, double b)
|
||||||
{
|
{
|
||||||
var (x, y) = Chromaticity(r, g, b);
|
var (x, y) = Chromaticity(r, g, b);
|
||||||
|
if (!IsNearPlanckianLocus(x, y)) return double.NaN;
|
||||||
|
|
||||||
|
// Il riquadro sulle coordinate cromatiche sa dire "in quale zona", non "quanto vicino
|
||||||
|
// alla curva": un rosso saturo ci cade dentro di misura e riceverebbe una temperatura
|
||||||
|
// dall'aria sensata. Il luogo dei corpi neri percorre l'asse caldo-freddo e si scosta
|
||||||
|
// pochissimo da quello verde-magenta, quindi è lo scostamento su quell'asse — la
|
||||||
|
// stessa grandezza che il motore corregge — a dire se il colore somiglia a un
|
||||||
|
// illuminante o a una tinta qualsiasi.
|
||||||
|
if (Math.Abs(GreenTintStops(Log2Safe(r), Log2Safe(g), Log2Safe(b))) > 0.45) return double.NaN;
|
||||||
|
|
||||||
double denominator = 0.1858 - y;
|
double denominator = 0.1858 - y;
|
||||||
if (Math.Abs(denominator) < 1e-9) return double.NaN;
|
if (Math.Abs(denominator) < 1e-6) return double.NaN;
|
||||||
|
|
||||||
double n = (x - 0.3320) / denominator;
|
double n = (x - 0.3320) / denominator;
|
||||||
double cct = 449 * n * n * n + 3525 * n * n + 6823.3 * n + 5520.33;
|
double cct = 449 * n * n * n + 3525 * n * n + 6823.3 * n + 5520.33;
|
||||||
return double.IsFinite(cct) ? Math.Clamp(cct, 1000, 40000) : double.NaN;
|
|
||||||
|
// Oltre questi estremi il polinomio non è più un'approssimazione di niente:
|
||||||
|
// sotto sta la brace, sopra un blu che nessun illuminante produce.
|
||||||
|
return double.IsFinite(cct) && cct is >= 1500 and <= 15000 ? cct : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vero se la cromaticità cade nella regione in cui l'approssimazione è stata ricavata.
|
||||||
|
/// Il riquadro è generoso: contiene tutti gli illuminanti reali, dalla candela al cielo
|
||||||
|
/// coperto, ed esclude i colori saturi che di temperatura non ne hanno una.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsNearPlanckianLocus(double x, double y)
|
||||||
|
=> x is >= 0.20 and <= 0.60 && y is >= 0.18 and <= 0.55;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deviazione verde-magenta, in stop: eccesso di verde rispetto alla media di rosso e blu.
|
/// Deviazione verde-magenta, in stop: eccesso di verde rispetto alla media di rosso e blu.
|
||||||
/// È la grandezza che il motore corregge davvero, quindi viene riportata così com'è invece
|
/// È la grandezza che il motore corregge davvero, quindi viene riportata così com'è invece
|
||||||
@@ -56,6 +82,8 @@ public static class ColorScience
|
|||||||
public static double GreenTintStops(double log2R, double log2G, double log2B)
|
public static double GreenTintStops(double log2R, double log2G, double log2B)
|
||||||
=> log2G - (log2R + log2B) * 0.5;
|
=> log2G - (log2R + log2B) * 0.5;
|
||||||
|
|
||||||
|
private static double Log2Safe(double value) => Math.Log2(Math.Max(value, 1.0 / 65536.0));
|
||||||
|
|
||||||
/// <summary>Descrizione compatta di una temperatura, per l'interfaccia e la diagnostica.</summary>
|
/// <summary>Descrizione compatta di una temperatura, per l'interfaccia e la diagnostica.</summary>
|
||||||
public static string Describe(double kelvin)
|
public static string Describe(double kelvin)
|
||||||
=> double.IsNaN(kelvin) ? "—" : $"{kelvin:0} K";
|
=> double.IsNaN(kelvin) ? "—" : $"{kelvin:0} K";
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ public sealed class HolyGrailAnalysis
|
|||||||
public double LargestStepStops { get; init; }
|
public double LargestStepStops { get; init; }
|
||||||
public int StepCount => StepFrames.Length;
|
public int StepCount => StepFrames.Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cambi dichiarati dai metadati che la luminanza non ha recepito, e che sono stati
|
||||||
|
/// quindi lasciati stare. Le cause sono due, entrambe legittime: il fotogramma era già
|
||||||
|
/// saturo e non poteva scurirsi oltre, oppure la scena è cambiata di altrettanto nel
|
||||||
|
/// verso opposto — l'esposizione automatica che insegue l'alba — e il salto non si vede.
|
||||||
|
/// </summary>
|
||||||
|
public int UnobservedSteps { get; init; }
|
||||||
|
|
||||||
public static HolyGrailAnalysis Empty(int count) => new()
|
public static HolyGrailAnalysis Empty(int count) => new()
|
||||||
{
|
{
|
||||||
ExposureValue = Filled(count, double.NaN),
|
ExposureValue = Filled(count, double.NaN),
|
||||||
@@ -149,8 +157,9 @@ public static class HolyGrailEngine
|
|||||||
double threshold = Math.Max(0.02, settings.StepThresholdStops);
|
double threshold = Math.Max(0.02, settings.StepThresholdStops);
|
||||||
bool metadataUsable = settings.UseMetadata && usable > n / 2;
|
bool metadataUsable = settings.UseMetadata && usable > n / 2;
|
||||||
|
|
||||||
|
int unobserved = 0;
|
||||||
var jumps = metadataUsable
|
var jumps = metadataUsable
|
||||||
? StepsFromMetadata(exposureValue, threshold)
|
? GateByObservability(StepsFromMetadata(exposureValue, threshold), stats, out unobserved)
|
||||||
: StepsFromLuminance(stats, threshold);
|
: StepsFromLuminance(stats, threshold);
|
||||||
|
|
||||||
var staircase = new double[n];
|
var staircase = new double[n];
|
||||||
@@ -199,9 +208,62 @@ public static class HolyGrailEngine
|
|||||||
TintStops = tint,
|
TintStops = tint,
|
||||||
MetadataUsable = metadataUsable,
|
MetadataUsable = metadataUsable,
|
||||||
LargestStepStops = largest,
|
LargestStepStops = largest,
|
||||||
|
UnobservedSteps = unobserved,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riduce ogni gradino alla quota che la luminanza ha davvero recepito.
|
||||||
|
///
|
||||||
|
/// I metadati dicono cosa la macchina ha cambiato, non cosa si vede: se il fotogramma è
|
||||||
|
/// già saturo, dimezzare la sensibilità non lo scurisce affatto, perché sopra il bianco
|
||||||
|
/// non c'è niente da togliere. Sottrarre comunque il gradino dichiarato introdurrebbe nel
|
||||||
|
/// segnale un salto che nell'immagine non esisteva — e la correzione, invece di
|
||||||
|
/// ammorbidire uno scalino, ne creerebbe uno. Il confronto è fatto su mediane di pochi
|
||||||
|
/// fotogrammi per lato, per non farsi ingannare dal rumore del singolo scatto.
|
||||||
|
/// </summary>
|
||||||
|
private static double[] GateByObservability(double[] jumps, IReadOnlyList<LuminanceStats> stats,
|
||||||
|
out int unobserved)
|
||||||
|
{
|
||||||
|
const int span = 3;
|
||||||
|
int n = jumps.Length;
|
||||||
|
var gated = new double[n];
|
||||||
|
unobserved = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
{
|
||||||
|
double predicted = jumps[i];
|
||||||
|
if (Math.Abs(predicted) < 1e-9) continue;
|
||||||
|
|
||||||
|
double before = MedianLuminance(stats, i - span, i - 1);
|
||||||
|
double after = MedianLuminance(stats, i, i + span - 1);
|
||||||
|
|
||||||
|
// Quota recepita: 1 se la luminanza si è mossa quanto previsto, 0 se non si è
|
||||||
|
// mossa affatto o si è mossa nel verso opposto.
|
||||||
|
double share = Math.Clamp((after - before) / predicted, 0, 1);
|
||||||
|
gated[i] = predicted * share;
|
||||||
|
if (share < 0.25) unobserved++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double MedianLuminance(IReadOnlyList<LuminanceStats> stats, int from, int to)
|
||||||
|
{
|
||||||
|
int count = stats.Count;
|
||||||
|
from = Math.Clamp(from, 0, count - 1);
|
||||||
|
to = Math.Clamp(to, 0, count - 1);
|
||||||
|
if (to < from) (from, to) = (to, from);
|
||||||
|
|
||||||
|
Span<double> values = stackalloc double[8];
|
||||||
|
int n = 0;
|
||||||
|
for (int i = from; i <= to && n < values.Length; i++) values[n++] = stats[i].Log2Average;
|
||||||
|
|
||||||
|
var slice = values[..n];
|
||||||
|
slice.Sort();
|
||||||
|
return slice[n / 2];
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Valore di esposizione dello scatto. Diaframma e sensibilità mancanti si considerano
|
/// Valore di esposizione dello scatto. Diaframma e sensibilità mancanti si considerano
|
||||||
/// costanti — quasi sempre lo sono in un time-lapse — mentre senza il tempo di posa il
|
/// costanti — quasi sempre lo sono in un time-lapse — mentre senza il tempo di posa il
|
||||||
|
|||||||
@@ -134,10 +134,6 @@ public static class RegionSegmenter
|
|||||||
double log2High = weightHigh > 1e-6 ? sumHigh / weightHigh : 0;
|
double log2High = weightHigh > 1e-6 ? sumHigh / weightHigh : 0;
|
||||||
double log2Low = weightLow > 1e-6 ? sumLow / weightLow : 0;
|
double log2Low = weightLow > 1e-6 ? sumLow / weightLow : 0;
|
||||||
|
|
||||||
string description = settings.Mode == RegionMode.SkyGround
|
|
||||||
? $"cielo {coverage * 100:0}% dell'inquadratura, {log2High - log2Low:0.0} EV sopra il paesaggio"
|
|
||||||
: $"regione chiara {coverage * 100:0}% dell'inquadratura, {log2High - log2Low:0.0} EV sopra la scura";
|
|
||||||
|
|
||||||
return new RegionMask
|
return new RegionMask
|
||||||
{
|
{
|
||||||
Width = width,
|
Width = width,
|
||||||
@@ -146,10 +142,48 @@ public static class RegionSegmenter
|
|||||||
Coverage = coverage,
|
Coverage = coverage,
|
||||||
Log2High = log2High,
|
Log2High = log2High,
|
||||||
Log2Low = log2Low,
|
Log2Low = log2Low,
|
||||||
Description = description,
|
Description = Describe(settings.Mode, coverage, log2High, log2Low),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Descrizione della divisione trovata, riportando quello che si è misurato invece di
|
||||||
|
/// quello che ci si aspettava.
|
||||||
|
///
|
||||||
|
/// La divisione per orizzonte assegna alla prima regione ciò che sta sopra la linea, e
|
||||||
|
/// nulla garantisce che sia il cielo. Un cielo più scuro del terreno però non basta a
|
||||||
|
/// dire che la divisione ha sbagliato: di notte, con un primo piano illuminato, è la
|
||||||
|
/// norma. Il sospetto nasce quando la regione superiore è insieme scura e piccola —
|
||||||
|
/// una tettoia, un ramo, un cornicione che coprono l'alto dell'inquadratura — perché un
|
||||||
|
/// cielo vero, per quanto scuro, occupa una porzione ampia del fotogramma.
|
||||||
|
/// </summary>
|
||||||
|
private static string Describe(RegionMode mode, double coverage, double log2High, double log2Low)
|
||||||
|
{
|
||||||
|
double separation = log2High - log2Low;
|
||||||
|
|
||||||
|
if (mode == RegionMode.Luminance)
|
||||||
|
{
|
||||||
|
return $"regione chiara {coverage * 100:0}% dell'inquadratura, " +
|
||||||
|
$"{Math.Abs(separation):0.0} EV sopra la scura";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (separation >= 0)
|
||||||
|
{
|
||||||
|
return $"cielo {coverage * 100:0}% dell'inquadratura, {separation:0.0} EV sopra il paesaggio";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverage >= 0.35)
|
||||||
|
{
|
||||||
|
return $"cielo {coverage * 100:0}% dell'inquadratura, {-separation:0.0} EV sotto il paesaggio " +
|
||||||
|
$"(scena notturna con primo piano illuminato)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"regione superiore {coverage * 100:0}% dell'inquadratura, e {-separation:0.0} EV più " +
|
||||||
|
$"scura di quella inferiore: stretta e scura insieme, sopra la linea è probabile ci sia " +
|
||||||
|
$"un primo piano che copre l'alto e non il cielo. Per questa scena conviene la " +
|
||||||
|
$"divisione per luminanza.";
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ campionamento
|
// ------------------------------------------------------------------ campionamento
|
||||||
|
|
||||||
private static List<GrayPlane> CollectSamples(IReadOnlyList<string> paths, int orientation, int width,
|
private static List<GrayPlane> CollectSamples(IReadOnlyList<string> paths, int orientation, int width,
|
||||||
@@ -182,13 +216,20 @@ public static class RegionSegmenter
|
|||||||
|
|
||||||
var plane = new GrayPlane(width, height);
|
var plane = new GrayPlane(width, height);
|
||||||
var data = buffer.Data;
|
var data = buffer.Data;
|
||||||
|
int saturated = 0;
|
||||||
|
|
||||||
for (int p = 0; p < plane.Data.Length; p++)
|
for (int p = 0; p < plane.Data.Length; p++)
|
||||||
{
|
{
|
||||||
int s = p * ImageBuffer.Channels;
|
int s = p * ImageBuffer.Channels;
|
||||||
float luma = ColorSpace.Luminance(data[s], data[s + 1], data[s + 2]);
|
float luma = ColorSpace.Luminance(data[s], data[s + 1], data[s + 2]);
|
||||||
|
if (luma >= 0.99f) saturated++;
|
||||||
plane.Data[p] = (float)Math.Log2(Math.Max(luma, 1.0 / 65536.0));
|
plane.Data[p] = (float)Math.Log2(Math.Max(luma, 1.0 / 65536.0));
|
||||||
}
|
}
|
||||||
results[i] = plane;
|
|
||||||
|
// Un fotogramma quasi interamente bruciato non ha struttura da votare: la sua
|
||||||
|
// mediana sarebbe bianco ovunque e cancellerebbe l'orizzonte visto dagli altri.
|
||||||
|
// Capita davvero in una ripresa che attraversa il giorno con pose lunghe.
|
||||||
|
if (saturated < plane.Data.Length * 0.9) results[i] = plane;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -285,40 +326,57 @@ public static class RegionSegmenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maschera per linea d'orizzonte. La soglia dà una prima ipotesi colonna per colonna;
|
/// Maschera per linea d'orizzonte.
|
||||||
/// il gradiente verticale la corregge, perché il passaggio cielo-terra è il contrasto
|
///
|
||||||
/// più forte della colonna e cade esattamente sul bordo, non dove capita la soglia.
|
/// L'ipotesi iniziale viene dal profilo di luminanza per riga: la linea d'orizzonte è il
|
||||||
/// Il profilo risultante viene poi lisciato: un orizzonte reale non fa scalini.
|
/// gradino più marcato del profilo, qualunque ne sia il verso. La versione precedente
|
||||||
|
/// cercava invece la prima riga sotto soglia scendendo dall'alto, il che presuppone un
|
||||||
|
/// cielo chiaro e sgombro sopra la testa; su una ripresa notturna fatta da sotto un
|
||||||
|
/// pergolato quella regola aggancia il bordo del tetto e chiama "cielo" le travi.
|
||||||
|
/// Il gradino del profilo non fa questa assunzione e vale anche quando il cielo è più
|
||||||
|
/// scuro del primo piano, cosa normalissima di notte.
|
||||||
|
///
|
||||||
|
/// Il gradiente verticale raffina poi l'ipotesi colonna per colonna, perché il passaggio
|
||||||
|
/// cade esattamente sul bordo e non dove capita la soglia, e il profilo risultante viene
|
||||||
|
/// lisciato: un orizzonte reale non fa scalini.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static float[] BuildFromHorizon(float[] median, int width, int height, double threshold,
|
private static float[] BuildFromHorizon(float[] median, int width, int height, double threshold,
|
||||||
RegionSettings settings)
|
RegionSettings settings)
|
||||||
{
|
{
|
||||||
|
_ = threshold;
|
||||||
|
|
||||||
|
var rowMean = new double[height];
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
double sum = 0;
|
||||||
|
int rowBase = y * width;
|
||||||
|
for (int x = 0; x < width; x++) sum += median[rowBase + x];
|
||||||
|
rowMean[y] = sum / width;
|
||||||
|
}
|
||||||
|
|
||||||
|
var smoothRows = SmoothProfile(rowMean, Math.Max(1, height / 48));
|
||||||
|
|
||||||
|
int span = Math.Max(1, height / 32);
|
||||||
|
double strongest = -1;
|
||||||
|
int horizon = height / 2;
|
||||||
|
|
||||||
|
for (int y = span; y < height - span; y++)
|
||||||
|
{
|
||||||
|
double step = Math.Abs(smoothRows[y + span] - smoothRows[y - span]);
|
||||||
|
if (step <= strongest) continue;
|
||||||
|
strongest = step;
|
||||||
|
horizon = y;
|
||||||
|
}
|
||||||
|
|
||||||
var boundary = new double[width];
|
var boundary = new double[width];
|
||||||
int band = Math.Max(2, height / 8);
|
int band = Math.Max(2, height / 8);
|
||||||
|
|
||||||
for (int x = 0; x < width; x++)
|
for (int x = 0; x < width; x++)
|
||||||
{
|
{
|
||||||
// Prima riga, dall'alto, che apre una serie stabile di pixel sotto soglia.
|
int from = Math.Max(1, horizon - band);
|
||||||
int guess = height;
|
int to = Math.Min(height - 2, horizon + band);
|
||||||
int run = 0;
|
|
||||||
int required = Math.Max(2, height / 40);
|
|
||||||
for (int y = 0; y < height; y++)
|
|
||||||
{
|
|
||||||
if (median[y * width + x] < threshold)
|
|
||||||
{
|
|
||||||
if (++run >= required) { guess = y - required + 1; break; }
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
run = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (guess >= height) guess = height - 1;
|
|
||||||
|
|
||||||
int from = Math.Max(1, guess - band);
|
|
||||||
int to = Math.Min(height - 2, guess + band);
|
|
||||||
double bestGradient = -1;
|
double bestGradient = -1;
|
||||||
int bestRow = guess;
|
int bestRow = horizon;
|
||||||
|
|
||||||
for (int y = from; y <= to; y++)
|
for (int y = from; y <= to; y++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,41 +56,69 @@ public static class LocalRegression
|
|||||||
int n = y.Length;
|
int n = y.Length;
|
||||||
double twoSigmaSq = 2.0 * sigma * sigma;
|
double twoSigmaSq = 2.0 * sigma * sigma;
|
||||||
|
|
||||||
for (int i = 0; i < n; i++)
|
for (int i = 0; i < n; i++) output[i] = Fit(i);
|
||||||
|
|
||||||
|
// Stima nel punto indicato, mescolando la retta robusta e quella semplice secondo
|
||||||
|
// quanta parte della finestra i pesi di robustezza hanno lasciato in vita.
|
||||||
|
//
|
||||||
|
// La robustezza di Tukey presuppone anomalie sparse. Quando invece un tratto contiguo
|
||||||
|
// si discosta — il crollo di luce del crepuscolo, una serie di fotogrammi bruciati —
|
||||||
|
// può azzerare l'intera finestra, e allora l'unica cosa che conta è cosa si fa dopo.
|
||||||
|
// Commutare fra "uso i soli sopravvissuti" e "uso tutto" produce uno scalino nel punto
|
||||||
|
// di commutazione: sulla ripresa reale di sedici ore la curva obiettivo arrivava così
|
||||||
|
// ad avere un salto di 2,3 stop, più grande di qualunque salto presente nel segnale
|
||||||
|
// che doveva lisciare. Il rimedio non è scegliere meglio fra le due stime ma non
|
||||||
|
// scegliere affatto: si passa dall'una all'altra con continuità, e dove la finestra
|
||||||
|
// sopravvive per intero il risultato resta identico a prima.
|
||||||
|
double Fit(int centre)
|
||||||
{
|
{
|
||||||
int from = Math.Max(0, i - radius);
|
int from = Math.Max(0, centre - radius);
|
||||||
int to = Math.Min(n - 1, i + radius);
|
int to = Math.Min(n - 1, centre + radius);
|
||||||
|
|
||||||
|
double pw = 0, pwt = 0, pwt2 = 0, pwy = 0, pwty = 0; // retta semplice
|
||||||
|
double rw = 0, rwt = 0, rwt2 = 0, rwy = 0, rwty = 0; // retta robusta
|
||||||
|
|
||||||
// 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++)
|
for (int j = from; j <= to; j++)
|
||||||
{
|
{
|
||||||
double t = j - i;
|
double t = j - centre;
|
||||||
double w = Math.Exp(-(t * t) / twoSigmaSq) * robust[j];
|
double plain = Math.Exp(-(t * t) / twoSigmaSq);
|
||||||
if (w <= 1e-9) continue;
|
if (plain <= 1e-12) continue;
|
||||||
sw += w;
|
|
||||||
swt += w * t;
|
pw += plain; pwt += plain * t; pwt2 += plain * t * t;
|
||||||
swt2 += w * t * t;
|
pwy += plain * y[j]; pwty += plain * t * y[j];
|
||||||
swy += w * y[j];
|
|
||||||
swty += w * t * y[j];
|
double weighted = plain * robust[j];
|
||||||
|
if (weighted <= 1e-12) continue;
|
||||||
|
|
||||||
|
rw += weighted; rwt += weighted * t; rwt2 += weighted * t * t;
|
||||||
|
rwy += weighted * y[j]; rwty += weighted * t * y[j];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sw <= 1e-9)
|
if (pw <= 1e-9) return y[centre];
|
||||||
{
|
|
||||||
output[i] = y[i];
|
double simple = Solve(pw, pwt, pwt2, pwy, pwty, y[centre]);
|
||||||
continue;
|
double survival = rw / pw;
|
||||||
|
if (survival <= 1e-6) return simple;
|
||||||
|
|
||||||
|
double sturdy = Solve(rw, rwt, rwt2, rwy, rwty, y[centre]);
|
||||||
|
|
||||||
|
// Sopra metà finestra sopravvissuta la stima è quella robusta e basta; sotto,
|
||||||
|
// si scivola verso quella semplice senza gradini.
|
||||||
|
double blend = Math.Clamp(survival / 0.5, 0, 1);
|
||||||
|
blend = blend * blend * (3 - 2 * blend);
|
||||||
|
return simple + (sturdy - simple) * blend;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Intercetta della retta pesata, ossia il valore stimato nel centro della finestra.</summary>
|
||||||
|
private static double Solve(double sw, double swt, double swt2, double swy, double swty, double fallback)
|
||||||
|
{
|
||||||
|
if (sw <= 1e-9) return fallback;
|
||||||
|
|
||||||
double det = sw * swt2 - swt * swt;
|
double det = sw * swt2 - swt * swt;
|
||||||
if (Math.Abs(det) < 1e-12)
|
if (Math.Abs(det) < 1e-12) return swy / sw; // finestra degenere: media pesata
|
||||||
{
|
|
||||||
output[i] = swy / sw; // finestra degenere: media pesata
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
double a = (swt2 * swy - swt * swty) / det; // intercetta = valore stimato in t = 0
|
return (swt2 * swy - swt * swty) / det;
|
||||||
output[i] = a;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateRobustWeights(double[] y, double[] fit, double[] robust)
|
private static void UpdateRobustWeights(double[] y, double[] fit, double[] robust)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ internal static class AdvancedModuleTests
|
|||||||
{
|
{
|
||||||
Stabilization(add);
|
Stabilization(add);
|
||||||
PhotometricRegions(workingDirectory, add, output);
|
PhotometricRegions(workingDirectory, add, output);
|
||||||
|
Smoothing(add);
|
||||||
ColourDrift(add);
|
ColourDrift(add);
|
||||||
Easing(add);
|
Easing(add);
|
||||||
Geometry(add);
|
Geometry(add);
|
||||||
@@ -295,6 +296,59 @@ internal static class AdvancedModuleTests
|
|||||||
double temperature = ColorScience.CorrelatedColorTemperature(1.0, 1.0, 1.0);
|
double temperature = ColorScience.CorrelatedColorTemperature(1.0, 1.0, 1.0);
|
||||||
add("Temperatura di colore — bianco di riferimento",
|
add("Temperatura di colore — bianco di riferimento",
|
||||||
Math.Abs(temperature - 6504) < 120, $"{ColorScience.Describe(temperature)} per un grigio neutro");
|
Math.Abs(temperature - 6504) < 120, $"{ColorScience.Describe(temperature)} per un grigio neutro");
|
||||||
|
|
||||||
|
// Su una ripresa notturna il colore medio cade lontanissimo dal luogo dei corpi neri,
|
||||||
|
// dove l'approssimazione diverge. Deve dichiararsi inapplicabile invece di restituire
|
||||||
|
// un numero troncato a un estremo, che avrebbe l'aria di essere una misura.
|
||||||
|
double nightSky = ColorScience.CorrelatedColorTemperature(0.004, 0.006, 0.020);
|
||||||
|
double deepRed = ColorScience.CorrelatedColorTemperature(0.90, 0.05, 0.02);
|
||||||
|
add("Temperatura di colore — dichiarata non significativa fuori dal luogo di Planck",
|
||||||
|
double.IsNaN(nightSky) && double.IsNaN(deepRed),
|
||||||
|
$"cielo notturno {ColorScience.Describe(nightSky)}, rosso saturo {ColorScience.Describe(deepRed)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// La curva obiettivo non deve mai essere più a scatti del segnale che liscia.
|
||||||
|
///
|
||||||
|
/// Sembra ovvio e invece è la proprietà che si è rotta per prima su materiale vero: dove i
|
||||||
|
/// residui restano grandi per un tratto intero, i pesi di robustezza azzerano l'intera
|
||||||
|
/// finestra, e ripiegando sul campione grezzo la curva obiettivo apre uno scalino proprio
|
||||||
|
/// dove doveva esserci la massima continuità. La scena qui riproduce quelle condizioni:
|
||||||
|
/// un crollo di luce ripido con sopra una serie contigua di fotogrammi anomali.
|
||||||
|
/// </summary>
|
||||||
|
private static void Smoothing(Report add)
|
||||||
|
{
|
||||||
|
const int count = 240;
|
||||||
|
var series = new double[count];
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
// Crepuscolo: quasi piatto, poi un crollo ripido, poi di nuovo piatto.
|
||||||
|
double t = (i - 100) / 30.0;
|
||||||
|
series[i] = -2.0 - 7.0 / (1.0 + Math.Exp(-t));
|
||||||
|
|
||||||
|
// Un tratto contiguo di fotogrammi fuori scala, non un singolo scatto isolato:
|
||||||
|
// è la condizione che fa collassare i pesi su tutta la finestra.
|
||||||
|
if (i is >= 118 and <= 132) series[i] += 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
var smoothed = LocalRegression.Smooth(series, 15, true);
|
||||||
|
|
||||||
|
double worstInput = 0, worstOutput = 0;
|
||||||
|
for (int i = 1; i < count; i++)
|
||||||
|
{
|
||||||
|
worstInput = Math.Max(worstInput, Math.Abs(series[i] - series[i - 1]));
|
||||||
|
worstOutput = Math.Max(worstOutput, Math.Abs(smoothed[i] - smoothed[i - 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
add("Regressione locale — la curva lisciata non è mai più a scatti dell'originale",
|
||||||
|
worstOutput <= worstInput,
|
||||||
|
$"salto massimo {worstInput:0.000} stop in ingresso, {worstOutput:0.000} in uscita");
|
||||||
|
|
||||||
|
// E deve comunque seguire il crollo: una curva piatta sarebbe continua ma inutile.
|
||||||
|
double range = smoothed.Max() - smoothed.Min();
|
||||||
|
add("Regressione locale — il crollo di luce viene seguito", range > 6.0,
|
||||||
|
$"escursione {range:0.00} EV su {series.Max() - series.Min():0.00} EV del segnale");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================== 4. accelerazione
|
// ================================================================== 4. accelerazione
|
||||||
|
|||||||
@@ -14,9 +14,24 @@ namespace Titano.Diagnostics;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SequenceDiagnostics
|
public static class SequenceDiagnostics
|
||||||
{
|
{
|
||||||
|
/// <summary>Moduli opzionali da accendere durante la diagnosi di una cartella reale.</summary>
|
||||||
|
[Flags]
|
||||||
|
public enum AdvancedModules
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Analysis = 1, // stabilizzazione, regioni, transizioni giorno-notte
|
||||||
|
Camera = 2, // movimento di macchina virtuale e rimappatura del tempo
|
||||||
|
MedianStack = 4,
|
||||||
|
StarTrails = 8,
|
||||||
|
|
||||||
|
/// <summary>Divide per luminanza invece che per linea d'orizzonte.</summary>
|
||||||
|
RegionsByLuminance = 16,
|
||||||
|
}
|
||||||
|
|
||||||
public static int Run(string directory, TextWriter output, int sampleCount = 6,
|
public static int Run(string directory, TextWriter output, int sampleCount = 6,
|
||||||
string? renderPath = null, int renderFrames = 24,
|
string? renderPath = null, int renderFrames = 24,
|
||||||
int renderWidth = 0, Video.VideoCodec renderCodec = Video.VideoCodec.H264)
|
int renderWidth = 0, Video.VideoCodec renderCodec = Video.VideoCodec.H264,
|
||||||
|
AdvancedModules advanced = AdvancedModules.None, int analysisFrames = 0)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(directory))
|
if (!Directory.Exists(directory))
|
||||||
{
|
{
|
||||||
@@ -147,18 +162,262 @@ public static class SequenceDiagnostics
|
|||||||
var distinct = all2.Select(m => (m.PixelWidth, m.PixelHeight)).Distinct().ToList();
|
var distinct = all2.Select(m => (m.PixelWidth, m.PixelHeight)).Distinct().ToList();
|
||||||
output.WriteLine($" dimensioni dichiarate distinte: {string.Join(", ", distinct.Select(d => $"{d.PixelWidth}×{d.PixelHeight}"))}");
|
output.WriteLine($" dimensioni dichiarate distinte: {string.Join(", ", distinct.Select(d => $"{d.PixelWidth}×{d.PixelHeight}"))}");
|
||||||
|
|
||||||
|
if (advanced != AdvancedModules.None)
|
||||||
|
RunAdvancedAnalysis(all2, advanced, analysisFrames, output);
|
||||||
|
|
||||||
if (renderPath is not null)
|
if (renderPath is not null)
|
||||||
RunTrialRender(all2, renderPath, renderFrames, renderWidth, renderCodec, output);
|
RunTrialRender(all2, renderPath, renderFrames, renderWidth, renderCodec, advanced, output);
|
||||||
|
|
||||||
return decoded == sampleMetadata.Count ? 0 : 1;
|
return decoded == sampleMetadata.Count ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fa girare i moduli avanzati sulla sequenza reale e riporta che cosa hanno trovato.
|
||||||
|
///
|
||||||
|
/// Non è un rendering: è la fase di analisi, l'unica che può dire se su materiale vero la
|
||||||
|
/// correlazione di fase aggancia qualcosa, se la scena si lascia dividere in cielo e
|
||||||
|
/// paesaggio, se la macchina ha davvero cambiato impostazioni lungo la ripresa. Sono le
|
||||||
|
/// domande a cui una scena sintetica, costruita perché la risposta sia nota, non risponde.
|
||||||
|
/// </summary>
|
||||||
|
private static void RunAdvancedAnalysis(List<FrameMetadata> metadata, AdvancedModules modules,
|
||||||
|
int analysisFrames, TextWriter output)
|
||||||
|
{
|
||||||
|
output.WriteLine();
|
||||||
|
output.WriteLine("MODULI AVANZATI SULLA SEQUENZA REALE");
|
||||||
|
output.WriteLine(new string('-', 74));
|
||||||
|
|
||||||
|
var ordered = metadata.OrderBy(m => m.CaptureTime ?? DateTime.MaxValue)
|
||||||
|
.ThenBy(m => m.FileName, NaturalFileNameComparer.Instance)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// La stabilizzazione confronta ogni fotogramma con il precedente: il campione deve
|
||||||
|
// essere contiguo, non sparso sulla sequenza.
|
||||||
|
var subset = analysisFrames > 0 && analysisFrames < ordered.Count
|
||||||
|
? ordered.Take(analysisFrames).ToList()
|
||||||
|
: ordered;
|
||||||
|
|
||||||
|
var project = new Pipeline.TitanoProject();
|
||||||
|
project.Sequence = TimelapseSequence.Build(subset);
|
||||||
|
project.Sequence.RecomputeTiming(project.General.CadenceTolerance);
|
||||||
|
project.DetectOrientation();
|
||||||
|
ConfigureAdvanced(project, modules);
|
||||||
|
|
||||||
|
output.WriteLine($" fotogrammi analizzati {subset.Count}" +
|
||||||
|
(subset.Count < ordered.Count ? $" di {ordered.Count} (sottoinsieme contiguo)" : string.Empty));
|
||||||
|
output.WriteLine($" larghezza di analisi {project.General.AnalysisWidth} px");
|
||||||
|
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
new Pipeline.RenderPipeline(project).AnalyzeAsync(null, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
output.WriteLine($" ANALISI NON RIUSCITA: {ex.GetType().Name} — {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
output.WriteLine($" tempo di analisi {stopwatch.Elapsed.TotalSeconds:0.0} s " +
|
||||||
|
$"({subset.Count / Math.Max(0.001, stopwatch.Elapsed.TotalSeconds):0.0} fotogrammi/s)");
|
||||||
|
output.WriteLine();
|
||||||
|
|
||||||
|
ReportStabilization(project, output);
|
||||||
|
ReportRegions(project, output);
|
||||||
|
ReportTransitions(project, output);
|
||||||
|
ReportDeflicker(project, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureAdvanced(Pipeline.TitanoProject project, AdvancedModules modules)
|
||||||
|
{
|
||||||
|
if (modules.HasFlag(AdvancedModules.Analysis))
|
||||||
|
{
|
||||||
|
project.Stabilization.Enabled = true;
|
||||||
|
project.Regions.Mode = modules.HasFlag(AdvancedModules.RegionsByLuminance)
|
||||||
|
? RegionMode.Luminance
|
||||||
|
: RegionMode.SkyGround;
|
||||||
|
project.HolyGrail.Enabled = true;
|
||||||
|
project.HolyGrail.SmoothColor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modules.HasFlag(AdvancedModules.Camera))
|
||||||
|
{
|
||||||
|
project.Camera.Enabled = true;
|
||||||
|
project.Camera.Keyframes =
|
||||||
|
[
|
||||||
|
new() { Time = 0.0, CentreX = 0.42, CentreY = 0.46, Zoom = 1.10 },
|
||||||
|
new() { Time = 1.0, CentreX = 0.58, CentreY = 0.54, Zoom = 1.45 },
|
||||||
|
];
|
||||||
|
project.TimeRamp.Enabled = true;
|
||||||
|
project.TimeRamp.Speed = [new(0.0, 1.6), new(0.5, 0.5), new(1.0, 1.6)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modules.HasFlag(AdvancedModules.MedianStack))
|
||||||
|
{
|
||||||
|
project.Stacking.Mode = Motion.StackingMode.Median;
|
||||||
|
project.Stacking.WindowFrames = 5;
|
||||||
|
}
|
||||||
|
else if (modules.HasFlag(AdvancedModules.StarTrails))
|
||||||
|
{
|
||||||
|
project.Stacking.Mode = Motion.StackingMode.Maximum;
|
||||||
|
project.Stacking.TrailFrames = 0; // scie che non si spengono
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportStabilization(Pipeline.TitanoProject project, TextWriter output)
|
||||||
|
{
|
||||||
|
if (project.Motion is not { } motion) return;
|
||||||
|
|
||||||
|
var (width, height) = project.ResolveSourceSize();
|
||||||
|
double toPixels = Math.Max(1, width);
|
||||||
|
|
||||||
|
double maxRotation = 0;
|
||||||
|
double maxShift = 0;
|
||||||
|
foreach (var correction in motion.Correction)
|
||||||
|
{
|
||||||
|
maxRotation = Math.Max(maxRotation, Math.Abs(correction.Rotation) * 180 / Math.PI);
|
||||||
|
maxShift = Math.Max(maxShift, Math.Sqrt(correction.Tx * correction.Tx + correction.Ty * correction.Ty));
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = motion.Measured[^1];
|
||||||
|
double drift = Math.Sqrt(last.Tx * last.Tx + last.Ty * last.Ty) * toPixels;
|
||||||
|
|
||||||
|
output.WriteLine(" STABILIZZAZIONE");
|
||||||
|
output.WriteLine($" correlazione debole {motion.UnreliableFraction * 100:0.0}% delle coppie");
|
||||||
|
output.WriteLine($" tremolio rimosso {motion.MeanShake * toPixels:0.00} px in media, " +
|
||||||
|
$"{maxShift * toPixels:0.00} px al massimo");
|
||||||
|
output.WriteLine($" rotazione compensata {maxRotation:0.000}° al massimo");
|
||||||
|
output.WriteLine($" deriva complessiva {drift:0.0} px dal primo all'ultimo fotogramma");
|
||||||
|
output.WriteLine($" ritaglio necessario {(motion.RequiredZoom(height / (double)width) - 1) * 100:0.0}%");
|
||||||
|
output.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportRegions(Pipeline.TitanoProject project, TextWriter output)
|
||||||
|
{
|
||||||
|
output.WriteLine(" SEGMENTAZIONE");
|
||||||
|
|
||||||
|
if (project.Mask is not { } mask)
|
||||||
|
{
|
||||||
|
output.WriteLine(" esito la scena non si divide in modo utile: resta la curva unica");
|
||||||
|
output.WriteLine();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine($" esito {mask.Description}");
|
||||||
|
output.WriteLine($" maschera {mask.Width}×{mask.Height}, copertura {mask.Coverage * 100:0.0}%");
|
||||||
|
|
||||||
|
var curve = project.Curve;
|
||||||
|
if (curve is { HasRegions: true, MeasuredLow: { } measuredLow, TargetLow: { } targetLow })
|
||||||
|
{
|
||||||
|
var underGlobal = new double[measuredLow.Length];
|
||||||
|
for (int i = 0; i < measuredLow.Length; i++) underGlobal[i] = measuredLow[i] + curve.GainStops[i];
|
||||||
|
|
||||||
|
double globalNoise = DeflickerCurve.FlickerIndex(underGlobal);
|
||||||
|
double regionalNoise = DeflickerCurve.FlickerIndex(targetLow);
|
||||||
|
double gain = globalNoise > 1e-9 ? 100 * (1 - regionalNoise / globalNoise) : 0;
|
||||||
|
|
||||||
|
output.WriteLine($" effetto sul paesaggio sfarfallio {globalNoise:0.0000} stop con la curva globale, " +
|
||||||
|
$"{regionalNoise:0.0000} con quella di regione ({gain:0.#}% meglio)");
|
||||||
|
}
|
||||||
|
output.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportTransitions(Pipeline.TitanoProject project, TextWriter output)
|
||||||
|
{
|
||||||
|
if (project.Transitions is not { } transitions) return;
|
||||||
|
|
||||||
|
int withExposureValue = transitions.ExposureValue.Count(v => !double.IsNaN(v));
|
||||||
|
var temperatures = transitions.TemperatureKelvin.Where(double.IsFinite).ToList();
|
||||||
|
var tints = transitions.TintStops.Where(double.IsFinite).ToList();
|
||||||
|
|
||||||
|
output.WriteLine(" TRANSIZIONI GIORNO-NOTTE");
|
||||||
|
output.WriteLine($" valore di esposizione ricostruito su {withExposureValue}/{transitions.ExposureValue.Length} fotogrammi");
|
||||||
|
output.WriteLine($" cambi di impostazione {transitions.StepCount}" +
|
||||||
|
(transitions.StepCount > 0
|
||||||
|
? $", il maggiore di {transitions.LargestStepStops:0.00} EV " +
|
||||||
|
$"({(transitions.MetadataUsable ? "dai metadati" : "dedotti dalla luminanza")})"
|
||||||
|
: string.Empty));
|
||||||
|
|
||||||
|
if (transitions.StepCount is > 0 and <= 12)
|
||||||
|
{
|
||||||
|
output.WriteLine($" ai fotogrammi {string.Join(", ", transitions.StepFrames.Select(f => f + 1))}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transitions.UnobservedSteps > 0)
|
||||||
|
{
|
||||||
|
// Due cause, entrambe legittime: il fotogramma era già saturo e non poteva
|
||||||
|
// scurirsi oltre, oppure la scena è cambiata di altrettanto nel verso opposto e
|
||||||
|
// il salto, semplicemente, non si vede. In nessuno dei due casi c'è qualcosa da
|
||||||
|
// ammorbidire, e correggere comunque introdurrebbe il gradino invece di toglierlo.
|
||||||
|
output.WriteLine($" non recepiti {transitions.UnobservedSteps} cambi dichiarati che la " +
|
||||||
|
$"luminanza non ha recepito (fotogramma saturo, o esposizione " +
|
||||||
|
$"compensata dalla scena): lasciati stare");
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = transitions.TemperatureKelvin.Length;
|
||||||
|
if (temperatures.Count > 0)
|
||||||
|
{
|
||||||
|
output.WriteLine($" temperatura di colore da {temperatures[0]:0} K a {temperatures[^1]:0} K " +
|
||||||
|
$"(intervallo {temperatures.Min():0}–{temperatures.Max():0} K)" +
|
||||||
|
(temperatures.Count < total
|
||||||
|
? $", significativa su {temperatures.Count}/{total} fotogrammi"
|
||||||
|
: string.Empty));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Su una ripresa notturna è il caso normale, non un guasto: il colore medio di un
|
||||||
|
// cielo stellato non sta da nessuna parte vicino al luogo dei corpi neri.
|
||||||
|
output.WriteLine($" temperatura di colore non significativa su nessuno dei {total} fotogrammi: " +
|
||||||
|
$"il colore medio è troppo lontano dal luogo di Planck perché una " +
|
||||||
|
$"temperatura voglia dire qualcosa");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tints.Count > 0)
|
||||||
|
{
|
||||||
|
output.WriteLine($" tinta verde-magenta da {tints[0]:+0.000;-0.000;0.000} a {tints[^1]:+0.000;-0.000;0.000} EV " +
|
||||||
|
$"(escursione {tints.Max() - tints.Min():0.000} EV)");
|
||||||
|
}
|
||||||
|
output.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReportDeflicker(Pipeline.TitanoProject project, TextWriter output)
|
||||||
|
{
|
||||||
|
if (project.Curve is not { } curve || curve.Count < 3) return;
|
||||||
|
|
||||||
|
double before = DeflickerCurve.FlickerIndex(curve.Measured);
|
||||||
|
var corrected = new double[curve.Count];
|
||||||
|
for (int i = 0; i < curve.Count; i++) corrected[i] = curve.Measured[i] + curve.GainStops[i];
|
||||||
|
double after = DeflickerCurve.FlickerIndex(corrected);
|
||||||
|
|
||||||
|
double maxStepBefore = 0, maxStepAfter = 0;
|
||||||
|
int worstBefore = 0, worstAfter = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < curve.Count; i++)
|
||||||
|
{
|
||||||
|
double stepBefore = Math.Abs(curve.Measured[i] - curve.Measured[i - 1]);
|
||||||
|
if (stepBefore > maxStepBefore) { maxStepBefore = stepBefore; worstBefore = i; }
|
||||||
|
|
||||||
|
double stepAfter = Math.Abs(curve.Target[i] - curve.Target[i - 1]);
|
||||||
|
if (stepAfter > maxStepAfter) { maxStepAfter = stepAfter; worstAfter = i; }
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine(" DEFLICKER");
|
||||||
|
output.WriteLine($" sfarfallio {before:0.0000} → {after:0.0000} stop RMS " +
|
||||||
|
$"({100 * (1 - after / Math.Max(before, 1e-9)):0.#}% di riduzione)");
|
||||||
|
output.WriteLine($" salto massimo {maxStepBefore:0.000} stop al fotogramma {worstBefore + 1} " +
|
||||||
|
$"→ {maxStepAfter:0.000} al fotogramma {worstAfter + 1}" +
|
||||||
|
(maxStepAfter > maxStepBefore ? " ← LA CORREZIONE LO PEGGIORA" : string.Empty));
|
||||||
|
output.WriteLine($" escursione della luce {curve.Measured.Max() - curve.Measured.Min():0.00} EV sull'intera sequenza");
|
||||||
|
output.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Esporta i primi fotogrammi della sequenza reale: è l'unica prova che dice davvero
|
/// Esporta i primi fotogrammi della sequenza reale: è l'unica prova che dice davvero
|
||||||
/// se decodifica, analisi, sfocatura, encoder e contenitore reggono questi file.
|
/// se decodifica, analisi, sfocatura, encoder e contenitore reggono questi file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void RunTrialRender(List<FrameMetadata> metadata, string outputPath, int frames,
|
private static void RunTrialRender(List<FrameMetadata> metadata, string outputPath, int frames,
|
||||||
int forcedWidth, Video.VideoCodec codec, TextWriter output)
|
int forcedWidth, Video.VideoCodec codec,
|
||||||
|
AdvancedModules advanced, TextWriter output)
|
||||||
{
|
{
|
||||||
output.WriteLine();
|
output.WriteLine();
|
||||||
output.WriteLine("RENDER DI PROVA");
|
output.WriteLine("RENDER DI PROVA");
|
||||||
@@ -178,6 +437,7 @@ public static class SequenceDiagnostics
|
|||||||
project.Export.BitrateMbps = 80;
|
project.Export.BitrateMbps = 80;
|
||||||
project.Export.Codec = codec;
|
project.Export.Codec = codec;
|
||||||
project.General.WorkingWidth = forcedWidth;
|
project.General.WorkingWidth = forcedWidth;
|
||||||
|
ConfigureAdvanced(project, advanced);
|
||||||
|
|
||||||
var (width, height) = project.ResolveWorkingSize();
|
var (width, height) = project.ResolveWorkingSize();
|
||||||
var (requestedWidth, requestedHeight) = project.ResolveRequestedSize();
|
var (requestedWidth, requestedHeight) = project.ResolveRequestedSize();
|
||||||
@@ -191,6 +451,14 @@ public static class SequenceDiagnostics
|
|||||||
: string.Empty));
|
: string.Empty));
|
||||||
output.WriteLine($" memoria per buffer {perFrame / (1024.0 * 1024.0):0.0} MiB");
|
output.WriteLine($" memoria per buffer {perFrame / (1024.0 * 1024.0):0.0} MiB");
|
||||||
output.WriteLine($" codec {codec}");
|
output.WriteLine($" codec {codec}");
|
||||||
|
if (advanced != AdvancedModules.None) output.WriteLine($" moduli attivi {advanced}");
|
||||||
|
|
||||||
|
if (project.NeedsGeometry)
|
||||||
|
{
|
||||||
|
var (sourceWidth, sourceHeight) = project.ResolveSourceSize();
|
||||||
|
output.WriteLine($" lettura sorgente {sourceWidth}×{sourceHeight} " +
|
||||||
|
$"({(long)sourceWidth * sourceHeight * 3 * sizeof(float) / (1024.0 * 1024.0):0.0} MiB per fotogramma)");
|
||||||
|
}
|
||||||
|
|
||||||
var pipeline = new Pipeline.RenderPipeline(project);
|
var pipeline = new Pipeline.RenderPipeline(project);
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
@@ -213,6 +481,8 @@ public static class SequenceDiagnostics
|
|||||||
|
|
||||||
var playback = Mp4Playback.Read(result.OutputPath);
|
var playback = Mp4Playback.Read(result.OutputPath);
|
||||||
output.WriteLine($" rilettura {playback.Error ?? $"{playback.FrameCount} fotogrammi decodificati"}");
|
output.WriteLine($" rilettura {playback.Error ?? $"{playback.FrameCount} fotogrammi decodificati"}");
|
||||||
|
|
||||||
|
ReportStackingEvidence(advanced, playback, output);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -220,6 +490,48 @@ public static class SequenceDiagnostics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica sull'uscita vera la proprietà che definisce ciascuna modalità di accumulo.
|
||||||
|
///
|
||||||
|
/// Che il rendering non si inceppi non prova che l'effetto ci sia. Il massimo progressivo
|
||||||
|
/// ha una proprietà controllabile senza guardare l'immagine: la luminanza non può mai
|
||||||
|
/// calare, perché ogni pixel trattiene il valore più alto incontrato. Se cala, qualcosa
|
||||||
|
/// nell'accumulatore non tiene.
|
||||||
|
/// </summary>
|
||||||
|
private static void ReportStackingEvidence(AdvancedModules advanced, Mp4Playback.Playback playback,
|
||||||
|
TextWriter output)
|
||||||
|
{
|
||||||
|
var luma = playback.MeanLuma;
|
||||||
|
if (luma.Count < 4) return;
|
||||||
|
|
||||||
|
if (advanced.HasFlag(AdvancedModules.StarTrails))
|
||||||
|
{
|
||||||
|
int rising = 0;
|
||||||
|
double worstDrop = 0;
|
||||||
|
for (int i = 1; i < luma.Count; i++)
|
||||||
|
{
|
||||||
|
if (luma[i] >= luma[i - 1] - 1e-4) rising++;
|
||||||
|
else worstDrop = Math.Max(worstDrop, luma[i - 1] - luma[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
double share = 100.0 * rising / (luma.Count - 1);
|
||||||
|
output.WriteLine($" scie stellari luminanza da {luma[0]:0.0000} a {luma[^1]:0.0000} " +
|
||||||
|
$"(×{luma[^1] / Math.Max(1e-6, luma[0]):0.0}), " +
|
||||||
|
$"non decrescente sul {share:0.#}% dei passi" +
|
||||||
|
(worstDrop > 1e-3 ? $", calo massimo {worstDrop:0.0000}" : string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advanced.HasFlag(AdvancedModules.MedianStack))
|
||||||
|
{
|
||||||
|
// La mediana temporale toglie ciò che passa una volta sola: la luminanza deve
|
||||||
|
// risultare più regolare di quella della sequenza sorgente.
|
||||||
|
double roughness = 0;
|
||||||
|
for (int i = 1; i < luma.Count; i++) roughness += Math.Abs(luma[i] - luma[i - 1]);
|
||||||
|
output.WriteLine($" mediana temporale variazione media fra fotogrammi " +
|
||||||
|
$"{roughness / (luma.Count - 1):0.00000}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<string> PickSamples(List<string> files, int count)
|
private static List<string> PickSamples(List<string> files, int count)
|
||||||
{
|
{
|
||||||
if (files.Count <= count) return files;
|
if (files.Count <= count) return files;
|
||||||
|
|||||||
@@ -118,10 +118,23 @@ public sealed class PhaseCorrelator
|
|||||||
/// Misura lo spostamento del contenuto del riquadro fra <paramref name="a"/> e
|
/// Misura lo spostamento del contenuto del riquadro fra <paramref name="a"/> e
|
||||||
/// <paramref name="b"/>: il risultato è il vettore d per cui b(p) ≈ a(p − d).
|
/// <paramref name="b"/>: il risultato è il vettore d per cui b(p) ≈ a(p − d).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Deviazione standard sotto la quale un riquadro si considera privo di tessitura.
|
||||||
|
/// Corrisponde a mezzo livello su 255: sotto, non c'è nulla di cui misurare lo spostamento.
|
||||||
|
/// </summary>
|
||||||
|
private const float TextureFloor = 0.002f;
|
||||||
|
|
||||||
public PhaseShift Correlate(GrayImage a, GrayImage b, int originX, int originY)
|
public PhaseShift Correlate(GrayImage a, GrayImage b, int originX, int originY)
|
||||||
{
|
{
|
||||||
Load(a, originX, originY, _aRe, _aIm);
|
float deviationA = Load(a, originX, originY, _aRe, _aIm);
|
||||||
Load(b, originX, originY, _bRe, _bIm);
|
float deviationB = Load(b, originX, originY, _bRe, _bIm);
|
||||||
|
|
||||||
|
// Un riquadro uniforme non ha spostamento misurabile: la normalizzazione al modulo
|
||||||
|
// unitario amplificherebbe il solo rumore numerico e l'antitrasformata darebbe un
|
||||||
|
// picco qualunque, indistinguibile da uno vero. Succede davvero — su una ripresa che
|
||||||
|
// attraversa il giorno con pose da trenta secondi il cielo esce bruciato e piatto —
|
||||||
|
// e senza questo controllo la stabilizzazione inseguiva spostamenti inventati.
|
||||||
|
if (deviationA < TextureFloor || deviationB < TextureFloor) return PhaseShift.None;
|
||||||
|
|
||||||
Fourier.Transform2D(_aRe, _aIm, _size, false);
|
Fourier.Transform2D(_aRe, _aIm, _size, false);
|
||||||
Fourier.Transform2D(_bRe, _bIm, _size, false);
|
Fourier.Transform2D(_bRe, _bIm, _size, false);
|
||||||
@@ -273,10 +286,15 @@ public sealed class PhaseCorrelator
|
|||||||
return MathF.Abs(offset) > 0.5f ? 0 : offset;
|
return MathF.Abs(offset) > 0.5f ? 0 : offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Estrae il riquadro, ne toglie la media e vi applica la finestra.</summary>
|
/// <summary>
|
||||||
private void Load(GrayImage image, int originX, int originY, float[] re, float[] im)
|
/// Estrae il riquadro, ne toglie la media e vi applica la finestra. Restituisce la
|
||||||
|
/// deviazione standard del riquadro, che dice se c'era qualcosa da misurare.
|
||||||
|
/// </summary>
|
||||||
|
private float Load(GrayImage image, int originX, int originY, float[] re, float[] im)
|
||||||
{
|
{
|
||||||
double sum = 0;
|
double sum = 0;
|
||||||
|
double sumSquares = 0;
|
||||||
|
|
||||||
for (int y = 0; y < _size; y++)
|
for (int y = 0; y < _size; y++)
|
||||||
{
|
{
|
||||||
int rowBase = y * _size;
|
int rowBase = y * _size;
|
||||||
@@ -285,14 +303,20 @@ public sealed class PhaseCorrelator
|
|||||||
float value = image.At(originX + x, originY + y);
|
float value = image.At(originX + x, originY + y);
|
||||||
re[rowBase + x] = value;
|
re[rowBase + x] = value;
|
||||||
sum += value;
|
sum += value;
|
||||||
|
sumSquares += (double)value * value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float mean = (float)(sum / re.Length);
|
double count = re.Length;
|
||||||
|
float mean = (float)(sum / count);
|
||||||
|
double variance = Math.Max(0, sumSquares / count - (double)mean * mean);
|
||||||
|
|
||||||
for (int i = 0; i < re.Length; i++)
|
for (int i = 0; i < re.Length; i++)
|
||||||
{
|
{
|
||||||
re[i] = (re[i] - mean) * _window[i];
|
re[i] = (re[i] - mean) * _window[i];
|
||||||
im[i] = 0f;
|
im[i] = 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (float)Math.Sqrt(variance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-6
@@ -38,12 +38,39 @@ internal static class Program
|
|||||||
{
|
{
|
||||||
EnsureConsole();
|
EnsureConsole();
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
return SequenceDiagnostics.Run(args[1], Console.Out,
|
|
||||||
renderPath: args.Length > 2 ? args[2] : null,
|
// Le parole chiave possono comparire in qualunque posizione: gli argomenti
|
||||||
renderFrames: args.Length > 3 && int.TryParse(args[3], out int n) ? n : 24,
|
// posizionali erano ormai troppi perché ricordarne l'ordine avesse senso.
|
||||||
renderWidth: args.Length > 4 && int.TryParse(args[4], out int w) ? w : 0,
|
var modules = SequenceDiagnostics.AdvancedModules.None;
|
||||||
renderCodec: args.Length > 5 && args[5].Equals("hevc", StringComparison.OrdinalIgnoreCase)
|
int analysisFrames = 0;
|
||||||
? Video.VideoCodec.Hevc : Video.VideoCodec.H264);
|
var positional = new List<string>();
|
||||||
|
|
||||||
|
foreach (string argument in args[1..])
|
||||||
|
{
|
||||||
|
if (argument.Equals("avanzato", StringComparison.OrdinalIgnoreCase))
|
||||||
|
modules |= SequenceDiagnostics.AdvancedModules.Analysis;
|
||||||
|
else if (argument.Equals("camera", StringComparison.OrdinalIgnoreCase))
|
||||||
|
modules |= SequenceDiagnostics.AdvancedModules.Camera;
|
||||||
|
else if (argument.Equals("mediana", StringComparison.OrdinalIgnoreCase))
|
||||||
|
modules |= SequenceDiagnostics.AdvancedModules.MedianStack;
|
||||||
|
else if (argument.Equals("scie", StringComparison.OrdinalIgnoreCase))
|
||||||
|
modules |= SequenceDiagnostics.AdvancedModules.StarTrails;
|
||||||
|
else if (argument.Equals("luminanza", StringComparison.OrdinalIgnoreCase))
|
||||||
|
modules |= SequenceDiagnostics.AdvancedModules.RegionsByLuminance;
|
||||||
|
else if (argument.StartsWith("analisi=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
int.TryParse(argument[8..], out analysisFrames);
|
||||||
|
else
|
||||||
|
positional.Add(argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SequenceDiagnostics.Run(positional[0], Console.Out,
|
||||||
|
renderPath: positional.Count > 1 ? positional[1] : null,
|
||||||
|
renderFrames: positional.Count > 2 && int.TryParse(positional[2], out int n) ? n : 24,
|
||||||
|
renderWidth: positional.Count > 3 && int.TryParse(positional[3], out int w) ? w : 0,
|
||||||
|
renderCodec: positional.Count > 4 && positional[4].Equals("hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? Video.VideoCodec.Hevc : Video.VideoCodec.H264,
|
||||||
|
advanced: modules,
|
||||||
|
analysisFrames: analysisFrames);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Length > 1 && args[0] == "--capture")
|
if (args.Length > 1 && args[0] == "--capture")
|
||||||
|
|||||||
+95
-5
@@ -114,9 +114,18 @@ sua inversa è la correzione. Una panoramica voluta sopravvive perché è già l
|
|||||||
del fotogramma; il deflicker legge un calo di luce e schiarisce tutto, paesaggio compreso, che
|
del fotogramma; il deflicker legge un calo di luce e schiarisce tutto, paesaggio compreso, che
|
||||||
invece non era cambiato. Titano divide il fotogramma in due regioni e dà a ciascuna la propria
|
invece non era cambiato. Titano divide il fotogramma in due regioni e dà a ciascuna la propria
|
||||||
curva. La maschera nasce una volta sola dalla mediana temporale di un campione di fotogrammi
|
curva. La maschera nasce una volta sola dalla mediana temporale di un campione di fotogrammi
|
||||||
— la mediana toglie di mezzo proprio ciò che passa e non resta — e la linea d'orizzonte viene
|
— la mediana toglie di mezzo proprio ciò che passa e non resta.
|
||||||
poi agganciata al massimo del gradiente verticale, che cade sul bordo vero e non dove capita
|
|
||||||
la soglia. Sulla scena di prova il terreno passa da 0,062 a 0,026 stop di oscillazione.
|
La linea d'orizzonte è il gradino più marcato del profilo di luminanza per riga, e viene poi
|
||||||
|
agganciata colonna per colonna al massimo del gradiente verticale. Il verso del gradino non
|
||||||
|
conta: cercare invece "la prima riga sotto soglia scendendo dall'alto" presuppone un cielo
|
||||||
|
chiaro e sgombro sopra la testa, e su una ripresa notturna fatta da sotto un pergolato quella
|
||||||
|
regola aggancia il bordo del tetto e chiama cielo le travi. Quando sopra la linea finisce
|
||||||
|
comunque un primo piano, la divisione lo dichiara e indica quella per luminanza, che non fa
|
||||||
|
ipotesi sulla forma.
|
||||||
|
|
||||||
|
Sulla sequenza reale della cometa il paesaggio passa da 0,156 a 0,020 stop di oscillazione:
|
||||||
|
l'87% dello sfarfallio che la curva globale gli avrebbe trasmesso non lo raggiunge.
|
||||||
|
|
||||||
**Transizioni giorno-notte.** Attraversando il tramonto la macchina deve cambiare tempo, ISO o
|
**Transizioni giorno-notte.** Attraversando il tramonto la macchina deve cambiare tempo, ISO o
|
||||||
diaframma, e ogni cambio è quantizzato: nel filmato si vede uno scalino netto. Il deflicker
|
diaframma, e ogni cambio è quantizzato: nel filmato si vede uno scalino netto. Il deflicker
|
||||||
@@ -128,6 +137,13 @@ già priva di gradini. Il bilanciamento del bianco riceve un trattamento paralle
|
|||||||
rapporto fra i canali invece della loro luminanza, così sparisce il tremolio del bilanciamento
|
rapporto fra i canali invece della loro luminanza, così sparisce il tremolio del bilanciamento
|
||||||
automatico e resta il viaggio verso il caldo del tramonto.
|
automatico e resta il viaggio verso il caldo del tramonto.
|
||||||
|
|
||||||
|
La temperatura di colore si ricava con l'approssimazione di McCamy, ma solo dove ha senso:
|
||||||
|
fuori dall'intorno del luogo di Planck il polinomio diverge, e su un cielo notturno — dove il
|
||||||
|
colore medio non somiglia a nessun corpo nero — restituiva decine di migliaia di kelvin.
|
||||||
|
Adesso in quel caso si dichiara inapplicabile invece di produrre un numero dall'aria
|
||||||
|
plausibile. Quello che resta sempre definito, e che il motore corregge davvero, è la deviazione
|
||||||
|
verde-magenta in stop.
|
||||||
|
|
||||||
**Movimento di macchina virtuale.** Un sensore da dodici megapixel contiene un 4K con
|
**Movimento di macchina virtuale.** Un sensore da dodici megapixel contiene un 4K con
|
||||||
abbondante margine di ritaglio: da lì si ricava una panoramica che in ripresa avrebbe richiesto
|
abbondante margine di ritaglio: da lì si ricava una panoramica che in ripresa avrebbe richiesto
|
||||||
una slitta motorizzata, decisa dopo, guardando il materiale. Fra un nodo e il successivo il
|
una slitta motorizzata, decisa dopo, guardando il materiale. Fra un nodo e il successivo il
|
||||||
@@ -193,6 +209,63 @@ video uscirebbe, peserebbe, e non si riprodurrebbe. Titano riduce quindi il foto
|
|||||||
massimo riproducibile conservando le proporzioni, e lo dichiara in interfaccia. Le sorgenti
|
massimo riproducibile conservando le proporzioni, e lo dichiara in interfaccia. Le sorgenti
|
||||||
16:9 fino al 4K UHD non vengono toccate.
|
16:9 fino al 4K UHD non vengono toccate.
|
||||||
|
|
||||||
|
## Cosa ha insegnato il materiale vero
|
||||||
|
|
||||||
|
I moduli avanzati sono stati provati su quattro sequenze reali — da 554 a 1005 scatti in DNG,
|
||||||
|
riprese notturne di comete e aurore con una GoPro HERO8 — dopo essere già passati da
|
||||||
|
cinquantaquattro controlli su scene sintetiche. Le scene sintetiche sono costruite perché la
|
||||||
|
risposta sia nota, e per questo non possono sorprendere: tutto ciò che segue è emerso solo dai
|
||||||
|
file veri, e ha portato a correzioni nel motore.
|
||||||
|
|
||||||
|
**La ricerca dell'orizzonte agganciava la tettoia.** Una delle riprese è fatta da sotto un
|
||||||
|
pergolato: travi scure in cima, cielo nel mezzo, alberi e case in fondo. Sul tratto notturno di
|
||||||
|
quella sequenza la vecchia regola — "la prima riga sotto soglia scendendo dall'alto" — trovava
|
||||||
|
il bordo del tetto e chiamava cielo le travi, il 7% dell'inquadratura e per giunta più scuro
|
||||||
|
del resto. Sulla ripresa di sedici ore l'errore si vedeva sull'intera sequenza: cielo al 29% e
|
||||||
|
separazione nulla fra le due regioni, cioè una divisione che non portava alcuna informazione.
|
||||||
|
Sostituita con il gradino più marcato del profilo di luminanza per riga, che non presuppone un
|
||||||
|
cielo chiaro in cima e funziona anche quando il cielo è il più scuro dei due: 91%
|
||||||
|
dell'inquadratura e 1,9 EV di separazione, con lo sfarfallio trasmesso al paesaggio ridotto del
|
||||||
|
79% invece che del 57%.
|
||||||
|
|
||||||
|
**La temperatura di colore inventava numeri.** Su un cielo stellato il colore medio non
|
||||||
|
somiglia a nessun corpo nero, l'approssimazione di McCamy diverge e usciva un intervallo da
|
||||||
|
1000 a 40000 K, troncato agli estremi. Ora fuori dall'intorno del luogo di Planck la misura si
|
||||||
|
dichiara inapplicabile. Il controllo che verifica la nuova regola ha subito trovato un buco nel
|
||||||
|
primo tentativo di filtro: un riquadro sulle coordinate cromatiche lascia passare un rosso
|
||||||
|
saturo, perché sa dire in quale zona si è ma non quanto si è vicini a una curva.
|
||||||
|
|
||||||
|
**La correlazione di fase inseguiva spostamenti inventati.** Una ripresa attraversa sedici ore
|
||||||
|
con pose da trenta secondi, quindi di giorno esce completamente bruciata. Su un riquadro
|
||||||
|
uniforme la normalizzazione al modulo unitario amplifica il solo rumore numerico e
|
||||||
|
l'antitrasformata dà un picco qualunque: la stabilizzazione misurava 9,8 px di tremolio medio,
|
||||||
|
181 px di punta, e chiedeva di ritagliare il 15% dell'inquadratura. Con un controllo di
|
||||||
|
tessitura sul riquadro il tremolio scende a 0,07 px e il ritaglio a 0,1%.
|
||||||
|
|
||||||
|
**I gradini dichiarati non sempre si vedono.** Se il fotogramma è già saturo, dimezzare la
|
||||||
|
sensibilità non lo scurisce: sopra il bianco non c'è niente da togliere. Sottrarre comunque il
|
||||||
|
gradino letto nei metadati introduceva nel segnale un salto che nell'immagine non esisteva. Ora
|
||||||
|
ogni gradino viene ridotto alla quota che la luminanza ha davvero recepito; su quella sequenza
|
||||||
|
7 cambi su 17 risultano non recepiti e vengono lasciati stare.
|
||||||
|
|
||||||
|
**La curva obiettivo poteva essere più a scatti del segnale che lisciava.** È il difetto più
|
||||||
|
serio, e stava nel nucleo del deflicker, non nei moduli nuovi. La robustezza di Tukey
|
||||||
|
presuppone anomalie sparse; dove invece un tratto contiguo si discosta — il crollo di luce del
|
||||||
|
crepuscolo — azzera l'intera finestra, e la stima commutava fra "uso i soli sopravvissuti" e
|
||||||
|
"uso tutto" aprendo uno scalino di 2,3 stop proprio dove serviva la massima continuità. Adesso
|
||||||
|
le due stime si mescolano con continuità secondo quanta finestra è sopravvissuta. Sulla ripresa
|
||||||
|
di sedici ore la riduzione dello sfarfallio passa dal 17% al 67%, e il salto massimo della
|
||||||
|
curva obiettivo da 1,73 a 0,21 stop.
|
||||||
|
|
||||||
|
Le scie stellari sono state verificate sull'uscita vera con la proprietà che le definisce: la
|
||||||
|
luminanza non può calare, perché ogni pixel trattiene il valore più alto incontrato. Su 150
|
||||||
|
fotogrammi cresce di 1,6 volte ed è non decrescente sul 99,3% dei passi — il resto è il rumore
|
||||||
|
della compressione.
|
||||||
|
|
||||||
|
Sul costo: con stabilizzazione, mediana, movimento virtuale e rimappatura tutti attivi insieme,
|
||||||
|
al profilo Massima e a 1920×1440, il rendering va a 0,7 fotogrammi al secondo. È il baratto che
|
||||||
|
il profilo dichiara, e si sceglie dal pannello Generale.
|
||||||
|
|
||||||
## Compilazione ed esecuzione
|
## Compilazione ed esecuzione
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -209,7 +282,7 @@ Titano.exe --selftest [cartella]
|
|||||||
```
|
```
|
||||||
|
|
||||||
Genera sequenze sintetiche dalle proprietà note e le fa attraversare l'intera pipeline,
|
Genera sequenze sintetiche dalle proprietà note e le fa attraversare l'intera pipeline,
|
||||||
confrontando 51 grandezze misurate con i valori attesi. Nessuna soglia è scelta a posteriori:
|
confrontando 54 grandezze misurate con i valori attesi. Nessuna soglia è scelta a posteriori:
|
||||||
la scena è costruita perché il valore atteso sia la conseguenza aritmetica di come è stata
|
la scena è costruita perché il valore atteso sia la conseguenza aritmetica di come è stata
|
||||||
generata — il tremolio ha un percorso noto, il gradino di esposizione un'ampiezza dichiarata
|
generata — il tremolio ha un percorso noto, il gradino di esposizione un'ampiezza dichiarata
|
||||||
nei metadati e visibile nei pixel, la nuvola attraversa il solo cielo.
|
nei metadati e visibile nei pixel, la nuvola attraversa il solo cielo.
|
||||||
@@ -221,12 +294,16 @@ della maschera delle regioni; riconoscimento e ridistribuzione dei cambi di espo
|
|||||||
lisciatura cromatica con la deriva del tramonto conservata; monotonia e simmetria delle curve
|
lisciatura cromatica con la deriva del tramonto conservata; monotonia e simmetria delle curve
|
||||||
di accelerazione; trasparenza e ritaglio dello stadio geometrico; monotonia della rimappatura
|
di accelerazione; trasparenza e ritaglio dello stadio geometrico; monotonia della rimappatura
|
||||||
temporale; rimozione dell'intruso da parte della mediana e conservazione dei passaggi da parte
|
temporale; rimozione dell'intruso da parte della mediana e conservazione dei passaggi da parte
|
||||||
del massimo; allineamento delle NAL dentro ogni campione del contenitore; e — prova conclusiva
|
del massimo; la garanzia che la curva lisciata non sia mai più a scatti del segnale che liscia,
|
||||||
|
nemmeno quando un tratto contiguo di fotogrammi si discosta; il rifiuto di dichiarare una
|
||||||
|
temperatura di colore dove non ne esiste una; allineamento delle NAL dentro ogni campione del
|
||||||
|
contenitore; e — prova conclusiva
|
||||||
— un rendering con tutti i moduli attivi insieme, con tetto di memoria volutamente stretto per
|
— un rendering con tutti i moduli attivi insieme, con tetto di memoria volutamente stretto per
|
||||||
esercitare il parcheggio su disco, riletto poi dal lettore di sistema.
|
esercitare il parcheggio su disco, riletto poi dal lettore di sistema.
|
||||||
|
|
||||||
```
|
```
|
||||||
Titano.exe --diagnose <cartella> [uscita.mp4] [fotogrammi] [larghezza] [h264|hevc]
|
Titano.exe --diagnose <cartella> [uscita.mp4] [fotogrammi] [larghezza] [h264|hevc]
|
||||||
|
[avanzato] [luminanza] [camera] [mediana|scie] [analisi=N]
|
||||||
```
|
```
|
||||||
|
|
||||||
Riporta cosa il programma riesce davvero a leggere da una cartella reale: metadati file per
|
Riporta cosa il programma riesce davvero a leggere da una cartella reale: metadati file per
|
||||||
@@ -235,6 +312,19 @@ risoluzioni diverse, cadenza sull'intera sequenza. Indicando un file di uscita e
|
|||||||
render di prova e ne riverifica il contenitore. È lo strumento con cui si distingue un difetto
|
render di prova e ne riverifica il contenitore. È lo strumento con cui si distingue un difetto
|
||||||
del motore da un formato che il sistema non sa aprire.
|
del motore da un formato che il sistema non sa aprire.
|
||||||
|
|
||||||
|
Le parole chiave finali, in qualunque ordine, accendono i moduli avanzati e aggiungono una
|
||||||
|
sezione che riporta cosa ciascuno ha trovato: entità del tremolio e ritaglio necessario, forma
|
||||||
|
della maschera e quanto la curva di regione stabilizza il paesaggio rispetto a quella globale,
|
||||||
|
numero e ampiezza dei cambi di impostazione, escursione della tinta. `luminanza` divide per
|
||||||
|
luminanza invece che per linea d'orizzonte, `camera` aggiunge movimento virtuale e rimappatura
|
||||||
|
del tempo al render di prova, `mediana` e `scie` scelgono l'accumulo temporale, `analisi=N`
|
||||||
|
limita l'analisi ai primi N fotogrammi — contigui, perché la stabilizzazione confronta ogni
|
||||||
|
fotogramma con il precedente.
|
||||||
|
|
||||||
|
È lo strumento che ha fatto emergere i due difetti visibili solo su materiale vero: la ricerca
|
||||||
|
dell'orizzonte che agganciava il bordo di una tettoia, e la temperatura di colore che su un
|
||||||
|
cielo notturno restituiva decine di migliaia di kelvin invece di dichiararsi inapplicabile.
|
||||||
|
|
||||||
```
|
```
|
||||||
Titano.exe --capture <file.png> [cartella-sequenza] [scheda] [avanzato]
|
Titano.exe --capture <file.png> [cartella-sequenza] [scheda] [avanzato]
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user