diff --git a/Wpf_AiSportsMicrospace/App.xaml b/Wpf_AiSportsMicrospace/App.xaml index 726f59f..7a315bd 100644 --- a/Wpf_AiSportsMicrospace/App.xaml +++ b/Wpf_AiSportsMicrospace/App.xaml @@ -2,7 +2,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views" - StartupUri="Views/Main.xaml"> + StartupUri="Views/JumpRope/GroupJumpRope.xaml"> + + diff --git a/Wpf_AiSportsMicrospace/App.xaml.cs b/Wpf_AiSportsMicrospace/App.xaml.cs index c26a4cf..c0a947f 100644 --- a/Wpf_AiSportsMicrospace/App.xaml.cs +++ b/Wpf_AiSportsMicrospace/App.xaml.cs @@ -1,6 +1,8 @@ using System.Configuration; using System.Data; using System.Windows; +using Yztob.AiSports.Common; +using Yztob.AiSports.Common.Implement; namespace Wpf_AiSportsMicrospace; @@ -9,5 +11,16 @@ namespace Wpf_AiSportsMicrospace; /// public partial class App : Application { + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + +#if DEBUG + // 初始化 DebugTracker + DebugTracker.Enabled = true; + DebugTracker.Channels.Add(new DiagnosisDebugTrackChannel()); +#endif + + } } diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs index f9cd20c..7648320 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs @@ -66,12 +66,12 @@ public partial class MainWindow : Window { Application.Current.Dispatcher.InvokeAsync(() => { - DrawJumpRope3DPointsWithGlow(); + DrawCirclesWithText(); }, DispatcherPriority.Loaded); videoImage.SizeChanged += (s, ev) => { - DrawJumpRope3DPointsWithGlow(); + DrawCirclesWithText(); }; } @@ -395,4 +395,103 @@ public partial class MainWindow : Window var distance = Math.Sqrt(dx * dx + dy * dy); return distance <= p.Radius; } + + + private void DrawCirclesWithText() + { + overlayCanvas.Children.Clear(); + //sports.Clear(); + //circleTexts.Clear(); + + double imgWidth = overlayCanvas.ActualWidth; + double imgHeight = overlayCanvas.ActualHeight; + double radius = 100; + + // 每个圆的位置:X 和 Y 都归一化 0~1 + var circlePositions = new List<(double XNorm, double YNorm)> + { + (0.07, 0.58), + (0.21, 0.88 ), + (0.36, 0.58 ), + (0.50, 0.88), + (0.64, 0.58 ), + (0.78, 0.88), + (0.92, 0.58 ) + }; + + foreach (var pos in circlePositions) + { + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + + // 绘制发光圆 + AddGlowEllipse(x, y, overlayCanvas); + + // 创建文本控件 + var text = new TextBlock + { + Text = "0", + Foreground = Brushes.Red, + FontWeight = FontWeights.Bold, + FontSize = 50, + TextAlignment = TextAlignment.Center, + Width = radius * 2 + }; + Canvas.SetLeft(text, x - radius); + Canvas.SetTop(text, y - radius - 25); + overlayCanvas.Children.Add(text); + //circleTexts.Add(text); + + //// 绑定运动对象 + //var sport = SportBase.Create("rope-skipping"); + //int index = circleTexts.Count - 1; + //sport.OnTicked += (count, times) => + //{ + // Application.Current.Dispatcher.Invoke(() => + // { + // circleTexts[index].Text = count.ToString(); + // }); + //}; + //sport.Start(); + //sports.Add(sport); + } + } + + + + /// + /// 添加带渐变光的圆圈(中心红色,边缘蓝色) + /// + private void AddGlowEllipse(double centerX, double centerY, Canvas canvas) + { + double radius = 70; // 统一半径 + double flattenFactor = 0.5; // 统一扁平化比例 + double opacity = 0.8; // 统一透明度 + + var ellipse = new Ellipse + { + Width = radius * 2, + Height = radius * flattenFactor, + 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(220, 255, 255, 0), 0.0), // 中心黄 + new GradientStop(Color.FromArgb(180, 255, 255, 0), 0.4), // 中间黄 + new GradientStop(Color.FromArgb(180, 0, 0, 255), 0.7), // 边缘蓝 + new GradientStop(Color.FromArgb(0, 0, 0, 255), 1.0) // 外部透明 + } + } + }; + + // 定位到中心 + Canvas.SetLeft(ellipse, centerX - radius); + Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2); + canvas.Children.Add(ellipse); + } } \ No newline at end of file diff --git a/Wpf_AiSportsMicrospace/Views/Home.xaml.cs b/Wpf_AiSportsMicrospace/Views/Home.xaml.cs index 08869a6..61a89c4 100644 --- a/Wpf_AiSportsMicrospace/Views/Home.xaml.cs +++ b/Wpf_AiSportsMicrospace/Views/Home.xaml.cs @@ -124,7 +124,6 @@ namespace Wpf_AiSportsMicrospace //} RouterGoNew(); } - private void StartFrameProcessing() { Task.Run(() => @@ -137,15 +136,7 @@ namespace Wpf_AiSportsMicrospace } else { - //_webcamClient.OnExtractFrame += frame => - //{ - // if (frame != null) - // _frameQueue.Enqueue(frame); - //}; - //_webcamClient.StartExtract(); - - _webcamClient.StartExtract(); - //Thread.Sleep(5); + Thread.Sleep(5); } } }, _cts.Token); @@ -173,6 +164,7 @@ namespace Wpf_AiSportsMicrospace if (human == null) return; + //检测挥手动作 var wavingaction = _sportOperate.VerifyWavingAction(human); diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml index 56423cc..48401f4 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml @@ -4,21 +4,13 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.Views" - mc:Ignorable="d" Height="500" Width="800"> - + mc:Ignorable="d" Height="1080" Width="1920" Loaded="UserControl_Loaded"> + - - - - - + + + diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs index e8da202..5388495 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs @@ -1,5 +1,9 @@ -using System; +using Emgu.CV.Flann; +using HandyControl.Controls; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -11,8 +15,20 @@ using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; +using System.Windows.Shapes; +using System.Windows.Threading; using Wpf_AiSportsMicrospace.Common; +using Wpf_AiSportsMicrospace.Dto; +using Wpf_AiSportsMicrospace.Enum; +using Wpf_AiSportsMicrospace.Service; using WpfAnimatedGif; +using Yztob.AiSports.Common; +using Yztob.AiSports.Common.Implement; +using Yztob.AiSports.Inferences.Abstractions; +using Yztob.AiSports.Inferences.Things; +using Yztob.AiSports.Postures.Sports; +using Yztob.AiSports.Sensors.Abstractions; +using Yztob.AiSports.Sensors.Things; namespace Wpf_AiSportsMicrospace.Views { @@ -21,17 +37,329 @@ namespace Wpf_AiSportsMicrospace.Views /// public partial class GroupJumpRope : UserControl { + private IHumanPredictor _humanPredictor; + private IObjectDetector _objectDetector; + private HumanGraphicsRenderer _humanGraphicsRenderer; + private WebcamClient _webcamClient; + private ConcurrentQueue _frameQueue = new(); + private CancellationTokenSource _cts = new(); + private SportOperate _sportOperate; + private SportBase _sport; + private readonly SportDetectionQueue _detectQueue; + + private List sports = new(); + private List circleTexts = new(); + private double[] circlePositionsX = { 0.07, 0.21, 0.36, 0.50, 0.64, 0.78, 0.92 }; + + ConfigService configService = new ConfigService(); public GroupJumpRope() { InitializeComponent(); + _humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.MultiMedium); + _objectDetector = ObjectDetectorFactory.CreateSportGoodsDetector(); + _humanGraphicsRenderer = new HumanGraphicsRenderer(); + _humanGraphicsRenderer.DrawLabel = false; - var image = new BitmapImage(); - image.BeginInit(); - image.UriSource = new Uri("../../Resources/Img/gif/1.gif", UriKind.Relative); // 替换成你的 GIF 路径 - image.EndInit(); + _detectQueue = new SportDetectionQueue(); + } + private void UserControl_Loaded(object sender, RoutedEventArgs e) + { + //DrawJumpRope3DPointsWithGlow(); - // 设置动画源 - ImageBehavior.SetAnimatedSource(GifImage, image); + DrawCirclesWithText(); + + _sportOperate = new SportOperate(); + _webcamClient = _sportOperate.CreateRTSP(); + + _webcamClient.OnExtractFrame += frame => + { + if (frame != null) + _frameQueue.Enqueue(frame); + }; + _webcamClient.StartExtract(); + + StartFrameProcessing(); + } + + private void StartFrameProcessing() + { + Task.Run(() => + { + while (!_cts.Token.IsCancellationRequested) + { + if (_frameQueue.TryDequeue(out var frame)) + { + ProcessFrame(frame); + } + else + { + Thread.Sleep(5); // 空队列时避免忙等 + } + } + }, _cts.Token); + } + + private void ProcessFrame(VideoFrame frame) + { + try + { + var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray(); + var humanResult = _humanPredictor.Predicting(buffer, frame.Number); + + var humans = humanResult?.Humans?.ToList(); + if (humans == null || humans.Count == 0) + return; + + UpdateCircleCounts(humans); + } + catch (Exception ex) + { + Console.WriteLine("OnFrameExtracted error: " + ex.Message); + } + } + public Human LocateHuman(List humans, double begin, double end, double frameWidth) + { + if (humans == null || humans.Count == 0) + return null; + + foreach (var hu in humans) + { + var nose = hu.Keypoints.FirstOrDefault(k => k.Name == "nose"); + if (nose == null) + continue; + + // 使用 Canvas 宽度归一化 + double xNorm = nose.X / frameWidth; + + if (xNorm >= begin && xNorm <= end) + return hu; + } + + return null; + } + + private void DrawCirclesWithText() + { + overlayCanvas.Children.Clear(); + sports.Clear(); + circleTexts.Clear(); + + double imgWidth = overlayCanvas.ActualWidth; + double imgHeight = overlayCanvas.ActualHeight; + double radius = 100; + + // 每个圆的位置:X 和 Y 都归一化 0~1 + var circlePositions = new List<(double XNorm, double YNorm)> + { + (0.07, 0.58), + (0.21, 0.88 ), + (0.36, 0.58 ), + (0.50, 0.88), + (0.64, 0.58 ), + (0.78, 0.88), + (0.92, 0.58 ) + }; + + foreach (var pos in circlePositions) + { + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + + // 绘制发光圆 + AddGlowEllipse(x, y, overlayCanvas); + + // 创建文本控件 + var text = new TextBlock + { + Text = "0", + Foreground = Brushes.Red, + FontWeight = FontWeights.Bold, + FontSize = 50, + TextAlignment = TextAlignment.Center, + Width = radius * 2 + }; + Canvas.SetLeft(text, x - radius); + Canvas.SetTop(text, y - radius - 25); + overlayCanvas.Children.Add(text); + circleTexts.Add(text); + + // 绑定运动对象 + var sport = SportBase.Create("rope-skipping"); + int index = circleTexts.Count - 1; + sport.OnTicked += (count, times) => + { + Application.Current.Dispatcher.Invoke(() => + { + circleTexts[index].Text = count.ToString(); + }); + }; + sport.Start(); + sports.Add(sport); + } + } + + + private void UpdateCircleCounts(List humans) + { + for (int i = 0; i < circlePositionsX.Length; i++) + { + double center = circlePositionsX[i]; + double range = 0.07; + double begin = center - range; + double end = center + range; + + var human = LocateHuman(humans, begin, end, overlayCanvas.ActualWidth); + if (human != null) + { + sports[i].Pushing(human); + } + } + } + + private void AddGlowEllipse(double centerX, double centerY, double radius, Canvas canvas) + { + var ellipse = new Ellipse + { + Width = radius * 2, + Height = radius, + Stroke = Brushes.Red, + StrokeThickness = 3, + Opacity = 0.7 + }; + + Canvas.SetLeft(ellipse, centerX - radius); + Canvas.SetTop(ellipse, centerY - radius / 2); + canvas.Children.Add(ellipse); + } + + private void DrawJumpRope3DPointsWithGlow() + { + configService.LoadAllConfigs(); + + ConfigSet jumpRopeConfig; + + double imgWidth = 1920; + double imgHeight = 1080; + + if (imgWidth <= 0 || imgHeight <= 0) return; + + overlayCanvas.Children.Clear(); + overlayCanvas.Width = imgWidth; + overlayCanvas.Height = imgHeight; + + bool needSaveConfig = false; + + if (configService.ConfigDic.TryGetValue("rope-skipping", out jumpRopeConfig) || jumpRopeConfig.Points.Count == 0) + { + jumpRopeConfig = new ConfigSet { Name = "rope-skipping" }; + needSaveConfig = true; + + double yOffset = -0.10; + double frontRadius = 85; + double backRadius = 70; + + double frontY = 0.70 + yOffset; + var frontPositions = new List<(double XNorm, double YNorm)> + { + (0.24, frontY), + (0.48, frontY), + (0.72, frontY) + }; + + foreach (var pos in frontPositions) + { + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + + jumpRopeConfig.Points.Add(new PointConfig + { + X = x, + Y = y, + Radius = frontRadius, + XNorm = pos.XNorm, + YNorm = pos.YNorm + }); + + AddGlowEllipse(x, y, overlayCanvas); + } + + double backY = 0.88 + yOffset; + var backPositions = new List<(double XNorm, double YNorm)> + { + (0.10, backY), + (0.35, backY), + (0.60, backY), + (0.88, backY) + }; + + foreach (var pos in backPositions) + { + double x = pos.XNorm * imgWidth; + double y = pos.YNorm * imgHeight; + + jumpRopeConfig.Points.Add(new PointConfig + { + X = x, + Y = y, + Radius = backRadius, + XNorm = pos.XNorm, + YNorm = pos.YNorm + }); + + AddGlowEllipse(x, y, overlayCanvas); + } + } + else + { + foreach (var point in jumpRopeConfig.Points) + { + double x = point.XNorm * imgWidth; + double y = point.YNorm * imgHeight; + AddGlowEllipse(x, y, overlayCanvas); + } + } + + if (needSaveConfig) + { + configService.ConfigDic[jumpRopeConfig.Name] = jumpRopeConfig; + configService.SaveAllConfigs(); + } + } + + /// + /// 添加带渐变光的圆圈(中心红色,边缘蓝色) + /// + private void AddGlowEllipse(double centerX, double centerY, Canvas canvas) + { + double radius = 70; // 统一半径 + double flattenFactor = 0.5; // 统一扁平化比例 + double opacity = 0.8; // 统一透明度 + + var ellipse = new Ellipse + { + Width = radius * 2, + Height = radius * flattenFactor, + 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(220, 255, 255, 0), 0.0), // 中心黄 + new GradientStop(Color.FromArgb(180, 255, 255, 0), 0.4), // 中间黄 + new GradientStop(Color.FromArgb(180, 0, 0, 255), 0.7), // 边缘蓝 + new GradientStop(Color.FromArgb(0, 0, 0, 255), 1.0) // 外部透明 + } + } + }; + + // 定位到中心 + Canvas.SetLeft(ellipse, centerX - radius); + Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2); + canvas.Children.Add(ellipse); } } } diff --git a/sdks/Yztob.AiSports.Common.dll b/sdks/Yztob.AiSports.Common.dll index 534f254..98ab8c8 100644 Binary files a/sdks/Yztob.AiSports.Common.dll and b/sdks/Yztob.AiSports.Common.dll differ diff --git a/sdks/Yztob.AiSports.Inferences.dll b/sdks/Yztob.AiSports.Inferences.dll index 610e1d0..7b24909 100644 Binary files a/sdks/Yztob.AiSports.Inferences.dll and b/sdks/Yztob.AiSports.Inferences.dll differ diff --git a/sdks/Yztob.AiSports.Inferences.xml b/sdks/Yztob.AiSports.Inferences.xml index 0f8bf63..f4bc390 100644 --- a/sdks/Yztob.AiSports.Inferences.xml +++ b/sdks/Yztob.AiSports.Inferences.xml @@ -52,7 +52,7 @@ 单人检测-高精度,3D结构 - + 多人检测-低精度低速率,2D结构 diff --git a/sdks/Yztob.AiSports.Postures.dll b/sdks/Yztob.AiSports.Postures.dll index 6902d42..3690e60 100644 Binary files a/sdks/Yztob.AiSports.Postures.dll and b/sdks/Yztob.AiSports.Postures.dll differ diff --git a/sdks/Yztob.AiSports.Sensors.WinForm.dll b/sdks/Yztob.AiSports.Sensors.WinForm.dll index a7a482d..5ee5135 100644 Binary files a/sdks/Yztob.AiSports.Sensors.WinForm.dll and b/sdks/Yztob.AiSports.Sensors.WinForm.dll differ diff --git a/sdks/Yztob.AiSports.Sensors.WinForm.xml b/sdks/Yztob.AiSports.Sensors.WinForm.xml index e6ddd23..8990204 100644 --- a/sdks/Yztob.AiSports.Sensors.WinForm.xml +++ b/sdks/Yztob.AiSports.Sensors.WinForm.xml @@ -55,6 +55,11 @@ 获取或设置自应用大小,保持同比缩放 + + + 获取或设置是否展示帧序号、帧率 + + 获取当前是否正在播放 @@ -75,9 +80,14 @@ 获取当画面缩放后X轴的偏移量,即黑边大小 + + + 获取或设置视频流拉取帧率 + + - 获取或设置抽帧响应 + 获取或设置抽帧响应,处理的是未解析的原始帧 diff --git a/sdks/Yztob.AiSports.Sensors.dll b/sdks/Yztob.AiSports.Sensors.dll index c90d584..350b6b0 100644 Binary files a/sdks/Yztob.AiSports.Sensors.dll and b/sdks/Yztob.AiSports.Sensors.dll differ diff --git a/sdks/Yztob.AiSports.Sensors.xml b/sdks/Yztob.AiSports.Sensors.xml index cab0d31..0d0d629 100644 --- a/sdks/Yztob.AiSports.Sensors.xml +++ b/sdks/Yztob.AiSports.Sensors.xml @@ -312,6 +312,12 @@ 获取或设置时间戳,从UTC:2000-01-01开始的耗秒数 + + + 获取当前时间戳 + + + 将当前帧转换成指定格式图像,并返回图像缓冲区数组