最佳实践|用腾讯云AI图像搜索打造属于自己的拍立淘

2022-09-08 14:57:32 浏览数 (2)

最近,在一个论坛交流会上, 有嘉宾提出自己运营多年的微信小程序商城经常收到用户反馈:自己在逛街时候发现别人穿的好看的衣服,很难通过关键字定位到具体的商品,如果能拍照定位相关的商品就好了,问目前小程序里面能否实现这样的功能。作为一名软件开发者, 日常网购也有类似的体会。如果能在小程序里集成商品搜索的功能,就能大大提升用户的体验,嘉宾的问题引发我极大的兴趣。

在调研过程中,发现腾讯云图像分析的图像搜索产品可以基于输入图片,智能识别图片中的商品主体,在自建图片库中搜索相同或相似的商品图片,并给出相似度打分。如果输入检索的图片包含服饰类商品,可智能识别上衣、下装、裙装、鞋、包、配饰等多种服饰的类别、颜色以及其他特征属性,实现电商场景下的以图搜图。

接下来 ,将详细分享一下我是如何在小程序里实现商品搜索的。

一、准备工作

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

0 人点赞