This commit is contained in:
KANE LAZENI 2026-02-23 10:03:15 +00:00
parent e2be1ed7ff
commit 0e5d2b0c9e
6 changed files with 0 additions and 952 deletions

View File

@ -1332,9 +1332,7 @@ public function getTarifActeAdherent($idAdherent)
$codeSociete = $_SESSION['p_codeSociete'];
$codePrestataire = $_SESSION['p_codePrestataire_C'];
$idBeneficiaire = $_SESSION['p_idBeneficiaire_C'];
$user = $_SESSION['p_login'];
// $sql = 'call sp_p_get_beneficiaire_tag(?, ?, ?, ?);';
$sql = 'call sp_p_checkdemandereconnaissancefaciale(?, ?, ?);';
$resultat = $this->executerRequete($sql, array($codeSociete, $codePrestataire, $idBeneficiaire));
$ligne = $resultat->fetch(PDO::FETCH_ASSOC);

View File

@ -1,151 +0,0 @@
<?php
$_SESSION['p_messageFace'] = "";
?>
<div class="modal fade" id="pop_rec_faciale" role="dialog" data-backdrop="static" data-keyboard="false" >
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button id="btn_close_pop_rec_faciale" name="btn_close_pop_rec_faciale" type="button" class="close" data-bs-dismiss="modal" onclick="javascript:fiche_beneficiaire();"> <?= _("Fermer") ?> </button>
<h4 class="modal-title"> <?= _("RECONNAISSANCE FACIALE") ?> </h4>
</div>
<div class="modal-body">
<!-- Début ajout 25/09/20225 -->
<div class="row">
<?php include 'liveness.php'; ?>
</div>
<!-- <div id="div_ebene" hidden> -->
<div id="div_ebene">
<!-- Fin ajout 25/09/20225 -->
<table class="table table-responsive table-condensed">
<tbody>
<tr>
<td width="48%">
<button id="ebene_take_photo_face" name="ebene_take_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:takephoto();"> <?= _("PRENDRE UNE PHOTO") ?> </button>
</td>
<td > </td>
<?php if ($faceRegistered=="1") : ?>
<td width="48%">
<button disabled id="ebene_confirmer_photo_face" name="ebene_confirmer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:ebene_confirmer_photo_face();"> <?= _("CONFIRMER LA PHOTO") ?> </button>
</td>
<?php endif; ?>
<?php if ($faceRegistered!="1") : ?>
<td width="48%">
<button disabled id="ebene_enregistrer_photo_face" name="ebene_enregistrer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:ebene_enregistrer_photo_face();"> <?= _("ENREGISTRER LA PHOTO") ?> </button>
</td>
<?php endif; ?>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-6" >
<legend style="text-align:center" >Webcam</legend>
<video id="video_face" name="video_face" autoplay height="250" align="center"></video><br />
</div>
<div class="col-6" >
<legend style="text-align:center" >Photo</legend>
<img id="photo_face" name="photo_face" src="" />
<form id="form_face" name="form_face" enctype="multipart/form-data" method="post" action="Fichebeneficiaire/ebeneenregistrerface">
<INPUT class="sr-only" TYPE="text" id="compare_face" name="compare_face" value="<?= $faceRegistered ?>">
<INPUT class="sr-only" TYPE="text" id="del_face" name="del_face" value="0">
<INPUT class="image-tag" TYPE="hidden" id="image_face" name="image_face" >
</form>
</div>
</div>
<canvas id="canvas" name="canvas" style="display: none;" width="350" height="260"></canvas>
<?php if ($faceRegistered=="1") : ?>
<!--
<button disabled id="ebene_supprimer_photo_face" name="ebene_supprimer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-danger" onclick="javascript:ebene_supprimer_photo_face();"> <?= _("SUPPRIMER LA PHOTO") ?> </button>
<span style='font-size:12pt;font-weight: bold;' ><?= _("Motif Suppression") ?> :</span> <INPUT disabled class="form-control" TYPE="text" id="motif" NAME="motif" style='font-size:12pt;font-weight: bold;' >
-->
<?php endif; ?>
<div id="message_face" name="message_face" >
<H2 style="background-color:yellow;">
<marquee behavior="scroll" direction="left" scrollamount="10"> </marquee>
</H2>
<INPUT class="sr-only" TYPE="text" id="photo_succes" name="photo_succes" value="0">
</div>
<div id="div_wait_face_ebene">
</div>
</div>
<script type="text/javascript">
var video = document.getElementById('video_face');
var canvas = document.getElementById('canvas');
var photo = document.getElementById('photo_face');
var image_face = document.getElementById('image_face');
navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.mediaDevices.getUserMedia || navigator.moxGetUserMedia;
if (navigator.mediaDevices.getUserMedia)
{
navigator.mediaDevices.getUserMedia({video: true })
.then(function (stream) {
video.srcObject = stream;
})
.catch(function (e) { alert(e.name + ": " + e.message); });
}
else
{
navigator.getMedia({ video: { mandatory: { maxWidth: 350, maxHeight: 260 } } }, function(stream) {
video.src = stream;
}, function(e) {
alert(e);
console.log("Failed!", e);
});
}
function takephoto() {
$('#message_face').html("");
$("#div_wait_face_ebene").html('');
// var ctx = canvas.getContext("2d").drawImage(video, 0, 0, 350, 260, 0, 0, 350, 260);
var ctx = canvas.getContext("2d").drawImage(video, 0, 0, 350, 260);
var data = canvas.toDataURL('image/jpeg');
// var data = canvas.toDataURL('image/jpeg', 0.7);
photo.setAttribute('src', data);
$("#image_face").val(data);
var faceRegistered = $("#faceRegistered").val();
$("#ebene_enregistrer_photo_face").enable();
if(faceRegistered=="1")
{
$("#ebene_confirmer_photo_face").enable();
$("#ebene_supprimer_photo_face").enable();
$("#motif").enable();
}
}
</script>
</div>
<div class="modal-footer">
<button id="close_poprec_faciane" name="close_poprec_faciane" type="button" class="btn btn-default" data-bs-dismiss="modal" onclick="javascript:fiche_beneficiaire();" > <?= _("Fermer") ?> </button>
</div>
</div>
</div>
</div>

