AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类
引言
人工智能(AI)技术的迅猛发展推动了各行各业的数字化转型。图像分类,作为计算机视觉领域的核心技术之一,能够让机器自动识别图像中的物体、场景或特征,已广泛应用于医疗诊断、安防监控、自动驾驶和电子商务等领域。
与此同时,.NET 平台凭借其高效性、跨平台能力和强大的 C# 编程语言支持,成为开发者构建企业级应用的首选技术栈。将 AI 图像分类模型与 .NET 技术结合,不仅能充分发挥两者的优势,还能为开发者提供一种高效、直观的实现方式。
本文将详细介绍如何在 .NET 环境下使用 C# 部署和调用 AI 图像分类模型。我们将从环境搭建、模型选择,到模型调用,再到实际应用场景,逐步展开讲解,并提供丰富的代码示例和实践指导,帮助开发者快速上手并应用到实际项目中。
准备工作
在开始实现图像分类之前,我们需要准备必要的开发环境和工具。以下是所需的软件和库:
- Visual Studio:Visual Studio 2022。 
- .NET SDK:安装 .NET 6.0 或更高版本,确保支持最新的功能和性能优化。 
- ML.NET:微软提供的开源机器学习框架,专为 .NET 开发者设计,支持模型训练和推理。 
- 模型文件:我们将使用预训练的图像分类模型 tensorflow_inception_graph.pb。 
安装步骤
创建项目并添加依赖:在命令行中运行以下命令,创建一个控制台应用程序并安装必要的 NuGet 包:
dotnet new console -n ImageClassificationDemo
cd ImageClassificationDemo
dotnet add package Microsoft.ML
dotnet add package Microsoft.ML.ImageAnalytics
dotnet add package Microsoft.ML.TensorFlow
dotnet add package SciSharp.TensorFlow.Redist
完成以上步骤后,你的环境就准备好了。接下来,我们将选择一个合适的图像分类模型。
图像分类模型的选择
图像分类模型是基于监督学习的神经网络,其目标是将输入图像分配到预定义的类别中。在选择模型时,我们需要考虑模型的性能、计算复杂度和适用场景。以下是几种常见的图像分类模型:
- 卷积神经网络(CNN):如 LeNet、AlexNet 和 VGGNet,适合基本的图像分类任务,但层数较深时可能面临梯度消失问题。 
- 残差网络(ResNet):通过引入残差连接(skip connections),解决了深层网络的训练难题,适用于高精度分类任务。 
- EfficientNet:通过平衡网络深度、宽度和分辨率,提供高效的性能,适合资源受限的场景。 
模型训练与导出
考虑到时间和资源成本,我们将直接使用预训练的 tensorflow_inception_graph.pb 模型。如果你有自定义需求,可以使用以下步骤训练并导出模型:
- 数据准备:收集并标注图像数据集,分为训练集和验证集。 
- 训练模型:使用 TensorFlow 或 PyTorch 等框架训练模型。 
- 导出模型:利用框架提供的导出工具导出模型。 
在本文中,我们选择 tensorflow_inception_graph.pb 作为示例模型,这是一种由Google开发的高性能卷积神经网络(CNN)架构。
❝
该模块通过并行使用不同大小的卷积核(如1x1、3x3、5x5)和池化层,提取图像的多尺度特征。这种设计提高了模型在图像分类任务中的表现,同时保持了计算效率。支持 1000 个类别的分类,且可以轻松集成到 .NET 中。
大家可以直接点击 tensorflow_inception_graph.pb 下载(文章最后也有下载方式)预训练的模型文件和分类文件,并将其放入项目目录中。
也可以到github上下载(文章最后也有下载方式),里面的内容相对来说也更丰富些。

在 .NET 中调用模型
现在,我们进入核心部分:在 .NET 中调用 tensorflow_inception_graph.pb。以下是逐步实现的过程。
1. 创建 .NET 项目
使用命令行创建一个控制台应用,项目基本结构如下:
ImageClassificationDemo/
├── ImageClassificationDemo.csproj
├── Program.cs
├── assets/inputs/inception/tensorflow_inception_graph.pb
├── assets/inputs/inception/imagenet_comp_graph_label_strings.txt
2. 定义输入和输出数据结构
如果在运行的时候报错说找不到模型或者label文件,可以进行如下操作:

输入类中定义数据的结构如下,后续会使用 TextLoader 加载数据时引用该类型。此处的类名为 ImageNetData:
    public class ImageNetData
    {
        [LoadColumn(0)]
        public string ImagePath;
        [LoadColumn(1)]
        public string Label;
        public static IEnumerable<ImageNetData> ReadFromCsv(string file, string folder)
        {
            return File.ReadAllLines(file)
             .Select(x => x.Split('\t'))
             .Select(x => new ImageNetData { ImagePath = Path.Combine(folder, x[0]), Label = x[1] } );
        }
    }
    public class ImageNetDataProbability : ImageNetData
    {
        public string PredictedLabel;
        public float Probability { get; set; }
    }
