-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9360ffa
commit b4425bc
Showing
11 changed files
with
429 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,55 @@ | ||
# mask-detection-web-demo | ||
# face-mask-detection-web-demo | ||
# 人脸口罩检测网页demo | ||
Face mask deteciton demo, by tensorflow.js | ||
|
||
## 原理 | ||
本demo是在浏览器运行的人脸口罩检测网页demo,介绍如何将深度学习的人脸口罩检测模型部署到浏览器里面。 | ||
关于人脸口罩检测的PyTorch、TensorFlow、Caffe、Keras、MXNet版本,可以进入相应Github仓库 | ||
[FaceMaskDetection](https://github.com/AIZOOTech/FaceMaskDetection) | ||
关于项目介绍,可以阅读一下两篇文章: | ||
[AIZOO开源人脸口罩检测数据+模型+代码+在线网页体验,通通都开源了](https://mp.weixin.qq.com/s/22U_v6IQ9PBHslI-65v_0Q) | ||
[人脸口罩检测现开源PyTorch、TensorFlow、MXNet等全部五大主流深度学习框架模型和代码 | ||
](https://mp.weixin.qq.com/s?__biz=MzIyMDY2MTUyNg==&mid=2247483779&idx=1&sn=b9ac5af31adf1dfdc3c87eb1c74836a5&exportkey=AX%2FANiIY8CWWMPQrHKh6A5E%3D&pass_ticket=aaNfWJGBgSum6CY5pvFqx0IIfljPPkeX%2BdMtPEl3zo5hQfPnYR5mEUlayz06kNKG) | ||
|
||
|
||
深度学习模型,可以借助TensorFlow.js库,运行在浏览器里。首先,需要使用`tensorflowjs_converter`将tensorflow的`graph model`或者keras的`layer model`转换为TensorFlow.js支持的模型。 | ||
该工具可以通过`pip install tensorflowjs`安装。 | ||
|
||
如果使用Keras模型转的操作如下: | ||
``` | ||
tensorflowjs_convert --input_format keras --output_format tfjs_layers_model /path/to/keras/hdf5/model /path/to/output/folder | ||
``` | ||
模型会生成`model.json`文件和一个或者多个`bin`文件,其中前者保存模型的拓扑,后者保存模型的权重。 | ||
使用JavaScript,需要在html中先引入`tfjs.min.js`库,然后加载模型 | ||
``` | ||
<script src="js/tfjs.min.js"></script> | ||
``` | ||
在`detection.js`中,加载模型 | ||
``` | ||
model = await tf.loadLayersModel('./tfjs-models/model.json'); | ||
``` | ||
置于anchor生成、输出解码、nms与使用python版本并无太大差异,大家可以查看`detection.js`中三个相关的函数,一目了然。 | ||
|
||
## 运行方法 | ||
在当前目录下打开终端,只需要建立一个最小web server即可。 | ||
对于使用python的用户 | ||
``` | ||
// python3用户 | ||
python -m http.server | ||
// python2用户 | ||
python -m SimpleHTTPServer | ||
``` | ||
如果你使用Node.js | ||
``` | ||
npm install serve -g //安装serve | ||
serve // this will open a mini web serve | ||
// 您也可以使用http-serve | ||
npm install http-server -g | ||
http-server | ||
``` | ||
|
||
## 效果 | ||
您可以点击网页中的上传图片按钮,或者拖拽图片到网页区域,然后模型会自动进行检测并画框。 | ||
![页面效果图](/images/result.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
|
||
html, body { | ||
margin: 0; | ||
padding: 0; | ||
font-family: Apercu; | ||
|
||
} | ||
|
||
.title { | ||
text-align: center | ||
} | ||
|
||
#tips { | ||
color:rosybrown | ||
} | ||
|
||
#container { | ||
background: #c79ef812; | ||
/* height: 500px; */ | ||
padding: 50px; | ||
font-size: 14px; | ||
border-radius: 33px; | ||
text-align: center; | ||
} | ||
|
||
#text-container { | ||
height: 100px; | ||
width: 100%; | ||
text-align: center; | ||
} | ||
|
||
#instruction { | ||
margin-top: 20px; | ||
} | ||
|
||
.result-color { | ||
color: #A159FC; | ||
} | ||
|
||
.green-color { | ||
color: #0bcf82; | ||
} | ||
|
||
.highlight { | ||
opacity: 0.4; | ||
background-color: rgba(161,89,252, 0.2) !important; | ||
} | ||
|
||
.center { | ||
display: block; | ||
margin-left: auto; | ||
margin-right: auto; | ||
} | ||
|
||
#fileUploader { | ||
opacity: 0; | ||
} | ||
|
||
#uploader-btn { | ||
background: #faebd700; | ||
margin-top: 1em; | ||
color: #0bcf81; | ||
border: solid 1px #0bcf81; | ||
padding: .6em 2em; | ||
line-height: 2; | ||
font-size: 12px; | ||
font-family: Apercu; | ||
cursor: pointer; | ||
border-radius: 10px; | ||
} | ||
|
||
#uploader-btn:hover { | ||
color: white; | ||
background-color: #0bcf82; | ||
border: solid 1px #0bcf82; | ||
-webkit-transition: all 0.3s ease; | ||
-moz-transition: all 0.3s ease; | ||
-o-transition: all 0.3s ease; | ||
transition: all 0.3s ease; | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<!-- | ||
Copyright (c) 2018 ml5 | ||
This software is released under the MIT License. | ||
https://opensource.org/licenses/MIT | ||
--> | ||
|
||
<html> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<link rel="stylesheet" type="text/css" href="css/style.css"> | ||
<script src="js/tfjs.min.js"></script> | ||
</head> | ||
|
||
<body onresize="windowResized()"> | ||
<a href="https://aizoo.com"><img src=images/aizoocom.png height="30px"></a> | ||
<h1 class='title'>借助TensorFlow.js</h1> | ||
<h1 class='title'>让人脸口罩识别跑在您本地浏览器里面</h1> | ||
<h5 class='title' id='tips'> 注意:部分手机拍摄的图片,读取出来是旋转90°,需要旋转正,为避免本demo过于复杂,本demo不对这种case进行处理</h3> | ||
<div id="container"> | ||
<img src="images/demo.jpg" id="image" class="center"> | ||
<div id="text-container"> | ||
<p id="instruction" class="green-color"> | ||
<button id="uploader-btn" onClick="clickUploader()">打开本地图片</button> | ||
<p class="green-color "> 或者将图片拖到这里 📂, 然后下方会显示结果</p> | ||
|
||
<p id="warning" class="result-color"></p> | ||
<p class="result-color"> | ||
检测结果如下: | ||
</p> | ||
<input name="imgFile" type="file" id="fileUploader" accept="image/*" onChange="handleFiles()"> | ||
</div> | ||
<canvas id="canvas"></canvas> | ||
</div> | ||
<script src="js/detection.js"></script> | ||
<script src="js/index.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// decode the output according to anchors | ||
function decodeBBox(anchors, rawOutput, variances=[0.1,0.1,0.2,0.2]) { | ||
const [anchorXmin, anchorYmin, anchorXmax, anchorYmax] = tf.split(anchors, [1,1,1,1], -1); | ||
const anchorCX = tf.div(tf.add(anchorXmin, anchorXmax), 2); | ||
const anchorCY = tf.div(tf.add(anchorYmin, anchorYmax), 2); | ||
|
||
const anchorW = tf.sub(anchorXmax, anchorXmin); | ||
const anchorH = tf.sub(anchorYmax, anchorYmin); | ||
|
||
const rawOutputScale = tf.mul(rawOutput, tf.tensor(variances)); | ||
const [rawOutputCX, rawOutputCY, rawOutputW, rawOutputH] = tf.split(rawOutputScale, [1,1,1,1], -1); | ||
const predictCX = tf.add(tf.mul(rawOutputCX, anchorW), anchorCX); | ||
const predictCY = tf.add(tf.mul(rawOutputCY,anchorH), anchorCY); | ||
const predictW = tf.mul(tf.exp(rawOutputW), anchorW); | ||
const predictH = tf.mul(tf.exp(rawOutputH), anchorH); | ||
const predictXmin = tf.sub(predictCX, tf.div(predictW, 2)); | ||
const predictYmin = tf.sub(predictCY, tf.div(predictH, 2)); | ||
const predictXmax = tf.add(predictCX, tf.div(predictW, 2)); | ||
const predictYmax = tf.add(predictCY, tf.div(predictH, 2)); | ||
// eslint-disable-next-line | ||
const predictBBox = tf.concat([predictYmin, predictXmin, predictYmax, predictXmax],-1); | ||
return predictBBox | ||
} | ||
|
||
// generate anchors | ||
function anchorGenerator(featureMapSizes, anchorSizes, anchorRatios) { | ||
let anchorBBoxes = []; | ||
// eslint-disable-next-line | ||
featureMapSizes.map((featureSize, idx) =>{ | ||
const cx = tf.div(tf.add(tf.linspace(0, featureSize[0] - 1, featureSize[0]) , 0.5) , featureSize[0]); | ||
const cy = tf.div(tf.add(tf.linspace(0, featureSize[1] - 1, featureSize[1]) , 0.5) , featureSize[1]); | ||
const cxGrid = tf.matMul(tf.ones([featureSize[1], 1]), cx.reshape([1,featureSize[0]])); | ||
const cyGrid = tf.matMul(cy.reshape([featureSize[1], 1]), tf.ones([1, featureSize[0]])); | ||
// eslint-disable-next-line | ||
const cxGridExpend = tf.expandDims(cxGrid, -1); | ||
// eslint-disable-next-line | ||
const cyGridExpend = tf.expandDims(cyGrid, -1); | ||
// eslint-disable-next-line | ||
const center = tf.concat([cxGridExpend, cyGridExpend], -1); | ||
const numAnchors = anchorSizes[idx].length + anchorRatios[idx].length -1; | ||
const centerTiled = tf.tile(center, [1, 1, 2*numAnchors]); | ||
// eslint-disable-next-line | ||
let anchorWidthHeights = []; | ||
|
||
// eslint-disable-next-line | ||
for (const scale of anchorSizes[idx]) { | ||
const ratio = anchorRatios[idx][0]; | ||
const width = scale * Math.sqrt(ratio); | ||
const height = scale / Math.sqrt(ratio); | ||
|
||
const halfWidth = width / 2; | ||
const halfHeight = height / 2; | ||
anchorWidthHeights.push(-halfWidth, -halfHeight, halfWidth, halfHeight); | ||
// width = tf.mul(scale, tf.sqrt(ratio)); | ||
// height = tf.div(scale, tf.sqrt(ratio)); | ||
|
||
// halfWidth = tf.div(width, 2); | ||
// halfHeight = tf.div(height, 2); | ||
// anchorWidthHeights.push(tf.neg(halfWidth), tf.neg(halfWidth), halfWidth, halfHeight); | ||
} | ||
|
||
// eslint-disable-next-line | ||
for ( const ratio of anchorRatios[idx].slice(1)) { | ||
const scale = anchorSizes[idx][0]; | ||
const width = scale * Math.sqrt(ratio); | ||
const height = scale / Math.sqrt(ratio); | ||
const halfWidth = width / 2; | ||
const halfHeight = height / 2; | ||
anchorWidthHeights.push(-halfWidth, -halfHeight, halfWidth, halfHeight); | ||
} | ||
const bboxCoord = tf.add(centerTiled , tf.tensor(anchorWidthHeights)); | ||
const bboxCoordReshape = bboxCoord.reshape([-1, 4]); | ||
anchorBBoxes.push(bboxCoordReshape); | ||
}) | ||
// eslint-disable-next-line | ||
anchorBBoxes = tf.concat(anchorBBoxes, 0); | ||
return anchorBBoxes; | ||
} | ||
|
||
// nms function | ||
function nonMaxSuppression(bboxes, confidences, confThresh, iouThresh, width, height, maxOutputSize=100) { | ||
const bboxMaxFlag = tf.argMax(confidences, -1); | ||
const bboxConf = tf.max(confidences, -1); | ||
const keepIndices = tf.image.nonMaxSuppression(bboxes, bboxConf, maxOutputSize, iouThresh, confThresh); | ||
// eslint-disable-next-line | ||
let results = [] | ||
const keepIndicesData = keepIndices.dataSync(); | ||
const bboxConfData = bboxConf.dataSync(); | ||
const bboxMaxFlagData = bboxMaxFlag.dataSync(); | ||
const bboxesData = bboxes.dataSync(); | ||
// eslint-disable-next-line | ||
keepIndicesData.map((idx) => { | ||
const xmin = Math.round(Math.max(bboxesData[4*idx + 1] * width, 0)); | ||
const ymin = Math.round(Math.max(bboxesData[4*idx + 0] * height, 0)); | ||
const xmax = Math.round(Math.min(bboxesData[4*idx+3] * width, width)) | ||
const ymax = Math.round(Math.min(bboxesData[4*idx + 2] * height, height)); | ||
results.push([[xmin, ymin, xmax, ymax], | ||
bboxMaxFlagData[idx], bboxConfData[idx]]) | ||
}); | ||
return results; | ||
} | ||
|
||
|
||
async function loadModel() { | ||
model = await tf.loadLayersModel('./tfjs-models/model.json'); | ||
return model; | ||
} | ||
|
||
async function detect(imgToPredict) { | ||
const detectionResults = tf.tidy(() => { | ||
// eslint-disable-next-line | ||
const width = imgToPredict.width; | ||
// eslint-disable-next-line | ||
const height = imgToPredict.height; | ||
let img = tf.browser.fromPixels(imgToPredict); | ||
img = tf.image.resizeBilinear(img, [260, 260]); | ||
img = img.expandDims(0).toFloat().div(tf.scalar(255)); | ||
const [rawBBoxes, rawConfidences] = model.predict(img); | ||
const bboxes = decodeBBox(anchors, tf.squeeze(rawBBoxes)); | ||
const Results = nonMaxSuppression(bboxes, tf.squeeze(rawConfidences), 0.5, 0.5, width, height ); | ||
return Results; | ||
}) | ||
return detectionResults; | ||
} | ||
|
||
featureMapSizes = [[33, 33], [17, 17], [9, 9], [5, 5], [3,3]]; | ||
anchorSizes = [[0.04, 0.056], [0.08, 0.11], [0.16, 0.22], [0.32, 0.45], [0.64, 0.72]]; | ||
anchorRatios = [[1, 0.62, 0.42], [1, 0.62, 0.42], [1, 0.62, 0.42], [1, 0.62, 0.42], [1, 0.62, 0.42]]; | ||
|
||
let anchors = anchorGenerator(featureMapSizes, anchorSizes, anchorRatios); |
Oops, something went wrong.