|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
|
|
|
<head></head> |
|
|
<meta charset="UTF-8"> |
|
|
|
|
|
|
|
|
<title>Sign Language Interpreter</title> |
|
|
|
|
|
|
|
|
<script> |
|
|
window.console = window.console || function (t) { }; |
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" type="text/css" href="static/browser_detect.css" /> |
|
|
|
|
|
|
|
|
</head> |
|
|
|
|
|
<body translate="no"> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" |
|
|
crossorigin="anonymous"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-cpu"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite/dist/tf-tflite.min.js"></script> |
|
|
|
|
|
|
|
|
<div class="container"> |
|
|
|
|
|
<video id="webcam" style="display:none" autoplay="" playsinline=""></video> |
|
|
<div class="canvas_wrapper" id="canvas_wrapper"> |
|
|
<button id="switch-camera" style="display:none; position: absolute; top:10px; left:10px; padding:5px; height:40px; width:40px; text-align: center; border-radius: 12.25px; font-size: 20px; font-weight: 900; border:none; background-color: #f2f2f2; color:black; |
|
|
box-shadow: 0px 4px 20px 4px rgba(0, 0, 0, 0.38); z-index:100"> |
|
|
<span>⟳</span> |
|
|
</button> |
|
|
<canvas class="output_canvas" id="output_canvas" width="100%" height="300%"></canvas> |
|
|
<center> |
|
|
<button id="webcamButton" style="font-weight: 600; color:black;"> |
|
|
<span>Enable Webcam</span> |
|
|
</button> |
|
|
</center> |
|
|
</div> |
|
|
</div> |
|
|
<center> |
|
|
<img id="output_image" style="display:none"></img> |
|
|
<div class="wrapper_result"> |
|
|
<div id="predicted_result">></div> |
|
|
</div> |
|
|
<div class="wrapper_text"> |
|
|
<textarea id="text" onkeyup="set_output_array(this.value)"></textarea> |
|
|
<button id="text-to-speech" onclick="speak(document.getElementById('text').value)"> |
|
|
<span>Listen 🔊</span> |
|
|
</button> |
|
|
|
|
|
</div> |
|
|
<center> |
|
|
<script> |
|
|
|
|
|
const originalFetch = window.fetch; |
|
|
|
|
|
|
|
|
window.fetch = async function (input, init) { |
|
|
|
|
|
const url = typeof input === 'string' ? input : input.url; |
|
|
var newUrl = url |
|
|
if (url == 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm/vision_wasm_internal.wasm') { |
|
|
|
|
|
newUrl = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm/vision_wasm_internal.wasm' |
|
|
|
|
|
} |
|
|
console.log("This was FETCHED: ", newUrl) |
|
|
|
|
|
return originalFetch(newUrl, init); |
|
|
}; |
|
|
|
|
|
|
|
|
var synthesis = window.speechSynthesis; |
|
|
|
|
|
if ('speechSynthesis' in window) { |
|
|
|
|
|
var synthesis = window.speechSynthesis; |
|
|
|
|
|
|
|
|
var voice = synthesis.getVoices().filter(function (voice) { |
|
|
return voice.lang === 'en'; |
|
|
})[0]; |
|
|
|
|
|
|
|
|
|
|
|
} else { |
|
|
speechSupported = false; |
|
|
console.log('Text-to-speech not supported.'); |
|
|
} |
|
|
|
|
|
function speak(text) { |
|
|
|
|
|
if (!speechSupported) { |
|
|
const audioPlayer = document.getElementById('audioPlayer'); |
|
|
if (prevSpeech != text) { |
|
|
prevSpeech = text |
|
|
audioPlayer.src = 'http://127.0.0.1:8125/speech?t=' + text; |
|
|
console.log("Set src: ", audioPlayer.src) |
|
|
} |
|
|
|
|
|
audioPlayer.play() |
|
|
.then(() => { |
|
|
|
|
|
console.log('Audio is playing'); |
|
|
}) |
|
|
.catch(error => { |
|
|
console.error('Error playing audio:', error); |
|
|
prevSpeech = '' |
|
|
}); |
|
|
} else if ('speechSynthesis' in window) { |
|
|
var utterance = new SpeechSynthesisUtterance(text); |
|
|
utterance.voice = voice; |
|
|
utterance.pitch = 0.6; |
|
|
utterance.rate = 0.8; |
|
|
utterance.volume = 0.8; |
|
|
synthesis.speak(utterance); |
|
|
} else { |
|
|
console.log("Text to speech is now not supported") |
|
|
} |
|
|
} |
|
|
var word_list = [] |
|
|
|
|
|
|
|
|
function set_output_array(text) { |
|
|
console.log(text) |
|
|
word_list = text.split(""); |
|
|
console.log(word_list) |
|
|
} |
|
|
</script> |
|
|
|
|
|
<script type="module"> |
|
|
|
|
|
import { HandLandmarker, FilesetResolver } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0"; |
|
|
let handLandmarker = undefined; |
|
|
let runningMode = "IMAGE"; |
|
|
let enableWebcamButton; |
|
|
let webcamRunning = false; |
|
|
var time_since_letter = 0 |
|
|
var last_letter_time = 0 |
|
|
var is_first_run = 1 |
|
|
|
|
|
|
|
|
|
|
|
const createHandLandmarker = async () => { |
|
|
const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm"); |
|
|
handLandmarker = await HandLandmarker.createFromOptions(vision, { |
|
|
baseOptions: { |
|
|
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`, |
|
|
delegate: "GPU" |
|
|
}, |
|
|
runningMode: runningMode, |
|
|
numHands: 1 |
|
|
}); |
|
|
}; |
|
|
createHandLandmarker(); |
|
|
|
|
|
const MODEL_PATH = "/exported" |
|
|
var objectDetector = tflite.loadTFLiteModel(MODEL_PATH); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var global_res = 0; |
|
|
const video = document.getElementById("webcam"); |
|
|
const canvasElement = document.getElementById("output_canvas"); |
|
|
const canvasCtx = canvasElement.getContext("2d"); |
|
|
var x_array = [] |
|
|
var y_array = [] |
|
|
var video_facing_mode = "user" |
|
|
|
|
|
const hasGetUserMedia = () => { var _a; return !!((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia); }; |
|
|
|
|
|
|
|
|
if (hasGetUserMedia()) { |
|
|
enableWebcamButton = document.getElementById("webcamButton"); |
|
|
enableWebcamButton.addEventListener("click", enableCam); |
|
|
document.getElementById("switch-camera").addEventListener("click", switch_camera); |
|
|
} |
|
|
else { |
|
|
console.warn("getUserMedia() is not supported by your browser"); |
|
|
} |
|
|
async function switch_camera() { |
|
|
if (video_facing_mode == 'user') { |
|
|
webcamRunning = false |
|
|
video_facing_mode = 'environment' |
|
|
await load_camera() |
|
|
webcamRunning = true |
|
|
} |
|
|
else { |
|
|
webcamRunning = false |
|
|
video_facing_mode = 'user' |
|
|
await load_camera() |
|
|
webcamRunning = true |
|
|
} |
|
|
} |
|
|
|
|
|
function enableCam(event) { |
|
|
if (!handLandmarker) { |
|
|
console.log("Wait! objectDetector not loaded yet."); |
|
|
return; |
|
|
} |
|
|
if (webcamRunning === true) { |
|
|
webcamRunning = false; |
|
|
enableWebcamButton.innerText = "ENABLE PREDICTIONS"; |
|
|
} |
|
|
else { |
|
|
webcamRunning = true; |
|
|
enableWebcamButton.style = "display:none" |
|
|
document.getElementById("switch-camera").style.display = "block" |
|
|
|
|
|
} |
|
|
|
|
|
load_camera() |
|
|
} |
|
|
function load_camera() { |
|
|
const constraints = { |
|
|
video: { |
|
|
facingMode: video_facing_mode |
|
|
} |
|
|
}; |
|
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints).then((stream) => { |
|
|
video.srcObject = stream; |
|
|
video.addEventListener("loadeddata", predictWebcam); |
|
|
}); |
|
|
} |
|
|
let lastVideoTime = -1; |
|
|
let results = undefined; |
|
|
console.log(video); |
|
|
async function predictWebcam() { |
|
|
if (video.videoHeight == 0) { |
|
|
return |
|
|
} |
|
|
canvasElement.width = window.innerWidth; |
|
|
|
|
|
if (runningMode === "IMAGE") { |
|
|
runningMode = "VIDEO"; |
|
|
await handLandmarker.setOptions({ runningMode: "VIDEO" }); |
|
|
} |
|
|
let startTimeMs = performance.now(); |
|
|
if (lastVideoTime !== video.currentTime) { |
|
|
lastVideoTime = video.currentTime; |
|
|
results = handLandmarker.detectForVideo(video, startTimeMs); |
|
|
} |
|
|
canvasCtx.save(); |
|
|
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height); |
|
|
canvasCtx.drawImage(video, 0, 0, canvasElement.width, (video.videoHeight / video.videoWidth) * canvasElement.width) |
|
|
if (is_first_run == 1) { |
|
|
var elem_rect = document.getElementById("output_canvas").getBoundingClientRect() |
|
|
console.log(elem_rect.height | 0); |
|
|
document.getElementById("canvas_wrapper").style.height = (elem_rect.height | 0).toString() + "px" |
|
|
|
|
|
is_first_run = 0 |
|
|
} |
|
|
|
|
|
if (results.landmarks && results.handednesses[0]) { |
|
|
var current_time = Math.round(Date.now()) |
|
|
document.getElementById("predicted_result").style.width = String((current_time - last_letter_time) / 10) + "%" |
|
|
if (results.handednesses[0][0].categoryName == "Left") { |
|
|
annotateImage() |
|
|
console.log("LEFT") |
|
|
|
|
|
} else { |
|
|
console.log("RIGHT") |
|
|
var current_result = "_" |
|
|
var previous_result = document.getElementById("predicted_result").innerText |
|
|
document.getElementById("predicted_result").innerText = current_result |
|
|
|
|
|
|
|
|
if (previous_result == current_result) { |
|
|
if (current_time - last_letter_time > 1000) { |
|
|
last_letter_time = current_time |
|
|
word_list.push(" ") |
|
|
console.log(word_list) |
|
|
document.getElementById("text").value = word_list.join('') |
|
|
} |
|
|
} |
|
|
else { |
|
|
last_letter_time = current_time |
|
|
} |
|
|
} |
|
|
} |
|
|
else { |
|
|
if (30 > calculateCanvasBrightness(canvasElement)) { |
|
|
|
|
|
var current_result = "<" |
|
|
var previous_result = document.getElementById("predicted_result").innerText |
|
|
document.getElementById("predicted_result").innerText = current_result |
|
|
var current_time = Math.round(Date.now()) |
|
|
console.log(current_time - last_letter_time) |
|
|
if (previous_result == current_result) { |
|
|
if (current_time - last_letter_time > 400) { |
|
|
last_letter_time = current_time |
|
|
word_list.pop() |
|
|
console.log(word_list) |
|
|
document.getElementById("text").value = word_list.join('') |
|
|
} |
|
|
} |
|
|
else { |
|
|
last_letter_time = current_time |
|
|
} |
|
|
} else { |
|
|
last_letter_time = Math.round(Date.now()) |
|
|
|
|
|
document.getElementById("predicted_result").style.width = String(0) + "%" |
|
|
} |
|
|
} |
|
|
|
|
|
canvasCtx.restore(); |
|
|
|
|
|
if (webcamRunning === true) { |
|
|
window.requestAnimationFrame(predictWebcam); |
|
|
} |
|
|
} |
|
|
function annotateImage() { |
|
|
|
|
|
|
|
|
if (results.landmarks[0]) { |
|
|
x_array = [] |
|
|
y_array = [] |
|
|
results.landmarks[0].forEach(iterate) |
|
|
|
|
|
var image_height = (video.videoHeight / video.videoWidth) * canvasElement.width |
|
|
var image_width = canvasElement.width |
|
|
var min_x = Math.min(...x_array) * image_width |
|
|
var min_y = Math.min(...y_array) * image_height |
|
|
var max_x = Math.max(...x_array) * image_width |
|
|
var max_y = Math.max(...y_array) * image_height |
|
|
|
|
|
var sect_height = max_y - (min_y) |
|
|
var sect_width = max_x - (min_x) |
|
|
var center_x = (min_x + max_x) / 2 |
|
|
var center_y = (min_y + max_y) / 2 |
|
|
|
|
|
var sect_diameter = 50 |
|
|
if (sect_height > sect_width) { |
|
|
sect_diameter = sect_height |
|
|
|
|
|
} |
|
|
if (sect_height < sect_width) { |
|
|
sect_diameter = sect_width |
|
|
|
|
|
} |
|
|
|
|
|
sect_diameter = sect_diameter + 50 |
|
|
var sect_radius = sect_diameter / 2 |
|
|
var crop_top = center_y - sect_radius |
|
|
var crop_bottom = center_y + sect_radius |
|
|
var crop_left = center_x - sect_radius |
|
|
var crop_right = center_x + sect_radius |
|
|
if (crop_top < 0) { |
|
|
crop_top = 0 |
|
|
} |
|
|
if (crop_left < 0) { |
|
|
crop_left = 0 |
|
|
} |
|
|
if (crop_right > image_width) { |
|
|
crop_right = image_width |
|
|
} |
|
|
if (crop_bottom > image_height) { |
|
|
crop_bottom = image_height |
|
|
} |
|
|
|
|
|
canvasCtx.beginPath(); |
|
|
canvasCtx.rect(crop_left, crop_top, crop_right - crop_left, crop_bottom - crop_top); |
|
|
canvasCtx.stroke(); |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const landmarks = results.landmarks; |
|
|
if (landmarks[0]) { |
|
|
var hand = landmarks[0] |
|
|
|
|
|
|
|
|
drawConnection(hand[4], hand[3], '#ffe5b4', 5); |
|
|
drawConnection(hand[3], hand[2], '#ffe5b4', 5); |
|
|
drawConnection(hand[2], hand[1], '#ffe5b4', 5); |
|
|
|
|
|
|
|
|
drawConnection(hand[8], hand[7], '#804080', 5); |
|
|
drawConnection(hand[7], hand[6], '#804080', 5); |
|
|
drawConnection(hand[6], hand[5], '#804080', 5); |
|
|
|
|
|
|
|
|
drawConnection(hand[12], hand[11], '#ffcc00', 5); |
|
|
drawConnection(hand[11], hand[10], '#ffcc00', 5); |
|
|
drawConnection(hand[10], hand[9], '#ffcc00', 5); |
|
|
|
|
|
|
|
|
drawConnection(hand[16], hand[15], '#30ff30', 5); |
|
|
drawConnection(hand[15], hand[14], '#30ff30', 5); |
|
|
drawConnection(hand[14], hand[13], '#30ff30', 5); |
|
|
|
|
|
|
|
|
drawConnection(hand[20], hand[19], '#1565c0', 5); |
|
|
drawConnection(hand[19], hand[18], '#1565c0', 5); |
|
|
drawConnection(hand[18], hand[17], '#1565c0', 5); |
|
|
|
|
|
drawConnection(hand[0], hand[1], '#808080', 5); |
|
|
drawConnection(hand[0], hand[5], '#808080', 5); |
|
|
drawConnection(hand[0], hand[17], '#808080', 5); |
|
|
drawConnection(hand[5], hand[9], '#808080', 5); |
|
|
drawConnection(hand[9], hand[13], '#808080', 5); |
|
|
drawConnection(hand[13], hand[17], '#808080', 5); |
|
|
|
|
|
|
|
|
drawLandmarks(canvasCtx, hand[2], '#ffe5b4'); |
|
|
drawLandmarks(canvasCtx, hand[3], '#ffe5b4'); |
|
|
drawLandmarks(canvasCtx, hand[4], '#ffe5b4'); |
|
|
|
|
|
|
|
|
drawLandmarks(canvasCtx, hand[6], '#804080'); |
|
|
drawLandmarks(canvasCtx, hand[7], '#804080'); |
|
|
drawLandmarks(canvasCtx, hand[8], '#804080'); |
|
|
|
|
|
|
|
|
drawLandmarks(canvasCtx, hand[10], '#ffcc00'); |
|
|
drawLandmarks(canvasCtx, hand[11], '#ffcc00'); |
|
|
drawLandmarks(canvasCtx, hand[12], '#ffcc00'); |
|
|
|
|
|
|
|
|
drawLandmarks(canvasCtx, hand[14], '#30ff30'); |
|
|
drawLandmarks(canvasCtx, hand[15], '#30ff30'); |
|
|
drawLandmarks(canvasCtx, hand[16], '#30ff30'); |
|
|
|
|
|
|
|
|
drawLandmarks(canvasCtx, hand[18], '#1565c0'); |
|
|
drawLandmarks(canvasCtx, hand[19], '#1565c0'); |
|
|
drawLandmarks(canvasCtx, hand[20], '#1565c0'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[0], '#ff3030'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[1], '#ff3030'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[5], '#ff3030'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[9], '#ff3030'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[13], '#ff3030'); |
|
|
|
|
|
drawLandmarks(canvasCtx, hand[17], '#ff3030'); |
|
|
cropCanvas(canvasElement, crop_left, crop_top, crop_right - crop_left, crop_bottom - crop_top) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function iterate(x, y) { |
|
|
x_array.push(x.x) |
|
|
y_array.push(x.y) |
|
|
} |
|
|
|
|
|
const cropCanvas = (sourceCanvas, left, top, width, height) => { |
|
|
let destCanvas = document.createElement('canvas'); |
|
|
destCanvas.width = 224; |
|
|
var cropAspectRatio = width / height; |
|
|
|
|
|
destCanvas.height = 224 / cropAspectRatio |
|
|
destCanvas.getContext("2d").drawImage( |
|
|
sourceCanvas, |
|
|
left, top, width, height, |
|
|
0, 0, 224, destCanvas.height); |
|
|
var predictionInput = tf.browser.fromPixels(destCanvas.getContext("2d").getImageData(0, 0, 224, 224)) |
|
|
|
|
|
predict(tf.expandDims(predictionInput, 0)); |
|
|
} |
|
|
async function predict(inputTensor) { |
|
|
|
|
|
|
|
|
var letter_list = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "#"] |
|
|
objectDetector.then(function (res) { |
|
|
var prediction = res.predict(inputTensor); |
|
|
var outputArray = prediction.dataSync(); |
|
|
var predictedClass = outputArray.indexOf(Math.max(...outputArray)); |
|
|
var current_result = letter_list[predictedClass] |
|
|
var previous_result = document.getElementById("predicted_result").innerText |
|
|
document.getElementById("predicted_result").innerText = current_result |
|
|
var current_time = Math.round(Date.now()) |
|
|
|
|
|
if (previous_result == current_result) { |
|
|
if (current_time - last_letter_time > 1000) { |
|
|
last_letter_time = current_time |
|
|
word_list.push(current_result) |
|
|
console.log(word_list) |
|
|
document.getElementById("text").value = word_list.join('') |
|
|
} |
|
|
} |
|
|
else { |
|
|
last_letter_time = current_time |
|
|
} |
|
|
console.log(letter_list[predictedClass]); |
|
|
}, function (err) { |
|
|
console.log(err); |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
function drawLandmarks(canvasCtx, landmarks, color) { |
|
|
var image_height = (video.videoHeight / video.videoWidth) * canvasElement.width |
|
|
var image_width = canvasElement.width |
|
|
|
|
|
canvasCtx.fillStyle = color; |
|
|
canvasCtx.strokeStyle = 'white'; |
|
|
canvasCtx.lineWidth = 1; |
|
|
canvasCtx.beginPath(); |
|
|
canvasCtx.arc(landmarks.x * image_width, landmarks.y * image_height, 6, 0, 2 * Math.PI); |
|
|
canvasCtx.fill(); |
|
|
canvasCtx.stroke(); |
|
|
|
|
|
} |
|
|
|
|
|
function drawConnection(startNode, endNode, strokeColor, strokeWidth) { |
|
|
|
|
|
var image_height = (video.videoHeight / video.videoWidth) * canvasElement.width |
|
|
var image_width = canvasElement.width |
|
|
|
|
|
canvasCtx.strokeStyle = strokeColor; |
|
|
canvasCtx.lineWidth = strokeWidth; |
|
|
canvasCtx.beginPath(); |
|
|
canvasCtx.moveTo(startNode.x * image_width, startNode.y * image_height); |
|
|
canvasCtx.lineTo(endNode.x * image_width, endNode.y * image_height); |
|
|
canvasCtx.stroke(); |
|
|
} |
|
|
function calculateCanvasBrightness(canvas) { |
|
|
const context = canvas.getContext('2d'); |
|
|
|
|
|
|
|
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height); |
|
|
const data = imageData.data; |
|
|
|
|
|
let totalBrightness = 0; |
|
|
let pixelCount = 0; |
|
|
|
|
|
|
|
|
for (let i = 0; i < data.length; i += 4) { |
|
|
const r = data[i]; |
|
|
const g = data[i + 1]; |
|
|
const b = data[i + 2]; |
|
|
|
|
|
|
|
|
const brightness = 0.299 * r + 0.587 * g + 0.114 * b; |
|
|
totalBrightness += brightness; |
|
|
pixelCount++; |
|
|
} |
|
|
|
|
|
|
|
|
const averageBrightness = totalBrightness / pixelCount; |
|
|
|
|
|
return averageBrightness; |
|
|
} |
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm/vision_wasm_internal.js" |
|
|
crossorigin="anonymous"></script> |
|
|
</body> |
|
|
|
|
|
</html> |