Created
April 8, 2025 16:08
-
-
Save CharStiles/962f78513beba1535f81241605d7fbcc to your computer and use it in GitHub Desktop.
This file contains hidden or 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Face Averaging</title> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]"></script> | |
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]"></script> | |
<style> | |
body { | |
margin: 0; | |
padding: 20px; | |
font-family: Arial, sans-serif; | |
background-color: #f0f0f0; | |
} | |
.container { | |
max-width: 800px; | |
margin: 0 auto; | |
background-color: white; | |
padding: 20px; | |
border-radius: 8px; | |
box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
} | |
h1 { | |
text-align: center; | |
color: #333; | |
} | |
#fileInput { | |
display: block; | |
margin: 20px auto; | |
} | |
#averageCanvas { | |
display: block; | |
margin: 20px auto; | |
border: 1px solid #ddd; | |
} | |
#status { | |
text-align: center; | |
margin: 10px 0; | |
font-weight: bold; | |
color: #555; | |
} | |
.gallery { | |
display: flex; | |
flex-wrap: wrap; | |
justify-content: center; | |
gap: 10px; | |
margin-top: 20px; | |
} | |
.gallery img { | |
width: 100px; | |
height: 100px; | |
object-fit: cover; | |
border: 1px solid #ddd; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>Face Averaging Tool</h1> | |
<p>Select multiple images containing faces to create an average face.</p> | |
<input type="file" id="fileInput" multiple accept="image/*"> | |
<div id="status">Loading face detection model...</div> | |
<canvas id="averageCanvas" width="400" height="400"></canvas> | |
<div class="gallery" id="imageGallery"></div> | |
</div> | |
<script> | |
// Global variables | |
let images = []; | |
let faceDetections = []; | |
let model; | |
// DOM elements | |
const fileInput = document.getElementById('fileInput'); | |
const statusText = document.getElementById('status'); | |
const averageCanvas = document.getElementById('averageCanvas'); | |
const imageGallery = document.getElementById('imageGallery'); | |
// Wait for document to be fully loaded | |
document.addEventListener('DOMContentLoaded', function() { | |
loadModel(); | |
}); | |
// Load face detection model | |
async function loadModel() { | |
statusText.textContent = "Loading face detection model..."; | |
try { | |
model = await faceDetection.createDetector( | |
faceDetection.SupportedModels.MediaPipeFaceDetector, | |
{ | |
runtime: 'tfjs', | |
modelType: 'short' | |
} | |
); | |
statusText.textContent = "Model loaded. Select images to begin."; | |
} catch (error) { | |
statusText.textContent = "Error loading model. Please refresh and try again."; | |
console.error("Error loading model:", error); | |
} | |
} | |
// Handle file selection | |
fileInput.addEventListener('change', async (event) => { | |
const files = event.target.files; | |
if (files.length === 0) return; | |
// Reset | |
images = []; | |
faceDetections = []; | |
imageGallery.innerHTML = ''; | |
statusText.textContent = `Selected ${files.length} images. Processing...`; | |
// Process each file | |
for (let file of files) { | |
try { | |
const img = await loadImage(file); | |
images.push(img); | |
// Create a canvas to process the image | |
const canvas = document.createElement('canvas'); | |
const maxSize = 640; | |
let width = img.width; | |
let height = img.height; | |
if (width > height && width > maxSize) { | |
height = Math.round((height * maxSize) / width); | |
width = maxSize; | |
} else if (height > maxSize) { | |
width = Math.round((width * maxSize) / height); | |
height = maxSize; | |
} | |
canvas.width = width; | |
canvas.height = height; | |
const ctx = canvas.getContext('2d'); | |
ctx.drawImage(img, 0, 0, width, height); | |
// Detect faces | |
const predictions = await model.estimateFaces(canvas); | |
console.log("Raw predictions for", file.name, ":", predictions); | |
if (predictions && predictions.length > 0) { | |
const face = predictions[0]; | |
// Check if the face detection result has the expected format | |
if (face && face.box) { | |
const bbox = face.box; | |
// Ensure we have valid coordinates | |
if (typeof bbox.xMin === 'number' && | |
typeof bbox.xMax === 'number' && | |
typeof bbox.yMin === 'number' && | |
typeof bbox.yMax === 'number') { | |
// Calculate face center and size | |
const centerX = (bbox.xMin + bbox.xMax) / 2; | |
const centerY = (bbox.yMin + bbox.yMax) / 2; | |
const faceWidth = bbox.xMax - bbox.xMin; | |
const faceHeight = bbox.yMax - bbox.yMin; | |
// Scale coordinates back to original image size | |
const scaleX = img.width / width; | |
const scaleY = img.height / height; | |
faceDetections.push({ | |
image: img, | |
center: { | |
x: centerX * scaleX, | |
y: centerY * scaleY | |
}, | |
width: faceWidth * scaleX, | |
height: faceHeight * scaleY | |
}); | |
displayImage(img); | |
} else { | |
console.log("Invalid box coordinates in", file.name, ":", bbox); | |
} | |
} else { | |
console.log("Missing box in", file.name, ":", face); | |
} | |
} else { | |
console.log("No face detected in", file.name); | |
} | |
} catch (error) { | |
console.error(`Error processing ${file.name}:`, error); | |
statusText.textContent = `Error processing ${file.name}`; | |
} | |
} | |
if (faceDetections.length > 0) { | |
createAverageFace(); | |
} else { | |
statusText.textContent = "No valid faces detected in any images. Please try with different images."; | |
} | |
}); | |
// Load image from file | |
function loadImage(file) { | |
return new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
const img = new Image(); | |
img.onload = () => resolve(img); | |
img.onerror = reject; | |
img.src = e.target.result; | |
}; | |
reader.onerror = reject; | |
reader.readAsDataURL(file); | |
}); | |
} | |
// Display image in gallery | |
function displayImage(img) { | |
const galleryImg = document.createElement('img'); | |
galleryImg.src = img.src; | |
imageGallery.appendChild(galleryImg); | |
} | |
// Create average face | |
function createAverageFace() { | |
const ctx = averageCanvas.getContext('2d'); | |
ctx.clearRect(0, 0, averageCanvas.width, averageCanvas.height); | |
// Calculate target center and size | |
const targetCenter = { x: 200, y: 200 }; | |
const targetSize = 300; | |
// Draw each face aligned to the target center | |
ctx.globalAlpha = 1 / faceDetections.length; | |
faceDetections.forEach(detection => { | |
const { image, center, width, height } = detection; | |
// Calculate scale to fit target size | |
const scale = targetSize / Math.max(width, height); | |
// Calculate position to center the face | |
const x = targetCenter.x - (width * scale) / 2; | |
const y = targetCenter.y - (height * scale) / 2; | |
// Draw the image | |
ctx.drawImage( | |
image, | |
center.x - width/2, | |
center.y - height/2, | |
width, | |
height, | |
x, | |
y, | |
width * scale, | |
height * scale | |
); | |
}); | |
ctx.globalAlpha = 1; | |
statusText.textContent = "Average face created!"; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment