使用 C# 和 ONNX Runtime 部署 PaDiM 异常检测模型

张开发
2026/4/4 16:34:23 15 分钟阅读
使用 C# 和 ONNX Runtime 部署 PaDiM 异常检测模型
目录说明效果模型信息项目代码下载说明PaDiMPatch Distribution Modeling是一种用于异常检测和定位的经典算法广泛应用于工业质检、医疗图像分析等领域。它通过提取预训练特征图并建模多元高斯分布来评估图像中的异常区域。本文介绍如何将训练好的 PaDiM 模型导出为 ONNX 格式并使用 C# 和 ONNX Runtime 进行高效推理实现图像异常检测与热力图可视化。效果模型信息Model Properties----------------------------------------------------------------------------------------Inputs-------------------------nameinputtensorFloat[-1, 3, 256, 256]---------------------------------------------------------------Outputs-------------------------namepred_scoretensorFloat[-1]namepred_labeltensorBool[-1]nameanomaly_maptensorFloat[-1, -1, -1, -1]namepred_masktensorBool[-1, -1, -1, -1]---------------------------------------------------------------项目代码using Microsoft.ML.OnnxRuntime;using Microsoft.ML.OnnxRuntime.Tensors;using OpenCvSharp;using System;using System.Collections.Generic;using System.Drawing;using System.Drawing.Imaging;using System.Linq;using System.Windows.Forms;namespace Onnx_Demo{public partial class Form1 : Form{public Form1(){InitializeComponent();}string fileFilter *.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.png;string image_path ;string startupPath;DateTime dt1 DateTime.Now;DateTime dt2 DateTime.Now;string model_path;Mat originalImage; // 原始图像BGRMat resultOverlayImage; // 最终叠加了热力图的图像SessionOptions options;InferenceSession onnx_session;Tensorfloat input_tensor;ListNamedOnnxValue input_container;IDisposableReadOnlyCollectionDisposableNamedOnnxValue result_infer;int inpHeight, inpWidth;private void button1_Click(object sender, EventArgs e){OpenFileDialog ofd new OpenFileDialog();ofd.Filter fileFilter;if (ofd.ShowDialog() ! DialogResult.OK) return;pictureBox1.Image null;image_path ofd.FileName;pictureBox1.Image new Bitmap(image_path);textBox1.Text ;originalImage new Mat(image_path);pictureBox2.Image null;}private void button2_Click(object sender, EventArgs e){if (image_path ){MessageBox.Show(请先选择图片);return;}button2.Enabled false;pictureBox2.Image null;textBox1.Text ;Application.DoEvents();// 读取原始图像originalImage new Mat(image_path);int originalWidth originalImage.Cols;int originalHeight originalImage.Rows;// ------------------ 预处理 ------------------// 1. BGR - RGBMat rgbImage new Mat();Cv2.CvtColor(originalImage, rgbImage, ColorConversionCodes.BGR2RGB);// 2. Resize 到模型输入尺寸 256x256Mat resized new Mat();Cv2.Resize(rgbImage, resized, new OpenCvSharp.Size(inpWidth, inpHeight));// 3. 归一化到 [0,1] 并转为 floatresized.ConvertTo(resized, MatType.CV_32FC3, 1.0 / 255.0);// 4. HWC - CHW构造输入张量int height inpHeight;int width inpWidth;Mat[] channels Cv2.Split(resized); // R, G, BListfloat dataList new Listfloat();for (int c 0; c 3; c){float[] channelData new float[height * width];System.Runtime.InteropServices.Marshal.Copy(channels[c].Data, channelData, 0, height * width);dataList.AddRange(channelData);}float[] inputData dataList.ToArray();input_tensor new DenseTensorfloat(inputData, new[] { 1, 3, height, width });// 构造输入容器注意名称必须是 inputinput_container.Clear();input_container.Add(NamedOnnxValue.CreateFromTensor(input, input_tensor));// ------------------ 推理 ------------------dt1 DateTime.Now;result_infer onnx_session.Run(input_container);dt2 DateTime.Now;// ------------------ 获取输出 ------------------// 输出名称pred_score, pred_label, anomaly_map, pred_maskvar pred_score_tensor result_infer.FirstOrDefault(x x.Name pred_score)?.AsTensorfloat();var pred_label_tensor result_infer.FirstOrDefault(x x.Name pred_label)?.AsTensorbool();var anomaly_map_tensor result_infer.FirstOrDefault(x x.Name anomaly_map)?.AsTensorfloat();var pred_mask_tensor result_infer.FirstOrDefault(x x.Name pred_mask)?.AsTensorbool();if (pred_score_tensor null || anomaly_map_tensor null){MessageBox.Show(模型输出不符合预期请检查 onnx 文件);button2.Enabled true;return;}// 解析分数和标签float score pred_score_tensor.First(); // 假设第一个元素即为整体异常分数bool label pred_label_tensor?.First() ?? false;// 解析 anomaly_map形状通常为 [1, 1, H, W] 或 [1, H, W]var dimensions anomaly_map_tensor.Dimensions.ToArray();int mapH dimensions.Length 3 ? dimensions[dimensions.Length - 2] : 0;int mapW dimensions.Length 2 ? dimensions[dimensions.Length - 1] : 0;float[] mapData anomaly_map_tensor.ToArray();// 确保是二维数据如果有多余维度则 reshapeif (dimensions.Length 4 dimensions[0] 1 dimensions[1] 1){// 已经是 [1,1,H,W] 直接使用mapH dimensions[2];mapW dimensions[3];}else if (dimensions.Length 3 dimensions[0] 1){// [1,H,W] 的情况mapH dimensions[1];mapW dimensions[2];}else{// 若形状不符合预期尝试平铺后重新组织int total mapData.Length;mapH (int)Math.Sqrt(total);mapW mapH;if (mapH * mapW ! total) mapW total / mapH;}// 将 anomaly_map 转为 Mat (CV_32FC1)Mat anomalyMat new Mat(mapH, mapW, MatType.CV_32FC1, mapData);// ------------------ 后处理 ------------------// 1. 将异常图 resize 到原始图像尺寸Mat anomalyResized new Mat();Cv2.Resize(anomalyMat, anomalyResized, new OpenCvSharp.Size(originalWidth, originalHeight), interpolation: InterpolationFlags.Linear);// 2. Min-Max 归一化到 [0,1] 范围double minVal, maxVal;Cv2.MinMaxLoc(anomalyResized, out minVal, out maxVal);Mat anomalyNorm new Mat();if (maxVal - minVal 1e-6){anomalyResized.ConvertTo(anomalyNorm, MatType.CV_32FC1, 1.0 / (maxVal - minVal), -minVal / (maxVal - minVal));}else{anomalyNorm anomalyResized.Clone();}// 3. 转换为 8bit 灰度图 [0,255]Mat anomalyGray new Mat();anomalyNorm.ConvertTo(anomalyGray, MatType.CV_8UC1, 255.0);// 4. 应用 JET 伪彩色生成热力图Mat heatmap new Mat();Cv2.ApplyColorMap(anomalyGray, heatmap, ColormapTypes.Jet);// 5. 热力图与原图融合权重 0.5 热力图 0.5 原图Mat originalBGR originalImage.Clone();Mat overlay new Mat();Cv2.AddWeighted(heatmap, 0.5, originalBGR, 0.5, 0, overlay);// 叠加 pred_mask 轮廓二值掩膜if (pred_mask_tensor ! null){bool[] maskData pred_mask_tensor.ToArray();// 假设 mask 形状与 anomaly_map 相同同样 resize 到原图大小Mat maskMat new Mat(mapH, mapW, MatType.CV_8UC1);for (int i 0; i maskData.Length; i)maskMat.Setbyte(i / mapW, i % mapW, maskData[i] ? (byte)255 : (byte)0);Mat maskResized new Mat();Cv2.Resize(maskMat, maskResized, new OpenCvSharp.Size(originalWidth, originalHeight));// 创建红色半透明图层Mat maskColor new Mat(originalHeight, originalWidth, MatType.CV_8UC3, new Scalar(0, 0, 255));// 先计算全图加权无 maskMat blended new Mat();Cv2.AddWeighted(overlay, 1.0, maskColor, 0.3, 0, blended);// 将 mask 区域从 blended 复制到 overlay 中blended.CopyTo(overlay, maskResized);}resultOverlayImage overlay.Clone();// 显示结果pictureBox2.Image new Bitmap(overlay.ToMemoryStream());// 显示推理信息string resultText $推理耗时: {(dt2 - dt1).TotalMilliseconds:F2} ms\r\n;resultText $异常分数: {score:F4}\r\n;resultText $异常判定: {(label ? 异常 : 正常)};textBox1.Text resultText;button2.Enabled true;}private void Form1_Load(object sender, EventArgs e){startupPath Application.StartupPath;model_path model/patchcore.onnx;// 使用 CPU 推理可改为 CUDAoptions new SessionOptions();options.LogSeverityLevel OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO;options.AppendExecutionProvider_CPU(0);onnx_session new InferenceSession(model_path, options);input_container new ListNamedOnnxValue();// 模型输入尺寸inpHeight 256;inpWidth 256;// 可选默认加载测试图片string testImg test_img/broken_large/000.png;if (System.IO.File.Exists(testImg)){image_path testImg;pictureBox1.Image new Bitmap(image_path);originalImage new Mat(image_path);}}private void pictureBox1_DoubleClick(object sender, EventArgs e){Common.ShowNormalImg(pictureBox1.Image);}private void pictureBox2_DoubleClick(object sender, EventArgs e){Common.ShowNormalImg(pictureBox2.Image);}SaveFileDialog sdf new SaveFileDialog();private void button3_Click(object sender, EventArgs e){if (resultOverlayImage null || resultOverlayImage.Empty()){MessageBox.Show(请先进行推理);return;}sdf.Title 保存带热力图的图片;sdf.Filter PNG图片 (*.png)|*.png|JPEG图片 (*.jpg)|*.jpg;sdf.FilterIndex 1;if (sdf.ShowDialog() DialogResult.OK){string ext System.IO.Path.GetExtension(sdf.FileName).ToLower();ImageFormat format ext .jpg || ext .jpeg ? ImageFormat.Jpeg : ImageFormat.Png;using (var stream resultOverlayImage.ToMemoryStream())using (var bitmap new Bitmap(stream)){bitmap.Save(sdf.FileName, format);}MessageBox.Show(保存成功 sdf.FileName);}}}}using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.Linq; using System.Windows.Forms; namespace Onnx_Demo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } string fileFilter *.*|*.bmp;*.jpg;*.jpeg;*.tiff;*.png; string image_path ; string startupPath; DateTime dt1 DateTime.Now; DateTime dt2 DateTime.Now; string model_path; Mat originalImage; // 原始图像BGR Mat resultOverlayImage; // 最终叠加了热力图的图像 SessionOptions options; InferenceSession onnx_session; Tensorfloat input_tensor; ListNamedOnnxValue input_container; IDisposableReadOnlyCollectionDisposableNamedOnnxValue result_infer; int inpHeight, inpWidth; private void button1_Click(object sender, EventArgs e) { OpenFileDialog ofd new OpenFileDialog(); ofd.Filter fileFilter; if (ofd.ShowDialog() ! DialogResult.OK) return; pictureBox1.Image null; image_path ofd.FileName; pictureBox1.Image new Bitmap(image_path); textBox1.Text ; originalImage new Mat(image_path); pictureBox2.Image null; } private void button2_Click(object sender, EventArgs e) { if (image_path ) { MessageBox.Show(请先选择图片); return; } button2.Enabled false; pictureBox2.Image null; textBox1.Text ; Application.DoEvents(); // 读取原始图像 originalImage new Mat(image_path); int originalWidth originalImage.Cols; int originalHeight originalImage.Rows; // ------------------ 预处理 ------------------ // 1. BGR - RGB Mat rgbImage new Mat(); Cv2.CvtColor(originalImage, rgbImage, ColorConversionCodes.BGR2RGB); // 2. Resize 到模型输入尺寸 256x256 Mat resized new Mat(); Cv2.Resize(rgbImage, resized, new OpenCvSharp.Size(inpWidth, inpHeight)); // 3. 归一化到 [0,1] 并转为 float resized.ConvertTo(resized, MatType.CV_32FC3, 1.0 / 255.0); // 4. HWC - CHW构造输入张量 int height inpHeight; int width inpWidth; Mat[] channels Cv2.Split(resized); // R, G, B Listfloat dataList new Listfloat(); for (int c 0; c 3; c) { float[] channelData new float[height * width]; System.Runtime.InteropServices.Marshal.Copy(channels[c].Data, channelData, 0, height * width); dataList.AddRange(channelData); } float[] inputData dataList.ToArray(); input_tensor new DenseTensorfloat(inputData, new[] { 1, 3, height, width }); // 构造输入容器注意名称必须是 input input_container.Clear(); input_container.Add(NamedOnnxValue.CreateFromTensor(input, input_tensor)); // ------------------ 推理 ------------------ dt1 DateTime.Now; result_infer onnx_session.Run(input_container); dt2 DateTime.Now; // ------------------ 获取输出 ------------------ // 输出名称pred_score, pred_label, anomaly_map, pred_mask var pred_score_tensor result_infer.FirstOrDefault(x x.Name pred_score)?.AsTensorfloat(); var pred_label_tensor result_infer.FirstOrDefault(x x.Name pred_label)?.AsTensorbool(); var anomaly_map_tensor result_infer.FirstOrDefault(x x.Name anomaly_map)?.AsTensorfloat(); var pred_mask_tensor result_infer.FirstOrDefault(x x.Name pred_mask)?.AsTensorbool(); if (pred_score_tensor null || anomaly_map_tensor null) { MessageBox.Show(模型输出不符合预期请检查 onnx 文件); button2.Enabled true; return; } // 解析分数和标签 float score pred_score_tensor.First(); // 假设第一个元素即为整体异常分数 bool label pred_label_tensor?.First() ?? false; // 解析 anomaly_map形状通常为 [1, 1, H, W] 或 [1, H, W] var dimensions anomaly_map_tensor.Dimensions.ToArray(); int mapH dimensions.Length 3 ? dimensions[dimensions.Length - 2] : 0; int mapW dimensions.Length 2 ? dimensions[dimensions.Length - 1] : 0; float[] mapData anomaly_map_tensor.ToArray(); // 确保是二维数据如果有多余维度则 reshape if (dimensions.Length 4 dimensions[0] 1 dimensions[1] 1) { // 已经是 [1,1,H,W] 直接使用 mapH dimensions[2]; mapW dimensions[3]; } else if (dimensions.Length 3 dimensions[0] 1) { // [1,H,W] 的情况 mapH dimensions[1]; mapW dimensions[2]; } else { // 若形状不符合预期尝试平铺后重新组织 int total mapData.Length; mapH (int)Math.Sqrt(total); mapW mapH; if (mapH * mapW ! total) mapW total / mapH; } // 将 anomaly_map 转为 Mat (CV_32FC1) Mat anomalyMat new Mat(mapH, mapW, MatType.CV_32FC1, mapData); // ------------------ 后处理 ------------------ // 1. 将异常图 resize 到原始图像尺寸 Mat anomalyResized new Mat(); Cv2.Resize(anomalyMat, anomalyResized, new OpenCvSharp.Size(originalWidth, originalHeight), interpolation: InterpolationFlags.Linear); // 2. Min-Max 归一化到 [0,1] 范围 double minVal, maxVal; Cv2.MinMaxLoc(anomalyResized, out minVal, out maxVal); Mat anomalyNorm new Mat(); if (maxVal - minVal 1e-6) { anomalyResized.ConvertTo(anomalyNorm, MatType.CV_32FC1, 1.0 / (maxVal - minVal), -minVal / (maxVal - minVal)); } else { anomalyNorm anomalyResized.Clone(); } // 3. 转换为 8bit 灰度图 [0,255] Mat anomalyGray new Mat(); anomalyNorm.ConvertTo(anomalyGray, MatType.CV_8UC1, 255.0); // 4. 应用 JET 伪彩色生成热力图 Mat heatmap new Mat(); Cv2.ApplyColorMap(anomalyGray, heatmap, ColormapTypes.Jet); // 5. 热力图与原图融合权重 0.5 热力图 0.5 原图 Mat originalBGR originalImage.Clone(); Mat overlay new Mat(); Cv2.AddWeighted(heatmap, 0.5, originalBGR, 0.5, 0, overlay); // 叠加 pred_mask 轮廓二值掩膜 if (pred_mask_tensor ! null) { bool[] maskData pred_mask_tensor.ToArray(); // 假设 mask 形状与 anomaly_map 相同同样 resize 到原图大小 Mat maskMat new Mat(mapH, mapW, MatType.CV_8UC1); for (int i 0; i maskData.Length; i) maskMat.Setbyte(i / mapW, i % mapW, maskData[i] ? (byte)255 : (byte)0); Mat maskResized new Mat(); Cv2.Resize(maskMat, maskResized, new OpenCvSharp.Size(originalWidth, originalHeight)); // 创建红色半透明图层 Mat maskColor new Mat(originalHeight, originalWidth, MatType.CV_8UC3, new Scalar(0, 0, 255)); // 先计算全图加权无 mask Mat blended new Mat(); Cv2.AddWeighted(overlay, 1.0, maskColor, 0.3, 0, blended); // 将 mask 区域从 blended 复制到 overlay 中 blended.CopyTo(overlay, maskResized); } resultOverlayImage overlay.Clone(); // 显示结果 pictureBox2.Image new Bitmap(overlay.ToMemoryStream()); // 显示推理信息 string resultText $推理耗时: {(dt2 - dt1).TotalMilliseconds:F2} ms\r\n; resultText $异常分数: {score:F4}\r\n; resultText $异常判定: {(label ? 异常 : 正常)}; textBox1.Text resultText; button2.Enabled true; } private void Form1_Load(object sender, EventArgs e) { startupPath Application.StartupPath; model_path model/patchcore.onnx; // 使用 CPU 推理可改为 CUDA options new SessionOptions(); options.LogSeverityLevel OrtLoggingLevel.ORT_LOGGING_LEVEL_INFO; options.AppendExecutionProvider_CPU(0); onnx_session new InferenceSession(model_path, options); input_container new ListNamedOnnxValue(); // 模型输入尺寸 inpHeight 256; inpWidth 256; // 可选默认加载测试图片 string testImg test_img/broken_large/000.png; if (System.IO.File.Exists(testImg)) { image_path testImg; pictureBox1.Image new Bitmap(image_path); originalImage new Mat(image_path); } } private void pictureBox1_DoubleClick(object sender, EventArgs e) { Common.ShowNormalImg(pictureBox1.Image); } private void pictureBox2_DoubleClick(object sender, EventArgs e) { Common.ShowNormalImg(pictureBox2.Image); } SaveFileDialog sdf new SaveFileDialog(); private void button3_Click(object sender, EventArgs e) { if (resultOverlayImage null || resultOverlayImage.Empty()) { MessageBox.Show(请先进行推理); return; } sdf.Title 保存带热力图的图片; sdf.Filter PNG图片 (*.png)|*.png|JPEG图片 (*.jpg)|*.jpg; sdf.FilterIndex 1; if (sdf.ShowDialog() DialogResult.OK) { string ext System.IO.Path.GetExtension(sdf.FileName).ToLower(); ImageFormat format ext .jpg || ext .jpeg ? ImageFormat.Jpeg : ImageFormat.Png; using (var stream resultOverlayImage.ToMemoryStream()) using (var bitmap new Bitmap(stream)) { bitmap.Save(sdf.FileName, format); } MessageBox.Show(保存成功 sdf.FileName); } } } }下载源码下载

更多文章