C#实现以图搜图:基于ResNet与ONNX Runtime的本地化图像检索方案
2026/9/15 7:15:54 网站建设 项目流程

简介:图像检索是计算机视觉领域的核心技术,其核心原理是将图像内容转化为高维特征向量进行相似度计算。通过深度学习模型(如ResNet)提取的语义特征,能够超越传统基于像素或纹理的比对,实现更精准的“理解”。这项技术的工程价值在于,它能将非结构化的图像数据转化为可计算、可索引的结构化向量,从而支撑起海量图像的高效检索。在应用场景上,广泛用于电商平台的商品去重与推荐、相册管理中的相似照片查找,以及内容安全领域的侵权检测等。本文聚焦于C#技术栈,详细阐述了如何利用ONNX Runtime加载预训练模型进行特征提取,并结合PCA降维与余弦相似度计算,构建一个轻量级、可离线运行的以图搜图系统,为.NET开发者提供了一套完整的工程实践方案。

1. 项目缘起:从“找图”到“搜图”的工程化跨越

最近在做一个桌面应用的后台管理模块,里面有个功能是让运营人员上传商品图片。问题来了,商品库越来越大,运营同学时不时就会上传一些“看起来差不多”的图片,可能是同一款商品的不同角度,也可能是换了背景的同一张图。手动去重?效率太低,而且人眼容易疲劳出错。这时候,一个“以图搜图”的功能就显得非常必要了。它不是简单地比对文件名或MD5,而是能理解图片的视觉内容,找出那些“看起来像”的图片。

网上关于“以图搜图”的教程很多,Python的、Java的,但作为一个主要技术栈是C#的桌面应用开发者,我更希望能在.NET生态里找到一套成熟、高效且易于集成的方案。毕竟,为了这一个功能引入Python环境或者搞一套复杂的微服务,有点杀鸡用牛刀了。于是,我开始在C#的领域里寻找答案,目标很明确:构建一个轻量级、可离线运行、准确度尚可的以图搜图示例。这不仅仅是调用一个API那么简单,它涉及到图像特征提取、向量化表示、相似度计算以及高效检索等一系列工程问题。今天,我就把这个探索和实现的过程,结合一个可运行的示例项目(基于C#以图搜图示例.zip),完整地分享出来。

2. 核心原理拆解:图像如何变成可计算的“指纹”

要实现以图搜图,第一步也是最重要的一步,就是把一张图片从像素矩阵,转换成一个能够代表其视觉内容的、固定长度的数字向量,我们通常称之为“特征向量”或“图像指纹”。这个向量需要满足一个核心特性:视觉上相似的图片,它们的特征向量在数学空间里的距离也应该很近。

2.1 特征提取的常见路径

在C#生态中,我们主要有以下几种路径来实现特征提取:

  1. 传统计算机视觉方法:例如SIFT、SURF、ORB等算法。它们通过检测图像中的关键点(如角点、边缘)并计算这些点周围的局部特征描述子来工作。AForge.NET和Emgu CV(OpenCV的.NET封装)库对这类算法有较好的支持。这类方法的优点是无需训练、可解释性强,但对光照、旋转、尺度变化比较敏感,且特征向量通常是变长的(一组描述子),不利于直接进行快速的相似度比对。

  2. 深度学习模型方法:这是目前的主流和更优方案。我们使用一个在大型图像数据集(如ImageNet)上预训练好的卷积神经网络(CNN),例如ResNet、VGG、EfficientNet等。我们去掉模型的最后一层分类头,将倒数第二层(通常是全连接层或全局池化层)的输出作为图像的特征向量。这个向量是一个高维度的、稠密的浮点数数组,它编码了图像的高级语义信息。语义相似的图片(如都是“狗”,尽管品种、姿态不同),其特征向量也会很接近。

  3. 专用特征提取服务/库:有些库专门为图像检索优化了特征提取模型,比如ImageSharp社区的一些实验性项目,或者微软认知服务中的计算机视觉API(在线)。对于我们的离线C#示例,深度学习方法是最佳平衡点。

为什么选择深度学习模型?在本次示例中,我选择了基于预训练CNN模型的方法。原因很简单:泛化能力强,语义理解好。传统方法在纹理丰富的场景(如建筑、艺术品)上可能表现不错,但对于我们常见的商品、人物、风景等包含复杂语义的图片,深度学习模型提取的特征更能抓住“这是什么”的本质,而不仅仅是“它有什么图案”。这对于“找相似商品”这类需求至关重要。

