splitter/splitter-cli/probe/FrameRotationDetector.cs

101 lines
2.5 KiB
C#

namespace splitter.probe;
public sealed class FrameRotationDetector
{
private readonly Mat _gray;
private readonly Mat _gx;
private readonly Mat _gy;
private readonly Mat _mag;
private readonly Mat _angle;
private readonly float[] _hist;
private readonly int _w;
private readonly int _h;
private readonly int _bins;
public FrameRotationDetector(int width = 320, int height = 180, int bins = 36)
{
_w = width;
_h = height;
_bins = bins;
_gray = new Mat(height, width, MatType.CV_8UC1);
_gx = new Mat(height, width, MatType.CV_32F);
_gy = new Mat(height, width, MatType.CV_32F);
_mag = new Mat(height, width, MatType.CV_32F);
_angle = new Mat(height, width, MatType.CV_32F);
_hist = new float[bins]; // allocated once
}
public int GetRotation(Mat frame)
{
// 1. Grayscale
Cv2.CvtColor(frame, _gray, ColorConversionCodes.BGR2GRAY);
// 2. Sobel
Cv2.Sobel(_gray, _gx, MatType.CV_32F, 1, 0, 3);
Cv2.Sobel(_gray, _gy, MatType.CV_32F, 0, 1, 3);
// 3. Magnitude + angle
Cv2.CartToPolar(_gx, _gy, _mag, _angle, angleInDegrees: true);
// 4. Clear histogram
for (var i = 0; i < _bins; i++)
_hist[i] = 0;
var binSize = 180f / _bins;
unsafe
{
var anglePtr = (float*)_angle.Data;
var magPtr = (float*)_mag.Data;
var total = _w * _h;
for (var i = 0; i < total; i++)
{
var m = magPtr[i];
if (m < 5f) continue; // ignore weak gradients
var a = anglePtr[i];
if (a < 0) a += 360f;
a = a % 180f;
var bin = (int)(a / binSize);
if (bin < 0) bin = 0;
if (bin >= _bins) bin = _bins - 1;
_hist[bin] += m;
}
}
// 5. Energy around 0° vs 90°
float e0 = 0, e90 = 0;
var window = 3;
var bin0 = 0;
var bin90 = _bins / 2;
for (var i = -window; i <= window; i++)
{
e0 += _hist[Wrap(bin0 + i)];
e90 += _hist[Wrap(bin90 + i)];
}
// 6. Decide upright vs sideways
if (e90 > e0 * 1.6f)
return 90; // sideways
return 0; // upright (concert default)
}
private int Wrap(int b)
{
if (b < 0) return b + _bins;
if (b >= _bins) return b - _bins;
return b;
}
}