mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-22 00:22:01 +00:00
Separated CameraController from the main loop
This commit is contained in:
parent
385e1c63e0
commit
7d2ccad070
155
CameraController.cs
Normal file
155
CameraController.cs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
using System;
|
||||||
|
using OpenCvSharp;
|
||||||
|
|
||||||
|
namespace splitter;
|
||||||
|
|
||||||
|
public enum TrackState
|
||||||
|
{
|
||||||
|
Tracking,
|
||||||
|
LostFreeze,
|
||||||
|
LostDrift
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CameraController
|
||||||
|
{
|
||||||
|
private readonly int _videoWidth;
|
||||||
|
private readonly int _videoHeight;
|
||||||
|
private readonly int _cropWidth;
|
||||||
|
private readonly int _cropHeight;
|
||||||
|
private readonly KalmanTracker _kalman;
|
||||||
|
private readonly int _lostFreezeFrames;
|
||||||
|
private readonly float _cameraEasing;
|
||||||
|
|
||||||
|
private int _lostFrames;
|
||||||
|
private Point2f _cameraCenter;
|
||||||
|
private TrackState _state;
|
||||||
|
private Point2f _smoothedCenter;
|
||||||
|
private Rect? _objectBox;
|
||||||
|
private Point2f? _objectCenter;
|
||||||
|
private Rect _roi;
|
||||||
|
|
||||||
|
public CameraController(
|
||||||
|
int videoWidth,
|
||||||
|
int videoHeight,
|
||||||
|
int cropWidth,
|
||||||
|
int cropHeight,
|
||||||
|
KalmanTracker kalman,
|
||||||
|
int lostFreezeFrames,
|
||||||
|
float cameraEasing)
|
||||||
|
{
|
||||||
|
_videoWidth = videoWidth;
|
||||||
|
_videoHeight = videoHeight;
|
||||||
|
_cropWidth = cropWidth;
|
||||||
|
_cropHeight = cropHeight;
|
||||||
|
_kalman = kalman;
|
||||||
|
_lostFreezeFrames = lostFreezeFrames;
|
||||||
|
_cameraEasing = cameraEasing;
|
||||||
|
|
||||||
|
_cameraCenter = new Point2f(videoWidth / 2f, videoHeight / 2f);
|
||||||
|
_state = TrackState.Tracking;
|
||||||
|
|
||||||
|
_kalman.Reset(_cameraCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int LostFrames => _lostFrames;
|
||||||
|
public Point2f CameraCenter => _cameraCenter;
|
||||||
|
public TrackState State => _state;
|
||||||
|
public Point2f SmoothedCenter => _smoothedCenter;
|
||||||
|
public Rect? ObjectBox => _objectBox;
|
||||||
|
public Point2f? ObjectCenter => _objectCenter;
|
||||||
|
public Rect Roi => _roi;
|
||||||
|
|
||||||
|
public void Update((Rect box, Point2f center)? primary)
|
||||||
|
{
|
||||||
|
Rect? objectBox = null;
|
||||||
|
Point2f? objectCenter = null;
|
||||||
|
|
||||||
|
if (primary.HasValue)
|
||||||
|
{
|
||||||
|
objectCenter = primary.Value.center;
|
||||||
|
objectBox = primary.Value.box;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isLost = !objectCenter.HasValue;
|
||||||
|
|
||||||
|
// LOST / REACQUIRE STATE MACHINE
|
||||||
|
if (isLost)
|
||||||
|
{
|
||||||
|
_lostFrames++;
|
||||||
|
|
||||||
|
if (_lostFrames <= _lostFreezeFrames)
|
||||||
|
{
|
||||||
|
// LOST_FREEZE: freeze camera
|
||||||
|
_state = TrackState.LostFreeze;
|
||||||
|
objectCenter = null; // Kalman predicts but camera won't move
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// LOST_DRIFT: drift camera to center
|
||||||
|
_state = TrackState.LostDrift;
|
||||||
|
objectCenter = new Point2f(_videoWidth / 2f, _videoHeight / 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Object reacquired
|
||||||
|
_state = TrackState.Tracking;
|
||||||
|
_lostFrames = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KALMAN + CAMERA UPDATE
|
||||||
|
Point2f smoothedCenter;
|
||||||
|
|
||||||
|
if (_state == TrackState.Tracking)
|
||||||
|
{
|
||||||
|
smoothedCenter = _kalman.Update(objectCenter);
|
||||||
|
|
||||||
|
// first, faster internal easing (as in your original code)
|
||||||
|
float fastEasing = 0.015f;
|
||||||
|
_cameraCenter = new Point2f(
|
||||||
|
_cameraCenter.X + (smoothedCenter.X - _cameraCenter.X) * fastEasing,
|
||||||
|
_cameraCenter.Y + (smoothedCenter.Y - _cameraCenter.Y) * fastEasing);
|
||||||
|
|
||||||
|
// then, external configurable easing
|
||||||
|
_cameraCenter = new Point2f(
|
||||||
|
_cameraCenter.X + (smoothedCenter.X - _cameraCenter.X) * _cameraEasing,
|
||||||
|
_cameraCenter.Y + (smoothedCenter.Y - _cameraCenter.Y) * _cameraEasing);
|
||||||
|
}
|
||||||
|
else if (_state == TrackState.LostFreeze)
|
||||||
|
{
|
||||||
|
// Freeze camera — do nothing
|
||||||
|
smoothedCenter = _kalman.LastMeasurement ?? _cameraCenter;
|
||||||
|
}
|
||||||
|
else // LOST_DRIFT
|
||||||
|
{
|
||||||
|
smoothedCenter = _kalman.Update(objectCenter);
|
||||||
|
|
||||||
|
float driftEasing = 0.01f;
|
||||||
|
var fallbackCenter = new Point2f(_videoWidth / 2f, _videoHeight / 2f);
|
||||||
|
|
||||||
|
_cameraCenter = new Point2f(
|
||||||
|
_cameraCenter.X + (fallbackCenter.X - _cameraCenter.X) * driftEasing,
|
||||||
|
_cameraCenter.Y + (fallbackCenter.Y - _cameraCenter.Y) * driftEasing);
|
||||||
|
}
|
||||||
|
|
||||||
|
var halfW = _cropWidth / 2f;
|
||||||
|
var halfH = _cropHeight / 2f;
|
||||||
|
|
||||||
|
smoothedCenter.X = Math.Clamp(smoothedCenter.X, halfW, _videoWidth - halfW);
|
||||||
|
smoothedCenter.Y = Math.Clamp(smoothedCenter.Y, halfH, _videoHeight - halfH);
|
||||||
|
|
||||||
|
_cameraCenter.X = Math.Clamp(_cameraCenter.X, halfW, _videoWidth - halfW);
|
||||||
|
_cameraCenter.Y = Math.Clamp(_cameraCenter.Y, halfH, _videoHeight - halfH);
|
||||||
|
|
||||||
|
var x = (int)Math.Round(_cameraCenter.X - halfW);
|
||||||
|
var y = (int)Math.Round(_cameraCenter.Y - halfH);
|
||||||
|
|
||||||
|
x = Math.Clamp(x, 0, _videoWidth - _cropWidth);
|
||||||
|
y = Math.Clamp(y, 0, _videoHeight - _cropHeight);
|
||||||
|
|
||||||
|
_roi = new Rect(x, y, _cropWidth, _cropHeight);
|
||||||
|
_smoothedCenter = smoothedCenter;
|
||||||
|
_objectBox = objectBox;
|
||||||
|
_objectCenter = objectCenter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
namespace splitter;
|
namespace splitter;
|
||||||
|
|
||||||
internal sealed class KalmanTracker
|
public sealed class KalmanTracker
|
||||||
{
|
{
|
||||||
// State vector: [x, y, vx, vy]
|
// State vector: [x, y, vx, vy]
|
||||||
private float[] _state = new float[4];
|
private float[] _state = new float[4];
|
||||||
|
|||||||
@ -15,13 +15,6 @@ public class TrackingSplitter(
|
|||||||
private const int LostFreezeFrames = 60; // 2 seconds at 30 FPS
|
private const int LostFreezeFrames = 60; // 2 seconds at 30 FPS
|
||||||
private const float CameraEasing = 0.03f;
|
private const float CameraEasing = 0.03f;
|
||||||
|
|
||||||
private enum TrackState
|
|
||||||
{
|
|
||||||
Tracking,
|
|
||||||
LostFreeze,
|
|
||||||
LostDrift
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TrackAndExtract(
|
public async Task TrackAndExtract(
|
||||||
string srcFileName,
|
string srcFileName,
|
||||||
string destFileName,
|
string destFileName,
|
||||||
@ -49,38 +42,39 @@ public class TrackingSplitter(
|
|||||||
|
|
||||||
Console.WriteLine($"[TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
|
Console.WriteLine($"[TrackingSplitter] skip={skip}, duration={duration}, fps={fps}, totalFrames={totalFrames}");
|
||||||
|
|
||||||
// encoder size depends on mode
|
|
||||||
var encWidth = debugOverlay ? videoWidth : originalCropWidth;
|
var encWidth = debugOverlay ? videoWidth : originalCropWidth;
|
||||||
var encHeight = debugOverlay ? videoHeight : originalCropHeight;
|
var encHeight = debugOverlay ? videoHeight : originalCropHeight;
|
||||||
|
|
||||||
var ffmpeg = StartFfmpegNvenc(
|
var ffmpeg = StartFfmpegNvenc(
|
||||||
srcFileName,
|
srcFileName,
|
||||||
destFileName,
|
destFileName,
|
||||||
encWidth,
|
encWidth,
|
||||||
encHeight,
|
encHeight,
|
||||||
fps,
|
fps,
|
||||||
skip,
|
skip,
|
||||||
passthrough);
|
passthrough);
|
||||||
|
|
||||||
using var stdin = ffmpeg.StandardInput.BaseStream;
|
using var stdin = ffmpeg.StandardInput.BaseStream;
|
||||||
|
|
||||||
// Reusable frame and output mat
|
|
||||||
using var frame = new Mat();
|
using var frame = new Mat();
|
||||||
using var outputBgr = new Mat(encHeight, encWidth, MatType.CV_8UC3);
|
using var outputBgr = new Mat(encHeight, encWidth, MatType.CV_8UC3);
|
||||||
|
|
||||||
// Reusable raw video buffer
|
var frameBytes = encWidth * encHeight * 3;
|
||||||
var frameBytes = encWidth * encHeight * 3;
|
|
||||||
var videoBuffer = new byte[frameBytes];
|
var videoBuffer = new byte[frameBytes];
|
||||||
|
|
||||||
var kalman = new KalmanTracker();
|
var kalman = new KalmanTracker();
|
||||||
kalman.Reset(new Point2f(videoWidth / 2f, videoHeight / 2f));
|
// initial reset is now done inside CameraController
|
||||||
|
|
||||||
var lostFrames = 0;
|
var camera = new CameraController(
|
||||||
var reacquireCounter = 0; // kept for overlay display
|
videoWidth,
|
||||||
|
videoHeight,
|
||||||
|
originalCropWidth,
|
||||||
|
originalCropHeight,
|
||||||
|
kalman,
|
||||||
|
LostFreezeFrames,
|
||||||
|
CameraEasing);
|
||||||
|
|
||||||
var cameraCenter = new Point2f(videoWidth / 2f, videoHeight / 2f);
|
var startTime = DateTime.UtcNow;
|
||||||
var startTime = DateTime.UtcNow;
|
|
||||||
var state = TrackState.Tracking;
|
|
||||||
|
|
||||||
for (var i = 0; i < totalFrames; i++)
|
for (var i = 0; i < totalFrames; i++)
|
||||||
{
|
{
|
||||||
@ -93,109 +87,19 @@ public class TrackingSplitter(
|
|||||||
var objects = detector.DetectAll(frame, videoWidth, videoHeight);
|
var objects = detector.DetectAll(frame, videoWidth, videoHeight);
|
||||||
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
var primary = SelectTrackedObject(objects, kalman.LastMeasurement);
|
||||||
|
|
||||||
if (primary.HasValue)
|
camera.Update(primary);
|
||||||
{
|
|
||||||
objectCenter = primary.Value.center;
|
|
||||||
objectBox = primary.Value.box;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isLost = !objectCenter.HasValue;
|
objectBox = camera.ObjectBox;
|
||||||
|
objectCenter = camera.ObjectCenter;
|
||||||
|
|
||||||
// LOST / REACQUIRE STATE MACHINE
|
var smoothedCenter = camera.SmoothedCenter;
|
||||||
if (isLost)
|
var cameraCenter = camera.CameraCenter;
|
||||||
{
|
var state = camera.State;
|
||||||
lostFrames++;
|
var lostFrames = camera.LostFrames;
|
||||||
|
var roi = camera.Roi;
|
||||||
if (lostFrames <= LostFreezeFrames)
|
|
||||||
{
|
|
||||||
// LOST_FREEZE: freeze camera
|
|
||||||
state = TrackState.LostFreeze;
|
|
||||||
objectCenter = null; // Kalman predicts but camera won't move
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// LOST_DRIFT: drift camera to center
|
|
||||||
state = TrackState.LostDrift;
|
|
||||||
objectCenter = new Point2f(videoWidth / 2f, videoHeight / 2f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Object reacquired
|
|
||||||
state = TrackState.Tracking;
|
|
||||||
lostFrames = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// KALMAN + CAMERA UPDATE
|
|
||||||
Point2f smoothedCenter;
|
|
||||||
|
|
||||||
if (state == TrackState.Tracking)
|
|
||||||
{
|
|
||||||
smoothedCenter = kalman.Update(objectCenter);
|
|
||||||
|
|
||||||
float easing = 0.015f; // faster tracking
|
|
||||||
cameraCenter = new Point2f(
|
|
||||||
cameraCenter.X + (smoothedCenter.X - cameraCenter.X) * easing,
|
|
||||||
cameraCenter.Y + (smoothedCenter.Y - cameraCenter.Y) * easing);
|
|
||||||
}
|
|
||||||
else if (state == TrackState.LostFreeze)
|
|
||||||
{
|
|
||||||
// Freeze camera — do nothing
|
|
||||||
smoothedCenter = kalman.LastMeasurement ?? new Point2f(0, 0);
|
|
||||||
}
|
|
||||||
else // LOST_DRIFT
|
|
||||||
{
|
|
||||||
smoothedCenter = kalman.Update(objectCenter);
|
|
||||||
|
|
||||||
float driftEasing = 0.01f;
|
|
||||||
var fallbackCenter = new Point2f(videoWidth / 2f, videoHeight / 2f);
|
|
||||||
|
|
||||||
cameraCenter = new Point2f(
|
|
||||||
cameraCenter.X + (fallbackCenter.X - cameraCenter.X) * driftEasing,
|
|
||||||
cameraCenter.Y + (fallbackCenter.Y - cameraCenter.Y) * driftEasing);
|
|
||||||
}
|
|
||||||
|
|
||||||
var halfW = originalCropWidth / 2f;
|
|
||||||
var halfH = originalCropHeight / 2f;
|
|
||||||
|
|
||||||
smoothedCenter.X = Math.Clamp(smoothedCenter.X, halfW, videoWidth - halfW);
|
|
||||||
smoothedCenter.Y = Math.Clamp(smoothedCenter.Y, halfH, videoHeight - halfH);
|
|
||||||
|
|
||||||
if (state == TrackState.Tracking)
|
|
||||||
{
|
|
||||||
smoothedCenter = kalman.Update(objectCenter);
|
|
||||||
|
|
||||||
cameraCenter = new Point2f(
|
|
||||||
cameraCenter.X + (smoothedCenter.X - cameraCenter.X) * CameraEasing,
|
|
||||||
cameraCenter.Y + (smoothedCenter.Y - cameraCenter.Y) * CameraEasing);
|
|
||||||
}
|
|
||||||
else if (state == TrackState.LostFreeze)
|
|
||||||
{
|
|
||||||
// Freeze camera — do nothing
|
|
||||||
}
|
|
||||||
else if (state == TrackState.LostDrift)
|
|
||||||
{
|
|
||||||
var fallbackCenter = new Point2f(videoWidth / 2f, videoHeight / 2f);
|
|
||||||
|
|
||||||
cameraCenter = new Point2f(
|
|
||||||
cameraCenter.X + (fallbackCenter.X - cameraCenter.X) * 0.01f,
|
|
||||||
cameraCenter.Y + (fallbackCenter.Y - cameraCenter.Y) * 0.01f);
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraCenter.X = Math.Clamp(cameraCenter.X, halfW, videoWidth - halfW);
|
|
||||||
cameraCenter.Y = Math.Clamp(cameraCenter.Y, halfH, videoHeight - halfH);
|
|
||||||
|
|
||||||
var x = (int)Math.Round(cameraCenter.X - halfW);
|
|
||||||
var y = (int)Math.Round(cameraCenter.Y - halfH);
|
|
||||||
|
|
||||||
x = Math.Clamp(x, 0, videoWidth - originalCropWidth);
|
|
||||||
y = Math.Clamp(y, 0, videoHeight - originalCropHeight);
|
|
||||||
|
|
||||||
var roi = new Rect(x, y, originalCropWidth, originalCropHeight);
|
|
||||||
|
|
||||||
if (debugOverlay)
|
if (debugOverlay)
|
||||||
{
|
{
|
||||||
// overlays always drawn on frame
|
|
||||||
if (objectBox.HasValue)
|
if (objectBox.HasValue)
|
||||||
{
|
{
|
||||||
var fb = objectBox.Value;
|
var fb = objectBox.Value;
|
||||||
@ -213,23 +117,18 @@ public class TrackingSplitter(
|
|||||||
|
|
||||||
DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White);
|
DrawText(frame, $"Faces: {objects.Count}", 20, 40, Scalar.White);
|
||||||
DrawText(frame, $"LostFrames: {lostFrames}", 20, 70, Scalar.White);
|
DrawText(frame, $"LostFrames: {lostFrames}", 20, 70, Scalar.White);
|
||||||
DrawText(frame, $"Reacquire: {reacquireCounter}", 20, 100, Scalar.White);
|
|
||||||
DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White);
|
DrawText(frame, $"Noise: {kalman.CurrentNoise:F3}", 20, 130, Scalar.White);
|
||||||
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
|
DrawText(frame, $"Camera: {cameraCenter.X:F1},{cameraCenter.Y:F1}", 20, 160, Scalar.White);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debugOverlay)
|
if (debugOverlay)
|
||||||
{
|
{
|
||||||
// DEBUG MODE: write FULL FRAME with overlays
|
|
||||||
// Ensure contiguous buffer by copying into preallocated outputBgr
|
|
||||||
frame.CopyTo(outputBgr);
|
frame.CopyTo(outputBgr);
|
||||||
|
|
||||||
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
Marshal.Copy(outputBgr.Data, videoBuffer, 0, frameBytes);
|
||||||
stdin.Write(videoBuffer, 0, frameBytes);
|
stdin.Write(videoBuffer, 0, frameBytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// PRODUCTION MODE: actual crop
|
|
||||||
using var cropped = new Mat(frame, roi);
|
using var cropped = new Mat(frame, roi);
|
||||||
cropped.CopyTo(outputBgr);
|
cropped.CopyTo(outputBgr);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user