diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml b/Wpf_AiSportsMicrospace/MainWindow.xaml index e3d086b..8bbde93 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml @@ -18,7 +18,6 @@ DisplayMemberPath="Name" SelectedValuePath="Key" SelectionChanged="sportList_SelectionChanged" d:IsHidden="True"/> - diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs index 81f60a8..76e710a 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs @@ -1,17 +1,12 @@ using Emgu.CV.Reg; +using SkiaSharp; using System.Diagnostics; +using System.Drawing; using System.IO; using System.Net.NetworkInformation; -using System.Text; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using Wpf_AiSportsMicrospace.Common; using Yztob.AiSports.Common; using Yztob.AiSports.Inferences.Abstractions; @@ -35,6 +30,7 @@ public partial class MainWindow : Window private readonly List _sports; private SportBase _sport; private readonly SportDetectionQueue _detectQueue; + private RTSPPreviewRenderer _rtspRenderer; #endregion @@ -54,6 +50,13 @@ public partial class MainWindow : Window this.sportList.ItemsSource = _sports; this.sportList.SelectedIndex = 0; this.OnSelectedSport(); + + // 确保 Canvas 已经加载完成 + _rtspRenderer = new RTSPPreviewRenderer(rtspPreviewCanvas) + { + Adaptive = true + }; + } private void sportList_SelectionChanged(object sender, SelectionChangedEventArgs e) @@ -136,9 +139,22 @@ public partial class MainWindow : Window private async void OnFrameExtracted(VideoFrame frame) { //获得帧二进制流,保存图像、人体识别竺 - var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray(); + var buffer = frame.GetImageBuffer(Yztob.AiSports.Sensors.Things.ImageFormat.Jpeg).ToArray(); await File.WriteAllBytesAsync($"./temps/{frame.Number}.jpg", buffer); + //// 2️⃣ 转换成 BGRA32 + //int width, height; + //var bgraBuffer = ConvertJpegToBGRA32(buffer, out width, out height); + + //// 3️⃣ 更新 WPF Canvas + //Application.Current.Dispatcher.Invoke(() => + //{ + // _rtspRenderer.UpdateFrame(bgraBuffer, width, height); + //}); + //_ = Task.Run(async () => + //{ + // await HumanPredictingAsync(frame.Number, buffer); + //}); //可以进一步进行人体识别等 var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number)); @@ -163,4 +179,50 @@ public partial class MainWindow : Window //VoiceBroadcast.PlayTick(); } + private byte[] ConvertJpegToBGRA32(byte[] jpegBuffer, out int width, out int height) + { + using var ms = new MemoryStream(jpegBuffer); + using var bmp = new Bitmap(ms); + + width = bmp.Width; + height = bmp.Height; + + var rect = new Rectangle(0, 0, width, height); + var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + int bytes = Math.Abs(bmpData.Stride) * height; + byte[] bgra = new byte[bytes]; + System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, bgra, 0, bytes); + + bmp.UnlockBits(bmpData); + return bgra; + } + private async Task HumanPredictingAsync(long frameNumber, byte[] buffer) + { + var humanResultTask = Task.Run(() => _humanPredictor.Predicting(buffer, frameNumber)); + var objectsTask = _sport.Equipment + ? Task.Run(() => _objectDetector.Detecting(buffer)) + : Task.FromResult(new List()); + + await Task.WhenAll(humanResultTask, objectsTask); + + var humanResult = humanResultTask.Result; + var objects = objectsTask.Result; + + if (_sport.MeasureApparatus != null) + objects.AddRange(_sport.MeasureApparatus.Apparatuses); + + var human = humanResult?.Humans?.FirstOrDefault(); + _detectQueue.Enqueue(frameNumber, human, objects); + + // 更新渲染器属性(UI线程) + Application.Current.Dispatcher.Invoke(() => + { + //_humanGraphicsRenderer.ScaleRatio = _rtspRenderer.ScaleRatio; + //_humanGraphicsRenderer.OffsetX = _rtspRenderer.OffsetX; + //_humanGraphicsRenderer.OffsetY = _rtspRenderer.OffsetY; + _humanGraphicsRenderer.Humans = humanResult?.Humans; + _humanGraphicsRenderer.Objects = objects; + }); + } } \ No newline at end of file diff --git a/Wpf_AiSportsMicrospace/RTSPPreview.cs b/Wpf_AiSportsMicrospace/RTSPPreview.cs new file mode 100644 index 0000000..5372c00 --- /dev/null +++ b/Wpf_AiSportsMicrospace/RTSPPreview.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Media.Imaging; +using System.Windows.Media; +using System.Windows; +using Yztob.AiSports.Sensors.Things; + +namespace Wpf_AiSportsMicrospace +{ + public class RTSPPreview : UserControl, IDisposable + { + private Image _imageControl; + private WriteableBitmap _bitmap; + private bool _isPlaying = false; + + public RTSPPreview() + { + _imageControl = new Image(); + this.Content = _imageControl; + this.Loaded += RTSPPreview_Loaded; + } + + private void RTSPPreview_Loaded(object sender, RoutedEventArgs e) + { + if (Adaptive) + { + _imageControl.Stretch = Stretch.Uniform; + } + else + { + _imageControl.Stretch = Stretch.Fill; + } + } + + #region 属性 + + [Browsable(true)] + [DefaultValue(true)] + [Description("是否自应用大小,保持同比缩放。")] + public bool Adaptive { get; set; } = true; + + public string Host { get; set; } + public uint Port { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + + public Action OnExtracted { get; set; } + public Action OnFrameProcessing { get; set; } + + public bool IsPlaying => _isPlaying; + + public float OffsetX { get; private set; } + public float OffsetY { get; private set; } + public float ScaleRatio { get; private set; } + + #endregion + + #region 播放/停止/保存 + + public void Play() + { + _isPlaying = true; + // TODO: RTSP 播放初始化 + // 可使用 FFmpeg.AutoGen 或 LibVLCSharp + } + + public void Stop() + { + _isPlaying = false; + // TODO: 停止播放 + } + + public bool SaveFrameToJpeg(string path) + { + if (_bitmap == null) return false; + + try + { + BitmapEncoder encoder = new JpegBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(_bitmap)); + + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write)) + { + encoder.Save(fs); + } + return true; + } + catch + { + return false; + } + } + + #endregion + + #region 更新帧 + + public void UpdateFrame(byte[] pixelData, int width, int height) + { + // 假设 pixelData 为 BGRA32 + if (_bitmap == null || _bitmap.PixelWidth != width || _bitmap.PixelHeight != height) + { + _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null); + _imageControl.Source = _bitmap; + } + + _bitmap.Lock(); + _bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixelData, width * 4, 0); + _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height)); + _bitmap.Unlock(); + + OnFrameProcessing?.Invoke(new VideoFrameWithBuffer + { + Width = width, + Height = height, + Buffer = pixelData + }); + } + + #endregion + + public void Dispose() + { + Stop(); + _bitmap = null; + } + } + + // 数据类示例 + + public class VideoFrameWithBuffer + { + public int Width; + public int Height; + public byte[] Buffer; + } +} diff --git a/Wpf_AiSportsMicrospace/RTSPPreviewRenderer.cs b/Wpf_AiSportsMicrospace/RTSPPreviewRenderer.cs new file mode 100644 index 0000000..ac2c86a --- /dev/null +++ b/Wpf_AiSportsMicrospace/RTSPPreviewRenderer.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Linq; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; + +public class RTSPPreviewRenderer : IDisposable +{ + private Canvas _canvas; + private Image _imageControl; + private WriteableBitmap _bitmap; + public bool Adaptive { get; set; } = true; + + public RTSPPreviewRenderer(Canvas canvas) + { + _canvas = canvas ?? throw new ArgumentNullException(nameof(canvas)); + + _imageControl = new Image(); + _canvas.Children.Add(_imageControl); + + Canvas.SetLeft(_imageControl, 0); + Canvas.SetTop(_imageControl, 0); + } + + /// 更新帧到 Canvas + public void UpdateFrame(byte[] pixelData, int width, int height) + { + if (_bitmap == null || _bitmap.PixelWidth != width || _bitmap.PixelHeight != height) + { + _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null); + _imageControl.Source = _bitmap; + } + + _bitmap.Lock(); + _bitmap.WritePixels(new System.Windows.Int32Rect(0, 0, width, height), pixelData, width * 4, 0); + _bitmap.AddDirtyRect(new System.Windows.Int32Rect(0, 0, width, height)); + _bitmap.Unlock(); + + // 自动适应 Canvas + if (Adaptive) + { + double scaleX = _canvas.ActualWidth / width; + double scaleY = _canvas.ActualHeight / height; + double scale = Math.Min(scaleX, scaleY); + + _imageControl.Width = width * scale; + _imageControl.Height = height * scale; + + Canvas.SetLeft(_imageControl, (_canvas.ActualWidth - _imageControl.Width) / 2); + Canvas.SetTop(_imageControl, (_canvas.ActualHeight - _imageControl.Height) / 2); + } + else + { + _imageControl.Width = _canvas.ActualWidth; + _imageControl.Height = _canvas.ActualHeight; + Canvas.SetLeft(_imageControl, 0); + Canvas.SetTop(_imageControl, 0); + } + } + + public void Dispose() + { + _bitmap = null; + } +} diff --git a/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj b/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj index 7a5ec6d..0291eca 100644 --- a/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj +++ b/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj @@ -21,6 +21,7 @@ +