@niivue/nv-ohif 0.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # @niivue/nv-ohif
2
+
3
+ A [NiiVue](https://github.com/niivue/niivue) viewport extension for the
4
+ [OHIF Viewer](https://ohif.org) (v3.12). Render series with NiiVue inside OHIF —
5
+ bringing 3D volume rendering, mesh/surface overlays, multiplanar with colormapped
6
+ overlays, and voxel drawing / vector annotation to your OHIF app.
7
+
8
+ > **Status: proven in a real OHIF app.** Renders **NIfTI (volume-URL) display sets**
9
+ > and **DICOM series** (fetched + converted to NIfTI with `@niivue/dcm2niix`), with a
10
+ > toolbar surfacing NiiVue's views / clip plane / overlay / window-level. Verified
11
+ > mounting inside a full local OHIF Viewer (registered via `pluginConfig.json` + a
12
+ > mode). See `PLAN.md`.
13
+
14
+ > **Consuming the local build (dev):** install **packed tarballs**, do not symlink.
15
+ > Symlinking the monorepo package into an OHIF app makes its bundler follow the link
16
+ > and bundle a duplicate of shared deps (breaks OHIF's floating-ui). Use
17
+ > `npm pack` + `pnpm add file:<dir>` so each resolves self-contained from the app tree.
18
+
19
+ ## Try the proof demo
20
+
21
+ `bun run dev` starts a small OHIF-shaped harness (`demo/`) that drives the real
22
+ extension — it pulls the viewport via `getViewportModule()` and renders it with a mock
23
+ OHIF display set pointing at a public NIfTI, so you can see NiiVue rendering a volume
24
+ without a full OHIF app.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ bun add @niivue/nv-ohif
30
+ # peers your OHIF app already provides: @ohif/core, @ohif/extension-default,
31
+ # react@^18.3.1, react-dom@^18.3.1, and @niivue/niivue
32
+ ```
33
+
34
+ ## Register the extension
35
+
36
+ Add it to your OHIF app's `pluginConfig.json` (or `addExtension`) and reference the
37
+ viewport from a mode. The extension id is `@niivue/nv-ohif`; the viewport name is
38
+ `niivue`.
39
+
40
+ ```js
41
+ // in a mode's viewport config
42
+ {
43
+ namespace: '@niivue/nv-ohif.viewportModule.niivue',
44
+ displaySetsToDisplay: ['<your NIfTI/volume display-set handler>'],
45
+ }
46
+ ```
47
+
48
+ ## What it does today
49
+
50
+ - Registers a **React 18 viewport** that owns a `<canvas>` + a NiiVue instance.
51
+ - Loads a display set whose URL is a NiiVue-readable volume (`.nii/.nii.gz`, `.nrrd`,
52
+ `.mgz`, `.mha`, `.mif`, …) via `nv.loadVolumes(...)`, opening in multiplanar.
53
+ - Loads a **DICOM** series by fetching it and converting to NIfTI with
54
+ `@niivue/dcm2niix` (see [DICOM support](#dicom-support) for the dependency caveat).
55
+ - Renders **whole-slide (SM)** series with **NVSlide** (tiled deep-zoom) on its own
56
+ canvas: **JPEG, TILED_FULL** slides are supported; **JPEG 2000** and **TILED_SPARSE**
57
+ are declined with an explanatory note (see [Whole-slide imaging](#whole-slide-imaging)).
58
+ - Mirrors OHIF's active primary tool (Window/Level, Pan) onto NiiVue's left-drag,
59
+ and reflects a manual NiiVue window/level drag onto any sibling OHIF viewport
60
+ showing the same series (`setViewportWindowLevel`).
61
+ - Ships **toolbar buttons + commands**: a views dropdown (axial / coronal /
62
+ sagittal / multiplanar / 3D render), a **clip-plane** dropdown (off / anterior /
63
+ posterior / left / right / superior / inferior), a **window/level** dropdown
64
+ (auto robust window + OHIF's modality presets, applied as NiiVue calMin/calMax),
65
+ a **colormap** dropdown (gray / hot / bone / cool / warm / viridis / plasma /
66
+ inferno / turbo / jet, applied to the base volume), a **colorbar** toggle (the
67
+ colormap legend), a **smoothing** toggle (nearest-neighbor vs linear
68
+ interpolation), a **crosshair** toggle (`NiivueCrosshair`, show/hide the
69
+ crosshair — use in place of cornerstone's Crosshairs button, which no-ops on a
70
+ NiiVue viewport), a **capture** button (`NiivueCapture`, save the viewport as
71
+ PNG — use in place of cornerstone's Capture, which errors on a NiiVue
72
+ viewport), an **overlay** toggle (load the study's next series as a colormapped
73
+ overlay), and a reset-view button — all with active/disabled state tracked per
74
+ viewport.
75
+ - Backs OHIF's full **Measurement** tool group (Length, ellipse / rectangle /
76
+ circle / freehand / spline / livewire ROIs, Bidirectional, ArrowAnnotate) with
77
+ NiiVue annotations, reflected into the Measurements panel with stats and
78
+ editable labels (see [Measurement tools](#measurement-tools)).
79
+
80
+ ## Toolbar buttons
81
+
82
+ The extension registers the commands (`niivueSetSliceType`, `niivueResetView`,
83
+ `niivueSetClipPlane`, `niivueToggleOverlay`, `niivueSetWindowLevel`,
84
+ `niivueSetWindowLevelPreset`, `niivueAutoWindowLevel`, `niivueSetColormap`,
85
+ `niivueToggleColorbar`, `niivueToggleInterpolation`, `niivueToggleCrosshair`), the toolbar evaluators, and a customization pack with the
86
+ button definitions. A mode pulls them in by reference and places them in its
87
+ primary bar:
88
+
89
+ ```js
90
+ // in a mode
91
+ toolbarButtons: [
92
+ { $reference: 'cornerstone.toolbarButtons' },
93
+ { $reference: 'niivue.toolbarButtons' },
94
+ ],
95
+ toolbarSections: [
96
+ { $reference: 'cornerstone.toolbarSections' },
97
+ { $reference: 'niivue.toolbarSections' },
98
+ // restate your primary bar with the NiiVue buttons added (sections
99
+ // shallow-merge per key, later wins). Use NiivueCapture / NiivueCrosshair
100
+ // in place of cornerstone's 'Capture' / 'Crosshairs' on NiiVue viewports:
101
+ // ToolbarService.register is first-registration-wins, so a same-id override
102
+ // of a cornerstone button is silently dropped.
103
+ { primary: [/* ...your button ids, */ 'NiivueViews', 'NiivueClip',
104
+ 'NiivueWindowLevel', 'NiivueColormap', 'NiivueColorbar', 'NiivueInterpolation',
105
+ 'NiivueRuler', 'NiivueOverlay', 'NiivueReset', 'NiivueCapture',
106
+ 'NiivueCrosshair'] },
107
+ ],
108
+ ```
109
+
110
+ ## Measurement tools
111
+
112
+ Every tool in OHIF's **Measurement** group is backed by NiiVue's vector
113
+ annotation system (one unified path, no separate ruler subsystem). Pick a tool in
114
+ the toolbar and it draws on the NiiVue viewport; a completed shape appears as a
115
+ row in OHIF's **Measurements** panel with its stats.
116
+
117
+ | OHIF tool | NiiVue annotation | Gesture | Reported |
118
+ |-----------|-------------------|---------|----------|
119
+ | Length (Ruler button) | `measureLine` | drag | length (mm), drawn as a graduated ruler |
120
+ | EllipticalROI | `measureEllipse` | drag | area (mm2) + min/mean/max/SD intensity |
121
+ | RectangleROI | `measureRect` | drag | area (mm2) + intensity |
122
+ | CircleROI | `measureCircle` | drag | area (mm2) + intensity |
123
+ | PlanarFreehandROI | `freehand` | drag | contour only (no stats) |
124
+ | SplineROI | `measureSpline` | multi-click | area (mm2) + intensity |
125
+ | LivewireContour | `measureLivewire` | multi-click | area (mm2) + intensity |
126
+ | Bidirectional | `measureBidirectional` | two drags | long + short diameter (mm) |
127
+ | ArrowAnnotate | `arrow` | drag | label only |
128
+
129
+ PlanarFreehandROI panel reflection currently requires exactly one contour without
130
+ holes. Multi-part or holed freehand annotations remain visible in NiiVue but are
131
+ not reflected into OHIF, whose measurement schema exposes only one polyline.
132
+
133
+ **Gestures.** Drag tools press-drag-release on a 2D slice. The multi-click tools
134
+ (SplineROI, LivewireContour) click to place each vertex, double-click to close the
135
+ contour, and Escape to cancel an in-progress contour. Bidirectional takes two
136
+ drags: the long axis first, then the perpendicular short axis.
137
+
138
+ **Intensity stats** are sampled from the base volume in the series' modality unit
139
+ (HU for CT, SUV for PT, unitless otherwise); area is in mm2 and voxel-spacing
140
+ aware. Length and the bidirectional diameters are in-plane mm.
141
+
142
+ **Rendering.** Shapes and their labels are drawn by the `@niivue/uikit` overlay
143
+ (the same widget set the whole-slide ruler uses), not OHIF's cornerstone canvas.
144
+ A measured line renders as a graduated ruler (end caps, mm ticks, numbered majors,
145
+ rotated length label) in the annotation's stroke color.
146
+
147
+ **Free-text labels.** Edit a measurement row's label in OHIF's panel and the text
148
+ is pushed onto the shape on the viewport; the label and the annotation stay in
149
+ sync both directions.
150
+
151
+ **Panel reflection needs a backing DICOM series.** A row is added only when the
152
+ viewport is backed by a loaded DICOM series with instances (so the row carries a
153
+ resolvable `referenceSeriesUID`). A NIfTI-URL display set still draws the shape on
154
+ the canvas, but it is not reflected into the panel.
155
+
156
+ ## DICOM support
157
+
158
+ DICOM series are rendered by fetching the instances and converting them to NIfTI
159
+ in-browser with `@niivue/dcm2niix` (a WASM build of dcm2niix). This is verified
160
+ working end-to-end in a real OHIF app for both uncompressed and JPEG-LS studies,
161
+ and it now works for `npm`-install consumers too.
162
+
163
+ > **History (resolved):** older `@niivue/dcm2niix` releases aborted every
164
+ > in-browser conversion. Emscripten's `exit()` *throws* inside a Web Worker (it
165
+ > returns the code under Node), and the worker did `const exitCode =
166
+ > mod.callMain(args)` and let that throw hit its catch, so it never read `/output`.
167
+ > The fix wraps `callMain` in try/catch and reads `err.status`. It is merged upstream
168
+ > (`rordenlab/dcm2niix`, commit `aae72ac`, both `js/src/worker.js` and
169
+ > `worker.jpeg.js`) and **published** in `@niivue/dcm2niix 1.3.20260724` (the
170
+ > calendar-versioned npm `latest`). This package pins `@niivue/dcm2niix` at
171
+ > `^1.3.20260724`, so `npm install @niivue/nv-ohif` pulls a dcm2niix that has the
172
+ > fix and DICOM conversion works out of the box. (Versions `1.2.0` and the
173
+ > `1.3.0-dev.0` prerelease predate the fix and are excluded by the pin.)
174
+
175
+ ## Whole-slide imaging
176
+
177
+ A whole-slide (SM) series renders with **NVSlide** (tiled deep-zoom) on its own
178
+ WebGL2 canvas, overlaid on the NiiVue viewport. The manifest is built from the
179
+ DICOM tile pyramid (`buildWsiManifest`) and tiles are fetched on demand
180
+ (`DicomWsiTileSource`).
181
+
182
+ Support boundary:
183
+
184
+ - **Supported:** **JPEG**-encoded tiles with **TILED_FULL** frame organization.
185
+ - **Declined (with an in-viewport note, not a crash):**
186
+ - **JPEG 2000** tiles (the viewer cannot decode them yet).
187
+ - **TILED_SPARSE** frame organization (the tile grid assumes row-major
188
+ TILED_FULL order, so a sparse slide would render scrambled).
189
+ - A slide with no tiled (VOLUME) pyramid levels.
190
+
191
+ The slide viewport supports pan/zoom and the whole-slide ruler. A NVSlide path for
192
+ 2-D single-frame images is still a TODO (see `PLAN.md`).
193
+
194
+ ## Compatibility
195
+
196
+ - **OHIF**: `^3.12` (developed against 3.12.6; also exercised against OHIF
197
+ `master`/3.13-beta in the dev rig).
198
+ - **React**: `^18.3.1` (OHIF is on React 18 — this does not use `@niivue/nvreact`,
199
+ which targets React 19).
200
+ - **`@niivue/dcm2niix`**: required for DICOM only — see [DICOM support](#dicom-support).
201
+
202
+ ## Roadmap
203
+
204
+ See `PLAN.md`. Landed: NIfTI + DICOM rendering, a toolbar for views / clip plane /
205
+ overlay / window-level (both directions) / colormap, the full **Measurement** tool
206
+ group (see [Measurement tools](#measurement-tools)), and **NVSlide whole-slide (SM)
207
+ rendering** for JPEG / TILED_FULL slides (see [Whole-slide imaging](#whole-slide-imaging)).
208
+ Next: segmentation overlays, mesh/surface overlay, JPEG 2000 / TILED_SPARSE slide
209
+ support, and **NVSlide for 2-D** single-frame series (see the
210
+ `## TODO — NVSlide for 2D` section in `PLAN.md`).