您当前的位置: 首页 > 慢生活 > 程序人生 网站首页程序人生
利用mediapipe实现人脸识别及人脸距离屏幕距离计算
发布时间:2023-10-15 20:45:06编辑:雪饮阅读()
-
mediapipe可以实现人脸识别,具体资料可以参考
https://mediapipe-studio.webapps.google.com/studio/demo/face_landmarker
这里主要是以web端的实现为例。当然了要用摄像头,那么最好你有https环境或者说你有本地localhost环境,曾经想过在uniapp里面内置一个httpd服务器。。。
uniapp内部虽然可以做webview引入html,但这里的js还有引入其它类似目录或者其它资源文件,这些资源文件需要在js里面用,而不是像是直接html里面的src那样。
然后uniapp会给你报错就是不支持file协议那种错误。
模型文件face_landmarker.task达到了3.58M
所以最好结合下layer,不然这真的很要命。
这个模型文件,可以在这里下载
https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task
也可以直接引用咯。
由于uniapp的上面file协议问题,暂时我不晓得怎么解决。所以也都是全部引用网络的文件资源。
那么唯一要重点说明的其实就是计算人眼睛与设备的距离。
最开始用的方法是左眼虹膜直径的平方+右眼虹膜直径的平方,然后给这个结果开平方根。
并且结合一个系数和图片高度。
反正这里挺模糊的。图片高度我大概知道,就是我们这个案例后面在页面中渲染的那个canvas的高度(我大概记得是video标签高度直接渲染给了canvas的高度).
由于这个方案细节模糊,后来我找到了第二个方案
https://medium.com/@susanne.thierfelder/create-your-own-depth-measuring-tool-with-mediapipe-facemesh-in-javascript-ae90abae2362
Susanne Thierfelder提供的这个方案

