using OpenCvSharp; using System.ComponentModel; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Management; using System.Threading; using TeamAAS.Camera.Images; using TeamAAS.Camera.Models; using TeamAAS.Camera.Attributes; using TeamAAS.Camera.Enums; using System.Threading.Tasks; namespace TeamAAS.Camera.Cameras { /// /// 基于 Windows UVC/DirectShow 的通用 USB 相机实现。 /// [Camera("UVC", "UVC")] public class UvcCamera : CameraBase { private readonly object _syncRoot = new object(); private VideoCapture _capture; // “增益(亮度)”这一项实际映射到的属性:优先写 Gain(增益); // 设备不支持时回退到 Brightness(亮度)。读写必须用同一个属性,否则读回的不是写入的那个。 private VideoCaptureProperties _gainProperty = VideoCaptureProperties.Gain; [Browsable(false)] public override string CameraBrand => "UVC"; public UvcCamera(Guid id, int index, string name, string serialNumber, string cameraIp, bool isFindByIp) : base(id, index, name, serialNumber, cameraIp, isFindByIp) { CameraType = CameraType.USB; } #region 私有属性 private bool _isAutoExposure=true; [Category("II.拓展参数"), DisplayName("1.自动曝光"), Description("勾选后由相机自动曝光;取消勾选则切换到手动曝光值")] public bool IsAutoExposure { get => _isAutoExposure; set { if (SetProperty(ref _isAutoExposure, value)) ApplyAutoExposure(); } } [Category("II.拓展参数"), DisplayName("2.亮度"), Description("画面亮度(Brightness)")] public float Brightness { get => (float)GetCaptureProperty(VideoCaptureProperties.Brightness); set => SetCaptureProperty(VideoCaptureProperties.Brightness, value); } [Category("II.拓展参数"), DisplayName("3.对比度"), Description("画面对比度(Contrast)")] public float Contrast { get => (float)GetCaptureProperty(VideoCaptureProperties.Contrast); set => SetCaptureProperty(VideoCaptureProperties.Contrast, value); } [Category("II.拓展参数"), DisplayName("4.清晰度"), Description("画面清晰度(Sharpness)")] public float Sharpness { get => (float)GetCaptureProperty(VideoCaptureProperties.Sharpness); set => SetCaptureProperty(VideoCaptureProperties.Sharpness, value); } [Category("II.拓展参数"), DisplayName("5.饱和度"), Description("画面饱和度(Saturation)")] public float Saturation { get => (float)GetCaptureProperty(VideoCaptureProperties.Saturation); set => SetCaptureProperty(VideoCaptureProperties.Saturation, value); } #endregion #region 设备发现 public static CameraInfo[] GetDevices() { var devices = QueryCameraDevices(); var infos = new List(); for (var index = 0; index < devices.Count; index++) { var device = devices[index]; var name = GetString(device, "Name"); var deviceId = GetString(device, "DeviceID"); var manufacturer = GetString(device, "Manufacturer"); infos.Add(new CameraInfo { CameraNo = index, CameraName = string.IsNullOrWhiteSpace(name) ? $"UVC Camera {index}" : name, Model = string.IsNullOrWhiteSpace(name) ? $"UVC Camera {index}" : name, SerialNumber = string.IsNullOrWhiteSpace(deviceId) ? $"UVC-{index}" : deviceId, ManufacturerName = string.IsNullOrWhiteSpace(manufacturer) ? "UVC" : manufacturer, CameraBrand = "UVC", CameraType = CameraType.USB, }); } // 某些系统不会在 Win32_PnPEntity 中暴露 PNPClass,保留 OpenCV 索引探测作为兜底。 if (!infos.Any()) { for (var index = 0; index < 10; index++) { using (var capture = new VideoCapture()) { if (!capture.Open(index, VideoCaptureAPIs.DSHOW) && !capture.Open(index)) { continue; } infos.Add(new CameraInfo { CameraNo = index, CameraName = $"UVC Camera {index}", Model = $"UVC Camera {index}", SerialNumber = $"UVC-{index}", ManufacturerName = "UVC", CameraBrand = "UVC", CameraType = CameraType.USB, }); } } } return infos.ToArray(); } public static (bool isInstalled, string version) CheckSoftwareInstalled() { return (true, "OpenCvSharp4"); } #endregion #region SDK 实现 public override bool OpenDevice() { using (WriteLockScope()) { try { _capture = new VideoCapture(); var opened = _capture.Open(Index, VideoCaptureAPIs.DSHOW); if (!opened) { opened = _capture.Open(Index); } IsConnected = opened && _capture.IsOpened(); if (IsConnected) { ImageWidth = (UInt32)Math.Max(0, _capture.Get(VideoCaptureProperties.FrameWidth)); ImageHeight = (UInt32)Math.Max(0, _capture.Get(VideoCaptureProperties.FrameHeight)); try { _capture.Set(VideoCaptureProperties.BufferSize, 1); } catch { } // 连接成功后按当前开关恢复自动曝光状态 ApplyAutoExposure(); } else { ErrorMessage = $"无法打开UVC相机:{Name}"; } if (!IsConnected) OnConnectChanged(false); else OnConnectChanged(true); return IsConnected; } catch (Exception ex) { ErrorMessage = ex.Message; IsConnected = false; OnConnectChanged(false); return false; } } } public override void CloseDevice() { using (WriteLockScope()) { if (_capture != null) { lock (_syncRoot) { if (_capture.IsOpened()) { _capture.Release(); } } _capture.Dispose(); _capture = null; } IsConnected = false; OnConnectChanged(false); } } public override GenImage Grab() { using (WriteLockScope()) { var stopwatch = Stopwatch.StartNew(); try { if (_capture == null || !_capture.IsOpened()) { ErrorMessage = "相机未连接"; return null; } // 原先在这里等待 50ms 试图"等一帧新的",但 Read 取的是队列最旧帧, // 等待只会让新帧堆在旧帧后面,无法消除滞后,故移除。 using (var frame = new Mat()) { lock (_syncRoot) { // OpenCV 的 Read 出队的是缓冲队列里【最旧】的一帧。 // 冲刷只在单拍场景(非连续采集)执行:此时队列中可能滞留上一轮的旧帧, // 先读一帧丢弃,第二次读到的才是当前帧。 // 连续采集(预览)模式下队列被循环持续消费、本就只差约一帧, // 若也冲刷会让每次 Grab 吃掉两帧、预览帧率减半,故跳过。 if (!IsGrabbing) { using (var stale = new Mat()) { _capture.Read(stale); } } if (!_capture.Read(frame) || frame.Empty()) { ErrorMessage = "UVC相机采集图像失败"; return null; } } ImageWidth = (UInt32)frame.Width; ImageHeight = (UInt32)frame.Height; Image = MatToGenImage(frame); ApplyGammaToImage(Image); TotalTime = stopwatch.Elapsed; ErrorMessage = string.Empty; return Image; } } catch (Exception ex) { ErrorMessage = ex.Message; return null; } finally { stopwatch.Stop(); TotalTime = stopwatch.Elapsed; } } } /// /// 将 OpenCvSharp Mat 转换为通用 GenImage。 /// 1 通道 → Grey8,3 通道 → BGR24(OpenCV 默认 BGR),4 通道 → BGRA32。 /// private GenImage MatToGenImage(Mat frame) { if (frame == null || frame.Empty()) { return null; } PixelType format; switch (frame.Channels()) { case 1: format = PixelType.Grey8; break; case 3: format = PixelType.BGR24; break; case 4: format = PixelType.BGRA32; break; default: ErrorMessage = $"不支持的通道数:{frame.Channels()}"; return null; } return GenImage.FromIntPtr(frame.Width, frame.Height, format, frame.Data, (int)frame.Step()); } public override bool SetExposureTime(float exposureTime) { using (WriteLockScope()) { // 自动曝光开启时,曝光由相机接管,手动值会被 AE 覆盖——此时忽略手动写入; // 需要手动调曝光请先取消自动曝光。 if (IsAutoExposure) return true; // UVC/DirectShow 标准语义:曝光值 E 表示 2^E 秒。 // 把用户输入的微秒换算成 E = log2(微秒 / 1e6),并钳制到常见 UVC 范围(约 -13 ~ 0,即 0.12ms ~ 1s)。 double seconds = exposureTime / 1e6; double logExp = Math.Log(seconds, 2.0); logExp = Math.Max(-13.0, Math.Min(0.0, logExp)); return SetCaptureProperty(VideoCaptureProperties.Exposure, logExp); } } public override float GetExposureTime() { using (ReadLockScope()) { // 对数秒 -> 微秒:2^E * 1e6 double logExp = GetCaptureProperty(VideoCaptureProperties.Exposure); return (float)(Math.Pow(2.0, logExp) * 1e6); } } public override bool SetGain(float gain) { using (WriteLockScope()) { // 与手动曝光同理:UVC 的自动曝光(AE)通常连同自动增益(AGC)一起接管, // 自动模式下手动写 Gain 会被覆盖——此时忽略手动写入;需手动请先取消自动曝光。 if (IsAutoExposure) return true; // 探测 + 回退:多数 UVC 摄像头不暴露 Gain(增益)(OpenCV DSHOW 后端 // 映射到 IAMVideoProcAmp::put_Gain 会返回“属性不支持”,即 _capture.Set 返回 false)。 // 此时回退到 Brightness(亮度) —— 界面项本就是“增益(亮度)”,让“变亮”真正生效。 // 注意:只有两种属性都失败才返回 false;成功后记录实际使用的属性,保证读写对称。 var used = VideoCaptureProperties.Gain; if (!SetCaptureProperty(VideoCaptureProperties.Gain, gain)) { if (!SetCaptureProperty(VideoCaptureProperties.Brightness, gain)) return false; used = VideoCaptureProperties.Brightness; } lock (_syncRoot) { _gainProperty = used; } return true; } } public override float GetGain() { using (ReadLockScope()) { VideoCaptureProperties used; lock (_syncRoot) { used = _gainProperty; } return (float)GetCaptureProperty(used); } } public override bool SetGamma(float gamma) { using (WriteLockScope()) { UpdateGammaSnapshot(gamma); return true; } } public override float GetGamma() { using (ReadLockScope()) { return Gamma; } } /// /// 把当前自动曝光开关写入硬件:DSHOW 后端的 AutoExposure 属性 0=手动,1=自动。 /// 连接后才真正下发;未连接仅保存状态,连接后由 OpenDevice 补发。 /// 成功与否仅记录,不抛出(UI 勾选状态以本地为准,实际效果以回读为准)。 /// private void ApplyAutoExposure() { if (!IsConnected) return; try { SetCaptureProperty(VideoCaptureProperties.AutoExposure, _isAutoExposure ? 1 : 0); } catch { } } #endregion #region 辅助方法 private bool EnsureOpened() { if (_capture != null && _capture.IsOpened()) { return true; } return OpenDevice(); } private bool SetCaptureProperty(VideoCaptureProperties property, double value) { if (!EnsureOpened()) { return false; } lock (_syncRoot) { return _capture.Set(property, value); } } private double GetCaptureProperty(VideoCaptureProperties property) { if (!EnsureOpened()) { return 0; } lock (_syncRoot) { return _capture.Get(property); } } private static List QueryCameraDevices() { var result = new List(); try { using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE PNPClass='Camera' OR PNPClass='Image'")) { foreach (ManagementObject device in searcher.Get()) { var deviceId = GetString(device, "DeviceID"); if (IsUvcDevice(deviceId)) { result.Add(device); } } } } catch { } return result; } private static bool IsUvcDevice(string deviceId) { if (string.IsNullOrWhiteSpace(deviceId)) { return false; } var normalized = deviceId.ToUpperInvariant(); return normalized.StartsWith("USB\\") || normalized.Contains("MI_00") || normalized.Contains("VID_"); } private static string GetString(ManagementBaseObject obj, string propertyName) { try { return obj[propertyName]?.ToString(); } catch { return string.Empty; } } #endregion } }