UvcCamera.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. using OpenCvSharp;
  2. using System.ComponentModel;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Linq;
  7. using System.Management;
  8. using System.Threading;
  9. using TeamAAS.Camera.Images;
  10. using TeamAAS.Camera.Models;
  11. using TeamAAS.Camera.Attributes;
  12. using TeamAAS.Camera.Enums;
  13. using System.Threading.Tasks;
  14. namespace TeamAAS.Camera.Cameras
  15. {
  16. /// <summary>
  17. /// 基于 Windows UVC/DirectShow 的通用 USB 相机实现。
  18. /// </summary>
  19. [Camera("UVC", "UVC")]
  20. public class UvcCamera : CameraBase
  21. {
  22. private readonly object _syncRoot = new object();
  23. private VideoCapture _capture;
  24. // “增益(亮度)”这一项实际映射到的属性:优先写 Gain(增益);
  25. // 设备不支持时回退到 Brightness(亮度)。读写必须用同一个属性,否则读回的不是写入的那个。
  26. private VideoCaptureProperties _gainProperty = VideoCaptureProperties.Gain;
  27. [Browsable(false)]
  28. public override string CameraBrand => "UVC";
  29. public UvcCamera(Guid id, int index, string name, string serialNumber, string cameraIp, bool isFindByIp)
  30. : base(id, index, name, serialNumber, cameraIp, isFindByIp)
  31. {
  32. CameraType = CameraType.USB;
  33. }
  34. #region 私有属性
  35. private bool _isAutoExposure=true;
  36. [Category("II.拓展参数"), DisplayName("1.自动曝光"), Description("勾选后由相机自动曝光;取消勾选则切换到手动曝光值")]
  37. public bool IsAutoExposure
  38. {
  39. get => _isAutoExposure;
  40. set
  41. {
  42. if (SetProperty(ref _isAutoExposure, value))
  43. ApplyAutoExposure();
  44. }
  45. }
  46. [Category("II.拓展参数"), DisplayName("2.亮度"), Description("画面亮度(Brightness)")]
  47. public float Brightness
  48. {
  49. get => (float)GetCaptureProperty(VideoCaptureProperties.Brightness);
  50. set => SetCaptureProperty(VideoCaptureProperties.Brightness, value);
  51. }
  52. [Category("II.拓展参数"), DisplayName("3.对比度"), Description("画面对比度(Contrast)")]
  53. public float Contrast
  54. {
  55. get => (float)GetCaptureProperty(VideoCaptureProperties.Contrast);
  56. set => SetCaptureProperty(VideoCaptureProperties.Contrast, value);
  57. }
  58. [Category("II.拓展参数"), DisplayName("4.清晰度"), Description("画面清晰度(Sharpness)")]
  59. public float Sharpness
  60. {
  61. get => (float)GetCaptureProperty(VideoCaptureProperties.Sharpness);
  62. set => SetCaptureProperty(VideoCaptureProperties.Sharpness, value);
  63. }
  64. [Category("II.拓展参数"), DisplayName("5.饱和度"), Description("画面饱和度(Saturation)")]
  65. public float Saturation
  66. {
  67. get => (float)GetCaptureProperty(VideoCaptureProperties.Saturation);
  68. set => SetCaptureProperty(VideoCaptureProperties.Saturation, value);
  69. }
  70. #endregion
  71. #region 设备发现
  72. public static CameraInfo[] GetDevices()
  73. {
  74. var devices = QueryCameraDevices();
  75. var infos = new List<CameraInfo>();
  76. for (var index = 0; index < devices.Count; index++)
  77. {
  78. var device = devices[index];
  79. var name = GetString(device, "Name");
  80. var deviceId = GetString(device, "DeviceID");
  81. var manufacturer = GetString(device, "Manufacturer");
  82. infos.Add(new CameraInfo
  83. {
  84. CameraNo = index,
  85. CameraName = string.IsNullOrWhiteSpace(name) ? $"UVC Camera {index}" : name,
  86. Model = string.IsNullOrWhiteSpace(name) ? $"UVC Camera {index}" : name,
  87. SerialNumber = string.IsNullOrWhiteSpace(deviceId) ? $"UVC-{index}" : deviceId,
  88. ManufacturerName = string.IsNullOrWhiteSpace(manufacturer) ? "UVC" : manufacturer,
  89. CameraBrand = "UVC",
  90. CameraType = CameraType.USB,
  91. });
  92. }
  93. // 某些系统不会在 Win32_PnPEntity 中暴露 PNPClass,保留 OpenCV 索引探测作为兜底。
  94. if (!infos.Any())
  95. {
  96. for (var index = 0; index < 10; index++)
  97. {
  98. using (var capture = new VideoCapture())
  99. {
  100. if (!capture.Open(index, VideoCaptureAPIs.DSHOW) && !capture.Open(index))
  101. {
  102. continue;
  103. }
  104. infos.Add(new CameraInfo
  105. {
  106. CameraNo = index,
  107. CameraName = $"UVC Camera {index}",
  108. Model = $"UVC Camera {index}",
  109. SerialNumber = $"UVC-{index}",
  110. ManufacturerName = "UVC",
  111. CameraBrand = "UVC",
  112. CameraType = CameraType.USB,
  113. });
  114. }
  115. }
  116. }
  117. return infos.ToArray();
  118. }
  119. public static (bool isInstalled, string version) CheckSoftwareInstalled()
  120. {
  121. return (true, "OpenCvSharp4");
  122. }
  123. #endregion
  124. #region SDK 实现
  125. public override bool OpenDevice()
  126. {
  127. using (WriteLockScope())
  128. {
  129. try
  130. {
  131. _capture = new VideoCapture();
  132. var opened = _capture.Open(Index, VideoCaptureAPIs.DSHOW);
  133. if (!opened)
  134. {
  135. opened = _capture.Open(Index);
  136. }
  137. IsConnected = opened && _capture.IsOpened();
  138. if (IsConnected)
  139. {
  140. ImageWidth = (UInt32)Math.Max(0, _capture.Get(VideoCaptureProperties.FrameWidth));
  141. ImageHeight = (UInt32)Math.Max(0, _capture.Get(VideoCaptureProperties.FrameHeight));
  142. try { _capture.Set(VideoCaptureProperties.BufferSize, 1); } catch { }
  143. // 连接成功后按当前开关恢复自动曝光状态
  144. ApplyAutoExposure();
  145. }
  146. else
  147. {
  148. ErrorMessage = $"无法打开UVC相机:{Name}";
  149. }
  150. if (!IsConnected)
  151. OnConnectChanged(false);
  152. else
  153. OnConnectChanged(true);
  154. return IsConnected;
  155. }
  156. catch (Exception ex)
  157. {
  158. ErrorMessage = ex.Message;
  159. IsConnected = false;
  160. OnConnectChanged(false);
  161. return false;
  162. }
  163. }
  164. }
  165. public override void CloseDevice()
  166. {
  167. using (WriteLockScope())
  168. {
  169. if (_capture != null)
  170. {
  171. lock (_syncRoot)
  172. {
  173. if (_capture.IsOpened())
  174. {
  175. _capture.Release();
  176. }
  177. }
  178. _capture.Dispose();
  179. _capture = null;
  180. }
  181. IsConnected = false;
  182. OnConnectChanged(false);
  183. }
  184. }
  185. public override GenImage Grab()
  186. {
  187. using (WriteLockScope())
  188. {
  189. var stopwatch = Stopwatch.StartNew();
  190. try
  191. {
  192. if (_capture == null || !_capture.IsOpened())
  193. {
  194. ErrorMessage = "相机未连接";
  195. return null;
  196. }
  197. // 原先在这里等待 50ms 试图"等一帧新的",但 Read 取的是队列最旧帧,
  198. // 等待只会让新帧堆在旧帧后面,无法消除滞后,故移除。
  199. using (var frame = new Mat())
  200. {
  201. lock (_syncRoot)
  202. {
  203. // OpenCV 的 Read 出队的是缓冲队列里【最旧】的一帧。
  204. // 冲刷只在单拍场景(非连续采集)执行:此时队列中可能滞留上一轮的旧帧,
  205. // 先读一帧丢弃,第二次读到的才是当前帧。
  206. // 连续采集(预览)模式下队列被循环持续消费、本就只差约一帧,
  207. // 若也冲刷会让每次 Grab 吃掉两帧、预览帧率减半,故跳过。
  208. if (!IsGrabbing)
  209. {
  210. using (var stale = new Mat())
  211. {
  212. _capture.Read(stale);
  213. }
  214. }
  215. if (!_capture.Read(frame) || frame.Empty())
  216. {
  217. ErrorMessage = "UVC相机采集图像失败";
  218. return null;
  219. }
  220. }
  221. ImageWidth = (UInt32)frame.Width;
  222. ImageHeight = (UInt32)frame.Height;
  223. Image = MatToGenImage(frame);
  224. ApplyGammaToImage(Image);
  225. TotalTime = stopwatch.Elapsed;
  226. ErrorMessage = string.Empty;
  227. return Image;
  228. }
  229. }
  230. catch (Exception ex)
  231. {
  232. ErrorMessage = ex.Message;
  233. return null;
  234. }
  235. finally
  236. {
  237. stopwatch.Stop();
  238. TotalTime = stopwatch.Elapsed;
  239. }
  240. }
  241. }
  242. /// <summary>
  243. /// 将 OpenCvSharp Mat 转换为通用 GenImage。
  244. /// 1 通道 → Grey8,3 通道 → BGR24(OpenCV 默认 BGR),4 通道 → BGRA32。
  245. /// </summary>
  246. private GenImage MatToGenImage(Mat frame)
  247. {
  248. if (frame == null || frame.Empty())
  249. {
  250. return null;
  251. }
  252. PixelType format;
  253. switch (frame.Channels())
  254. {
  255. case 1:
  256. format = PixelType.Grey8;
  257. break;
  258. case 3:
  259. format = PixelType.BGR24;
  260. break;
  261. case 4:
  262. format = PixelType.BGRA32;
  263. break;
  264. default:
  265. ErrorMessage = $"不支持的通道数:{frame.Channels()}";
  266. return null;
  267. }
  268. return GenImage.FromIntPtr(frame.Width, frame.Height, format, frame.Data, (int)frame.Step());
  269. }
  270. public override bool SetExposureTime(float exposureTime)
  271. {
  272. using (WriteLockScope())
  273. {
  274. // 自动曝光开启时,曝光由相机接管,手动值会被 AE 覆盖——此时忽略手动写入;
  275. // 需要手动调曝光请先取消自动曝光。
  276. if (IsAutoExposure)
  277. return true;
  278. // UVC/DirectShow 标准语义:曝光值 E 表示 2^E 秒。
  279. // 把用户输入的微秒换算成 E = log2(微秒 / 1e6),并钳制到常见 UVC 范围(约 -13 ~ 0,即 0.12ms ~ 1s)。
  280. double seconds = exposureTime / 1e6;
  281. double logExp = Math.Log(seconds, 2.0);
  282. logExp = Math.Max(-13.0, Math.Min(0.0, logExp));
  283. return SetCaptureProperty(VideoCaptureProperties.Exposure, logExp);
  284. }
  285. }
  286. public override float GetExposureTime()
  287. {
  288. using (ReadLockScope())
  289. {
  290. // 对数秒 -> 微秒:2^E * 1e6
  291. double logExp = GetCaptureProperty(VideoCaptureProperties.Exposure);
  292. return (float)(Math.Pow(2.0, logExp) * 1e6);
  293. }
  294. }
  295. public override bool SetGain(float gain)
  296. {
  297. using (WriteLockScope())
  298. {
  299. // 与手动曝光同理:UVC 的自动曝光(AE)通常连同自动增益(AGC)一起接管,
  300. // 自动模式下手动写 Gain 会被覆盖——此时忽略手动写入;需手动请先取消自动曝光。
  301. if (IsAutoExposure)
  302. return true;
  303. // 探测 + 回退:多数 UVC 摄像头不暴露 Gain(增益)(OpenCV DSHOW 后端
  304. // 映射到 IAMVideoProcAmp::put_Gain 会返回“属性不支持”,即 _capture.Set 返回 false)。
  305. // 此时回退到 Brightness(亮度) —— 界面项本就是“增益(亮度)”,让“变亮”真正生效。
  306. // 注意:只有两种属性都失败才返回 false;成功后记录实际使用的属性,保证读写对称。
  307. var used = VideoCaptureProperties.Gain;
  308. if (!SetCaptureProperty(VideoCaptureProperties.Gain, gain))
  309. {
  310. if (!SetCaptureProperty(VideoCaptureProperties.Brightness, gain))
  311. return false;
  312. used = VideoCaptureProperties.Brightness;
  313. }
  314. lock (_syncRoot)
  315. {
  316. _gainProperty = used;
  317. }
  318. return true;
  319. }
  320. }
  321. public override float GetGain()
  322. {
  323. using (ReadLockScope())
  324. {
  325. VideoCaptureProperties used;
  326. lock (_syncRoot)
  327. {
  328. used = _gainProperty;
  329. }
  330. return (float)GetCaptureProperty(used);
  331. }
  332. }
  333. public override bool SetGamma(float gamma)
  334. {
  335. using (WriteLockScope())
  336. {
  337. UpdateGammaSnapshot(gamma);
  338. return true;
  339. }
  340. }
  341. public override float GetGamma()
  342. {
  343. using (ReadLockScope())
  344. {
  345. return Gamma;
  346. }
  347. }
  348. /// <summary>
  349. /// 把当前自动曝光开关写入硬件:DSHOW 后端的 AutoExposure 属性 0=手动,1=自动。
  350. /// 连接后才真正下发;未连接仅保存状态,连接后由 OpenDevice 补发。
  351. /// 成功与否仅记录,不抛出(UI 勾选状态以本地为准,实际效果以回读为准)。
  352. /// </summary>
  353. private void ApplyAutoExposure()
  354. {
  355. if (!IsConnected) return;
  356. try
  357. {
  358. SetCaptureProperty(VideoCaptureProperties.AutoExposure, _isAutoExposure ? 1 : 0);
  359. }
  360. catch { }
  361. }
  362. #endregion
  363. #region 辅助方法
  364. private bool EnsureOpened()
  365. {
  366. if (_capture != null && _capture.IsOpened())
  367. {
  368. return true;
  369. }
  370. return OpenDevice();
  371. }
  372. private bool SetCaptureProperty(VideoCaptureProperties property, double value)
  373. {
  374. if (!EnsureOpened())
  375. {
  376. return false;
  377. }
  378. lock (_syncRoot)
  379. {
  380. return _capture.Set(property, value);
  381. }
  382. }
  383. private double GetCaptureProperty(VideoCaptureProperties property)
  384. {
  385. if (!EnsureOpened())
  386. {
  387. return 0;
  388. }
  389. lock (_syncRoot)
  390. {
  391. return _capture.Get(property);
  392. }
  393. }
  394. private static List<ManagementObject> QueryCameraDevices()
  395. {
  396. var result = new List<ManagementObject>();
  397. try
  398. {
  399. using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PnPEntity WHERE PNPClass='Camera' OR PNPClass='Image'"))
  400. {
  401. foreach (ManagementObject device in searcher.Get())
  402. {
  403. var deviceId = GetString(device, "DeviceID");
  404. if (IsUvcDevice(deviceId))
  405. {
  406. result.Add(device);
  407. }
  408. }
  409. }
  410. }
  411. catch
  412. {
  413. }
  414. return result;
  415. }
  416. private static bool IsUvcDevice(string deviceId)
  417. {
  418. if (string.IsNullOrWhiteSpace(deviceId))
  419. {
  420. return false;
  421. }
  422. var normalized = deviceId.ToUpperInvariant();
  423. return normalized.StartsWith("USB\\") || normalized.Contains("MI_00") || normalized.Contains("VID_");
  424. }
  425. private static string GetString(ManagementBaseObject obj, string propertyName)
  426. {
  427. try
  428. {
  429. return obj[propertyName]?.ToString();
  430. }
  431. catch
  432. {
  433. return string.Empty;
  434. }
  435. }
  436. #endregion
  437. }
  438. }