Can the images in the thumbnails folder be checked for orientation and rotated if needed before starting the matching process?

import os
import shutil
from PIL import Image, ExifTags
import imagehash
from concurrent.futures import ProcessPoolExecutor, as_completed
import logging
import cv2
from skimage.metrics import structural_similarity as ssim

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

def get_image_orientation(image):
    """Retrieve the orientation tag from an image's EXIF data."""
    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_hash(image_path):
    """Calculate the perceptual hash of an image after correcting orientation."""
    try:
        image = Image.open(image_path).convert("RGB")
        
        # Handle orientations
        orientation = get_image_orientation(image)
        if orientation == 6:
            image = image.transpose(Image.ROTATE_270)
        elif orientation == 3:
            image = image.transpose(Image.ROTATE_180)
        elif orientation == 8:
            image = image.transpose(Image.ROTATE_90)
                
        # Compute the perceptual hash
        hash_value = imagehash.phash(image)
        return hash_value
    except Exception as e:
        logging.error(f"Error processing {image_path}: {str(e)}")
        return None

def calculate_thumbnail_hashes(image_path):
    """Calculate perceptual hashes for a thumbnail image at multiple rotations."""
    try:
        image = Image.open(image_path).convert("RGB")
        
        # Handle orientations
        orientation = get_image_orientation(image)
        if orientation == 6:
            image = image.transpose(Image.ROTATE_270)
        elif orientation == 3:
            image = image.transpose(Image.ROTATE_180)
        elif orientation == 8:
            image = image.transpose(Image.ROTATE_90)
                
        rotated_hashes = []
        for angle in [0, 90, 180, 270]:
            rotated_image = image.rotate(angle, expand=True)
            hash_value = imagehash.phash(rotated_image)
            rotated_hashes.append(hash_value)
        return rotated_hashes
    except Exception as e:
        logging.error(f"Error processing {image_path}: {str(e)}")
        return []

def compare_hashes(hash1, hash2, max_distance=5):
    """Compare two hashes and return True if the distance is within max_distance."""
    if hash1 is None or hash2 is None:
        return False
    distance = hash1 - hash2
    return distance <= max_distance

def secondary_ssim_check(image1_path, image2_path, threshold=0.85):
    """Perform a secondary check using SSIM to confirm image match."""
    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
            
        # Ensure same size
        if img1.shape != img2.shape:
            img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
        
        # Calculate SSIM
        ssim_score = ssim(img1, img2, data_range=img1.max() - img1.min())
        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):
    """Process a thumbnail image to find and copy the matching original image."""
    thumbnail_file = os.path.basename(thumbnail_path)
    thumbnail_hashes = calculate_thumbnail_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

    # Find matching original images
    matches = []
    for original_path, original_hash in original_hash_dict.items():
        for thumbnail_hash in thumbnail_hashes:
            if compare_hashes(thumbnail_hash, original_hash, max_distance):
                matches.append(original_path)
                break  # Stop at first match for this original image
    
    # If matches found, proceed with secondary SSIM check
    for best_match in matches:
        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}")
                    return True, original_file, 0  # Assuming perfect match after SSIM
                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 find_matching_images(thumbnails_folder, originals_folder, output_folder, max_distance=5):
    """Main function to find and copy matching images from originals to output based on thumbnails."""
    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.")

    # Compute hashes for original images
    original_hash_dict = {}
    with ProcessPoolExecutor() as executor:
        futures = {executor.submit(calculate_image_hash, path): path for path in original_image_paths}
        for future in as_completed(futures):
            path = futures[future]
            hash_value = future.result()
            original_hash_dict[path] = hash_value
            if not hash_value:
                logging.warning(f"No hash 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

    # Process thumbnails (small number, so sequential processing is acceptable)
    for thumbnail_path in thumbnail_image_paths:
        matched, original_file, _ = process_thumbnail(thumbnail_path, original_hash_dict, output_folder, max_distance, copied_files)
        if matched:
            total_matches += 1
            logging.info(f"Matched thumbnail with original: {original_file}")

    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=50  # Adjust the threshold as needed
    )