MNutils 1.2.0__py3-none-any.whl
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.
- mnutils/GEExam.py +495 -0
- mnutils/GESeries.py +1578 -0
- mnutils/__init__.py +54 -0
- mnutils/_scripts.py +177 -0
- mnutils/fitting/AMARES.py +831 -0
- mnutils/fitting/__init__.py +5 -0
- mnutils/matlab/__init__.py +13 -0
- mnutils/matlab/setup_matlab.py +116 -0
- mnutils/plotting/__init__.py +38 -0
- mnutils/plotting/_widgets/__init__.py +5 -0
- mnutils/plotting/_widgets/_html.py +34 -0
- mnutils/plotting/_widgets/_shared/__init__.py +50 -0
- mnutils/plotting/_widgets/_shared/dom.js +124 -0
- mnutils/plotting/_widgets/_shared/theme.css +140 -0
- mnutils/plotting/_widgets/image_grid.css +202 -0
- mnutils/plotting/_widgets/image_grid.js +212 -0
- mnutils/plotting/_widgets/image_grid.py +86 -0
- mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
- mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
- mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
- mnutils/plotting/_widgets/slice_viewer.css +38 -0
- mnutils/plotting/_widgets/slice_viewer.js +41 -0
- mnutils/plotting/_widgets/slice_viewer.py +35 -0
- mnutils/plotting/images.py +1399 -0
- mnutils/plotting/saving.py +138 -0
- mnutils/plotting/spectra.py +357 -0
- mnutils/py.typed +0 -0
- mnutils/testing/__init__.py +3 -0
- mnutils/testing/_spectra.py +338 -0
- mnutils/testing/synthetic.py +373 -0
- mnutils/utils/__init__.py +25 -0
- mnutils/utils/data_loaders.py +218 -0
- mnutils/utils/file_helpers.py +708 -0
- mnutils/utils/images.py +202 -0
- mnutils/utils/nifti.py +379 -0
- mnutils/utils/spectra.py +149 -0
- mnutils-1.2.0.dist-info/METADATA +161 -0
- mnutils-1.2.0.dist-info/RECORD +41 -0
- mnutils-1.2.0.dist-info/WHEEL +4 -0
- mnutils-1.2.0.dist-info/entry_points.txt +7 -0
- mnutils-1.2.0.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
.mnutils-mrsi-inspector {
|
|
2
|
+
/* Both panels give their middle element exactly this height, which is what
|
|
3
|
+
makes the anatomical frame and the spectrum plot line up top and bottom.
|
|
4
|
+
Clamped rather than fixed so it still adapts to short viewports, but never
|
|
5
|
+
grows into a full-screen-tall plot on a large one. */
|
|
6
|
+
--mnu-mrsi-media-h: clamp(200px, 40vh, 380px);
|
|
7
|
+
|
|
8
|
+
/* Every slider in the widget -- the native slice input and both nouislider
|
|
9
|
+
ones -- is drawn from these two, so the tracks stay the same weight and
|
|
10
|
+
the handle offsets below can be derived rather than hand-tuned per
|
|
11
|
+
orientation.
|
|
12
|
+
|
|
13
|
+
Sized in `em` so the controls follow the host's type rather than a fixed
|
|
14
|
+
pixel target: the same 22px thumb that reads as comfortable in a VS Code
|
|
15
|
+
notebook (13px base) is oversized on the docs site (16px+). The clamps keep
|
|
16
|
+
it a real pointing target at the small end and stop it turning into a knob
|
|
17
|
+
at the large one. */
|
|
18
|
+
--mnu-mrsi-track: clamp(5px, 0.4em, 8px);
|
|
19
|
+
--mnu-mrsi-thumb: clamp(13px, 1.15em, 18px);
|
|
20
|
+
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
gap: 1em;
|
|
24
|
+
padding: 0.7em;
|
|
25
|
+
outline: none;
|
|
26
|
+
box-shadow: none;
|
|
27
|
+
border: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.mnutils-mrsi-left-panel,
|
|
31
|
+
.mnutils-mrsi-right-panel {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 0.4em;
|
|
35
|
+
min-width: 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* One third image, two thirds spectrum: the anatomical frame is a square the
|
|
39
|
+
reader only needs big enough to aim at, while the spectrum is where the
|
|
40
|
+
detail is. `flex-basis: 0` makes the 1:2 grow ratio govern the split
|
|
41
|
+
outright, instead of being applied to whatever is left over after two
|
|
42
|
+
different content-derived basis widths. `flex-wrap` on the container still
|
|
43
|
+
stacks them at narrow widths, once min-width kicks in.
|
|
44
|
+
|
|
45
|
+
The two min-widths also decide *when* the panels stop sharing a row: each is
|
|
46
|
+
the narrowest its panel still reads at -- a spectrum needs far more room than
|
|
47
|
+
a head, hence the asymmetry. Set this way the layout stacks while there is
|
|
48
|
+
still space to spare, instead of squeezing both until the ppm handles merge
|
|
49
|
+
and the slice readout is pushed out of its panel. Below their sum the widget
|
|
50
|
+
is one column and every control gets the full width.
|
|
51
|
+
|
|
52
|
+
Wrapped in `min(..., 100%)` so the floor never exceeds what the widget
|
|
53
|
+
actually has: on a phone the 480px panel is wider than the screen, and a
|
|
54
|
+
plain min-width would push the ppm bar off the side instead of stacking. */
|
|
55
|
+
.mnutils-mrsi-left-panel {
|
|
56
|
+
flex: 1 1 0;
|
|
57
|
+
min-width: min(320px, 100%);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mnutils-mrsi-right-panel {
|
|
61
|
+
flex: 2 1 0;
|
|
62
|
+
min-width: min(480px, 100%);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* The fixed-height row both panels hang their main content in. The image is
|
|
66
|
+
centred inside it rather than stretched: the frame keeps its aspect ratio
|
|
67
|
+
and the leftover width stays empty, which is cheaper to read than a squashed
|
|
68
|
+
head. */
|
|
69
|
+
.mnutils-mrsi-media {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
gap: 0.4em;
|
|
74
|
+
height: var(--mnu-mrsi-media-h);
|
|
75
|
+
min-height: 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Sized by the frame's own aspect ratio (`--mnu-mrsi-ar`, set inline from the
|
|
79
|
+
payload's image_width/image_height -- square for the usual T1 grid), so the
|
|
80
|
+
image is never distorted: the box is as tall as the media row allows, unless
|
|
81
|
+
that would make it wider than the panel, in which case the width caps and
|
|
82
|
+
the height follows it down. Setting `height: 100%` and capping `max-width`
|
|
83
|
+
instead is what squashed the frame -- the width shrank and the height didn't.
|
|
84
|
+
|
|
85
|
+
The wrapper also has to be exactly the image box, because the SVG overlay is
|
|
86
|
+
positioned against it (inset: 0); any letterboxing between the two would
|
|
87
|
+
offset the voxel outline from the voxel it marks. */
|
|
88
|
+
.mnutils-mrsi-image-wrap {
|
|
89
|
+
--mnu-mrsi-ar: 1;
|
|
90
|
+
|
|
91
|
+
position: relative;
|
|
92
|
+
flex: 0 0 auto;
|
|
93
|
+
width: min(100%, calc(var(--mnu-mrsi-media-h) * var(--mnu-mrsi-ar)));
|
|
94
|
+
aspect-ratio: var(--mnu-mrsi-ar);
|
|
95
|
+
background: var(--mnu-panel);
|
|
96
|
+
border: 1px solid var(--mnu-border);
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
box-sizing: border-box;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.mnutils-mrsi-image {
|
|
103
|
+
display: block;
|
|
104
|
+
width: 100%;
|
|
105
|
+
height: 100%;
|
|
106
|
+
cursor: crosshair;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Purely a drawing surface: the voxel outline sits on top of the frame, but
|
|
110
|
+
the click handler lives on the <img> underneath it. Without this the SVG --
|
|
111
|
+
a full-size absolutely positioned box, painted last -- wins every hit test
|
|
112
|
+
over the image and swallows the clicks the voxel picker exists to receive.
|
|
113
|
+
Inherited by the polygons, which have nothing to handle either. */
|
|
114
|
+
.mnutils-mrsi-overlay {
|
|
115
|
+
position: absolute;
|
|
116
|
+
inset: 0;
|
|
117
|
+
width: 100%;
|
|
118
|
+
height: 100%;
|
|
119
|
+
pointer-events: none;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Two stacked polygons, not one: over a T1 the box crosses both black
|
|
123
|
+
background and bright tissue, and a single accent stroke disappears into
|
|
124
|
+
whichever of the two happens to sit under it. The halo is drawn first
|
|
125
|
+
(wider, dark, semi-transparent) so the accent line always has contrast, and
|
|
126
|
+
a faint fill marks the selected cell without hiding the MRSI colour under
|
|
127
|
+
it. */
|
|
128
|
+
.mnutils-mrsi-voxel-box {
|
|
129
|
+
fill: color-mix(in srgb, var(--mnu-accent) 12%, transparent);
|
|
130
|
+
stroke: var(--mnu-accent);
|
|
131
|
+
stroke-width: 2;
|
|
132
|
+
stroke-linejoin: round;
|
|
133
|
+
vector-effect: non-scaling-stroke;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.mnutils-mrsi-voxel-box-halo {
|
|
137
|
+
fill: none;
|
|
138
|
+
stroke: rgba(0, 0, 0, 0.55);
|
|
139
|
+
stroke-width: 4;
|
|
140
|
+
stroke-linejoin: round;
|
|
141
|
+
vector-effect: non-scaling-stroke;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Fixed line box, not just centred text: the two titles are what sits above the
|
|
145
|
+
two media boxes, so they have to occupy the same height or the image and the
|
|
146
|
+
plot start at different y positions. */
|
|
147
|
+
.mnutils-mrsi-slice-title,
|
|
148
|
+
.mnutils-mrsi-spectrum-title {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
text-align: center;
|
|
153
|
+
font-size: 1.05em;
|
|
154
|
+
min-height: 2.2em;
|
|
155
|
+
line-height: 1.25;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.mnutils-mrsi-spectrum-container {
|
|
159
|
+
position: relative;
|
|
160
|
+
flex: 1 1 auto;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
height: 100%;
|
|
163
|
+
padding: 0.4em;
|
|
164
|
+
box-sizing: border-box;
|
|
165
|
+
background: var(--mnu-panel);
|
|
166
|
+
border: 1px solid var(--mnu-border);
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.mnutils-mrsi-spectrum-container > canvas {
|
|
171
|
+
display: block;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Both control rows read as one component: a bordered strip under the media
|
|
175
|
+
box, rather than a bare slider floating on the widget background. */
|
|
176
|
+
.mnutils-mrsi-control-bar {
|
|
177
|
+
gap: 0.7em;
|
|
178
|
+
padding: 0.45em 0.7em;
|
|
179
|
+
background: var(--mnu-panel);
|
|
180
|
+
border: 1px solid var(--mnu-border);
|
|
181
|
+
border-radius: 6px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.mnutils-mrsi-range-bar {
|
|
185
|
+
gap: 0.8em;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* The shared bar text is sized for the slice viewer's compact strip; next to a
|
|
189
|
+
full-height plot and thumbs this size it reads as fine print. Bumped here
|
|
190
|
+
rather than in theme.css so the other widgets keep their own proportions.
|
|
191
|
+
The readout also gets more room -- it carries two signed ppm values. */
|
|
192
|
+
.mnutils-mrsi-inspector .mnu-lbl,
|
|
193
|
+
.mnutils-mrsi-inspector .mnu-readout {
|
|
194
|
+
font-size: 0.95em;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Never the part that gives way when the bar tightens: the readout is the only
|
|
198
|
+
place the numeric value appears, so it holds its content width and the track
|
|
199
|
+
absorbs the loss. */
|
|
200
|
+
.mnutils-mrsi-inspector .mnu-lbl {
|
|
201
|
+
flex: 0 0 auto;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.mnutils-mrsi-inspector .mnu-readout {
|
|
205
|
+
flex: 0 0 auto;
|
|
206
|
+
min-width: 5.5em;
|
|
207
|
+
white-space: nowrap;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Native range inputs are styled per-browser and next to nouislider's flat
|
|
211
|
+
track the default one looks like a different control; match the two.
|
|
212
|
+
|
|
213
|
+
`min-width: 0` is what keeps the bar intact when the panel narrows: a range
|
|
214
|
+
input carries an intrinsic width (~129px in Chromium) that `flex: 1` alone
|
|
215
|
+
will not shrink past, so the row grew wider than its panel and shoved the
|
|
216
|
+
readout out of sight rather than giving up track. */
|
|
217
|
+
.mnutils-mrsi-inspector .mnu-slider {
|
|
218
|
+
-webkit-appearance: none;
|
|
219
|
+
appearance: none;
|
|
220
|
+
min-width: 0;
|
|
221
|
+
height: var(--mnu-mrsi-track);
|
|
222
|
+
border-radius: 999px;
|
|
223
|
+
background: var(--mnu-border);
|
|
224
|
+
cursor: pointer;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.mnutils-mrsi-inspector .mnu-slider:disabled {
|
|
228
|
+
opacity: 0.5;
|
|
229
|
+
cursor: default;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.mnutils-mrsi-inspector .mnu-slider::-webkit-slider-thumb {
|
|
233
|
+
-webkit-appearance: none;
|
|
234
|
+
appearance: none;
|
|
235
|
+
width: var(--mnu-mrsi-thumb);
|
|
236
|
+
height: var(--mnu-mrsi-thumb);
|
|
237
|
+
border: 1px solid var(--mnu-border);
|
|
238
|
+
border-radius: 50%;
|
|
239
|
+
background: var(--mnu-accent);
|
|
240
|
+
cursor: grab;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.mnutils-mrsi-inspector .mnu-slider::-moz-range-thumb {
|
|
244
|
+
width: var(--mnu-mrsi-thumb);
|
|
245
|
+
height: var(--mnu-mrsi-thumb);
|
|
246
|
+
border: 1px solid var(--mnu-border);
|
|
247
|
+
border-radius: 50%;
|
|
248
|
+
background: var(--mnu-accent);
|
|
249
|
+
cursor: grab;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* Sits under the ppm bar as a secondary control, so keep it visually
|
|
253
|
+
subordinate to it: a self-shrinking pill rather than a full-width row, muted
|
|
254
|
+
until hovered or checked. */
|
|
255
|
+
.mnutils-mrsi-ylim-toggle {
|
|
256
|
+
display: inline-flex;
|
|
257
|
+
align-self: center;
|
|
258
|
+
align-items: center;
|
|
259
|
+
gap: 0.45em;
|
|
260
|
+
margin-top: 0.1em;
|
|
261
|
+
padding: 0.35em 0.8em;
|
|
262
|
+
border: 1px solid var(--mnu-border);
|
|
263
|
+
border-radius: 999px;
|
|
264
|
+
background: var(--mnu-panel);
|
|
265
|
+
font-size: 0.95em;
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
color: var(--mnu-label);
|
|
268
|
+
line-height: 1.4;
|
|
269
|
+
opacity: 0.75;
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
user-select: none;
|
|
272
|
+
transition:
|
|
273
|
+
opacity 0.12s ease,
|
|
274
|
+
border-color 0.12s ease;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.mnutils-mrsi-ylim-toggle:hover,
|
|
278
|
+
.mnutils-mrsi-ylim-toggle:focus-within {
|
|
279
|
+
opacity: 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.mnutils-mrsi-ylim-toggle:has(input:checked) {
|
|
283
|
+
opacity: 1;
|
|
284
|
+
border-color: var(--mnu-accent);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.mnutils-mrsi-ylim-toggle input {
|
|
288
|
+
width: 1.1em;
|
|
289
|
+
height: 1.1em;
|
|
290
|
+
accent-color: var(--mnu-accent);
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
margin: 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Theme nouislider (the ppm dual-range and the y-limit one) to match the rest
|
|
296
|
+
of the widget's accent/border tokens instead of its default blue skin. */
|
|
297
|
+
.mnutils-mrsi-slider.noUi-target {
|
|
298
|
+
background: var(--mnu-border);
|
|
299
|
+
border: none;
|
|
300
|
+
box-shadow: none;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.mnutils-mrsi-slider .noUi-connect {
|
|
304
|
+
background: var(--mnu-accent);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.mnutils-mrsi-slider .noUi-handle {
|
|
308
|
+
width: var(--mnu-mrsi-thumb);
|
|
309
|
+
height: var(--mnu-mrsi-thumb);
|
|
310
|
+
border-color: var(--mnu-border);
|
|
311
|
+
border-radius: 50%;
|
|
312
|
+
box-shadow: none;
|
|
313
|
+
background: var(--mnu-accent);
|
|
314
|
+
cursor: grab;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Centre the handle on the track, whichever way the track runs: the cross-axis
|
|
318
|
+
offset is half the difference between the two, the along-axis one half a
|
|
319
|
+
handle. Both derive from the size tokens, so resizing the target needs no
|
|
320
|
+
re-tuning here. nouislider stamps the orientation class on the target itself.
|
|
321
|
+
|
|
322
|
+
The two orientations anchor the handle to opposite edges of its origin
|
|
323
|
+
element -- `right` + `top` horizontally, `right` + *`bottom`* vertically --
|
|
324
|
+
because a vertical origin marks its value at its bottom edge. Overriding
|
|
325
|
+
`top` on a vertical handle therefore doesn't nudge it, it re-anchors it to
|
|
326
|
+
the far end of the track: both handles ended up a full track-length from the
|
|
327
|
+
values they stood for, off the side of the panel. Reset the edge the
|
|
328
|
+
stylesheet used, don't just add to it. */
|
|
329
|
+
.mnutils-mrsi-slider.noUi-horizontal .noUi-handle {
|
|
330
|
+
top: calc((var(--mnu-mrsi-track) - var(--mnu-mrsi-thumb)) / 2);
|
|
331
|
+
bottom: auto;
|
|
332
|
+
right: calc(var(--mnu-mrsi-thumb) / -2);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.mnutils-mrsi-slider.noUi-vertical .noUi-handle {
|
|
336
|
+
top: auto;
|
|
337
|
+
bottom: calc(var(--mnu-mrsi-thumb) / -2);
|
|
338
|
+
right: calc((var(--mnu-mrsi-track) - var(--mnu-mrsi-thumb)) / 2);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.mnutils-mrsi-slider .noUi-handle:focus-visible {
|
|
342
|
+
outline: 2px solid var(--mnu-focus-ring);
|
|
343
|
+
outline-offset: 2px;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.mnutils-mrsi-slider .noUi-active {
|
|
347
|
+
background: var(--mnu-accent-strong);
|
|
348
|
+
cursor: grabbing;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.mnutils-mrsi-slider .noUi-handle::before,
|
|
352
|
+
.mnutils-mrsi-slider .noUi-handle::after {
|
|
353
|
+
display: none;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.mnutils-mrsi-ppm-slider {
|
|
357
|
+
flex: 1;
|
|
358
|
+
height: var(--mnu-mrsi-track);
|
|
359
|
+
margin: 0 0.6em;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Sits inside the media row, left of the chart, and is sized off the row's own
|
|
363
|
+
height rather than stretched: nouislider needs a resolved height at create
|
|
364
|
+
time, and the 3em trimmed off the bottom is roughly the strip Chart.js gives
|
|
365
|
+
the x axis and its title, which keeps the track spanning the plot area
|
|
366
|
+
rather than the whole canvas. */
|
|
367
|
+
.mnutils-mrsi-slider.mnutils-mrsi-ylim-slider {
|
|
368
|
+
flex: 0 0 auto;
|
|
369
|
+
width: var(--mnu-mrsi-track);
|
|
370
|
+
height: calc(var(--mnu-mrsi-media-h) - 3em);
|
|
371
|
+
align-self: flex-start;
|
|
372
|
+
/* Room for the thumb, which overhangs the track by half its width on every
|
|
373
|
+
side: without it the handles graze the plot panel and the topmost one
|
|
374
|
+
clips out of the media row. */
|
|
375
|
+
margin: 0.9em 0.5em 0 0.4em;
|
|
376
|
+
}
|