| |
| SUPABASE_URL=your_supabase_url_here |
| SUPABASE_KEY=your_supabase_anon_key_here |
| SUPABASE_SERVICE_KEY=your_supabase_service_role_key_here |
|
|
| |
| JWT_SECRET=your_jwt_secret_here |
| JWT_ALGORITHM=HS256 |
| JWT_EXPIRATION_MINUTES=1440 |
|
|
| |
| DEBUG=false |
| HOST=0.0.0.0 |
| PORT=8000 |
| REQUEST_TIMEOUT_SECONDS=120 |
|
|
| |
| |
| CORS_ORIGINS= |
|
|
| |
| UPLOAD_DIR=./uploads |
| MAX_UPLOAD_SIZE_MB=500 |
| SERVE_UPLOADS_IN_DEBUG=true |
|
|
| |
| GPU_ENABLED=true |
| CUDA_DEVICE=0 |
|
|
| |
| PLAYER_DETECTOR_PATH=models/player_detector.pt |
| BALL_DETECTOR_PATH=models/ball_detector_model.pt |
| COURT_KEYPOINT_DETECTOR_PATH=models/court_keypoint_detector.pt |
| POSE_MODEL_PATH=models/yolov8n-pose.pt |
| |
|
|
|
|
| What matters is not just that the model predicts 18 keypoints, but that your backend has a canonical meaning for each index so the system always knows: |
|
|
| which side of the court the point belongs to |
|
|
| whether it is boundary, paint, or midline |
|
|
| where it should land on the tactical map |
|
|
| how to recover if some points are missing or swapped |
|
|
| From your new model: |
|
|
| 0 = far left, top boundary |
|
|
| 10 = far right, top boundary |
|
|
| 5 = far left, bottom boundary |
|
|
| 15 = far right, bottom boundary |
|
|
| 8 and 9 = centre line top/bottom |
|
|
| 2, 3, 6, 7 = left paint box |
|
|
| 12, 13, 16, 17 = right paint box |
|
|
| 1 and 4 = left baseline inner markers |
|
|
| 11 and 14 = right baseline inner markers |
|
|
| So the first thing is: your code must stop treating these as just “18 anonymous points”. It should treat them as a court geometry schema. |
|
|
| Recommended canonical keypoint meaning |
|
|
| Use this exact mapping in code: |
|
|
| COURT_KEYPOINTS = { |
| 0: "left_outer_top", |
| 1: "left_baseline_upper_inner", |
| 2: "left_paint_outer_top", |
| 3: "left_paint_outer_bottom", |
| 4: "left_baseline_lower_inner", |
| 5: "left_outer_bottom", |
|
|
| 6: "left_paint_inner_top", |
| 7: "left_paint_inner_bottom", |
|
|
| 8: "midline_top", |
| 9: "midline_bottom", |
|
|
| 10: "right_outer_top", |
| 11: "right_baseline_upper_inner", |
| 12: "right_paint_outer_top", |
| 13: "right_paint_outer_bottom", |
| 14: "right_baseline_lower_inner", |
| 15: "right_outer_bottom", |
|
|
| 16: "right_paint_inner_top", |
| 17: "right_paint_inner_bottom", |
| } |
|
|
| That gives the model output real meaning. |
|
|
| Very important logic rule |
|
|
| Your system should always think in two spaces: |
|
|
| 1. Image space |
|
|
| These are the predicted coordinates from the model: |
|
|
| [(x0, y0), (x1, y1), ..., (x17, y17)] |
| 2. Court space |
|
|
| These are the fixed coordinates on your tactical board. |
|
|
| For example: |
|
|
| COURT_MODEL_POINTS = { |
| 0: (0, 0), |
| 1: (0, 10), |
| 2: (0, 20), |
| 3: (0, 30), |
| 4: (0, 40), |
| 5: (0, 50), |
|
|
| 6: (15, 20), |
| 7: (15, 30), |
|
|
| 8: (47, 0), |
| 9: (47, 50), |
|
|
| 10: (94, 0), |
| 11: (94, 10), |
| 12: (94, 20), |
| 13: (94, 30), |
| 14: (94, 40), |
| 15: (94, 50), |
|
|
| 16: (79, 20), |
| 17: (79, 30), |
| } |
|
|
| These values are example tactical-map coordinates using your 94 × 50 style court. |
|
|
| Then your homography becomes: |
|
|
| src = predicted_image_points |
| dst = tactical_model_points |
| H, _ = cv2.findHomography(src, dst, cv2.RANSAC) |
|
|
| That is what makes player projection work. |
|
|
| The real refinement you need |
|
|
| Your current logic probably assumes: |
|
|
| all points are present |
|
|
| all points are in the correct order |
|
|
| no side confusion happens |
|
|
| That is risky. |
|
|
| You need 4 layers of logic: |
|
|
| 1. Semantic index map |
|
|
| Every index must have a fixed court meaning. |
|
|
| 2. Side validation |
|
|
| The system should check if left-side points are actually left of right-side points. |
|
|
| Example: |
|
|
| x(0) < x(8) < x(10) |
|
|
| x(5) < x(9) < x(15) |
|
|
| left paint should be left of midline |
|
|
| right paint should be right of midline |
|
|
| 3. Structural validation |
|
|
| Check the court shape is geometrically plausible. |
|
|
| Examples: |
|
|
| 0 and 10 should be roughly same y-level |
|
|
| 5 and 15 should be roughly same y-level |
|
|
| 8 should be above 9 |
|
|
| 2 and 6 should be roughly same y-level |
|
|
| 3 and 7 should be roughly same y-level |
|
|
| 12 and 16 should be roughly same y-level |
|
|
| 13 and 17 should be roughly same y-level |
|
|
| 4. Missing-point recovery |
|
|
| If one or two points are low-confidence or absent, use the rest and continue. |
|
|
| Best mental grouping for the 18 points |
|
|
| This helps your code a lot. |
|
|
| Outer boundary |
| OUTER_CORNERS = [0, 5, 10, 15] |
| Midline |
| MIDLINE = [8, 9] |
| Left baseline vertical markers |
| LEFT_BASELINE_CHAIN = [0, 1, 2, 3, 4, 5] |
| Right baseline vertical markers |
| RIGHT_BASELINE_CHAIN = [10, 11, 12, 13, 14, 15] |
| Left paint box |
| LEFT_PAINT = [2, 6, 7, 3] |
| Right paint box |
| RIGHT_PAINT = [12, 16, 17, 13] |
|
|
| This lets you reason about the court as shapes, not isolated dots. |
|
|
| Adjacency logic you should encode |
|
|
| This is useful for drawing, validation, and debugging. |
|
|
| COURT_EDGES = [ |
| (0, 8), (8, 10), # top boundary |
| (5, 9), (9, 15), # bottom boundary |
| (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), # left side chain |
| (10, 11), (11, 12), (12, 13), (13, 14), (14, 15), # right side chain |
| (8, 9), # center line |
| (2, 6), (6, 7), (7, 3), # left paint |
| (12, 16), (16, 17), (17, 13), # right paint |
| ] |
|
|
| This makes your rendered court match the actual geometry in your Roboflow template. |
|
|
| What the model should “know” |
|
|
| The model itself does not truly know basketball meaning unless your logic gives it that meaning. |
|
|
| So after inference, do this: |
|
|
| Step 1: collect points with confidence |
| keypoints = [ |
| {"id": 0, "x": x0, "y": y0, "conf": c0}, |
| ... |
| ] |
| Step 2: attach semantics |
| for kp in keypoints: |
| kp["name"] = COURT_KEYPOINTS[kp["id"]] |
| kp["side"] = ( |
| "left" if kp["id"] in [0,1,2,3,4,5,6,7] |
| else "right" if kp["id"] in [10,11,12,13,14,15,16,17] |
| else "center" |
| ) |
| Step 3: validate court orientation |
|
|
| For example: |
|
|
| left average x must be less than centre average x |
|
|
| centre average x must be less than right average x |
|
|
| Step 4: build homography only from valid points |
| Strong recommendation: use named coordinates instead of raw index logic everywhere |
|
|
| Instead of this: |
|
|
| p0 = pts[0] |
| p1 = pts[1] |
| p2 = pts[2] |
|
|
| Do this: |
|
|
| named = {COURT_KEYPOINTS[i]: pts[i] for i in range(len(pts))} |
|
|
| left_outer_top = named["left_outer_top"] |
| midline_top = named["midline_top"] |
| right_outer_top = named["right_outer_top"] |
|
|
| That makes the whole pipeline easier to debug. |
|
|
| Suggested tactical coordinates for your exact new model |
|
|
| Here is a cleaner court-map layout you can start with: |
|
|
| TACTICAL_POINTS = { |
| 0: (0, 0), |
| 1: (0, 8), |
| 2: (0, 18), |
| 3: (0, 32), |
| 4: (0, 42), |
| 5: (0, 50), |
|
|
| 6: (18, 18), |
| 7: (18, 32), |
|
|
| 8: (47, 0), |
| 9: (47, 50), |
|
|
| 10: (94, 0), |
| 11: (94, 8), |
| 12: (94, 18), |
| 13: (94, 32), |
| 14: (94, 42), |
| 15: (94, 50), |
|
|
| 16: (76, 18), |
| 17: (76, 32), |
| } |
|
|
| You can tune these to match your SVG court exactly. |
|
|
| Validation function you should add |
|
|
| Something like this: |
|
|
| def validate_court_keypoints(points): |
| """ |
| points: dict[int, tuple[float, float]] |
| """ |
| errors = [] |
|
|
| required = [0,5,8,9,10,15] |
| for idx in required: |
| if idx not in points: |
| errors.append(f"Missing critical point {idx}") |
|
|
| if all(k in points for k in [0, 8, 10]): |
| if not (points[0][0] < points[8][0] < points[10][0]): |
| errors.append("Top row x-order invalid") |
|
|
| if all(k in points for k in [5, 9, 15]): |
| if not (points[5][0] < points[9][0] < points[15][0]): |
| errors.append("Bottom row x-order invalid") |
|
|
| if all(k in points for k in [8, 9]): |
| if not (points[8][1] < points[9][1]): |
| errors.append("Midline top/bottom order invalid") |
|
|
| if all(k in points for k in [2, 6]): |
| if not (points[2][0] < points[6][0]): |
| errors.append("Left paint width invalid") |
|
|
| if all(k in points for k in [16, 12]): |
| if not (points[16][0] < points[12][0]): |
| errors.append("Right paint width invalid") |
|
|
| return errors |
|
|
| This alone will save you from many bad projections. |
|
|
| Best upgrade for your backend prompt / Antigravity |
|
|
| Tell it this: |
|
|
| The court keypoint model has 18 semantically fixed points. The backend must not treat them as anonymous ordered points only. It must maintain a canonical index-to-court-location mapping, validate court orientation and geometry after inference, and compute homography using only structurally valid keypoints. Missing or low-confidence points should be handled with graceful degradation, and all downstream tactical projection must rely on named semantic court points rather than raw positional assumptions. |
|
|
| In simple terms |
|
|
| Your new logic should understand: |
|
|
| 0–5 = left boundary column |
|
|
| 10–15 = right boundary column |
|
|
| 8 and 9 = centre line |
|
|
| 2,3,6,7 = left paint rectangle |
|
|
| 12,13,16,17 = right paint rectangle |
|
|
| So yes — your model can still work, but your code now needs a court-geometry interpretation layer, not just raw keypoint indexing. |
|
|
| I can write you the exact Python module for this next: a court_keypoints.py file with mappings, validation, homography preparation, and named-point conversion. |