You can use DISPL Player to detect a variation of your visitor's head angles (pitch and yaw), to detect nodding movements with their heads! This means that even without touch screen, it is possible to collect feedback from your customers! This is a very interesting project!
Before starting the next steps, make sure to prepare your DISPL Player and DISPL Content Management System to support JavaScript templates.
Follow these steps:
- Create an Interface Editor
- Add a WebFrame element, and change the "Type" to "HTML code".
- Paste the HTML code you developed, or paste the example code provided below. Press Apply.
- It is a good practice to hide the WebFrame element outside of the screen boundaries, so the customer cannot see it.
- Having the WebFrame element selected, add the desired effect that responds to the JS postMessage command received, for example: change scenes with the "go to" action.
- Insert the JS postMessage command expected, such as "NOD_NO" without quotes.
- Select the desired scene to change to. Apply changes.
- Test playback your template!
HTML Code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Face Direction and Nod Detector</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Inter', sans-serif;
background-color: #f0f4f8;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.container {
background-color: #ffffff;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 500px;
width: 100%;
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
font-size: 2.25rem; /* text-4xl */
font-weight: 700; /* font-bold */
}
p {
font-size: 1.125rem; /* text-lg */
color: #34495e;
margin-bottom: 10px;
}
#output-yaw, #output-pitch {
font-weight: 600; /* font-semibold */
color: #2980b9;
min-height: 25px; /* Ensure space for text */
}
#nod-no-status {
font-size: 1.5rem; /* text-2xl */
color: #e74c3c; /* Red for "No" */
margin-top: 15px;
font-weight: 700;
}
#nod-yes-status {
font-size: 1.5rem; /* text-2xl */
color: #27ae60; /* Green for "Yes" */
margin-top: 10px;
font-weight: 700;
}
.error-message {
color: #e74c3c;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<h1>Face Direction and Nod Detector</h1>
<p id="output-yaw" class="text-lg font-semibold text-blue-700">Yaw Direction: Initializing...</p>
<p id="output-pitch" class="text-lg font-semibold text-blue-700">Pitch Direction: Initializing...</p>
<p id="nod-no-status" class="text-2xl font-bold text-red-600"></p>
<p id="nod-yes-status" class="text-2xl font-bold text-green-600"></p>
</div>
<script>
// Constants for "No" nod (horizontal) detection
const YAW_VARIATION_THRESHOLD = 5; // Degrees of yaw change to consider a movement
const NO_NOD_TIME_WINDOW_MS = 1500; // Time window for detecting a full "no" shake
const REQUIRED_YAW_CHANGES = 2; // Number of significant alternating direction changes
// State variables for "No" nod detection
let yawDirectionSequence = []; // Stores sequence of 'left'/'right' movements with timestamps
let noNodCount = 0; // Counter for detected "no" nods
let prevYaw = null; // Store the previous yaw value
// Constants for "Yes" nod (vertical) detection
const PITCH_VARIATION_THRESHOLD = 5; // Degrees of pitch change to consider a movement
const YES_NOD_TIME_WINDOW_MS = 1500; // Time window for detecting a full "yes" nod
const REQUIRED_PITCH_CHANGES = 2; // Number of significant alternating direction changes
// State variables for "Yes" nod detection
let pitchDirectionSequence = []; // Stores sequence of 'up'/'down' movements with timestamps
let yesNodCount = 0; // Counter for detected "yes" nods
let prevPitch = null; // Store the previous pitch value
/**
* Determines the current horizontal face direction based on yaw angle variation.
* Only returns a direction if there's a significant movement from the previous state.
* @param {number} currentYaw - The current yaw angle of the face.
* @param {number} previousYaw - The previous yaw angle of the face.
* @returns {string|null} 'LOOK_LEFT', 'LOOK_RIGHT', or null if no significant movement.
*/
function getHorizontalDirection(currentYaw, previousYaw) {
if (previousYaw === null) return null; // Can't determine direction without a previous value
const yawChange = currentYaw - previousYaw;
if (yawChange < -YAW_VARIATION_THRESHOLD) {
return 'LOOK_LEFT'; // Moved significantly to the left
} else if (yawChange > YAW_VARIATION_THRESHOLD) {
return 'LOOK_RIGHT'; // Moved significantly to the right
}
return null; // Not a significant horizontal movement
}
/**
* Determines the current vertical face direction based on pitch angle variation.
* Only returns a direction if there's a significant movement from the previous state.
* @param {number} currentPitch - The current pitch angle of the face.
* @param {number} previousPitch - The previous pitch angle of the face.
* @returns {string|null} 'LOOK_UP', 'LOOK_DOWN', or null if no significant movement.
*/
function getVerticalDirection(currentPitch, previousPitch) {
if (previousPitch === null) return null; // Can't determine direction without a previous value
const pitchChange = currentPitch - previousPitch;
if (pitchChange > PITCH_VARIATION_THRESHOLD) {
return 'LOOK_UP'; // Moved significantly up (pitch value increased)
} else if (pitchChange < -PITCH_VARIATION_THRESHOLD) {
return 'LOOK_DOWN'; // Moved significantly down (pitch value decreased)
}
return null; // Not a significant vertical movement
}
/**
* Checks the face position by fetching data from the recognition API.
*/
function checkFacePosition() {
fetch('http://localhost:55080/v1/recognition', {
headers: {
'X-PIN-code': '2693'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => {
const outputYawElement = document.getElementById('output-yaw');
const outputPitchElement = document.getElementById('output-pitch');
const nodNoStatusElement = document.getElementById('nod-no-status');
const nodYesStatusElement = document.getElementById('nod-yes-status');
const faces = data.faces;
if (Array.isArray(faces) && faces.length > 0) {
const currentYaw = faces[0].yaw;
const currentPitch = faces[0].pitch;
if (typeof currentYaw === 'number' && typeof currentPitch === 'number') {
// Initialize prevYaw and prevPitch if they are null
if (prevYaw === null) {
prevYaw = currentYaw;
}
if (prevPitch === null) {
prevPitch = currentPitch;
}
// --- Horizontal (Yaw) Direction and "No" Nod Detection ---
const newHorizontalDirection = getHorizontalDirection(currentYaw, prevYaw);
outputYawElement.textContent = `Yaw Direction: ${newHorizontalDirection || 'NO_SIGNIFICANT_CHANGE'} (Yaw: ${currentYaw.toFixed(2)})`;
if (newHorizontalDirection) {
const lastSequenceEntry = yawDirectionSequence.length > 0 ? yawDirectionSequence[yawDirectionSequence.length - 1] : null;
const lastSequenceDirection = lastSequenceEntry ? lastSequenceEntry.direction : null;
if (newHorizontalDirection !== lastSequenceDirection) {
yawDirectionSequence.push({ direction: newHorizontalDirection, timestamp: Date.now() });
}
}
// Clean up old direction changes (outside the time window)
while (yawDirectionSequence.length > 0 &&
(Date.now() - yawDirectionSequence[0].timestamp > NO_NOD_TIME_WINDOW_MS)) {
yawDirectionSequence.shift();
}
// Check for "no" nod pattern (alternating directions)
if (yawDirectionSequence.length >= REQUIRED_YAW_CHANGES + 1) {
const firstMove = yawDirectionSequence[0];
const lastMove = yawDirectionSequence[yawDirectionSequence.length - 1];
if ((lastMove.timestamp - firstMove.timestamp) <= NO_NOD_TIME_WINDOW_MS) {
let isAlternating = true;
for (let i = 0; i < yawDirectionSequence.length - 1; i++) {
const dir1 = yawDirectionSequence[i].direction;
const dir2 = yawDirectionSequence[i+1].direction;
if (!((dir1 === 'LOOK_LEFT' && dir2 === 'LOOK_RIGHT') ||
(dir1 === 'LOOK_RIGHT' && dir2 === 'LOOK_LEFT'))) {
isAlternating = false;
break;
}
}
if (isAlternating) {
noNodCount++;
nodNoStatusElement.textContent = `NO NOD DETECTED! Total: ${noNodCount}`;
// *** ADDED postMessage for NO_NOD ***
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage("NOD_NO");
}
yawDirectionSequence = []; // Clear sequence to prevent re-triggering
}
}
}
// Send simple horizontal direction messages
if (prevYaw !== null) {
if (currentYaw < prevYaw - (YAW_VARIATION_THRESHOLD / 2)) {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage("LOOK_LEFT");
}
} else if (currentYaw > prevYaw + (YAW_VARIATION_THRESHOLD / 2)) {
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage("LOOK_RIGHT");
}
}
}
// --- Vertical (Pitch) Direction and "Yes" Nod Detection ---
const newVerticalDirection = getVerticalDirection(currentPitch, prevPitch);
outputPitchElement.textContent = `Pitch Direction: ${newVerticalDirection || 'NO_SIGNIFICANT_CHANGE'} (Pitch: ${currentPitch.toFixed(2)})`;
if (newVerticalDirection) {
const lastSequenceEntry = pitchDirectionSequence.length > 0 ? pitchDirectionSequence[pitchDirectionSequence.length - 1] : null;
const lastSequenceDirection = lastSequenceEntry ? lastSequenceEntry.direction : null;
if (newVerticalDirection !== lastSequenceDirection) {
pitchDirectionSequence.push({ direction: newVerticalDirection, timestamp: Date.now() });
}
}
// Clean up old direction changes (outside the time window)
while (pitchDirectionSequence.length > 0 &&
(Date.now() - pitchDirectionSequence[0].timestamp > YES_NOD_TIME_WINDOW_MS)) {
pitchDirectionSequence.shift();
}
// Check for "yes" nod pattern (alternating directions)
if (pitchDirectionSequence.length >= REQUIRED_PITCH_CHANGES + 1) {
const firstMove = pitchDirectionSequence[0];
const lastMove = pitchDirectionSequence[pitchDirectionSequence.length - 1];
if ((lastMove.timestamp - firstMove.timestamp) <= YES_NOD_TIME_WINDOW_MS) {
let isAlternating = true;
for (let i = 0; i < pitchDirectionSequence.length - 1; i++) {
const dir1 = pitchDirectionSequence[i].direction;
const dir2 = pitchDirectionSequence[i+1].direction;
if (!((dir1 === 'LOOK_UP' && dir2 === 'LOOK_DOWN') ||
(dir1 === 'LOOK_DOWN' && dir2 === 'LOOK_UP'))) {
isAlternating = false;
break;
}
}
if (isAlternating) {
yesNodCount++;
nodYesStatusElement.textContent = `YES NOD DETECTED! Total: ${yesNodCount}`;
// *** ADDED postMessage for NOD_YES ***
if (window.chrome && window.chrome.webview) {
window.chrome.webview.postMessage("NOD_YES");
}
pitchDirectionSequence = []; // Clear sequence to prevent re-triggering
}
}
}
// Update previous values for the next iteration
prevYaw = currentYaw;
prevPitch = currentPitch;
} else {
outputYawElement.textContent = 'Invalid face data: Yaw or Pitch not a number.';
outputYawElement.classList.add('error-message');
outputPitchElement.textContent = '';
}
} else {
outputYawElement.textContent = 'No face detected.';
outputPitchElement.textContent = '';
nodNoStatusElement.textContent = '';
nodYesStatusElement.textContent = '';
yawDirectionSequence = [];
prevYaw = null;
pitchDirectionSequence = [];
prevPitch = null;
}
})
.catch(error => {
const outputYawElement = document.getElementById('output-yaw');
outputYawElement.textContent = 'Error: ' + error.message;
outputYawElement.classList.add('error-message');
document.getElementById('output-pitch').textContent = '';
document.getElementById('nod-no-status').textContent = '';
document.getElementById('nod-yes-status').textContent = '';
yawDirectionSequence = [];
prevYaw = null;
pitchDirectionSequence = [];
prevPitch = null;
});
}
setInterval(checkFacePosition, 100);
</script>
</body>
</html>
Comments
0 comments
Please sign in to leave a comment.