ScreenshotNeo

BlogHow-to

How to detect a price change in product screenshots with OpenCV

Compare aligned price crops with OpenCV, or use OCR when digits and formats change. Includes runnable Python code, localization, troubleshooting, and alerting guidance.

By the ScreenshotNeo team4 October 202611 min read

To detect a price change in product screenshots, isolate the same price region in each capture and compare aligned crops. OpenCV can locate a stable region and measure visual differences. When the digits, currency, or formatting may change, run OCR on each crop and compare normalized values: template matching locates image patterns, but it does not read a new price.

This guide uses Python and OpenCV for localization and image comparison, with optional Tesseract OCR for reading prices. Treat thresholds and OCR results as signals to validate on your own pages, not as guaranteed detection.

1. Choose a detection method

Page conditions Start with Watch for
Same layout, position, font, and price format Aligned crop comparison Antialiasing, scaling, and rendering variation can look like changes.
Price can gain or lose digits, or formatting can change OCR followed by normalized numeric comparison OCR can misread characters; inspect uncertain results.
Price moves, but a nearby visual anchor stays put Template matching to locate the anchor, then crop and compare or OCR Matching finds a patch; it does not recognize the price text.
Several copies of an anchor could appear Threshold the match result map and filter candidates Validate the threshold and remove overlapping detections.

OpenCV documents six template-matching methods. With the two TM_SQDIFF methods, the minimum score is the best match; with the other methods, the maximum is best. Its result map has dimensions (W-w+1, H-h+1) for a source image of W×H and template of w×h. See the OpenCV template matching tutorial.

2. Install Python dependencies and prepare screenshots

Install OpenCV and NumPy. Save two screenshots of the same product page as before.png and after.png. Keep viewport, device scale, browser state, and capture timing consistent. The example below uses a known stable patch near the price as an anchor. Create anchor.png by cropping a distinctive nearby element from the before screenshot. It should not include the changing digits.

python -m pip install opencv-python numpy

Check that the files exist, open successfully, and have the expected dimensions. Reject missing, empty, or failed captures rather than treating them as price changes.

3. Locate the price with template matching

This runnable script searches for the anchor in both screenshots, then crops a fixed price rectangle at a known offset from that anchor. Adjust PRICE_OFFSET_X, PRICE_OFFSET_Y, PRICE_WIDTH, and PRICE_HEIGHT to match your page. The example uses normalized correlation, for which a higher score is a better match. Set MATCH_THRESHOLD only after checking representative screenshots; 0.8 appears in OpenCV’s tutorial as a demonstration value for repeated matches, not as a validated price detection threshold.

import cv2
import sys

BEFORE_PATH = "before.png"
AFTER_PATH = "after.png"
ANCHOR_PATH = "anchor.png"

# Set these offsets and dimensions for your product page.
PRICE_OFFSET_X = 0
PRICE_OFFSET_Y = 40
PRICE_WIDTH = 220
PRICE_HEIGHT = 70
MATCH_THRESHOLD = 0.85


def load_image(path, flags=cv2.IMREAD_COLOR):
    image = cv2.imread(path, flags)
    if image is None or image.size == 0:
        raise ValueError(f"Could not load a valid image: {path}")
    return image


def find_anchor(image, template):
    ih, iw = image.shape[:2]
    th, tw = template.shape[:2]
    if th > ih or tw > iw:
        raise ValueError("Anchor template is larger than the screenshot")
    result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
    _min_val, max_val, _min_loc, max_loc = cv2.minMaxLoc(result)
    if max_val < MATCH_THRESHOLD:
        raise ValueError(
            f"Anchor match score {max_val:.3f} is below threshold "
            f"{MATCH_THRESHOLD:.3f}"
        )
    return max_loc, max_val


def crop_price(image, anchor_xy):
    x = anchor_xy[0] + PRICE_OFFSET_X
    y = anchor_xy[1] + PRICE_OFFSET_Y
    h, w = image.shape[:2]
    x2, y2 = x + PRICE_WIDTH, y + PRICE_HEIGHT
    if x < 0 or y < 0 or x2 > w or y2 > h:
        raise ValueError("Configured price crop falls outside the screenshot")
    return image[y:y2, x:x2]


def main():
    before = load_image(BEFORE_PATH)
    after = load_image(AFTER_PATH)
    anchor = load_image(ANCHOR_PATH)

    before_loc, before_score = find_anchor(before, anchor)
    after_loc, after_score = find_anchor(after, anchor)
    before_crop = crop_price(before, before_loc)
    after_crop = crop_price(after, after_loc)

    if before_crop.shape != after_crop.shape:
        raise ValueError("Price crops differ in size; check capture and crop settings")

    # Mean absolute pixel difference: useful as a review signal, not a universal cutoff.
    diff = cv2.absdiff(before_crop, after_crop)
    mean_diff = float(diff.mean())
    print(f"Before anchor score: {before_score:.3f}")
    print(f"After anchor score:  {after_score:.3f}")
    print(f"Mean crop difference: {mean_diff:.3f} on a 0-255 scale")

    cv2.imwrite("price-before.png", before_crop)
    cv2.imwrite("price-after.png", after_crop)
    cv2.imwrite("price-diff.png", diff)
    print("Saved crops and difference image. Calibrate a review threshold on your data.")


