Skip to content

Commit 94a4f64

Browse files
authored
Merge pull request #2 from noahbenson/nyu-rebase
Nyu rebase
2 parents feb8641 + c29f802 commit 94a4f64

File tree

8 files changed

+469
-64
lines changed

8 files changed

+469
-64
lines changed

Dockerfile

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# Configuration ################################################################
1111

1212
# Start with the python 3.10 Jupyter scipy-notebook docker-image.
13-
FROM jupyter/scipy-notebook:python-3.10
13+
FROM jupyter/scipy-notebook:x86_64-python-3.10
1414
# Note the Maintainer.
1515
MAINTAINER Noah C. Benson <nben@uw.edu>
1616

@@ -78,19 +78,19 @@ RUN curl -L -o /data/required_subjects/fsaverage_sym.tar.gz \
7878
USER $NB_USER
7979
# Install some stuff we are likely to need, including neuropythy.
8080

81-
# The following line that installs jupyter_contrib_nbextensions is a
82-
# bug workaround; it should be replaced by just this line once the
83-
# bug is fixed:
84-
# && conda install -y -cconda-forge ipywidgets pip jupyter_contrib_nbextensions
85-
RUN conda update -y -n base conda \
86-
&& conda install -y nibabel s3fs \
87-
&& conda install -y -cconda-forge ipywidgets pip 'jupyter_contrib_nbextensions < 0.7' 'traitlets = 5.9.0' \
88-
&& pip install --upgrade setuptools \
89-
&& pip install ipycanvas pyyaml neuropythy
81+
RUN mamba update -y -n base mamba
82+
RUN mamba update --all -y
83+
#RUN mamba install -y -cconda-forge nibabel s3fs
84+
RUN mamba install -y -cconda-forge \
85+
ipywidgets pip traitlets webcolors jsonschema-with-format-nongpl
86+
RUN pip install --upgrade setuptools
87+
RUN pip install ipycanvas pyyaml neuropythy nibabel s3fs
88+
RUN pip install diplib
9089
# Install collapsible cell extensions...
91-
RUN jupyter contrib nbextension install --user \
92-
&& jupyter nbextension enable collapsible_headings/main \
93-
&& jupyter nbextension enable select_keymap/main
90+
#RUN mamba install -cconda-forge jupyter_contrib_nbextensions \
91+
# && jupyter contrib nbextension install --user \
92+
# && jupyter nbextension enable collapsible_headings/main \
93+
# && jupyter nbextension enable select_keymap/main
9494
RUN mkdir -p /home/$NB_USER/.jupyter/custom
9595
# Copy the config directory's requirements over and install them.
9696
COPY config/requirements.txt /build/

