2025-10-14 13:51:02 +08:00

522 lines
18 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.Flann;
using HandyControl.Controls;
using SharpDX.Direct3D9;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Shapes;
using System.Windows.Threading;
using Wpf_AiSportsMicrospace.Common;
using Wpf_AiSportsMicrospace.Dto;
using Wpf_AiSportsMicrospace.Enum;
using Wpf_AiSportsMicrospace.MyUserControl;
using Wpf_AiSportsMicrospace.Service;
using WpfAnimatedGif;
using Yztob.AiSports.Common;
using Yztob.AiSports.Common.Implement;
using Yztob.AiSports.Inferences.Abstractions;
using Yztob.AiSports.Inferences.Things;
using Yztob.AiSports.Postures.Sports;
using Yztob.AiSports.Sensors.Abstractions;
using Yztob.AiSports.Sensors.Things;
namespace Wpf_AiSportsMicrospace.Views
{
/// <summary>
/// GroupJumpRope.xaml 的交互逻辑
/// </summary>
public partial class GroupJumpRope : UserControl
{
private List<SportBase> sports = new();
private List<TextBlock> circleTexts = new();
private List<SportUserItem> userList = new();
private List<string> userNumberList = ["0", "0", "0", "0", "0", "0"];
private double[] circlePositionsX = { 0.07, 0.21, 0.36, 0.50, 0.64, 0.78, 0.92 };
private Main _mainWin => Application.Current.MainWindow as Main;
private List<(double XNorm, double YNorm)> circlePositions = new();
private MediaPlayer _mediaPlayer = new MediaPlayer();
private bool IsGameStarted = false;
List<string> _useName = ["一号位", "四号位", "二号位", "五号位", "三号位", "六号位",];
public GroupJumpRope()
{
InitializeComponent();
Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded;
}
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
{
DrawCirclesWithText();
// 播放音乐
PlayMusic("raisehand.mp3");
}
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
{
_mainWin.HumanFrameUpdated -= OnHumanFrameUpdated;
}
private void PlayMusic(string musicFileName)
{
// 获取项目根目录
string projectRoot = System.IO.Path.Combine(AppContext.BaseDirectory, @"..\..\..");
string musicPath = System.IO.Path.Combine(projectRoot, "Resources", "Music", musicFileName);
string imgPath = System.IO.Path.Combine(projectRoot, "Resources", "Img", "提示图.png");
if (!File.Exists(musicPath))
{
Console.WriteLine($"音乐文件不存在: {musicPath}");
return;
}
_mediaPlayer.Open(new Uri(musicPath, UriKind.Absolute));
ShowCenterTip(imgPath, TimeSpan.FromSeconds(3));
// 监听播放完成事件
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
_mediaPlayer.Play();
}
private void ShowCenterTip(string imagePath, TimeSpan duration)
{
var tipImage = new Image
{
Source = new BitmapImage(new Uri(imagePath, UriKind.Absolute)),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Opacity = 0,
Margin = new Thickness(0, -100, 0, 0),
};
// 增加图片的大小,调整比例
tipImage.Width = 1920 * 0.9; // 宽度为 Canvas 宽度的 90%
tipImage.Height = 1080 * 0.6; // 高度为 Canvas 高度的 60%
// 将图片添加到 Overlay Canvas
userBox.Children.Add(tipImage);
// 渐变出现动画
var fadeInAnimation = new DoubleAnimation
{
From = 0,
To = 1,
Duration = TimeSpan.FromSeconds(1.5)
};
tipImage.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
// 定时移除,并且渐变消失
Task.Delay(duration).ContinueWith(_ =>
{
Dispatcher.Invoke(() =>
{
// 渐变消失动画
var fadeOutAnimation = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromSeconds(1.5)
};
tipImage.BeginAnimation(UIElement.OpacityProperty, fadeOutAnimation);
// 完成后移除图片
fadeOutAnimation.Completed += (s, e) =>
{
userBox.Children.Remove(tipImage);
};
});
});
}
private void MediaPlayer_MediaEnded(object sender, EventArgs e)
{
// 音乐播放完成后的逻辑
Console.WriteLine("音乐播放完成!");
// 可在这里绑定抽帧事件
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
}
private void OnHumanFrameUpdated(object sender, List<Human> humans)
{
try
{
if (!IsGameStarted)
{
if (humans == null || humans.Count == 0) return;
int wavingaction = 0;
foreach (var human in humans)
{
wavingaction = DetectRightHandRaise(human);
if (wavingaction < 3)
continue;
switch (wavingaction)
{
case (int)WavingAction.FirstHand:
// 第一次举手,初始化倒计时
StartCountdown(3);
break;
case (int)WavingAction.Raising:
// 持续倒计时中
UpdateCountdown();
break;
case (int)WavingAction.RaiseHand:
// 举手完成,倒计时结束
FinishCountdown();
break;
default:
// 没检测到动作,重置倒计时显示
Utils.StopBackgroundMusic();
countdownText.Text = "3";
countdownGrid.Visibility = Visibility.Hidden;
break;
}
}
}
else
{
UpdateCircleCounts(humans);
}
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
private int _currentCountdown = 3;
private DateTime _lastUpdateTime = DateTime.Now;
private void StartCountdown(int start = 3)
{
_currentCountdown = start;
countdownText.Text = _currentCountdown.ToString();
countdownGrid.Visibility = Visibility.Visible;
_lastUpdateTime = DateTime.Now;
Utils.PlayBackgroundMusic("countdown_3.mp3", false);
}
private void UpdateCountdown()
{
if ((DateTime.Now - _lastUpdateTime).TotalSeconds >= 1)
{
_lastUpdateTime = DateTime.Now;
_currentCountdown--;
if (_currentCountdown > 0)
{
countdownText.Text = _currentCountdown.ToString();
}
else
{
FinishCountdown();
}
}
}
private async void FinishCountdown()
{
countdownText.Text = "GO!";
countdownGrid.Visibility = Visibility.Hidden;
IsGameStarted = true;
// 播放背景音乐(循环)
Utils.PlayBackgroundMusic("homeprojectselected1.mp3", true);
// 启动60秒倒计时独立任务
StartGameCountdown(60);
}
private async void StartGameCountdown(int seconds)
{
countdownGrid.Visibility = Visibility.Visible;
for (int i = seconds; i >= 0; i--)
{
countdownText.Text = i.ToString();
await Task.Delay(1000);
}
countdownGrid.Visibility = Visibility.Hidden;
userList.ForEach(x =>
{
x.ImageState = "1";
});
IsGameStarted = false;
Utils.StopBackgroundMusic();
}
private DateTime? _raiseStartTime;
private DateTime? _wristStartTime;
private bool _firstHandTriggered;
private int _lastCountdownSecond = 3;
private DateTime? _countdownStartTime;
public int DetectRightHandRaise(Human human)
{
var rightWrist = human.Keypoints.FirstOrDefault(k => k.Name == "right_wrist");
var rightElbow = human.Keypoints.FirstOrDefault(k => k.Name == "right_elbow");
const double raiseThreshold = 60; // 举手阈值
const double holdDuration = 1; // 持续 2 秒触发倒计时
const int countdownSeconds = 3; // 倒计时长度
bool handRaised = false;
if (rightWrist != null && rightElbow != null)
{
double verticalRise = rightElbow.Y - rightWrist.Y;
handRaised = verticalRise >= raiseThreshold;
}
// ---------- 第一步:持续举手触发倒计时 ----------
if (!_firstHandTriggered)
{
if (handRaised)
{
_raiseStartTime ??= DateTime.Now;
var holdElapsed = (DateTime.Now - _raiseStartTime.Value).TotalSeconds;
if (holdElapsed >= holdDuration)
{
// 举手达到 2 秒,启动倒计时
_firstHandTriggered = true;
_countdownStartTime = DateTime.Now;
_lastCountdownSecond = countdownSeconds;
return (int)WavingAction.FirstHand;
}
}
else
{
// 手放下,重置举手开始时间
_raiseStartTime = DateTime.Now;
}
return (int)WavingAction.None; // 倒计时未开始
}
// ---------- 第二步:倒计时逻辑(独立于手状态) ----------
var countdownElapsed = (DateTime.Now - _countdownStartTime.Value).TotalSeconds;
int currentSecond = countdownSeconds - (int)Math.Floor(countdownElapsed);
if (currentSecond > 0 && currentSecond != _lastCountdownSecond)
{
_lastCountdownSecond = currentSecond;
Console.WriteLine($"倒计时:{currentSecond}");
return (int)WavingAction.Raising; // 倒计时中
}
if (countdownElapsed >= countdownSeconds)
{
ResetRaiseState();
return (int)WavingAction.RaiseHand; // 举手完成
}
return (int)WavingAction.Raising; // 倒计时中
}
private void ResetRaiseState()
{
_raiseStartTime = null;
_wristStartTime = null;
_countdownStartTime = null;
_firstHandTriggered = false;
_lastCountdownSecond = 3;
}
private Dictionary<int, DateTime> _lastJumpUpdateTime = new(); // 用于存储最后更新时间
private Dictionary<int, DateTime> _lastUIUpdateTime = new(); // 用于防抖处理
public Human LocateHuman(List<Human> humans, double frameWidth, double frameHeight, int circleIndex)
{
if (humans == null || humans.Count == 0)
{
return null;
}
double circleX = circlePositions[circleIndex].XNorm;
double circleY = circlePositions[circleIndex].YNorm;
double radiusNormX = 0.073;
double radiusNormY = 0.135;
Human inCircleHuman = null;
bool isOut = false;
foreach (var hu in humans)
{
var rightFoot = hu.Keypoints.FirstOrDefault(k => k.Name == "right_ankle");
var leftFoot = hu.Keypoints.FirstOrDefault(k => k.Name == "left_ankle");
if (rightFoot == null || leftFoot == null)
continue;
double xRightNorm = rightFoot.X / frameWidth;
double xLeftNorm = leftFoot.X / frameWidth;
double yRightNorm = rightFoot.Y / frameHeight;
double yLeftNorm = leftFoot.Y / frameHeight;
bool outOfCircle =
xRightNorm < (circleX - radiusNormX) || xRightNorm > (circleX + radiusNormX) ||
xLeftNorm < (circleX - radiusNormX) || xLeftNorm > (circleX + radiusNormX) ||
yRightNorm < (circleY - radiusNormY) || yRightNorm > (circleY + radiusNormY) ||
yLeftNorm < (circleY - radiusNormY) || yLeftNorm > (circleY + radiusNormY);
if (outOfCircle)
{
isOut = true;
}
else if (inCircleHuman == null)
{
inCircleHuman = hu;
}
}
if (isOut)
userList[circleIndex].ImageState = "3";
else
{
//if (userList[circleIndex].ImageState == "3")
//{
// userList[circleIndex].ImageState = "1";
//}
// 检查当前编号是否未变化超过 2 秒
if (userNumberList[circleIndex] == userList[circleIndex].NumberText)
{
// 获取上次变化时间
if (!_lastJumpUpdateTime.TryGetValue(circleIndex, out var lastTime))
lastTime = DateTime.Now; // 默认当前时间
var elapsed = (DateTime.Now - lastTime).TotalSeconds;
if (elapsed > 1)
{
userList[circleIndex].ImageState = "1";
_lastJumpUpdateTime[circleIndex] = DateTime.Now;
}
// 否则2 秒内仍在变化,不更新状态
}
else
{
// 编号发生变化,更新记录,并保持状态不变
userNumberList[circleIndex] = userList[circleIndex].NumberText;
_lastJumpUpdateTime[circleIndex] = DateTime.Now;
}
}
return inCircleHuman;
}
private void DrawCirclesWithText()
{
userBox.Children.Clear();
sports.Clear();
circleTexts.Clear();
userList.Clear(); // 清空用户控件列表
double imgWidth = userBox.ActualWidth;
double imgHeight = userBox.ActualHeight;
double radius = 100;
// 每个圆的位置X 和 Y 都归一化 0~1
circlePositions = new List<(double XNorm, double YNorm)>
{
//(0.07, 0.58),
(0.21, 0.88 ),
(0.36, 0.68 ),
(0.50, 0.88),
(0.64, 0.68 ),
(0.78, 0.88),
(0.92, 0.68 )
};
for (int i = 0; i < circlePositions.Count; i++)
{
var pos = circlePositions[i];
double x = pos.XNorm * imgWidth;
double y = pos.YNorm * imgHeight;
// 绘制发光圆
//AddGlowEllipse(x, y, overlayCanvas);
var userItem = AddUserItem(x, y, i);
// 绑定运动对象
var sport = SportBase.Create("rope-skipping");
int indexCopy = i;
var currentItem = userItem;
// 订阅事件
sport.OnTicked += (count, times) =>
{
currentItem.NumberText = count.ToString();
if (currentItem.ImageState != "2")
{
currentItem.ImageState = "2";
}
};
sport.PointThreshold = 0.03f;
sport.Start();
sports.Add(sport);
}
}
private void UpdateCircleCounts(List<Human> humans)
{
// 在后台线程运行检测逻辑
//Parallel.For(0, circlePositions.Count, i =>
//{
// var human = LocateHuman(humans, userBox.ActualWidth, userBox.ActualHeight, i);
// sports[i].Pushing(human);
//});
for (int i = 0; i < circlePositions.Count; i++)
{
var human = LocateHuman(humans, userBox.ActualWidth, userBox.ActualHeight, i);
if (human != null)
{
sports[i].Pushing(human);
}
}
}
/// <summary>
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
/// </summary>
private SportUserItem AddUserItem(double centerX, double centerY, int index)
{
var userItem = new SportUserItem();
userItem.Width = 270;
userItem.Height = 560;
userItem.DisplayText = _useName[index];
userItem.VerticalAlignment = VerticalAlignment.Top;
userItem.HorizontalAlignment = HorizontalAlignment.Left;
userItem.ImageState = "1";
//userItem.Margin = new Thickness(centerX - (index == 0 ? 80 : index == 6 ? 190 : 135), centerY - 540, 0, 0);
userItem.Margin = new Thickness(centerX - 265, centerY - 500, 0, 0);
userBox.Children.Add(userItem);
userList.Add(userItem);
return userItem; //
}
}
}