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>
160 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|