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();