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>
82 lines
3.1 KiB
C#
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);
|
|
}
|