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
}
}