mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Crop rectangle and gravitate to (meveable) point added to preview.
This commit is contained in:
parent
61c94d4661
commit
c6ca4fcbb6
@ -7,12 +7,14 @@ public class PreviewData
|
|||||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||||
public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { 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;
|
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;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
namespace Splitter_UI.ViewModels;
|
namespace Splitter_UI.ViewModels;
|
||||||
@ -8,6 +9,8 @@ public partial class InspectorPaneViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private JobViewModel? _selected;
|
private JobViewModel? _selected;
|
||||||
|
|
||||||
|
public ObservableCollection<JobViewModel> Files { get; set; } = [];
|
||||||
|
|
||||||
public List<string> DetectModes =>
|
public List<string> DetectModes =>
|
||||||
[
|
[
|
||||||
"face", "body", "none"
|
"face", "body", "none"
|
||||||
@ -24,6 +27,22 @@ public partial class InspectorPaneViewModel : ObservableObject
|
|||||||
if (Selected is null)
|
if (Selected is null)
|
||||||
return;
|
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; }
|
public IRelayCommand RotateLeftCommand { get; }
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
private SingleJob Job { get; }
|
private SingleJob Job { get; }
|
||||||
|
|
||||||
[ObservableProperty] private VideoInfo? _probe;
|
[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 ProgressInfo? _progress;
|
||||||
[ObservableProperty] private Bitmap? _thumbnail;
|
[ObservableProperty] private Bitmap? _thumbnail;
|
||||||
[ObservableProperty] private string _suggestedAction = "";
|
[ObservableProperty] private string _suggestedAction = "";
|
||||||
@ -84,6 +84,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
Job.GravitateTo = new Point2f(x, y);
|
Job.GravitateTo = new Point2f(x, y);
|
||||||
}
|
}
|
||||||
OnPropertyChanged();
|
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
|
public double? OverrideTargetDuration
|
||||||
{
|
{
|
||||||
get => Job.OverrideTargetDuration;
|
get => Job.OverrideTargetDuration;
|
||||||
@ -188,14 +203,14 @@ public partial class JobViewModel : ObservableObject
|
|||||||
_detectorFactory = detectorFactory;
|
_detectorFactory = detectorFactory;
|
||||||
_log = log;
|
_log = log;
|
||||||
|
|
||||||
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames", ""));
|
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("EmaFactor", ""));
|
ParametersList.Add(new ParameterEntry("EmaFactor" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("CameraEasing", ""));
|
ParametersList.Add(new ParameterEntry("CameraEasing" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("LostFreezeFrames", ""));
|
ParametersList.Add(new ParameterEntry("LostFreezeFrames" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount", ""));
|
ParametersList.Add(new ParameterEntry("RotationDetectorSampleCount" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
|
ParametersList.Add(new ParameterEntry("RotationDetectorSampleLength", ""));
|
||||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth", ""));
|
ParametersList.Add(new ParameterEntry("RotationDetectorFrameWidth" , ""));
|
||||||
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight", ""));
|
ParametersList.Add(new ParameterEntry("RotationDetectorFrameHeight" , ""));
|
||||||
|
|
||||||
foreach (var entry in ParametersList)
|
foreach (var entry in ParametersList)
|
||||||
{
|
{
|
||||||
@ -224,13 +239,34 @@ public partial class JobViewModel : ObservableObject
|
|||||||
if ( frame == null )
|
if ( frame == null )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Preview = new PreviewData(frame, [], null);
|
Preview = new PreviewData(frame, [], null, Job.GravitateTo ?? new (0.5f, 0.5f));
|
||||||
|
|
||||||
var detector = _detectorFactory(Job.Detect ?? "");
|
var detector = _detectorFactory(Job.Detect ?? "");
|
||||||
var detections = detector.DetectAll(frame.ToMatContinuous());
|
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();
|
Rect? crop = null;
|
||||||
Preview = new PreviewData(frame, boxes, 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)
|
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)
|
private void OnParameterChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
|
if (sender is ParameterEntry p && e.PropertyName == nameof(ParameterEntry.Value))
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
Preview.Selected = file;
|
Preview.Selected = file;
|
||||||
Inspector.Selected = file;
|
Inspector.Selected = file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Inspector.Files = FileList.Files;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@ -12,6 +12,17 @@ public partial class PreviewPaneViewModel : ObservableObject
|
|||||||
public PreviewData? Preview => Selected?.Preview;
|
public PreviewData? Preview => Selected?.Preview;
|
||||||
public Point2f? Sar => Selected?.Probe?.Sar;
|
public Point2f? Sar => Selected?.Probe?.Sar;
|
||||||
public int Rotate => Selected?.Rotate ?? 0;
|
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)
|
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using splitter.algo;
|
using splitter.algo;
|
||||||
@ -15,6 +16,8 @@ public sealed class PreviewCanvas : Control
|
|||||||
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
|
AvaloniaProperty.Register<PreviewCanvas, Point2f?>(nameof(Sar));
|
||||||
public static readonly StyledProperty<int> RotateAngleProperty =
|
public static readonly StyledProperty<int> RotateAngleProperty =
|
||||||
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
|
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
|
||||||
|
public static readonly StyledProperty<Point2f> GravitateToProperty =
|
||||||
|
AvaloniaProperty.Register<PreviewCanvas, Point2f>(nameof(GravitateTo));
|
||||||
|
|
||||||
public PreviewData? Preview
|
public PreviewData? Preview
|
||||||
{
|
{
|
||||||
@ -34,6 +37,17 @@ public sealed class PreviewCanvas : Control
|
|||||||
set => SetValue(RotateAngleProperty, value);
|
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()
|
static PreviewCanvas()
|
||||||
{
|
{
|
||||||
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
|
PreviewProperty.Changed.AddClassHandler<PreviewCanvas>(
|
||||||
@ -42,6 +56,13 @@ public sealed class PreviewCanvas : Control
|
|||||||
args.NewValue as PreviewData));
|
args.NewValue as PreviewData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PreviewCanvas()
|
||||||
|
{
|
||||||
|
PointerPressed += OnPointerPressed;
|
||||||
|
PointerMoved += OnPointerMoved;
|
||||||
|
PointerReleased += OnPointerReleased;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
|
private void OnPreviewChanged(PreviewData? oldValue, PreviewData? newValue)
|
||||||
{
|
{
|
||||||
if (oldValue is INotifyPropertyChanged oldNotify)
|
if (oldValue is INotifyPropertyChanged oldNotify)
|
||||||
@ -50,7 +71,6 @@ public sealed class PreviewCanvas : Control
|
|||||||
if (newValue is INotifyPropertyChanged newNotify)
|
if (newValue is INotifyPropertyChanged newNotify)
|
||||||
newNotify.PropertyChanged += PreviewPropertyChanged;
|
newNotify.PropertyChanged += PreviewPropertyChanged;
|
||||||
|
|
||||||
// Always marshal to UI thread
|
|
||||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +80,6 @@ public sealed class PreviewCanvas : Control
|
|||||||
e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
|
e.PropertyName == nameof(PreviewData.DetectedBoxes) ||
|
||||||
e.PropertyName == nameof(PreviewData.CropRect))
|
e.PropertyName == nameof(PreviewData.CropRect))
|
||||||
{
|
{
|
||||||
// Always marshal to UI thread
|
|
||||||
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,80 +87,51 @@ public sealed class PreviewCanvas : Control
|
|||||||
protected override Size MeasureOverride(Size availableSize) => availableSize;
|
protected override Size MeasureOverride(Size availableSize) => availableSize;
|
||||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||||
|
|
||||||
public override void Render(DrawingContext context)
|
// ------------------------------------------------------------
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
var preview = Preview;
|
switch (rotate)
|
||||||
if (preview?.Frame is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var frame = preview.Frame;
|
|
||||||
var rawW = frame.PixelSize.Width;
|
|
||||||
var rawH = frame.PixelSize.Height;
|
|
||||||
|
|
||||||
var dispW = Bounds.Width;
|
|
||||||
var dispH = Bounds.Height;
|
|
||||||
|
|
||||||
if (dispW <= 0 || dispH <= 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var rotate = RotateAngle; // 0, 90, 180, 270
|
|
||||||
|
|
||||||
// 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;
|
case 90:
|
||||||
sarY = 1;
|
(x, y) = (rawH - y, x);
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
x = rawW - x;
|
||||||
|
y = rawH - y;
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
(x, y) = (y, rawW - x);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pixelAspect = sarX / sarY;
|
|
||||||
|
|
||||||
double displayW;
|
|
||||||
double displayH;
|
|
||||||
|
|
||||||
if (rotate == 0 || rotate == 180)
|
if (rotate == 0 || rotate == 180)
|
||||||
{
|
x *= pixelAspect;
|
||||||
// encoded horizontal axis = rawW
|
|
||||||
displayW = rawW * pixelAspect;
|
|
||||||
displayH = rawH;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
y *= pixelAspect;
|
||||||
// encoded horizontal axis = rawH (bitmap already rotated)
|
|
||||||
displayW = rawW;
|
var sx = offsetX + x * scale;
|
||||||
displayH = rawH * pixelAspect;
|
var sy = offsetY + y * scale;
|
||||||
|
|
||||||
|
return (sx, sy);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scale = Math.Min(dispW / displayW, dispH / displayH);
|
private Rect TransformRect(
|
||||||
|
double x, double y, double w, double h,
|
||||||
var scaledW = displayW * scale;
|
double rawW, double rawH,
|
||||||
var scaledH = displayH * scale;
|
double offsetX, double offsetY,
|
||||||
|
double scale,
|
||||||
var offsetX = (dispW - scaledW) / 2;
|
int rotate,
|
||||||
var offsetY = (dispH - scaledH) / 2;
|
double pixelAspect)
|
||||||
|
|
||||||
// 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)
|
switch (rotate)
|
||||||
{
|
{
|
||||||
case 90:
|
case 90:
|
||||||
@ -160,7 +150,6 @@ public sealed class PreviewCanvas : Control
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply SAR to the axis that originated from encoded width
|
|
||||||
if (rotate == 0 || rotate == 180)
|
if (rotate == 0 || rotate == 180)
|
||||||
{
|
{
|
||||||
x *= pixelAspect;
|
x *= pixelAspect;
|
||||||
@ -172,15 +161,319 @@ public sealed class PreviewCanvas : Control
|
|||||||
h *= pixelAspect;
|
h *= pixelAspect;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rr = new Rect(
|
return new Rect(
|
||||||
offsetX + x * scale,
|
offsetX + x * scale,
|
||||||
offsetY + y * scale,
|
offsetY + y * scale,
|
||||||
w * scale,
|
w * scale,
|
||||||
h * 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);
|
context.DrawRectangle(null, pen, rr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// Main Render
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
public override void Render(DrawingContext context)
|
||||||
|
{
|
||||||
|
var preview = Preview;
|
||||||
|
if (preview?.Frame is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var frame = preview.Frame;
|
||||||
|
var rawW = frame.PixelSize.Width;
|
||||||
|
var rawH = frame.PixelSize.Height;
|
||||||
|
|
||||||
|
var dispW = Bounds.Width;
|
||||||
|
var dispH = Bounds.Height;
|
||||||
|
|
||||||
|
if (dispW <= 0 || dispH <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var rotate = RotateAngle;
|
||||||
|
|
||||||
|
var sar = Sar ?? new Point2f(1, 1);
|
||||||
|
var sarX = sar.X <= 0 ? 1 : sar.X;
|
||||||
|
var sarY = sar.Y <= 0 ? 1 : sar.Y;
|
||||||
|
var pixelAspect = sarX / sarY;
|
||||||
|
|
||||||
|
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 scaledW = displayW * scale;
|
||||||
|
var scaledH = displayH * scale;
|
||||||
|
|
||||||
|
var offsetX = (dispW - scaledW) / 2;
|
||||||
|
var offsetY = (dispH - scaledH) / 2;
|
||||||
|
|
||||||
|
context.DrawImage(
|
||||||
|
frame,
|
||||||
|
new Rect(0, 0, rawW, rawH),
|
||||||
|
new Rect(offsetX, offsetY, scaledW, scaledH));
|
||||||
|
|
||||||
|
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"
|
Grid.Row="0"
|
||||||
Preview="{Binding Preview}"
|
Preview="{Binding Preview}"
|
||||||
Sar="{Binding Sar}"
|
Sar="{Binding Sar}"
|
||||||
RotateAngle="{Binding Rotate}" />
|
RotateAngle="{Binding Rotate}"
|
||||||
|
GravitateTo="{Binding GravitateTo}"/>
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
<Grid Grid.Row="1"
|
||||||
ColumnDefinitions="Auto,*,Auto"
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
|
|||||||
@ -6,6 +6,10 @@ namespace splitter;
|
|||||||
|
|
||||||
public sealed class CommandLine
|
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 Master { get; } = new SingleJob();
|
||||||
public SingleJob[] Jobs { get; }
|
public SingleJob[] Jobs { get; }
|
||||||
|
|
||||||
@ -231,13 +235,9 @@ public sealed class CommandLine
|
|||||||
|
|
||||||
private static (int width, int height)? ParseCrop(string v)
|
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
|
// Empty or whitespace → default crop
|
||||||
if (string.IsNullOrWhiteSpace(v))
|
if (string.IsNullOrWhiteSpace(v))
|
||||||
return (defaultW, defaultH);
|
return (DefaultW, DefaultH);
|
||||||
|
|
||||||
var s = v.Trim().ToLowerInvariant();
|
var s = v.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user