Command line switches for changing various internal parameters.

This commit is contained in:
Alexander Shabarshov 2026-05-12 06:55:10 +01:00
parent 2b412694fb
commit a6b9d9c069
4 changed files with 136 additions and 35 deletions

View File

@ -17,13 +17,14 @@ public sealed class CameraController
private readonly int _cropWidth; private readonly int _cropWidth;
private readonly int _cropHeight; private readonly int _cropHeight;
private readonly KalmanTracker _kalman; private readonly KalmanTracker _kalman;
private readonly CommandLine _cmd;
private int _dropoutCounter; private int _dropoutCounter;
// --- Dropout tolerance --- // --- Dropout tolerance ---
private const int DropoutToleranceFrames = 20; private int _dropoutToleranceFrames = 20;
private const float EmaFactor = 0.65f; // smoother but responsive private float _emaFactor = 0.65f; // smoother but responsive
private const float CameraEasing = 0.03f; // stronger follow-through private float _cameraEasing = 0.03f; // stronger follow-through
private const int LostFreezeFrames = 60; // 2 seconds at 30 FPS private int _lostFreezeFrames = 60; // 2 seconds at 30 FPS
private int _lostFrames; private int _lostFrames;
@ -39,7 +40,8 @@ public sealed class CameraController
int videoHeight, int videoHeight,
int cropWidth, int cropWidth,
int cropHeight, int cropHeight,
KalmanTracker kalman KalmanTracker kalman,
CommandLine cmd
) )
{ {
_videoWidth = videoWidth; _videoWidth = videoWidth;
@ -47,13 +49,20 @@ public sealed class CameraController
_cropWidth = cropWidth; _cropWidth = cropWidth;
_cropHeight = cropHeight; _cropHeight = cropHeight;
_kalman = kalman; _kalman = kalman;
_cmd = cmd;
_cameraCenter = new Point2f(videoWidth / 2f, videoHeight / 2f); _cameraCenter = DefaultCenter;
_state = TrackState.Tracking; _state = TrackState.Tracking;
cmd.Override(ref _dropoutToleranceFrames, "DropoutToleranceFrames");
cmd.Override(ref _emaFactor, "EmaFactor");
cmd.Override(ref _cameraEasing, "CameraEasing");
cmd.Override(ref _lostFreezeFrames, "LostFreezeFrames");
_kalman.Reset(_cameraCenter); _kalman.Reset(_cameraCenter);
} }
private Point2f DefaultCenter => _cmd.GravitateTo ?? new Point2f(_videoWidth / 2f, _videoHeight / 2f);
public int LostFrames => _lostFrames; public int LostFrames => _lostFrames;
public Point2f CameraCenter => _cameraCenter; public Point2f CameraCenter => _cameraCenter;
public TrackState State => _state; public TrackState State => _state;
@ -78,7 +87,7 @@ public sealed class CameraController
// --------------------------------------------------------- // ---------------------------------------------------------
if (!objectCenter.HasValue) if (!objectCenter.HasValue)
{ {
if (_dropoutCounter < DropoutToleranceFrames) if (_dropoutCounter < _dropoutToleranceFrames)
{ {
objectCenter = _kalman.LastMeasurement; objectCenter = _kalman.LastMeasurement;
_dropoutCounter++; _dropoutCounter++;
@ -96,7 +105,7 @@ public sealed class CameraController
{ {
_lostFrames++; _lostFrames++;
if (_lostFrames <= LostFreezeFrames) if (_lostFrames <= _lostFreezeFrames)
{ {
_state = TrackState.LostFreeze; _state = TrackState.LostFreeze;
objectCenter = null; objectCenter = null;
@ -104,7 +113,7 @@ public sealed class CameraController
else else
{ {
_state = TrackState.LostDrift; _state = TrackState.LostDrift;
objectCenter = new Point2f(_videoWidth / 2f, _videoHeight / 2f); objectCenter = DefaultCenter;
} }
} }
else else
@ -124,13 +133,13 @@ public sealed class CameraController
// NEW: EMA smoothing // NEW: EMA smoothing
// --------------------------------------------------------- // ---------------------------------------------------------
smoothedCenter = new Point2f( smoothedCenter = new Point2f(
smoothedCenter.X * (1 - EmaFactor) + _cameraCenter.X * EmaFactor, smoothedCenter.X * (1 - _emaFactor) + _cameraCenter.X * _emaFactor,
smoothedCenter.Y * (1 - EmaFactor) + _cameraCenter.Y * EmaFactor smoothedCenter.Y * (1 - _emaFactor) + _cameraCenter.Y * _emaFactor
); );
_cameraCenter = new Point2f( _cameraCenter = new Point2f(
_cameraCenter.X + (smoothedCenter.X - _cameraCenter.X) * CameraEasing, _cameraCenter.X + (smoothedCenter.X - _cameraCenter.X) * _cameraEasing,
_cameraCenter.Y + (smoothedCenter.Y - _cameraCenter.Y) * CameraEasing); _cameraCenter.Y + (smoothedCenter.Y - _cameraCenter.Y) * _cameraEasing);
} }
else if (_state == TrackState.LostFreeze) else if (_state == TrackState.LostFreeze)
@ -165,6 +174,7 @@ public sealed class CameraController
y = Math.Clamp(y, 0, _videoHeight - _cropHeight); y = Math.Clamp(y, 0, _videoHeight - _cropHeight);
_roi = new Rect(x, y, _cropWidth, _cropHeight); _roi = new Rect(x, y, _cropWidth, _cropHeight);
_smoothedCenter = smoothedCenter; _smoothedCenter = smoothedCenter;
_objectBox = objectBox; _objectBox = objectBox;
_objectCenter = objectCenter; _objectCenter = objectCenter;

View File

@ -10,6 +10,7 @@ public sealed class CommandLine
public string InputFile { get; private init; } public string InputFile { get; private init; }
public string OutputFolder { get; private init; } public string OutputFolder { get; private init; }
public (int width, int height)? Crop { get; private init; } public (int width, int height)? Crop { get; private init; }
public Point2f? GravitateTo { get; private init; }
public string? Mask { get; private init; } public string? Mask { get; private init; }
public bool Debug { get; private init; } public bool Debug { get; private init; }
public string? Detect { get; private init; } public string? Detect { get; private init; }
@ -19,6 +20,7 @@ public sealed class CommandLine
public bool EstimateOnly { get; private init; } public bool EstimateOnly { get; private init; }
public bool ForceFixed { get; private init; } public bool ForceFixed { get; private init; }
public bool SingleThreaded { get; private init; } public bool SingleThreaded { get; private init; }
public Dictionary<string, string> Parameters { get; } = [];
public bool IsValid => !string.IsNullOrEmpty(InputFile) && !string.IsNullOrEmpty(OutputFolder); public bool IsValid => !string.IsNullOrEmpty(InputFile) && !string.IsNullOrEmpty(OutputFolder);
@ -84,6 +86,11 @@ public sealed class CommandLine
{ {
SingleThreaded = true; SingleThreaded = true;
} }
else if (arg.StartsWith("--gravitate="))
{
var val = arg.Substring("--gravitate=".Length);
GravitateTo = ParseGravitate(val);
}
else if (arg.StartsWith("--duration=")) else if (arg.StartsWith("--duration="))
{ {
var dur = arg.Substring("--duration=".Length); var dur = arg.Substring("--duration=".Length);
@ -94,6 +101,17 @@ public sealed class CommandLine
return; return;
} }
} }
else if (arg.StartsWith("-p:", StringComparison.Ordinal))
{
var spec = arg.Substring("-p:".Length);
if (!TryParseParameter(spec, out var key, out var value))
{
Console.WriteLine($"Invalid -p parameter: {spec}");
return;
}
Parameters[key] = value;
}
else if (arg == "--estimate") else if (arg == "--estimate")
{ {
EstimateOnly = true; EstimateOnly = true;
@ -105,6 +123,63 @@ public sealed class CommandLine
} }
} }
public void Override<T>(ref T member, string name)
{
if (!Parameters.TryGetValue(name, out var raw))
return;
try
{
// Convert.ChangeType handles int, float, double, etc.
var converted = (T)Convert.ChangeType(
raw,
typeof(T),
CultureInfo.InvariantCulture
);
member = converted;
}
catch
{
Console.WriteLine($"Invalid value for parameter '{name}': {raw}");
}
}
private static bool TryParseParameter(string spec, out string key, out string value)
{
key = "";
value = "";
var idx = spec.IndexOf('=');
if (idx <= 0 || idx == spec.Length - 1)
return false;
key = spec.Substring(0, idx).Trim();
value = spec.Substring(idx + 1).Trim();
return key.Length > 0;
}
private static Point2f? ParseGravitate(string value)
{
// Expected format: "<x>:<y>"
var parts = value.Split(':');
if (parts.Length != 2)
return null;
if (!float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var x))
return null;
if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var y))
return null;
// Normalized range check (0.01.0)
if (x < 0f || x > 1f || y < 0f || y > 1f)
return null;
return new Point2f(x, y);
}
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 // Default vertical Full HD for YouTube Shorts
@ -201,6 +276,10 @@ Options:
--detect=<name> Object detector to use for tracking. --detect=<name> Object detector to use for tracking.
Values: face (UltraFace), body (YoloOnnx, default), none (no tracking, just a center) Values: face (UltraFace), body (YoloOnnx, default), none (no tracking, just a center)
--gravitate=<x:y> Gravitate towards a specific point (x, y) in the video frame.
Coordinates are normalized (0.0 to 1.0).
Example: --gravitate=0.2:0.5 (gravitate towards left-center)
--text Display log in plain text. --text Display log in plain text.
--single-thread Run in single-threaded mode (no parallel ffmpeg processes). --single-thread Run in single-threaded mode (no parallel ffmpeg processes).
@ -208,6 +287,15 @@ Options:
--debug Show debug overlay during face tracking. --debug Show debug overlay during face tracking.
-p:<name>=<value> Set a custom parameter for the object detector.
Example: -p:confidence=0.5
Tracking splitter defaults:
DropoutToleranceFrames = 20;
EmaFactor = 0.65;
CameraEasing = 0.03;
LostFreezeFrames = 60;
Passthrough: Passthrough:
Anything after -- is passed directly to ffmpeg. Anything after -- is passed directly to ffmpeg.

