Skip to content

Commit 249d8ca

Browse files
feat(graph): add afni.ipynb notebook example
Add the afni.ipynb notebook example, which includes the following changes: - add `Graph` traitlet class - add `nv.graph` - change `file_serializer` to accept null - add `pairedImgData` option to volumes
1 parent 2fe9f6b commit 249d8ca

File tree

7 files changed

+576
-13
lines changed

7 files changed

+576
-13
lines changed

examples/afni.ipynb

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "8ac54f8d-2d2c-4e57-a8a1-a1a1463b6451",
6+
"metadata": {},
7+
"source": [
8+
"# Import necessary modules"
9+
]
10+
},
11+
{
12+
"cell_type": "code",
13+
"execution_count": null,
14+
"id": "e57b7f74-2402-4580-a3c9-bd1dbb3b8cdc",
15+
"metadata": {},
16+
"outputs": [],
17+
"source": [
18+
"import asyncio\n",
19+
"import pathlib\n",
20+
"\n",
21+
"import ipywidgets as widgets\n",
22+
"from IPython.display import display\n",
23+
"\n",
24+
"import ipyniivue\n",
25+
"from ipyniivue import NiiVue, ShowRender, SliceType, download_dataset"
26+
]
27+
},
28+
{
29+
"cell_type": "markdown",
30+
"id": "b000ac63-b5b8-4258-b1be-e83868675c25",
31+
"metadata": {},
32+
"source": [
33+
"# Download required data"
34+
]
35+
},
36+
{
37+
"cell_type": "code",
38+
"execution_count": null,
39+
"id": "8f176e83",
40+
"metadata": {},
41+
"outputs": [],
42+
"source": [
43+
"DATA_FOLDER = pathlib.Path(ipyniivue.__file__).parent / \"images\"\n",
44+
"\n",
45+
"download_dataset(\n",
46+
" api_url=\"https://niivue.com/demos/images/\",\n",
47+
" dest_folder=DATA_FOLDER,\n",
48+
" files=[\n",
49+
" \"example4d+orig.HEAD\",\n",
50+
" \"example4d+orig.BRIK.gz\",\n",
51+
" ],\n",
52+
")"
53+
]
54+
},
55+
{
56+
"cell_type": "markdown",
57+
"id": "a3678fb5-65b1-4528-9a74-d7f27c62c8df",
58+
"metadata": {},
59+
"source": [
60+
"# Create the NiiVue widget"
61+
]
62+
},
63+
{
64+
"cell_type": "code",
65+
"execution_count": null,
66+
"id": "a873dc99-8c09-4518-a053-c6a4720aff8d",
67+
"metadata": {},
68+
"outputs": [],
69+
"source": [
70+
"nv = NiiVue()\n",
71+
"\n",
72+
"nv.set_radiological_convention(False)\n",
73+
"nv.set_slice_type(SliceType.MULTIPLANAR)\n",
74+
"nv.opts.multiplanar_show_render = ShowRender.ALWAYS\n",
75+
"\n",
76+
"# Configure graph values\n",
77+
"nv.graph.auto_size_multiplanar = True\n",
78+
"nv.graph.normalize_values = False\n",
79+
"nv.graph.opacity = 1.0\n",
80+
"\n",
81+
"# Load 4D volume with paired HEAD and BRIK files\n",
82+
"nv.load_volumes(\n",
83+
" [\n",
84+
" {\n",
85+
" \"path\": DATA_FOLDER / \"example4d+orig.HEAD\",\n",
86+
" \"paired_img_path\": DATA_FOLDER / \"example4d+orig.BRIK.gz\",\n",
87+
" \"colormap\": \"gray\",\n",
88+
" \"opacity\": 1.0,\n",
89+
" \"visible\": True,\n",
90+
" },\n",
91+
" ]\n",
92+
")"
93+
]
94+
},
95+
{
96+
"cell_type": "markdown",
97+
"id": "dd0e9aee-0bdb-4eba-829c-e6e5acd68835",
98+
"metadata": {},
99+
"source": [
100+
"# Create other buttons/checkboxes"
101+
]
102+
},
103+
{
104+
"cell_type": "code",
105+
"execution_count": null,
106+
"id": "3fd88e96-ca86-4b36-92eb-1e1f2d80f8ed",
107+
"metadata": {},
108+
"outputs": [],
109+
"source": [
110+
"display_frame = widgets.Label(value=\"Volume: 0\")\n",
111+
"\n",
112+
"normalize_checkbox = widgets.Checkbox(\n",
113+
" value=False,\n",
114+
" description=\"Normalize Graph\",\n",
115+
")\n",
116+
"\n",
117+
"prev_button = widgets.Button(description=\"Back\")\n",
118+
"next_button = widgets.Button(description=\"Forward\")"
119+
]
120+
},
121+
{
122+
"cell_type": "markdown",
123+
"id": "111a97d2-714e-47ae-81b8-80c44f6726a9",
124+
"metadata": {},
125+
"source": [
126+
"# Implement the callbacks"
127+
]
128+
},
129+
{
130+
"cell_type": "code",
131+
"execution_count": null,
132+
"id": "167128f8-a16a-43bd-803f-da10f08c0d4d",
133+
"metadata": {},
134+
"outputs": [],
135+
"source": [
136+
"def on_normalize_change(change):\n",
137+
" \"\"\"Normalize graph.\"\"\"\n",
138+
" nv.graph.normalize_values = change[\"new\"]\n",
139+
"\n",
140+
"\n",
141+
"normalize_checkbox.observe(on_normalize_change, names=\"value\")\n",
142+
"\n",
143+
"\n",
144+
"def on_prev_button_clicked(b):\n",
145+
" \"\"\"Decrement the frame index.\"\"\"\n",
146+
" if nv.volumes:\n",
147+
" current_frame = nv.volumes[0].frame4D\n",
148+
" new_frame = max(current_frame - 1, 0)\n",
149+
" nv.volumes[0].frame4D = new_frame\n",
150+
" display_frame.value = f\"Volume: {new_frame}\"\n",
151+
"\n",
152+
"\n",
153+
"def on_next_button_clicked(b):\n",
154+
" \"\"\"Increment the frame index.\"\"\"\n",
155+
" if nv.volumes:\n",
156+
" current_frame = nv.volumes[0].frame4D\n",
157+
" n_frames = nv.volumes[0].n_frame4D\n",
158+
" new_frame = min(current_frame + 1, n_frames - 1)\n",
159+
" nv.volumes[0].frame4D = new_frame\n",
160+
" display_frame.value = f\"Volume: {new_frame}\"\n",
161+
"\n",
162+
"\n",
163+
"prev_button.on_click(on_prev_button_clicked)\n",
164+
"next_button.on_click(on_next_button_clicked)"
165+
]
166+
},
167+
{
168+
"cell_type": "markdown",
169+
"id": "b310d908-6f11-4d66-83fa-117af002799e",
170+
"metadata": {},
171+
"source": [
172+
"# Create animate button"
173+
]
174+
},
175+
{
176+
"cell_type": "code",
177+
"execution_count": null,
178+
"id": "24726cb0-0d09-4e98-a16c-630d3b746cd3",
179+
"metadata": {},
180+
"outputs": [],
181+
"source": [
182+
"animate_button = widgets.Button(description=\"Animate\")\n",
183+
"\n",
184+
"animation_running = False\n",
185+
"animation_task = None\n",
186+
"\n",
187+
"\n",
188+
"async def animate_frames():\n",
189+
" \"\"\"Animation loop.\"\"\"\n",
190+
" global animation_running\n",
191+
" if not nv.volumes:\n",
192+
" return\n",
193+
" n_frames = nv.volumes[0].n_frame4D\n",
194+
" try:\n",
195+
" while animation_running:\n",
196+
" current_frame = nv.volumes[0].frame4D\n",
197+
" current_frame = (current_frame + 1) % n_frames\n",
198+
" nv.volumes[0].frame4D = current_frame\n",
199+
" display_frame.value = f\"Volume: {current_frame}\"\n",
200+
" await asyncio.sleep(0.1)\n",
201+
" except asyncio.CancelledError:\n",
202+
" pass\n",
203+
"\n",
204+
"\n",
205+
"def on_animate_button_clicked(b):\n",
206+
" \"\"\"Define 'Animate' button click handler.\"\"\"\n",
207+
" global animation_running, animation_task\n",
208+
" if not animation_running:\n",
209+
" # Start animation\n",
210+
" animation_running = True\n",
211+
" animate_button.description = \"Stop\"\n",
212+
" # Schedule the animation coroutine and store the future\n",
213+
" animation_task = asyncio.ensure_future(animate_frames())\n",
214+
" else:\n",
215+
" # Stop animation\n",
216+
" animation_running = False\n",
217+
" animate_button.description = \"Animate\"\n",
218+
" # Cancel the running task if it's active\n",
219+
" if animation_task is not None:\n",
220+
" animation_task.cancel()\n",
221+
" animation_task = None\n",
222+
"\n",
223+
"\n",
224+
"animate_button.on_click(on_animate_button_clicked)"
225+
]
226+
},
227+
{
228+
"cell_type": "markdown",
229+
"id": "5f6f5a15-6b36-4312-b735-10e185900c19",
230+
"metadata": {},
231+
"source": [
232+
"# Reset frame index on image loaded"
233+
]
234+
},
235+
{
236+
"cell_type": "code",
237+
"execution_count": null,
238+
"id": "71ba69a0-6fe0-48a7-a03b-29822e24d676",
239+
"metadata": {},
240+
"outputs": [],
241+
"source": [
242+
"@nv.on_image_loaded\n",
243+
"def update_number_of_frames(volume):\n",
244+
" \"\"\"Reset to first frame.\"\"\"\n",
245+
" nv.volumes[0].frame4D = 0\n",
246+
" display_frame.value = \"Volume: 0\""
247+
]
248+
},
249+
{
250+
"cell_type": "markdown",
251+
"id": "906aee0a-cb57-46b6-9808-a8ac5443a408",
252+
"metadata": {},
253+
"source": [
254+
"# Display all"
255+
]
256+
},
257+
{
258+
"cell_type": "code",
259+
"execution_count": null,
260+
"id": "44999f74-851a-40c7-aee1-84a65aeb69b0",
261+
"metadata": {},
262+
"outputs": [],
263+
"source": [
264+
"controls = widgets.HBox(\n",
265+
" [\n",
266+
" normalize_checkbox,\n",
267+
" prev_button,\n",
268+
" next_button,\n",
269+
" animate_button,\n",
270+
" ]\n",
271+
")\n",
272+
"\n",
273+
"display(widgets.VBox([controls, display_frame, nv]))"
274+
]
275+
}
276+
],
277+
"metadata": {},
278+
"nbformat": 4,
279+
"nbformat_minor": 5
280+
}

