BASCORRO
LearningComputer Vision

Ball Detection

Teknik deteksi bola untuk RoboCup - color segmentation, circle detection, dan deep learning

Computer Vision Fundamentals
0 dari 11 halaman selesai
In Progress
Scroll sampai 80% untuk menandai halaman selesai.

Ball Detection

Deteksi bola adalah tugas vision paling fundamental dalam robot soccer. Halaman ini membahas berbagai metode dari yang sederhana hingga advanced.

Secara konsep, deteksi bola adalah pipeline: kita memilih warna/fitur, membersihkan noise, mencari kandidat, lalu memvalidasi bentuk agar bukan objek lain. Bagian-bagian ini sederhana, tetapi akurasinya sangat dipengaruhi pencahayaan dan sudut kamera.


Interactive Demo

Coba visualisasi ball detection pipeline secara interaktif:

⚽ Ball Detection Simulator

Detection Method:
10px
35px
70%
30%
Detected
False Positive
2/3
Detected
0
False Positives
67%
Recall
60 FPS
Speed

Overview

Purpose: Memberi gambaran cepat alur deteksi bola dari input kamera sampai validasi bentuk. Inputs: frame kamera BGR dari OP3. Outputs: kandidat bola terverifikasi (pusat, radius, confidence). Steps:

  1. Color filter untuk mengisolasi warna bola.
  2. Morphology untuk membersihkan mask.
  3. Contour untuk menemukan kandidat.
  4. Validation untuk cek bentuk dan ukuran. Pitfalls: lighting berubah, blur saat walking, dan objek oranye lain bisa menipu. Validation: cek /vision/debug_image apakah mask putih hanya muncul di bola.
┌─────────────────────────────────────────────────────────────┐
│                    BALL DETECTION PIPELINE                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Image → Color Filter → Morphology → Contour → Validation │
│              ↓              ↓           ↓          ↓       │
│            HSV           Clean       Find        Check     │
│            Mask          Mask        Circles     Shape     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

asd


Method 1: HSV Color Segmentation

Metode paling umum dan cepat untuk deteksi bola berwarna.

Intinya kita membuat mask biner berdasarkan range HSV. Hasilnya adalah gambar hitam/putih: putih berarti kandidat bola. Tantangan utamanya adalah menentukan range HSV yang stabil di berbagai pencahayaan.

Step 1: Define Color Range

Purpose: Menentukan batas HSV agar mask hanya menangkap bola. Inputs: sampel frame di beberapa kondisi lighting. Outputs: BALL_HSV_LOWER dan BALL_HSV_UPPER per kondisi. Steps:

  1. Ambil 20 sampai 30 frame di arena.
  2. Sampling warna bola dan catat nilai HSV.
  3. Tetapkan range untuk kondisi terang, normal, redup. Pitfalls: range terlalu sempit membuat bola hilang, terlalu lebar membuat false positive. Validation: mask menutup bola secara konsisten saat head pan.
import cv2
import numpy as np

# Orange ball (RoboCup standard)
BALL_HSV_LOWER = np.array([5, 100, 100])
BALL_HSV_UPPER = np.array([15, 255, 255])

# Alternative ranges for different lighting
BALL_HSV_RANGES = {
    'bright': ([5, 150, 150], [15, 255, 255]),
    'normal': ([5, 100, 100], [15, 255, 255]),
    'dim':    ([3, 80, 80], [18, 255, 255]),
}

Step 2: Create Mask

Purpose: Menghasilkan mask biner dan membersihkan noise sebelum contour. Inputs: image BGR, HSV lower dan upper. Outputs: mask biner dengan bola berwarna putih. Steps:

  1. Konversi ke HSV.
  2. inRange untuk membuat mask.
  3. Morphology open dan close untuk noise cleanup. Pitfalls: kernel terlalu besar dapat menghapus bola kecil. Validation: debug mask harus halus, bukan bintik-bintik acak.
