tij
This commit is contained in:
parent
5d8bad21de
commit
349649087d
@ -51,5 +51,13 @@ namespace AiSportsMicrospaceDB.Entities
|
||||
[Comment("密码")]
|
||||
[Column(TypeName = "nvarchar(20)")]
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///端口
|
||||
/// </summary>
|
||||
[Display(Name = "端口")]
|
||||
[Comment("端口")]
|
||||
[Column(TypeName = "nvarchar(20)")]
|
||||
public string Port { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ namespace Wpf_AiSportsMicrospace.Common
|
||||
return null;
|
||||
}
|
||||
|
||||
_webcamClient = WebcamClient.CreateRTSP(baseData.Host, baseData.UserName, baseData.Password, 554u);
|
||||
_webcamClient = WebcamClient.CreateRTSP(baseData.Host, baseData.UserName, baseData.Password);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@ -7,32 +7,8 @@
|
||||
Loaded="Window_Loaded" Closing="Window_Closing"
|
||||
mc:Ignorable="d" Title="AI运动微空间" Height="700" Width="1200">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧控制面板 -->
|
||||
<StackPanel Grid.Column="0" Margin="10">
|
||||
<ComboBox x:Name="sportList"
|
||||
Height="25"
|
||||
DisplayMemberPath="Name"
|
||||
SelectedValuePath="Key"
|
||||
SelectionChanged="sportList_SelectionChanged" d:IsHidden="True"/>
|
||||
<TextBox x:Name="host" Margin="0,10,0,0" Text="172.17.30.65"/>
|
||||
<TextBox x:Name="userName" Margin="0,10,0,0" Text="admin"/>
|
||||
<TextBox x:Name="password" Margin="0,10,0,0" Text="yd708090"/>
|
||||
<TextBox x:Name="port" Margin="0,10,0,0" Text="554"/>
|
||||
|
||||
<Button Content="Ping" Margin="0,10,0,0" Click="ping_Click"/>
|
||||
<Button x:Name="startOrStop" Content="开始" Margin="0,10,0,0" Click="startOrStop_Click"/>
|
||||
|
||||
<TextBlock x:Name="sportCounts" Text="0" Margin="0,20,0,0"/>
|
||||
<TextBlock x:Name="sportTimes" Text="00'00''"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧视频预览 -->
|
||||
<Grid Grid.Column="1" Background="Black">
|
||||
<Grid Background="Black">
|
||||
<Image x:Name="videoImage" Stretch="Uniform" />
|
||||
<Canvas x:Name="overlayCanvas" IsHitTestVisible="False" />
|
||||
</Grid>
|
||||
|
||||
@ -3,6 +3,7 @@ using Common;
|
||||
using Emgu.CV.Reg;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using SharpDX.Direct3D9;
|
||||
using SkiaSharp;
|
||||
using System.Diagnostics;
|
||||
@ -36,37 +37,15 @@ namespace Wpf_AiSportsMicrospace;
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
#region 私有成员
|
||||
private IHumanPredictor _humanPredictor;
|
||||
private IObjectDetector _objectDetector;
|
||||
private HumanGraphicsRenderer _humanGraphicsRenderer;
|
||||
private readonly List<SportDescriptor> _sports;
|
||||
private SportBase _sport;
|
||||
private readonly SportDetectionQueue _detectQueue;
|
||||
|
||||
private WriteableBitmap _videoBitmap;
|
||||
private int _lastFrameNumber = -1;
|
||||
|
||||
private readonly AppDbContext _context;
|
||||
#endregion
|
||||
|
||||
public MainWindow(AppDbContext context)
|
||||
{
|
||||
InitializeComponent();
|
||||
_context = context;
|
||||
|
||||
// 输出到控制台
|
||||
_humanPredictor = HumanPredictorFactory.Create(HumanPredictorType.SingleHigh);
|
||||
_objectDetector = ObjectDetectorFactory.CreateSportGoodsDetector();
|
||||
_humanGraphicsRenderer = new HumanGraphicsRenderer();
|
||||
_humanGraphicsRenderer.DrawLabel = false;
|
||||
|
||||
_sports = SportBase.GetSports();
|
||||
|
||||
_detectQueue = new SportDetectionQueue();
|
||||
this.sportList.ItemsSource = _sports;
|
||||
this.sportList.SelectedIndex = 0;
|
||||
this.OnSelectedSport();
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
@ -82,109 +61,25 @@ public partial class MainWindow : Window
|
||||
using var scope = App.AppHost.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var baseData = db.BasicConfig.FirstOrDefault(x => x.Code == AppSettings.Code);
|
||||
if (baseData != null)
|
||||
|
||||
if (baseData == null)
|
||||
{
|
||||
host.Text = baseData.Host;
|
||||
userName.Text = baseData.UserName;
|
||||
password.Text = baseData.Password;
|
||||
this.ShowError("相机主机地址/IP不能为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
var _webcamClient = WebcamClient.CreateRTSP(baseData.Host, baseData.UserName, baseData.Password);
|
||||
|
||||
//处理抽帧回调
|
||||
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
|
||||
_webcamClient.StartExtract();//开始抽帧
|
||||
}
|
||||
|
||||
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
base.OnClosed(e);
|
||||
|
||||
// 释放 WriteableBitmap
|
||||
//_videoBitmap = null;
|
||||
//videoImage.Source = null;
|
||||
//overlayCanvas = null;
|
||||
}
|
||||
|
||||
private void sportList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
this.OnSelectedSport();
|
||||
}
|
||||
private void OnSelectedSport()
|
||||
{
|
||||
var sport = SportBase.Create(_sports[this.sportList.SelectedIndex].Key);
|
||||
sport.OnTicked += this.OnSportTick;
|
||||
if (_sport?.IsCounting == true)
|
||||
sport.Start();
|
||||
|
||||
_sport?.Stop();
|
||||
_detectQueue.Stop();
|
||||
_sport = sport;
|
||||
_detectQueue.Sport = _sport;
|
||||
}
|
||||
|
||||
private void ping_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
//var config = _context.BasicConfig.FirstOrDefault();
|
||||
|
||||
//if (config != null)
|
||||
//{
|
||||
// //var ip = config.Ip;
|
||||
//}
|
||||
|
||||
var host = this.host.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
this.ShowError("相机主机地址/IP不能为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
var pinger = new Ping();
|
||||
var reply = pinger.Send(host, 10);
|
||||
if (reply.Status == IPStatus.Success)
|
||||
this.ShowInformation("与相机通信正常。");
|
||||
else
|
||||
this.ShowError($"与相机通信失败,错误:{reply.Status}。");
|
||||
}
|
||||
|
||||
private void startOrStop_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var host = this.host.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
this.ShowError("相机主机地址/IP不能为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
var userName = this.userName.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
this.ShowError("用户名不能为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
var password = this.password.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
this.ShowError("密码不能为空。");
|
||||
return;
|
||||
}
|
||||
var port = 554u;//RTSP协议默认端口是554
|
||||
|
||||
var tempPath = "./temps";
|
||||
if (!Directory.Exists(tempPath))
|
||||
Directory.CreateDirectory(tempPath);
|
||||
|
||||
_sport.Reset();
|
||||
_sport.Start();
|
||||
_detectQueue.Start();
|
||||
|
||||
var _webcamClient = WebcamClient.CreateRTSP(host, userName, password, port);
|
||||
|
||||
//处理抽帧回调
|
||||
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
|
||||
_webcamClient.StartExtract();//开始抽帧
|
||||
|
||||
|
||||
|
||||
this.startOrStop.Content = "停止(&S)";
|
||||
this.sportCounts.Text = "0";
|
||||
this.sportTimes.Text = "00'00\"";
|
||||
}
|
||||
private async void OnFrameExtracted(VideoFrame frame)
|
||||
{
|
||||
if (frame == null) return;
|
||||
@ -232,254 +127,83 @@ public partial class MainWindow : Window
|
||||
_videoBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
|
||||
_videoBitmap.Unlock();
|
||||
});
|
||||
|
||||
//var actualPositions = new List<(double X, double Y)>
|
||||
//{
|
||||
// (290, 510),
|
||||
// (500, 700),
|
||||
// (810, 710),
|
||||
// (120, 880),
|
||||
// (350, 880),
|
||||
// (660, 880)
|
||||
//};
|
||||
|
||||
////判断是否出圈
|
||||
//if (configService.ConfigDic.TryGetValue("JumpRope", out var obj) && obj is ConfigSet cfg)
|
||||
//{
|
||||
// for (int i = 0; i < actualPositions.Count && i < cfg.Points.Count; i++)
|
||||
// {
|
||||
// var (ax, ay) = actualPositions[i];
|
||||
// var pointCfg = cfg.Points[i];
|
||||
|
||||
// if (!IsInsideCircle(ax, ay, pointCfg))
|
||||
// {
|
||||
// Console.WriteLine($"⚠️ 学生{i + 1} 已经出圈!(坐标 {ax},{ay})");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Console.WriteLine($"✅ 学生{i + 1} 在圈内。");
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
//可以进一步进行人体识别等
|
||||
//var humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number));
|
||||
|
||||
//var human = humanResult?.Humans?.FirstOrDefault();
|
||||
//_detectQueue.Enqueue(frame.Number, human, null);
|
||||
}
|
||||
private void OnSportTick(int counts, int times)
|
||||
{
|
||||
var ts = TimeSpan.FromSeconds(times);
|
||||
|
||||
// 使用 Dispatcher 调用 UI 更新
|
||||
this.Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
this.sportCounts.Text = _sport.GetFormatCounts(); // counts.ToString();
|
||||
this.sportTimes.Text = _sport.GetFormatTimes(); // ts.ToString(@"mm\'ss\"");
|
||||
|
||||
// 如果需要触发停止逻辑
|
||||
//if (!_sport.IsCounting)
|
||||
// this.startOrStop_Click(null, null);
|
||||
}));
|
||||
|
||||
//VoiceBroadcast.PlayTick();
|
||||
}
|
||||
|
||||
private async Task DrawJumpRope3DPointsWithGlow()
|
||||
{
|
||||
if (videoImage == null || overlayCanvas == null) return;
|
||||
|
||||
ConfigSet jumpRopeConfig = new ConfigSet();
|
||||
|
||||
double imgWidth = videoImage.ActualWidth;
|
||||
double imgHeight = videoImage.ActualHeight;
|
||||
|
||||
if (imgWidth <= 0 || imgHeight <= 0) return;
|
||||
|
||||
overlayCanvas.Children.Clear();
|
||||
overlayCanvas.Width = imgWidth;
|
||||
overlayCanvas.Height = imgHeight;
|
||||
|
||||
bool needSaveConfig = false;
|
||||
|
||||
var config = await _context.BasicConfig.FirstOrDefaultAsync();
|
||||
|
||||
if (config!=null)
|
||||
{
|
||||
// 没有配置,则生成默认配置
|
||||
jumpRopeConfig = new ConfigSet { Name = "rope-skipping" };
|
||||
needSaveConfig = true;
|
||||
|
||||
// ===== 前排 3 人 =====
|
||||
double frontRadius = 80;
|
||||
var frontPositions = new List<(double XNorm, double YNorm)>
|
||||
{
|
||||
(0.25, 0.70), (0.5, 0.70), (0.75, 0.70)
|
||||
};
|
||||
|
||||
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, frontRadius, 0.85, overlayCanvas, flattenFactor: 0.4);
|
||||
}
|
||||
|
||||
// ===== 后排 3 人 =====
|
||||
double backRadius = 70;
|
||||
var backPositions = new List<(double XNorm, double YNorm)>
|
||||
{
|
||||
(0.2, 0.88), (0.5, 0.88), (0.8, 0.88)
|
||||
};
|
||||
|
||||
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, backRadius, 0.7, overlayCanvas, flattenFactor: 0.55);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已有配置,按归一化坐标计算实际位置绘制
|
||||
foreach (var point in jumpRopeConfig.Points)
|
||||
{
|
||||
double x = point.XNorm * imgWidth;
|
||||
double y = point.YNorm * imgHeight;
|
||||
|
||||
AddGlowEllipse(x, y, point.Radius, 0.8, overlayCanvas, flattenFactor: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存配置(如果是新生成的)
|
||||
if (needSaveConfig)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
|
||||
/// </summary>
|
||||
private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas, double flattenFactor = 0.5)
|
||||
{
|
||||
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(Color.FromArgb(220, 255, 80, 80), 0.0), // 中心亮红
|
||||
new GradientStop(Color.FromArgb(180, 255, 0, 0), 0.4), // 中间红
|
||||
new GradientStop(Color.FromArgb(180, 0, 128, 255), 0.7), // 边缘蓝
|
||||
new GradientStop(Color.FromArgb(0, 0, 128, 255), 1.0) // 外部透明
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 定位到中心(Y 要根据压缩高度来调整)
|
||||
Canvas.SetLeft(ellipse, centerX - radius);
|
||||
Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2);
|
||||
canvas.Children.Add(ellipse);
|
||||
}
|
||||
|
||||
private bool IsInsideCircle(double actualX, double actualY, PointConfig p)
|
||||
{
|
||||
var dx = actualX - p.X;
|
||||
var dy = actualY - p.Y;
|
||||
var distance = Math.Sqrt(dx * dx + dy * dy);
|
||||
return distance <= p.Radius;
|
||||
}
|
||||
|
||||
|
||||
private void DrawCirclesWithText()
|
||||
{
|
||||
overlayCanvas.Children.Clear();
|
||||
//sports.Clear();
|
||||
//circleTexts.Clear();
|
||||
double canvasWidth = overlayCanvas.ActualWidth;
|
||||
double canvasHeight = overlayCanvas.ActualHeight;
|
||||
|
||||
double imgWidth = overlayCanvas.ActualWidth;
|
||||
double imgHeight = overlayCanvas.ActualHeight;
|
||||
double radius = 100;
|
||||
|
||||
// 每个圆的位置:X 和 Y 都归一化 0~1
|
||||
var 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 )
|
||||
};
|
||||
var circlePositions = GenerateCirclePositionsStaggered(
|
||||
roomWidth: 5, // 房间宽度(米)
|
||||
roomHeight: 3, // 房间高度(米)
|
||||
yOffset: 0.6, // 前排纵坐标
|
||||
yStep: 1.5, // 前后排间距
|
||||
minDistance: 1.5, // 同排最小间距
|
||||
minEdge: 1 // 两侧到边缘最小距离
|
||||
);
|
||||
|
||||
foreach (var pos in circlePositions)
|
||||
{
|
||||
double x = pos.XNorm * imgWidth;
|
||||
double y = pos.YNorm * imgHeight;
|
||||
|
||||
// 绘制发光圆
|
||||
double x = pos.XNorm * canvasWidth;
|
||||
double y = pos.YNorm * canvasHeight;
|
||||
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.Invoke(() =>
|
||||
// {
|
||||
// circleTexts[index].Text = count.ToString();
|
||||
// });
|
||||
//};
|
||||
//sport.Start();
|
||||
//sports.Add(sport);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 生成前后两排交错圆点坐标(归一化)
|
||||
/// 同排最小间距 = minDistance
|
||||
/// 两侧到边缘距离 ≥ minEdge
|
||||
/// 前后排间距 = yStep
|
||||
/// </summary>
|
||||
public List<(double XNorm, double YNorm)> GenerateCirclePositionsStaggered(
|
||||
double roomWidth,
|
||||
double roomHeight,
|
||||
double yOffset = 0.6,
|
||||
double yStep = 1.5,
|
||||
double minDistance = 1.5,
|
||||
double minEdge = 1.0)
|
||||
{
|
||||
var circlePositions = new List<(double XNorm, double YNorm)>();
|
||||
|
||||
// 1️⃣ 可用宽度
|
||||
double usableWidth = roomWidth - 2 * minEdge;
|
||||
|
||||
// 2️⃣ 前排点数
|
||||
int count = (int)Math.Floor(usableWidth / minDistance) + 1;
|
||||
if (count < 1) count = 1;
|
||||
|
||||
// 3️⃣ 前排实际步长(均分可用宽度)
|
||||
double step = (count == 1) ? 0 : usableWidth / (count - 1);
|
||||
|
||||
// 4️⃣ 前排 X/Y
|
||||
var frontX = new List<double>();
|
||||
double yFront = Math.Min(yOffset, roomHeight); // 避免超出高度
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double x = minEdge + i * step;
|
||||
frontX.Add(x);
|
||||
circlePositions.Add((x / roomWidth, yFront / roomHeight));
|
||||
}
|
||||
|
||||
// 5️⃣ 后排 X/Y(交错)
|
||||
double yBack = Math.Min(yOffset + yStep, roomHeight); // 避免超出高度
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double x;
|
||||
if (i < count - 1)
|
||||
x = (frontX[i] + frontX[i + 1]) / 2;
|
||||
else
|
||||
x = frontX[count - 1]; // 最后一个点和前排对齐
|
||||
circlePositions.Add((x / roomWidth, yBack / roomHeight));
|
||||
}
|
||||
|
||||
return circlePositions;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user