2.2 相似度度量与检索

得到特征向量后,比如一个长度为512的float数组,接下来的问题就是:如何计算两个向量的相似度?以及,如何在拥有数十万张图片的库中快速找到最相似的几个?

  1. 相似度计算:最常用的方法是计算两个特征向量之间的余弦相似度。余弦相似度关注的是向量在方向上的差异,而非长度(模),这非常适合我们的场景,因为特征向量的“强度”可能受图像亮度影响,但“方向”才代表了其内容本质。余弦相似度值域在[-1, 1]之间,越接近1表示越相似。计算起来也很简单,就是两个向量的点积除以它们模长的乘积。

  2. 近似最近邻搜索:当图片库很大时,对每张待查询图片都进行全库的线性扫描(计算与库中所有图片的相似度)是不可接受的,时间复杂度是O(N)。这时就需要ANN算法。在C#中,我们可以使用Microsoft.ML(ML.NET)库中的Lsh(局部敏感哈希)相关组件,或者更专业的WeaviateMilvus的.NET客户端(但这引入了外部依赖)。为了示例的简洁和轻量,我采用了另一种工程上常见的折中方案:PCA降维 + 内存倒排索引。先通过PCA将高维特征(如512维)降至一个较低的维度(如64维),这能大幅减少计算量。然后,我们可以使用简单的KD-Tree(对于低维数据)或者基于System.Numerics的向量化计算来加速检索。对于示例级别的数据量(几千到几万张),在内存中构建索引并进行搜索是完全可行的。

3. 实战环境搭建与核心库选型

明确了原理,我们开始动手。首先需要建立一个C#项目,并引入必要的NuGet包。

3.1 项目创建与依赖安装

我使用的是.NET 6/8的控制台应用或类库项目,这保证了跨平台性。核心的NuGet包如下:

<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.16.3" /> <PackageReference Include="Microsoft.ML.OnnxRuntime.Managed" Version="1.16.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.2" /> <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
  • Microsoft.ML.OnnxRuntime: 这是我们的核心引擎。ONNX(Open Neural Network Exchange)是一个开放的模型格式标准。我们可以从PyTorch或TensorFlow导出预训练好的特征提取模型为.onnx格式,然后使用这个运行时在C#中高效地加载和运行模型,进行前向推理得到特征向量。它支持CPU和GPU推理,性能优异。
  • SixLabors.ImageSharp: 现代、高性能、跨平台的.NET图像处理库。我们用它来加载、解码、调整和预处理图片(如缩放、归一化、转换为Tensor需要的格式)。它比传统的System.Drawing更安全(不依赖GDI+)且功能强大。
  • MathNet.Numerics: 强大的数学计算库。我们主要用它来进行PCA降维、计算余弦相似度、矩阵运算等。它的API非常优雅,性能也很好。

为什么不直接用ML.NET的ImageClassification?因为ML.NET内置的图像相关任务主要面向训练和微调,对于直接加载预训练ONNX模型进行特征提取,OnnxRuntime是更直接、更底层的选择,给我们更大的控制权。

3.2 获取与准备预训练模型

这是关键一步。我们需要一个在ImageNet上预训练好的、去掉了分类头的CNN模型。一个经典且高效的选择是ResNet50。你可以从ONNX Model Zoo(GitHub)下载预训练好的ResNet50-v1或ResNet50-v2的ONNX模型。下载后,将其放入项目的Models文件夹中。

这个模型原始的输入是224x224的RGB图像,输出是一个1000维的向量(对应ImageNet的1000个类别)。我们需要对其进行“改造”。实际上,我们不需要这1000个分类得分,我们需要的是倒数第二层(通常是avg_poolflatten层的输出)的特征。幸运的是,ONNX Model Zoo提供的模型通常已经包含了中间层的输出。我们可以用Netron工具打开模型,找到一个合适的层作为特征输出,例如resnetv17_dense0_fwd(这是一个2048维的特征)。在我们的代码中,在创建OnnxRuntime的InferenceSession时,可以指定输出节点的名称,来获取这一层的输出。

4. 核心流程代码实现详解

让我们深入到代码层面,看看每一个环节具体如何实现。我会把关键代码拆解出来,并解释其背后的意图。

4.1 图像预处理标准化

