462 lines
16 KiB
C#
462 lines
16 KiB
C#
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.Imaging;
|
||
using System.Windows.Shapes;
|
||
using System.Windows.Threading;
|
||
using Wpf_AiSportsMicrospace.Common;
|
||
using Wpf_AiSportsMicrospace.Dto;
|
||
using Wpf_AiSportsMicrospace.Enum;
|
||
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 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;
|
||
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);
|
||
|
||
if (!File.Exists(musicPath))
|
||
{
|
||
Console.WriteLine($"音乐文件不存在: {musicPath}");
|
||
return;
|
||
}
|
||
|
||
_mediaPlayer.Open(new Uri(musicPath, UriKind.Absolute));
|
||
|
||
// 监听播放完成事件
|
||
_mediaPlayer.MediaEnded += MediaPlayer_MediaEnded;
|
||
|
||
_mediaPlayer.Play();
|
||
}
|
||
|
||
private void MediaPlayer_MediaEnded(object sender, EventArgs e)
|
||
{
|
||
// 音乐播放完成后的逻辑
|
||
Console.WriteLine("音乐播放完成!");
|
||
|
||
// 可在这里绑定抽帧事件
|
||
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
|
||
}
|
||
|
||
private void OnHumanFrameUpdated(object sender, List<Human> humans)
|
||
{
|
||
try
|
||
{
|
||
if (!IsGameStarted)
|
||
{
|
||
var human = humans.LastOrDefault();
|
||
if (human == null) return;
|
||
|
||
//检测挥手动作
|
||
var wavingaction = DetectRightHandRaise(human);
|
||
|
||
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";
|
||
countdownText.Visibility = Visibility.Collapsed;
|
||
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();
|
||
countdownText.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 = "✔";
|
||
countdownText.Visibility = Visibility.Collapsed;
|
||
IsGameStarted = true;
|
||
// 播放背景音乐(循环)
|
||
Utils.PlayBackgroundMusic("homeprojectselected1.mp3", true);
|
||
|
||
// 启动60秒倒计时(独立任务)
|
||
StartGameCountdown(60);
|
||
}
|
||
private async void StartGameCountdown(int seconds)
|
||
{
|
||
countdownText.Visibility = Visibility.Visible;
|
||
|
||
for (int i = seconds; i >= 0; i--)
|
||
{
|
||
countdownText.Text = i.ToString();
|
||
await Task.Delay(1000); // 不阻塞主线程,计数逻辑继续执行
|
||
}
|
||
|
||
countdownText.Visibility = Visibility.Collapsed;
|
||
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");
|
||
|
||
if (rightWrist == null || rightElbow == null)
|
||
return (int)WavingAction.None;
|
||
|
||
double verticalRise = rightElbow.Y - rightWrist.Y; // 手腕高于肘部 → 正值
|
||
const double raiseThreshold = 60; // 举手阈值,可调整
|
||
|
||
if (verticalRise >= raiseThreshold)
|
||
{
|
||
// 初始化
|
||
_raiseStartTime ??= DateTime.Now;
|
||
_wristStartTime ??= DateTime.Now;
|
||
_countdownStartTime ??= DateTime.Now;
|
||
|
||
var wristDuration = DateTime.Now - _wristStartTime.Value;
|
||
|
||
// 第一次举手触发
|
||
if (!_firstHandTriggered && wristDuration.TotalSeconds >= 1)
|
||
{
|
||
_firstHandTriggered = true;
|
||
_lastCountdownSecond = 3;
|
||
_countdownStartTime = DateTime.Now;
|
||
return (int)WavingAction.FirstHand;
|
||
}
|
||
|
||
// 倒计时逻辑
|
||
if (_firstHandTriggered)
|
||
{
|
||
var elapsed = DateTime.Now - _countdownStartTime.Value;
|
||
int currentSecond = 3 - (int)Math.Floor(elapsed.TotalSeconds);
|
||
|
||
if (currentSecond != _lastCountdownSecond && currentSecond > 0)
|
||
{
|
||
_lastCountdownSecond = currentSecond;
|
||
// 你可以在这里触发倒计时提示(比如语音播报或 UI 更新)
|
||
Console.WriteLine($"倒计时:{currentSecond}");
|
||
return (int)WavingAction.Raising; // 可选:倒计时状态
|
||
}
|
||
|
||
// 举手完成
|
||
if (elapsed.TotalSeconds >= 3)
|
||
{
|
||
ResetRaiseState();
|
||
return (int)WavingAction.RaiseHand; // 举手完成
|
||
}
|
||
|
||
return (int)WavingAction.Raising; // 举手中
|
||
}
|
||
|
||
return (int)WavingAction.Raising;
|
||
}
|
||
else
|
||
{
|
||
// 手放下重置状态
|
||
ResetRaiseState();
|
||
return (int)WavingAction.None;
|
||
}
|
||
}
|
||
|
||
private void ResetRaiseState()
|
||
{
|
||
_raiseStartTime = null;
|
||
_wristStartTime = null;
|
||
_countdownStartTime = null;
|
||
_firstHandTriggered = false;
|
||
_lastCountdownSecond = 3;
|
||
}
|
||
|
||
public Human LocateHuman(List<Human> humans, double begin, double end, 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.07; // X方向半径
|
||
double radiusNormY = 0.1; // Y方向半径
|
||
|
||
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;
|
||
|
||
// 前后出圈检测(Y方向)
|
||
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)
|
||
{
|
||
Application.Current.Dispatcher.BeginInvoke(() =>
|
||
{
|
||
circleTexts[circleIndex].Text = "您已出圈!";
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// X 范围匹配 → 未出圈的人
|
||
if (xRightNorm >= (circleX - radiusNormX) && xRightNorm <= (circleX + radiusNormX))
|
||
{
|
||
IsGameStarted = true;
|
||
return hu; // 返回给计数逻辑
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
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
|
||
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);
|
||
|
||
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.BeginInvoke(() =>
|
||
{
|
||
circleTexts[index].Text = count.ToString();
|
||
});
|
||
};
|
||
sport.Start();
|
||
sports.Add(sport);
|
||
}
|
||
}
|
||
private void UpdateCircleCounts(List<Human> humans)
|
||
{
|
||
for (int i = 0; i < circlePositionsX.Length; i++)
|
||
{
|
||
double center = circlePositionsX[i];
|
||
double range = 0.07;
|
||
double begin = center - range;
|
||
double end = center + range;
|
||
|
||
var human = LocateHuman(humans, begin, end, overlayCanvas.ActualWidth, overlayCanvas.ActualHeight, i);
|
||
if (human != null)
|
||
{
|
||
sports[i].Pushing(human);
|
||
}
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
|
||
/// </summary>
|
||
private Ellipse AddGlowEllipse(double centerX, double centerY, Canvas canvas)
|
||
{
|
||
double radius = 70; // 统一半径
|
||
double flattenFactor = 0.5; // 扁平化比例
|
||
double opacity = 0.8; // 透明度
|
||
|
||
// 默认颜色
|
||
Color cColor = Color.FromArgb(220, 255, 255, 0); // 黄
|
||
Color eColor = Color.FromArgb(180, 0, 0, 255); // 蓝
|
||
|
||
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(cColor, 0.0), // 中心颜色
|
||
new GradientStop(cColor, 0.4), // 内部颜色
|
||
new GradientStop(eColor, 0.7), // 边缘颜色
|
||
new GradientStop(Color.FromArgb(0, eColor.R, eColor.G, eColor.B), 1.0) // 外部透明
|
||
}
|
||
}
|
||
};
|
||
|
||
// 定位到中心
|
||
Canvas.SetLeft(ellipse, centerX - radius);
|
||
Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2);
|
||
|
||
canvas.Children.Add(ellipse);
|
||
return ellipse; // 返回 Ellipse 对象方便后续修改或移除
|
||
}
|
||
|
||
|
||
public bool IsInsideCircle(double px, double py, double cx, double cy, double r)
|
||
{
|
||
double dx = px - cx;
|
||
double dy = py - cy;
|
||
return dx * dx + dy * dy <= r * r;
|
||
}
|
||
}
|
||
}
|