update:本地化&人脸

This commit is contained in:
ltx 2025-11-18 15:08:03 +08:00
commit c7979ae8f9
15 changed files with 306 additions and 75 deletions

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AiSportsMicrospaceDB.DBContext namespace AiSportsMicrospaceDB.DBContext
@ -15,25 +16,9 @@ namespace AiSportsMicrospaceDB.DBContext
{ {
} }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SixGroupPoints>().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> BasicConfig { get; set; } public DbSet<BasicConfig> BasicConfig { get; set; }
public DbSet<SixGroupPoints> SixGroupPoints { get; set; } public DbSet<SixGroupPoints> SixGroupPoints { get; set; }
} }
} }

View File

@ -15,10 +15,4 @@ namespace AiSportsMicrospaceDB.Entities
public string UserName { get; set; } public string UserName { get; set; }
public string Password { get; set; } public string Password { get; set; }
} }
public class SixGroupPoints {
// ✅ 主键
public int Id { get; set; }
public List<(double,double)> points { get; set; }
}
} }

View File

@ -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; }
}
}

View File

@ -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; }
}
}

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,6 @@ namespace Common
public static class AppSettings public static class AppSettings
{ {
public static string SchoolCode = "202501060001"; public static string SchoolCode = "202501060001";
public static string BackendUrl = "http://localhost:9992/api/StudentFace"; public static string BackendUrl = "http://localhost:9991/api/StudentFace";
} }
} }

View File

