你是否曾经为了部署一个AI模型而头疼不已?训练好的模型在不同平台间迁移困难,性能优化复杂,部署成本居高不下......作为C#开发者,我们迫切需要一个高效、跨平台的AI推理解决方案。
今天,我将带你用最简单的方式搭建第一个ONNX Runtime程序,让你在5分钟内体验到AI模型部署的魅力。本文将解决初学者最关心的三个问题:如何快速上手、常见坑点避免、实际项目应用。
在传统的AI模型部署中,开发者通常面临以下挑战:
ONNX Runtime完美解决了这些问题:它是微软开源的高性能机器学习推理引擎,支持多种硬件平台,专为生产环境优化。
C#// 安装ONNX Runtime CPU版本
dotnet add package Microsoft.ML.OnnxRuntime --version 1.23.2
⚠️ 重要提醒:选择CPU版本还是GPU版本要根据实际需求,初学者建议先从CPU版本开始。
下载一个简单的ONNX模型用于测试(建议使用mnist手写数字识别模型):
C#// 模型文件放在项目根目录下
// mnist-8.onnx (28x28像素的手写数字识别模型)
C#using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
namespace AppOnnx
{
internal class Program
{
static void Main(string[] args)
{
try
{
// 步骤1:初始化推理会话
var sessionOptions = new SessionOptions();
using var session = new InferenceSession("mnist-8.onnx", sessionOptions);
// 检查模型的输入输出信息
PrintModelInfo(session);
// 步骤2:准备输入数据
var inputData = CreateSampleInput();
// 获取正确的输入节点名称
var inputName = session.InputMetadata.Keys.First();
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor(inputName, inputData)
};
// 步骤3:执行推理
using var results = session.Run(inputs);
// 步骤4:处理输出结果
var output = results.FirstOrDefault()?.AsTensor<float>();
if (output != null)
{
var predictedDigit = GetPredictedDigit(output);
Console.WriteLine($"🎉 预测结果: {predictedDigit}");
Console.WriteLine($"📊 置信度分布:");
PrintConfidenceScores(output);
}
else
{
Console.WriteLine("❌ 无法获取输出结果");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 执行出错: {ex.Message}");
Console.WriteLine($"📍 堆栈跟踪: {ex.StackTrace}");
}
}
// 打印模型信息
private static void PrintModelInfo(InferenceSession session)
{
Console.WriteLine("📋 模型输入信息:");
foreach (var input in session.InputMetadata)
{
Console.WriteLine($" 名称: {input.Key}");
Console.WriteLine($" 类型: {input.Value.ElementType}");
Console.WriteLine($" 维度: [{string.Join(", ", input.Value.Dimensions)}]");
}
Console.WriteLine("\n📋 模型输出信息:");
foreach (var output in session.OutputMetadata)
{
Console.WriteLine($" 名称: {output.Key}");
Console.WriteLine($" 类型: {output.Value.ElementType}");
Console.WriteLine($" 维度: [{string.Join(", ", output.Value.Dimensions)}]");
}
Console.WriteLine();
}
// 创建示例输入数据(模拟28x28的手写数字图像)
private static Tensor<float> CreateSampleInput()
{
// 标准MNIST输入格式:[batch_size, channels, height, width] 或 [batch_size, height, width, channels]
var tensor = new DenseTensor<float>(new[] { 1, 1, 28, 28 });
// 模拟一个简单的数字"1"
for (int i = 10; i < 18; i++)
{
for (int j = 12; j < 16; j++)
{
if (i < 28 && j < 28) // 添加边界检查
{
tensor[0, 0, i, j] = 1.0f;
}
}
}
return tensor;
}
// 获取预测结果
private static int GetPredictedDigit(Tensor<float> output)
{
if (output == null || output.Length == 0) return -1;
var maxIndex = 0;
var maxValue = float.MinValue;
// 安全的索引访问
var span = output.ToArray(); // 转换为数组进行安全访问
for (int i = 0; i < span.Length && i < 10; i++) // MNIST有10个类别(0-9)
{
if (span[i] > maxValue)
{
maxValue = span[i];
maxIndex = i;
}
}
return maxIndex;
}
// 打印置信度分数
private static void PrintConfidenceScores(Tensor<float> output)
{
if (output == null || output.Length == 0) return;
var span = output.ToArray();
var length = Math.Min(span.Length, 10); // 确保不超过10个类别
for (int i = 0; i < length; i++)
{
Console.WriteLine($"数字 {i}: {span[i]:F4}");
}
}
}
}

