AI analysis · SeedSigner · Machine-first document

Image entropy: data and methodology

Every measurement behind the image entropy analysis, with the method that produced it, the raw frames it was computed from, and the command that recomputes it. Written to be checked rather than read.

Companion to
The analysis
Version
0.8.7
Raw data
32 runs, ~340 MB
Claims
Addressable by ID
Revision
0804db1d
Read this first

This document is written for an AI reviewer.

Its companion, the analysis, is the document for a person deciding how to use the device. Everything a user needs in order to generate a seed safely is there, and none of it requires this page. This one is the working: canonical figures, the estimators that produced them, the preview-window structure of every dark run, the release history, every concern raised and its disposition, and what was not verified.

If you are a human: pointing a model at this URL is the fastest way to get a claim checked, and that is what the structure is built for. It is deliberately dense, declarative and regular in shape, so a reviewer can work claim by claim instead of following an argument. None of that puts it off limits: read it start to finish if you want to, and the claim ledger is the map if you would rather go straight to a specific claim.

A prompt that works:

Review this analysis of SeedSigner's camera-based seed generation: <paste this page's URL> Read section 1 before forming any view. It gives the precedence order for conflicting sources, what each estimator bounds and in which direction, the accounting rules for a sequential hash chain, and the settled false positives that will be rejected on sight. The raw capture frames ship with the document, alongside a per-directory RUNS.md manifest. Recompute figures from the frames rather than trusting the tables. Report each finding against the claim ID it contradicts. Section 1 also sets out three levels of checking, the deepest being to rebuild the capture process and take your own frames on your own hardware. State which level you reached. Do not imply you captured frames if you only recomputed from the published ones. State the analysis revision you reviewed: the commit hash in this page's masthead Revision entry (also the current commit of https://github.com/kdmukAI-bot/seedsigner-ai-analysis, whose main branch this site deploys). Findings are adjudicated against that revision, and the document's corrections list records what has changed since.

The raw frames are published precisely so a reviewer can disagree numerically. Work that contradicts these figures is more useful than work that confirms them.

1Reviewer brief

The rules that govern how to read every figure below. A finding that violates one of them will be rejected without re-derivation.

Precedence

The shipped raw frames outrank the manifests. The manifests outrank this document's tables. The tables outrank the prose. Each data directory carries a RUNS.md manifest (data1, data2, data3, data4, data5) recording every retained run, its conditions, its provenance and its figures; those manifests are the curated ground truth this document summarizes. Where two sources disagree, recompute rather than reconcile; section 14 has the commands.

Estimators, and what each one bounds

EstimatorBounds entropy fromWhat it answersScript
Most-common-value min-entropy
SP 800-90B §6.3.1, worst of all 45 pairs
Below, approximatelyWhat an attacker guessing seeds faces. Every headline figure in both documentsanalyze_mcv.py
Preview-window structure
digest counts, content classes, pairwise MCV among distinct frames
Structural facts are exact; MCV as aboveHow many of the 50 slots carried anything, and whatanalyze_preview.py
Compressed size
best of bzip2 and LZMA
AboveHow much structure remains in a difference. Historical; no headline figure uses itanalyze_burst.py

Three rules that follow:

Accounting rules for a sequential hash chain

The seed is hash = sha256(hash + frame_bytes), once per preview frame, then once for the final image (IE-S-01). The rules every total in this document follows:

What "dark" means in this document

"Dark" throughout means effectively total light exclusion at the sensor: a completely or effectively blocked lens. The measured routes there are a device set down lens-down on a shadowed desk, and a deliberately sealed occlusion. Every dark run is verified in-band by the pixel discriminator (IE-M-17). Auto-exposure is only a one-directional corroborant: every properly dark run locked at its unit's stable dark gain (3.25 or 3.625), and a low locked gain indicates light was present, but a high locked gain indicates nothing, because most lit indoor runs also locked at 3.1–3.6 (only the bright wall runs locked low). It does not mean a dim room. No condition between ordinary lighting and near-total blackout was measured (IE-L-14), and every flagged light contamination of the dark state, however small, moved the figure upward by a large factor (roughly 7× to over 100× across the flagged runs), so the collapse phenomena in this document live at the blackout end of the illumination axis, not merely in low light.

Comparability and naming hazards

Settled false positives

These have been raised, considered and dispositioned. Findings resting on them will be rejected, with the reason:

FindingWhy it is rejected
Absence of a camera-validation check in code, filed as a novel criticalKnown and adjudicated (IE-A-03). The sharpened, evidence-backed form, a distinct-digest health check on the preview window, is already this analysis's own recommendation (section 8). A re-filing adds nothing unless it carries new evidence
Byte-identical frames in a preview window prove the instrumentation failedBackwards for that series type. Preview duplicates are the finding: a light-starved scene legitimately quantizes to a constant. analyze_preview.py exists precisely because analyze_mcv.py's duplicate guard is correct for final-image bursts and wrong for windows
The dark runs never clear the requirement / the device is broken in the darkEvery measured chain cleared 256, including all six preview-zero runs (IE-M-04). The finding is thin, luck-governed margins and a failed redundancy argument, not failure
Exfiltration, or entropy data at restThe image buffer is never transmitted and never written to storage on a release image
Persistence of secrets across bootThe device runs entirely in RAM; root filesystem is an initramfs image
Physical access, or a malicious microSD cardOutside the stated threat model
os.urandom or a hardware TRNG should be mixed inThe absence is deliberate: SeedSigner uses only external, user-supplied entropy. A design debate, not a vulnerability. See IE-A-11
Statistical randomness tests should be run on the outputEvery path ends in SHA-256, whose output is statistically perfect regardless of input. Guaranteed reassuring, guaranteed uninformative

Three levels of checking

Go as deep as you can actually execute. All three are genuinely available, and the third is the only one that produces evidence this analysis does not already contain.

LevelWhat you doWhat it requiresWhat it can settle
1Read the claims and attack the reasoningThis pageLogic errors, overstated language, an estimator quoted on the wrong side, a disposition whose weakest point is load-bearing
2Recompute every figure from the shipped raw framesPython and numpy. No hardwareWhether the numbers in section 5 are what the published frames actually produce
3Rebuild the capture process and take your own framesDocker, a Pi Zero variant, an OV5647 camera module, a microSD cardEverything level 2 cannot: whether any of this generalizes beyond the author's own four units

Level 3 is what capture-rig/ exists for. Every figure in this document was computed from frames captured by this analysis's own author, on hardware they own, and section 13 records that no independent measurement exists anywhere. That is the largest gap in the work, and re-reading cannot close it. Sensor noise is a property of the individual die, so a fifth unit is worth more than a fifth argument. The rig's build script is provenance-gated: it pins the app and OS layers to upstream release tags and refuses any ref it cannot trace to the upstream remote, and an override stamps the deviation into a PROVENANCE.txt that ships with the data (section 3).

Where automation stops

The capture itself cannot be automated. It needs a physical board, a camera module, and a person to flash a card, boot it, arrange a scene or a dark condition, and press a button. An agent can build and verify the image unattended; it cannot produce frames without hardware in the loop.

Stating that plainly because a reviewer planning its work should know which half it can finish alone. If you can only reach level 2, say so in your findings rather than implying you reproduced the captures.

How to report a finding

Cite the claim ID it contradicts, state which estimator and which run (with its dataN/ prefix) you used, and give the command that reproduces your figure. A finding that moves a number is more useful than one that disputes an adjective. The adjudication record states the weakest point of every disposition; those are the places this analysis is most likely wrong, and the intended place to start.

Pin what you reviewed. These documents are revised after publication (every substantive change is recorded in the section 15 corrections list), so name the revision your finding responds to: the commit hash of the analysis repo at the time of review, alongside the SeedSigner release the analysis itself pins (tag 0.8.7 @ e0a80d4b). The hash is on the page: the masthead's Revision entry states the commit this page was deployed from, stamped at deploy time (a raw checkout reads "working copy" there, and the repo's main branch is what this site deploys). A finding that cites its commit stays adjudicable after the documents move: the diff between your commit and current shows exactly what changed underneath your commentary, and commentary against a superseded revision reads as responding to that revision rather than as silently wrong. Findings can be filed as issues on the repo.

2Claim ledger

Every load-bearing claim in both documents, with a stable ID, its status, and where it is established. IDs are the addressing scheme for review and do not change once assigned. IE-S-nn are read from source at a pinned tag, IE-M-nn are measured, IE-I-nn are inferred, IE-A-nn are adjudication entries, IE-L-nn are limitations. A claim whose status chip carries a scope qualifier (a small n, a confound, non-release behavior, a degenerate case, an estimator caveat) wears the amber caveat badge; plain verified claims wear green.

Source claims

IE-S-01Verified · source

Seed entropy is a running SHA-256 chain folding four inputs in this order: CPU serial number, time.time(), up to 50 live preview frames, and the full-resolution final image.

Source
tools_views.py L121–190 @ tag 0.8.7, commit e0a80d4b
Detail
serial L151–158 · clock L161 · frames L165–167 · final image L170
IE-S-02Verified · source

The preview-frame window is a rolling last 50 frames, not the first 50. Holding the preview open longer than the fill time refreshes the frames' content but adds no frames.

Source
tools_screens.py L116–120, constant at L34 (max_entropy_frames = 50)
History
First-50 until v0.7.0 changed it to last-50; see IE-S-07
IE-S-03Verified · source

A 12-word seed keeps only the first 128 bits of the digest, so its threshold is 128 rather than 256. Every margin in these documents is quoted against 256, the stricter case, and doubles against a 12-word seed.

Source
tools_views.py L172–174
IE-S-04Verified · source, every tag

No release could ever produce a seed with zero camera contribution, and the flow has no camera-error fallback. The final image is unconditionally hashed in every version from v0.4.3 to v0.8.7. At tag 0.8.7, the capture and hashing steps catch no camera exceptions (the outer try around the chain carries only a finally for spinner cleanup), and a missing final image raises at the display step before any hashing, so a failure that surfaces as an error or an absent frame stops the flow before a seed exists. The one input with a catch-and-continue fallback is the CPU serial, which falls back to the constant b'0' (and is counted at zero regardless).

