Unified Mat to Bitmap converter and shared BufferPool.

This commit is contained in:
Alexander Shabarshov 2026-06-21 12:42:28 +01:00
parent 5d9530382d
commit 9646527948
8 changed files with 272 additions and 69 deletions

View File

@ -36,6 +36,8 @@ internal sealed class Program
services.AddSingleton<YoloV10ObjectDetector>();
services.AddSingleton<OSNetEmbeddingExtractor>();
services.AddSingleton<IObjectTracker, ObjectTracker>();
services.AddSingleton<IBufferPool, BufferPool>();
services.AddSingleton<IMatToBitmapConverter, MatToBitmapConverter>();
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>()));

View File

@ -0,0 +1,59 @@
namespace Splitter_UI.Services;
public sealed class BufferPool : IBufferPool
{
private readonly int _capacity;
public sealed class Entry
{
public readonly int Width;
public readonly int Height;
public readonly byte[] Bgr;
public readonly byte[] Bgra;
public Entry(int w, int h)
{
Width = w;
Height = h;
Bgr = new byte[w * h * 3];
Bgra = new byte[w * h * 4];
}
}
private readonly Dictionary<(int w, int h), LinkedListNode<Entry>> _map;
private readonly LinkedList<Entry> _lru;
public BufferPool()
{
_capacity = 8;
_map = new Dictionary<(int w, int h), LinkedListNode<Entry>>(_capacity);
_lru = new LinkedList<Entry>();
}
public Entry Get(int w, int h)
{
var key = (w, h);
if (_map.TryGetValue(key, out var node))
{
_lru.Remove(node);
_lru.AddLast(node);
return node.Value;
}
var created = new Entry(w, h);
var newNode = new LinkedListNode<Entry>(created);
_lru.AddLast(newNode);
_map[key] = newNode;
if (_lru.Count > _capacity)
{
var first = _lru.First!;
_lru.RemoveFirst();
_map.Remove((first.Value.Width, first.Value.Height));
}
return created;
}
}

View File

@ -0,0 +1,6 @@
namespace Splitter_UI.Services;
public interface IBufferPool
{
BufferPool.Entry Get(int w, int h);
}

View File

@ -0,0 +1,9 @@
using Avalonia.Media.Imaging;
namespace Splitter_UI.Services;
public interface IMatToBitmapConverter
{
Bitmap Convert(Mat mat, Bitmap? existing = null);
Bitmap Convert(byte[] bgr, int width, int height, Bitmap? existing = null);
}

View File

