Skip to content

Commit aeba4de

Browse files
authored
Update index.html
1 parent d7ee4ce commit aeba4de

File tree

1 file changed

+65
-57
lines changed

1 file changed

+65
-57
lines changed

index.html

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ <h1>Wall Ball Rep Tracker</h1>
295295
<div class="menu-item" id="account-label">Account</div>
296296
<div class="menu-item" id="history-label">History</div>
297297
<div class="menu-item">
298-
<label for="toggle-detection">Show Ball Detection</label>
298+
<label for="toggle-detection">Show Sleeve Detection</label>
299299
<label class="toggle-label">
300300
<input type="checkbox" id="toggle-detection">
301301
<span class="toggle-switch"></span>
@@ -349,78 +349,86 @@ <h2>Workout History</h2>
349349
let lastRepTime = 0;
350350
const repCooldown = 300; // 300ms cooldown between reps
351351

352-
class LacrosseBallTracker {
352+
class ArmSleeveTracker {
353353
constructor() {
354-
this.ballState = 'waiting'; // waiting, ball_present, ball_away
355-
this.lastThrowTime = 0;
356-
this.minTimeBetweenThrows = 1000; // 1 second
357-
this.minBallRadius = 10;
358-
this.maxBallRadius = 50;
359-
this.framesSinceBallSeen = 0;
360-
this.framesWithBall = 0;
361-
this.detectionThreshold = 3; // needs 3 consecutive frames to change state
354+
this.sleeveState = 'down'; // down, up
355+
this.lastSleeveCenterY = null;
356+
this.minContourArea = 2000; // Minimum area to be considered a sleeve
362357
}
363358

364359
detect(context, videoElement) {
365360
let src = new cv.Mat(videoElement.height, videoElement.width, cv.CV_8UC4);
366-
let gray = new cv.Mat();
367-
let circles = new cv.Mat();
361+
let hsv = new cv.Mat();
362+
let mask = new cv.Mat();
363+
let contours = new cv.MatVector();
364+
let hierarchy = new cv.Mat();
368365

369366
context.drawImage(videoElement, 0, 0, videoElement.width, videoElement.height);
370367
src.data.set(context.getImageData(0, 0, videoElement.width, videoElement.height).data);
371368

372-
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY);
373-
cv.GaussianBlur(gray, gray, new cv.Size(9, 9), 2, 2);
374-
375-
cv.HoughCircles(gray, circles, cv.HOUGH_GRADIENT, 1, 45, 70, 38, this.minBallRadius, this.maxBallRadius);
376-
377-
const ballDetectedThisFrame = circles.cols > 0;
378-
379-
if (showDetection && ballDetectedThisFrame) {
380-
for (let i = 0; i < circles.cols; ++i) {
381-
let x = circles.data32F[i * 3];
382-
let y = circles.data32F[i * 3 + 1];
383-
let radius = circles.data32F[i * 3 + 2];
384-
context.strokeStyle = '#00FF00';
385-
context.lineWidth = 2;
386-
context.beginPath();
387-
context.arc(x, y, radius, 0, 2 * Math.PI);
388-
context.stroke();
369+
cv.cvtColor(src, hsv, cv.COLOR_RGBA2RGB);
370+
cv.cvtColor(hsv, hsv, cv.COLOR_RGB2HSV);
371+
372+
// Define range for white color in HSV
373+
// This might need tuning based on lighting conditions
374+
let lowerWhite = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [0, 0, 180, 0]);
375+
let upperWhite = new cv.Mat(hsv.rows, hsv.cols, hsv.type(), [180, 40, 255, 255]);
376+
cv.inRange(hsv, lowerWhite, upperWhite, mask);
377+
378+
cv.findContours(mask, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
379+
380+
let largestContour = null;
381+
let maxArea = 0;
382+
for (let i = 0; i < contours.size(); ++i) {
383+
let cnt = contours.get(i);
384+
let area = cv.contourArea(cnt, false);
385+
if (area > maxArea) {
386+
maxArea = area;
387+
largestContour = cnt;
389388
}
390389
}
391390

392391
const currentTime = Date.now();
393392

394-
if (ballDetectedThisFrame) {
395-
this.framesWithBall++;
396-
this.framesSinceBallSeen = 0;
397-
} else {
398-
this.framesWithBall = 0;
399-
this.framesSinceBallSeen++;
400-
}
393+
if (largestContour && maxArea > this.minContourArea) {
394+
if (showDetection) {
395+
let rect = cv.boundingRect(largestContour);
396+
context.strokeStyle = '#00FF00';
397+
context.lineWidth = 2;
398+
context.strokeRect(rect.x, rect.y, rect.width, rect.height);
399+
}
401400

402-
if (this.ballState === 'waiting' && this.framesWithBall > this.detectionThreshold) {
403-
this.ballState = 'ball_present';
404-
} else if (this.ballState === 'ball_present' && this.framesSinceBallSeen > this.detectionThreshold) {
405-
this.ballState = 'ball_away';
406-
this.lastThrowTime = currentTime;
407-
} else if (this.ballState === 'ball_away' && this.framesWithBall > this.detectionThreshold) {
408-
const flightTime = currentTime - this.lastThrowTime;
409-
if (flightTime > 250 && flightTime < 4000 && currentTime - lastRepTime > repCooldown) { // Debounce, Cooldown, and Flight Time Window
410-
reps++;
411-
repsDisplay.textContent = `Reps: ${reps}`;
412-
repSound.play();
413-
this.ballState = 'ball_present';
414-
repTimestamps.push(currentTime);
415-
lastRepTime = currentTime;
401+
const M = cv.moments(largestContour);
402+
const sleeveCenterY = M.m01 / M.m00;
403+
404+
// Rep counting logic based on vertical movement
405+
if (this.lastSleeveCenterY !== null) {
406+
// Arm is moving up
407+
if (sleeveCenterY < this.lastSleeveCenterY - 5 && this.sleeveState === 'down') {
408+
this.sleeveState = 'up';
409+
}
410+
// Arm is moving down after being up, count as a rep
411+
else if (sleeveCenterY > this.lastSleeveCenterY + 5 && this.sleeveState === 'up') {
412+
if (currentTime - lastRepTime > repCooldown) { // Cooldown
413+
reps++;
414+
repsDisplay.textContent = `Reps: ${reps}`;
415+
repSound.play();
416+
lastRepTime = currentTime;
417+
repTimestamps.push(currentTime);
418+
}
419+
this.sleeveState = 'down';
420+
}
416421
}
417-
} else if (this.ballState === 'ball_away' && this.framesSinceBallSeen > 200) { // ~6.6 seconds at 30fps
418-
this.ballState = 'waiting';
422+
this.lastSleeveCenterY = sleeveCenterY;
423+
} else {
424+
this.lastSleeveCenterY = null; // Reset if sleeve is not detected
419425
}
420426

421427
src.delete();
422-
gray.delete();
423-
circles.delete();
428+
hsv.delete();
429+
mask.delete();
430+
contours.delete();
431+
hierarchy.delete();
424432
}
425433
}
426434

@@ -445,7 +453,7 @@ <h2>Workout History</h2>
445453

446454
async function main() {
447455
await setupCamera();
448-
const tracker = new LacrosseBallTracker();
456+
const tracker = new ArmSleeveTracker();
449457

450458
function gameLoop() {
451459
tracker.detect(ctx, video);
@@ -480,7 +488,7 @@ <h2>Workout History</h2>
480488
reps = 0;
481489
repTimestamps = [];
482490
repsDisplay.textContent = 'Reps: 0';
483-
tracker.ballState = 'waiting';
491+
tracker.sleeveState = 'down';
484492
resetTimer();
485493
startTimer();
486494
});
@@ -569,6 +577,6 @@ <h2>Workout History</h2>
569577
script.onload = onOpenCvReady;
570578
}
571579
</script>
572-
<div id="version-counter">v0.07</div>
580+
<div id="version-counter">v0.08</div>
573581
</body>
574582
</html>

0 commit comments

Comments
 (0)