Skip to main content

tarsius logo

tarsius

CI Coverage PyPI Python License

uv Ruff prek

Local, training-free state detection for a region of a camera image: is the car in the garage, is the mailbox flag up, is the gate open.

You confirm a few frames per state, tarsius compares new frames against them and answers with a state or "unsure". Nothing leaves the machine and nothing gets trained.

Why tarsius

Tarsiers are small nocturnal primates whose eyes are each about as large as their brain. Lots of eye, little brain, and very good at seeing in the dark. That is the idea here too: no training and no model to learn, just careful looking, including at IR night frames.

from tarsius import Roi, StateDetector

# The detector picks the backend for the task; detector.backend_id tells which.
detector = StateDetector(Roi.from_percent(10, 10, 60, 50), ("on", "off"))

# Teaching: check the frame, store prepared.original and prepared.cache, add it.
prepared = detector.prepare(car_jpeg)
evicted = detector.add("ref-1", prepared, state="on")  # keys dropped by the cap
detector.add("ref-2", detector.prepare(empty_jpeg), state="off")

result = detector.classify(new_jpeg)
result.status  # Status.CONFIDENT, Status.UNSURE or Status.NOT_READY
result.state  # "on" or "off" when confident
result.candidate  # best guess, also when unsure
result.probabilities  # {"on": 0.97, "off": 0.03}, estimated from the scores

probabilities is a softmax of the per-state scores with a temperature per backend, fitted by scripts/calibrate.py on the week before the test week of the history below. Only the harder camera had both states in that week, so the fit rests on its 142 frames. The test week has 284 frames, each classified with 15 sets of references (1, 2 and 5 segments per state, 5 seeds each). Of those answers ncnn gives 0.9 or more to 97 %, all of them right, with a calibration error of 0.009. edges gives 0.9 or more to 79 %. With one segment per state the estimates are slightly too sure, ncnn erred at up to 0.85. With more references they are rather too cautious, edges clearly. The status comes from the margin, not from the probabilities: with two states, unsure means below 0.67 for ncnn. A blank region gets the same probability for every state, a detector that is not ready gives none.

After a restart, hand the stored originals back with detector.load([(key, original, state), ...], caches={key: cache, ...}). A cache spares the extraction, about 14 ms per reference on an i5-8400T, one reference is still extracted to check that the cached ones fit the backend. A cache made for another region, frame_side, backend or release of tarsius, Pillow or ncnn is ignored and the reference is extracted again. A cache that is damaged or does not hold a unit vector is ignored too. LoadReport.caches holds only the new caches: store them, keep the others and drop those of skipped and evicted keys. Development builds all report version 0.0.0, so clear stored caches after changing tarsius code there. Unusable references (blank, undecodable, unknown state) are skipped and reported in the returned LoadReport instead of failing. prepare() and classify() raise ImageError for undecodable data and BlankFrameError when the frame shows nothing, for example a grey frame from an offline camera. prepare() also raises BlankRegionError (the base class) when the region has nothing to compare, classify() answers such a region as unsure, without a candidate. tarsius.outline(frame, roi) draws the region on a frame with the same box rule as the comparison.

All methods block, and so does creating the first detector, which loads the model; call them in an executor. They are safe to call from several threads: changes are serialized and classify() never waits for them, only inferences on the shared backend take turns.

Backends

  • NcnnBackend: SqueezeNet 1.1 ImageNet features on ncnn. Tolerates clutter and lighting changes, a rough region is enough.
  • EdgeBackend: numpy only, a baseline. Needs a tight region that still shows some static structure when the object is gone.

StateDetector uses NcnnBackend unless you pass backend=. It loads one instance per process (about 20 MB) and shares it, inferences take turns on it. ncnn is a regular dependency. Since ncnn 1.0.20261020, opencv-python is an optional extra of ncnn, so tarsius installs on Alpine (musl) systems such as Home Assistant OS. Checked in python:3.14-alpine on x86_64 and aarch64. Both backends support Python 3.14 and 3.15.

Results so far

One household, two garage cameras, 24 days of recorded history, day and IR night. References come from older frames, the test is the newest week. The table is the harder camera: a dark car and clutter that gets moved around in front of it (459 frames, 164 in the test week). The other camera 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 30 ms per 4 MP frame with edges and 36 ms with ncnn, including JPEG decoding, on one thread of an Intel i5-8400T. A small sample, not a guarantee. scripts/history_split.py runs the same evaluation with StateDetector on your own recordings. It also reports how often the detector would ask when unsure answers are confirmed.

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 ncnn 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).

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.

Metadata

Release files for tarsius 0.4.0

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.4.0
File Size Uploaded Provenance
tarsius-0.4.0.tar.gz 2.3 MB With provenance Details

Built distribution (wheel)

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

Total release size: 4.7 MB

Release files / tarsius-0.4.0.tar.gz

Download URL tarsius-0.4.0.tar.gz
Size 2.3 MB
Tags Source
SHA-256 checksum
How to use checksums
5850c1d68026376a02a49edb2ac8b8652919b32370184569c0cc7ca4a6b149ba
BLAKE2b-256 checksum
How to use checksums
6bddca5f07b652089e101362d3dd6c21d125422be9b8ec8d76ef0cca01d59cf2
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 21, 2026.

Transparency log

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

Download URL tarsius-0.4.0-py3-none-any.whl
Size 2.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
eb85798fe8e03f3ddeca509490fe02be3725ab86f4742f3e3b6092394526906b
BLAKE2b-256 checksum
How to use checksums
1bb29f8aa9ada0a162d34a5bd0a1d5a597538254900e56b53a0b13011e921b31
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 21, 2026.

Transparency log

Security for tarsius 0.4.0

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.4.0.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 21, 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