C#using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenCvSharp;
namespace AppOnnx
{
internal class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
try
{
// 步骤1:初始化推理会话
var sessionOptions = new SessionOptions();
using var session = new InferenceSession("mnist-8.onnx", sessionOptions);
// 检查模型的输入输出信息
PrintModelInfo(session);
// 步骤2:从图片文件加载和预处理输入数据
var inputData = LoadAndPreprocessImage("hand.jpg");
if (inputData == null)
{
Console.WriteLine("❌ 无法加载图片文件 hand.jpg");
return;
}
// 获取正确的输入节点名称
var inputName = session.InputMetadata.Keys.First();
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor(inputName, inputData)
};
// 步骤3:执行推理
using var results = session.Run(inputs);
// 步骤4:处理输出结果
var output = results.FirstOrDefault()?.AsTensor<float>();
if (output != null)
{
var predictedDigit = GetPredictedDigit(output);
Console.WriteLine($"🎉 预测结果: {predictedDigit}");
Console.WriteLine($"📊 置信度分布:");
PrintConfidenceScores(output);
}
else
{
Console.WriteLine("❌ 无法获取输出结果");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 执行出错: {ex.Message}");
Console.WriteLine($"📍 堆栈跟踪: {ex.StackTrace}");
}
}
// 打印模型信息
private static void PrintModelInfo(InferenceSession session)
{
Console.WriteLine("📋 模型输入信息:");
foreach (var input in session.InputMetadata)
{
Console.WriteLine($" 名称: {input.Key}");
Console.WriteLine($" 类型: {input.Value.ElementType}");
Console.WriteLine($" 维度: [{string.Join(", ", input.Value.Dimensions)}]");
}
Console.WriteLine("\n📋 模型输出信息:");
foreach (var output in session.OutputMetadata)
{
Console.WriteLine($" 名称: {output.Key}");
Console.WriteLine($" 类型: {output.Value.ElementType}");
Console.WriteLine($" 维度: [{string.Join(", ", output.Value.Dimensions)}]");
}
Console.WriteLine();
}
// 🔥 使用OpenCvSharp加载和预处理图片
private static Tensor<float>? LoadAndPreprocessImage(string imagePath)
{
try
{
if (!File.Exists(imagePath))
{
Console.WriteLine($"❌ 图片文件不存在: {imagePath}");
return null;
}
Console.WriteLine($"📸 正在使用OpenCV加载图片: {imagePath}");
// 1. 加载图片(自动转换为灰度图)
using var originalImage = Cv2.ImRead(imagePath, ImreadModes.Grayscale);
if (originalImage.Empty())
{
Console.WriteLine("❌ 无法读取图片文件");
return null;
}
Console.WriteLine($"📏 原始图片尺寸: {originalImage.Width}x{originalImage.Height}");
// 2. 调整大小到28x28
using var resizedImage = new Mat();
Cv2.Resize(originalImage, resizedImage, new Size(28, 28), interpolation: InterpolationFlags.Area);
// 3. 应用高斯模糊以减少噪声
using var blurredImage = new Mat();
Cv2.GaussianBlur(resizedImage, blurredImage, new Size(3, 3), 0);
// 4. 应用阈值处理以增强对比度
using var thresholdImage = new Mat();
Cv2.Threshold(blurredImage, thresholdImage, 128, 255, ThresholdTypes.Binary);
// 5. 可选:保存预处理后的图片用于调试
SavePreprocessedImage(thresholdImage, "preprocessed_hand.png");
// 6. 转换为张量
var tensor = ConvertMatToTensor(thresholdImage);
Console.WriteLine("✅ 图片预处理完成");
return tensor;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 图片处理失败: {ex.Message}");
return null;
}
}
// 将OpenCV Mat转换为ONNX张量
private static Tensor<float> ConvertMatToTensor(Mat image)
{
var tensor = new DenseTensor<float>(new[] { 1, 1, 28, 28 });
// 获取图像数据
var imageData = new byte[28 * 28];
image.GetArray(out imageData);
for (int y = 0; y < 28; y++)
{
for (int x = 0; x < 28; x++)
{
var pixelValue = imageData[y * 28 + x];
// 归一化到0-1范围
// 对于MNIST,白色背景(255)应该是0,黑色数字(0)应该是1
float normalizedValue = (255.0f - pixelValue) / 255.0f;
tensor[0, 0, y, x] = normalizedValue;
}
}
return tensor;
}
// 保存预处理后的图片用于调试
private static void SavePreprocessedImage(Mat image, string filename)
{
try
{
Cv2.ImWrite(filename, image);
Console.WriteLine($"🔍 预处理图片已保存: {filename}");
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ 保存预处理图片失败: {ex.Message}");
}
}
// 高级图像预处理版本(可选使用)
private static Tensor<float>? LoadAndPreprocessImageAdvanced(string imagePath)
{
try
{
Console.WriteLine($"📸 正在进行高级图像预处理: {imagePath}");
using var originalImage = Cv2.ImRead(imagePath, ImreadModes.Grayscale);
if (originalImage.Empty())
{
Console.WriteLine("❌ 无法读取图片文件");
return null;
}
// 1. 自适应阈值处理
using var adaptiveThresh = new Mat();
Cv2.AdaptiveThreshold(originalImage, adaptiveThresh, 255,
AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 11, 2);
// 2. 形态学操作去除噪声
using var kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(2, 2));
using var morphImage = new Mat();
Cv2.MorphologyEx(adaptiveThresh, morphImage, MorphTypes.Close, kernel);
// 3. 寻找轮廓并提取数字区域
Cv2.FindContours(morphImage, out var contours, out var hierarchy,
RetrievalModes.External, ContourApproximationModes.ApproxSimple);
if (contours.Length > 0)
{
// 找到最大的轮廓(假设是数字)
var largestContour = contours.OrderByDescending(c => Cv2.ContourArea(c)).First();
var boundingRect = Cv2.BoundingRect(largestContour);
// 扩展边界框
var margin = 10;
var expandedRect = new Rect(
Math.Max(0, boundingRect.X - margin),
Math.Max(0, boundingRect.Y - margin),
Math.Min(originalImage.Width - boundingRect.X + margin, boundingRect.Width + 2 * margin),
Math.Min(originalImage.Height - boundingRect.Y + margin, boundingRect.Height + 2 * margin)
);
// 提取数字区域
using var digitRegion = new Mat(morphImage, expandedRect);
// 4. 调整大小到28x28,保持宽高比
using var resizedImage = ResizeWithPadding(digitRegion, 28, 28);
SavePreprocessedImage(resizedImage, "advanced_preprocessed_hand.png");
return ConvertMatToTensor(resizedImage);
}
else
{
// 如果没找到轮廓,使用标准处理
using var resizedImage = new Mat();
Cv2.Resize(morphImage, resizedImage, new Size(28, 28));
return ConvertMatToTensor(resizedImage);
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 高级图像处理失败: {ex.Message}");
return null;
}
}
// 保持宽高比的缩放和填充
private static Mat ResizeWithPadding(Mat image, int targetWidth, int targetHeight)
{
var aspectRatio = (double)image.Width / image.Height;
var targetAspectRatio = (double)targetWidth / targetHeight;
int newWidth, newHeight;
if (aspectRatio > targetAspectRatio)
{
newWidth = targetWidth;
newHeight = (int)(targetWidth / aspectRatio);
}
else
{
newHeight = targetHeight;
newWidth = (int)(targetHeight * aspectRatio);
}
// 调整大小
using var resized = new Mat();
Cv2.Resize(image, resized, new Size(newWidth, newHeight));
// 创建目标尺寸的黑色图像
var result = Mat.Zeros(targetHeight, targetWidth, MatType.CV_8UC1);
// 计算居中位置
var x = (targetWidth - newWidth) / 2;
var y = (targetHeight - newHeight) / 2;
// 将调整后的图像复制到中心
var roi = new Rect(x, y, newWidth, newHeight);
resized.CopyTo(new Mat(result, roi));
return result;
}
// 获取预测结果
private static int GetPredictedDigit(Tensor<float> output)
{
if (output == null || output.Length == 0) return -1;
var maxIndex = 0;
var maxValue = float.MinValue;
var span = output.ToArray();
for (int i = 0; i < span.Length && i < 10; i++)
{
if (span[i] > maxValue)
{
maxValue = span[i];
maxIndex = i;
}
}
return maxIndex;
}
// 打印置信度分数
private static void PrintConfidenceScores(Tensor<float> output)
{
if (output == null || output.Length == 0) return;
var span = output.ToArray();
var length = Math.Min(span.Length, 10);
// 应用Softmax来获得概率分布
var softmax = ApplySoftmax(span.Take(length).ToArray());
Console.WriteLine("🏆 Top 3 预测结果:");
var topResults = softmax
.Select((prob, index) => new { Digit = index, Probability = prob })
.OrderByDescending(x => x.Probability)
.Take(3);
foreach (var result in topResults)
{
Console.WriteLine($"数字 {result.Digit}: {result.Probability:P2}");
}
Console.WriteLine("\n📊 完整置信度分布:");
for (int i = 0; i < length; i++)
{
var bar = new string('█', (int)(softmax[i] * 20));
Console.WriteLine($"数字 {i}: {softmax[i]:P2} {bar}");
}
}
// 应用Softmax函数
private static float[] ApplySoftmax(float[] values)
{
var max = values.Max();
var exp = values.Select(x => Math.Exp(x - max)).ToArray();
var sum = exp.Sum();
return exp.Select(x => (float)(x / sum)).ToArray();
}
}
}


