diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml b/Wpf_AiSportsMicrospace/MainWindow.xaml index d00a1bd..30dd2fe 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml @@ -4,6 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Wpf_AiSportsMicrospace" + Loaded="Window_Loaded" Closing="Window_Closing" mc:Ignorable="d" Title="AI运动微空间" Height="700" Width="1200"> @@ -31,9 +32,9 @@ - - - - + + + + diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs index 7873411..3f40171 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs @@ -1,15 +1,19 @@ using Emgu.CV.Reg; +using SharpDX.Direct3D9; using SkiaSharp; using System.Diagnostics; -using System.Drawing; +using System.Drawing.Imaging; using System.IO; using System.Net.NetworkInformation; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; +using System.Windows.Interop; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Windows.Threading; using Wpf_AiSportsMicrospace.Common; -using Yztob.AiSports.Common; using Yztob.AiSports.Inferences.Abstractions; using Yztob.AiSports.Inferences.Things; using Yztob.AiSports.Postures.Sports; @@ -31,7 +35,9 @@ public partial class MainWindow : Window private readonly List _sports; private SportBase _sport; private readonly SportDetectionQueue _detectQueue; - private RTSPPreviewRenderer _rtspRenderer; + + private WriteableBitmap _videoBitmap; + private int _lastFrameNumber = -1; #endregion @@ -51,13 +57,28 @@ public partial class MainWindow : Window this.sportList.ItemsSource = _sports; this.sportList.SelectedIndex = 0; this.OnSelectedSport(); - - // 确保 Canvas 已经加载完成 - _rtspRenderer = new RTSPPreviewRenderer(rtspPreviewCanvas) + } + private void Window_Loaded(object sender, RoutedEventArgs e) + { + Application.Current.Dispatcher.InvokeAsync(() => { - Adaptive = true - }; + DrawJumpRope3DPointsWithGlow(); + }, System.Windows.Threading.DispatcherPriority.Loaded); + videoImage.SizeChanged += (s, ev) => + { + DrawJumpRope3DPointsWithGlow(); + }; + } + + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + base.OnClosed(e); + + // 释放 WriteableBitmap + _videoBitmap = null; + videoImage.Source = null; + overlayCanvas = null; } private void sportList_SelectionChanged(object sender, SelectionChangedEventArgs e) @@ -138,28 +159,62 @@ public partial class MainWindow : Window } private async void OnFrameExtracted(VideoFrame frame) { + if (frame == null) return; + + // 跳帧:每秒只渲染 10~15 帧,降低 CPU 占用 + if (_lastFrameNumber != -1 && frame.Number - _lastFrameNumber < 2) return; + _lastFrameNumber = (int)frame.Number; + //获得帧二进制流,保存图像、人体识别竺 var buffer = frame.GetImageBuffer(Yztob.AiSports.Sensors.Things.ImageFormat.Jpeg).ToArray(); - await File.WriteAllBytesAsync($"./temps/{frame.Number}.jpg", buffer); + //await File.WriteAllBytesAsync($"./temps/{frame.Number}.jpg", buffer); - //// 2️⃣ 转换成 BGRA32 - //int width, height; - //var bgraBuffer = ConvertJpegToBGRA32(buffer, out width, out height); + // === 显示到 WPF === + BitmapImage bitmap = null; + await Task.Run(() => + { + using var ms = new MemoryStream(buffer); + bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = ms; + bitmap.EndInit(); + bitmap.Freeze(); // 跨线程安全 + }); - //// 3️⃣ 更新 WPF Canvas - //Application.Current.Dispatcher.Invoke(() => - //{ - // _rtspRenderer.UpdateFrame(bgraBuffer, width, height); - //}); - //_ = Task.Run(async () => - //{ - // await HumanPredictingAsync(frame.Number, buffer); - //}); + // UI 线程显示 + Application.Current.Dispatcher.Invoke(() => + { + if (_videoBitmap == null || + _videoBitmap.PixelWidth != bitmap.PixelWidth || + _videoBitmap.PixelHeight != bitmap.PixelHeight) + { + _videoBitmap = new WriteableBitmap(bitmap.PixelWidth, bitmap.PixelHeight, + 96, 96, PixelFormats.Bgra32, null); + videoImage.Source = _videoBitmap; + } + + _videoBitmap.Lock(); + bitmap.CopyPixels(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight), + _videoBitmap.BackBuffer, + _videoBitmap.BackBufferStride * bitmap.PixelHeight, + _videoBitmap.BackBufferStride); + _videoBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight)); + _videoBitmap.Unlock(); + + //// 延迟一帧等待控件布局完成 + //Application.Current.Dispatcher.InvokeAsync(() => + //{ + // DrawJumpRopePointsOnVideoImage(); + //}, System.Windows.Threading.DispatcherPriority.Loaded); + }); + + //可以进一步进行人体识别等 - var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number)); + //var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number)); - var human = humanResult?.Humans?.FirstOrDefault(); - _detectQueue.Enqueue(frame.Number, human, null); + //var human = humanResult?.Humans?.FirstOrDefault(); + //_detectQueue.Enqueue(frame.Number, human, null); } private void OnSportTick(int counts, int times) { @@ -178,51 +233,80 @@ public partial class MainWindow : Window //VoiceBroadcast.PlayTick(); } - private byte[] ConvertJpegToBGRA32(byte[] jpegBuffer, out int width, out int height) + + private void DrawJumpRope3DPointsWithGlow() { - using var ms = new MemoryStream(jpegBuffer); - using var bmp = new Bitmap(ms); + if (videoImage == null || overlayCanvas == null) return; - width = bmp.Width; - height = bmp.Height; + overlayCanvas.Children.Clear(); - var rect = new Rectangle(0, 0, width, height); - var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + double imgWidth = videoImage.ActualWidth; + double imgHeight = videoImage.ActualHeight; - int bytes = Math.Abs(bmpData.Stride) * height; - byte[] bgra = new byte[bytes]; - System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, bgra, 0, bytes); + if (imgWidth <= 0 || imgHeight <= 0) return; - bmp.UnlockBits(bmpData); - return bgra; - } + overlayCanvas.Width = imgWidth; + overlayCanvas.Height = imgHeight; - private async Task HumanPredictingAsync(long frameNumber, byte[] buffer) + // 前排 3 人(近景) + double frontRadius = 40; + var frontPositions = new List<(double XNorm, double YNorm)> { - var humanResultTask = Task.Run(() => _humanPredictor.Predicting(buffer, frameNumber)); - var objectsTask = _sport.Equipment - ? Task.Run(() => _objectDetector.Detecting(buffer)) - : Task.FromResult(new List()); + (0.2, 0.70), (0.5, 0.70), (0.8, 0.70) // 前排略高 + }; - 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(() => + foreach (var pos in frontPositions) { - //_humanGraphicsRenderer.ScaleRatio = _rtspRenderer.ScaleRatio; - //_humanGraphicsRenderer.OffsetX = _rtspRenderer.OffsetX; - //_humanGraphicsRenderer.OffsetY = _rtspRenderer.OffsetY; - _humanGraphicsRenderer.Humans = humanResult?.Humans; - _humanGraphicsRenderer.Objects = objects; - }); + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + AddGlowEllipse(x, y, frontRadius, 0.8, overlayCanvas); + } + + // 后排 4 人(远景,更靠下面,大一点) + double backRadius = 50; + var backPositions = new List<(double XNorm, double YNorm)> + { + (0.1, 0.88), (0.35, 0.88), (0.65, 0.88), (0.9, 0.88) // 后排靠底 + }; + + foreach (var pos in backPositions) + { + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + AddGlowEllipse(x, y, backRadius, 0.6, overlayCanvas); + } } + + /// + /// 添加带渐变光的圆圈 + /// + private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas) + { + var ellipse = new Ellipse + { + Width = radius * 2, + Height = radius, // 压扁成椭圆模拟地面 + Stroke = Brushes.White, + StrokeThickness = 2, + Opacity = opacity, + Fill = new RadialGradientBrush + { + GradientOrigin = new Point(0.5, 0.5), + Center = new Point(0.5, 0.5), + RadiusX = 0.5, + RadiusY = 0.5, + GradientStops = new GradientStopCollection + { + new GradientStop(Color.FromArgb(180, 255, 0, 0), 0), // 中心红色 + new GradientStop(Color.FromArgb(0, 255, 0, 0), 1) // 外部透明 + } + } + }; + + Canvas.SetLeft(ellipse, centerX - radius); + Canvas.SetTop(ellipse, centerY - radius / 2); + canvas.Children.Add(ellipse); + } + + } \ No newline at end of file diff --git a/Wpf_AiSportsMicrospace/RTSPPreview.cs b/Wpf_AiSportsMicrospace/RTSPPreview.cs deleted file mode 100644 index 5372c00..0000000 --- a/Wpf_AiSportsMicrospace/RTSPPreview.cs +++ /dev/null @@ -1,143 +0,0 @@ -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 deleted file mode 100644 index ac2c86a..0000000 --- a/Wpf_AiSportsMicrospace/RTSPPreviewRenderer.cs +++ /dev/null @@ -1,66 +0,0 @@ -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 0291eca..311ea4d 100644 --- a/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj +++ b/Wpf_AiSportsMicrospace/Wpf_AiSportsMicrospace.csproj @@ -9,7 +9,7 @@ - + @@ -18,6 +18,7 @@ +