Quickstart: Python
The deployment path: gateways, NVRs, test rigs and provisioning lines. A ctypes binding over the same C ABI, so it is the same engine with the same behaviour — not a reimplementation that can drift.
Install#
The binding is pure Python over ctypes and needs only the built shared library on the loader path.
pip install -e bindings/python
export XCAICX_LIBRARY=/path/to/libxcaicx.so # if it is not on the default pathimport xcaicx
print(xcaicx.version())An engine#
import xcaicx
eng = xcaicx.Engine(
license_path="unit.token", # or license_token="XCAICX1.…"
state_dir="/var/lib/xcaicx", # writable, survives reboot
modules=xcaicx.Module.DETECT | xcaicx.Module.PPE,
cloud_endpoint="https://licensing.xcaicx.com",
heartbeat_seconds=3600,
offline=False,
)| Argument | Default | Notes |
|---|---|---|
license_token | None | The token string itself. Provide exactly one of token/path. |
license_path | None | File containing the token. |
license_pubkey | None | Override the compiled-in verification key. Used for key rotation. |
state_dir | /var/lib/xcaicx | Activation record, clock high-water mark, usage spool. |
model_dir | None | Model bundle directory for production backends. |
modules | 0 | Bitmask. Every bit must be entitled by the licence. |
backend | Backend.AUTO | See Backends. |
num_threads | 0 | 0 = auto. |
log_level | LogLevel.INFO | |
cloud_endpoint | None | None means offline-only; usage still spools locally. |
heartbeat_seconds | 3600 | |
offline | False | True never touches the network. |
The constructor raises on an unusable licence — LicenseError or EntitlementError. Failing at startup rather than mid-shift is deliberate.
try:
eng = xcaicx.Engine(license_path="unit.token", state_dir="./state")
except xcaicx.EntitlementError as exc:
print("SKU does not include a requested module:", exc)
except xcaicx.LicenseError as exc:
print("licence problem:", exc)Use it as a context manager so metering is flushed on the way out:
with xcaicx.Engine(license_path="unit.token", state_dir="./state") as eng:
...Inspect the licence#
lic = eng.license
print(lic.sku, lic.state.name, lic.entitlements)
print("streams <=", lic.max_streams, "fps <=", lic.max_fps_per_stream)
print("perpetual" if lic.perpetual else f"{lic.seconds_remaining}s left")
print("metered so far:", lic.metered_inferences)
if not lic.is_usable:
raise SystemExit("licence is not usable")
if lic.state is xcaicx.LicenseState.GRACE:
warn_operator("licence expired, running on grace")A stream#
with eng.stream("line-3",
modules=xcaicx.Module.DETECT | xcaicx.Module.PPE,
detect_threshold=0.35,
nms_threshold=0.45,
detect_every_n=1) as s:
s.add_zone("packing-bay",
[(0.55, 0), (1, 0), (1, 1), (0.55, 1)],
dwell_seconds=30)
s.add_line("doorway", (0.5, 0), (0.5, 1))Coordinates are normalised to [0,1], so they survive a resolution change.
Process frames#
process() accepts anything with the buffer protocol — bytes, bytearray, memoryview, or a numpy array straight out of OpenCV. numpy is not a dependency.
result = s.process(frame_bgr, width=1920, height=1080,
fmt=xcaicx.PixelFormat.BGR8)
for d in result.detections:
print(d.class_name, round(d.score, 2), d.track_id, d.box)
for ev in result.events:
print(ev.kind.name, ev.zone_name, ev.detail)
print(f"{result.inference_ms:.1f}ms inference, {result.total_ms:.1f}ms total")Box is normalised; convert when you need pixels:
x, y, w, h = d.box.to_pixels(1920, 1080)Asynchronous#
s.submit(frame, width=w, height=h, fmt=xcaicx.PixelFormat.NV12)
result = s.poll(timeout_ms=5)
if result is not None:
handle(result)submit() raises LimitError on backpressure — treat that as flow control and drop the frame. poll() returns None when nothing is ready.
A complete worked example#
Reads from a camera, draws boxes, prints events, and exits cleanly so metering is flushed.
import signal
import cv2
import xcaicx
running = True
signal.signal(signal.SIGINT, lambda *_: globals().__setitem__("running", False))
cap = cv2.VideoCapture(0)
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
with xcaicx.Engine(license_path="unit.token", state_dir="./state",
modules=xcaicx.Module.DETECT | xcaicx.Module.PPE) as eng:
print("licence:", eng.license.sku, eng.license.state.name)
with eng.stream("cam0", detect_every_n=2) as s:
s.add_line("doorway", (0.5, 0.0), (0.5, 1.0))
s.add_zone("bay", [(0.6, 0.4), (1.0, 0.4), (1.0, 1.0), (0.6, 1.0)],
dwell_seconds=10)
while running:
ok, frame = cap.read()
if not ok:
break
result = s.process(frame, width=w, height=h,
fmt=xcaicx.PixelFormat.BGR8)
for d in result.detections:
x, y, bw, bh = d.box.to_pixels(w, h)
colour = (40, 200, 90) if d.class_name == "person" else (200, 160, 40)
cv2.rectangle(frame, (x, y), (x + bw, y + bh), colour, 2)
tag = f"#{d.track_id} {d.class_name} {d.score:.2f}"
cv2.putText(frame, tag, (x, max(12, y - 6)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, colour, 1)
for ev in result.events:
print(f"{ev.kind.name:16} {ev.zone_name or '-':14} {ev.detail or ''}")
cv2.imshow("xcaicx", frame)
if cv2.waitKey(1) == 27:
break
print("metered inferences:", eng.license.metered_inferences)Errors#
| Exception | Raised when |
|---|---|
xcaicx.LicenseError | Any licence-state failure: missing, malformed, bad signature, expired, wrong device, revoked, clock rollback, activation required |
xcaicx.EntitlementError | A requested module is not in the SKU |
xcaicx.LimitError | Stream cap, FPS cap or quota exhausted; also queue-full backpressure |
xcaicx.XcaicxError | Base class, carrying .status and the detail string |
except xcaicx.XcaicxError as exc:
log.error("xcaicx failed: status=%s detail=%s", exc.status, exc).status is the numeric xcaicx_status, so it maps directly onto the status-code table.
Enumerations#
xcaicx.Module.DETECT | xcaicx.Module.DEFECT | xcaicx.Module.PPE | xcaicx.Module.ANPR
xcaicx.PixelFormat.RGB8 / BGR8 / GRAY8 / NV12 / I420
xcaicx.EventKind.ZONE_ENTER / ZONE_EXIT / LINE_CROSS / DWELL_EXCEEDED /
PPE_VIOLATION / DEFECT_FOUND / PLATE_READ
xcaicx.LicenseState.INVALID / VALID / GRACE / EXPIRED / REVOKED
xcaicx.Backend.AUTO / REFERENCE / ORT_CPU / ORT_CUDA / TENSORRT / OPENVINO / RKNN / HAILO
xcaicx.LogLevel.OFF / ERROR / WARN / INFO / DEBUGFull signatures in the Python API reference.
Next#
- Feeding frames — formats, strides, and the copy you should not make
- Zones, tripwires & events — what fires, when, and once or repeatedly
- Performance & tuning —
detect_every_nand the rest