Issue with Image Processing: Circle Enveloping object glitches between sizes and as such error is a bit too high

67 views Asked by At

I have an orange ping pong and I am writing a python program that gives the distance of the orange ping pong ball from the camera. So far it is working decently well, it is able to give the distance of the orange ping pong ball to the camera within 2 cm of error. I would like to decrease the error even more. The reason this error has come up is because I think that the green ball enveloping the ping pong in the program is repeatedly getting smaller or larger. As such, I would like to improve the program so that the green circle is able to perfectly envelope the ball. This is so that I can get the error of the distance between the ball and the camera to be at a minimum, preferably down to millimeters. Additionally, when the ball gets too close to the camera, sometimes the green circle enveloping the ping pong ball goes away entirely. I would also like the noise of small green circles to go away. I currently using OpenCV 4.8 along with Python version 3.8.16. Also, the ping pong ball has a diameter of 40mm as is bright orange.

An example video on Google Drive

One frame, with the green circle is not precisely covering ping pong ball:

https://i.stack.imgur.com/GxTwZ.png

I have tried different methods of contouring. I have also tried using a linear search algorithm to find the edges of the ball to no avail. Additionally, the current iteration I am on uses subpixel refinement. I have calibrated my camera dozens of times and I think it is currently well calibrated. I have tried to remove the Gaussian blur, but that ends up creating too much noise. Increasing the blur results in the ball not being detected up close. Here is the code I have written so far.

import cv2
import numpy as np

def calculate_distance(pixel_diameter, camera_matrix, real_diameter):
    # Calculate the distance using the formula: distance = (real_diameter * focal_length) / pixel_diameter
    focal_length = camera_matrix[0, 0]
    return round((real_diameter * focal_length) / pixel_diameter, 6)

def filter_contours(contours, min_area, circularity_threshold):
    filtered_contours = []
    for contour in contours:
        # Calculate contour area
        area = cv2.contourArea(contour)
        if area > min_area:
            # Calculate circularity of the contour
            perimeter = cv2.arcLength(contour, True)
            circularity = 4 * np.pi * area / (perimeter ** 2)
            if circularity > circularity_threshold:
                filtered_contours.append(contour)
    return filtered_contours

