Guides
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.
- Windows
- Mac
- Emulator
- Cloud device
- Phone
- Studio
- Python SDK
On this page
- Before you start
- How Tesseract OCR works in an Android game bot
- Pick an OCR region in Asset Lab
- Choose the model
- Pick a page segmentation mode
- Fix light text on dark backgrounds
- Set a timeout
- Parse numbers from the text
- Use the confidence
- Reuse one screenshot
- Read text on an Android emulator in Python: a complete example
- Troubleshooting
- OCR returns an empty string
- Digits misread
- Slow OCR
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.
- The app or game shows the text you want to read, at the resolution you will keep.
- You have read the image recognition guide or know what a
Regionis.
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.
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,
) -> TextRecognitionResultThe 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
- Bring the device to the screen with the counter.
- In the Code Editor, open the Assets panel and click Open Asset Helper. Asset Lab opens on the live screen.
- Draw a box around the text. Include a little padding, but no icons, borders or neighbouring numbers.
- Run the live OCR test. Adjust the box until the text comes back clean; the test uses the same engine as your script.
- Copy the four coordinates into a
Region(x1, y1, x2, y2).
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.
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 |
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.
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_WHITEconverts 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_GRAYorRGB_TO_GRAYgives plain grayscale, which helps when the threshold inBLACK_WHITEeats 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.
try:
result = mas.read_text(region=ENERGY, timeout_ms=5000)
except mas.RPCError as e:
mas.log(f"OCR failed: {e}", level="warning")
result = NoneParse 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.
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.
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.
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.
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. If you would rather describe the task, MAS 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.
Next steps
Related pages
Thanks. If something is wrong, tell us in Discord.
Questions? Ask in Discord