Please find error in Python code:
import cv2
import os
import numpy as np
from tqdm import tqdm
from skimage.metrics import structural_similarity as ssim
from concurrent.futures import ThreadPoolExecutor

def crop_black_bars(image):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # Apply a binary threshold to convert the image to binary
    thresh = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY)[1]

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

    if len(contours) == 0:
        return image  # No contours found, return the original image

    # Get the largest contour, which should be the area with the actual content
    cnt = max(contours, key=cv2.contourArea)
    x, y, w, h = cv2.boundingRect(cnt)

    # Crop the image using the dimensions of the bounding rectangle
    return image[y:y+h, x:x+w]

def resize_image(image, size):
    return cv2.resize(image, size, interpolation=cv2.INTER_AREA)

def process_frame(frame, prev_frame, frame_index, output_folder, similarity_threshold, target_size):
    # Crop the black bars from each frame
    cropped_frame = crop_black_bars(frame)
    gray = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2GRAY)
    resized_gray = resize_image(gray, target_size)

    # Compute the similarity between current and previous frames only if prev_frame exists
    frame_sim = ssim(prev_frame, resized_gray) if prev_frame is not None else 0

    if prev_frame is None or frame_sim < similarity_threshold:
        frame_filename = os.path.join(output_folder, f"frame_{frame_index:05d}.png")
        cv2.imwrite(frame_filename, cropped_frame)
        return resized_gray, True  # Return the resized_gray for updating prev_frame and Boolean to update frame counter
    else:
        return resized_gray, False

def extract_unique_frames(video_path, output_folder, similarity_threshold=0.9, target_size=(800, 600), max_workers=4):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)

    cap = cv2.VideoCapture(video_path)

    try:
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    except:
        total_frames = -1  # In case the total frame count couldn’t be determined

    prev_frame = None
    saved_frames = 0

    with tqdm(total=total_frames, desc="Processing frames", unit="frame") as pbar:
        ThreadPoolExecutor(max_workers=max_workers) as executor:
        # Submit frame for processing
        for frame_index in range(total_frames):
            ret, frame = cap.read()
            if not ret:
                break
            
            # Process the frames asynchronously using concurrent threads
            future = executor.submit(process_frame, frame, prev_frame, frame_index, output_folder, similarity_threshold, target_size)
            resized_gray, is_saved = future.result()

            if is_saved:
                saved_frames += 1
            prev_frame = resized_gray  # Update prev_frame for the computation of the next frame
            
            pbar.update(1)  # Update progress bar

    cap.release()
    print(f"Extraction complete. {saved_frames} unique frames saved.")


# Set the path to your video and the folder where you want to save the frames
video_path = "video.mp4"
output_folder = "out"
target_size = (800, 600)

# Extract unique frames with a default similarity threshold
extract_unique_frames(video_path, output_folder)

Output:
  File "C:\Users\Zzz\Desktop\video.py", line 60
    ThreadPoolExecutor(max_workers=max_workers) as executor:
                                                ^^
SyntaxError: invalid syntax