我独立开发了一个叫 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 个维度。
关键设计思路:
-
哈希用两种而非一种:aHash(均值哈希)对亮度分布敏感,适合区分「白瓷」和「青铜」;dHash(梯度哈希)对边缘敏感,适合区分「素面」和「雕花」。两者互补,计算成本几乎为零。
-
rowProfile/columnProfile 捕获物体轮廓:把前景像素投影到水平和垂直轴上,得到一个 16 维的「形状签名」。这让系统能区分「细长的花瓶」和「扁平的盘子」,即使颜色完全一样。
-
前景分割不用深度学习:采样边框像素作为背景色估计,然后对每个像素计算与背景的颜色距离。对于古董拍卖这种通常有纯色摄影背景的场景,效果足够好。
为什么是 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-artwork 和 match-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
评论
评论已关闭。