Privacy-First Image Set Contract

Privacy-First Image Set Contract

When you're building an on-device image capture pipeline, privacy can't be something you handle later. If a full-frame or identifiable capture ever gets persisted or shipped, even briefly, you've already created a privacy risk and a testing headache.

A stronger pattern is making privacy a data-format guarantee. The system should be incapable of producing an uploadable package containing identifiable context unless a deliberately different mode is enabled. That shifts privacy from policy to contract plus invariants.

This post lays out a blueprint that keeps the architecture intact: the MCU writes to flash first, then streams to a Linux host over CAN. The MCU generates a minimized image set (ROI-only or downscaled) and persists it atomically via a commit marker. The host verifies completeness and CRC, enforces a strict schema allow-list, and emits a final "safe package" for downstream use, optionally encrypted. The result is privacy-by-construction that's deterministic, testable, and resilient to partial failure.

The real issue: privacy drift in normal pipelines

Most pipelines drift because "temporary" becomes permanent. Raw frames leak into debug dumps, staging buffers get reused, partial writes become ambiguous after power loss, and host parsers start accepting "best-effort" data just to keep the system moving.

Instead of patching these leaks downstream, the goal is a simpler promise: only minimized content can exist in persistent storage, and only minimized content can leave the device by default. Everything else becomes explicitly "unsafe mode," gated and separated.

The approach: a contract that enforces privacy and integrity

The contract is intentionally small and strict, a manifest plus a set of payloads referenced by that manifest. Privacy and integrity get enforced at multiple boundaries, but critically, minimization happens before flash persistence. The flow: the MCU minimizes images into an image set (ROI-only or downscaled), persists that set atomically using a commit marker, streams the set from flash to host over CAN in CRC-checked chunks, the host validates and enforces schema allow-lists, and the host emits a "safe package" for upload or processing, optionally encrypted.

This design makes failure boring. If a set isn't committed, it doesn't exist. If CRC fails, it retries. If a schema violates the allow-list, it gets rejected. No guessing.

Contract design: what "safe" means on the wire

A privacy-first contract is defined as much by what it doesn't include as by what it does. The manifest should never carry fields that indirectly reintroduce identifying context, things like original full-frame dimensions, raw frames, debug attachments, or "extra metadata." A good manifest is versioned, deterministically parseable, and allow-list friendly. Here's an illustrative example:

{
  "contract_version": "1.0",
  "set_id": "1699871234-0042",
  "timestamp_ms": 1699871234123,
  "privacy_mode": "ROI_ONLY",
  "rgb": { "format": "JPEG", "w": 320, "h": 240 },
  "thermal": { "format": "RAW16", "w": 64, "h": 64, "scale": "0.01C" },
  "roi": { "x": 48, "y": 36, "w": 320, "h": 240, "space": "RGB_DOWNSCALED" },
  "payloads": [
    { "type": "RGB_IMAGE", "offset": 4096, "len": 18234, "crc32": "0x9a31..." },
    { "type": "THERMAL_ROI", "offset": 22528, "len": 8192, "crc32": "0x11bf..." }
  ],
  "set_crc32": "0x5c2a..."
}

The important part is the constraint: only minimized dimensions and minimized payloads exist. There's no pathway in the contract for raw context to "accidentally" ride along.

Privacy minimization at source

This is the non-negotiable boundary: minimization has to happen before anything gets written to flash. If full frames are ever persisted, the contract becomes a documentation exercise instead of a guarantee. In practice, there are two workable minimization strategies. ROI-only, the preferred approach, crops RGB to the ROI and persists only that crop, storing thermal as an ROI-equivalent region. Downscaled image ROI aggressively downscales RGB while still keeping the ROI minimal.

Firmware should be structured so the flash writer can only accept "minimized payload" types. Raw buffers may exist in RAM, but the storage API shouldn't accept them unless you deliberately build a separate unsafe mode.

Atomic completeness: commit marker that makes state obvious

Flash-first pipelines have to treat power loss and partial writes as normal. Your storage format should make a simple question easy to answer: is this set complete? Two patterns work well depending on your storage layer. A footer-based commit marker (append-style) writes a header, appends payloads, writes a footer (payload table plus set CRC and magic), then writes a final COMMIT word, on boot, a set is only valid when both footer and COMMIT exist and lengths/CRC match. A rename-based commit marker (filesystem-style) writes into a temp name, fsyncs, then renames to "ready," and on boot only "ready" names are considered valid.

The contract-friendly rule stays the same either way: no commit means no set. That's what keeps ingestion deterministic.

Integrity: why CRC per chunk and per set

Integrity needs two layers because corruption doesn't happen in just one place. Per-chunk CRC catches corruption during transport (CAN) and enables efficient retries. Per-set CRC catches incorrect assembly, stale chunks, and mismatched payload tables. That leads to clear behavior: if chunk CRC fails, retry that chunk; if chunk CRCs pass but set CRC fails, discard and re-fetch the whole set, or re-read from flash. You don't need anything exotic here, CRC32 is fast, practical, and widely supported.

Reliability and privacy guarantees

