YoloOpenVINOInference.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Drawing.Drawing2D;
  5. using System.Drawing.Imaging;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Runtime.InteropServices;
  9. using OpenVinoSharp;
  10. using YamlDotNet.Serialization;
  11. using YamlDotNet.Serialization.NamingConventions;
  12. namespace WpfTest.core
  13. {
  14. /// <summary>
  15. /// 检测结果类
  16. /// </summary>
  17. public class DetectionResult
  18. {
  19. public float[] Box { get; set; } // [x1, y1, x2, y2]
  20. public float Confidence { get; set; }
  21. public int ClassId { get; set; }
  22. public string ClassName { get; set; }
  23. // 用于调试的字符串表示
  24. public override string ToString()
  25. {
  26. return $"{ClassName}: {Confidence:F3} [{Box[0]:F0}, {Box[1]:F0}, {Box[2]:F0}, {Box[3]:F0}]";
  27. }
  28. }
  29. /// <summary>
  30. /// 模型元数据配置类
  31. /// </summary>
  32. public class ModelMetadata
  33. {
  34. public string description { get; set; }
  35. public string author { get; set; }
  36. public string date { get; set; }
  37. public string version { get; set; }
  38. public string license { get; set; }
  39. public string docs { get; set; }
  40. public int stride { get; set; }
  41. public string task { get; set; }
  42. public int batch { get; set; }
  43. public List<int> imgsz { get; set; }
  44. public Dictionary<int, string> names { get; set; }
  45. public Dictionary<string, object> args { get; set; }
  46. public int channels { get; set; }
  47. public bool end2end { get; set; }
  48. }
  49. /// <summary>
  50. /// OpenVINO YOLO 推理引擎
  51. /// </summary>
  52. public class YoloOpenVINOInference : IDisposable
  53. {
  54. private Core _core;
  55. private CompiledModel _compiledModel;
  56. private InferRequest _inferRequest;
  57. private readonly string[] _classNames;
  58. private readonly int _inputWidth;
  59. private readonly int _inputHeight;
  60. private readonly float _confThreshold;
  61. private readonly float _iouThreshold;
  62. private readonly bool _isEnd2End;
  63. private bool _disposed = false;
  64. // 存储预处理信息
  65. private int _padLeft, _padTop;
  66. private float _scale;
  67. /// <summary>
  68. /// 构造函数
  69. /// </summary>
  70. /// <param name="modelPath">模型文件路径 (.xml)</param>
  71. /// <param name="metadataPath">元数据文件路径 (metadata.yaml)</param>
  72. /// <param name="confThreshold">置信度阈值</param>
  73. /// <param name="iouThreshold">IOU阈值</param>
  74. public YoloOpenVINOInference(string modelPath, string metadataPath,
  75. float confThreshold = 0.25f, float iouThreshold = 0.45f)
  76. {
  77. _confThreshold = confThreshold;
  78. _iouThreshold = iouThreshold;
  79. // 加载类别名称和模型配置
  80. var metadata = LoadMetadata(metadataPath);
  81. _classNames = metadata.names.OrderBy(x => x.Key).Select(x => x.Value).ToArray();
  82. _isEnd2End = metadata.end2end;
  83. // 初始化 OpenVINO
  84. _core = new Core();
  85. // 读取模型
  86. var model = _core.read_model(modelPath);
  87. // 编译模型到指定设备
  88. _compiledModel = _core.compile_model(model, "CPU");
  89. // 创建推理请求
  90. _inferRequest = _compiledModel.create_infer_request();
  91. // 获取输入尺寸
  92. var input = _compiledModel.input();
  93. var inputShape = input.get_shape();
  94. _inputHeight = (int)inputShape[2];
  95. _inputWidth = (int)inputShape[3];
  96. }
  97. /// <summary>
  98. /// 加载元数据
  99. /// </summary>
  100. private ModelMetadata LoadMetadata(string metadataPath)
  101. {
  102. try
  103. {
  104. var yaml = File.ReadAllText(metadataPath);
  105. var deserializer = new DeserializerBuilder()
  106. .WithNamingConvention(UnderscoredNamingConvention.Instance)
  107. .Build();
  108. return deserializer.Deserialize<ModelMetadata>(yaml);
  109. }
  110. catch (Exception ex)
  111. {
  112. System.Diagnostics.Debug.WriteLine($"加载元数据失败: {ex.Message}");
  113. // 返回默认配置
  114. return new ModelMetadata
  115. {
  116. names = new Dictionary<int, string>
  117. {
  118. {0, "NG1"}, {1, "NG2"}, {2, "NG3"}, {3, "NG4"}
  119. },
  120. end2end = true,
  121. imgsz = new List<int> { 640, 640 }
  122. };
  123. }
  124. }
  125. /// <summary>
  126. /// Letterbox 预处理(保持宽高比,添加填充)
  127. /// </summary>
  128. private (Bitmap resizedImage, int padLeft, int padTop, float scale) LetterboxImage(Bitmap image, int targetWidth, int targetHeight)
  129. {
  130. // 计算缩放比例
  131. float scale = Math.Min((float)targetWidth / image.Width, (float)targetHeight / image.Height);
  132. int newWidth = (int)(image.Width * scale);
  133. int newHeight = (int)(image.Height * scale);
  134. // 调整图像尺寸
  135. var resizedImage = new Bitmap(newWidth, newHeight);
  136. using (var g = Graphics.FromImage(resizedImage))
  137. {
  138. g.InterpolationMode = InterpolationMode.HighQualityBicubic;
  139. g.DrawImage(image, 0, 0, newWidth, newHeight);
  140. }
  141. // 计算填充位置
  142. int padLeft = (targetWidth - newWidth) / 2;
  143. int padTop = (targetHeight - newHeight) / 2;
  144. // 创建带填充的目标图像
  145. var result = new Bitmap(targetWidth, targetHeight);
  146. using (var g = Graphics.FromImage(result))
  147. {
  148. // 使用与训练时相同的填充颜色 (114, 114, 114)
  149. g.Clear(Color.FromArgb(114, 114, 114));
  150. g.DrawImage(resizedImage, padLeft, padTop, newWidth, newHeight);
  151. }
  152. resizedImage.Dispose();
  153. return (result, padLeft, padTop, scale);
  154. }
  155. /// <summary>
  156. /// 预处理图像
  157. /// </summary>
  158. private float[] PreprocessImage(Bitmap image)
  159. {
  160. // Letterbox 处理
  161. var (resizedImage, padLeft, padTop, scale) = LetterboxImage(image, _inputWidth, _inputHeight);
  162. // 存储填充信息用于后续坐标转换
  163. _padLeft = padLeft;
  164. _padTop = padTop;
  165. _scale = scale;
  166. var inputData = new float[3 * _inputHeight * _inputWidth];
  167. var bitmapData = resizedImage.LockBits(
  168. new Rectangle(0, 0, _inputWidth, _inputHeight),
  169. ImageLockMode.ReadOnly,
  170. PixelFormat.Format24bppRgb);
  171. try
  172. {
  173. int stride = bitmapData.Stride;
  174. IntPtr scan0 = bitmapData.Scan0;
  175. byte[] pixelData = new byte[stride * _inputHeight];
  176. Marshal.Copy(scan0, pixelData, 0, pixelData.Length);
  177. // NCHW 格式,BGR 顺序
  178. for (int y = 0; y < _inputHeight; y++)
  179. {
  180. for (int x = 0; x < _inputWidth; x++)
  181. {
  182. int pixelIndex = y * stride + x * 3;
  183. float b = pixelData[pixelIndex] / 255.0f;
  184. float g = pixelData[pixelIndex + 1] / 255.0f;
  185. float r = pixelData[pixelIndex + 2] / 255.0f;
  186. int index = (y * _inputWidth + x);
  187. inputData[index] = b; // B
  188. inputData[_inputHeight * _inputWidth + index] = g; // G
  189. inputData[2 * _inputHeight * _inputWidth + index] = r; // R
  190. }
  191. }
  192. }
  193. finally
  194. {
  195. resizedImage.UnlockBits(bitmapData);
  196. resizedImage.Dispose();
  197. }
  198. return inputData;
  199. }
  200. /// <summary>
  201. /// 解析 End2End 模型输出 [1, 300, 6]
  202. /// 注意:模型输出的是归一化坐标 (0-1之间)
  203. /// </summary>
  204. /// <summary>
  205. /// 解析 End2End 模型输出 [1, 300, 6]
  206. /// 模型输出格式: [cx, cy, w, h, confidence, classId]
  207. /// </summary>
  208. private List<DetectionResult> ParseEnd2EndOutput(float[] outputData, int[] shape, int imageWidth, int imageHeight)
  209. {
  210. int numDetections = shape[1];
  211. int numAttributes = shape[2]; // 应该是 6
  212. var detections = new List<DetectionResult>();
  213. for (int i = 0; i < numDetections; i++)
  214. {
  215. int offset = i * numAttributes;
  216. if (offset + 5 >= outputData.Length)
  217. break;
  218. // 模型输出的是预处理后图像(640x640)的坐标
  219. float x1_pre = outputData[offset];
  220. float y1_pre = outputData[offset + 1];
  221. float x2_pre = outputData[offset + 2];
  222. float y2_pre = outputData[offset + 3];
  223. float confidence = outputData[offset + 4];
  224. int classId = (int)outputData[offset + 5];
  225. // 过滤低置信度检测
  226. if (confidence < _confThreshold)
  227. continue;
  228. // 关键:将预处理图像的坐标还原到原始图像坐标
  229. // 使用之前存储的 _padLeft, _padTop, _scale
  230. float x1 = (x1_pre - _padLeft) / _scale;
  231. float y1 = (y1_pre - _padTop) / _scale;
  232. float x2 = (x2_pre - _padLeft) / _scale;
  233. float y2 = (y2_pre - _padTop) / _scale;
  234. // 确保坐标在原始图像范围内
  235. x1 = Math.Max(0, Math.Min(imageWidth, x1));
  236. y1 = Math.Max(0, Math.Min(imageHeight, y1));
  237. x2 = Math.Max(0, Math.Min(imageWidth, x2));
  238. y2 = Math.Max(0, Math.Min(imageHeight, y2));
  239. // 检查有效性
  240. if (x2 <= x1 || y2 <= y1)
  241. continue;
  242. detections.Add(new DetectionResult
  243. {
  244. Box = new float[] { x1, y1, x2, y2 },
  245. Confidence = confidence,
  246. ClassId = classId,
  247. ClassName = classId < _classNames.Length ? _classNames[classId] : $"Class{classId}"
  248. });
  249. }
  250. return ApplyNMS(detections);
  251. }
  252. /// <summary>
  253. /// 解析标准 YOLO 输出 [1, 84, 8400] 或类似格式
  254. /// </summary>
  255. private List<DetectionResult> ParseStandardOutput(float[] outputData, int[] shape, int imageWidth, int imageHeight)
  256. {
  257. int numClasses = _classNames.Length;
  258. // shape[0] = batch size (1)
  259. // shape[1] = number of attributes (4 + numClasses)
  260. // shape[2] = number of predictions (8400)
  261. int numPredictions = shape[2]; // 8400
  262. int numAttributes = shape[1]; // 4 + numClasses = 8
  263. var detections = new List<DetectionResult>();
  264. for (int i = 0; i < numPredictions; i++)
  265. {
  266. // 获取边界框坐标 (cx, cy, w, h) - 归一化坐标
  267. float cx = outputData[0 * numPredictions + i];
  268. float cy = outputData[1 * numPredictions + i];
  269. float w = outputData[2 * numPredictions + i];
  270. float h = outputData[3 * numPredictions + i];
  271. // 转换为 x1, y1, x2, y2
  272. float x1 = cx - w / 2;
  273. float y1 = cy - h / 2;
  274. float x2 = cx + w / 2;
  275. float y2 = cy + h / 2;
  276. // 获取类别置信度
  277. float maxConf = 0;
  278. int bestClass = 0;
  279. for (int c = 0; c < numClasses; c++)
  280. {
  281. int confIndex = (4 + c) * numPredictions + i;
  282. if (confIndex >= outputData.Length)
  283. break;
  284. float conf = outputData[confIndex];
  285. if (conf > maxConf)
  286. {
  287. maxConf = conf;
  288. bestClass = c;
  289. }
  290. }
  291. if (maxConf < _confThreshold)
  292. continue;
  293. // 坐标转换为像素坐标
  294. float finalX1 = x1 * imageWidth;
  295. float finalY1 = y1 * imageHeight;
  296. float finalX2 = x2 * imageWidth;
  297. float finalY2 = y2 * imageHeight;
  298. finalX1 = Math.Max(0, Math.Min(imageWidth, finalX1));
  299. finalY1 = Math.Max(0, Math.Min(imageHeight, finalY1));
  300. finalX2 = Math.Max(0, Math.Min(imageWidth, finalX2));
  301. finalY2 = Math.Max(0, Math.Min(imageHeight, finalY2));
  302. detections.Add(new DetectionResult
  303. {
  304. Box = new float[] { finalX1, finalY1, finalX2, finalY2 },
  305. Confidence = maxConf,
  306. ClassId = bestClass,
  307. ClassName = _classNames[bestClass]
  308. });
  309. }
  310. return ApplyNMS(detections);
  311. }
  312. /// <summary>
  313. /// 非极大值抑制
  314. /// </summary>
  315. private List<DetectionResult> ApplyNMS(List<DetectionResult> detections)
  316. {
  317. if (detections.Count == 0)
  318. return new List<DetectionResult>();
  319. // 按置信度降序排序
  320. detections = detections.OrderByDescending(d => d.Confidence).ToList();
  321. var results = new List<DetectionResult>();
  322. while (detections.Count > 0)
  323. {
  324. var best = detections[0];
  325. results.Add(best);
  326. detections.RemoveAt(0);
  327. for (int i = detections.Count - 1; i >= 0; i--)
  328. {
  329. float iou = CalculateIOU(best.Box, detections[i].Box);
  330. if (iou > _iouThreshold)
  331. {
  332. detections.RemoveAt(i);
  333. }
  334. }
  335. }
  336. return results;
  337. }
  338. /// <summary>
  339. /// 计算两个边界框的 IOU
  340. /// </summary>
  341. private float CalculateIOU(float[] box1, float[] box2)
  342. {
  343. float x1 = Math.Max(box1[0], box2[0]);
  344. float y1 = Math.Max(box1[1], box2[1]);
  345. float x2 = Math.Min(box1[2], box2[2]);
  346. float y2 = Math.Min(box1[3], box2[3]);
  347. float intersection = Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1);
  348. float area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]);
  349. float area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]);
  350. float union = area1 + area2 - intersection;
  351. return union > 0 ? intersection / union : 0;
  352. }
  353. /// <summary>
  354. /// 执行推理
  355. /// </summary>
  356. /// <param name="image">输入图像</param>
  357. /// <returns>检测结果列表</returns>
  358. public List<DetectionResult> Infer(Bitmap image)
  359. {
  360. if (image == null)
  361. throw new ArgumentNullException(nameof(image));
  362. // 预处理
  363. var inputData = PreprocessImage(image);
  364. // 获取输入张量并设置数据
  365. var inputTensor = _inferRequest.get_input_tensor();
  366. inputTensor.set_data(inputData);
  367. // 执行推理
  368. _inferRequest.infer();
  369. // 获取输出
  370. var outputTensor = _inferRequest.get_output_tensor();
  371. var outputShape = outputTensor.get_shape();
  372. var outputSize = outputTensor.get_size();
  373. // 获取输出数据
  374. var outputData = outputTensor.get_data<float>((int)outputSize);
  375. // 获取形状信息
  376. int[] shapeArray = new int[outputShape.Count];
  377. for (int i = 0; i < outputShape.Count; i++)
  378. {
  379. shapeArray[i] = (int)outputShape[i];
  380. }
  381. // 解析输出
  382. List<DetectionResult> detections;
  383. // 判断是否为 End2End 模型输出
  384. // End2End 模型输出形状为 [1, 300, 6]
  385. if (_isEnd2End || (shapeArray.Length == 3 && shapeArray[2] == 6))
  386. {
  387. detections = ParseEnd2EndOutput(outputData, shapeArray, image.Width, image.Height);
  388. }
  389. else
  390. {
  391. detections = ParseStandardOutput(outputData, shapeArray, image.Width, image.Height);
  392. }
  393. return detections;
  394. }
  395. /// <summary>
  396. /// 从文件路径执行推理
  397. /// </summary>
  398. public List<DetectionResult> InferFromFile(string imagePath)
  399. {
  400. using (var image = new Bitmap(imagePath))
  401. {
  402. return Infer(image);
  403. }
  404. }
  405. /// <summary>
  406. /// 释放资源
  407. /// </summary>
  408. public void Dispose()
  409. {
  410. Dispose(true);
  411. GC.SuppressFinalize(this);
  412. }
  413. protected virtual void Dispose(bool disposing)
  414. {
  415. if (!_disposed)
  416. {
  417. if (disposing)
  418. {
  419. _inferRequest?.Dispose();
  420. _compiledModel?.Dispose();
  421. _core?.Dispose();
  422. }
  423. _disposed = true;
  424. }
  425. }
  426. }
  427. }