Scope
Unconditional hashing: v0.4.3 (2021-07-31) through v0.8.7, read at each tag. The no-fallback trace: tag 0.8.7
Observed
One physical fault was tested on-device (2026-08-07): loosening the camera ribbon cable during the live preview froze the preview and then the entire app, which stopped responding to input. Ungraceful, but terminal: the flow cannot be completed and no seed is produced. Informal test, one unit, not instrumented; a hang is a third failure surface alongside an error and an absent frame, with the same security outcome
What this cannot exclude
A failure mode that keeps returning valid-looking frames: a re-served stale buffer (IE-M-08, measured, credited zero) or a hardware fault delivering a solid buffer (IE-A-03, its stated weakest point). Source reading cannot enumerate hardware failure behaviors, which is why the ribbon test above was worth running
Bears on
The "was it ever weaker?" question (section 7), and the recurring "what if the camera fails mid-flow?" reviewer question
IE-S-05Verified · source, every tag

The four-input chain arrived complete in v0.4.4 (2021-08-27, PR #76) and has the same four inputs in the same order in every release through v0.8.7. Anything generated from August 2021 onward went through the chain that ships today.

Exceptions
None to composition or order. Changes to resolution, frame window and pixel format are itemized in section 7
IE-S-06Verified · source

check_for_low is a bare GPIO level read: no edge detection, no debounce, no release required. A button already held when a screen opens is seen on that screen's first loop iteration.

Source
buttons.py L148–157
Consequence
A 225 ms repeat threshold against a 10 ms poll means one continuous hold carries through the preview, the capture-accept and the mnemonic-length menu as a single gesture
IE-S-07Verified · source + history

The preview loop appends each frame after checking the buttons, so a held button returns an empty frame list deterministically. From v0.4.4 to v0.6.0 the order was reversed. Commit 91b02061 (PR #453, August 2023) reordered it to fix issue #450, a PIL paste(None) crash; it first shipped in v0.7.0.

Effect
A held button used to crash. The fix converted that into a silent, deterministically reachable zero-preview-frame path
Measured
IE-M-06, 10 of 10 captures
Full treatment
Section 8
IE-S-08Verified · source

v0.8.6 made the final image 2×max_dim square (PR #741). On the 240 px panel that is 720×480 → 480×480, the only resolution reduction in the feature's history, roughly 33% fewer pixels. On a 320×240 panel the same rule gives 640×640, an 18.5% increase.

Consequence
Everything measured here was measured at the historical floor for the 240 px panel: no earlier release gave the chain less scene content. v0.4.3/v0.4.4 hashed a genuinely larger (unrotated 720×480) image; v0.4.5–v0.8.5's rotated buffer carries a 480×480 scene region, equal to today's, plus one third constant-black fill (see the v0.4.5 row in section 7)
Caveat
A pixel count is not an entropy count. The claim is comparative only; no earlier release was measured directly. Reasoning in section 7
IE-S-09Verified · source + platform

The first two chain inputs contribute almost nothing. SeedSigner OS has no real-time clock and no network, so the system clock starts at zero every boot and time.time() returns fractional seconds since power-on. The CPU serial is a fixed per-device value and is not secret.

Bound
The clock's unpredictability is the sub-second timing of a button press plus how long the user took to reach the feature. Quantified estimate: IE-I-02
Note
The code comment says "millis" where the call returns seconds. Its own characterization, "modest entropy", is fair
Adjudicated
IE-A-06
IE-S-10Verified · project statement

Release images have been reproducible since v0.7.0 by the project's own statement, and release images are prepared and signed by one individual who is the sole holder of the signing keys.

Source
README.md L106–L108 @ 0.8.7 (sole-signer statement at L106, reproducibility at L108); build procedure
Bears on
Step 2 of the trust chain in the analysis. Source review says nothing about the binary on a given microSD card

Measurement claims

IE-M-01Verified · measured

picamera does not encode at quality 85. Its Python source defaults to 85, but the MMAL hardware encoder emits quantization tables of all 1s, effectively quality 100 and no quantization. Less quantization preserves more sensor noise.

Method
Quantization tables read from the JPEGs the device encoder emitted
Significance
Reading the source was insufficient and would have been wrong. The capture settled it
Provenance
The device JPEGs this was read from belong to an early capture set not retained in this publication; the claim is not recomputable from the shipped data, though any instrumented device reproduces it directly
IE-M-02Verified · measured · caveat

The encoder's SOF0 sampling factors are Y:(2,2), Cb:(1,1), Cr:(1,1), which is 4:2:0 chroma subsampling. Three quarters of the chroma samples are discarded before encoding and reconstructed by upsampling on decode: a lossy stage entirely independent of the quantization tables in IE-M-01. Measured in the hashed bytes: 99.3% of 2×2 blocks holding a red change, and 97.3% holding a blue change, also hold a green change, against a 17.8% base rate.

Caveat
Any per-channel breakdown of changed bytes overstates independence: red and blue positions are largely determined by the green noise through upsampling. No figure here is arithmetically wrong as a result, because every estimate is computed on the hashed bytes whole rather than by summing channels
Provenance
The SOF0 sampling factors are verifiable from any device JPEG; the block co-occurrence percentages were measured on an early capture pair not retained here and are not recomputable from the shipped data
IE-M-03Verified · measured · headline

With the lens effectively blocked, the preview layer can contribute exactly zero. In six of six dark runs on the Pi Zero 2 W, across two boots and two rounds, all 50 window slots held one byte-identical pure-black frame (content_changes: 0, verified from dumped frames, not only the log). The frame is computable from the panel size alone: sha256((0,0,0,255) × 57600) = 6f77f852… at 240×240; the 320×320 sibling is b71fb8a8…. A repeated known frame contributes zero (accounting rules), so the seed rested on the final image alone.

Runs
data3/000025-934f, data3/000117-934f, data3/000202-934f, data4/000025-934f, data4/000117-934f, data4/000212-934f
Condition
Face-down (lens toward the desk surface) on a shadowed desk. No seal, no enclosure, no intent
Recompute
python3 analyze_preview.py data3/burst-19700101-000117-934f (and siblings)
Geometry note
Preview frames are square at the panel's larger dimension, so the 320×240 Plus panel yields 320×320 preview frames; both constants remain computable from the panel dimensions alone
Weakest point
One unit exhibits the full collapse (the below-rail unit, IE-M-09); the railed units held 2–5 distinct frames. Which side of that line a given user's device sits on is a per-unit accident
IE-M-04Verified · measured · headline

The thinnest measured chain totaled 389 bits against the required 256: a 1.52× margin. A preview-zero run whose total was its final image. The six preview-zero finals to date, in descending order: 2,969, 1,110, 550, 517, 496, 389, with nothing changed between sessions; the lower tail thins by pure repetition. Every measured chain in this work, dark and lit, cleared 256.

Run
data4/000117-934f
Scope
"Measured chain" means every camera layer measured (data3–data5), or the final image alone already clearing 256 (data1). The data2 chains are indeterminate, not failures: finals as low as 173 with preview windows that predate the window instrumentation. No capture anywhere in this work was shown to fall short of 256
Recompute
python3 analyze_mcv.py data4/burst-19700101-000117-934f/stock
Weakest point
MCV over-estimates the floor on non-IID data (IE-L-01), so the true margin of that run is below 1.52× by an unquantified amount. The 12-word threshold (128) is cleared at 3.0×
IE-M-05Verified · measured

The final image alone fell below 256 bits with no engineering. Face-down desk runs produced final images of 113, 177, 190 and 221 bits (all worst-of-45-pairs MCV). 113 is below the 173 that the most heavily engineered light seal produced (IE-M-14). In each sub-256 case the chain was carried past 256 by the live preview frames present in that window (for the 221-bit run, via the cross-run analogy credit, IE-L-05); in the preview-zero runs the final image happened to be thicker.

Runs
113: data3/000150-6b66 · 177: data4/000412-6b66 · 190: data4/000236-6b66 · 221: data3/000303-6b66
Spread
The same desk position, minutes apart, nothing moved, also produced 26,318 (data3/000031-6b66, light-present band). Session variance spans two orders of magnitude (IE-L-04)
IE-M-06Verified · measured · degenerate case

A held button chains zero preview frames, deterministically: 10 of 10 captures on two boards. The frame is displayed regardless (it is pasted to the canvas before the button check), so no external observation distinguishes a zero-frame capture from a normal one.

Mechanism
IE-S-06, IE-S-07. Full treatment in section 8
Consequence
The second demonstrated route to a one-layer chain, independent of lighting
Provenance
The ten zero-preview captures belong to an early instrumented set not retained here, so the count is not recomputable from the shipped data; the deterministic mechanism is verifiable from source (IE-S-06, IE-S-07)
IE-M-07Verified · measured · refutes a prediction

The window fills at the display loop's rate, not the camera's: 11.1–11.6 fps on the Pi Zero, 13.9–14.1 on the 2 W, 8.5–9.1 on the Plus geometry, all far under the camera's configured 24 fps. Filling 50 slots takes ~3.5–5.9 s; camera warm-up is ~0.2 s. On the Plus the window routinely arrives under-filled (42 and 44 of 50 observed): short windows are the norm on the larger panel, not an operator artifact.

Refuted prediction
Registered before measurement: the loop would outpace the camera (25–35 fps), making duplicate slots structural. Wrong in both directions
Data
Loop-rate table in data3/RUNS.md; Plus rows in data4/RUNS.md
IE-M-08Verified · measured

Preview slots are not guaranteed to be separate sensor reads. PiVideoStream.read() has no synchronization, and on single-core boards the capture thread occasionally starves: the lit Pi Zero window held 46 of 50 distinct (4 scattered cached repeats) against the quad-core 2 W's 50 of 50. Byte-identical live frames in separate slots are cached buffers, not repeated exposures: distinct live frames differ in hundreds of bytes, so two independent reads reproducing all 230,400 exactly does not happen in practice. Observed lit (data3/000630-6b66), dark (data4/000523-6b66 and data3/000150-6b66, one live digest in two slots each) and on the white wall (data5/000053-6b66, adjacent-slot doubles).

Corroboration
The Zero's within-window append-gap spread is roughly an order of magnitude wider than the 2 W's (tens of milliseconds against single digits)
Accounting
A cached duplicate contributes zero, same as a constant
IE-M-09Verified · measured · n=4, confounded

Which board collapses in the dark rides a gain knife-edge the code never inspects. In identical darkness, auto-exposure settled at digital gain 3.625 (the rail) on three units and 3.25 on the 2 W, stably in every recorded dark session per unit. Every railed unit produced live frames in every dark window; the below-rail unit produced only the constant, every time. The correlation is unanimous across all recorded dark sessions on four units, three rounds and multiple boots: the engineered-seal round's logs corroborate it (its 2 W run locked 3.25 before the AE lock; every other unit's dark runs locked 3.625).

Weakest point
A correlation over four physical units, confounded with board family, sensor unit and enclosure. Mechanism is inferred (IE-I-05), not isolated
Consequence
Which layer is thin in the dark is a stable per-unit accident, not a design property
IE-M-10Verified · measured

A dark live preview frame is a near-binary green plane, and its honest per-read novelty is hundreds to a few thousand bits. Typical live frame (data3/000150-6b66 frame18): 57,254 nonzero bytes, every one green (57,233 ones), 99.4% of pixels, 346 held back at zero. Adjacent live frames differ in 820–1,004 bytes (0.36–0.44%); worst within-run live-live MCV: 469–1,428 bits across the properly dark 240 px windows, 4,618 on the properly dark Plus window (4,034 occurs only in a light-flagged run). Live-vs-constant MCV (~92,000–94,000) is dominated by the global threshold crossing and must not be used as the layer's contribution. The volatile minority is genuinely fresh per read: across a run's live frames, the union of held-back pixel positions is 1,728 and the intersection is 0, so there is no unit-specific replayable signature in it.

Why green
Twice the Bayer photosites and unity AWB gain; red and blue are scaled from a smaller base (measured AWB gains 1.355 / 1.445)
Lowest novelty
469 bits across 375 changed bytes, data4/000412-6b66: the same darkness that thins the final image thins the live frames' own variation
IE-M-11Verified · measured · small effect

The preview and final layers are measurably coupled, weakly. Comparing each of data3/000150-6b66's four live preview frames against the final still of the same capture (2×2 block downsample to match geometry), preview-crossed pixels read very slightly brighter in the final image than preview-cold pixels: z = 4.0 on all four frames, magnitude +0.02%. Statistically detectable, consistent in direction, negligible next to the margins, and non-zero: both layers watch the same scene through the same gain, which is the structural reason they thin together.

Procedure
Downsample the final still 2×2 to preview geometry; compare its mean value over preview-crossed versus preview-held-back pixel positions; one z-score per live frame
Consequence
Summing layer credits assumes an independence that is imperfect. The totals in section 5 lean optimistic for this reason among others
IE-M-12Verified · measured

Lit scenes deliver what the design assumes. Lit preview windows: 46 of 50 distinct on the single-core Zero, 50 of 50 on the 2 W. Lit final images on all four units: 1.3–3.1 million bits across the eight lit runs (worst of 45 pairs, MCV; the low end is the 2 W lit control at 1,332,455), three to four orders of magnitude above requirement, at both still geometries.

Runs
Baselines: the four data1 bookshelf runs. Lit preview controls: data3/000630-6b66, data3/000322-934f
Geometry note
The Plus figure (3,131,353) is a 640×640 still, 1.78× the values; normalized per-value it sits in line (~1.76M equivalent)
IE-M-13Verified · measured · n=2

A featureless scene is not a degenerate scene. Two blank-white-wall runs (one handheld, one stationary, natural light only, single-core Zero): finals 1,397,876 and 1,490,163 bits, within 10% of the same unit's bookshelf baseline (1,505,982). The stationary run held 20 of 20 distinct preview frames with a worst live-live pair of 151,069 bits; between any two distinct preview frames, 37–75% of bytes changed, all three color channels populated. AE settled at digital gain 1.46 / 2.07, nowhere near the dark rail. The degenerate axis is illumination, not scene content; hand motion is not what the entropy rests on.

Runs
data5/000053-6b66 (handheld, 43 of 50 distinct), data5/000306-6b66 (stationary, clicked 1.868 s after stream start, so only 20 slots existed to chain)
Scope
One unit, one wall, one morning, n=2 (IE-L-10)
IE-M-14Verified · measured

An engineered floor is not a floor. The most heavily engineered occlusion (a conformable gel-pack seal at the lens inside a closed refrigerator, room-temperature packs as sealing material) produced 173 bits; a second unit under the same method reproduced the effect (423). Ordinary face-down-in-shadow later produced 113 on the same unit (IE-M-05): the engineering bought reliability of the dark state, not the state itself. Swapping cloth-in-a-bag for the gel seal roughly halved the changed-value count on both units tested.

Data
data2/RUNS.md, including the occlusion methods, the two-population split and the deleted light-present runs' summary statistics
Amendment
data2's own manifest records the refutation of its "deliberate effort required" framing by the data3 desk runs
IE-M-15Verified · measured

The on-device review screen is a light test, not an entropy test, and it fails in both directions. Mechanism: autocontrast(cutoff=2) returns a fully dark frame byte-identical (red and blue fall below the 2% cut; green collapses to one level), so it displays pure black; anything else stretches into a faintly visible image. Simulated over every retained final at the shipped library version (Pillow 11.0.0, buildroot bf2a2858 at OS tag v0.8.7; results byte-identical under 11.1.0): frames displaying a faint image occur in runs measuring as low as 113 bits (2 of 10 finals in data3/000150-6b66; 1–3 of 10 in four of the five sub-256 runs), frames displaying pure black occur in runs measuring as high as 36,462 (4 of 10 finals in data2/000427; 8 of 10 in the 173-bit run), and the display class flickers between consecutive finals of one unchanged setup. Both display classes occur across essentially the full measured range, so no user-facing check based on the review screen can bound entropy in either direction. Display-only: the hash uses the unboosted original.

Recompute
python3 analyze_review_screen.py (per-frame table for every retained run; state the Pillow version when comparing)
Amendment
The run-level black/stretched classification an earlier data2 table carried is superseded: the class is per-frame, not per-run. data2/RUNS.md carries the amendment
Legitimate use
An operator protocol for achieving darkness during measurement (repeat until the screen shows nothing), which is how the data2 runs were selected
IE-M-16Verified · measured

No final image ever repeated. 320 final frames across the 32 retained runs, lit and dark, all mutually distinct corpus-wide: a global SHA-256 over every retained final returns 320 distinct digests, with no duplicate within or across series. (analyze_mcv.py additionally refuses any final-image series containing duplicates, so every reported series passed that gate.) The uniqueness claim is scoped to the final image: preview slots do repeat, and that is the subject of IE-M-03/IE-M-08.

Recompute
python3 -c "import hashlib,glob; h=[hashlib.sha256(open(f,'rb').read()).hexdigest() for f in glob.glob('data[1-9]/burst-*/stock/frame*.raw')]; print(len(h), len(set(h)))"
Bears on
The "same photograph twice" question, and it rules out temporal denoising (IE-I-01)
IE-M-17Verified · measured

Dark-capture quality is bimodal, and light contamination inflates the answer. Occlusion quality split the sixteen occlusion-round runs (nine retained, seven deleted but summarized) into two groups separated by 10–20× on the share of first-frame pixels at or above value 4 (properly dark: 0.001–0.007%; light present: 0.048–0.166%), with AE gain and edge gradients as corroborating tells. Stray light is signal, signal carries shot noise, and shot noise is what the estimator measures, so the failure mode of a dark measurement is a reassuring wrong answer: one leaked run read roughly 7.5× the clean figure for the same unit. Light-present runs are flagged in every table and excluded from every dark claim.

Discriminator
Share of first-frame pixel values ≥ 4; thresholds and per-run values in data2/RUNS.md, data4/RUNS.md
Corollary
Multi-unit dark comparisons under differing occlusion rank enclosures, not sensors
IE-M-18Verified · measured · not v0.8.7

A 20 s auto-exposure window made the dark case worse, not better: the share of values differing fell ~5× despite analog gain rising to 8.0. Deeper zero-clipping and gain-scaled denoise thresholds are candidate mechanisms; not isolated.

Do not quote as v0.8.7
The long-AE configuration is not release behavior. v0.8.7's window is 0.25 s (start_single_frame_mode constructs a fresh camera, sleeps 0.25 s, then capture_frame() freezes AE on first call, so one AE state governs a burst). The release's short window is the better of the two for dark-case entropy, and nobody chose it for that reason
Provenance
Not recomputable from the shipped data. Measured on an early capture set that predates the provenance gate and is not retained in this publication. The claim is kept as a warning to anyone building a capture rig (the patch carries the long-AE phase, commented out), and no other figure in either document rests on it

Inferred claims

IE-I-01Inferred · from data

No temporal denoising is active on the final-image path. Averaging across frames is the one mechanism that could make consecutive captures resemble each other, and the data rules it out: no two final frames identical in 320, with per-pair differences covering 74–98% of values across the lit series' pairs.

Flips if
The camera tuning changes. A move to different camera hardware or a stack enabling temporal denoise or frame stacking invalidates it
IE-I-02Inferred · estimated, not measured

time.time() on an RTC-less, network-less board is seconds since power-on, and str() preserves fractional digits. Its value against a realistic attacker is the sub-second timing of a button press plus uncertainty about elapsed time since boot: perhaps 25–30 bits, estimated from mechanism, excluded from every total in this document.

Why excluded
Not measured, attacker-window-dependent, and leaning on it would blur the layer accounting the dark findings depend on
IE-I-03Inferred · not measured here

Shining a bright light into the sensor reduces available randomness. The mechanism is clipping, not brightness: once every photosite well fills, every pixel reads maximum and frame-to-frame variation is truncated.

Basis
Others' reports plus the physics. Not measured in this work
Supporting evidence
The lit scenes measured here, bright but unsaturated, produced the largest figures in the document. Ordinary well-lit scenes are not a problem; deliberate saturation takes deliberate effort
IE-I-04Inferred · downgraded finding

A frozen or stale preview would be conspicuous in normal handheld use, because a live preview tracks the user's own motion. The mechanism behind it is real and now measured (IE-M-08: the stream re-returns cached buffers under contention), and its entropy cost is priced into the accounting as zero-credit duplicates. The one deliberately induced hardware freeze (camera ribbon loosened mid-preview; see IE-S-04, Observed) froze not just the preview but the whole app, input included, which is more conspicuous still.

Residual case
A device resting on a surface, which is how the measurement rig was arranged and not how the feature is used
Weakest point
A freshness check remains cheap insurance, and the distinct-digest health check in section 8 subsumes it
IE-I-05Inferred · mechanism

The gain knife-edge's proposed mechanism: a dark live frame is the green plane's mean crossing the video-port rounding threshold (IE-M-10), and the 11.5% digital-gain difference between 3.625 and 3.25 is exactly the kind of multiplier that puts one unit just above that crossing and another just below.

Status
Consistent with every recorded dark session on four units; not isolated from board family, sensor unit or enclosure (IE-M-09)

3Instrumentation and capture provenance

Traced, not just read

Every retained figure comes from instrumented builds of v0.8.7 flashed to real hardware, patched to dump the exact bytes the app feeds into its SHA-256 chain, before any hashing. The patch touches views/tools_views.py and a version stamp; hardware/camera.py is byte-identical to stock, so the capture parameters are the release's own. The instrumentation was verified present in the flashed images rather than merely staged.

Verified · every layer pinned upstream

All retained data was captured on builds pinned to the upstream release tags at every layer: app 0.8.7 @ e0a80d4b and OS v0.8.7 @ d1385939, both verified against the SeedSigner upstream remotes rather than any fork. The build script, build-instrumented-image.sh, enforces this: it refuses to build from any ref it cannot trace to upstream, and the override flag stamps the deviation into a PROVENANCE.txt that ships with the data.

Why the gate exists: an earlier capture round was invalidated when an OS layer was silently taken from a personal fork branch. A measurement claiming to characterize a release must be traceable to that release at every layer, and a fork can carry a release tag's name without the release's contents. The gate makes that mistake loud instead of silent.

The retained population

32 runs in five directories, four physical units, two panel geometries. Units are identified by a truncated SHA-256 of the CPU serial (unit_id), because two are the same board model. The enclosure is part of the measured system in every dark run (it is the first light barrier the lens sees) and is recorded per unit in data2/RUNS.md.

UnitBoardPanel / stillEnclosure
934f…Pi Zero 2 W Rev 1.0240×240 / 480×4803D-printed case
6b66…Pi Zero Rev 1.3240×240 / 480×480Milled aluminum case, tightest camera-aperture fit
074c…Pi Zero W Rev 1.1240×240 / 480×480None (bare board)
a92a…Pi Zero W Rev 1.1 + Plus display320×240 / 640×640Large 3D-printed Plus-style case
The four units, camera side
Four SeedSigner units camera-side up: an orange 3D-printed case, a milled aluminum case, a bare Pi Zero board with its camera ribbon taped on, and a large oval Plus-style case.
The fleet, camera side up: upper left the 2 W in its orange 3D-printed case (934f), upper right the milled aluminum case with the tightest aperture fit (6b66), center the bare board with its camera ribbon taped to the back (074c), bottom the oval Plus-style case (a92a). Operator photograph, resized only.
DirectoryWhat it isRunsUnitsPatchProvenance
data1Lit bookshelf baseline, one run per unit4all fourf1c059dfSame-campaign import; basis in manifest
data2Engineered dark occlusion (cloth/bag, gel/refrigerator)9all fourf1c059dfBuild record in manifest
data3Preview-layer measurement: face-down dark + lit controls86b66, 934f21dd1fd3PROVENANCE-round2-pi0.txt, PROVENANCE-round3-pi02w.txt
data4Replication round + Plus-geometry preview measurement9934f, 6b66, a92a21dd1fd3Three PROVENANCE-round4-*.txt
data5Blank white wall, handheld and stationary26b6621dd1fd3PROVENANCE-round5-pi0.txt

Preview-window measurement exists only from patch 21dd1fd3 onward (data3 and later): it logs a SHA-256 digest and timing for every frame in the rolling window, and dumps a sample. The data1/data2 runs predate it; their figures are final-image-only, which their manifests state.

The dumping rule, and the sampling lesson behind it

Hashing every window frame costs nothing and settles the most consequential single number, the distinct count. Dumping every frame would cost tens of MB per lit run, so the dump is a sample: all frames when the window held 20 or fewer, otherwise the first and last 10 plus a first-occurrence representative of every logged digest the positional sample missed, and dumped frames are verified to re-hash to their logged digests.

The digest-coverage clause is load-bearing, and it was learned the hard way: positional sampling over a near-constant window loses exactly the frames that matter. A dark window can hold one live frame at an arbitrary interior index; a first-N/last-N rule silently drops it, and with it the only evidence of what the layer contributed. The rule that dumps a representative of every digest is why rare mid-window live frames (indices 13, 29, 37 in various runs) exist in the retained data at all.

Conditions

Dark rounds: face-down at one desk position chosen to sit in shadow, three consecutive runs per board, device not lifted between them, so they isolate same-scene new-session variance; one lit control per board. Engineered rounds (data2): occlusion methods, their caveats (heat, the cord aperture) and the temperature question are documented in that manifest, including why temperature was deliberately not pursued (cycling a camera through a large temperature change condenses moisture inside the lens assembly and wrecks the measurement). White wall (data5): the most well-lit blank-wall area available, operator casting no shadow, natural overcast morning light, one handheld and one stationary run. Sensor temperature is unmeasured in every run (IE-L-06).

4Method: estimators and accounting

Why no randomness tests were run

Every SeedSigner entropy path ends in SHA-256. A SHA-256 output is statistically perfect regardless of its input, so a randomness test applied to the result cannot distinguish a rich input from an impoverished one. The same limitation applies to single images: a near-black photograph has abundant byte-level structure that scores well on distribution tests while carrying almost no unpredictability. The question asked throughout is therefore:

Across repeated captures of an unchanging scene, how much does the image actually change, and how many of the chained frames carried anything at all?

Whatever does not change between captures cannot be entropy.

Final images: most-common-value min-entropy

An attacker guessing seeds is limited by min-entropy, the probability of the single most likely outcome. analyze_mcv.py implements NIST SP 800-90B §6.3.1 on the value-by-value difference between two captures, including the 99% upper confidence bound on the modal probability, which is not optional: omitting it overstates the floor by 1.7–21% on this data, growing exactly as the margin shrinks. The reported figure per series is the worst of all 45 pairs, selected on the estimator being reported. The script refuses series containing byte-identical frames, because for a final-image burst a duplicate means the instrumentation failed.

Differences are signed (later − earlier), and the domain matters on lit series. Signed, absolute and modular-256 differences agree only while the modal difference is 0 and the zeros outnumber the ±1s: that holds for every dark series here (the 113-bit run scores 113 in either domain), but fails for lit ones, where taking absolute values moves the mode to 1 and drops the bookshelf baseline from 1,505,982 to 1,156,876 bits, 23% low. A recomputation that does not match a lit figure should check its difference domain before filing anything.

What this estimate is not

MCV assumes IID differences, and spatially correlated sensor noise is not IID. A full SP 800-90B assessment would fail the IID test and take the minimum across ten non-IID estimators, every one of which can only return a figure at or below this one. Every margin in this document is therefore an over-estimate of the floor by an unquantified amount, and this caveat travels with every figure, most heavily with the 1.52× headline (IE-L-01).

Preview windows: structure first, entropy second

analyze_preview.py reports the window's digest structure from the log (distinct count over all slots, not just the dumped sample), classifies each distinct frame's content (a constant frame is reported with its value; a live frame with its nonzero profile), verifies dumped frames re-hash to their logged digests, and computes pairwise MCV among distinct dumped frames only. Duplicates are reported as the finding, not refused as an error: the same guard that protects a final-image burst would hide the most important result in a window. Digest-count facts are exact and carry no estimator caveat.

Chain totals

Computed under the accounting rules in the brief. Three standing caveats, all leaning the same direction: MCV over-estimates the floor; multiplying a pairwise figure by the distinct-frame count overstates, because conditioning on several known frames tells an attacker more than conditioning on one; and summing layers assumes an independence the coupling measurement shows is imperfect (IE-M-11). Every total leans optimistic, which matters most for the thinnest rows.

5Results

VERIFIED by measurement. Shipping path, picamera/MMAL stack, upstream-pinned builds. Every figure recomputes from the raw frames shipped with this document (section 14). A 24-word seed requires 256 bits, a 12-word seed 128 (IE-S-03). All entropy figures are worst-of-45-pairs MCV, in bits. Light-present rows (by the IE-M-17 discriminator) are flagged: their figures are inflated by stray light and are not dark measurements.

Properly dark runs (blocked lens): window structure and finals

RunBoarddigital gainWindowDistinctLiveFinal MCVvs 256
data3/000150-6b66Zero3.62550541130.44×
data3/000303-6b66Zero3.62550212210.86×
data3/000031-6b66Zero3.625502126,318102× (light)
data3/000025-934f2 W3.2550102,96911.6×
data3/000117-934f2 W3.2550105502.1×
data3/000202-934f2 W3.2550105172.0×
data4/000117-934f2 W3.2550103891.5×
data4/000025-934f2 W3.2550104961.9×
data4/000212-934f2 W3.2550101,1104.3×
data4/000412-6b66Zero3.62550321770.69×
data4/000236-6b66Zero3.62550321900.74×
data4/000523-6b66Zero3.6254843 (one cached ×2)4321.7×
data4/000123-6b66Zero3.625483224,75496× (light)
data4/000450-a92aPlus3.625425427,576107×
data4/000038-a92aPlus3.6254443248,977972× (light)

Reading the table:

Chain totals, dark runs

Preview credit per the accounting rules. The two single-live-frame data3 runs are credited by cross-run analogy (their lone frames' worst pairs against other runs' frames) and marked; every data4 credit is a within-run measurement.

RunPreview creditFinalTotalClears 256?
data4/000117-934f0389389yes, 1.5×
data4/000025-934f0496496yes, 1.9×
data3/000202-934f0517517yes, 2.0×
data3/000117-934f0550550yes, 2.1×
data3/000303-6b661 × 764 (cross-run est.)221~985yes, 3.8×
data4/000212-934f01,1101,110yes, 4.3×
data4/000412-6b662 × 4691771,115yes, 4.4×
data4/000236-6b662 × 1,1831902,556yes, 10.0×
data3/000025-934f02,9692,969yes, 11.6×
data3/000150-6b664 × 1,0781134,425yes, 17×
data4/000523-6b663 × 1,4284324,716yes, 18×
data3/000031-6b661 × 1,651 (cross-run est.)26,318~27,969yes, 109× (light)
data4/000123-6b662 × 4,03424,75432,822yes, 128× (light)
data4/000450-a92a4 × 4,61827,57646,048yes, 179×
data4/000038-a92a3 × 982248,977251,923yes, 984× (light)

Every run clears 256. In the run with the lowest final image ever measured (113 bits), the preview layer carried the seed: the layered design doing exactly its intended work. In the six preview-zero runs the design's headline mechanism contributed nothing and the final image carried everything. Either layer can be the one that carries, and nothing in the code knows which. The 113-bit final also answers the 12-word case: it is the lone sub-128 final image, and it sat in a window holding four live frames; every dark total clears 128 at 3.0× or better.

Lit scenes

RunSceneBoardStillPreview distinctFinal MCVvs 256
data1/000025Bookshelf2 W480×4801,748,7136,830×
data1/000029BookshelfZero W (bare)480×4801,581,1926,176×
data1/000111BookshelfZero W + Plus640×6403,131,35312,231× (see geometry note)
data1/000303BookshelfZero Rev 1.3480×4801,505,9825,882×
data3/000630-6b66Lit controlZero Rev 1.3480×48046 of 501,544,1076,031×
data3/000322-934fLit control2 W480×48050 of 501,332,4555,204×
data5/000053-6b66White wall, handheldZero Rev 1.3480×48043 of 501,397,8765,460×
data5/000306-6b66White wall, stationaryZero Rev 1.3480×48020 of 201,490,1635,820×

Engineered dark floor (data2)

Nine retained runs across all four units, lens occluded by escalating methods, selection by the on-device review screen (repeat until it displayed pure black). Full tables, including the two-population split, the deleted light-present runs' statistics and the occlusion caveats, are in data2/RUNS.md.

Camera-down on the first gel pack
A SeedSigner in an orange case resting camera-down on a blue gel pack in its fabric sleeve on a refrigerator shelf, screen lit, USB cable running off-frame.
Second pack over it, seal complete
A second bare gel pack laid over the device, completing the sandwich; only the USB cable emerges.
The best seal of the engineered round, photographed in place: the device camera-down on a gel pack inside its fabric sleeve on a refrigerator shelf, then a second bare pack laid over it, with the battery pack inside so the door gasket closes unbroken. Both packs were at room temperature: sealing material, not coolant. The lit screen in the first photograph is the boot splash; during a capture it shows the live preview, which in a sealed dark enclosure is itself black. Operator photographs, resized only. This is the apparatus that produced 173 bits, and that an unsealed desk in shadow later undercut at 113: kept here as a record of the lengths gone to before learning the engineering was not the point.

What this round established, in order of importance:

What the spread means

"Can this fail?" is an existence question, and it is answered: yes, reachably. Four sub-256 finals and six zero-contribution windows exist in provenance-recorded data, from conditions requiring no intent. "What do users typically get?" is a distribution question, and it is not answerable from this data: one desk position spanned 113 to 26,318 bits with nothing moved (the high end light-present-flagged, so the swing is imperceptible leak variation, itself part of the answer), and any realistic condition measures one room, one surface, one lamp angle. Measured points are reported as points; the distribution was not characterized, and this document declines to extrapolate from either tail (IE-L-04).

6The preview layer, mechanically

The condensed mechanics behind the section 5 window tables. Everything here is measured on v0.8.7's Python preview loop (PiVideoStream); the loop-rate and stall findings are specific to that loop, while the content findings (quantization collapse, green-plane anatomy) are sensor and pipeline properties that should be expected to carry to any stack watching the same scene (IE-L-09).

  1. Two independent rates. The camera produces frames at a configured 24 fps into a latest-frame buffer; the display loop consumes at 8.5–14.1 fps depending on board and panel, appending one frame per iteration (IE-M-07). The window is a rolling last-50 (IE-S-02), so dwell past the fill time refreshes content but adds no frames.
  2. No read synchronization. Nothing marks a frame consumed; a starved capture thread on a single-core board re-serves the same buffer (IE-M-08).
  3. Dark content collapse. The 240×240 RGBA video-port path has no JPEG stage and quantizes a nearly signal-free frame to literal zeros: most slots hold the computable constant (IE-M-03). The still path quantizes less aggressively, which is why the final image retains a sparse flicker when the window has collapsed entirely.
  4. What a live frame is. One global event, the green plane crossing the rounding threshold, with the information in the minority of pixels held back and in which pixels flip between reads (IE-M-10).
  5. The knife-edge. Auto-exposure gain, settled per unit, decides whether any live frames exist at all in the dark (IE-M-09, IE-I-05).
  6. Common cause, inside and between layers. The darkness that collapses the window thins the final image and the surviving live frames' novelty simultaneously (IE-M-05, IE-M-10), and the layers are weakly positively coupled (IE-M-11). Layer count is not independence.

7Every release, checked

Image entropy shipped in v0.4.3 (2021-07-31), commit d7bea686, via PR #46. Every release since has been examined. Full working: 10-image-entropy-history.md.

ReleaseChange to the entropy chain
v0.4.3
2021-07-31
Feature introduced. A single SHA-256 of one 720×480 image. No preview frames, no CPU serial, no clock. 24-word only
v0.4.4
2021-08-27
PR #76, the modern four-input chain arrives. The same four inputs, in the same order, in every release through v0.8.7 (IE-S-05)
v0.4.5Capture rotation moved into Camera.capture_frame() and became a user setting (default 0), so from here the hashed image is the rotated one. On the era's non-square 720×480 final, PIL's rotate(90) keeps the canvas: the hashed buffer becomes a 480×480 scene region plus one third constant-black fill (verified empirically: 115,200 of 345,600 pixels), and a third of the scene columns are cropped away. The constant fill contributes nothing under the accounting rules; for square frames (previews, and finals from v0.8.6) a rotation is a pure pixel permutation
v0.5.0
2022-04-22
MVC port; 12-word option added, first 128 digest bits. Chain unchanged
v0.7.0
2023-09-04
Preview loop restructured (PR #453); frame window changed from first-50 to last-50. Introduces the held-button path (IE-S-07)
v0.8.0Preview frames become RGBA, adding a constant alpha byte. No entropy effect
v0.8.5Capture trigger widened from one key to any of four buttons
v0.8.6Final image becomes 2×max_dim square (PR #741). The only resolution reduction ever, on the 240 px panel (IE-S-08)
v0.8.7No chain changes

v0.4.6, v0.5.1 and v0.6.0 are byte-identical to their predecessors in this path. v0.4.5's two update_last_input_time() calls are screensaver bookkeeping with no entropy effect.

Was the chain ever weaker in composition? No

VERIFIED from source at every tag (IE-S-04). No release could produce a seed with zero camera contribution. The final image is unconditionally hashed in every version, and the flow has no camera-error fallback: every camera-failure surface identified in code review or observed on hardware (an error, an absent frame, and in one on-device ribbon test an outright app freeze) stops the flow before a seed exists.

No release ever gave the chain less scene content than the configuration measured here (IE-S-08): every release from v0.4.3 through v0.8.5 captured 720×480, but from v0.4.5 the default rotation reduced the hashed buffer's scene content to a 480×480 region plus constant fill (v0.4.5 row above), and v0.8.6 made the capture itself 480×480. Everything here was measured at that historical floor, which v0.4.5–v0.8.5 effectively shared.

What that argument is not. A pixel count is not an entropy count, and more pixels of a dark frame are still mostly zeros. The claim is only comparative: no release gave the chain less to work with than the configuration measured here. No earlier release was measured directly, because no instrumented build of one was made. And the composition claim does not extend to behavior: v0.7.0's loop reordering created the held-button path (IE-S-07), a behavioral regression in defense-in-depth that composition analysis alone would miss.

The serial and clock were never load-bearing. v0.4.3 did not include them at all; v0.4.4 added them explicitly as supplements (IE-S-09).

For a user with an older seed: anything generated on v0.4.4 or later, from August 2021 onward, went through the identical chain composition that ships in v0.8.7. The dark findings in this document apply to every one of those releases alike; they are properties of the sensor and scene, not of a version.

8Two routes to a one-layer chain

The four-input chain is designed as redundant layers. Two demonstrated, independent, accidental routes reduce it to one meaningful input, and they compound with each other.

Route 1: a blocked lens (probabilistic, per-unit)

Measured in section 5: the window collapses to the computable constant, entirely on the below-rail unit (six of six), nearly on the railed units (2–5 distinct of 50). The seed rests on the final image, which the same light starvation has thinned; the thinnest measured result is the 389-bit / 1.52× chain (IE-M-04). Reaching it took setting the device down lens-down on a desk in shadow.

Route 2: a held button (deterministic, any lighting)

VERIFIED (IE-M-06). Each preview-loop iteration reads a frame, paints it, checks the buttons, and only then appends:

frame: Image = self.camera.read_video_stream(as_image=True)
...
self.renderer.canvas.paste(frame.crop(box=box))   # displayed
if self.hw_inputs.check_for_low(keys=HardwareButtonsConstants.KEYS__ANYCLICK):
    ...
    return preview_images                          # returns here
...
preview_images.append(frame)                       # never reached

A button already held when the screen opens is seen on the first iteration (IE-S-06), before anything is appended, so an empty list returns every time: zero preview frames, 10 of 10 measured captures, on both boards tested. The frame is displayed regardless, so nothing observable distinguishes it from a normal capture. Origin: a v0.7.0 crash fix (IE-S-07); the discussion on that PR is entirely about the crash, and the silent path it created went unnoticed until instrumentation.

What would fix it, and what would not

These are recommendations to the project, stated as the measurements support them. Whether and how they land is a project decision; nothing in this document depends on their adoption.

9Other edge cases

Bright light

INFERRED, not measured here (IE-I-03). Saturation clips the photosite wells and truncates frame-to-frame variation. The mechanism is clipping, not brightness: the lit scenes measured here are bright and produced the largest figures in the document. Ordinary well-lit scenes are not a problem; deliberate saturation takes deliberate effort.

Fixed sensor pattern

Every CMOS sensor carries a fixed-pattern component (bias, hot pixels, PRNU), stable enough that forensics identifies individual cameras by it. It is predictable, so it carries no entropy, and the difference-based estimator cancels it by construction: a fixed component appears identically in both frames and subtracts to zero.

The worry that the cancellation is artificially narrowing the estimate was measured and does not hold: differencing frames from two physically different sensors gave 266 bits against 173 for one of those units against itself, the same order rather than a leap. At these light levels there is no signal for PRNU to modulate. Within-burst (173), across sessions on one device (167) and across devices (266) are separate conditions, not a ranking; reading structure into their ordering over-interprets order statistics. (The cross-condition pairings were drawn from the engineered-seal round's frames but not recorded, so treat 167 and 266 as order-of-magnitude illustrations rather than recomputable figures.)

A finger over the lens

A finger over the lens is not a dark condition; it is a dim red-lit scene. Flesh transmits red and near-infrared strongly (the fingertip-over-flashlight effect, the mechanism pulse oximetry rides on), blood flow modulates it over time, and the transmission is clearly visible in the live preview. It therefore sits brighter than face-down-in-shadow and cannot probe the thin-final-image regime, which is why the 2026-08-06 rounds deliberately skipped it as redundant rather than severe.

The review screen shows black

Covered by IE-M-15: a display-side light test with no entropy effect and no entropy information, recorded here because a dark capture showing an unchanged black review screen also looks exactly like a dead camera, and will be reported as one.

10Adjudication record

Read this before filing a finding

Everything below was raised and consciously dispositioned. The reasoning is recorded, not just the verdict, so it can be attacked efficiently rather than rediscovered. The weakest point on each entry is where this analysis is most likely wrong. Start there.

IE-A-01Confirmed, priced in

Noise reduction removes entropy before the hash. Confirmed present (firmware-default denoise, plus a Bayer-domain stage inside the closed ISP that no property reaches), and priced in: every measurement here was taken downstream of it, on bytes the device had finished processing, so whatever it removed is already absent from the figures.

Weakest point
Flips if the camera stack or tuning changes. Denoise strength is not knowable from configuration because the ISP is closed; only downstream measurement is sound
IE-A-02Partly conceded

JPEG compression destroys sensor noise before hashing. Resolved favorably on quantization by hardware measurement (IE-M-01: all-1s tables). But quantization is only half the encoder: 4:2:0 chroma subsampling is a lossy stage independent of it and measurable in the hashed bytes (IE-M-02).

Weakest point
A per-channel breakdown overstates independence (red and blue positions largely follow green through upsampling). No figure here sums channels, but any reading that does is wrong
IE-A-03True, deliberate, now bounded by measurement

Nothing in code verifies the camera produced a real image. True of v0.8.7 and deliberate: verification is performed by the user. The measurements now bound what that human check can and cannot do. It is real against a dead, disconnected or covered camera in a lit setting (a black or frozen preview is conspicuous). It cannot grade entropy (IE-M-15: both display classes occur across runs from 113 to 36,462 bits), and it cannot detect a collapsed preview window, because the frames are displayed whether or not they are chained (IE-M-06).

Weakest point
A hardware fault returning a solid buffer would still flow through to a valid-looking seed; not reachable by occluding the lens, needs a fault. The distinct-digest check in section 8 is the code-side complement this analysis recommends
IE-A-04Raised, downgraded, now measured

A frozen or stale frame would be invisible. The mechanism is real and measured (IE-M-08): the stream re-serves cached buffers under single-core contention, lit and dark. Its cost is priced in as zero-credit duplicates, and in normal handheld use a fully frozen preview stops tracking the hand and is conspicuous (IE-I-04).

Weakest point
Occasional single-slot staleness is invisible by construction and was observed; only its accounting cost (zero credit) is controlled, not its occurrence
IE-A-05Confirmed, and it changed the analysis

The preview layer can contribute nothing. Confirmed by two independent routes: deterministically via a held button (IE-M-06) and probabilistically via a blocked lens (IE-M-03, six of six on one unit). The tempting disposition is to call this harmless whenever the final image alone clears the requirement; that reasoning does not survive the measurements: the final image can simultaneously be thin (IE-M-05), the failure is common-cause rather than independent (IE-M-11), and the thinnest measured chain stands at 1.52× (IE-M-04).

Weakest point
Every measured chain still cleared 256. The finding is a failed redundancy argument with thin, luck-governed margins, not an observed failure; a reviewer arguing "cleared is cleared" has the measured record on their side and the design argument against them
IE-A-06True and documented

The CPU serial and clock contribute almost nothing. True and documented (IE-S-09, IE-I-02). Not load-bearing; both are excluded from every total.

Weakest point
Harmless only while the camera contribution is real. "The serial and clock are also in there" is not acceptable reassurance when the camera path is in doubt, and the dark rows are exactly where it would be reached for
IE-A-07Canceled by construction, and measured

Fixed-pattern noise inflates the estimate. Canceled by construction in a difference, and the residual worry (that the cancellation narrows the estimate) was measured and does not hold: cross-sensor differencing lands in the same order as same-sensor (section 9).

Weakest point
Exact cancellation requires identical per-frame gains. Gain and white-balance drift leak scene structure into differences, in the inflating direction
IE-A-08Real, requires deliberate action

Bright light saturating the sensor reduces entropy. Real; clipping truncates variation once wells fill (IE-I-03).

Weakest point
Not measured in this work. Rests on others' reports plus physics
IE-A-09Rejected

Statistical randomness tests should be run. Rejected. Output-side testing after SHA-256 is guaranteed uninformative (section 4).

Weakest point
Rejecting output-side tests leaves input-side estimation as the only check, and IE-A-10 concedes that pipeline's own limits. The rejection also removes the only test a non-expert could run unaided
IE-A-10Conceded

None of these estimators is a true entropy measurement. Conceded. Compression bounds from above and cannot certify unpredictability; MCV bounds from below only under an IID assumption this data violates, so it too over-states the floor. The response is structural: lead with the floor-side figure, state the violation with it, and treat digest-structure facts (which are exact) as the primary evidence in the degenerate cases.

Weakest point
What is still missing is a physical noise model (electrons per photosite against read noise) that would bound the source from first principles rather than from its output. Nobody, this work included, has produced one for this sensor in this pipeline
IE-A-11Rejected as scope mismatch

os.urandom or a hardware TRNG should be mixed in. Rejected as scope mismatch, not as a bug. SeedSigner deliberately uses only external, user-supplied entropy.

Weakest point
A project values decision, not a technical finding. The dark findings sharpen the debate's stakes without changing its nature: argue it as design, not as a vulnerability
IE-A-12Inapplicable by design

Network exfiltration or data persisted to disk. The entropy buffer is neither transmitted nor written to storage on a release image; the root filesystem is an initramfs with nothing to persist to.

Weakest point
Verified on the release configuration, not on every board variant. The instrumented builds used in this work deliberately violate it (they dump frames to the card), which is why they stamp themselves un-mistakably and must never hold funds
IE-A-13Conceded, permanent

An individual seed cannot be shown to have had real entropy. Conceded, and permanent. The entropy input never leaves the device, so no third party can re-derive or audit a particular capture: a materially different assurance from the dice path. The analysis works through the trade.

Weakest point
The residual risk is carried by the user's scene choice and observation at capture time, and IE-M-15 shows observation cannot grade a capture. Scene choice (light) is the effective control
IE-A-14Refuted as stated; replaced

"The chain has four inputs, so no single failure can weaken it." Refuted by measurement as an independence claim: the serial and clock are static (IE-A-06), and the two camera layers watch the same scene through the same gain, degrade together (IE-M-03, IE-M-05), and are measurably coupled (IE-M-11). The defensible replacement: the layers protect against different accidents (a degenerate final still, a short window), not against a light-starved sensor, which degrades all of them at once.

Weakest point
In the run where the final image was thinnest (113), the preview layer did carry the chain, so the redundancy is not worthless; it is unreliable in exactly the conditions it was assumed to cover
IE-A-15Refuted by measurement

"A featureless scene (blank wall) is a weak seed." Refuted directly (IE-M-13): finals within 10% of a detail-rich baseline, fully distinct windows, full-color frames, AE in the ordinary lit regime. The worry conflates featurelessness with darkness; the degenerate axis is illumination.

Weakest point
n=2, one unit, one wall, one morning (IE-L-10). The mechanism (per-pixel shot and read noise needing only light, not structure) is general; the sample is small
IE-A-16Direction established

"The dark figures might be inflated by setup error." Correct to worry, and the direction is the dangerous one: light contamination inflates a dark figure, so setup error produces reassuring wrong answers, not alarming ones (IE-M-17). Every retained dark run passes the pixel-band discriminator; light-present runs are flagged in every table; the deleted light-present runs' statistics are preserved in the data2 manifest.

Weakest point
The discriminator was derived from this work's own sixteen occlusion runs, not from an external standard. Its two populations are cleanly separated here, but the threshold has not been validated on other hardware

11Traps: claims that look right and are not

Each of these is a reading that survives a careless check. They are listed because a reviewer who walks into one will produce a confident finding that costs time to unwind, and because several of them caught this analysis during drafting.

The plausible readingWhy it is wrongWhere
The preview layer's contribution is live-vs-constant MCV (~92–94k bits per frame) That figure is dominated by the global quantization-threshold crossing, which every frame shares. Per-read novelty, live against live, is hundreds to a few thousand bits (469–1,428 in the properly dark 240 px windows) IE-M-10
50 window slots means 50 sensor exposures Slots can hold the quantization constant or a cached buffer, and byte-identical live frames are re-served buffers, not repeated exposures. A repeated frame contributes zero IE-M-03, IE-M-08
A repeated frame in a dark window demonstrates the caching stall In the dark, separate reads legitimately quantize to identical bytes. Only a byte-identical repeat of non-constant content evidences frame reuse IE-M-08
The review screen showing an image means the capture was safe (or black means it was not) The review screen is a light test that fails both ways: a faint image appeared on 2 of 10 finals in the 113-bit run, pure black appeared on finals of runs measuring up to 36,462 bits, and the display flickers within one unchanged setup IE-M-15
1.52× is a measured floor with 52% headroom MCV over-estimates the floor on non-IID data, and the chain totals additionally lean on pairwise-times-count and independence assumptions. The true margin of that run is smaller by an unquantified amount IE-L-01, §4
A recomputed lit figure lands 20–35% below the published one Difference-domain mismatch: published figures use signed differences (later − earlier). Absolute values move the lit mode to 1 and drop the bookshelf baseline from 1,505,982 to 1,156,876 and the stationary wall run from 1,490,163 to 971,412; dark figures are domain-invariant, so the error hides there §4
A bigger margin means a proportionally stronger seed SHA-256 emits 256 bits regardless of how much goes in. 256 is a threshold; an attacker attacks 256 bits, not a multi-million-bit photograph Analysis §1
The unit with the highest dark figure has the best sensor Usually it has the worst light seal. Contamination inflates, and enclosure sealing masquerades as sensor quality; under one held-constant method a claimed 20× "quieter" unit became the noisiest IE-M-17
The Plus unit's 3.1M-bit lit figure means better hardware Its still is 640×640, 1.78× the values. Per-value it sits in line with the other units. Compare per-value, never raw totals across geometries §5
data3/000025-934f and data4/000025-934f are the same run Elapsed-time naming recurs across boots of one unit. They are different captures from different boots. Cite with the dataN/ prefix, never merge directories §1
The dark findings reproduce in any dim room "Dark" here is near-total light exclusion at the sensor (a blocked or sealed lens), verified per run by the pixel band. Flagged light leaks measured roughly 7× to over 100× higher, and no dim-ambient condition was measured at all §1, IE-L-14
A finger over the lens probes the dark case Flesh transmits red and near-IR; it is a dim red-lit scene, brighter than face-down-in-shadow, and lands in the light-present band §9
picamera encodes at quality 85, so noise is quantized away The Python default is 85; the MMAL hardware encoder emits all-1s quantization tables. Source reading was insufficient IE-M-01
The camera pipeline can be characterized from current Raspberry Pi camera documentation v0.8.7 runs python-picamera 1.13 over MMAL and boots start_x=1. Tuning files, denoise controls and property names from any other camera stack describe a pipeline this release does not run §14
A dense-mask overlap statistic (Jaccard ~0.94) shows a fixed spatial pattern For masks covering ~94% of the frame the chance baseline is already 0.89. Compare on the sparse complement; done properly, no position is consistently held back and the frames carry real per-read novelty IE-M-10

These are drafting traps, not published corrections. Corrections made after publication are recorded separately, in section 15.

12What we did not verify

  1. IE-L-01 · No full NIST SP 800-90B assessment. Only the most-common-value estimator was run, and it assumes IID data that this is not. The permutation testing, the nine other non-IID estimators and a physical noise model have not been done, and all of them can only lower the figures. This caveat binds every margin in the document, most heavily the 1.52× headline.
  2. IE-L-02 · The chain totals compound optimistic assumptions. Distinct-count × worst-pair overstates (conditioning on several known frames tells an attacker more than one), and summing layers assumes an independence measured to be imperfect (IE-M-11).
  3. IE-L-03 · The gain knife-edge is a correlation over four units, confounded with board family, sensor unit and enclosure, however unanimous across sessions and boots. The mechanism (IE-I-05) is inferred, not isolated.
  4. IE-L-04 · The distribution question is unanswerable from this data. One desk position spanned 113 to 26,318 bits with nothing moved; one unit spanned 384 to 2,527 under one unchanged engineered method. Measured points are points. Nothing here says what a typical user gets, and no figure should be quoted as if it does.
  5. IE-L-05 · Two runs' preview credits rest on a cross-run analogy. data3/000303-6b66 and data3/000031-6b66 hold a single live frame each, so they have no within-run pair; their credits use cross-run worst pairs. This is the one estimate-by-analogy the accounting carries, it is intrinsic to single-live-frame windows rather than a harness artifact, and those two totals should be read as weaker than the rest. It is also the through-line to the distinct-digest recommendation: a window with one live frame has no within-window way to price itself.
  6. IE-L-06 · Sensor temperature is uncontrolled in every figure. No run carries temperature telemetry. Testing it safely requires sealed desiccated equilibration (condensation inside the lens assembly otherwise wrecks the measurement); the data2 manifest records why this was deliberately not pursued, and the direction of the effect is inferred from physics rather than measured.
  7. IE-L-07 · time.time()'s 25–30 bits is an estimate from mechanism, not a measurement, which is why it is excluded from totals rather than credited (IE-I-02).
  8. IE-L-08 · Scene and occlusion descriptions are operator-supplied. The instrumentation records pixel statistics, exposure and gain; what was in front of the lens and how it was occluded is recorded by the operator in the manifests and cannot be checked against the data beyond the pixel-band discriminator. The data2 seal arrangement and each unit's enclosure are now photo-documented (§3, §5), which narrows this for that round but does not close it.
  9. IE-L-09 · The loop-rate and stall findings are specific to v0.8.7's Python preview loop. A different camera stack or preview implementation changes them. The content findings (dark quantization collapse, green-plane anatomy) are sensor and pipeline properties expected to carry, but "expected" is not "measured".
  10. IE-L-10 · The white-wall series is n=2: one unit, one wall, one morning, natural light. The conclusion it supports is a refutation (featureless does not imply degenerate), which two runs can carry; it does not characterize featureless scenes in general.
  11. IE-L-11 · Memory handling. Entropy buffers are dereferenced but never scrubbed, in any release. Out of scope given the threat model, but unexamined.
  12. IE-L-12 · No release-tagged test coverage of the chain composition exists, in any version.
  13. IE-L-13 · Scope is image entropy only. Dice and coin-flip entropy, transaction signing and everything else are outside this analysis.
  14. IE-L-14 · The middle of the illumination axis is unmeasured. The measured populations are ordinary lit scenes and effectively total light exclusion (the properly-dark band). Genuinely dim ambient conditions, a dim room or dusk, sit between them and were never captured; nothing in this document characterizes them, in either direction.

13External analyses

Independent analyses, measurements or validations of SeedSigner's image entropy that we are aware of:

None, at the time of writing.

There is no published external review of this path: no independent measurement, no third-party reproduction of the figures here, and no prior analysis to check this one against. Every figure rests on captures taken by its own author, and the adversarial review it has had was commissioned by that author.

Absence is not evidence

An empty list is not a clean bill of health, and it is not a claim that nobody has looked. It records only that we are unaware of published external work. If you know of any, it belongs here whether or not it agrees with this analysis.

The raw captures ship with this document precisely so this section can stop being empty. Section 14 is written to be followed by someone with no connection to the project. Work that contradicts these figures is more useful than work that confirms them.

To submit findings, or to have published work listed here: open an issue on the analysis repo. Name the revision your work responds to (the repo commit hash at the time of review, per the reporting guidance) as well as the SeedSigner release you measured, and include your own provenance record if you captured frames. The cited commit is what keeps your work aligned with the version it examined when these documents are later revised.

14How to check this yourself

From the published frames, no hardware needed

All 32 runs ship in full (~340 MB), each with its on-device capture.log, under per-round provenance records. Every figure in section 5 comes back out of them:

# final images, min-entropy, every retained series
python3 analyze_mcv.py

# preview-window structure, per run (data3 onward)
python3 analyze_preview.py data3/burst-19700101-000150-6b66
python3 analyze_preview.py data4/burst-19700101-000117-934f
python3 analyze_preview.py data5/burst-19700101-000306-6b66

# the attacker-computable dark constant, from nothing but the panel size
python3 -c "import hashlib; print(hashlib.sha256(bytes([0,0,0,255])*57600).hexdigest()[:8])"   # 6f77f852
python3 -c "import hashlib; print(hashlib.sha256(bytes([0,0,0,255])*102400).hexdigest()[:8])"  # b71fb8a8

Final frames are raw RGB888 (np.fromfile(p, np.uint8).reshape(480, 480, 3), or 640 for the Plus); preview frames are panel-sized RGBA. Both analyzers validate before reporting: analyze_mcv.py refuses duplicate frames (right for final bursts), analyze_preview.py reports them (right for windows), and the latter re-hashes every dumped frame against its logged digest.

From scratch, on hardware

The instrumented build is packaged in capture-rig/: a one-shot, provenance-gated build script, a verifier that reads the app source back out of the finished artifact rather than trusting the staged tree, and the patches themselves: the burst instrumentation, plus a no-seed kill-switch (added 2026-08-07) that deletes the image-to-mnemonic conversion so a capture build cannot turn the bytes it just dumped into a seed. The build refuses refs it cannot trace to the upstream remotes and stamps any override into the PROVENANCE.txt shipped with the data; it also refuses an OS tree without BR2_PACKAGE_PYTHON_PICAMERA=y, because without that package the release's capture path cannot run at all. Flash, boot, and confirm the splash reads a -BURST-DEBUG version before trusting anything. One run is New Seed → Image entropy, about a minute.

The instrumented build is not a wallet

Any seed generated on it is compromised, because the image bytes that seed is derived from are written to the microSD card in the clear. That is the entire point of the build, and it is why no release does this. The version stamp exists so a card cannot be mistaken for a release. Do not fund a wallet created on it.

Update 2026-08-07: the rig now removes that failure mode outright. A second patch deletes the image-to-mnemonic conversion, so the image-entropy flow ends at a "Seed creation disabled" notice instead of a seed, and verify-image.py refuses images built without it. The published data predates the kill-switch, which sits downstream of every capture write and alters no measurement. The other seed flows (dice, manual entry) are stock; the build remains a lab tool, never a wallet.

What would be worth the most

The measurements this analysis cannot take

Any unit that is not one of the four measured here. The gain knife-edge (IE-M-09) is unanimous over n=4 and confounded; a fifth unit's dark sessions either extend the correlation or break it, and both outcomes are findings.

Dark preview windows on more units. The preview-zero state has been observed on one unit (six of six). A second below-rail unit, or a railed unit that collapses, would move the knife-edge claim from correlation toward mechanism.

Any measurement of the post-0.8.7 native camera engine's preview path. The content findings are expected to carry to it (IE-L-09); nobody has measured it.

Figures that disagree with these are more useful than figures that match, and section 13 is written to be filled in.

Traps for anyone repeating this work, all of which have bitten it

Working notes

Served raw rather than rendered, and intended for a reviewer rather than a reader: the per-directory RUNS.md manifests (the curated ground truth for every figure here), and 10-image-entropy-history.md, the per-tag working behind section 7. The build, capture and validation procedure is capture-rig/README.md.

15Scope and provenance

Analysis2026-07-31; instrumented rounds 2026-08-03/04; measurement programme and full data rebuild 2026-08-05/06
What it is notAn audit. No independent engagement, no certification. Not an official SeedSigner project publication
App reviewedtag 0.8.7 · commit e0a80d4b · upstream-verified
OS reviewedtag v0.8.7 · commit d1385939 · upstream-verified
Instrumentationpatches f1c059df (data1/data2, final-image) and 21dd1fd3 (data3–data5, adds preview-window logging) · camera.py byte-identical to stock · per-round PROVENANCE files in each data directory
History tracedv0.4.3 (2021-07-31) through v0.8.7
Devices4 units: Pi Zero 2 W Rev 1.0, Pi Zero Rev 1.3, Pi Zero W Rev 1.1 (bare), Pi Zero W Rev 1.1 + Plus display
SensorOV5647 (one module per unit; modules not swapped between units)

Models used

RoleModel
Coordinating analysis, measurement design, synthesisClaude, via Claude Code (see note)
Release-history researchClaude Fable 5
Camera stack and denoise configuration reviewClaude Fable 5
Adversarial verification passes on the earlier draftClaude Fable 5
Operating-system entropy posture reviewClaude Sonnet 5
Instrumented builds and capture supportClaude Code (parallel sessions)
Min-entropy estimator, pair-selection audit, document splitClaude Opus 5
Preview-layer measurement rounds, data curation, provenance recordsClaude Code sessions, 2026-08-05/06
Two-document rebuild on the curated populationClaude Fable 5

The coordinating session was switched between models during setup, so this document declines to assert a single exact model string for that role rather than guess. The subagent and review models were selected explicitly per task and are stated with certainty.

Review status

An earlier draft of this analysis, built on a since-superseded capture population, went through a hostile-reader review by a separate agent with no involvement in producing it: it re-ran the analysis from raw captures, checked every cited source line at the pinned tag, and its findings changed that draft in substance. The measurement-methodology lessons that came out of that pass and the subsequent rebuild are recorded in the repository's knowledge documents and are baked into the analyzers (the confidence bound, worst-of-all-pairs selection, the duplicate-guard split, the digest-coverage dumping rule).

The rebuilt documents and population went through an independent adversarial pass on 2026-08-07: three fresh-context Claude Fable 5 reviewers with no involvement in the drafting, one recomputing every figure from the shipped frames with its own tooling (not only the shipped analyzers), one attacking claims, logic and convention compliance, one verifying every source claim against the pinned tags. Every finding was re-verified by recomputation before any change was made. Confirmed clean: all 32 runs' figures, all 15 chain totals, both computable constants, every caption statistic, every cited source line and commit/PR attribution, and the release-history code claims. What the pass changed, applied the same day: the v0.4.5 rotation claim (PIL rotate(90) on the era's non-square final crops a third of the scene and injects constant fill, so "rotation permutes pixels" was wrong for v0.4.5–v0.8.5, and "earlier releases were larger" became "no release gave the chain less scene content"); the verdict's scope (restricted to chains that were fully measured, because the engineered-seal round's chains are indeterminate); the review-screen spans restated per-frame (both display classes occur across runs from 113 to 36,462 bits); the AE-gain corroborant demoted to one-directional; a set of range and count corrections (lit floor 1.3M, live-live novelty 469–1,428 properly dark, loop rate to 11.6 fps, sixteen occlusion runs, one 179× multiplier, 2–5 distinct frames on railed units, a 45-slot constant plus one cached live repeat in the 113-bit window); the sub-256 finals attributed to one board rather than two; and corpus-wide final-image uniqueness verified globally, upgrading IE-M-16. One finding was declined: retired figures quoted inside manifest supersession notes remain there deliberately, as supersession records. Claims with no deterministic reproduction path from the shipped data are now marked as such where they stand (IE-M-01, IE-M-02's block statistics, IE-M-18, the cross-sensor pattern-noise figures, and IE-M-06, whose held-button captures were not retained).

Corrections after publication will be recorded here, with what changed and why. Errors caught during drafting are in section 11 as reviewer aid, which is a different thing and is not presented as a correction history.

Figure parameters

All figures appear in the analysis; their parameters are recorded here, and every plate regenerates from the shipped raw frames via the named script. No plate uses autocontrast or per-image normalization.

Lit bookshelf plates (make_figures_lit.py). data1/burst-19700101-000303 frames 00 and 01, 480×480 RGB888 as dumped before hashing. Differences are |a − b| at fixed linear gains of ×12 and ×40 with clipping. Detail crops are 96×96 at offset (150, 60), scaled 6× nearest-neighbor; the two blink crops also ship as unquantized stills (14_lit_detail_frame00.png, 15_lit_detail_frame01.png) so the comparator can be checked frame by frame. The social-sharing card (social_card.png, 1200×630, the analysis page's og:image) is the as-shot frame 00 and the ×40 difference side by side, both full frame (no crop), scaled to 596×596 (LANCZOS for the photograph, nearest-neighbor for the difference) on a black 1200×630 canvas; neither panel carries processing the published plates do not, beyond the JPEG encode (quality 88) that keeps the card under messenger size limits.

Rig photographs (rig_01…, rig_02…, rig_03…). Operator photographs of the data2 occlusion arrangement and the four units' camera-side enclosures, resized to 1400 px long edge (JPEG, quality 85) and otherwise unedited. They are documentation of the apparatus, not measurement exhibits; the full-resolution originals are retained outside the repository.

Blank-wall plates (make_figures_whitewall.py). data5/burst-19700101-000306-6b66 frames 03 and 06, the run's worst pair by MCV, selected on the estimator reported. Same gains (×12, ×40) and treatment as the bookshelf plates so the two sets compare directly. Detail crop 96×96 at offset (192, 192), scaled 6× nearest-neighbor. The wall is white; auto white balance renders the pale blue-green cast, and the radial gradient is part window light, part the optical system's natural vignetting.

Dark plates (make_figures_dark150.py). data3/burst-19700101-000150-6b66. Final-image plates are the worst pair (frames 00 and 06, 105 changed values), as shot and at fixed linear gain ×60 with clipping; their difference is ×60 and 5×5 max-dilated, because undilated its 105 values are invisible, and the caption states the dilation. Preview plates are live frame 18 (×60) and the 18-vs-19 difference (×60, no dilation). The review-screen plate (dark_review_stretch_frame08.png) is ImageOps.autocontrast(cutoff=2) applied to stock frame 08 of the same run, with no gain or other processing, rendered at Pillow 11.0.0, the version the release ships: the identity-vs-stretch threshold is version-sensitive (IE-M-15), so regenerate that plate at that version. The black constant is stated in captions and deliberately not shown as an image: a picture of pure black communicates nothing its digest does not.

The blink comparators (13_lit_detail_blink.png, whitewall_detail_blink.png) are animated PNGs alternating two detail crops every 500 ms. APNG rather than GIF, deliberately: GIF's 256-color palette would quantize away much of the change (in testing, a shared 256-color palette lost more than half the crop's differing pixels). APNG is lossless, so every pixel that appears to move is a pixel that moved. Each frame carries a caption strip added below the image, never over it, blue for capture 1 and orange for capture 2 (#1B4B7A / #C1621E, 46 px): the standard colorblind-safe opposed pair, kept clear of the series' semantic colors, where a green-versus-red pair on two equally valid captures would read as pass-versus-fail. Browsers without APNG support show the first frame.