Can the images in the thumbnails folder be checked for orientation and rotated if needed before starting the matching process? Also, save the processed images in a separate folder, as I’d like to check them for accuracy.

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
    )