[Model] add Paddle.js web demo (#392)

* add application include paddle.js web demo and xcx

* cp PR #5

* add readme

* fix comments and link

* fix xcx readme

* fix Task 1

* fix bugs

* refine readme

* delete ocrxcx readme

* refine readme

* fix bugs

* delete old readme

* 200px to 300px

* revert 200px to 300px

Co-authored-by: Jason <jiangjiajun@baidu.com>
This commit is contained in:
Double_V
2022-10-20 17:01:38 +08:00
committed by GitHub
parent 587ffd4caf
commit 0a22979c35
302 changed files with 63930 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
/* global wx, App */
import * as paddlejs from '@paddlejs/paddlejs-core';
import '@paddlejs/paddlejs-backend-webgl';
// eslint-disable-next-line no-undef
const plugin = requirePlugin('paddlejs-plugin');
plugin.register(paddlejs, wx);
App({
globalData: {
Paddlejs: paddlejs.Runner
}
});

View File

@@ -0,0 +1,12 @@
{
"pages": [
"pages/index/index"
],
"plugins": {
"paddlejs-plugin": {
"version": "2.0.1",
"provider": "wx7138a7bb793608c3"
}
},
"sitemapLocation": "sitemap.json"
}

View File

@@ -0,0 +1,33 @@
{
"name": "paddlejs-demo",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@paddlejs/paddlejs-backend-webgl": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@paddlejs/paddlejs-backend-webgl/-/paddlejs-backend-webgl-1.2.0.tgz",
"integrity": "sha512-bKJKJkGldC3NPOuJyk+372z0XW1dd1D9lR0f9OHqWQboY0Mkah+gX+8tkerrNg+QjYz88IW0iJaRKB0jm+6d9g=="
},
"@paddlejs/paddlejs-core": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/@paddlejs/paddlejs-core/-/paddlejs-core-2.1.18.tgz",
"integrity": "sha512-QrXxwaHm4llp1sxbUq/oCCqlYx4ciXanBn/Lfq09UqR4zkYi5SptacQlIxgJ70HOO6RWIxjWN4liQckMwa2TkA=="
},
"d3-polygon": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz",
"integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ=="
},
"js-clipper": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/js-clipper/-/js-clipper-1.0.1.tgz",
"integrity": "sha1-TWsHQ0pECOfBKeMiAc5m0hR07SE="
},
"number-precision": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/number-precision/-/number-precision-1.5.2.tgz",
"integrity": "sha512-q7C1ZW3FyjsJ+IpGB6ykX8OWWa5+6M+hEY0zXBlzq1Sq1IPY9GeI3CQ9b2i6CMIYoeSuFhop2Av/OhCxClXqag=="
}
}
}

View File