❝
需要强调的是,ImageNetData 类中的标签在使用 TensorFlow 模型进行评分时并没有真正使用。而是在测试预测时使用它,这样就可以将每个样本数据的实际标签与 TensorFlow 模型提供的预测标签进行比较。
输出类的结构如下:
public class ImageNetPrediction
{
    [ColumnName(TFModelScorer.InceptionSettings.outputTensorName)]
    public float[] PredictedLabels;
}
Inception 模型还需要几个传入的默认参数:
public struct ImageNetSettings
{
    public const int imageHeight = 224;
    public const int imageWidth = 224;
    public const float mean = 117;
    public const bool channelsLast = true;
}      
3. 定义 estimator 管道
❝
在处理深度神经网络时,必须使图像适应网络期望的格式。这就是图像被调整大小然后转换的原因(主要是像素值在所有R,G,B通道上被归一化)。
var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: imagesFolder, inputColumnName: nameof(ImageNetData.ImagePath))
    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "input"))
    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: ImageNetSettings.channelsLast, offsetImage: ImageNetSettings.mean))
    .Append(mlContext.Model.LoadTensorFlowModel(modelLocation)
        .ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2" }, inputColumnNames: new[] { "input" },
            addBatchDimensionInput:true));
运行代码后,模型将被成功加载到内存中,接下来我们可以调用它进行图像分类。
❝
通常情况下,这里经常报的错就是输入/输出节点的名称不正确,你可以通过 Netron (https://netron.app/)工具查看输入/输出节点的名称。
因为这两个节点的名称后面会在 estimator 的定义中使用:在 inception 网络的情况下,输入张量命名为 'input',输出命名为 'softmax2'。
下图是通过 Netron 读取的 tensorflow_inception_graph.pb 模型分析图:


4. 提取预测结果
填充 estimator 管道
ITransformer model = pipeline.Fit(data);
var predictionEngine = mlContext.Model.CreatePredictionEngine<ImageNetData, ImageNetPrediction>(model);
当获得预测结果后,我们会在属性中得到一个浮点数数组。数组中的每个位置都会分配到一个标签。
例如,如果模型有5个不同的标签,则数组将为length = 5。数组中的每个位置都表示标签在该位置的概率;所有数组值(概率)的和等于1。
然后,您需要选择最大的值(概率),并检查配给了该位置的那个以填充 estimator 管道标签。
调用模型进行图像分类
接下来我们需要编写代码来加载图像、进行预测并解析结果。
1. 准备素材与分类文件
定义图像文件夹目录和图像分类目录。以下代码加载并预处理图像:
string assetsRelativePath = @"../../../assets";
string assetsPath = GetAbsolutePath(assetsRelativePath);
string tagsTsv = Path.Combine(assetsPath, "inputs", "images", "tags.tsv");
string imagesFolder = Path.Combine(assetsPath, "inputs", "images");
string inceptionPb = Path.Combine(assetsPath, "inputs", "inception", "tensorflow_inception_graph.pb");
string labelsTxt = Path.Combine(assetsPath, "inputs", "inception", "imagenet_comp_graph_label_strings.txt");
2. 加载模型
private PredictionEngine<ImageNetData, ImageNetPrediction> LoadModel(string dataLocation, string imagesFolder, string modelLocation)
{
    ConsoleWriteHeader("Read model");
    Console.WriteLine($"Model location: {modelLocation}");
    Console.WriteLine($"Images folder: {imagesFolder}");
    Console.WriteLine($"Training file: {dataLocation}");
    Console.WriteLine($"Default parameters: image size=({ImageNetSettings.imageWidth},{ImageNetSettings.imageHeight}), image mean: {ImageNetSettings.mean}");
    var data = mlContext.Data.LoadFromTextFile<ImageNetData>(dataLocation, hasHeader: true);
    var pipeline = mlContext.Transforms.LoadImages(outputColumnName: "input", imageFolder: imagesFolder, inputColumnName: nameof(ImageNetData.ImagePath))
                    .Append(mlContext.Transforms.ResizeImages(outputColumnName: "input", imageWidth: ImageNetSettings.imageWidth, imageHeight: ImageNetSettings.imageHeight, inputColumnName: "input"))
                    .Append(mlContext.Transforms.ExtractPixels(outputColumnName: "input", interleavePixelColors: ImageNetSettings.channelsLast, offsetImage: ImageNetSettings.mean))
                    .Append(mlContext.Model.LoadTensorFlowModel(modelLocation).
                    ScoreTensorFlowModel(outputColumnNames: new[] { "softmax2" },
                                        inputColumnNames: new[] { "input" }, addBatchDimensionInput:true));
                
    ITransformer model = pipeline.Fit(data);
    var predictionEngine = mlContext.Model.CreatePredictionEngine<ImageNetData, ImageNetPrediction>(model);
    return predictionEngine;
}
3. 解析输出结果
protected IEnumerable<ImageNetData> PredictDataUsingModel(string testLocation, 
                                                          string imagesFolder, 
                                                          string labelsLocation, 
                                                          PredictionEngine<ImageNetData, ImageNetPrediction> model)
{
    ConsoleWriteHeader("Classify images");
    Console.WriteLine($"Images folder: {imagesFolder}");
    Console.WriteLine($"Training file: {testLocation}");
    Console.WriteLine($"Labels file: {labelsLocation}");
    var labels = ReadLabels(labelsLocation);
    var testData = ImageNetData.ReadFromCsv(testLocation, imagesFolder);
    foreach (var sample in testData)
    {
        var probs = model.Predict(sample).PredictedLabels;
        var imageData = new ImageNetDataProbability()
        {
            ImagePath = sample.ImagePath,
            Label = sample.Label
        };
        (imageData.PredictedLabel, imageData.Probability) = GetBestLabel(labels, probs);
        imageData.ConsoleWrite();
        yield return imageData;
    }
}
在 Main 方法中调用,完整代码如下:
static void Main(string[] args)
{
    string assetsRelativePath = @"../../../assets";
    string assetsPath = GetAbsolutePath(assetsRelativePath);
    string tagsTsv = Path.Combine(assetsPath, "inputs", "images", "tags.tsv");
    string imagesFolder = Path.Combine(assetsPath, "inputs", "images");
    string inceptionPb = Path.Combine(assetsPath, "inputs", "inception", "tensorflow_inception_graph.pb");
    string labelsTxt = Path.Combine(assetsPath, "inputs", "inception", "imagenet_comp_graph_label_strings.txt");
    try
    {
        TFModelScorer modelScorer = new TFModelScorer(tagsTsv, imagesFolder, inceptionPb, labelsTxt);
        modelScorer.Score();
    }
    catch (Exception ex)
    {
        ConsoleHelpers.ConsoleWriteException(ex.ToString());
    }
    ConsoleHelpers.ConsolePressAnyKey();
}
运行程序后,你将看到类似以下的输出:

其他实现方式
在实际应用中,我们也可以使用ONNX模型,此处不做额外叙述。由于模型的性能和效率至关重要,只是提供一些优化建议:
- 模型量化:使用 ONNX Runtime 的量化工具,将模型从浮点数(FP32)转换为整数(INT8),减少模型大小和推理时间。 
- 硬件加速:结合 ONNX Runtime 的 GPU 支持,利用 CUDA 或 DirectML 加速推理。 
- 批处理:如果需要处理多张图像,可以将输入组织为批次(batch),提高吞吐量。例如: 
var inputs = new List<ImageInput> { input1, input2, input3 };
var batchPrediction = mlContext.Data.LoadFromEnumerable(inputs);
var predictions = model.Transform(batchPrediction);
- 缓存机制:对于频繁使用的模型,保持预测引擎的单例实例,避免重复加载。 
通过这些优化,模型可以在 .NET 环境中实现更高的性能,满足实时应用的需求。
实际应用场景
图像分类模型在 .NET 应用中有广泛的用途,以下是几个典型场景:
- 医疗影像分析 
 在医疗系统中,部署图像分类模型可以辅助医生识别 X 光片或 MRI 图像中的异常。例如,检测肺部结节或肿瘤。
- 智能安防 
 在监控系统中,模型可以实时识别可疑物体或行为,如检测闯入者或遗留物品。
- 电子商务 
 在商品管理系统中,自动分类上传的商品图像,提升搜索和推荐的准确性。
挑战与解决方案
- 数据隐私:通过加密传输和本地推理保护用户数据。 
- 模型更新:定期从云端下载新模型,并使用版本控制管理。 
- 计算资源:在资源受限的设备上,使用轻量化模型(如 MobileNet)。 
结论
本文详细介绍了如何在 .NET 环境下使用 C# 部署和调用 AI 图像分类模型。从环境搭建到模型选择、部署与调用,再到性能优化和应用场景,我们提供了一套完整的实践指南。通过 ML.NET 和预测模式的支持,开发者可以轻松地将强大的 AI 能力集成到 .NET 应用中。
随着 AI 技术的不断进步和 .NET 平台的持续发展,二者的结合将为开发者带来更多可能性。无论是构建智能桌面应用、Web 服务还是跨平台解决方案,图像分类模型都能为项目增添创新价值。希望本文能为你的 AI 之旅提供启发和帮助!
参考资料
- 素材下载地址: https://storage.googleapis.com/download.tensorflow.org/models/inception5h.zip 
- Netron工具地址: https://netron.app/ 
- 224x224图像素材: https://www.kaggle.com/datasets/abhinavnayak/catsvdogs-transformed/data 
- tensorflow教程及模型文件和label文件: https://github.com/martinwicke/tensorflow-tutorial 
- Image Classification - Scoring sample: https://github.com/dotnet/machinelearning-samples/blob/main/samples/csharp/getting-started/DeepLearning_ImageClassification_TensorFlow/README.md 
- ML.NET 官方文档: https://dotnet.microsoft.com/apps/machinelearning-ai/ml-dotnet 
- ONNX Model Zoo: https://github.com/onnx/models 
AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类的更多相关文章
- Keil MDK STM32系列(六) 基于抽象外设库HAL的ADC模数转换
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- 技术实操丨HBase 2.X版本的元数据修复及一种数据迁移方式
		摘要:分享一个HBase集群恢复的方法. 背景 在HBase 1.x中,经常会遇到元数据不一致的情况,这个时候使用HBCK的命令,可以快速修复元数据,让集群恢复正常. 另外HBase数据迁移时,大家经 ... 
- Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401开发
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- Keil MDK STM32系列(四) 基于抽象外设库HAL的STM32F401开发
		Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ... 
- ABP入门系列(1)——学习Abp框架之实操演练
		作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ... 
- .net基础学java系列(四)Console实操
		上一篇文章 .net基础学java系列(三)徘徊反思 本章节没啥营养,请绕路! 看视频,不实操,对于上了年龄的人来说,是记不住的!我已经看了几遍IDEA的教学视频: https://edu.51cto ... 
- Linux基础实操六
		实操一: 临时配置网络(ip,网关,dns)+永久配置 #ifconfig ens33 192.168.145.134/24 #vim /etc/resolv.conf #route add defa ... 
随机推荐
- Qt可视化大屏电子看板系统全平台效果图
- IDEA利用阿里云插件部署Springboot项目
			下载插件 搜索 Alibaba Cloud Toolkit 插件,并安装. IDEA增加Run/Debug Configurations Add New Configuration - Deploy ... 
- OpenCV4.1.0编译时提示“CV_BGR2GRAY”: 未声明的标识符
			OpenCV版本为4.1.0 使用CV_BGR2GRAY时报错: "CV_BGR2GRAY": 未声明的标识符 解决方法一:添加头文件:#include <opencv2/i ... 
- 总是被低估,从未被超越,揭秘QQ极致丝滑背后的硬核IM技术优化
			本文由腾讯云开发者张曌.毕磊分享,原题"QQ 9"傻快傻快"的?!带你看看背后的技术秘密",本文进行了排版和内容优化等. 1.引言 最新发布的 QQ 9 自上线 ... 
- blast只保留一个最优结果
			使用blast比对时,只保留一个最优结果 代码: blastn -db nt.blast.db -query seq.fa -out blast.nt.result -evalue 1e-5 -out ... 
- (十).NET6.0 搭建基于Quartz组件的定时调度任务
			1.添加Quartz定时器组件 2.新建类库项目Wsk.Core.QuartzNet,并且引用包类库项目.然后新建一个中间调度类,叫QuartzMiddleJob 3.新建一个Job工厂类,叫YsqJ ... 
- cpa-会计
			会计整体介绍 1.总结 2.会计政策.会计估计及其变更和差错更正 3.存货 4.固定资产 5.无形资产 6.投资性房地产 7.长期股权投资与合营安排 8.资产减值 9.负债 10.职工薪酬 11.借款 ... 
- 6种微服务RPC框架-copy
			一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的. 跟语言平台绑定的开源 RPC 框架主要有下面几种. Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并于 2011 年末 ... 
- 《SpringBoot》EasyExcel实现百万数据的导入导出
			24年11月6日消息,阿里巴巴旗下的Java Excel工具库EasyExcel近日宣布,将停止更新,未来将逐步进入维护模式,将继续修复Bug,但不再主动新增功能. EasyExcel 是一款知名的 ... 
- XReport通过数据控制控件是否打印
			需求场景:医嘱单在患者出院的时候,需要标记一条红线,表示以下没有医嘱了.数据库中此记录的一个字段属性isRed值来标记这一行. 实现:XReport报表的明细区域增加一个line1对象.然后在明细表格 ... 
