From 8c611e31d70c32bde78a55a3417a66e5ae65bbdf Mon Sep 17 00:00:00 2001 From: unclshura Date: Mon, 8 Jun 2026 08:51:17 +0100 Subject: [PATCH] Configurable DetectAbove added. UI added too. --- Splitter-UI/ViewModels/JobViewModel.cs | 11 + .../ViewModels/PreviewPaneViewModel.cs | 12 + Splitter-UI/Views/InspectorPane.axaml | 6 + Splitter-UI/Views/PreviewCanvas.cs | 221 +++++++++++++++--- Splitter-UI/Views/PreviewPane.axaml | 4 +- splitter-cli/CommandLine.cs | 11 + splitter-cli/SingleJob.cs | 5 + splitter-cli/TrackingSplitter.cs | 4 + splitter-cli/algo/YoloOnnxObjectDetector.cs | 4 - 9 files changed, 244 insertions(+), 34 deletions(-) diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs index 9e1dd4c..2bccbb9 100644 --- a/Splitter-UI/ViewModels/JobViewModel.cs +++ b/Splitter-UI/ViewModels/JobViewModel.cs @@ -194,6 +194,17 @@ public partial class JobViewModel : ObservableObject } } + public float DetectAbove + { + get => Job.DetectAbove; + set + { + Job.DetectAbove = value; + OnPropertyChanged(); + Task.Run(CreatePreview); + } + } + public double? OverrideTargetDuration { get => Job.OverrideTargetDuration; diff --git a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs index c50fe02..8a45d73 100644 --- a/Splitter-UI/ViewModels/PreviewPaneViewModel.cs +++ b/Splitter-UI/ViewModels/PreviewPaneViewModel.cs @@ -23,6 +23,18 @@ public partial class PreviewPaneViewModel : ObservableObject } } + public float DetectAbove + { + get => Selected?.DetectAbove ?? 0.7f; + set + { + if (Selected == null) + return; + Selected.DetectAbove = value; + OnPropertyChanged(nameof(DetectAbove)); + } + } + partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue) { if (oldValue != null) diff --git a/Splitter-UI/Views/InspectorPane.axaml b/Splitter-UI/Views/InspectorPane.axaml index 916fcf4..1457c41 100644 --- a/Splitter-UI/Views/InspectorPane.axaml +++ b/Splitter-UI/Views/InspectorPane.axaml @@ -86,6 +86,12 @@ x:DataType="vm:InspectorPaneViewModel"> Width="160"/> + + + + + + diff --git a/Splitter-UI/Views/PreviewCanvas.cs b/Splitter-UI/Views/PreviewCanvas.cs index 85be8e3..8fea4c8 100644 --- a/Splitter-UI/Views/PreviewCanvas.cs +++ b/Splitter-UI/Views/PreviewCanvas.cs @@ -18,6 +18,10 @@ public sealed class PreviewCanvas : Control public static readonly StyledProperty GravitateToProperty = AvaloniaProperty.Register(nameof(GravitateTo)); + // normalized 0..1 from top of frame + public static readonly StyledProperty DetectAboveProperty = + AvaloniaProperty.Register(nameof(DetectAbove), 0.2f); + public PreviewData? Preview { get => GetValue(PreviewProperty); @@ -43,10 +47,20 @@ public sealed class PreviewCanvas : Control set => SetValue(GravitateToProperty, value); } - private bool _dragging; + // DetectAbove is normalized (0..1) from top + public float DetectAbove + { + get => GetValue(DetectAboveProperty); + set => SetValue(DetectAboveProperty, value); + } + + private bool _draggingGravitate; private Avalonia.Point _dragStartCanvas; private Point2f _dragStartValue; + private bool _draggingDetectAbove; + private double _dragStartDetectAbove; // normalized 0..1 + static PreviewCanvas() { PreviewProperty.Changed.AddClassHandler( @@ -228,6 +242,65 @@ public sealed class PreviewCanvas : Control return hit; } + // ------------------------------------------------------------ + // Hit test for DetectAbove knob (normalized) + // ------------------------------------------------------------ + + private bool HitDetectAbove(Avalonia.Point p, out double value) + { + value = default; + + var preview = Preview; + if (preview?.Frame is null) + return false; + + 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); + var offsetX = (dispW - displayW * scale) / 2; + var offsetY = (dispH - displayH * scale) / 2; + + var da = DetectAbove; + var py = da * rawH; + var px = rawW / 2.0; + + 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 = da; + + return hit; + } + // ------------------------------------------------------------ // Pointer events // ------------------------------------------------------------ @@ -238,23 +311,32 @@ public sealed class PreviewCanvas : Control if (HitGravitate(p, out var g)) { - _dragging = true; + _draggingGravitate = true; _dragStartCanvas = p; _dragStartValue = g; // normalized e.Pointer.Capture(this); + return; + } + + if (HitDetectAbove(p, out var da)) + { + _draggingDetectAbove = true; + _dragStartCanvas = p; + _dragStartDetectAbove = da; // normalized + e.Pointer.Capture(this); } } private void OnPointerMoved(object? sender, PointerEventArgs e) { - if (!_dragging || Preview?.Frame is null) + var preview = Preview; + if (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; @@ -287,43 +369,74 @@ public sealed class PreviewCanvas : Control else dy /= pixelAspect; - // start normalized → pixel - var gx = _dragStartValue.X * rawW + dx; - var gy = _dragStartValue.Y * rawH + dy; - - switch (rotate) + if (_draggingGravitate) { - 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; + var gx = _dragStartValue.X * rawW + dx; + var 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; + } + + 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); } + else if (_draggingDetectAbove) + { + var gx = rawW / 2.0; + var gy = _dragStartDetectAbove * rawH + dy; - // pixel → normalized - var nx = (float)(gx / rawW); - var ny = (float)(gy / rawH); + 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; + } - if (nx < 0) nx = 0; - if (ny < 0) ny = 0; - if (nx > 1) nx = 1; - if (ny > 1) ny = 1; + var ny = gy / rawH; + if (ny < 0) ny = 0; + if (ny > 1) ny = 1; - GravitateTo = new Point2f(nx, ny); + DetectAbove = (float)ny; + } + else + { + return; + } Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Render); } private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { - if (_dragging) + if (_draggingGravitate || _draggingDetectAbove) { - _dragging = false; + _draggingGravitate = false; + _draggingDetectAbove = false; e.Pointer.Capture(null); } } @@ -418,6 +531,55 @@ public sealed class PreviewCanvas : Control } } + private void RenderDetectAbove( + DrawingContext context, + PreviewData preview, + double rawW, double rawH, + double offsetX, double offsetY, + double scale, + int rotate, + double pixelAspect) + { + var da = DetectAbove; + var rawY = da * rawH; + + var (x1, y1) = TransformPoint( + 0, rawY, + rawW, rawH, + offsetX, offsetY, + scale, + rotate, + pixelAspect); + + var (x2, y2) = TransformPoint( + rawW, rawY, + rawW, rawH, + offsetX, offsetY, + scale, + rotate, + pixelAspect); + + var pen = new Pen(Brushes.Lime, 2); + context.DrawLine(pen, new Avalonia.Point(x1, y1), new Avalonia.Point(x2, y2)); + + const double radius = 10; + var (kx, ky) = TransformPoint( + rawW / 2.0, rawY, + rawW, rawH, + offsetX, offsetY, + scale, + rotate, + pixelAspect); + + var knob = new EllipseGeometry( + new Rect(kx - radius, ky - radius, radius * 2, radius * 2)); + + var knobPen = new Pen(Brushes.Lime, 2); + var knobBrush = Brushes.Lime; + + context.DrawGeometry(knobBrush, knobPen, knob); + } + // ------------------------------------------------------------ // Main Render // ------------------------------------------------------------ @@ -474,5 +636,6 @@ public sealed class PreviewCanvas : Control 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); + RenderDetectAbove(context, preview, rawW, rawH, offsetX, offsetY, scale, rotate, pixelAspect); } } diff --git a/Splitter-UI/Views/PreviewPane.axaml b/Splitter-UI/Views/PreviewPane.axaml index d686040..e19f13c 100644 --- a/Splitter-UI/Views/PreviewPane.axaml +++ b/Splitter-UI/Views/PreviewPane.axaml @@ -14,7 +14,9 @@ Preview="{Binding Preview}" Sar="{Binding Sar}" RotateAngle="{Binding Rotate}" - GravitateTo="{Binding GravitateTo, Mode=TwoWay}"/> + GravitateTo="{Binding GravitateTo, Mode=TwoWay}" + DetectAbove="{Binding DetectAbove, Mode=TwoWay}" + /> Object detector to use for tracking. Values: face (UltraFace), body (YoloOnnx, default), none (no tracking, just a center) + --detect-above=<0-1> Face or human detectors should only report detections if their upper bound starts below this threshold. + This is a value between 0.0 and 1.0 mapped to 0..Height. + --gravitate= Gravitate towards a specific point (x, y) in the video frame when tracking. Coordinates are normalized (0.0 to 1.0). Example: --gravitate=0.2:0.5 (gravitate towards left-center) diff --git a/splitter-cli/SingleJob.cs b/splitter-cli/SingleJob.cs index 19f06e6..3a1d1c5 100644 --- a/splitter-cli/SingleJob.cs +++ b/splitter-cli/SingleJob.cs @@ -31,6 +31,11 @@ public class SingleJob /// public Point2f? GravitateTo { get; set; } /// + /// Face or human detectors should only report detections if their upper bound starts below this threshold. + /// This is a value between 0.0 and 1.0 mapped to 0..Height. + /// + public float DetectAbove { get; set; } = 0.3f; + /// /// Destination file mask. /// public string? Mask { get; set; } diff --git a/splitter-cli/TrackingSplitter.cs b/splitter-cli/TrackingSplitter.cs index 5bde9ca..2d74d03 100644 --- a/splitter-cli/TrackingSplitter.cs +++ b/splitter-cli/TrackingSplitter.cs @@ -131,6 +131,10 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable Marshal.Copy(inBuffer, 0, frameMat.Data, inBytes); var objects = _detector.DetectAll(job, frameMat); + + // Ignore detections starting in the lower 1/2 of the frame + objects = objects.Where(o => o.center.Y <= frameMat.Height * job.Job.DetectAbove).ToList(); + var primary = SelectTrackedObject(objects, kalman.LastMeasurement); camera.Update(primary); diff --git a/splitter-cli/algo/YoloOnnxObjectDetector.cs b/splitter-cli/algo/YoloOnnxObjectDetector.cs index d1aa60e..3ee4bd6 100644 --- a/splitter-cli/algo/YoloOnnxObjectDetector.cs +++ b/splitter-cli/algo/YoloOnnxObjectDetector.cs @@ -139,10 +139,6 @@ public sealed class YoloOnnxObjectDetector : LoggingBase, IObjectDetector, IDisp w = Math.Clamp(w, 1, frameCont.Width - x); h = Math.Clamp(h, 1, frameCont.Height - y); - // Ignore detections starting in the lower 1/2 of the frame - if (y > frameCont.Height * 0.5f) - continue; - var rect = new Rect(x, y, w, h); var center = new Point2f(x + w / 2f, y + h / 2f);