如何在浏览器里实现一个古董拍卖平台的「以图搜图」引擎

我独立开发了一个叫 Eastwood Auction 的古董拍卖平台。用户浏览瓷器、玉器、书画、铜器等古董时,常常手里有张参考照片,但不知道这件东西叫什么、属于哪个类别、哪个年代。

关键词搜索在这种情况下基本失效——一个普通买家不会写「清乾隆青花缠枝莲纹赏瓶」,他只会掏出一张照片。

所以我需要以图搜图。但问题是:我一个人开发,没有 GPU 服务器;用户上传的可能是价值几十万的古董照片,隐私敏感;拍卖浏览是快速探索型的,等不起服务器返回的几百毫秒延迟。

最终我的方案是:把整个视觉搜索引擎塞进浏览器里跑

整体架构

用户上传图片
    │
    ▼
┌─────────────────────────────┐
│  浏览器端特征提取            │
│  • 48×48 像素分析画布        │
│  • 8维颜色特征向量           │
│  • 14维扩展签名              │
└──────────────┬──────────────┘
               │
    ┌──────────┴──────────┐
    │                      │
    ▼                      ▼
┌──────────────┐   ┌──────────────────┐
│ 客户端匹配     │   │ 服务端匹配(可选) │
│ • 签名相似度   │   │ • HuggingFace API │
│ • 置信度门控   │   │ • pgvector 向量搜索│
│ • 即时返回     │   │ • 512维embedding  │
└──────────────┘   └──────────────────┘

默认走客户端路径:零服务器成本、零延迟、保护隐私。服务端路径作为知识库规模增大后的 fallback。

核心一:图像签名设计

第一层:8维颜色特征向量

type ArtworkFeatureVector = [
  red: number,        // 平均红色通道强度
  green: number,      // 平均绿色通道强度
  blue: number,       // 平均蓝色通道强度
  brightness: number, // 整体亮度
  saturation: number, // 色彩饱和度
  warmth: number,     // 暖色调倾向(红+金)
  coolness: number,   // 冷色调倾向(蓝+绿)
  contrast: number    // 明暗对比度
];

这个 8 维向量是「粗筛」。比如一件青花瓷(蓝白色调)和一件铜器(暖棕色调),仅靠这个向量就能拉开距离。但光有颜色不够——两件都是青花瓷的时候,颜色几乎一样,需要更深层的特征来区分。

第二层:14维扩展签名

包括 48-bin RGB 颜色直方图、16-bin 边缘强度空间分布、两种感知哈希(aHash + dHash)、8×8 亮度空间分布、8-bin 边缘方向分布、前景像素的水平和垂直投影、前景占比和重心位置等 14 个维度。

关键设计思路:

  1. 哈希用两种而非一种:aHash(均值哈希)对亮度分布敏感,适合区分「白瓷」和「青铜」;dHash(梯度哈希)对边缘敏感,适合区分「素面」和「雕花」。两者互补,计算成本几乎为零。

  2. rowProfile/columnProfile 捕获物体轮廓:把前景像素投影到水平和垂直轴上,得到一个 16 维的「形状签名」。这让系统能区分「细长的花瓶」和「扁平的盘子」,即使颜色完全一样。

  3. 前景分割不用深度学习:采样边框像素作为背景色估计,然后对每个像素计算与背景的颜色距离。对于古董拍卖这种通常有纯色摄影背景的场景,效果足够好。

为什么是 48×48?

分辨率 像素数 边缘检测质量 处理时间
32×32 1,024 边缘模糊 ~2ms
48×48 2,304 足够清晰 ~5ms
64×64 4,096 略好 ~18ms

48×48 是一个 sweet spot:2,304 像素的数据量足够做边缘检测和空间分布分析,而处理时间仅约 5ms。

核心二:加权相似度打分

向量相似度(8维)

RGB 通道的权重是 0.3,因为颜色信息已经在下层签名中更精细地表达了,不应在向量层重复主导。

签名相似度(14维,加权组合)

特征 权重 捕获什么
rowProfile 18% 物体水平轮廓
columnProfile 18% 物体垂直轮廓
edgeOrientation 14% 纹理方向
edgeHistogram 12% 边缘空间分布
luminanceGrid 10% 明暗分布
differenceHash 8% 梯度结构
averageHash 8% 亮度结构
aspectRatio 5% 物体比例
centroid 5% 物体位置
foregroundRatio 4% 物体大小
texture 3% 纹理复杂度
colorHistogram 3% 颜色分布

