@@ -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