def create_ball_mask(image, lower, upper):
    """Create binary mask for ball color."""
    # Convert to HSV
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

    # Create mask
    mask = cv2.inRange(hsv, lower, upper)

    # Clean up mask
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

    return mask

Step 3: Find Ball Contour

Purpose: Memilih kontur terbaik yang paling mirip bola. Inputs: mask biner. Outputs: best_ball berisi center, radius, circularity, area. Steps:

  1. Cari kontur eksternal.
  2. Hitung enclosing circle.
  3. Filter berdasarkan radius dan circularity.
  4. Pilih skor tertinggi. Pitfalls: occlusion membuat circularity turun; objek oranye lain bisa menang. Validation: overlay circle pada debug image harus pas di bola.
def find_ball(mask, min_radius=10, max_radius=200):
    """Find ball from binary mask."""
    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
                                    cv2.CHAIN_APPROX_SIMPLE)

    best_ball = None
    best_score = 0

    for contour in contours:
        # Get enclosing circle
        (x, y), radius = cv2.minEnclosingCircle(contour)

        # Check size constraints
        if radius < min_radius or radius > max_radius:
            continue

        # Calculate circularity
        area = cv2.contourArea(contour)
        perimeter = cv2.arcLength(contour, True)
        if perimeter == 0:
            continue
        circularity = 4 * np.pi * area / (perimeter ** 2)

        # Score based on circularity and size
        score = circularity * radius

        if score > best_score:
            best_score = score
            best_ball = {
                'center': (int(x), int(y)),
                'radius': int(radius),
                'circularity': circularity,
                'area': area
            }

    return best_ball

Complete Pipeline

Purpose: Menggabungkan semua langkah menjadi class yang bisa dipakai di node ROS 2. Inputs: image BGR per frame. Outputs: tuple (ball, mask) untuk dipublish atau ditampilkan. Steps:

  1. Buat mask HSV.
  2. Bersihkan mask dengan morphology.
  3. Cari ball terbaik.
  4. Gambar overlay untuk debug. Pitfalls: lupa mengembalikan mask membuat debugging sulit. Validation: ball tidak None saat bola terlihat, dan mask stabil saat head scan.
