Files
Titano/Titano/Imaging/ImageBuffer.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

117 lines
4.0 KiB
C#

using System.Collections.Concurrent;
namespace Titano.Imaging;
/// <summary>
/// Fotogramma in memoria: RGB impacchettato, float32, luce lineare (gamma rimossa).
/// L'array proviene sempre da un <see cref="FrameBufferPool"/>: nessuna allocazione
/// per fotogramma dopo il riscaldamento, quindi impronta di memoria stazionaria.
/// </summary>
public sealed class ImageBuffer : IDisposable
{
public const int Channels = 3;
public int Width { get; private set; }
public int Height { get; private set; }
public float[] Data { get; private set; }
internal FrameBufferPool? Owner;
private int _disposed;
public int PixelCount => Width * Height;
public int SampleCount => Width * Height * Channels;
internal ImageBuffer(int width, int height, float[] data, FrameBufferPool? owner)
{
Width = width;
Height = height;
Data = data;
Owner = owner;
}
/// <summary>Indice del primo campione (canale R) del pixel indicato.</summary>
public int Offset(int x, int y) => (y * Width + x) * Channels;
public void CopyFrom(ImageBuffer other)
{
if (other.Width != Width || other.Height != Height)
throw new ArgumentException("Dimensioni non compatibili.", nameof(other));
Array.Copy(other.Data, Data, SampleCount);
}
public ImageBuffer CloneFromPool(FrameBufferPool pool)
{
var copy = pool.Rent(Width, Height);
Array.Copy(Data, copy.Data, SampleCount);
return copy;
}
/// <summary>Restituisce il buffer al pool. Idempotente.</summary>
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
var owner = Owner;
Owner = null;
owner?.Return(Data, Width, Height);
Data = [];
Width = Height = 0;
}
}
/// <summary>
/// Pool di array float per fotogrammi di dimensione omogenea. Gli array sono trattenuti
/// per lunghezza esatta e in numero limitato: l'occupazione totale è funzione della
/// profondità della pipeline, non del numero di fotogrammi della sequenza.
/// </summary>
public sealed class FrameBufferPool(int maxRetained = 8)
{
private readonly ConcurrentDictionary<int, ConcurrentBag<float[]>> _bins = new();
private readonly int _maxRetained = Math.Max(2, maxRetained);
private int _retained;
private long _allocatedBytes;
/// <summary>Numero di array attualmente trattenuti dal pool.</summary>
public int Retained => Volatile.Read(ref _retained);
/// <summary>
/// Byte di memoria pixel effettivamente allocati dall'avvio. Poiché gli array vengono
/// riutilizzati, questo valore si stabilizza dopo i primi fotogrammi e rappresenta
/// l'impronta stazionaria della pipeline.
/// </summary>
public long AllocatedBytes => Interlocked.Read(ref _allocatedBytes);
public ImageBuffer Rent(int width, int height)
{
if (width <= 0 || height <= 0) throw new ArgumentOutOfRangeException(nameof(width));
int length = checked(width * height * ImageBuffer.Channels);
if (_bins.TryGetValue(length, out var bag) && bag.TryTake(out var array))
{
Interlocked.Decrement(ref _retained);
return new ImageBuffer(width, height, array, this);
}
var fresh = GC.AllocateUninitializedArray<float>(length);
Interlocked.Add(ref _allocatedBytes, (long)length * sizeof(float));
return new ImageBuffer(width, height, fresh, this);
}
internal void Return(float[] array, int width, int height)
{
if (array.Length == 0) return;
if (Volatile.Read(ref _retained) >= _maxRetained) return; // eccedenza lasciata al GC
var bag = _bins.GetOrAdd(array.Length, static _ => []);
bag.Add(array);
Interlocked.Increment(ref _retained);
}
/// <summary>Svuota il pool: usato al cambio di risoluzione di lavoro.</summary>
public void Clear()
{
_bins.Clear();
Interlocked.Exchange(ref _retained, 0);
Interlocked.Exchange(ref _allocatedBytes, 0);
}
}