2025-10-11 14:38:02 +08:00

497 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Emgu.CV.Reg;
using SharpDX.Direct3D9;
using SkiaSharp;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.IO;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
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.Service;
using Yztob.AiSports.Inferences.Abstractions;
using Yztob.AiSports.Inferences.Things;
using Yztob.AiSports.Postures.Sports;
using Yztob.AiSports.Postures.Things;
using Yztob.AiSports.Sensors.Abstractions;
using Yztob.AiSports.Sensors.Things;
namespace Wpf_AiSportsMicrospace;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
#region
private IHumanPredictor _humanPredictor;
private IObjectDetector _objectDetector;
private HumanGraphicsRenderer _humanGraphicsRenderer;
private readonly List<SportDescriptor> _sports;
private SportBase _sport;
private readonly SportDetectionQueue _detectQueue;
private WriteableBitmap _videoBitmap;
private int _lastFrameNumber = -1;
ConfigService configService = new ConfigService();
#endregion
public MainWindow()
{
InitializeComponent();
// 输出到控制台
_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.SingleHigh);
_objectDetector = ObjectDetectorFactory.CreateSportGoodsDetector();
_humanGraphicsRenderer = new HumanGraphicsRenderer();
_humanGraphicsRenderer.DrawLabel = false;
_sports = SportBase.GetSports();
_detectQueue = new SportDetectionQueue();
this.sportList.ItemsSource = _sports;
this.sportList.SelectedIndex = 0;
this.OnSelectedSport();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
DrawCirclesWithText();
}, DispatcherPriority.Loaded);
videoImage.SizeChanged += (s, ev) =>
{
DrawCirclesWithText();
};
}
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)
{
this.OnSelectedSport();
}
private void OnSelectedSport()
{
var sport = SportBase.Create(_sports[this.sportList.SelectedIndex].Key);
sport.OnTicked += this.OnSportTick;
if (_sport?.IsCounting == true)
sport.Start();
_sport?.Stop();
_detectQueue.Stop();
_sport = sport;
_detectQueue.Sport = _sport;
}
private void ping_Click(object sender, RoutedEventArgs e)
{
var host = this.host.Text.Trim();
if (string.IsNullOrWhiteSpace(host))
{
this.ShowError("相机主机地址/IP不能为空。");
return;
}
var pinger = new Ping();
var reply = pinger.Send(host, 10);
if (reply.Status == IPStatus.Success)
this.ShowInformation("与相机通信正常。");
else
this.ShowError($"与相机通信失败,错误:{reply.Status}。");
}
private void startOrStop_Click(object sender, RoutedEventArgs e)
{
var host = this.host.Text.Trim();
if (string.IsNullOrWhiteSpace(host))
{
this.ShowError("相机主机地址/IP不能为空。");
return;
}
var userName = this.userName.Text.Trim();
if (string.IsNullOrWhiteSpace(userName))
{
this.ShowError("用户名不能为空。");
return;
}
var password = this.password.Text.Trim();
if (string.IsNullOrWhiteSpace(password))
{
this.ShowError("密码不能为空。");
return;
}
var port = 554u;//RTSP协议默认端口是554
var tempPath = "./temps";
if (!Directory.Exists(tempPath))
Directory.CreateDirectory(tempPath);
_sport.Reset();
_sport.Start();
_detectQueue.Start();
var _webcamClient = WebcamClient.CreateRTSP(host, userName, password, port);
//处理抽帧回调
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
_webcamClient.StartExtract();//开始抽帧
this.startOrStop.Content = "停止(&S)";
this.sportCounts.Text = "0";
this.sportTimes.Text = "00'00\"";
}
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();
//await File.WriteAllBytesAsync($"./temps/{frame.Number}.jpg", buffer);
// === 显示到 WPF ===
BitmapImage bitmap = null;
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(); // 跨线程安全
});
// UI 线程显示
Application.Current.Dispatcher.Invoke(() =>
{
if (videoImage == null) return;
if (_videoBitmap == null ||
_videoBitmap.PixelWidth != bitmap.PixelWidth ||
_videoBitmap.PixelHeight != bitmap.PixelHeight)
{
_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();
});
//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));
//var human = humanResult?.Humans?.FirstOrDefault();
//_detectQueue.Enqueue(frame.Number, human, null);
}
private void OnSportTick(int counts, int times)
{
var ts = TimeSpan.FromSeconds(times);
// 使用 Dispatcher 调用 UI 更新
this.Dispatcher.BeginInvoke(new Action(() =>
{
this.sportCounts.Text = _sport.GetFormatCounts(); // counts.ToString();
this.sportTimes.Text = _sport.GetFormatTimes(); // ts.ToString(@"mm\'ss\"");
// 如果需要触发停止逻辑
//if (!_sport.IsCounting)
// this.startOrStop_Click(null, null);
}));
//VoiceBroadcast.PlayTick();
}
private void DrawJumpRope3DPointsWithGlow()
{
if (videoImage == null || overlayCanvas == null) return;
configService.LoadAllConfigs(); // 从文件加载 ConfigDic
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;
bool needSaveConfig = false;
if (!configService.ConfigDic.TryGetValue("rope-skipping", out jumpRopeConfig) || jumpRopeConfig.Points.Count == 0)
{
// 没有配置,则生成默认配置
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);
}
}
// 保存配置(如果是新生成的)
if (needSaveConfig)
{
configService.ConfigDic[jumpRopeConfig.Name] = jumpRopeConfig;
configService.SaveAllConfigs();
}
}
/// <summary>
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
/// </summary>
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 * 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, 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) // 外部透明
}
}
};
// 定位到中心Y 要根据压缩高度来调整)
Canvas.SetLeft(ellipse, centerX - radius);
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;
}
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);
}
}
/// <summary>
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
/// </summary>
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);
}
}