搜图这件事有不同的实现方案,但其实最底层的逻辑都是一致的:提取归一化后的特征进行距离计算。
搜图原理
图像组成
在了解搜图算法之前,我们需要先了解下图片的组成:像素
、RGB
、像素分布
、元数据
。
像素:像素是一张图片的最小组成单位,一个个像素拼接为最终的图片。
RGB:颜色组成基础就是 RGB
三原色,一张图片的每个像素都承载了不同的 RGB
数值代表颜色。
像素分布:一张图片要想表达信息(比如一只猫或者一个茶杯),那就需要将承载不同 RGB
数值的像素块按一定序列排列并组合在一起。从信息的角度来说这种组合又分为两种:
- 结构数据:结构数据就是一个图片的骨架,也叫做基础信息,它把整个图片切割为不同的区域,就像是画素描一样,要先去勾勒轮廓。
- 筋肉数据:筋肉数据用于填充一个图片的骨架,它主要是提供细节,诸如某个具体区域的颜色,些微的光泽(光泽本质其实就是颜色)等等。
而我们一般进行诸如无损图像压缩、搜图等等,都是从结构数据入手,尽可能多的提取结构数据,抛弃筋肉数据和元数据。当然不同场景对于提取和抛弃程度不一样,但是底层逻辑是相通的。这个提取出来的就是图片的特征值。
元数据:诸如创建/拍摄时间、相机厂牌、定位信息、盲水印等等。
归一化/标准化
而什么是归一化/标准化呢?通俗来讲就是尽可能让不同的图片在同一标准下提取特征数据。
搜图处理常用的手段有:灰度化
、图像缩小
、边框去除
。
灰度化:这样能确保即使图片调了曝光、加了滤镜,灰度化以后,特征提取是稳定的。
图像缩小:这种方式能够确保同一张图片在执行不同的缩放或纵横比后,能回归一致。
边框去除:去除一行或者一列都是同一极端颜色的行或列,比如纯黑或纯白。这种方式能够保证不受边框干扰。
两类搜图算法:HASH 和 向量
HASH
HASH
搜索中最简单的是直接把图片进行简单归一化后 HASH
。但是这种 HASH
方式只适用于简单的去重或搜索操作,并且很容易受到干扰。只需要加下水印,两张肉眼相同或相近的图片他们的 HASH
值就完全不一样了。
除此之外还有三种图像感知 HASH
算法:AHASH
、DHASH
、PHASH
。
AHASH:图像均值 HASH
算法。这种算法主要步骤是:
- 将图片灰度化并缩小为
8 X 8
的图像。 - 计算图像平均颜色。
- 根据每个像素颜色与平均颜色的比较来决定对应的
64
位HASH
中的每一个值。
DHASH:图像梯度 HASH
算法。这种算法的主要步骤是:
- 将图片灰度化并缩小为
9 X 8
的图像。 - 计算相邻像素之前的差异,这个差异就是相对梯度。每行
9
个像素能计算出8
个差异。8
行8
个差异就变成了64
位HASH
。
PHASH:图像频率 HASH
算法。这种算法的主要步骤是:
- 将图片灰度化并缩小为
32 x 32
的图像。 - 使用
离散余弦变换(DCT)
计算出图像内频率和标量
的集合。 - 保留左上角
8 X 8
的低频数据。 - 排除第一个
CD
项后,计算DCT
平均值。 - 根据
64
个DCT
与DCT
平均值的对比设置来转换为64
位HASH
。
上述三种方式的底层逻辑是一致的。先进行标准化将其变为 指定范围内的像素块。之后对每个像素块采用不同的方式进行计算,得到最终的 HASH
。而判断两个图片是否相似,只需要计算两个图片的最终 HASH
值的汉明距离是否在一定范围内。
关于汉明距离,通俗理解就是:两个字符串他们最小需要更改多少个字符能变成同一个字符串,这个更改字符串的次数就是汉明距离。
常用于错字推测,比如 错误单词 CPT
可以推测为真实想要输入的是 OPT
或 CAT
,这两个单词只需要更改一个字符就可以,CPT
与 OPT
的汉明距离是 1,CPT
与 CAT
的汉明距离也是 1,但 OPT
与 CAT
的汉明距离是 2 。
图像感知 HASH
算法很大程度上可以解决搜图中遇到的许多问题以及图像干扰。但是还不够好,对于诸如:特征搜索(商品搜索)、高近似度(包括颜色相近) 等场景下,精准度就下来了。这个时候我们就可以使用向量进行特征提取。
向量
相对于 HASH
来说,向量可以提取多个维度的特征值。常用的有 cnn
下的 alexNet
、vgg16
、googLeNet
、resNet
等。
将一张图片的 RGB
序列化为多维矩阵,这个多维矩阵就是向量值。而我们一般只使用中低维度的向量,比如 512 维
、256 维
等等。
而使用不同的模型,会有不同的提取原理和步骤。
使用向量搜索的特点在于,它会去匹配相近、相似的图片,比如搜索一个机器人, HASH
的搜图只能根据这张图片去搜索相近的,而向量甚至可以帮你搜索出来各种各样的相似机器人。这是他们两者之前最大的差别。
而判断两个图片的向量是否相似,则是直接计算两个向量之间的距离。
Milvus 向量库
目前我们的搜图系统在用的是 milvus
向量库。在聊 milvus
之前我们先说说,为什么要使用专门的向量检索库:
- 向量检索库一般都内置了常用的矩阵计算方法,拿来即用,能极大的降低开发和维护成本
- 向量检索库本身就是为了向量而生,所以对于一些大规模向量数据集,它的搜索性能和可靠性会更好。不使用普通数据库进行搜索的原因是,普通数据库的搜索侧重与向量数据库并不一样。向量偏向于矩阵间的差值,普通数据库偏重于字符或纯数间的比较、计算。
milvus
目前支持多节点集群,确保milvus
的高可用。对于节点也方便横向扩展。- 而选择
milvus
,还有一个原因是:支持国产,并且社区响应及时。目前来看基本上官方是有问必答。
以图搜图实践
目前 milvus
主要是用于以图搜图的场景。
在版本上采用的是最新的 2.2
版本进行集群化部署。
这主要是考虑到数据量比较大,单机的数据库支撑起来比较吃力,并且单节点的稳定性是不如集群的。
稳定性这里指的并非是 milvus
,而是主要考虑到机器故障、网络故障等一系列因素。
在整体的技术架构上,我将图片特征向量提取单独地作为一个服务进行部署,这一块主要是为了将图片特征向量丛业务中抽离出来,方面后续有其他业务使用图片向量提取。
Milvus 架构简述
在使用 milvus
中我也遇到了一些问题,在分享之前,我想简单聊一下,milvus
各个组件的用途,这在问题排查时很有帮助。
milvus
整体分为两部分,外部依赖及内部组件。外部依赖主要有:etcd
、pulsar/kafka
、minio/s3
。
etcd
的作用有两个:存储元数据(主要是 collection
等结构相关数据)和服务发现。
如果 etcd
出现问题则可能导致:
- 元数据丢失,会导致整体的数据自动被删除,因为元数据与存储的向量数据都是对应的。如果没有元数据,那么向量数据也是无法直接用的。
- 服务不稳定,会导致整体的
milvus
服务不稳定。
minio/s3
的作用只有一个:存储向量数据的 log 文件。minio/s3
内存储的 log
文件设计的有点像是 mysql
的 binlog
。如果 minio/s3
的数据丢失了,那向量数据就是彻底丢失了。
pulsar/kafka
是作为 milvus
的血管,他的主要作用就是任务执行以及流式处理平台。如果你的请求走不动了,请务必看看 pulsar/kafka
有没有问题。
索引选型
Collection 创建/搜索步骤
collection
的整体创建步骤如下:
- 创建指定数据结构的
collection
,创建数据结构时,需要至少指定一个主键字段和一个向量字段。 - 创建
collection
后需要创建索引,或者创建数据后创建索引都行。 collection
创建后只需要创建一次索引即可,collection
不论是否加载,数据插入都会根据配置制定的时间去创更新索引。- 如果想更改
collection
的索引类型,需要先释放collection
,删除索引、创建新索引,才能再加载搜索。
问题及解决方法
链接/访问/搜索问题
查询/访问比较慢:对于集群服务部署后,查询特别慢的问题,如果是数据量很小就查询很慢,一般因为 pulsar / kafka
的性能问题。如果是数据量小查询快,数据量大查询慢,则需要去看一下资源占用情况,相应的可以做扩容或者按 官方文档 来去调节限额。
attu / 客户端链接:attu
工具无法登陆的问题,可以排查下 attu
与 milvus
的 proxy
节点是否网络相通,milvus
是否有节点异常。milvus
内部或依赖如果有部分节点异常(尤其是 coord
、pulsar
、etcd
等都可能导致 attu
无法登陆)。客户端同理。
查询/搜索异常: 查询/搜索前,必须确保:
collection
已经创建了索引并已经被加载了。- 只有创建了索引的情况下,才能确保
collection
被加载。只有被加载的情况下,才能对collection
内容进行搜索/查询。
非预期内查询结果:有时候搜索数据,会出现前几个是匹配的,后面的全是不匹配的。这是因为查询结果只会按差异得分从低到高来排序,并返回指定的 TopK
条内的数据,暂时不支持 score
分值范围筛选,需要拿到结果后对差异得分 score
进行筛选,建议 score <= 0.5
,score
超过 1
的结果基本是不相干的。
数据相关问题
数据丢失:
- 检查下你的代码,如果你使用的是
python
,并且在创建或更新collection
设置了collection.ttl.seconds
,请把它去掉或更改为你需要的值。目前仅python
客户端支持ttl
设置。 - 检查下
etcd
的文件是否丢失了,milvus
会根据配置的定时,自动去清理不存在etcd
中的元数据对应的所有文件。 - 检查下
minio/s3
和其他服务节点有没有异常。
数据删除:数据在 milvus
中删除后,不会立即释放磁盘,会到指定的过期时间之后从磁盘层面删除已删除的数据。这个时间默认是一天。
数据写入后搜索延时:如果数据写入后搜索不到,需要检查下是否配置了延时搜索。默认是插入后立即就能搜索。
其他
**配置**:官方镜像中默认会自带一个标准配置,如果需要进行自自定义配置的话,可以把配置文件挂载到 `/milvus/configs/milvus.yaml` 上。如果你使用的 `minio/etcd` 需要指定 `bucket` ,也是需要去在配置文档里制定的。
依赖服务:对于依赖服务建议:
etcd
组集群,运行于ssd
硬盘上,并定期备份。pulsar
的bookkeeper
,建议对journal
和ledgers
进行多磁盘挂载,可以较好的提高吞吐量。