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}"
+ />
= 0.0f && detectAbove <= 1.0f)
+ Master.DetectAbove = detectAbove;
+ else
+ Master.DetectAbove = 0.7f;
+ }
else if (arg == "--crop")
{
Master.Crop = ParseCrop("");
@@ -351,6 +359,9 @@ Options:
--detect= 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);