This commit is contained in:
tanglong 2025-09-16 13:47:40 +08:00
parent b3feab76d6
commit 0cdfe7c23d
5 changed files with 152 additions and 275 deletions

View File

@ -4,6 +4,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace" xmlns:local="clr-namespace:Wpf_AiSportsMicrospace"
Loaded="Window_Loaded" Closing="Window_Closing"
mc:Ignorable="d" Title="AI运动微空间" Height="700" Width="1200"> mc:Ignorable="d" Title="AI运动微空间" Height="700" Width="1200">
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -31,9 +32,9 @@
</StackPanel> </StackPanel>
<!-- 右侧视频预览 --> <!-- 右侧视频预览 -->
<Canvas x:Name="rtspPreviewCanvas" Grid.Column="1" Background="Black"> <Grid Grid.Column="1" Background="Black">
<Image x:Name="rtspPreviewImage" Stretch="Uniform" /> <Image x:Name="videoImage" Stretch="Uniform" />
</Canvas> <Canvas x:Name="overlayCanvas" IsHitTestVisible="False" />
</Grid>
</Grid> </Grid>
</Window> </Window>

View File

@ -1,15 +1,19 @@
using Emgu.CV.Reg; using Emgu.CV.Reg;
using SharpDX.Direct3D9;
using SkiaSharp; using SkiaSharp;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using Wpf_AiSportsMicrospace.Common; using Wpf_AiSportsMicrospace.Common;
using Yztob.AiSports.Common;
using Yztob.AiSports.Inferences.Abstractions; using Yztob.AiSports.Inferences.Abstractions;
using Yztob.AiSports.Inferences.Things; using Yztob.AiSports.Inferences.Things;
using Yztob.AiSports.Postures.Sports; using Yztob.AiSports.Postures.Sports;
@ -31,7 +35,9 @@ public partial class MainWindow : Window
private readonly List<SportDescriptor> _sports; private readonly List<SportDescriptor> _sports;
private SportBase _sport; private SportBase _sport;
private readonly SportDetectionQueue _detectQueue; private readonly SportDetectionQueue _detectQueue;
private RTSPPreviewRenderer _rtspRenderer;
private WriteableBitmap _videoBitmap;
private int _lastFrameNumber = -1;
#endregion #endregion
@ -51,13 +57,28 @@ public partial class MainWindow : Window
this.sportList.ItemsSource = _sports; this.sportList.ItemsSource = _sports;
this.sportList.SelectedIndex = 0; this.sportList.SelectedIndex = 0;
this.OnSelectedSport(); this.OnSelectedSport();
}
// 确保 Canvas 已经加载完成 private void Window_Loaded(object sender, RoutedEventArgs e)
_rtspRenderer = new RTSPPreviewRenderer(rtspPreviewCanvas) {
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) private void sportList_SelectionChanged(object sender, SelectionChangedEventArgs e)
@ -138,28 +159,62 @@ public partial class MainWindow : Window
} }
private async void OnFrameExtracted(VideoFrame frame) 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(); 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 // === 显示到 WPF ===
//int width, height; BitmapImage bitmap = null;
//var bgraBuffer = ConvertJpegToBGRA32(buffer, out width, out height); 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 // UI 线程显示
//Application.Current.Dispatcher.Invoke(() => Application.Current.Dispatcher.Invoke(() =>
//{ {
// _rtspRenderer.UpdateFrame(bgraBuffer, width, height); if (_videoBitmap == null ||
//}); _videoBitmap.PixelWidth != bitmap.PixelWidth ||
//_ = Task.Run(async () => _videoBitmap.PixelHeight != bitmap.PixelHeight)
//{ {
// await HumanPredictingAsync(frame.Number, buffer); _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<HumanInferenceResult>(() => _humanPredictor.Predicting(buffer, frame.Number)); //var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number));
var human = humanResult?.Humans?.FirstOrDefault(); //var human = humanResult?.Humans?.FirstOrDefault();
_detectQueue.Enqueue(frame.Number, human, null); //_detectQueue.Enqueue(frame.Number, human, null);
} }
private void OnSportTick(int counts, int times) private void OnSportTick(int counts, int times)
{ {
@ -178,51 +233,80 @@ public partial class MainWindow : Window
//VoiceBroadcast.PlayTick(); //VoiceBroadcast.PlayTick();
} }
private byte[] ConvertJpegToBGRA32(byte[] jpegBuffer, out int width, out int height)
private void DrawJumpRope3DPointsWithGlow()
{ {
using var ms = new MemoryStream(jpegBuffer); if (videoImage == null || overlayCanvas == null) return;
using var bmp = new Bitmap(ms);
width = bmp.Width; overlayCanvas.Children.Clear();
height = bmp.Height;
var rect = new Rectangle(0, 0, width, height); double imgWidth = videoImage.ActualWidth;
var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); double imgHeight = videoImage.ActualHeight;
int bytes = Math.Abs(bmpData.Stride) * height; if (imgWidth <= 0 || imgHeight <= 0) return;
byte[] bgra = new byte[bytes];
System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, bgra, 0, bytes);
bmp.UnlockBits(bmpData); overlayCanvas.Width = imgWidth;
return bgra; 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)); (0.2, 0.70), (0.5, 0.70), (0.8, 0.70) // 前排略高
var objectsTask = _sport.Equipment };
? Task.Run(() => _objectDetector.Detecting(buffer))
: Task.FromResult(new List<BoundingBox>());
await Task.WhenAll(humanResultTask, objectsTask); foreach (var pos in frontPositions)
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; double x = pos.XNorm * imgWidth;
//_humanGraphicsRenderer.OffsetX = _rtspRenderer.OffsetX; double y = pos.YNorm * imgHeight;
//_humanGraphicsRenderer.OffsetY = _rtspRenderer.OffsetY; AddGlowEllipse(x, y, frontRadius, 0.8, overlayCanvas);
_humanGraphicsRenderer.Humans = humanResult?.Humans; }
_humanGraphicsRenderer.Objects = objects;
}); // 后排 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);
}
} }
/// <summary>
/// 添加带渐变光的圆圈
/// </summary>
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);
}
} }

View File

@ -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<VideoFrame> OnExtracted { get; set; }
public Action<VideoFrameWithBuffer> 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;
}
}

View File

@ -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);
}
/// <summary>更新帧到 Canvas</summary>
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;
}
}

View File

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="15.0.1" /> <PackageReference Include="AutoMapper" Version="14.0.0" />
<PackageReference Include="Emgu.CV" Version="4.12.0.5764" /> <PackageReference Include="Emgu.CV" Version="4.12.0.5764" />
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.12.0.5764" /> <PackageReference Include="Emgu.CV.runtime.windows" Version="4.12.0.5764" />
<PackageReference Include="FFmpeg.AutoGen" Version="7.1.1" /> <PackageReference Include="FFmpeg.AutoGen" Version="7.1.1" />
@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" /> <PackageReference Include="Microsoft.Management.Infrastructure" Version="3.0.0" />
<PackageReference Include="Microsoft.ML" Version="4.0.2" /> <PackageReference Include="Microsoft.ML" Version="4.0.2" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.22.1" /> <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.22.1" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SkiaSharp" Version="3.119.0" /> <PackageReference Include="SkiaSharp" Version="3.119.0" />