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

目录

说明

效果

模型信息

项目

代码

下载


说明

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);
            }
        }
    }
}

下载

源码下载

相关推荐
波诺波1 小时前
p1项目system_model.py代码
开发语言·python
危笑ioi1 小时前
helm部署skywalking链路追踪 java
java·开发语言·skywalking
静心观复2 小时前
Python 虚拟环境与 pipx 详解
开发语言·python
卷心菜狗2 小时前
Re.从零开始使用Python构建本地大模型网页智慧聊天机器人
开发语言·python·机器人
书到用时方恨少!2 小时前
Python NumPy 使用指南:科学计算的基石
开发语言·python·numpy
2501_933329552 小时前
技术深度拆解:Infoseek舆情系统的全链路架构与核心实现
开发语言·人工智能·分布式·架构
Chan162 小时前
MCP 开发实战:Git 信息查询 MCP 服务开发
java·开发语言·spring boot·git·spring·java-ee·intellij-idea
web前端进阶者3 小时前
Rust初学知识点快速记忆
开发语言·后端·rust
lucky九年3 小时前
GO语言模拟C++封装,继承,多态
开发语言·c++·golang