Iriscope is a local-first capture and enhancement pipeline for a Raspberry Pi Zero W with an IMX477 Raspberry Pi HQ camera. The Pi captures DNG/JPEG frames with rpicam-still; the computer pulls the session and performs RAW development, frame filtering, alignment, stacking, iris masking, enhancement, and review export.
Iris images are biometric data. This tool keeps processing local and intentionally does not upload images or perform identity matching. See SECURITY.md for local data-handling and remote API guidance.
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e .[dev]
Copy-Item config.example.toml .iriscope.tomlEdit .iriscope.toml for your Pi hostname/user, then verify the camera:
iriscope calibrateCapture a session:
iriscope capture --subject S001 --eye left --count 12Process a pulled or manually copied session:
iriscope process captures\S001_left_20260616_153000
iriscope review captures\S001_left_20260616_153000 --openUse Raspberry Pi OS (Legacy) Lite, 32-bit on the original Pi Zero W. See provision/pi-zero-w/README.md for the SD-card image choice and provisioning script.
Enable SSH, configure Wi-Fi, and verify the camera:
rpicam-hello --list-camerasThe capture command generated by Iriscope follows this pattern:
rpicam-still --raw --immediate --nopreview \
--awb auto --metering centre --exposure normal --denoise cdn_fast \
--metadata frame_0001.json --metadata-format json \
--quality 95 -o frame_0001.jpgrpicam-still --raw writes a paired .dng next to the JPEG. The Zero W should only capture frames; RAW processing and stacking happen on the computer.
- Use a fixed camera stand, fixed head/chin support, fixed camera distance, and manual lens focus.
- Use diffuse, consistent white LED illumination.
- Add cross-polarization if corneal reflections dominate.
- Capture dark, flat-field, and white/color reference frames at the start of a session when possible.
- Use 8-20 same-focus frames for denoising/detail stacking.
- Use a separate focus bracket only when depth of field is visibly insufficient.
Processing writes files under SESSION/processed/:
enhanced.tif: 16-bit enhanced master.enhanced.jpg: review/share JPEG.stacked.tif: stacked pre-enhancement image.iris_mask.png: detected iris enhancement mask.contact_sheet.jpg: before/after and frame overview.report.json: deterministic processing report.review.html: generated byiriscope review.
For offline algorithm testing, use a local iris dataset folder whose leaf directories contain same-eye frame stacks. I tested with the CASIA-Iris-Interval subset from CASIA-IrisV4. The original CASIA page describes it as 8-bit grayscale JPEG NIR iris imagery and releases it for research/educational use only, so keep downloads and derived reports local.
Example:
iriscope eval-dataset datasets\CASIA-Iris-Interval --offset 0 --limit 60 --max-working-edge 640The evaluator writes dataset_report.json and dataset_sessions.csv under datasets\eval-runs\.... It records frame quality, post-alignment stack frame count, mask geometry, contrast/edge gains, and heuristic flags such as heavy clipping, weak alignment, bad mask geometry, and possible oversharpening.
Processing quality thresholds are configurable under [processing.quality] in .iriscope.toml. The defaults preserve the built-in heuristics for clipping, focus, luminance, alignment, mask coverage, pupil/iris ratio, dataset geometry checks, and oversharpening flags.
iriscope init-config --output .iriscope.toml
iriscope calibrate --host raspberrypi.local
iriscope capture --subject S001 --eye right --count 16 --awb auto --iso 0
iriscope process captures\S001_right_20260616_154500 --dark calibration\dark.dng --flat calibration\flat.dng
iriscope eval-dataset datasets\CASIA-Iris-Interval --offset 60 --limit 60 --max-working-edge 640The host-side GUI is a React/Vite app backed by the local Python API.
Install everything:
python -m pip install -e ".[web,webrtc,ssh,dev]"
cd web
npm installRun the API and frontend in two terminals:
python -m iriscope web --host 127.0.0.1 --port 8765cd web
npm run dev -- --port 5173Open http://127.0.0.1:5173.
The host GUI/API can also run as one Docker container. The image builds the Vite frontend into static files and serves it from the FastAPI process on port 8765.
Create a Docker environment file:
Copy-Item .env.example .envEdit .env for your Pi. If Iriscope.local does not resolve inside Docker, use the Pi IP address instead. To use SSH from the container, set IRISCOPE_SSH_KEY_HOST_PATH to the host key path and keep IRISCOPE_PI_SSH_KEY=/run/secrets/iriscope_ssh_key.
Run without an SSH key mount:
docker compose up -d --buildRun with the SSH key mount enabled:
docker compose -f docker-compose.yml -f docker-compose.ssh.yml up -d --buildOpen http://127.0.0.1:8765. Captures are persisted under the local captures/ folder, and .iriscope.toml is mounted as the container config file. Docker binds to 127.0.0.1 by default; set IRISCOPE_BIND_ADDRESS and IRISCOPE_ADMIN_TOKEN before exposing it on a shared network. Docker-specific environment variables such as IRISCOPE_PI_HOST, IRISCOPE_PI_USER, and IRISCOPE_PI_SSH_KEY override the mounted TOML at runtime.
Docker uses the MJPEG preview transport by default. WebRTC relies on dynamic UDP ICE candidates, which Docker Desktop does not expose through the single HTTP port mapping, so the app starts MJPEG directly instead of attempting WebRTC and falling back. Set IRISCOPE_WEBRTC_ENABLED=true only when the container is running with host networking or another relay setup that makes WebRTC ICE candidates reachable.
To stop the container:
docker compose downThe Docker watchdog keeps a deployed checkout aligned with GitHub. It compares the local HEAD with origin/main; when GitHub is newer, it runs git fetch, resets the checkout to origin/main, cleans untracked files, and rebuilds/restarts iriscope-host.
Run one check:
.\scripts\iriscope-docker-watchdog.ps1 -OnceRun continuously:
.\scripts\iriscope-docker-watchdog.ps1For Bash environments:
scripts/iriscope-docker-watchdog.sh --once
scripts/iriscope-docker-watchdog.shThe watchdog intentionally discards local edits in the deployed checkout so it matches GitHub. Use a separate development checkout for uncommitted work.
The GUI provides:
- local device/dependency status, including COM port, UVC camera, SSH config, and Python dependencies
- live Pi HQ camera preview fed by
rpicam-vidover SSH, using WebRTC when available and MJPEG/still snapshot fallbacks otherwise - UVC preview snapshots for a directly attached USB camera when no Pi host is configured
- capture controls for subject, eye, frame count, shutter, gain/ISO equivalent, AWB mode/gains, metering, exposure, and tuning file
- pre-processing inspection that writes
preprocess_report.json - processing controls for the existing stacking/enhancement pipeline
- biometric label sidecars written as
iriscope_labels.json
For the most reliable preview, run the Pi Zero as a USB Ethernet gadget and set [pi].host to the USB-side address, for example 10.42.0.2. The preview stream uses the [preview] config section:
[preview]
width = 640
height = 480
framerate = 12
quality = 70
stream_timeout_s = 0Before calibration, still snapshots, or RAW stack capture, the host API stops the preview stream so rpicam-still can acquire the camera.
Label records are for local dataset governance only. They track consent, allowed use, exclusion from model training, quality notes, lens/lighting metadata, and subject codes. Iriscope does not perform identity matching or biometric enrollment.
The API is unauthenticated only for loopback use. If you bind it to a non-loopback host, set IRISCOPE_ADMIN_TOKEN and pass it as Authorization: Bearer <token> or X-Iriscope-Token. Web API session and artifact paths are bounded to the configured capture root; calibration files are accepted only from the capture root or project calibration/ directory.