View File

@ -1,139 +0,0 @@
<?php
$_SESSION['p_messageFace'] = "";
?>
<div class="modal fade" id="pop_rec_faciale" role="dialog" data-backdrop="static" data-keyboard="false" >
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button id="btn_close_pop_rec_faciale" name="btn_close_pop_rec_faciale" type="button" class="close" data-bs-dismiss="modal" onclick="javascript:fiche_beneficiaire();"> <?= _("Fermer") ?> </button>
<h4 class="modal-title"> <?= _("RECONNAISSANCE FACIALE") ?> </h4>
</div>
<div class="modal-body">
<table class="table table-responsive table-condensed">
<tbody>
<tr>
<td width="48%">
<button id="ebene_take_photo_face" name="ebene_take_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:takephoto();"> <?= _("PRENDRE UNE PHOTO") ?> </button>
</td>
<td > </td>
<?php if ($faceRegistered=="1") : ?>
<td width="48%">
<button disabled id="ebene_confirmer_photo_face" name="ebene_confirmer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:ebene_confirmer_photo_face();"> <?= _("CONFIRMER LA PHOTO") ?> </button>
</td>
<?php endif; ?>
<?php if ($faceRegistered!="1") : ?>
<td width="48%">
<button disabled id="ebene_enregistrer_photo_face" name="ebene_enregistrer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-primary" onclick="javascript:ebene_enregistrer_photo_face();"> <?= _("ENREGISTRER LA PHOTO") ?> </button>
</td>
<?php endif; ?>
</tr>
</tbody>
</table>
<div class="row">
<div class="col-6" >
<legend style="text-align:center" >Webcam</legend>
<video id="video_face" name="video_face" autoplay 350="450" height="250" align="center"></video><br />
</div>
<div class="col-6" >
<legend style="text-align:center" >Photo</legend>
<img id="photo_face" name="photo_face" src="" />
<form id="form_face" name="form_face" enctype="multipart/form-data" method="post" action="Fichebeneficiaire/ebeneenregistrerface">
<INPUT class="sr-only" TYPE="text" id="compare_face" name="compare_face" value="<?= $faceRegistered ?>">
<INPUT class="sr-only" TYPE="text" id="del_face" name="del_face" value="0">
<INPUT class="image-tag" TYPE="hidden" id="image_face" name="image_face" >
</form>
</div>
</div>
<canvas id="canvas" name="canvas" style="display: none;" width="350" height="260"></canvas>
<?php if ($faceRegistered=="1") : ?>
<!--
<button disabled id="ebene_supprimer_photo_face" name="ebene_supprimer_photo_face" style='font-size:15pt;' type="button" class="form-control btn btn-danger" onclick="javascript:ebene_supprimer_photo_face();"> <?= _("SUPPRIMER LA PHOTO") ?> </button>
<span style='font-size:12pt;font-weight: bold;' ><?= _("Motif Suppression") ?> :</span> <INPUT disabled class="form-control" TYPE="text" id="motif" NAME="motif" style='font-size:12pt;font-weight: bold;' >
-->
<?php endif; ?>
<div id="message_face" name="message_face" >
<H2 style="background-color:yellow;">
<marquee behavior="scroll" direction="left" scrollamount="10"> </marquee>
</H2>
<INPUT class="sr-only" TYPE="text" id="photo_succes" name="photo_succes" value="0">
</div>
<div id="div_wait_face_ebene">
</div>
<script type="text/javascript">
var video = document.getElementById('video_face');
var canvas = document.getElementById('canvas');
var photo = document.getElementById('photo_face');
var image_face = document.getElementById('image_face');
navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.mediaDevices.getUserMedia || navigator.moxGetUserMedia;
if (navigator.mediaDevices.getUserMedia)
{
navigator.mediaDevices.getUserMedia({video: true })
.then(function (stream) {
video.srcObject = stream;
})
.catch(function (e) { alert(e.name + ": " + e.message); });
}
else
{
navigator.getMedia({ video: { mandatory: { maxWidth: 350, maxHeight: 260 } } }, function(stream) {
video.src = stream;
}, function(e) {
alert(e);
console.log("Failed!", e);
});
}
function takephoto() {
$('#message_face').html("");
$("#div_wait_face_ebene").html('');
// var ctx = canvas.getContext("2d").drawImage(video, 0, 0, 350, 260, 0, 0, 350, 260);
var ctx = canvas.getContext("2d").drawImage(video, 0, 0, 350, 260);
var data = canvas.toDataURL('image/jpeg');
// var data = canvas.toDataURL('image/jpeg', 0.7);
photo.setAttribute('src', data);
$("#image_face").val(data);
var faceRegistered = $("#faceRegistered").val();
$("#ebene_enregistrer_photo_face").enable();
if(faceRegistered=="1")
{
$("#ebene_confirmer_photo_face").enable();
$("#ebene_supprimer_photo_face").enable();
$("#motif").enable();
}
}
</script>
</div>
<div class="modal-footer">
<button id="close_poprec_faciane" name="close_poprec_faciane" type="button" class="btn btn-default" data-bs-dismiss="modal" onclick="javascript:fiche_beneficiaire();" > <?= _("Fermer") ?> </button>
</div>
</div>
</div>
</div>