config/config.yaml

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,12 @@ targets:
112112
# from the sections above.
113113
cortex: |
114114
# Extract the subject object and the hsmisphere name.
115+
sid = target['Subject ID']
115116
sub = target['subject']
116117
h = target['Hemisphere'].lower()
117118
# Load retinotopic mapping data from the label directory, where these data
118119
# are stored on the NSD repository.
119-
label_path = nsd_path.subpath('freesurfer', sub, 'label')
120+
label_path = nsd_path.subpath('freesurfer', sid, 'label')
120121
props = {
121122
k: ny.load(label_path.subpath(f'{h}.{filename}'))
122123
for (k,filename) in nsd_prf_files.items()}
@@ -284,3 +285,65 @@ figures:
284285
cmap='hot', vmin=0, vmax=100)
285286
curvature: |
286287
ny.cortex_plot(target['flatmap'], axes=axes)
288+
289+
review: |
290+
#from annotate import watershed_contours
291+
#im = watershed_contours(annotations.values(), max_depth=1)
292+
#axes.imshow(im, cmap='hsv', vmin=0, vmax=1.5*np.max(im))
293+
import numpy as np
294+
# V1:
295+
v1uvm = annotations['V1 UVM (ventral)']
296+
v1lvm = annotations['V1 LVM (dorsal)']
297+
v1per = annotations['V1 Periphery']
298+
v1pol = np.vstack([v1uvm, v1per, np.flipud(v1lvm)])
299+
# V2:
300+
v2uvm = annotations['V2 UVM (ventral)']
301+
v2lvm = annotations['V2 LVM (dorsal)']
302+
v2pev = annotations['V2 Ventral Periphery']
303+
v2ped = annotations['V2 Dorsal Periphery']
304+
v2pol = np.vstack(
305+
[v2uvm, v2pev, np.flipud(v1uvm), v1lvm, np.flipud(v2ped), np.flipud(v2lvm)])
306+
# V3:
307+
v3uvm = annotations['V3 UVM (ventral)']
308+
v3lvm = annotations['V3 LVM (dorsal)']
309+
v3pev = annotations['V3 Ventral Periphery']
310+
v3ped = annotations['V3 Dorsal Periphery']
311+
v3pol = np.vstack(
312+
[v3uvm, v3pev, np.flipud(v2uvm), v2lvm, np.flipud(v3ped), np.flipud(v3lvm)])
313+
# Turn these into traces:
314+
fmap = target['flatmap']
315+
v1_trace = ny.path_trace(fmap, v1pol.T, closed=True)
316+
v2_trace = ny.path_trace(fmap, v2pol.T, closed=True)
317+
v3_trace = ny.path_trace(fmap, v3pol.T, closed=True)
318+
# Convert the path traces into paths then into labels:
319+
cortex = target['cortex']
320+
v1_path = v1_trace.to_path(cortex)
321+
v2_path = v2_trace.to_path(cortex)
322+
v3_path = v3_trace.to_path(cortex)
323+
v1 = v1_path.label > 0.5
324+
v2 = v2_path.label > 0.5
325+
v3 = v3_path.label > 0.5
326+
if np.sum(v1) > np.sum(~v1):
327+
v1 = ~v1
328+
if np.sum(v2) > np.sum(~v2):
329+
v2 = ~v2
330+
if np.sum(v3) > np.sum(~v3):
331+
v3 = ~v3
332+
labels = np.zeros(cortex.vertex_count, dtype=int)
333+
labels[v1] = 1
334+
labels[v2] = 2
335+
labels[v3] = 3
336+
fmap_labels = labels[fmap.labels]
337+
# If the user saves the contours, we want to save these labels.
338+
save_hooks['v123-labels.mgz'] = lambda filename: ny.save(filename, labels)
339+
# Plot the results:
340+
ny.cortex_plot(
341+
fmap,
342+
color=fmap_labels,
343+
cmap='rainbow',
344+
mask=(fmap_labels > 0),
345+
axes=axes,
346+
alpha=0.5)
347+
axes.axis('equal')
348+
axes.axis('off')
349+

src/annotate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
# Imports ######################################################################
2020

21-
from ._util import (delay, ldict)
21+
from ._util import (delay, ldict, watershed_contours)
2222
from ._core import AnnotationTool
2323

2424

src/annotate/_config.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,50 @@ def __init__(self, yaml, init, all_figures):
511511
init)
512512
# Same these into the dictionary.
513513
self.update(res)
514+
class ReviewConfig:
515+
"""An object that stores the configuration of the review panel.
516+
517+
The `ReviewConfig` type stores information from the `review` section of the
518+
`config.yaml` file for the `cortex-annotate` project. This section stores
519+
only a function that generates the figure to plot in the `Review` tab of the
520+
display panel. The review function requires the arguments `target`,
521+
`annotations`, `figure`, and `axes`, and it must draw the desired graphics
522+
on the given matplotlib axes. The `annotations` argument is a dictionary
523+
whose keys are the annotation names and whose values are the drawn
524+
annotation. Annotations may be missing if the user opens the review tab
525+
before completing the annotations. If an error is raised from this function,
526+
then the error message is printed to the display.
527+
"""
528+
__slots__ = ('code', 'function', 'figsize', 'dpi')
529+
@staticmethod
530+
def _compile(code, initcfg):
531+
return _compile_fn(
532+
"target, annotations, figure, axes, save_hooks",
533+
f"{code}\n",
534+
initcfg)
535+
def __init__(self, yaml, init):
536+
if yaml is None:
537+
self.code = None
538+
self.function = None
539+
self.figsize = None
540+
self.dpi = None
541+
return
542+
elif isinstance(yaml, str):
543+
yaml = {'function': yaml}
544+
elif not isinstance(yaml, dict):
545+
raise ConfigError(
546+
"review",
547+
"review section must contain a Python code string or a dict",
548+
yaml)
549+
self.code = yaml.get('function')
550+
if self.code is None:
551+
raise ConfigError(
552+
"review",
553+
"review section must contain the key function",
554+
yaml)
555+
self.figsize = yaml.get('figsize', (3,3))
556+
self.dpi = yaml.get('dpi', 256)
557+
self.function = ReviewConfig._compile(self.code, init)
514558
class Config:
515559
"""The configuration object for the `cortex-annotate` project.
516560
@@ -521,8 +565,9 @@ class Config:
521565
not recognized by `Config` are not parsed, but they are available in the
522566
`Config.yaml` member variable.
523567
"""
524-
__slots__ = ('config_path', 'yaml', 'display', 'init', 'targets', 'figures',
525-
'annotations', 'builtin_annotations', 'annotation_types')
568+
__slots__ = (
569+
'config_path', 'yaml', 'display', 'init', 'targets', 'figures',
570+
'annotations', 'builtin_annotations', 'review', 'annotation_types')
526571
def __init__(self, config_path='/config/config.yaml'):
527572
self.config_path = config_path
528573
with open(config_path, 'rt') as f:
@@ -534,15 +579,20 @@ def __init__(self, config_path='/config/config.yaml'):
534579
# Parse the targets section.
535580
self.targets = TargetsConfig(self.yaml.get('targets', None), self.init)
536581
# Parse the annotations section.
537-
self.annotations = AnnotationsConfig(self.yaml.get('annotations', None),
538-
self.init)
582+
self.annotations = AnnotationsConfig(
583+
self.yaml.get('annotations', None),
584+
self.init)
539585
# Parse the builtin_annotations section.
540586
self.builtin_annotations = BuiltinAnnotationsConfig(
541587
self.yaml.get('builtin_annotations', None),
542588
self.init)
543589
# Parse the figures section.
544-
self.figures = FiguresConfig(self.yaml.get('figures', None), self.init,
545-
self.annotations.all_figures)
590+
self.figures = FiguresConfig(
591+
self.yaml.get('figures', None),
592+
self.init,
593+
self.annotations.all_figures)
594+
# Parse the review section.
595+
self.review = ReviewConfig(self.yaml.get('review', None), self.init)
546596
# Make the annotation types dictionary.
547597
d = self.annotations.types.copy()
548598
d.update(self.builtin_annotations.types)

