引言

人工智能(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音视频开发28-Onvif信息获取

    一.前言 严格意义上来说,Onvif处理这块算不上音视频开发的内容,为何重新整理放在音视频开发这个类别,主要是为了方便统一管理,而且在视频监控处理这块,通过onvif来拿到音视频流这是必经的阶段,也算 ...

  2. 命名空间“System.Web.UI.Design”中不存在类型或命名空间名称“ControlDesigner”

    命名空间"System.Web.UI.Design"中不存在类型或命名空间名称"ControlDesigner" 命名空间"System.Web.UI ...

  3. 使用百度地图API服务中的问题汇总

    1.服务器端与浏览器端的AK的区别 服务端就是指数据操作需要在百度地图服务器上进行接口数据交互,不能在前端代码中直接调用,跨域不支持,开发多一个后端: 浏览器端就是指数据操作需要在Web前端就可以完成 ...

  4. IM跨平台技术学习(四):蘑菇街基于Electron开发IM客户端的技术实践

    本文由蘑菇街前端技术团队分享,原题"Electron 从零到一",有修订和改动. 1.引言 本系列文章的前面几篇主要是从Electron技术本身进行了讨论(包括:第1篇初步了解El ...

  5. WxPython跨平台开发框架之模块字段权限的管理

    在我的很多Winform开发项目中,统一采用了权限管理模块来进行各种权限的控制,包括常规的功能权限(工具栏.按钮.菜单权限),另外还可以进行字段级别的字段权限控制,字段权限是我们在一些对权限要求比较严 ...

  6. Linux C语言面试考点

    数组 数组初始化方法 /* 以下为自动类型 */​/* 一维数组 */int arr[] = {1, 3, 5}; //不指定长度,由编译器自动计算int arr[5] = {0, }; //指定长度 ...

  7. Solution -「PKUWC 2018」「洛谷 P5298」Minimax

    \(\mathscr{Description}\)   Link.   给定一棵二叉树,每片叶子有一个权值,所有权值互不相同.每个非叶结点 \(u\) 有一个概率 \(p_u\in(0,1)\),表示 ...

  8. Spring Boot进阶教程--注解大全

    springboot注解大全 SpringBoot注解就是给代码打上标签的能力.通过引入注解,我们可以简单快速赋予代码生命力,大大提高代码可读性和扩展性.注解本身不具有任何能力,只是一个标签,但是我们 ...

  9. javascript与css3动画学习笔记

    当Html5,css3已渐渐成为主流的时候,我还非常习惯的用js去做一些简单的动画.因为在桌面浏览器上, 并非所有的都支持css3.用户也倒是很奇怪,用户习惯并不是每个用户都可以被培养.总有不少人会觉 ...

  10. 你所不知道的 C/C++ 宏知识——基于《C/C++ 宏编程的艺术》

    前言 刚学 C++ 的时候,就知道它糅合了四种编程模式:基于预处理器的宏.基于 C 语言的面向过程.基于类的面向对象.以及基于模板的泛型编程.其中,宏和模板元编程因为是在编译期出结果,能有效提升程序运 ...