mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Selected object tracking. Better tracked object Id generation. Not stable enough yet.
This commit is contained in:
parent
6ebeccd761
commit
f2493c1709
@ -3,13 +3,13 @@
|
||||
public class PreviewData
|
||||
{
|
||||
public Avalonia.Media.Imaging.Bitmap? Frame { get; }
|
||||
public IReadOnlyList<OpenCvSharp.Rect> DetectedBoxes { get; }
|
||||
public IReadOnlyList<DetectedPerson> DetectedBoxes { get; }
|
||||
public Rect? CropRect { get; }
|
||||
public Point2f GravitateTo { get; }
|
||||
public TimeSpan Position { get; }
|
||||
public int? Rotate { get; }
|
||||
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<OpenCvSharp.Rect> boxes, Rect? crop, Point2f gravitateTo, TimeSpan position, int? rotate)
|
||||
public PreviewData(Avalonia.Media.Imaging.Bitmap? frame, IReadOnlyList<DetectedPerson> boxes, Rect? crop, Point2f gravitateTo, TimeSpan position, int? rotate)
|
||||
{
|
||||
Frame = frame;
|
||||
DetectedBoxes = boxes;
|
||||
|
||||
@ -39,19 +39,16 @@ internal sealed class Program
|
||||
services.AddSingleton<YoloV10ObjectDetector>();
|
||||
services.AddSingleton<OSNetEmbeddingExtractor>();
|
||||
services.AddSingleton<IObjectTracker, ObjectTracker>();
|
||||
services.AddSingleton(x => new SingleThreadedDetector<UltraFaceDetector>(x.GetRequiredService<UltraFaceDetector>()));
|
||||
services.AddSingleton(x => new SingleThreadedDetector<YoloV10ObjectDetector>(x.GetRequiredService<YoloV10ObjectDetector>()));
|
||||
services.AddSingleton(x => new SingleThreadedDetector<DummyDetector>(x.GetRequiredService<DummyDetector>()));
|
||||
services.AddKeyedSingleton<IObjectDetector>("face", (x,_) => new SingleThreadedDetector<UltraFaceDetector>(x.GetRequiredService<UltraFaceDetector>()));
|
||||
services.AddKeyedSingleton<IObjectDetector>("body", (x,_) => new SingleThreadedDetector<YoloV10ObjectDetector>(x.GetRequiredService<YoloV10ObjectDetector>()));
|
||||
services.AddKeyedSingleton<IObjectDetector>("none", (x,_) => new SingleThreadedDetector<DummyDetector>(x.GetRequiredService<DummyDetector>()));
|
||||
services.AddSingleton<IEmbeddingExtractor>(x => new SingleThreadedEmbeddingExtractor<OSNetEmbeddingExtractor>(x.GetRequiredService<OSNetEmbeddingExtractor>()));
|
||||
services.AddSingleton<Func<string, IObjectDetector>>( x => detectorName =>
|
||||
services.AddSingleton<Func<string, IObjectDetector>>(x => detectorName => x.GetKeyedService<IObjectDetector>(detectorName) ?? new DummyDetector());
|
||||
services.AddSingleton<Func<string, IObjectTracker>>(x => detectorName =>
|
||||
{
|
||||
return detectorName switch
|
||||
{
|
||||
"face" => x.GetRequiredService<SingleThreadedDetector<UltraFaceDetector>>(),
|
||||
"body" => x.GetRequiredService<SingleThreadedDetector<YoloV10ObjectDetector>>(),
|
||||
"none" => x.GetRequiredService<SingleThreadedDetector<DummyDetector>>(),
|
||||
_ => new DummyDetector()
|
||||
};
|
||||
var detectorFactory = x.GetRequiredService<Func<string, IObjectDetector>>();
|
||||
var extractor = x.GetRequiredService<IEmbeddingExtractor>();
|
||||
return new ObjectTracker(detectorFactory(detectorName), extractor);
|
||||
});
|
||||
services.AddSingleton<ILogger, GlobalLogger>();
|
||||
services.AddSingleton<IJobProcessor, JobProcessor>();
|
||||
|
||||
@ -30,22 +30,7 @@ public partial class InspectorPaneViewModel : ObservableObject
|
||||
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.Enhance = Selected.Enhance;
|
||||
|
||||
job.ParametersList.Clear();
|
||||
foreach (var param in Selected.ParametersList)
|
||||
job.ParametersList.Add(param);
|
||||
}
|
||||
job.CopyFrom(Selected);
|
||||
}
|
||||
|
||||
public IRelayCommand RotateLeftCommand { get; }
|
||||
|
||||
@ -28,7 +28,7 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
private readonly IThumbnailService _thumbnails;
|
||||
private readonly DispatcherTimer _debounceTimer;
|
||||
private readonly Func<string, IObjectDetector> _detectorFactory;
|
||||
private readonly Func<string, IObjectTracker> _trackerFactory;
|
||||
private readonly ILogger _log;
|
||||
|
||||
public string FileName => Path.GetFileName(Job.InputFile);
|
||||
@ -220,6 +220,19 @@ public partial class JobViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public ulong? DetectId
|
||||
{
|
||||
get => Job.DetectId;
|
||||
set
|
||||
{
|
||||
if (DetectId == value)
|
||||
return;
|
||||
Job.DetectId = value;
|
||||
OnPropertyChanged();
|
||||
Task.Run(CreatePreview);
|
||||
}
|
||||
}
|
||||
|
||||
public double? OverrideTargetDuration
|
||||
{
|
||||
get => Job.OverrideTargetDuration;
|
||||
@ -231,11 +244,12 @@ public partial class JobViewModel : ObservableObject
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, Func<string, IObjectDetector> detectorFactory, ILogger log)
|
||||
|
||||
public JobViewModel(SingleJob job, IThumbnailService thumbnails, Func<string, IObjectTracker> trackerFactory, ILogger log)
|
||||
{
|
||||
Job = job;
|
||||
_thumbnails = thumbnails;
|
||||
_detectorFactory = detectorFactory;
|
||||
_trackerFactory = trackerFactory;
|
||||
_log = log;
|
||||
|
||||
ParametersList.Add(new ParameterEntry("DropoutToleranceFrames" , ""));
|
||||
@ -271,6 +285,12 @@ public partial class JobViewModel : ObservableObject
|
||||
_debounceTimer.Tick += DebounceTimerTick;
|
||||
}
|
||||
|
||||
public void CopyFrom(JobViewModel src)
|
||||
{
|
||||
Job.CopyFrom(src.Job);
|
||||
OnPropertyChanged(string.Empty); // Refresh all properties
|
||||
}
|
||||
|
||||
public async Task CreatePreview()
|
||||
{
|
||||
if ( Probe == null)
|
||||
@ -289,7 +309,7 @@ public partial class JobViewModel : ObservableObject
|
||||
|
||||
Preview = new PreviewData(frame, [], null, Job.GravitateTo, pos, Job.Rotate);
|
||||
|
||||
var detector = _detectorFactory(Job.Detect ?? "");
|
||||
var tracker = _trackerFactory(Job.Detect ?? "");
|
||||
var j = new SingleTask
|
||||
(
|
||||
Job : Job,
|
||||
@ -301,31 +321,26 @@ public partial class JobViewModel : ObservableObject
|
||||
SegmentLength : 1, // 1 second segment for detection
|
||||
ProcessorFactory: _ => throw new NotImplementedException()
|
||||
);
|
||||
var detections = detector.DetectAll(j, frame.ToMatContinuous());
|
||||
|
||||
var (detections, primaryDetection) = tracker.SelectTrackedObject(j, frame.ToMatContinuous(), j.Job.GravitateTo);
|
||||
|
||||
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 w = Probe.Width;
|
||||
var h = Probe.Height;
|
||||
var cropWidth = Job.Crop?.width ?? CommandLine.DefaultW;
|
||||
var cropHeight = Job.Crop?.height ?? CommandLine.DefaultH;
|
||||
|
||||
var cropWidth = Job.Crop?.width ?? CommandLine.DefaultW;
|
||||
var cropHeight = Job.Crop?.height ?? CommandLine.DefaultH;
|
||||
var p = primaryDetection?.Center ?? new Point2f(w * Job.GravitateTo.X, h * Job.GravitateTo.Y);
|
||||
|
||||
var cx = primaryDetection.Center.X - cropWidth / 2f;
|
||||
var cy = primaryDetection.Center.Y - cropHeight / 2f;
|
||||
var cx = p.X - cropWidth / 2f;
|
||||
var cy = p.Y - cropHeight / 2f;
|
||||
|
||||
var r = new Rect(cx, cy, cropWidth, cropHeight);
|
||||
var r = new Rect(cx, cy, cropWidth, cropHeight);
|
||||
|
||||
crop = ClampCrop(r, w, h);
|
||||
}
|
||||
crop = ClampCrop(r, w, h);
|
||||
|
||||
var boxes = detections.Select(x => x.Box).ToList();
|
||||
Preview = new PreviewData(frame, boxes, crop, Job.GravitateTo, pos, Job.Rotate);
|
||||
Preview = new PreviewData(frame, detections ?? [], crop, Job.GravitateTo, pos, Job.Rotate);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -35,6 +35,18 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public ulong? TrackedId
|
||||
{
|
||||
get => Selected?.DetectId;
|
||||
set
|
||||
{
|
||||
if (Selected == null)
|
||||
return;
|
||||
Selected.DetectId = value;
|
||||
OnPropertyChanged(nameof(TrackedId));
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedChanged(JobViewModel? oldValue, JobViewModel? newValue)
|
||||
{
|
||||
if (oldValue != null)
|
||||
@ -46,6 +58,8 @@ public partial class PreviewPaneViewModel : ObservableObject
|
||||
OnPropertyChanged(nameof(Preview));
|
||||
OnPropertyChanged(nameof(Sar));
|
||||
OnPropertyChanged(nameof(Rotate));
|
||||
OnPropertyChanged(nameof(TrackedId));
|
||||
OnPropertyChanged(nameof(DetectAbove));
|
||||
}
|
||||
|
||||
private void SelectedPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
|
||||
@ -9,6 +9,21 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="2">
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,10,0,0">
|
||||
|
||||
<Button Content="Apply to Selected"
|
||||
Command="{Binding ApplyOverridesCommand}"/>
|
||||
|
||||
<Button Content="Transform all"
|
||||
Background="#AA0000"
|
||||
Foreground="White"
|
||||
Command="{Binding TransformAllCommand}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Parameters" FontSize="10" Margin="0,0,0,10" FontWeight="Bold"/>
|
||||
|
||||
<!-- InputFile -->
|
||||
@ -111,6 +126,12 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<TextBox Text="{Binding Selected.DetectAbove}" Width="160"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- DetectId -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Object to track" Width="120"/>
|
||||
<TextBox Text="{Binding Selected.DetectId}" Width="160"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- OverrideTargetDuration -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Target Duration" Width="120"/>
|
||||
@ -182,22 +203,6 @@ x:DataType="vm:InspectorPaneViewModel">
|
||||
<TextBox Text="{Binding Selected.PassthroughText}" Width="260"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,10,0,0">
|
||||
|
||||
<Button Content="Apply to Selected"
|
||||
Command="{Binding ApplyOverridesCommand}"/>
|
||||
|
||||
<Button Content="Transform all"
|
||||
Background="#AA0000"
|
||||
Foreground="White"
|
||||
Command="{Binding TransformAllCommand}"/>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
x:DataType="vm:MainViewModel"
|
||||
x:Name="Root"
|
||||
Width="1800"
|
||||
Height="830"
|
||||
Height="870"
|
||||
Title="Splitter UI"
|
||||
Icon="avares://Splitter-UI/Assets/splitter.png">
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
@ -17,10 +18,10 @@ public sealed class PreviewCanvas : Control
|
||||
AvaloniaProperty.Register<PreviewCanvas, int>(nameof(RotateAngle));
|
||||
public static readonly StyledProperty<Point2f> GravitateToProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, Point2f>(nameof(GravitateTo));
|
||||
|
||||
// normalized 0..1 from top of frame
|
||||
public static readonly StyledProperty<float> DetectAboveProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, float>(nameof(DetectAbove), 0.2f);
|
||||
public static readonly StyledProperty<ulong?> DetectIdProperty =
|
||||
AvaloniaProperty.Register<PreviewCanvas, ulong?>(nameof(DetectId));
|
||||
|
||||
public PreviewData? Preview
|
||||
{
|
||||
@ -47,6 +48,12 @@ public sealed class PreviewCanvas : Control
|
||||
set => SetValue(GravitateToProperty, value);
|
||||
}
|
||||
|
||||
public ulong? DetectId
|
||||
{
|
||||
get => GetValue(DetectIdProperty);
|
||||
set => SetValue(DetectIdProperty, value);
|
||||
}
|
||||
|
||||
// DetectAbove is normalized (0..1) from top
|
||||
public float DetectAbove
|
||||
{
|
||||
@ -181,30 +188,22 @@ public sealed class PreviewCanvas : Control
|
||||
h * scale);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Hit test for gravitate point (normalized)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private bool HitGravitate(Avalonia.Point p, out Point2f value)
|
||||
private void GetAspects(
|
||||
PreviewData preview,
|
||||
out int rawW,
|
||||
out int rawH,
|
||||
out int rotate,
|
||||
out float pixelAspect,
|
||||
out double scale,
|
||||
out double offsetX,
|
||||
out double offsetY)
|
||||
{
|
||||
value = default;
|
||||
rawW = preview.Frame!.PixelSize.Width;
|
||||
rawH = preview.Frame.PixelSize.Height;
|
||||
rotate = RotateAngle;
|
||||
|
||||
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;
|
||||
pixelAspect = sar.X / sar.Y;
|
||||
|
||||
var dispW = Bounds.Width;
|
||||
var dispH = Bounds.Height;
|
||||
@ -221,9 +220,32 @@ public sealed class PreviewCanvas : Control
|
||||
displayH = rawH * pixelAspect;
|
||||
}
|
||||
|
||||
var scale = Math.Min(dispW / displayW, dispH / displayH);
|
||||
var offsetX = (dispW - displayW * scale) / 2;
|
||||
var offsetY = (dispH - displayH * scale) / 2;
|
||||
scale = Math.Min(dispW / displayW, dispH / displayH);
|
||||
offsetX = (dispW - displayW * scale) / 2;
|
||||
offsetY = (dispH - displayH * scale) / 2;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
int rawW, rawH, rotate;
|
||||
float pixelAspect;
|
||||
double scale, offsetX, offsetY;
|
||||
GetAspects(preview, out rawW, out rawH, out rotate, out pixelAspect, out scale, out offsetX, out offsetY);
|
||||
|
||||
double px = g.X * rawW;
|
||||
double py = g.Y * rawH;
|
||||
|
||||
var (cx, cy) = TransformPoint(
|
||||
px, py,
|
||||
@ -254,31 +276,10 @@ public sealed class PreviewCanvas : Control
|
||||
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;
|
||||
int rawW, rawH, rotate;
|
||||
float pixelAspect;
|
||||
double scale, offsetX, offsetY;
|
||||
GetAspects(preview, out rawW, out rawH, out rotate, out pixelAspect, out scale, out offsetX, out offsetY);
|
||||
|
||||
var da = DetectAbove;
|
||||
var py = da * rawH;
|
||||
@ -301,6 +302,42 @@ public sealed class PreviewCanvas : Control
|
||||
return hit;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Hit test for detected boxes
|
||||
// ------------------------------------------------------------
|
||||
private bool HitDetectedBox(Avalonia.Point p, out ulong? value)
|
||||
{
|
||||
value = null;
|
||||
|
||||
var preview = Preview;
|
||||
if (preview?.Frame is null)
|
||||
return false;
|
||||
|
||||
int rawW, rawH, rotate;
|
||||
float pixelAspect;
|
||||
double scale, offsetX, offsetY;
|
||||
GetAspects(preview, out rawW, out rawH, out rotate, out pixelAspect, out scale, out offsetX, out offsetY);
|
||||
|
||||
var frame = preview.Frame;
|
||||
foreach (var box in preview.DetectedBoxes)
|
||||
{
|
||||
var rect = TransformRect(
|
||||
box.Box.X, box.Box.Y, box.Box.Width, box.Box.Height,
|
||||
frame.PixelSize.Width, frame.PixelSize.Height,
|
||||
offsetX, offsetY, scale,
|
||||
RotateAngle,
|
||||
Sar?.X / Sar?.Y ?? 1);
|
||||
|
||||
if (rect.Contains(p))
|
||||
{
|
||||
value = box.Id;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Pointer events
|
||||
// ------------------------------------------------------------
|
||||
@ -324,6 +361,12 @@ public sealed class PreviewCanvas : Control
|
||||
_dragStartCanvas = p;
|
||||
_dragStartDetectAbove = da; // normalized
|
||||
e.Pointer.Capture(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HitDetectedBox(p, out var id))
|
||||
{
|
||||
DetectId = id;
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,29 +380,10 @@ public sealed class PreviewCanvas : Control
|
||||
var dxCanvas = p.X - _dragStartCanvas.X;
|
||||
var dyCanvas = p.Y - _dragStartCanvas.Y;
|
||||
|
||||
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);
|
||||
int rawW, rawH, rotate;
|
||||
float pixelAspect;
|
||||
double scale, offsetX, offsetY;
|
||||
GetAspects(preview, out rawW, out rawH, out rotate, out pixelAspect, out scale, out offsetX, out offsetY);
|
||||
|
||||
var dx = dxCanvas / scale;
|
||||
var dy = dyCanvas / scale;
|
||||
@ -480,7 +504,6 @@ public sealed class PreviewCanvas : Control
|
||||
{
|
||||
var g = GravitateTo;
|
||||
|
||||
// normalized → pixel
|
||||
var px = g.X * rawW;
|
||||
var py = g.Y * rawH;
|
||||
|
||||
@ -516,18 +539,24 @@ public sealed class PreviewCanvas : Control
|
||||
return;
|
||||
|
||||
var pen = new Pen(Brushes.Lime, 2);
|
||||
var selectedPen = new Pen(Brushes.Magenta, 2);
|
||||
|
||||
foreach (var r in preview.DetectedBoxes)
|
||||
var detected = preview.DetectedBoxes.ToList();
|
||||
|
||||
foreach (var r in detected)
|
||||
{
|
||||
var rr = TransformRect(
|
||||
r.X, r.Y, r.Width, r.Height,
|
||||
r.Box.X, r.Box.Y, r.Box.Width, r.Box.Height,
|
||||
rawW, rawH,
|
||||
offsetX, offsetY,
|
||||
scale,
|
||||
rotate,
|
||||
pixelAspect);
|
||||
|
||||
context.DrawRectangle(null, pen, rr);
|
||||
context.DrawRectangle(null, r.Id == DetectId ? selectedPen : pen, rr);
|
||||
context.DrawText(
|
||||
new FormattedText($"ID: {r.Id}", CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 12, r.Id == DetectId ? Brushes.Magenta : Brushes.Lime),
|
||||
new Avalonia.Point(rr.X + 5, rr.Y + 5));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
RotateAngle="{Binding Rotate}"
|
||||
GravitateTo="{Binding GravitateTo, Mode=TwoWay}"
|
||||
DetectAbove="{Binding DetectAbove, Mode=TwoWay}"
|
||||
DetectId="{Binding Selected.DetectId, Mode=TwoWay}"
|
||||
/>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
|
||||
@ -86,6 +86,14 @@ public sealed class CommandLine
|
||||
else
|
||||
throw new FormatException($"Invalid --rotate value: {val}");
|
||||
}
|
||||
else if (arg.StartsWith("--detect-id="))
|
||||
{
|
||||
var val = arg.Substring("--detect-id=".Length);
|
||||
if (ulong.TryParse(val, out var detectId))
|
||||
Master.DetectId = detectId;
|
||||
else
|
||||
throw new FormatException($"Invalid --detect-id value: {val}");
|
||||
}
|
||||
else if (arg.StartsWith("--crop="))
|
||||
{
|
||||
Master.Crop = ParseCrop(arg.Substring("--crop=".Length));
|
||||
@ -164,24 +172,11 @@ public sealed class CommandLine
|
||||
|
||||
var files = inputFiles.SelectMany(x => FileMaskExpander.Expand(x));
|
||||
|
||||
Jobs = files.Select(x => new SingleJob
|
||||
Jobs = files.Select(x =>
|
||||
{
|
||||
InputFile = x,
|
||||
OutputFolder = Master.OutputFolder,
|
||||
Crop = Master.Crop,
|
||||
GravitateTo = Master.GravitateTo,
|
||||
Mask = Master.Mask,
|
||||
Debug = Master.Debug,
|
||||
Detect = Master.Detect,
|
||||
OverrideTargetDuration = Master.OverrideTargetDuration,
|
||||
Passthrough = Master.Passthrough,
|
||||
PlainText = Master.PlainText,
|
||||
EstimateOnly = Master.EstimateOnly,
|
||||
ForceFixed = Master.ForceFixed,
|
||||
SingleThreaded = Master.SingleThreaded,
|
||||
Rotate = Master.Rotate,
|
||||
RotateAuto = Master.RotateAuto,
|
||||
Parameters = new Dictionary<string, string>(Master.Parameters)
|
||||
var job = new SingleJob { InputFile = x };
|
||||
Master.CopyTo(job);
|
||||
return job;
|
||||
}).ToArray();
|
||||
|
||||
if ( Jobs.Length == 0)
|
||||
@ -370,6 +365,11 @@ Options:
|
||||
--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.
|
||||
|
||||
--detect-id=<hex> Object ID to track. This is a hexadecimal string that identifies a specific face or
|
||||
person to track across segments. This is useful when you want to consistently track the same person
|
||||
across all segments of a video, even if there are multiple people present.
|
||||
The ID can be obtained when running with --debug or from the debug overlay.
|
||||
|
||||
--gravitate=<x:y> 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)
|
||||
|
||||
@ -54,6 +54,13 @@ public class SingleJob
|
||||
/// </summary>
|
||||
public float DetectAbove { get; set; } = 0.7f;
|
||||
/// <summary>
|
||||
/// Object ID to track. This is a hexadecimal string that identifies a specific face or
|
||||
/// person to track across segments. This is useful when you want to consistently track the same person
|
||||
/// publacross all segments of a video, even if there are multiple people present
|
||||
/// The ID can be obtained when running with --debug or from the debug overlay.
|
||||
/// </summary>
|
||||
public ulong? DetectId { get; set; }
|
||||
/// <summary>
|
||||
/// Set starget segments length explicitly. By default, the splitter calculates segment
|
||||
/// lengths to be equal and not exceed 58 seconds.
|
||||
/// </summary>
|
||||
@ -128,4 +135,28 @@ public class SingleJob
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo(SingleJob target)
|
||||
{
|
||||
target.OutputFolder = OutputFolder;
|
||||
target.Crop = Crop;
|
||||
target.GravitateTo = GravitateTo;
|
||||
target.Mask = Mask;
|
||||
target.Debug = Debug;
|
||||
target.Detect = Detect;
|
||||
target.ScoreThreshold = ScoreThreshold;
|
||||
target.DetectAbove = DetectAbove;
|
||||
target.DetectId = DetectId;
|
||||
target.OverrideTargetDuration = OverrideTargetDuration;
|
||||
target.Passthrough = Passthrough.ToArray();
|
||||
target.PlainText = PlainText;
|
||||
target.EstimateOnly = EstimateOnly;
|
||||
target.ForceFixed = ForceFixed;
|
||||
target.SingleThreaded = SingleThreaded;
|
||||
target.Rotate = Rotate;
|
||||
target.RotateAuto = RotateAuto;
|
||||
target.Parameters = new Dictionary<string, string>(Parameters);
|
||||
target.Enhance = Enhance;
|
||||
}
|
||||
|
||||
public void CopyFrom(SingleJob source) => source.CopyTo(this);
|
||||
}
|
||||
|
||||
@ -2,5 +2,5 @@
|
||||
|
||||
public interface IObjectTracker
|
||||
{
|
||||
(List<DetectedPerson>, DetectedPerson?) SelectTrackedObject(SingleTask job, Mat frameMat, Point2f? lastMeasurement);
|
||||
(List<DetectedPerson> objects, DetectedPerson? primary) SelectTrackedObject(SingleTask job, Mat frameMat, Point2f? lastMeasurement);
|
||||
}
|
||||
78
splitter-cli/algo/IdentityCache.cs
Normal file
78
splitter-cli/algo/IdentityCache.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace splitter.algo;
|
||||
|
||||
public sealed class IdentityCache
|
||||
{
|
||||
private sealed class Identity
|
||||
{
|
||||
public ulong Id;
|
||||
public float[] Embedding; // EMA
|
||||
public int Samples;
|
||||
}
|
||||
|
||||
private readonly List<Identity> _ids = new();
|
||||
private ulong _nextId = 1;
|
||||
|
||||
private const float Threshold = 0.35f; // good for OSNet
|
||||
private const float EmaAlpha = 0.2f;
|
||||
|
||||
public ulong ResolveId(float[] embedding)
|
||||
{
|
||||
if (_ids.Count == 0)
|
||||
return CreateNew(embedding);
|
||||
|
||||
int bestIndex = -1;
|
||||
float bestDist = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < _ids.Count; i++)
|
||||
{
|
||||
float d = CosineDistance(_ids[i].Embedding, embedding);
|
||||
if (d < bestDist)
|
||||
{
|
||||
bestDist = d;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDist <= Threshold)
|
||||
{
|
||||
UpdateEma(_ids[bestIndex].Embedding, embedding);
|
||||
_ids[bestIndex].Samples++;
|
||||
return _ids[bestIndex].Id;
|
||||
}
|
||||
|
||||
return CreateNew(embedding);
|
||||
}
|
||||
|
||||
private ulong CreateNew(float[] embedding)
|
||||
{
|
||||
var id = _nextId++;
|
||||
|
||||
_ids.Add(new Identity
|
||||
{
|
||||
Id = id,
|
||||
Embedding = embedding.ToArray(),
|
||||
Samples = 1
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private static float CosineDistance(float[] a, float[] b)
|
||||
{
|
||||
float dot = 0f;
|
||||
for (int i = 0; i < a.Length; i++)
|
||||
dot += a[i] * b[i];
|
||||
|
||||
return 1f - dot;
|
||||
}
|
||||
|
||||
private static void UpdateEma(float[] ema, float[] v)
|
||||
{
|
||||
for (int i = 0; i < ema.Length; i++)
|
||||
ema[i] = ema[i] * (1 - EmaAlpha) + v[i] * EmaAlpha;
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,21 @@
|
||||
|
||||
public class ObjectTracker(IObjectDetector _detector, IEmbeddingExtractor _embeddingExtractor) : IObjectTracker
|
||||
{
|
||||
public (List<DetectedPerson> /*objects*/, DetectedPerson? /*primary*/) SelectTrackedObject(SingleTask job, Mat frameMat, Point2f? lastMeasurement)
|
||||
private readonly IdentityCache _identityCache = new();
|
||||
|
||||
public (List<DetectedPerson> objects, DetectedPerson? primary) SelectTrackedObject(SingleTask job, Mat frameMat, Point2f? lastMeasurement)
|
||||
{
|
||||
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();
|
||||
// filter by DetectAbove
|
||||
objects = objects
|
||||
.Where(o => o.Center.Y <= frameMat.Height * job.Job.DetectAbove)
|
||||
.ToList();
|
||||
|
||||
// attach embeddings to all persons
|
||||
// attach embeddings
|
||||
for (int i = 0; i < objects.Count; i++)
|
||||
{
|
||||
var p = objects[i]; // copy struct
|
||||
var p = objects[i];
|
||||
|
||||
var rect = p.Box;
|
||||
|
||||
@ -21,38 +25,32 @@ public class ObjectTracker(IObjectDetector _detector, IEmbeddingExtractor _embed
|
||||
rect.Width = Math.Clamp(rect.Width, 1, frameMat.Width - rect.X);
|
||||
rect.Height = Math.Clamp(rect.Height, 1, frameMat.Height - rect.Y);
|
||||
|
||||
var embedding = _embeddingExtractor.Extract(frameMat, rect);
|
||||
p.Id = HashEmbedding(embedding); // assign ID based on embedding hash
|
||||
var embedding = _embeddingExtractor.Extract(frameMat, rect).ToArray(); // make a copy of the embedding array
|
||||
p.Id = _identityCache.ResolveId(embedding);
|
||||
|
||||
objects[i] = p; // write back
|
||||
objects[i] = p;
|
||||
}
|
||||
|
||||
var primary = SelectPrimaryObject(objects, lastMeasurement);
|
||||
// DeepSeek tracker assigns stable IDs
|
||||
var primary = SelectPrimaryObject(objects, lastMeasurement, job.Job.DetectId);
|
||||
return (objects, primary);
|
||||
}
|
||||
|
||||
private static ulong HashEmbedding(float[] emb)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
ulong hash = 146527;
|
||||
for (int i = 0; i < emb.Length; i++)
|
||||
{
|
||||
// convert float to int bits
|
||||
uint bits = (uint)BitConverter.SingleToInt32Bits(emb[i]);
|
||||
hash = (hash * 16777619) ^ bits;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
private DetectedPerson? SelectPrimaryObject(
|
||||
List<DetectedPerson> foundObjects,
|
||||
Point2f? previousCenter)
|
||||
Point2f? previousCenter,
|
||||
ulong? detectId)
|
||||
{
|
||||
if (foundObjects == null || foundObjects.Count == 0)
|
||||
return null;
|
||||
|
||||
if (detectId != null)
|
||||
{
|
||||
var match = foundObjects.FirstOrDefault(o => o.Id == detectId.Value);
|
||||
if (match.Id != 0) // default struct has Id=0, so this means we found a match
|
||||
return match;
|
||||
}
|
||||
|
||||
if (!previousCenter.HasValue)
|
||||
{
|
||||
var bestIndex = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user