mirror of
https://github.com/unclshura/splitter.git
synced 2026-06-21 16:12:01 +00:00
Unified Mat to Bitmap converter and shared BufferPool.
This commit is contained in:
parent
5d9530382d
commit
9646527948
@ -36,6 +36,8 @@ internal sealed class Program
|
|||||||
services.AddSingleton<YoloV10ObjectDetector>();
|
services.AddSingleton<YoloV10ObjectDetector>();
|
||||||
services.AddSingleton<OSNetEmbeddingExtractor>();
|
services.AddSingleton<OSNetEmbeddingExtractor>();
|
||||||
services.AddSingleton<IObjectTracker, ObjectTracker>();
|
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>("face", (x,_) => new SingleThreadedDetector<UltraFaceDetector>(x.GetRequiredService<UltraFaceDetector>()));
|
||||||
services.AddKeyedSingleton<IObjectDetector>("body", (x,_) => new SingleThreadedDetector<YoloV10ObjectDetector>(x.GetRequiredService<YoloV10ObjectDetector>()));
|
services.AddKeyedSingleton<IObjectDetector>("body", (x,_) => new SingleThreadedDetector<YoloV10ObjectDetector>(x.GetRequiredService<YoloV10ObjectDetector>()));
|
||||||
services.AddKeyedSingleton<IObjectDetector>("none", (x,_) => new SingleThreadedDetector<DummyDetector>(x.GetRequiredService<DummyDetector>()));
|
services.AddKeyedSingleton<IObjectDetector>("none", (x,_) => new SingleThreadedDetector<DummyDetector>(x.GetRequiredService<DummyDetector>()));
|
||||||
|
|||||||
59
Splitter-UI/Services/BufferPool.cs
Normal file
59
Splitter-UI/Services/BufferPool.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Splitter-UI/Services/IBufferPool.cs
Normal file
6
Splitter-UI/Services/IBufferPool.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
|
public interface IBufferPool
|
||||||
|
{
|
||||||
|
BufferPool.Entry Get(int w, int h);
|
||||||
|
}
|
||||||
9
Splitter-UI/Services/IMatToBitmapConverter.cs
Normal file
9
Splitter-UI/Services/IMatToBitmapConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
136
Splitter-UI/Services/MatToBitmapConverter.cs
Normal file
136
Splitter-UI/Services/MatToBitmapConverter.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform;
|
|
||||||
|
|
||||||
namespace Splitter_UI.Services;
|
namespace Splitter_UI.Services;
|
||||||
|
|
||||||
@ -10,17 +8,25 @@ public sealed class ThumbnailService : IThumbnailService
|
|||||||
public const int ThumbWidth = 160;
|
public const int ThumbWidth = 160;
|
||||||
public const int ThumbHeight = 90;
|
public const int ThumbHeight = 90;
|
||||||
|
|
||||||
private readonly byte [] _bgrBuffer = new byte[ThumbWidth * ThumbHeight * 3];
|
private readonly IMatToBitmapConverter _converter;
|
||||||
private readonly byte [] _bgraBuffer = new byte[ThumbWidth * ThumbHeight * 4];
|
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(
|
public async Task<Bitmap?> CreateThumbnailAsync(
|
||||||
string file,
|
string file,
|
||||||
VideoInfo probe,
|
VideoInfo probe,
|
||||||
TimeSpan? skip = null,
|
TimeSpan? skip = null,
|
||||||
int? width = null,
|
int? width = null,
|
||||||
int? height = null,
|
int? height = null,
|
||||||
int? rotateDegree = null)
|
int? rotateDegree = null)
|
||||||
{
|
{
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync();
|
||||||
@ -33,7 +39,7 @@ public sealed class ThumbnailService : IThumbnailService
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
rotateDegree
|
rotateDegree
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -49,36 +55,37 @@ public sealed class ThumbnailService : IThumbnailService
|
|||||||
int? height = null,
|
int? height = null,
|
||||||
int? rotateDegree = null)
|
int? rotateDegree = null)
|
||||||
{
|
{
|
||||||
width ??= ThumbWidth;
|
width ??= ThumbWidth;
|
||||||
height ??= ThumbHeight;
|
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 =
|
var ok = await DecodeFrameAsync(
|
||||||
width.Value == ThumbWidth &&
|
entry.Bgr,
|
||||||
height.Value == ThumbHeight;
|
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)
|
if (!ok)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Convert BGR24 → BGRA32
|
return _converter.Convert(entry.Bgr, width.Value, height.Value);
|
||||||
ConvertBgrToBgra(bgrBuffer, bgraBuffer, width.Value, height.Value);
|
|
||||||
|
|
||||||
// Create Avalonia Bitmap
|
|
||||||
return CreateBitmap(bgraBuffer, width.Value, height.Value, rotateDegree == 90 || rotateDegree == 270);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
var rotationStr = TrackingSplitter.GetRorationArg(rotateDegree);
|
||||||
|
|
||||||
// ffmpeg command: decode one frame, resize, output raw BGR24
|
|
||||||
var args =
|
var args =
|
||||||
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
|
$"-ss {skip.TotalSeconds} -t 0.1 -i \"{file}\" " +
|
||||||
"-an -sn " +
|
"-an -sn " +
|
||||||
@ -123,45 +130,4 @@ public sealed class ThumbnailService : IThumbnailService
|
|||||||
{
|
{
|
||||||
try { p.Kill(); } catch { }
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,6 +53,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
|
|
||||||
public IRelayCommand StepForwardCommand { get; }
|
public IRelayCommand StepForwardCommand { get; }
|
||||||
public IRelayCommand StepBackwardCommand { get; }
|
public IRelayCommand StepBackwardCommand { get; }
|
||||||
|
public IRelayCommand PlayPreviewCommand { get; }
|
||||||
|
|
||||||
private readonly IThumbnailService _thumbnails;
|
private readonly IThumbnailService _thumbnails;
|
||||||
private readonly DispatcherTimer _debounceTimer;
|
private readonly DispatcherTimer _debounceTimer;
|
||||||
@ -322,6 +323,7 @@ public partial class JobViewModel : ObservableObject
|
|||||||
|
|
||||||
StepForwardCommand = new RelayCommand(StepForward);
|
StepForwardCommand = new RelayCommand(StepForward);
|
||||||
StepBackwardCommand = new RelayCommand(StepBackward);
|
StepBackwardCommand = new RelayCommand(StepBackward);
|
||||||
|
PlayPreviewCommand = new RelayCommand(PlayPreview);
|
||||||
|
|
||||||
_debounceTimer = new DispatcherTimer
|
_debounceTimer = new DispatcherTimer
|
||||||
{
|
{
|
||||||
@ -488,6 +490,11 @@ public partial class JobViewModel : ObservableObject
|
|||||||
SliderLiveValue = Segments[current - 1].Start;
|
SliderLiveValue = Segments[current - 1].Start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PlayPreview()
|
||||||
|
{
|
||||||
|
// Implementation for playing preview
|
||||||
|
}
|
||||||
|
|
||||||
private int GetCurrentSegment()
|
private int GetCurrentSegment()
|
||||||
{
|
{
|
||||||
double pos = SliderLiveValue;
|
double pos = SliderLiveValue;
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
x:DataType="vm:PreviewPaneViewModel">
|
x:DataType="vm:PreviewPaneViewModel">
|
||||||
|
|
||||||
<Border Background="#202020" Padding="10">
|
<Border Background="#202020" Padding="10">
|
||||||
<Grid RowDefinitions="*,Auto">
|
<Grid RowDefinitions="*,Auto,Auto">
|
||||||
|
|
||||||
<local:PreviewCanvas
|
<local:PreviewCanvas
|
||||||
Grid.Row="0"
|
Grid.Row="0"
|
||||||
@ -21,6 +21,24 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Grid Grid.Row="1"
|
<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=""
|
||||||
|
FontSize="12"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2"
|
||||||
ColumnDefinitions="Auto,*,Auto"
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
Margin="0,10,0,0">
|
Margin="0,10,0,0">
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user