# Tesseract OCR for an Android Game Bot: Read Counters

> Give an Android game bot Tesseract OCR with read_text in MAS: pick a region in Asset Lab, tune psm and colour conversion, and parse counters and timers.

Source: https://automationmacro.com/docs/guides/ocr-text-reading (Guides, updated 2026-09-04)

Template matching tells an Android game bot whether a button is on screen. Tesseract OCR tells it what a counter says: energy 8 of 50, a timer at 00:42, a level number. This guide takes `mas.read_text` from a region drawn in Asset Lab to a macro that stops when energy drops below a limit. It is for anyone writing Macro Automation Studio (MAS) macros in Python on an emulator, a cloud device or a phone.

## Before you start

- A Code-Based project is open in the Code Editor and a device is selected. See [Getting started](/docs/getting-started).
- The app or game shows the text you want to read, at the resolution you will keep.
- You have read the [image recognition guide](/docs/guides/image-recognition-macros) or know what a `Region` is.

## How Tesseract OCR works in an Android game bot

`read_text` sends a screenshot, or the part of it inside a region, to Tesseract OCR inside the app. It returns what the engine read.

```python
def read_text(
    region: Region | None = None,
    screenshot: Screenshot | None = None,
    model: str = "eng_best",
    psm: int = 7,
    color_conversion: ColorConversion = ColorConversion.NONE,
    timeout_ms: int = 30000,
) -> TextRecognitionResult
```

The result is a `TextRecognitionResult` with three fields: `text` (the string), `confidence` (0.0 to 1.0) and `region` (where it looked). Reading the whole screen works, but a tight region is faster and far more accurate, so the first step is always the region.

## Pick an OCR region in Asset Lab

1. Bring the device to the screen with the counter.
2. In the Code Editor, open the **Assets** panel and click **Open Asset Helper**. Asset Lab opens on the live screen.
3. Draw a box around the text. Include a little padding, but no icons, borders or neighbouring numbers.
4. Run the live OCR test. Adjust the box until the text comes back clean; the test uses the same engine as your script.
5. Copy the four coordinates into a `Region(x1, y1, x2, y2)`.

```python
import mas
from mas import Region

ENERGY = Region(x1=380, y1=20, x2=520, y2=60)

result = mas.read_text(region=ENERGY)
print(repr(result.text), result.confidence)
```

Print with `repr` while you tune: it shows stray spaces and newlines that `print` hides. Regions are pixel coordinates at the capture resolution, so keep every device on that resolution. See [Asset Lab](/docs/asset-lab).

## Choose the model

`model` selects the Tesseract data. `"eng_best"` is the default and the most accurate; `"eng_fast"` trades accuracy for speed on clean text; `"eng"` is the standard English set. Start with the default and switch only when a loop reads dozens of times a minute.

## Pick a page segmentation mode

`psm` tells Tesseract what shape of text to expect. The wrong mode is the most common cause of empty or garbled results.

| psm | Expects | Use it for |
|---|---|---|
| 7 | A single line of text (default) | Counters, timers, one-line labels |
| 8 | A single word | A lone number or a short badge |
| 6 | One uniform block of text | A paragraph in a dialog |
| 11 | Sparse text anywhere, no order | A whole screen, a results list |

```python
level = mas.read_text(region=Region(x1=20, y1=20, x2=90, y2=60), psm=8)
dialog = mas.read_text(region=Region(x1=60, y1=300, x2=480, y2=600), psm=6)
```

Values run from 0 to 13, but these four cover almost every macro. When a single-line region returns nothing, try 8 first, then 6.

## Fix light text on dark backgrounds

Tesseract expects dark text on a light background. Game counters are usually the opposite, white digits on a dark bar, often with a coloured outline. `color_conversion` preprocesses the crop before OCR.

```python
from mas import ColorConversion

energy = mas.read_text(
    region=ENERGY,
    psm=7,
    color_conversion=ColorConversion.BLACK_WHITE,
)
```

- `ColorConversion.NONE` (default) sends the pixels as they are.
- `ColorConversion.BLACK_WHITE` converts to grayscale and applies an automatic threshold, giving pure black and white. It is the best first try for high-contrast text on noisy or coloured backgrounds.
- `ColorConversion.BGR_TO_GRAY` or `RGB_TO_GRAY` gives plain grayscale, which helps when the threshold in `BLACK_WHITE` eats thin strokes.

Test each option in Asset Lab on the real screen. There is no `GRAYSCALE` member; use one of the names above.

## Set a timeout

`timeout_ms` defaults to 30000. A tight region with `psm=7` returns well under a second; a full-screen `psm=11` read on a busy screen takes longer. Lower the timeout in a loop that must stay responsive, and wrap the call so a slow read does not end the run.

```python
try:
    result = mas.read_text(region=ENERGY, timeout_ms=5000)
except mas.RPCError as e:
    mas.log(f"OCR failed: {e}", level="warning")
    result = None
```

## Parse numbers from the text

