# Conflicts:
#	Wpf_AiSportsMicrospace/Views/JumpRope/GroupJumpRope.xaml
This commit is contained in:
ltx 2025-10-12 18:44:33 +08:00
commit 5134b1ff3e
13 changed files with 422 additions and 546 deletions

View File

@ -65,14 +65,12 @@ namespace Wpf_AiSportsMicrospace.Common
return _webcamClient;
}
public int VerifyWavingAction(Human human)
{
var nose = human.Keypoints.FirstOrDefault(k => k.Name == "right_ankle");
if (nose == null)
return (int)WavingAction.None;
// 使用 Canvas 宽度归一化
double xNorm = nose.X / 1920;
double yNorm = nose.Y / 1080;
@ -93,10 +91,16 @@ namespace Wpf_AiSportsMicrospace.Common
ref _lastLeftWrist,
ref _leftWristDeltaX,
true);
if (leftResult != (int)WavingAction.None) return leftResult;
if (leftResult != (int)WavingAction.None)
return leftResult;
}
// --- 右手挥手或举手 ---
// --- 右手举手优先判断 ---
int rightHandAction = DetectRightHandRaise(human);
if (rightHandAction != (int)WavingAction.None)
return rightHandAction;
// --- 右手挥手,仅当未举手时判断 ---
if (rightWrist != null && rightElbow != null)
{
int rightWaveResult = DetectHorizontalWave(
@ -105,51 +109,64 @@ namespace Wpf_AiSportsMicrospace.Common
ref _lastRightWrist,
ref _rightWristDeltaX,
false);
if (rightWaveResult != (int)WavingAction.None) return rightWaveResult;
if (rightWaveResult != (int)WavingAction.None)
return rightWaveResult;
}
// --- 举手逻辑 ---
double verticalRise = rightElbow.Y - rightWrist.Y; // 手腕在肘上方 → 正值
if (verticalRise >= 60) // 举手阈值
return (int)WavingAction.None;
}
/// <summary>
/// 检测右手举手状态
/// </summary>
/// <param name="human">人体关键点信息</param>
/// <returns>返回 WavingActionFirstHand / Raising / RaiseHand / None</returns>
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)
{
// 初始化计时
if (_raiseStartTime == null) _raiseStartTime = DateTime.Now;
if (_wristStartTime == null) _wristStartTime = DateTime.Now;
var wristDuration = DateTime.Now - _wristStartTime.Value;
if (!_firstHandTriggered && wristDuration.TotalSeconds >= 1)
{
// 初始化计时
if (_raiseStartTime == null)
_raiseStartTime = DateTime.Now;
_firstHandTriggered = true;
return (int)WavingAction.FirstHand; // 举手开始
}
if (_wristStartTime == null)
_wristStartTime = DateTime.Now;
var wristDuration = DateTime.Now - _wristStartTime.Value;
// 保持 >1 秒才触发一次 FirstHand
if (!_firstHandTriggered && wristDuration.TotalSeconds >= 1)
{
_firstHandTriggered = true;
return (int)WavingAction.FirstHand; // 举手开始,只触发一次
}
// 判断是否完成3秒举手
var duration = DateTime.Now - _raiseStartTime.Value;
if (duration.TotalSeconds >= 3)
{
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false; // 重置状态
return (int)WavingAction.RaiseHand; // 举手完成
}
else
{
return (int)WavingAction.Raising; // 举手中
}
var duration = DateTime.Now - _raiseStartTime.Value;
if (duration.TotalSeconds >= 3)
{
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false; // 重置状态
return (int)WavingAction.RaiseHand; // 举手完成
}
else
{
// 手放下,重置计时和状态
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false;
return (int)WavingAction.Raising; // 举手中
}
}
return (int)WavingAction.None;
else
{
// 手放下,重置状态
_raiseStartTime = null;
_wristStartTime = null;
_firstHandTriggered = false;
return (int)WavingAction.None;
}
}
/// <summary>

View File

