using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.InteropServices; using OpenVinoSharp; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace WpfTest.core { /// /// 检测结果类 /// public class DetectionResult { public float[] Box { get; set; } // [x1, y1, x2, y2] public float Confidence { get; set; } public int ClassId { get; set; } public string ClassName { get; set; } // 用于调试的字符串表示 public override string ToString() { return $"{ClassName}: {Confidence:F3} [{Box[0]:F0}, {Box[1]:F0}, {Box[2]:F0}, {Box[3]:F0}]"; } } /// /// 模型元数据配置类 /// public class ModelMetadata { public string description { get; set; } public string author { get; set; } public string date { get; set; } public string version { get; set; } public string license { get; set; } public string docs { get; set; } public int stride { get; set; } public string task { get; set; } public int batch { get; set; } public List imgsz { get; set; } public Dictionary names { get; set; } public Dictionary args { get; set; } public int channels { get; set; } public bool end2end { get; set; } } /// /// OpenVINO YOLO 推理引擎 /// public class YoloOpenVINOInference : IDisposable { private Core _core; private CompiledModel _compiledModel; private InferRequest _inferRequest; private readonly string[] _classNames; private readonly int _inputWidth; private readonly int _inputHeight; private readonly float _confThreshold; private readonly float _iouThreshold; private readonly bool _isEnd2End; private bool _disposed = false; // 存储预处理信息 private int _padLeft, _padTop; private float _scale; /// /// 构造函数 /// /// 模型文件路径 (.xml) /// 元数据文件路径 (metadata.yaml) /// 置信度阈值 /// IOU阈值 public YoloOpenVINOInference(string modelPath, string metadataPath, float confThreshold = 0.25f, float iouThreshold = 0.45f) { _confThreshold = confThreshold; _iouThreshold = iouThreshold; // 加载类别名称和模型配置 var metadata = LoadMetadata(metadataPath); _classNames = metadata.names.OrderBy(x => x.Key).Select(x => x.Value).ToArray(); _isEnd2End = metadata.end2end; // 初始化 OpenVINO _core = new Core(); // 读取模型 var model = _core.read_model(modelPath); // 编译模型到指定设备 _compiledModel = _core.compile_model(model, "CPU"); // 创建推理请求 _inferRequest = _compiledModel.create_infer_request(); // 获取输入尺寸 var input = _compiledModel.input(); var inputShape = input.get_shape(); _inputHeight = (int)inputShape[2]; _inputWidth = (int)inputShape[3]; } /// /// 加载元数据 /// private ModelMetadata LoadMetadata(string metadataPath) { try { var yaml = File.ReadAllText(metadataPath); var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); return deserializer.Deserialize(yaml); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"加载元数据失败: {ex.Message}"); // 返回默认配置 return new ModelMetadata { names = new Dictionary { {0, "NG1"}, {1, "NG2"}, {2, "NG3"}, {3, "NG4"} }, end2end = true, imgsz = new List { 640, 640 } }; } } /// /// Letterbox 预处理(保持宽高比,添加填充) /// private (Bitmap resizedImage, int padLeft, int padTop, float scale) LetterboxImage(Bitmap image, int targetWidth, int targetHeight) { // 计算缩放比例 float scale = Math.Min((float)targetWidth / image.Width, (float)targetHeight / image.Height); int newWidth = (int)(image.Width * scale); int newHeight = (int)(image.Height * scale); // 调整图像尺寸 var resizedImage = new Bitmap(newWidth, newHeight); using (var g = Graphics.FromImage(resizedImage)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.DrawImage(image, 0, 0, newWidth, newHeight); } // 计算填充位置 int padLeft = (targetWidth - newWidth) / 2; int padTop = (targetHeight - newHeight) / 2; // 创建带填充的目标图像 var result = new Bitmap(targetWidth, targetHeight); using (var g = Graphics.FromImage(result)) { // 使用与训练时相同的填充颜色 (114, 114, 114) g.Clear(Color.FromArgb(114, 114, 114)); g.DrawImage(resizedImage, padLeft, padTop, newWidth, newHeight); } resizedImage.Dispose(); return (result, padLeft, padTop, scale); } /// /// 预处理图像 /// private float[] PreprocessImage(Bitmap image) { // Letterbox 处理 var (resizedImage, padLeft, padTop, scale) = LetterboxImage(image, _inputWidth, _inputHeight); // 存储填充信息用于后续坐标转换 _padLeft = padLeft; _padTop = padTop; _scale = scale; var inputData = new float[3 * _inputHeight * _inputWidth]; var bitmapData = resizedImage.LockBits( new Rectangle(0, 0, _inputWidth, _inputHeight), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); try { int stride = bitmapData.Stride; IntPtr scan0 = bitmapData.Scan0; byte[] pixelData = new byte[stride * _inputHeight]; Marshal.Copy(scan0, pixelData, 0, pixelData.Length); // NCHW 格式,BGR 顺序 for (int y = 0; y < _inputHeight; y++) { for (int x = 0; x < _inputWidth; x++) { int pixelIndex = y * stride + x * 3; float b = pixelData[pixelIndex] / 255.0f; float g = pixelData[pixelIndex + 1] / 255.0f; float r = pixelData[pixelIndex + 2] / 255.0f; int index = (y * _inputWidth + x); inputData[index] = b; // B inputData[_inputHeight * _inputWidth + index] = g; // G inputData[2 * _inputHeight * _inputWidth + index] = r; // R } } } finally { resizedImage.UnlockBits(bitmapData); resizedImage.Dispose(); } return inputData; } /// /// 解析 End2End 模型输出 [1, 300, 6] /// 注意:模型输出的是归一化坐标 (0-1之间) /// /// /// 解析 End2End 模型输出 [1, 300, 6] /// 模型输出格式: [cx, cy, w, h, confidence, classId] /// private List ParseEnd2EndOutput(float[] outputData, int[] shape, int imageWidth, int imageHeight) { int numDetections = shape[1]; int numAttributes = shape[2]; // 应该是 6 var detections = new List(); for (int i = 0; i < numDetections; i++) { int offset = i * numAttributes; if (offset + 5 >= outputData.Length) break; // 模型输出的是预处理后图像(640x640)的坐标 float x1_pre = outputData[offset]; float y1_pre = outputData[offset + 1]; float x2_pre = outputData[offset + 2]; float y2_pre = outputData[offset + 3]; float confidence = outputData[offset + 4]; int classId = (int)outputData[offset + 5]; // 过滤低置信度检测 if (confidence < _confThreshold) continue; // 关键:将预处理图像的坐标还原到原始图像坐标 // 使用之前存储的 _padLeft, _padTop, _scale float x1 = (x1_pre - _padLeft) / _scale; float y1 = (y1_pre - _padTop) / _scale; float x2 = (x2_pre - _padLeft) / _scale; float y2 = (y2_pre - _padTop) / _scale; // 确保坐标在原始图像范围内 x1 = Math.Max(0, Math.Min(imageWidth, x1)); y1 = Math.Max(0, Math.Min(imageHeight, y1)); x2 = Math.Max(0, Math.Min(imageWidth, x2)); y2 = Math.Max(0, Math.Min(imageHeight, y2)); // 检查有效性 if (x2 <= x1 || y2 <= y1) continue; detections.Add(new DetectionResult { Box = new float[] { x1, y1, x2, y2 }, Confidence = confidence, ClassId = classId, ClassName = classId < _classNames.Length ? _classNames[classId] : $"Class{classId}" }); } return ApplyNMS(detections); } /// /// 解析标准 YOLO 输出 [1, 84, 8400] 或类似格式 /// private List ParseStandardOutput(float[] outputData, int[] shape, int imageWidth, int imageHeight) { int numClasses = _classNames.Length; // shape[0] = batch size (1) // shape[1] = number of attributes (4 + numClasses) // shape[2] = number of predictions (8400) int numPredictions = shape[2]; // 8400 int numAttributes = shape[1]; // 4 + numClasses = 8 var detections = new List(); for (int i = 0; i < numPredictions; i++) { // 获取边界框坐标 (cx, cy, w, h) - 归一化坐标 float cx = outputData[0 * numPredictions + i]; float cy = outputData[1 * numPredictions + i]; float w = outputData[2 * numPredictions + i]; float h = outputData[3 * numPredictions + i]; // 转换为 x1, y1, x2, y2 float x1 = cx - w / 2; float y1 = cy - h / 2; float x2 = cx + w / 2; float y2 = cy + h / 2; // 获取类别置信度 float maxConf = 0; int bestClass = 0; for (int c = 0; c < numClasses; c++) { int confIndex = (4 + c) * numPredictions + i; if (confIndex >= outputData.Length) break; float conf = outputData[confIndex]; if (conf > maxConf) { maxConf = conf; bestClass = c; } } if (maxConf < _confThreshold) continue; // 坐标转换为像素坐标 float finalX1 = x1 * imageWidth; float finalY1 = y1 * imageHeight; float finalX2 = x2 * imageWidth; float finalY2 = y2 * imageHeight; finalX1 = Math.Max(0, Math.Min(imageWidth, finalX1)); finalY1 = Math.Max(0, Math.Min(imageHeight, finalY1)); finalX2 = Math.Max(0, Math.Min(imageWidth, finalX2)); finalY2 = Math.Max(0, Math.Min(imageHeight, finalY2)); detections.Add(new DetectionResult { Box = new float[] { finalX1, finalY1, finalX2, finalY2 }, Confidence = maxConf, ClassId = bestClass, ClassName = _classNames[bestClass] }); } return ApplyNMS(detections); } /// /// 非极大值抑制 /// private List ApplyNMS(List detections) { if (detections.Count == 0) return new List(); // 按置信度降序排序 detections = detections.OrderByDescending(d => d.Confidence).ToList(); var results = new List(); while (detections.Count > 0) { var best = detections[0]; results.Add(best); detections.RemoveAt(0); for (int i = detections.Count - 1; i >= 0; i--) { float iou = CalculateIOU(best.Box, detections[i].Box); if (iou > _iouThreshold) { detections.RemoveAt(i); } } } return results; } /// /// 计算两个边界框的 IOU /// private float CalculateIOU(float[] box1, float[] box2) { float x1 = Math.Max(box1[0], box2[0]); float y1 = Math.Max(box1[1], box2[1]); float x2 = Math.Min(box1[2], box2[2]); float y2 = Math.Min(box1[3], box2[3]); float intersection = Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1); float area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]); float area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]); float union = area1 + area2 - intersection; return union > 0 ? intersection / union : 0; } /// /// 执行推理 /// /// 输入图像 /// 检测结果列表 public List Infer(Bitmap image) { if (image == null) throw new ArgumentNullException(nameof(image)); // 预处理 var inputData = PreprocessImage(image); // 获取输入张量并设置数据 var inputTensor = _inferRequest.get_input_tensor(); inputTensor.set_data(inputData); // 执行推理 _inferRequest.infer(); // 获取输出 var outputTensor = _inferRequest.get_output_tensor(); var outputShape = outputTensor.get_shape(); var outputSize = outputTensor.get_size(); // 获取输出数据 var outputData = outputTensor.get_data((int)outputSize); // 获取形状信息 int[] shapeArray = new int[outputShape.Count]; for (int i = 0; i < outputShape.Count; i++) { shapeArray[i] = (int)outputShape[i]; } // 解析输出 List detections; // 判断是否为 End2End 模型输出 // End2End 模型输出形状为 [1, 300, 6] if (_isEnd2End || (shapeArray.Length == 3 && shapeArray[2] == 6)) { detections = ParseEnd2EndOutput(outputData, shapeArray, image.Width, image.Height); } else { detections = ParseStandardOutput(outputData, shapeArray, image.Width, image.Height); } return detections; } /// /// 从文件路径执行推理 /// public List InferFromFile(string imagePath) { using (var image = new Bitmap(imagePath)) { return Infer(image); } } /// /// 释放资源 /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { _inferRequest?.Dispose(); _compiledModel?.Dispose(); _core?.Dispose(); } _disposed = true; } } } }