Files
Titano/Titano/Video/Nv12Converter.cs
T
Alby96andClaude Opus 5 cc1d040ac0 Titano: motore e interfaccia per time-lapse, senza dipendenze di terze parti
Applicazione desktop completa per creazione e ottimizzazione di time-lapse.
Il vincolo che ne definisce l'architettura è l'assenza totale di componenti
di terze parti: il progetto non ha alcun PackageReference e non invoca processi
esterni. Oltre alla libreria standard di .NET si usano solo API native di
Windows (WIC, Media Foundation, GDI+, DWM) richiamate via P/Invoke scritto a mano.

Sono in-house tutte le parti che di norma si delegherebbero a una libreria:
il parser binario EXIF/XMP, la misura di luminanza e la curva di deflicker,
il calcolo del campo vettoriale di movimento con il motion blur sintetico,
il multiplexer MP4 e ogni controllo dell'interfaccia.

Scelte algoritmiche che meritano una nota:
- il deflicker usa una regressione lineare locale pesata con seconda passata
  robusta, così le rampe reali di luce (alba, tramonto) sopravvivono mentre
  lo sfarfallio del diaframma viene rimosso; una media mobile semplice le
  appiattirebbe entrambe;
- la sfocatura mancante si compone in quadratura con quella già incisa nello
  scatto, perché sommarla linearmente renderebbe l'immagine troppo morbida;
- la luminanza si misura come media logaritmica troncata, invariante alla
  scala e insensibile a cieli bruciati e ombre chiuse.

L'elaborazione non produce file temporanei e mantiene un'occupazione di memoria
stazionaria: buffer poolati e canale a capacità limitata rendono i fotogrammi
vivi indipendenti dalla lunghezza della sequenza.

Verificato con "Titano.exe --selftest": 23 controlli su una sequenza sintetica
dalle proprietà note, incluse la struttura del contenitore prodotto e la sua
ri-decodifica con il lettore di sistema. Tutti superati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:40:26 +02:00

82 lines
3.1 KiB
C#

using Titano.Imaging;
namespace Titano.Video;
/// <summary>
/// Conversione da RGB lineare a NV12 (Y a piena risoluzione + CbCr interlacciato a metà
/// risoluzione), il formato d'ingresso atteso dagli encoder hardware.
///
/// La matrice è quella BT.709 in range televisivo (Y 16-235, C 16-240): è la convenzione
/// dichiarata nel box "colr" del contenitore, quindi il colore resta coerente in riproduzione.
/// La codifica di gamma viene riapplicata qui, all'ultimo passaggio utile: tutta l'elaborazione
/// a monte è avvenuta in luce lineare.
/// </summary>
public static class Nv12Converter
{
public static int RequiredSize(int width, int height) => width * height * 3 / 2;
public static unsafe void Convert(ImageBuffer source, byte* destination)
{
int width = source.Width;
int height = source.Height;
var data = source.Data;
byte* yPlane = destination;
byte* uvPlane = destination + (long)width * height;
int chromaRows = height / 2;
Parallel.For(0, chromaRows, cy =>
{
int y0 = cy * 2;
int y1 = Math.Min(y0 + 1, height - 1);
for (int cx = 0; cx < width / 2; cx++)
{
int x0 = cx * 2;
int x1 = Math.Min(x0 + 1, width - 1);
double cbSum = 0, crSum = 0;
cbSum += Encode(data, width, x0, y0, yPlane, out double cr00); crSum += cr00;
cbSum += Encode(data, width, x1, y0, yPlane, out double cr10); crSum += cr10;
cbSum += Encode(data, width, x0, y1, yPlane, out double cr01); crSum += cr01;
cbSum += Encode(data, width, x1, y1, yPlane, out double cr11); crSum += cr11;
long uvIndex = (long)cy * width + cx * 2;
uvPlane[uvIndex] = Quantize(128.0 + 224.0 * (cbSum * 0.25));
uvPlane[uvIndex + 1] = Quantize(128.0 + 224.0 * (crSum * 0.25));
}
});
// Riga o colonna dispari residua: la luma va comunque scritta.
if ((height & 1) != 0)
{
int y = height - 1;
for (int x = 0; x < width; x++) Encode(data, width, x, y, yPlane, out _);
}
if ((width & 1) != 0)
{
int x = width - 1;
for (int y = 0; y < height; y++) Encode(data, width, x, y, yPlane, out _);
}
}
/// <summary>Scrive la luma del pixel indicato e restituisce le due crominanze normalizzate.</summary>
private static unsafe double Encode(float[] data, int width, int x, int y, byte* yPlane, out double cr)
{
int index = (y * width + x) * ImageBuffer.Channels;
float r = ColorSpace.ToSrgb(data[index]);
float g = ColorSpace.ToSrgb(data[index + 1]);
float b = ColorSpace.ToSrgb(data[index + 2]);
double luma = ColorSpace.LumaR * r + ColorSpace.LumaG * g + ColorSpace.LumaB * b;
yPlane[(long)y * width + x] = Quantize(16.0 + 219.0 * luma);
cr = (r - luma) / 1.5748;
return (b - luma) / 1.8556;
}
private static byte Quantize(double value)
=> (byte)Math.Clamp((int)(value + 0.5), 0, 255);
}