class BallDetector:
    def __init__(self):
        self.lower_hsv = np.array([5, 100, 100])
        self.upper_hsv = np.array([15, 255, 255])
        self.kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))

    def detect(self, image):
        """Detect ball in image."""
        # Create mask
        hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
        mask = cv2.inRange(hsv, self.lower_hsv, self.upper_hsv)

        # Clean mask
        mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, self.kernel)
        mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, self.kernel)

        # Find ball
        ball = find_ball(mask)

        return ball, mask

    def draw_detection(self, image, ball):
        """Draw detection on image."""
        if ball is None:
            return image

        output = image.copy()
        cx, cy = ball['center']
        radius = ball['radius']

        # Draw circle
        cv2.circle(output, (cx, cy), radius, (0, 255, 0), 2)
        cv2.circle(output, (cx, cy), 5, (0, 0, 255), -1)

        # Draw info
        cv2.putText(output, f"R:{radius}", (cx+10, cy-10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

        return output

Method 2: Hough Circle Transform

Deteksi lingkaran langsung tanpa color filtering.

def detect_ball_hough(image, min_radius=10, max_radius=100):
    """Detect ball using Hough Circle Transform."""
    # Convert to grayscale
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # Blur to reduce noise
    blurred = cv2.GaussianBlur(gray, (9, 9), 2)

    # Detect circles
    circles = cv2.HoughCircles(
        blurred,
        cv2.HOUGH_GRADIENT,
        dp=1,
        minDist=50,
        param1=100,
        param2=30,
        minRadius=min_radius,
        maxRadius=max_radius
    )

    if circles is None:
        return None

    # Get best circle (usually the first one)
    circles = np.uint16(np.around(circles))
    best = circles[0][0]

    return {
        'center': (best[0], best[1]),
        'radius': best[2]
    }

Hough Circle lebih robust terhadap variasi warna tapi lebih lambat dan bisa mendeteksi objek bulat lain (kepala robot, dll).


Method 3: Blob Detection

Menggunakan SimpleBlobDetector dari OpenCV.

def detect_ball_blob(image):
    """Detect ball using blob detection."""
    # Setup SimpleBlobDetector parameters
    params = cv2.SimpleBlobDetector_Params()

    # Filter by area
    params.filterByArea = True
    params.minArea = 100
    params.maxArea = 50000

    # Filter by circularity
    params.filterByCircularity = True
    params.minCircularity = 0.7

    # Filter by convexity
    params.filterByConvexity = True
    params.minConvexity = 0.8

    # Filter by inertia (roundness)
    params.filterByInertia = True
    params.minInertiaRatio = 0.5

    # Create detector
    detector = cv2.SimpleBlobDetector_create(params)

    # Create mask for orange
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, np.array([5, 100, 100]), np.array([15, 255, 255]))

    # Invert mask (blob detector looks for dark blobs)
    mask_inv = cv2.bitwise_not(mask)

    # Detect blobs
    keypoints = detector.detect(mask_inv)

    if not keypoints:
        return None

    # Get largest blob
    largest = max(keypoints, key=lambda k: k.size)

    return {
        'center': (int(largest.pt[0]), int(largest.pt[1])),
        'radius': int(largest.size / 2)
    }

Method 4: Deep Learning (YOLO)

Menggunakan YOLO untuk deteksi yang lebih robust.

Installation

Purpose: Menyiapkan dependency YOLO agar training dan inference bisa berjalan. Inputs: environment Python aktif. Outputs: paket ultralytics terpasang. Steps:

  1. Aktifkan venv atau conda env.
  2. Jalankan install via pip. Pitfalls: versi Python tidak kompatibel akan memicu error install. Validation: python -c "from ultralytics import YOLO" sukses.
pip install ultralytics

Training Custom Model

Purpose: Fine-tune YOLO dengan dataset RoboCup agar lebih robust di lapangan. Inputs: yolov8n.pt, robocup_ball.yaml, jumlah epoch. Outputs: weights hasil training di runs/detect/train/weights/. Steps:

  1. Load pretrained model.
  2. Jalankan train dengan dataset dan hyperparameter. Pitfalls: path dataset salah membuat training berhenti. Validation: log training menunjukkan loss turun dan mAP naik.
from ultralytics import YOLO

# Load pretrained model
model = YOLO('yolov8n.pt')

# Train on RoboCup dataset
results = model.train(
    data='robocup_ball.yaml',
    epochs=100,
    imgsz=640,
    batch=16
)

Dataset YAML (robocup_ball.yaml)

Purpose: Mendefinisikan lokasi dataset dan daftar kelas untuk YOLO. Inputs: path dataset dan daftar kelas. Outputs: file YAML yang dipakai training. Steps:

  1. Isi path, train, val.
  2. Set nc sesuai jumlah kelas. Pitfalls: nc tidak sesuai names membuat label mismatch. Validation: training membaca dataset tanpa error.
# RoboCup Ball Dataset
path: /path/to/dataset
train: images/train
val: images/val

nc: 1 # number of classes
names: ["ball"]

Inference

Purpose: Menjalankan deteksi cepat untuk verifikasi kualitas model. Inputs: weights hasil training dan image uji. Outputs: dict hasil deteksi berisi center, radius, confidence. Steps:

  1. Load model dari best.pt.
  2. Jalankan inference pada image.
  3. Ambil bounding box dan confidence. Pitfalls: confidence threshold terlalu tinggi membuat bola tidak muncul. Validation: bounding box konsisten pada beberapa frame berbeda.
from ultralytics import YOLO
import cv2

class YOLOBallDetector:
    def __init__(self, model_path='best.pt'):
        self.model = YOLO(model_path)

    def detect(self, image):
        """Detect ball using YOLO."""
        results = self.model(image, verbose=False)

        for result in results:
            boxes = result.boxes
            for box in boxes:
                # Get bounding box
                x1, y1, x2, y2 = box.xyxy[0].cpu().numpy()
                confidence = box.conf[0].cpu().numpy()

                # Calculate center and radius
                cx = (x1 + x2) / 2
                cy = (y1 + y2) / 2
                radius = max(x2 - x1, y2 - y1) / 2

                return {
                    'center': (int(cx), int(cy)),
                    'radius': int(radius),
                    'confidence': float(confidence),
                    'bbox': (int(x1), int(y1), int(x2), int(y2))
                }

        return None

Comparison of Methods

MethodSpeedAccuracyRobustnessUse Case
HSV Segmentation★★★★★★★★☆☆★★☆☆☆Real-time, controlled lighting
Hough Circle★★★☆☆★★★☆☆★★★☆☆Multiple ball colors
Blob Detection★★★★☆★★★☆☆★★★☆☆Simple scenes
YOLO★★☆☆☆★★★★★★★★★★Variable conditions

HSV Tuning Guide

Interactive HSV Tuner

Gunakan tuner interaktif untuk eksperimen dengan nilai HSV:

🎨 Interactive HSV Tuner

Presets:
5-15
100-255
100-255
Low
High
Sample Pixels (matched: 2/200)1.0% matched
# Python/OpenCV code:
lower_hsv = np.array([5, 100, 100])
upper_hsv = np.array([15, 255, 255])
mask = cv2.inRange(hsv_image, lower_hsv, upper_hsv)

Python Tuning Script

Purpose: Tuning HSV secara real-time dengan slider. Inputs: stream kamera atau video. Outputs: nilai HSV lower dan upper yang bisa disimpan. Steps:

  1. Buat trackbar untuk H, S, V.
  2. Update mask setiap frame.
  3. Simpan nilai saat bola terlihat stabil. Pitfalls: lupa lock exposure kamera membuat HSV berubah-ubah. Validation: mask menangkap bola saat head pan.
import cv2
import numpy as np

def nothing(x):
    pass

def create_hsv_tuner(image_source=0):
    """Interactive HSV tuner for ball detection."""
    cv2.namedWindow('HSV Tuner')

    # Create trackbars
    cv2.createTrackbar('H Low', 'HSV Tuner', 5, 179, nothing)
    cv2.createTrackbar('H High', 'HSV Tuner', 15, 179, nothing)
    cv2.createTrackbar('S Low', 'HSV Tuner', 100, 255, nothing)
    cv2.createTrackbar('S High', 'HSV Tuner', 255, 255, nothing)
    cv2.createTrackbar('V Low', 'HSV Tuner', 100, 255, nothing)
    cv2.createTrackbar('V High', 'HSV Tuner', 255, 255, nothing)

    cap = cv2.VideoCapture(image_source)

    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # Get trackbar values
        h_low = cv2.getTrackbarPos('H Low', 'HSV Tuner')
        h_high = cv2.getTrackbarPos('H High', 'HSV Tuner')
        s_low = cv2.getTrackbarPos('S Low', 'HSV Tuner')
        s_high = cv2.getTrackbarPos('S High', 'HSV Tuner')
        v_low = cv2.getTrackbarPos('V Low', 'HSV Tuner')
        v_high = cv2.getTrackbarPos('V High', 'HSV Tuner')

        # Create mask
        hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        lower = np.array([h_low, s_low, v_low])
        upper = np.array([h_high, s_high, v_high])
        mask = cv2.inRange(hsv, lower, upper)

        # Apply mask to original image
        result = cv2.bitwise_and(frame, frame, mask=mask)

        # Show results
        cv2.imshow('Original', frame)
        cv2.imshow('Mask', mask)
        cv2.imshow('Result', result)

        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('s'):
            print(f"Lower: [{h_low}, {s_low}, {v_low}]")
            print(f"Upper: [{h_high}, {s_high}, {v_high}]")

    cap.release()
    cv2.destroyAllWindows()

# Run tuner
create_hsv_tuner(0)  # Use webcam

3D Position Estimation

Setelah mendeteksi bola di image, kita perlu menghitung posisi 3D.

Purpose: Mengubah radius pixel menjadi estimasi jarak dan posisi 3D bola. Inputs: camera_matrix, radius pixel, diameter bola. Outputs: posisi 3D di frame kamera dan jarak. Steps:

  1. Hitung jarak dengan model pinhole.
  2. Proyeksikan pixel ke koordinat kamera.
  3. Kembalikan posisi 3D. Pitfalls: occlusion membuat radius terlalu kecil dan jarak terlalu jauh. Validation: jarak mendekati nilai ground truth saat jarak terukur.
class BallPositionEstimator:
    def __init__(self, camera_matrix, ball_diameter=0.065):
        """
        Args:
            camera_matrix: 3x3 intrinsic matrix
            ball_diameter: Real ball diameter in meters (6.5cm for RoboCup)
        """
        self.camera_matrix = camera_matrix
        self.ball_diameter = ball_diameter
        self.fx = camera_matrix[0, 0]
        self.fy = camera_matrix[1, 1]
        self.cx = camera_matrix[0, 2]
        self.cy = camera_matrix[1, 2]

    def estimate_distance(self, radius_pixels):
        """Estimate distance to ball based on apparent size."""
        # Distance = (real_size * focal_length) / apparent_size
        distance = (self.ball_diameter * self.fx) / (2 * radius_pixels)
        return distance

    def pixel_to_camera(self, u, v, distance):
        """Convert pixel coordinates to camera frame coordinates."""
        x = (u - self.cx) * distance / self.fx
        y = (v - self.cy) * distance / self.fy
        z = distance
        return np.array([x, y, z])

    def estimate_3d_position(self, ball_2d):
        """Estimate 3D position of ball."""
        if ball_2d is None:
            return None

        cx, cy = ball_2d['center']
        radius = ball_2d['radius']

        distance = self.estimate_distance(radius)
        position = self.pixel_to_camera(cx, cy, distance)

        return {
            'position': position,
            'distance': distance
        }

ROS 2 Integration

Ball Detector Node

Purpose: Contoh node ROS 2 untuk publish posisi bola dan debug image. Inputs: /camera/image_raw dari kamera OP3. Outputs: /ball_position dan /ball_debug. Steps:

  1. Subscribe image.
  2. Jalankan HSV detection.
  3. Publish PointStamped dan debug image. Pitfalls: lupa set header.stamp membuat sinkronisasi TF gagal. Validation: ros2 topic echo /ball_position --once menunjukkan header valid.
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from geometry_msgs.msg import PointStamped
from cv_bridge import CvBridge
import cv2
import numpy as np

class BallDetectorNode(Node):
    def __init__(self):
        super().__init__('ball_detector')

        # Parameters
        self.declare_parameter('h_low', 5)
        self.declare_parameter('h_high', 15)
        self.declare_parameter('s_low', 100)
        self.declare_parameter('s_high', 255)
        self.declare_parameter('v_low', 100)
        self.declare_parameter('v_high', 255)

        # Subscribers
        self.image_sub = self.create_subscription(
            Image, '/camera/image_raw', self.image_callback, 10)

        # Publishers
        self.ball_pub = self.create_publisher(PointStamped, '/ball_position', 10)
        self.debug_pub = self.create_publisher(Image, '/ball_debug', 10)

        self.bridge = CvBridge()
        self.get_logger().info('Ball detector initialized')

    def image_callback(self, msg):
        # Convert ROS image to OpenCV
        image = self.bridge.imgmsg_to_cv2(msg, 'bgr8')

        # Get HSV parameters
        h_low = self.get_parameter('h_low').value
        h_high = self.get_parameter('h_high').value
        s_low = self.get_parameter('s_low').value
        s_high = self.get_parameter('s_high').value
        v_low = self.get_parameter('v_low').value
        v_high = self.get_parameter('v_high').value

        # Detect ball
        hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
        lower = np.array([h_low, s_low, v_low])
        upper = np.array([h_high, s_high, v_high])
        mask = cv2.inRange(hsv, lower, upper)

        # Find contours
        contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL,
                                        cv2.CHAIN_APPROX_SIMPLE)

        if contours:
            largest = max(contours, key=cv2.contourArea)
            (x, y), radius = cv2.minEnclosingCircle(largest)

            if radius > 10:
                # Publish ball position
                ball_msg = PointStamped()
                ball_msg.header = msg.header
                ball_msg.point.x = float(x)
                ball_msg.point.y = float(y)
                ball_msg.point.z = float(radius)
                self.ball_pub.publish(ball_msg)

                # Draw detection
                cv2.circle(image, (int(x), int(y)), int(radius), (0, 255, 0), 2)

        # Publish debug image
        debug_msg = self.bridge.cv2_to_imgmsg(image, 'bgr8')
        self.debug_pub.publish(debug_msg)