rowProfile 和 columnProfile 权重最高(各 18%),因为对于古董来说,器形是最关键的分类特征——一个梅瓶和一个笔筒,颜色和纹理可能相似,但轮廓完全不同。

形状门控(Shape Gate)

这是整个系统最重要的防误判机制。计算「形状一致性」——row + column + aspectRatio 三者的平均相似度,然后用形状门控因子修正最终分数。如果形状一致度低于 0.38,分数硬性上限为 42 分(满分 100)。

这意味着:即使颜色完全一样,如果形状不匹配,分数也会被大幅压低。防止系统把「红色花瓶」错误匹配到「红色盘子」。

最终分数合成

同时有签名和向量时:finalScore = 0.94 × signatureScore + 0.06 × vectorScore。只有向量时(旧数据),降低权重到 0.58,因为缺少签名的匹配可信度更低。

核心三:置信度门控——主动说「不知道」

大多数搜索系统无论匹不匹配都会返回 Top-K 结果。我们选择了不同的策略:

  • 最低总分 ≥72
  • 最低签名分 ≥0.64
  • 最低形状一致性 ≥0.58
  • 最低向量分 ≥0.52
  • 无签名时的更高门槛 ≥92
  • 第一名和第二名的最小分差 ≥6

六道门槛全部通过,才能返回结果。如果全都不通过,系统会明确告知:"这张图片与当前知识库中的藏品差异较大,系统已主动拒绝低置信度结果。"

这在古董交易场景中尤其重要——买家可能基于匹配结果做出购买决策,误导性的匹配比没有匹配更糟糕。

核心四:混合架构——客户端 + 服务端

服务端路径用于后端索引和知识库规模增大后的检索:

图片 → HuggingFace Embedding API → 512维向量 → L2归一化 → pgvector 相似度搜索

通过 Supabase 的 match_artworks_by_image RPC 函数实现,支持 index-artworkmatch-image 两种操作。

性能数据

在 M2 MacBook Pro 上实测(Chrome):

操作 耗时
图片加载 + 缩放 ~2ms
特征向量提取 ~0.01ms
签名构建 ~5ms
搜索(100 件) ~3ms
总计(首次) ~10ms

对于 1000 件艺术品,预计搜索时间约 30ms——仍然远低于人类感知阈值。内存占用:每件艺术品约 2KB 特征数据,1000 件约 2MB。

工程实践亮点

类型安全:所有特征向量和签名都是 TypeScript 强类型,编译器会检查维度长度和顺序。

Blob URL 内存管理:上传的图片创建的 Blob URL 在搜索完成后手动释放,避免长时间浏览拍卖目录时的内存泄漏。

渐进式图片编码:上传到知识库的图片自动尝试不同 JPEG 质量等级(0.82→0.72→0.6→0.5),找到 ≤1.6MB 的最佳编码。

CDN 代理:所有外部图片经过 /api/proxy-image 代理,解决中国大陆无法访问 Unsplash 等图源的问题。

未来方向

  • WASM 加速:签名提取编译成 WebAssembly,预计 2-3× 加速
  • Service Worker 缓存:签名存到 IndexedDB,打开页面瞬间可用
  • 混合排序:客户端签名分 + 服务端 embedding 分做 learned ranking
  • 3D 搜索:平台已支持 LiDAR 3D 模型(USDZ/GLB),扩展到 3D 形状匹配

总结

这个项目证明了:不需要 GPU 服务器、不需要深度学习框架,在浏览器里也能做出实用级别的以图搜图功能

关键点: - 多维特征签名 > 单一特征(14 个维度,加权组合) - 形状门控 防止颜色误导(最重要的一道防线) - 置信度门控 主动拒绝不可靠结果 - 客户端优先 降低成本和延迟,服务端做 fallback

代码开源在 github.com/hankkyy/EastWood-Auction

关于 Zihao Zhang

后端开发工程师。关注 Java/Spring Boot/Redis/MySQL 技术栈,分布式系统,OLAP 数据库,AI Agent 开发与应用。

评论

评论已关闭。