From 96465279483387f57c3dc9db07f9212a2c9d3d5c Mon Sep 17 00:00:00 2001 From: unclshura Date: Sun, 21 Jun 2026 12:42:28 +0100 Subject: [PATCH] Unified Mat to Bitmap converter and shared BufferPool. --- Splitter-UI/Program.cs | 2 + Splitter-UI/Services/BufferPool.cs | 59 ++++++++ Splitter-UI/Services/IBufferPool.cs | 6 + Splitter-UI/Services/IMatToBitmapConverter.cs | 9 ++ Splitter-UI/Services/MatToBitmapConverter.cs | 136 ++++++++++++++++++ Splitter-UI/Services/ThumbnailService.cs | 102 +++++-------- Splitter-UI/ViewModels/JobViewModel.cs | 7 + Splitter-UI/Views/PreviewPane.axaml | 20 ++- 8 files changed, 272 insertions(+), 69 deletions(-) create mode 100644 Splitter-UI/Services/BufferPool.cs create mode 100644 Splitter-UI/Services/IBufferPool.cs create mode 100644 Splitter-UI/Services/IMatToBitmapConverter.cs create mode 100644 Splitter-UI/Services/MatToBitmapConverter.cs diff --git a/Splitter-UI/Program.cs b/Splitter-UI/Program.cs index 71a6ffc..2d7633a 100644 --- a/Splitter-UI/Program.cs +++ b/Splitter-UI/Program.cs @@ -36,6 +36,8 @@ internal sealed class Program services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddKeyedSingleton("face", (x,_) => new SingleThreadedDetector(x.GetRequiredService())); services.AddKeyedSingleton("body", (x,_) => new SingleThreadedDetector(x.GetRequiredService())); services.AddKeyedSingleton("none", (x,_) => new SingleThreadedDetector(x.GetRequiredService())); diff --git a/Splitter-UI/Services/BufferPool.cs b/Splitter-UI/Services/BufferPool.cs new file mode 100644 index 0000000..a2b2c57 --- /dev/null +++ b/Splitter-UI/Services/BufferPool.cs @@ -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> _map; + private readonly LinkedList _lru; + + public BufferPool() + { + _capacity = 8; + _map = new Dictionary<(int w, int h), LinkedListNode>(_capacity); + _lru = new LinkedList(); + } + + 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(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; + } +} diff --git a/Splitter-UI/Services/IBufferPool.cs b/Splitter-UI/Services/IBufferPool.cs new file mode 100644 index 0000000..8c28bc0 --- /dev/null +++ b/Splitter-UI/Services/IBufferPool.cs @@ -0,0 +1,6 @@ +namespace Splitter_UI.Services; + +public interface IBufferPool +{ + BufferPool.Entry Get(int w, int h); +} \ No newline at end of file diff --git a/Splitter-UI/Services/IMatToBitmapConverter.cs b/Splitter-UI/Services/IMatToBitmapConverter.cs new file mode 100644 index 0000000..0f2b829 --- /dev/null +++ b/Splitter-UI/Services/IMatToBitmapConverter.cs @@ -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); +} \ No newline at end of file diff --git a/Splitter-UI/Services/MatToBitmapConverter.cs b/Splitter-UI/Services/MatToBitmapConverter.cs new file mode 100644 index 0000000..660758b --- /dev/null +++ b/Splitter-UI/Services/MatToBitmapConverter.cs @@ -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); + } + } +} diff --git a/Splitter-UI/Services/ThumbnailService.cs b/Splitter-UI/Services/ThumbnailService.cs index d934ceb..7ffaa60 100644 --- a/Splitter-UI/Services/ThumbnailService.cs +++ b/Splitter-UI/Services/ThumbnailService.cs @@ -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 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 DecodeFrameAsync(byte [] bgrBuffer, string file, TimeSpan skip, int width, int height, int? rotateDegree) + private static async Task 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); - } - } - } diff --git a/Splitter-UI/ViewModels/JobViewModel.cs b/Splitter-UI/ViewModels/JobViewModel.cs index f1e05e6..f65ca28 100644 --- a/Splitter-UI/ViewModels/JobViewModel.cs +++ b/Splitter-UI/ViewModels/JobViewModel.cs @@ -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; diff --git a/Splitter-UI/Views/PreviewPane.axaml b/Splitter-UI/Views/PreviewPane.axaml index 440cd5c..b52fef0 100644 --- a/Splitter-UI/Views/PreviewPane.axaml +++ b/Splitter-UI/Views/PreviewPane.axaml @@ -8,7 +8,7 @@ x:DataType="vm:PreviewPaneViewModel"> - + + + + + +