綜述
之前我們有講到在做一些深度學習圖像算法開發(fā)時,為了更好的將算法效果展示出來,經(jīng)常需要開發(fā)一些演示Demo應用,使用在線C/S交互式應用開發(fā)(類似于百度、騰訊、阿里的開放平臺)。C代表Client(客戶端),S代表Server(服務端),也就是UI處理與底層處理分離的方式,兩端的連接交互媒介就是Http網(wǎng)絡請求。像這里的算法演示在C端主要實現(xiàn)界面顯示、交互、發(fā)送請求與圖像繪制,S端主要是實現(xiàn)請求接收與返回、算法推理。C端主要包含移動端App/網(wǎng)頁/小程序,S端就是后臺算法運行服務器,本文主要介紹C端應用實例(微信小程序人臉識別開放平臺)的開發(fā)。
條件準備
前面既然提到是C/S開放,那么這兩者是缺一不可的。C就是我們本文要講的,S代表后端服務,在這里主要實現(xiàn)的是人臉檢測/五官定位/比對算法http請求接收接口,用以接收處理前端發(fā)來的算法請求。
服務器
請求地址:局域網(wǎng)/廣域網(wǎng)
請求協(xié)議(可以根據(jù)需要自行定義)
以下是我自己定義的協(xié)議:
人臉檢測請求協(xié)議
Request:
{
"command": "detection",// 算法類型
"image": imageBase64Str,// 待檢測圖片base64字符串碼
"max_face_num": 1,// 最大檢測人臉數(shù)
"image_url": "",// 待檢測圖片URL
"face_attribute": 1,// 是否進行人臉屬性檢測
"face_quality": 1// 是否進行人臉質(zhì)量檢測
}
Response:
{
"face_num": 1,//人臉個數(shù)
"face_list": [
{
"location": {//人臉位置
"x1": 132,
"y1": 305,
"x2": 259,
"y2": 493
},
"score": 0.999789,
"attributes": {//人臉屬性
"mask": 0,//是否戴口罩
"eyeopen": 1,//是否睜眼
"expression": 0,//是否正常表情
"yaw": 5,//偏轉(zhuǎn)角
"pitch": 6,//俯仰角
"roll": 7//翻滾角
},
"quality": {
"brightness": 0,//亮度是否合格
"blur": 0,//清晰度是否合格
"occlusion": {//人臉7區(qū)域遮擋情況
"left_eye": 1,
"right_eye": 1,
"nose": 1,
"mouth": 1,
"left_cheek": 1,
"right_cheek": 1,
"chin": 1
}
}
}
],
"request_id": "123456" //請求ID
}
人臉五官定位請求協(xié)議
Request:
{
"command": "landmark",// 算法類型
"image": image base64,// 待檢測圖片base64字符串碼
"image_url": "",// 待檢測圖片URL
"max_face": 1// 最大檢測人臉數(shù)
}
Response
{
"face_num": 1,//人臉個數(shù)
"face_list": [
{
"landmark": [//人臉關(guān)鍵點坐標位置
{
"x": 50,
"y": 108
},
{
"x": 49,
"y": 114
},
......
{
"x": 71,
"y": 111
},
{
"x": 109,
"y": 110
}
]
}
],
"request_id": "123456"http://請求ID
}
人臉比對請求協(xié)議
Request:
{
"command": "compare",// 算法類型
"image1": image base64,// 待檢測圖片1 base64字符串碼
"image2": image base64,// 待檢測圖片2 base64字符串碼
"image_url1": "",// 待檢測圖片1 URL
"image_url2": "",// 待檢測圖片2 URL
"quality_control": 0// 是否進行人臉質(zhì)量控制
}
Response:
{
"score": 99,//相似度分數(shù)(0~100)
"request_id": "123456",//請求ID
"error": 0 //錯誤碼
}
以上三個條件可以自己搭建,也可以購買第三方的服務。
開發(fā)所用關(guān)鍵技術(shù)(接口)
圖片文件選擇與數(shù)據(jù)獲取
我們在訪問開放平臺時經(jīng)常需要找一些自己的圖片進行測試,所以就需要這樣一個方法來實現(xiàn)
如下代碼:
wx.chooseImage({
success: function(csi) {
//獲取圖片緩存地址
var tempFilePaths = csi.tempFilePaths
imageUrl = tempFilePaths[0]
//獲取圖片信息
wx.getImageInfo({
src: imageUrl,
success (gii) {
//根據(jù)圖片拍攝方向獲取圖片的寬高
if(gii.orientation=="up" || gii.orientation=="down" || gii.orientation=="up-mirrored" || gii.orientation=="down-mirrored"){
imageWidth = gii.width
imageHeight = gii.height
}else{
imageWidth = gii.height
imageHeight = gii.width
}
//獲取圖片Base64碼,一般網(wǎng)絡請求發(fā)送的圖片數(shù)據(jù)均是該格式數(shù)據(jù)
let imageBase64 = wx.getFileSystemManager().readFileSync(imageUrl, "base64")
}
})
}
})
圖片算法處理網(wǎng)絡請求
在獲取到圖片數(shù)據(jù)后,就需要將圖片數(shù)據(jù)發(fā)送到后端進行算法處理請求來獲取算法運行結(jié)果
如下代碼:
wx.request({
url: 'http://123.45.67.89:8080/face', //局域網(wǎng)接口地址(也可以是廣域網(wǎng)/https地址)
data: {
"command": "detect",// 算法類型
"image": imageBase64Str,// 待檢測圖片base64字符串碼
"max_face_num": 1,// 最大檢測人臉數(shù)
"image_url": "",// 待檢測圖片URL
"face_attribute": 1,// 是否進行人臉屬性檢測
"face_quality": 1// 是否進行人臉質(zhì)量檢測
},
header: {
'content-type': 'application/json' // 請求頭
},
method: "POST",// 請求方式
timeout: 5000,// 請求超時
dataType: "json",
success (response) {
// 請求成功處理
// 打印返回結(jié)果
console.log("face detect success:\n" + JSON.stringify(response, null ,2))
......
},
fail(res) {
// 請求失敗處理
// 打印返回結(jié)果
console.log("face detect fail:\n" + JSON.stringify(res, null ,2))
//請求失敗提示
wx.showToast({
title: '請求失敗:' + res.errMsg,
icon: 'none',
duration: 2000
})
},
complete(res) {
// 請求完成處理
console.log("face detect complete.")
}
})
圖片算法處理結(jié)果人臉框繪制
我們在拿到請求結(jié)果以后如果只是打印reponse數(shù)據(jù)并不能直觀感受算法處理效果,這時如果能將結(jié)果通過圖像繪制將人臉位置顯示出來將是不錯的處理方式
如下代碼:
wx.createSelectorQuery()
.select('#canvas_preview')//顯示控件
.fields({ node: true, size: true })
.exec((scvs) => {
// 獲取控件像素尺寸
const dom = scvs[0]
const canvas = dom.node
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = dom.width * dpr
canvas.height = dom.height * dpr
ctx.scale(dpr, dpr)
const canvasMaxWidth = dom.width
const canvasMaxHeight = dom.height
// 獲取圖像縮放比例,保證圖片在控件內(nèi)完全顯示
const widthAdjustPara = canvasMaxWidth / imageWidth
const heightAdjustPara = canvasMaxHeight / imageHeight
var currentAdjustPara = 1
if(widthAdjustPara<1 || heightAdjustPara<1) {
currentAdjustPara = (widthAdjustPara <= heightAdjustPara ? widthAdjustPara : heightAdjustPara)
}
const canvasCurrentWidth = (currentAdjustPara===1 ? imageWidth : (imageWidth * currentAdjustPara))
const canvasCurrentHeight = (currentAdjustPara===1 ? imageHeight : (imageHeight * currentAdjustPara))
// 在控件內(nèi)繪制原圖
var img = canvas.createImage()
img.src = imageUrl
img.onload = function () {
// 居中繪制
ctx.drawImage(
img,
(canvasMaxWidth-canvasCurrentWidth)/2 + 1, // image x start
(canvasMaxHeight-canvasCurrentHeight)/2 + 1, // image y start
canvasCurrentWidth, canvasCurrentHeight // image dst w, h
)
// 檢測到人臉繪制人臉框(從返回數(shù)據(jù)reponseData中獲取人臉框位置信息)
if(isDetected){
for (var i = 0; i < reponseData.data.face_num; i++) {
var x1 = reponseData.data.face_list[i].location.x1
var y1 = reponseData.data.face_list[i].location.y1
var x2 = reponseData.data.face_list[i].location.x2
var y2 = reponseData.data.face_list[i].location.y2
ctx.strokeStyle = '#00e5ff' // 繪制線顏色
// 矩形繪制
ctx.rect(
(canvasMaxWidth-canvasCurrentWidth)/2 + x1 * currentAdjustPara, // rect left value
(canvasMaxHeight-canvasCurrentHeight)/2 + y1 * currentAdjustPara, // rect top value
(x2-x1) * currentAdjustPara, (y2-y1) * currentAdjustPara // rect w, h
)
}
ctx.stroke()
}
}
})
圖片算法處理結(jié)果人臉關(guān)鍵點繪制
我們在拿到請求結(jié)果以后如果只是打印reponse數(shù)據(jù)并不能直觀感受算法處理效果,這時如果能將結(jié)果通過圖像繪制將人臉關(guān)鍵點位置顯示出來將是不錯的處理方式
如下代碼:
wx.createSelectorQuery()
.select('#canvas_preview')//顯示控件
.fields({ node: true, size: true })
.exec((scvs) => {
// 獲取控件像素尺寸
const dom = scvs[0]
const canvas = dom.node
const ctx = canvas.getContext('2d')
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = dom.width * dpr
canvas.height = dom.height * dpr
ctx.scale(dpr, dpr)
const canvasMaxWidth = dom.width
const canvasMaxHeight = dom.height
// 獲取圖像縮放比例,保證圖片在控件內(nèi)完全顯示
const widthAdjustPara = canvasMaxWidth / imageWidth
const heightAdjustPara = canvasMaxHeight / imageHeight
var currentAdjustPara = 1
if(widthAdjustPara<1 || heightAdjustPara<1) {
currentAdjustPara = (widthAdjustPara <= heightAdjustPara ? widthAdjustPara : heightAdjustPara)
}
const canvasCurrentWidth = (currentAdjustPara===1 ? imageWidth : (imageWidth * currentAdjustPara))
const canvasCurrentHeight = (currentAdjustPara===1 ? imageHeight : (imageHeight * currentAdjustPara))
// 在控件內(nèi)繪制原圖
var img = canvas.createImage()
img.src = imageUrl
img.onload = function () {
ctx.drawImage(
img,
(canvasMaxWidth-canvasCurrentWidth)/2 + 1, // image x start
(canvasMaxHeight-canvasCurrentHeight)/2 + 1, // image y start
canvasCurrentWidth, canvasCurrentHeight // image dst w, h
)
// 檢測到人臉繪制人臉關(guān)鍵點(從返回數(shù)據(jù)reponseData中獲取人臉關(guān)鍵點位置信息)
if(isDetected){
var dot_radius = 1 //繪制點半徑
for (var i = 0; i < reponseData.data.face_num; i++) {
var pointNum = reponseData.data.face_list[i].landmark.length
for (var j = 0; j < pointNum; j++) {
var x = reponseData.data.face_list[i].landmark[j].x
var y = reponseData.data.face_list[i].landmark[j].y
ctx.beginPath()
ctx.fillStyle = "#00FFFF" // 繪制點顏色
// 圓點繪制
ctx.arc(
(canvasMaxWidth-canvasCurrentWidth)/2 + x * currentAdjustPara,
(canvasMaxHeight-canvasCurrentHeight)/2 + y * currentAdjustPara,
dot_radius, 0, 2 * Math.PI, true
)
ctx.closePath()
ctx.fill()
}
}
}
}
})
圖片算法處理結(jié)果人臉屬性列表顯示
我們在拿到請求結(jié)果以后如果只是打印reponse數(shù)據(jù)會顯得比較亂,如果能通過列表整理的方式顯示出來將是不錯的選擇
如下代碼:
index.js
Page({
data: {
systemInfo: {}, // 界面信息
isHiddenTable: false, //是否隱藏檢測結(jié)果列表
scrollToView: "0", //滑動控件位置
resultListData:[], // 人臉檢測結(jié)果列表
},
......
)
//拿到響應結(jié)果后的處理
if(data.data.face_num===1){ //單人臉情況
that.setData({
userInfo:{
tips:'單人臉檢測結(jié)果'
},
isHiddenTable: false,//是否隱藏結(jié)果列表
scrollToView: "單人臉檢測結(jié)果",
resultListData:[
{"attribute":"坐標","valuetext":data.data.face_list[0].location.x1 + " "
+ data.data.face_list[0].location.y1 + " "
+ data.data.face_list[0].location.x2 + " "
+ data.data.face_list[0].location.y2
},
{"attribute":"口罩","valuetext":data.data.face_list[0].attributes.mask===0 ? "無" : "有"},
{"attribute":"睜眼","valuetext":data.data.face_list[0].attributes.eyeopen===0 ? "否" : "是"},
{"attribute":"表情","valuetext":data.data.face_list[0].attributes.expression===0 ? "正常" : "夸張"},
{"attribute":"姿態(tài)","valuetext":data.data.face_list[0].attributes.yaw + " "
+ data.data.face_list[0].attributes.pitch + " "
+ data.data.face_list[0].attributes.roll
},
{"attribute":"亮度","valuetext":data.data.face_list[0].quality.brightness===0 ? "正常" : "異常"},
{"attribute":"模糊","valuetext":data.data.face_list[0].quality.blur===0 ? "否" : "是"},
{"attribute":"遮擋","valuetext":data.data.face_list[0].quality.occlusion.left_eye===0
&& data.data.face_list[0].quality.occlusion.right_eye===0
&& data.data.face_list[0].quality.occlusion.nose===0
&& data.data.face_list[0].quality.occlusion.mouth===0
&& data.data.face_list[0].quality.occlusion.chin===0
&& data.data.face_list[0].quality.occlusion.left_cheek===0
&& data.data.face_list[0].quality.occlusion.right_cheek===0 ? "無" : "有"
}
]
})
}
index.wxml
<scroll-view class="userinfo-tips-scroll-view" style="width:{{canvasViewWidth}}px; height:{{canvasViewHeight/2}}px; display:flex; justify-content:center" scroll-y="true" scroll-top="{{scrollToView}}">
<text class="userinfo-tips" style="display:flex; justify-content:center">{{userInfo.tips}}</text>
<view class="table" hidden="{{isHiddenTable}}">
<view class="tr bg-w">
<view class="th">屬性</view>
<view class="th">結(jié)果值</view>
</view>
<block wx:for="{{resultListData}}" wx:key="{[attribute]}">
<view class="tr bg-g" wx:if="{{index % 2 == 0}}">
<view class="td">{{item.attribute}}</view>
<view class="td">{{item.valuetext}}</view>
</view>
<view class="tr" wx:else>
<view class="td">{{item.attribute}}</view>
<view class="td">{{item.valuetext}}</view>
</view>
</block>
</view>
</scroll-view>
功能切換導航欄定義
為了便于不同功能之間的切換演示,我們需要引入導航欄功能
如下代碼:
index,js
Page({
data: {
systemInfo: {}, // 界面信息
navbar: ['人臉檢測', '人臉五官', '人臉比對'], // 功能界面選項列表
currentNavbar: '0', //界面選項ID
isHiddenTable: false, //是否隱藏檢測結(jié)果列表
scrollToView: "0", //滑動控件位置
resultListData:[], // 人臉檢測結(jié)果列表
compareResultData:[], //人臉比對結(jié)果列表
}
......
)
index.wxml
<!--index.wxml-->
<view class="navbar">
<!-- wx:for-index="idx" -->
<view class="navbar-item" wx:for="{{navbar}}" wx:for-index="idx" data-idx="{{idx}}" bindtap="swichNav">
<text class="navbar-text {{currentNavbar==idx ? 'active' : ''}}">{{item}}</text>
</view>
</view>
<!-- 人臉檢測功能欄 -->
<view class="hot-item-container {{currentNavbar==0 ? '' : 'hidden'}}">
<!-- 人臉檢測主界面代碼 -->
</view>
<!-- 人臉五官功能欄 -->
<view class="hot-item-container {{currentNavbar==1 ? '' : 'hidden'}}">
<!-- 人臉五官主界面代碼 -->
</view>
<!-- 人臉比對功能欄 -->
<view class="hot-item-container {{currentNavbar==2 ? '' : 'hidden'}}">
<!-- 人臉比對主界面代碼 -->
</view>