Hello. I am working to calibrate a camera for pose esitmation in OpenCV to account for depth when measuring objects using aruco markers. 

Here is the code I am using to calibrate the camera. It keeps failing. can you help me fix it? 

import cv2
import numpy as np
import os
import glob
import pickle


def generate_chessboard(grid_size=(7, 4), square_size=50, output_file="chessboard.png"):
    """
    Generates a chessboard pattern and saves it as an image.

    Parameters:
    - grid_size: Number of inner corners per chessboard row and column.
    - square_size: Size of a square in pixels (default: 50).
    - output_file: Path to save the chessboard image.
    """
    rows, cols = grid_size
    board_width = cols * square_size
    board_height = rows * square_size

    # Create a blank image for the chessboard
    chessboard = np.zeros((board_height, board_width), dtype=np.uint8)

    for i in range(rows):
        for j in range(cols):
            if (i + j) % 2 == 0:
                cv2.rectangle(
                    chessboard,
                    (j * square_size, i * square_size),
                    ((j + 1) * square_size, (i + 1) * square_size),
                    255,
                    -1,
                )

    # Save the chessboard pattern
    cv2.imwrite(output_file, chessboard)
    print(f"Chessboard saved as {output_file}. Please print this file.")


def capture_chessboard_images(save_folder="calibration_images", grid_size=(7, 4), num_images=20):
    """
    Captures images of the chessboard using the webcam for calibration.

    Parameters:
    - save_folder: Folder to save captured images.
    - grid_size: Number of inner corners per chessboard row and column.
    - num_images: Number of images to capture.

    Returns:
    - List of file paths to the captured images.
    """
    if not os.path.exists(save_folder):
        os.makedirs(save_folder)

    print("Press 's' to save an image, 'q' to quit.")
    cap = cv2.VideoCapture(0)

    if not cap.isOpened():
        raise RuntimeError("Could not open the webcam. Ensure a camera is connected.")

    image_count = 0
    captured_files = []

    while image_count < num_images:
        ret, frame = cap.read()
        if not ret:
            print("Failed to capture frame.")
            break

        # Convert to grayscale
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # Detect chessboard corners
        ret, corners = cv2.findChessboardCorners(
            gray, grid_size, cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE
        )

        # Draw the corners on the frame
        if ret:
            cv2.drawChessboardCorners(frame, grid_size, corners, ret)

        # Show the live video
        cv2.imshow("Chessboard Capture", frame)

        # Use waitKey to detect key presses
        key = cv2.waitKey(1)
        if key == ord('s') and ret:  # Save the image only if corners are detected
            file_path = os.path.join(save_folder, f"chessboard_{image_count + 1}.jpg")
            cv2.imwrite(file_path, frame)
            captured_files.append(file_path)
            image_count += 1
            print(f"Saved image {image_count}/{num_images}: {file_path}")

        elif key == ord('q'):  # Quit if 'q' is pressed
            print("Exiting capture.")
            break

    cap.release()
    cv2.destroyAllWindows()

    if len(captured_files) < num_images:
        print(f"Captured {len(captured_files)} images out of {num_images}.")
    else:
        print("Captured all images successfully.")

    return captured_files


def calibrate_camera(images_path, grid_size=(7, 4), square_size=1.0, output_file="calibration_data.pkl"):
    """
    Calibrates the camera using chessboard images and saves the calibration data.

    Parameters:
    - images_path: Path to calibration images (e.g., "*.jpg" or "*.png").
    - grid_size: Number of inner corners per chessboard row and column.
    - square_size: Size of a square in your defined units (e.g., cm or mm).
    - output_file: File to save the calibration data.

    Returns:
    - camera_matrix: Intrinsic camera matrix.
    - dist_coeffs: Distortion coefficients.
    """
    # Termination criteria for corner refinement
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

    # Prepare object points (3D points in real-world space)
    objp = np.zeros((grid_size[0] * grid_size[1], 3), np.float32)
    objp[:, :2] = np.mgrid[0:grid_size[0], 0:grid_size[1]].T.reshape(-1, 2)
    objp *= square_size

    # Arrays to store object points and image points
    objpoints = []  # 3D points
    imgpoints = []  # 2D points
    image_shape = None

    # Load calibration images
    images = glob.glob(images_path)

    if not images:
        raise FileNotFoundError(f"No images found at the specified path: {images_path}")

    for fname in images:
        img = cv2.imread(fname)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

        # Find the chessboard corners
        ret, corners = cv2.findChessboardCorners(
            gray, grid_size, cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_FAST_CHECK + cv2.CALIB_CB_NORMALIZE_IMAGE
        )

        if ret:
            print(f"Successfully detected corners in {fname}.")
            objpoints.append(objp)
            corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
            imgpoints.append(corners2)

            # Store the image shape for calibration
            if image_shape is None:
                image_shape = gray.shape[::-1]
        else:
            # Save failed detections for review
            debug_path = os.path.join("calibration_debug", os.path.basename(fname))
            if not os.path.exists("calibration_debug"):
                os.makedirs("calibration_debug")
            cv2.imwrite(debug_path, img)
            print(f"Failed to detect corners in {fname}. Saved for debugging at {debug_path}")

    if not objpoints or not imgpoints:
        raise RuntimeError("Failed to detect chessboard corners in any image. Check your images or grid size.")

    # Calibrate the camera
    ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, image_shape, None, None)

    if ret:
        # Save calibration data
        with open(output_file, "wb") as f:
            pickle.dump({"camera_matrix": camera_matrix, "dist_coeffs": dist_coeffs}, f)
        print(f"Calibration data saved to {output_file}.")
        return camera_matrix, dist_coeffs
    else:
        raise RuntimeError("Camera calibration failed.")


def load_calibration_data(file_path="calibration_data.pkl"):
    """
    Loads the camera calibration data from a file.

    Parameters:
    - file_path: Path to the calibration data file.

    Returns:
    - camera_matrix: Intrinsic camera matrix.
    - dist_coeffs: Distortion coefficients.
    """
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"Calibration file not found: {file_path}")

    with open(file_path, "rb") as f:
        data = pickle.load(f)

    print(f"Loaded calibration data from {file_path}.")
    return data["camera_matrix"], data["dist_coeffs"]


if __name__ == "__main__":
    # Step 1: Generate chessboard pattern
    generate_chessboard()

    # Step 2: Capture images of the chessboard
    captured_images = capture_chessboard_images()

    # Step 3: Calibrate the camera and save the calibration data
    if captured_images:
        try:
            camera_matrix, dist_coeffs = calibrate_camera("calibration_images/*.jpg")
            print("Camera Matrix:\n", camera_matrix)
            print("Distortion Coefficients:\n", dist_coeffs)

            # Step 4: Load the calibration data for verification
            loaded_camera_matrix, loaded_dist_coeffs = load_calibration_data()
            print("Loaded Camera Matrix:\n", loaded_camera_matrix)
            print("Loaded Distortion Coefficients:\n", loaded_dist_coeffs)
        except Exception as e:
            print(f"Calibration failed: {e}")
    else:
        print("No images were captured. Exiting.")