增加了图像缩放的支持

This commit is contained in:
2025-12-27 07:05:07 +08:00
parent f9027e856e
commit d4a8b63031
9 changed files with 269 additions and 3 deletions

View File

@@ -0,0 +1,25 @@
using System.ComponentModel;
namespace SHH.CameraSdk;
/// <summary>
/// 图像缩放/变换类型枚举
/// 用于控制帧数据的尺寸调整策略,适配不同的显示/分析场景
/// </summary>
public enum FrameScaleType
{
/// <summary> 无变化(保持原始尺寸)</summary>
/// <remarks>适用于高精度分析、原始帧存储等场景,无性能损耗</remarks>
[Description("原始尺寸")]
None = 0,
/// <summary> 缩小 (Downscale) - 通常用于降低带宽或 UI 预览 </summary>
/// <remarks>适用于监控大屏、低性能设备预览,可降低 CPU/内存占用</remarks>
[Description("图像缩小")]
Shrink = 1,
/// <summary> 放大 (Upscale) - 通常用于增强显示效果 </summary>
/// <remarks>适用于细节观测场景,会增加一定的性能开销,建议配合硬件加速</remarks>
[Description("图像放大")]
Expand = 2
}

View File

@@ -0,0 +1,16 @@
namespace SHH.CameraSdk;
/// <summary>
/// [标准处理契约] 帧处理器接口
/// 职责:定义所有图像预处理服务(缩放、增强、去雾等)必须遵循的标准行为
/// </summary>
public interface IFrameProcessor : IDisposable
{
/// <summary>
/// 投递任务进入处理环节
/// </summary>
/// <param name="deviceId">设备ID用于保序路由</param>
/// <param name="frame">智能帧(携带原始图像)</param>
/// <param name="decision">决策上下文(用于透传给下一站)</param>
void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision);
}

View File

@@ -31,6 +31,26 @@ public class SmartFrame : IDisposable
#endregion #endregion
#region --- (Derivative Properties) ---
/// <summary>
/// [衍生] 目标图像副本(处理后的图像)
/// <para>用途:存储经过缩放、增强后的图像,供 UI 预览或 Web 推流直接使用</para>
/// <para>生命周期:完全跟随 SmartFrame主帧销毁时自动释放</para>
/// </summary>
public Mat? TargetMat { get; private set; }
/// <summary> 变换类型(标记该 TargetMat 是被缩小了还是放大了) </summary>
public FrameScaleType ScaleType { get; private set; } = FrameScaleType.None;
/// <summary> [快捷属性] 目标图像宽度 (若 TargetMat 为空则返回 0) </summary>
public int TargetWidth => TargetMat?.Width ?? 0;
/// <summary> [快捷属性] 目标图像高度 (若 TargetMat 为空则返回 0) </summary>
public int TargetHeight => TargetMat?.Height ?? 0;
#endregion
#region --- (Constructor & Activation) --- #region --- (Constructor & Activation) ---
/// <summary> /// <summary>
@@ -45,6 +65,9 @@ public class SmartFrame : IDisposable
_pool = pool; _pool = pool;
// 预分配物理内存:内存块在帧池生命周期内复用,避免频繁申请/释放 // 预分配物理内存:内存块在帧池生命周期内复用,避免频繁申请/释放
InternalMat = new Mat(height, width, type); InternalMat = new Mat(height, width, type);
// 确保激活时清理旧的衍生数据(防止脏数据残留)
ResetDerivatives();
} }
/// <summary> /// <summary>
@@ -61,6 +84,40 @@ public class SmartFrame : IDisposable
#endregion #endregion
#region --- (Derivatives Management) ---
/// <summary>
/// 挂载处理后的目标图像
/// </summary>
/// <param name="processedMat">已处理的 Mat所有权移交给 SmartFrame</param>
/// <param name="scaleType">变换类型(缩小/放大/无)</param>
public void AttachTarget(Mat processedMat, FrameScaleType scaleType)
{
// 防御性编程:如果之前已有 TargetMat先释放防止内存泄漏
if (TargetMat != null)
{
TargetMat.Dispose();
}
TargetMat = processedMat;
ScaleType = scaleType;
}
/// <summary>
/// 内部清理:释放衍生数据
/// </summary>
private void ResetDerivatives()
{
if (TargetMat != null)
{
TargetMat.Dispose();
TargetMat = null;
}
ScaleType = FrameScaleType.None;
}
#endregion
#region --- (Reference Count Management) --- #region --- (Reference Count Management) ---
/// <summary> /// <summary>
@@ -86,7 +143,10 @@ public class SmartFrame : IDisposable
// 原子递减:线程安全,确保计数准确 // 原子递减:线程安全,确保计数准确
if (Interlocked.Decrement(ref _refCount) <= 0) if (Interlocked.Decrement(ref _refCount) <= 0)
{ {
// 引用归零:所有消费者均已释放,将帧归还池复用 // 1. 彻底清理衍生数据TargetMat 通常是 new 出来的,必须 Dispose
ResetDerivatives();
// 2. 归还到池中复用 (InternalMat 不释放,继续保留在内存池中)
_pool.Return(this); _pool.Return(this);
} }
} }

View File

@@ -0,0 +1,36 @@
namespace SHH.CameraSdk;
/// <summary>
/// [改道分发中心] 全局管道路由器
/// 职责:驱动层只管把数据扔到这里,不用关心后面是缩放、增强还是直接分发。
/// </summary>
public static class GlobalPipelineRouter
{
// 当前激活的处理器 (默认可为空,为空则直接透传)
private static IFrameProcessor? _currentProcessor;
/// <summary>
/// 配置具体的处理策略 (在 Program.cs 中初始化)
/// </summary>
public static void SetProcessor(IFrameProcessor processor)
{
_currentProcessor = processor;
}
/// <summary>
/// [驱动层入口] 提交帧数据
/// </summary>
public static void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision)
{
if (_currentProcessor != null)
{
// 场景 A: 有处理器 (如缩放服务) -> 改道进入处理器
_currentProcessor.Enqueue(deviceId, frame, decision);
}
else
{
// 场景 B: 无处理器 -> 直接进入全局分发中心 (回退到原始逻辑)
GlobalProcessingCenter.Submit(deviceId, frame, decision);
}
}
}

View File

@@ -31,4 +31,6 @@ public class FrameDecision
public List<string> TargetAppIds { get; } = new(); public List<string> TargetAppIds { get; } = new();
#endregion #endregion
public double ProcessCostMs { get; set; }
} }

View File