@ -11,7 +11,8 @@ namespace Wpf_AiSportsMicrospace.Common
public static class Utils
{
private static MediaPlayer _bgPlayer;
public static void PlayBackgroundMusic(string musicFileName)
public static void PlayBackgroundMusic(string musicFileName, bool loop)
{
string projectRoot = Path.Combine(AppContext.BaseDirectory, @"..\..\..");
string musicPath = Path.Combine(projectRoot, "Resources", "Music", musicFileName);
@ -20,11 +21,6 @@ namespace Wpf_AiSportsMicrospace.Common
{
_bgPlayer = new MediaPlayer();
_bgPlayer.Volume = 0.5;
_bgPlayer.MediaEnded += (s, e) =>
{
_bgPlayer.Position = TimeSpan.Zero; // 循环播放
_bgPlayer.Play();
};
}
else
{
@ -33,7 +29,25 @@ namespace Wpf_AiSportsMicrospace.Common
_bgPlayer.Open(new Uri(musicPath, UriKind.Absolute));
_bgPlayer.Play();
if (loop)
{
// 循环播放 → 直接设置 MediaEnded 事件即可
_bgPlayer.MediaEnded -= BgPlayer_MediaEnded; // 避免重复绑定
_bgPlayer.MediaEnded += BgPlayer_MediaEnded;
}
else
{
_bgPlayer.MediaEnded -= BgPlayer_MediaEnded;
}
}
private static void BgPlayer_MediaEnded(object sender, EventArgs e)
{
_bgPlayer.Position = TimeSpan.Zero;
_bgPlayer.Play();
}
public static void StopBackgroundMusic()
{
if (_bgPlayer != null)

View File

@ -57,6 +57,6 @@ namespace Wpf_AiSportsMicrospace.MyUserControl
}
public static readonly DependencyProperty ImageStateProperty =
DependencyProperty.Register(nameof(NumberText), typeof(string), typeof(SportUserItem), new PropertyMetadata("0"));
DependencyProperty.Register(nameof(ImageState), typeof(string), typeof(SportUserItem), new PropertyMetadata("0"));
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -65,6 +65,7 @@
Source="/Resources/Img/Album/change_right.png"
HorizontalAlignment="right"
VerticalAlignment="Top"
MouseDown="GoNew"
Width="330"
Margin="0,100,0,0"
/>

View File

@ -33,12 +33,6 @@ namespace Wpf_AiSportsMicrospace.Views
/// </summary>
public partial class CenterHome : UserControl
{
private IHumanPredictor _humanPredictor;
private IObjectDetector _objectDetector;
private HumanGraphicsRenderer _humanGraphicsRenderer;
private WebcamClient _webcamClient;
private ConcurrentQueue<VideoFrame> _frameQueue = new();
private CancellationTokenSource _cts = new();
private SportOperate _sportOperate;
private Main _mainWin => Application.Current.MainWindow as Main;
@ -59,15 +53,6 @@ namespace Wpf_AiSportsMicrospace.Views
public CenterHome()
{
InitializeComponent();
//_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.SingleHigh);
//_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.MultiMedium);
//_objectDetector = ObjectDetectorFactory.CreateSportGoodsDetector();
//_humanGraphicsRenderer = new HumanGraphicsRenderer();
//_humanGraphicsRenderer.DrawLabel = false;
//_sports = SportBase.GetSports();
//_detectQueue = new SportDetectionQueue();
//string imgPath = Path.Combine(projectRoot, "Resources", "Img" , _nowSelect == "test" ? "test_img" : "play_img");
// 默认选中第1张
@ -83,7 +68,9 @@ namespace Wpf_AiSportsMicrospace.Views
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
Utils.PlayBackgroundMusic("homeprojectselected.mp3");
Utils.PlayBackgroundMusic("homeprojectselected.mp3", true);
}
private void CenterHome_Unloaded(object sender, RoutedEventArgs e)
@ -103,27 +90,26 @@ namespace Wpf_AiSportsMicrospace.Views
switch (wavingaction)
{
case (int)WavingAction.LeftWave: // 左手挥动
coverFlow.SlideRight();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
break;
case (int)WavingAction.RightWave: // 右手挥动
coverFlow.SlideLeft();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
break;
case (int)WavingAction.FirstHand: // 举手开始
coverFlow.StartSelectedProgress();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
break;
case (int)WavingAction.Raising: // 举手中,实时更新进度
break;
case (int)WavingAction.RaiseHand: // 举手完成
_cts.Cancel();
coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
break;
default: // 没有动作 → 取消进度
coverFlow.CancelSelectedProgress();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
break;
}
}
@ -162,91 +148,6 @@ namespace Wpf_AiSportsMicrospace.Views
//}
}
private void StartFrameProcessing()
{
Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
if (_frameQueue.TryDequeue(out var frame))
{
try
{
ProcessFrame(frame);
}
catch (Exception ex)
{
Console.WriteLine("ProcessFrame error: " + ex.Message);
}
}
else
{
await Task.Delay(5, _cts.Token);
}
}
}, _cts.Token);
}
private void ProcessFrame(VideoFrame frame)
{
try
{
var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray();
var humanResult = _humanPredictor.Predicting(buffer, frame.Number);
var humans = humanResult?.Humans?.ToList();
if (humans == null || humans.Count == 0)
return;
var human = humans.FirstOrDefault();
if (human == null) return;
//检测挥手动作
var wavingaction = _sportOperate.VerifyWavingAction(human);
switch (wavingaction)
{
case (int)WavingAction.LeftWave: // 左手挥动
Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
break;
case (int)WavingAction.RightWave: // 右手挥动
Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
break;
case (int)WavingAction.FirstHand: // 举手开始
Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
break;
case (int)WavingAction.Raising: // 举手中,实时更新进度
break;
case (int)WavingAction.RaiseHand: // 举手完成
_cts.Cancel();
coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
break;
default: // 没有动作 → 取消进度
Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
break;
}
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
public void Dispose()
{
_cts.Cancel();
_webcamClient?.StopExtract();
_webcamClient = null;
_humanPredictor.Dispose();
}
public void InitImg()
{
string projectRoot = Path.Combine(AppContext.BaseDirectory, @"..\..\..");
@ -277,5 +178,12 @@ namespace Wpf_AiSportsMicrospace.Views
var newPage = new GroupJumpRope();
mainWin?.SwitchPage(newPage, true);
}
private void GoNew(object sender, MouseButtonEventArgs e)
{
var mainWin = Application.Current.MainWindow as Main;
var newPage = new GroupJumpRope();
mainWin?.SwitchPage(newPage, true);
}
}
}

