事实上这篇文章属于一个大的分类,属于个人的一个愿景。

我一直在寻找属于自己的知识管理方式,其实说起来也非常简单,满足两个基本需求:

  1. 快速找到需要的内容,包括自己总结过的结构化与非结构化数据。
  2. 组成知识网络,根据某一个关键点找到关联的知识。

经过较长时间的实践,试过了很多软件,都感觉要将自己的一套归类总结逻辑迁移去出去并被迫改变,同时又没有很好的编程接口。最终对于知识总结和录入方式选择为 Obsidian ,输出结构化 markdown 文本后通过编程完成解析和选择输出模板,同时完成知识提取和展示。

在非结构化数据管理上图像管理也是一个大难题,图像与文本不同,逻辑上的管理还停留在 TAGmate dta (元数据)的层面,这最大的问题是做图片标签和分类以及元数据管理。这完全是重复性体力劳动,再加上图片存量相当大,对于个人来说几乎是不可能完成的。逻辑分类其实是倾向于准确的逻辑分析归纳式管理,目前似乎只有机器学习通过已有模型来实现。关于这点还在探索中。

图像管理上还有一条路就是以图搜图,以图搜图更像是基于观感上的,图像结构上的图片比较。对于图像风格比对也仅仅是看过文章说明,没有见到过可用的产品或开源项目。这里简单描述一下最近对于结构上以图搜图的探索和时间结果。

TL;DR点击列表跳转

1. Milvus vector database

milvus 官方主页项目主页.

Vector database for scalable similarity search and AI applications.

Milvus is an open-source vector database built to power embedding similarity search and AI applications. Milvus makes unstructured data search more accessible, and provides a consistent user experience regardless of the deployment environment.

就像上面所介绍的那样,Milvus 是一个矢量数据库,同时提供了相似性搜索以及一些AI应用。

看到了一些熟悉的使用者。

首先瞄一眼架构图大概知道有什么,然后直接去项目 wiki 去看部署方法。

当前最新的稳定版本为 v2.0.2 以及对应的各种语言的 SDK版本。

看了下文件,只有 rpm 包与 deb ,也就是说 redhat/centosdebian 系统能直接部署, windwosmac 需要安装 go 语言环境编辑或者使用 docker 了。

windows 10 环境下单机部署

  1. 安装与配置好 WSL 下载好任意 linux 镜像
  2. 安装 docker 环境
  3. 下载 milvus-standalone-docker-compose.yml 并保存为 docker-compose.yml 文件
  4. 使用 docker-compose up -d 启动,使用 docker-compose down 停止。

整个过程非常顺滑,就像官方文档写的那样

1
2
3
4
5
6
$ sudo docker-compose ps
Name Command State Ports
----------------------------------------------------------------------------------------------------------------
milvus-etcd etcd -listen-peer-urls=htt ... Up (healthy) 2379/tcp, 2380/tcp
milvus-minio /usr/bin/docker-entrypoint ... Up (healthy) 9000/tcp
milvus-standalone /tini -- milvus run standalone Up 0.0.0.0:19530->19530/tcp,:::19530->19530/tcp

启动了三个容器,分别的 etcd , minio 以及 milvus 本身。
同时 milvus 监听 19530 接口,似乎使用的 protobuf 通信

1.1 替换存储组件

因为我本地也是使用 minio 做文件管理,自然想直接用本地,不需要再开一个容器

1
2
3
4
5
6
7
8
9
10
11
minio:
enabled: false

externalS3:
enabled: true
host: "127.0.0.1"
port: "9000"
accessKey: "minioadmin"
secretKey: "minioadmin"
useSSL: false
bucketName: "milvus-1"

然后重新下载一个全新的 minio,结果这次文件由116M变成了92M,还以为下载错误。

将目录指向一个图片目录结果目录识别成桶,但是文件无法识别。重新上传一下发现了问题。之前的 minio 是依赖于文件系统,文件放在桶里面保持原状,改文件系统也会反映出来。新版文件变成同名的文件夹,再下一级是元数据文件和分片文件,将原始文件分成若干小文件,当前文件都不是很大,不确定是否是一般文件管理系统那样的分成多个文件。

所以这导致一个问题就是之前的 minio 存储无法识别,同时由于最新版的 minio 做了文件变更,那就有文件转移和文件备份的问题。之前直接从目录拷出来,这么一改可能要通过接口或命令导出了。

