Scarnox · Blog

Image and text captcha OCR: how to read distorted captchas

An image captcha shows a short string of letters or digits as a picture, sometimes with noise, rotation and wavy lines. Optical character recognition (OCR) can read simple versions of these with an open-source engine such as Tesseract. The accuracy depends on how clean the image is, so most of the work is preprocessing. This guide shows a working Python setup, explains where it fails and makes one thing clear: Scarnox's live API solves PopularCaptcha widget tokens, not image-to-text captchas. Plan for OCR as your own code, not as an API call.

When OCR works and when it does not

OCR is a reasonable first try when the text is:

It struggles with rotated letters, letters that touch, background textures and lines drawn through the glyphs. Each of those needs a specific preprocessing step, and some need a different model entirely. Be ready to measure accuracy rather than assume it.

Install the tools

You need Python, the Tesseract binary and two Python packages:

pip install pillow pytesseract

Tesseract itself is a separate install. On Windows, use the installer from the Tesseract project and make sure the executable is on your PATH, or point pytesseract.pytesseract.tesseract_cmd at it.

Preprocess, then read

Most of the accuracy comes from clean input. The sequence below converts to grayscale, boosts contrast, applies a hard threshold, scales the image up and restricts the output to the characters the captcha uses:

from PIL import Image, ImageOps
import pytesseract

# Characters the captcha can use. Leave out look-alikes such as 0/O and 1/I if the
# captcha never uses them.
ALLOWED = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"

def read_text(path: str) -> str:
    img = Image.open(path)
    gray = ImageOps.grayscale(img)
    gray = ImageOps.autocontrast(gray)
    # Hard threshold: pixels brighter than 140 become white, the rest black.
    bw = gray.point(lambda p: 255 if p > 140 else 0)
    # Tesseract does better with larger glyphs.
    bw = bw.resize((bw.width * 3, bw.height * 3))
    config = f"--psm 7 -c tessedit_char_whitelist={ALLOWED}"  # psm 7: one text line
    return pytesseract.image_to_string(bw, config=config).strip()

if __name__ == "__main__":
    print(read_text("sample_captcha.png"))

The threshold value of 140 is a starting point. Print the processed image, compare it with the original, and adjust until the letters are solid and the background is clean.

Measure accuracy before you rely on it

One correct read on one image proves nothing. Save a set of captchas with known answers, run the function on all of them, and count matches:

def accuracy(samples: dict[str, str]) -> float:
    # samples maps a file path to the true text
    hits = sum(read_text(path) == truth for path, truth in samples.items())
    return hits / len(samples)

labelled = {
    "samples/0001.png": "K7PQ2",
    "samples/0002.png": "M3XN9",
}
print(f"accuracy: {accuracy(labelled):.0%}")

Track this number whenever you change the preprocessing. If the figure is low after several changes, OCR is probably the wrong tool for that captcha style, and you should look at a different approach.

Where it breaks

A preprocessing checklist

When a read fails, work through these in order. Change one thing at a time so you can see which step helped:

  1. Check the input size. Very small captchas often read better after a 3x upscale.
  2. Convert to grayscale and stretch the contrast so the letters stand out.
  3. Apply a threshold, and try a few values before settling on one.
  4. Remove speckle noise with a median filter, then re-threshold if needed.
  5. Crop the image to the text area so the background does not confuse the engine.
  6. Restrict the character set to the one the captcha uses.
  7. Try a different page segmentation mode if the text is spread out or stacked.

Keep the order and the settings in version control next to your labelled samples. A preprocessing pipeline is code, and it should be reviewed like code.

OCR or a model you run yourself

Classic OCR is fast and runs on a laptop, but it is brittle when a captcha changes style. Image models trained on your own labelled set can handle more variation, at the cost of collecting data and running inference. Both are your responsibility to build and maintain. Neither is a hosted Scarnox endpoint today, so plan the infrastructure before you commit to one.

What Scarnox does and does not do here

Scarnox's live API solves PopularCaptcha widget tokens. The request is POST https://api.scarnox.com/api/tasks/create with a PopularCaptcha task type, a sitekey and a page URL, and the response is a token for the form field. That is a different job from reading the text in an image, and Scarnox does not offer an image-to-text endpoint today. If your problem is an image captcha, use OCR or a model you run yourself, and see the other guides for what Scarnox covers. You can create an account and start with free trial credit, no card needed, for the PopularCaptcha flows it does support. Current pricing is on the pricing page.

Legitimate uses

Do not use OCR to get past a captcha on a site that does not permit automated access. Image captchas exist to slow down exactly that.

Next steps