View File

@ -34,36 +34,16 @@ namespace Wpf_AiSportsMicrospace
/// </summary>
public partial class Home : UserControl
{
private IHumanPredictor _humanPredictor;
private HumanGraphicsRenderer _humanGraphicsRenderer;
private WebcamClient _webcamClient;
private ConcurrentQueue<VideoFrame> _frameQueue = new();
private CancellationTokenSource _cts = new();
private SportOperate _sportOperate;
public static Uri loadingImage = new Uri("/Resources/Img/Album/1.gif", UriKind.Relative);
private Main _mainWin => Application.Current.MainWindow as Main;
public Home()
{
InitializeComponent();
//_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.SingleHigh);
//_humanGraphicsRenderer = new HumanGraphicsRenderer();
//_humanGraphicsRenderer.DrawLabel = false;
//_sports = SportBase.GetSports();
//_detectQueue = new SportDetectionQueue();
string projectRoot = Path.Combine(AppContext.BaseDirectory, @"..\..\..");
string albumPath = Path.Combine(projectRoot, "Resources", "Img", "Album");
// 转换为 Uri
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_play.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_test.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "home_history.png")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "4.jpg")));
//coverFlow.Images.Add(new Uri(Path.Combine(albumPath, "5.jpg")));
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_test.png")), ProgressColor1 = "#215bc7", ProgressColor2 = "#fc640e" });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_play.png")), ProgressColor1 = "#e73d42", ProgressColor2 = "#fd8212" });
coverFlow.Images.Add(new CoverFlowItem { ImageUri = new Uri(Path.Combine(albumPath, "home_history.png")), ProgressColor1 = "#e73d42", ProgressColor2 = "#215bc7" });
@ -73,7 +53,7 @@ namespace Wpf_AiSportsMicrospace
Loaded += Window_Loaded;
Unloaded += Home_Unloaded;
//AnimationBehavior.SetSourceUri(LoadingImage, loadingImage);
Utils.PlayBackgroundMusic("homeprojectselected.mp3", true);
}
private void Home_Unloaded(object sender, RoutedEventArgs e)
@ -90,61 +70,29 @@ namespace Wpf_AiSportsMicrospace
//检测挥手动作
var wavingaction = _mainWin.SportOperate.VerifyWavingAction(human);
// 把低 8 位作为动作类型,高 8 位作为进度
//int actionType = wavingaction & 0xFF;
//int progress = (wavingaction >> 8) & 0xFF;
//switch (wavingaction)
//{
// case (int)WavingAction.LeftWave: // 左手挥动
// Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
// break;
// case (int)WavingAction.RightWave: // 右手挥动
// Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
// break;
// case (int)WavingAction.FirstHand: // 举手开始
// Application.Current.Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
// break;
// case (int)WavingAction.Raising: // 举手中,实时更新进度
// break;
// case (int)WavingAction.RaiseHand: // 举手完成
// _cts.Cancel();
// coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
// break;
// default: // 没有动作 → 取消进度
// Application.Current.Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
// break;
//}
switch (wavingaction)
{
case (int)WavingAction.LeftWave: // 左手挥动
coverFlow.SlideRight();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
break;
case (int)WavingAction.RightWave: // 右手挥动
coverFlow.SlideLeft();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
break;
case (int)WavingAction.FirstHand: // 举手开始
coverFlow.StartSelectedProgress();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
break;
case (int)WavingAction.Raising: // 举手中,实时更新进度
break;
case (int)WavingAction.RaiseHand: // 举手完成
_cts.Cancel();
coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
break;
default: // 没有动作 → 取消进度
coverFlow.CancelSelectedProgress();
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
break;
}
}
@ -153,22 +101,6 @@ namespace Wpf_AiSportsMicrospace
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
//_sportOperate = new SportOperate();
//_webcamClient = _sportOperate.CreateRTSP();
//_webcamClient.OnExtractFrame += frame =>
//{
// if (frame != null)
// _frameQueue.Enqueue(frame);
//};
//_webcamClient.StartExtract();
//StartFrameProcessing();
//Utils.PlayBackgroundMusic("homeprojectselected.mp3");
//coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
}
private void CoverFlow_ProgressCompleted(CoverFlowItem item)
{
@ -182,116 +114,7 @@ namespace Wpf_AiSportsMicrospace
{
Debug.WriteLine($"停止抽帧异常: {ex.Message}");
}
// 解绑事件,防止重复触发
//coverFlow.ProgressCompleted -= CoverFlow_ProgressCompleted;
// 根据图片跳转新窗口
//string uri = item.ImageUri.ToString();
//if (uri.EndsWith("1.jpg"))
// newWindow = new GroupJumpRope();
//else if (uri.EndsWith("2.jpg"))
// newWindow = new GroupJumpRope();
//else if (uri.EndsWith("3.jpg"))
// newWindow = new GroupJumpRope();
// 找到主窗口,切换内容到 GroupJumpRopePage
}
private void StartFrameProcessing()
{
Task.Run(async () =>
{
while (!_cts.Token.IsCancellationRequested)
{
if (_frameQueue.TryDequeue(out var frame))
{
try
{
ProcessFrame(frame);
}
catch (Exception ex)
{
Console.WriteLine("ProcessFrame error: " + ex.Message);
}
}
else
{
await Task.Delay(5, _cts.Token);
}
}
}, _cts.Token);
}
private void ProcessFrame(VideoFrame frame)
{
try
{
var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray();
var humanResult = _humanPredictor.Predicting(buffer, frame.Number);
var humans = humanResult?.Humans?.ToList();
if (humans == null || humans.Count == 0)
return;
//var human = humans
// .Where(h =>
// h.Keypoints.Any(kp => kp.Name == "left_ankle" && kp.X < 1020 && kp.Y > 900 && kp.Y < 1020) &&
// h.Keypoints.Any(kp => kp.Name == "right_ankle" && kp.X > 750 && kp.Y > 900 && kp.Y < 1020)
// )
// .FirstOrDefault();
var human = humans.FirstOrDefault();
if (human == null) return;
//检测挥手动作
var wavingaction = _sportOperate.VerifyWavingAction(human);
// 把低 8 位作为动作类型,高 8 位作为进度
//int actionType = wavingaction & 0xFF;
//int progress = (wavingaction >> 8) & 0xFF;
switch (wavingaction)
{
case (int)WavingAction.LeftWave: // 左手挥动
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideRight());
break;
case (int)WavingAction.RightWave: // 右手挥动
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.SlideLeft());
break;
case (int)WavingAction.FirstHand: // 举手开始
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.StartSelectedProgress());
break;
case (int)WavingAction.Raising: // 举手中,实时更新进度
break;
case (int)WavingAction.RaiseHand: // 举手完成
_cts.Cancel();
coverFlow.ProgressCompleted += CoverFlow_ProgressCompleted;
break;
default: // 没有动作 → 取消进度
Application.Current.Dispatcher.BeginInvoke(() => coverFlow.CancelSelectedProgress());
break;
}
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
public void Dispose()
{
_cts.Cancel();
_webcamClient?.StopExtract();
_webcamClient = null;
_humanPredictor.Dispose();
}
//测试点击
private void GoNew(object sender, MouseButtonEventArgs e)
{

View File

@ -6,8 +6,8 @@
xmlns:local="clr-namespace:Wpf_AiSportsMicrospace.MyUserControl"
mc:Ignorable="d" Height="1080" Width="1920" Loaded="UserControl_Loaded" Unloaded="UserControl_Unloaded">
<Grid >
<Grid.Background>
<ImageBrush ImageSource="/Resources/Img/test_img/test_home_bg.png" Stretch="UniformToFill"/>
<Grid.Background>
<ImageBrush ImageSource="/Resources/Img/test_img/test_home_bg.png" Stretch="UniformToFill"/>
</Grid.Background>
<Grid>
<Image
@ -17,9 +17,20 @@
Width="615"
Margin="0,0,0,0"
/>
<local:SportUserItem DisplayText="一号位" NumberText="100000" ImageState="0" />
<local:SportUserItem DisplayText="一号位" NumberText="100000" ImageState="1" />
<!-- title -->
<Canvas x:Name="overlayCanvas" IsHitTestVisible="False" Height="1080" Width="1920" Margin="0,10,0,-10"/>
<TextBlock x:Name="countdownText"
Text="3"
FontSize="120"
FontWeight="Bold"
Foreground="Red"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,50,0,0"
TextAlignment="Center"
Visibility="Collapsed"/>
</Grid>
</Grid>
</UserControl>

View File

@ -38,122 +38,291 @@ namespace Wpf_AiSportsMicrospace.Views
/// </summary>
public partial class GroupJumpRope : UserControl
{
private IHumanPredictor _humanPredictor;
private IObjectDetector _objectDetector;
private HumanGraphicsRenderer _humanGraphicsRenderer;
private WebcamClient _webcamClient;
private ConcurrentQueue<VideoFrame> _frameQueue = new();
private CancellationTokenSource _cts = new();
private SportOperate _sportOperate;
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;
ConfigService configService = new ConfigService();
private List<(double XNorm, double YNorm)> circlePositions = new();
private MediaPlayer _mediaPlayer = new MediaPlayer();
private bool IsGameStarted = false;
public GroupJumpRope()
{
InitializeComponent();
//_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.MultiMedium);
//_objectDetector = ObjectDetectorFactory.CreateSportGoodsDetector();
//_humanGraphicsRenderer = new HumanGraphicsRenderer();
//_humanGraphicsRenderer.DrawLabel = false;
Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
{
//DrawJumpRope3DPointsWithGlow();
DrawCirclesWithText();
_mainWin.HumanFrameUpdated += OnHumanFrameUpdated;
//_sportOperate = new SportOperate();
//_webcamClient = _sportOperate.CreateRTSP();
//_webcamClient.OnExtractFrame += frame =>
//{
// if (frame != null)
// _frameQueue.Enqueue(frame);
//};
//_webcamClient.StartExtract();
//StartFrameProcessing();
// 播放音乐
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
{
UpdateCircleCounts(humans);
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
private void StartFrameProcessing()
{
Task.Run(() =>
{
while (!_cts.Token.IsCancellationRequested)
if (!IsGameStarted)
{
if (_frameQueue.TryDequeue(out var frame))
var human = humans.LastOrDefault();
if (human == null) return;
//检测挥手动作
var wavingaction = DetectRightHandRaise(human);
switch (wavingaction)
{
ProcessFrame(frame);
}
else
{
Thread.Sleep(5);
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;
}
}
}, _cts.Token);
}
private void ProcessFrame(VideoFrame frame)
{
try
{
var buffer = frame.GetImageBuffer(ImageFormat.Jpeg).ToArray();
var humanResult = _humanPredictor.Predicting(buffer, frame.Number);
var humans = humanResult?.Humans?.ToList();
if (humans == null || humans.Count == 0)
return;
UpdateCircleCounts(humans);
else
{
UpdateCircleCounts(humans);
}
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
public Human LocateHuman(List<Human> humans, double begin, double end, double frameWidth)
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 nose = hu.Keypoints.FirstOrDefault(k => k.Name == "right_ankle");
if (nose == null)
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;
// 使用 Canvas 宽度归一化
double xNorm = nose.X / frameWidth;
double xRightNorm = rightFoot.X / frameWidth;
double xLeftNorm = leftFoot.X / frameWidth;
if (xNorm >= begin && xNorm <= end)
return hu;
// 前后出圈检测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))
{
return hu; // 返回给计数逻辑
}
}
return null;
@ -170,7 +339,7 @@ namespace Wpf_AiSportsMicrospace.Views
double radius = 100;
// 每个圆的位置X 和 Y 都归一化 0~1
var circlePositions = new List<(double XNorm, double YNorm)>
circlePositions = new List<(double XNorm, double YNorm)>
{
(0.07, 0.58),
(0.21, 0.88 ),
@ -187,8 +356,11 @@ namespace Wpf_AiSportsMicrospace.Views
double y = pos.YNorm * imgHeight;
// 绘制发光圆
//AddGlowEllipse(x, y, overlayCanvas);
AddGlowEllipse(x, y, overlayCanvas);
// 创建文本控件
var text = new TextBlock
{
@ -199,6 +371,7 @@ namespace Wpf_AiSportsMicrospace.Views
TextAlignment = TextAlignment.Center,
Width = radius * 2
};
Canvas.SetLeft(text, x - radius);
Canvas.SetTop(text, y - radius - 25);
overlayCanvas.Children.Add(text);
@ -209,7 +382,7 @@ namespace Wpf_AiSportsMicrospace.Views
int index = circleTexts.Count - 1;
sport.OnTicked += (count, times) =>
{
Application.Current.Dispatcher.Invoke(() =>
Application.Current.Dispatcher.BeginInvoke(() =>
{
circleTexts[index].Text = count.ToString();
});
@ -227,131 +400,25 @@ namespace Wpf_AiSportsMicrospace.Views
double begin = center - range;
double end = center + range;
var human = LocateHuman(humans, begin, end, overlayCanvas.ActualWidth);
var human = LocateHuman(humans, begin, end, overlayCanvas.ActualWidth, overlayCanvas.ActualHeight, i);
if (human != null)
{
sports[i].Pushing(human);
}
}
}
private void AddGlowEllipse(double centerX, double centerY, double radius, Canvas canvas)
{
var ellipse = new Ellipse
{
Width = radius * 2,
Height = radius,
Stroke = Brushes.Red,
StrokeThickness = 3,
Opacity = 0.7
};
Canvas.SetLeft(ellipse, centerX - radius);
Canvas.SetTop(ellipse, centerY - radius / 2);
canvas.Children.Add(ellipse);
}
private void DrawJumpRope3DPointsWithGlow()
{
configService.LoadAllConfigs();
ConfigSet jumpRopeConfig;
double imgWidth = 1920;
double imgHeight = 1080;
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;
double yOffset = -0.10;
double frontRadius = 85;
double backRadius = 70;
double frontY = 0.70 + yOffset;
var frontPositions = new List<(double XNorm, double YNorm)>
{
(0.24, frontY),
(0.48, frontY),
(0.72, frontY)
};
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, overlayCanvas);
}
double backY = 0.88 + yOffset;
var backPositions = new List<(double XNorm, double YNorm)>
{
(0.10, backY),
(0.35, backY),
(0.60, backY),
(0.88, backY)
};
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, overlayCanvas);
}
}
else
{
foreach (var point in jumpRopeConfig.Points)
{
double x = point.XNorm * imgWidth;
double y = point.YNorm * imgHeight;
AddGlowEllipse(x, y, overlayCanvas);
}
}
if (needSaveConfig)
{
configService.ConfigDic[jumpRopeConfig.Name] = jumpRopeConfig;
configService.SaveAllConfigs();
}
}
/// <summary>
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
/// </summary>
private void AddGlowEllipse(double centerX, double centerY, Canvas canvas)
private Ellipse AddGlowEllipse(double centerX, double centerY, Canvas canvas)
{
double radius = 70; // 统一半径
double flattenFactor = 0.5; // 统一扁平化比例
double opacity = 0.8; // 统一透明度
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
{
@ -366,10 +433,10 @@ namespace Wpf_AiSportsMicrospace.Views
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) // 外部透明
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) // 外部透明
}
}
};
@ -377,8 +444,17 @@ namespace Wpf_AiSportsMicrospace.Views
// 定位到中心
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;
}
}
}