View File

@ -1,337 +0,0 @@
<style>
:root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
/*body { margin: 0; background: #0f172a; color: #e2e8f0; }*/
header { padding: 16px 24px; background: #111827; display:flex; align-items:center; gap:12px; }
h1 { font-size: 18px; margin: 0; }
/*main { display:grid; grid-template-columns: 1fr 360px; gap: 16px; padding: 16px; }*/
.stage { position: relative; aspect-ratio: 16/9; background: #111827; border: 1px solid #1f2937; border-radius: 12px; overflow: hidden; }
/* video, canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } */
.panel { background: #111827; border: 1px solid #1f2937; border-radius: 12px; padding: 16px; }
.row { display:flex; justify-content: space-between; align-items:center; margin: 8px 0; }
.pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px; border:1px solid #1f2937; font-size:12px; }
.ok { color:#10b981; }
.warn { color:#f59e0b; }
.bad { color:#ef4444; }
button { background:#1f2937; color:#e5e7eb; border:1px solid #374151; border-radius:10px; padding:10px 12px; cursor:pointer; }
button:disabled { opacity:.5; cursor:not-allowed; }
small { color:#94a3b8; }
.meter { height: 8px; border-radius: 999px; background:#0b1220; border:1px solid #1f2937; overflow:hidden; }
.meter > div { height: 100%; background: linear-gradient(90deg,#22c55e,#16a34a); width:0%; }
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:8px; }
.kpi { background:#0b1220; border:1px solid #1f2937; border-radius:10px; padding:10px; }
code { background:#0b1220; padding:2px 6px; border-radius:6px; }
</style>
<header>
<h1>Détection de vivacité (Liveness)</h1>
<div class="pill"><span>🎥</span><span id="camStatus">Caméra : inactif</span></div>
<div class="pill"><span>🧠</span><span id="mpStatus">Modèle : non chargé</span></div>
</header>
<div class="col-6" >
<section class="stage">
<video id="video" playsinline muted height="250"></video>
<canvas id="overlay"></canvas>
</section>
</div>
<div class="col-6" >
<div class="row" style="margin-bottom:8px">
<button id="btnStart">Démarrer</button>
<button class="sr-only" id="btnStop" disabled>Arrêter</button>
</div>
<div class="row">
<strong>Statut vivacité</strong>
<span id="liveBadge" class="pill bad">Non vérifié</span>
</div>
<div class="meter" style="margin:8px 0 16px">
<div id="liveMeter"></div>
</div>
<div class="grid">
<div class="kpi"><div>Clignements</div><div id="blinkCount" style="font-size:22px">0</div><small>EAR&lt;seuil</small></div>
<div class="kpi"><div>Mouvements tête</div><div id="headMoves" style="font-size:22px">0</div><small>yaw/roll Δ</small></div>
<div class="kpi"><div>Confiance visage</div><div id="faceScore" style="font-size:22px">0.00</div><small>presence score</small></div>
<div class="kpi"><div>FPS</div><div id="fps" style="font-size:22px">0</div><small>approx</small></div>
</div>
<!--
<div id="blinkCount" style="font-size:22px">0</div>
<div id="headMoves" style="font-size:22px">0</div>
<div id="faceScore" style="font-size:22px">0.00</div>
<div id="fps" style="font-size:22px">0</div>
-->
<div class="sr-only">
<input id="earThresh" type="number" min="0" max="1" step="0.01" value="0.21">
<input id="closedFrames" type="number" min="1" max="15" step="1" value="3">
<input id="moveThresh" type="number" min="0" max="30" step="0.5" value="6">
<!-- <input id="proofNeeded" type="number" min="1" max="10" step="1" value="3"> -->
<input id="proofNeeded" type="number" min="1" max="10" step="1" value="2">
</div>
</div>
<!-- </main> -->
<!-- MediaPipe Tasks Vision (web) -->
<script type="module">
// -- Dépendances MediaPipe Tasks Vision
import {
FilesetResolver,
FaceLandmarker,
DrawingUtils
} from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.js";
// URLs modèles (hébergés par Google)
const MP_FACE_TASK = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
// Références DOM
const video = document.getElementById('video');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const camStatus = document.getElementById('camStatus');
const mpStatus = document.getElementById('mpStatus');
const blinkCountEl = document.getElementById('blinkCount');
const headMovesEl = document.getElementById('headMoves');
const faceScoreEl = document.getElementById('faceScore');
const fpsEl = document.getElementById('fps');
const liveBadge = document.getElementById('liveBadge');
const liveMeter = document.getElementById('liveMeter');
const earThreshEl = document.getElementById('earThresh');
const closedFramesEl= document.getElementById('closedFrames');
const moveThreshEl = document.getElementById('moveThresh');
const proofNeededEl = document.getElementById('proofNeeded');
// État
let running = false;
let faceLandmarker; // modèle
let lastVideoTime = -1;
let rafId = null;
// Métriques liveness
let blinkCount = 0;
let closedConsec = 0;
let lastEAR = 1;
let headMoves = 0;
let lastYaw = null, lastRoll = null;
let facePresenceScore = 0;
let livenessScore = 0; // somme pondérée des preuves
let lastFpsT = performance.now();
let frames = 0;
// Indices MediaPipe FaceMesh pour calcul EAR (6 points par œil)
// Schéma: EAR = (||p2-p6|| + ||p3-p5||) / (2*||p1-p4||)
const LEFT_EYE = [33,160,158,133,153,144];
const RIGHT_EYE = [263,387,385,362,380,373];
const distance = (a,b)=> Math.hypot(a.x-b.x, a.y-b.y);
function eyeEAR(landmarks, idxs){
const [p1,p2,p3,p4,p5,p6] = idxs.map(i=>landmarks[i]);
const vert = distance(p2,p6) + distance(p3,p5);
const horiz= distance(p1,p4)*2;
return horiz>0 ? (vert/horiz) : 0;
}
function estimateYawRoll(landmarks){
// yaw ~ orientation horizontale via ligne yeux, roll ~ inclinaison de la tête
const left = avgPoint([33,133].map(i=>landmarks[i]));
const right= avgPoint([263,362].map(i=>landmarks[i]));
const dx = right.x - left.x;
const dy = right.y - left.y;
const roll = -rad2deg(Math.atan2(dy, dx));
// yaw approximé par asymétrie distance nez-centres yeux
const nose = landmarks[1] || landmarks[4];
const midEye = {x:(left.x+right.x)/2, y:(left.y+right.y)/2};
const yaw = rad2deg(Math.atan2(nose.x - midEye.x, 0.5)); // pseudo-yaw basé sur décalage nez
return {yaw, roll};
}
const rad2deg = r=> r*180/Math.PI;
const avgPoint = (pts)=>({x: pts.reduce((s,p)=>s+p.x,0)/pts.length, y: pts.reduce((s,p)=>s+p.y,0)/pts.length});
function updateLiveUI(){
blinkCountEl.textContent = String(blinkCount);
headMovesEl.textContent = String(headMoves);
faceScoreEl.textContent = facePresenceScore.toFixed(2);
// Score simple: 1 point par clignement (max 2), 1 point par move tête (max 2), +1 si présence stable (>0.5)
const maxBlinkPoints = Math.min(blinkCount, 2);
const maxMovePoints = Math.min(headMoves, 2);
const presencePoint = facePresenceScore > 0.5 ? 1 : 0;
livenessScore = maxBlinkPoints + maxMovePoints + presencePoint;
const proofNeeded = Number(proofNeededEl.value);
const pct = Math.min(100, Math.round(100*livenessScore/Math.max(1,proofNeeded)));
liveMeter.style.width = pct + '%';
const div_ebene = document.getElementById("div_ebene");
if(livenessScore >= proofNeeded){
liveBadge.className = 'pill ok';
liveBadge.textContent = 'Vivant confirmé';
// alert('Vivant confirmé');
stop();
// $("#div_ebene").fadeIn();
// $("#div_ebene").show();
// div_ebene.style.display = "block";
// div_ebene.classList.remove("hidden");
} else if (livenessScore>0){
liveBadge.className = 'pill warn';
liveBadge.textContent = 'Indices de vivacité';
} else {
liveBadge.className = 'pill bad';
liveBadge.textContent = 'Non vérifié';
}
}
async function loadModel(){
mpStatus.textContent = 'Modèle : chargement…';
const filesetResolver = await FilesetResolver.forVisionTasks(
// wasm path (CDN jsDelivr) — laisse MediaPipe gérer les dépendances
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.11/wasm'
);
faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver,{
baseOptions: { modelAssetPath: MP_FACE_TASK },
runningMode: 'VIDEO',
numFaces: 1,
outputFaceBlendshapes: false,
outputFacialTransformationMatrixes: true
});
mpStatus.textContent = 'Modèle : prêt';
}
async function start(){
try{
btnStart.disabled = true;
await loadModel();
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: {ideal: 1280}, height:{ideal:720} },
audio: false
});
video.srcObject = stream;
await video.play();
camStatus.textContent = 'Caméra : OK';
running = true;
canvas.width = video.videoWidth || 1280;
canvas.height= video.videoHeight|| 720;
btnStop.disabled = false;
lastVideoTime = -1;
loop();
}catch(err){
console.error(err);
camStatus.textContent = 'Caméra : échec ('+ (err?.name||'Erreur') +')';
btnStart.disabled = false;
}
}
function stop(){
running = false;
if(rafId) cancelAnimationFrame(rafId);
const stream = video.srcObject;
if(stream){
stream.getTracks().forEach(t=>t.stop());
}
video.srcObject = null;
btnStart.disabled = false;
btnStop.disabled = true;
camStatus.textContent = 'Caméra : inactif';
mpStatus.textContent = faceLandmarker ? 'Modèle : prêt' : 'Modèle : non chargé';
}
function drawLandmarks(landmarks){
ctx.clearRect(0,0,canvas.width, canvas.height);
const drawUtils = new DrawingUtils(ctx);
// Points clés yeux + traits simples
const eyePts = [...LEFT_EYE, ...RIGHT_EYE];
drawUtils.drawLandmarks(landmarks.filter((_,i)=> eyePts.includes(i)), {lineWidth: 2, color: '#22c55e', radius: 2});
}
function loop(){
if(!running) return;
const startT = performance.now();
const nowVideoTime = video.currentTime;
if(nowVideoTime !== lastVideoTime){
lastVideoTime = nowVideoTime;
const res = faceLandmarker.detectForVideo(video, performance.now());
const faces = res.faceLandmarks || [];
if(faces.length){
const lm = faces[0];
drawLandmarks(lm);
// EAR pour chaque œil
const earL = eyeEAR(lm, LEFT_EYE);
const earR = eyeEAR(lm, RIGHT_EYE);
const ear = (earL + earR)/2;
const earThresh = Number(earThreshEl.value);
const closedNeeded = Number(closedFramesEl.value);
if(ear < earThresh){
closedConsec++;
} else {
if(closedConsec >= closedNeeded){
blinkCount++;
}
closedConsec = 0;
}
// Head movement (yaw/roll change)
const {yaw, roll} = estimateYawRoll(lm);
const moveThresh = Number(moveThreshEl.value);
if(lastYaw!==null && lastRoll!==null){
const dYaw = Math.abs(yaw - lastYaw);
const dRoll= Math.abs(roll - lastRoll);
if(dYaw > moveThresh || dRoll > moveThresh){
headMoves++;
}
}
lastYaw = yaw; lastRoll = roll;
// Présence visage (proxy via nb de points vs bruit)
facePresenceScore = 0.7; // simplifié: présent
} else {
ctx.clearRect(0,0,canvas.width, canvas.height);
facePresenceScore = 0.0;
}
// MAJ UI
updateLiveUI();
}
// FPS approx
frames++;
const t = performance.now();
if(t - lastFpsT > 1000){
fpsEl.textContent = String(frames);
frames = 0; lastFpsT = t;
}
rafId = requestAnimationFrame(loop);
const endT = performance.now();
// Option: utiliser endT-startT si besoin de profiler
}
btnStart.addEventListener('click', start);
btnStop.addEventListener('click', stop);
// Conseil : demander permission caméra immédiatement (optionnel)
// navigator.mediaDevices.getUserMedia({video:true}).then(s=>s.getTracks().forEach(t=>t.stop()));
</script>

View File

@ -1,321 +0,0 @@
<style>
:root { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
body { margin: 0; background: #0f172a; color: #e2e8f0; }
header { padding: 16px 24px; background: #111827; display:flex; align-items:center; gap:12px; }
h1 { font-size: 18px; margin: 0; }
main { display:grid; grid-template-columns: 1fr 360px; gap: 16px; padding: 16px; }
.stage { position: relative; aspect-ratio: 16/9; background: #111827; border: 1px solid #1f2937; border-radius: 12px; overflow: hidden; }
video, canvas { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.panel { background: #111827; border: 1px solid #1f2937; border-radius: 12px; padding: 16px; }
.row { display:flex; justify-content: space-between; align-items:center; margin: 8px 0; }
.pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px; border:1px solid #1f2937; font-size:12px; }
.ok { color:#10b981; }
.warn { color:#f59e0b; }
.bad { color:#ef4444; }
button { background:#1f2937; color:#e5e7eb; border:1px solid #374151; border-radius:10px; padding:10px 12px; cursor:pointer; }
button:disabled { opacity:.5; cursor:not-allowed; }
small { color:#94a3b8; }
.meter { height: 8px; border-radius: 999px; background:#0b1220; border:1px solid #1f2937; overflow:hidden; }
.meter > div { height: 100%; background: linear-gradient(90deg,#22c55e,#16a34a); width:0%; }
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:8px; }
.kpi { background:#0b1220; border:1px solid #1f2937; border-radius:10px; padding:10px; }
code { background:#0b1220; padding:2px 6px; border-radius:6px; }
</style>
<header>
<h1>Détection de vivacité (Liveness)</h1>
<div class="pill"><span>🎥</span><span id="camStatus">Caméra : inactif</span></div>
<div class="pill"><span>🧠</span><span id="mpStatus">Modèle : non chargé</span></div>
</header>
<main>
<section class="stage">
<video id="video" playsinline muted></video>
<canvas id="overlay"></canvas>
</section>
<aside class="panel">
<div class="row" style="margin-bottom:8px">
<button id="btnStart">Démarrer</button>
<button id="btnStop" disabled>Arrêter</button>
</div>
<div class="row">
<strong>Statut vivacité</strong>
<span id="liveBadge" class="pill bad">Non vérifié</span>
</div>
<div class="meter" style="margin:8px 0 16px">
<div id="liveMeter"></div>
</div>
<div class="grid">
<div class="kpi"><div>Clignements</div><div id="blinkCount" style="font-size:22px">0</div><small>EAR&lt;seuil</small></div>
<div class="kpi"><div>Mouvements tête</div><div id="headMoves" style="font-size:22px">0</div><small>yaw/roll Δ</small></div>
<div class="kpi"><div>Confiance visage</div><div id="faceScore" style="font-size:22px">0.00</div><small>presence score</small></div>
<div class="kpi"><div>FPS</div><div id="fps" style="font-size:22px">0</div><small>approx</small></div>
</div>
<hr style="border-color:#1f2937; margin:16px 0" />
<input class="sr-only" id="earThresh" type="number" min="0" max="1" step="0.01" value="0.21">
<input class="sr-only" id="closedFrames" type="number" min="1" max="15" step="1" value="3">
<input class="sr-only" id="moveThresh" type="number" min="0" max="30" step="0.5" value="6">
<input class="sr-only" id="proofNeeded" type="number" min="1" max="10" step="1" value="3">
<hr style="border-color:#1f2937; margin:16px 0" />
<p><strong>Conseils :</strong> placez-vous face caméra, éclairez le visage, clignez des yeux et tournez légèrement la tête.</p>
</aside>
</main>
<!-- MediaPipe Tasks Vision (web) -->
<script type="module">
// -- Dépendances MediaPipe Tasks Vision
import {
FilesetResolver,
FaceLandmarker,
DrawingUtils
} from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.js";
// URLs modèles (hébergés par Google)
const MP_FACE_TASK = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
// Références DOM
const video = document.getElementById('video');
const canvas = document.getElementById('overlay');
const ctx = canvas.getContext('2d');
const btnStart = document.getElementById('btnStart');
const btnStop = document.getElementById('btnStop');
const camStatus = document.getElementById('camStatus');
const mpStatus = document.getElementById('mpStatus');
const blinkCountEl = document.getElementById('blinkCount');
const headMovesEl = document.getElementById('headMoves');
const faceScoreEl = document.getElementById('faceScore');
const fpsEl = document.getElementById('fps');
const liveBadge = document.getElementById('liveBadge');
const liveMeter = document.getElementById('liveMeter');
const earThreshEl = document.getElementById('earThresh');
const closedFramesEl= document.getElementById('closedFrames');
const moveThreshEl = document.getElementById('moveThresh');
const proofNeededEl = document.getElementById('proofNeeded');
// État
let running = false;
let faceLandmarker; // modèle
let lastVideoTime = -1;
let rafId = null;
// Métriques liveness
let blinkCount = 0;
let closedConsec = 0;
let lastEAR = 1;
let headMoves = 0;
let lastYaw = null, lastRoll = null;
let facePresenceScore = 0;
let livenessScore = 0; // somme pondérée des preuves
let lastFpsT = performance.now();
let frames = 0;
// Indices MediaPipe FaceMesh pour calcul EAR (6 points par œil)
// Schéma: EAR = (||p2-p6|| + ||p3-p5||) / (2*||p1-p4||)
const LEFT_EYE = [33,160,158,133,153,144];
const RIGHT_EYE = [263,387,385,362,380,373];
const distance = (a,b)=> Math.hypot(a.x-b.x, a.y-b.y);
function eyeEAR(landmarks, idxs){
const [p1,p2,p3,p4,p5,p6] = idxs.map(i=>landmarks[i]);
const vert = distance(p2,p6) + distance(p3,p5);
const horiz= distance(p1,p4)*2;
return horiz>0 ? (vert/horiz) : 0;
}
function estimateYawRoll(landmarks){
// yaw ~ orientation horizontale via ligne yeux, roll ~ inclinaison de la tête
const left = avgPoint([33,133].map(i=>landmarks[i]));
const right= avgPoint([263,362].map(i=>landmarks[i]));
const dx = right.x - left.x;
const dy = right.y - left.y;
const roll = -rad2deg(Math.atan2(dy, dx));
// yaw approximé par asymétrie distance nez-centres yeux
const nose = landmarks[1] || landmarks[4];
const midEye = {x:(left.x+right.x)/2, y:(left.y+right.y)/2};
const yaw = rad2deg(Math.atan2(nose.x - midEye.x, 0.5)); // pseudo-yaw basé sur décalage nez
return {yaw, roll};
}
const rad2deg = r=> r*180/Math.PI;
const avgPoint = (pts)=>({x: pts.reduce((s,p)=>s+p.x,0)/pts.length, y: pts.reduce((s,p)=>s+p.y,0)/pts.length});
function updateLiveUI(){
blinkCountEl.textContent = String(blinkCount);
headMovesEl.textContent = String(headMoves);
faceScoreEl.textContent = facePresenceScore.toFixed(2);
// Score simple: 1 point par clignement (max 2), 1 point par move tête (max 2), +1 si présence stable (>0.5)
const maxBlinkPoints = Math.min(blinkCount, 2);
const maxMovePoints = Math.min(headMoves, 2);
const presencePoint = facePresenceScore > 0.5 ? 1 : 0;
livenessScore = maxBlinkPoints + maxMovePoints + presencePoint;
const proofNeeded = Number(proofNeededEl.value);
const pct = Math.min(100, Math.round(100*livenessScore/Math.max(1,proofNeeded)));
liveMeter.style.width = pct + '%';
if(livenessScore >= proofNeeded){
liveBadge.className = 'pill ok';
liveBadge.textContent = 'Vivant confirmé';
} else if (livenessScore>0){
liveBadge.className = 'pill warn';
liveBadge.textContent = 'Indices de vivacité';
} else {
liveBadge.className = 'pill bad';
liveBadge.textContent = 'Non vérifié';
}
}
async function loadModel(){
mpStatus.textContent = 'Modèle : chargement…';
const filesetResolver = await FilesetResolver.forVisionTasks(
// wasm path (CDN jsDelivr) — laisse MediaPipe gérer les dépendances
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.11/wasm'
);
faceLandmarker = await FaceLandmarker.createFromOptions(filesetResolver,{
baseOptions: { modelAssetPath: MP_FACE_TASK },
runningMode: 'VIDEO',
numFaces: 1,
outputFaceBlendshapes: false,
outputFacialTransformationMatrixes: true
});
mpStatus.textContent = 'Modèle : prêt';
}
async function start(){
try{
btnStart.disabled = true;
await loadModel();
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: {ideal: 1280}, height:{ideal:720} },
audio: false
});
video.srcObject = stream;
await video.play();
camStatus.textContent = 'Caméra : OK';
running = true;
canvas.width = video.videoWidth || 1280;
canvas.height= video.videoHeight|| 720;
btnStop.disabled = false;
lastVideoTime = -1;
loop();
}catch(err){
console.error(err);
camStatus.textContent = 'Caméra : échec ('+ (err?.name||'Erreur') +')';
btnStart.disabled = false;
}
}
function stop(){
running = false;
if(rafId) cancelAnimationFrame(rafId);
const stream = video.srcObject;
if(stream){
stream.getTracks().forEach(t=>t.stop());
}
video.srcObject = null;
btnStart.disabled = false;
btnStop.disabled = true;
camStatus.textContent = 'Caméra : inactif';
mpStatus.textContent = faceLandmarker ? 'Modèle : prêt' : 'Modèle : non chargé';
}
function drawLandmarks(landmarks){
ctx.clearRect(0,0,canvas.width, canvas.height);
const drawUtils = new DrawingUtils(ctx);
// Points clés yeux + traits simples
const eyePts = [...LEFT_EYE, ...RIGHT_EYE];
drawUtils.drawLandmarks(landmarks.filter((_,i)=> eyePts.includes(i)), {lineWidth: 2, color: '#22c55e', radius: 2});
}
function loop(){
if(!running) return;
const startT = performance.now();
const nowVideoTime = video.currentTime;
if(nowVideoTime !== lastVideoTime){
lastVideoTime = nowVideoTime;
const res = faceLandmarker.detectForVideo(video, performance.now());
const faces = res.faceLandmarks || [];
if(faces.length){
const lm = faces[0];
drawLandmarks(lm);
// EAR pour chaque œil
const earL = eyeEAR(lm, LEFT_EYE);
const earR = eyeEAR(lm, RIGHT_EYE);
const ear = (earL + earR)/2;
const earThresh = Number(earThreshEl.value);
const closedNeeded = Number(closedFramesEl.value);
if(ear < earThresh){
closedConsec++;
} else {
if(closedConsec >= closedNeeded){
blinkCount++;
}
closedConsec = 0;
}
// Head movement (yaw/roll change)
const {yaw, roll} = estimateYawRoll(lm);
const moveThresh = Number(moveThreshEl.value);
if(lastYaw!==null && lastRoll!==null){
const dYaw = Math.abs(yaw - lastYaw);
const dRoll= Math.abs(roll - lastRoll);
if(dYaw > moveThresh || dRoll > moveThresh){
headMoves++;
}
}
lastYaw = yaw; lastRoll = roll;
// Présence visage (proxy via nb de points vs bruit)
facePresenceScore = 0.7; // simplifié: présent
} else {
ctx.clearRect(0,0,canvas.width, canvas.height);
facePresenceScore = 0.0;
}
// MAJ UI
updateLiveUI();
}
// FPS approx
frames++;
const t = performance.now();
if(t - lastFpsT > 1000){
fpsEl.textContent = String(frames);
frames = 0; lastFpsT = t;
}
rafId = requestAnimationFrame(loop);
const endT = performance.now();
// Option: utiliser endT-startT si besoin de profiler
}
btnStart.addEventListener('click', start);
btnStop.addEventListener('click', stop);
// Conseil : demander permission caméra immédiatement (optionnel)
// navigator.mediaDevices.getUserMedia({video:true}).then(s=>s.getTracks().forEach(t=>t.stop()));
</script>
</body>
</html>

File diff suppressed because one or more lines are too long