@@ -0,0 +1,19 @@
{
"name": "paddlejs-demo",
"version": "0.0.1",
"description": "",
"main": "app.js",
"dependencies": {
"@paddlejs/paddlejs-backend-webgl": "^1.2.0",
"@paddlejs/paddlejs-core": "^2.1.18",
"d3-polygon": "2.0.0",
"js-clipper": "1.0.1",
"number-precision": "1.5.2"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

@@ -0,0 +1,337 @@
/* global wx, Page */
import * as paddlejs from '@paddlejs/paddlejs-core';
import '@paddlejs/paddlejs-backend-webgl';
import clipper from 'js-clipper';
import { divide, enableBoundaryChecking, plus } from 'number-precision';
// eslint-disable-next-line no-undef
const plugin = requirePlugin('paddlejs-plugin');
const Polygon = require('d3-polygon');
global.wasm_url = 'pages/index/wasm/opencv_js.wasm.br';
const CV = require('./wasm/opencv.js');
plugin.register(paddlejs, wx);
const imgList = [
'https://paddlejs.bj.bcebos.com/xcx/ocr.png',
'./img/width.png'
];
// eslint-disable-next-line max-lines-per-function
const outputBox = res => {
const thresh = 0.3;
const box_thresh = 0.5;
const max_candidates = 1000;
const min_size = 3;
const width = 960;
const height = 960;
const pred = res;
const segmentation = [];
pred.forEach(item => {
segmentation.push(item > thresh ? 255 : 0);
});
function get_mini_boxes(contour) {
// 生成最小外接矩形
const bounding_box = CV.minAreaRect(contour);
const points = [];
const mat = new CV.Mat();
// 获取矩形的四个顶点坐标
CV.boxPoints(bounding_box, mat);
for (let i = 0; i < mat.data32F.length; i += 2) {
const arr = [];
arr[0] = mat.data32F[i];
arr[1] = mat.data32F[i + 1];
points.push(arr);
}
function sortNumber(a, b) {
return a[0] - b[0];
}
points.sort(sortNumber);
let index_1 = 0;
let index_2 = 1;
let index_3 = 2;
let index_4 = 3;
if (points[1][1] > points[0][1]) {
index_1 = 0;
index_4 = 1;
}
else {
index_1 = 1;
index_4 = 0;
}
if (points[3][1] > points[2][1]) {
index_2 = 2;
index_3 = 3;
}
else {
index_2 = 3;
index_3 = 2;
}
const box = [
points[index_1],
points[index_2],
points[index_3],
points[index_4]
];
const side = Math.min(bounding_box.size.height, bounding_box.size.width);
mat.delete();
return {
points: box,
side
};
}
function box_score_fast(bitmap, _box) {
const h = height;
const w = width;
const box = JSON.parse(JSON.stringify(_box));
const x = [];
const y = [];
box.forEach(item => {
x.push(item[0]);
y.push(item[1]);
});
// clip这个函数将将数组中的元素限制在a_min, a_max之间大于a_max的就使得它等于 a_max小于a_min,的就使得它等于a_min。
const xmin = clip(Math.floor(Math.min(...x)), 0, w - 1);
const xmax = clip(Math.ceil(Math.max(...x)), 0, w - 1);
const ymin = clip(Math.floor(Math.min(...y)), 0, h - 1);
const ymax = clip(Math.ceil(Math.max(...y)), 0, h - 1);
// eslint-disable-next-line new-cap
const mask = new CV.Mat.zeros(ymax - ymin + 1, xmax - xmin + 1, CV.CV_8UC1);
box.forEach(item => {
item[0] = Math.max(item[0] - xmin, 0);
item[1] = Math.max(item[1] - ymin, 0);
});
const npts = 4;
const point_data = new Uint8Array(box.flat());
const points = CV.matFromArray(npts, 1, CV.CV_32SC2, point_data);
const pts = new CV.MatVector();
pts.push_back(points);
const color = new CV.Scalar(255);
// 多个多边形填充
CV.fillPoly(mask, pts, color, 1);
const sliceArr = [];
for (let i = ymin; i < ymax + 1; i++) {
sliceArr.push(...bitmap.slice(960 * i + xmin, 960 * i + xmax + 1));
}
const mean = num_mean(sliceArr, mask.data);
mask.delete();
points.delete();
pts.delete();
return mean;
}
function clip(data, min, max) {
return data < min ? min : data > max ? max : data;
}
function unclip(box) {
const unclip_ratio = 1.6;
const area = Math.abs(Polygon.polygonArea(box));
const length = Polygon.polygonLength(box);
const distance = area * unclip_ratio / length;
const tmpArr = [];
box.forEach(item => {
const obj = {
X: 0,
Y: 0
};
obj.X = item[0];
obj.Y = item[1];
tmpArr.push(obj);
});
const offset = new clipper.ClipperOffset();
offset.AddPath(tmpArr, clipper.JoinType.jtRound, clipper.EndType.etClosedPolygon);
const expanded = [];
offset.Execute(expanded, distance);
let expandedArr = [];
expanded[0] && expanded[0].forEach(item => {
expandedArr.push([item.X, item.Y]);
});
expandedArr = [].concat(...expandedArr);
return expandedArr;
}
function num_mean(data, mask) {
let sum = 0;
let length = 0;
for (let i = 0; i < data.length; i++) {
if (mask[i]) {
sum = plus(sum, data[i]);
length++;
}
}
return divide(sum, length);
}
// eslint-disable-next-line new-cap
const src = new CV.matFromArray(960, 960, CV.CV_8UC1, segmentation);
const contours = new CV.MatVector();
const hierarchy = new CV.Mat();
// 获取轮廓
CV.findContours(src, contours, hierarchy, CV.RETR_LIST, CV.CHAIN_APPROX_SIMPLE);
const num_contours = Math.min(contours.size(), max_candidates);
const boxes = [];
const scores = [];
const arr = [];
for (let i = 0; i < num_contours; i++) {
const contour = contours.get(i);
let {
points,
side
} = get_mini_boxes(contour);
if (side < min_size) {
continue;
}
const score = box_score_fast(pred, points);
if (box_thresh > score) {
continue;
}
let box = unclip(points);
// eslint-disable-next-line new-cap
const boxMap = new CV.matFromArray(box.length / 2, 1, CV.CV_32SC2, box);
const resultObj = get_mini_boxes(boxMap);
box = resultObj.points;
side = resultObj.side;
if (side < min_size + 2) {
continue;
}
box.forEach(item => {
item[0] = clip(Math.round(item[0]), 0, 960);
item[1] = clip(Math.round(item[1]), 0, 960);
});
boxes.push(box);
scores.push(score);
arr.push(i);
boxMap.delete();
}
src.delete();
contours.delete();
hierarchy.delete();
return {
boxes,
scores
};
};
let detectRunner;
Page({
data: {
imgList: imgList,
imgInfo: {},
result: '',
loaded: false
},
onLoad() {
enableBoundaryChecking(false);
const me = this;
detectRunner = new paddlejs.Runner({
modelPath: 'https://paddleocr.bj.bcebos.com/PaddleJS/PP-OCRv3/ch/ch_PP-OCRv3_det_infer_js_960/model.json',
mean: [0.485, 0.456, 0.406],
std: [0.229, 0.224, 0.225],
bgr: true,
webglFeedProcess: true
});
detectRunner.init().then(_ => {
me.setData({
loaded: true
});
});
},
selectImage(event) {
const imgPath = this.data.imgList[event.target.dataset.index];
this.getImageInfo(imgPath);
},
getImageInfo(imgPath) {
const me = this;
wx.getImageInfo({
src: imgPath,
success: imgInfo => {
const {
path,
width,
height
} = imgInfo;
const canvasPath = imgPath.includes('http') ? path : imgPath;
const canvasId = 'myCanvas';
const ctx = wx.createCanvasContext(canvasId);
let sw = 960;
let sh = 960;
let x = 0;
let y = 0;
if (height / width >= 1) {
sw = Math.round(sh * width / height);
x = Math.floor((960 - sw) / 2);
}
else {
sh = Math.round(sw * height / width);
y = Math.floor((960 - sh) / 2);
}
ctx.drawImage(canvasPath, x, y, sw, sh);
ctx.draw(false, () => {
// API 1.9.0 获取图像数据
wx.canvasGetImageData({
canvasId: canvasId,
x: 0,
y: 0,
width: 960,
height: 960,
success(res) {
me.predict({
data: res.data,
width: 960,
height: 960
}, {
canvasPath,
sw,
sh,
x,
y
});
}
});
});
}
});
},
predict(res, img) {
const me = this;
detectRunner.predict(res, function (data) {
// 获取坐标
const points = outputBox(data);
me.drawCanvasPoints(img, points.boxes);
me.setData({
result: JSON.stringify(points.boxes)
});
});
},
drawCanvasPoints(img, points) {
const canvasId = 'result';
const ctx = wx.createCanvasContext(canvasId);
ctx.drawImage(img.canvasPath, img.x, img.y, img.sw, img.sh);
points.length && points.forEach(point => {
// 开始一个新的绘制路径
ctx.beginPath();
// 设置线条颜色为蓝色
ctx.strokeStyle = 'blue';
// 设置路径起点坐标
ctx.moveTo(point[0][0], point[0][1]);
ctx.lineTo(point[1][0], point[1][1]);
ctx.lineTo(point[2][0], point[2][1]);
ctx.lineTo(point[3][0], point[3][1]);
ctx.closePath();
ctx.stroke();
});
ctx.draw();
}
});

View File

@@ -0,0 +1,29 @@
<view>
<view class="img-view">
<text class="title">点击图片进行预测</text>
<scroll-view class="imgWrapper" scroll-x="true">
<image
class="img {{selectedIndex == index ? 'selected' : ''}}"
wx:for="{{imgList}}"
wx:key="index"
src="{{item}}"
mode="aspectFit"
bindtap="selectImage"
data-index="{{index}}"
></image>
<canvas
canvas-id="myCanvas"
style="width: 960px; height: 960px; position: absolute; z-index: -1"
></canvas>
<canvas
canvas-id="result"
style="width: 960px; height: 960px;"
></canvas>
</scroll-view>
<text class="result" wx:if="{{result}}" style="height: 300rpx;">文本框选坐标:{{result}}</text>
</view>
</view>
<view class="mask" wx:if="{{!loaded}}">
<text class="loading">loading…</text>
</view>

View File

@@ -0,0 +1,56 @@
text {
display: block;
}
.title {
margin-top: 10px;
font-size: 16px;
line-height: 32px;
font-weight: bold;
}
.imgWrapper {
margin: 10px 10px 0;
white-space: nowrap;
}
.img {
width: 960px;
height: 960px;
border: 1px solid #f1f1f1;
}
.result {
margin-top: 5px;
}
.selected {
border: 1px solid #999;
}
.select-btn {
margin-top: 20px;
width: 60%;
}
.mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, .7);
}
.loading {
color: #fff;
font-size: 20px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.img-view {
padding-bottom: 20px;
border-bottom: 1px solid #f1f1f1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,75 @@
{
"description": "项目配置文件",
"packOptions": {
"ignore": []
},
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": true,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"compileType": "miniprogram",
"libVersion": "2.22.1",
"appid": "wxc43cbd2fafe0aec2",
"projectname": "mobilenet",
"debugOptions": {
"hidedInDevtools": []
},
"scripts": {},
"isGameTourist": false,
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {
"search": {
"list": []
},
"conversation": {
"list": []
},
"game": {
"list": []
},
"plugin": {
"list": []
},
"gamePlugin": {
"list": []
},
"miniprogram": {
"list": []
}
}
}

View File

@@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}