@ -0,0 +1,136 @@
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Splitter_UI.Services;
public sealed class MatToBitmapConverter(IBufferPool _pool) : IMatToBitmapConverter
{
private readonly object _sync = new();
public Bitmap Convert(Mat mat, Bitmap? existing = null)
{
if (mat.Empty())
throw new ArgumentException("Mat is empty.", nameof(mat));
var w = mat.Width;
var h = mat.Height;
var channels = mat.Channels();
if (channels != 3 && channels != 4)
throw new NotSupportedException($"Only 3 or 4 channel Mats are supported. Got {channels}.");
lock (_sync)
{
var entry = _pool.Get(w, h);
var src = mat;
if (!src.IsContinuous())
src = src.Clone();
unsafe
{
var srcPtr = (byte*)src.DataPointer;
var totalBytes = w * h * channels;
if (channels == 3)
{
fixed (byte* dstBgr = entry.Bgr)
{
Buffer.MemoryCopy(srcPtr, dstBgr, entry.Bgr.Length, totalBytes);
}
ConvertBgrToBgra(entry.Bgr, entry.Bgra, w, h);
}
else
{
fixed (byte* dstBgra = entry.Bgra)
{
Buffer.MemoryCopy(srcPtr, dstBgra, entry.Bgra.Length, totalBytes);
}
}
}
if (existing is WriteableBitmap wb &&
wb.PixelSize.Width == w &&
wb.PixelSize.Height == h)
{
UpdateWriteableBitmap(wb, entry.Bgra, w, h);
return wb;
}
return CreateBitmap(entry.Bgra, w, h);
}
}
public Bitmap Convert(byte[] bgr, int width, int height, Bitmap? existing = null)
{
var entry = _pool.Get(width, height);
ConvertBgrToBgra(bgr, entry.Bgra, width, height);
if (existing is WriteableBitmap wb &&
wb.PixelSize.Width == width &&
wb.PixelSize.Height == height)
{
UpdateWriteableBitmap(wb, entry.Bgra, width, height);
return wb;
}
return CreateBitmap(entry.Bgra, width, height);
}
private static void ConvertBgrToBgra(byte[] bgr, byte[] bgra, int width, int height)
{
var si = 0;
var di = 0;
var totalPixels = width * height;
for (var i = 0; i < totalPixels; i++)
{
bgra[di + 0] = bgr[si + 0];
bgra[di + 1] = bgr[si + 1];
bgra[di + 2] = bgr[si + 2];
bgra[di + 3] = 255;
si += 3;
di += 4;
}
}
private static unsafe void UpdateWriteableBitmap(WriteableBitmap wb, byte[] bgra, int width, int height)
{
using var fb = wb.Lock();
var dstPtr = (byte*)fb.Address;
var dstStride = fb.RowBytes;
var srcStride = width * 4;
fixed (byte* srcPtr = bgra)
{
for (var y = 0; y < height; y++)
{
var srcRow = srcPtr + y * srcStride;
var dstRow = dstPtr + y * dstStride;
Buffer.MemoryCopy(srcRow, dstRow, dstStride, srcStride);
}
}
}
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height)
{
var stride = width * 4;
fixed (byte* p = bgra)
{
return new WriteableBitmap(
PixelFormat.Bgra8888,
AlphaFormat.Premul,
(nint)p,
new PixelSize(width, height),
new Vector(96, 96),
stride);
}
}
}

View File