View File

@ -36,31 +36,34 @@ namespace Wpf_AiSportsMicrospace.Views
{
InitializeComponent();
var options = new InferenceOptions()
{
GpuEnabled = true
};
Yztob.AiSports.Common.SportAppSettingService.Set("inferences", options);
//var options = new InferenceOptions()
//{
// GpuEnabled = true
//};
//Yztob.AiSports.Common.SportAppSettingService.Set("inferences", options);
_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.MultiMedium);
_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.MultiLow);
_humanGraphicsRenderer = new HumanGraphicsRenderer();
_humanGraphicsRenderer.DrawLabel = false;
SportOperate = new SportOperate();
WebcamClient = SportOperate.CreateRTSP();
// 开始抽帧线程
StartFrameProcessing();
//WebcamClient.OnExtractFrame += this.ProcessFrame;
//WebcamClient.StartExtract();//开始抽帧
// 默认显示首页
MainContent.Content = new Home();
}
private void StartFrameProcessing()
{
SportOperate = new SportOperate();
WebcamClient = SportOperate.CreateRTSP();
WebcamClient.OnExtractFrame += frame =>
{
if (frame != null)
_frameQueue.Enqueue(frame);
//if (frame != null)
_frameQueue.Enqueue(frame);
};
WebcamClient.StartExtract();
@ -76,7 +79,17 @@ namespace Wpf_AiSportsMicrospace.Views
else
{
// 避免 CPU 占满
Thread.Sleep(5);
//Thread.Sleep(5);
//if (frame == null)
//{
// WebcamClient.OnExtractFrame += frame =>
// {
// //if (frame != null)
// _frameQueue.Enqueue(frame);
// };
//}
//else
Thread.Sleep(5);
}
}
}, _cts.Token);
@ -94,10 +107,11 @@ namespace Wpf_AiSportsMicrospace.Views
return;
// 触发全局事件
Application.Current.Dispatcher.Invoke(() =>
Application.Current.Dispatcher.BeginInvoke(() =>
{
HumanFrameUpdated?.Invoke(this, humans);
});
}
catch (Exception ex)
{

View File

@ -57,10 +57,13 @@
<None Remove="Resources\Img\test_img\test_jump.png" />
<None Remove="Resources\Img\test_img\test_rope.png" />
<None Remove="Resources\Img\test_img\test_situp.png" />
<None Remove="Resources\Music\comeon.mp3" />
<None Remove="Resources\Music\countdown_3.mp3" />
<None Remove="Resources\Music\homeprojectselected.mp3" />
<None Remove="Resources\Music\homeprojectselected1.mp3" />
<None Remove="Resources\Music\musicjumprope.mp3" />
<None Remove="Resources\Music\musicjumprope1.mp3" />
<None Remove="Resources\Music\raisehand.mp3" />
</ItemGroup>
<ItemGroup>
@ -246,6 +249,12 @@
<Resource Include="Resources\Img\test_img\test_situp.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Music\comeon.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Music\countdown_3.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Music\homeprojectselected.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
@ -258,6 +267,9 @@
<Resource Include="Resources\Music\musicjumprope1.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Music\raisehand.mp3">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
</ItemGroup>
</Project>