mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Crop rectangle and gravitate to (meveable) point added to preview.
This commit is contained in:
parent
61c94d4661
commit
c6ca4fcbb6
@ -4,15 +4,17 @@ namespace Splitter_UI.Models;
|
||||
|
||||
public class PreviewData
|
||||
{
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||
public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { get; }
|
||||
public Rect? CropRect { get; }
|
||||
public Rect? CropRect { get; }
|
||||
public Point2f GravitateTo { get; }
|
||||
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop)
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop, Point2f gravitateTo)
|
||||
{
|
||||
Frame = frame;
|
||||
Frame = frame;
|
||||
DetectedBoxes = boxes;
|
||||
CropRect = crop;
|
||||
CropRect = crop;
|
||||
GravitateTo = gravitateTo;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace Splitter_UI.ViewModels;
|
||||
@ -8,6 +9,8 @@ public partial class InspectorPaneViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private JobViewModel? _selected;
|
||||
|
||||
public ObservableCollection<JobViewModel> Files { get; set; } = [];
|
||||
|
||||
public List<string> DetectModes =>
|
||||
[
|
||||
"face", "body", "none"
|
||||
@ -24,6 +27,22 @@ public partial class InspectorPaneViewModel : ObservableObject
|
||||
if (Selected is null)
|
||||
return;
|
||||
|
||||
foreach (JobViewModel job in Files.Where(x => !ReferenceEquals(x, Selected)))
|
||||
{
|
||||
job.Detect = Selected.Detect;
|
||||
job.Rotate = Selected.Rotate;
|
||||
job.CropText = Selected.CropText;
|
||||
job.ForceFixed = Selected.ForceFixed;
|
||||
job.GravitateText = Selected.GravitateText;
|
||||
job.Mask = Selected.Mask;
|
||||
job.OutputFolder = Selected.OutputFolder;
|
||||
job.OverrideTargetDuration = Selected.OverrideTargetDuration;
|
||||
job.PassthroughText = Selected.PassthroughText;
|
||||
|
||||
job.ParametersList.Clear();
|
||||
foreach (var param in Selected.ParametersList)
|
||||
job.ParametersList.Add(param);
|
||||
}
|
||||
}
|
||||
|
||||
public IRelayCommand RotateLeftCommand { get; }
|
||||
|
||||
@ -16,7 +16,7 @@ public partial class JobViewModel : ObservableObject
|
||||
private SingleJob Job { get; }
|
||||
|
||||
[ObservableProperty] private VideoInfo? _probe;
|
||||
[ObservableProperty] private PreviewData? _preview = new(null, [], null);
|
||||
[ObservableProperty] private PreviewData? _preview = new(null, [], null, new(0.5f, 0.5f));
|
||||
[ObservableProperty] private ProgressInfo? _progress;
|
||||
[ObservableProperty] private Bitmap? _thumbnail;
|
||||
[ObservableProperty] private string _suggestedAction = "";
|
||||
@ -84,6 +84,7 @@ public partial class JobViewModel : ObservableObject
|
||||
Job.GravitateTo = new Point2f(x, y);
|
||||
}
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(GravitateTo));
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,6 +171,20 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public Point2f GravitateTo
|
||||
{
|
||||
get => Job.GravitateTo ?? new Point2f(0.5f, 0.5f);
|
||||
set
|
||||
{
|
||||
if (Job.GravitateTo != null && Math.Abs(Job.GravitateTo.Value.X - value.X) < 0.001 && Math.Abs(Job.GravitateTo.Value.Y - value.Y) < 0.001)
|
||||
return;
|
||||
|
||||
Job.GravitateTo = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(GravitateText));
|
||||
}
|
||||
}
|
||||
|
||||
public double? OverrideTargetDuration
|
||||
{
|
||||
get => Job.OverrideTargetDuration;
|
||||
@ -188,14 +203,14 @@ public partial class JobViewModel : ObservableObject
|
||||
_detectorFactory = detectorFactory;
|
||||
_log = log;
|
||||
|
||||
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", ""));
|
||||
ParametersList.Add(new ParameterEntry("EmaFactor", ""));
|
||||
ParametersList.Add(new ParameterEntry("CameraEasing", ""));
|
||||
ParametersList.Add(new ParameterEntry("LostFreezeFrames", ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount", ""));
|
||||
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames" , ""));
|
||||
ParametersList.Add(new ParameterEntry("EmaFactor" , ""));
|
||||
ParametersList.Add(new ParameterEntry("CameraEasing" , ""));
|
||||
ParametersList.Add(new ParameterEntry("LostFreezeFrames" , ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount" , ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth", ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight", ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth" , ""));
|
||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight" , ""));
|
||||
|
||||
foreach (var entry in ParametersList)
|
||||
{
|
||||
@ -204,7 +219,7 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
ParametersList.CollectionChanged += OnParametersCollectionChanged;
|
||||
|
||||
StepForwardCommand = new RelayCommand(StepForward);
|
||||
StepForwardCommand = new RelayCommand(StepForward);
|
||||
StepBackwardCommand = new RelayCommand(StepBackward);
|
||||
|
||||
_debounceTimer = new DispatcherTimer
|
||||
@ -224,13 +239,34 @@ public partial class JobViewModel : ObservableObject
|
||||
if ( frame == null )
|
||||
return;
|
||||
|
||||
Preview = new PreviewData(frame, [], null);
|
||||
Preview = new PreviewData(frame, [], null, Job.GravitateTo ?? new (0.5f, 0.5f));
|
||||
|
||||
var detector = _detectorFactory(Job.Detect ?? "");
|
||||
var detections = detector.DetectAll(frame.ToMatContinuous());
|
||||
|
||||
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);
|
||||
Rect? crop = null;
|
||||
if (detections.Count > 0)
|
||||
{
|
||||
var primaryDetection = detections
|
||||
.OrderByDescending(d => d.box.Height * d.box.Width)
|
||||
.FirstOrDefault();
|
||||
|
||||
var w = Probe.Width;
|
||||
var h = Probe.Height;
|
||||
|
||||
var cropWidth = Job.Crop?.width ?? CommandLine.DefaultW;
|
||||
var cropHeight = Job.Crop?.height ?? CommandLine.DefaultH;
|
||||
|
||||
var cx = primaryDetection.center.X - cropWidth / 2f;
|
||||
var cy = primaryDetection.center.Y - cropHeight / 2f;
|
||||
|
||||
var r = new Rect(cx, cy, cropWidth, cropHeight);
|
||||
|
||||
crop = ClampCrop(r, w, h);
|
||||
}
|
||||
|
||||
var boxes = detections.Select(x => x.box).ToList();
|
||||
Preview = new PreviewData(frame, boxes, crop, Job.GravitateTo ?? new (0.5f, 0.5f));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -238,6 +274,26 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
private static Rect ClampCrop(Rect r, float w, float h)
|
||||
{
|
||||
var x = r.X;
|
||||
var y = r.Y;
|
||||
var cw = r.Width;
|
||||
var ch = r.Height;
|
||||
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
if (x + cw > w) x = w - cw;
|
||||
if (y + ch > h) y = h - ch;
|
||||
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
return new Rect(x, y, cw, ch);
|
||||
}
|
||||
|
||||
|
||||
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
|
||||
|
||||
@ -19,6 +19,8 @@ public partial class MainViewModel : ViewModelBase
|
||||
Preview.Selected = file;
|
||||
Inspector.Selected = file;
|
||||
};
|
||||
|
||||
Inspector.Files = FileList.Files;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@ -12,6 +12,17 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
public PreviewData? Preview => Selected?.Preview;
|
||||
public Point2f? Sar => Selected?.Probe?.Sar;
|
||||
public int Rotate => Selected?.Rotate ?? 0;
|
||||
public Point2f GravitateTo
|
||||
{
|
||||
get => Selected?.GravitateTo ?? new Point2f(0.5f, 0.5f);
|
||||
set
|
||||
{
|
||||
if (Selected == null)
|
||||
return;
|
||||
Selected.GravitateTo = value;
|
||||
OnPropertyChanged(nameof(GravitateTo));
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||
{
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using splitter.algo;
|
||||
@ -15,6 +16,8 @@ public sealed class PreviewCanvas : Control
|
||||
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
|
||||
public static readonly StyledProperty<int> RotateAngleProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
|
||||
public static readonly StyledProperty<Point2f> GravitateToProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, Point2f>(nameof(GravitateTo));
|
||||
|
||||
public PreviewData? Preview
|
||||
{
|
||||
@ -34,6 +37,17 @@ public sealed class PreviewCanvas : Control
|
||||
set => SetValue(RotateAngleProperty, value);
|
||||
}
|
||||
|
||||
// GravitateTo is normalized (0..1)
|
||||
public Point2f GravitateTo
|
||||
{
|
||||
get => GetValue(GravitateToProperty);
|
||||
set => SetValue(GravitateToProperty, value);
|
||||
}
|
||||
|
||||
private bool _dragging;
|
||||
private Avalonia.Point _dragStartCanvas;
|
||||
private Point2f _dragStartValue;
|
||||
|
||||
static PreviewCanvas()
|
||||
{
|
||||
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
|
||||
@ -42,6 +56,13 @@ public sealed class PreviewCanvas : Control
|
||||
args.NewValue as PreviewData));
|
||||
}
|
||||
|
||||
public PreviewCanvas()
|
||||
{
|
||||
PointerPressed += OnPointerPressed;
|
||||
PointerMoved += OnPointerMoved;
|
||||
PointerReleased += OnPointerReleased;
|
||||
}
|
||||
|
||||
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
|
||||
{
|
||||
if (oldValue is INotifyPropertyChanged oldNotify)
|
||||
@ -50,7 +71,6 @@ public sealed class PreviewCanvas : Control
|
||||
if (newValue is INotifyPropertyChanged newNotify)
|
||||
newNotify.PropertyChanged += PreviewPropertyChanged;
|
||||
|
||||
// Always marshal to UI thread
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
@ -60,7 +80,6 @@ public sealed class PreviewCanvas : Control
|
||||
e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
|
||||
e.PropertyName == nameof(PreviewData.CropRect))
|
||||
{
|
||||
// Always marshal to UI thread
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||
}
|
||||
}
|
||||
@ -68,6 +87,342 @@ public sealed class PreviewCanvas : Control
|
||||
protected override Size MeasureOverride(Size availableSize) => availableSize;
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Unified transform helpers
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private (double X, double Y) TransformPoint(
|
||||
double x, double y,
|
||||
double rawW, double rawH,
|
||||
double offsetX, double offsetY,
|
||||
double scale,
|
||||
int rotate,
|
||||
double pixelAspect)
|
||||
{
|
||||
switch (rotate)
|
||||
{
|
||||
case 90:
|
||||
(x, y) = (rawH - y, x);
|
||||
break;
|
||||
case 180:
|
||||
x = rawW - x;
|
||||
y = rawH - y;
|
||||
break;
|
||||
case 270:
|
||||
(x, y) = (y, rawW - x);
|
||||
break;
|
||||
}
|
||||
|
||||
if (rotate == 0 || rotate == 180)
|
||||
x *= pixelAspect;
|
||||
else
|
||||
y *= pixelAspect;
|
||||
|
||||
var sx = offsetX + x * scale;
|
||||
var sy = offsetY + y * scale;
|
||||
|
||||
return (sx, sy);
|
||||
}
|
||||
|
||||
private Rect TransformRect(
|
||||
double x, double y, double w, double h,
|
||||
double rawW, double rawH,
|
||||
double offsetX, double offsetY,
|
||||
double scale,
|
||||
int rotate,
|
||||
double pixelAspect)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (rotate == 0 || rotate == 180)
|
||||
{
|
||||
x *= pixelAspect;
|
||||
w *= pixelAspect;
|
||||
}
|
||||
else
|
||||
{
|
||||
y *= pixelAspect;
|
||||
h *= pixelAspect;
|
||||
}
|
||||
|
||||
return new Rect(
|
||||
offsetX + x * scale,
|
||||
offsetY + y * scale,
|
||||
w * scale,
|
||||
h * scale);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Hit test for gravitate point (normalized)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private bool HitGravitate(Avalonia.Point p, out Point2f value)
|
||||
{
|
||||
value = default;
|
||||
|
||||
var preview = Preview;
|
||||
if (preview?.Frame is null)
|
||||
return false;
|
||||
|
||||
var g = GravitateTo;
|
||||
|
||||
var rawW = preview.Frame.PixelSize.Width;
|
||||
var rawH = preview.Frame.PixelSize.Height;
|
||||
|
||||
// normalized → pixel
|
||||
double px = g.X * rawW;
|
||||
double py = g.Y * rawH;
|
||||
|
||||
var rotate = RotateAngle;
|
||||
var sar = Sar ?? new Point2f(1, 1);
|
||||
var pixelAspect = sar.X / sar.Y;
|
||||
|
||||
var dispW = Bounds.Width;
|
||||
var dispH = Bounds.Height;
|
||||
|
||||
double displayW, displayH;
|
||||
if (rotate == 0 || rotate == 180)
|
||||
{
|
||||
displayW = rawW * pixelAspect;
|
||||
displayH = rawH;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayW = rawW;
|
||||
displayH = rawH * pixelAspect;
|
||||
}
|
||||
|
||||
var scale = Math.Min(dispW / displayW, dispH / displayH);
|
||||
var offsetX = (dispW - displayW * scale) / 2;
|
||||
var offsetY = (dispH - displayH * scale) / 2;
|
||||
|
||||
var (cx, cy) = TransformPoint(
|
||||
px, py,
|
||||
rawW, rawH,
|
||||
offsetX, offsetY,
|
||||
scale,
|
||||
rotate,
|
||||
pixelAspect);
|
||||
|
||||
const double radius = 10;
|
||||
var hit = (p.X - cx) * (p.X - cx) + (p.Y - cy) * (p.Y - cy) <= radius * radius;
|
||||
|
||||
if (hit)
|
||||
value = g;
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Pointer events
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var p = e.GetPosition(this);
|
||||
|
||||
if (HitGravitate(p, out var g))
|
||||
{
|
||||
_dragging = true;
|
||||
_dragStartCanvas = p;
|
||||
_dragStartValue = g; // normalized
|
||||
e.Pointer.Capture(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (!_dragging || Preview?.Frame is null)
|
||||
return;
|
||||
|
||||
var p = e.GetPosition(this);
|
||||
var dxCanvas = p.X - _dragStartCanvas.X;
|
||||
var dyCanvas = p.Y - _dragStartCanvas.Y;
|
||||
|
||||
var preview = Preview;
|
||||
var rawW = preview.Frame.PixelSize.Width;
|
||||
var rawH = preview.Frame.PixelSize.Height;
|
||||
|
||||
var rotate = RotateAngle;
|
||||
var sar = Sar ?? new Point2f(1, 1);
|
||||
var pixelAspect = sar.X / sar.Y;
|
||||
|
||||
var dispW = Bounds.Width;
|
||||
var dispH = Bounds.Height;
|
||||
|
||||
double displayW, displayH;
|
||||
if (rotate == 0 || rotate == 180)
|
||||
{
|
||||
displayW = rawW * pixelAspect;
|
||||
displayH = rawH;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayW = rawW;
|
||||
displayH = rawH * pixelAspect;
|
||||
}
|
||||
|
||||
var scale = Math.Min(dispW / displayW, dispH / displayH);
|
||||
|
||||
double dx = dxCanvas / scale;
|
||||
double dy = dyCanvas / scale;
|
||||
|
||||
if (rotate == 0 || rotate == 180)
|
||||
dx /= pixelAspect;
|
||||
else
|
||||
dy /= pixelAspect;
|
||||
|
||||
// start normalized → pixel
|
||||
double gx = _dragStartValue.X * rawW + dx;
|
||||
double gy = _dragStartValue.Y * rawH + dy;
|
||||
|
||||
switch (rotate)
|
||||
{
|
||||
case 90:
|
||||
(gx, gy) = (gy, rawH - gx);
|
||||
break;
|
||||
case 180:
|
||||
gx = rawW - gx;
|
||||
gy = rawH - gy;
|
||||
break;
|
||||
case 270:
|
||||
(gx, gy) = (rawW - gy, gx);
|
||||
break;
|
||||
}
|
||||
|
||||
// pixel → normalized
|
||||
var nx = (float)(gx / rawW);
|
||||
var ny = (float)(gy / rawH);
|
||||
|
||||
if (nx < 0) nx = 0;
|
||||
if (ny < 0) ny = 0;
|
||||
if (nx > 1) nx = 1;
|
||||
if (ny > 1) ny = 1;
|
||||
|
||||
GravitateTo = new Point2f(nx, ny);
|
||||
|
||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||
}
|
||||
|
||||
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (_dragging)
|
||||
{
|
||||
_dragging = false;
|
||||
e.Pointer.Capture(null);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Overlay renderers
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private void RenderCropRectangle(
|
||||
DrawingContext context,
|
||||
PreviewData preview,
|
||||
double rawW, double rawH,
|
||||
double offsetX, double offsetY,
|
||||
double scale,
|
||||
int rotate,
|
||||
double pixelAspect)
|
||||
{
|
||||
if (preview.CropRect is not { } crop)
|
||||
return;
|
||||
|
||||
var rr = TransformRect(
|
||||
crop.X, crop.Y, crop.Width, crop.Height,
|
||||
rawW, rawH,
|
||||
offsetX, offsetY,
|
||||
scale,
|
||||
rotate,
|
||||
pixelAspect);
|
||||
|
||||
var pen = new Pen(Brushes.Yellow, 2);
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
}
|
||||
|
||||
private void RenderGravitateTo(
|
||||
DrawingContext context,
|
||||
PreviewData preview,
|
||||
double rawW, double rawH,
|
||||
double offsetX, double offsetY,
|
||||
double scale,
|
||||
int rotate,
|
||||
double pixelAspect)
|
||||
{
|
||||
var g = GravitateTo;
|
||||
|
||||
// normalized → pixel
|
||||
double px = g.X * rawW;
|
||||
double py = g.Y * rawH;
|
||||
|
||||
var (sx, sy) = TransformPoint(
|
||||
px, py,
|
||||
rawW, rawH,
|
||||
offsetX, offsetY,
|
||||
scale,
|
||||
rotate,
|
||||
pixelAspect);
|
||||
|
||||
const double radius = 10;
|
||||
|
||||
var circle = new EllipseGeometry(
|
||||
new Rect(sx - radius, sy - radius, radius * 2, radius * 2));
|
||||
|
||||
var pen = new Pen(Brushes.Yellow, 2);
|
||||
var brush = Brushes.Yellow;
|
||||
|
||||
context.DrawGeometry(brush, pen, circle);
|
||||
}
|
||||
|
||||
private void RenderDetectedBoxes(
|
||||
DrawingContext context,
|
||||
PreviewData preview,
|
||||
double rawW, double rawH,
|
||||
double offsetX, double offsetY,
|
||||
double scale,
|
||||
int rotate,
|
||||
double pixelAspect)
|
||||
{
|
||||
if (preview.DetectedBoxes is not { Count: > 0 })
|
||||
return;
|
||||
|
||||
var pen = new Pen(Brushes.Lime, 2);
|
||||
|
||||
foreach (var r in preview.DetectedBoxes)
|
||||
{
|
||||
var rr = TransformRect(
|
||||
r.X, r.Y, r.Width, r.Height,
|
||||
rawW, rawH,
|
||||
offsetX, offsetY,
|
||||
scale,
|
||||
rotate,
|
||||
pixelAspect);
|
||||
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Main Render
|
||||
// ------------------------------------------------------------
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
var preview = Preview;
|
||||
@ -84,33 +439,22 @@ public sealed class PreviewCanvas : Control
|
||||
if (dispW <= 0 || dispH <= 0)
|
||||
return;
|
||||
|
||||
var rotate = RotateAngle; // 0, 90, 180, 270
|
||||
var rotate = RotateAngle;
|
||||
|
||||
// SAR (always original, never rotated)
|
||||
var sar = Sar ?? new Point2f(1, 1);
|
||||
var sarX = sar.X;
|
||||
var sarY = sar.Y;
|
||||
|
||||
if (sarX <= 0 || sarY <= 0)
|
||||
{
|
||||
sarX = 1;
|
||||
sarY = 1;
|
||||
}
|
||||
|
||||
var sarX = sar.X <= 0 ? 1 : sar.X;
|
||||
var sarY = sar.Y <= 0 ? 1 : sar.Y;
|
||||
var pixelAspect = sarX / sarY;
|
||||
|
||||
double displayW;
|
||||
double displayH;
|
||||
double displayW, 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;
|
||||
}
|
||||
@ -123,64 +467,13 @@ public sealed class PreviewCanvas : Control
|
||||
var offsetX = (dispW - scaledW) / 2;
|
||||
var offsetY = (dispH - scaledH) / 2;
|
||||
|
||||
// draw frame
|
||||
context.DrawImage(
|
||||
frame,
|
||||
new Rect(0, 0, rawW, rawH),
|
||||
new Rect(offsetX, offsetY, scaledW, scaledH));
|
||||
|
||||
// overlays
|
||||
if (preview.DetectedBoxes is { Count: > 0 })
|
||||
{
|
||||
var pen = new Pen(Brushes.Lime, 2);
|
||||
|
||||
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 + x * scale,
|
||||
offsetY + y * scale,
|
||||
w * scale,
|
||||
h * scale);
|
||||
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
}
|
||||
}
|
||||
RenderDetectedBoxes(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
|
||||
RenderCropRectangle(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
|
||||
RenderGravitateTo(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
Grid.Row="0"
|
||||
Preview="{Binding Preview}"
|
||||
Sar="{Binding Sar}"
|
||||
RotateAngle="{Binding Rotate}" />
|
||||
RotateAngle="{Binding Rotate}"
|
||||
GravitateTo="{Binding GravitateTo}"/>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="Auto,*,Auto"
|
||||
|
||||
@ -6,6 +6,10 @@ namespace splitter;
|
||||
|
||||
public sealed class CommandLine
|
||||
{
|
||||
// Default vertical Full HD for YouTube Shorts
|
||||
public const int DefaultW = 607;
|
||||
public const int DefaultH = 1080;
|
||||
|
||||
public SingleJob Master { get; } = new SingleJob();
|
||||
public SingleJob[] Jobs { get; }
|
||||
|
||||
@ -231,13 +235,9 @@ public sealed class CommandLine
|
||||
|
||||
private static (int width, int height)? ParseCrop(string v)
|
||||
{
|
||||
// Default vertical Full HD for YouTube Shorts
|
||||
const int defaultW = 607;
|
||||
const int defaultH = 1080;
|
||||
|
||||
// Empty or whitespace → default crop
|
||||
if (string.IsNullOrWhiteSpace(v))
|
||||
return (defaultW, defaultH);
|
||||
return (DefaultW, DefaultH);
|
||||
|
||||
var s = v.Trim().ToLowerInvariant();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user