How to Use OpenCV to Find Changes Between Website Screenshots
Compare website screenshots with OpenCV using pixel differences, thresholds, and region highlighting. Learn how to prepare captures and handle common sources of noise.
Use OpenCV’s cv2.absdiff to calculate per-pixel differences between two aligned screenshots. Then convert the difference to grayscale and apply a threshold to create a mask of candidate changes. The mask shows where pixels vary; it does not determine whether a variation is a bug.
For a useful comparison, capture both pages at the same viewport, device scale, scroll position, and UI state. If you need screenshots to compare, ScreenshotNeo can capture a page through its screenshot API; the main workflow below starts with two image files you already have.
1. Prepare screenshots that can be compared
Pixel comparison works on corresponding array positions. Before running it, check that the screenshots use matching capture conditions:
- Use the same viewport width and height and the same device scale.
- Capture the same scroll position, page state, and target content.
- Wait for the same loading state. Differences in loaded fonts, images, or other content can create a large diff.
- Keep animations and other changing content in a consistent state where possible.
- Use the same image format and color handling when you can.
If dimensions differ, decide whether the size change is itself meaningful. Otherwise, crop or resize deliberately before comparison. Do not silently resize: interpolation can introduce pixel differences, and stretching can hide layout changes.
2. Install OpenCV
For the Python example below, install the headless package if you only need to read and write image files. Use the regular package if you want OpenCV’s GUI display functions:
python -m pip install opencv-python-headless
Save the baseline as before.png and the candidate screenshot as after.png in the same directory as the script.
3. Generate a difference image and a change mask
This runnable script validates the input files and dimensions, computes the absolute per-pixel difference, creates a thresholded mask, and writes both a raw diff and a highlighted candidate-change image.
import cv2
before = cv2.imread("before.png")
after = cv2.imread("after.png")
if before is None:
raise FileNotFoundError("Could not read before.png")
if after is None:
raise FileNotFoundError("Could not read after.png")
if before.shape != after.shape:
raise ValueError(
f"Screenshots must have matching dimensions and channels: "
f"{before.shape} vs {after.shape}"
)
# Absolute difference for each corresponding array element.
diff = cv2.absdiff(before, after)
# A pixel is a candidate change when its grayscale difference exceeds the cutoff.
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
cutoff = 25 # Starter value only; tune against your capture conditions.
_, mask = cv2.threshold(gray, cutoff, 255, cv2.THRESH_BINARY)
# Mark candidate changes red in a copy of the candidate screenshot (OpenCV uses BGR).
highlight = after.copy()
highlight[mask > 0] = (0, 0, 255)
if not cv2.imwrite("diff.png", diff):
raise OSError("Could not write diff.png")
if not cv2.imwrite("mask.png", mask):
raise OSError("Could not write mask.png")
if not cv2.imwrite("highlight.png", highlight):
raise OSError("Could not write highlight.png")
print("Wrote diff.png, mask.png, and highlight.png")
Open diff.png to inspect the raw channel differences, mask.png to see the thresholded candidate regions, and highlight.png to view those regions in context on the candidate. Review them alongside both original screenshots.
4. Tune the threshold and interpret the output
The cutoff value in the script is a starting point, not a universal setting. A lower value includes smaller intensity differences and more rendering noise; a higher value suppresses more subtle variations and can miss small changes. Tune it against representative pairs from your site and inspect the output rather than treating one cutoff as a general rule. OpenCV’s thresholding tutorial covers thresholding methods.
cv2.absdiff computes an absolute difference between corresponding array elements and returns an array with the same shape and type. That makes it a direct way to produce a location-preserving pixel diff when the input arrays are compatible. See the OpenCV array operations reference.
A thresholded mask is a candidate-change map, not a bug detector. A one-pixel shift can light up many edges. A changed timestamp may be irrelevant, while a tiny text change may matter. Filtering small regions can make an image easier to scan, but can also hide meaningful details. Always inspect the original screenshots before deciding what a diff means.
5. Choose the comparison that answers your question
| Method | What it tells you | Best for | Limitation |
|---|---|---|---|
absdiff plus threshold |
Where pixel values differ beyond a cutoff | Finding the changed area in a screenshot | Sensitive to capture noise, shifts, and threshold choice |
| Histogram comparison | How similar overall image distributions are | A coarse image-level similarity score | Does not identify where the UI changed |
OpenCV’s histogram comparison tutorial describes several histogram comparison metrics. Use a histogram score when a global comparison is enough; use a diff map when you need to locate a change. A global score cannot tell you which component moved.
6. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
Could not read / image is None |
Wrong path, missing file, or unsupported/corrupt input | Check the working directory and filename; verify that each screenshot opens as an image. |
| Dimension or shape mismatch | Different screenshot sizes, channel counts, or image modes | Decide whether the size difference is meaningful. If not, crop or resize intentionally and make channel formats consistent before calling absdiff. |
| Almost the whole page is highlighted | Different capture conditions, a shifted page, incomplete loading, or a cutoff that is too low | Align viewport, scale, scroll position, and page state; wait for consistent loading; tune the threshold. |
| Small text changes disappear | The cutoff is too high, or small regions are being filtered out | Lower the cutoff or disable area filtering, then inspect the text in the original images. |
| Edges appear doubled in the diff | Content moved by a pixel or more | Check alignment and capture settings. The diff reports positional pixel variation; it does not compensate for movement. |
| Red highlighting appears blue | Color channels were treated as RGB instead of OpenCV’s BGR order | For OpenCV arrays use (0, 0, 255) for red. Convert color ordering only when passing data to software that expects a different convention. |
| Diff output is empty or unexpectedly subtle | Images may be identical, or the cutoff may exceed the differences | Inspect diff.png and lower the cutoff to see more candidate changes. |
7. Performance and reliability notes
The direct workflow reads two images, computes an array difference, and writes a few output images. For repeatable results, keep the input capture process stable and retain the original images with the diff artifacts so a reviewer can distinguish page changes from capture noise.
For large screenshots or many pairs, image dimensions and the number of comparisons determine how much image data the script processes. Compare only the regions you need when that matches the question, but record the crop so comparisons remain consistent. Do not assume that a compact score means the important UI is unchanged.
Pixel-level methods have known challenges on web interfaces with layout diversity, dense text, and fine-grained changes; a 2026 paper discusses these limitations at the abstract level in its work on web UI visual regression. This is context for careful interpretation, not a benchmark of this script. See Beyond Pixel Diffs: Benchmarking Image Change Captioning for Web UI Visual Regression Testing.
Or skip the browser setup
If you need consistent screenshots for the OpenCV comparison, ScreenshotNeo captures a page with one GET request. See the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Consistent capture settings still matter when you compare images. Sign up for 1,000 free screenshots a month, with no card.
FAQ
Does a pixel diff tell me whether a visual change is a defect?
No. It identifies pixel variation. A developer still needs to judge whether the change is expected and important.
Should I compare screenshots with different dimensions?
Only if you have a deliberate reason. Different dimensions change the pixel correspondence; first decide whether that difference is itself the result you want to detect.
Is histogram comparison a replacement for a diff image?
No. It answers a global similarity question and does not locate changed regions.


