Files
orisys-sdk/docs/sensor-bundles.md
T
ykcheungab 230b04a79d Add a sensor bundle contract separating family, backend and profile
Product name decided the pipeline: Mini worked live but replay refused it. A bundle descriptor now declares profile_id, product_family and processing_backend, with computational and visualization assets kept apart, and one resolver serves live and recorded input. Legacy bundles load through a documented adapter that never guesses a backend from a folder name.
2026-10-04 20:55:39 +08:00

8.0 KiB

Sensor bundles and the processing contract

A sensor bundle is a folder that describes one processing profile. This document owns the bundle descriptor, the resolver, the backend registry and the fingerprints. sensor.json keeps its existing meaning: it is the processing configuration, and its type field is the image-processing arrangement (compound_eye, single_camera, uv_unroll) - never a product family.

Why the descriptor exists

"Which pipeline processes these frames" used to be inferred from a product name. Those are four different things, and collapsing them is what made Mini work live but be refused in replay:

Concept Meaning Example
Product family What the operator calls it Box, Mini, Finger
Processing backend Which SDK pipeline processes the images compound_eye, uv_unroll
Configuration profile Camera, geometry and algorithm settings for one design revision orisys.mini.prototype-r1
Device instance The physical unit Serial number, its calibration

Several families can share one backend, one family can have several profiles, and a recorded clip has no device instance at all - only a profile and, optionally, a compatible calibration.

Descriptor

manifest.json in the bundle root. Every field except the five identifying keys is optional, so a bundle with no manifest still resolves through the compatibility adapter.

{
  "schema_version": 1,
  "profile_id": "orisys.mini.prototype-r1",
  "display_name": "Mini prototype R1",
  "product_family": "mini",
  "processing_backend": "compound_eye",
  "sensor_json": "sensor.json",
  "default_calibration": "force_calibration.json",
  "computational_assets": { "unroll_map": "assets/30fps-0703/unroll_map_uv.npz" },
  "assets": { "scene": "scene/finger-display-1.0.0.oryscene" }
}
  • computational_assets are required to process at all. A missing one fails the profile.
  • assets are visualization only. A missing one is reported and never blocks processing - a missing 3D scene is not a missing deformation field.
  • Paths resolve relative to the bundle root, so moving or renaming the folder changes nothing. Absolute paths and .. escapes are rejected at parse time.
  • An unsupported processing_backend or schema_version raises DescriptorError. The supported descriptor schema version is 1.

Compatibility for existing bundles

resolve_bundle reads a new-shape descriptor when schema_version is present. Otherwise - no manifest, or the older name/sensor_json/assets_dir/viz_pose shape - the adapter describes it. That is the only place a folder name is consulted, and only for the built-in set:

Folder Product family Backend (when the config does not decide)
box box compound_eye
mini mini compound_eye
finger finger uv_unroll
finger-mems finger-mems compound_eye

For a bundle without a descriptor the backend follows the configuration, using the rule Sensor._load_config already applies: type when present, else compound_eye when the config has a stitcher block, else single_camera. Only finger declares type today; box, mini and finger-mems are decided by the stitcher. A folder that is neither built-in nor carries a descriptor is rejected - never routed to compound_eye because box works that way.

Resolver

from orisys import resolve_bundle, validate_profile, create_processor, CalibrationSelection

profile = resolve_bundle("sensors/mini")                      # or any bundle folder
profile = resolve_bundle("sensors/mini", calibration_selection=CalibrationSelection.NONE)
session = validate_profile(profile)                           # re-check against the filesystem now
rebuilt = create_processor(profile, source=0)                 # live camera
rebuilt = create_processor(profile, source="clip.mp4")        # recorded frames, same semantics

resolve_bundle(bundle_path, *, calibration_selection=None) -> ResolvedSensorProfile. The profile is plain data - no Qt, no application preferences, no device handle - so the same object drives a camera and a recorded clip. _set_calibration on a device affects only its own calibration selection.

validate_profile(profile) -> ValidationReport re-reads the filesystem. It reports fingerprint.stale when a computational asset changed since the profile was resolved.

create_processor(profile, *, sensor=None, source=None, include=..., contact=...) returns a ProcessingSession with start/wait/latest/reset/pause/resume/stop. It raises ProcessorCreationError naming what is missing rather than returning a session that does nothing.

Backends

Identifier Requires Produces
compound_eye camera, stitcher, tracker config stitched frame, flow, force
single_camera camera config frames only - no deformation field, so no force
uv_unroll camera config, the mapped computational asset unrolled frame, flow, force

known_backends() and spec_for(...) are the registry. uv_unroll is not yet materialised from a profile alone, because it needs geometry, a UV domain, a tactile processor and a capture object assembled together - that is orisys.finger.FingerRuntime.open's shape. create_processor says so instead of pretending.

Capabilities

Each capability is decided by the facts it depends on, not by the presence of a file:

Capability Unavailable when
Video processing the configuration cannot be loaded
Optical flow no tracker block
Raw force the backend produces no deformation field
Calibrated force per axis: the calibration has no curve for it
Deformation the backend computes no deformation field
3D visualization no scene asset is declared, or the declared one is missing

Deformation is relative unless a calibration says otherwise; it is reported with its meaning rather than as a bare "available". ValidationReport.state is ok, degraded (usable, something optional is missing) or failed.

Calibration

CalibrationSelection is BUNDLE_DEFAULT (the default), NONE, or an explicit path.

  • NONE is a choice, not a failure: raw force stays available and the calibration fingerprint records none.
  • An explicitly selected calibration that is missing or unparseable is an error. It is never replaced by another file.
  • Applicability is declared when the document carries meta.sensor_type, unknown when legacy metadata is absent, or none. unknown means it applies but its fit is unverified - which is what the shipped box calibration reports.
Read Meaning
FrameSnapshot.normal_force_raw / shear_x_raw / shear_y_raw device counts, pre-calibration
FrameSnapshot.normal_force / shear_x / shear_y newtons when a curve applies, counts otherwise
FrameSnapshot.depth, centroid, contour, vnormal relative quantities, not newtons

Sensor.force_triplets() and _force_raw remain the raw and unfiltered view.

Fingerprints

processing_fingerprint hashes the effective backend, the paired data, and each computational asset's role name with its content hash. It excludes the bundle location, display_name, and every visualization asset: a relocated or cosmetically renamed bundle keeps its fingerprint, and a missing scene never changes it. It includes the config minus name and version.

calibration_fingerprint hashes the selected document's content, or the constant none.

profile.inventory() returns the reproducible snapshot: bundle-relative paths, asset hashes and both fingerprints. Compare an inventory, not a path, to decide whether two installations would compute the same numbers.

Resetting the reference

Both backends process externally supplied frames through the same profile. After a gap in the input - a pause, a seek, a scene change - call session.reset(). It is queued to the stage that owns the tracker, so the reference frame is rebased on the frame about to be computed rather than on a buffer the pipeline is still reading.