src/annotate/_control.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,11 +333,31 @@ def __init__(self, state,
333333
self.imagesize_slider = self._make_imagesize_slider(imagesize)
334334
self.selection_panel = SelectionPanel(state)
335335
self.style_panel = StylePanel(state)
336+
self.review_button = ipw.Button(
337+
description='Review',
338+
button_style='',
339+
tooltip='Review the annotations.')
336340
self.save_button = ipw.Button(
337341
description='Save',
338342
button_style='',
339-
tooltip='Save all annotations and preferences.',
340-
layout={'margin':"3% 33% 3% 33%", "width": "34%"})
343+
tooltip='Save all annotations and preferences.')
344+
self.edit_button = ipw.Button(
345+
description='Edit',
346+
button_style='',
347+
tooltip='Continue editing annotation.')
348+
if state.config.review.function is not None:
349+
buttons = [self.review_button, self.save_button, self.edit_button]
350+
self.review_button.disabled = False
351+
self.save_button.disabled = True
352+
self.edit_button.disabled = True
353+
layout = {'margin':"3% 3% 3% 3%", "width": "94%"}
354+
else:
355+
buttons = [self.save_button]
356+
self.review_button.disabled = True
357+
self.save_button.disabled = False
358+
self.edit_button.disabled = True
359+
layout = {'margin':"3% 33% 3% 33%", "width": "34%"}
360+
self.button_box = ipw.HBox(buttons, layout=layout)
341361
self.info_message = self._make_infomsg()
342362
hline = self._make_hline()
343363
self.vbox_children = [
@@ -348,7 +368,7 @@ def __init__(self, state,
348368
hline,
349369
self.style_panel,
350370
hline,
351-
self.save_button,
371+
self.button_box,
352372
hline,
353373
self.info_message]
354374
vbox_layout = {'width': '250px'}
@@ -433,10 +453,24 @@ def observe_imagesize(self, fn):
433453
def observe_save(self, fn):
434454
"""Registers the argument to be called when the save button is clicked.
435455
436-
The function is called with a single argument, which is the save buttom
456+
The function is called with a single argument, which is the save button
437457
instance.
438458
"""
439459
self.save_button.on_click(fn)
460+
def observe_review(self, fn):
461+
"""Registers the argument to be called when the save button is clicked.
462+
463+
The function is called with a single argument, which is the review
464+
button instance.
465+
"""
466+
self.review_button.on_click(fn)
467+
def observe_edit(self, fn):
468+
"""Registers the argument to be called when the edit button is clicked.
469+
470+
The function is called with a single argument, which is the edit button
471+
instance.
472+
"""
473+
self.edit_button.on_click(fn)
440474
@property
441475
def target(self):
442476
"""Compute the current target selection."""

0 commit comments

Comments
 (0)