Contract definition (versioned and strict): define a binary image-set container with format_version, set_id, capture_time_bucket, and sensor_config_hash, a payload directory of type/offset/length/crc32 entries, and ROI geometry (x, y, w, h plus source frame size). Payload types at minimum should include RGB_ROI (cropped) or RGB_DOWNSCALED, THERMAL_ROI (16-bit), and optionally META_MIN (only what's needed for alignment or ordering). Strict parsing rules should reject unknown fields by default (or ignore them behind version gates) and enforce hard caps on payload sizes and ROI bounds.

Privacy minimization at source (MCU): enforce ROI-first packaging before the flash write, cropping RGB to the ROI and never storing background in default mode, and storing thermal only as ROI unless explicitly required. Maintain a controlled fallback mode for engineering builds only, full-frame capture behind a flag with a time-limited enable, and a CM4 policy preventing raw-frame upload unless explicitly permitted.

Atomic completeness: ensure consumers never see partial sets, using either rename-based commit (writing set_<id>.tmp then renaming to set_<id>.bin) or footer-based commit (appending MAGIC plus version plus final_crc at the end). Define clear recovery rules after power loss: tmp files get auto-cleaned, and a missing footer means an invalid set.

Integrity: use per-chunk CRC32 for transport reliability and fast rejection, plus a per-set final CRC for end-to-end integrity. The CM4 should refuse to process or upload a set unless all payload CRCs pass, with bounded retries or re-requests for missing chunks.

CAN streaming compatibility (flash to CM4): the chunk protocol should support set_id, chunk_id, offset, len, and crc32, with selective retransmit or at least resume-from-offset. Pacing and backpressure should avoid starving control frames (priority IDs or a token bucket) while maintaining stable throughput during bulk transfers.

CM4 enforcement layer (privacy firewall): responsibilities include validating the commit marker and CRCs, enforcing a schema allow-list that drops any unexpected metadata, creating an upload manifest documenting what was included and its hashes, and optionally encrypting before upload or storage while managing retention policies.

Validation plan: soak test with sustained capture and transfer under flash-fill conditions; fault injection covering dropped chunks, corrupted chunks, and reboot mid-write; privacy validation through automated checks confirming only ROI payload types exist in production sets, and that the manifest verifies outside-ROI content is genuinely absent, not just ignored.

Host enforcement: strict parsing and schema allow-lists

The host should behave like a security boundary, not a forgiving parser. That means strictness by default: reject if the contract version is unsupported, reject if unknown fields appear, reject if unknown payload types appear, reject if sizes exceed bounds, and reject if CRC or completeness checks fail. "Forward compatibility" is exactly where privacy holes sneak in. If you want extensibility, build it in deliberately with version gates and explicit support.

Safe package emission

After validation, the host emits a final "safe package," manifest plus payloads. At that point, even if you upload it, you're uploading minimized content that already passed allow-list enforcement. Encryption is optional but often useful when moving data off-device. The important point: encryption isn't the privacy mechanism here, minimization is. Encryption mainly protects against leakage in transit or storage.

At Hoomanely, this contract fits naturally when you already run flash-first capture and stream to a Linux host for ingestion. The contract becomes a stable boundary shared across firmware, host-side ingestion, and downstream processing. What this unlocks is iterative improvement: ROI heuristics and insight quality can evolve without changing the privacy guarantees, because the system still emits only minimized, contract-validated sets by default.

Validation: prove it with tests, not policy

A privacy-first contract becomes powerful once it's testable. Three kinds of tests matter here. Soak tests validate stability under long runs, capture, persist, stream, and package, repeated for hours, aiming for a stable system with no memory creep or retry storms. Fault injection validates determinism, power cuts mid-write, dropped or reordered CAN frames, bit flips in payload chunks, with crisp expected outcomes: uncommitted sets vanish, corrupted chunks retry, invalid sets reject. Privacy checks validate minimization, automated rules confirming only allowed dimensions and payload types exist, and that no field can carry full-frame context. This is where contract-first privacy becomes provable.

Key takeaways

Privacy is a format guarantee, not a policy, if the artifact doesn't contain identifiable context, no downstream service can accidentally leak it. Minimize at the earliest point of truth, create ROI-only or downscaled payloads before writing to flash, so raw background context never gets persisted in the default path. Make completeness explicit and atomic, use a commit marker so partial sets are treated as invalid and ignored. Integrity is non-negotiable, add per-chunk and per-set CRC so corruption gets detected early and deterministically. The host is a privacy firewall, parsing via a strict allow-list of known payload types, bounded sizes, bounded metadata, and version-gated fields, with everything else rejected. Separate debug from production by design, full-frame capture for lab debugging must sit behind explicit flags, time limits, and a hard rule preventing raw uploads by default. Metadata is a leak vector, keep only what's required for ordering and alignment. Transport should preserve guarantees, streaming only committed sets, supporting resume and retry without mixing chunks across sets. Build proof into the pipeline with a small privacy manifest per set. And measure it like reliability, with explicit pass/fail checks and continuous soak testing so privacy doesn't degrade throughput.