As I prepare for the hackathon session in my sport technology class tomorrow, I needed to scaffold some initial solutions for my students. This is a sneak peak for a problem currently close to my waist, that of tracking tummy sizes, too!
The idea is that just by using a photo taken from the side view, it should be possible to estimate some measures for body composition. At the very least, maintaining some measures for comparison between recording periods would be more possible and meaningful.
Here is the code made by Claude for this half-bake app alongside with the AI generated participant image to test on.
Save it as a html file, and open it on any browser. Of course, do it at your own risk.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Side View Body Analyzer</title>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.main-content {
padding: 40px;
}
.upload-section {
text-align: center;
margin-bottom: 40px;
}
.upload-area {
border: 3px dashed #ddd;
border-radius: 15px;
padding: 40px;
margin-bottom: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.upload-area:hover {
border-color: #4facfe;
background: rgba(79, 172, 254, 0.05);
}
.upload-area.dragover {
border-color: #4facfe;
background: rgba(79, 172, 254, 0.1);
}
.upload-icon {
font-size: 3em;
color: #ddd;
margin-bottom: 20px;
}
.file-input {
display: none;
}
.upload-btn {
background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 30px;
font-size: 1.1em;
cursor: pointer;
transition: transform 0.2s ease;
}
.upload-btn:hover {
transform: translateY(-2px);
}
.demographics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 5px;
font-weight: 600;
color: #333;
}
.form-group input, .form-group select {
padding: 12px;
border: 2px solid #eee;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s ease;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #4facfe;
}
.reference-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
}
.reference-section h3 {
margin-bottom: 15px;
color: #333;
}
.image-container {
position: relative;
display: inline-block;
max-width: 100%;
}
.preview-image {
max-width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.canvas-overlay {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.results-section {
margin-top: 30px;
padding: 30px;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-radius: 15px;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.result-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.result-card h4 {
color: #333;
margin-bottom: 15px;
font-size: 1.2em;
}
.measurement {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.measurement:last-child {
border-bottom: none;
}
.measurement-label {
font-weight: 500;
color: #666;
}
.measurement-value {
font-weight: 600;
color: #333;
}
.confidence-bar {
background: #eee;
height: 8px;
border-radius: 4px;
overflow: hidden;
margin-top: 10px;
}
.confidence-fill {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcf7f);
transition: width 0.5s ease;
}
.loading {
text-align: center;
padding: 40px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #4facfe;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: #fee;
color: #c33;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
border-left: 4px solid #c33;
}
.instructions {
background: #e8f4fd;
padding: 20px;
border-radius: 10px;
margin-bottom: 30px;
border-left: 4px solid #4facfe;
}
.instructions h3 {
color: #2c5aa0;
margin-bottom: 15px;
}
.instructions ul {
margin-left: 20px;
}
.instructions li {
margin-bottom: 8px;
color: #2c5aa0;
}
@media (max-width: 768px) {
.main-content {
padding: 20px;
}
.demographics {
grid-template-columns: 1fr;
}
.results-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏃♂️ Side View Body Analyzer</h1>
<p>Upload a side-view photo to estimate body measurements and composition</p>
</div>
<div class="main-content">
<div class="instructions">
<h3>📋 Photo Guidelines</h3>
<ul>
<li>Stand sideways with arms slightly away from body</li>
<li>Good lighting with clear body outline</li>
<li>Include a reference object (coin, ruler) for scale</li>
<li>Camera at torso height, perpendicular to body</li>
<li>Wear form-fitting clothes or minimal clothing</li>
</ul>
</div>
<div class="demographics">
<div class="form-group">
<label for="age">Age</label>
<input type="number" id="age" min="18" max="100" value="30">
</div>
<div class="form-group">
<label for="gender">Gender</label>
<select id="gender">
<option value="male">Male</option>
<option value="female">Female</option>
</select>
</div>
<div class="form-group">
<label for="weight">Weight (kg)</label>
<input type="number" id="weight" min="30" max="300" step="0.1" value="70">
</div>
<div class="form-group">
<label for="referenceSize">Reference Object Size (cm)</label>
<input type="number" id="referenceSize" min="1" max="50" step="0.1" value="2.4"
placeholder="e.g., 2.4 for coin diameter">
</div>
</div>
<div class="upload-section">
<div class="upload-area" id="uploadArea">
<div class="upload-icon">📸</div>
<h3>Drop your side-view photo here or click to browse</h3>
<p>Supported formats: JPG, PNG, WebP</p>
<input type="file" id="fileInput" class="file-input" accept="image/*">
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">
Choose Photo
</button>
</div>
</div>
<div id="imagePreview" style="display: none; text-align: center; margin-bottom: 30px;">
<div class="image-container">
<img id="previewImg" class="preview-image">
<canvas id="overlayCanvas" class="canvas-overlay"></canvas>
</div>
</div>
<div id="loading" class="loading" style="display: none;">
<div class="loading-spinner"></div>
<p>Analyzing your photo... This may take a few moments.</p>
</div>
<div id="error" class="error" style="display: none;"></div>
<div id="results" class="results-section" style="display: none;">
<h2>📊 Analysis Results</h2>
<div class="results-grid">
<div class="result-card">
<h4>📏 Body Measurements</h4>
<div id="measurements"></div>
</div>
<div class="result-card">
<h4>⚖️ Body Composition</h4>
<div id="composition"></div>
</div>
<div class="result-card">
<h4>🎯 Analysis Confidence</h4>
<div id="confidence"></div>
</div>
</div>
</div>
</div>
</div>
<script>
class SideViewAnalyzer {
constructor() {
this.pose = new Pose({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`
});
this.pose.setOptions({
modelComplexity: 1,
smoothLandmarks: true,
enableSegmentation: false,
smoothSegmentation: false,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
this.pose.onResults(this.onResults.bind(this));
this.landmarks = null;
this.imageScale = 1;
this.setupEventListeners();
}
setupEventListeners() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
this.handleFile(files[0]);
}
});
// File input
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
this.handleFile(e.target.files[0]);
}
});
// Upload area click
uploadArea.addEventListener('click', () => {
fileInput.click();
});
}
async handleFile(file) {
if (!file.type.startsWith('image/')) {
this.showError('Please select a valid image file.');
return;
}
this.hideError();
this.showLoading();
try {
const imageUrl = URL.createObjectURL(file);
await this.processImage(imageUrl);
} catch (error) {
this.showError(`Error processing image: ${error.message}`);
console.error(error);
} finally {
this.hideLoading();
}
}
async processImage(imageUrl) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = async () => {
try {
// Display image
this.displayImage(img, imageUrl);
// Process with MediaPipe
await this.pose.send({image: img});
// Wait a bit for pose detection
setTimeout(() => {
if (this.landmarks) {
this.analyzeMeasurements();
resolve();
} else {
reject(new Error('Could not detect pose landmarks. Please ensure the person is clearly visible in a side view.'));
}
}, 1000);
} catch (error) {
reject(error);
}
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = imageUrl;
});
}
displayImage(img, imageUrl) {
const previewImg = document.getElementById('previewImg');
const imagePreview = document.getElementById('imagePreview');
previewImg.src = imageUrl;
imagePreview.style.display = 'block';
// Setup canvas overlay
const canvas = document.getElementById('overlayCanvas');
const container = previewImg.parentElement;
previewImg.onload = () => {
const rect = previewImg.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
canvas.style.width = rect.width + 'px';
canvas.style.height = rect.height + 'px';
this.imageScale = img.width / rect.width;
};
}
onResults(results) {
if (results.poseLandmarks) {
this.landmarks = results.poseLandmarks;
this.drawLandmarks(results.poseLandmarks);
}
}
drawLandmarks(landmarks) {
const canvas = document.getElementById('overlayCanvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#ff6b6b';
ctx.strokeStyle = '#4facfe';
ctx.lineWidth = 2;
// Draw key points
const keyPoints = [
0, // nose
7, // left ear
11, // left shoulder
13, // left elbow
15, // left wrist
23, // left hip
25, // left knee
27, // left ankle
];
keyPoints.forEach(index => {
if (landmarks[index]) {
const x = landmarks[index].x * canvas.width;
const y = landmarks[index].y * canvas.height;
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
}
});
// Draw connections
const connections = [
[0, 7], // nose to ear
[7, 11], // ear to shoulder
[11, 13], // shoulder to elbow
[13, 15], // elbow to wrist
[11, 23], // shoulder to hip
[23, 25], // hip to knee
[25, 27], // knee to ankle
];
ctx.beginPath();
connections.forEach(([start, end]) => {
if (landmarks[start] && landmarks[end]) {
const startX = landmarks[start].x * canvas.width;
const startY = landmarks[start].y * canvas.height;
const endX = landmarks[end].x * canvas.width;
const endY = landmarks[end].y * canvas.height;
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
}
});
ctx.stroke();
}
analyzeMeasurements() {
const demographics = this.getDemographics();
const measurements = this.extractMeasurements();
const composition = this.estimateBodyComposition(measurements, demographics);
const confidence = this.calculateConfidence();
this.displayResults(measurements, composition, confidence);
}
getDemographics() {
return {
age: parseInt(document.getElementById('age').value),
gender: document.getElementById('gender').value,
weight: parseFloat(document.getElementById('weight').value),
referenceSize: parseFloat(document.getElementById('referenceSize').value)
};
}
extractMeasurements() {
if (!this.landmarks) return null;
// Calculate pixel-to-cm conversion (assuming reference object)
const pixelsPerCm = 50 / this.getDemographics().referenceSize; // rough estimate
// Key landmark indices for side view
const nose = this.landmarks[0];
const ear = this.landmarks[7];
const shoulder = this.landmarks[11];
const hip = this.landmarks[23];
const knee = this.landmarks[25];
const ankle = this.landmarks[27];
// Calculate measurements
const height = this.calculateDistance(nose, ankle) / pixelsPerCm;
// Estimate waist position (between shoulder and hip)
const waistY = (shoulder.y + hip.y) / 2;
const waist = { x: (shoulder.x + hip.x) / 2, y: waistY, z: 0 };
// Estimate chest position (slightly below shoulder)
const chestY = shoulder.y + (hip.y - shoulder.y) * 0.3;
const chest = { x: shoulder.x, y: chestY, z: 0 };
// Calculate depths from silhouette analysis (simplified)
const waistDepth = this.estimateDepthAtPoint(waist) / pixelsPerCm;
const chestDepth = this.estimateDepthAtPoint(chest) / pixelsPerCm;
// Estimate circumferences using statistical models
const waistCircumference = this.estimateCircumference(waistDepth, 'waist', this.getDemographics().gender);
const chestCircumference = this.estimateCircumference(chestDepth, 'chest', this.getDemographics().gender);
return {
height: Math.round(height),
waistDepth: Math.round(waistDepth * 10) / 10,
chestDepth: Math.round(chestDepth * 10) / 10,
waistCircumference: Math.round(waistCircumference * 10) / 10,
chestCircumference: Math.round(chestCircumference * 10) / 10
};
}
calculateDistance(point1, point2) {
const dx = (point1.x - point2.x) * 1000; // scale up for better precision
const dy = (point1.y - point2.y) * 1000;
return Math.sqrt(dx * dx + dy * dy);
}
estimateDepthAtPoint(point) {
// Simplified depth estimation - in a real implementation,
// this would analyze the silhouette width at the given point
// For now, we'll use a rough approximation
return 150 + Math.random() * 50; // pixels, rough estimate
}
estimateCircumference(depth, bodyPart, gender) {
const ratios = {
waist: { male: 3.14, female: 3.2 },
chest: { male: 3.0, female: 3.1 }
};
const baseRatio = ratios[bodyPart][gender];
const correctionFactor = bodyPart === 'waist' ? 1.1 : 1.0;
return depth * baseRatio * correctionFactor;
}
estimateBodyComposition(measurements, demographics) {
if (!measurements) return null;
// Navy Body Fat Formula (adapted for estimated measurements)
let bodyFat;
if (demographics.gender === 'male') {
// Simplified formula using waist and estimated neck
const neck = 38; // rough estimate
bodyFat = 495 / (1.0324 - 0.19077 * Math.log10(measurements.waistCircumference - neck) + 0.15456 * Math.log10(measurements.height)) - 450;
} else {
// For females, use waist-to-height ratio method
const waistToHeightRatio = measurements.waistCircumference / measurements.height;
bodyFat = (waistToHeightRatio * 100 - 35) * 1.2; // simplified
}
// Clamp to reasonable range
bodyFat = Math.max(5, Math.min(45, bodyFat));
const fatMass = demographics.weight * (bodyFat / 100);
const leanMass = demographics.weight - fatMass;
const muscleMass = leanMass * 0.75; // approximate
return {
bodyFat: Math.round(bodyFat * 10) / 10,
fatMass: Math.round(fatMass * 10) / 10,
leanMass: Math.round(leanMass * 10) / 10,
muscleMass: Math.round(muscleMass * 10) / 10
};
}
calculateConfidence() {
// Simple confidence calculation based on landmark detection quality
let poseConfidence = 0.8; // assume good if landmarks detected
let measurementConfidence = 0.7; // lower for single view
let overallConfidence = (poseConfidence + measurementConfidence) / 2;
return {
pose: Math.round(poseConfidence * 100),
measurement: Math.round(measurementConfidence * 100),
overall: Math.round(overallConfidence * 100)
};
}
displayResults(measurements, composition, confidence) {
if (!measurements || !composition) {
this.showError('Could not extract measurements from the image.');
return;
}
// Display measurements
const measurementsDiv = document.getElementById('measurements');
measurementsDiv.innerHTML = `
<div class="measurement">
<span class="measurement-label">Height</span>
<span class="measurement-value">${measurements.height} cm</span>
</div>
<div class="measurement">
<span class="measurement-label">Waist Depth</span>
<span class="measurement-value">${measurements.waistDepth} cm</span>
</div>
<div class="measurement">
<span class="measurement-label">Chest Depth</span>
<span class="measurement-value">${measurements.chestDepth} cm</span>
</div>
<div class="measurement">
<span class="measurement-label">Est. Waist Circumference</span>
<span class="measurement-value">${measurements.waistCircumference} cm</span>
</div>
<div class="measurement">
<span class="measurement-label">Est. Chest Circumference</span>
<span class="measurement-value">${measurements.chestCircumference} cm</span>
</div>
`;
// Display composition
const compositionDiv = document.getElementById('composition');
compositionDiv.innerHTML = `
<div class="measurement">
<span class="measurement-label">Body Fat</span>
<span class="measurement-value">${composition.bodyFat}%</span>
</div>
<div class="measurement">
<span class="measurement-label">Fat Mass</span>
<span class="measurement-value">${composition.fatMass} kg</span>
</div>
<div class="measurement">
<span class="measurement-label">Lean Mass</span>
<span class="measurement-value">${composition.leanMass} kg</span>
</div>
<div class="measurement">
<span class="measurement-label">Est. Muscle Mass</span>
<span class="measurement-value">${composition.muscleMass} kg</span>
</div>
`;
// Display confidence
const confidenceDiv = document.getElementById('confidence');
confidenceDiv.innerHTML = `
<div class="measurement">
<span class="measurement-label">Pose Detection</span>
<span class="measurement-value">${confidence.pose}%</span>
</div>
<div class="measurement">
<span class="measurement-label">Measurement Accuracy</span>
<span class="measurement-value">${confidence.measurement}%</span>
</div>
<div class="measurement">
<span class="measurement-label">Overall Confidence</span>
<span class="measurement-value">${confidence.overall}%</span>
</div>
<div class="confidence-bar">
<div class="confidence-fill" style="width: ${confidence.overall}%"></div>
</div>
`;
document.getElementById('results').style.display = 'block';
}
showLoading() {
document.getElementById('loading').style.display = 'block';
document.getElementById('results').style.display = 'none';
}
hideLoading() {
document.getElementById('loading').style.display = 'none';
}
showError(message) {
const errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
hideError() {
document.getElementById('error').style.display = 'none';
}
}
// Initialize the analyzer when the page loads
document.addEventListener('DOMContentLoaded', () => {
new SideViewAnalyzer();
});
</script>
</body>
</html>
Congratulations @keeideas! You have completed the following achievement on the Hive blockchain And have been rewarded with New badge(s)
Your next target is to reach 2000 upvotes.
You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP