Orientamento regolabile, profilo di qualita' e icona dell'applicazione
ORIENTAMENTO Il tag Exif dice come raddrizzare l'immagine, ma non basta leggerlo: alcuni decodificatori di sistema lo applicano gia' per conto proprio e altri no, e la stessa installazione di Windows si comporta diversamente a seconda del formato. Applicare la trasformazione due volte ribalta il fotogramma, non applicarla mai lo lascia coricato. In modalita' automatica Titano confronta ora le dimensioni che il decodificatore restituisce con quelle che il file dichiara di contenere: se la trasformazione scambia gli assi ed e' gia' stata applicata, non la riapplica. La sezione Generale mostra cosa ha dedotto e con quale motivo, e permette di imporre una delle sei trasformazioni Exif quando i metadati sono sbagliati. L'anteprima mostra subito l'effetto della scelta. Sui DNG GoPro il rilevamento riporta "specchiata orizzontalmente (dal tag Exif del file)": e' quanto dichiara il file e quanto applicano i lettori conformi alla specifica DNG. Se all'occhio risulta sbagliato, ora si cambia dal menu. QUALITA' PRIMA DELLA VELOCITA' Nuovo profilo di elaborazione con tre livelli, predefinito su Massima. Governa la finezza della griglia del campo vettoriale, i livelli della piramide, le iterazioni per livello, i campioni per pixel della scia e la risoluzione della passata fotometrica. Costa circa tre volte il tempo del profilo Standard e si vede nei bordi dei soggetti in movimento. Non e' stato introdotto alcun uso del disco come memoria d'appoggio: nessuna fase della pipeline supera cio' che la RAM regge, e i file temporanei sono esclusi per scelta architetturale. Se in futuro servira' tenere in vita molti fotogrammi a piena risoluzione, quella e' la porta da cui entrare. ICONA Scie stellari attorno al polo celeste: l'immagine in cui il tempo diventa visibile, e che dice insieme time-lapse e cielo notturno senza ricorrere a orologi o otturatori. Disegnata in-house con GDI+, contenitore ICO scritto a mano con sette livelli. Sotto i venti pixel il disegno si semplifica a due archi piu' spessi: alla dimensione della barra delle applicazioni sette scie sottili diventerebbero una macchia. La verifica del motore sale a 25 controlli: si aggiunge che lo specchiamento richiesto sposti davvero i pixel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,274 @@
|
|||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
|
||||||
|
namespace Titano.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generatore dell'icona dell'applicazione.
|
||||||
|
///
|
||||||
|
/// Il soggetto sono le scie stellari: in una posa lunga le stelle lasciano archi concentrici
|
||||||
|
/// attorno al polo celeste, ed è l'immagine in cui il tempo diventa visibile. Dice insieme
|
||||||
|
/// "time-lapse" e "cielo notturno" senza ricorrere a orologi o otturatori.
|
||||||
|
///
|
||||||
|
/// L'icona non è un file d'arte importato: viene disegnata qui e il contenitore ICO è scritto
|
||||||
|
/// a mano, coerentemente con il resto del progetto.
|
||||||
|
/// </summary>
|
||||||
|
public static class IconGenerator
|
||||||
|
{
|
||||||
|
private static readonly int[] Sizes = [16, 24, 32, 48, 64, 128, 256];
|
||||||
|
|
||||||
|
public static void Write(string path)
|
||||||
|
{
|
||||||
|
var images = new List<(int Size, byte[] Data, bool Png)>();
|
||||||
|
|
||||||
|
foreach (int size in Sizes)
|
||||||
|
{
|
||||||
|
using var bitmap = Render(size);
|
||||||
|
// Le dimensioni piccole restano in DIB, la più grande in PNG: è la combinazione
|
||||||
|
// che ogni versione di Windows sa leggere senza sorprese.
|
||||||
|
images.Add(size >= 256
|
||||||
|
? (size, EncodePng(bitmap), true)
|
||||||
|
: (size, EncodeDib(bitmap), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = new FileStream(path, FileMode.Create, FileAccess.Write);
|
||||||
|
Span<byte> header = stackalloc byte[6];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header, 0); // riservato
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[2..], 1); // tipo: icona
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[4..], (ushort)images.Count);
|
||||||
|
stream.Write(header);
|
||||||
|
|
||||||
|
int offset = 6 + 16 * images.Count;
|
||||||
|
Span<byte> entry = stackalloc byte[16];
|
||||||
|
|
||||||
|
foreach (var (size, data, _) in images)
|
||||||
|
{
|
||||||
|
entry.Clear();
|
||||||
|
entry[0] = (byte)(size >= 256 ? 0 : size); // 0 significa 256
|
||||||
|
entry[1] = (byte)(size >= 256 ? 0 : size);
|
||||||
|
entry[2] = 0; // colori della tavolozza
|
||||||
|
entry[3] = 0; // riservato
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(entry[4..], 1); // piani
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(entry[6..], 32); // bit per pixel
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(entry[8..], (uint)data.Length);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(entry[12..], (uint)offset);
|
||||||
|
stream.Write(entry);
|
||||||
|
offset += data.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (_, data, _) in images) stream.Write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ disegno
|
||||||
|
|
||||||
|
private static Bitmap Render(int size)
|
||||||
|
{
|
||||||
|
var bitmap = new Bitmap(size, size, PixelFormat.Format32bppArgb);
|
||||||
|
using var g = Graphics.FromImage(bitmap);
|
||||||
|
|
||||||
|
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
|
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
g.Clear(Color.Transparent);
|
||||||
|
|
||||||
|
float s = size;
|
||||||
|
var bounds = new RectangleF(s * 0.015f, s * 0.015f, s * 0.97f, s * 0.97f);
|
||||||
|
|
||||||
|
// Cielo: dal blu notte dell'ora blu al nero dello zenit.
|
||||||
|
using (var path = RoundedRectangle(bounds, s * 0.22f))
|
||||||
|
{
|
||||||
|
using var sky = new LinearGradientBrush(
|
||||||
|
new PointF(0, bounds.Top), new PointF(0, bounds.Bottom),
|
||||||
|
Color.FromArgb(0x0E, 0x16, 0x2C), Color.FromArgb(0x04, 0x06, 0x0D));
|
||||||
|
g.FillPath(sky, path);
|
||||||
|
g.SetClip(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var pole = new PointF(s * 0.50f, s * 0.44f);
|
||||||
|
|
||||||
|
// Sotto i venti pixel non c'è spazio per sette archi sottili: diventano una macchia.
|
||||||
|
// Le dimensioni piccole ricevono quindi meno scie, più spesse e più distanti fra loro.
|
||||||
|
// È la stessa immagine, detta con meno parole.
|
||||||
|
bool tiny = size <= 20;
|
||||||
|
bool small = size <= 36;
|
||||||
|
|
||||||
|
float trailWidth = Math.Max(1.4f, s * (tiny ? 0.10f : small ? 0.072f : 0.042f));
|
||||||
|
|
||||||
|
Color coolWhite = Color.FromArgb(0xD8, 0xEA, 0xFF);
|
||||||
|
Color iceBlue = Color.FromArgb(0x8F, 0xC8, 0xFF);
|
||||||
|
Color warm = Color.FromArgb(0xFF, 0xDA, 0xA8);
|
||||||
|
|
||||||
|
// Archi concentrici: raggio crescente, apertura crescente verso l'esterno, come
|
||||||
|
// accade davvero — le stelle lontane dal polo percorrono più cielo.
|
||||||
|
(float Radius, float Start, float Sweep, Color Tint)[] trails = tiny
|
||||||
|
?
|
||||||
|
[
|
||||||
|
(0.23f, 165f, 150f, coolWhite),
|
||||||
|
(0.40f, 335f, 135f, warm),
|
||||||
|
]
|
||||||
|
: small
|
||||||
|
?
|
||||||
|
[
|
||||||
|
(0.17f, 190f, 130f, warm),
|
||||||
|
(0.30f, 10f, 150f, coolWhite),
|
||||||
|
(0.43f, 150f, 165f, iceBlue),
|
||||||
|
]
|
||||||
|
:
|
||||||
|
[
|
||||||
|
(0.10f, 200f, 120f, Color.FromArgb(0xFF, 0xE6, 0xC8)),
|
||||||
|
(0.19f, 20f, 140f, Color.FromArgb(0xCF, 0xE4, 0xFF)),
|
||||||
|
(0.19f, 215f, 95f, Color.FromArgb(0xFF, 0xD3, 0x9C)),
|
||||||
|
(0.29f, 165f, 160f, Color.FromArgb(0x9C, 0xD4, 0xFF)),
|
||||||
|
(0.29f, 355f, 85f, Color.FromArgb(0xFF, 0xFF, 0xFF)),
|
||||||
|
(0.39f, 130f, 175f, Color.FromArgb(0x7F, 0xC4, 0xFF)),
|
||||||
|
(0.39f, 330f, 100f, Color.FromArgb(0xFF, 0xE1, 0xB4)),
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach (var (radiusFactor, start, sweep, tint) in trails)
|
||||||
|
{
|
||||||
|
float radius = s * radiusFactor;
|
||||||
|
var box = new RectangleF(pole.X - radius, pole.Y - radius, radius * 2, radius * 2);
|
||||||
|
|
||||||
|
// Un tratto continuo tenue sotto, e sopra la sfumatura a segmenti: senza la base
|
||||||
|
// i segmenti si leggerebbero come una collana di perle invece che come una scia.
|
||||||
|
using (var basePen = new Pen(Color.FromArgb(26, tint), trailWidth)
|
||||||
|
{
|
||||||
|
StartCap = LineCap.Round,
|
||||||
|
EndCap = LineCap.Round,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
g.DrawArc(basePen, box, start, sweep);
|
||||||
|
}
|
||||||
|
|
||||||
|
int steps = Math.Max(24, (int)(sweep / 1.5f));
|
||||||
|
for (int i = 0; i < steps; i++)
|
||||||
|
{
|
||||||
|
float t = i / (float)(steps - 1);
|
||||||
|
// La coda sfuma, la testa resta accesa: è così che si legge il verso del moto.
|
||||||
|
int alpha = (int)(10 + 245 * t * t);
|
||||||
|
float a0 = start + sweep * i / steps;
|
||||||
|
float a1 = start + sweep * (i + 1) / steps;
|
||||||
|
|
||||||
|
using var pen = new Pen(Color.FromArgb(alpha, tint), trailWidth)
|
||||||
|
{
|
||||||
|
StartCap = LineCap.Round,
|
||||||
|
EndCap = LineCap.Round,
|
||||||
|
};
|
||||||
|
// I segmenti si sovrappongono: le estremità arrotondate si fondono fra loro.
|
||||||
|
g.DrawArc(pen, box, a0, a1 - a0 + (sweep / steps) * 0.9f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stella in testa alla scia.
|
||||||
|
double head = (start + sweep) * Math.PI / 180.0;
|
||||||
|
var tip = new PointF(pole.X + radius * (float)Math.Cos(head), pole.Y + radius * (float)Math.Sin(head));
|
||||||
|
float star = trailWidth * 0.85f;
|
||||||
|
using var glow = new SolidBrush(Color.FromArgb(235, Color.White));
|
||||||
|
g.FillEllipse(glow, tip.X - star, tip.Y - star, star * 2, star * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polo celeste: il punto fermo attorno a cui gira tutto.
|
||||||
|
float core = Math.Max(1f, s * (tiny ? 0.055f : 0.035f));
|
||||||
|
float haloFactor = tiny ? 1.7f : 2.4f;
|
||||||
|
using (var halo = new SolidBrush(Color.FromArgb(tiny ? 50 : 70, 0xBF, 0xDC, 0xFF)))
|
||||||
|
g.FillEllipse(halo, pole.X - core * haloFactor, pole.Y - core * haloFactor,
|
||||||
|
core * haloFactor * 2, core * haloFactor * 2);
|
||||||
|
using (var center = new SolidBrush(Color.White))
|
||||||
|
g.FillEllipse(center, pole.X - core, pole.Y - core, core * 2, core * 2);
|
||||||
|
|
||||||
|
// Orizzonte: una collina scura che ancora la scena alla terra. Sotto i venti pixel
|
||||||
|
// occuperebbe righe che servono alle scie, e sparisce.
|
||||||
|
if (!tiny)
|
||||||
|
{
|
||||||
|
using var horizon = new GraphicsPath();
|
||||||
|
horizon.AddBezier(
|
||||||
|
new PointF(bounds.Left, s * 0.86f),
|
||||||
|
new PointF(s * 0.30f, s * 0.74f),
|
||||||
|
new PointF(s * 0.66f, s * 0.90f),
|
||||||
|
new PointF(bounds.Right, s * 0.79f));
|
||||||
|
horizon.AddLine(bounds.Right, bounds.Bottom, bounds.Left, bounds.Bottom);
|
||||||
|
horizon.CloseFigure();
|
||||||
|
|
||||||
|
using var ground = new SolidBrush(Color.FromArgb(0xFF, 0x02, 0x03, 0x07));
|
||||||
|
g.FillPath(ground, horizon);
|
||||||
|
}
|
||||||
|
|
||||||
|
g.ResetClip();
|
||||||
|
|
||||||
|
// Bordo appena percettibile: stacca l'icona dagli sfondi chiari.
|
||||||
|
using (var path = RoundedRectangle(RectangleF.Inflate(bounds, -0.5f, -0.5f), s * 0.22f))
|
||||||
|
using (var edge = new Pen(Color.FromArgb(60, 0x8A, 0xB4, 0xE8), Math.Max(1f, s * 0.012f)))
|
||||||
|
{
|
||||||
|
g.DrawPath(edge, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GraphicsPath RoundedRectangle(RectangleF bounds, float radius)
|
||||||
|
{
|
||||||
|
var path = new GraphicsPath();
|
||||||
|
float diameter = Math.Min(radius * 2, Math.Min(bounds.Width, bounds.Height));
|
||||||
|
var arc = new RectangleF(bounds.X, bounds.Y, diameter, diameter);
|
||||||
|
|
||||||
|
path.AddArc(arc, 180, 90);
|
||||||
|
arc.X = bounds.Right - diameter;
|
||||||
|
path.AddArc(arc, 270, 90);
|
||||||
|
arc.Y = bounds.Bottom - diameter;
|
||||||
|
path.AddArc(arc, 0, 90);
|
||||||
|
arc.X = bounds.X;
|
||||||
|
path.AddArc(arc, 90, 90);
|
||||||
|
path.CloseFigure();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ codifica
|
||||||
|
|
||||||
|
private static byte[] EncodePng(Bitmap bitmap)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
bitmap.Save(stream, ImageFormat.Png);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DIB a 32 bit come lo vuole il formato ICO: intestazione con altezza doppia, pixel
|
||||||
|
/// dal basso verso l'alto e una maschera AND che resta comunque presente, anche se con
|
||||||
|
/// il canale alfa non serve più a nulla.
|
||||||
|
/// </summary>
|
||||||
|
private static byte[] EncodeDib(Bitmap bitmap)
|
||||||
|
{
|
||||||
|
int width = bitmap.Width;
|
||||||
|
int height = bitmap.Height;
|
||||||
|
int maskStride = (width + 31) / 32 * 4;
|
||||||
|
|
||||||
|
var output = new byte[40 + width * height * 4 + maskStride * height];
|
||||||
|
Span<byte> header = output.AsSpan(0, 40);
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header, 40);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(header[4..], width);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(header[8..], height * 2);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[12..], 1);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[14..], 32);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[20..], (uint)(width * height * 4));
|
||||||
|
|
||||||
|
var locked = bitmap.LockBits(new Rectangle(0, 0, width, height),
|
||||||
|
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
IntPtr row = locked.Scan0 + y * locked.Stride;
|
||||||
|
int destination = 40 + (height - 1 - y) * width * 4;
|
||||||
|
System.Runtime.InteropServices.Marshal.Copy(row, output, destination, width * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bitmap.UnlockBits(locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,6 +119,25 @@ public static class SelfTest
|
|||||||
Add(checks, "Decodifica — luminanza invariante alla scala", worstScaleError < 0.12,
|
Add(checks, "Decodifica — luminanza invariante alla scala", worstScaleError < 0.12,
|
||||||
$"scarto massimo {worstScaleError:0.000} EV fra piena risoluzione e versioni ridotte");
|
$"scarto massimo {worstScaleError:0.000} EV fra piena risoluzione e versioni ridotte");
|
||||||
|
|
||||||
|
// L'orientamento richiesto deve tradursi in pixel spostati davvero: si decodifica lo
|
||||||
|
// stesso file chiedendo lo specchiamento orizzontale e si confronta colonna per colonna.
|
||||||
|
using (var mirrored = ImageDecoder.Decode(paths[10], definition.Width, definition.Height, 2, pool))
|
||||||
|
{
|
||||||
|
double worstMirrorError = 0;
|
||||||
|
for (int y = 4; y < definition.Height - 4; y += 7)
|
||||||
|
{
|
||||||
|
for (int x = 4; x < definition.Width - 4; x += 7)
|
||||||
|
{
|
||||||
|
int direct = frameA.Offset(x, y);
|
||||||
|
int flipped = mirrored.Offset(definition.Width - 1 - x, y);
|
||||||
|
worstMirrorError = Math.Max(worstMirrorError,
|
||||||
|
Math.Abs(frameA.Data[direct] - mirrored.Data[flipped]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Add(checks, "Orientamento — lo specchiamento sposta i pixel", worstMirrorError < 1e-5,
|
||||||
|
$"scarto massimo {worstMirrorError:0.000000} fra fotogramma diretto e specchiato ribaltato");
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 5. optical flow
|
// ---------------------------------------------------------------- 5. optical flow
|
||||||
|
|
||||||
var flowEngine = new OpticalFlowEngine(project.Flow);
|
var flowEngine = new OpticalFlowEngine(project.Flow);
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ public static class SequenceDiagnostics
|
|||||||
var project = new Pipeline.TitanoProject();
|
var project = new Pipeline.TitanoProject();
|
||||||
project.Sequence = TimelapseSequence.Build(subset);
|
project.Sequence = TimelapseSequence.Build(subset);
|
||||||
project.Sequence.RecomputeTiming();
|
project.Sequence.RecomputeTiming();
|
||||||
|
project.DetectOrientation();
|
||||||
project.Export.OutputPath = outputPath;
|
project.Export.OutputPath = outputPath;
|
||||||
project.Export.FrameRate = 24;
|
project.Export.FrameRate = 24;
|
||||||
project.Export.BitrateMbps = 80;
|
project.Export.BitrateMbps = 80;
|
||||||
@@ -182,6 +183,8 @@ public static class SequenceDiagnostics
|
|||||||
var (requestedWidth, requestedHeight) = project.ResolveRequestedSize();
|
var (requestedWidth, requestedHeight) = project.ResolveRequestedSize();
|
||||||
long perFrame = (long)width * height * 3 * sizeof(float);
|
long perFrame = (long)width * height * 3 * sizeof(float);
|
||||||
output.WriteLine($" fotogrammi {subset.Count}");
|
output.WriteLine($" fotogrammi {subset.Count}");
|
||||||
|
output.WriteLine($" orientamento {project.Orientation.Description} ({project.Orientation.Reason})");
|
||||||
|
output.WriteLine($" profilo di qualità {project.General.Quality}");
|
||||||
output.WriteLine($" risoluzione {width}×{height}" +
|
output.WriteLine($" risoluzione {width}×{height}" +
|
||||||
(width != requestedWidth || height != requestedHeight
|
(width != requestedWidth || height != requestedHeight
|
||||||
? $" (ridotta da {requestedWidth}×{requestedHeight} per il limite del codec)"
|
? $" (ridotta da {requestedWidth}×{requestedHeight} per il limite del codec)"
|
||||||
|
|||||||
@@ -165,6 +165,20 @@ public static class ImageDecoder
|
|||||||
|
|
||||||
public static bool SwapsAxes(int orientation) => orientation is 5 or 6 or 7 or 8;
|
public static bool SwapsAxes(int orientation) => orientation is 5 or 6 or 7 or 8;
|
||||||
|
|
||||||
|
/// <summary>Descrizione leggibile di una trasformazione Exif.</summary>
|
||||||
|
public static string DescribeOrientation(int orientation) => orientation switch
|
||||||
|
{
|
||||||
|
1 => "nessuna trasformazione",
|
||||||
|
2 => "specchiata orizzontalmente",
|
||||||
|
3 => "ruotata di 180°",
|
||||||
|
4 => "specchiata verticalmente",
|
||||||
|
5 => "trasposta",
|
||||||
|
6 => "ruotata di 90° in senso orario",
|
||||||
|
7 => "trasversa",
|
||||||
|
8 => "ruotata di 90° in senso antiorario",
|
||||||
|
_ => "sconosciuta",
|
||||||
|
};
|
||||||
|
|
||||||
// ------------------------------------------------------------------ conversione + orientamento
|
// ------------------------------------------------------------------ conversione + orientamento
|
||||||
|
|
||||||
private static unsafe void Convert32(byte[] scratch, int stride, int width, int height,
|
private static unsafe void Convert32(byte[] scratch, int stride, int width, int height,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Titano.Metadata;
|
||||||
|
|
||||||
|
namespace Titano.Imaging;
|
||||||
|
|
||||||
|
/// <summary>Esito del rilevamento automatico dell'orientamento.</summary>
|
||||||
|
public sealed record OrientationDetection(int Orientation, string Reason)
|
||||||
|
{
|
||||||
|
public string Description => ImageDecoder.DescribeOrientation(Orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determina quale trasformazione applicare ai pixel dopo la decodifica.
|
||||||
|
///
|
||||||
|
/// Il tag Exif dice come va raddrizzata l'immagine, ma non basta leggerlo: alcuni
|
||||||
|
/// decodificatori di sistema lo applicano già per conto proprio e altri no, e la stessa
|
||||||
|
/// installazione di Windows si comporta diversamente a seconda del formato. Applicare la
|
||||||
|
/// trasformazione una seconda volta produce fotogrammi ribaltati; non applicarla mai
|
||||||
|
/// produce sequenze coricate. Qui si confronta ciò che il decodificatore restituisce con
|
||||||
|
/// ciò che il file dichiara di contenere, e si decide di conseguenza.
|
||||||
|
/// </summary>
|
||||||
|
public static class OrientationDetector
|
||||||
|
{
|
||||||
|
public static OrientationDetection Detect(FrameMetadata metadata)
|
||||||
|
{
|
||||||
|
int declared = metadata.Orientation;
|
||||||
|
if (declared is < 1 or > 8)
|
||||||
|
return new OrientationDetection(1, "il file non dichiara un orientamento valido");
|
||||||
|
|
||||||
|
if (declared == 1)
|
||||||
|
return new OrientationDetection(1, "il file non richiede trasformazioni");
|
||||||
|
|
||||||
|
// Le dimensioni memorizzate provengono dalla directory dell'immagine principale,
|
||||||
|
// quindi descrivono i pixel così come stanno nel file, prima di ogni rotazione.
|
||||||
|
int storedWidth = metadata.PixelWidth;
|
||||||
|
int storedHeight = metadata.PixelHeight;
|
||||||
|
if (storedWidth <= 0 || storedHeight <= 0)
|
||||||
|
return new OrientationDetection(declared, "dimensioni dichiarate assenti, si applica il tag Exif");
|
||||||
|
|
||||||
|
int decodedWidth, decodedHeight;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
(decodedWidth, decodedHeight) = ImageDecoder.ProbeDisplaySize(metadata.FilePath, 1);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return new OrientationDetection(declared, "sonda non riuscita, si applica il tag Exif");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decodedWidth <= 0 || decodedHeight <= 0)
|
||||||
|
return new OrientationDetection(declared, "sonda senza esito, si applica il tag Exif");
|
||||||
|
|
||||||
|
// Se la trasformazione scambia gli assi e il decodificatore restituisce già le
|
||||||
|
// dimensioni scambiate, l'ha applicata lui: riapplicarla ribalterebbe il fotogramma.
|
||||||
|
if (ImageDecoder.SwapsAxes(declared) &&
|
||||||
|
decodedWidth == storedHeight && decodedHeight == storedWidth)
|
||||||
|
{
|
||||||
|
return new OrientationDetection(1, "già raddrizzata dal decodificatore di sistema");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrientationDetection(declared, "dal tag Exif del file");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ public sealed class RenderPipeline(TitanoProject project)
|
|||||||
int analysisHeight = Math.Max(2, (int)Math.Round(analysisWidth * workingHeight / (double)workingWidth));
|
int analysisHeight = Math.Max(2, (int)Math.Round(analysisWidth * workingHeight / (double)workingWidth));
|
||||||
|
|
||||||
int parallelism = Math.Clamp(_project.General.DecodeParallelism, 1, 16);
|
int parallelism = Math.Clamp(_project.General.DecodeParallelism, 1, 16);
|
||||||
|
int orientation = _project.EffectiveOrientation;
|
||||||
var pool = new FrameBufferPool(parallelism + 2);
|
var pool = new FrameBufferPool(parallelism + 2);
|
||||||
var stats = new LuminanceStats[count];
|
var stats = new LuminanceStats[count];
|
||||||
var failures = new bool[count];
|
var failures = new bool[count];
|
||||||
@@ -97,7 +98,7 @@ public sealed class RenderPipeline(TitanoProject project)
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var buffer = ImageDecoder.Decode(record.FilePath, analysisWidth, analysisHeight,
|
using var buffer = ImageDecoder.Decode(record.FilePath, analysisWidth, analysisHeight,
|
||||||
record.Metadata.Orientation, pool);
|
orientation, pool);
|
||||||
stats[i] = LuminanceAnalyzer.Analyze(buffer);
|
stats[i] = LuminanceAnalyzer.Analyze(buffer);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
@@ -184,6 +185,7 @@ public sealed class RenderPipeline(TitanoProject project)
|
|||||||
var curve = _project.Curve;
|
var curve = _project.Curve;
|
||||||
|
|
||||||
int parallelism = Math.Clamp(_project.General.DecodeParallelism, 1, 16);
|
int parallelism = Math.Clamp(_project.General.DecodeParallelism, 1, 16);
|
||||||
|
int orientation = _project.EffectiveOrientation;
|
||||||
|
|
||||||
// Buffer contemporaneamente vivi: i cinque fissi del ciclo (corrente, successivo,
|
// Buffer contemporaneamente vivi: i cinque fissi del ciclo (corrente, successivo,
|
||||||
// sfocatura, interpolato, interpolato sfocato), le decodifiche in volo e la copia
|
// sfocatura, interpolato, interpolato sfocato), le decodifiche in volo e la copia
|
||||||
@@ -216,8 +218,8 @@ public sealed class RenderPipeline(TitanoProject project)
|
|||||||
{
|
{
|
||||||
cancellation.ThrowIfCancellationRequested();
|
cancellation.ThrowIfCancellationRequested();
|
||||||
int index = i;
|
int index = i;
|
||||||
var task = Task.Run(() => DecodeAndCorrect(sequence, index, width, height, pool,
|
var task = Task.Run(() => DecodeAndCorrect(sequence, index, width, height, orientation,
|
||||||
curve, deflickerSettings), cancellation);
|
pool, curve, deflickerSettings), cancellation);
|
||||||
await channel.Writer.WriteAsync(task, cancellation).ConfigureAwait(false);
|
await channel.Writer.WriteAsync(task, cancellation).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
channel.Writer.Complete();
|
channel.Writer.Complete();
|
||||||
@@ -342,14 +344,14 @@ public sealed class RenderPipeline(TitanoProject project)
|
|||||||
|
|
||||||
/// <summary>Decodifica un fotogramma e vi applica il guadagno di esposizione calcolato.</summary>
|
/// <summary>Decodifica un fotogramma e vi applica il guadagno di esposizione calcolato.</summary>
|
||||||
private static ImageBuffer? DecodeAndCorrect(TimelapseSequence sequence, int index, int width, int height,
|
private static ImageBuffer? DecodeAndCorrect(TimelapseSequence sequence, int index, int width, int height,
|
||||||
FrameBufferPool pool, DeflickerCurve? curve,
|
int orientation, FrameBufferPool pool, DeflickerCurve? curve,
|
||||||
DeflickerSettings settings)
|
DeflickerSettings settings)
|
||||||
{
|
{
|
||||||
var record = sequence.Frames[index];
|
var record = sequence.Frames[index];
|
||||||
ImageBuffer buffer;
|
ImageBuffer buffer;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
buffer = ImageDecoder.Decode(record.FilePath, width, height, record.Metadata.Orientation, pool);
|
buffer = ImageDecoder.Decode(record.FilePath, width, height, orientation, pool);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ using Titano.Video;
|
|||||||
|
|
||||||
namespace Titano.Pipeline;
|
namespace Titano.Pipeline;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compromesso fra qualità del risultato e tempo di elaborazione. Agisce sulla finezza
|
||||||
|
/// del campo di movimento, sui campioni della sfocatura e sulla risoluzione della passata
|
||||||
|
/// fotometrica: tutte grandezze che migliorano il risultato e costano tempo.
|
||||||
|
/// </summary>
|
||||||
|
public enum QualityProfile
|
||||||
|
{
|
||||||
|
Bozza,
|
||||||
|
Standard,
|
||||||
|
Massima,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Impostazioni della sezione "Generale".</summary>
|
/// <summary>Impostazioni della sezione "Generale".</summary>
|
||||||
public sealed class GeneralSettings
|
public sealed class GeneralSettings
|
||||||
{
|
{
|
||||||
@@ -12,7 +24,15 @@ public sealed class GeneralSettings
|
|||||||
public int WorkingWidth { get; set; }
|
public int WorkingWidth { get; set; }
|
||||||
|
|
||||||
/// <summary>Larghezza usata nella passata di analisi fotometrica: incide solo sulla velocità.</summary>
|
/// <summary>Larghezza usata nella passata di analisi fotometrica: incide solo sulla velocità.</summary>
|
||||||
public int AnalysisWidth { get; set; } = 1024;
|
public int AnalysisWidth { get; set; } = 2048;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trasformazione da applicare ai pixel decodificati; null lascia decidere al rilevamento
|
||||||
|
/// automatico. Serve quando i metadati del file sono sbagliati e l'occhio dice altro.
|
||||||
|
/// </summary>
|
||||||
|
public int? OrientationOverride { get; set; }
|
||||||
|
|
||||||
|
public QualityProfile Quality { get; set; } = QualityProfile.Massima;
|
||||||
|
|
||||||
/// <summary>Decodifiche simultanee. Limita anche i buffer in volo, quindi la memoria occupata.</summary>
|
/// <summary>Decodifiche simultanee. Limita anche i buffer in volo, quindi la memoria occupata.</summary>
|
||||||
public int DecodeParallelism { get; set; } = Math.Clamp(Environment.ProcessorCount / 2, 2, 8);
|
public int DecodeParallelism { get; set; } = Math.Clamp(Environment.ProcessorCount / 2, 2, 8);
|
||||||
@@ -35,6 +55,14 @@ public sealed class TitanoProject
|
|||||||
public OpticalFlowSettings Flow { get; set; } = new();
|
public OpticalFlowSettings Flow { get; set; } = new();
|
||||||
public ExportSettings Export { get; set; } = new();
|
public ExportSettings Export { get; set; } = new();
|
||||||
|
|
||||||
|
public TitanoProject()
|
||||||
|
{
|
||||||
|
// I valori predefiniti delle singole sezioni devono corrispondere al profilo
|
||||||
|
// dichiarato, altrimenti l'interfaccia mostrerebbe "Massima" con i parametri di
|
||||||
|
// "Standard" sotto i cursori.
|
||||||
|
ApplyQualityProfile(General.Quality);
|
||||||
|
}
|
||||||
|
|
||||||
public TimelapseSequence? Sequence { get; set; }
|
public TimelapseSequence? Sequence { get; set; }
|
||||||
|
|
||||||
/// <summary>Curva di deflicker dell'ultima analisi, usata dal grafico e dall'esportazione.</summary>
|
/// <summary>Curva di deflicker dell'ultima analisi, usata dal grafico e dall'esportazione.</summary>
|
||||||
@@ -46,6 +74,67 @@ public sealed class TitanoProject
|
|||||||
public bool HasSequence => Sequence is { Count: > 0 };
|
public bool HasSequence => Sequence is { Count: > 0 };
|
||||||
public bool IsAnalyzed => Curve is not null && Stats is not null;
|
public bool IsAnalyzed => Curve is not null && Stats is not null;
|
||||||
|
|
||||||
|
/// <summary>Orientamento dedotto dal primo fotogramma della sequenza.</summary>
|
||||||
|
public Imaging.OrientationDetection Orientation { get; private set; } = new(1, "nessuna sequenza caricata");
|
||||||
|
|
||||||
|
/// <summary>Trasformazione effettivamente applicata: la scelta dell'utente ha la precedenza.</summary>
|
||||||
|
public int EffectiveOrientation => General.OrientationOverride ?? Orientation.Orientation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Esamina il primo fotogramma per stabilire come vanno raddrizzati i pixel. Va invocato
|
||||||
|
/// dopo l'ingestion: la sequenza è omogenea, quindi un solo campione basta per tutti.
|
||||||
|
/// </summary>
|
||||||
|
public void DetectOrientation()
|
||||||
|
{
|
||||||
|
Orientation = Sequence is { Count: > 0 }
|
||||||
|
? Imaging.OrientationDetector.Detect(Sequence.Frames[0].Metadata)
|
||||||
|
: new Imaging.OrientationDetection(1, "nessuna sequenza caricata");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allinea i parametri di elaborazione al profilo di qualità scelto. Sovrascrive i
|
||||||
|
/// cursori delle sezioni avanzate: è il senso stesso di un profilo.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplyQualityProfile(QualityProfile profile)
|
||||||
|
{
|
||||||
|
General.Quality = profile;
|
||||||
|
|
||||||
|
switch (profile)
|
||||||
|
{
|
||||||
|
case QualityProfile.Bozza:
|
||||||
|
General.AnalysisWidth = 768;
|
||||||
|
Flow.AnalysisWidth = 640;
|
||||||
|
Flow.CellSize = 12;
|
||||||
|
Flow.PyramidLevels = 3;
|
||||||
|
Flow.WindowRadius = 5;
|
||||||
|
Flow.Iterations = 3;
|
||||||
|
MotionBlur.MaxSamples = 13;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case QualityProfile.Standard:
|
||||||
|
General.AnalysisWidth = 1024;
|
||||||
|
Flow.AnalysisWidth = 960;
|
||||||
|
Flow.CellSize = 8;
|
||||||
|
Flow.PyramidLevels = 4;
|
||||||
|
Flow.WindowRadius = 6;
|
||||||
|
Flow.Iterations = 5;
|
||||||
|
MotionBlur.MaxSamples = 25;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Il campo di movimento si infittisce e la scia guadagna campioni: sono le
|
||||||
|
// due voci che si vedono davvero nel fotogramma finale.
|
||||||
|
General.AnalysisWidth = 2048;
|
||||||
|
Flow.AnalysisWidth = 1440;
|
||||||
|
Flow.CellSize = 6;
|
||||||
|
Flow.PyramidLevels = 5;
|
||||||
|
Flow.WindowRadius = 7;
|
||||||
|
Flow.Iterations = 8;
|
||||||
|
MotionBlur.MaxSamples = 49;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Risoluzione di lavoro effettiva: parte dal primo fotogramma, applica l'eventuale limite
|
/// Risoluzione di lavoro effettiva: parte dal primo fotogramma, applica l'eventuale limite
|
||||||
/// dell'utente e arrotonda a valori pari, richiesti dal sottocampionamento cromatico 4:2:0.
|
/// dell'utente e arrotonda a valori pari, richiesti dal sottocampionamento cromatico 4:2:0.
|
||||||
@@ -55,14 +144,15 @@ public sealed class TitanoProject
|
|||||||
if (Sequence is not { Count: > 0 }) return (0, 0);
|
if (Sequence is not { Count: > 0 }) return (0, 0);
|
||||||
|
|
||||||
var first = Sequence.Frames[0].Metadata;
|
var first = Sequence.Frames[0].Metadata;
|
||||||
|
int orientation = EffectiveOrientation;
|
||||||
int sourceWidth = first.PixelWidth;
|
int sourceWidth = first.PixelWidth;
|
||||||
int sourceHeight = first.PixelHeight;
|
int sourceHeight = first.PixelHeight;
|
||||||
|
|
||||||
if (sourceWidth <= 0 || sourceHeight <= 0)
|
if (sourceWidth <= 0 || sourceHeight <= 0)
|
||||||
{
|
{
|
||||||
(sourceWidth, sourceHeight) = Imaging.ImageDecoder.ProbeDisplaySize(first.FilePath, first.Orientation);
|
(sourceWidth, sourceHeight) = Imaging.ImageDecoder.ProbeDisplaySize(first.FilePath, orientation);
|
||||||
}
|
}
|
||||||
else if (Imaging.ImageDecoder.SwapsAxes(first.Orientation))
|
else if (Imaging.ImageDecoder.SwapsAxes(orientation))
|
||||||
{
|
{
|
||||||
(sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
(sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
||||||
}
|
}
|
||||||
@@ -98,7 +188,7 @@ public sealed class TitanoProject
|
|||||||
int sourceWidth = first.PixelWidth;
|
int sourceWidth = first.PixelWidth;
|
||||||
int sourceHeight = first.PixelHeight;
|
int sourceHeight = first.PixelHeight;
|
||||||
if (sourceWidth <= 0 || sourceHeight <= 0) return (width, height);
|
if (sourceWidth <= 0 || sourceHeight <= 0) return (width, height);
|
||||||
if (Imaging.ImageDecoder.SwapsAxes(first.Orientation)) (sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
if (Imaging.ImageDecoder.SwapsAxes(EffectiveOrientation)) (sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
||||||
|
|
||||||
int requestedWidth = Export.Width > 0 ? Export.Width
|
int requestedWidth = Export.Width > 0 ? Export.Width
|
||||||
: General.WorkingWidth > 0 ? Math.Min(General.WorkingWidth, sourceWidth)
|
: General.WorkingWidth > 0 ? Math.Min(General.WorkingWidth, sourceWidth)
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ internal static class Program
|
|||||||
return SelfTest.Run(directory, Console.Out);
|
return SelfTest.Run(directory, Console.Out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Length > 1 && args[0] == "--make-icon")
|
||||||
|
{
|
||||||
|
EnsureConsole();
|
||||||
|
IconGenerator.Write(args[1]);
|
||||||
|
Console.WriteLine($"Icona generata in {args[1]}.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Length > 1 && args[0] == "--diagnose")
|
if (args.Length > 1 && args[0] == "--diagnose")
|
||||||
{
|
{
|
||||||
EnsureConsole();
|
EnsureConsole();
|
||||||
|
|||||||
+22
-1
@@ -84,6 +84,18 @@ miniatura, e le dimensioni vere vivono in una SubIFD marcata come non ridotta. T
|
|||||||
la directory dell'immagine principale invece di fidarsi della prima, e legge i metadati con una
|
la directory dell'immagine principale invece di fidarsi della prima, e legge i metadati con una
|
||||||
finestra che si estende su richiesta: di un DNG da 15 MiB ne tocca 128 KiB.
|
finestra che si estende su richiesta: di un DNG da 15 MiB ne tocca 128 KiB.
|
||||||
|
|
||||||
|
**Orientamento.** Il tag Exif dice come raddrizzare l'immagine, ma non basta leggerlo:
|
||||||
|
alcuni decodificatori di sistema lo applicano già per conto proprio e altri no. Applicarlo
|
||||||
|
una seconda volta ribalta il fotogramma, non applicarlo mai lo lascia coricato. In modalità
|
||||||
|
automatica Titano confronta le dimensioni che il decodificatore restituisce con quelle che il
|
||||||
|
file dichiara di contenere e decide di conseguenza; la sezione Generale mostra cosa ha dedotto
|
||||||
|
e permette di imporre una trasformazione diversa, con riscontro immediato nell'anteprima.
|
||||||
|
|
||||||
|
**Qualità.** Il profilo di elaborazione governa finezza della griglia del campo vettoriale,
|
||||||
|
livelli della piramide, iterazioni per livello, campioni per pixel della scia e risoluzione
|
||||||
|
della passata fotometrica. Il valore predefinito è "Massima": costa circa tre volte il tempo
|
||||||
|
di "Standard" e si vede nei bordi dei soggetti in movimento.
|
||||||
|
|
||||||
La risoluzione di lavoro viene infine ricondotta ai limiti del codec. Un encoder hardware
|
La risoluzione di lavoro viene infine ricondotta ai limiti del codec. Un encoder hardware
|
||||||
accetta volentieri un fotogramma 4000×3000 e dichiara un livello alto: il flusso è formalmente
|
accetta volentieri un fotogramma 4000×3000 e dichiara un livello alto: il flusso è formalmente
|
||||||
valido ma supera il Livello 5.2 di H.264, e i decodificatori comuni si rifiutano di aprirlo. Il
|
valido ma supera il Livello 5.2 di H.264, e i decodificatori comuni si rifiutano di aprirlo. Il
|
||||||
@@ -108,7 +120,7 @@ Titano.exe --selftest [cartella]
|
|||||||
|
|
||||||
Genera una sequenza sintetica dalle proprietà note — traslazione, sfarfallio e rampa di luce
|
Genera una sequenza sintetica dalle proprietà note — traslazione, sfarfallio e rampa di luce
|
||||||
imposti, pausa dell'intervallometro inclusa — e la fa attraversare l'intera pipeline,
|
imposti, pausa dell'intervallometro inclusa — e la fa attraversare l'intera pipeline,
|
||||||
confrontando 24 grandezze misurate con i valori attesi: campi Exif, cadenza, shutter angle,
|
confrontando 25 grandezze misurate con i valori attesi: campi Exif, cadenza, shutter angle,
|
||||||
riduzione dello sfarfallio, conservazione della rampa, invarianza della luminanza alla scala di
|
riduzione dello sfarfallio, conservazione della rampa, invarianza della luminanza alla scala di
|
||||||
decodifica, modulo e direzione del campo vettoriale, attenuazione del dettaglio dovuta alla
|
decodifica, modulo e direzione del campo vettoriale, attenuazione del dettaglio dovuta alla
|
||||||
sfocatura, allineamento delle NAL dentro ogni campione del contenitore e — prova conclusiva —
|
sfocatura, allineamento delle NAL dentro ogni campione del contenitore e — prova conclusiva —
|
||||||
@@ -129,3 +141,12 @@ Titano.exe --capture <file.png> [cartella-sequenza] [scheda]
|
|||||||
```
|
```
|
||||||
|
|
||||||
Cattura l'interfaccia in un'immagine, per verificarne la resa in modo riproducibile.
|
Cattura l'interfaccia in un'immagine, per verificarne la resa in modo riproducibile.
|
||||||
|
|
||||||
|
```
|
||||||
|
Titano.exe --make-icon <file.ico>
|
||||||
|
```
|
||||||
|
|
||||||
|
Ridisegna l'icona dell'applicazione — scie stellari attorno al polo celeste, l'immagine in cui
|
||||||
|
il tempo diventa visibile — e ne scrive il contenitore ICO a mano. I livelli sotto i venti
|
||||||
|
pixel ricevono meno archi e più spessi: alla dimensione della barra delle applicazioni sette
|
||||||
|
scie sottili diventerebbero una macchia.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<Platforms>x64</Platforms>
|
<Platforms>x64</Platforms>
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<AssemblyName>Titano</AssemblyName>
|
<AssemblyName>Titano</AssemblyName>
|
||||||
|
<ApplicationIcon>Titano.ico</ApplicationIcon>
|
||||||
<RootNamespace>Titano</RootNamespace>
|
<RootNamespace>Titano</RootNamespace>
|
||||||
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
|
||||||
<ApplicationDefaultFont>Segoe UI, 9pt</ApplicationDefaultFont>
|
<ApplicationDefaultFont>Segoe UI, 9pt</ApplicationDefaultFont>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
+22
-1
@@ -70,11 +70,28 @@ internal sealed class MainForm : Form
|
|||||||
_exportButton = new DarkButton { Text = "Esporta video", Width = 140, Primary = true };
|
_exportButton = new DarkButton { Text = "Esporta video", Width = 140, Primary = true };
|
||||||
_cancelButton = new DarkButton { Text = "Annulla", Width = 96, Danger = true, Visible = false };
|
_cancelButton = new DarkButton { Text = "Annulla", Width = 96, Danger = true, Visible = false };
|
||||||
|
|
||||||
|
LoadApplicationIcon();
|
||||||
BuildLayout();
|
BuildLayout();
|
||||||
WireEvents();
|
WireEvents();
|
||||||
UpdateCommandState();
|
UpdateCommandState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riprende l'icona dalle risorse dell'eseguibile: è la stessa che Esplora risorse
|
||||||
|
/// mostra sul file, quindi finestra, barra delle applicazioni e cartella restano coerenti.
|
||||||
|
/// </summary>
|
||||||
|
private void LoadApplicationIcon()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Environment.ProcessPath is { } executable) Icon = Icon.ExtractAssociatedIcon(executable);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Senza icona la finestra usa quella predefinita: nulla di grave.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnHandleCreated(EventArgs e)
|
protected override void OnHandleCreated(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnHandleCreated(e);
|
base.OnHandleCreated(e);
|
||||||
@@ -337,6 +354,8 @@ internal sealed class MainForm : Form
|
|||||||
_project.Sequence = sequence;
|
_project.Sequence = sequence;
|
||||||
_project.Curve = null;
|
_project.Curve = null;
|
||||||
_project.Stats = null;
|
_project.Stats = null;
|
||||||
|
_project.DetectOrientation();
|
||||||
|
_settings.ShowDetectedOrientation(_project.Orientation);
|
||||||
|
|
||||||
_table.SetSequence(sequence);
|
_table.SetSequence(sequence);
|
||||||
_chart.SetData(sequence, null);
|
_chart.SetData(sequence, null);
|
||||||
@@ -345,7 +364,9 @@ internal sealed class MainForm : Form
|
|||||||
ShowPreview(0);
|
ShowPreview(0);
|
||||||
|
|
||||||
SetStatus($"{sequence.Count} fotogrammi caricati. Cadenza nominale {sequence.NominalInterval:0.###} s, " +
|
SetStatus($"{sequence.Count} fotogrammi caricati. Cadenza nominale {sequence.NominalInterval:0.###} s, " +
|
||||||
$"{sequence.CadenceAnomalies} intervalli anomali." + DescribeResolutionClamp());
|
$"{sequence.CadenceAnomalies} intervalli anomali. " +
|
||||||
|
$"Orientamento {_project.Orientation.Description} ({_project.Orientation.Reason})." +
|
||||||
|
DescribeResolutionClamp());
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,12 +116,13 @@ internal sealed class PreviewPanel : Control
|
|||||||
{
|
{
|
||||||
var record = sequence.Frames[index];
|
var record = sequence.Frames[index];
|
||||||
var metadata = record.Metadata;
|
var metadata = record.Metadata;
|
||||||
|
int orientation = project.EffectiveOrientation;
|
||||||
|
|
||||||
int sourceWidth = metadata.PixelWidth;
|
int sourceWidth = metadata.PixelWidth;
|
||||||
int sourceHeight = metadata.PixelHeight;
|
int sourceHeight = metadata.PixelHeight;
|
||||||
if (sourceWidth <= 0 || sourceHeight <= 0)
|
if (sourceWidth <= 0 || sourceHeight <= 0)
|
||||||
(sourceWidth, sourceHeight) = ImageDecoder.ProbeDisplaySize(metadata.FilePath, metadata.Orientation);
|
(sourceWidth, sourceHeight) = ImageDecoder.ProbeDisplaySize(metadata.FilePath, orientation);
|
||||||
else if (ImageDecoder.SwapsAxes(metadata.Orientation))
|
else if (ImageDecoder.SwapsAxes(orientation))
|
||||||
(sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
(sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
|
||||||
|
|
||||||
if (sourceWidth <= 0 || sourceHeight <= 0) return (null, "Immagine non leggibile");
|
if (sourceWidth <= 0 || sourceHeight <= 0) return (null, "Immagine non leggibile");
|
||||||
@@ -132,7 +133,7 @@ internal sealed class PreviewPanel : Control
|
|||||||
int height = Math.Max(16, (int)(sourceHeight * scale) & ~1);
|
int height = Math.Max(16, (int)(sourceHeight * scale) & ~1);
|
||||||
|
|
||||||
var pool = new FrameBufferPool(4);
|
var pool = new FrameBufferPool(4);
|
||||||
using var frame = ImageDecoder.Decode(metadata.FilePath, width, height, metadata.Orientation, pool);
|
using var frame = ImageDecoder.Decode(metadata.FilePath, width, height, orientation, pool);
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var status = new System.Text.StringBuilder();
|
var status = new System.Text.StringBuilder();
|
||||||
@@ -152,7 +153,7 @@ internal sealed class PreviewPanel : Control
|
|||||||
if (project.MotionBlur.Enabled && index + 1 < sequence.Count)
|
if (project.MotionBlur.Enabled && index + 1 < sequence.Count)
|
||||||
{
|
{
|
||||||
var nextMetadata = sequence.Frames[index + 1].Metadata;
|
var nextMetadata = sequence.Frames[index + 1].Metadata;
|
||||||
using var next = ImageDecoder.Decode(nextMetadata.FilePath, width, height, nextMetadata.Orientation, pool);
|
using var next = ImageDecoder.Decode(nextMetadata.FilePath, width, height, orientation, pool);
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (project.Deflicker.Enabled && curve is not null && index + 1 < curve.Count)
|
if (project.Deflicker.Enabled && curve is not null && index + 1 < curve.Count)
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ namespace Titano.UI;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class SettingsPanel : Panel
|
internal sealed class SettingsPanel : Panel
|
||||||
{
|
{
|
||||||
|
/// <summary>Valori Exif corrispondenti alle voci del menu dell'orientamento; null = automatico.</summary>
|
||||||
|
private static readonly int?[] OrientationValues = [null, 1, 2, 4, 6, 3, 8];
|
||||||
|
|
||||||
private readonly TitanoProject _project;
|
private readonly TitanoProject _project;
|
||||||
private readonly TabStrip _tabs;
|
private readonly TabStrip _tabs;
|
||||||
private readonly Panel[] _pages;
|
private readonly Panel[] _pages;
|
||||||
|
private LabeledCombo? _orientationCombo;
|
||||||
|
|
||||||
/// <summary>Un parametro del deflicker è cambiato: basta ricalcolare la curva.</summary>
|
/// <summary>Un parametro del deflicker è cambiato: basta ricalcolare la curva.</summary>
|
||||||
public event EventHandler? DeflickerChanged;
|
public event EventHandler? DeflickerChanged;
|
||||||
@@ -66,6 +70,18 @@ internal sealed class SettingsPanel : Panel
|
|||||||
ShowPage(0);
|
ShowPage(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Riporta nel menu l'esito del rilevamento, così la voce "Automatico" dice cosa ha deciso.
|
||||||
|
/// </summary>
|
||||||
|
public void ShowDetectedOrientation(Imaging.OrientationDetection detection)
|
||||||
|
{
|
||||||
|
if (_orientationCombo is null || _orientationCombo.Combo.Items.Count == 0) return;
|
||||||
|
|
||||||
|
int selected = _orientationCombo.Combo.SelectedIndex;
|
||||||
|
_orientationCombo.Combo.Items[0] = $"Automatico — {detection.Description}";
|
||||||
|
_orientationCombo.Combo.SelectedIndex = selected;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Seleziona una delle tre sezioni; usata anche dalla modalità di cattura.</summary>
|
/// <summary>Seleziona una delle tre sezioni; usata anche dalla modalità di cattura.</summary>
|
||||||
internal void SelectPage(int index) => _tabs.SelectedIndex = index;
|
internal void SelectPage(int index) => _tabs.SelectedIndex = index;
|
||||||
|
|
||||||
@@ -97,6 +113,37 @@ internal sealed class SettingsPanel : Panel
|
|||||||
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
_orientationCombo = Combo("Orientamento dei fotogrammi",
|
||||||
|
["Automatico", "Nessuna rotazione", "Specchia orizzontalmente", "Specchia verticalmente",
|
||||||
|
"Ruota 90° in senso orario", "Ruota 180°", "Ruota 90° in senso antiorario"],
|
||||||
|
Array.IndexOf(OrientationValues, _project.General.OrientationOverride),
|
||||||
|
index =>
|
||||||
|
{
|
||||||
|
_project.General.OrientationOverride = OrientationValues[Math.Clamp(index, 0, OrientationValues.Length - 1)];
|
||||||
|
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||||
|
});
|
||||||
|
stack.Add(_orientationCombo);
|
||||||
|
|
||||||
|
stack.Add(Note("In automatico l'orientamento viene dedotto dai metadati del primo scatto e " +
|
||||||
|
"confrontato con ciò che il decodificatore di sistema restituisce, così la " +
|
||||||
|
"trasformazione non viene applicata due volte. Se il risultato non convince, " +
|
||||||
|
"l'anteprima mostra subito l'effetto di una scelta manuale."));
|
||||||
|
|
||||||
|
stack.Add(new SectionHeader("Qualità di elaborazione"));
|
||||||
|
|
||||||
|
stack.Add(Combo("Profilo",
|
||||||
|
["Bozza — veloce", "Standard — equilibrio", "Massima — resa migliore"],
|
||||||
|
(int)_project.General.Quality,
|
||||||
|
index =>
|
||||||
|
{
|
||||||
|
_project.ApplyQualityProfile((QualityProfile)Math.Clamp(index, 0, 2));
|
||||||
|
AnalysisInvalidated?.Invoke(this, EventArgs.Empty);
|
||||||
|
}));
|
||||||
|
|
||||||
|
stack.Add(Note("Il profilo governa finezza del campo vettoriale, campioni della sfocatura e " +
|
||||||
|
"risoluzione della passata fotometrica, e sovrascrive i cursori delle sezioni " +
|
||||||
|
"avanzate. Più qualità significa più tempo, non un file più grande."));
|
||||||
|
|
||||||
stack.Add(new SectionHeader("Prestazioni"));
|
stack.Add(new SectionHeader("Prestazioni"));
|
||||||
stack.Add(Slider("Larghezza della passata di analisi", 256, 2048, _project.General.AnalysisWidth, 64, "0", "px",
|
stack.Add(Slider("Larghezza della passata di analisi", 256, 2048, _project.General.AnalysisWidth, 64, "0", "px",
|
||||||
value =>
|
value =>
|
||||||
@@ -282,6 +329,16 @@ internal sealed class SettingsPanel : Panel
|
|||||||
control.Top = _y;
|
control.Top = _y;
|
||||||
control.Width = Panel.ClientSize.Width - 34;
|
control.Width = Panel.ClientSize.Width - 34;
|
||||||
control.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
|
control.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
|
||||||
|
|
||||||
|
// Le note esplicative variano in lunghezza: si misurano sulla larghezza reale
|
||||||
|
// della colonna, altrimenti le più lunghe finirebbero tagliate a metà frase.
|
||||||
|
if (control is Label note && !note.AutoSize)
|
||||||
|
{
|
||||||
|
var measured = TextRenderer.MeasureText(note.Text, note.Font,
|
||||||
|
new Size(control.Width, 0), TextFormatFlags.WordBreak);
|
||||||
|
control.Height = measured.Height + 8;
|
||||||
|
}
|
||||||
|
|
||||||
Panel.Controls.Add(control);
|
Panel.Controls.Add(control);
|
||||||
_y += control.Height + 6;
|
_y += control.Height + 6;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user