Skip to content

About

Render plane projections of a 3D voxel volume: NRRD + CSV planes → MP4, one frame per plane. Python, fully tested and benchmarked (I2SE Exercise 1).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Plane Projection logo

Plane Projection

CI Python 3.12+ License: MIT Live demo

Render any planar cross-section of a 3D voxel volume as a video: one NRRD volume + one CSV plane-list → one .mp4, one frame per plane, in list order.

Live demo & interactive walkthrough: https://ofeq-dev.github.io/plane-projection/

Croissant projection frame

Contents

The problem

Given a 3D raster matrix M and a plane p, compute the 2D image of the projection of M onto p.

— I2SE_Exe1.docx (exercise brief, repo root)

A plane arrives as 3 or 4 points in a CSV row:

Points Geometry Validity rule (with EPS = 0.001)
3 isosceles triangle (a, b, c) |dist(a,b) − dist(a,c)| < EPS
4 isosceles trapezoid (a1, a2, b, c) |dist(a1,b) − dist(a2,c)| < EPS, dist(a1,a2) ≤ dist(b,c), both legs on one plane

An invalid row raises internally and becomes a black error frame with a red banner naming the row and the failing rule; the remaining rows still render, so one bad plane never kills the run. Each sample picks the closest voxel (nearest-neighbour, half-to-even tie-break) — no interpolation or "super-resolution", per the brief's Q&A. Samples outside M are black.

Requirements, acceptance criteria and the exact frame geometry live in docs/srs.md and docs/algorithm.md; terms are defined in GLOSSARY.md.

How it works

flowchart LR
    A["NRRD volume\n(8/16/32-bit)"] --> L["Volume loader"]
    B["CSV plane list"] --> P["Parse + validate\n(3- or 4-point planes)"]
    L --> R["Closest-voxel\nprojection renderer"]
    P --> R
    R --> F["Frame per plane\n(invalid → error frame)"]
    F --> V["MP4 writer\n(OpenCV mp4v)"]
Loading

Each plane is parsed and validated against the isosceles-triangle / trapezoid rules; valid planes are rendered by nearest-voxel sampling onto a canvas sized to the largest plane in the list, and everything — good and bad frames — is written to one MP4. The full frame geometry and rounding rules are in docs/algorithm.md; key implementation choices and why they were made are in docs/adr/:

ADR Decision
0001 Store large volumes/videos in Git LFS
0002 Canonical voxel-index space (no world-space headers)
0004 Video encoding via OpenCV mp4v (pip-only, no system ffmpeg)

Scope

In scope: NRRD volume input, CSV plane-list input, per-plane rendering, per-row validation and error frames, MP4 output, a documented runtime budget, and a CLI with the brief's exact call signature.

Out of scope: interactive/guided use, world-space (physically scaled) rendering (voxel-index space only — ADR 0002), and out-of-plane interpolation (rationale).

Quick start

Python ≥ 3.12 (numpy 2.5.x requires it). Video encoding uses OpenCV's cv2.VideoWriter with the mp4v codec — everything is pip-installable, no system binary needed (ADR 0004).

Large assets (.nrrd volumes, .mp4 outputs) are stored in Git LFS — clone normally, then run git lfs pull to fetch the real files (see assets/nrrd/ and assets/videos/).

git clone https://github.com/ofeq-dev/plane-projection.git
cd plane-projection
git lfs pull
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
# development extras (tests, typechecking):
pip install -r requirements-dev.txt

Run the brief's demo contract (SRS FR-1):

python Ex0.py file.nrrd file.csv file.mp4

# the lecturer's example: 2 valid planes + 2 deliberately invalid ones
python Ex0.py assets/nrrd/MRHead_1.nrrd assets/csv/lecturer_example.csv out.mp4
row 1: ok (triangle)
row 2: ok (trapezoid)
row 3: |dist(a,b) − dist(a,c)| = 20.5284 ≥ EPS = 0.001 (dist(a,b) = 497.393, dist(a,c) = 476.865)
row 4: |dist(a1,b) − dist(a2,c)| = 100 ≥ EPS = 0.001 (dist(a1,b) = 300, dist(a2,c) = 400)
wrote 4 frames (500x448, mp4v @30 fps) to 'out.mp4' (2 error row(s))

Exit code is non-zero on usage errors (missing file, wrong argument count), never because an individual plane is invalid — an invalid plane is reported on stderr and in the video.

Input CSV format

One header row, then one row per plane; the 4th point's cells are empty for 3-point rows:

Number of Points (3 or 4),x1,y1,z1,x2,y2,z2,x3,y3,z3,x4,y4,z4
3,200,200,0,0,200,400,400,200,400,,,
4,200,0,0,200,0,500,200,400,0,200,400,500

Coordinates are voxel indices into M[x][y][z].

Testing

python -m pytest        # 123 tests, ~10 s

The suite decodes the produced MP4 and compares rendered frames against hand-derived pixel tables and committed goldens; it covers validation, error frames, dtype scaling, canvas/padding, the CLI contract, and the benchmark harness. Seams, oracles and the golden protocol are documented in docs/testing-strategy.md.

Performance

The brief asks for ≥ 30 FPS (target 100 FPS) on a 500³ byte volume. Measured end-to-end (load + render, min-of-repeats) on the reference machine:

Input Frames FPS
croissant.nrrd × croissant.csv — 500³ uint8, the canonical 500³ case 100 50.1
onion.nrrd × head.csv 280 433.5
MRHead_1.nrrd × head.csv 280 380.2

The ≥ 30 FPS hard bar is met; the 100 FPS aspirational target is not met on this (2013 dual-core) machine. Every row of the full 30-case cross-product — with the exact one-line command that reproduced it — is in benchmarks/results-2026-11-07.md. Reproduce with:

python bench.py assets/nrrd/croissant.nrrd assets/csv/croissant.csv --repeats 3
python bench.py --corpus --repeats 3 --out benchmarks/results-$(date +%F).md

Example runs & outputs

Video Volume / planes What it shows
lecturer_example.mp4 MRHead_1 × lecturer_example.csv The brief's demo: 2 projections + 2 error frames
head.mp4 MRHead_1 × head.csv 280 axial slices, black outside the volume
onion.mp4 onion (uint8) 120 frames with real texture

MRHead_1.nrrd is int16; frames are shown over the full dtype range (no content stretch), so in-volume samples are mid-gray and the projection is visible as the gray/black boundary. A uint8 volume (onion) has full contrast:

Onion frame

Frame-by-frame commentary, canvas sizing and byte-identical reproducibility notes: docs/example-run.md.

Sample data

The corpus covers the brief's 8/16/32-bit unsigned element types (plus the lecturer's signed int16 starter volume) and both 3- and 4-point planes:

Volume Dimensions Type Plane list
MRHead_1.nrrd 256 × 256 × 130 int16 lecturer_example, head
croissant.nrrd 500 × 500 × 500 uint8 croissant
onion.nrrd 256 × 256 × 130 uint8 onion
biscuit_cake.nrrd 256 × 256 × 130 uint16 biscuit_cake
swiss_roll.nrrd 256 × 256 × 130 uint32 swiss_roll

MRHead_1.nrrd is the lecturer's 3D Slicer MRI sample (donated to the Slicer project without restrictions — see docs/research/01-data-formats.md); its signed int16 type is tolerated per SRS FR-4. The other volumes are the exercise corpus covering each unsigned element type. Every volume has a matching CSV in assets/csv/, and bench.py --corpus runs the whole cross-product.

Docs

Document Contents
docs/srs.md Requirements (FR-1…FR-21, NFR-1…NFR-4), scope, acceptance criteria
docs/algorithm.md Frame geometry, sampling, rounding and orientation
docs/algorithm.html Interactive, plain-words walkthrough of the algorithm
docs/testing-strategy.md Seams, oracles, goldens, traceability to requirements
docs/example-run.md A real run, frame by frame, with exported videos
docs/adr/ Architecture decision records
docs/research/ Verified research notes (formats, primitives, perf testing)
benchmarks/ Benchmark harness results

Repository layout

Ex0.py                    CLI orchestrator (imports from modules below)
errors.py                 Shared UsageError exception
geometry.py               Scalar geometry helpers (dist, cross, sub, dot)
csv_reader.py             FR-9 tolerant CSV row iterator
plane_validation.py       Plane parsing + validation (parse_planes, count_planes)
bench.py                  Benchmark harness (corpus cross-product, min-of-repeats)
tests/                    pytest suite (unit, golden, CLI/MP4, benchmark)
assets/nrrd|csv|videos/   Sample volumes, plane lists and rendered outputs (Git LFS for large files)
docs/                     SRS, algorithm, testing strategy, example run, ADRs, research
site/                     Static GitHub Pages demo (deployed on master)

License

MIT — © 2026 Josh Hall (ofeq-dev). Sample NRRD volumes retain their original provenance; see assets/nrrd/ for details.

About

Render plane projections of a 3D voxel volume: NRRD + CSV planes → MP4, one frame per plane. Python, fully tested and benchmarked (I2SE Exercise 1).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages