mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
SingleJob made private. Correct SAR for rotated images. Structure refactoing on splitter side.
This commit is contained in:
parent
417d511bc8
commit
61c94d4661
@ -2,7 +2,15 @@
|
||||
global using System.Collections.Generic;
|
||||
global using System.Threading.Tasks;
|
||||
|
||||
global using OpenCvSharp;
|
||||
global using Size = Avalonia.Size;
|
||||
global using Rect = Avalonia.Rect;
|
||||
|
||||
global using splitter;
|
||||
global using splitter.tui;
|
||||
global using splitter.algo;
|
||||
global using splitter.probe;
|
||||
|
||||
global using Splitter_UI.Models;
|
||||
global using Splitter_UI.Services;
|
||||
global using Splitter_UI.ViewModels;
|
||||
|
||||
@ -5,10 +5,10 @@ namespace Splitter_UI.Models;
|
||||
public class PreviewData
|
||||
{
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||
public IReadOnlyList<Rect> DetectedBoxes { get; }
|
||||
public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { get; }
|
||||
public Rect? CropRect { get; }
|
||||
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<Rect> boxes, Rect? crop)
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop)
|
||||
{
|
||||
Frame = frame;
|
||||
DetectedBoxes = boxes;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using splitter.algo;
|
||||
using splitter.tui;
|
||||
|
||||
namespace Splitter_UI;
|
||||
|
||||
@ -45,7 +47,7 @@ internal sealed class Program
|
||||
_ => new DummyDetector()
|
||||
};
|
||||
});
|
||||
services.AddSingleton<splitter.ILogger, GlobalLogger>();
|
||||
services.AddSingleton<ILogger, GlobalLogger>();
|
||||
|
||||
// Domain services (your pipeline)
|
||||
services.AddTransient<IFileProbeService, FileProbeService>();
|
||||
|
||||
@ -1,8 +1,33 @@
|
||||
namespace Splitter_UI.Services;
|
||||
using NcnnDotNet.Layers;
|
||||
using OpenCvSharp;
|
||||
using splitter.tui;
|
||||
|
||||
public sealed class AutoDecisionService : IAutoDecisionService
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class AutoDecisionService(IThumbnailService _thumbnails, IFileProbeService _fileProbe, ILogger _log) : IAutoDecisionService
|
||||
{
|
||||
public void ApplyAutoDecisions(SingleJob job, VideoInfo probe)
|
||||
public void ApplyAutoDecisions(JobViewModel job)
|
||||
{
|
||||
Task.Run(() => Detect(job));
|
||||
}
|
||||
|
||||
private async Task Detect(JobViewModel job)
|
||||
{
|
||||
try
|
||||
{
|
||||
job.Probe = await _fileProbe.ProbeAsync(job.InputFile);
|
||||
job.Thumbnail = await _thumbnails.CreateThumbnailAsync(job.InputFile, job.Probe, rotateDegree: job.Rotate);
|
||||
|
||||
var sampler = new VideoRotationSampler(null);
|
||||
job.Rotate = await sampler.DetectRotationAsync(job.InputFile, job.Probe.Duration);
|
||||
job.SuggestedAction = job.Rotate == 0 ? "crop" : "rotate";
|
||||
|
||||
if (job.SuggestedAction == "crop")
|
||||
job.Detect = "body";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError($"Error creating thumbnail for {Path.GetFileName(job.InputFile)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
using OpenCvSharp;
|
||||
using splitter.algo;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
internal class DummyDetector : IObjectDetector
|
||||
{
|
||||
public List<(Rect box, splitter.Point2f center)> DetectAll(Mat frameCont) => [];
|
||||
public List<(OpenCvSharp.Rect box, Point2f center)> DetectAll(Mat frameCont) => [];
|
||||
public void Dispose() {}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
namespace Splitter_UI.Services;
|
||||
using splitter.probe;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public sealed class FileProbeService : IFileProbeService
|
||||
{
|
||||
public async Task<VideoInfo> ProbeAsync(SingleJob job)
|
||||
public async Task<VideoInfo> ProbeAsync(string inputFile)
|
||||
{
|
||||
var res = await Task.Run(() =>ProbeVideo.Probe(job));
|
||||
var res = await Task.Run(() => ProbeVideo.Probe(inputFile, false));
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
namespace Splitter_UI.Services;
|
||||
using splitter.tui;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
internal class GlobalLogger(ILogService _logService) : ILogger
|
||||
{
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
public interface IAutoDecisionService
|
||||
{
|
||||
void ApplyAutoDecisions(SingleJob job, VideoInfo probe);
|
||||
void ApplyAutoDecisions(JobViewModel job);
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
namespace Splitter_UI.Services;
|
||||
using splitter.probe;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
public interface IFileProbeService
|
||||
{
|
||||
Task<VideoInfo> ProbeAsync(SingleJob job);
|
||||
Task<VideoInfo> ProbeAsync(string inputFile);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Avalonia.Media.Imaging;
|
||||
using splitter.probe;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using OpenCvSharp;
|
||||
using splitter.algo;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
@ -7,7 +8,7 @@ public class SingleThreadedDetector<T>(IObjectDetector _detector) : IObjectDetec
|
||||
{
|
||||
private Lock _lock = new();
|
||||
|
||||
public List<(Rect box, splitter.Point2f center)> DetectAll(Mat frameCont)
|
||||
public List<(OpenCvSharp.Rect box, Point2f center)> DetectAll(Mat frameCont)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
using splitter.probe;
|
||||
|
||||
namespace Splitter_UI.Services;
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ namespace Splitter_UI.ViewModels;
|
||||
public partial class FileListViewModel : ObservableObject
|
||||
{
|
||||
private readonly IFileJobFactory _factory;
|
||||
private readonly IAutoDecisionService _autoDecisionService;
|
||||
public ObservableCollection<JobViewModel> Files { get; } = [];
|
||||
public ObservableCollection<JobViewModel> SelectedFiles { get; } = [];
|
||||
|
||||
@ -15,9 +16,10 @@ public partial class FileListViewModel : ObservableObject
|
||||
|
||||
public event Action<JobViewModel?>? SelectedFileChanged;
|
||||
|
||||
public FileListViewModel(IFileJobFactory factory)
|
||||
public FileListViewModel(IFileJobFactory factory, IAutoDecisionService autoDecisionService)
|
||||
{
|
||||
_factory = factory;
|
||||
_autoDecisionService = autoDecisionService;
|
||||
}
|
||||
|
||||
partial void OnSelectedChanged(JobViewModel? value)
|
||||
@ -32,6 +34,9 @@ public partial class FileListViewModel : ObservableObject
|
||||
var job = new SingleJob { InputFile = path };
|
||||
var vm = _factory.Create(job);
|
||||
Files.Add(vm);
|
||||
_autoDecisionService.ApplyAutoDecisions(vm);
|
||||
}
|
||||
|
||||
Selected = Files.LastOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,10 +37,10 @@ public partial class InspectorPaneViewModel : ObservableObject
|
||||
|
||||
private void AdjustRotation(int delta)
|
||||
{
|
||||
if (Selected?.Job == null)
|
||||
if ( Selected == null)
|
||||
return;
|
||||
|
||||
var r = Selected.Job.Rotate ?? 0;
|
||||
var r = Selected.Rotate;
|
||||
r = (r + delta) % 360;
|
||||
if (r < 0) r += 360;
|
||||
|
||||
|
||||
@ -5,12 +5,15 @@ using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using splitter.algo;
|
||||
using splitter.probe;
|
||||
using splitter.tui;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
public partial class JobViewModel : ObservableObject
|
||||
{
|
||||
public SingleJob Job { get; }
|
||||
private SingleJob Job { get; }
|
||||
|
||||
[ObservableProperty] private VideoInfo? _probe;
|
||||
[ObservableProperty] private PreviewData? _preview = new(null, [], null);
|
||||
@ -20,13 +23,13 @@ public partial class JobViewModel : ObservableObject
|
||||
[ObservableProperty] private double _sliderLiveValue;
|
||||
[ObservableProperty] private double _positionSeconds;
|
||||
|
||||
public string InputFile => Job.InputFile;
|
||||
public double DurationSeconds => Probe?.Duration ?? 0;
|
||||
|
||||
public IRelayCommand StepForwardCommand { get; }
|
||||
public IRelayCommand StepForwardCommand { get; }
|
||||
public IRelayCommand StepBackwardCommand { get; }
|
||||
|
||||
private readonly IThumbnailService _thumbnails;
|
||||
private readonly IFileProbeService _fileProbe;
|
||||
private readonly DispatcherTimer _debounceTimer;
|
||||
private readonly Func<string, IObjectDetector> _detectorFactory;
|
||||
private readonly ILogger _log;
|
||||
@ -96,6 +99,66 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public string? Detect
|
||||
{
|
||||
get => Job.Detect;
|
||||
set
|
||||
{
|
||||
if (Job.Detect == value)
|
||||
return;
|
||||
Job.Detect = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string? Mask
|
||||
{
|
||||
get => Job.Mask;
|
||||
set
|
||||
{
|
||||
if (Job.Mask == value)
|
||||
return;
|
||||
Job.Mask = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string OutputFolder
|
||||
{
|
||||
get => Job.OutputFolder;
|
||||
set
|
||||
{
|
||||
if (Job.OutputFolder == value)
|
||||
return;
|
||||
Job.OutputFolder = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool ForceFixed
|
||||
{
|
||||
get => Job.ForceFixed;
|
||||
set
|
||||
{
|
||||
if (Job.ForceFixed == value)
|
||||
return;
|
||||
Job.ForceFixed = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Debug
|
||||
{
|
||||
get => Job.Debug;
|
||||
set
|
||||
{
|
||||
if (Job.Debug == value)
|
||||
return;
|
||||
Job.Debug = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int? Rotate
|
||||
{
|
||||
get => Job.Rotate;
|
||||
@ -107,11 +170,21 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, IFileProbeService fileProbe, Func<string, IObjectDetector> detectorFactory, ILogger log)
|
||||
public double? OverrideTargetDuration
|
||||
{
|
||||
get => Job.OverrideTargetDuration;
|
||||
set
|
||||
{
|
||||
if (Job.OverrideTargetDuration != null && value != null && Math.Abs(Job.OverrideTargetDuration.Value - value.Value) < 0.01)
|
||||
return;
|
||||
Job.OverrideTargetDuration = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, Func<string, IObjectDetector> detectorFactory, ILogger log)
|
||||
{
|
||||
Job = job;
|
||||
_thumbnails = thumbnails;
|
||||
_fileProbe = fileProbe;
|
||||
_detectorFactory = detectorFactory;
|
||||
_log = log;
|
||||
|
||||
@ -139,27 +212,9 @@ public partial class JobViewModel : ObservableObject
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
_debounceTimer.Tick += DebounceTimerTick;
|
||||
|
||||
_ = Task.Run( LoadThumbnailAsync );
|
||||
}
|
||||
|
||||
private async Task LoadThumbnailAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Probe = await _fileProbe.ProbeAsync(Job);
|
||||
Thumbnail = await _thumbnails.CreateThumbnailAsync(Job.InputFile, Probe, rotateDegree: Job.Rotate);
|
||||
SuggestedAction = Probe.Rotation == 0 ? "crop" : "rotate";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogError($"Error creating thumbnail for {FileName}: {ex.Message}");
|
||||
}
|
||||
|
||||
await CreatePreview();
|
||||
}
|
||||
|
||||
private async Task CreatePreview()
|
||||
public async Task CreatePreview()
|
||||
{
|
||||
if ( Probe == null)
|
||||
return;
|
||||
@ -174,7 +229,7 @@ public partial class JobViewModel : ObservableObject
|
||||
var detector = _detectorFactory(Job.Detect ?? "");
|
||||
var detections = detector.DetectAll(frame.ToMatContinuous());
|
||||
|
||||
var boxes = detections.Select(x => new Avalonia.Rect(x.box.X, x.box.Y, x.box.Width, x.box.Height)).ToList();
|
||||
var boxes = detections.Select(x => new OpenCvSharp.Rect(x.box.X, x.box.Y, x.box.Width, x.box.Height)).ToList();
|
||||
Preview = new PreviewData(frame, boxes, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -10,9 +10,9 @@ public partial class MainViewModel : ViewModelBase
|
||||
public StatusBarViewModel StatusBar { get; } = new StatusBarViewModel();
|
||||
public LogPaneViewModel LogPane { get; } = new LogPaneViewModel();
|
||||
|
||||
public MainViewModel(IFileJobFactory fileJobFactory)
|
||||
public MainViewModel(IFileJobFactory fileJobFactory, IAutoDecisionService autoDecisionService)
|
||||
{
|
||||
FileList = new FileListViewModel(fileJobFactory);
|
||||
FileList = new FileListViewModel(fileJobFactory, autoDecisionService);
|
||||
// Wire selection → preview + inspector
|
||||
FileList.SelectedFileChanged += file =>
|
||||
{
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using splitter.algo;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
|
||||
@ -9,8 +10,8 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
private JobViewModel? _selected;
|
||||
|
||||
public PreviewData? Preview => Selected?.Preview;
|
||||
public Point2f? Sar => Selected?.Probe?.Sar;
|
||||
public Point2f? Dar => Selected?.Probe?.Dar;
|
||||
public Point2f? Sar => Selected?.Probe?.Sar;
|
||||
public int Rotate => Selected?.Rotate ?? 0;
|
||||
|
||||
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||
{
|
||||
@ -22,7 +23,7 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
|
||||
OnPropertyChanged(nameof(Preview));
|
||||
OnPropertyChanged(nameof(Sar));
|
||||
OnPropertyChanged(nameof(Dar));
|
||||
OnPropertyChanged(nameof(Rotate));
|
||||
}
|
||||
|
||||
private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
@ -33,7 +34,7 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
if (e.PropertyName == nameof(JobViewModel.Probe))
|
||||
{
|
||||
OnPropertyChanged(nameof(Sar));
|
||||
OnPropertyChanged(nameof(Dar));
|
||||
OnPropertyChanged(nameof(Rotate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,13 +57,13 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<!-- Mask -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Mask" Width="120"/>
|
||||
<TextBox Text="{Binding Selected.Job.Mask}" Width="260"/>
|
||||
<TextBox Text="{Binding Selected.Mask}" Width="260"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- OutputFolder -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Output Folder" Width="120"/>
|
||||
<TextBox Text="{Binding Selected.Job.OutputFolder}" Width="260"/>
|
||||
<TextBox Text="{Binding Selected.OutputFolder}" Width="260"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Crop -->
|
||||
@ -82,31 +82,23 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Detect" Width="120"/>
|
||||
<ComboBox ItemsSource="{Binding DetectModes}"
|
||||
SelectedItem="{Binding Selected.Job.Detect}"
|
||||
SelectedItem="{Binding Selected.Detect}"
|
||||
Width="160"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- OverrideTargetDuration -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Target Duration" Width="120"/>
|
||||
<NumericUpDown Value="{Binding Selected.Job.OverrideTargetDuration}" Width="120"/>
|
||||
<NumericUpDown Value="{Binding Selected.OverrideTargetDuration}" Width="120"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- RotateAuto -->
|
||||
<CheckBox Content="Rotate Auto"
|
||||
IsChecked="{Binding Selected.Job.RotateAuto}"/>
|
||||
|
||||
<!-- ForceFixed -->
|
||||
<CheckBox Content="Force Fixed Duration"
|
||||
IsChecked="{Binding Selected.Job.ForceFixed}"/>
|
||||
|
||||
<!-- SingleThreaded -->
|
||||
<CheckBox Content="Single Threaded"
|
||||
IsChecked="{Binding Selected.Job.SingleThreaded}"/>
|
||||
IsChecked="{Binding Selected.ForceFixed}"/>
|
||||
|
||||
<!-- Debug -->
|
||||
<CheckBox Content="Debug Mode"
|
||||
IsChecked="{Binding Selected.Job.Debug}"/>
|
||||
IsChecked="{Binding Selected.Debug}"/>
|
||||
|
||||
<!-- Parameters dictionary -->
|
||||
<TextBlock Text="Advanced Parameters" FontSize="10" Margin="0,10,0,0" FontWeight="Bold"/>
|
||||
|
||||
@ -2,7 +2,7 @@ using Avalonia.Controls;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
public partial class MainWindow : Avalonia.Controls.Window
|
||||
{
|
||||
public MainViewModel Data { get; } = null!; // set by DI
|
||||
public MainWindow()
|
||||
|
||||
@ -3,6 +3,7 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using splitter.algo;
|
||||
|
||||
namespace Splitter_UI.Views;
|
||||
|
||||
@ -12,8 +13,8 @@ public sealed class PreviewCanvas : Control
|
||||
AvaloniaProperty.Register<PreviewCanvas, PreviewData?>(nameof(Preview));
|
||||
public static readonly StyledProperty<Point2f?> SarProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
|
||||
public static readonly StyledProperty<Point2f?> DarProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Dar));
|
||||
public static readonly StyledProperty<int> RotateAngleProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
|
||||
|
||||
public PreviewData? Preview
|
||||
{
|
||||
@ -27,10 +28,10 @@ public sealed class PreviewCanvas : Control
|
||||
set => SetValue(SarProperty, value);
|
||||
}
|
||||
|
||||
public Point2f? Dar
|
||||
public int RotateAngle
|
||||
{
|
||||
get => GetValue(DarProperty);
|
||||
set => SetValue(DarProperty, value);
|
||||
get => GetValue(RotateAngleProperty);
|
||||
set => SetValue(RotateAngleProperty, value);
|
||||
}
|
||||
|
||||
static PreviewCanvas()
|
||||
@ -83,10 +84,12 @@ public sealed class PreviewCanvas : Control
|
||||
if (dispW <= 0 || dispH <= 0)
|
||||
return;
|
||||
|
||||
// SAR
|
||||
var rotate = RotateAngle; // 0, 90, 180, 270
|
||||
|
||||
// SAR (always original, never rotated)
|
||||
var sar = Sar ?? new Point2f(1, 1);
|
||||
var sarX = (double)sar.X;
|
||||
var sarY = (double)sar.Y;
|
||||
var sarX = sar.X;
|
||||
var sarY = sar.Y;
|
||||
|
||||
if (sarX <= 0 || sarY <= 0)
|
||||
{
|
||||
@ -94,22 +97,23 @@ public sealed class PreviewCanvas : Control
|
||||
sarY = 1;
|
||||
}
|
||||
|
||||
// DAR override (only if SAR missing or invalid)
|
||||
if ((sarX == 1 && sarY == 1) && Dar is { } dar && dar.X > 0 && dar.Y > 0)
|
||||
{
|
||||
var darRatio = dar.X / dar.Y;
|
||||
var encodedRatio = rawW / (double)rawH;
|
||||
|
||||
// recompute SAR from DAR
|
||||
sarX = darRatio / encodedRatio;
|
||||
sarY = 1;
|
||||
}
|
||||
|
||||
var pixelAspect = sarX / sarY;
|
||||
|
||||
// display size after SAR correction
|
||||
var displayW = rawW * pixelAspect;
|
||||
var displayH = rawH;
|
||||
double displayW;
|
||||
double displayH;
|
||||
|
||||
if (rotate == 0 || rotate == 180)
|
||||
{
|
||||
// encoded horizontal axis = rawW
|
||||
displayW = rawW * pixelAspect;
|
||||
displayH = rawH;
|
||||
}
|
||||
else
|
||||
{
|
||||
// encoded horizontal axis = rawH (bitmap already rotated)
|
||||
displayW = rawW;
|
||||
displayH = rawH * pixelAspect;
|
||||
}
|
||||
|
||||
var scale = Math.Min(dispW / displayW, dispH / displayH);
|
||||
|
||||
@ -132,11 +136,47 @@ public sealed class PreviewCanvas : Control
|
||||
|
||||
foreach (var r in preview.DetectedBoxes)
|
||||
{
|
||||
double x = r.X;
|
||||
double y = r.Y;
|
||||
double w = r.Width;
|
||||
double h = r.Height;
|
||||
|
||||
// rotate overlay coordinates (still using your existing logic)
|
||||
switch (rotate)
|
||||
{
|
||||
case 90:
|
||||
(x, y) = (rawH - (y + h), x);
|
||||
(w, h) = (h, w);
|
||||
break;
|
||||
|
||||
case 180:
|
||||
x = rawW - (x + w);
|
||||
y = rawH - (y + h);
|
||||
break;
|
||||
|
||||
case 270:
|
||||
(x, y) = (y, rawW - (x + w));
|
||||
(w, h) = (h, w);
|
||||
break;
|
||||
}
|
||||
|
||||
// apply SAR to the axis that originated from encoded width
|
||||
if (rotate == 0 || rotate == 180)
|
||||
{
|
||||
x *= pixelAspect;
|
||||
w *= pixelAspect;
|
||||
}
|
||||
else
|
||||
{
|
||||
y *= pixelAspect;
|
||||
h *= pixelAspect;
|
||||
}
|
||||
|
||||
var rr = new Rect(
|
||||
offsetX + (r.X * pixelAspect) * scale,
|
||||
offsetY + r.Y * scale,
|
||||
(r.Width * pixelAspect) * scale,
|
||||
r.Height * scale);
|
||||
offsetX + x * scale,
|
||||
offsetY + y * scale,
|
||||
w * scale,
|
||||
h * scale);
|
||||
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
Grid.Row="0"
|
||||
Preview="{Binding Preview}"
|
||||
Sar="{Binding Sar}"
|
||||
Dar="{Binding Dar}" />
|
||||
RotateAngle="{Binding Rotate}" />
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user