这个基础框架可以应用于多种场景:
C#// 🔥 收藏级代码模板:高性能配置
private static SessionOptions CreateOptimizedSessionOptions()
{
var options = new SessionOptions();
// 启用CPU优化
options.EnableCpuMemArena = true;
options.EnableMemoryPattern = true;
// 设置执行提供者
options.AppendExecutionProvider_CPU(0);
// 设置线程数(根据CPU核心数调整)
options.IntraOpNumThreads = Environment.ProcessorCount;
return options;
}
C#// 🔥 金句总结:正确的资源释放是ONNX Runtime应用的关键
public class OnnxModelWrapper : IDisposable
{
private InferenceSession _session;
private bool _disposed = false;
public OnnxModelWrapper(string modelPath)
{
var options = CreateOptimizedSessionOptions();
_session = new InferenceSession(modelPath, options);
}
public float[] Predict(float[] inputData)
{
if (_disposed)
throw new ObjectDisposedException(nameof(OnnxModelWrapper));
// 推理逻辑...
return new float[0]; // 简化返回
}
public void Dispose()
{
if (!_disposed)
{
_session?.Dispose();
_disposed = true;
}
}
}
C#// ❌ 错误做法
var wrongInput = new float[784]; // 直接使用一维数组
// ✅ 正确做法
var correctInput = new DenseTensor<float>(new[] { 1, 1, 28, 28 });
C#// ⚠️ 记住:所有IDisposable对象都要正确释放
using var session = new InferenceSession(modelPath);
using var results = session.Run(inputs);
C#// 🔥 实战经验:InferenceSession是线程安全的,可以并发调用Run方法
// 但建议为每个线程创建独立的会话实例以获得最佳性能
通过这个简单的示例,我们掌握了ONNX Runtime的三个核心要点:
ONNX Runtime为C#开发者提供了一个强大而简洁的AI推理解决方案。无论你是想在Web应用中集成智能功能,还是构建桌面AI工具,这个基础框架都能为你节省大量开发时间。
下一步学习建议:尝试集成不同类型的ONNX模型,如目标检测、自然语言处理等,体验ONNX Runtime的真正威力。
💬 互动时间:
觉得有用请转发给更多同行 🚀 让更多C#开发者体验到AI开发的简单与高效!
关注我,获取更多C#开发实战技巧和AI编程干货!
相关信息
通过网盘分享的文件:AppOnnx.zip 链接: https://pan.baidu.com/s/1g4Qf4g8JL5X0voj358J8eA?pwd=nt2x 提取码: nt2x --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!