the following python code is working fine to an extent but it is too slow, pls check how to hasten the comparison process:

import os
import shutil
from PIL import Image, ExifTags
import imagehash
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
import cv2
from skimage.metrics import structural_similarity as ssim
import torch
import torchvision.transforms as transforms
from torch.cuda import is_available

# Configure logging
logging.basicConfig(
    filename='image_matching_debug.log',
    filemode='w',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# Set up CUDA device
device = torch.device('cuda' if is_available() else 'cpu')

def initialize_cuda():
    if is_available():
        torch.cuda.empty_cache()
        # Warm up CUDA
        dummy_tensor = torch.zeros(1).cuda()
        del dummy_tensor

def get_image_orientation(image):
    try:
        exif = image._getexif()
        if exif is not None:
            for tag, value in exif.items():
                decoded = ExifTags.TAGS.get(tag, tag)
                if decoded == "Orientation":
                    return value
        return None
    except Exception as e:
        logging.error(f"Error retrieving EXIF data: {str(e)}")
        return None

def calculate_image_hashes(image_path):
    try:
        image = Image.open(image_path).convert("RGB")
        # Convert PIL Image to tensor and move to GPU
        transform = transforms.ToTensor()
        image_tensor = transform(image).unsqueeze(0).to(device)
        
        # Handle orientations
        orientation = get_image_orientation(image)
        if orientation == 6:
            image_tensor = torch.rot90(image_tensor, k=3, dims=[2, 3])
        elif orientation == 3:
            image_tensor = torch.rot90(image_tensor, k=2, dims=[2, 3])
        elif orientation == 8:
            image_tensor = torch.rot90(image_tensor, k=1, dims=[2, 3])
            
        hashes = []
        for angle in [0, 90, 180, 270]:
            if angle != 0:
                rotated_tensor = torch.rot90(image_tensor, k=angle//90, dims=[2, 3])
            else:
                rotated_tensor = image_tensor
            # Convert back to PIL for hashing
            rotated_image = transforms.ToPILImage()(rotated_tensor.squeeze(0).cpu())
            hash_value = imagehash.phash(rotated_image)
            hashes.append(hash_value)
        return hashes
    except Exception as e:
        logging.error(f"Error processing {image_path}: {str(e)}")
        return []

def compare_images(thumbnail_hashes, original_hashes, max_distance=5):
    closest_distance = float('inf')
    for t_hash in thumbnail_hashes:
        for o_hash in original_hashes:
            distance = t_hash - o_hash
            if distance < closest_distance:
                closest_distance = distance
            if distance == 0:
                return True, 0
    return closest_distance <= max_distance, closest_distance

def secondary_ssim_check(image1_path, image2_path, threshold=0.85):
    try:
        img1 = cv2.imread(image1_path, cv2.IMREAD_GRAYSCALE)
        img2 = cv2.imread(image2_path, cv2.IMREAD_GRAYSCALE)
        
        if img1 is None or img2 is None:
            logging.error(f"Error reading images for SSIM: {image1_path}, {image2_path}")
            return False
            
        # Convert to tensors and move to GPU
        img1_tensor = torch.from_numpy(img1).float().unsqueeze(0).unsqueeze(0).to(device)
        img2_tensor = torch.from_numpy(img2).float().unsqueeze(0).unsqueeze(0).to(device)
        
        # Ensure same size
        if img1_tensor.shape != img2_tensor.shape:
            img2_tensor = torch.nn.functional.interpolate(
                img2_tensor, 
                size=(img1_tensor.shape[2], img1_tensor.shape[3])
            )
        
        # Calculate SSIM on GPU
        ssim_score = ssim(
            img1_tensor.cpu().numpy().squeeze(),
            img2_tensor.cpu().numpy().squeeze(),
            data_range=255
        )
        return ssim_score >= threshold
        
    except Exception as e:
        logging.error(f"SSIM error between {image1_path} and {image2_path}: {str(e)}")
        return False

def process_thumbnail(thumbnail_path, original_hash_dict, output_folder, max_distance, copied_files):
    thumbnail_file = os.path.basename(thumbnail_path)
    thumbnail_hashes = calculate_image_hashes(thumbnail_path)
    
    if not thumbnail_hashes:
        logging.warning(f"Skipping thumbnail {thumbnail_file} due to hash calculation error.")
        print(f"Skipping thumbnail {thumbnail_file} due to hash calculation error.")
        return False, None, None

    best_match = None
    best_distance = float('inf')
    
    for original_path, original_hashes in original_hash_dict.items():
        if not original_hashes:
            continue
        matched, distance = compare_images(thumbnail_hashes, original_hashes, max_distance)
        logging.debug(f"Comparing {thumbnail_file} with {os.path.basename(original_path)}: distance = {distance}")
        if matched and distance < best_distance:
            best_distance = distance
            best_match = original_path

    if best_match:
        original_file = os.path.basename(best_match)
        if original_file not in copied_files:
            ssim_passed = secondary_ssim_check(best_match, thumbnail_path)
            if ssim_passed:
                try:
                    output_path = os.path.join(output_folder, original_file)
                    shutil.copy2(best_match, output_path)
                    copied_files.add(original_file)
                    print(f"Copied matching image: {original_file} to {output_folder} (Distance: {best_distance})")
                    return True, original_file, best_distance
                except Exception as e:
                    logging.error(f"Error copying {original_file} to output: {str(e)}")
                    return False, None, None
            else:
                logging.info(f"SSIM check failed for: {original_file} with thumbnail: {thumbnail_file}")

    logging.info(f"No suitable match found for thumbnail: {thumbnail_file}")
    print(f"No suitable match found for thumbnail: {thumbnail_file}")
    return False, None, None

def process_image_batch(image_paths, batch_size=16):
    images = []
    for i in range(0, len(image_paths), batch_size):
        batch_paths = image_paths[i:i + batch_size]
        batch_tensors = []
        for path in batch_paths:
            img = Image.open(path).convert("RGB")
            tensor = transforms.ToTensor()(img).unsqueeze(0)
            batch_tensors.append(tensor)
        batch = torch.cat(batch_tensors, dim=0).to(device)
        images.append(batch)
    return images

def find_matching_images(thumbnails_folder, originals_folder, output_folder, max_distance=5, batch_size=50):
    if is_available():
        initialize_cuda()
        
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
    logging.info(f"Created output folder at {output_folder}")
    print(f"Created output folder at {output_folder}")

    original_files = [f for f in os.listdir(originals_folder) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
    original_image_paths = [os.path.join(originals_folder, f) for f in original_files]
    print(f"Found {len(original_files)} original images.")
    logging.info(f"Found {len(original_files)} original images.")

    original_hash_dict = {}
    with ThreadPoolExecutor() as executor:
        futures = {executor.submit(calculate_image_hashes, path): path for path in original_image_paths}
        for future in as_completed(futures):
            path = futures[future]
            hashes = future.result()
            original_hash_dict[path] = hashes
            if not hashes:
                logging.warning(f"No hashes computed for original image: {path}")

    thumbnail_files = [f for f in os.listdir(thumbnails_folder) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
    thumbnail_image_paths = [os.path.join(thumbnails_folder, f) for f in thumbnail_files]
    print(f"Found {len(thumbnail_files)} thumbnail images.")
    logging.info(f"Found {len(thumbnail_files)} thumbnail images.")

    copied_files = set()
    total_matches = 0
    
    with ThreadPoolExecutor() as executor:
        futures = []
        for thumbnail_path in thumbnail_image_paths:
            futures.append(executor.submit(process_thumbnail, thumbnail_path, original_hash_dict, output_folder, max_distance, copied_files))
        
        for future in as_completed(futures):
            matched, original_file, distance = future.result()
            if matched:
                total_matches += 1
                logging.info(f"Matched thumbnail with original: {original_file} (Distance: {distance})")

    print(f"Image matching completed. Total matches found: {total_matches}")
    logging.info(f"Image matching completed. Total matches found: {total_matches}")

if __name__ == "__main__":
    thumbnails_folder = r"D:\TBD\Deepa Maternity Shoot Thumbnails"
    originals_folder = r"D:\TBD\Deepa Maternity Shoot"
    output_folder = r"D:\TBD\Deepa Maternity Shoot Selects"
    
    find_matching_images(
        thumbnails_folder,
        originals_folder,
        output_folder,
        max_distance=2  # Even stricter threshold for higher accuracy
    )