| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- 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
- {
- /// <summary>
- /// 基于 Windows UVC/DirectShow 的通用 USB 相机实现。
- /// </summary>
- [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<CameraInfo>();
- 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;
- }
- }
- }
- /// <summary>
- /// 将 OpenCvSharp Mat 转换为通用 GenImage。
- /// 1 通道 → Grey8,3 通道 → BGR24(OpenCV 默认 BGR),4 通道 → BGRA32。
- /// </summary>
- 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;
- }
- }
- /// <summary>
- /// 把当前自动曝光开关写入硬件:DSHOW 后端的 AutoExposure 属性 0=手动,1=自动。
- /// 连接后才真正下发;未连接仅保存状态,连接后由 OpenDevice 补发。
- /// 成功与否仅记录,不抛出(UI 勾选状态以本地为准,实际效果以回读为准)。
- /// </summary>
- 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<ManagementObject> QueryCameraDevices()
- {
- var result = new List<ManagementObject>();
- 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
- }
- }
|