This commit is contained in:
tanglong 2026-01-20 15:07:09 +08:00
parent 5d8bad21de
commit 349649087d
4 changed files with 85 additions and 377 deletions

View File

@ -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; }
}
}

View File

@ -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)
{

View File

@ -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>

View File

@ -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>
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)