| | |
| | """ |
| | Validate 100 frames with ball annotations from a COCO dataset. |
| | Generates HTML with toggleable bounding boxes. |
| | """ |
| | import json |
| | import base64 |
| | from pathlib import Path |
| | from typing import List, Dict |
| | from PIL import Image |
| | import io |
| |
|
| |
|
| | def load_coco_annotations(annotation_path: str) -> Dict: |
| | """Load COCO format annotation file.""" |
| | with open(annotation_path, 'r') as f: |
| | return json.load(f) |
| |
|
| |
|
| | def get_images_with_balls(coco_data: Dict) -> List[Dict]: |
| | """Get images that have ball annotations.""" |
| | categories = {cat['id']: cat['name'] for cat in coco_data['categories']} |
| | |
| | |
| | ball_category_id = None |
| | for cat_id, cat_name in categories.items(): |
| | if cat_name.lower() == 'ball': |
| | ball_category_id = cat_id |
| | break |
| | |
| | if ball_category_id is None: |
| | raise ValueError("Ball category not found in annotations") |
| | |
| | |
| | image_annotations = {} |
| | for ann in coco_data['annotations']: |
| | if ann['category_id'] == ball_category_id: |
| | img_id = ann['image_id'] |
| | if img_id not in image_annotations: |
| | image_annotations[img_id] = [] |
| | image_annotations[img_id].append(ann['bbox']) |
| | |
| | |
| | images = {img['id']: img for img in coco_data['images']} |
| | images_with_balls = [] |
| | |
| | for img_id in sorted(image_annotations.keys()): |
| | if img_id in images: |
| | images_with_balls.append({ |
| | 'image': images[img_id], |
| | 'bboxes': image_annotations[img_id] |
| | }) |
| | |
| | return images_with_balls |
| |
|
| |
|
| | def image_to_base64(image_path: Path) -> str: |
| | """Convert image to base64 string.""" |
| | try: |
| | with open(image_path, 'rb') as f: |
| | img_data = f.read() |
| | img = Image.open(io.BytesIO(img_data)) |
| | |
| | max_width = 1920 |
| | if img.width > max_width: |
| | ratio = max_width / img.width |
| | new_height = int(img.height * ratio) |
| | img = img.resize((max_width, new_height), Image.Resampling.LANCZOS) |
| | |
| | |
| | buffer = io.BytesIO() |
| | img.save(buffer, format='PNG') |
| | img_str = base64.b64encode(buffer.getvalue()).decode() |
| | return f"data:image/png;base64,{img_str}" |
| | except Exception as e: |
| | print(f"Error loading image {image_path}: {e}") |
| | return "" |
| |
|
| |
|
| | def generate_html(images_data: List[Dict], annotation_file: str, output_path: Path): |
| | """Generate HTML with toggleable bounding boxes.""" |
| | |
| | annotation_name = Path(annotation_file).name |
| | |
| | html_content = f"""<!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Ball Validation - {annotation_name}</title> |
| | <style> |
| | * {{ |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | }} |
| | |
| | body {{ |
| | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | background: #1a1a1a; |
| | color: #e0e0e0; |
| | padding: 20px; |
| | }} |
| | |
| | .header {{ |
| | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | padding: 20px; |
| | border-radius: 10px; |
| | margin-bottom: 20px; |
| | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); |
| | }} |
| | |
| | .header h1 {{ |
| | color: white; |
| | margin-bottom: 10px; |
| | }} |
| | |
| | .header p {{ |
| | color: rgba(255, 255, 255, 0.9); |
| | font-size: 14px; |
| | }} |
| | |
| | .controls {{ |
| | background: #2a2a2a; |
| | padding: 15px; |
| | border-radius: 8px; |
| | margin-bottom: 20px; |
| | display: flex; |
| | align-items: center; |
| | gap: 15px; |
| | flex-wrap: wrap; |
| | }} |
| | |
| | .toggle-btn {{ |
| | background: #4CAF50; |
| | color: white; |
| | border: none; |
| | padding: 12px 24px; |
| | border-radius: 6px; |
| | cursor: pointer; |
| | font-size: 16px; |
| | font-weight: bold; |
| | transition: all 0.3s; |
| | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); |
| | }} |
| | |
| | .toggle-btn:hover {{ |
| | background: #45a049; |
| | transform: translateY(-2px); |
| | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); |
| | }} |
| | |
| | .toggle-btn.off {{ |
| | background: #ff9800; |
| | }} |
| | |
| | .toggle-btn.off:hover {{ |
| | background: #f57c00; |
| | }} |
| | |
| | .stats {{ |
| | color: #b0b0b0; |
| | font-size: 14px; |
| | }} |
| | |
| | .grid {{ |
| | display: grid; |
| | grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); |
| | gap: 20px; |
| | }} |
| | |
| | .frame-container {{ |
| | background: #2a2a2a; |
| | border-radius: 8px; |
| | padding: 15px; |
| | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); |
| | transition: transform 0.2s; |
| | }} |
| | |
| | .frame-container:hover {{ |
| | transform: translateY(-4px); |
| | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); |
| | }} |
| | |
| | .image-wrapper {{ |
| | position: relative; |
| | width: 100%; |
| | margin-bottom: 10px; |
| | border-radius: 6px; |
| | overflow: hidden; |
| | background: #1a1a1a; |
| | }} |
| | |
| | .image-wrapper img {{ |
| | width: 100%; |
| | height: auto; |
| | display: block; |
| | }} |
| | |
| | .bbox-overlay {{ |
| | position: absolute; |
| | top: 0; |
| | left: 0; |
| | width: 100%; |
| | height: 100%; |
| | pointer-events: none; |
| | }} |
| | |
| | .bbox-overlay.hidden {{ |
| | display: none; |
| | }} |
| | |
| | .bbox {{ |
| | position: absolute; |
| | border: 3px solid #FFC107; |
| | background: rgba(255, 193, 7, 0.3); |
| | box-sizing: border-box; |
| | }} |
| | |
| | .bbox-label {{ |
| | position: absolute; |
| | top: -20px; |
| | left: 0; |
| | background: rgba(0, 0, 0, 0.8); |
| | color: #FFC107; |
| | padding: 2px 6px; |
| | font-size: 12px; |
| | font-weight: bold; |
| | border-radius: 3px; |
| | white-space: nowrap; |
| | }} |
| | |
| | .frame-info {{ |
| | color: #b0b0b0; |
| | font-size: 13px; |
| | margin-top: 8px; |
| | }} |
| | |
| | .frame-info strong {{ |
| | color: #FFC107; |
| | }} |
| | </style> |
| | </head> |
| | <body> |
| | <div class="header"> |
| | <h1>β½ Ball Validation - 100 Samples</h1> |
| | <p>Dataset: {annotation_name}</p> |
| | <p>Total frames with balls: {len(images_data)}</p> |
| | </div> |
| | |
| | <div class="controls"> |
| | <button class="toggle-btn" id="toggleBtn" onclick="toggleBoxes()">Hide Boxes</button> |
| | <div class="stats"> |
| | Showing {len(images_data)} frames with ball annotations |
| | </div> |
| | </div> |
| | |
| | <div class="grid"> |
| | """ |
| | |
| | for idx, img_data in enumerate(images_data): |
| | image_info = img_data['image'] |
| | bboxes = img_data['bboxes'] |
| | |
| | |
| | annotation_dir = Path(annotation_file).parent |
| | image_path = annotation_dir / image_info['file_name'] |
| | |
| | |
| | if not image_path.exists(): |
| | |
| | images_dir = annotation_dir / 'images' |
| | if images_dir.exists(): |
| | image_path = images_dir / image_info['file_name'] |
| | |
| | if not image_path.exists(): |
| | print(f"Warning: Image not found: {image_path}") |
| | continue |
| | |
| | |
| | img_base64 = image_to_base64(image_path) |
| | if not img_base64: |
| | continue |
| | |
| | |
| | img_width = image_info['width'] |
| | img_height = image_info['height'] |
| | |
| | bbox_html = "" |
| | for bbox in bboxes: |
| | |
| | x, y, w, h = bbox |
| | x_percent = (x / img_width) * 100 |
| | y_percent = (y / img_height) * 100 |
| | w_percent = (w / img_width) * 100 |
| | h_percent = (h / img_height) * 100 |
| | |
| | bbox_html += f""" |
| | <div class="bbox" style="left: {x_percent}%; top: {y_percent}%; width: {w_percent}%; height: {h_percent}%;"> |
| | <div class="bbox-label">ball</div> |
| | </div>""" |
| | |
| | html_content += f""" |
| | <div class="frame-container"> |
| | <div class="image-wrapper"> |
| | <img src="{img_base64}" alt="Frame {idx + 1}"> |
| | <div class="bbox-overlay" id="overlay-{idx}"> |
| | {bbox_html} |
| | </div> |
| | </div> |
| | <div class="frame-info"> |
| | <strong>Frame {idx + 1}:</strong> {image_info['file_name']} | |
| | <strong>{len(bboxes)}</strong> ball(s) | |
| | Size: {img_width}x{img_height} |
| | </div> |
| | </div> |
| | """ |
| | |
| | html_content += """ |
| | </div> |
| | |
| | <script> |
| | let boxesVisible = true; |
| | |
| | function toggleBoxes() { |
| | boxesVisible = !boxesVisible; |
| | const overlays = document.querySelectorAll('.bbox-overlay'); |
| | const btn = document.getElementById('toggleBtn'); |
| | |
| | overlays.forEach(overlay => { |
| | if (boxesVisible) { |
| | overlay.classList.remove('hidden'); |
| | } else { |
| | overlay.classList.add('hidden'); |
| | } |
| | }); |
| | |
| | if (boxesVisible) { |
| | btn.textContent = 'Hide Boxes'; |
| | btn.classList.remove('off'); |
| | } else { |
| | btn.textContent = 'Show Boxes'; |
| | btn.classList.add('off'); |
| | } |
| | } |
| | |
| | // Keyboard shortcut: 'H' to toggle |
| | document.addEventListener('keydown', function(event) { |
| | if (event.key === 'h' || event.key === 'H') { |
| | toggleBoxes(); |
| | } |
| | }); |
| | </script> |
| | </body> |
| | </html> |
| | """ |
| | |
| | with open(output_path, 'w') as f: |
| | f.write(html_content) |
| | |
| | print(f"β
Generated HTML: {output_path}") |
| |
|
| |
|
| | def main(): |
| | """Main function to validate 100 frames.""" |
| | annotation_file = "/workspace/soccer_coach_cv/models/ball_detection/dataset/train/_annotations.coco.json" |
| | annotation_path = Path(annotation_file) |
| | |
| | if not annotation_path.exists(): |
| | print(f"Error: Annotation file not found: {annotation_file}") |
| | return |
| | |
| | print(f"π Loading annotations from: {annotation_file}") |
| | coco_data = load_coco_annotations(annotation_file) |
| | |
| | print("π Finding images with ball annotations...") |
| | images_with_balls = get_images_with_balls(coco_data) |
| | |
| | print(f"π Found {len(images_with_balls)} images with ball annotations") |
| | |
| | |
| | samples = images_with_balls[:100] |
| | print(f"β
Selected {len(samples)} samples for validation") |
| | |
| | |
| | annotation_name = annotation_path.stem |
| | if annotation_name.startswith('_'): |
| | annotation_name = annotation_name[1:] |
| | output_html = annotation_path.parent / f"9_validate_100_frames_{annotation_name}.html" |
| | |
| | print(f"π¨ Generating HTML visualization...") |
| | generate_html(samples, annotation_file, output_html) |
| | |
| | print(f"\nβ
Done! Open {output_html} in your browser to view the validation.") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | main() |
| |
|