图片模糊搜索项目一阶段项目小结
历时近一个月,经过多次的算法优化,加上工程上的调整,初期版本完成,尤其是元素种类相对清晰的图片搜索结果更能体现效果.
这里从以下几个方面展开表述 : 特征提取jni接口配置 / 图片特征保存 / 相关算法的调整优化 / 科学合理的测试策略 / 大数据集工程实现的思考 .
1.借助shell脚本清洗数据
原始图片共10w张,这里洗特征存入hbase,如果采用传统的hbase的put方法耗时太长,这里将数据存储为符合hbase数据文件格式的文本,借助脚本直接导入hbase集群.
first,提取图片特征到内存,按照图片/特征筒的形式存入两个CacheMap
second,为避免堆栈溢出,设置为1000张为一匝,保存到本地
third,清空缓存,重新进行第一步
terminal , 操作100个图片信息文本 和 100个特征筒信息文本
图片信息文本中,每一行包括路径url的md5 / hbase表存储格式 / 路径src / sift特征 / clf类别特征 / scd颜色特征
这里首先使用 cat 命令 , 100 > 1
cat image* > image_all.txt
实际搜索中,考虑到几种特征字节占比不同,故分开存不同的表, 使用 awk 命令 , 提取对应的列到不同的数据文本
awk '{print $1" "$2" "$3" "$4}' image_all.txt > image_clf.txt
至此即完成图片信息文本的整理,为校验可对比三个数据文本行数是否一致,使用 wc 命令
wc -l image_*
接下来就是最为关键的特征筒的提取,这里有几个问题要明确:
first,每张图片都属于一个筒,(目前的设计,理论总筒数为2^64个)
second,一个筒将包含0-n个图片,且随着量级递增
third,由上可知,最初提取的特征筒对应图片的数据文本只是针对1000张图片的结果,
整合为10w张,需要统一100个文件的所有映射信息,分门别类重新生成,这里我采用的是shell + Java代码的操作
首先,cat命令整合所有特征筒文件
cat hash_* hash_all.txt
其次,排序,为后续Java操作文件做准备
sort -n hash_all.txt -O hash_sorted.txt
经过上述两步,Java代码要做的事情就很清晰了:
a.读排序后的特征筒文件,获取每一行数据
b.新建写文件,保存终版的特征筒信息
c.按照key-特征筒hash,val-图片唯一路径md5,判定当前行是否已存在key,存在则concat后再push,不存在则存入新文件,并除旧建新
d.释放资源,结束
Java代码如下:
File file = new File("/home/nya/image_search/test/hbase_hash.txt"); BufferedReader reader = null; BufferedWriter bw = null; int line = 1; String lineStr; try { reader = new BufferedReader(new FileReader(file)); bw = new BufferedWriter(new FileWriter("/home/nya/image_search/test/hash_test.txt")); Map<String,String> map = new HashMap(); StringBuffer sb = new StringBuffer() ; String split = " "; while (StringUtils.isNotBlank(lineStr = reader.readLine())){ String[] splits = lineStr.split(" "); String key = splits[0]; if (!map.containsKey(key)) { if (map.size() == 0) { map.put(key,"hash"); } else { String lineHa = sb.toString(); log.info("this line str : " + lineHa + " line : " + line); bw.write(sb.toString()); bw.newLine(); bw.flush(); map.clear(); sb = new StringBuffer(); map.put(key,"hash"); } sb.append(key).append(split).append("d"); for (int i = 2 ; i < splits.length ; i++) { sb.append(split).append(splits[i]); } } else { for (int i = 2 ; i < splits.length ; i++) { sb.append(split).append(splits[i]); } } line++; } } catch (Exception e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { log.error("reader io then close error : " , e); } } if (bw != null) { try { bw.close(); } catch (IOException e) { log.error("reader io then close error : " , e); } } }
2.JNI接口的调度与使用
提取图片特征是一个比较耗时的工作,基于INVIDIA的GPU实现调度是一个不错的选择,同时在一些底层逻辑的处理上C++的优势也十分明显,而为了产品接口的复用,转化为JNI接口已完成Java的调度也是必需。
首先指定本地jni路径,完成C++代码的修改,借助cmake完成编译生成动态库,测试无误,开始Java工程方面的配置。注:此处为Ubuntu环境下开发
1、动态库导入系统LD_LIBRARY_PATH路径下
2、项目启动时静态加载动态库,此处可以在具体类中设置,如果纯粹是简单的测试的话。一般情况下,此静态代码块推荐在项目配置类中完成设置。
@Configuration public class CommonConfig { static { String javaLibraryPath = System.getProperty("java.library.path"); System.out.println("java.library.path===" + javaLibraryPath); // opencv lib System.loadLibrary(Core.NATIVE_LIBRARY_NAME); // feature get lib System.loadLibrary(ImageRetrievalConstant.SIFT_DETECT_LIBRARY_NAME); } }
Java加载动态库包括 System.load(absolute path) / System.loadLibrary(so name) 两种方式 , 采用方法一则无需将动态库so导入具体LD_LIBRARY_PATH路径下。
拓展:LD_LIBRARY_PATH vs LIBRARY_PATH
LIBRARY_PATH和LD_LIBRARY_PATH是Linux下的两个环境变量,二者的含义和作用分别如下:
LIBRARY_PATH环境变量用于在程序编译期间查找动态链接库时指定查找共享库的路径。
LD_LIBRARY_PATH环境变量用于在程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径,注意,LD_LIBRARY_PATH中指定的路径会在系统默认路径之前进行查找。
区别与使用:
开发时,设置LIBRARY_PATH,以便gcc能够找到编译时需要的动态链接库。
发布时,设置LD_LIBRARY_PATH,以便程序加载运行时能够自动找到需要的动态链接库。
3、JNI方法的调试
在Java工程中新建匹配对应jni方法完整类路径的类,并将所使用的方法声明为私有静态本地方法。
此时便可以通过main方法完成接口的测试工作,以及相关的封装以适应Java方面调用的习惯。
@Service public class ImageFeaturesExtractor { // 本地私有静态方法 private static native String detectImageClf(long imgAddress,int imgWidth,int imgHeight,int imgChannel,int featureType); // 封装成符合Java调度习惯的方法 public List<String> detectClfToList(Mat bgrImg) throws DetectException {...} // 测试 public static void main(String[] args) { String path = "..."; try { Mat srcImg = Highgui.imread(path,Highgui.CV_LOAD_IMAGE_COLOR) List<String> result = detectClfToList(srcImg); System.out.println(result); } } }
至此,针对JNI接口方法的调度已与传统Spring加载bean中方法的使用无异。其实整个调度过程并不复杂,关键在于本地方法的二次封装,这才是后续可读性、可拓展性的根基。
3.图片特征保存蜕变过程
1、已知的相关数据详情:
clf - key 64位无符号二进制
scd - key 64位无符号二进制
csd - 长度32的一维0-255数组
clf - 长度10的key-value映射
sift - 长度不定 200 - 4000 无序数据集
hash - key 对应的图片结果集,长度不定,随着图片总量递增
2、日常业务实现情况