简介:本资源为基于C#实现的以图搜图功能示例源码包,面向具备一定C#基础、希望学习图像检索与人像比对开发的开发者。项目围绕图片上传、人像比对搜索展开,涵盖图像加载、特征提取、相似度计算与结果展示等核心环节,并涉及远程API接口调用与网络通信知识,适合作为图像处理与检索方向的学习起点。压缩包共108个文件,约197.63MB,以32个cs源码文件为核心,辅以25个dll依赖库、7个resx资源文件、6个png图片及config、csproj、sln等配置与项目文件,结构完整,便于在Visual Studio中直接打开研究。目前已有84人学习下载。通过研读源码,读者可掌握图像读取、处理、特征存储、相似度比对及结果呈现的完整流程,理解本地搜索与远程接口调用的实现思路,并在此基础上进行二次开发与功能扩展。
1. 从一张截图到相似图结果:C# 以图搜图到底在解决什么
电商后台收到用户投诉,说买到的鞋和详情页「看起来不一样」,运营丢过来一张手机拍的实物图,问你库里哪几张商品主图跟它最像。这时候关键词搜索没用,因为图里没有可读的文字,你要的是「以图搜图」。这个标题里的基于C#以图搜图示例.zip,本质就是一套用 C# 把「输入一张查询图 → 输出若干张相似图」跑通的工程骨架,通常包含特征提取、特征比对、结果排序三段。它适合两类人:一类是做上位机、WinForm/WPF 桌面工具的 C# 开发者,被要求给现有系统加一个「找相似」按钮;另一类是想把 OpenCV、ONNX 这类能力接进 .NET 生态,却不想整套换 Python 的工程师。下面我按能复现的顺序,把选型、代码、参数和翻车点讲清楚。
2. 特征怎么选:ORB、SIFT 还是 CNN 嵌入
2.1 三种主流路线与适用边界
以图搜图的核心不是「比较像素」,而是把图片压成一个可比较的向量或关键点集合。C# 里能落地的路线大致三条,选错了后面全是白干。
第一条是传统局部特征,代表是 ORB 和 SIFT。它们从图里找角点、边缘这类稳定位置,每个点生成一个描述子,比对时做匹配点统计。优点是无需训练、无需 GPU、纯 CPU 就能跑,OpenCvSharp 直接提供封装。缺点是面对整体色调相近但内容不同的图容易误判,对旋转、缩放有一定鲁棒性,但对大角度透视和强光照变化会掉点。
第二条是全局颜色/纹理直方图。把图切成网格,统计每个格子的颜色分布,拼成一个长向量,用余弦或卡方距离比对。实现最简单,几十行代码,但几乎不抗裁剪和旋转,只适合「同一商品不同角度」这种温和场景。
第三条是 CNN 嵌入,用预训练模型(如 ResNet、MobileNet)把图映射成 512 或 1024 维向量,再算余弦相似度。这是目前效果最稳的做法,代价是要引入 ONNX Runtime 或 OpenVINO,模型文件几十 MB 起。C# 里通过Microsoft.ML.OnnxRuntime加载 ONNX 模型是常见做法,热词里出现的c#创建openvino输入张量也属于这条线。
我的建议很直接:如果库规模在几千张以内、图是商品图这类主体明确的,先用 ORB 跑通;如果要做「拍立淘」那种任意拍摄角度的检索,直接上 CNN 嵌入,别在传统特征上耗时间。
2.2 用 OpenCvSharp 提取 ORB 特征的最小代码
先装包。在项目里通过 NuGet 引入OpenCvSharp4和OpenCvSharp4.runtime.win,后者提供原生 DLL,缺了它运行时会报找不到OpenCvSharpExtern。
using OpenCvSharp; using System; using System.Collections.Generic; public class OrbFeatureExtractor { // nfeatures:最多保留多少个关键点,太多会拖慢匹配,太少召回不够 private readonly ORB _orb = ORB.Create(nfeatures: 500); public (KeyPoint[] points, Mat descriptors) Extract(string imagePath) { // 以灰度读入,彩色信息对 ORB 无意义,反而增加计算量 using var src = Cv2.ImRead(imagePath, ImreadModes.Grayscale); if (src.Empty()) throw new ArgumentException($"图片读取失败: {imagePath}"); // 统一缩放到长边 640,避免大图关键点分布过散 using var resized = ResizeKeepRatio(src, 640); var points = _orb.Detect(resized, out var descriptors); return (points, descriptors); } private static Mat ResizeKeepRatio(Mat src, int longSide) { double scale = (double)longSide / Math.Max(src.Width, src.Height); if (scale >= 1.0) return src.Clone(); var size = new Size((int)(src.Width * scale), (int)(src.Height * scale)); var dst = new Mat(); Cv2.Resize(src, dst, size, interpolation: InterpolationFlags.Area); return dst; } }逻辑上分三步:灰度读入去掉颜色干扰,等比缩放把长边压到 640 控制计算量,然后Detect出关键点并计算描述子。参数里nfeatures: 500是我常用的起点,商品图这种纹理不复杂的场景够用;如果是书籍封面、海报这类文字密集的图,可以提到 1000。缩放那步别省,我见过直接拿 4000 像素原图跑 ORB 的,单张耗时从 30ms 涨到 400ms,检索一百张就是几十秒,用户早关窗口了。
2.3 特征比对与相似度打分
拿到查询图和库图的描述子后,用 BFMatcher 做暴力匹配,再按「好匹配点占比」打分。
using OpenCvSharp; using System.Linq; public class FeatureMatcher { private readonly BFMatcher _matcher = new BFMatcher(NormTypes.Hamming, crossCheck: true); // 返回 0~1 的相似度,越大越像 public double Score(Mat queryDesc, Mat trainDesc) { if (queryDesc.Empty() || trainDesc.Empty()) return 0; var matches = _matcher.Match(queryDesc, trainDesc); // 距离阈值 50 是 Hamming 空间的经验值,越小越严格 int good = matches.Count(m => m.Distance < 50); int denom = System.Math.Min(queryDesc.Rows, trainDesc.Rows); return denom == 0 ? 0 : (double)good / denom; } }NormTypes.Hamming是 ORB 这类二进制描述子必须用的距离度量,用欧氏距离会得到无意义的结果。crossCheck: true表示双向匹配,能过滤掉一批单向的偶然匹配,代价是速度慢约一倍。阈值 50 不是金科玉律,纹理丰富的图可以放宽到 60,纯净背景的图收紧到 40 更准。打分用「好匹配数 ÷ 较小描述子数量」,比直接用匹配总数更公平,否则描述子多的图天然占便宜。
3. 把检索跑起来:索引、比对、排序的完整链路
3.1 离线建索引:把特征存下来
每次查询都重新提取全库特征是不现实的。正确做法是离线把每张图的描述子算好存盘,查询时只算查询图。描述子是Mat,不能直接序列化,我一般转成字节数组存二进制文件,或者存进数据库的 BLOB 字段。
using OpenCvSharp; using System.IO; public static class FeatureStore { public static void Save(string indexPath, string imagePath, Mat descriptors) { // 用追加模式,每张图一段:路径长度 + 路径 + 描述子行数 + 描述子字节 using var fs = new FileStream(indexPath, FileMode.Append, FileAccess.Write); using var bw = new BinaryWriter(fs); byte[] pathBytes = System.Text.Encoding.UTF8.GetBytes(imagePath); bw.Write(pathBytes.Length); bw.Write(pathBytes); bw.Write(descriptors.Rows); bw.Write(descriptors.Cols); // Mat 连续内存转字节,注意 GetArray 需要 Mat 是连续的 byte[] descBytes = new byte[descriptors.Total() * descriptors.ElemSize()]; System.Runtime.InteropServices.Marshal.Copy( descriptors.Data, descBytes, 0, descBytes.Length); bw.Write(descBytes.Length); bw.Write(descBytes); } }这里有个容易翻车的点:descriptors.Data只有在 Mat 连续时才指向一整块内存。ORB 输出的描述子通常是连续的,但如果你中间做过 ROI 裁剪或转置,就得先descriptors = descriptors.Clone()保证连续,否则Marshal.Copy会读到脏数据,表现为检索结果莫名其妙。存的时候把路径一起写进去,查询时才能反查是哪张图。
3.2 在线查询:加载索引并排序
查询阶段把索引文件读回内存,逐条比对打分,取 Top-N。
using OpenCvSharp; using System.Collections.Generic; using System.IO; using System.Linq; public class ImageSearcher { private readonly List<(string path, Mat desc)> _index = new(); private readonly FeatureMatcher _matcher = new(); public void LoadIndex(string indexPath) { using var fs = new FileStream(indexPath, FileMode.Open, FileAccess.Read); using var br = new BinaryReader(fs); while (fs.Position < fs.Length) { int pathLen = br.ReadInt32(); string path = System.Text.Encoding.UTF8.GetString(br.ReadBytes(pathLen)); int rows = br.ReadInt32(); int cols = br.ReadInt32(); int byteLen = br.ReadInt32(); byte[] bytes = br.ReadBytes(byteLen); var mat = new Mat(rows, cols, MatType.CV_8UC1); System.Runtime.InteropServices.Marshal.Copy(bytes, 0, mat.Data, byteLen); _index.Add((path, mat)); } } public List<(string path, double score)> Search(Mat queryDesc, int topN = 10) { return _index .Select(item => (item.path, score: _matcher.Score(queryDesc, item.desc))) .OrderByDescending(x => x.score) .Take(topN) .ToList(); } }MatType.CV_8UC1对应 ORB 描述子的类型,单通道 8 位无符号。如果你换成 SIFT,描述子是CV_32F,这里必须同步改,否则读出来的数据全是乱的,相似度会变成随机数。OrderByDescending对几千条记录完全够快,超过十万条就该考虑用 KD-Tree 或向量数据库了,但在 C# 桌面场景里很少到这个量级。
3.3 用 WinForm 把结果展示出来
热词里c# winform如何更新状态栏与进度条正好对应这个场景:建索引是耗时操作,得让用户看到进度。
private async void btnBuildIndex_Click(object sender, EventArgs e) { var files = Directory.GetFiles(@"D:\images", "*.jpg"); progressBar1.Maximum = files.Length; progressBar1.Value = 0; await Task.Run(() => { foreach (var file in files) { var (_, desc) = _extractor.Extract(file); FeatureStore.Save(indexPath, file, desc); // 跨线程更新 UI 必须用 Invoke,直接改控件会抛异常 this.Invoke(new Action(() => { progressBar1.Value++; statusLabel.Text = $"已处理 {progressBar1.Value}/{files.Length}"; })); } }); MessageBox.Show("索引构建完成"); }关键点是Task.Run把耗时循环丢到后台线程,UI 线程只负责刷新。Invoke那行是血泪经验:直接在后台线程写progressBar1.Value会抛InvalidOperationException,提示控件被非创建线程访问。热词里的c# timer 访问控件也是同一类问题,Timer 回调同样在非 UI 线程上。
4. 避坑与排查:以图搜图在 C# 里最容易翻车的五件事
4.1 现象:所有图片相似度都接近 0
原因通常是描述子类型不匹配。ORB 输出CV_8UC1,SIFT 输出CV_32F,如果建索引时用 ORB、加载时按 SIFT 的类型去读,字节解释方式完全不同,匹配自然全废。解决方法是把特征类型和 MatType 一起存进索引文件头,加载时校验,不一致直接报错而不是静默返回 0。
4.2 现象:同一张图查询自己,相似度却不是 1.0
这多半是缩放不一致导致的。建索引时缩放到 640,查询时忘了缩放,关键点数量和位置都对不上。解决方法是把缩放逻辑抽成一个公共方法,建索引和查询都调它,别在两处各写一遍。另外 ORB 本身有随机性,同一张图两次提取的关键点可能略有差异,相似度 0.95 以上就算正常。
4.3 现象:程序跑一会儿内存暴涨
Mat和KeyPoint[]都是非托管资源,Mat实现了IDisposable,用完必须释放。常见错误是在循环里new Mat()却不using,几百张图下来内存就上 G 了。解决方法是所有Mat都用using包裹,或者显式调Dispose()。索引常驻内存的那部分描述子是有意保留的,不算泄漏。
4.4 现象:匹配速度慢到无法接受
如果库里有几万张图,每次查询都全量暴力匹配,单次可能要几秒。解决方向有两个:一是先用颜色直方图做粗筛,把候选集从几万降到几百,再用 ORB 精排;二是把描述子转成向量后用近似最近邻(如 HNSW)索引。前者实现简单,后者需要引入额外库,按库规模选。
4.5 现象:发布到客户机器上报找不到 DLL
OpenCvSharp4依赖原生OpenCvSharpExtern.dll,开发机上装了运行时包所以能跑,拷到客户机就崩。热词里的c# costura.fody 合并dll是一种思路,但 Costura 对原生 DLL 支持有限。更稳的做法是把runtimes目录整个带上,或者用c#应用防反编译工具之外的单文件发布方案,确认原生库被正确复制到输出目录。
5. 进阶:把 ORB 换成 CNN 嵌入,以及怎么验证效果
ORB 在商品图上够用,但遇到「拍摄角度差异大、背景杂乱」的查询就会掉链子。这时候把特征提取换成 CNN 嵌入是自然的下一步。C# 里通过 ONNX Runtime 加载一个预训练的 MobileNet 或 ResNet,把倒数第二层输出当作特征向量。
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; public class CnnEmbedder { private readonly InferenceSession _session; public CnnEmbedder(string modelPath) { // 用 CPU 执行提供程序,有 GPU 可换成 CUDA 或 DirectML _session = new InferenceSession(modelPath); } public float[] Embed(float[] inputTensor) { // 输入形状通常是 [1, 3, 224, 224],需按模型要求调整 var tensor = new DenseTensor<float>(inputTensor, new[] { 1, 3, 224, 224 }); var inputs = new List<NamedOnnxValue> { NamedOnnxValue.CreateFromTensor("input", tensor) }; using var results = _session.Run(inputs); // 取第一个输出展平为向量 return results.First().AsEnumerable<float>().ToArray(); } }输入张量的构造是热词c#创建openvino输入张量的同类问题:模型要的是 NCHW 布局,归一化到 0~1 或按 ImageNet 均值方差标准化,这两步错了,输出向量就没有区分度。比对时用余弦相似度,向量先做 L2 归一化,点积就是余弦值。
验证效果别靠肉眼扫结果。准备一个小的标注集,比如 50 张查询图,每张人工标出库里真正相似的前 5 张,然后算 Top-5 命中率。ORB 在商品图上通常能到 70% 左右,CNN 嵌入能到 90% 以上。这个数字比任何主观感受都可靠,也是你决定要不要上模型的依据。
我自己的习惯是:任何以图搜图方案上线前,先跑一遍这个标注集,把命中率和单次查询耗时两个数记下来。命中率不达标就调特征,耗时超标就加粗筛。这套流程帮我省过好几次「上线后才发现效果不行」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取