@@ -620,7 +620,7 @@ <h2>Session History</h2>
620620 class LacrosseBallTracker {
621621 constructor ( ) {
622622 this . previousBalls = [ ] ;
623- this . ballHistory = [ ] ;
623+ this . ballHistory = { } ;
624624 this . maxHistoryLength = 10 ;
625625 this . minBallRadius = 15 ;
626626 this . maxBallRadius = 50 ;
@@ -663,24 +663,44 @@ <h2>Session History</h2>
663663 circles ,
664664 cv . HOUGH_GRADIENT ,
665665 1 , // dp: inverse ratio of accumulator resolution
666- 50 , // minDist: minimum distance between circle centers
666+ 80 , // minDist: minimum distance between circle centers (increased)
667667 100 , // param1: upper threshold for edge detection
668- 30 , // param2: accumulator threshold for center detection
668+ 35 , // param2: accumulator threshold for center detection (increased for better quality)
669669 this . minBallRadius , // minRadius
670670 this . maxBallRadius // maxRadius
671671 ) ;
672672
673- const detectedBalls = [ ] ;
673+ let bestBall = null ;
674+ let bestScore = 0 ;
674675
675- // Process detected circles
676+ // Process detected circles and find the best one
676677 for ( let i = 0 ; i < circles . cols ; i ++ ) {
677678 const x = circles . data32F [ i * 3 ] ;
678679 const y = circles . data32F [ i * 3 + 1 ] ;
679680 const radius = circles . data32F [ i * 3 + 2 ] ;
680681
681682 // Validate detection
682683 if ( this . isValidBall ( x , y , radius , videoElement . width , videoElement . height ) ) {
683- detectedBalls . push ( { x, y, radius, timestamp : Date . now ( ) } ) ;
684+ // Calculate quality score based on radius (prefer balls closer to expected size)
685+ const idealRadius = ( this . minBallRadius + this . maxBallRadius ) / 2 ;
686+ const radiusScore = 1 - Math . abs ( radius - idealRadius ) / idealRadius ;
687+
688+ // Prefer balls in center of frame
689+ const centerX = videoElement . width / 2 ;
690+ const centerY = videoElement . height / 2 ;
691+ const distanceFromCenter = Math . sqrt (
692+ Math . pow ( x - centerX , 2 ) + Math . pow ( y - centerY , 2 )
693+ ) ;
694+ const maxDistance = Math . sqrt ( Math . pow ( centerX , 2 ) + Math . pow ( centerY , 2 ) ) ;
695+ const centerScore = 1 - ( distanceFromCenter / maxDistance ) ;
696+
697+ // Combined score
698+ const totalScore = radiusScore * 0.7 + centerScore * 0.3 ;
699+
700+ if ( totalScore > bestScore ) {
701+ bestScore = totalScore ;
702+ bestBall = { x, y, radius, timestamp : Date . now ( ) , score : totalScore } ;
703+ }
684704 }
685705 }
686706
@@ -690,7 +710,8 @@ <h2>Session History</h2>
690710 blurred . delete ( ) ;
691711 circles . delete ( ) ;
692712
693- return this . filterAndTrackBalls ( detectedBalls ) ;
713+ // Return single best ball or empty array
714+ return bestBall ? [ bestBall ] : [ ] ;
694715
695716 } catch ( error ) {
696717 console . error ( 'Ball detection error:' , error ) ;
@@ -714,94 +735,145 @@ <h2>Session History</h2>
714735 }
715736
716737 filterAndTrackBalls ( detectedBalls ) {
717- // If no previous balls, return current detections
718- if ( this . previousBalls . length === 0 ) {
719- this . previousBalls = detectedBalls ;
720- return detectedBalls ;
738+ const currentTime = Date . now ( ) ;
739+
740+ // Since we only track one ball, simplify the logic
741+ if ( detectedBalls . length === 0 ) {
742+ this . previousBalls = [ ] ;
743+ return [ ] ;
721744 }
722745
723- // Match current detections with previous balls
724- const matchedBalls = [ ] ;
746+ const ball = detectedBalls [ 0 ] ; // Only one ball
747+ let trackedBall ;
725748
726- detectedBalls . forEach ( currentBall => {
727- let bestMatch = null ;
728- let minDistance = Infinity ;
729-
730- this . previousBalls . forEach ( prevBall => {
731- const distance = Math . sqrt (
732- Math . pow ( currentBall . x - prevBall . x , 2 ) +
733- Math . pow ( currentBall . y - prevBall . y , 2 )
734- ) ;
735-
736- if ( distance < minDistance && distance < this . movementThreshold ) {
737- minDistance = distance ;
738- bestMatch = prevBall ;
739- }
740- } ) ;
749+ // Check if we have a previous ball to track from
750+ if ( this . previousBalls . length > 0 ) {
751+ const prevBall = this . previousBalls [ 0 ] ;
752+ const distance = Math . sqrt (
753+ Math . pow ( ball . x - prevBall . x , 2 ) +
754+ Math . pow ( ball . y - prevBall . y , 2 )
755+ ) ;
741756
742- if ( bestMatch ) {
743- // Update ball with movement data
744- currentBall . velocity = {
745- x : currentBall . x - bestMatch . x ,
746- y : currentBall . y - bestMatch . y
757+ // If the ball moved too far, treat as new detection
758+ if ( distance > this . maxTrackingDistance ) {
759+ trackedBall = {
760+ ...ball ,
761+ id : Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ,
762+ velocity : { x : 0 , y : 0 , magnitude : 0 }
763+ } ;
764+ } else {
765+ // Continue tracking the same ball
766+ trackedBall = {
767+ ...ball ,
768+ id : prevBall . id ,
769+ velocity : {
770+ x : ball . x - prevBall . x ,
771+ y : ball . y - prevBall . y ,
772+ magnitude : Math . sqrt (
773+ Math . pow ( ball . x - prevBall . x , 2 ) +
774+ Math . pow ( ball . y - prevBall . y , 2 )
775+ )
776+ }
747777 } ;
748- currentBall . speed = Math . sqrt (
749- Math . pow ( currentBall . velocity . x , 2 ) +
750- Math . pow ( currentBall . velocity . y , 2 )
751- ) ;
752- matchedBalls . push ( currentBall ) ;
753778 }
754- } ) ;
779+ } else {
780+ // First detection
781+ trackedBall = {
782+ ...ball ,
783+ id : Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ,
784+ velocity : { x : 0 , y : 0 , magnitude : 0 }
785+ } ;
786+ }
787+
788+ // Update previous balls for next frame
789+ this . previousBalls = [ trackedBall ] ;
755790
756- this . previousBalls = matchedBalls ;
757- this . updateBallHistory ( matchedBalls ) ;
791+ // Update ball history for movement analysis
792+ this . updateBallHistory ( [ trackedBall ] ) ;
758793
759- return matchedBalls ;
794+ return [ trackedBall ] ;
760795 }
761796
762797 updateBallHistory ( balls ) {
763- this . ballHistory . push ( {
764- timestamp : Date . now ( ) ,
765- balls : balls . map ( ball => ( { ...ball } ) )
766- } ) ;
798+ const currentTime = Date . now ( ) ;
799+
800+ // Since we only track one ball, simplify history management
801+ if ( balls . length === 0 ) {
802+ // Clear history if no ball detected
803+ this . ballHistory = { } ;
804+ return ;
805+ }
806+
807+ const ball = balls [ 0 ] ; // Only one ball
767808
768- // Keep only recent history
769- if ( this . ballHistory . length > this . maxHistoryLength ) {
770- this . ballHistory . shift ( ) ;
809+ if ( ! this . ballHistory [ ball . id ] ) {
810+ this . ballHistory [ ball . id ] = [ ] ;
771811 }
812+
813+ this . ballHistory [ ball . id ] . push ( {
814+ x : ball . x ,
815+ y : ball . y ,
816+ timestamp : currentTime ,
817+ velocity : ball . velocity || { x : 0 , y : 0 , magnitude : 0 }
818+ } ) ;
819+
820+ // Keep only recent history (last 2 seconds)
821+ const cutoffTime = currentTime - 2000 ;
822+ this . ballHistory [ ball . id ] = this . ballHistory [ ball . id ] . filter (
823+ point => point . timestamp > cutoffTime
824+ ) ;
825+
826+ // Clean up old ball histories (keep only current ball)
827+ Object . keys ( this . ballHistory ) . forEach ( ballId => {
828+ if ( ballId !== ball . id ) {
829+ delete this . ballHistory [ ballId ] ;
830+ }
831+ } ) ;
772832 }
773833
774834 analyzeMovementPattern ( ) {
775- if ( this . ballHistory . length < 3 ) {
835+ // Get the current ball's history
836+ const ballIds = Object . keys ( this . ballHistory ) ;
837+ if ( ballIds . length === 0 ) {
776838 return { isThrow : false , direction : null } ;
777839 }
778840
779- const recent = this . ballHistory . slice ( - 3 ) ;
780- const balls = recent . map ( frame => frame . balls [ 0 ] ) . filter ( Boolean ) ;
841+ const ballId = ballIds [ 0 ] ; // Only one ball
842+ const history = this . ballHistory [ ballId ] ;
781843
782- if ( balls . length < 3 ) {
844+ if ( history . length < 3 ) {
783845 return { isThrow : false , direction : null } ;
784846 }
785847
786- // Analyze vertical movement pattern
787- const verticalMovements = [ ] ;
788- for ( let i = 1 ; i < balls . length ; i ++ ) {
789- verticalMovements . push ( balls [ i ] . y - balls [ i - 1 ] . y ) ;
848+ const recent = history . slice ( - 5 ) ; // Last 5 points
849+ let totalMovement = 0 ;
850+ let verticalMovement = 0 ;
851+
852+ for ( let i = 1 ; i < recent . length ; i ++ ) {
853+ const prev = recent [ i - 1 ] ;
854+ const curr = recent [ i ] ;
855+
856+ const dx = curr . x - prev . x ;
857+ const dy = curr . y - prev . y ;
858+ const movement = Math . sqrt ( dx * dx + dy * dy ) ;
859+
860+ totalMovement += movement ;
861+ verticalMovement += Math . abs ( dy ) ;
790862 }
791863
792- // Check for throw pattern: upward then downward movement
793- const hasUpwardMovement = verticalMovements . some ( movement => movement < - 10 ) ;
794- const hasDownwardMovement = verticalMovements . some ( movement => movement > 10 ) ;
864+ const avgMovement = totalMovement / ( recent . length - 1 ) ;
865+ const isSignificantMovement = avgMovement > this . movementThreshold ;
866+ const isVerticalMovement = verticalMovement > totalMovement * 0.6 ;
795867
796868 return {
797- isThrow : hasUpwardMovement && hasDownwardMovement ,
798- direction : verticalMovements [ verticalMovements . length - 1 ] > 0 ? 'down' : 'up' ,
799- speed : balls [ balls . length - 1 ] . speed || 0
869+ isThrow : isSignificantMovement && isVerticalMovement ,
870+ direction : verticalMovement > 0 ? 'down' : 'up' ,
871+ movement : avgMovement
800872 } ;
801873 }
802874
803875 reset ( ) {
804- this . ballHistory = [ ] ;
876+ this . ballHistory = { } ;
805877 this . previousBalls = [ ] ;
806878 }
807879 }
0 commit comments