From 37184654639a8b42ed2fa44083e240b0bb169d1a Mon Sep 17 00:00:00 2001
From: twice109 <3518499@qq.com>
Date: Sat, 27 Dec 2025 14:16:50 +0800
Subject: [PATCH] =?UTF-8?q?=E9=92=88=E5=AF=B9=E7=95=8C=E9=9D=A2=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E7=9A=84=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Abstractions/Enums/SubscriptionType.cs | 45 ++++++++
.../Abstractions/Enums/TransportProtocol.cs | 5 +-
.../Abstractions/Models/ProcessingOptions.cs | 29 +++++
.../Controllers/CamerasController.cs | 106 +++++++++---------
.../Controllers/Dto/SubscriptionDto.cs | 63 +++++++++--
.../Dto/UpdateProcessingRequest.cs | 19 ++++
.../Controllers/MonitorController.cs | 60 +++++++++-
.../Core/Scheduling/FrameRequirement.cs | 76 +++++++++++--
.../Core/Services/BaseFrameProcessor.cs | 10 +-
.../Core/Services/DisplayWindowManager.cs | 1 +
.../Core/Services/ImageEnhanceCluster.cs | 79 ++++++++-----
.../Core/Services/ImageScaleCluster.cs | 58 ++++++++--
.../Core/Services/ProcessingConfigManager.cs | 37 ++++++
SHH.CameraSdk/Program.cs | 35 +++---
14 files changed, 495 insertions(+), 128 deletions(-)
create mode 100644 SHH.CameraSdk/Abstractions/Enums/SubscriptionType.cs
create mode 100644 SHH.CameraSdk/Abstractions/Models/ProcessingOptions.cs
create mode 100644 SHH.CameraSdk/Controllers/Dto/UpdateProcessingRequest.cs
create mode 100644 SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs
diff --git a/SHH.CameraSdk/Abstractions/Enums/SubscriptionType.cs b/SHH.CameraSdk/Abstractions/Enums/SubscriptionType.cs
new file mode 100644
index 0000000..2a30adc
--- /dev/null
+++ b/SHH.CameraSdk/Abstractions/Enums/SubscriptionType.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel;
+
+namespace SHH.CameraSdk;
+
+///
+/// 订阅业务类型枚举
+/// 描述视频流的最终去向和业务用途,用于帧分发策略的路由决策
+///
+public enum SubscriptionType
+{
+ ///
+ /// 本地窗口渲染
+ /// 直接在服务器端显示器绘制(如 OpenCV Window、WinForm 控件)
+ ///
+ [Description("本地窗口显示")]
+ LocalWindow = 0,
+
+ ///
+ /// 本地录像存储
+ /// 写入磁盘文件(如 MP4/AVI 格式,支持定时切割、循环覆盖)
+ ///
+ [Description("本地录像存储")]
+ LocalRecord = 1,
+
+ ///
+ /// 句柄绑定显示
+ /// 渲染到指定 HWND 窗口句柄(如 SDK 硬件解码渲染到客户端控件)
+ ///
+ [Description("句柄绑定显示")]
+ HandleDisplay = 2,
+
+ ///
+ /// 自定义网络传输
+ /// 通过私有协议转发给第三方系统(如工控机、告警服务器)
+ ///
+ [Description("自定义网络传输")]
+ NetworkTrans = 3,
+
+ ///
+ /// 网页端推流
+ /// 转码为 Web 标准协议(如 WebRTC、HLS、RTMP)供浏览器播放
+ ///
+ [Description("网页端推流")]
+ WebPush = 4
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs b/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs
index b8ecbe8..21f1d9f 100644
--- a/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs
+++ b/SHH.CameraSdk/Abstractions/Enums/TransportProtocol.cs
@@ -12,5 +12,8 @@ public enum TransportProtocol
Udp = 1,
/// 组播 (节省带宽)
- Multicast = 2
+ Multicast = 2,
+
+ /// 内存交互
+ Memory = 99,
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Abstractions/Models/ProcessingOptions.cs b/SHH.CameraSdk/Abstractions/Models/ProcessingOptions.cs
new file mode 100644
index 0000000..d94f5f3
--- /dev/null
+++ b/SHH.CameraSdk/Abstractions/Models/ProcessingOptions.cs
@@ -0,0 +1,29 @@
+namespace SHH.CameraSdk
+{
+ public class ProcessingOptions
+ {
+ // --- 缩放控制 ---
+
+ /// 是否允许缩小 (默认 True: 节约性能与带宽)
+ public bool EnableShrink { get; set; } = true;
+
+ /// 是否允许放大 (默认 False: 防止性能浪费与失真)
+ public bool EnableExpand { get; set; } = false;
+
+ /// 目标宽度
+ public int TargetWidth { get; set; } = 640;
+
+ /// 目标高度
+ public int TargetHeight { get; set; } = 360;
+
+ // --- 增亮控制 ---
+
+ /// 是否启用图像增强
+ public bool EnableEnhance { get; set; } = false; // 默认关闭,按需开启
+
+ /// 增亮强度 (0-100, 默认30)
+ public double BrightnessLevel { get; set; } = 30.0;
+
+ public static ProcessingOptions Default => new ProcessingOptions();
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/CamerasController.cs b/SHH.CameraSdk/Controllers/CamerasController.cs
index bd96574..5b3c326 100644
--- a/SHH.CameraSdk/Controllers/CamerasController.cs
+++ b/SHH.CameraSdk/Controllers/CamerasController.cs
@@ -237,60 +237,72 @@ public class CamerasController : ControllerBase
};
}
- ///
- /// [新增] 查询某台设备的当前流控策略
- ///
- [HttpGet("{id}/subscriptions")]
- public IActionResult GetSubscriptions(long id)
- {
- var device = _manager.GetDevice(id);
- if (device == null) return NotFound();
-
- // 调用刚才在 FrameController 写的方法
- var subs = device.Controller.GetCurrentRequirements();
-
- return Ok(subs);
- }
private readonly DisplayWindowManager _displayManager; // [新增]
-
+ ///
+ /// 综合订阅策略更新接口
+ /// 支持:本地 OpenCV 窗口、海康句柄穿透、本地录像、网络传输
+ ///
[HttpPost("{id}/subscriptions")]
public IActionResult UpdateSubscription(int id, [FromBody] SubscriptionDto dto)
{
var device = _manager.GetDevice(id);
if (device == null) return NotFound("设备不存在");
- if (device is HikVideoSource hikCam)
+ // 1. 自动生成 ID 逻辑
+ if (string.IsNullOrWhiteSpace(dto.AppId))
{
- // 1. 更新流控策略 (FrameController)
- // 告诉底层:这个 AppId 需要多少帧
- int totalFps = dto.DisplayFps + dto.AnalysisFps;
-
- if (totalFps > 0)
- {
- // 情况 A: 这是一个新增或更新订阅
- hikCam.Controller.Register(dto.AppId, totalFps);
-
- // 如果是预览模式,启动窗口
- if (dto.DisplayFps > 0)
- {
- _displayManager.StartDisplay(dto.AppId, id);
- }
- }
- else
- {
- // 情况 B: 这是一个停止订阅请求 (FPS 为 0)
- // 1. 【核心修复】从调度中心物理删除,不再出现在列表中
- hikCam.Controller.Unregister(dto.AppId);
-
- // 2. 关闭可能存在的本地窗口
- _displayManager.StopDisplay(dto.AppId);
- }
-
- return Ok(new { message = "Policy updated", currentConfig = hikCam.Controller.GetCurrentRequirements() });
+ dto.AppId = $"SUB_{Guid.NewGuid().ToString("N").Substring(0, 8).ToUpper()}";
}
- return BadRequest("Device implies no controller");
+ // 2. 获取流控控制器
+ var controller = device.Controller;
+ if (controller == null) return BadRequest("该设备类型不支持流控调度");
+
+ // 3. 处理注销逻辑 (FPS 为 0 代表停止订阅)
+ if (dto.DisplayFps <= 0)
+ {
+ controller.Unregister(dto.AppId);
+
+ // 停止显示管理器中所有相关的显示任务 (无论是本地窗口还是句柄绑定)
+ _displayManager.StopDisplay(dto.AppId);
+
+ device.AddAuditLog($"注销订阅: {dto.AppId}");
+ return Ok(new { Message = "Subscription removed", AppId = dto.AppId });
+ }
+
+ // 4. 业务参数合法性校验
+ switch (dto.Type)
+ {
+ case SubscriptionType.LocalRecord when string.IsNullOrEmpty(dto.SavePath):
+ return BadRequest("录像模式必须指定存放路径");
+ case SubscriptionType.HandleDisplay when string.IsNullOrEmpty(dto.Handle):
+ return BadRequest("句柄显示模式必须提供窗口句柄");
+ }
+
+ // 5. 将需求注册到流控控制器
+ controller.Register(dto.AppId, dto.DisplayFps);
+
+ // 6. 路由显示逻辑 (核心整合点)
+ if (dto.Type == SubscriptionType.LocalWindow)
+ {
+ // --- 保留旧版功能:启动本地 OpenCV 渲染窗口 ---
+ _displayManager.StartDisplay(dto.AppId, id);
+ }
+ else if (dto.Type == SubscriptionType.HandleDisplay && !string.IsNullOrEmpty(dto.Handle))
+ {
+
+ }
+
+ device.AddAuditLog($"更新订阅: {dto.AppId} ({dto.Type}), 目标 {dto.DisplayFps} FPS");
+
+ return Ok(new
+ {
+ Success = true,
+ AppId = dto.AppId,
+ Message = "订阅策略已应用",
+ CurrentConfig = controller.GetCurrentRequirements() // 返回当前所有订阅状态供前端同步
+ });
}
// 1. 获取单个设备详情(用于编辑回填)
@@ -302,14 +314,6 @@ public class CamerasController : ControllerBase
return Ok(cam.Config); // 返回原始配置对象
}
- // 2. 更新设备(保存功能)
- [HttpPut("{id}")]
- public async Task UpdateDevice(int id, [FromBody] VideoSourceConfig config)
- {
- // 核心逻辑:先停止旧设备 -> 更新配置 -> 重新添加到容器 -> 如果之前在运行则重新启动
- await _manager.UpdateDeviceAsync(id, config);
- return Ok();
- }
// 3. 清除特定设备的日志
[HttpDelete("{id}/logs")]
diff --git a/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
index 2460736..e091a5a 100644
--- a/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
+++ b/SHH.CameraSdk/Controllers/Dto/SubscriptionDto.cs
@@ -2,10 +2,10 @@
namespace SHH.CameraSdk;
-// ==============================================================================
-// 2. 订阅策略 DTO (对应 A/B/C/D 进程需求)
-// 用于多进程帧需求的注册与更新,支持显示帧和分析帧的独立配置
-// ==============================================================================
+///
+/// 视频流订阅配置请求对象
+/// 用于定义第三方应用或内部模块对指定相机流的消费需求
+///
public class SubscriptionDto
{
///
@@ -15,17 +15,62 @@ public class SubscriptionDto
[MaxLength(50, ErrorMessage = "AppId 长度不能超过 50 个字符")]
public string AppId { get; set; } = string.Empty;
+ ///
+ /// 订阅业务类型。
+ /// 决定了后端流控引擎后续的资源分配(如是否开启录像机或渲染器)。
+ ///
+ public SubscriptionType Type { get; set; }
+
///
/// 显示帧率需求 (单位: fps)
/// 不需要显示则设为 0,控制器会自动注销该类型需求
///
- [Range(0, 60, ErrorMessage = "显示帧率需在 0-60 fps 范围内")]
+ [Range(0, 30, ErrorMessage = "显示帧率需在 0-30 fps 范围内")]
public int DisplayFps { get; set; }
///
- /// 分析帧率需求 (单位: fps)
- /// 不需要分析则设为 0,控制器会自动注销该类型需求
+ /// 备注信息。
+ /// 用于记录订阅的用途、申请人或关联业务系统。
///
- [Range(0, 30, ErrorMessage = "分析帧率需在 0-30 fps 范围内")]
- public int AnalysisFps { get; set; }
+ public string Memo { get; set; }
+ = string.Empty;
+
+ ///
+ /// 窗口句柄(HWND)。
+ /// 仅在 Type 为 HandleDisplay 时必填。格式通常为十六进制或十进制字符串。
+ ///
+ public string Handle { get; set; }
+ = string.Empty;
+
+ ///
+ /// 录像持续时长(分钟,范围 1-60)。
+ /// 仅在 Type 为 LocalRecord 时有效。
+ ///
+ public int RecordDuration { get; set; }
+
+ ///
+ /// 录像文件存放绝对路径。
+ /// 仅在 Type 为 LocalRecord 时有效,例如:C:\Recordings\Room01。
+ ///
+ public string SavePath { get; set; }
+ = string.Empty;
+
+ ///
+ /// 通讯方式协议。
+ /// 仅在 Type 为 NetworkTrans 或 WebPush 时有效,默认为 Network。
+ ///
+ public TransportProtocol Protocol { get; set; }
+
+ ///
+ /// 目标接收端 IP 地址。
+ /// 仅在 Type 为 NetworkTrans 或 WebPush 且 Protocol 为 Network 时必填。
+ ///
+ public string TargetIp { get; set; }
+ = string.Empty;
+
+ ///
+ /// 目标接收端端口号。
+ /// 仅在 Type 为 NetworkTrans 或 WebPush 时必填。
+ ///
+ public int TargetPort { get; set; }
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Controllers/Dto/UpdateProcessingRequest.cs b/SHH.CameraSdk/Controllers/Dto/UpdateProcessingRequest.cs
new file mode 100644
index 0000000..a204eb2
--- /dev/null
+++ b/SHH.CameraSdk/Controllers/Dto/UpdateProcessingRequest.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SHH.CameraSdk
+{
+ public class UpdateProcessingRequest
+ {
+ public long DeviceId { get; set; }
+ public bool EnableShrink { get; set; }
+ public bool EnableExpand { get; set; }
+ public int TargetWidth { get; set; }
+ public int TargetHeight { get; set; }
+ public bool EnableEnhance { get; set; }
+ public double BrightnessLevel { get; set; }
+ }
+}
diff --git a/SHH.CameraSdk/Controllers/MonitorController.cs b/SHH.CameraSdk/Controllers/MonitorController.cs
index 2458f72..eb30ace 100644
--- a/SHH.CameraSdk/Controllers/MonitorController.cs
+++ b/SHH.CameraSdk/Controllers/MonitorController.cs
@@ -8,27 +8,50 @@ namespace SHH.CameraSdk;
/// 核心功能:提供相机设备遥测数据查询、单设备详情查询、系统日志查询
///
[ApiController]
-[Route("api/monitor")] // [建议] 显式指定路由为小写,确保与前端 ${API}/monitor/... 匹配
+[Route("api/[controller]")]
public class MonitorController : ControllerBase
{
#region --- 依赖注入 (Dependency Injection) ---
private readonly CameraManager _cameraManager;
private readonly IStorageService _storage; // [新增] 存储服务引用
+ private readonly ProcessingConfigManager _configManager;
///
/// 构造函数:注入 CameraManager 和 IStorageService
///
- public MonitorController(CameraManager cameraManager, IStorageService storage)
+ public MonitorController(CameraManager cameraManager, IStorageService storage, ProcessingConfigManager configManager)
{
_cameraManager = cameraManager;
_storage = storage;
+ _configManager = configManager;
}
#endregion
#region --- API 接口定义 (API Endpoints) ---
+ ///
+ /// 获取所有设备及配置 (对应前端 /api/Monitor/all)
+ ///
+ [HttpGet("all")] // <--- 必须明确写上 "all",否则前端找不到
+ public IActionResult GetAll()
+ {
+ var cameras = _cameraManager.GetAllDevices();
+ var list = cameras.Select(c => new {
+ c.Id,
+ Status = c.Status.ToString(),
+ c.IsPhysicalOnline,
+ c.RealFps,
+ c.TotalFrames,
+ c.Config.Name,
+ c.Config.IpAddress,
+ // 务必包含配置信息,供前端回显
+ ProcessingOptions = _configManager.GetOptions(c.Id)
+ });
+ return Ok(list);
+ }
+
///
/// 获取全量相机实时遥测数据快照
/// 适用场景:监控大屏首页数据看板
@@ -52,12 +75,18 @@ public class MonitorController : ControllerBase
return Ok(new
{
device.Id,
- device.Status,
+ Status = device.Status.ToString(),
device.IsOnline,
device.RealFps,
device.TotalFrames,
device.Config.Name,
- device.Config.IpAddress
+ device.Config.IpAddress,
+ // --- 新增:将内存中的订阅需求列表传给前端 ---
+ Requirements = device.Controller.GetCurrentRequirements().Select(r => new {
+ r.AppId,
+ r.TargetFps,
+ r.LastActive
+ })
});
}
@@ -129,4 +158,27 @@ public class MonitorController : ControllerBase
return Ok(logs);
}
+
+ [HttpPost("update-processing")]
+ public IActionResult UpdateProcessing([FromBody] UpdateProcessingRequest request)
+ {
+ // 1. 验证设备是否存在 (可选)
+
+ // 2. 构造配置对象
+ var options = new ProcessingOptions
+ {
+ EnableShrink = request.EnableShrink,
+ EnableExpand = request.EnableExpand,
+ TargetWidth = request.TargetWidth,
+ TargetHeight = request.TargetHeight,
+ EnableEnhance = request.EnableEnhance,
+ BrightnessLevel = request.BrightnessLevel
+ };
+
+ // 3. 提交给配置管理器 (实时生效)
+ // 这里的 _configManager 是通过构造函数注入的单例
+ _configManager.UpdateOptions(request.DeviceId, options);
+
+ return Ok(new { msg = $"设备 {request.DeviceId} 配置已更新", time = DateTime.Now });
+ }
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs b/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs
index 8f03a0e..4b6f8a3 100644
--- a/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs
+++ b/SHH.CameraSdk/Core/Scheduling/FrameRequirement.cs
@@ -13,25 +13,85 @@ public class FrameRequirement
/// 用于区分不同的帧消费模块,作为帧分发路由的关键标识
public string AppId { get; set; } = string.Empty;
+ /// 订阅业务类型(描述视频流的最终去向和处理逻辑)
+ /// 决定了后端流控引擎后续的资源分配(如录像机或渲染器)
+ public SubscriptionType Type { get; set; } = SubscriptionType.LocalWindow;
+
#endregion
- #region --- 帧需求参数 (Frame Requirement Parameters) ---
+ #region --- 帧率控制参数 (Frame Rate Control Parameters) ---
- /// 目标帧率(单位:fps,订阅者期望的每秒接收帧数,0 表示无特定需求)
- /// 例如:UI 预览需 8fps,AI 分析需 2fps,按需分配以节省计算资源
- public int TargetFps { get; set; } = 0;
+ /// 目标帧率(单位:fps,订阅者期望的每秒接收帧数)
+ /// 范围 1-25 fps。例如:UI 预览需 15fps,AI 分析需 5fps
+ public int TargetFps { get; set; } = 15;
- /// 上次获取帧的时间点(单位:毫秒,通常为 Environment.TickCount64)
- /// 用于帧率控制算法,判断是否达到订阅者的目标帧率需求
+ /// 上次成功分发帧的时间点(单位:毫秒,基于 Environment.TickCount64)
+ /// 帧率控制算法的核心,用于判断当前时刻是否应向此订阅者推送帧
public long LastCaptureTick { get; set; } = 0;
#endregion
- #region --- 需求状态控制 (Requirement Status Control) ---
+ #region --- 业务动态参数 (Business Dynamic Parameters) ---
+
+ /// 备注信息(记录订阅用途、申请人或关联业务系统)
+ public string Memo { get; set; } = string.Empty;
+
+ /// 窗口句柄(仅在 Type 为 HandleDisplay 时有效)
+ /// 格式通常为十六进制或十进制字符串,用于跨进程或底层渲染对接
+ public string Handle { get; set; } = string.Empty;
+
+ /// 录像持续时长(单位:分钟,仅在 Type 为 LocalRecord 时有效)
+ public int RecordDuration { get; set; } = 5;
+
+ /// 录像存放路径(仅在 Type 为 LocalRecord 时有效)
+ /// 服务器本地磁盘的绝对路径,例如:D:\Recordings\Camera_01
+ public string SavePath { get; set; } = string.Empty;
+
+ /// 通讯传输协议(仅在网络转发模式下有效)
+ public TransportProtocol Protocol { get; set; }
+ = TransportProtocol.Tcp;
+
+ /// 目标接收端 IP 地址(仅在网络转发模式下有效)
+ public string TargetIp { get; set; } = "127.0.0.1";
+
+ /// 目标接收端端口号(仅在网络转发模式下有效)
+ public int TargetPort { get; set; } = 8080;
+
+ #endregion
+
+ #region --- 运行统计与控制 (Runtime Stats & Control) ---
+
+ /// 实时计算的输出帧率(单位:fps,实际分发给订阅者的频率)
+ /// 反映系统真实运行状态,若低于 TargetFps 则说明分发链路存在瓶颈
+ public double RealFps { get; set; } = 0.0;
/// 需求是否激活(true=正常接收帧,false=暂停接收,保留配置)
- /// 支持动态启停订阅,无需删除需求配置,提升灵活性
public bool IsActive { get; set; } = true;
#endregion
+
+ #region --- 内部逻辑控制 (Internal Logic Control) ---
+
+ private int _frameCount = 0;
+ private long _lastFpsCalcTick = Environment.TickCount64;
+
+ ///
+ /// 更新实际帧率统计
+ ///
+ /// 每当成功分发一帧后调用,内部自动按秒计算 RealFps
+ public void UpdateRealFps()
+ {
+ _frameCount++;
+ long currentTick = Environment.TickCount64;
+ long elapsed = currentTick - _lastFpsCalcTick;
+
+ if (elapsed >= 1000) // 达到 1 秒周期
+ {
+ RealFps = Math.Round(_frameCount / (elapsed / 1000.0), 1);
+ _frameCount = 0;
+ _lastFpsCalcTick = currentTick;
+ }
+ }
+
+ #endregion
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs b/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs
index 1fc15e5..40e9bea 100644
--- a/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs
+++ b/SHH.CameraSdk/Core/Services/BaseFrameProcessor.cs
@@ -24,6 +24,8 @@
/// 流水线的下一个处理环节
private IFrameProcessor? _nextStep;
+ protected readonly ProcessingConfigManager _configManager; // 基类持有
+
#endregion
#region --- 构造函数 ---
@@ -33,12 +35,13 @@
///
/// Worker 线程数量(并行度)
/// 服务名称(用于日志标识)
- protected BaseFrameProcessor(int workerCount, string serviceName)
+ protected BaseFrameProcessor(int workerCount, string serviceName, ProcessingConfigManager configManager)
{
// 校验并行度参数,避免无效配置
if (workerCount < 1)
throw new ArgumentOutOfRangeException(nameof(workerCount), "Worker数量必须大于0");
+ _configManager = configManager; // 先赋值配置管理器
_workerCount = workerCount;
// 通过抽象工厂模式创建 Worker 实例
@@ -218,7 +221,7 @@
try
{
// 调用子类实现的具体图像处理算法
- PerformAction(frame, taskItem.Decision);
+ PerformAction(taskItem.DeviceId, frame, taskItem.Decision);
// 通知父集群:当前帧处理完成,准备传递到下一个环节
NotifyFinished(taskItem.DeviceId, frame, taskItem.Decision);
@@ -252,9 +255,10 @@
/// 子类实现:具体的图像处理算法逻辑
/// 如:缩放、灰度转换、AI推理等
///
+ /// 设备ID
/// 待处理的智能帧
/// 帧处理决策指令
- protected abstract void PerformAction(SmartFrame frame, FrameDecision decision);
+ protected abstract void PerformAction(long deviceId, SmartFrame frame, FrameDecision decision);
///
/// 子类实现:通知父集群处理完成
diff --git a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
index f1e5461..504d9dc 100644
--- a/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
+++ b/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs
@@ -32,6 +32,7 @@ namespace SHH.CameraSdk
public int DeviceId { get; set; }
/// 窗口暂停状态(volatile 保证多线程可见性)
public volatile bool IsPaused;
+
/// 鼠标事件回调函数
public MouseCallback MouseHandler;
/// 窗口是否已实际创建(防止重复初始化)
diff --git a/SHH.CameraSdk/Core/Services/ImageEnhanceCluster.cs b/SHH.CameraSdk/Core/Services/ImageEnhanceCluster.cs
index f7abbd2..5b4dc41 100644
--- a/SHH.CameraSdk/Core/Services/ImageEnhanceCluster.cs
+++ b/SHH.CameraSdk/Core/Services/ImageEnhanceCluster.cs
@@ -1,40 +1,65 @@
using OpenCvSharp;
+using SHH.CameraSdk;
-namespace SHH.CameraSdk.Core.Services
+public class ImageEnhanceCluster : BaseFrameProcessor
{
- ///
- /// [图像增亮服务]
- /// 实现:对流水线中的 TargetMat 执行像素级亮度提升
- ///
- public class ImageEnhanceCluster : BaseFrameProcessor
+ public ImageEnhanceCluster(int count, ProcessingConfigManager configManager)
+ : base(count, "EnhanceCluster", configManager)
{
- public ImageEnhanceCluster(int count) : base(count, "EnhanceCluster") { }
-
- protected override EnhanceWorker CreateWorker(int id) => new EnhanceWorker(this);
}
- public class EnhanceWorker : BaseWorker
+ protected override EnhanceWorker CreateWorker(int id) => new EnhanceWorker(this, _configManager);
+}
+
+public class EnhanceWorker : BaseWorker
+{
+ private readonly ImageEnhanceCluster _parent;
+ private readonly ProcessingConfigManager _configManager;
+
+ public EnhanceWorker(ImageEnhanceCluster parent, ProcessingConfigManager configManager)
{
- private readonly ImageEnhanceCluster _parent;
- public EnhanceWorker(ImageEnhanceCluster parent) => _parent = parent;
+ _parent = parent;
+ _configManager = configManager;
+ }
- protected override void PerformAction(SmartFrame frame, FrameDecision decision)
+ protected override void PerformAction(long deviceId, SmartFrame frame, FrameDecision decision)
+ {
+ // 1. 获取配置
+ var options = _configManager.GetOptions(deviceId);
+
+ // 2. 检查开关:如果没开启增强,直接跳过
+ if (!options.EnableEnhance) return;
+
+ // 3. 确定操作对象
+ // 策略:如果上一站生成了 TargetMat (缩放图),我们处理缩放图;
+ // 如果没有 (例如缩放被禁用),我们是否要处理原图?
+ // 通常 UI 预览场景下,如果不缩放,直接处理 4K 原图会非常卡。
+ // 建议:仅当 TargetMat 存在时处理,或者强制 clone 一份原图作为 TargetMat
+
+ Mat srcMat = frame.TargetMat;
+ bool createdNew = false;
+
+ // 如果没有 TargetMat (上一站透传了),但开启了增亮
+ // 我们必须基于原图生成一个 TargetMat,否则下游 UI 拿不到处理结果
+ if (srcMat == null || srcMat.IsDisposed)
{
- // 业务逻辑:只处理已经过缩放的 TargetMat
- if (frame.TargetMat != null && !frame.TargetMat.IsDisposed)
- {
- Mat brightMat = new Mat();
- // 亮度线性提升:原像素 * 1.0 + 30 偏移量
- frame.TargetMat.ConvertTo(brightMat, -1, 1.0, 30);
-
- // 替换掉原来的 TargetMat(旧的会在 AttachTarget 内部被自动 Dispose)
- frame.AttachTarget(brightMat, frame.ScaleType);
- }
+ // 注意:处理 4K 原图非常耗时,生产环境建议这里做个限制
+ srcMat = frame.InternalMat;
+ createdNew = true; // 标记我们需要 Attach 新的
}
- protected override void NotifyFinished(long did, SmartFrame frame, FrameDecision dec)
- {
- _parent.PassToNext(did, frame, dec);
- }
+ // 4. 执行增亮
+ Mat brightMat = new Mat();
+ // Alpha=1.0, Beta=配置值
+ srcMat.ConvertTo(brightMat, -1, 1.0, options.BrightnessLevel);
+
+ // 5. 挂载结果
+ // 这会自动释放上一站生成的旧 TargetMat (如果存在)
+ frame.AttachTarget(brightMat, frame.ScaleType);
+ }
+
+ protected override void NotifyFinished(long did, SmartFrame frame, FrameDecision dec)
+ {
+ _parent.PassToNext(did, frame, dec);
}
}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Core/Services/ImageScaleCluster.cs b/SHH.CameraSdk/Core/Services/ImageScaleCluster.cs
index 3cfc36e..5472571 100644
--- a/SHH.CameraSdk/Core/Services/ImageScaleCluster.cs
+++ b/SHH.CameraSdk/Core/Services/ImageScaleCluster.cs
@@ -7,29 +7,67 @@ namespace SHH.CameraSdk
///
public class ImageScaleCluster : BaseFrameProcessor
{
- public ImageScaleCluster(int count) : base(count, "ScaleCluster") { }
+ public ImageScaleCluster(int count, ProcessingConfigManager configManager)
+ : base(count, "ScaleCluster", configManager)
+ {
+ }
- protected override ScaleWorker CreateWorker(int id) => new ScaleWorker(this);
+ protected override ScaleWorker CreateWorker(int id) => new ScaleWorker(this, _configManager);
}
public class ScaleWorker : BaseWorker
{
private readonly ImageScaleCluster _parent;
- public ScaleWorker(ImageScaleCluster parent) => _parent = parent;
+ private readonly ProcessingConfigManager _configManager;
- protected override void PerformAction(SmartFrame frame, FrameDecision decision)
+ public ScaleWorker(ImageScaleCluster parent, ProcessingConfigManager configManager)
{
- int targetW = 704;
- int targetH = 576;
+ _parent = parent;
+ _configManager = configManager;
+ }
- // 算法逻辑:若尺寸符合要求则执行 Resize
- if (frame.InternalMat.Width > targetW)
+ protected override void PerformAction(long deviceId, SmartFrame frame, FrameDecision decision)
+ {
+ // 1. 获取实时配置 (极快,内存读取)
+ var options = _configManager.GetOptions(deviceId);
+
+ // 2. 原始尺寸
+ int srcW = frame.InternalMat.Width;
+ int srcH = frame.InternalMat.Height;
+
+ // 3. 目标尺寸
+ int targetW = options.TargetWidth;
+ int targetH = options.TargetHeight;
+
+ // 4. 判断是否需要缩放
+ bool needResize = false;
+
+ // 场景 A: 原始图比目标大,且允许缩小
+ if (options.EnableShrink && (srcW > targetW || srcH > targetH))
+ {
+ needResize = true;
+ }
+ // 场景 B: 原始图比目标小,且允许放大 (通常为 False)
+ else if (options.EnableExpand && (srcW < targetW || srcH < targetH))
+ {
+ needResize = true;
+ }
+
+ // 5. 执行动作
+ if (needResize)
{
Mat targetMat = new Mat();
+ // 线性插值
Cv2.Resize(frame.InternalMat, targetMat, new Size(targetW, targetH), 0, 0, InterpolationFlags.Linear);
- // 挂载到衍生属性
- frame.AttachTarget(targetMat, FrameScaleType.Shrink);
+ // 挂载
+ var scaleType = (srcW > targetW) ? FrameScaleType.Shrink : FrameScaleType.Expand;
+ frame.AttachTarget(targetMat, scaleType);
+ }
+ else
+ {
+ // [透传] 不需要缩放
+ // 此时 frame.TargetMat 为 null,代表“无变化”
}
}
diff --git a/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs b/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs
new file mode 100644
index 0000000..a9c9b29
--- /dev/null
+++ b/SHH.CameraSdk/Core/Services/ProcessingConfigManager.cs
@@ -0,0 +1,37 @@
+namespace SHH.CameraSdk;
+
+///
+/// [配置中心] 预处理参数管理器
+/// 职责:提供线程安全的配置读写接口,连接 Web API 与 底层 Worker
+///
+public class ProcessingConfigManager
+{
+ // 内存字典:Key=设备ID, Value=配置对象
+ private readonly ConcurrentDictionary _configs = new();
+
+ ///
+ /// 获取指定设备的配置(如果不存在则返回默认值)
+ ///
+ /// 设备ID
+ /// 配置对象(非空)
+ public ProcessingOptions GetOptions(long deviceId)
+ {
+ // GetOrAdd 保证了永远能拿回一个有效的配置,防止 Worker 报空指针
+ return _configs.GetOrAdd(deviceId, _ => ProcessingOptions.Default);
+ }
+
+ ///
+ /// 更新指定设备的配置(实时生效)
+ ///
+ public void UpdateOptions(long deviceId, ProcessingOptions newOptions)
+ {
+ if (newOptions == null) return;
+
+ // 直接覆盖旧配置,由于是引用替换,原子性较高
+ _configs.AddOrUpdate(deviceId, newOptions, (key, old) => newOptions);
+
+ Console.WriteLine($"[ConfigManager] 设备 {deviceId} 预处理参数已更新: " +
+ $"Expand={newOptions.EnableExpand} Shrink:{newOptions.EnableShrink} 分辨率:({newOptions.TargetWidth}x{newOptions.TargetHeight}), " +
+ $"Enhance={newOptions.EnableEnhance}");
+ }
+}
\ No newline at end of file
diff --git a/SHH.CameraSdk/Program.cs b/SHH.CameraSdk/Program.cs
index e8145c7..6967fb1 100644
--- a/SHH.CameraSdk/Program.cs
+++ b/SHH.CameraSdk/Program.cs
@@ -107,22 +107,27 @@ public class Program
{
var builder = WebApplication.CreateBuilder();
- // 注册缩放集群服务 (建议 Worker 数 = CPU 核心数,这里设为 4)
- var scaleService = new ImageScaleCluster(4); // 环节一:缩放
- var enhanceService = new ImageEnhanceCluster(4); // 环节二:增亮
+ // 1. 注册配置管理器(指挥部)
+ var configManager = new ProcessingConfigManager();
+ builder.Services.AddSingleton(configManager);
- // 逻辑:缩放 -> 增亮 -> (自动到终点)
+ // 2. 初始化预处理流水线环节
+ // 建议:此处直接手动创建实例,以便精确控制链条顺序
+ var scaleService = new ImageScaleCluster(4, configManager); // 环节一
+ var enhanceService = new ImageEnhanceCluster(4, configManager); // 环节二
+
+ // 3. 编排流水线:缩放 -> 增亮 -> 终点(GlobalProcessingCenter)
scaleService.SetNext(enhanceService);
- // 2. [核心] 将缩放服务“挂载”到全局路由上
- // 从此刻起,所有驱动层的帧都会先流经 scaleService
+ // 4. 将流水线入口挂载到全局路由(驱动层改道)
GlobalPipelineRouter.SetProcessor(scaleService);
- // 3. 注册到 DI 容器 (以便 Controller 或其他服务可以管理它,例如动态调整并行度)
- builder.Services.AddSingleton(scaleService);
- builder.Services.AddSingleton(enhanceService);
+ // 5. 【修复点】将具体实例注册到 DI 容器
+ // 这样 Controller 可以通过构造函数拿到具体的实例进行动态管理
+ builder.Services.AddSingleton(scaleService);
+ builder.Services.AddSingleton(enhanceService);
- // 1. 配置 CORS
+ // 6. 配置 CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
@@ -131,15 +136,15 @@ public class Program
});
});
+ // 7. 依赖注入注册
+ builder.Services.AddSingleton(storage);
+ builder.Services.AddSingleton(manager);
+ builder.Services.AddSingleton(displayMgr);
+
//// 2. 日志降噪
//builder.Logging.SetMinimumLevel(LogLevel.Warning);
//builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
- // 3. 【核心】依赖注入注册
- // 将 storageService 注册为单例,这样 UserActionFilter 和 MonitorController 就能拿到它了
- builder.Services.AddSingleton(storage);
- builder.Services.AddSingleton(manager);
- builder.Services.AddSingleton(displayMgr);
// 显式注册过滤器 (这是防止 500 错误的关键)
builder.Services.AddScoped();