模型对输入数据有严格的要求。ResNet通常要求输入图像尺寸为224x224,像素值归一化到[0, 1]或特定的均值和标准差。

using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; public static float[] PreprocessImage(string imagePath) { // 1. 使用ImageSharp加载图像 using var image = Image.Load<Rgb24>(imagePath); // 2. 调整大小:等比例缩放并裁剪到224x224的中心区域 // 这是为了保持图像内容不变形。也可以选择拉伸,但裁剪通常能保留核心内容。 image.Mutate(x => x.Resize(new ResizeOptions { Size = new Size(224, 224), Mode = ResizeMode.Crop // 裁剪模式 })); // 3. 将像素值从[0, 255]转换为[0.0, 1.0]的float数组 // 同时,模型通常要求特定的归一化,即减去均值再除以标准差。 // 对于ImageNet预训练模型,常用均值=[0.485, 0.456, 0.406],标准差=[0.229, 0.224, 0.225] (RGB通道) float[] inputData = new float[3 * 224 * 224]; // CHW格式: 3通道,高224,宽224 float[] mean = { 0.485f, 0.456f, 0.406f }; float[] std = { 0.229f, 0.224f, 0.225f }; image.ProcessPixelRows(accessor => { for (int y = 0; y < accessor.Height; y++) { Span<Rgb24> pixelRow = accessor.GetRowSpan(y); for (int x = 0; x < pixelRow.Length; x++) { Rgb24 pixel = pixelRow[x]; // 转换为float并归一化 inputData[y * 224 * 3 + x * 3 + 0] = (pixel.R / 255f - mean[0]) / std[0]; // R inputData[y * 224 * 3 + x * 3 + 1] = (pixel.G / 255f - mean[1]) / std[1]; // G inputData[y * 224 * 3 + x * 3 + 2] = (pixel.B / 255f - mean[2]) / std[2]; // B } } }); return inputData; }

关键点:预处理必须与模型训练时保持一致。不同的预训练模型(如ResNet, EfficientNet, MobileNet)可能有不同的输入尺寸和归一化参数。务必查清你所使用模型的要求。ImageSharpMutateProcessPixelRowsAPI能高效地进行像素级操作。

4.2 使用ONNX Runtime进行特征提取

预处理后的数据需要被组织成模型期望的Tensor格式,然后送入模型推理。

using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class FeatureExtractor : IDisposable { private readonly InferenceSession _session; private readonly string _inputName; private readonly string _outputName; public FeatureExtractor(string modelPath) { // 创建推理会话。可以设置SessionOptions来使用GPU等。 var options = new SessionOptions(); // 如果需要GPU加速,可以取消注释下一行(确保有CUDA环境) // options.AppendExecutionProvider_CUDA(0); _session = new InferenceSession(modelPath, options); // 获取模型的输入输出节点名称。可以通过_session.InputMetadata和_session.OutputMetadata查看。 // 这里假设我们已知输入名为“data”,输出特征层名为“feature” _inputName = _session.InputMetadata.Keys.First(); _outputName = "feature"; // 需要根据实际模型调整,用Netron查看 } public float[] Extract(float[] preprocessedData) { // 1. 将float数组包装为Tensor // 注意维度顺序:模型通常期望 [batch_size, channels, height, width] var dimensions = new int[] { 1, 3, 224, 224 }; // NCHW var tensor = new DenseTensor<float>(preprocessedData, dimensions); // 2. 创建输入数据容器 var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor(_inputName, tensor) }; // 3. 运行推理 using (var results = _session.Run(inputs)) { // 4. 获取特征输出 var featureTensor = results.FirstOrDefault(r => r.Name == _outputName)?.AsTensor<float>(); if (featureTensor == null) throw new InvalidOperationException("未能找到指定的特征输出节点。"); // 5. 将特征Tensor转换回一维float数组 return featureTensor.ToArray(); } } public void Dispose() => _session?.Dispose(); }

踩坑提醒_outputName的确定是整个流程最容易出错的地方。你必须使用像Netron这样的可视化工具打开你的.onnx模型文件,找到你想要的、代表全局特征的那一层的输出节点名称。它可能不叫“feature”,而是“flatten”、“avg_pool”或“global_pooling”的输出。这一步错了,提取的特征就毫无意义。

4.3 构建内存图像索引与PCA降维

当图片库很大时,直接存储和比对2048维的原始特征向量,内存和计算开销都很大。我们可以引入PCA降维。

using MathNet.Numerics.LinearAlgebra; using MathNet.Numerics.Statistics; public class ImageSearchEngine { private List<ImageItem> _imageItems = new(); private Matrix<float> _featureMatrix; // 降维后的特征矩阵 private PCA _pca; private bool _isPcaFitted = false; public class ImageItem { public string Id { get; set; } public string FilePath { get; set; } public float[] OriginalFeatures { get; set; } // 原始高维特征 public float[] ReducedFeatures { get; set; } // 降维后的特征 } // 添加图片到索引 public void AddImage(string id, string filePath, float[] originalFeatures) { var item = new ImageItem { Id = id, FilePath = filePath, OriginalFeatures = originalFeatures }; _imageItems.Add(item); } // 在所有添加的图片上拟合PCA模型,并进行降维 public void BuildIndex(int targetDimensions = 64) { if (_imageItems.Count == 0) return; // 1. 构建原始特征矩阵 (样本数 x 特征维度) int sampleCount = _imageItems.Count; int featureDim = _imageItems[0].OriginalFeatures.Length; var rawMatrix = Matrix<float>.Build.Dense(sampleCount, featureDim); for (int i = 0; i < sampleCount; i++) { rawMatrix.SetRow(i, _imageItems[i].OriginalFeatures); } // 2. 计算PCA // 注意:PCA前通常需要对数据进行中心化(减去均值)。 _pca = new PCA(rawMatrix, targetDimensions, Center: true); // 3. 对原始特征进行降维转换 _featureMatrix = _pca.Transform(rawMatrix); // 4. 将降维后的特征存回ImageItem for (int i = 0; i < sampleCount; i++) { _imageItems[i].ReducedFeatures = _featureMatrix.Row(i).ToArray(); } _isPcaFitted = true; Console.WriteLine($"PCA降维完成,原始维度{featureDim} -> 目标维度{targetDimensions}"); } // 搜索相似图片 public List<(ImageItem Item, float Score)> Search(float[] queryOriginalFeatures, int topK = 10) { if (!_isPcaFitted || _imageItems.Count == 0) throw new InvalidOperationException("索引尚未构建,请先调用BuildIndex。"); // 1. 将查询图片的特征也用拟合好的PCA模型降维 var queryVector = Vector<float>.Build.Dense(queryOriginalFeatures); var queryReduced = _pca.Transform(queryVector.ToRowMatrix()).Row(0); var results = new List<(ImageItem, float)>(); // 2. 计算与库中所有图片的余弦相似度 (在降维空间) for (int i = 0; i < _imageItems.Count; i++) { var dbVector = Vector<float>.Build.Dense(_imageItems[i].ReducedFeatures); // 余弦相似度 = (A·B) / (||A|| * ||B||) float dotProduct = queryReduced.DotProduct(dbVector); float normQuery = (float)queryReduced.L2Norm(); float normDb = (float)dbVector.L2Norm(); if (normQuery == 0 || normDb == 0) continue; // 避免除零 float cosineSimilarity = dotProduct / (normQuery * normDb); results.Add((_imageItems[i], cosineSimilarity)); } // 3. 按相似度降序排序,返回TopK return results.OrderByDescending(r => r.Score).Take(topK).ToList(); } } // 一个简化的PCA实现类(核心部分) public class PCA { private Vector<float> _mean; private Matrix<float> _components; // 主成分(特征向量) public PCA(Matrix<float> data, int components, bool Center = true) { int n = data.RowCount; int p = data.ColumnCount; // 1. 中心化 _mean = Center ? data.ColumnMeans() : Vector<float>.Build.Dense(p, 0.0f); var centeredData = data.Clone(); if (Center) { for (int i = 0; i < n; i++) { centeredData.SetRow(i, data.Row(i) - _mean); } } // 2. 计算协方差矩阵 (简化版,实际生产环境可能用SVD更稳定) var covariance = (centeredData.Transpose() * centeredData) / (n - 1); // 3. 特征值分解 (这里使用MathNet的Evd) var evd = covariance.Evd(); var eigenvalues = evd.EigenValues.Real().ToArray(); var eigenvectors = evd.EigenVectors; // 4. 按特征值降序排列,选取前components个主成分 var sortedIndices = eigenvalues.Select((val, idx) => new { Val = val, Idx = idx }) .OrderByDescending(x => x.Val) .Select(x => x.Idx) .Take(components) .ToArray(); _components = Matrix<float>.Build.Dense(p, components); for (int i = 0; i < components; i++) { _components.SetColumn(i, eigenvectors.Column(sortedIndices[i])); } } public Matrix<float> Transform(Matrix<float> data) { // 将数据投影到主成分空间 var centered = data.Clone(); for (int i = 0; i < data.RowCount; i++) { centered.SetRow(i, data.Row(i) - _mean); } return centered * _components; } }

核心考量:为什么要在添加所有图片后才调用BuildIndex?因为PCA需要在整个数据集上计算协方差矩阵,以找到最能代表数据方差的主成分方向。如果图片是动态增删的,一种策略是定期重新训练PCA模型,或者使用增量PCA算法。对于示例项目,我们采用简单的批处理方式。targetDimensions(目标维度)是一个需要权衡的超参数,维度太低会丢失信息,太高则压缩效果不佳。通常通过观察特征值的累计贡献率来选择,在示例中64或128是一个不错的起点。

4.4 串联整个流程:从图片到搜索结果

现在,我们把所有模块组合起来,形成一个完整的、可执行的流程。

class Program { static async Task Main(string[] args) { // 0. 初始化 string modelPath = @"Models\resnet50.onnx"; string imageDatabaseFolder = @"Images\Database"; string queryImagePath = @"Images\Query\test.jpg"; using var featureExtractor = new FeatureExtractor(modelPath); var searchEngine = new ImageSearchEngine(); // 1. 构建图像库索引 Console.WriteLine("开始构建图像库索引..."); var imageFiles = Directory.GetFiles(imageDatabaseFolder, "*.jpg"); foreach (var file in imageFiles) { try { // 预处理 -> 特征提取 var preprocessed = ImagePreprocessor.PreprocessImage(file); var features = featureExtractor.Extract(preprocessed); // 添加到引擎(暂存原始特征) string id = Path.GetFileNameWithoutExtension(file); searchEngine.AddImage(id, file, features); Console.WriteLine($"已处理: {id}"); } catch (Exception ex) { Console.WriteLine($"处理图片 {file} 时出错: {ex.Message}"); } } // 2. 在所有图片特征上拟合PCA并降维,构建最终索引 searchEngine.BuildIndex(targetDimensions: 64); Console.WriteLine("索引构建完成!"); // 3. 执行以图搜图 Console.WriteLine($"\n开始搜索与 {Path.GetFileName(queryImagePath)} 相似的图片..."); var queryFeatures = featureExtractor.Extract(ImagePreprocessor.PreprocessImage(queryImagePath)); var results = searchEngine.Search(queryFeatures, topK: 5); // 4. 输出结果 Console.WriteLine("\n搜索结果(按相似度降序):"); int rank = 1; foreach (var (item, score) in results) { Console.WriteLine($"{rank}. ID: {item.Id}, 文件: {Path.GetFileName(item.FilePath)}, 相似度: {score:F4}"); rank++; } } }

这个主程序清晰地展示了离线以图搜图系统的四个核心阶段:特征提取索引构建查询处理结果排序。运行后,你就能得到一个基于本地图片库的搜索结果。

5. 性能优化与生产环境考量

上面的示例为了清晰,牺牲了一些性能和可扩展性。在实际生产环境中,我们需要考虑更多。

5.1 特征提取的批处理与异步

ONNX Runtime支持批量推理。与其一张一张地处理图片,不如将多张图片的预处理数据堆叠成一个[batch_size, 3, 224, 224]的Tensor,一次性送入模型。这能极大利用CPU/GPU的并行计算能力,显著提升特征提取速度,尤其是在构建大型图库时。

public float[][] ExtractBatch(List<float[]> batchPreprocessedData) { int batchSize = batchPreprocessedData.Count; var dimensions = new int[] { batchSize, 3, 224, 224 }; // 将多个一维数组合并成一个大的Tensor var combinedData = batchPreprocessedData.SelectMany(arr => arr).ToArray(); var tensor = new DenseTensor<float>(combinedData, dimensions); var inputs = new List<NamedOnnxValue> { /* ... */ }; using var results = _session.Run(inputs); // 输出也是一个Batch,需要按样本拆分开 // ... }

同时,整个处理流程(IO读取、预处理、模型推理)可以设计成异步流水线,避免阻塞主线程。

5.2 索引的持久化与增量更新

每次启动都重新提取特征和训练PCA是不现实的。我们需要将ImageItem列表(至少包含ID、文件路径、降维后的特征向量)以及PCA模型的参数(均值向量、主成分矩阵)序列化到磁盘(如使用System.Text.JsonMessagePack保存为二进制文件)。下次启动时直接加载,实现“热启动”。

对于增量更新(新增图片),一个简单的策略是:将新图片的特征用现有的PCA模型进行降维,然后追加到索引中。但这会导致PCA模型逐渐偏离最优(因为它是基于旧数据训练的)。更严谨的做法是定期(例如每天)用全量数据重新训练PCA,或者在新增数据达到一定比例后触发重训。

5.3 近似最近邻搜索的进阶方案

当图片库达到百万级别时,即使降维到64维,线性扫描(计算与库中每一个向量的距离)的复杂度O(N)依然很高。这时必须引入更专业的ANN索引。

  • 本地集成方案:可以考虑使用ML.NETLsh(局部敏感哈希)来构建索引,它适用于中等规模的数据集。或者,使用KdTree(对于64维以下数据效果尚可)的C#实现。
  • 专用向量数据库:这是大规模生产系统的标准选择。你可以将特征向量导入到像MilvusWeaviateQdrant这样的专用向量数据库中。它们提供了高效的ANN索引(如HNSW、IVF)和丰富的查询API。你的C#应用通过对应的.NET客户端SDK与之交互。这实现了计算和存储的分离,扩展性极强。

5.4 准确度调优与后处理

  • 模型选择:ResNet50是一个很好的起点,但你也可以尝试更轻量的MobileNet(速度快)或更强大的EfficientNet、Vision Transformer(准确度高)。可以在Hugging Face或TensorFlow Hub上找到这些模型的ONNX格式。
  • 特征融合:有时,单一模型的特征可能不够鲁棒。可以尝试提取多个不同层次或不同模型的特征,将它们拼接(Concatenate)或加权平均,形成更强大的特征表示。
  • 重排序:先用快速的ANN索引(如HNSW)召回Top K(例如1000个)候选,然后再用原始的高维特征(或更复杂的相似度计算方式)对这K个候选进行精确的相似度计算和重排序,以提升最终Top N结果的精度。这是一种经典的“召回-重排序”两阶段策略。

6. 示例项目基于C#以图搜图示例.zip内容导览

为了让这个分享不只是纸上谈兵,我准备了一个完整的示例项目压缩包。解压后,你会看到如下的结构:

基于C#以图搜图示例/ ├── ImageSearchDemo.sln # 解决方案文件 ├── ImageSearchCore/ # 核心类库项目 │ ├── FeatureExtractor.cs # ONNX模型推理封装 │ ├── ImagePreprocessor.cs # 图像预处理 │ ├── ImageSearchEngine.cs # 搜索引擎核心(含PCA) │ ├── PCA.cs # PCA降维实现 │ └── ImageSearchCore.csproj ├── ImageSearchConsole/ # 控制台演示项目 │ ├── Program.cs # 主程序,串联全流程 │ └── ImageSearchConsole.csproj ├── Models/ │ └── resnet50.onnx # **需要你自行下载放置** ├── Images/ │ ├── Database/ # 用于构建索引的图片库(示例图片) │ └── Query/ # 用于搜索的查询图片 ├── README.md # 项目说明、环境配置、运行指南 └── 运行演示.bat # 一键运行脚本(Windows)

如何使用这个示例:

  1. 环境准备:确保安装.NET 6或更高版本的SDK。用Visual Studio 2022或VS Code打开解决方案。
  2. 模型下载:按照README.md中的链接,下载ResNet50的ONNX模型文件,放入Models文件夹。
  3. 准备图片:在Images\Database下放入一些用于构建索引的图片(如各种猫、狗、风景图)。在Images\Query下放入你想搜索的图片。
  4. 运行:直接运行ImageSearchConsole项目,或者执行运行演示.bat。控制台会输出索引构建过程和最终的相似图片搜索结果。

这个示例项目麻雀虽小,五脏俱全。它没有依赖任何外部服务,所有计算都在本地完成,非常适合作为理解以图搜图原理和进行二次开发的起点。你可以基于此,轻松地将其集成到你的WPF、WinForms、ASP.NET Core或者MAUI应用中,实现一个属于自己的智能图库管理功能。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询