2025-10-17 14:18:01 +08:00

695 lines
27 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 Dto;
using Enum;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
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.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Views.JumpRope;
using Wpf_AiSportsMicrospace.Common;
using Wpf_AiSportsMicrospace.Enum;
using Wpf_AiSportsMicrospace.MyUserControl;
using Wpf_AiSportsMicrospace.Views;
using Yztob.AiSports.Inferences.Things;
using Yztob.AiSports.Postures.Sports;
namespace Wpf_AiSportsMicrospace.Views.JumpRope
{
/// <summary>
/// MusicJumpRope.xaml 的交互逻辑
/// </summary>
public partial class MusicJumpRope : UserControl
{
private Main _mainWin => Application.Current.MainWindow as Main;
private MediaPlayer _mediaPlayer = new MediaPlayer();
private readonly object _updateLock = new object();
private readonly Dictionary<int, (string lastNumber, DateTime lastChangeTime, string currentState)> _jumpStatus = new Dictionary<int, (string, DateTime, string)>();
private MusicJumpRopeContext _musicJumpRopeContext;
public Dictionary<int, List<double>> _musicBeatsDic;
private GameState _currentGameState = GameState.NotStarted;
List<RankItem> RankingItemList = new();
private List<double> _musicBeats = new List<double>()
{
0.545,1.09,1.635,2.18,2.725,3.27,3.815,4.36,4.905,5.45,
5.995,6.54,7.085,7.63,8.175,8.72,9.265,9.81,10.355,10.9,
11.445,11.99,12.535,13.08,13.625,14.17,14.715,15.26,15.805,16.35,
16.895,17.44,17.985,18.53,19.075,19.62,20.165,20.71,21.255,21.8,
22.345,22.89,23.435,23.98,24.525,25.07,25.615,26.16,26.705,27.25,
27.795,28.34,28.885,29.43,29.975,30.52,31.065,31.61,32.155,32.7,
33.245,33.79,34.335,34.88,35.425,35.97,36.515,37.06,37.605,38.15,
38.695,39.24,39.785,40.33,40.875,41.42,41.965,42.51,43.055,43.6,
44.145,44.69,45.235,45.78,46.325,46.87,47.415,47.96,48.505,49.05,
49.595,50.14,50.685,51.23,51.775,52.32,52.865,53.41,53.955,54.5,
55.045,55.59,56.135,56.68,57.225,57.77,58.315,58.86,59.405,59.95,
60.495,61.04,61.585,62.13,62.675,63.22,63.765,64.31,64.855,65.4,
65.945,66.49,67.035,67.58,68.125,68.67,69.215,69.76,70.305,70.85,
71.395,71.94,72.485,73.03,73.575,74.12,74.665,75.21,75.755,76.3,
76.845,77.39,77.935,78.48,79.025,79.57,80.115,80.66,81.205,81.75,
82.295,82.84,83.385,83.93,84.475,85.02,85.565,86.11,86.655,87.2,
87.745,88.29,88.835,89.38,89.925,90.47,91.015,91.56,92.105,92.65,
93.195,93.74,94.285,94.83,95.375,95.92,96.465,97.01,97.555,98.1,
98.645,99.19,99.735,100.28,100.825,101.37,101.915,102.46,103.005,103.55,
104.095,104.64,105.185,105.73,106.275,106.82,107.365,107.91,108.455
};
private List<TextBlock> _musicBeatTextBlock = new List<TextBlock>();
// 容忍时间(节拍误差)
public double _beatTolerance = 0.15; // ±150ms
private int _totalDots = 0;
// 滚动显示的节拍点集合
public ObservableCollection<BeatItem> BeatDisplayLeft { get; set; } = new();
public ObservableCollection<BeatItem> BeatDisplayRight { get; set; } = new();
public MusicJumpRope()
{
InitializeComponent();
Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded;
_musicJumpRopeContext = new MusicJumpRopeContext();
_musicBeatsDic = _musicJumpRopeContext.MusicBeatsDic;
_totalDots = _musicJumpRopeContext.MusicBeats["1"].Count();
// 左控件
LeftBeats.DotCount = _totalDots;
LeftBeats.MusicBeatsDic = _musicBeatsDic;
// 右控件
RightBeats.DotCount = _totalDots;
RightBeats.MusicBeatsDic = _musicBeatsDic;
}
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 (humans == null || humans.Count == 0) return;
switch (_currentGameState)
{
case GameState.NotStarted: // 未开始
int rightWaving = DetectRightHandRaise(humans);
if (rightWaving >= 3)
{
switch (rightWaving)
{
case (int)WavingAction.FirstHand:
StartCountdown(3);
break;
case (int)WavingAction.Raising:
UpdateCountdown();
break;
case (int)WavingAction.RaiseHand:
FinishCountdown();
_currentGameState = GameState.Running; // 倒计时完成 → 游戏开始
break;
}
}
break;
case GameState.Running: // 游戏进行中
UpdateCircleCounts(humans);
// 可选:判断游戏结束条件,将状态切换到 Finished
// if (CheckGameFinished()) _currentGameState = GameState.Finished;
break;
case GameState.Finished: // 游戏完成
int leftWaving = DetectLeftHandRaise(humans);
if (leftWaving == 5)
{
_mainWin.HumanFrameUpdated -= OnHumanFrameUpdated;
_mainWin.WebcamClient.StopExtract();
// 举左手逻辑,例如结束动画或退出
var newPage = new Home();
_mainWin?.SwitchPageWithMaskAnimation(newPage, true);
}
break;
}
}
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;
// 启动60秒倒计时独立任务
StartGameCountdown(105);
}
private async void StartGameCountdown(int seconds)
{
//_musicCurrentTime = Utils.GetMusicTotalDuration();
countdownGrid.Visibility = Visibility.Visible;
// 播放背景音乐(循环)
Utils.PlayBackgroundMusic("1.MP3", true);
StartBeatScrollTimer();
for (int i = seconds; i >= 0; i--)
{
countdownText.Text = i.ToString();
await Task.Delay(1000);
}
// 停止音乐
Utils.StopBackgroundMusic();
// 停止实时检测和视频流
countdownGrid.Visibility = Visibility.Hidden;
_mainWin.HumanFrameUpdated -= OnHumanFrameUpdated;
_mainWin.WebcamClient.StopExtract();
// 计算排名
var rankList = _musicJumpRopeContext.UpdateRankList();
var scoreList = _musicJumpRopeContext.UpdateScoreRankList();
// 更新游戏状态
_currentGameState = GameState.Finished;
// 恢复视频流
Application.Current.Dispatcher.Invoke(() =>
{
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
_mainWin.WebcamClient.StartExtract();
// 更新 UI排行榜或分数板
// ShowRankingBoard(rankList);
});
}
private DateTime? _raiseStartTime;
private bool _firstHandTriggered;
private int _lastCountdownSecond = 3;
private DateTime? _countdownStartTime;
public int DetectRightHandRaise(List<Human> humans)
{
if (humans == null || humans.Count == 0)
return (int)WavingAction.None;
foreach (var human in humans)
{
if (human?.Keypoints == null)
continue;
// --- 筛选右脚踝坐标 ---
var rightAnkle = human.Keypoints.FirstOrDefault(k => k.Name == "right_ankle");
if (rightAnkle == null)
continue;
double xNorm = rightAnkle.X / 1920;
double yNorm = rightAnkle.Y / 1080;
// 仅检测中心区域内的人
if (!(xNorm >= 0.44 && xNorm <= 0.57 && yNorm >= 0.81))
continue;
// --- 获取右臂关键点 ---
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; // 持续时间(秒)
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)
{
// 举手达到指定时间,启动倒计时
_firstHandTriggered = true;
_countdownStartTime = DateTime.Now;
_lastCountdownSecond = countdownSeconds;
return (int)WavingAction.FirstHand;
}
}
else
{
// 手放下,重置举手开始时间
_raiseStartTime = DateTime.Now;
}
continue; // 本人未触发,检测下一个人
}
// ---------- 第二步:倒计时逻辑 ----------
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;
}
// 没有任何中心区域内的人举手
return (int)WavingAction.None;
}
public int DetectLeftHandRaise(List<Human> humans)
{
if (humans == null || humans.Count == 0)
return (int)WavingAction.None;
foreach (var human in humans)
{
if (human?.Keypoints == null)
continue;
// --- 筛选右脚踝坐标 ---
var rightAnkle = human.Keypoints.FirstOrDefault(k => k.Name == "right_ankle");
if (rightAnkle == null)
continue;
double xNorm = rightAnkle.X / 1920;
double yNorm = rightAnkle.Y / 1080;
// 仅检测中心区域内的人
if (!(xNorm >= 0.44 && xNorm <= 0.57 && yNorm >= 0.81))
continue;
// 获取左手关键点
var leftWrist = human.Keypoints.FirstOrDefault(k => k.Name == "left_wrist");
var leftElbow = human.Keypoints.FirstOrDefault(k => k.Name == "left_elbow");
if (leftWrist == null || leftElbow == null)
continue;
const double raiseThreshold = 60; // 举手阈值
// 判断左手是否举起
double verticalRise = leftElbow.Y - leftWrist.Y;
if (verticalRise >= raiseThreshold)
{
return (int)WavingAction.RaiseHand; // 一旦检测到左手举起,立即返回
}
}
return (int)WavingAction.None; // 没有检测到举手
}
private void ResetRaiseState()
{
_raiseStartTime = null;
_countdownStartTime = null;
_firstHandTriggered = false;
_lastCountdownSecond = 3;
}
private void DrawCirclesWithText()
{
userBox.Children.Clear();
_musicJumpRopeContext.Sports.Clear();
_musicJumpRopeContext.UserList.Clear();
_musicJumpRopeContext.UserNumberList.Clear();
_musicJumpRopeContext.UserBeatSyncList.Clear();
_musicBeatTextBlock.Clear();
double imgWidth = userBox.ActualWidth;
double imgHeight = userBox.ActualHeight;
double radius = 100;
for (int i = 0; i < _musicJumpRopeContext.CirclePositions.Count; i++)
{
//var pos = _musicJumpRopeContext.CirclePositions[i == 0 ? 1 : 0];
var pos = _musicJumpRopeContext.CirclePositions[i];
double x = pos.XNorm * imgWidth;
double y = pos.YNorm * imgHeight;
// 绘制发光圆
var userItem = AddUserItem(x, y, i);
// 绑定运动对象
var sport = SportBase.Create("rope-skipping");
int indexCopy = i;
var currentItem = userItem;
// 订阅事件
sport.OnTicked += (count, times) =>
{
// 更新UI
userItem.NumberText = count.ToString();
// 更新数字源
_musicJumpRopeContext.UserNumberList[indexCopy] = count.ToString();
if (userItem.ImageState != "2")
userItem.ImageState = "2";
var currentTime = Utils.GetMusicCurrentTime();
int currentSecond = (int)Math.Floor(currentTime);
// 判断是否命中节拍
var beats = _musicJumpRopeContext.MusicBeats["1"];
bool hit = beats.Any(b => Math.Abs(b - currentTime) <= _beatTolerance);
if (hit)
{
_musicJumpRopeContext.UserBeatSyncList[indexCopy]++;
_musicBeatTextBlock[indexCopy].Text = $"卡点 x{_musicJumpRopeContext.UserBeatSyncList[indexCopy]}";
if (indexCopy == 0)
LeftBeats.SetSelected(currentSecond, true);
else
RightBeats.SetSelected(currentSecond, true);
//Application.Current.Dispatcher.BeginInvoke(() =>
//{
// if (indexCopy == 0)
// LeftBeats.SetSelected(currentSecond, true);
// else
// RightBeats.SetSelected(currentSecond, true);
//});
}
};
sport.Start();
_musicJumpRopeContext.Sports.Add(sport);
}
}
private void UpdateCircleCounts(List<Human> humans)
{
double radiusNormX = 0.07;
double radiusNormY = 0.14;
for (int i = 0; i < _musicJumpRopeContext.CirclePositions.Count; i++)
{
var circleX = _musicJumpRopeContext.CirclePositions[i].XNorm;
var circleY = _musicJumpRopeContext.CirclePositions[i].YNorm;
Human humanInCircle = null;
// 找圈内的人
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 / userBox.ActualWidth;
double xLeftNorm = leftFoot.X / userBox.ActualWidth;
double yRightNorm = rightFoot.Y / userBox.ActualHeight;
double yLeftNorm = leftFoot.Y / userBox.ActualHeight;
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)
{
humanInCircle = hu;
break; // 每圈只处理一个人
}
}
// 根据是否有人和跳绳数字判断状态
bool hasHuman = humanInCircle != null;
lock (_updateLock)
{
_musicJumpRopeContext.UserList[i].ImageState = GetJumpState(i, hasHuman);
}
// 推送计数
//if (hasHuman)
_musicJumpRopeContext.Sports[i].Pushing(humanInCircle);
}
}
private string GetJumpState(int circleIndex, bool humanInCircle)
{
if (!humanInCircle)
{
// 无人 → 出圈
return "3";
}
string currentNumber = _musicJumpRopeContext.UserNumberList[circleIndex];
if (!_jumpStatus.ContainsKey(circleIndex))
{
_jumpStatus[circleIndex] = (currentNumber, DateTime.Now, "1"); // 初始状态先为停止
return "1";
}
var (lastNumber, lastChangeTime, lastState) = _jumpStatus[circleIndex];
if (currentNumber != lastNumber)
{
// 数字变化 → 跳绳中
_jumpStatus[circleIndex] = (currentNumber, DateTime.Now, "2");
return "2";
}
// 数字未变化,判断是否超过 2 秒
double elapsed = (DateTime.Now - lastChangeTime).TotalSeconds;
if (elapsed >= 0.8)
{
// 超过 2 秒未变化 → 停止
_jumpStatus[circleIndex] = (currentNumber, lastChangeTime, "1");
return "1";
}
// 维持上次状态
return lastState;
}
private SportUserItem AddUserItem(double centerX, double centerY, int index)
{
var userItem = new SportUserItem();
userItem.Width = 270;
userItem.Height = 560;
userItem.DisplayText = _musicJumpRopeContext.UseNameList[index];
userItem.VerticalAlignment = VerticalAlignment.Top;
userItem.HorizontalAlignment = HorizontalAlignment.Left;
userItem.ImageState = "1";
userItem.Margin = new Thickness(centerX + 120, centerY - 700, 0, 0);
// ----------- 创建上方 TextBlock ------------
var textBlock = new TextBlock
{
Text = "0", // 卡点个数
FontSize = 50,
FontWeight = FontWeights.Bold,
Foreground = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
TextAlignment = TextAlignment.Center,
Width = userItem.Width // 文本宽度和 userItem 一样
};
// 设置 TextBlock 的 Margin使其在 userItem 上方居中
double textLeft = userItem.Margin.Left;
double textTop = userItem.Margin.Top - 50; // 50 可以根据需要调整高度
textBlock.Margin = new Thickness(textLeft, textTop, 0, 0);
// 添加控件
userBox.Children.Add(userItem);
userBox.Children.Add(textBlock);
_musicJumpRopeContext.UserList.Add(userItem);
_musicJumpRopeContext.UserNumberList.Add("0");
_musicJumpRopeContext.UserBeatSyncList.Add(0);
_musicBeatTextBlock.Add(textBlock);
return userItem;
}
private DispatcherTimer _beatScrollTimer;
private double totalTime = 108.455; // 音乐总时长
private int _lastSecond = -1; // 上一次已经处理的秒数
private void StartBeatScrollTimer()
{
_beatScrollTimer = new DispatcherTimer();
_beatScrollTimer.Interval = TimeSpan.FromMilliseconds(500); // 每秒一次
int lastSecond = -1;
_beatScrollTimer.Tick += (s, e) =>
{
double currentTime = Utils.GetMusicCurrentTime();
int currentSecond = (int)Math.Floor(currentTime)+1;
if (currentSecond <= lastSecond) return;
lastSecond = currentSecond;
// 左控件
if (_musicBeatsDic.TryGetValue(currentSecond, out var leftBeats))
{
LeftBeats.SetSelected(currentSecond, true);
LeftBeats.ScrollToDotCenter(currentSecond, mirror: true);
}
// 右控件
if (_musicBeatsDic.TryGetValue(currentSecond, out var rightBeats))
{
RightBeats.SetSelected(currentSecond, true);
RightBeats.ScrollToDotCenter(currentSecond, mirror: false);
}
};
_beatScrollTimer.Start();
}
}
}