最近,在一个论坛交流会上, 有嘉宾提出自己运营多年的微信小程序商城经常收到用户反馈:自己在逛街时候发现别人穿的好看的衣服,很难通过关键字定位到具体的商品,如果能拍照定位相关的商品就好了,问目前小程序里面能否实现这样的功能。作为一名软件开发者, 日常网购也有类似的体会。如果能在小程序里集成商品搜索的功能,就能大大提升用户的体验,嘉宾的问题引发我极大的兴趣。
在调研过程中,发现腾讯云图像分析的图像搜索产品可以基于输入图片,智能识别图片中的商品主体,在自建图片库中搜索相同或相似的商品图片,并给出相似度打分。如果输入检索的图片包含服饰类商品,可智能识别上衣、下装、裙装、鞋、包、配饰等多种服饰的类别、颜色以及其他特征属性,实现电商场景下的以图搜图。
接下来 ,将详细分享一下我是如何在小程序里实现商品搜索的。
一、准备工作
1.1明确目标
在小程序里,通过输入商品图片来定位相似的商品图,类似于下面这个:
1.2了解图像搜索
在开始使用之前,还是得对我即将要用的产品进行一个比较详细的了解。
官网文档介绍:
接口文档:
1.3开通图像分析服务
接下来就可以按照官网文档的指引在腾讯云官网开通图像分析服务:
开通服务后 ,创建图片和检索图片分别会发放一万次免费资源包, 可以在资源包管理页面查看使用情况:
二、开发流程
2.1获取个人密钥
在腾讯云官网访问管理页面, 新建个人密钥:
2.2在线调试
下面通过在线调试的方式简单的实现拍立淘的功能。
(1)图库类型选择
首先查看图像搜索的文档,我们选择商品图像搜索的服务类型。
(2)创建图库
创建图片库,指定商品图像搜索, 腾讯云官网提供了在线调用 API Explorer 工具,方便我们可视化调用。
(3)图片入库
创建图片, 将商品图片入库到指定图库中,返回值中包含了主体位置信息。
(4)商品搜索
检索图片, 指定商品图,在图库中进行检索 ,获取相似或者相同的商品图, 返回结果中包含了与输入图类似的商品ID。
这样就基本上演示出了商品搜索的过程,对于使用而言,可以对输入的参数进行一些微调,满足更多的要求。
2.3使用SDK调用
(1)文档介绍
正式接入的话,需要集成腾讯云官网上提供的SDK。 在文档的最下方,提供了多个语言的SDK,可以根据自己熟悉的语言进行接入。
(2)示例
以go语言为例,演示下如何使用。
第一步: 安装基础包:
go get -v -u github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common |
---|
安装图像分析依赖
go get -v -u github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tiia |
---|
第二步: 实现调用,以创建图库为例。
代码语言:javascript复制credential := common.NewCredential(***, ***)
clientProfile := profile.NewClientProfile()
clientProfile.HttpProfile.Endpoint = "tiia.tencentcloudapi.com"
cli, err := tiia.NewClient(credential, regions.Guangzhou, clientProfile)
if err != nil {
panic(err)
}
//实例化一个请求结构
req := tiia.NewCreateGroupRequest()
//对请求参数进行赋值
req.GroupId = common.StringPtr("groupId")
req.GroupName = common.StringPtr("groupName")
req.Brief = common.StringPtr("brief")
req.MaxCapacity = common.Uint64Ptr(uint64(100))
req.MaxQps = common.Uint64Ptr(uint64(10))
req.GroupType = common.Uint64Ptr(uint64(5))
//调用请求
resp, err := cli.CreateGroup(req)
if err != nil {
return
}
三、小程序实现商品搜索
上面介绍了图像搜索的基本能力,如何应用商品搜索能力,来实现拍立淘的效果呢,接下来以小程序为例,来演示一个简单的应用:
3.1构建底库
根据上述文档, 我们在服务端使用sdk来初始化图片库,然后通过小程序端来访问,这里不再赘述。
1. 使用2.1申请的secretID,secretkey来创建图库。
2. 商品图入库。
3.2构建小程序
index.js
代码语言:javascript复制const app = getApp()
Page({
data: {
urls: [],
inputValue: "图片URL",
buttonStatus: false,
category: ['Jacket', 'dress', 'trousers', 'bag', 'shoe', 'accessories'],
motto: 'Hello World',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
canIUseGetUserProfile: false,
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName') // 如需尝试获取用户信息可改为false
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs'
})
},
onLoad() {
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
},
getUserProfile(e) {
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
getUserInfo(e) {
// 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息
console.log(e)
this.setData({
userInfo: e.detail.userInfo,
hasUserInfo: true
})
},
isUrl (url) {
var urlRegExp=/^(?:http(s)?://)?[w.-] (?:.[w.-] ) [w-._~:/?#[]@!$&'* ,;=.] $/;
if(urlRegExp.test(url)){
return true;
}else{
return false;
}
},
imageSearch() {
var that = this
wx.request({
url: 'http://****/search',
data: {
"GroupId": "***",
"ImageUrl": this.data.inputValue
},
method: "POST",
header: {
'Content-Type': "application/json"
},
success (res) {
if (res.data == null) {
wx.showToast({
icon: "error",
title: '请求失败',
})
return
}
console.log(res.data)
that.setData({
urls: res.data.Urls,
object: res.data.Object
})
that.drawInputImage()
},
fail(res) {
wx.showToast({
icon: "error",
title: '请求失败',
})
}
})
},
drawInputImage: function() {
var that = this;
wx.downloadFile({
url: that.data.inputValue,
success: function(res) {
var imagePath = res.tempFilePath
// 绘制
wx.getImageInfo({
src: imagePath,
success: function(res) {
wx.createSelectorQuery()
.select('#input_canvas') // 在 WXML 中填入的 id
.fields({ node: true, size: true })
.exec((r) => {
// Canvas 对象
const canvas = r[0].node
// 渲染上下文
const ctx = canvas.getContext('2d')
// Canvas 画布的实际绘制宽高
const width = r[0].width
const height = r[0].height
// 初始化画布大小
const dpr = wx.getWindowInfo().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 清空画布
ctx.clearRect(0, 0, width, height)
let radio = height / res.height
console.log("radio:", radio)
const img = canvas.createImage()
var x = width / 2 - (res.width * radio / 2)
img.src = imagePath
img.onload = function() {
ctx.drawImage(img, x, 0, res.width * radio, res.height * radio)
var allBox = that.data.object.AllBox
ctx.fillStyle = 'rgba(247, 15, 2, 0.7)';
for (var i in allBox) {
if (allBox[i].Rect.X == that.data.object.Box.Rect.X &&
allBox[i].Rect.Y == that.data.object.Box.Rect.Y) {
continue
}
ctx.fillRect(x (allBox[i].Rect.X * radio),
allBox[i].Rect.Y * radio,
allBox[i].Rect.Width * radio,
allBox[i].Rect.Height * radio);
}
// 绘制主体
ctx.fillStyle = 'rgba(159, 255, 125, 0.7)'; //rgba(0, 0, 200, 0.5)
ctx.fillRect(x (that.data.object.Box.Rect.X * radio),
that.data.object.Box.Rect.Y * radio,
that.data.object.Box.Rect.Width * radio,
that.data.object.Box.Rect.Height * radio);
console.log(that.data.object.AllBox)
// 绘制文字背景
let text = `${that.data.category[that.data.object.CategoryId]} ${that.data.object.Box.Score}`
ctx.fillStyle = '#221329'
ctx.fillRect(x (that.data.object.Box.Rect.X * radio), that.data.object.Box.Rect.Y * radio, that.data.object.Box.Rect.Width * radio, 15)
// 绘制文字
ctx.fillStyle = '#fcfafc'
console.log(that.data.category[that.data.object.CategoryId])
ctx.fillText(text, x (that.data.object.Box.Rect.X * radio), that.data.object.Box.Rect.Y * radio 10)
}
})
}
})
}
})
},
handlerInput(e) {
console.log(e)
this.setData({
inputValue: e.detail.value
})
},
handlerSearch(e) {
console.log(this.data.inputValue)
if (!this.isUrl(this.data.inputValue)) {
console.log("error url: ", this.data.inputValue)
wx.showToast({
icon: "error",
title: '请输入正确url',
})
return
}
this.setData({buttonStatus: true})
this.imageSearch()
this.setData({buttonStatus: false})
},
handlerInputImage(e) {
console.log(e)
}
})
index.wxss:
代码语言:javascript复制/**index.wxss**/
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}
page {
background: rgb(255, 255, 255);
}
.page-section-spacing {
width: 100%;
}
.container {
padding: 0;
}
.button_container {
margin-top: 600rpx;
}
.flex-wrp{
margin-top: 60rpx;
margin: auto;
width: 90%;
overflow: hidden;
}
.form-item {
margin-bottom: 10px;
margin-top: 10px;
width: 90%;
overflow: hidden;
}
.flex-item{
float: left;
width: 300rpx;
height: 400rpx;
font-size: 26rpx;
margin: 10rpx 10rpx 0rpx 20rpx;
overflow: hidden;
}
.flex-item-V{
margin: 0 auto;
width: 300rpx;
height: 200rpx;
}
.input {
width: 75%;
float: left;
height: 36px;
border: 1px solid #ddd;
}
.button {
float: right;
height: 36px;
background: rgb(236, 111, 61);
color: white;
}
.view_line {
width: 96%;
height: 50rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
margin: 0rpx 2% 0rpx 2%;
}
.view_line view {
width: 75%;
height: 2rpx;
background: linear-gradient(to right,#706f70, #706f70);
}
.text_line {
font-size: 25rpx;
color: rgb(66, 66, 66);
margin: 0rpx 2% 0rpx 2%;
}
.image {
width: 100%;
}
.image image {
width: 100%;
background-size: auto 100%;
background-repeat: no-repeat;
}
index.wxml:
代码语言:javascript复制<view class="container">
<div class="form-item">
<input placeholder=" 商品URL" class="input" bindinput="handlerInput" />
<button class="button" loading="{{buttonStatus}}" bindtap="handlerSearch" size="mini">搜索 </button>
</div>
<view class="view_line">
<text class="text_line">输入图片</text>
<view></view>
</view>
<canvas
type="2d"
id="input_canvas"
style="background:rgb(228, 228, 225); width: 360px; height: 240px;"
></canvas>
<view class="view_line">
<text class="text_line">搜索结果</text>
<view></view>
</view>
<view class="page-section-spacing">
<view class="flex-wrp" style="flex-direction:row;">
<view wx:for="{{urls}}" class="flex-item">
<view class="image">
<image mode="aspectFit" style="background-color: #eeeeee;" src="{{item}}"></image>
</view>
</view>
</view>
</view>
</view>
编译即可:
3.3构建运行
输入商品图,图搜服务会进行主体识别,然后再进行相似图搜索,可以看到效果如下:
如果不做主体识别,可能搜不到我们想要的结果,例如,我们想要这个墨镜,但是不开主体识别的话可能会定位到衣服,导致没有结果:
看下图搜主体识别的文档介绍:
118px使用商品图像搜索默认会开启主体识别,涉及到以下两个参数:
默认情况下主体识别是打开的,服务会针对输入图中的服饰进行主体位置识别,以实现更好的搜索效果,如下:
按照上述主体识别的描述,指定下配饰类目,服务会定位到墨镜的位置,就可以搜索到预期的商品了:
到这一步就实现了小程序中商品搜索的基本功能,涉及到商品搜索的场景,都可以参考下。后面给朋友展示了小程序demo, 体验下来,使用效果相当满意。
3.4查看调用量
在后续观察中,可以在腾讯云官网, 进入到图像分析控制台,可以查看最近的调用情况。
了解更多图像搜索功能:https://cloud.tencent.com/product/imagesearch