他这个方案其实也是结合官方的,只是官方没有具体的实现,他给实现了,但是既然官方的是有10%以内的误差,那么他这个照样会有这个误差。
他的具体实现可以查看这里
https://codepen.io/Susanne-Thierfelder/pen/yLEOQXq
他这个方案虽然实现了,但是有浏览器兼容性,pc版chrome肯定没有问题。测试了移动端vivo内置浏览器有问题,移动端chrome没有问题,uniapp里面的chrome(webview内核)可能也是有问题的。反正半天没有出现效果,当时也没有详细的排查,忽略了uniapp里面又加载这么大的一个模型,应该需要好久的时间的问题,然后也在代码里面忘记了写加载模型那部分的loading效果(具体是里面比较乱,没有看的太明白该加在哪里,由于时间太紧所以没有过于纠结,时间是硬伤)。
虽然他这个方案还是没有彻底解决我的问题,但从他计算设备到眼睛的距离(他只计算了一个眼睛)的方式,我照搬代码并理解了之前图高以及系数还有图宽与实际距离的计算的逻辑了,照搬过来结合自己的情况修改后就行了。
所以最重要的计算距离的代码是这部分:
if(results.faceLandmarks.length>0){ if(typeof(results.faceLandmarks[0])!="undefined"){ //左眼坐标 let left_x1=results.faceLandmarks[0][471].x; let left_x2=results.faceLandmarks[0][469].x; let left_y1=results.faceLandmarks[0][471].y; let left_y2=results.faceLandmarks[0][469].y; //右眼坐标 let right_x1=results.faceLandmarks[0][476].x; let right_x2=results.faceLandmarks[0][474].x; let right_y1=results.faceLandmarks[0][476].y; let right_y2=results.faceLandmarks[0][474].y; console.log("左眼x1",left_x1); console.log("左眼x2",left_x2); console.log("右眼x1",right_x1); console.log("右眼x2",right_x2); //var irisLeftMinX = -1; //实际情况是要计算深度的时候要反转坐标 var left_x1_f=right_x1; var left_x2_f=right_x2; var right_x1_f=left_x1; var right_x2_f=left_x2; console.log("计算深度左眼x1",left_x1_f); console.log("计算深度左眼x2",left_x2_f); console.log("计算深度右眼x1",right_x1_f); console.log("计算深度右眼x2",right_x2_f); var irisLeftMinX = left_x1_f * canvasElement.width; var irisLeftMaxX = left_x2_f * canvasElement.width; var irisRightMinX = right_x1_f * canvasElement.width; var irisRightMaxX = right_x2_f * canvasElement.width; console.log("左眼最大深度irisLeftMaxX",irisLeftMaxX); console.log("左眼最小深度irisLeftMinX",irisLeftMinX); console.log("右眼最大深度irisRightMaxX",irisRightMaxX); console.log("右眼最小深度irisRightMinX",irisRightMinX); var left_dx = irisLeftMaxX - irisLeftMinX; var right_dx=irisRightMaxX-irisRightMinX; //人眼的水平虹膜直径大致保持在 11.7±0.5 毫米。 var dX = 11.7; //Logitech HD Pro C922 Norm focal // 罗技 HD Pro C922 标准焦点 var normalizedFocaleX = 1.40625; var fx = Math.min(canvasElement.width, canvasElement.height) * normalizedFocaleX; //常量10是1厘米等于10毫米 var left_dZ = (fx * (dX / left_dx)) / 10.0; var right_dZ=(fx * (dX / right_dx)) / 10.0; //左右眼分别与相机距离的均值 var dZ=((left_dZ+right_dZ)/2.0).toFixed(2); layer.msg("距离:"+dZ+"cm"); let zuogen=Math.pow(Math.pow(left_x1-left_x2,2)+Math.pow(left_y1-left_y2,2),0.5); let yougen=Math.pow(Math.pow(right_x1-right_x2,2)+Math.pow(right_y1-right_y2,2),0.5); let averageValue=((zuogen+yougen)/2).toFixed(2); //0.025平均值示例数据1,38对应真实距离 // layer.msg("averageValue:"+(averageValue/(0.025/38)).toFixed(2)+"cm"); } }太乱了,有时间再优化了。
这里的系数的计算我确实搞懵了,他默认提供了一个罗技高清网络摄像头的系数。
好像是他自己测算的,我问了这个数据我该怎么测算到。
他提供了
https://nikatsanka.github.io/camera-calibration-using-opencv-and-python.html
我按照这个提示,但是最后的出来的有好多数据,而并不是一个系数。
这里问了他,他说的我其实不是很懂。
所以系数暂时也就用他这个也可以,因为我感觉距离也还行,确实有点误差而已。
毕竟官方本来都是有误差的。
如果能知道这个系数怎么得出的,那么就更好呗。暂时先这样了。
另外就是这里用到的左右眼坐标数据,其实可以参考。
这张图
https://storage.googleapis.com/mediapipe-assets/documentation/mediapipe_face_landmark_fullsize.png
最后完整的示例如:
camera.html:
<!-- Copyright 2023 The MediaPipe Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> <html> <head> <meta charset="utf-8"> <meta http-equiv="Cache-control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Pragma" content="no-cache"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <title>Face Landmarker</title> <link href="./css/camera.css" rel="stylesheet"> <script src="./js/jquery.js"></script> <script src="./js/layer/layer.js"></script> <script type="text/javascript"> document.write("<script src='./js/camera.js?time="+new Date().getTime()+"' type='module'><\/script>"); </script> </head> <body> <div id="liveView" class="videoView"> <button id="webcamButton" class="mdc-button mdc-button--raised"> <span class="mdc-button__ripple"></span> <span class="mdc-button__label">ENABLE WEBCAM</span> </button> <div style="position: relative;"> <video id="webcam" style="position: abso" autoplay playsinline></video> <canvas class="output_canvas" id="output_canvas" style="position: absolute; left: 0px; top: 0px;"></canvas> </div> </div> <div class="blend-shapes"> <ul class="blend-shapes-list" id="video-blend-shapes"></ul> </div> </body> </html>camera.js:
// Copyright 2023 The MediaPipe Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* import vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3"; */ /*相当于从这个js里面https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm/vision_wasm_internal.js:9*/ /* import vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3"; */ /* import vision from "cdn.jsdelivr.net_npm_@mediapipe_tasks-vision@0.10.3"; */ /* import vision from "cdn.jsdelivr.net_npm_@mediapipe_tasks-vision@0.10.3_wasm_vision_wasm_internal"; */ /* import vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3"; */ import vision from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3"; /*import vision from "./tasks-vision";*/ console.log("vision"); const { FaceLandmarker, FilesetResolver, DrawingUtils } = vision; const videoBlendShapes = document.getElementById("video-blend-shapes"); let faceLandmarker; let enableWebcamButton; let webcamRunning= false; const videoWidth = 720; let video=null; let canvasElement=null; let canvasCtx=null; let lastVideoTime = -1; let results = undefined; let timer=null; function BlobToBlobURL(BlobFile,type) { // application/octet-stream,image/jpeg" let blob = new Blob([BlobFile], { type : type }); const blobUrl = URL.createObjectURL(blob); return blobUrl ; } function urlToBlobBase64(imgUrl){ return new Promise((resolve) => { window.URL = window.URL || window.webkitURL; var xhr = new XMLHttpRequest(); xhr.open("get", imgUrl, true); xhr.responseType = "blob"; xhr.onload = function () { if (this.status == 200) { var blob = this.response; let oFileReader = new FileReader(); oFileReader.onloadend = function (e) { resolve({ code:"success",blob}); }; oFileReader.readAsDataURL(blob); } else { resolve({ code:"fail"}); } }; xhr.send(); xhr.onerror = function(){ resolve({ code:"fail"}); }; }); } // Before we can use HandLandmarker class we must wait for it to finish // loading. Machine Learning models can be large and take a moment to // get everything needed to run. async function createFaceLandmarker() { /* const filesetResolver = await FilesetResolver.forVisionTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm" ); */ console.log("layer",layer); var layerLoadIndex=layer.load(); const filesetResolver = await FilesetResolver.forVisionTasks( "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm" ); layer.close(layerLoadIndex); /* modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`, */ var layerLoadIndex=layer.load(); var urlToBlobBase64Res= await urlToBlobBase64("./model/face_landmarker.task"); layer.close(layerLoadIndex); if(urlToBlobBase64Res.code=="fail"){ return layer.msg("获取模型失败",{icon:2}); } console.log("urlToBlobBase64Res.blob",urlToBlobBase64Res.blob); // `./model/face_landmarker.task` var layerLoadIndex=layer.load(); var modelAssetPath=BlobToBlobURL(urlToBlobBase64Res.blob,"application/octet-stream"); layer.close(layerLoadIndex); var layerLoadIndex=layer.load(); faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver, { baseOptions: { modelAssetPath: modelAssetPath /* delegate: "GPU" */ }, outputFaceBlendshapes: true, runningMode:'VIDEO', numFaces: 1 }); layer.close(layerLoadIndex); //demosSection.classList.remove("invisible"); } //检测用户设备是否支持获取摄像头视频流 function hasGetUserMedia() { return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); } //采集视频流事件绑定 function webcamBind(){ video = document.getElementById("webcam"); canvasElement = document.getElementById("output_canvas"); canvasCtx = canvasElement.getContext("2d"); if (hasGetUserMedia()==false){ return layer.msg("getUserMedia() is not supported by your browser",{icon:2}); } //开启采集视频流事件绑定 enableWebcamButton = document.getElementById("webcamButton"); enableWebcamButton.addEventListener("click", enableCam); } //获取用户媒体结果 function getUserMediaRes(){ return new Promise(function(resolve, reject){ navigator.mediaDevices.getUserMedia( { video: { facingMode:'user' } }).then(function(stream){ resolve({code:"success",data:stream}); }).catch(function(error){ resolve({code:"fail",data:error}); }); }); } // 启动Cam async function enableCam(event) { if (!faceLandmarker) { console.log("Wait! faceLandmarker not loaded yet."); layer.msg("Wait! faceLandmarker not loaded yet.",{icon:2}); return; } if (webcamRunning === true) { webcamRunning = false; enableWebcamButton.innerText = "ENABLE PREDICTIONS"; } else { webcamRunning = true; enableWebcamButton.innerText = "DISABLE PREDICTIONS"; } var layerIndex=layer.load(); var getUserMediaResData=await getUserMediaRes(); layer.close(layerIndex); if(getUserMediaResData.code=="success"){ video.srcObject = getUserMediaResData.data; video.addEventListener("loadeddata", predictWebcam); } if(getUserMediaResData.code=="fail"){ var error=getUserMediaResData.data; console.log("getUserMedia 错误:", error); console.log("getUserMedia 错误 name:", error.name); if (error.name === 'NotAllowedError') { // 用户未授予访问权限 console.log('用户未授予访问权限'); layer.msg('用户未授予访问权限',{icon:2}); } if(error.name=='NotReadableError'){ layer.msg('没有数据可读,请检查摄像头是否被占用',{icon:2}); } if (error.name === 'NotFoundError') { // 摄像头或麦克风未找到 layer.msg('摄像头或麦克风未找到',{icon:2}); } } } //预览Webcam async function predictWebcam() { const drawingUtils = new DrawingUtils(canvasCtx); const radio = video.videoHeight / video.videoWidth; video.style.width = videoWidth + "px"; video.style.height = videoWidth * radio + "px"; canvasElement.style.width = videoWidth + "px"; canvasElement.style.height = videoWidth * radio + "px"; canvasElement.width = video.videoWidth; canvasElement.height = video.videoHeight; console.log(" canvasElement.width", canvasElement.width); let startTimeMs = performance.now(); if (lastVideoTime !== video.currentTime) { lastVideoTime = video.currentTime; results = faceLandmarker.detectForVideo(video, startTimeMs); } //console.log("脸部数据测试",results.faceBlendshapes); //console.log("results.faceBlendshapes.length",results.faceBlendshapes.length); if(results.faceBlendshapes.length>0){ console.log("检测到脸部"); //layer.msg("检测到脸部"); } else{ console.log("未检测到脸部"); layer.msg("未检测到脸部"); } console.log("results",results); console.log("results.faceLandmarks478",results.faceLandmarks); if(results.faceLandmarks.length>0){ if(typeof(results.faceLandmarks[0])!="undefined"){ //左眼坐标 let left_x1=results.faceLandmarks[0][471].x; let left_x2=results.faceLandmarks[0][469].x; let left_y1=results.faceLandmarks[0][471].y; let left_y2=results.faceLandmarks[0][469].y; //右眼坐标 let right_x1=results.faceLandmarks[0][476].x; let right_x2=results.faceLandmarks[0][474].x; let right_y1=results.faceLandmarks[0][476].y; let right_y2=results.faceLandmarks[0][474].y; console.log("左眼x1",left_x1); console.log("左眼x2",left_x2); console.log("右眼x1",right_x1); console.log("右眼x2",right_x2); //var irisLeftMinX = -1; //实际情况是要计算深度的时候要反转坐标 var left_x1_f=right_x1; var left_x2_f=right_x2; var right_x1_f=left_x1; var right_x2_f=left_x2; console.log("计算深度左眼x1",left_x1_f); console.log("计算深度左眼x2",left_x2_f); console.log("计算深度右眼x1",right_x1_f); console.log("计算深度右眼x2",right_x2_f); var irisLeftMinX = left_x1_f * canvasElement.width; var irisLeftMaxX = left_x2_f * canvasElement.width; var irisRightMinX = right_x1_f * canvasElement.width; var irisRightMaxX = right_x2_f * canvasElement.width; console.log("左眼最大深度irisLeftMaxX",irisLeftMaxX); console.log("左眼最小深度irisLeftMinX",irisLeftMinX); console.log("右眼最大深度irisRightMaxX",irisRightMaxX); console.log("右眼最小深度irisRightMinX",irisRightMinX); var left_dx = irisLeftMaxX - irisLeftMinX; var right_dx=irisRightMaxX-irisRightMinX; //人眼的水平虹膜直径大致保持在 11.7±0.5 毫米。 var dX = 11.7; //Logitech HD Pro C922 Norm focal // 罗技 HD Pro C922 标准焦点 var normalizedFocaleX = 1.40625; var fx = Math.min(canvasElement.width, canvasElement.height) * normalizedFocaleX; //常量10是1厘米等于10毫米 var left_dZ = (fx * (dX / left_dx)) / 10.0; var right_dZ=(fx * (dX / right_dx)) / 10.0; //左右眼分别与相机距离的均值 var dZ=((left_dZ+right_dZ)/2.0).toFixed(2); layer.msg("距离:"+dZ+"cm"); let zuogen=Math.pow(Math.pow(left_x1-left_x2,2)+Math.pow(left_y1-left_y2,2),0.5); let yougen=Math.pow(Math.pow(right_x1-right_x2,2)+Math.pow(right_y1-right_y2,2),0.5); let averageValue=((zuogen+yougen)/2).toFixed(2); //0.025平均值示例数据1,38对应真实距离 // layer.msg("averageValue:"+(averageValue/(0.025/38)).toFixed(2)+"cm"); } } if (results.faceLandmarks) { for (const landmarks of results.faceLandmarks) { drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_TESSELATION, { color: "#C0C0C070", lineWidth: 1 } ); console.log("右眼数据", FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE, { color: "#FF3030" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYEBROW, { color: "#FF3030" } ); console.log("左眼数据",FaceLandmarker.FACE_LANDMARKS_LEFT_EYE); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYE, { color: "#30FF30" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_EYEBROW, { color: "#30FF30" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_FACE_OVAL, { color: "#E0E0E0" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_LIPS, { color: "#E0E0E0" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS, { color: "#FF3030" } ); drawingUtils.drawConnectors( landmarks, FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS, { color: "#30FF30" } ); } } drawBlendShapes(videoBlendShapes, results.faceBlendshapes); // Call this function again to keep predicting when the browser is ready. if (webcamRunning === true) { window.requestAnimationFrame(predictWebcam); } } //脸部数据结果参数细节 function drawBlendShapes(el, blendShapes) { if (!blendShapes.length) { return; } console.log(blendShapes[0]); let htmlMaker = ""; blendShapes[0].categories.map((shape) => { htmlMaker += ` <li class="blend-shapes-item"> <span class="blend-shapes-label">${ shape.displayName || shape.categoryName }</span> <span class="blend-shapes-value" style="width: calc(${ +shape.score * 100 }% - 120px)">${(+shape.score).toFixed(4)}</span> </li> `; }); el.innerHTML = htmlMaker; } console.log("createFaceLandmarker开始执行"); //创建模型对象 createFaceLandmarker(); //采集视频流事件绑定 webcamBind(); var layerLoadIndex=layer.load(); /*setTimeout(function(){ layer.close(layerLoadIndex); $("#webcamButton").click(); },1500);*/ timer=setInterval(function(){ if (faceLandmarker){ layer.close(layerLoadIndex); $("#webcamButton").click(); clearInterval(timer); } }); $(function(){ })
虹膜测距方案补充:
/* * 1.836来源 * 物理测得直视相机距离*(虹膜左眼的mediapipe中的直径长度+虹膜右眼的mediapipe中的直径长度/2) * */
layer.msg("距离:"+(1.836*0.7/((zuogen+yougen)/2)).toFixed(2));
关键字词:mediapipe,人脸识别,距离