def main(args=None):
    rclpy.init(args=args)
    node = BallDetectorNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

Performance Optimization

1. Region of Interest (ROI)

Purpose: Meningkatkan FPS dengan membatasi area pencarian. Inputs: posisi terakhir dan radius pencarian. Outputs: deteksi dari ROI atau fallback full image. Steps:

  1. Buat ROI di sekitar posisi terakhir.
  2. Deteksi hanya pada ROI.
  3. Jika gagal, kembali ke full frame. Pitfalls: ROI terlalu kecil membuat bola hilang saat bergerak cepat. Validation: FPS naik dan recall tetap stabil.
def detect_with_roi(image, last_position=None, search_radius=100):
    """Detect ball using ROI from last known position."""
    if last_position is not None:
        x, y = last_position
        h, w = image.shape[:2]

        # Define ROI
        x1 = max(0, x - search_radius)
        y1 = max(0, y - search_radius)
        x2 = min(w, x + search_radius)
        y2 = min(h, y + search_radius)

        roi = image[y1:y2, x1:x2]

        # Detect in ROI
        ball = detect_ball(roi)

        if ball:
            # Adjust coordinates to full image
            ball['center'] = (ball['center'][0] + x1,
                             ball['center'][1] + y1)
            return ball

    # Fallback to full image search
    return detect_ball(image)

