Files
Titano/Titano/Diagnostics/SyntheticSequence.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

160 lines
6.3 KiB
C#

using System.Drawing;
using System.Drawing.Imaging;
namespace Titano.Diagnostics;
/// <summary>
/// Generatore di sequenze sintetiche per la diagnostica: produce JPEG con Exif completo,
/// una traslazione nota del contenuto (verifica dell'optical flow), uno sfarfallio noto
/// sovrapposto a una rampa di luce (verifica del deflicker) e una pausa dell'intervallometro
/// (verifica dell'analisi di cadenza).
/// </summary>
internal static class SyntheticSequence
{
public sealed record Definition(
int FrameCount = 48,
int Width = 640,
int Height = 360,
double IntervalSeconds = 2.0,
double ExposureSeconds = 0.02,
double FlickerStops = 0.12,
double RampStops = -0.9,
int PauseAtFrame = 20,
int PauseFrames = 4,
double PauseMultiplier = 3.0,
int ShiftX = 6,
int ShiftY = 2);
/// <summary>Scrive la sequenza nella cartella indicata e restituisce i percorsi generati.</summary>
public static List<string> Write(string directory, Definition definition)
{
Directory.CreateDirectory(directory);
var paths = new List<string>(definition.FrameCount);
var origin = new DateTime(2026, 6, 1, 18, 30, 0, DateTimeKind.Unspecified);
double elapsed = 0;
for (int i = 0; i < definition.FrameCount; i++)
{
double exposureScale = Math.Pow(2.0, Ramp(definition, i) + Flicker(definition, i));
byte[] jpeg = RenderJpeg(definition, i, exposureScale);
var timestamp = origin.AddSeconds(elapsed);
int subSecond = (int)Math.Round((elapsed - Math.Floor(elapsed)) * 100) % 100;
byte[] exif = ExifWriter.BuildExifBlock(timestamp, subSecond, definition.ExposureSeconds,
8.0, 200, definition.Width, definition.Height,
"TITANO", "TITANO Synthetic");
string path = Path.Combine(directory, $"TITANO_{i:D4}.jpg");
File.WriteAllBytes(path, ExifWriter.InsertIntoJpeg(jpeg, exif));
paths.Add(path);
bool inPause = i >= definition.PauseAtFrame && i < definition.PauseAtFrame + definition.PauseFrames;
elapsed += definition.IntervalSeconds * (inPause ? definition.PauseMultiplier : 1.0) + 0.37;
}
return paths;
}
public static double Ramp(Definition definition, int index)
=> definition.RampStops * index / Math.Max(1, definition.FrameCount - 1);
/// <summary>Sfarfallio deterministico ma non periodico rispetto alla finestra di smoothing.</summary>
public static double Flicker(Definition definition, int index)
{
int pattern = (index * 7 + (index * index) % 5) % 7;
return definition.FlickerStops * ((pattern - 3) / 3.0);
}
private static byte[] RenderJpeg(Definition definition, int index, double exposureScale)
{
int width = definition.Width;
int height = definition.Height;
double offsetX = index * definition.ShiftX;
double offsetY = index * definition.ShiftY;
using var bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
var rect = new Rectangle(0, 0, width, height);
var locked = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
try
{
unsafe
{
byte* basePtr = (byte*)locked.Scan0;
for (int y = 0; y < height; y++)
{
byte* row = basePtr + (long)y * locked.Stride;
for (int x = 0; x < width; x++)
{
double sx = x + offsetX;
double sy = y + offsetY;
// Rumore di valore su tre ottave: tessitura ovunque e a banda limitata,
// condizioni ideali per uno schema differenziale di optical flow.
// L'ottava più fine serve a rendere misurabile l'effetto del motion blur.
double texture = 0.50 * ValueNoise(sx / 24.0, sy / 24.0, 11)
+ 0.30 * ValueNoise(sx / 7.0, sy / 7.0, 37)
+ 0.20 * ValueNoise(sx / 3.0, sy / 3.0, 53);
double value = Math.Clamp((0.18 + 0.62 * texture) * exposureScale, 0.0, 1.0);
byte gray = (byte)Math.Clamp((int)(Math.Pow(value, 1.0 / 2.2) * 255.0 + 0.5), 0, 255);
byte* pixel = row + x * 3;
pixel[0] = (byte)(gray * 0.94); // B
pixel[1] = gray; // G
pixel[2] = (byte)Math.Min(255, gray * 1.04); // R
}
}
}
}
finally
{
bitmap.UnlockBits(locked);
}
using var stream = new MemoryStream();
var codec = ImageCodecInfo.GetImageEncoders().First(c => c.FormatID == ImageFormat.Jpeg.Guid);
using var parameters = new EncoderParameters(1);
using var quality = new EncoderParameter(Encoder.Quality, 92L);
parameters.Param[0] = quality;
bitmap.Save(stream, codec, parameters);
return stream.ToArray();
}
/// <summary>Rumore di valore bilineare su reticolo intero, deterministico.</summary>
private static double ValueNoise(double x, double y, int seed)
{
int x0 = (int)Math.Floor(x);
int y0 = (int)Math.Floor(y);
double fx = x - x0;
double fy = y - y0;
// Interpolazione con curva di Hermite: derivata continua, niente discontinuità di gradiente.
double sx = fx * fx * (3 - 2 * fx);
double sy = fy * fy * (3 - 2 * fy);
double a = Hash(x0, y0, seed);
double b = Hash(x0 + 1, y0, seed);
double c = Hash(x0, y0 + 1, seed);
double d = Hash(x0 + 1, y0 + 1, seed);
double top = a + (b - a) * sx;
double bottom = c + (d - c) * sx;
return top + (bottom - top) * sy;
}
private static double Hash(int x, int y, int seed)
{
unchecked
{
uint h = (uint)(x * 73856093) ^ (uint)(y * 19349663) ^ (uint)(seed * 83492791);
h ^= h >> 13;
h *= 0x85EBCA6B;
h ^= h >> 16;
return (h & 0xFFFFFF) / (double)0xFFFFFF;
}
}
}