之前的 minio 是文件系统原样管理,很方便直接或三放工具管理。因为就是文件系统的样式。新版的 minio 这么管理对于文件管理工具来说肯定效率更高,但是可能失去了小规模文件管理的优势。

就我个人的使用来说网络访问使用 minio API接口,但是本机直接通过文件系统读取,这样的修改那么即便的是本地也要使用 minio SDK 通过网络访问了。

更严重的是在改了 yaml 文件后竟然报错不识别 minio.enableexternalS3 节点,暂时改不了先向后吧

1.2 Attu

Milvus management GUI

attu项目地址 来源于已停止维护的旧UI 管理系统 milvus-insight

源码下下来发现是 TS 写的,服务端使用 express 框架, 客户端使用 react

我想这是因为 Milvus 使用 protobuf 通信,导致通信只能通过流的方式,对于前端普遍使用的 restful json 文本的方式似乎比较困难,同时SDK不知道自是否支持前端环境。

这不是这次的主要问题,先不管。

Milvus 的文档中,已经给出几种使用的场景,此外官方的另外一个项目 bootcamp ,给出了几个可直接部署使用的项目,而这其中的 Reverse Image Search 子模块就是本次的最终目的。

注意:如果项目无法部署或部署各种报错,尤其是 python 端报错,其中 99% 的原因都是网络原因导致的 python 各种依赖没有安装成功

Reverse Image Search Based on Milvus & Towhee

This demo uses towhee image embedding operator to extract image features by ResNet50, and uses Milvus to build a system that can perform reverse image search.

值得注意的是这个样例在使用了 Milvus 的同时也使用了 towhee 项目用于抽取图像特征值。

先看了一眼源码发现时似乎的 JS ,于是直接下下来编译。于是又发现这个小项目也分为前后端。

  1. 前端(client):前端使用 react + TS 的方式开发
  2. 后端(server):后端使用 python + uvicorn 开发

再执行各种 npm installpip3 install -r 后迅速跑了起来

结果在扫面路径时发现 python 后端开始下载 pytorch 然后报错,随后在windows 上尝试装 CUDApytorch,结果各种原因装不上,于是找了个 linux 环境直接跑容器

如上图所示使用 bootcamp 提供的文件重启容器,发现起了一个 reactwebclient 一个 pythonwebserver 以及一个 mysql,在容器内修改了 pip 的配置后发现顺畅的安装了 torchtimmopencv ,这其中 opencv_python 60M,torch 750M,没有镜像这速度可想而知。

下载完成后开始去 github 下载 resnet50 模型文件 resnet50_a1_0-14fe96d1.pth,这个无法使用 pip 镜像分流,于是立刻换了个上 mac 电脑直接跑前端和 python 后端,手动下载模型后放入用户目录下 torch 的缓存目录。

2.1 启动

当前运行配置

  1. Milvus 为最开始的单机容器配置,milvus-standalone-docker-compose.yml.
  2. mysql:使用外部配置,在 python 中配置
  3. python与react 源码运行

python端 server/src/config.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import os

############### Milvus Configuration ###############
MILVUS_HOST = os.getenv("MILVUS_HOST", "127.0.0.1")
MILVUS_PORT = int(os.getenv("MILVUS_PORT", "19530"))
VECTOR_DIMENSION = int(os.getenv("VECTOR_DIMENSION", "2048"))
INDEX_FILE_SIZE = int(os.getenv("INDEX_FILE_SIZE", "1024"))
METRIC_TYPE = os.getenv("METRIC_TYPE", "L2")
DEFAULT_TABLE = os.getenv("DEFAULT_TABLE", "milvus_img_search")
TOP_K = int(os.getenv("TOP_K", "10"))

############### MySQL Configuration ###############
MYSQL_HOST = os.getenv("MYSQL_HOST", "192.168.31.141")
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PWD = os.getenv("MYSQL_PWD", "123456")
MYSQL_DB = os.getenv("MYSQL_DB", "milvus")

############### Data Path ###############
UPLOAD_PATH = os.getenv("UPLOAD_PATH", "tmp/search-images")

############### Number of log files ###############
LOGS_NUM = int(os.getenv("logs_num", "0"))
  1. 使用外部数据库必须新建 MYSQL_DB = os.getenv("MYSQL_DB", "milvus") 数据库,数据表 milvus_img_search 会在运行时检测并创建
  2. 连接容器中的 milvusMILVUS_HOST = os.getenv("MILVUS_HOST", "127.0.0.1") ,检测网络是否畅通,检测容器所在宿主机防火墙

react 客户端client/src/utils/Endpoint.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
declare global {
interface Window {
_env_: any;
}
}

let endpoint = `http://192.168.31.81:5000`;
if (window._env_ && window._env_.API_URL) {
endpoint = window._env_.API_URL;
}

export const Train = `${endpoint}/img/load`;
export const Processing = `${endpoint}/progress`;
export const Count = `${endpoint}/img/count`;
export const ClearAll = `${endpoint}/img/drop`;
export const Search = `${endpoint}/img/search`;
export const GetImageUrl = `${endpoint}/data`;

本地运行不知为何 window._env 没有生效,这里直接改 endpoint 为python服务端地址

先找了一张图发现原图匹配没有问题,随后加入50张图并截图部分匹配发现也可以。

2.2 bootcamp 源码分析

客户端只是起到一个接口转发功能,不做分析

首先粗略看一下这种架构图,并直接上代码

1
2
3
4
5
6
7
8
9
10
11
# main.py
@app.post('/img/load')
async def load_images(item: Item):
    # Insert all the image under the file path to Milvus/MySQL
    try:
        total_num = do_load(item.Table, item.File, MODEL, MILVUS_CLI, MYSQL_CLI)
        LOGGER.info(f"Successfully loaded data, total count: {total_num}")
        return "Successfully loaded data!"
    except Exception as e:
        LOGGER.error(e)
        return {'status': False, 'msg': e}, 400

2.2.1 目录扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# main.py 调用 operations/load.py ->do_load 方法
# Get the path to the image
def get_imgs(path):
pics = []
for f in os.listdir(path):
if ((f.endswith(extension) for extension in
['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG']) and not f.startswith('.DS_Store')):
pics.append(os.path.join(path, f))
return pics


# Get the vector of images
def extract_features(img_dir, model):
try:
cache = Cache('./tmp')
feats = []
names = []
img_list = get_imgs(img_dir)
total = len(img_list)
cache['total'] = total
for i, img_path in enumerate(img_list):
try:
norm_feat = model.resnet50_extract_feat(img_path)
feats.append(norm_feat)
names.append(img_path.encode())
cache['current'] = i + 1
print(f"Extracting feature from image No. {i + 1} , {total} images in total")
except Exception as e:
LOGGER.error(f"Error with extracting feature from image {e}")
continue
return feats, names
except Exception as e:
LOGGER.error(f"Error with extracting feature from image {e}")
sys.exit(1)


# Combine the id of the vector and the name of the image into a list
def format_data(ids, names):
data = []
for i in range(len(ids)):
value = (str(ids[i]), names[i])
data.append(value)
return data


# Import vectors to Milvus and data to Mysql respectively
def do_load(table_name, image_dir, model, milvus_client, mysql_cli):
if not table_name:
table_name = DEFAULT_TABLE
vectors, names = extract_features(image_dir, model)
ids = milvus_client.insert(table_name, vectors)
milvus_client.create_index(table_name)
mysql_cli.create_mysql_table(table_name)
mysql_cli.load_data_to_mysql(table_name, format_data(ids, names))
return len(ids)

可以看到核心都在这个 do_load(table_name, image_dir, model, milvus_client, mysql_cli) 方法上

table_name 默认为 mysql 表名 milvus_img_search ,同时也是 milvus 中的集合名

  1. extract_features:图像特征值提取,这里的参数 model 是在 main.py 初始化好的 src/encode.py,其中又调用了 towhee 使用 resnet50 模型
  2. milvus_client.insert:将图片特征值 vectors 存入 milvus 中的 table_name 集合中,并返回记录ID
  3. milvus_client.create_indexmilvus sdk 方法,创建索引
  4. mysql_cli.create_mysql_table:测试 mysql 是否存在表 table_name ,如果不存在则新建。
  5. mysql_cli.load_data_to_mysql:将 milvus 的特征值 ID 和图片地址存入 mysql。

看到这里再看上面的架构图,能大致猜到这个 demo 的运行逻辑

  1. python : 负责特征值提取,通俗来讲就是把图片的特征读取出来,不过这里使用的是 torch 框架个和 resnet50 模型(这个模型文件就有97M)。
  2. milvus:负责存储 python 传过来的图片特征值并返回一个存储ID,这里就体现出 milvus 介绍说的矢量数据库的价值了
  3. mysql:完全是业务功能,将 milvus 返回的特征值ID和图片文件(路径)相关联。

2.2.3 图片搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# main.py
@app.post('/img/search')
async def search_images(image: UploadFile = File(...), topk: int = Form(TOP_K), table_name: str = None):
# Search the upload image in Milvus/MySQL
try:
# Save the upload image to server.
content = await image.read()
print('read pic succ')
img_path = os.path.join(UPLOAD_PATH, image.filename)
with open(img_path, "wb+") as f:
f.write(content)
paths, distances = do_search(table_name, img_path, topk, MODEL, MILVUS_CLI, MYSQL_CLI)
res = dict(zip(paths, distances))
res = sorted(res.items(), key=lambda item: item[1])
LOGGER.info("Successfully searched similar images!")
return res
except Exception as e:
LOGGER.error(e)
return {'status': False, 'msg': e}, 400

main.py 中的上传接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# search.py
def do_search(table_name, img_path, top_k, model, milvus_client, mysql_cli):
try:
if not table_name:
table_name = DEFAULT_TABLE
feat = model.resnet50_extract_feat(img_path)
vectors = milvus_client.search_vectors(table_name, [feat], top_k)
vids = [str(x.id) for x in vectors[0]]
paths = mysql_cli.search_by_milvus_ids(vids, table_name)
distances = [x.distance for x in vectors[0]]
return paths, distances
except Exception as e:
LOGGER.error(f"Error with search : {e}")
sys.exit(1)

调用到 src/operations/search.py 中的 do_search

  1. model.resnet50_extract_feat:使用 resnet50 模型提取图像特征值
  2. milvus_client.search_vectors:提取特征值后在 milvus 做向量计算,实际就是计算向量距离,并返回 top_k 个。这里返回的是匹配到的文件特征值和对应的ID;
  3. mysql_cli.search_by_milvus_ids:拿到ID后从 mysql 中查找这个ID,并找到对应的关联图片(就是图片地址)。

这里体现出 milvus 的另一个重要功能,即相似度计算也就是介绍里说的 similarity search

其实以图搜图简单的说也就是这两个步骤,找到图片的一些特性信息或者说是指纹。与其他图片的指纹进行比较与计算,并返回相似的程度有多少。当然现实中图片获取本身可能就是非常困难的事情。

2.2.3 测试

既然跑起来了就要对个功能先有个大致的感性的理解,于是就拿数据集内部的图和外部的图片做了些测试。

首先拿数据集的图片计算向量距离为 0 ,算是完全匹配,截下来头像距离是 0.7

在看到右下角的 空银子 后我想了个办法

如上图左侧为图库中已有的原图,然后找了图库中不存在的,TV版本,小人儿版,绘画风格A,绘画风格B四张图。

感觉上来说原图能识别的人物特色是头发弧度与颜色,那么应该最佳匹配是塑料小人版,其次是绘画风格B。

可以看出TV版没有匹配到,小人版本向量距离为 0.74 最近。不出意料

图3和图4,让人没想到。从结构上与整个观感上来说绘画风格4应该更接近,但是图三反而距离最近,这但是完全没有意料到的。

之后需要改造 python 端并重新设计数据库,并尝试计算现有图库的特征值,并最终用起来。

3. Milvus

看了官方给的例子,反倒是对 Milvus 本身更感兴趣,于是又回去看了 Milvus 的文档。

官方的文档给出的适应方式实际上就是 Reverse Image Search 的应用方式,首先做数据信息提取,这里一般用深度学习。然后 milvus 本身做的是存储与数据对比与匹配工作。

按照官方描述 bootcamp 项目还包括一个问答系统,一个推荐系统,一个视频相似度搜索,一个音频相似度搜索,分子相似度搜索,一个DNS串行分类,一个文本搜索引擎。

看了这些对 milvus 更感兴趣,对这些子项目是如何运行,以及如何与 milvus 交互更感兴趣了。这些项目与我需要的场景有很高的重合度,希望之后能用到。

4. 最后

图片搜索除了 milvus 外还有几个其他框架,比如基于 Apcahe SolrLIRE 实现,这其中有几个已经运行很久且效果不错的开源项目,在看到 milvus 前我对 LIRE 是更感兴趣的。

然后哪个可以最终在个人有限硬件环境中运行,这才是我需要关注的。