diff --git a/AiSportsMicrospaceDB/DBContext/AppDbContext.cs b/AiSportsMicrospaceDB/DBContext/AppDbContext.cs index 28e070f..29152b5 100644 --- a/AiSportsMicrospaceDB/DBContext/AppDbContext.cs +++ b/AiSportsMicrospaceDB/DBContext/AppDbContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace AiSportsMicrospaceDB.DBContext @@ -15,25 +16,9 @@ namespace AiSportsMicrospaceDB.DBContext { } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasData( - new SixGroupPoints - { - Id = 1, - points = [ - (0.21, 0.88 ), - (0.36, 0.58 ), - (0.50, 0.88), - (0.64, 0.58 ), - (0.78, 0.88), - (0.92, 0.58 ) - ] - }); - } - public DbSet BasicConfig { get; set; } public DbSet SixGroupPoints { get; set; } + } } diff --git a/AiSportsMicrospaceDB/Entities/BasicConfig.cs b/AiSportsMicrospaceDB/Entities/BasicConfig.cs index 5cef090..ed40770 100644 --- a/AiSportsMicrospaceDB/Entities/BasicConfig.cs +++ b/AiSportsMicrospaceDB/Entities/BasicConfig.cs @@ -15,10 +15,4 @@ namespace AiSportsMicrospaceDB.Entities public string UserName { get; set; } public string Password { get; set; } } - - public class SixGroupPoints { - // ✅ 主键 - public int Id { get; set; } - public List<(double,double)> points { get; set; } - } } diff --git a/AiSportsMicrospaceDB/Entities/Point.cs b/AiSportsMicrospaceDB/Entities/Point.cs new file mode 100644 index 0000000..087d7b7 --- /dev/null +++ b/AiSportsMicrospaceDB/Entities/Point.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AiSportsMicrospaceDB.Entities +{ + [Owned] + public class Point + { + public double X { get; set; } + public double Y { get; set; } + } +} diff --git a/AiSportsMicrospaceDB/Entities/SixGroupPoints.cs b/AiSportsMicrospaceDB/Entities/SixGroupPoints.cs new file mode 100644 index 0000000..5100e5d --- /dev/null +++ b/AiSportsMicrospaceDB/Entities/SixGroupPoints.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AiSportsMicrospaceDB.Entities +{ + + public class SixGroupPoints { + // ✅ 主键 + public int Id { get; set; } + public Point point1 { get; set; } + public Point point2 { get; set; } + public Point point3 { get; set; } + public Point point4 { get; set; } + public Point point5 { get; set; } + public Point point6 { get; set; } + } + +} diff --git a/Wpf_AiSportsMicrospace/App.xaml.cs b/Wpf_AiSportsMicrospace/App.xaml.cs index fb87b50..64f36c6 100644 --- a/Wpf_AiSportsMicrospace/App.xaml.cs +++ b/Wpf_AiSportsMicrospace/App.xaml.cs @@ -55,6 +55,18 @@ public partial class App : Application var db = scope.ServiceProvider.GetRequiredService(); //db.Database.EnsureDeleted(); // 删除数据库和表 db.Database.EnsureCreated(); + + //db.SixGroupPoints.Add(new AiSportsMicrospaceDB.Entities.SixGroupPoints + //{ + // Id = 1, + // point1 = new AiSportsMicrospaceDB.Entities.Point { X = 0.21, Y = 0.88 }, + // point2 = new AiSportsMicrospaceDB.Entities.Point { X = 0.36, Y = 0.58 }, + // point3 = new AiSportsMicrospaceDB.Entities.Point { X = 0.50, Y = 0.88 }, + // point4 = new AiSportsMicrospaceDB.Entities.Point { X = 0.64, Y = 0.58 }, + // point5 = new AiSportsMicrospaceDB.Entities.Point { X = 0.78, Y = 0.88 }, + // point6 = new AiSportsMicrospaceDB.Entities.Point { X = 0.92, Y = 0.58 } + //}); + //db.SaveChanges(); } // 用 DI 创建窗口 diff --git a/Wpf_AiSportsMicrospace/Common/AppSettings.cs b/Wpf_AiSportsMicrospace/Common/AppSettings.cs index 26d57e4..ed5c9df 100644 --- a/Wpf_AiSportsMicrospace/Common/AppSettings.cs +++ b/Wpf_AiSportsMicrospace/Common/AppSettings.cs @@ -7,6 +7,6 @@ namespace Common public static class AppSettings { public static string SchoolCode = "202501060001"; - public static string BackendUrl = "http://localhost:9992/api/StudentFace"; + public static string BackendUrl = "http://localhost:9991/api/StudentFace"; } } diff --git a/Wpf_AiSportsMicrospace/Common/HttpManager.cs b/Wpf_AiSportsMicrospace/Common/HttpManager.cs index 11751f8..5cbb496 100644 --- a/Wpf_AiSportsMicrospace/Common/HttpManager.cs +++ b/Wpf_AiSportsMicrospace/Common/HttpManager.cs @@ -5,7 +5,7 @@ namespace Wpf_AiSportsMicrospace.Common { public class HttpManager { - public static async Task HttpPostAsync(string url, string postData = null, string contentType = "application/json", int timeOut = 30, Dictionary headers = null) + public static string HttpPost(string url, string postData = null, string contentType = "application/json", int timeOut = 30, Dictionary headers = null) { using (var client = new HttpClient()) { @@ -27,9 +27,10 @@ namespace Wpf_AiSportsMicrospace.Common try { - var response = await client.PostAsync(url, content); + // 注意:要调用 PostAsync 而不是 po() + var response = client.PostAsync(url, content).Result; // 同步等待 response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); + return response.Content.ReadAsStringAsync().Result; } catch (Exception ex) { @@ -37,6 +38,7 @@ namespace Wpf_AiSportsMicrospace.Common } } } + public static async Task HttpGetAsync(string url, Dictionary headers = null) { using (var client = new HttpClient()) diff --git a/Wpf_AiSportsMicrospace/Common/Utils.cs b/Wpf_AiSportsMicrospace/Common/Utils.cs index 998cf66..7a25598 100644 --- a/Wpf_AiSportsMicrospace/Common/Utils.cs +++ b/Wpf_AiSportsMicrospace/Common/Utils.cs @@ -214,10 +214,10 @@ namespace Wpf_AiSportsMicrospace.Common return _cache.GetValueOrDefault(relativePath); } - public static async Task> FacialRecognition(byte[] buffer, double[] xNorms) + public static List FacialRecognition(byte[] buffer, double[] xNorms) { - if (xNorms == null || xNorms.Length < 2) - throw new ArgumentException("xNorms 至少需要两个元素"); + if (xNorms == null || xNorms.Length == 0) + throw new ArgumentException("xNorms 不能为空"); using var image = SharpImage.Image.Load(buffer); @@ -225,43 +225,66 @@ namespace Wpf_AiSportsMicrospace.Common int imgHeight = image.Height; int rectHeight = imgHeight / 2; - // 计算分割边界 + // 1. 排序中心点 var sortedX = xNorms.OrderBy(x => x).ToList(); + int faceCount = sortedX.Count; + + // 2. 计算分割边界 var splitXs = new List(); for (int i = 0; i < sortedX.Count - 1; i++) splitXs.Add((sortedX[i] + sortedX[i + 1]) / 2.0); + + // 3. 起止边界 splitXs.Insert(0, 0.0); splitXs.Add(1.0); + // 4. 输出目录 + string imgDir = Path.Combine(AppContext.BaseDirectory, "img"); + Directory.CreateDirectory(imgDir); + var studentInfos = new List(); - for (int i = 0; i < 6 && i < splitXs.Count - 1; i++) + // 5. 循环裁剪 + 同步识别 + for (int i = 0; i < faceCount; i++) { - int x1 = (int)(splitXs[i] * imgWidth); - int x2 = (int)(splitXs[i + 1] * imgWidth); - int w = x2 - x1; + double xStart = splitXs[i]; + double xEnd = splitXs[i + 1]; + + int x1 = (int)(xStart * imgWidth); + int x2 = (int)(xEnd * imgWidth); + int w = Math.Max(1, x2 - x1); var cropRect = new Rectangle(x1, 0, w, rectHeight); using var cropped = image.Clone(ctx => ctx.Crop(cropRect)); - // 转为 Base64 using var ms = new MemoryStream(); cropped.Save(ms, new JpegEncoder()); + ms.Position = 0; + + string imgPath = Path.Combine(imgDir, $"crop_{i + 1}_{DateTime.Now:HHmmssfff}.jpg"); + File.WriteAllBytes(imgPath, ms.ToArray()); + string base64 = Convert.ToBase64String(ms.ToArray()); - // 调用人脸识别方法(假设返回姓名+头像Base64) - StudentInfoDto studentInfo = await FaceRecognitionAsync(base64); - studentInfos.Add(studentInfo); + Console.WriteLine($"[{i + 1}] 范围: {xStart:0.00}-{xEnd:0.00}, 宽: {w}"); + + // === 同步调用人脸识别接口 === + var result = FaceRecognitionAsync(base64); + + if (result != null) + studentInfos.Add(result); } return studentInfos; } + + /// /// 调用人脸识别接口,返回学生信息 /// - public static async Task FaceRecognitionAsync(string base64Image) + public static StudentInfoDto FaceRecognitionAsync(string base64Image) { var param = new { @@ -272,23 +295,26 @@ namespace Wpf_AiSportsMicrospace.Common string json = JsonSerializer.Serialize(param); // 调用 HttpPostAsync - string response = await HttpManager.HttpPostAsync(AppSettings.BackendUrl, json); + string response = HttpManager.HttpPost(AppSettings.BackendUrl, json); if (string.IsNullOrEmpty(response)) return null; try { - // 反序列化为接口返回对象 - var faceInfo = JsonSerializer.Deserialize(response, new JsonSerializerOptions + // 假设 response 是 JSON 字符串 + var apiResponse = JsonSerializer.Deserialize>(response, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - if (faceInfo == null) + // 取 data + var studentInfo = apiResponse.Data; + + if (studentInfo == null) return null; - return faceInfo; + return studentInfo; } catch { diff --git a/Wpf_AiSportsMicrospace/Dto/ApiResponse.cs b/Wpf_AiSportsMicrospace/Dto/ApiResponse.cs new file mode 100644 index 0000000..4c037f7 --- /dev/null +++ b/Wpf_AiSportsMicrospace/Dto/ApiResponse.cs @@ -0,0 +1,12 @@ + + +namespace Dto +{ + public class ApiResponse + { + public bool Status { get; set; } + public int Code { get; set; } + public string Message { get; set; } + public T Data { get; set; } + } +} diff --git a/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs b/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs index a7c1913..13317b0 100644 --- a/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs +++ b/Wpf_AiSportsMicrospace/Dto/MusicJumpRopeContext.cs @@ -67,6 +67,12 @@ namespace Dto Sports = new List(); } + // 更新点位方法 + public void UpdateCirclePositions(List<(double XNorm, double YNorm)> newPositions) + { + CirclePositions = newPositions; + } + // 更新排行榜方法 public List UpdateRankList() { diff --git a/Wpf_AiSportsMicrospace/Dto/StudentInfoDto.cs b/Wpf_AiSportsMicrospace/Dto/StudentInfoDto.cs index 882aa8a..3a682fe 100644 --- a/Wpf_AiSportsMicrospace/Dto/StudentInfoDto.cs +++ b/Wpf_AiSportsMicrospace/Dto/StudentInfoDto.cs @@ -9,13 +9,14 @@ namespace Dto /// public class StudentInfoDto { - public string Name { get; set; } + public string StudentName { get; set; } public string Photo { get; set; } - public string StudentNo { get; set; } - public string SchoolCode { get; set; } + public string StudentCode { get; set; } + public int GradeId { get; set; } + public int ClassId { get; set; } public string GradeName { get; set; } public string ClassName { get; set; } public int Age { get; set; } - public string Sex { get; set; } + public int Sex { get; set; } } } diff --git a/Wpf_AiSportsMicrospace/Resources/Img/gif/avatar.png b/Wpf_AiSportsMicrospace/Resources/Img/gif/avatar.png new file mode 100644 index 0000000..78d519f Binary files /dev/null and b/Wpf_AiSportsMicrospace/Resources/Img/gif/avatar.png differ diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs index 4d7d463..88cc654 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml.cs @@ -3,6 +3,8 @@ using Dto; using Emgu.CV.Flann; using Enum; using HandyControl.Controls; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using SharpDX.Direct3D9; using System; using System.Collections.Concurrent; @@ -60,36 +62,31 @@ namespace Wpf_AiSportsMicrospace.Views private readonly object _updateLock = new object(); private readonly Dictionary _jumpStatus = new Dictionary(); private GroupJumpRopeContext _groupJumpRopeContext; - public GroupJumpRope() + public GroupJumpRope() { InitializeComponent(); _detectQueue = new SportDetectionQueue(); Loaded += UserControl_Loaded; Unloaded += UserControl_Unloaded; _groupJumpRopeContext = new GroupJumpRopeContext(); - - // 将原来的赋值方式: - //_groupJumpRopeContext.CirclePositions = config != null ? config.sixJumpPoints : _groupJumpRopeContext.CirclePositions; - - // 修改为:先清空再添加元素(因为 CirclePositions 只有 get,没有 set) - //_groupJumpRopeContext.CirclePositions.Clear(); - //if (config != null && config.sixJumpPoints != null) - //{ - // foreach (var pt in config.sixJumpPoints) - // { - // _groupJumpRopeContext.CirclePositions.Add(pt); - // } - //} + } private async void UserControl_Loaded(object sender, RoutedEventArgs e) { - - // 绑定人脸识别事件 - _mainWin.FacialRecognitionEvent += async (sender, buffer) => - { - var studentList = await Utils.FacialRecognition(buffer, new double[] { 0.50, 0.78 }); - }; + + var app = (App)Application.Current; + using var scope = App.AppHost.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var cfg = await db.SixGroupPoints.FirstOrDefaultAsync(); + //var pointlist = JsonSerializer.Deserialize>(cfg!.PointsJson); + + List<(double XNorm, double YNorm)> pointlist = + [(cfg.point1.X, cfg.point1.Y),(cfg.point2.X, cfg.point2.Y), + (cfg.point3.X, cfg.point3.Y), (cfg.point4.X, cfg.point4.Y), + (cfg.point5.X, cfg.point5.Y),(cfg.point6.X, cfg.point6.Y)]; + _groupJumpRopeContext.CirclePositions = pointlist; + DrawCirclesWithText(); @@ -132,7 +129,7 @@ namespace Wpf_AiSportsMicrospace.Views HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Opacity = 0, - Margin = new Thickness(0, -100, 0, 0), + Margin = new Thickness(0, -100, 0, 0), }; // 增加图片的大小,调整比例 @@ -252,6 +249,37 @@ namespace Wpf_AiSportsMicrospace.Views //_lastUpdateTime = DateTime.Now; Utils.PlayBackgroundMusic("countdown_3.mp3", false); + + //开始人脸识别============================================= + EventHandler handler = null; + + handler = async (sender, buffer) => + { + double[] xValues = [.. _groupJumpRopeContext.CirclePositions.Select(p => p.XNorm)]; + + var studentList = Utils.FacialRecognition(buffer, xValues); + + if (studentList != null && studentList.Count > 0) + { + // 解绑事件 + _mainWin.FacialRecognitionEvent -= handler; + + for (int i = 0; i < studentList.Count; i++) + { + var student = studentList[i]; + if (student != null) + { + _groupJumpRopeContext.UserList[i].DisplayText = student.StudentName; + } + } + + } + + // 这里可以继续处理 studentList + }; + + // 绑定事件 + _mainWin.FacialRecognitionEvent += handler; } private void UpdateCountdown() diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml index a391f22..64fa8cf 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml @@ -38,9 +38,62 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs index 3a25992..fa6c266 100644 --- a/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs +++ b/Wpf_AiSportsMicrospace/Views/JumpRope/MusicJumpRope.xaml.cs @@ -1,10 +1,14 @@ -using Dto; +using AiSportsMicrospaceDB.DBContext; +using Dto; using Enum; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; +using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Windows; @@ -40,7 +44,7 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope private readonly Dictionary _jumpStatus = new Dictionary(); private MusicJumpRopeContext _musicJumpRopeContext; private GameState _currentGameState = GameState.NotStarted; - + private readonly HttpClient _httpClient; // 容忍时间(节拍误差) public double _beatTolerance = 0.24; // ±150ms @@ -77,6 +81,15 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope // await Task.Delay(1000); // 200ms 间隔,不卡UI线程 //} + + var app = (App)Application.Current; + using var scope = App.AppHost.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var cfg = await db.SixGroupPoints.FirstOrDefaultAsync(); + //var pointlist = JsonSerializer.Deserialize>(cfg!.PointsJson); + + List<(double XNorm, double YNorm)> pointlist = [(cfg.point3.X, cfg.point3.Y),(cfg.point5.X, cfg.point5.Y)]; + _musicJumpRopeContext.UpdateCirclePositions(pointlist); } private void UserControl_Unloaded(object sender, RoutedEventArgs e) @@ -229,6 +242,66 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope _lastUpdateTime = DateTime.Now; _mainWin.ShowCountDownAnimation(); Utils.PlayBackgroundMusic("countdown_3.mp3", false); + + //开始人脸识别============================================= + EventHandler handler = null; + + handler = async (sender, buffer) => + { + double[] xValues = [.. _musicJumpRopeContext.CirclePositions.Select(p => p.XNorm)]; + + var studentList = Utils.FacialRecognition(buffer, xValues); + + if (studentList != null && studentList.Count > 0) + { + // 解绑事件 + _mainWin.FacialRecognitionEvent -= handler; + name1.Text = studentList[0]?.StudentName ?? "一号玩家"; + name2.Text = studentList[1]?.StudentName ?? "二号玩家"; + if (studentList.Count > 1 && studentList[0].Photo != "") { + await LoadImageFromUrlAsync(studentList[0]?.Photo ?? "", avatar1); + } + if (studentList.Count > 2 && studentList[1].Photo != "") + { + await LoadImageFromUrlAsync(studentList[1]?.Photo ?? "", avatar2); + } + + + } + + // 这里可以继续处理 studentList + }; + + // 绑定事件 + _mainWin.FacialRecognitionEvent += handler; + } + + private async Task LoadImageFromUrlAsync(string imageUrl , Image image) + { + try + { + // 异步下载图片数据 + byte[] imageData = await _httpClient.GetByteArrayAsync(imageUrl); + + // 创建BitmapImage并设置源 + var bitmap = new BitmapImage(); + using (var stream = new MemoryStream(imageData)) + { + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; // 图片加载后缓存到内存 + bitmap.StreamSource = stream; + bitmap.EndInit(); // 确保EndInit,否则可能导致图片加载不完整 + } + bitmap.Freeze(); // 跨线程访问时可选 + + // 在UI线程上设置Image控件的Source + image.Source = bitmap; + } + catch (Exception ex) + { + // 处理异常(例如,网络错误、URL无效、图片格式不支持等) + //MessageBox.Show($"加载图片失败: {ex.Message}"); + } } private void UpdateCountdown()