出圈逻辑
This commit is contained in:
parent
e95b792fef
commit
37c22bfa61
@ -1,84 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Drawing.Imaging;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Interop;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows;
|
|
||||||
|
|
||||||
namespace Wpf_AiSportsMicrospace
|
|
||||||
{
|
|
||||||
public class D3DFrameRenderer
|
|
||||||
{
|
|
||||||
private D3DImage _d3dImage;
|
|
||||||
|
|
||||||
public D3DFrameRenderer(D3DImage d3dImage)
|
|
||||||
{
|
|
||||||
_d3dImage = d3dImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当有新帧时调用
|
|
||||||
public void RenderFrame(byte[] jpegBuffer)
|
|
||||||
{
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
int width, height;
|
|
||||||
var bgra = ConvertJpegToBGRA32(jpegBuffer, out width, out height);
|
|
||||||
|
|
||||||
// 更新 UI
|
|
||||||
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
|
|
||||||
{
|
|
||||||
if (!_d3dImage.IsFrontBufferAvailable) return;
|
|
||||||
|
|
||||||
if (_d3dImage.PixelWidth != width || _d3dImage.PixelHeight != height)
|
|
||||||
{
|
|
||||||
_d3dImage.Lock();
|
|
||||||
_d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); // 先清空
|
|
||||||
_d3dImage.Unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用 WriteableBitmap 临时显示
|
|
||||||
var wb = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
|
|
||||||
wb.Lock();
|
|
||||||
Marshal.Copy(bgra, 0, wb.BackBuffer, bgra.Length);
|
|
||||||
wb.AddDirtyRect(new Int32Rect(0, 0, width, height));
|
|
||||||
wb.Unlock();
|
|
||||||
_d3dImage.Lock();
|
|
||||||
_d3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); // 暂时不绑定真实 D3D Surface
|
|
||||||
_d3dImage.Unlock();
|
|
||||||
|
|
||||||
// 临时用 ImageSource 显示
|
|
||||||
FrameReady?.Invoke(wb, width, height);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void FrameReadyHandler(ImageSource frame, int width, int height);
|
|
||||||
public event FrameReadyHandler FrameReady;
|
|
||||||
|
|
||||||
private byte[] ConvertJpegToBGRA32(byte[] jpegBuffer, out int width, out int height)
|
|
||||||
{
|
|
||||||
using var ms = new MemoryStream(jpegBuffer);
|
|
||||||
using var bmp = new Bitmap(ms);
|
|
||||||
|
|
||||||
width = bmp.Width;
|
|
||||||
height = bmp.Height;
|
|
||||||
|
|
||||||
var rect = new Rectangle(0, 0, width, height);
|
|
||||||
var bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
|
|
||||||
int bytes = bmpData.Stride * height;
|
|
||||||
byte[] raw = new byte[bytes];
|
|
||||||
Marshal.Copy(bmpData.Scan0, raw, 0, bytes);
|
|
||||||
|
|
||||||
bmp.UnlockBits(bmpData);
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
22
Wpf_AiSportsMicrospace/Dto/PointConfig.cs
Normal file
22
Wpf_AiSportsMicrospace/Dto/PointConfig.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Wpf_AiSportsMicrospace.Dto
|
||||||
|
{
|
||||||
|
public class PointConfig
|
||||||
|
{
|
||||||
|
public double X { get; set; } // 原始X(像素)
|
||||||
|
public double Y { get; set; } // 原始Y(像素)
|
||||||
|
public double Radius { get; set; } // 半径
|
||||||
|
public double XNorm { get; set; } // 归一化X(0~1)
|
||||||
|
public double YNorm { get; set; } // 归一化Y(0~1)
|
||||||
|
}
|
||||||
|
public class ConfigSet
|
||||||
|
{
|
||||||
|
public string Name { get; set; } // 配置名,比如 "JumpRope"
|
||||||
|
public List<PointConfig> Points { get; set; } = new List<PointConfig>();
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Wpf_AiSportsMicrospace
|
|
||||||
{
|
|
||||||
public class JumpRopePoint
|
|
||||||
{
|
|
||||||
/// <summary>横坐标归一化(0~1,相对于视频显示宽度)</summary>
|
|
||||||
public double XNorm { get; set; }
|
|
||||||
|
|
||||||
/// <summary>纵坐标归一化(0~1,相对于视频显示高度)</summary>
|
|
||||||
public double YNorm { get; set; }
|
|
||||||
|
|
||||||
/// <summary>圆半径,可选,单位像素</summary>
|
|
||||||
public double Radius { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -14,6 +14,8 @@ using System.Windows.Media.Imaging;
|
|||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Wpf_AiSportsMicrospace.Common;
|
using Wpf_AiSportsMicrospace.Common;
|
||||||
|
using Wpf_AiSportsMicrospace.Dto;
|
||||||
|
using Wpf_AiSportsMicrospace.Service;
|
||||||
using Yztob.AiSports.Inferences.Abstractions;
|
using Yztob.AiSports.Inferences.Abstractions;
|
||||||
using Yztob.AiSports.Inferences.Things;
|
using Yztob.AiSports.Inferences.Things;
|
||||||
using Yztob.AiSports.Postures.Sports;
|
using Yztob.AiSports.Postures.Sports;
|
||||||
@ -38,6 +40,8 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private WriteableBitmap _videoBitmap;
|
private WriteableBitmap _videoBitmap;
|
||||||
private int _lastFrameNumber = -1;
|
private int _lastFrameNumber = -1;
|
||||||
|
|
||||||
|
ConfigService configService = new ConfigService();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +67,7 @@ public partial class MainWindow : Window
|
|||||||
Application.Current.Dispatcher.InvokeAsync(() =>
|
Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
DrawJumpRope3DPointsWithGlow();
|
DrawJumpRope3DPointsWithGlow();
|
||||||
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
}, DispatcherPriority.Loaded);
|
||||||
|
|
||||||
videoImage.SizeChanged += (s, ev) =>
|
videoImage.SizeChanged += (s, ev) =>
|
||||||
{
|
{
|
||||||
@ -153,6 +157,8 @@ public partial class MainWindow : Window
|
|||||||
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
|
_webcamClient.OnExtractFrame += this.OnFrameExtracted;
|
||||||
_webcamClient.StartExtract();//开始抽帧
|
_webcamClient.StartExtract();//开始抽帧
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.startOrStop.Content = "停止(&S)";
|
this.startOrStop.Content = "停止(&S)";
|
||||||
this.sportCounts.Text = "0";
|
this.sportCounts.Text = "0";
|
||||||
this.sportTimes.Text = "00'00\"";
|
this.sportTimes.Text = "00'00\"";
|
||||||
@ -203,15 +209,39 @@ public partial class MainWindow : Window
|
|||||||
_videoBitmap.BackBufferStride);
|
_videoBitmap.BackBufferStride);
|
||||||
_videoBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
|
_videoBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight));
|
||||||
_videoBitmap.Unlock();
|
_videoBitmap.Unlock();
|
||||||
|
|
||||||
//// 延迟一帧等待控件布局完成
|
|
||||||
//Application.Current.Dispatcher.InvokeAsync(() =>
|
|
||||||
//{
|
|
||||||
// DrawJumpRopePointsOnVideoImage();
|
|
||||||
//}, System.Windows.Threading.DispatcherPriority.Loaded);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 humanResult = await Task.Run(() => _humanPredictor.Predicting(buffer, frame.Number));
|
||||||
|
|
||||||
@ -239,57 +269,104 @@ public partial class MainWindow : Window
|
|||||||
private void DrawJumpRope3DPointsWithGlow()
|
private void DrawJumpRope3DPointsWithGlow()
|
||||||
{
|
{
|
||||||
if (videoImage == null || overlayCanvas == null) return;
|
if (videoImage == null || overlayCanvas == null) return;
|
||||||
|
configService.LoadAllConfigs(); // 从文件加载 ConfigDic
|
||||||
|
|
||||||
overlayCanvas.Children.Clear();
|
ConfigSet jumpRopeConfig;
|
||||||
|
|
||||||
double imgWidth = videoImage.ActualWidth;
|
double imgWidth = videoImage.ActualWidth;
|
||||||
double imgHeight = videoImage.ActualHeight;
|
double imgHeight = videoImage.ActualHeight;
|
||||||
|
|
||||||
if (imgWidth <= 0 || imgHeight <= 0) return;
|
if (imgWidth <= 0 || imgHeight <= 0) return;
|
||||||
|
|
||||||
|
overlayCanvas.Children.Clear();
|
||||||
overlayCanvas.Width = imgWidth;
|
overlayCanvas.Width = imgWidth;
|
||||||
overlayCanvas.Height = imgHeight;
|
overlayCanvas.Height = imgHeight;
|
||||||
|
|
||||||
// 前排 3 人(近景)
|
bool needSaveConfig = false;
|
||||||
double frontRadius = 60;
|
|
||||||
|
if (!configService.ConfigDic.TryGetValue("rope-skipping", out jumpRopeConfig) || jumpRopeConfig.Points.Count == 0)
|
||||||
|
{
|
||||||
|
// 没有配置,则生成默认配置
|
||||||
|
jumpRopeConfig = new ConfigSet { Name = "rope-skipping" };
|
||||||
|
needSaveConfig = true;
|
||||||
|
|
||||||
|
// ===== 前排 3 人 =====
|
||||||
|
double frontRadius = 80;
|
||||||
var frontPositions = new List<(double XNorm, double YNorm)>
|
var frontPositions = new List<(double XNorm, double YNorm)>
|
||||||
{
|
{
|
||||||
(0.2, 0.50), (0.5, 0.50), (0.8, 0.50) // 前排略高
|
(0.25, 0.70), (0.5, 0.70), (0.75, 0.70)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var pos in frontPositions)
|
foreach (var pos in frontPositions)
|
||||||
{
|
{
|
||||||
double x = pos.XNorm * imgWidth;
|
double x = pos.XNorm * imgWidth;
|
||||||
double y = pos.YNorm * imgHeight;
|
double y = pos.YNorm * imgHeight;
|
||||||
AddGlowEllipse(x, y, frontRadius, 0.8, overlayCanvas);
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 后排 4 人(远景,更靠下面,大一点)
|
// ===== 后排 3 人 =====
|
||||||
double backRadius = 60;
|
double backRadius = 70;
|
||||||
var backPositions = new List<(double XNorm, double YNorm)>
|
var backPositions = new List<(double XNorm, double YNorm)>
|
||||||
{
|
{
|
||||||
(0.1, 0.88), (0.35, 0.88), (0.65, 0.88), (0.9, 0.88) // 后排靠底
|
(0.2, 0.88), (0.5, 0.88), (0.8, 0.88)
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var pos in backPositions)
|
foreach (var pos in backPositions)
|
||||||
{
|
{
|
||||||
double x = pos.XNorm * imgWidth;
|
double x = pos.XNorm * imgWidth;
|
||||||
double y = pos.YNorm * imgHeight;
|
double y = pos.YNorm * imgHeight;
|
||||||
AddGlowEllipse(x, y, backRadius, 0.6, overlayCanvas);
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
configService.ConfigDic[jumpRopeConfig.Name] = jumpRopeConfig;
|
||||||
|
configService.SaveAllConfigs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
|
/// 添加带渐变光的圆圈(中心红色,边缘蓝色)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas)
|
private void AddGlowEllipse(double centerX, double centerY, double radius, double opacity, Canvas canvas, double flattenFactor = 0.5)
|
||||||
{
|
{
|
||||||
var ellipse = new Ellipse
|
var ellipse = new Ellipse
|
||||||
{
|
{
|
||||||
Width = radius * 2,
|
Width = radius * 2,
|
||||||
Height = radius, // 压扁成椭圆模拟地面
|
Height = radius * flattenFactor, // 扁平化:越小越“贴地”
|
||||||
Stroke = Brushes.White,
|
|
||||||
StrokeThickness = 2,
|
|
||||||
Opacity = opacity,
|
Opacity = opacity,
|
||||||
Fill = new RadialGradientBrush
|
Fill = new RadialGradientBrush
|
||||||
{
|
{
|
||||||
@ -299,19 +376,25 @@ public partial class MainWindow : Window
|
|||||||
RadiusY = 0.5,
|
RadiusY = 0.5,
|
||||||
GradientStops = new GradientStopCollection
|
GradientStops = new GradientStopCollection
|
||||||
{
|
{
|
||||||
new GradientStop(Color.FromArgb(200, 255, 0, 0), 0.0), // 中心红
|
new GradientStop(Color.FromArgb(220, 255, 80, 80), 0.0), // 中心亮红
|
||||||
new GradientStop(Color.FromArgb(150, 255, 0, 0), 0.4), // 中间仍是红
|
new GradientStop(Color.FromArgb(180, 255, 0, 0), 0.4), // 中间红
|
||||||
new GradientStop(Color.FromArgb(180, 0, 128, 255), 0.7), // 边缘蓝
|
new GradientStop(Color.FromArgb(180, 0, 128, 255), 0.7), // 边缘蓝
|
||||||
new GradientStop(Color.FromArgb(0, 0, 128, 255), 1.0) // 最外透明
|
new GradientStop(Color.FromArgb(0, 0, 128, 255), 1.0) // 外部透明
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 定位到中心
|
// 定位到中心(Y 要根据压缩高度来调整)
|
||||||
Canvas.SetLeft(ellipse, centerX - radius);
|
Canvas.SetLeft(ellipse, centerX - radius);
|
||||||
Canvas.SetTop(ellipse, centerY - radius / 2);
|
Canvas.SetTop(ellipse, centerY - (radius * flattenFactor) / 2);
|
||||||
canvas.Children.Add(ellipse);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
52
Wpf_AiSportsMicrospace/Service/ConfigService.cs
Normal file
52
Wpf_AiSportsMicrospace/Service/ConfigService.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wpf_AiSportsMicrospace.Dto;
|
||||||
|
|
||||||
|
namespace Wpf_AiSportsMicrospace.Service
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置服务
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigService
|
||||||
|
{
|
||||||
|
public Dictionary<string, ConfigSet> ConfigDic { get; set; } = new Dictionary<string, ConfigSet>();
|
||||||
|
|
||||||
|
public ConfigService()
|
||||||
|
{
|
||||||
|
LoadAllConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存配置信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath"></param>
|
||||||
|
public void SaveAllConfigs(string fileName = "configs.json")
|
||||||
|
{
|
||||||
|
string basePath = AppContext.BaseDirectory; // 当前运行目录
|
||||||
|
string filePath = Path.Combine(basePath, fileName);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(ConfigDic, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加载配置信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileName"></param>
|
||||||
|
public void LoadAllConfigs(string fileName = "configs.json")
|
||||||
|
{
|
||||||
|
string basePath = AppContext.BaseDirectory;
|
||||||
|
string filePath = Path.Combine(basePath, fileName);
|
||||||
|
|
||||||
|
if (!File.Exists(filePath)) return;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
ConfigDic = JsonSerializer.Deserialize<Dictionary<string, ConfigSet>>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user