2. Multi-scale Detection

Purpose: Menangkap bola jauh dan dekat dengan beberapa skala. Inputs: image dan list scales. Outputs: kandidat terbaik dari semua skala. Steps:

  1. Resize image ke beberapa skala.
  2. Deteksi di setiap skala.
  3. Pilih kandidat dengan confidence terbaik. Pitfalls: terlalu banyak skala menurunkan FPS. Validation: bola kecil di kejauhan tetap terdeteksi.
def detect_multiscale(image, scales=[1.0, 0.5, 0.25]):
    """Detect ball at multiple scales."""
    best_ball = None
    best_conf = 0

    for scale in scales:
        h, w = image.shape[:2]
        resized = cv2.resize(image, (int(w*scale), int(h*scale)))

        ball = detect_ball(resized)

        if ball and ball.get('circularity', 0) > best_conf:
            # Scale back coordinates
            ball['center'] = (int(ball['center'][0]/scale),
                             int(ball['center'][1]/scale))
            ball['radius'] = int(ball['radius']/scale)
            best_ball = ball
            best_conf = ball['circularity']

    return best_ball

Resources

Datasets

Papers

Tools


Practice Exercises

  1. Implement HSV ball detector dengan webcam
  2. Tune HSV values untuk berbagai kondisi pencahayaan
  3. Bandingkan akurasi HSV vs Hough Circle
  4. Train YOLO model dengan dataset custom

Next Steps

On this page