`result.text` is a string, sometimes with spaces, commas, a slash or a stray letter. Parse it with a regular expression and treat "no number" as a real outcome.

```python
import re

def first_int(text: str) -> int | None:
    cleaned = text.replace(",", "").replace("O", "0").replace("l", "1")
    m = re.search(r"\d+", cleaned)
    return int(m.group()) if m else None

def current_and_max(text: str) -> tuple[int | None, int | None]:
    m = re.search(r"(\d+)\s*/\s*(\d+)", text.replace(",", ""))
    return (int(m.group(1)), int(m.group(2))) if m else (None, None)
```

`first_int("Energy 1,250")` gives `1250`; `current_and_max("8/50")` gives `(8, 50)`. The `O` and `l` swaps fix the two most common digit misreads; only apply them to regions that hold nothing but digits.

## Use the confidence

`confidence` runs from 0.0 to 1.0 across the words Tesseract found. A clean counter reads at 0.9 or better. When it drops, read again before you act on the value, and log both so you can see the pattern later.

```python
for attempt in range(3):
    result = mas.read_text(region=ENERGY, psm=7)
    value = first_int(result.text)
    if value is not None and result.confidence >= 0.8:
        break
    mas.log(f"Low confidence {result.confidence:.2f} for {result.text!r}", level="warning")
```

## Reuse one screenshot

Every `read_text` captures a fresh frame unless you pass one. When a loop reads several regions at once, take one screenshot and pass it to each call; the values then belong to the same instant.

```python
shot = mas.take_screenshot()
energy = mas.read_text(region=ENERGY, screenshot=shot)
gold = mas.read_text(region=GOLD, screenshot=shot)
```

## Read text on an Android emulator in Python: a complete example

The script taps a button until the energy counter drops below a limit or a time budget passes. Replace the region, the image ID and the limit with yours.

```python
import random
import re
import sys
import time

import mas
from mas import ColorConversion, Region

images = mas.images({"attack_button": 101})

ENERGY = Region(x1=380, y1=20, x2=520, y2=60)
MIN_ENERGY = 10
TIME_BUDGET_S = 15 * 60


def read_energy() -> int | None:
    for _ in range(3):
        result = mas.read_text(
            region=ENERGY,
            psm=7,
            color_conversion=ColorConversion.BLACK_WHITE,
            timeout_ms=5000,
        )
        m = re.search(r"\d+", result.text.replace(",", "").replace("O", "0"))
        if m and result.confidence >= 0.7:
            return int(m.group())
        mas.log(f"Unclear energy {result.text!r} ({result.confidence:.2f})", level="warning")
        time.sleep(1)
    return None


def main():
    started = time.monotonic()
    unreadable = 0

    while time.monotonic() - started < TIME_BUDGET_S:
        energy = read_energy()
        if energy is None:
            unreadable += 1
            if unreadable >= 5:
                mas.log("Energy unreadable five times, stopping", level="error")
                sys.exit(2)
            continue
        unreadable = 0
        if energy < MIN_ENERGY:
            mas.log(f"Energy {energy} is below {MIN_ENERGY}, done")
            break

        button = mas.find_object_retry(images.attack_button, total_tries=3, time_sleep=2.0)
        if button is None:
            mas.log("Attack button not found", level="warning")
            continue
        mas.click(button.x, button.y, delay_ms=1000)
        time.sleep(random.uniform(0.8, 2.0))

    mas.log("Finished")


if __name__ == "__main__":
    main()
```

Reading before every tap keeps the macro honest: it never spends energy it cannot see. The `unreadable` counter turns a broken region into a failed run, which a webhook on `macro.failed` can report. See [Loops and scheduling](/docs/guides/loops-and-scheduling). If you would rather describe the task, [MAS Agent](/agent) measures the region and writes this code for you.

## Troubleshooting

### OCR returns an empty string

The region misses the text, the mode is wrong, or the text is light on dark. Check the region in Asset Lab, switch `psm` to 8 for a single number or 6 for a block, and add `color_conversion=ColorConversion.BLACK_WHITE`. Very small text also reads as nothing. If the digits are only a few pixels tall, raise the emulator's resolution in its display settings and recapture every region.

### Digits misread

Zero reads as the letter O, one as l, eight as B, or a comma vanishes. First tighten the region so no icon touches the digits. Then try `BLACK_WHITE`, and if strokes break up, `BGR_TO_GRAY` instead. Finally, normalise in code: replace `O` with `0` and `l` with `1` in digit-only regions, strip commas, and reject values that jump wildly between reads.

### Slow OCR

Full-screen reads with `psm=11` and `eng_best` are the slow case. Read a region, not the screen; use `psm=7` or `8`; and pass one `screenshot` to several calls. `model="eng_fast"` helps on clean text. If a read still takes seconds, the region is probably large; a counter needs a box a few hundred pixels wide at most.

No automation tool is 100% risk-free, so automate responsibly and at your own discretion.
