I want to use Python and OpenCV to align two rectangular images then merge them into one. The rectangles will mostly overlap, but the resulting image will have overhangs. I need to obtain the largest rectangle where each pixel contains data from both originals then crop the outside. ChatGPT has produced code which aligns and merges them. However despite much "discussion" it doesn't crop the overhangs properly. I've tried my own idea but it doesn't work. I expected there to be code around to do this but can't find any. I can post the code from other attempts if that would help. This version: For each image Find the highest corner, the lowest etc Work out from this whether the image leans left or right. From that find the min and max x and y to use for the extremes of the final image. Take the innermost figures from the 2 images. I may have misunderstood how OpenCV stores its images. I don't know how the code would deal with an image that was horizontal and it may not deal with other possibilities. #! /usr/bin/python3 import cv2 import numpy as np def find_corners(image): """ Finds the extreme corners of the valid image region (non-black pixels). """ coords = np.column_stack(np.where(np.any(image > 0, axis=2))) if coords.size == 0: return None # No valid pixels found bottom_y, bottom_x = coords[np.argmax(coords[:, 0])] right_y, right_x = coords[np.argmax(coords[:, 1])] top_y, top_x = coords[np.argmin(coords[:, 0])] left_y, left_x = coords[np.argmin(coords[:, 1])] print('Bottom', bottom_x, bottom_y, 'right', right_x, right_y, 'top', top_x, top_y, 'left',left_x, left_y) return bottom_x, bottom_y, right_x, right_y, top_x, top_y, left_x, left_y def find_alignment(imageL, imageR): """ Finds the rotation and vertical shift needed to align imageR to imageL using ECC. Only vertical shifting and rotation are allowed (no horizontal shift). """ grayL = cv2.cvtColor(imageL, cv2.COLOR_BGR2GRAY) grayR = cv2.cvtColor(imageR, cv2.COLOR_BGR2GRAY) warp_matrix = np.eye(2, 3, dtype=np.float32) criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 50, 1e-6) cc, warp_matrix = cv2.findTransformECC(grayL, grayR, warp_matrix, cv2.MOTION_AFFINE, criteria) angle = np.arctan2(warp_matrix[1, 0], warp_matrix[0, 0]) * (180.0 / np.pi) vertical_shift = int(warp_matrix[1, 2]) warp_matrix[0, 2] = 0 alignedR = cv2.warpAffine(imageR, warp_matrix, (imageR.shape[1], imageR.shape[0])) print(f"ECC Alignment → Rotation: {angle:.2f}° | Vertical Shift: {vertical_shift} pixels") return imageL, alignedR def get_overlap_region(imageL, imageR): """ Compute the largest overlapping area after alignment. """ left_corners = find_corners(imageL) right_corners = find_corners(imageR) if left_corners is None or right_corners is None: print("Error: No valid pixels found in one or both images.") return None, None left_B_x, left_B_y, left_R_x, left_R_y, left_T_x, left_T_y, left_L_x, left_L_y = left_corners right_B_x, right_B_y, right_R_x, right_R_y, right_T_x, right_T_y, right_L_x, right_L_y = right_corners # Determine inner bounding box based on rotation left_y_min, left_y_max = (left_L_y, left_R_y) if left_R_y > left_L_y else (left_R_y, left_L_y) left_x_min, left_x_max = (left_B_x, left_T_x) if left_R_y > left_L_y else (left_T_x, left_B_x) right_y_min, right_y_max = (right_L_y, right_R_y) if right_R_y > right_L_y else (right_R_y, right_L_y) right_x_min, right_x_max = (right_B_x, right_T_x) if right_R_y > right_L_y else (right_T_x, right_B_x) # Find cropping limits top_limit = max(left_y_min, right_y_min) bottom_limit = min(left_y_max, right_y_max) left_limit = max(left_x_min, right_x_min) right_limit = min(left_x_max, right_x_max) if bottom_limit <= top_limit or right_limit <= left_limit: print("Error: No valid overlap region.") return None, None print(f"Cropping to: top={top_limit}, bottom={bottom_limit}, left={left_limit}, right={right_limit}") return imageL[top_limit:bottom_limit, left_limit:right_limit], imageR[top_limit:bottom_limit, left_limit:right_limit] def create_anaglyph(imageL, imageR): if imageL is None or imageR is None: print("Error: Cropped images are invalid.") return None red_channel = imageL[:, :, 2] green_channel = imageR[:, :, 1] blue_channel = imageR[:, :, 0] return cv2.merge((blue_channel, green_channel, red_channel)) if __name__ == "__main__": file = input("Enter: ") fileL = file + 'L.jpg' fileR = file + 'R.jpg' imageL = cv2.imread(fileL, cv2.IMREAD_COLOR) imageR = cv2.imread(fileR, cv2.IMREAD_COLOR) if imageL is None or imageR is None: print("Error: Could not load one or both images.") exit(1) imageL_aligned, imageR_aligned = find_alignment(imageL, imageR) imageL_cropped, imageR_cropped = get_overlap_region(imageL_aligned, imageR_aligned) if imageL_cropped is None or imageR_cropped is None: print("Error: Unable to generate anaglyph due to invalid cropping region.") exit(1) final_height = min(imageL_cropped.shape[0], imageR_cropped.shape[0]) final_width = min(imageL_cropped.shape[1], imageR_cropped.shape[1]) imageL_cropped = imageL_cropped[:final_height, :final_width] imageR_cropped = imageR_cropped[:final_height, :final_width] anaglyph_image = create_anaglyph(imageL_cropped, imageR_cropped) if anaglyph_image is not None: cv2.imwrite(file+"-anaglyph.jpg", anaglyph_image) Continue reading...