@ -5,7 +5,7 @@ namespace Wpf_AiSportsMicrospace.Common
{ {
public class HttpManager public class HttpManager
{ {
public static async Task<string> HttpPostAsync(string url, string postData = null, string contentType = "application/json", int timeOut = 30, Dictionary<string, string> headers = null) public static string HttpPost(string url, string postData = null, string contentType = "application/json", int timeOut = 30, Dictionary<string, string> headers = null)
{ {
using (var client = new HttpClient()) using (var client = new HttpClient())
{ {
@ -27,9 +27,10 @@ namespace Wpf_AiSportsMicrospace.Common
try try
{ {
var response = await client.PostAsync(url, content); // 注意:要调用 PostAsync 而不是 po()
var response = client.PostAsync(url, content).Result; // 同步等待
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(); return response.Content.ReadAsStringAsync().Result;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -37,6 +38,7 @@ namespace Wpf_AiSportsMicrospace.Common
} }
} }
} }
public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null) public static async Task<string> HttpGetAsync(string url, Dictionary<string, string> headers = null)
{ {
using (var client = new HttpClient()) using (var client = new HttpClient())

View File

@ -214,10 +214,10 @@ namespace Wpf_AiSportsMicrospace.Common
return _cache.GetValueOrDefault(relativePath); return _cache.GetValueOrDefault(relativePath);
} }
public static async Task<List<StudentInfoDto>> FacialRecognition(byte[] buffer, double[] xNorms) public static List<StudentInfoDto> FacialRecognition(byte[] buffer, double[] xNorms)
{ {
if (xNorms == null || xNorms.Length < 2) if (xNorms == null || xNorms.Length == 0)
throw new ArgumentException("xNorms 至少需要两个元素"); throw new ArgumentException("xNorms 不能为空");
using var image = SharpImage.Image.Load<Rgba32>(buffer); using var image = SharpImage.Image.Load<Rgba32>(buffer);
@ -225,43 +225,66 @@ namespace Wpf_AiSportsMicrospace.Common
int imgHeight = image.Height; int imgHeight = image.Height;
int rectHeight = imgHeight / 2; int rectHeight = imgHeight / 2;
// 计算分割边界 // 1. 排序中心点
var sortedX = xNorms.OrderBy(x => x).ToList(); var sortedX = xNorms.OrderBy(x => x).ToList();
int faceCount = sortedX.Count;
// 2. 计算分割边界
var splitXs = new List<double>(); var splitXs = new List<double>();
for (int i = 0; i < sortedX.Count - 1; i++) for (int i = 0; i < sortedX.Count - 1; i++)
splitXs.Add((sortedX[i] + sortedX[i + 1]) / 2.0); splitXs.Add((sortedX[i] + sortedX[i + 1]) / 2.0);
// 3. 起止边界
splitXs.Insert(0, 0.0); splitXs.Insert(0, 0.0);
splitXs.Add(1.0); splitXs.Add(1.0);
// 4. 输出目录
string imgDir = Path.Combine(AppContext.BaseDirectory, "img");
Directory.CreateDirectory(imgDir);
var studentInfos = new List<StudentInfoDto>(); var studentInfos = new List<StudentInfoDto>();
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); double xStart = splitXs[i];
int x2 = (int)(splitXs[i + 1] * imgWidth); double xEnd = splitXs[i + 1];
int w = x2 - x1;
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); var cropRect = new Rectangle(x1, 0, w, rectHeight);
using var cropped = image.Clone(ctx => ctx.Crop(cropRect)); using var cropped = image.Clone(ctx => ctx.Crop(cropRect));
// 转为 Base64
using var ms = new MemoryStream(); using var ms = new MemoryStream();
cropped.Save(ms, new JpegEncoder()); 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()); string base64 = Convert.ToBase64String(ms.ToArray());
// 调用人脸识别方法(假设返回姓名+头像Base64 Console.WriteLine($"[{i + 1}] 范围: {xStart:0.00}-{xEnd:0.00}, 宽: {w}");
StudentInfoDto studentInfo = await FaceRecognitionAsync(base64);
studentInfos.Add(studentInfo); // === 同步调用人脸识别接口 ===
var result = FaceRecognitionAsync(base64);
if (result != null)
studentInfos.Add(result);
} }
return studentInfos; return studentInfos;
} }
/// <summary> /// <summary>
/// 调用人脸识别接口,返回学生信息 /// 调用人脸识别接口,返回学生信息
/// </summary> /// </summary>
public static async Task<StudentInfoDto> FaceRecognitionAsync(string base64Image) public static StudentInfoDto FaceRecognitionAsync(string base64Image)
{ {
var param = new var param = new
{ {
@ -272,23 +295,26 @@ namespace Wpf_AiSportsMicrospace.Common
string json = JsonSerializer.Serialize(param); string json = JsonSerializer.Serialize(param);
// 调用 HttpPostAsync // 调用 HttpPostAsync
string response = await HttpManager.HttpPostAsync(AppSettings.BackendUrl, json); string response = HttpManager.HttpPost(AppSettings.BackendUrl, json);
if (string.IsNullOrEmpty(response)) if (string.IsNullOrEmpty(response))
return null; return null;
try try
{ {
// 反序列化为接口返回对象 // 假设 response 是 JSON 字符串
var faceInfo = JsonSerializer.Deserialize<StudentInfoDto>(response, new JsonSerializerOptions var apiResponse = JsonSerializer.Deserialize<ApiResponse<StudentInfoDto>>(response, new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}); });
if (faceInfo == null) // 取 data
var studentInfo = apiResponse.Data;
if (studentInfo == null)
return null; return null;
return faceInfo; return studentInfo;
} }
catch catch
{ {

View File

@ -0,0 +1,12 @@

namespace Dto
{
public class ApiResponse<T>
{
public bool Status { get; set; }
public int Code { get; set; }
public string Message { get; set; }
public T Data { get; set; }
}
}

View File

@ -67,6 +67,12 @@ namespace Dto
Sports = new List<SportBase>(); Sports = new List<SportBase>();
} }
// 更新点位方法
public void UpdateCirclePositions(List<(double XNorm, double YNorm)> newPositions)
{
CirclePositions = newPositions;
}
// 更新排行榜方法 // 更新排行榜方法
public List<RankItem> UpdateRankList() public List<RankItem> UpdateRankList()
{ {

View File

@ -9,13 +9,14 @@ namespace Dto
/// </summary> /// </summary>
public class StudentInfoDto public class StudentInfoDto
{ {
public string Name { get; set; } public string StudentName { get; set; }
public string Photo { get; set; } public string Photo { get; set; }
public string StudentNo { get; set; } public string StudentCode { get; set; }
public string SchoolCode { get; set; } public int GradeId { get; set; }
public int ClassId { get; set; }
public string GradeName { get; set; } public string GradeName { get; set; }
public string ClassName { get; set; } public string ClassName { get; set; }
public int Age { get; set; } public int Age { get; set; }
public string Sex { get; set; } public int Sex { get; set; }
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -3,6 +3,8 @@ using Dto;
using Emgu.CV.Flann; using Emgu.CV.Flann;
using Enum; using Enum;
using HandyControl.Controls; using HandyControl.Controls;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SharpDX.Direct3D9; using SharpDX.Direct3D9;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@ -60,7 +62,7 @@ namespace Wpf_AiSportsMicrospace.Views
private readonly object _updateLock = new object(); private readonly object _updateLock = new object();
private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>(); private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>();
private GroupJumpRopeContext _groupJumpRopeContext; private GroupJumpRopeContext _groupJumpRopeContext;
public GroupJumpRope() public GroupJumpRope()
{ {
InitializeComponent(); InitializeComponent();
_detectQueue = new SportDetectionQueue(); _detectQueue = new SportDetectionQueue();
@ -68,28 +70,23 @@ namespace Wpf_AiSportsMicrospace.Views
Unloaded += UserControl_Unloaded; Unloaded += UserControl_Unloaded;
_groupJumpRopeContext = new GroupJumpRopeContext(); _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) private async void UserControl_Loaded(object sender, RoutedEventArgs e)
{ {
// 绑定人脸识别事件 var app = (App)Application.Current;
_mainWin.FacialRecognitionEvent += async (sender, buffer) => using var scope = App.AppHost.Services.CreateScope();
{ var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var studentList = await Utils.FacialRecognition(buffer, new double[] { 0.50, 0.78 }); var cfg = await db.SixGroupPoints.FirstOrDefaultAsync();
}; //var pointlist = JsonSerializer.Deserialize<List<(double, double)>>(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(); DrawCirclesWithText();
@ -252,6 +249,37 @@ namespace Wpf_AiSportsMicrospace.Views
//_lastUpdateTime = DateTime.Now; //_lastUpdateTime = DateTime.Now;
Utils.PlayBackgroundMusic("countdown_3.mp3", false); Utils.PlayBackgroundMusic("countdown_3.mp3", false);
//开始人脸识别=============================================
EventHandler<byte[]> 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() private void UpdateCountdown()

View File

@ -38,9 +38,62 @@
</Grid> </Grid>
<!--进度条--> <!--进度条-->
<Grid VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,155,0,0"> <Grid VerticalAlignment="Top" HorizontalAlignment="Center" Margin="0,95,0,0">
<TextBlock x:Name="score1" Text="0" FontSize="70" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Margin="-220,15" TextAlignment="Center" HorizontalAlignment="Left" Width="200" /> <Grid HorizontalAlignment="Left" Margin="-220,15">
<TextBlock x:Name="score2" Text="0" FontSize="70" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Margin="-220,15" TextAlignment="Center" HorizontalAlignment="Right" Width="200" /> <TextBlock x:Name="score1" Text="0" FontSize="50" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Width="200" Margin="0,15,0,0" TextAlignment="Center" RenderTransformOrigin="0.5,0.5" >
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="1.31"/>
<TranslateTransform/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
<Image Source="/Resources/Img/gif/avatar.png" HorizontalAlignment="Center" x:Name="avatar1" Width="100" Margin="0,15,0,0" >
<Image.Clip>
<EllipseGeometry Center="50,50" RadiusX="50" RadiusY="50"/>
</Image.Clip>
</Image>
<TextBlock x:Name="name1" Text="一号玩家" FontSize="40" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Width="200" Margin="0,180,0,0" TextAlignment="Center" VerticalAlignment="Top" Height="54" RenderTransformOrigin="0.5,0.5" >
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform AngleY="0"/>
<RotateTransform/>
<TranslateTransform Y="0"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
<Grid HorizontalAlignment="Right" Margin="-220,15">
<TextBlock x:Name="score2" Text="0" FontSize="50" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Width="200" Margin="0,15,0,0" TextAlignment="Center" RenderTransformOrigin="0.5,0.5" >
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform Angle="1.31"/>
<TranslateTransform/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
<Image Source="/Resources/Img/gif/avatar.png" HorizontalAlignment="Center" x:Name="avatar2" Width="100" Margin="0,15,0,0" >
<Image.Clip>
<EllipseGeometry Center="50,50" RadiusX="50" RadiusY="50"/>
</Image.Clip>
</Image>
<TextBlock x:Name="name2" Text="二号玩家" FontSize="40" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Width="200" Margin="0,180,0,0" TextAlignment="Center" VerticalAlignment="Top" Height="54" RenderTransformOrigin="0.5,0.5" >
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform AngleY="0"/>
<RotateTransform/>
<TranslateTransform Y="0"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
<!--<TextBlock x:Name="score2" Text="0" FontSize="70" FontWeight="UltraBold" FontStyle="Italic" Foreground="#fff" Margin="-220,15" TextAlignment="Center" HorizontalAlignment="Right" Width="200" />-->
<local:WxProgressBar x:Name="PkBar"/> <local:WxProgressBar x:Name="PkBar"/>
</Grid> </Grid>

View File

@ -1,10 +1,14 @@
using Dto; using AiSportsMicrospaceDB.DBContext;
using Dto;
using Enum; using Enum;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Windows; using System.Windows;
@ -40,7 +44,7 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>(); private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>();
private MusicJumpRopeContext _musicJumpRopeContext; private MusicJumpRopeContext _musicJumpRopeContext;
private GameState _currentGameState = GameState.NotStarted; private GameState _currentGameState = GameState.NotStarted;
private readonly HttpClient _httpClient;
// 容忍时间(节拍误差) // 容忍时间(节拍误差)
public double _beatTolerance = 0.24; // ±150ms public double _beatTolerance = 0.24; // ±150ms
@ -77,6 +81,15 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
// await Task.Delay(1000); // 200ms 间隔不卡UI线程 // await Task.Delay(1000); // 200ms 间隔不卡UI线程
//} //}
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var cfg = await db.SixGroupPoints.FirstOrDefaultAsync();
//var pointlist = JsonSerializer.Deserialize<List<(double, double)>>(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) private void UserControl_Unloaded(object sender, RoutedEventArgs e)
@ -229,6 +242,66 @@ namespace Wpf_AiSportsMicrospace.Views.JumpRope
_lastUpdateTime = DateTime.Now; _lastUpdateTime = DateTime.Now;
_mainWin.ShowCountDownAnimation(); _mainWin.ShowCountDownAnimation();
Utils.PlayBackgroundMusic("countdown_3.mp3", false); Utils.PlayBackgroundMusic("countdown_3.mp3", false);
//开始人脸识别=============================================
EventHandler<byte[]> 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() private void UpdateCountdown()