com.mbreissi.edgecommons.ImageProcessor is the EdgeCommons Python inference component for image
models. It loads signed, content-addressed ONNX model bundles, runs inference on finalized image
files from a spool it owns or on subscription-triggered images, and publishes one durably confirmed
inference result per image.
camera spool ─┐ ┌─► app/inference/result (confirmed, cleanup-gating)
├─► readiness ─► ledger ─┼─► data/<signal> (decision mirror, best effort)
trigger topic ┘ │ │ ├─► <image>.inference.json (evidence sidecar)
│ │ └─► processed/ | failed/ (the input, after confirmation)
│ └─ executor cell (ONNX Runtime, CUDA or CPU)
└─ model bundle cache (verified, warmed, atomically activated)
One process hosts many routes. A route binds one input source to one immutable model version, one set of outputs, and one completion policy, and routes are independent: a backlog on one never stalls another.
You need a local MQTT broker (docker run -d -p 1883:1883 --name emqx emqx/emqx:latest), or run
docker compose up --build, which starts one for you.
-
Install the component and its test tooling.
python3 -m venv .venv && . .venv/bin/activate pip install -e ".[cpu]" -r requirements-test.txt
-
Build the test corpus and pack the model bundle
test-configs/config.jsonpins. The corpus is generated rather than downloaded, so the digest is reproducible on the same ONNX Runtime;make_bundle.pyprints it.python3 tests/fixtures/build.py --out tests/fixtures/out mkdir -p /var/lib/edgecommons/image-processor/bundles python3 tools/make_bundle.py tests/fixtures/out/bundles/synthetic-anomaly-scalar-1.0.0 \ --out /var/lib/edgecommons/image-processor/bundles/synthetic-anomaly-scalar-1.0.0.tar
-
Create the directories the shipped configuration names, and run the component.
mkdir -p /var/spool/camera-adapter/cam-01 /var/spool/image-processor/processed/cam-01 \ /var/spool/image-processor/failed/cam-01 /var/spool/inspection python3 main.py --platform HOST --transport MQTT ./test-configs/standalone-messaging.json \ -c FILE ./test-configs/config.json -t smoke-device
It stages the bundle, warms it, activates it on both routes, and reports ready.
-
Watch what it publishes.
mosquitto_sub -h localhost -p 1883 -v \ -t 'ecv1/+/image-processor/+/app/inference/result' \ -t 'ecv1/+/image-processor/+/data/#' \ -t 'ecv1/+/image-processor/+/evt/#' \ -t 'ecv1/+/image-processor/state'
-
Drop a capture into the spool the way a camera does, metadata sidecar first and then the image.
tests/fixtures/out/images/anomaly-good.pngdecidesCLEAR,anomaly-bad.pngdecidesHOLD. The tutorial walks the whole run, including the sidecar you write. -
Ask it about itself with
ec-uns-cmd:ec-uns-cmd --device smoke-device --component image-processor status ec-uns-cmd --device smoke-device --component image-processor get-models ec-uns-cmd --device smoke-device --component image-processor get-queue
| Path | What it is |
|---|---|
main.py |
Entry point: builds EdgeCommons, registers the configuration validator and the command verbs, runs the app. |
image_processor/ImageProcessor.py |
The wiring and the result pipeline: sidecar, ledger transaction, confirmed publish, mirror, completion. |
image_processor/config/ |
The schema-backed configuration model and the pre-commit candidate validator. |
image_processor/bundles/ |
Bundle fetch, digest and signature verification, bounded extraction, the content-addressed cache. |
image_processor/ledger/, image_processor/completion/ |
The SQLite job ledger and outbox, and the archive, delete, retain, and quarantine actions under write-ahead intents. |
image_processor/engine/ |
Decode, task families, decision rules, the executor cells, the supervisor, and the model-aware scheduler. |
image_processor/sources/ |
Spool discovery and readiness, the camera hint and capture-status reconciler, the trigger subscription. |
image_processor/outputs/ |
The result body, the evidence sidecar, the confirmed outbox publisher, the decision mirror, the events. |
image_processor/artifacts.py |
Model staging, warmup, and atomic route-generation activation. |
config.schema.json, schemas/ |
The configuration contract, the result body contract, and the bundle manifest contract. |
test-configs/ |
A working config.json and the MQTT standalone-messaging.json for local HOST runs. |
tools/ |
make_bundle.py builds and signs a bundle; the rest fetch and verify the tier-2 test assets. |
config.schema.json is the contract and
docs/reference/configuration.md documents every field.
component.global sets the paths, the runtime and GPU profile, the scheduler and publication
policy, the model sources and signing trust, the model set, and the completion defaults;
component.instances[] is one route each.
Validation is fail-closed and runs before a configuration becomes current: unknown fields,
duplicate route ids, unresolved model references, overlapping mutating roots, a missing archive
directory, an unsatisfiable provider policy, and a trigger route staging outside paths.staging
all reject the candidate and leave the running configuration untouched.
docs/sample-configurations.md has the shapes for the common
deployments.
| Surface | Topic | Notes |
|---|---|---|
| Inference result | ecv1/{device}/image-processor/{routeId}/app/inference/result |
The authoritative output. Prepared once, stored byte for byte, published with positive transport confirmation. |
| Decision mirror | ecv1/{device}/image-processor/{routeId}/data/{signalId} |
Best effort, derived from the committed result, never cleanup-gating. |
| Events | ecv1/{device}/image-processor/{routeId}/evt/{severity}/{type} |
Bounded operator conditions. Success is not an event. |
| Metrics | ecv1/{device}/image-processor/metric/{group} |
Eight groups; nothing that identifies one image is a dimension. |
| Commands | ecv1/{device}/image-processor/{instance}/cmd/{verb} |
The library built-ins plus get-models, get-queue, trigger-rescan, preload-model, evict-model, reload-model-catalog, set-route-activation-override, retry-publication, retry-cleanup, reconcile, pause, and resume. |
| Trigger input | whatever source.subscribe names |
An inline image within the envelope's 64 KiB binary cap, or {relativePath, sha256, bytes} resolved under fileRoot. A request carrying reply_to gets the bounded result as its correlated reply. |
docs/reference/messaging-interface.md has the bodies, the request and reply shapes, and the error codes; docs/reference/metrics.md has the measures.
- Exactly one mutating owner per spool. The component archives or deletes only roots it owns, and validation rejects overlapping mutating roots.
- The
app/inference/resultmessage is the only cleanup-gating output. It is prepared once, its exact bytes stored in the ledger, and published withpublish_confirmed()at QoS 1; the input is archived, deleted, or quarantined only after that confirmation. Thedatadecision signals are a mirror, and a consumer enforcing a safety gate reads the result instead. - Ordered durability. Sidecar temp, flush, atomic install, then one SQLite transaction (result,
gating outbox rows, sidecar digest,
RESULT_COMMITTED), and only then is the outbox eligible. Cleanup intents are persisted before any file mutation, and a cleanup failure isCLEANUP_FAILED— retried by policy, repairable by command, never success. - No silent CPU fallback. A route whose provider policy is not satisfied fails closed;
allowCpuOnlywith an emptygpu.devicesis the development profile. Theproviderson every result are the actual session assignment. - Bundles are verified, never trusted. Tarball digest, then the signature when one is required, then bounded extraction, per-file digests, the manifest schema, and task-family support, before staging completes. No bundle-supplied code executes.
- A failed, missing, stale, or unverified inference is
HOLD, neverCLEAR. - Pixels never enter the parent process. Decode and inference happen in executor cells that read the immutable staged file by path and expected digest.
- Accepted jobs are never dropped. Backpressure stops claiming new work and reports degradation; it never discards an admitted image.
python3 -m pytest # tier 1: no broker, no GPU, no network
EC_LIVE_MODELS=1 python3 -m pytest # tier 2: real models, assets fetched and cached
EC_NVIDIA=1 python3 -m pytest # tier 3: the NVIDIA residency and burst suiteThe org gate is 90% line coverage over image_processor/ (ci.yml). Run
edgecommons component validate -p . before a PR.
docs/ follows Diátaxis: a tutorial,
how-to guides, reference, and an
explanation. DESIGN.md is the design-fidelity contract and LLD.md is the
module structure.
{ "component": { "token": "image-processor", "global": { "paths": { "stateDb": "…/state.db", "modelCache": "…/models", "staging": "…/staging" }, "runtime": { "providers": ["CUDAExecutionProvider"], "requiredProvider": "CUDAExecutionProvider" }, "gpu": { "devices": ["0"] }, "models": [{ "id": "line-clearance", "version": "2026.08.20", "digest": "sha256:…", "uri": "s3://approved-models/…" }] }, "instances": [ { "id": "clearance-cam-01", "source": { "kind": "spool", "root": "/var/spool/camera-adapter/cam-01", "readiness": { "mode": "cameraSidecar" }, "camera": { "component": "camera-adapter", "instance": "cam-01" } }, "modelRef": { "id": "line-clearance", "version": "2026.08.20", "digest": "sha256:…" }, "outputs": { "decisionSignals": [{ "id": "line-clearance/pass", "value": "$.decision.pass" }] }, "completion": { "onSuccess": "archive", "archiveDir": "/var/spool/image-processor/processed/cam-01" } } ] } }