497 lines
16 KiB
C#
497 lines
16 KiB
C#
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);
|
||
}
|
||
} |