diff --git a/Wpf_AiSportsMicrospace/D3DFrameRenderer.cs b/Wpf_AiSportsMicrospace/D3DFrameRenderer.cs deleted file mode 100644 index 255c302..0000000 --- a/Wpf_AiSportsMicrospace/D3DFrameRenderer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing.Imaging; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Interop; -using System.Windows.Media.Imaging; -using System.Windows.Media; -using System.Windows; - -namespace Wpf_AiSportsMicrospace -{ - public class D3DFrameRenderer - { - private D3DImage _d3dImage; - - public D3DFrameRenderer(D3DImage d3dImage) - { - _d3dImage = d3dImage; - } - - // 当有新帧时调用 - public void RenderFrame(byte[] jpegBuffer) - { - Task.Run(() => - { - int width, height; - var bgra = ConvertJpegToBGRA32(jpegBuffer, out width, out height); - - // 更新 UI - Application.Current.Dispatcher.BeginInvoke(new Action(() => - { - if (!_d3dImage.IsFrontBufferAvailable) return; - - if (_d3dImage.PixelWidth != width || _d3dImage.PixelHeight != height) - { - _d3dImage.Lock(); - _d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); // 先清空 - _d3dImage.Unlock(); - } - - // 用 WriteableBitmap 临时显示 - var wb = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null); - wb.Lock(); - Marshal.Copy(bgra, 0, wb.BackBuffer, bgra.Length); - wb.AddDirtyRect(new Int32Rect(0, 0, width, height)); - wb.Unlock(); - _d3dImage.Lock(); - _d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); // 暂时不绑定真实 D3D Surface - _d3dImage.Unlock(); - - // 临时用 ImageSource 显示 - FrameReady?.Invoke(wb, width, height); - })); - }); - } - - public delegate void FrameReadyHandler(ImageSource frame, int width, int height); - public event FrameReadyHandler FrameReady; - - 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, ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); - - int bytes = bmpData.Stride * height; - byte[] raw = new byte[bytes]; - Marshal.Copy(bmpData.Scan0, raw, 0, bytes); - - bmp.UnlockBits(bmpData); - return raw; - } - } -} diff --git a/Wpf_AiSportsMicrospace/Dto/PointConfig.cs b/Wpf_AiSportsMicrospace/Dto/PointConfig.cs new file mode 100644 index 0000000..5ebbde0 --- /dev/null +++ b/Wpf_AiSportsMicrospace/Dto/PointConfig.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Wpf_AiSportsMicrospace.Dto +{ + public class PointConfig + { + public double X { get; set; } // 原始X(像素) + public double Y { get; set; } // 原始Y(像素) + public double Radius { get; set; } // 半径 + public double XNorm { get; set; } // 归一化X(0~1) + public double YNorm { get; set; } // 归一化Y(0~1) + } + public class ConfigSet + { + public string Name { get; set; } // 配置名,比如 "JumpRope" + public List Points { get; set; } = new List(); + } +} diff --git a/Wpf_AiSportsMicrospace/JumpRopePoint.cs b/Wpf_AiSportsMicrospace/JumpRopePoint.cs deleted file mode 100644 index 14d20f1..0000000 --- a/Wpf_AiSportsMicrospace/JumpRopePoint.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Wpf_AiSportsMicrospace -{ - public class JumpRopePoint - { - /// 横坐标归一化(0~1,相对于视频显示宽度) - public double XNorm { get; set; } - - /// 纵坐标归一化(0~1,相对于视频显示高度) - public double YNorm { get; set; } - - /// 圆半径,可选,单位像素 - public double Radius { get; set; } - } - -} diff --git a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs index 66fba8e..2158be4 100644 --- a/Wpf_AiSportsMicrospace/MainWindow.xaml.cs +++ b/Wpf_AiSportsMicrospace/MainWindow.xaml.cs @@ -14,6 +14,8 @@ using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.Windows.Threading; using Wpf_AiSportsMicrospace.Common; +using Wpf_AiSportsMicrospace.Dto; +using Wpf_AiSportsMicrospace.Service; using Yztob.AiSports.Inferences.Abstractions; using Yztob.AiSports.Inferences.Things; using Yztob.AiSports.Postures.Sports; @@ -38,6 +40,8 @@ public partial class MainWindow : Window private WriteableBitmap _videoBitmap; private int _lastFrameNumber = -1; + + ConfigService configService = new ConfigService(); #endregion @@ -63,7 +67,7 @@ public partial class MainWindow : Window Application.Current.Dispatcher.InvokeAsync(() => { DrawJumpRope3DPointsWithGlow(); - }, System.Windows.Threading.DispatcherPriority.Loaded); + }, DispatcherPriority.Loaded); videoImage.SizeChanged += (s, ev) => { @@ -153,6 +157,8 @@ public partial class MainWindow : Window _webcamClient.OnExtractFrame += this.OnFrameExtracted; _webcamClient.StartExtract();//开始抽帧 + + this.startOrStop.Content = "停止(&S)"; this.sportCounts.Text = "0"; this.sportTimes.Text = "00'00\""; @@ -185,7 +191,7 @@ public partial class MainWindow : Window // UI 线程显示 Application.Current.Dispatcher.Invoke(() => { - if (videoImage == null ) return; + if (videoImage == null) return; if (_videoBitmap == null || _videoBitmap.PixelWidth != bitmap.PixelWidth || @@ -203,15 +209,39 @@ public partial class MainWindow : Window _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 actualPositions = new List<(double X, double Y)> + { + (290, 510), + (500, 700), + (810, 710), + (120, 880), + (350, 880), + (660, 880) + }; + + //判断是否出圈 + if (configService.ConfigDic.TryGetValue("JumpRope", out var obj) && obj is ConfigSet cfg) + { + for (int i = 0; i < actualPositions.Count && i < cfg.Points.Count; i++) + { + var (ax, ay) = actualPositions[i]; + var pointCfg = cfg.Points[i]; + + if (!IsInsideCircle(ax, ay, pointCfg)) + { + Console.WriteLine($"⚠️ 学生{i + 1} 已经出圈!(坐标 {ax},{ay})"); + } + else + { + Console.WriteLine($"✅ 学生{i + 1} 在圈内。"); + } + } + } + //可以进一步进行人体识别等 //var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number)); @@ -239,57 +269,104 @@ public partial class MainWindow : Window private void DrawJumpRope3DPointsWithGlow() { if (videoImage == null || overlayCanvas == null) return; + configService.LoadAllConfigs(); // 从文件加载 ConfigDic - overlayCanvas.Children.Clear(); + ConfigSet jumpRopeConfig; double imgWidth = videoImage.ActualWidth; double imgHeight = videoImage.ActualHeight; if (imgWidth <= 0 || imgHeight <= 0) return; + overlayCanvas.Children.Clear(); overlayCanvas.Width = imgWidth; overlayCanvas.Height = imgHeight; - // 前排 3 人(近景) - double frontRadius = 60; - var frontPositions = new List<(double XNorm, double YNorm)> - { - (0.2, 0.50), (0.5, 0.50), (0.8, 0.50) // 前排略高 - }; + bool needSaveConfig = false; - foreach (var pos in frontPositions) + if (!configService.ConfigDic.TryGetValue("rope-skipping", out jumpRopeConfig) || jumpRopeConfig.Points.Count == 0) { - double x = pos.XNorm * imgWidth; - double y = pos.YNorm * imgHeight; - AddGlowEllipse(x, y, frontRadius, 0.8, overlayCanvas); + // 没有配置,则生成默认配置 + jumpRopeConfig = new ConfigSet { Name = "rope-skipping" }; + needSaveConfig = true; + + // ===== 前排 3 人 ===== + double frontRadius = 80; + var frontPositions = new List<(double XNorm, double YNorm)> + { + (0.25, 0.70), (0.5, 0.70), (0.75, 0.70) + }; + + 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, frontRadius, 0.85, overlayCanvas, flattenFactor: 0.4); + } + + // ===== 后排 3 人 ===== + double backRadius = 70; + var backPositions = new List<(double XNorm, double YNorm)> + { + (0.2, 0.88), (0.5, 0.88), (0.8, 0.88) + }; + + 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, backRadius, 0.7, overlayCanvas, flattenFactor: 0.55); + } + } + else + { + // 已有配置,按归一化坐标计算实际位置绘制 + foreach (var point in jumpRopeConfig.Points) + { + double x = point.XNorm * imgWidth; + double y = point.YNorm * imgHeight; + + AddGlowEllipse(x, y, point.Radius, 0.8, overlayCanvas, flattenFactor: 0.5); + } } - // 后排 4 人(远景,更靠下面,大一点) - double backRadius = 60; - 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) + // 保存配置(如果是新生成的) + if (needSaveConfig) { - double x = pos.XNorm * imgWidth; - double y = pos.YNorm * imgHeight; - AddGlowEllipse(x, y, backRadius, 0.6, overlayCanvas); + configService.ConfigDic[jumpRopeConfig.Name] = jumpRopeConfig; + configService.SaveAllConfigs(); } } /// /// 添加带渐变光的圆圈(中心红色,边缘蓝色) /// - private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas) + private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas, double flattenFactor = 0.5) { var ellipse = new Ellipse { Width = radius * 2, - Height = radius, // 压扁成椭圆模拟地面 - Stroke = Brushes.White, - StrokeThickness = 2, + Height = radius * flattenFactor, // 扁平化:越小越“贴地” Opacity = opacity, Fill = new RadialGradientBrush { @@ -299,19 +376,25 @@ public partial class MainWindow : Window RadiusY = 0.5, GradientStops = new GradientStopCollection { - new GradientStop(Color.FromArgb(200, 255, 0, 0), 0.0), // 中心红 - new GradientStop(Color.FromArgb(150, 255, 0, 0), 0.4), // 中间仍是红 + new GradientStop(Color.FromArgb(220, 255, 80, 80), 0.0), // 中心亮红 + new GradientStop(Color.FromArgb(180, 255, 0, 0), 0.4), // 中间红 new GradientStop(Color.FromArgb(180, 0, 128, 255), 0.7), // 边缘蓝 - new GradientStop(Color.FromArgb(0, 0, 128, 255), 1.0) // 最外透明 + new GradientStop(Color.FromArgb(0, 0, 128, 255), 1.0) // 外部透明 } } }; - // 定位到中心 + // 定位到中心(Y 要根据压缩高度来调整) Canvas.SetLeft(ellipse, centerX - radius); - Canvas.SetTop(ellipse, centerY - radius / 2); + Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2); canvas.Children.Add(ellipse); } - + private bool IsInsideCircle(double actualX, double actualY, PointConfig p) + { + var dx = actualX - p.X; + var dy = actualY - p.Y; + var distance = Math.Sqrt(dx * dx + dy * dy); + return distance <= p.Radius; + } } \ No newline at end of file diff --git a/Wpf_AiSportsMicrospace/Service/ConfigService.cs b/Wpf_AiSportsMicrospace/Service/ConfigService.cs new file mode 100644 index 0000000..52685e1 --- /dev/null +++ b/Wpf_AiSportsMicrospace/Service/ConfigService.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Wpf_AiSportsMicrospace.Dto; + +namespace Wpf_AiSportsMicrospace.Service +{ + /// + /// 配置服务 + /// + public class ConfigService + { + public Dictionary ConfigDic { get; set; } = new Dictionary(); + + public ConfigService() + { + LoadAllConfigs(); + } + + /// + /// 保存配置信息 + /// + /// + public void SaveAllConfigs(string fileName = "configs.json") + { + string basePath = AppContext.BaseDirectory; // 当前运行目录 + string filePath = Path.Combine(basePath, fileName); + + var json = JsonSerializer.Serialize(ConfigDic, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(filePath, json); + } + + /// + /// 加载配置信息 + /// + /// + public void LoadAllConfigs(string fileName = "configs.json") + { + string basePath = AppContext.BaseDirectory; + string filePath = Path.Combine(basePath, fileName); + + if (!File.Exists(filePath)) return; + + var json = File.ReadAllText(filePath); + ConfigDic = JsonSerializer.Deserialize>(json); + } + } +}