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:
2026-08-14 15:53:10 +02:00
co-authored by Claude Opus 5
parent e476ebb648
commit 332f02d17d
14 changed files with 588 additions and 15 deletions
+22 -1
View File
@@ -70,11 +70,28 @@ internal sealed class MainForm : Form
_exportButton = new DarkButton { Text = "Esporta video", Width = 140, Primary = true };
_cancelButton = new DarkButton { Text = "Annulla", Width = 96, Danger = true, Visible = false };
LoadApplicationIcon();
BuildLayout();
WireEvents();
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)
{
base.OnHandleCreated(e);
@@ -337,6 +354,8 @@ internal sealed class MainForm : Form
_project.Sequence = sequence;
_project.Curve = null;
_project.Stats = null;
_project.DetectOrientation();
_settings.ShowDetectedOrientation(_project.Orientation);
_table.SetSequence(sequence);
_chart.SetData(sequence, null);
@@ -345,7 +364,9 @@ internal sealed class MainForm : Form
ShowPreview(0);
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)
{
+5 -4
View File
@@ -116,12 +116,13 @@ internal sealed class PreviewPanel : Control
{
var record = sequence.Frames[index];
var metadata = record.Metadata;
int orientation = project.EffectiveOrientation;
int sourceWidth = metadata.PixelWidth;
int sourceHeight = metadata.PixelHeight;
if (sourceWidth <= 0 || sourceHeight <= 0)
(sourceWidth, sourceHeight) = ImageDecoder.ProbeDisplaySize(metadata.FilePath, metadata.Orientation);
else if (ImageDecoder.SwapsAxes(metadata.Orientation))
(sourceWidth, sourceHeight) = ImageDecoder.ProbeDisplaySize(metadata.FilePath, orientation);
else if (ImageDecoder.SwapsAxes(orientation))
(sourceWidth, sourceHeight) = (sourceHeight, sourceWidth);
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);
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();
var status = new System.Text.StringBuilder();
@@ -152,7 +153,7 @@ internal sealed class PreviewPanel : Control
if (project.MotionBlur.Enabled && index + 1 < sequence.Count)
{
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();
if (project.Deflicker.Enabled && curve is not null && index + 1 < curve.Count)
+57
View File
@@ -11,9 +11,13 @@ namespace Titano.UI;
/// </summary>
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 TabStrip _tabs;
private readonly Panel[] _pages;
private LabeledCombo? _orientationCombo;
/// <summary>Un parametro del deflicker è cambiato: basta ricalcolare la curva.</summary>
public event EventHandler? DeflickerChanged;
@@ -66,6 +70,18 @@ internal sealed class SettingsPanel : Panel
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>
internal void SelectPage(int index) => _tabs.SelectedIndex = index;
@@ -97,6 +113,37 @@ internal sealed class SettingsPanel : Panel
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(Slider("Larghezza della passata di analisi", 256, 2048, _project.General.AnalysisWidth, 64, "0", "px",
value =>
@@ -282,6 +329,16 @@ internal sealed class SettingsPanel : Panel
control.Top = _y;
control.Width = Panel.ClientSize.Width - 34;
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);
_y += control.Height + 6;
}