一:先看效果
二: 整体思路以及用到的第三方图像识别算法
1.调用微信小程序的相机功能(主要使用实时帧数据,已附链接);
CameraFrameListener | 微信开放文档
2. 将获取到的相机实时帧数据转成base64.调取第三方图像识别算法;
Docs
该文档里边有详细的介绍怎么使用去训练我们要识别的图片或者物体,如果有问题可以随时私信我!
3.第三方识别接口识别成功以后会告诉前端扫描的是什么,以便我们去判断该播放哪一个IP人物形象的序列帧动画;
4.提前准备好我们要用到的IP人物资源,我这里是用的序列帧动画(一张张帧照片拼接起来一个人物动画),这些资源建议放在服务器上;
5.创建画布,通过小程序的canvas对所有帧动画进行渲染以及播放;
三:附上全部代码
1.index.wxml
<view class="content"> <block wx:if="{{!isShowCamera}}"> <view class="scan" bindtap="open"> <view class="pup"> <view class="pupTitle">活动规则介绍</view> <view class="pupbox"> <view class="top"> <view class="list"> <image mode="aspectFit" src="../../img/1.png"></image> </view> <view class="list"> <image mode="aspectFit" src="../../img/2.png"></image> </view> <view class="list"> <image mode="aspectFit" src="../../img/3.png"></image> </view> <view class="list"> <image mode="aspectFit" src="../../img/4.png"></image> </view> <view class="list"> <image mode="aspectFit" src="../../img/5.png"></image> </view> <view class="list"> <image mode="aspectFit" src="../../img/6.png"></image> </view> </view> <view> <view class="pupbot">纸上的人物竟然还能动起来?</view> <view class="pupbot">不信?!那就对着人物形象扫一扫</view> <view class="pupbot">解锁更多精彩吧</view> </view> </view> </view> <button>知道啦</button> </view> </block> <block wx:else> <view class="page-section page-section-spacing swiper" style="height: 100vh;"> <swiper easing-function="easeOutCubic" bindchange="swiperChange" style="height: 100vh;" vertical="{{true}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}"> <block> <swiper-item style="height: 100vh;"> <block> <view class="mask" style="height: 100vh;background: transparent;"> <camera class="camera" device-position="back" flash="off" frame-size="medium" bindinitdone="bindinitdone"> <cover-view hidden="{{!show}}" class='scan-animation' animation="{{animation}}"> <cover-image src="../../img/saomiao.png"></cover-image> </cover-view> <canvas class="prize-frame-canvas" type="2d" id="oneTitleFrameCanvas" style="width: {{width}}rpx;height: {{height}}rpx"></canvas> <block> <view style="display: flex;justify-content: center;"> <text wx:if="{{!textShow}}" class="bottom">扫一扫,开启你的奇妙体验</text> <view wx:if="{{textShow}}" class="bottom2"> <image class="shanghua" src="../../img/shanghua.png"></image> <text>上划发现更多精彩</text> </view> <view wx:if="{{textShow}}" class="left" hover-class="leftActive" bindtap="toIndex">再玩一次</view> </view> </block> </camera> </view> </block> </swiper-item> <swiper-item wx:if="{{textShow}}" catchtouchmove='stopTouchMove' style="height: 100vh;"> <block> <view class="box" style="background-color: block;"> </view> </block> </swiper-item> </block> </swiper> </view> </block> </view> <canvas canvas-id="myCanvas"></canvas>
2.md5.js(第三方识别接口需要用到)
/* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. * Version 1.1 Copyright (C) Paul Johnston 1999 - 2002. * Code also contributed by Greg Holt * See http://pajhome.org.uk/site/legal.html for details. */ /* * Add integers, wrapping at 2^32. This uses 16-bit operations internally * to work around bugs in some JS interpreters. */ function safe_add(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF) var msw = (x >> 16) + (y >> 16) + (lsw >> 16) return (msw << 16) | (lsw & 0xFFFF) } /* * Bitwise rotate a 32-bit number to the left. */ function rol(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)) } /* * These functions implement the four basic operations the algorithm uses. */ function cmn(q, a, b, x, s, t) { return safe_add(rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b) } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t) } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t) } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t) } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t) } /* * Calculate the MD5 of an array of little-endian words, producing an array * of little-endian words. */ function coreMD5(x) { var a = 1732584193 var b = -271733879 var c = -1732584194 var d = 271733878 for(var i = 0; i < x.length; i += 16) { var olda = a var oldb = b var oldc = c var oldd = d a = ff(a, b, c, d, x[i+ 0], 7 , -680876936) d = ff(d, a, b, c, x[i+ 1], 12, -389564586) c = ff(c, d, a, b, x[i+ 2], 17, 606105819) b = ff(b, c, d, a, x[i+ 3], 22, -1044525330) a = ff(a, b, c, d, x[i+ 4], 7 , -176418897) d = ff(d, a, b, c, x[i+ 5], 12, 1200080426) c = ff(c, d, a, b, x[i+ 6], 17, -1473231341) b = ff(b, c, d, a, x[i+ 7], 22, -45705983) a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416) d = ff(d, a, b, c, x[i+ 9], 12, -1958414417) c = ff(c, d, a, b, x[i+10], 17, -42063) b = ff(b, c, d, a, x[i+11], 22, -1990404162) a = ff(a, b, c, d, x[i+12], 7 , 1804603682) d = ff(d, a, b, c, x[i+13], 12, -40341101) c = ff(c, d, a, b, x[i+14], 17, -1502002290) b = ff(b, c, d, a, x[i+15], 22, 1236535329) a = gg(a, b, c, d, x[i+ 1], 5 , -165796510) d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632) c = gg(c, d, a, b, x[i+11], 14, 643717713) b = gg(b, c, d, a, x[i+ 0], 20, -373897302) a = gg(a, b, c, d, x[i+ 5], 5 , -701558691) d = gg(d, a, b, c, x[i+10], 9 , 38016083) c = gg(c, d, a, b, x[i+15], 14, -660478335) b = gg(b, c, d, a, x[i+ 4], 20, -405537848) a = gg(a, b, c, d, x[i+ 9], 5 , 568446438) d = gg(d, a, b, c, x[i+14], 9 , -1019803690) c = gg(c, d, a, b, x[i+ 3], 14, -187363961) b = gg(b, c, d, a, x[i+ 8], 20, 1163531501) a = gg(a, b, c, d, x[i+13], 5 , -1444681467) d = gg(d, a, b, c, x[i+ 2], 9 , -51403784) c = gg(c, d, a, b, x[i+ 7], 14, 1735328473) b = gg(b, c, d, a, x[i+12], 20, -1926607734) a = hh(a, b, c, d, x[i+ 5], 4 , -378558) d = hh(d, a, b, c, x[i+ 8], 11, -2022574463) c = hh(c, d, a, b, x[i+11], 16, 1839030562) b = hh(b, c, d, a, x[i+14], 23, -35309556) a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060) d = hh(d, a, b, c, x[i+ 4], 11, 1272893353) c = hh(c, d, a, b, x[i+ 7], 16, -155497632) b = hh(b, c, d, a, x[i+10], 23, -1094730640) a = hh(a, b, c, d, x[i+13], 4 , 681279174) d = hh(d, a, b, c, x[i+ 0], 11, -358537222) c = hh(c, d, a, b, x[i+ 3], 16, -722521979) b = hh(b, c, d, a, x[i+ 6], 23, 76029189) a = hh(a, b, c, d, x[i+ 9], 4 , -640364487) d = hh(d, a, b, c, x[i+12], 11, -421815835) c = hh(c, d, a, b, x[i+15], 16, 530742520) b = hh(b, c, d, a, x[i+ 2], 23, -995338651) a = ii(a, b, c, d, x[i+ 0], 6 , -198630844) d = ii(d, a, b, c, x[i+ 7], 10, 1126891415) c = ii(c, d, a, b, x[i+14], 15, -1416354905) b = ii(b, c, d, a, x[i+ 5], 21, -57434055) a = ii(a, b, c, d, x[i+12], 6 , 1700485571) d = ii(d, a, b, c, x[i+ 3], 10, -1894986606) c = ii(c, d, a, b, x[i+10], 15, -1051523) b = ii(b, c, d, a, x[i+ 1], 21, -2054922799) a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359) d = ii(d, a, b, c, x[i+15], 10, -30611744) c = ii(c, d, a, b, x[i+ 6], 15, -1560198380) b = ii(b, c, d, a, x[i+13], 21, 1309151649) a = ii(a, b, c, d, x[i+ 4], 6 , -145523070) d = ii(d, a, b, c, x[i+11], 10, -1120210379) c = ii(c, d, a, b, x[i+ 2], 15, 718787259) b = ii(b, c, d, a, x[i+ 9], 21, -343485551) a = safe_add(a, olda) b = safe_add(b, oldb) c = safe_add(c, oldc) d = safe_add(d, oldd) } return [a, b, c, d] } /* * Convert an array of little-endian words to a hex string. */ function binl2hex(binarray) { var hex_tab = "0123456789abcdef" var str = "" for(var i = 0; i < binarray.length * 4; i++) { str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8)) & 0xF) } return str } /* * Convert an array of little-endian words to a base64 encoded string. */ function binl2b64(binarray) { var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" var str = "" for(var i = 0; i < binarray.length * 32; i += 6) { str += tab.charAt(((binarray[i>>5] << (i%32)) & 0x3F) | ((binarray[i>>5+1] >> (32-i%32)) & 0x3F)) } return str } /* * Convert an 8-bit character string to a sequence of 16-word blocks, stored * as an array, and append appropriate padding for MD4/5 calculation. * If any of the characters are >255, the high byte is silently ignored. */ function str2binl(str) { var nblk = ((str.length + 8) >> 6) + 1 // number of 16-word blocks var blks = new Array(nblk * 16) for(var i = 0; i < nblk * 16; i++) blks[i] = 0 for(var i = 0; i < str.length; i++) blks[i>>2] |= (str.charCodeAt(i) & 0xFF) << ((i%4) * 8) blks[i>>2] |= 0x80 << ((i%4) * 8) blks[nblk*16-2] = str.length * 8 return blks } /* * Convert a wide-character string to a sequence of 16-word blocks, stored as * an array, and append appropriate padding for MD4/5 calculation. */ function strw2binl(str) { var nblk = ((str.length + 4) >> 5) + 1 // number of 16-word blocks var blks = new Array(nblk * 16) for(var i = 0; i < nblk * 16; i++) blks[i] = 0 for(var i = 0; i < str.length; i++) blks[i>>1] |= str.charCodeAt(i) << ((i%2) * 16) blks[i>>1] |= 0x80 << ((i%2) * 16) blks[nblk*16-2] = str.length * 16 return blks } /* * External interface */ function hexMD5 (str) { return binl2hex(coreMD5( str2binl(str))) } function hexMD5w(str) { return binl2hex(coreMD5(strw2binl(str))) } function b64MD5 (str) { return binl2b64(coreMD5( str2binl(str))) } function b64MD5w(str) { return binl2b64(coreMD5(strw2binl(str))) } /* Backward compatibility */ function calcMD5(str) { return binl2hex(coreMD5( str2binl(str))) } module.exports = { hexMD5: hexMD5 }
3.index.js(重点重点重点)
const app = getApp() import { hexMD5 } from "../../utils/md5.js" // 提示音 let innerAudioContext = wx.createInnerAudioContext() innerAudioContext.src = '/img/scanvoice.mp3' let animation = wx.createAnimation({}); let donghuaInterVal; // 创建img标签 function createImage(canvas, url) { return new Promise((resolve, reject) => { const img = canvas.createImage(); img.src = url; img.onload = res => { resolve(img); }; img.onerror = err => { console.log('create image error', err, url) reject(err); }; }); } function querySelector(elementId, page) { const query = wx.createSelectorQuery(); return new Promise(resove => { query .in(page) .select(/^#/.test(elementId) ? elementId : '#' + elementId) .fields({ node: true, size: true, rect: true }) .exec(res => { resove(res); }); }); } Page({ data: { indicatorDots: true, autoplay: false, interval: 2000, duration: 500, isShowCamera: false, isShowScan: false, // 相机实例 ctx: '', // 帧数据实例 listener: '', // 锁 lock: false, // 每n秒请求间隔 fpsTime: 500, isHidden: true, num: 0, imgShow: false, isShow: true, imgList: [], name: '', tempImgUrl: '', i: 0, imgNum: '000', textShow: false, show: true, getframe: false, width: 1334, height: 750, }, t: Date.now(), s: 0, changeIndicatorDots() { this.setData({ indicatorDots: !this.data.indicatorDots }) }, changeAutoplay() { this.setData({ autoplay: !this.data.autoplay }) }, intervalChange(e) { this.setData({ interval: e.detail.value }) }, durationChange(e) { this.setData({ duration: e.detail.value }) }, // 再玩一次 toIndex() { wx.redirectTo({ url: '/pages/index/index', }) }, // 分享 onShareAppMessage(options) { return { title: '参与就有机会免费领取观影券', imageUrl: '/img/share.png', path: '/pages/index/index', success: function (res) { console.log(res, '成功') console.info(res + '成功'); wx.showToast({ title: '分享成功', }) // 转发成功 }, fail: function (res) { console.log(res + '失败'); // 转发失败 }, complete: function (res) { // 不管成功失败都会执行 console.log(res, '成功或失败') wx.showToast({ title: '成功或失败', }) } } }, onLoad() { // console.log('进入小程序'); this.donghua() // 监听小程序内存 wx.onMemoryWarning(function () { console.log('内存不足') }) }, // 修复方法:unload 中进行清理 onUnload() { console.log('小程序销毁'); this.fnStop() // clearInterval(this.data.timer) clearInterval(donghuaInterVal) }, // 权限 open() { wx.authorize({ scope: 'scope.camera' }).then(res => { this.setData({ isShowCamera: true }) }).catch(() => { wx.getSetting().then(res => { if (!res.authSetting['scope.camera']) { wx.showModal({ title: '是否授权摄像头', content: '请确认授权,否则无法正常使用', success: (tip) => { if (tip.confirm) wx.openSetting() } }) } }) }) }, // 1.相机初始化 bindinitdone() { this.data.ctx = wx.createCameraContext() this.data.listener = this.data.ctx.onCameraFrame(frame => { if (!this.data.lock) { this.data.lock = true this.scan(frame) } }) this.data.listener.start() this.setData({ isShowScan: true }) }, // 2.相机实时帧数据转成base64,调取第三方图像识别算法 scan(frame) { let that = this // console.log('222'); console.log(frame) // 将像素数据绘制到画布 let data = new Uint8ClampedArray(frame.data) let clamped = new Uint8ClampedArray(data) wx.canvasPutImageData({ canvasId: 'myCanvas', x: 0, y: 0, width: frame.width, height: frame.height, data: clamped, success(res) { // 转换临时文件 setTimeout(() => { wx.canvasToTempFilePath({ x: 0, y: 0, width: frame.width, height: frame.height, canvasId: 'myCanvas', fileType: 'jpg', destWidth: frame.width, destHeight: frame.height, // 精度修改 quality: 0.5, success(res) { // 临时文件转base64 wx.getFileSystemManager().readFile({ filePath: res.tempFilePath, //选择图片返回的相对路径 encoding: 'base64', //编码格式 success: res => { // 保存base64 // console.log(res.data); let base64 = 'data:image/jpeg;base64,' + res.data // console.log(base64); that.fnCloudDetect(base64) } }) }, fail(res) { console.log(res); } }, that) setTimeout(() => { that.data.lock = false }, that.data.fpsTime) }, 600) }, fail(err) { console.log('图像数据到canvas失败', err) } }) // } }, // 3.第三方图像识别算法接口 fnCloudDetect(base64) { let that = this const appId = '100000000628'; const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = 'weopripowis'; const requestId = 'cvjkxcvjxcvnm'; const version = 'v2'; const appKey = '954cnKRvf9P57JmL'; const requestUrl = 'https://gw-ezxr.netease.com/pigeon-image/api/alg/pigeon/image/recog'; const md5_string = "appId=" + appId + "&imageEncodingData=" + encodeURIComponent(base64) + "&nonce=" + encodeURIComponent(nonce) + "&requestId=" + encodeURIComponent(requestId) + "×tamp=" + encodeURIComponent(timestamp) + "&appkey=" + appKey; const sign = hexMD5(md5_string).toString().toUpperCase(); return new Promise((resolve, reject) => { wx.request({ url: requestUrl, method: "POST", header: { appId, nonce, timestamp, sign, version, }, data: { appId, requestId, imageEncodingData: base64, }, success: (result) => { // console.log("success", result); if (result.data.detail.status === 1 && result.data.status === '000000') { resolve(result.data) if (!that.data.getframe) { that.setData({ name: result.data.detail.returnData, show: false, getframe: true }) that.initImgList(result.data.detail.returnData) that.data.listener.stop(); clearInterval(donghuaInterVal) innerAudioContext.play() wx.showLoading({ title: '精彩即将呈现' }) console.log('停止获取实时帧') } } reject(result.data.desc); }, fail: (err) => { // console.log("fail", err); reject("request faild"); } }) }) }, // 4.图片列表 initImgList(name) { var tempList = [], baseurl, countNum = 0; if (name == '伯爵') { // baseurl = 'http://bronet.qiniu.bronet.cn/Dracula/Dracula_00' // 自己的 baseurl = 'https://hpkxjar.una-ad.com/public/Dracula/Dracula_00' // 客户的 countNum = 144 } else if (name == '女婿') { // baseurl = 'http://bronet.qiniu.bronet.cn/Johnny/johnny_00' baseurl = 'https://hpkxjar.una-ad.com/public/Johnny/johnny_00' countNum = 103 } else { // baseurl = 'http://bronet.qiniu.bronet.cn/Blobby/Blobby_00' baseurl = 'https://hpkxjar.una-ad.com/public/Blobby/Blobby_00' countNum = 139 } for (let i = 0; i < countNum; i++) { let l = JSON.stringify(i).length let baseIndex = l == 1 ? ('00' + i) : l == 2 ? ('0' + i) : i; let tempUrl = baseurl + baseIndex + '.png' tempList.push(tempUrl) } this.initCanvas(tempList); }, // 5.创建画布 async initCanvas(imgs) { const canvasQuery = await querySelector('#oneTitleFrameCanvas', this); const cnv = canvasQuery[0].node; const ctx = cnv.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; cnv.width = cnv.width * dpr; cnv.height = cnv.height * dpr; this.cnv = cnv; this.ctx = ctx; const imgNodes = await Promise.all(imgs.map(v => createImage(cnv, v))); this.imgNodes = imgNodes; this.fnPlay(); }, // 6.序列帧动画播放 fnPlay() { // console.log('开始播放'); wx.hideLoading() this.tickCount = this.cnv.requestAnimationFrame(() => { if (Date.now() - this.t >= 1000 / 16) { this.t = Date.now(); if (this.s > this.imgNodes.length) { this.s = 0; this.fnPlay(); this.setData({ textShow: true }) } else { this.ctx.clearRect(0, 0, this.cnv.width, this.cnv.height); if (this.imgNodes[this.s]) this.ctx.drawImage( this.imgNodes[this.s], 0, 0, this.cnv.width, this.cnv.height ); this.fnPlay(); this.setData({ textShow: true }) this.s++; } } else { this.fnPlay(); } }); }, // 释放序列帧 fnStop() { console.log('停止播放序列帧'); this.cnv.cancelAnimationFrame(this.tickCount); }, // 禁止第二屏滑动 stopTouchMove: function () { return true; }, // 扫描动画 donghua() { var that = this; // 控制向上还是向下移动 let m = true donghuaInterVal = setInterval(function () { setTimeout(() => { if (m) { animation.translateY(500).step({ duration: 2000 }) m = !m; } else { animation.translateY(-500).step({ duration: 2000 }) m = !m; } that.setData({ animation: animation.export() }) }, 1) }.bind(this), 3000) }, // 上滑动画 swiperChange(e) { this.fnStop() if (e.detail.current == 1) { wx.redirectTo({ url: '/pages/video/video', }) } } })