if __name__ == "__main__":
    try:
        main()
    except (ValueError, cv2.error) as exc:
        print(f"Capture comparison failed: {exc}", file=sys.stderr)
        sys.exit(2)

A positive crop difference means some pixels changed, not necessarily that the numeric price changed. Inspect price-before.png, price-after.png, and price-diff.png. If only a region-change alert is needed, you can choose a threshold on mean_diff from known unchanged and changed examples. Keep that threshold page-specific.

4. Use OCR when the price text can change shape

If the price might change from $9.99 to $10.99, an image template of the old price is the wrong comparison. Use the localized crop as input to OCR, then normalize according to the page’s known locale before comparing parsed values. Install Tesseract separately from its Python wrapper; the wrapper does not install the OCR engine.

python -m pip install pytesseract

Install the Tesseract executable using the official Tesseract documentation, and make sure it is available on your system path. Then add this code after the two crops are produced:

import re
import pytesseract


def read_price(crop):
    # Upscale and convert to grayscale to give OCR a larger, simpler image.
    gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
    enlarged = cv2.resize(gray, None, fx=3, fy=3, interpolation=cv2.INTER_CUBIC)
    # Preserve digits and common price punctuation/currency for inspection.
    raw = pytesseract.image_to_string(
        enlarged,
        config="--psm 7",
        lang="eng",
    ).strip()
    return raw


before_text = read_price(before_crop)
after_text = read_price(after_crop)
print("Before OCR:", repr(before_text))
print("After OCR: ", repr(after_text))

OCR text still needs locale-aware parsing. For example, punctuation has different meanings in different locales: a comma may separate decimals in one locale and thousands in another. Do not blindly remove punctuation and parse the result. A safe first implementation for a known US dollar format can normalize whitespace and currency symbols, then validate the expected pattern:

import re
from decimal import Decimal

US_PRICE = re.compile(r"^\$?\s*([0-9]{1,3}(?:,[0-9]{3})*|[0-9]+)(?:\.([0-9]{2}))?$")


def parse_usd(text):
    normalized = " ".join(text.replace("\u00a0", " ").split())
    match = US_PRICE.fullmatch(normalized)
    if not match:
        return None
    whole = match.group(1).replace(",", "")
    fraction = match.group(2) or "00"
    return Decimal(f"{whole}.{fraction}")


old_value = parse_usd(before_text)
new_value = parse_usd(after_text)
if old_value is None or new_value is None:
    print("Could not confidently parse both prices; send for review")
elif old_value != new_value:
    print(f"Price changed: {old_value} -> {new_value}")
else:
    print(f"No numeric price change detected: {old_value}")

This parser intentionally supports only a constrained US dollar format. Add a separate parser for each known locale and currency, or preserve the recognized string for human review. Tesseract describes its official language model coverage as more than 100 languages and 35 scripts; that is coverage information, not a guarantee of accuracy on a particular product screenshot.

5. Compare fixed crops without an anchor

If the price stays at exactly the same coordinates, skip template matching and crop both screenshots directly. Coordinates are in pixels from the upper left; use identical capture dimensions and scale.

import cv2

before = cv2.imread("before.png")
after = cv2.imread("after.png")
if before is None or after is None:
    raise SystemExit("A screenshot could not be loaded")
if before.shape != after.shape:
    raise SystemExit("Screenshots have different dimensions; standardize capture settings")

x, y, width, height = 500, 260, 240, 80
before_crop = before[y:y + height, x:x + width]
after_crop = after[y:y + height, x:x + width]
if before_crop.shape != after_crop.shape or before_crop.size == 0:
    raise SystemExit("Crop is empty or inconsistent; check coordinates")

score = float(cv2.absdiff(before_crop, after_crop).mean())
print(f"Mean absolute pixel difference: {score:.3f}")

Fixed coordinates are simple and fast but break when responsive layout, page content, or viewport changes move the price. Template matching can tolerate movement relative to the anchor, while still relying on stable nearby artwork.

6. Find multiple possible anchors

If the anchor could repeat, a single best location can be wrong. OpenCV’s tutorial demonstrates thresholding the response map to collect multiple matches. Its threshold is an example only; tune it using your page images, then filter overlapping rectangles and verify each candidate’s relation to the price area.

import cv2

image = cv2.imread("after.png")
template = cv2.imread("anchor.png")
if image is None or template is None:
    raise SystemExit("Could not load screenshot or anchor")

response = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.85  # Tune against representative captures; not a universal cutoff.
ys, xs = (response >= threshold).nonzero()
th, tw = template.shape[:2]
candidates = [(int(x), int(y)) for y, x in zip(ys, xs)]

# Nearby response pixels often describe the same match; collapse them simply.
unique = []
for x, y in candidates:
    if all(abs(x - ux) > tw // 2 or abs(y - uy) > th // 2 for ux, uy in unique):
        unique.append((x, y))

print("Candidate anchor locations:", unique)

7. Reduce false alerts and make checks reviewable

  • Capture the same product URL, viewport, zoom, and device scale each time.
  • Wait for the product price to render before taking the screenshot; avoid capturing during skeleton loading or animation.
  • Mask or exclude known dynamic nearby content such as rotating promotions, review counts, or recommendations.
  • Calibrate both image-difference and matching thresholds from unchanged and changed examples for each page family.
  • Store the before and after crops, full capture timestamps, match scores, OCR strings, and parsed values with each alert.
  • Send ambiguous matches, unparsable OCR, and unusually large layout shifts to manual review rather than silently treating them as a change.
  • Check taxes, sale labels, subscription prices, unit pricing, and selected-variant changes separately if they matter to the use case.

8. Capture the screenshots consistently

The quality of detection depends on comparable inputs. A useful monitoring job records the requested URL, capture time, viewport, device scale, and capture outcome. Use the same product variant and consent state on each run. If you collect screenshots with your own browser automation, wait for a stable price selector or a known page-ready condition before saving the image; do not compare a failed load, CAPTCHA, or blank capture against a valid prior price.

Performance, reliability, and cost

For small screenshots, the crop comparison itself is inexpensive; template matching work grows with the screenshot and template sizes, and OCR adds a separate processing step. Crop early where possible and avoid running OCR when a stable visual comparison already answers the question. The end-to-end cost also includes capturing pages, storing screenshots and evidence, and reviewing false alerts; choose a polling interval based on how quickly a change must be noticed. No benchmark or accuracy rate for product-price screenshot detection is established by the cited sources.

For reliability, treat image loading errors, low-confidence anchor matches, changed screenshot dimensions, and unparseable OCR as explicit outcomes. Retry transient capture failures according to your job policy, but do not convert a failed or partial capture into a price-change alert. Keep enough evidence to reproduce and review each decision.

Or skip the browser setup

Instead of maintaining browser capture and cleanup code, call ScreenshotNeo, a website screenshot API and MCP server. Its clean-shot flow accepts cookie banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. AI agents can use its MCP server tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Sign up for 1,000 free screenshots a month, with no card.

Common errors and fixes

Symptom Likely cause Fix
Could not load a valid image Wrong path, unsupported or corrupt image, or failed capture. Check the file path and image; confirm the capture job produced a complete screenshot.
Anchor score is below threshold The anchor changed, is too generic, or the screenshot differs in scale or appearance. Choose a stable distinctive patch, standardize capture size and scale, and calibrate the threshold.
Price crop falls outside screenshot Offsets or crop dimensions do not match the page layout. Recalculate anchor-relative offsets and confirm the price is visible in the viewport.
Many detections for one anchor The patch occurs repeatedly or the response threshold is too permissive. Use a more distinctive patch, filter nearby candidates, and verify the corresponding price region.
Pixel difference fires on an unchanged price Antialiasing, dynamic content, font rendering, scaling, or a changed banner altered pixels. Align and tighten the crop, mask dynamic regions, standardize captures, and calibrate from unchanged examples.
OCR returns empty or malformed text Crop excludes digits, text is small or low contrast, Tesseract is missing, or language configuration is wrong. Inspect saved crops, install the Tesseract executable, enlarge/preprocess the crop, and select the appropriate language model.
Parsed number is incorrect Locale separators or currency formats were interpreted incorrectly. Use a parser designed for the known locale and reject strings that do not match its expected format.
Code reports a size mismatch Captures or crops have different dimensions. Standardize viewport and scale; verify both crop coordinates and dimensions.

FAQ

Can template matching tell me the new price?

No. It locates a matching image patch. Use OCR to read changing price text, or compare aligned crops when you only need to know that the visible region changed.

Is OpenCV’s example threshold of 0.8 a good price-change threshold?

It is an illustrative value in the tutorial’s multiple-match example, not a validated cutoff for price monitoring. Calibrate on your own unchanged and changed captures.

What if the product page changes its layout?

Re-select a stable anchor near the price, or update page-specific coordinates. If the anchor and layout both change, route the capture for review until localization is reliable again.

Can OCR detect prices in every language?

Tesseract provides official language data for many languages and scripts, but coverage does not guarantee accurate recognition on your image. Choose the appropriate language data and validate actual crops.