基于 Taro
开发的垃圾分类辅助小程序,支持拍照识别、关键词搜索、字典检索等功能。
<封面摄于浙江·杭州的西溪湿地,与小潘同学秋游。>
GitHub: garbage-classification
口袋拾荒
技术栈
小程序: Taro, GraphQL (Apollo), TypeScript, React (Hooks), Canvas
后台: AntDesign, TypeScript, React
服务端: Nest.js, Mongoose, GraphQL (Apollo), TypeScript, 百度AI
爬虫: cheerio, superagent, Koa, Mongoose
Taro 图片(URL) 转 base64 百度AI 接口接受 base64
形式的图片,并且识别主体内容和轮廓。所以我们需要通过相机拍照,以 base64
的形式上传到服务端。然而 Taro.createCameraContext
返回的是图片在本地缓存地址,而不是图片文件,无法转换为 base64
,Taro
中也没相应的文档。后来发现,原来 Taro
是小程序的子集,在 Taro
中也能使用 wx API
。
export function url2base64 (url ) { return new Promise ((resolve, reject ) => { wx.getFileSystemManager().readFile({ filePath : url, encoding : 'base64' , success : res => { resolve(res.data) }, fail : (err ) => { reject(err) } }) }) }
import {url2base64} from '~/func.js' onCamera = () => { const ctx = Taro.createCameraContext() ctx.takePhoto({ quality : 'high' , success : async res => { const base64 = await url2base64(res.tempImagePath) const { width, height } = await Taro.getImageInfo({ src : res.tempImagePath }) this .handleCanvas(base64, res.tempImagePath, { width, height }) } }) }
Canvas 绘制物品轮廓(自适应设备)
细说移动端 经典的REM布局 与 新秀VW布局
将图片转为 base64
后就可以请求 百度AI
了,成功识别出了物品的名称(text),轮廓(四个坐标点),现在需要通过 Canvas
将它们绘制在画布中。
遇到一个问题:图片在微信小程序IDE中调试没问题,但是一旦传到真机调试,比例就会失调,主要是因为文字和轮廓受设备分辨率影响进行了偏移。
之前看过一篇文章,有个概念:设备像素比 = 物理像素 / 设备独立像素
。这里的偏移就是 dpr
导致的,所以我们只需要计算出 dpr
,再将它缩放回来即可。(机智如我)
通过以下代码可以获取页面的实际尺寸:
const window = Taro.getSystemInfo()const w = window .windowWidthconst h = window .windowHeight
再将 页面尺寸 与 照片尺寸 做个除法计算即可:
handleCanvas = (base64, src, photo ) => { const w = res.windowWidth const h = res.windowHeight - 90 const scale = { w : w / photo.width, h : h / photo.height } const ctx = Taro.createCanvasContext('canvas' , this .$scope) Taro.getSystemInfo({ success : async res => { ctx.drawImage(src, 0 , 0 , w, h) const subject: any = await baiduSubjectDetection(this .state.baiduToken, { image : base64 }) const { height, width, top, left } = subject.result const advance: any = await baiduAdvancedGeneral(this .state.baiduToken, { image : base64 }) const { keyword } = advance.result[0 ] this .setState({ keyword : keyword }) ctx.setStrokeStyle("#00ff00" ) ctx.setLineWidth(2 ) ctx.rect(left, top, width, height) ctx.scale(scale.w, scale.h) ctx.setFillStyle("#00ff00" ) ctx.setFontSize(18 / scale.w) ctx.fillText(keyword, left, (top + 30 )) ctx.stroke() ctx.draw() } }) }
Canvas 绘制海报并保存到相册 推广页面的二维码海报也是用 Canvas
绘制的,实现了长按图片保存到相册功能,代码如下:
const window = Taro.getSystemInfoSync()const w = window .windowWidth * 0.8 const h = window .windowWidth * 0.8 * 9 / 5 const handleSaveImage = async () => { let res = await Taro.canvasToTempFilePath({ x : 0 , y : 0 , width : w, height : h, canvasId : 'canvas' , fileType : 'png' }) let saveRes = await Taro.saveImageToPhotosAlbum({ filePath : res.tempFilePath }) if (saveRes.errMsg === 'saveImageToPhotosAlbum:ok' ) { Taro.showToast({ title : '图片已保存到相册' }) } else { Taro.showToast({ title : '保存失败' }) } }const renderCanvas = (text = '快来扫一扫吧' ) => { const ctx = Taro.createCanvasContext('canvas' , null ) ctx.drawImage(background, 0 , 0 , w, h) ctx.setFillStyle("#ffffff" ) ctx.setFontSize(22 ) ctx.setTextAlign('center' ) ctx.fillText('口袋拾荒' , w / 2 , h - 200 ) ctx.setFontSize(12 ) ctx.fillText(`- ${text} -` , w / 2 , h - 180 ) ctx.draw() }
by the way,文字居中官方文档中是用 setTextAlign('center')
实现的,但是实际却没有效果。因为它是以整个画布的宽度的一半的中心轴为基准线,我们还需要将文字的横轴移到画布中心轴:ctx.fillText(text, w / 2, h)
。
数据爬虫 爬虫功能很简单,用 superagent 库请求网站获取 html
文档,再用 cheerio 库解析标签清洗数据,最后用 mongoose 存到数据库中。
const superagent = require ('superagent' )const cheerio = require ('cheerio' )class Eight { constructor ( ) { this .url = { other : 'https://www.8684.cn/ljfl_glj' , food : 'https://www.8684.cn/ljfl_slj' , harmful : 'https://www.8684.cn/ljfl_yhlj' , recyclable : 'https://www.8684.cn/ljfl_khslj' } } parse = (body ) => { const $ = cheerio.load(body.text) let arr = [] $('.list-col4 li a' ).each((index, ele ) => { arr.push($(ele).text()) }) return arr } run = async () => { const { url, parse } = this let data = [] for (const i in url) { const res = await superagent.get(url[i]) data.push({ key : i, value : parse(res) }) } return data } }exports .Eight = Eight
const Koa = require ('koa' )const app = new Koa()const mongoose = require ('mongoose' )const { Eight } = require ('./source' )const config = { hostname : 'localhost' , port : 3000 , } app.use(async ctx => { ctx.body = 'Hello World' }) app.listen(config.port, config.hostname)console .info('Server is running at http://%s:%s . Press Ctrl+C to stop.' , config.hostname, config.port) mongoose.connect('mongodb://localhost/garbage' )const Garbage = mongoose.model('Garbage' , { name : String , categoryId : String });const eight = new Eight() eight.run().then(res => { const data = res[0 ].value for (const i of data) { Garbage.create({name :i, categoryId : '5e427fe8558c2a31cd450fbc' }) } })
Taro 封装 Apollo 请求库 简单封装一下 GraphQL
请求库,方便调用。
import Taro from '@tarojs/taro' import ApolloClient from 'apollo-boost' const uri = 'https://xxx.com/graphql' const fetch = (url, { body: data, ...fetchOptions } ) => { return Taro.request({ url, data, ...fetchOptions, dataType : 'txt' , responseType : 'text' }) .then((res ) => { res.text = () => Promise .resolve(res.data) return res }).catch(error => { console .error(error) }, ); }export default new ApolloClient({ uri, fetch })
import { gql } from 'apollo-boost' export const category = gql` query { category { name id image } } `
import { category } from '~/api/gql' const res = await graphql.query({ query : category })
真机调试内网穿透 详见该文章 -> 小程序真机调试问题