def main():
    # Load camera calibration parameters
    calibration_file = "camera_calibration.npz"
    calibration_data = np.load(calibration_file)
    camera_matrix = calibration_data["camera_matrix"]
    dist_coeffs = calibration_data["dist_coeffs"]

    # Create a VideoCapture object for the camera
    cap = cv2.VideoCapture(0)

    real_diameter = 0.04  # Actual diameter of the ping pong ball in meters

    # Set frame rate to 120 FPS
    # cap.set(cv2.CAP_PROP_FPS, 30)

    while True:
        # Read a frame from the camera
        ret, frame = cap.read()

        if not ret:
            break

        # Increase exposure
        cap.set(cv2.CAP_PROP_EXPOSURE, 0.5)

        # Undistort the frame using the calibration parameters
        undistorted_frame = cv2.undistort(frame, camera_matrix, dist_coeffs)

        # Convert the frame to HSV color space
        img_hsv = cv2.cvtColor(undistorted_frame, cv2.COLOR_BGR2HSV)

        # Threshold the image to isolate the ball
        orange_lower = np.array([0, 100, 100])
        orange_upper = np.array([30, 255, 255])
        mask_ball = cv2.inRange(img_hsv, orange_lower, orange_upper)

        # Apply morphological operations to remove noise
        kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
        kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))

        mask_ball = cv2.morphologyEx(mask_ball, cv2.MORPH_OPEN, kernel_open)
        mask_ball = cv2.morphologyEx(mask_ball, cv2.MORPH_CLOSE, kernel_close)

        # Find contours of the ball
        contours, _ = cv2.findContours(mask_ball, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # Filter contours based on size and circularity
        min_area = 1000  # Adjusted minimum area threshold
        circularity_threshold = 0.5  # Adjusted circularity threshold
        filtered_contours = filter_contours(contours, min_area, circularity_threshold)

        if len(filtered_contours) > 0:
            # Find the contour with the largest area (the ball)
            ball_contour = max(filtered_contours, key=cv2.contourArea)

            # Find the minimum enclosing circle
            (x, y), radius = cv2.minEnclosingCircle(ball_contour)

            if radius >= 10:  # Adjusted minimum enclosing circle radius threshold
                # Refine the circle center position using subpixel accuracy
                center, radius = cv2.minEnclosingCircle(ball_contour)
                center = np.array(center, dtype=np.float32)

                # Draw a circle around the ball
                cv2.circle(frame, (int(center[0]), int(center[1])), int(radius), (0, 255, 0), 2)

                # Calculate and print the distance to the ball
                diameter = radius * 2
                distance = calculate_distance(diameter, camera_matrix, real_diameter)
                print("Distance to the ball: {:.6f} meters".format(distance))

                # Calculate the x and y coordinates in the camera's image plane
                x_coordinate = (center[0] - camera_matrix[0, 2]) / camera_matrix[0, 0]
                y_coordinate = (center[1] - camera_matrix[1, 2]) / camera_matrix[1, 1]
                x_coordinate = round(x_coordinate, 5)
                y_coordinate = round(y_coordinate, 5)
                print("x-coordinate: {:.5f}, y-coordinate: {:.5f}".format(x_coordinate, y_coordinate))

            # Display the frame
            cv2.imshow("Live Feed", frame)
        else:
            # Display the original frame if the ball is not detected
            cv2.imshow("Live Feed", frame)

        # Check for key press (press 'q' to exit)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # Release the VideoCapture object and close windows
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

Edit After discussing in the comments, I was able to come to a solution to my problem by not using the minimum enclosing circle. Instead, I chose to create a polygon of the edges that it could detect. This resulted in my program being much more efficient at creating the polygon around the ball I wish to be detected. The code is below along with a picture.

import cv2
import numpy as np

def calculate_distance(pixel_diameter, camera_matrix, real_diameter):
    # Calculate the distance using the formula: distance = (real_diameter * focal_length) / pixel_diameter
    focal_length = camera_matrix[0, 0]
    return round((real_diameter * focal_length) / pixel_diameter, 6)

def filter_contours(contours, min_area, circularity_threshold):
    filtered_contours = []
    for contour in contours:
        # Calculate contour area
        area = cv2.contourArea(contour)
        if area > min_area:
            # Calculate circularity of the contour
            perimeter = cv2.arcLength(contour, True)
            circularity = 4 * np.pi * area / (perimeter ** 2)
            if circularity > circularity_threshold:
                filtered_contours.append(contour)
    return filtered_contours

def main():
    # Load camera calibration parameters
    calibration_file = "camera_calibration.npz"
    calibration_data = np.load(calibration_file)
    camera_matrix = calibration_data["camera_matrix"]
    dist_coeffs = calibration_data["dist_coeffs"]

    # Create a VideoCapture object for the camera
    cap = cv2.VideoCapture(0)

    real_diameter = 0.04  # Actual diameter of the ping pong ball in meters

    # Set frame rate to 120 FPS
    cap.set(cv2.CAP_PROP_FPS, 120)

    while True:
        # Read a frame from the camera
        ret, frame = cap.read()

        if not ret:
            break

        # Increase exposure
        cap.set(cv2.CAP_PROP_EXPOSURE, 0.5)

        # Undistort the frame using the calibration parameters
        undistorted_frame = cv2.undistort(frame, camera_matrix, dist_coeffs)

        # Convert the frame to HSV color space
        img_hsv = cv2.cvtColor(undistorted_frame, cv2.COLOR_BGR2HSV)

        # Threshold the image to isolate the ball
        orange_lower = np.array([0, 100, 100])
        orange_upper = np.array([30, 255, 255])

        mask_ball = cv2.inRange(img_hsv, orange_lower, orange_upper)

        # Apply morphological operations to remove noise
        kernel_open = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
        kernel_close = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))

        mask_ball = cv2.morphologyEx(mask_ball, cv2.MORPH_OPEN, kernel_open)
        mask_ball = cv2.morphologyEx(mask_ball, cv2.MORPH_CLOSE, kernel_close)

        # Find contours of the ball
        contours, _ = cv2.findContours(mask_ball, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        # Filter contours based on size and circularity
        min_area = 1000  # Adjusted minimum area threshold
        circularity_threshold = 0.5  # Adjusted circularity threshold
        filtered_contours = filter_contours(contours, min_area, circularity_threshold)

        if len(filtered_contours) > 0:
            # Find the contour with the largest area (the ball)
            ball_contour = max(filtered_contours, key=cv2.contourArea)

            # Find the precise boundary of the ball
            epsilon = 0.00001 * cv2.arcLength(ball_contour, True)
            ball_boundary = cv2.approxPolyDP(ball_contour, epsilon, True)

            if len(ball_boundary) > 2:
                # Draw the precise boundary of the ball
                cv2.drawContours(frame, [ball_boundary], -1, (0, 255, 0), 2)

                # Calculate the diameter of the ball
                (x, y), radius = cv2.minEnclosingCircle(ball_boundary)
                diameter = radius * 2

                # Calculate and print the distance to the ball
                distance = calculate_distance(diameter, camera_matrix, real_diameter)
                print("Distance to the ball: {:.6f} meters".format(distance))

                # Calculate the x and y coordinates in the camera's image plane
                x_coordinate = (x - camera_matrix[0, 2]) / camera_matrix[0, 0]
                y_coordinate = (y - camera_matrix[1, 2]) / camera_matrix[1, 1]
                x_coordinate = round(x_coordinate, 5)
                y_coordinate = round(y_coordinate, 5)
                print("x-coordinate: {:.5f}, y-coordinate: {:.5f}".format(x_coordinate, y_coordinate))

            # Display the frame
            cv2.imshow("Live Feed", frame)
        else:
            # Display the original frame if the ball is not detected
            cv2.imshow("Live Feed", frame)

        # Check for key press (press 'q' to exit)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # Release the VideoCapture object and close windows
    cap.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()

Image of better ball: https://i.stack.imgur.com/O3Mul.png

0

There are 0 answers