引言

人工智能(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 模型。如果你有自定义需求,可以使用以下步骤训练并导出模型:

  1. 数据准备:收集并标注图像数据集,分为训练集和验证集。
  2. 训练模型:使用 TensorFlow 或 PyTorch 等框架训练模型。
  3. 导出模型:利用框架提供的导出工具导出模型。

在本文中,我们选择 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模型,此处不做额外叙述。由于模型的性能和效率至关重要,只是提供一些优化建议:

  1. 模型量化:使用 ONNX Runtime 的量化工具,将模型从浮点数(FP32)转换为整数(INT8),减少模型大小和推理时间。
  2. 硬件加速:结合 ONNX Runtime 的 GPU 支持,利用 CUDA 或 DirectML 加速推理。
  3. 批处理:如果需要处理多张图像,可以将输入组织为批次(batch),提高吞吐量。例如:
var inputs = new List<ImageInput> { input1, input2, input3 };
var batchPrediction = mlContext.Data.LoadFromEnumerable(inputs);
var predictions = model.Transform(batchPrediction);
  1. 缓存机制:对于频繁使用的模型,保持预测引擎的单例实例,避免重复加载。

通过这些优化,模型可以在 .NET 环境中实现更高的性能,满足实时应用的需求。


实际应用场景

图像分类模型在 .NET 应用中有广泛的用途,以下是几个典型场景:

  1. 医疗影像分析

    在医疗系统中,部署图像分类模型可以辅助医生识别 X 光片或 MRI 图像中的异常。例如,检测肺部结节或肿瘤。

  2. 智能安防

    在监控系统中,模型可以实时识别可疑物体或行为,如检测闯入者或遗留物品。

  3. 电子商务

    在商品管理系统中,自动分类上传的商品图像,提升搜索和推荐的准确性。

挑战与解决方案

  • 数据隐私:通过加密传输和本地推理保护用户数据。
  • 模型更新:定期从云端下载新模型,并使用版本控制管理。
  • 计算资源:在资源受限的设备上,使用轻量化模型(如 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技术实操系列(六):基于图像分类模型对图像进行分类的更多相关文章

  1. Keil MDK STM32系列(六) 基于抽象外设库HAL的ADC模数转换

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  2. 技术实操丨HBase 2.X版本的元数据修复及一种数据迁移方式

    摘要:分享一个HBase集群恢复的方法. 背景 在HBase 1.x中,经常会遇到元数据不一致的情况,这个时候使用HBCK的命令,可以快速修复元数据,让集群恢复正常. 另外HBase数据迁移时,大家经 ...

  3. Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  4. Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  5. Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401开发

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  6. Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  7. Keil MDK STM32系列(四) 基于抽象外设库HAL的STM32F401开发

    Keil MDK STM32系列 Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发 Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401 ...

  8. ABP入门系列(1)——学习Abp框架之实操演练

    作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ...

  9. .net基础学java系列(四)Console实操

    上一篇文章 .net基础学java系列(三)徘徊反思 本章节没啥营养,请绕路! 看视频,不实操,对于上了年龄的人来说,是记不住的!我已经看了几遍IDEA的教学视频: https://edu.51cto ...

  10. Linux基础实操六

    实操一: 临时配置网络(ip,网关,dns)+永久配置 #ifconfig ens33 192.168.145.134/24 #vim /etc/resolv.conf #route add defa ...

随机推荐

  1. Qt可视化大屏电子看板系统全平台效果图

  2. IDEA利用阿里云插件部署Springboot项目

    下载插件 搜索 Alibaba Cloud Toolkit 插件,并安装. IDEA增加Run/Debug Configurations Add New Configuration - Deploy ...

  3. OpenCV4.1.0编译时提示“CV_BGR2GRAY”: 未声明的标识符

    OpenCV版本为4.1.0 使用CV_BGR2GRAY时报错: "CV_BGR2GRAY": 未声明的标识符 解决方法一:添加头文件:#include <opencv2/i ...

  4. 总是被低估,从未被超越,揭秘QQ极致丝滑背后的硬核IM技术优化

    本文由腾讯云开发者张曌.毕磊分享,原题"QQ 9"傻快傻快"的?!带你看看背后的技术秘密",本文进行了排版和内容优化等. 1.引言 最新发布的 QQ 9 自上线 ...

  5. blast只保留一个最优结果

    使用blast比对时,只保留一个最优结果 代码: blastn -db nt.blast.db -query seq.fa -out blast.nt.result -evalue 1e-5 -out ...

  6. (十).NET6.0 搭建基于Quartz组件的定时调度任务

    1.添加Quartz定时器组件 2.新建类库项目Wsk.Core.QuartzNet,并且引用包类库项目.然后新建一个中间调度类,叫QuartzMiddleJob 3.新建一个Job工厂类,叫YsqJ ...

  7. cpa-会计

    会计整体介绍 1.总结 2.会计政策.会计估计及其变更和差错更正 3.存货 4.固定资产 5.无形资产 6.投资性房地产 7.长期股权投资与合营安排 8.资产减值 9.负债 10.职工薪酬 11.借款 ...

  8. 6种微服务RPC框架-copy

    一类是跟某种特定语言平台绑定的,另一类是与语言无关即跨语言平台的. 跟语言平台绑定的开源 RPC 框架主要有下面几种. Dubbo:国内最早开源的 RPC 框架,由阿里巴巴公司开发并于 2011 年末 ...

  9. 《SpringBoot》EasyExcel实现百万数据的导入导出

    24年11月6日消息,阿里巴巴旗下的Java Excel工具库EasyExcel近日宣布,将停止更新,未来将逐步进入维护模式,将继续修复Bug,但不再主动新增功能. EasyExcel 是一款知名的 ...

  10. XReport通过数据控制控件是否打印

    需求场景:医嘱单在患者出院的时候,需要标记一条红线,表示以下没有医嘱了.数据库中此记录的一个字段属性isRed值来标记这一行. 实现:XReport报表的明细区域增加一个line1对象.然后在明细表格 ...