@@ -169,7 +169,10 @@ namespace SHH.CameraSdk
try try
{ {
// 深拷贝帧数据:原帧属于解码线程,必须克隆后移交 UI 线程 // 深拷贝帧数据:原帧属于解码线程,必须克隆后移交 UI 线程
frameClone = frame.InternalMat.Clone(); if (frame.TargetMat != null)
frameClone = frame.TargetMat.Clone();
else
frameClone = frame.InternalMat.Clone();
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,113 @@
using OpenCvSharp;
namespace SHH.CameraSdk;
/// <summary>
/// [标准动作环境] 图像预处理集群
/// 职责:透明拦截,计算缩放图挂载到 TargetMat 注入 SmartFrame然后提交给全局中心
/// 特性:
/// 1. 设备分片:基于 DeviceId 哈希路由,保证单设备帧顺序严格一致
/// 2. 零内存拷贝:直接操作 SmartFrame 引用,仅在生成新 TargetMat 时申请内存
/// 3. 闭环流转:处理完成后自动投递到 GlobalProcessingCenter
/// </summary>
/// <summary>
/// 职责:透明拦截,计算缩放图挂载到 TargetMat然后提交给全局中心
/// </summary>
public class ImageScaleCluster : IFrameProcessor
{
private readonly List<ProcessingWorker> _workers = new();
private readonly int _workerCount;
public ImageScaleCluster(int workerCount = 4)
{
_workerCount = workerCount;
for (int i = 0; i < workerCount; i++)
{
_workers.Add(new ProcessingWorker(i));
}
Console.WriteLine($"[ScaleCluster] 缩放服务已就绪 (Worker: {_workerCount})");
}
public void Enqueue(long deviceId, SmartFrame frame, FrameDecision decision)
{
// 1. 增加引用计数:跨线程持有
frame.AddRef();
// 2. 哈希分片路由:保证保序
int index = (int)(Math.Abs(deviceId) % _workerCount);
_workers[index].Post(deviceId, frame, decision);
}
public void Dispose() => _workers.ForEach(w => w.Dispose());
}
internal class ProcessingWorker : IDisposable
{
private readonly BlockingCollection<(long DeviceId, SmartFrame Frame, FrameDecision Decision)> _queue = new(100);
private readonly Task _thread;
private readonly CancellationTokenSource _cts = new();
public ProcessingWorker(int id)
{
_thread = Task.Factory.StartNew(ProcessLoop, TaskCreationOptions.LongRunning);
}
public void Post(long did, SmartFrame frame, FrameDecision decision)
{
if (!_queue.TryAdd((did, frame, decision)))
{
// 背压丢弃
frame.Dispose();
}
}
private void ProcessLoop()
{
foreach (var item in _queue.GetConsumingEnumerable(_cts.Token))
{
using (var frame = item.Frame)
{
try
{
// -------------------------------------------------
// 核心动作:缩放逻辑
// -------------------------------------------------
int targetW = 704;
int targetH = 576;
// 仅当原图大于目标时才缩放
if (frame.InternalMat.Width > targetW)
{
Mat targetMat = new Mat();
Cv2.Resize(frame.InternalMat, targetMat, new Size(targetW, targetH), 0, 0, InterpolationFlags.Linear);
// [关键] 挂载到 SmartFrame 的衍生属性中
// 标记为 Shrink (缩小)
frame.AttachTarget(targetMat, FrameScaleType.Shrink);
}
// -------------------------------------------------
// 交付下一站GlobalProcessingCenter
// 消费端对此无感知,它收到的是同一个 frame 对象
// -------------------------------------------------
GlobalProcessingCenter.Submit(item.DeviceId, frame, item.Decision);
}
catch (Exception ex)
{
Console.WriteLine($"[ScaleWorker] 异常: {ex.Message}");
// 即使处理失败,也要尝试把原图发出去,保证画面不断
GlobalProcessingCenter.Submit(item.DeviceId, frame, item.Decision);
}
}
}
}
public void Dispose()
{
_cts.Cancel();
_queue.CompleteAdding();
while (_queue.TryTake(out var item)) item.Frame.Dispose();
_queue.Dispose();
_cts.Dispose();
}
}

View File

@@ -346,7 +346,8 @@ public class HikVideoSource : BaseVideoSource
// 4. [分发] 将决策结果传递给处理中心 // 4. [分发] 将决策结果传递给处理中心
// decision.TargetAppIds 包含了 "谁需要这一帧" 的信息 // decision.TargetAppIds 包含了 "谁需要这一帧" 的信息
GlobalProcessingCenter.Submit(this.Id, smartFrame, decision); //GlobalProcessingCenter.Submit(this.Id, smartFrame, decision);
GlobalPipelineRouter.Enqueue(Id, smartFrame, decision);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -107,6 +107,16 @@ public class Program
{ {
var builder = WebApplication.CreateBuilder(); var builder = WebApplication.CreateBuilder();
// 注册缩放集群服务 (建议 Worker 数 = CPU 核心数,这里设为 4)
var scaleService = new ImageScaleCluster(4);
// 2. [核心] 将缩放服务“挂载”到全局路由上
// 从此刻起,所有驱动层的帧都会先流经 scaleService
GlobalPipelineRouter.SetProcessor(scaleService);
// 3. 注册到 DI 容器 (以便 Controller 或其他服务可以管理它,例如动态调整并行度)
builder.Services.AddSingleton<IFrameProcessor>(scaleService);
// 1. 配置 CORS // 1. 配置 CORS
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {