机器视觉:十亿规模的深度描述子如何有效索引

来源:互联网 发布:淘宝关键词组合工具 编辑:程序博客网 时间:2024/06/14 16:04

前一段时间山世光老师开源了SeetaFaceEngine,而在小白菜读研的时候,除了做图像检索主要的工作外,也看了一些人脸识别方面的文章,并有过相应的一些笔记Deep Face Recognition论文阅读,所以这次自然也吸引了小白菜测试的兴趣。作为最早一批测试的用户,当时并没有相关资料说明如何在Mac下让它work起来,所以,为了能让它在Xcode中运行,小白菜在SeetaFaceEngine的基础上,对它做了一些小的改动,使得它能够在Xcode里运行起来。当然,在Xcode里面做完了测试后,小白菜在此基础上继续推荐了一步,为它创建了一个QT项目,想在此基础上为它做一个简单的人脸识别和人脸检索的小应用。

人脸识别和人脸检索应用代码:SeetaFaceLib,目前代码还在不断迭代中,已经完成的部分是人脸检索。整个小应用的界面是下面这个样子:drawing

在QT项目刚上传到github上传不久后,远在锐捷的罗兄说这个QT工程对他帮助很大,这个反馈让小白菜深感欣慰。所谓开源,有人用或对人有帮助,这个代码写得就有它的价值了。

回到本篇博客的主题,即我们如何为大规模的描述子建立高效的索引,使得在进行查询的时候,检索系统能够快速地对我们的查询做出响应。对这个问题的探讨,小白菜在之前的文章图像检索:基于内容的图像检索技术以及哈希的文章中做过一些探讨与总结,而人脸检索作为图像检索大范畴的一类具体应用,除了在特征表达上有其自身的特点外,其他过程基本一样。所以在人脸检索中,我们也会碰到图像检索中的3座大山,即:

  • 图像数据量大
  • 特征维度高
  • 要求响应速度快

前两座大山具有相互关联性,涉及到的主题是特征表达的问题,而后一个表象涉及到的是信息检索领域的索引问题。任何有关图像检索方面的问题,几乎都是围绕着这两个核心问题而展开的。对于在这两个方面而展开的工作,暂且不表。我们还是回到人脸检索这个具体的应用中,来谈谈深度描述子的索引问题。

刚开始在建立这个人脸检索QT工程的时候,小白菜并没有做很多复杂的考虑,比如要考虑人脸图像规模,当时建立这个QT工程之初的动机非常简单,就是OpenCV对于检索结果的展示实在是太不方便了,而且小白菜也不需要很大规模的图像体量,另外最重要的一点是,小白菜急切地想看到检索可视化的结果,所以直接用brute-force search的方式应该还ok。但是在做完后,小白菜发现在搜索阶段,搜索不够实时,在小白菜的本机上,3000张的人脸图片,在查询的时候,会出现卡顿的情况,具体表现可以见下图:drawing

从上图可以看到,在查询的时候,会出现较长时间的卡顿现象,于是小白菜猜测,应该是线性扫描耗时过长造成的卡顿,因为在查询触发的时候,在第一次查询开启之时会读取图库特征,以后便一直保存在内存中,也就是在后面的查询中,不会再有读取图库特征这一步了。所以,对于后面的查询,时间主要消耗纯粹的相似度计算以及排序上。具体地,会先计算查询图像(在线提取查询图像的特征)的特征,然后挨个计算查询图像到图像库(特征已离线提取)中特征的余弦距离,即:

// Calculate cosine distance between query and data base facesstd::vector<std::pair<float, size_t> > dists_idxs;int i = 0;for(auto featItem: namesFeats.second){    // http://stackoverflow.com/questions/2923272/how-to-convert-vector-to-array-c    float tmp_cosine_dist = face_recognizer->CalcSimilarity(query_feat, &featItem[0]);    dists_idxs.push_back(std::make_pair(tmp_cosine_dist, i++));}

在上面计算完查询特征到图库各个特征的余弦距离后,会再对计算的距离进行全局排序:

std::sort(dists_idxs.begin(), dists_idxs.end());std::reverse(dists_idxs.begin(), dists_idxs.end());for (size_t i = 0 ; i != dists_idxs.size() ; i++) {   //qDebug()<<dists_idxs[i].first<<namesFeats.first.at(dists_idxs[i].second).c_str();   QString tmpImgName = dir + '/' + namesFeats.first.at(dists_idxs[i].second).c_str();   imgs_listeWidget->addItem(new QListWidgetItem(QIcon(tmpImgName), QString::fromStdString(namesFeats.first.at(dists_idxs[i].second).c_str())));}

为了进一步确认,还可以对执行的时间进行测试确认。从上面的过程可以看到,这种brute-search的方式实在是太耗时了,那么有没有方式能够缓解这种相应不够实时的问题呢。答案是有的,我们可以通过以下手段对这种brute-force search低效的方式做一些缓解:

  • PCA降维。对于通过深度学习方式得到的特征,降维不会对信息造成大的损失,事实上在Neural codes for image retrieval这篇文章中已经指出了PCA对于深度描述子几乎不会造成损失,小白菜之前也对这样一个结论做过验证,见下图:drawing

  • 使用多线程技术。在计算余弦距离的时候,使用OpenMP多线程技术,这种方式能够较大幅度的降低搜索时间,比如CPU是8线程的,则可以将原来的时间降低为原来的8分之一。

上所列举的两种方式,能够缓解brute-force search的低效,但并不能从根本上解决索引的低效问题。因此,为了适应大规模描述子的索引,需要寻求一种合理的索引结构。目前,在近似最近邻搜索(ANN, Approximate Nearest Neighbors)方面,主要存在的方法有:

  • 树结构索引方式。最经典的代表就是KD树了,小白菜用过的KD树的场合有:SIFT匹配、BoW模型。
  • PQ量化以及以PQ量化进行演变的改进方法。对向量进行切分,单独做聚类进行编码。

KD树和PQ量化方式用图示意表述如下:对于KD树和PQ量化方法的优缺点,可以参阅小白菜的博文图像检索:基于内容的图像检索技术。

  • 基于哈希编码编码的方式。这类方法在小白菜读书的时候,为其主要研究方向。采用哈希进行图像检索的主要思想可以用下面这种图进行示意

更多关于哈希方法的介绍和总结,可以参阅小白菜以前对哈希方法写的一些博文。另外,关于ANN的benchmark,可以阅读开源代码ann-benchmarks,小白菜觉得这个非常棒,果断star吖。

因而,针对上面的人脸检索,为了构建一个比较高效的索引,小白菜选用了哈希方法。当然,除了哈希方法,还可以选用PQ方法,不推荐使用KD树方法。说一下选用哈希方法的理由:看到了篇文章Practical and Optimal LSH for Angular Distance,2015年NIPS的文章,对应的开源库是Falconn,Cross-polytope LSH把LSH优化到了极致(个人读完论文的结论),此外,自己对哈希方面很熟悉,用起来应该比较顺手。其实刚开始的时候,并没有打算用LSH的,是打算用Efficient Indexing of Billion-Scale Datasets of Deep Descriptors的,正如你所看到的,连博文的标题都是为其而准备的,这篇论文小白菜详细的读过,虽然现在用的是LSH,但后面还可以继续把对应的代码读一遍。在当前这个人间检索小应用中,LSH的调用代码如下:

if(namesFeats.first.empty()){    featExtractor->loadFeaturesFilePair(namesFeats, path_namesFeats);    qDebug()<<"first loaded";        // LSH搜索方案    int numFeats = (int)namesFeats.first.size();    int dim = (int)namesFeats.second[0].size();        // Data set parameters    uint64_t seed = 119417657;        // Common LSH parameters    int num_tables = 8;    int num_setup_threads = 0;    StorageHashTable storage_hash_table = StorageHashTable::FlatHashTable;    DistanceFunction distance_function = DistanceFunction::NegativeInnerProduct;        // 转换数据类型    for (int ii = 0; ii < numFeats; ++ii) {        falconn::DenseVector<float> v = Eigen::VectorXf::Map(&namesFeats.second[ii][0], dim);        v.normalize(); // L2归一化        data.push_back(v);    }        // Cross polytope hashing    params_cp.dimension = dim;    params_cp.lsh_family = LSHFamily::CrossPolytope;    params_cp.distance_function = distance_function;    params_cp.storage_hash_table = storage_hash_table;    params_cp.k = 2; // 每个哈希表的哈希函数数目    params_cp.l = num_tables; // 哈希表数目    params_cp.last_cp_dimension = 2;    params_cp.num_rotations = 2;    params_cp.num_setup_threads = num_setup_threads;    params_cp.seed = seed ^ 833840234;        }cptable = unique_ptr<falconn::LSHNearestNeighborTable<falconn::DenseVector<float>>>(std::move(construct_table<falconn::DenseVector<float>>(data, params_cp)));cptable->set_num_probes(896);qDebug() << "index build finished ...";cptable->find_k_nearest_neighbors(q, 20, &idxCandidate);

上面代码还有很大的速度方面的优化空间,anyway,我们先暂时抛开代码的优化,来看一下改用LSH索引后的效果,下面是改用LSH后查询响应的效果:drawing从展示的效果来看,对于查询相应的速度,有了较大的改善,但是我们还是可以看到在查询的时候,有一段空白的白板,这个问题小白菜的猜测是它应该不是由于索引慢而导致的,对于这个短时的空白白板的优化,小白菜把它放到下期进行,同时,也会对Efficient Indexing of Billion-Scale Datasets of Deep Descriptors这篇文章做一个理解总结。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 脚瘦穿高跟鞋撑不起来怎么办 银川市阅海幼儿园进不去怎么办 考编专业不对口怎么办 北京55中国际部怎么办 初中数学没学好高中怎么办 靴子大了一码怎么办 靴子买大了一码怎么办 马丁靴大了一码怎么办 社保掌上通登录密码忘记怎么办 录微课时忘词怎么办 微课掌上通看不到信息怎么办 五年级学生上课很吵新老师怎么办 跟财务老师吵起来怎么办 qq把微信冻结了怎么办 微信给封号了怎么办 微信久了没登录冻结了怎么办 换手机号了微店怎么办 ai文件置入后都是字怎么办 excel加载项被禁用了怎么办 被期刊网骗了怎么办 发表的文章不想被收录怎么办? 农村长说的眼睛害了怎么办 普通党员不认同领导的决定怎么办 大学读不下去了怎么办 教师因病长期不能上班怎么办 长按win键黑屏了怎么办 巡视组巡视出问题后续怎么办 货运资格证两次没继续教育怎么办 电子注册备案表学信网查不到怎么办 学信网学籍档案没照片怎么办 信访局不给答复怎么办 发票跳了一个号怎么办 报税用的ca证书怎么办 报税u盘丢了怎么办 地税ca证书丢了怎么办 深圳ca证书丢了怎么办 武汉国税报税证书过期怎么办 江苏大学专业选修课挂了怎么办 电信翼企享福卡怎么办 教育部学籍在线验证报告过期怎么办 身份证被别人注册了学信网怎么办