update:添加循环跳远,手势返回等。

This commit is contained in:
ltx 2025-12-10 09:53:06 +08:00
parent 1c1b0cce0d
commit ba381d50bc
7 changed files with 399 additions and 40 deletions

View File

@ -29,11 +29,6 @@
<conv:DotColorConverter x:Key="DotColorConverter"/>
<!--<conv:SpacingConverter x:Key="SpacingConverter"/>-->
<!-- 其他自定义样式 -->
<Style x:Key="PrimaryButtonStyle" TargetType="Button" BasedOn="{StaticResource ButtonPrimary}">
<Setter Property="FontFamily" Value="{StaticResource AppFont}"/>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -12,18 +12,25 @@
<ImageBrush ImageSource="/Resources/Img/test_img/test_home_bg.png" Stretch="UniformToFill"/>
</Grid.Background>
<Grid Height="1080" Width="1920" x:Name="userBox" >
<Image Source="/Resources/Img/test_img/jump_long/title.png" HorizontalAlignment="Center" VerticalAlignment="Top" Width="615" Margin="0,0,0,0"/>
<Image Source="/Resources/Img/test_img/jump_long/mat.png" HorizontalAlignment="Center" VerticalAlignment="Top" Width="1190" Margin="0,600,0,0"/>
<Image Source="/Resources/Img/test_img/jump_long/people.png" HorizontalAlignment="Left" VerticalAlignment="Top" Width="244" Margin="1278,282,0,0" Height="458"/>
<Label x:Name="sportCounts" Height="50" Width="200" HorizontalAlignment="Left" BorderBrush="Black" BorderThickness="2"></Label>
<Label x:Name="sportTimes" Height="50" Width="200" HorizontalAlignment="Center" BorderBrush="Black" BorderThickness="2" ></Label>
<Image x:Name="videoImage" Stretch="Uniform" />
<Image x:Name="centerImg1" Source="/Resources/Img/test_img/jump_long/title.png" HorizontalAlignment="Center" VerticalAlignment="Top" Width="615" Margin="0,0,0,0"/>
<Image x:Name="centerImg2" Source="/Resources/Img/test_img/jump_long/mat.png" HorizontalAlignment="Center" VerticalAlignment="Top" Width="1337" Margin="0,556,0,0" Height="210"/>
<Image
x:Name="centerImg3"
gif:ImageBehavior.AnimatedSource="/Resources/Img/test_img/jump_long/right_hand.gif"
gif:ImageBehavior.RepeatBehavior="Forever"
gif:ImageBehavior.AnimateInDesignMode="True"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="1242,301,0,0" Height="438" Width="360"/>
<Image
x:Name="centerImg4"
gif:ImageBehavior.AnimatedSource="/Resources/Img/test_img/jump_long/jumping.gif"
gif:ImageBehavior.RepeatBehavior="Forever"
gif:ImageBehavior.AnimateInDesignMode="True"
HorizontalAlignment="Left" VerticalAlignment="Top" Margin="824,72,0,0" Width="824" Height="610"/>
<Grid Width="1920" Height="1080" Background="#80000000" x:Name="TipBox" Opacity="0">
<Image Source="/Resources/Img/test_img/jump_long/tip.png" HorizontalAlignment="Center" VerticalAlignment="Center" Width="1416" Margin="0,0,0,-700"/>
</Grid>
<!--成绩-->
<Grid x:Name="RankingGrid" Margin="0,0,0,0" Height="1080" Width="1920" Opacity="0">
<Image
@ -35,15 +42,34 @@
/>
<Grid Width="1380" Height="697" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,180,0,0" x:Name="ScoreGrid">
<!--<Border Background="#FEDE50" CornerRadius="20"/>-->
<Image Source="/Resources/Img/test_img/jump_long/rotate_img.png" Width="1000" Height="1000" Margin="205,-454,175,151" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<RotateTransform x:Name="rotateTransform" Angle="0"/>
</Image.RenderTransform>
<Image.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="rotateTransform"
Storyboard.TargetProperty="Angle"
From="0"
To="360"
Duration="0:0:6"
RepeatBehavior="Forever"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Image.Triggers>
</Image>
<Image Source="/Resources/Img/test_img/jump_long/head_top.png" HorizontalAlignment="Center" VerticalAlignment="Top" Height="110" Margin="0,-104,0,0" />
<Image Source="/Resources/Img/test_img/jump_long/win_bg.png" HorizontalAlignment="Center" VerticalAlignment="Center" Width="1530" Height="770" />
<Border Background="#ff8600" CornerRadius="20" Width="300" Height="90" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,20,0,0" >
<Grid>
<TextBlock Text="跳远成绩" FontSize="22" TextAlignment="Center" Padding="0,10,0,0" FontWeight="Bold" Foreground="#80ffffff" />
<TextBlock Text="165cm" FontSize="49" TextAlignment="Center" Padding="0,35,0,0" FontWeight="Bold" Foreground="#fff" FontStyle="Italic" />
<TextBlock x:Name="count" Text="165cm" FontSize="49" TextAlignment="Center" Padding="0,35,0,0" FontWeight="Bold" Foreground="#fff" FontStyle="Italic" />
</Grid>
</Border>
<StackPanel Margin="60,130,50,30" Orientation="Horizontal">
@ -67,17 +93,21 @@
<TextBlock Text="380°" FontSize="55" FontWeight="Bold" FontStyle="Italic" Margin="54,81,-54,-81" />
<TextBlock Text="2" FontSize="55" FontWeight="Bold" FontStyle="Italic" Margin="432,81,-432,-81" />
<TextBlock Text="105cm" FontSize="55" FontWeight="Bold" FontStyle="Italic" Margin="54,373,-54,-373" />
<TextBlock Text="00:11" FontSize="55" FontWeight="Bold" FontStyle="Italic" Margin="397,373,-397,-373" />
<TextBlock x:Name="time" Text="00:11" FontSize="55" FontWeight="Bold" FontStyle="Italic" Margin="397,373,-397,-373" />
</Grid>
</StackPanel>
</Grid>
<Grid Margin="0,0,0,50" VerticalAlignment="Bottom" Height="182" Width="340" >
<Grid Margin="0,0,700,50" VerticalAlignment="Bottom" Height="182" Width="340" >
<Image Source="/Resources/Img/Album/change_left.png" HorizontalAlignment="Left" VerticalAlignment="Top" Width="330" Margin="0,103,0,0" />
<Image gif:ImageBehavior.AnimatedSource="/Resources/Img/Album/3.gif" gif:ImageBehavior.RepeatBehavior="Forever" gif:ImageBehavior.AnimateInDesignMode="True" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="-20,10,30,20" />
</Grid>
<Grid Margin="700,0,0,50" VerticalAlignment="Bottom" Height="182" Width="340" >
<Image Source="/Resources/Img/Album/change_bg.png" HorizontalAlignment="Left" VerticalAlignment="Top" Width="330" Margin="0,103,0,0" />
<TextBlock Text="10s" x:Name="countDown" FontSize="28" FontWeight="Bold" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="65,127,0,0" Foreground="red"/>
<TextBlock Text="后继续挑战" FontSize="28" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="30,127,0,0" />
</Grid>
</Grid>
</Grid>
</Grid>

