Introduction
This tutorial shows how to build an OpenCV image comparison pipeline in Python that detects visual differences between two images and highlights them with bounding boxes.
You will learn how to preprocess the input, isolate the two pictures inside a collage, align their sizes, compute pixel-wise differences, reduce noise, and extract clean contours of the changed regions.
The final result is a simple and effective “spot the difference” solution that is fast, easy to understand, and production-ready for many computer vision tasks.
We optimize this guide for the keyword OpenCV image comparison, and reinforce it with related phrases like find differences between two images Python, cv2.absdiff, Canny edge detection, contours, and HSV masking to improve search visibility.
check out our video here : https://youtu.be/03tY_OF0_Jg&list=UULFTiWJJhaH6BviSWKLJUM9sg
You can find more similar tutorials in my blog posts page here : https://eranfeit.net/blog/
You can find the full code here : https://ko-fi.com/s/755696b059
Complete OpenCV Pipeline for Spotting Differences
We load the collage image, locate and crop the two panels, align their sizes, compute absolute differences, suppress noise with Gaussian blur, and detect contours over the changed regions.
We then draw bounding boxes on both images and a result canvas to clearly visualize all detected differences.
This is our two images :
### Import OpenCV for image processing and computer vision operations. import cv2 ### Import NumPy for numerical array operations and mask handling. import numpy as np ### Read the collage that contains two side-by-side images into a NumPy array. img = cv2.imread("Open-CV/Find-10-Differences/two-images.jpg") ### Convert the image to grayscale to simplify thresholding and edge detection. gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) ### Apply a binary inverse threshold to separate the two image panels from the background. _ , thresh = cv2.threshold(gray , 230, 255 , cv2.THRESH_BINARY_INV) ### Run Canny edge detection to get crisp borders around the two panels. canny = cv2.Canny(thresh , 254 , 255) ### Find external contours to capture the two largest rectangular regions that contain the images. contours , hierarchy = cv2.findContours(canny.copy() , cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ### Optionally draw contours for debugging to verify panel detection accuracy. #cv2.drawContours(img, contours, -1 , (0,255,0) ,2 ) ### Sort contours by area so we can retrieve the two biggest panels reliably. sortedCon = sorted(contours, key = cv2.contourArea) ### Compute a bounding rectangle around the largest panel. x,y,w,h = cv2.boundingRect(sortedCon[0]) ### Draw a rectangle on the collage to visualize the first panel boundaries. cv2.rectangle(img, (x,y), (x+w , y+h), (255,0,0), 3) ### Crop the first region of interest (ROI) that represents the first image. roi = img.copy()[y:y+h , x:x+w] ### Compute a bounding rectangle around the second panel. x1,y1,w1,h1 = cv2.boundingRect(sortedCon[1]) ### Draw a rectangle to visualize the second panel boundaries. cv2.rectangle(img, (x1,y1), (x1+w1 , y1+h1), (255,0,0), 3) ### Crop the second region of interest (ROI) that represents the second image. roi2 = img.copy()[y1:y1+h1 , x1:x1+w1] ### Print the shapes so we can confirm both ROIs and plan alignment if needed. print(roi.shape) ### Print the second ROI shape for comparison against the first ROI. print(roi2.shape) ### Retrieve the height of the second ROI to trim one row if necessary for shape alignment. roi2H = roi2.shape[0] ### Print the current height for transparency during debugging. print(roi2H) ### Align heights by removing a bottom row if the sizes are off by one pixel. roi2 = roi2[0:roi2H-1, :] ### Confirm the new shape after alignment. print(roi2.shape) ### Retrieve the width of the first ROI to trim one column if necessary for shape alignment. roi1W = roi.shape[1] ### Print the current width for transparency during debugging. print(roi1W) ### Align widths by removing the last column if the sizes are off by one pixel. roi = roi[: , 0:roi1W-1] ### Confirm the new shape after alignment. print(roi.shape) ### Compute pixel-wise absolute difference and invert to emphasize similar background while keeping differences bright. diff = 255 - cv2.absdiff(roi,roi2) ### Define the lower HSV threshold for masking potential difference regions. lower = np.array([0,0,0]) ### Define the upper HSV threshold to include the full range of values. upper = np.array([180,255,255]) ### Convert the difference image from BGR to HSV for robust value-based masking. imgHSV = cv2.cvtColor(diff, cv2.COLOR_BGR2HSV) ### Create a binary mask selecting all HSV values in the defined range. mask = cv2.inRange(imgHSV, lower, upper) ### Keep only the masked regions from the difference image for cleaner downstream processing. result = cv2.bitwise_and(diff, diff , mask=mask) ### Apply Gaussian blur to reduce salt-and-pepper noise before edge detection. blur = cv2.GaussianBlur(result, (11,11), 0) ### Run Canny a second time on the denoised image to outline difference boundaries sharply. canny = cv2.Canny(blur, 50 , 150) ### Dilate the edges to close small gaps and make contours easier to detect. edges = cv2.dilate(canny, None) ### Extract contours corresponding to distinct difference regions between the two images. contours , hierarchy = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ### Iterate over each contour to compute bounding boxes around detected differences. for cnt in contours: ### Get the top-left coordinate and size of the bounding rectangle for the current difference region. x, y, width, height = cv2.boundingRect(cnt) ### Draw the bounding box on the result canvas to visualize detected differences. cv2.rectangle(result, (x,y), (x+width, y+height), (0,0,255), 2) ### Draw the same bounding box on the first ROI for side-by-side visualization. cv2.rectangle(roi, (x,y), (x+width, y+height), (0,0,255), 2) ### Draw the same bounding box on the second ROI for side-by-side visualization. cv2.rectangle(roi2, (x,y), (x+width, y+height), (0,0,255), 2) ### Display the composite result with highlighted differences. cv2.imshow("result", result) ### Display the first cropped image with bounding boxes. cv2.imshow("roi",roi) ### Display the second cropped image with bounding boxes. cv2.imshow("roi2",roi2) ### Display the Canny output used to detect edges of the difference regions. cv2.imshow("canny", canny) ### Display the dilated edges that feed the contour extraction. cv2.imshow("edges", edges) ### Wait for a key press to keep windows open until the user is ready to close them. cv2.waitKey(0) ### Close all OpenCV windows to release resources gracefully. cv2.destroyAllWindows()
You can find the full code here : https://ko-fi.com/s/755696b059
You detected and visualized differences between two images using a robust OpenCV workflow.
Key steps included panel detection, ROI alignment, absolute difference computation, HSV masking, denoising, edge detection, contour extraction, and drawing bounding boxes.
This approach is flexible and can be adapted for quality control, spot-the-difference games, or change detection in visual monitoring systems.
The result :
Connect :
☕ Buy me a coffee — https://ko-fi.com/eranfeit
🖥️ Email : feitgemel@gmail.com
🤝 Fiverr : https://www.fiverr.com/s/mB3Pbb
Enjoy,
Eran