js/types.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,30 @@ type LUT = {
2424
labels?: string[];
2525
};
2626

27+
type Graph = {
28+
LTWH: number[];
29+
opacity: number;
30+
vols: number[];
31+
autoSizeMultiplanar: boolean;
32+
normalizeValues: boolean;
33+
isRangeCalMinMax: boolean;
34+
35+
plotLTWH?: number[];
36+
backColor?: number[];
37+
lineColor?: number[];
38+
textColor?: number[];
39+
lineThickness?: number;
40+
gridLineThickness?: number;
41+
lineAlpha?: number;
42+
lines?: number[][];
43+
selectedColumn?: number;
44+
lineRGB?: number[][];
45+
};
46+
2747
export type VolumeModel = AnyModel<{
2848
path: File;
2949
id: string;
50+
paired_img_path: File;
3051
name: string;
3152
colormap: string;
3253
opacity: number;
@@ -39,6 +60,7 @@ export type VolumeModel = AnyModel<{
3960
colormap_label: LUT;
4061

4162
colormap_invert: boolean;
63+
n_frame4D: number | null;
4264
}>;
4365

4466
export type MeshModel = AnyModel<{
@@ -92,6 +114,7 @@ export type Model = AnyModel<{
92114
draw_lut: LUT;
93115
draw_opacity: number;
94116
draw_fill_overwrites: boolean;
117+
graph: Graph;
95118
}>;
96119

97120
// Custom message datas

js/volume.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,20 @@ async function create_volume(
103103
const idx = nv.getVolumeIndexByID(vmodel.get("id"));
104104
volume = nv.volumes[idx];
105105
} else {
106+
let pairedImgData = null;
107+
const pairedImg = vmodel.get("paired_img_path").data;
108+
109+
if (pairedImg !== null) {
110+
pairedImgData = pairedImg.buffer as ArrayBuffer;
111+
}
112+
console.log("pairedImgData", pairedImgData);
113+
106114
volume = await niivue.NVImage.new(
107115
vmodel.get("path").data.buffer as ArrayBuffer, // dataBuffer
108116
vmodel.get("path").name, // name
109117
vmodel.get("colormap"), // colormap
110118
vmodel.get("opacity"), // opacity
111-
null, // pairedImgData
119+
pairedImgData, // pairedImgData
112120
vmodel.get("cal_min") ?? Number.NaN, // cal_min
113121
vmodel.get("cal_max") ?? Number.NaN, // cal_max
114122
true, // trustCalMinMax
@@ -136,6 +144,7 @@ async function create_volume(
136144

137145
vmodel.set("id", volume.id);
138146
vmodel.set("name", volume.name);
147+
vmodel.set("n_frame4D", volume.nFrame4D ?? null);
139148
vmodel.save_changes();
140149

141150
// Handle changes to the volume properties

js/widget.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ function attachModelEventHandlers(
9595
nv.drawFillOverwrites = model.get("draw_fill_overwrites");
9696
});
9797

98+
model.on("change:graph", () => {
99+
const graphData = model.get("graph");
100+
for (const [key, value] of Object.entries(graphData)) {
101+
// biome-ignore lint/suspicious/noExplicitAny: Update graph vals, only clear out old vals when needed
102+
(nv.graph as any)[key] = value;
103+
}
104+
nv.updateGLVolume();
105+
});
106+
98107
// Handle any message directions from the nv object.
99108
model.on(
100109
"msg:custom",
@@ -576,7 +585,8 @@ export default {
576585
model.off("change:clip_plane_depth_azi_elev");
577586
model.off("change:draw_lut");
578587
model.off("change:draw_opacity");
579-
model.off("change:change:draw_fill_overwrites");
588+
model.off("change:draw_fill_overwrites");
589+
model.off("change:graph");
580590
};
581591
},
582592
async render({ model, el }: { model: Model; el: HTMLElement }) {

0 commit comments

Comments
 (0)