Skip to main content

tarsius logo

tarsius

CI Coverage PyPI Python License

uv Ruff prek

Local image recognition for regions of a camera image:

  • State detection: is the car in the garage, is the gate open, is the mailbox flag up.
  • Meter reading: water, gas and electricity meters with number wheels and pointer dials.

Everything runs on your machine, without a cloud service and without training on your frames.

Tarsiers are small nocturnal primates whose eyes are each about as large as their brain: lots of eye, little brain, very good at seeing in the dark. That is the idea here too.

Install

pip install tarsius

Python 3.14 or newer. Works on Alpine (musl), so also in Home Assistant OS.

Detect a state

from tarsius import Roi, StateDetector

detector = StateDetector(Roi.from_percent(10, 10, 60, 50), ("on", "off"))

# Teach a few frames per state.
detector.add("ref-1", detector.prepare(car_jpeg), state="on")
detector.add("ref-2", detector.prepare(empty_jpeg), state="off")

result = detector.classify(new_jpeg)
result.status  # CONFIDENT, UNSURE or NOT_READY
result.state  # "on" or "off" when confident
result.candidate  # best guess, also when unsure
result.probabilities  # {"on": 0.97, "off": 0.03}
  • You confirm a few frames per state. tarsius compares new frames against them and answers with a state, or "unsure" with its best guess.
  • Store prepared.original when you teach, and give it back with detector.load(...) after a restart.
  • A grey frame from an offline camera raises an error, an empty region answers "unsure".
  • All methods block. Call them in an executor; they are safe to use from several threads.

Two backends compare the frames:

  • NcnnBackend (default): image features on ncnn. Tolerates clutter and changing light, a rough region is enough.
  • EdgeBackend: numpy only, a baseline that needs a tight region.

Results

One household, two garage cameras, 24 days, day and IR night. The table shows the harder camera (dark car, clutter that gets moved); the other one scores 100 % with both backends.

Backend Region 2 confirmed segments per state (about 10 frames) 5 per state (about 25 frames)
ncnn tight 0 % wrong, 0 % unsure 0 % wrong, 0 % unsure
ncnn wide, with clutter 0 % wrong, 0 % unsure 0 % wrong, 0 % unsure
edges tight 1.8 % wrong, 3.5 % unsure 0.1 % wrong, 1.7 % unsure
edges wide, with clutter 27 % wrong, 7.4 % unsure 20 % wrong, 8.8 % unsure

About 36 ms per 4 MP frame with ncnn on one thread of an Intel i5-8400T. A small sample, not a guarantee: scripts/history_split.py runs the same test on your own recordings.

docs/state.md covers probabilities, stored references and errors in detail.

Read a meter

from tarsius import Roi
from tarsius.meter import Dial, MeterLayout, MeterReader, Plausibility

layout = MeterLayout(
    digits=Roi.from_percent(45, 13, 78, 28),  # the counter window, all wheels
    wheels=5,
    dials=(  # most significant first: x0.1, x0.01
        Dial(Roi.from_percent(74, 46, 92, 70)),
        Dial(Roi.from_percent(62, 69, 80, 93)),
    ),
    rotation=-25.0,  # degrees counter-clockwise that make the window level
)
reader = MeterReader()
calibration = reader.calibrate([jpeg_1, jpeg_2], layout)  # once, store as_dict()
reading = reader.read(jpeg, layout, calibration)
reading.value  # 627.8846, or None when rejected
reading.candidate  # the composed value, also when rejected
reading.rejection  # None, FAULT, LOW_CONFIDENCE, INCONSISTENT or AMBIGUOUS

guard = Plausibility(max_rate=4.0, tolerance=0.0002, period=layout.period)
guard.accept(627.88, at=confirmed_at)  # a value the user confirmed, aware time
reading = guard.review(reading, at=now)  # may add DECREASED, TOO_FAST, ...
store(guard.state.as_dict())  # and pass state=PlausibilityState.from_dict(...)
  • Wheels and dials are combined with the carry rule, so a wheel rolling between two digits reads right.
  • A reading is rejected with a reason when a wheel is covered or glared, when the reader is unsure, or when the wheels contradict each other.
  • Plausibility checks each reading against the last value a user confirmed: it never goes down, rises at most max_rate per hour, and asks the user when readings keep disagreeing.
  • Run calibrate() once at setup; a reading then takes about 18 ms.

On one water meter with five wheels and four dials, 99.7 % of 696 frames read within 0.01 m³. That is one meter, not a guarantee for others.

The models were trained by the author on generated meters and the Word-Wheel Water Meter Dataset (Zhao et al., Dryad, CC0-1.0, doi:10.5061/dryad.7d7wm3860), with thanks to its authors.

docs/meter.md covers the carry rule, the checks and calibration in detail.

Suggest the regions (alpha)

from tarsius.meter.finder import suggest

suggestion = suggest(jpeg)  # rotation, digits, dials, scores
layout = suggestion.layout(wheels=5)  # check it with the user

On a straight-on photo of one meter it usually finds the counter and the dials, but it can miss them or mark a serial number instead. Always let the user check the suggestion. Alpha: results and API may change in any release.

Development

scripts/build_model.sh   # once, builds the ncnn model into src/tarsius/models
uv sync
uv run pytest
uvx prek run --all-files

The SqueezeNet model is not stored in git. scripts/build_model.sh converts the original SqueezeNet 1.1 release with the ncnn tools. Inputs and outputs are checked against pinned sha256 sums, so CI and a local run produce the same files. It needs x86_64 Linux with curl, unzip, libgomp1 and libprotobuf32t64 (Ubuntu 24.04).

The meter models are stored in git with their sha256 sums, since their training outputs are not public. scripts/build_meter_models.sh reproduces them from those outputs and scripts/meter_finder_parity.py compares the converted finders with Keras, see src/tarsius/meter/models/MODELS.md.

Releases: every PR gets a label, Release Drafter collects them into a draft, publishing the GitHub release builds and uploads to PyPI.

License

Apache-2.0. The bundled SqueezeNet model is BSD-2-Clause, see NOTICE. The meter models are Apache-2.0.

Metadata

Release files for tarsius 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tarsius 0.5.1
File Size Uploaded Provenance
tarsius-0.5.1.tar.gz 6.4 MB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for tarsius 0.5.1
File Interpreter ABI Platform
tarsius-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 12.7 MB

Release files / tarsius-0.5.1.tar.gz

Download URL tarsius-0.5.1.tar.gz
Size 6.4 MB
Tags Source
SHA-256 checksum
How to use checksums
74e9550db46144e9a4e3cd311c4b35f0494448f0e084e5e9faa8ec6b4c1badbf
BLAKE2b-256 checksum
How to use checksums
bf7b8d7bfbb94c3035441eee13094fc5769379c3f7650f1dae10b1eb689f6f63
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tarsius-0.5.1-py3-none-any.whl

Download URL tarsius-0.5.1-py3-none-any.whl
Size 6.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
e6edd1a39fcb7ee142e5691981ead3c6be6392043f339df4775196cc466dcf05
BLAKE2b-256 checksum
How to use checksums
04f2b9fe6a5f827eaaac07d46d5cbdadb25522fd1b1a732d1c207566e4a923f7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for tarsius 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for tarsius-0.5.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page