View File

@ -46,6 +46,9 @@ namespace Views.JumpLong
private ApparatusLongJumpDelineation _measureApparatus;
private MediaPlayer _mediaPlayer = new MediaPlayer();
private readonly SportDetectionQueue _detectQueue;
private int _sportType = 0; // 0:未开始 1:运动中 2:运动结束展示排行榜
#endregion
public StandingLeap()
@ -53,7 +56,6 @@ namespace Views.JumpLong
InitializeComponent();
Loaded += UserControl_Loaded;
Unloaded += UserControl_Unloaded;
_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.SingleHigh);
_humanGraphicsRenderer = new HumanGraphicsRenderer();
_humanGraphicsRenderer.DrawLabel = false;
@ -62,19 +64,13 @@ namespace Views.JumpLong
_sport = SportBase.Create("standing-long-jump");
_sport.OnTicked += this.OnSportTick;
//if (_sport?.IsCounting == true)
_sport.Start();
_mainWin.HumanFrameUpdated += this.OnFrameUpdated;
_measureApparatus = new ApparatusLongJumpDelineation();
_measureApparatus.MeasureLength = 300;
var _isCalibrated = this.ReactangleCalibrating(1920, 1080);
_sport.MeasureApparatus = _measureApparatus;
_detectQueue.Sport = _sport;
_detectQueue.Start();
// 开始抽帧线程
_mainWin.FacialRecognitionEvent += HumanPredicting;
}
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
@ -83,6 +79,47 @@ namespace Views.JumpLong
PlayMusic("raisehand.mp3");
}
//举手检测
private void OnFrameUpdated(object sender, List<Human> humans)
{
try
{
if (humans == null || humans.Count == 0) return;
//举手检测 未开始 :退出& 开始 运动中:不检测 2.结束:继续测试 & 退出
if (_sportType != 1)
{
//左手返回
int leftWaving = DetectLeftHandRaise(humans);
if (leftWaving == 5)
{
_mainWin.HumanFrameUpdated -= this.OnFrameUpdated;
_mainWin.WebcamClient.StopExtract();
// 举左手逻辑,例如结束动画或退出
var newPage = new Home();
_mainWin?.SwitchPageWithMaskAnimation(newPage, true);
return; // 提前退出
}
//右手判断
int rightWaving = DetectRightHandRaise(humans);
if (rightWaving >= 3 && _sportType == 0) //举手完成 开始跳远检测
{
Utils.PlayBackgroundMusic("countdown_3.mp3", false);
_sportType = 1;
StartSport();
}
}
}
catch (Exception ex)
{
Console.WriteLine("OnFrameExtracted error: " + ex.Message);
}
}
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
{
_sport.Stop();
@ -110,6 +147,7 @@ namespace Views.JumpLong
}
}
//播放音乐
private void PlayMusic(string musicFileName)
{
// 获取项目根目录
@ -158,6 +196,7 @@ namespace Views.JumpLong
});
});
}
private void MediaPlayer_MediaEnded(object sender, EventArgs e)
{
@ -168,24 +207,29 @@ namespace Views.JumpLong
private void OnSportTick(int counts, int times)
{
var ts = TimeSpan.FromSeconds(times);
Dispatcher.BeginInvoke(() =>
_ = Dispatcher.BeginInvoke(() =>
{
//sportCounts.Content = _sport.GetFormatCounts(); //counts.ToString();
//sportTimes.Content = _sport.GetFormatTimes();//ts.ToString(@"mm\'ss\""");
count.Text = _sport.GetFormatCounts();
var counts = _sport.GetFormatCounts();
if (double.Parse(counts[..^1]) > 0 && _sportType == 1) //正向跳远时候&&处于测试阶段时候触发更新
{
_sport.Stop();//关闭运动
_mainWin.FacialRecognitionEvent -= this.HumanPredicting;
count.Text = counts;
time.Text = _sport.GetFormatTimes();
ShowRope();
//触发停止
if (!_sport.IsCounting)
{
_sport.Stop();
_sport.Start();
}
}
});
}
//展示成绩
private void ShowRope()
private async void ShowRope()
{
// 渐变出现动画
var fadeInAnimation = new DoubleAnimation
@ -194,9 +238,47 @@ namespace Views.JumpLong
To = 1,
Duration = TimeSpan.FromSeconds(1.5)
};
var fadeInAnimation1 = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromSeconds(1.5)
};
//videoImage.Opacity = 0;
//centerImg1.Opacity = 1;
//centerImg2.Opacity = 1;
//centerImg3.Opacity = 1;
_sportType = 2;
RankingGrid.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
//_mainWin.WebcamClient.OnExtractFrame -= this.OnFrameExtracted;
for (int i = 12; i >= 0; i--)
{
countDown.Text = i.ToString() + "s";
if (i == 1)
{
RankingGrid.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation1);
_sportType = 0;
PlayMusic("raisehand.mp3");
}
await Task.Delay(1000);
}
// 关闭跳远抽帧线程
}
//关闭成绩展示 显示跳远页面
private void closRope()
{
//关闭动画
//var fadeInAnimation = new DoubleAnimation
//{
// From = 1,
// To = 0,
// Duration = TimeSpan.FromSeconds(1)
//};
// RankingGrid.BeginAnimation(UIElement.OpacityProperty, fadeInAnimation);
RankingGrid.Opacity = 0;
_sportType = 0;
PlayMusic("raisehand.mp3");
}
private bool ReactangleCalibrating(int frameWidth, int frameHeight)
{
@ -256,5 +338,245 @@ namespace Views.JumpLong
return _measureApparatus.Calibrating(boxes);
}
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;
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var points = db.JumpLongPoints.FirstOrDefault();
// 检测起跳区域的人
var isinx = (rightAnkle.X < points.TopPoint3.X && rightAnkle.X > points.TopPoint2.X) || (rightAnkle.X < points.BottomPoint3.X && rightAnkle.X > points.BottomPoint2.X);
if (points.TopPoint3.X < points.TopPoint2.X)
{
isinx = (rightAnkle.X > points.TopPoint3.X && rightAnkle.X < points.TopPoint2.X) || (rightAnkle.X > points.BottomPoint3.X && rightAnkle.X < points.BottomPoint2.X);
}
var isiny = (rightAnkle.Y > points.TopPoint3.Y && rightAnkle.X < points.BottomPoint3.Y) || (rightAnkle.Y < points.TopPoint2.Y && rightAnkle.X > points.BottomPoint2.Y);
if (!isinx || !isiny) 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 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;
var app = (App)Application.Current;
using var scope = App.AppHost.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var points = db.JumpLongPoints.FirstOrDefault();
// 检测起跳区域的人
var isinx = (rightAnkle.X < points.TopPoint3.X && rightAnkle.X > points.TopPoint2.X) || (rightAnkle.X < points.BottomPoint3.X && rightAnkle.X > points.BottomPoint2.X);
if (points.TopPoint3.X < points.TopPoint2.X)
{
isinx = (rightAnkle.X > points.TopPoint3.X && rightAnkle.X < points.TopPoint2.X) || (rightAnkle.X > points.BottomPoint3.X && rightAnkle.X < points.BottomPoint2.X);
}
var isiny = (rightAnkle.Y > points.TopPoint3.Y && rightAnkle.X < points.BottomPoint3.Y) || (rightAnkle.Y < points.TopPoint2.Y && rightAnkle.X > points.BottomPoint2.Y);
if (!isinx || !isiny) 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;
ResetRaiseState();
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;
}
private void ResetRaiseState()
{
_raiseStartTime = null;
_countdownStartTime = null;
_firstHandTriggered = false;
_lastCountdownSecond = 3;
}
private void StartSport()
{
//if (_sport?.IsCounting == true)
//_mainWin.WebcamClient.OnExtractFrame += this.OnFrameExtracted;
_sport.Start();
_detectQueue.Start();
// 开始跳远抽帧线程
_mainWin.FacialRecognitionEvent += this.HumanPredicting;
}
private WriteableBitmap _videoBitmap;
private int _lastFrameNumber = -1;
private async void OnFrameExtracted(VideoFrame frame)
{
if (frame == null) return;
// 跳帧:每秒只渲染 10~15 帧,降低 CPU 占用
if (frame.Number % 2 != 0) return;
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();
videoImage.Opacity = 1;
centerImg1.Opacity = 0;
centerImg2.Opacity = 0;
centerImg3.Opacity = 0;
});
}
}
}

View File

@ -68,8 +68,11 @@
<None Remove="Resources\Img\play_img\play_vs.png" />
<None Remove="Resources\Img\test_img\jump_long\bottom.png" />
<None Remove="Resources\Img\test_img\jump_long\head_top.png" />
<None Remove="Resources\Img\test_img\jump_long\jumping.gif" />
<None Remove="Resources\Img\test_img\jump_long\mat.png" />
<None Remove="Resources\Img\test_img\jump_long\people.png" />
<None Remove="Resources\Img\test_img\jump_long\right_hand.gif" />
<None Remove="Resources\Img\test_img\jump_long\rotate_img.png" />
<None Remove="Resources\Img\test_img\jump_long\tip.png" />
<None Remove="Resources\Img\test_img\jump_long\title.png" />
<None Remove="Resources\Img\test_img\jump_long\top.png" />
@ -119,12 +122,21 @@
<Resource Include="Resources\Img\test_img\jump_long\head_top.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\jumping.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\mat.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\people.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\right_hand.gif">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\rotate_img.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Img\test_img\jump_long\tip.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>