目录
说明
PaDiM(Patch Distribution Modeling)是一种用于异常检测和定位的经典算法,广泛应用于工业质检、医疗图像分析等领域。它通过提取预训练特征图并建模多元高斯分布来评估图像中的异常区域。本文介绍如何将训练好的 PaDiM 模型导出为 ONNX 格式,并使用 C# 和 ONNX Runtime 进行高效推理,实现图像异常检测与热力图可视化。
效果


模型信息
Model Properties
Inputs
name:input
tensor:Float[-1, 3, 256, 256]
Outputs
name:pred_score
tensor:Float[-1]
name:pred_label
tensor:Bool[-1]
name:anomaly_map
tensor:Float[-1, -1, -1, -1]
name:pred_mask
tensor:Bool[-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; // 原始图像(BGR)
Mat resultOverlayImage; // 最终叠加了热力图的图像
SessionOptions options;
InferenceSession onnx_session;
Tensor<float> input_tensor;
List<NamedOnnxValue> input_container;
IDisposableReadOnlyCollection<DisposableNamedOnnxValue> 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
List<float> dataList = new List<float>();
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 DenseTensor<float>(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")?.AsTensor<float>();
var pred_label_tensor = result_infer.FirstOrDefault(x => x.Name == "pred_label")?.AsTensor<bool>();
var anomaly_map_tensor = result_infer.FirstOrDefault(x => x.Name == "anomaly_map")?.AsTensor<float>();
var pred_mask_tensor = result_infer.FirstOrDefault(x => x.Name == "pred_mask")?.AsTensor<bool>();
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.Set<byte>(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 List<NamedOnnxValue>();
// 模型输入尺寸
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;
Tensor<float> input_tensor;
List<NamedOnnxValue> input_container;
IDisposableReadOnlyCollection<DisposableNamedOnnxValue> 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
List<float> dataList = new List<float>();
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 DenseTensor<float>(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")?.AsTensor<float>();
var pred_label_tensor = result_infer.FirstOrDefault(x => x.Name == "pred_label")?.AsTensor<bool>();
var anomaly_map_tensor = result_infer.FirstOrDefault(x => x.Name == "anomaly_map")?.AsTensor<float>();
var pred_mask_tensor = result_infer.FirstOrDefault(x => x.Name == "pred_mask")?.AsTensor<bool>();
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.Set<byte>(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 List<NamedOnnxValue>();
// 模型输入尺寸
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);
}
}
}
}