You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenCV乒乓球相机测距程序优化:包围框抖动与误差问题

橙色乒乓球相机测距程序优化需求与实现

我正在用Python 3.8.16和OpenCV 4.8开发一款橙色乒乓球(直径40mm)的相机测距程序,目前测距误差约2cm,目标是将误差降至毫米级。当前存在以下问题:

  • 包裹乒乓球的绿色包围圈频繁出现大小抖动,导致测距误差难以缩小
  • 乒乓球过于靠近相机时,绿色包围圈会完全消失
  • 画面存在小绿色噪点干扰

我已经尝试了多种轮廓提取方法、线性边缘搜索算法、亚像素优化,并且完成了数十次相机校准,确保相机参数准确。移除高斯模糊会产生大量噪点,增加模糊程度则会导致近距离乒乓球无法被检测到。

初始实现代码

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()

优化后代码

改用轮廓多边形拟合替代最小包围圆后,检测效果有所优化,代码如下:

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()

优化后检测效果

优化后的画面中,绿色多边形轮廓精准贴合橙色乒乓球边缘,无明显小噪点干扰,包围框的抖动情况有所改善。

内容的提问来源于stack exchange,提问作者Banana Blitz Coding

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 05:45:54