@ -1,7 +1,5 @@
using System.Diagnostics;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Splitter_UI.Services;
@ -10,17 +8,25 @@ public sealed class ThumbnailService : IThumbnailService
public const int ThumbWidth = 160;
public const int ThumbHeight = 90;
private readonly byte [] _bgrBuffer = new byte[ThumbWidth * ThumbHeight * 3];
private readonly byte [] _bgraBuffer = new byte[ThumbWidth * ThumbHeight * 4];
private readonly IMatToBitmapConverter _converter;
private readonly IBufferPool _pool;
public SemaphoreSlim _lock = new(1,1);
private SemaphoreSlim _lock = new(1,1);
public ThumbnailService(
IMatToBitmapConverter converter,
IBufferPool pool)
{
_converter = converter;
_pool = pool;
}
public async Task<Bitmap?> CreateThumbnailAsync(
string file,
VideoInfo probe,
TimeSpan? skip = null,
int? width = null,
int? height = null,
TimeSpan? skip = null,
int? width = null,
int? height = null,
int? rotateDegree = null)
{
await _lock.WaitAsync();
@ -33,7 +39,7 @@ public sealed class ThumbnailService : IThumbnailService
width,
height,
rotateDegree
);
);
}
finally
{
@ -49,36 +55,37 @@ public sealed class ThumbnailService : IThumbnailService
int? height = null,
int? rotateDegree = null)
{
width ??= ThumbWidth;
width ??= ThumbWidth;
height ??= ThumbHeight;
skip ??= TimeSpan.Zero;
skip ??= TimeSpan.Zero;
// buffer for BGR24 → 3 bytes per pixel
var entry = _pool.Get(width.Value, height.Value);
var canUseStaticBuffers =
width.Value == ThumbWidth &&
height.Value == ThumbHeight;
var ok = await DecodeFrameAsync(
entry.Bgr,
file,
skip.Value,
width.Value,
height.Value,
rotateDegree
);
var bgrBuffer = canUseStaticBuffers ? _bgrBuffer : new byte[width.Value * height.Value * 3];
var bgraBuffer = canUseStaticBuffers ? _bgraBuffer : new byte[width.Value * height.Value * 4];
// Decode a single frame using ffmpeg → raw BGR24 into _bgrBuffer
var ok = await DecodeFrameAsync(bgrBuffer, file, skip.Value, width.Value, height.Value, rotateDegree);
if (!ok)
return null;
// Convert BGR24 → BGRA32
ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value);
// Create Avalonia Bitmap
return CreateBitmap(bgraBuffer, width.Value, height.Value, rotateDegree == 90 || rotateDegree == 270);
return _converter.Convert(entry.Bgr, width.Value, height.Value);
}
private static async Task<bool> DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height, int? rotateDegree)
private static async Task<bool> DecodeFrameAsync(
byte[] bgrBuffer,
string file,
TimeSpan skip,
int width,
int height,
int? rotateDegree)
{
var rotationStr = TrackingSplitter.GetRorationArg(rotateDegree);
// ffmpeg command: decode one frame, resize, output raw BGR24
var args =
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
"-an -sn " +
@ -123,45 +130,4 @@ public sealed class ThumbnailService : IThumbnailService
{
try { p.Kill(); } catch { }
}
private static void ConvertBgrToBgra(byte[] bgr, byte[] bgra, int width, int height)
{
var si = 0;
var di = 0;
var totalPixels = width * height;
for (var i = 0; i < totalPixels; i++)
{
bgra[di + 0] = bgr[si + 0]; // B
bgra[di + 1] = bgr[si + 1]; // G
bgra[di + 2] = bgr[si + 2]; // R
bgra[di + 3] = 255; // A
si += 3;
di += 4;
}
}
private static unsafe Bitmap CreateBitmap(byte[] bgra, int width, int height, bool isRotated)
{
if (isRotated)
{
(height, width) = (width, height);
}
var stride = width * 4;
fixed (byte* p = bgra)
{
return new Bitmap(
PixelFormat.Bgra8888,
AlphaFormat.Premul,
(nint)p,
new PixelSize(width, height),
new Vector(96, 96),
stride);
}
}
}

View File

@ -53,6 +53,7 @@ public partial class JobViewModel : ObservableObject
public IRelayCommand StepForwardCommand { get; }
public IRelayCommand StepBackwardCommand { get; }
public IRelayCommand PlayPreviewCommand { get; }
private readonly IThumbnailService _thumbnails;
private readonly DispatcherTimer _debounceTimer;
@ -322,6 +323,7 @@ public partial class JobViewModel : ObservableObject
StepForwardCommand = new RelayCommand(StepForward);
StepBackwardCommand = new RelayCommand(StepBackward);
PlayPreviewCommand = new RelayCommand(PlayPreview);
_debounceTimer = new DispatcherTimer
{
@ -488,6 +490,11 @@ public partial class JobViewModel : ObservableObject
SliderLiveValue = Segments[current - 1].Start;
}
private void PlayPreview()
{
// Implementation for playing preview
}
private int GetCurrentSegment()
{
double pos = SliderLiveValue;

View File

@ -8,7 +8,7 @@
x:DataType="vm:PreviewPaneViewModel">
<Border Background="#202020" Padding="10">
<Grid RowDefinitions="*,Auto">
<Grid RowDefinitions="*,Auto,Auto">
<local:PreviewCanvas
Grid.Row="0"
@ -21,6 +21,24 @@
/>
<Grid Grid.Row="1"
ColumnDefinitions="Auto"
Margin="0,10,0,0">
<Button Grid.Column="1"
HorizontalAlignment="Left"
Width="24" Height="24"
Padding="0"
Margin="0,0,5,0"
Command="{Binding Selected.PlayPreviewCommand}">
<TextBlock FontFamily="{StaticResource FontAwesome}"
Text="&#xf04b;"
FontSize="12"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Button>
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="Auto,*,Auto"
Margin="0,10,0,0">