View File

@ -16,8 +16,9 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
private readonly bool _plainText; private readonly bool _plainText;
private readonly IObjectDetector _detector; private readonly IObjectDetector _detector;
private readonly CommandLine _cmd;
public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector) public TrackingSplitter(int segmentNo, int cropWidth, int cropHeight, bool debugOverlay, bool plainText, IObjectDetector detector, CommandLine cmd)
: base(segmentNo) : base(segmentNo)
{ {
_segmentNo = segmentNo; _segmentNo = segmentNo;
@ -26,6 +27,7 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
_debugOverlay = debugOverlay; _debugOverlay = debugOverlay;
_plainText = plainText; _plainText = plainText;
_detector = detector; _detector = detector;
_cmd = cmd;
} }
public void Dispose() public void Dispose()
@ -83,7 +85,8 @@ public class TrackingSplitter : LoggingBase, ISegmentProcessor, IDisposable
videoHeight, videoHeight,
originalCropWidth, originalCropWidth,
originalCropHeight, originalCropHeight,
kalman kalman,
_cmd
); );
var startTime = DateTime.UtcNow; var startTime = DateTime.UtcNow;

View File

@ -87,7 +87,7 @@ static class Program
"body" => new YoloOnnxObjectDetector(), "body" => new YoloOnnxObjectDetector(),
_ => throw new InvalidOperationException($"Unknown detector: {detect}") _ => throw new InvalidOperationException($"Unknown detector: {detect}")
}; };
return new TrackingSplitter(i, crop.Value.width, crop.Value.height, debug, cmd.PlainText, detector); return new TrackingSplitter(i, crop.Value.width, crop.Value.height, debug, cmd.PlainText, detector, cmd);
}; };
} }
else else