内容较多、请先马后看;借助es分布式计算的能力,使得早期易企秀APP端图片搜索功能就具备了高可用、可扩展的能力
1、背景
易企秀商场为我们提供了大量免付费的模板,这些模板多以固定的图片及样式组合而成,用户在这个基础上稍加修改便可以快速实现自己的H5场景,为了满足小白用户能够快速制作H5场景的需求,方便用户能够从海量商城作品中快速找到符合自己使用的风格模板,为此产品上提供了通过文本搜索快速获取样例商品的途径,也提供了基于图片搜索样例商品的功能,做图片搜索的目的是为了拓展用户获取商品的途径,同时也满足了用户基于图片风格样式获取商品的诉求。
以下内容进入实战,项目来自易企秀一线工程师操刀实践,干货满满
2、流程介绍
业务处理流程相对比较简单,这里就不放架构图了,整个项目中用到了sqoop、hive、spark、elasticsearch等大数据组件,步骤如下:
1、商品模板主要来自设计师、秀客以及运营精选,每个小时都有大量新增商品入库,我们通过sqoop实现商品数据增量同步到数据仓库(hive),主要包括商品库中的商品封面图、标题、描述、Id等信息
2、借助spark分布式计算的能力快速清洗并抽取图片特征
3、将抽取后的特征与商品模板建立对应关系,并存储到es
4、编写查询script脚本,用于计算用户输入图片与候选集的相似度。
3、具体操作
- ETL
通过sqoop实现增量数据同步非常简单,需要指定一个用于监控增量变化的字段:
sqoop job --create jobname -- import --connect jdbc:mysql://host:3306/mall --username 'bigdata' --password pwd
--table mysqlablename --hive-import --hive-table hivetablename
--incremental lastmodified --check-column create_time --last-value '2019-04-22 13:00:00'
以下几点需要注意:
1、不能在sqoop job中指定-m参数,指定了-m参数会在数据迁移过程中产生临时数据文件,下次导入时会报数据目录已存在的错误;
2、因为我们执行的是增量操作,所以需要提前在hive中创建hivetablename对应的数据表;
3、增量同步需将incremental配置为lastmodified,并在第一次导入数据时设置--last-value为数据下届,每次sqoop会同步大于该下届的数据并自动更新该下届值;
- 特征提取
图片特征提取是本项目的核心模块之一,由于图片特征提取方式较多,通过调研这里我们先对几种常用的传统特征提取算法做简要说明:
算法 | 描述 | 应用场景 |
---|---|---|
颜色直方图 | 提取图片中各种颜色的分布数据,对图片翻转、缩放、模糊处理后的特征影响比较小 | 自然环境、色彩风格 |
颜色向量 | 在颜色直方图基础上增加了色彩空间分布特征的提取 | - |
文理特征 | 提取图片中颜色渐变与物体纹理数据特征 | 物体分类、图像搜索 |
形状特征 | 提取图片中物体轮廓特征与区域形状特征 | 物体分类 |
SIFT | 通过复杂的数据公式实现物体局部特征提取,具有平移、旋转、光照不变性 | 物体识别、图像检测 |
SURF | 采用了SIFT相近的实现原理,但计算复杂度降低很多 | - |
在实际操作后我们选用了颜色灰度直方图算法,以下是相关代码,原生jdk代码实现,没有第三方依赖,直接拷贝可运行(需要全部工程代码的请留下你的邮箱):
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.Base64;
import javax.imageio.ImageIO;
public class Hog extends FeatureSelect {
private static int GRAYBIT = 2; //GRAYBIT=4;用12位的int表示灰度值,前4位表示red,中间4们表示green,后面4位表示blue
/**
* 求三维的灰度直方图
* @throws IOException
* @throws MalformedURLException
*/
public static void main(String[] args) {
/*double[] data5 = getHistgram2("http://pic15.nipic.com/20110713/2328079_172740212177_2.jpg");
ImageVector.print(data5);
double[] data1 = getHistgram2("http://imgup01.sj88.com/2018-07/04/09/15306691026479_3.jpg");
ImageVector.print(data1);*/
double[] data2 = getHistgram2("http://res.eqh5.com/o_1cjacked6nsv1m4du77esr1mr4u.jpg");
print(data2);
// double[] data3 = getHistgram2("http://res.eqh5.com/o_1cgqee47bfb966fmf8j472559.jpg");
// ImageVector.print(data3);
// double[] data4 = getHistgram2("http://res.eqh5.com/o_1ci40kmlv1c7b16ob1imfk961kjae.png");
// print(data4);
// double[] data6 = getHistgram2("http://res.eqh5.com/o_1ci40kmlv1c7b16ob1imfk961kjae.png");
// print(data6);
}
public static void print(double[] data){
StringBuffer sb = new StringBuffer();
StringBuffer sb2 = new StringBuffer();
for(int i=0; i<data.length; i++){
sb.append(i+"|"+data[i]+" ");
sb2.append( Double.valueOf(data[i])+",");
}
// System.out.println(sb);
System.out.println(sb2);
System.out.println( convertArrayToBase64(data));
}
public static final String convertArrayToBase64(double[] array) {
final int capacity = 8 * array.length;
final ByteBuffer bb = ByteBuffer.allocate(capacity);
for (int i = 0; i < array.length; i++) {
bb.putDouble(array[i]);
}
bb.rewind();
final ByteBuffer encodedBB = Base64.getEncoder().encode(bb);
return new String(encodedBB.array());
}
private static BufferedImage readImg(String url) {
try {
return ImageIO.read(new URL(url).openStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static double[][] getHistgram(String srcPath) {
BufferedImage img = readImg(srcPath);
return getHistogram(img);
}
/**
* hist[0][]red的直方图,hist[1][]green的直方图,hist[2][]blue的直方图
* @param img 要获取直方图的图像
* @return 返回r,g,b的三维直方图
*/
public static double[][] getHistogram(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
double[][] hist = new double[3][256];
int r, g, b;
int pix[] = new int[w*h];
pix = img.getRGB(0, 0, w, h, pix, 0, w);
for(int i=0; i<w*h; i++) {
r = pix[i]>>16 & 0xff;
g = pix[i]>>8 & 0xff;
b = pix[i] & 0xff;
/*hr[r] ++;
hg[g] ++;
hb[b] ++;*/
hist[0][r] ++;
hist[1][g] ++;
hist[2][b] ++;
}
for(int j=0; j<256; j++) {
for(int i=0; i<3; i++) {
hist[i][j] = hist[i][j]/(w*h);
//System.out.println(hist[i][j] + " ");
}
}
return hist;
}
/**
* 求一维的灰度直方图
* @param srcPath
* @return
*/
public static double[] getHistgram2(String srcPath) {
BufferedImage img = readImg(srcPath);
return getHistogram2(img);
}
/**
* 求一维的灰度直方图
* @param img
* @return
*/
public static double[] getHistogram2(BufferedImage img) {
int w = img.getWidth();
int h = img.getHeight();
int series = (int) Math.pow(2, GRAYBIT); //GRAYBIT=4;用12位的int表示灰度值,前4位表示red,中间4们表示green,后面4位表示blue
int greyScope = 256/series;
double[] hist = new double[series*series*series];
int r, g, b, index;
int pix[] = new int[w*h];
pix = img.getRGB(0, 0, w, h, pix, 0, w);
for(int i=0; i<w*h; i++) {
r = pix[i]>>16 & 0xff;
r = r/greyScope;
g = pix[i]>>8 & 0xff;
g = g/greyScope;
b = pix[i] & 0xff;
b = b/greyScope;
index = r<<(2*GRAYBIT) | g<<GRAYBIT | b;
hist[index] ++;
}
for(int i=0; i<hist.length; i++) {
hist[i] = hist[i]/(w*h);
//System.out.println(hist[i] + " ");
}
return hist;
}
}
- 特征存储
首先在mapping中定义存储特征field
"features": {
"type": "binary",
"doc_values": true
}
其次借助spark的并行计算能力,每小时增量读取hive表中新增商品的数据,对封面图进行特征提取,并将提取后的特征字段连同其它属性值一并存入ES,由于features存储的是binary类型,数据需要转化为base64字符串进行存储,所以spark中主要代码是:
String b64 = Hog.convertArrayToBase64(Hog.getHistgram2( imgUrl ));
- 图片检索
和构建索引库的方式一样,我们在检索前也需要对图片进行特征提取,但这次提取后的特征不需要进行base64转化,以下是query的核心语句:
{
"query": {
"function_score": {
"boost_mode": "replace",
"script_score": {
"script": {
"inline": "binary_vector_score",
"lang": "knn",
"params": {
"cosine": true,
"field": "features",
"vector": [
-0.09217305481433868, 0.010635560378432274, -0.02878434956073761, 0.06988169997930527, 0.1273992955684662, -0.023723633959889412, 0.05490724742412567, -0.12124507874250412, -0.023694118484854698
}
}
}
}
}
如果你觉得上述查询返回的结果相关度不高或者响应很慢,也可以重写query增加过滤条件,以限制参与计算的数据范围。
需要注意的是es5.6中并不原生支持cosine等计算相似度的函数,开始执行上述query之前,我们要先安装一个script脚本,在这里下载
4、小结
上述工程虽然实现了图片与文本相结合搜索功能,但检索效果和性能并不是很出色,可优化的空间还有很多,比如特征提取部分可以尝试使用深度学习模型,通过卷积神经网络提取的特征可能效果会更好,另外新版ES7.0支持了vector数据类型(图片数据存储为该类型更合适),并且内部实现了基于vector的余弦相似度计算,切换到新版本实现性能应该也会好很多。
网友评论