From 9545730a40fbd6786ffbc53afd774ead2c64f2f4 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Tue, 12 Sep 2023 15:45:10 +0100 Subject: [PATCH 1/9] change atlas imports --- brainbox/atlas.py | 2 +- brainbox/ephys_plots.py | 2 +- .../best_available_channels_from_insertion_id.py | 2 +- brainbox/examples/docs_load_spike_sorting.py | 2 +- brainbox/examples/plot_atlas_color_values.py | 2 +- brainbox/io/one.py | 13 +++++++------ examples/ephys/example_amp_depth_scatter.py | 2 +- .../example_single_cluster_stim_aligned_activity.py | 2 +- examples/ephys/example_stim_aligned_over_depth.py | 2 +- .../loading_multi_photon_imaging_data.ipynb | 2 +- examples/loading_data/loading_passive_data.ipynb | 2 +- examples/loading_data/loading_spike_waveforms.ipynb | 2 +- .../loading_data/loading_spikesorting_data.ipynb | 2 +- .../one/ephys/docs_get_cluster_brain_locations.py | 2 +- examples/one/histology/brain_regions_navigation.py | 2 +- examples/one/histology/coverage_map.py | 2 +- .../histology/docs_find_dist_neighbouring_region.py | 2 +- .../one/histology/docs_find_nearby_trajectories.py | 2 +- .../one/histology/docs_find_previous_alignments.py | 2 +- .../docs_visualization3D_subject_channels.py | 2 +- .../docs_visualize_session_coronal_tilted.py | 2 +- .../one/histology/register_lasagna_tracks_alyx.py | 2 +- .../visualization3D_alyx_traj_planned_histology.py | 2 +- .../one/histology/visualization3D_repeated_site.py | 2 +- ...isualization3D_rotating_gif_firstpassmap_plan.py | 2 +- .../visualization3D_rotating_gif_selectedmice.py | 2 +- .../histology/visualization3D_subject_histology.py | 2 +- .../histology/visualize_alyx_channels_coronal.py | 2 +- .../visualize_alyx_traj_coronal_sagittal_raster.py | 2 +- .../visualize_track_file_coronal_GUIoption.py | 2 +- .../visualize_track_file_coronal_sagittal_slice.py | 2 +- ibllib/pipes/ephys_alignment.py | 2 +- ibllib/pipes/histology.py | 4 ++-- ibllib/pipes/mesoscope_tasks.py | 2 +- ibllib/pipes/widefield_tasks.py | 2 +- ibllib/plots/snapshot.py | 6 +++--- ibllib/qc/alignment_qc.py | 2 +- ibllib/tests/qc/test_alignment_qc.py | 2 +- ibllib/tests/test_histology.py | 2 +- 39 files changed, 48 insertions(+), 47 deletions(-) diff --git a/brainbox/atlas.py b/brainbox/atlas.py index 3c10ebdf1..a28718b37 100644 --- a/brainbox/atlas.py +++ b/brainbox/atlas.py @@ -7,7 +7,7 @@ import numpy as np import seaborn as sns import matplotlib.pyplot as plt -from ibllib import atlas +from ibatlas import atlas def _label2values(imlabel, fill_values, ba): diff --git a/brainbox/ephys_plots.py b/brainbox/ephys_plots.py index 56ec34b40..66075cd26 100644 --- a/brainbox/ephys_plots.py +++ b/brainbox/ephys_plots.py @@ -5,7 +5,7 @@ plot_image, plot_probe, plot_scatter, arrange_channels2banks) from brainbox.processing import compute_cluster_average from iblutil.numerical import bincount2D -from ibllib.atlas.regions import BrainRegions +from iblatlas.regions import BrainRegions def image_lfp_spectrum_plot(lfp_power, lfp_freq, chn_coords=None, chn_inds=None, freq_range=(0, 300), diff --git a/brainbox/examples/best_available_channels_from_insertion_id.py b/brainbox/examples/best_available_channels_from_insertion_id.py index 390e9d41f..023aca30c 100644 --- a/brainbox/examples/best_available_channels_from_insertion_id.py +++ b/brainbox/examples/best_available_channels_from_insertion_id.py @@ -1,6 +1,6 @@ from one.api import ONE -from ibllib.atlas import atlas +from iblatlas import atlas from brainbox.io.one import load_channels_from_insertion pid = "8413c5c6-b42b-4ec6-b751-881a54413628" diff --git a/brainbox/examples/docs_load_spike_sorting.py b/brainbox/examples/docs_load_spike_sorting.py index 36611c79c..8a94ba0ad 100644 --- a/brainbox/examples/docs_load_spike_sorting.py +++ b/brainbox/examples/docs_load_spike_sorting.py @@ -14,7 +14,7 @@ """ from one.api import ONE -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas from brainbox.io.one import SpikeSortingLoader diff --git a/brainbox/examples/plot_atlas_color_values.py b/brainbox/examples/plot_atlas_color_values.py index 61758aa7f..a62d7a31d 100755 --- a/brainbox/examples/plot_atlas_color_values.py +++ b/brainbox/examples/plot_atlas_color_values.py @@ -1,6 +1,6 @@ import numpy as np import matplotlib.pyplot as plt -from ibllib import atlas +from iblatlas import atlas from brainbox.atlas import plot_atlas diff --git a/brainbox/io/one.py b/brainbox/io/one.py index b63faebbe..e11896198 100644 --- a/brainbox/io/one.py +++ b/brainbox/io/one.py @@ -21,7 +21,8 @@ from iblutil.util import Bunch from ibllib.io.extractors.training_wheel import extract_wheel_moves, extract_first_movement_times -from ibllib.atlas import atlas, AllenAtlas, BrainRegions +from iblatlas.atlas import AllenAtlas, BrainRegions +from iblatlas import atlas from ibllib.pipes import histology from ibllib.pipes.ephys_alignment import EphysAlignment from ibllib.plots import vertical_lines @@ -243,7 +244,7 @@ def channel_locations_interpolation(channels_aligned, channels=None, brain_regio 'x', 'y', 'z', 'acronym', 'axial_um' those are the guide for the interpolation :param channels: Bunch or dictionary of aligned channels containing at least keys 'localCoordinates' - :param brain_regions: None (default) or ibllib.atlas.BrainRegions object + :param brain_regions: None (default) or iblatlas.regions.BrainRegions object if None will return a dict with keys 'localCoordinates', 'mlapdv', 'brainLocationIds_ccf_2017 if a brain region object is provided, outputts a dict with keys 'x', 'y', 'z', 'acronym', 'atlas_id', 'axial_um', 'lateral_um' @@ -372,7 +373,7 @@ def load_channel_locations(eid, probe=None, one=None, aligned=False, brain_atlas An instance of ONE (shouldn't be in 'local' mode) aligned : bool Whether to get the latest user aligned channel when not resolved or use histology track - brain_atlas : ibllib.atlas.BrainAtlas + brain_atlas : iblatlas.BrainAtlas Brain atlas object (default: Allen atlas) Returns ------- @@ -417,7 +418,7 @@ def load_spike_sorting_fast(eid, one=None, probe=None, dataset_types=None, spike :param dataset_types: additional spikes/clusters objects to add to the standard default list :param spike_sorter: name of the spike sorting you want to load (None for default) :param collection: name of the spike sorting collection to load - exclusive with spike sorter name ex: "alf/probe00" - :param brain_regions: ibllib.atlas.regions.BrainRegions object - will label acronyms if provided + :param brain_regions: iblatlas.regions.BrainRegions object - will label acronyms if provided :param nested: if a single probe is required, do not output a dictionary with the probe name as key :param return_collection: (False) if True, will return the collection used to load :return: spikes, clusters, channels (dict of bunch, 1 bunch per probe) @@ -458,7 +459,7 @@ def load_spike_sorting(eid, one=None, probe=None, dataset_types=None, spike_sort :param probe: name of probe to load in, if not given all probes for session will be loaded :param dataset_types: additional spikes/clusters objects to add to the standard default list :param spike_sorter: name of the spike sorting you want to load (None for default) - :param brain_regions: ibllib.atlas.regions.BrainRegions object - will label acronyms if provided + :param brain_regions: iblatlas.regions.BrainRegions object - will label acronyms if provided :param return_collection:(bool - False) if True, returns the collection for loading the data :return: spikes, clusters (dict of bunch, 1 bunch per probe) """ @@ -497,7 +498,7 @@ def load_spike_sorting_with_channel(eid, one=None, probe=None, aligned=False, da spike_sorter : str Name of the spike sorting you want to load (None for default which is pykilosort if it's available otherwise the default MATLAB kilosort) - brain_atlas : ibllib.atlas.BrainAtlas + brain_atlas : iblatlas.atlas.BrainAtlas Brain atlas object (default: Allen atlas) return_collection: bool Returns an extra argument with the collection chosen diff --git a/examples/ephys/example_amp_depth_scatter.py b/examples/ephys/example_amp_depth_scatter.py index 89d08e34f..0fd79ef1b 100644 --- a/examples/ephys/example_amp_depth_scatter.py +++ b/examples/ephys/example_amp_depth_scatter.py @@ -6,7 +6,7 @@ from one.api import ONE from brainbox.io.one import SpikeSortingLoader -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas import matplotlib.pyplot as plt import numpy as np diff --git a/examples/ephys/example_single_cluster_stim_aligned_activity.py b/examples/ephys/example_single_cluster_stim_aligned_activity.py index 2628d3f65..5bfeb085e 100644 --- a/examples/ephys/example_single_cluster_stim_aligned_activity.py +++ b/examples/ephys/example_single_cluster_stim_aligned_activity.py @@ -6,7 +6,7 @@ from one.api import ONE from brainbox.io.one import SpikeSortingLoader -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas import matplotlib.pyplot as plt import numpy as np diff --git a/examples/ephys/example_stim_aligned_over_depth.py b/examples/ephys/example_stim_aligned_over_depth.py index 0ded0bd3b..0da94a237 100644 --- a/examples/ephys/example_stim_aligned_over_depth.py +++ b/examples/ephys/example_stim_aligned_over_depth.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt from one.api import ONE -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas from brainbox.io.one import SpikeSortingLoader from brainbox.task.passive import get_stim_aligned_activity as stim_aligned_activity_over_depth diff --git a/examples/loading_data/loading_multi_photon_imaging_data.ipynb b/examples/loading_data/loading_multi_photon_imaging_data.ipynb index d2bb08a1a..d80aa5452 100644 --- a/examples/loading_data/loading_multi_photon_imaging_data.ipynb +++ b/examples/loading_data/loading_multi_photon_imaging_data.ipynb @@ -134,7 +134,7 @@ " print('%s (%s: %i)' % (area['name'], area['acronym'], area['id']))\n", "\n", "\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "atlas = AllenAtlas()\n", "\n", "# Interconvert ID and acronym\n", diff --git a/examples/loading_data/loading_passive_data.ipynb b/examples/loading_data/loading_passive_data.ipynb index f27ae5af9..8c49a186b 100644 --- a/examples/loading_data/loading_passive_data.ipynb +++ b/examples/loading_data/loading_passive_data.ipynb @@ -176,7 +176,7 @@ "pid = one.alyx.rest('insertions', 'list', session=eid)[0]['id']\n", "\n", "from brainbox.io.one import SpikeSortingLoader\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "import numpy as np\n", "ba = AllenAtlas()\n", "\n", diff --git a/examples/loading_data/loading_spike_waveforms.ipynb b/examples/loading_data/loading_spike_waveforms.ipynb index 6fa32b654..7009b7855 100644 --- a/examples/loading_data/loading_spike_waveforms.ipynb +++ b/examples/loading_data/loading_spike_waveforms.ipynb @@ -57,7 +57,7 @@ "source": [ "from one.api import ONE\n", "from brainbox.io.one import SpikeSortingLoader\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "\n", "one = ONE()\n", "ba = AllenAtlas()\n", diff --git a/examples/loading_data/loading_spikesorting_data.ipynb b/examples/loading_data/loading_spikesorting_data.ipynb index d765642d0..5f22cc3d2 100644 --- a/examples/loading_data/loading_spikesorting_data.ipynb +++ b/examples/loading_data/loading_spikesorting_data.ipynb @@ -59,7 +59,7 @@ "source": [ "from one.api import ONE\n", "from brainbox.io.one import SpikeSortingLoader\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "\n", "one = ONE()\n", "ba = AllenAtlas()\n", diff --git a/examples/one/ephys/docs_get_cluster_brain_locations.py b/examples/one/ephys/docs_get_cluster_brain_locations.py index b86e6f5b4..8ca1cdbb2 100644 --- a/examples/one/ephys/docs_get_cluster_brain_locations.py +++ b/examples/one/ephys/docs_get_cluster_brain_locations.py @@ -13,7 +13,7 @@ from one.api import ONE import numpy as np import matplotlib.pyplot as plt -from ibllib.atlas import BrainRegions +from iblatlas.regions import BrainRegions one = ONE(base_url='https://openalyx.internationalbrainlab.org', silent=True) diff --git a/examples/one/histology/brain_regions_navigation.py b/examples/one/histology/brain_regions_navigation.py index 9fb0043dd..2bc0e7b2d 100644 --- a/examples/one/histology/brain_regions_navigation.py +++ b/examples/one/histology/brain_regions_navigation.py @@ -1,4 +1,4 @@ -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas ba = AllenAtlas() diff --git a/examples/one/histology/coverage_map.py b/examples/one/histology/coverage_map.py index b7689eeb7..31d60aa74 100644 --- a/examples/one/histology/coverage_map.py +++ b/examples/one/histology/coverage_map.py @@ -3,7 +3,7 @@ from one.api import ONE from neurodsp.utils import fcn_cosine -import ibllib.atlas as atlas +import iblatlas.atlas as atlas from ibllib.pipes.histology import coverage ba = atlas.AllenAtlas() diff --git a/examples/one/histology/docs_find_dist_neighbouring_region.py b/examples/one/histology/docs_find_dist_neighbouring_region.py index dd46ee271..8f4e27bee 100644 --- a/examples/one/histology/docs_find_dist_neighbouring_region.py +++ b/examples/one/histology/docs_find_dist_neighbouring_region.py @@ -19,7 +19,7 @@ import one.alf.io as alfio from ibllib.pipes.ephys_alignment import EphysAlignment -from ibllib.atlas import atlas +from iblatlas import atlas # Instantiate brain atlas and one diff --git a/examples/one/histology/docs_find_nearby_trajectories.py b/examples/one/histology/docs_find_nearby_trajectories.py index e22ff6066..22f22fd3a 100644 --- a/examples/one/histology/docs_find_nearby_trajectories.py +++ b/examples/one/histology/docs_find_nearby_trajectories.py @@ -12,7 +12,7 @@ from mayavi import mlab import ibllib.pipes.histology as histology -import ibllib.atlas as atlas +import iblatlas.atlas as atlas from atlaselectrophysiology import rendering # Instantiate brain atlas and one diff --git a/examples/one/histology/docs_find_previous_alignments.py b/examples/one/histology/docs_find_previous_alignments.py index c57e02f1a..790e2b8bd 100644 --- a/examples/one/histology/docs_find_previous_alignments.py +++ b/examples/one/histology/docs_find_previous_alignments.py @@ -13,7 +13,7 @@ from one.api import ONE from ibllib.pipes.ephys_alignment import EphysAlignment -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # Instantiate brain atlas and one brain_atlas = atlas.AllenAtlas(25) diff --git a/examples/one/histology/docs_visualization3D_subject_channels.py b/examples/one/histology/docs_visualization3D_subject_channels.py index 75c5f9cee..bda59251e 100644 --- a/examples/one/histology/docs_visualization3D_subject_channels.py +++ b/examples/one/histology/docs_visualization3D_subject_channels.py @@ -18,7 +18,7 @@ import ibllib.plots from atlaselectrophysiology import rendering -import ibllib.atlas as atlas +import iblatlas.atlas as atlas one = ONE(base_url='https://openalyx.internationalbrainlab.org') subject = 'KS023' diff --git a/examples/one/histology/docs_visualize_session_coronal_tilted.py b/examples/one/histology/docs_visualize_session_coronal_tilted.py index 5f3336930..570e975a3 100644 --- a/examples/one/histology/docs_visualize_session_coronal_tilted.py +++ b/examples/one/histology/docs_visualize_session_coronal_tilted.py @@ -10,7 +10,7 @@ import numpy as np from one.api import ONE -import ibllib.atlas as atlas +import iblatlas.atlas as atlas import brainbox.io.one as bbone # === Parameters section (edit) === diff --git a/examples/one/histology/register_lasagna_tracks_alyx.py b/examples/one/histology/register_lasagna_tracks_alyx.py index 6ab0977db..5f79a86fe 100644 --- a/examples/one/histology/register_lasagna_tracks_alyx.py +++ b/examples/one/histology/register_lasagna_tracks_alyx.py @@ -24,7 +24,7 @@ """ # Author: Olivier, Gaelle from pathlib import Path -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas from one.api import ONE from ibllib.pipes import histology diff --git a/examples/one/histology/visualization3D_alyx_traj_planned_histology.py b/examples/one/histology/visualization3D_alyx_traj_planned_histology.py index ee5c5e1f3..0dcbf4203 100644 --- a/examples/one/histology/visualization3D_alyx_traj_planned_histology.py +++ b/examples/one/histology/visualization3D_alyx_traj_planned_histology.py @@ -10,7 +10,7 @@ from mayavi import mlab from atlaselectrophysiology import rendering -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # === Parameters section (edit) === diff --git a/examples/one/histology/visualization3D_repeated_site.py b/examples/one/histology/visualization3D_repeated_site.py index ec90b8b62..9646c2846 100644 --- a/examples/one/histology/visualization3D_repeated_site.py +++ b/examples/one/histology/visualization3D_repeated_site.py @@ -9,7 +9,7 @@ import ibllib.plots from iblutil.util import Bunch -import ibllib.atlas as atlas +import iblatlas.atlas as atlas from atlaselectrophysiology import rendering import brainbox.io.one as bbone diff --git a/examples/one/histology/visualization3D_rotating_gif_firstpassmap_plan.py b/examples/one/histology/visualization3D_rotating_gif_firstpassmap_plan.py index f5e63216a..085127c06 100644 --- a/examples/one/histology/visualization3D_rotating_gif_firstpassmap_plan.py +++ b/examples/one/histology/visualization3D_rotating_gif_firstpassmap_plan.py @@ -6,7 +6,7 @@ from mayavi import mlab from atlaselectrophysiology import rendering -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # the csv file is available here: # https://github.com/int-brain-lab/ibllib-matlab/blob/master/needles/maps/first_pass_map.csv diff --git a/examples/one/histology/visualization3D_rotating_gif_selectedmice.py b/examples/one/histology/visualization3D_rotating_gif_selectedmice.py index 42bb7e4f7..88a17829c 100644 --- a/examples/one/histology/visualization3D_rotating_gif_selectedmice.py +++ b/examples/one/histology/visualization3D_rotating_gif_selectedmice.py @@ -15,7 +15,7 @@ import ibllib.plots from atlaselectrophysiology import rendering -import ibllib.atlas as atlas +import iblatlas.atlas as atlas from iblutil.util import Bunch one = ONE(base_url="https://alyx.internationalbrainlab.org") diff --git a/examples/one/histology/visualization3D_subject_histology.py b/examples/one/histology/visualization3D_subject_histology.py index 0e0aaa44f..64c02cfe9 100644 --- a/examples/one/histology/visualization3D_subject_histology.py +++ b/examples/one/histology/visualization3D_subject_histology.py @@ -13,7 +13,7 @@ from one.api import ONE from atlaselectrophysiology import rendering -import ibllib.atlas as atlas +import iblatlas.atlas as atlas one = ONE(base_url="https://alyx.internationalbrainlab.org") diff --git a/examples/one/histology/visualize_alyx_channels_coronal.py b/examples/one/histology/visualize_alyx_channels_coronal.py index 7768db53b..9a8cde4d3 100644 --- a/examples/one/histology/visualize_alyx_channels_coronal.py +++ b/examples/one/histology/visualize_alyx_channels_coronal.py @@ -8,7 +8,7 @@ import numpy as np from one.api import ONE -import ibllib.atlas as atlas +import iblatlas.atlas as atlas import brainbox.io.one as bbone # === Parameters section (edit) === diff --git a/examples/one/histology/visualize_alyx_traj_coronal_sagittal_raster.py b/examples/one/histology/visualize_alyx_traj_coronal_sagittal_raster.py index c02fe8b86..1cedfb606 100644 --- a/examples/one/histology/visualize_alyx_traj_coronal_sagittal_raster.py +++ b/examples/one/histology/visualize_alyx_traj_coronal_sagittal_raster.py @@ -5,7 +5,7 @@ import matplotlib.pyplot as plt from one.api import ONE -import ibllib.atlas as atlas +import iblatlas.atlas as atlas import brainbox.io.one as bbone import brainbox.plot as bbplot diff --git a/examples/one/histology/visualize_track_file_coronal_GUIoption.py b/examples/one/histology/visualize_track_file_coronal_GUIoption.py index f655647c1..03a2b5ac6 100644 --- a/examples/one/histology/visualize_track_file_coronal_GUIoption.py +++ b/examples/one/histology/visualize_track_file_coronal_GUIoption.py @@ -7,7 +7,7 @@ import numpy as np from ibllib.pipes import histology -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # === Parameters section (edit) === track_file = "/Users/gaelle/Downloads/electrodetracks_lic3/2019-08-27_lic3_002_probe00_pts.csv" diff --git a/examples/one/histology/visualize_track_file_coronal_sagittal_slice.py b/examples/one/histology/visualize_track_file_coronal_sagittal_slice.py index 84ffacccd..02e22e656 100644 --- a/examples/one/histology/visualize_track_file_coronal_sagittal_slice.py +++ b/examples/one/histology/visualize_track_file_coronal_sagittal_slice.py @@ -10,7 +10,7 @@ import matplotlib.pyplot as plt from ibllib.pipes import histology -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # === Parameters section (edit) === diff --git a/ibllib/pipes/ephys_alignment.py b/ibllib/pipes/ephys_alignment.py index 9080b9053..cf99d3564 100644 --- a/ibllib/pipes/ephys_alignment.py +++ b/ibllib/pipes/ephys_alignment.py @@ -1,7 +1,7 @@ import scipy import numpy as np import ibllib.pipes.histology as histology -import ibllib.atlas as atlas +import iblatlas.atlas as atlas TIP_SIZE_UM = 200 diff --git a/ibllib/pipes/histology.py b/ibllib/pipes/histology.py index 6081f6eaa..ccf7ade22 100644 --- a/ibllib/pipes/histology.py +++ b/ibllib/pipes/histology.py @@ -7,7 +7,7 @@ import one.alf.io as alfio from neuropixel import TIP_SIZE_UM, trace_header -import ibllib.atlas as atlas +import iblatlas.atlas as atlas from ibllib.ephys.spikes import probes_description as extract_probes from ibllib.qc import base @@ -595,7 +595,7 @@ def coverage(trajs, ba=None, dist_fcn=[100, 150]): """ Computes a coverage volume from :param trajs: dictionary of trajectories from Alyx rest endpoint (one.alyx.rest...) - :param ba: ibllib.atlas.BrainAtlas instance + :param ba: iblatlas.atlas.BrainAtlas instance :return: 3D np.array the same size as the volume provided in the brain atlas """ # in um. Coverage = 1 below the first value, 0 after the second, cosine taper in between diff --git a/ibllib/pipes/mesoscope_tasks.py b/ibllib/pipes/mesoscope_tasks.py index c379ca1e3..e922eaefb 100644 --- a/ibllib/pipes/mesoscope_tasks.py +++ b/ibllib/pipes/mesoscope_tasks.py @@ -36,7 +36,7 @@ from ibllib.pipes import base_tasks from ibllib.io.extractors import mesoscope -from ibllib.atlas import ALLEN_CCF_LANDMARKS_MLAPDV_UM, MRITorontoAtlas +from iblatlas.atlas import ALLEN_CCF_LANDMARKS_MLAPDV_UM, MRITorontoAtlas _logger = logging.getLogger(__name__) diff --git a/ibllib/pipes/widefield_tasks.py b/ibllib/pipes/widefield_tasks.py index 37a793135..adf49f9eb 100644 --- a/ibllib/pipes/widefield_tasks.py +++ b/ibllib/pipes/widefield_tasks.py @@ -177,7 +177,7 @@ def _run(self): outfiles = [] # from wfield import load_allen_landmarks, SVDStack, atlas_from_landmarks_file - # from ibllib.atlas.regions import BrainRegions + # from iblatlas.regions import BrainRegions # from iblutil.numerical import ismember # import numpy as np # U = np.load(self.session_path.joinpath('alf/widefield', 'widefieldU.images.npy')) diff --git a/ibllib/plots/snapshot.py b/ibllib/plots/snapshot.py index da9f8cdd6..295ba7cc6 100644 --- a/ibllib/plots/snapshot.py +++ b/ibllib/plots/snapshot.py @@ -13,7 +13,7 @@ from ibllib import __version__ as ibllib_version from ibllib.pipes.ephys_alignment import EphysAlignment from ibllib.pipes.histology import interpolate_along_track -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas _logger = logging.getLogger(__name__) @@ -56,8 +56,8 @@ def __init__(self, pid, session_path=None, one=None, brain_regions=None, brain_a """ :param pid: probe insertion UUID from Alyx :param one: one instance - :param brain_regions: (optional) ibllib.atlas.BrainRegion object - :param brain_atlas: (optional) ibllib.atlas.AllenAtlas object + :param brain_regions: (optional) iblatlas.regions.BrainRegion object + :param brain_atlas: (optional) iblatlas.atlas.AllenAtlas object :param kwargs: """ assert one diff --git a/ibllib/qc/alignment_qc.py b/ibllib/qc/alignment_qc.py index 05d308427..20d686ec6 100644 --- a/ibllib/qc/alignment_qc.py +++ b/ibllib/qc/alignment_qc.py @@ -6,7 +6,7 @@ from neuropixel import trace_header import spikeglx -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas from ibllib.pipes import histology from ibllib.pipes.ephys_alignment import EphysAlignment from ibllib.qc import base diff --git a/ibllib/tests/qc/test_alignment_qc.py b/ibllib/tests/qc/test_alignment_qc.py index 55eddb68c..bb3e6a433 100644 --- a/ibllib/tests/qc/test_alignment_qc.py +++ b/ibllib/tests/qc/test_alignment_qc.py @@ -13,7 +13,7 @@ from neuropixel import trace_header from ibllib.tests import TEST_DB -from ibllib.atlas import AllenAtlas +from iblatlas.atlas import AllenAtlas from ibllib.pipes.misc import create_alyx_probe_insertions from ibllib.qc.alignment_qc import AlignmentQC from ibllib.pipes.histology import register_track, register_chronic_track diff --git a/ibllib/tests/test_histology.py b/ibllib/tests/test_histology.py index acd26b5d2..dce24ed43 100644 --- a/ibllib/tests/test_histology.py +++ b/ibllib/tests/test_histology.py @@ -5,7 +5,7 @@ from ibllib.pipes import histology from ibllib.pipes.ephys_alignment import (EphysAlignment, TIP_SIZE_UM, _cumulative_distance) -import ibllib.atlas as atlas +import iblatlas.atlas as atlas # TODO Place this in setUpModule() brain_atlas = atlas.AllenAtlas(res_um=25) From a62facb48d2e532888eaa39ef07b50556f9f0c4f Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 10:18:31 +0100 Subject: [PATCH 2/9] atlas imports refactor --- ibllib/atlas/atlas.py | 1474 ++------------------------------------ ibllib/atlas/flatmaps.py | 318 +------- ibllib/atlas/genes.py | 29 +- ibllib/atlas/plots.py | 908 +---------------------- ibllib/atlas/regions.py | 673 +---------------- 5 files changed, 107 insertions(+), 3295 deletions(-) diff --git a/ibllib/atlas/atlas.py b/ibllib/atlas/atlas.py index d4ec0914b..b21075c5c 100644 --- a/ibllib/atlas/atlas.py +++ b/ibllib/atlas/atlas.py @@ -4,16 +4,31 @@ from pathlib import Path, PurePosixPath from dataclasses import dataclass import logging +import warnings -import matplotlib.pyplot as plt import numpy as np -import nrrd -from one.webclient import http_download_file -import one.params -import one.remote.aws as aws -from iblutil.numerical import ismember -from ibllib.atlas.regions import BrainRegions, FranklinPaxinosRegions +import iblatlas.atlas + + +def deprecated_decorator(function): + def deprecated_function(*args, **kwargs): + warning_text = f"{function.__module__}.{function.__name__} is deprecated. " \ + f"Use iblatlas.{function.__module__.split('.')[-1]}.{function.__name__} instead" + warnings.warn(warning_text, DeprecationWarning) + return function(*args, **kwargs) + + return deprecated_function + +def deprecated_dataclass(dataclass): + def deprecated_function(): + warning_text = f"{dataclass.__module__}.{dataclass.__name__} is deprecated. " \ + f"Use iblatlas.{dataclass.__module__.split('.')[-1]}.{dataclass.__name__} instead" + warnings.warn(warning_text, DeprecationWarning) + return dataclass + + return deprecated_function() + ALLEN_CCF_LANDMARKS_MLAPDV_UM = {'bregma': np.array([5739, 5400, 332])} """dict: The ML AP DV voxel coordinates of brain landmarks in the Allen atlas.""" @@ -27,958 +42,17 @@ _logger = logging.getLogger(__name__) -def cart2sph(x, y, z): - """ - Converts cartesian to spherical coordinates. - - Returns spherical coordinates (r, theta, phi). - - Parameters - ---------- - x : numpy.array - A 1D array of x-axis coordinates. - y : numpy.array - A 1D array of y-axis coordinates. - z : numpy.array - A 1D array of z-axis coordinates. +@deprecated_decorator +def BrainCoordinates(*args, **kwargs): + return iblatlas.atlas.BrainCoordinates(*args, **kwargs) - Returns - ------- - numpy.array - The radial distance of each point. - numpy.array - The polar angle. - numpy.array - The azimuthal angle. - See Also - -------- - sph2cart - """ - r = np.sqrt(x ** 2 + y ** 2 + z ** 2) - phi = np.arctan2(y, x) * 180 / np.pi - theta = np.zeros_like(r) - iok = r != 0 - theta[iok] = np.arccos(z[iok] / r[iok]) * 180 / np.pi - if theta.size == 1: - theta = float(theta) - return r, theta, phi +@deprecated_decorator +def BrainAtlas(*args, **kwargs): + return iblatlas.atlas.BrainAtlas(*args, **kwargs) -def sph2cart(r, theta, phi): - """ - Converts Spherical to Cartesian coordinates. - - Returns Cartesian coordinates (x, y, z). - - Parameters - ---------- - r : numpy.array - A 1D array of radial distances. - theta : numpy.array - A 1D array of polar angles. - phi : numpy.array - A 1D array of azimuthal angles. - - Returns - ------- - x : numpy.array - A 1D array of x-axis coordinates. - y : numpy.array - A 1D array of y-axis coordinates. - z : numpy.array - A 1D array of z-axis coordinates. - - See Also - -------- - cart2sph - """ - x = r * np.cos(phi / 180 * np.pi) * np.sin(theta / 180 * np.pi) - y = r * np.sin(phi / 180 * np.pi) * np.sin(theta / 180 * np.pi) - z = r * np.cos(theta / 180 * np.pi) - return x, y, z - - -class BrainCoordinates: - """ - Class for mapping and indexing a 3D array to real-world coordinates. - - * x = ml, right positive - * y = ap, anterior positive - * z = dv, dorsal positive - - The layout of the Atlas dimension is done according to the most used sections so they lay - contiguous on disk assuming C-ordering: V[iap, iml, idv] - - Parameters - ---------- - nxyz : array_like - Number of elements along each Cartesian axis (nx, ny, nz) = (nml, nap, ndv). - xyz0 : array_like - Coordinates of the element volume[0, 0, 0] in the coordinate space. - dxyz : array_like, float - Spatial interval of the volume along the 3 dimensions. - - Attributes - ---------- - xyz0 : numpy.array - The Cartesian coordinates of the element volume[0, 0, 0], i.e. the origin. - x0 : int - The x-axis origin coordinate of the element volume. - y0 : int - The y-axis origin coordinate of the element volume. - z0 : int - The z-axis origin coordinate of the element volume. - """ - - def __init__(self, nxyz, xyz0=(0, 0, 0), dxyz=(1, 1, 1)): - if np.isscalar(dxyz): - dxyz = [dxyz] * 3 - self.x0, self.y0, self.z0 = list(xyz0) - self.dx, self.dy, self.dz = list(dxyz) - self.nx, self.ny, self.nz = list(nxyz) - - @property - def dxyz(self): - """numpy.array: Spatial interval of the volume along the 3 dimensions.""" - return np.array([self.dx, self.dy, self.dz]) - - @property - def nxyz(self): - """numpy.array: Coordinates of the element volume[0, 0, 0] in the coordinate space.""" - return np.array([self.nx, self.ny, self.nz]) - - """Methods ratios to indices""" - def r2ix(self, r): - # FIXME Document - return int((self.nx - 1) * r) - - def r2iy(self, r): - # FIXME Document - return int((self.nz - 1) * r) - - def r2iz(self, r): - # FIXME Document - return int((self.nz - 1) * r) - - """Methods distance to indices""" - @staticmethod - def _round(i, round=True): - """ - Round an input value to the nearest integer, replacing NaN values with 0. - - Parameters - ---------- - i : int, float, numpy.nan, numpy.array - A value or array of values to round. - round : bool - If false this function is identity. - - Returns - ------- - int, float, numpy.nan, numpy.array - If round is true, returns the nearest integer, replacing NaN values with 0, otherwise - returns the input unaffected. - """ - nanval = 0 - if round: - ii = np.array(np.round(i)).astype(int) - ii[np.isnan(i)] = nanval - return ii - else: - return i - - def x2i(self, x, round=True, mode='raise'): - """ - Find the nearest volume image index to a given x-axis coordinate. - - Parameters - ---------- - x : float, numpy.array - One or more x-axis coordinates, relative to the origin, x0. - round : bool - If true, round to the nearest index, replacing NaN values with 0. - mode : {'raise', 'clip', 'wrap'}, default='raise' - How to behave if the coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - The nearest indices of the image volume along the first dimension. - - Raises - ------ - ValueError - At least one x value lies outside of the atlas volume. Change 'mode' input to 'wrap' to - keep these values unchanged, or 'clip' to return the nearest valid indices. - """ - i = np.asarray(self._round((x - self.x0) / self.dx, round=round)) - if np.any(i < 0) or np.any(i >= self.nx): - if mode == 'clip': - i[i < 0] = 0 - i[i >= self.nx] = self.nx - 1 - elif mode == 'raise': - raise ValueError("At least one x value lies outside of the atlas volume.") - elif mode == 'wrap': # This is only here for legacy reasons - pass - return i - - def y2i(self, y, round=True, mode='raise'): - """ - Find the nearest volume image index to a given y-axis coordinate. - - Parameters - ---------- - y : float, numpy.array - One or more y-axis coordinates, relative to the origin, y0. - round : bool - If true, round to the nearest index, replacing NaN values with 0. - mode : {'raise', 'clip', 'wrap'} - How to behave if the coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - The nearest indices of the image volume along the second dimension. - - Raises - ------ - ValueError - At least one y value lies outside of the atlas volume. Change 'mode' input to 'wrap' to - keep these values unchanged, or 'clip' to return the nearest valid indices. - """ - i = np.asarray(self._round((y - self.y0) / self.dy, round=round)) - if np.any(i < 0) or np.any(i >= self.ny): - if mode == 'clip': - i[i < 0] = 0 - i[i >= self.ny] = self.ny - 1 - elif mode == 'raise': - raise ValueError("At least one y value lies outside of the atlas volume.") - elif mode == 'wrap': # This is only here for legacy reasons - pass - return i - - def z2i(self, z, round=True, mode='raise'): - """ - Find the nearest volume image index to a given z-axis coordinate. - - Parameters - ---------- - z : float, numpy.array - One or more z-axis coordinates, relative to the origin, z0. - round : bool - If true, round to the nearest index, replacing NaN values with 0. - mode : {'raise', 'clip', 'wrap'} - How to behave if the coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - The nearest indices of the image volume along the third dimension. - - Raises - ------ - ValueError - At least one z value lies outside of the atlas volume. Change 'mode' input to 'wrap' to - keep these values unchanged, or 'clip' to return the nearest valid indices. - """ - i = np.asarray(self._round((z - self.z0) / self.dz, round=round)) - if np.any(i < 0) or np.any(i >= self.nz): - if mode == 'clip': - i[i < 0] = 0 - i[i >= self.nz] = self.nz - 1 - elif mode == 'raise': - raise ValueError("At least one z value lies outside of the atlas volume.") - elif mode == 'wrap': # This is only here for legacy reasons - pass - return i - - def xyz2i(self, xyz, round=True, mode='raise'): - """ - Find the nearest volume image indices to the given Cartesian coordinates. - - Parameters - ---------- - xyz : array_like - One or more Cartesian coordinates, relative to the origin, xyz0. - round : bool - If true, round to the nearest index, replacing NaN values with 0. - mode : {'raise', 'clip', 'wrap'} - How to behave if any coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - The nearest indices of the image volume. - - Raises - ------ - ValueError - At least one coordinate lies outside of the atlas volume. Change 'mode' input to 'wrap' - to keep these values unchanged, or 'clip' to return the nearest valid indices. - """ - xyz = np.array(xyz) - dt = int if round else float - out = np.zeros_like(xyz, dtype=dt) - out[..., 0] = self.x2i(xyz[..., 0], round=round, mode=mode) - out[..., 1] = self.y2i(xyz[..., 1], round=round, mode=mode) - out[..., 2] = self.z2i(xyz[..., 2], round=round, mode=mode) - return out - - """Methods indices to distance""" - def i2x(self, ind): - """ - Return the x-axis coordinate of a given index. - - Parameters - ---------- - ind : int, numpy.array - One or more indices along the first dimension of the image volume. - - Returns - ------- - float, numpy.array - The corresponding x-axis coordinate(s), relative to the origin, x0. - """ - return ind * self.dx + self.x0 - - def i2y(self, ind): - """ - Return the y-axis coordinate of a given index. - - Parameters - ---------- - ind : int, numpy.array - One or more indices along the second dimension of the image volume. - - Returns - ------- - float, numpy.array - The corresponding y-axis coordinate(s), relative to the origin, y0. - """ - return ind * self.dy + self.y0 - - def i2z(self, ind): - """ - Return the z-axis coordinate of a given index. - - Parameters - ---------- - ind : int, numpy.array - One or more indices along the third dimension of the image volume. - - Returns - ------- - float, numpy.array - The corresponding z-axis coordinate(s), relative to the origin, z0. - """ - return ind * self.dz + self.z0 - - def i2xyz(self, iii): - """ - Return the Cartesian coordinates of a given index. - - Parameters - ---------- - iii : array_like - One or more image volume indices. - - Returns - ------- - numpy.array - The corresponding xyz coordinates, relative to the origin, xyz0. - """ - - iii = np.array(iii, dtype=float) - out = np.zeros_like(iii) - out[..., 0] = self.i2x(iii[..., 0]) - out[..., 1] = self.i2y(iii[..., 1]) - out[..., 2] = self.i2z(iii[..., 2]) - return out - - """Methods bounds""" - @property - def xlim(self): - # FIXME Document - return self.i2x(np.array([0, self.nx - 1])) - - @property - def ylim(self): - # FIXME Document - return self.i2y(np.array([0, self.ny - 1])) - - @property - def zlim(self): - # FIXME Document - return self.i2z(np.array([0, self.nz - 1])) - - def lim(self, axis): - # FIXME Document - if axis == 0: - return self.xlim - elif axis == 1: - return self.ylim - elif axis == 2: - return self.zlim - - """returns scales""" - @property - def xscale(self): - # FIXME Document - return self.i2x(np.arange(self.nx)) - - @property - def yscale(self): - # FIXME Document - return self.i2y(np.arange(self.ny)) - - @property - def zscale(self): - # FIXME Document - return self.i2z(np.arange(self.nz)) - - """returns the 3d mgrid used for 3d visualization""" - @property - def mgrid(self): - # FIXME Document - return np.meshgrid(self.xscale, self.yscale, self.zscale) - - -class BrainAtlas: - """ - Objects that holds image, labels and coordinate transforms for a brain Atlas. - Currently this is designed for the AllenCCF at several resolutions, - yet this class can be used for other atlases arises. - """ - - """numpy.array: An image volume.""" - image = None - """numpy.array: An annotation label volume.""" - label = None - - def __init__(self, image, label, dxyz, regions, iorigin=[0, 0, 0], - dims2xyz=[0, 1, 2], xyz2dims=[0, 1, 2]): - """ - self.image: image volume (ap, ml, dv) - self.label: label volume (ap, ml, dv) - self.bc: atlas.BrainCoordinate object - self.regions: atlas.BrainRegions object - self.top: 2d np array (ap, ml) containing the z-coordinate (m) of the surface of the brain - self.dims2xyz and self.zyz2dims: map image axis order to xyz coordinates order - """ - - self.image = image - self.label = label - self.regions = regions - self.dims2xyz = dims2xyz - self.xyz2dims = xyz2dims - assert np.all(self.dims2xyz[self.xyz2dims] == np.array([0, 1, 2])) - assert np.all(self.xyz2dims[self.dims2xyz] == np.array([0, 1, 2])) - # create the coordinate transform object that maps volume indices to real world coordinates - nxyz = np.array(self.image.shape)[self.dims2xyz] - bc = BrainCoordinates(nxyz=nxyz, xyz0=(0, 0, 0), dxyz=dxyz) - self.bc = BrainCoordinates(nxyz=nxyz, xyz0=-bc.i2xyz(iorigin), dxyz=dxyz) - - self.surface = None - self.boundary = None - - @staticmethod - def _get_cache_dir(): - par = one.params.get(silent=True) - path_atlas = Path(par.CACHE_DIR).joinpath('histology', 'ATLAS', 'Needles', 'Allen', 'flatmaps') - return path_atlas - - def compute_surface(self): - """ - Get the volume top, bottom, left and right surfaces, and from these the outer surface of - the image volume. This is needed to compute probe insertions intersections. - - NOTE: In places where the top or bottom surface touch the top or bottom of the atlas volume, the surface - will be set to np.nan. If you encounter issues working with these surfaces check if this might be the cause. - """ - if self.surface is None: # only compute if it hasn't already been computed - axz = self.xyz2dims[2] # this is the dv axis - _surface = (self.label == 0).astype(np.int8) * 2 - l0 = np.diff(_surface, axis=axz, append=2) - _top = np.argmax(l0 == -2, axis=axz).astype(float) - _top[_top == 0] = np.nan - _bottom = self.bc.nz - np.argmax(np.flip(l0, axis=axz) == 2, axis=axz).astype(float) - _bottom[_bottom == self.bc.nz] = np.nan - self.top = self.bc.i2z(_top + 1) - self.bottom = self.bc.i2z(_bottom - 1) - self.surface = np.diff(_surface, axis=self.xyz2dims[0], append=2) + l0 - idx_srf = np.where(self.surface != 0) - self.surface[idx_srf] = 1 - self.srf_xyz = self.bc.i2xyz(np.c_[idx_srf[self.xyz2dims[0]], idx_srf[self.xyz2dims[1]], - idx_srf[self.xyz2dims[2]]].astype(float)) - - def _lookup_inds(self, ixyz, mode='raise'): - """ - Performs a 3D lookup from volume indices ixyz to the image volume - :param ixyz: [n, 3] array of indices in the mlapdv order - :return: n array of flat indices - """ - idims = np.split(ixyz[..., self.xyz2dims], [1, 2], axis=-1) - inds = np.ravel_multi_index(idims, self.bc.nxyz[self.xyz2dims], mode=mode) - return inds.squeeze() - - def _lookup(self, xyz, mode='raise'): - """ - Performs a 3D lookup from real world coordinates to the flat indices in the volume, - defined in the BrainCoordinates object. - - Parameters - ---------- - xyz : numpy.array - An (n, 3) array of Cartesian coordinates. - mode : {'raise', 'clip', 'wrap'} - How to behave if any coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - A 1D array of flat indices. - """ - return self._lookup_inds(self.bc.xyz2i(xyz, mode=mode), mode=mode) - - def get_labels(self, xyz, mapping=None, radius_um=None, mode='raise'): - """ - Performs a 3D lookup from real world coordinates to the volume labels - and return the regions ids according to the mapping - :param xyz: [n, 3] array of coordinates - :param mapping: brain region mapping (defaults to original Allen mapping) - :param radius_um: if not null, returns a regions ids array and an array of proportion - of regions in a sphere of size radius around the coordinates. - :param mode: {‘raise’, 'clip'} determines what to do when determined index lies outside the atlas volume - 'raise' will raise a ValueError (default) - 'clip' will replace the index with the closest index inside the volume - :return: n array of region ids - """ - mapping = mapping or self.regions.default_mapping - - if radius_um: - nrx = int(np.ceil(radius_um / abs(self.bc.dx) / 1e6)) - nry = int(np.ceil(radius_um / abs(self.bc.dy) / 1e6)) - nrz = int(np.ceil(radius_um / abs(self.bc.dz) / 1e6)) - nr = [nrx, nry, nrz] - iii = self.bc.xyz2i(xyz, mode=mode) - # computing the cube radius and indices is more complicated as volume indices are not - # necessarily in ml, ap, dv order so the indices order is dynamic - rcube = np.meshgrid(*tuple((np.arange( - -nr[i], nr[i] + 1) * self.bc.dxyz[i]) ** 2 for i in self.xyz2dims)) - rcube = np.sqrt(rcube[0] + rcube[1], rcube[2]) * 1e6 - icube = tuple(slice(-nr[i] + iii[i], nr[i] + iii[i] + 1) for i in self.xyz2dims) - cube = self.regions.mappings[mapping][self.label[icube]] - ilabs, counts = np.unique(cube[rcube <= radius_um], return_counts=True) - return self.regions.id[ilabs], counts / np.sum(counts) - else: - regions_indices = self._get_mapping(mapping=mapping)[self.label.flat[self._lookup(xyz, mode=mode)]] - return self.regions.id[regions_indices] - - def _get_mapping(self, mapping=None): - """ - Safe way to get mappings if nothing defined in regions. - A mapping transforms from the full allen brain Atlas ids to the remapped ids - new_ids = ids[mapping] - """ - mapping = mapping or self.regions.default_mapping - if hasattr(self.regions, 'mappings'): - return self.regions.mappings[mapping] - else: - return np.arange(self.regions.id.size) - - def _label2rgb(self, imlabel): - """ - Converts a slice from the label volume to its RGB equivalent for display - :param imlabel: 2D np-array containing label ids (slice of the label volume) - :return: 3D np-array of the slice uint8 rgb values - """ - if getattr(self.regions, 'rgb', None) is None: - return self.regions.id[imlabel] - else: # if the regions exist and have the rgb attribute, do the rgb lookup - return self.regions.rgb[imlabel] - - def tilted_slice(self, xyz, axis, volume='image'): - """ - From line coordinates, extracts the tilted plane containing the line from the 3D volume - :param xyz: np.array: points defining a probe trajectory in 3D space (xyz triplets) - if more than 2 points are provided will take the best fit - :param axis: - 0: along ml = sagittal-slice - 1: along ap = coronal-slice - 2: along dv = horizontal-slice - :param volume: 'image' or 'annotation' - :return: np.array, abscissa extent (width), ordinate extent (height), - squeezed axis extent (depth) - """ - if axis == 0: # sagittal slice (squeeze/take along ml-axis) - wdim, hdim, ddim = (1, 2, 0) - elif axis == 1: # coronal slice (squeeze/take along ap-axis) - wdim, hdim, ddim = (0, 2, 1) - elif axis == 2: # horizontal slice (squeeze/take along dv-axis) - wdim, hdim, ddim = (0, 1, 2) - # get the best fit and find exit points of the volume along squeezed axis - trj = Trajectory.fit(xyz) - sub_volume = trj._eval(self.bc.lim(axis=hdim), axis=hdim) - sub_volume[:, wdim] = self.bc.lim(axis=wdim) - sub_volume_i = self.bc.xyz2i(sub_volume) - tile_shape = np.array([np.diff(sub_volume_i[:, hdim])[0] + 1, self.bc.nxyz[wdim]]) - # get indices along each dimension - indx = np.arange(tile_shape[1]) - indy = np.arange(tile_shape[0]) - inds = np.linspace(*sub_volume_i[:, ddim], tile_shape[0]) - # compute the slice indices and output the slice - _, INDS = np.meshgrid(indx, np.int64(np.around(inds))) - INDX, INDY = np.meshgrid(indx, indy) - indsl = [[INDX, INDY, INDS][i] for i in np.argsort([wdim, hdim, ddim])[self.xyz2dims]] - if isinstance(volume, np.ndarray): - tslice = volume[indsl[0], indsl[1], indsl[2]] - elif volume.lower() == 'annotation': - tslice = self._label2rgb(self.label[indsl[0], indsl[1], indsl[2]]) - elif volume.lower() == 'image': - tslice = self.image[indsl[0], indsl[1], indsl[2]] - elif volume.lower() == 'surface': - tslice = self.surface[indsl[0], indsl[1], indsl[2]] - - # get extents with correct convention NB: matplotlib flips the y-axis on imshow ! - width = np.sort(sub_volume[:, wdim])[np.argsort(self.bc.lim(axis=wdim))] - height = np.flipud(np.sort(sub_volume[:, hdim])[np.argsort(self.bc.lim(axis=hdim))]) - depth = np.flipud(np.sort(sub_volume[:, ddim])[np.argsort(self.bc.lim(axis=ddim))]) - return tslice, width, height, depth - - def plot_tilted_slice(self, xyz, axis, volume='image', cmap=None, ax=None, return_sec=False, **kwargs): - """ - From line coordinates, extracts the tilted plane containing the line from the 3D volume - :param xyz: np.array: points defining a probe trajectory in 3D space (xyz triplets) - if more than 2 points are provided will take the best fit - :param axis: - 0: along ml = sagittal-slice - 1: along ap = coronal-slice - 2: along dv = horizontal-slice - :param volume: 'image' or 'annotation' - :return: matplotlib axis - """ - if axis == 0: - axis_labels = np.array(['ap (um)', 'dv (um)', 'ml (um)']) - elif axis == 1: - axis_labels = np.array(['ml (um)', 'dv (um)', 'ap (um)']) - elif axis == 2: - axis_labels = np.array(['ml (um)', 'ap (um)', 'dv (um)']) - - tslice, width, height, depth = self.tilted_slice(xyz, axis, volume=volume) - width = width * 1e6 - height = height * 1e6 - depth = depth * 1e6 - if not ax: - plt.figure() - ax = plt.gca() - ax.axis('equal') - if not cmap: - cmap = plt.get_cmap('bone') - # get the transfer function from y-axis to squeezed axis for second axe - ab = np.linalg.solve(np.c_[height, height * 0 + 1], depth) - height * ab[0] + ab[1] - ax.imshow(tslice, extent=np.r_[width, height], cmap=cmap, **kwargs) - sec_ax = ax.secondary_yaxis('right', functions=( - lambda x: x * ab[0] + ab[1], - lambda y: (y - ab[1]) / ab[0])) - ax.set_xlabel(axis_labels[0]) - ax.set_ylabel(axis_labels[1]) - sec_ax.set_ylabel(axis_labels[2]) - if return_sec: - return ax, sec_ax - else: - return ax - - @staticmethod - def _plot_slice(im, extent, ax=None, cmap=None, volume=None, **kwargs): - """ - Plot an atlas slice. - - Parameters - ---------- - im : numpy.array - A 2D image slice to plot. - extent : array_like - The bounding box in data coordinates that the image will fill specified as (left, - right, bottom, top) in data coordinates. - ax : matplotlib.pyplot.Axes - An optional Axes object to plot to. - cmap : str, matplotlib.colors.Colormap - The Colormap instance or registered colormap name used to map scalar data to colors. - Defaults to 'bone'. - volume : str - If 'boundary', assumes image is an outline of boundaries between all regions. - FIXME How does this affect the plot? - **kwargs - See matplotlib.pyplot.imshow. - - Returns - ------- - matplotlib.pyplot.Axes - The image axes. - """ - if not ax: - ax = plt.gca() - ax.axis('equal') - if not cmap: - cmap = plt.get_cmap('bone') - - if volume == 'boundary': - imb = np.zeros((*im.shape[:2], 4), dtype=np.uint8) - imb[im == 1] = np.array([0, 0, 0, 255]) - im = imb - - ax.imshow(im, extent=extent, cmap=cmap, **kwargs) - return ax - - def extent(self, axis): - """ - :param axis: direction along which the volume is stacked: - (2 = z for horizontal slice) - (1 = y for coronal slice) - (0 = x for sagittal slice) - :return: - """ - - if axis == 0: - extent = np.r_[self.bc.ylim, np.flip(self.bc.zlim)] * 1e6 - elif axis == 1: - extent = np.r_[self.bc.xlim, np.flip(self.bc.zlim)] * 1e6 - elif axis == 2: - extent = np.r_[self.bc.xlim, np.flip(self.bc.ylim)] * 1e6 - return extent - - def slice(self, coordinate, axis, volume='image', mode='raise', region_values=None, - mapping=None, bc=None): - """ - Get slice through atlas - - :param coordinate: coordinate to slice in metres, float - :param axis: xyz convention: 0 for ml, 1 for ap, 2 for dv - - 0: sagittal slice (along ml axis) - - 1: coronal slice (along ap axis) - - 2: horizontal slice (along dv axis) - :param volume: - - 'image' - allen image volume - - 'annotation' - allen annotation volume - - 'surface' - outer surface of mesh - - 'boundary' - outline of boundaries between all regions - - 'volume' - custom volume, must pass in volume of shape ba.image.shape as regions_value argument - - 'value' - custom value per allen region, must pass in array of shape ba.regions.id as regions_value argument - :param mode: error mode for out of bounds coordinates - - 'raise' raise an error - - 'clip' gets the first or last index - :param region_values: custom values to plot - - if volume='volume', region_values must have shape ba.image.shape - - if volume='value', region_values must have shape ba.regions.id - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :return: 2d array or 3d RGB numpy int8 array - """ - if axis == 0: - index = self.bc.x2i(np.array(coordinate), mode=mode) - elif axis == 1: - index = self.bc.y2i(np.array(coordinate), mode=mode) - elif axis == 2: - index = self.bc.z2i(np.array(coordinate), mode=mode) - - # np.take is 50 thousand times slower than straight slicing ! - def _take(vol, ind, axis): - if mode == 'clip': - ind = np.minimum(np.maximum(ind, 0), vol.shape[axis] - 1) - if axis == 0: - return vol[ind, :, :] - elif axis == 1: - return vol[:, ind, :] - elif axis == 2: - return vol[:, :, ind] - - def _take_remap(vol, ind, axis, mapping): - # For the labels, remap the regions indices according to the mapping - return self._get_mapping(mapping=mapping)[_take(vol, ind, axis)] - - if isinstance(volume, np.ndarray): - return _take(volume, index, axis=self.xyz2dims[axis]) - elif volume in 'annotation': - iregion = _take_remap(self.label, index, self.xyz2dims[axis], mapping) - return self._label2rgb(iregion) - elif volume == 'image': - return _take(self.image, index, axis=self.xyz2dims[axis]) - elif volume == 'value': - return region_values[_take_remap(self.label, index, self.xyz2dims[axis], mapping)] - elif volume == 'image': - return _take(self.image, index, axis=self.xyz2dims[axis]) - elif volume in ['surface', 'edges']: - self.compute_surface() - return _take(self.surface, index, axis=self.xyz2dims[axis]) - elif volume == 'boundary': - iregion = _take_remap(self.label, index, self.xyz2dims[axis], mapping) - return self.compute_boundaries(iregion) - - elif volume == 'volume': - if bc is not None: - index = bc.xyz2i(np.array([coordinate] * 3))[axis] - return _take(region_values, index, axis=self.xyz2dims[axis]) - - def compute_boundaries(self, values): - """ - Compute the boundaries between regions on slice - :param values: - :return: - """ - boundary = np.abs(np.diff(values, axis=0, prepend=0)) - boundary = boundary + np.abs(np.diff(values, axis=1, prepend=0)) - boundary = boundary + np.abs(np.diff(values, axis=1, append=0)) - boundary = boundary + np.abs(np.diff(values, axis=0, append=0)) - - boundary[boundary != 0] = 1 - - return boundary - - def plot_slices(self, xyz, *args, **kwargs): - """ - From a single coordinate, plots the 3 slices that intersect at this point in a single - matplotlib figure - :param xyz: mlapdv coordinate in m - :param args: arguments to be forwarded to plot slices - :param kwargs: keyword arguments to be forwarded to plot slices - :return: 2 by 2 array of axes - """ - fig, axs = plt.subplots(2, 2) - self.plot_cslice(xyz[1], *args, ax=axs[0, 0], **kwargs) - self.plot_sslice(xyz[0], *args, ax=axs[0, 1], **kwargs) - self.plot_hslice(xyz[2], *args, ax=axs[1, 0], **kwargs) - xyz_um = xyz * 1e6 - axs[0, 0].plot(xyz_um[0], xyz_um[2], 'g*') - axs[0, 1].plot(xyz_um[1], xyz_um[2], 'g*') - axs[1, 0].plot(xyz_um[0], xyz_um[1], 'g*') - return axs - - def plot_cslice(self, ap_coordinate, volume='image', mapping=None, region_values=None, **kwargs): - """ - Plot coronal slice through atlas at given ap_coordinate - - :param: ap_coordinate (m) - :param volume: - - 'image' - allen image volume - - 'annotation' - allen annotation volume - - 'surface' - outer surface of mesh - - 'boundary' - outline of boundaries between all regions - - 'volume' - custom volume, must pass in volume of shape ba.image.shape as regions_value argument - - 'value' - custom value per allen region, must pass in array of shape ba.regions.id as regions_value argument - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param region_values: custom values to plot - - if volume='volume', region_values must have shape ba.image.shape - - if volume='value', region_values must have shape ba.regions.id - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param **kwargs: matplotlib.pyplot.imshow kwarg arguments - :return: matplotlib ax object - """ - - cslice = self.slice(ap_coordinate, axis=1, volume=volume, mapping=mapping, region_values=region_values) - return self._plot_slice(np.moveaxis(cslice, 0, 1), extent=self.extent(axis=1), volume=volume, **kwargs) - - def plot_hslice(self, dv_coordinate, volume='image', mapping=None, region_values=None, **kwargs): - """ - Plot horizontal slice through atlas at given dv_coordinate - - :param: dv_coordinate (m) - :param volume: - - 'image' - allen image volume - - 'annotation' - allen annotation volume - - 'surface' - outer surface of mesh - - 'boundary' - outline of boundaries between all regions - - 'volume' - custom volume, must pass in volume of shape ba.image.shape as regions_value argument - - 'value' - custom value per allen region, must pass in array of shape ba.regions.id as regions_value argument - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param region_values: custom values to plot - - if volume='volume', region_values must have shape ba.image.shape - - if volume='value', region_values must have shape ba.regions.id - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param **kwargs: matplotlib.pyplot.imshow kwarg arguments - :return: matplotlib ax object - """ - - hslice = self.slice(dv_coordinate, axis=2, volume=volume, mapping=mapping, region_values=region_values) - return self._plot_slice(hslice, extent=self.extent(axis=2), volume=volume, **kwargs) - - def plot_sslice(self, ml_coordinate, volume='image', mapping=None, region_values=None, **kwargs): - """ - Plot sagittal slice through atlas at given ml_coordinate - - :param: ml_coordinate (m) - :param volume: - - 'image' - allen image volume - - 'annotation' - allen annotation volume - - 'surface' - outer surface of mesh - - 'boundary' - outline of boundaries between all regions - - 'volume' - custom volume, must pass in volume of shape ba.image.shape as regions_value argument - - 'value' - custom value per allen region, must pass in array of shape ba.regions.id as regions_value argument - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param region_values: custom values to plot - - if volume='volume', region_values must have shape ba.image.shape - - if volume='value', region_values must have shape ba.regions.id - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param **kwargs: matplotlib.pyplot.imshow kwarg arguments - :return: matplotlib ax object - """ - - sslice = self.slice(ml_coordinate, axis=0, volume=volume, mapping=mapping, region_values=region_values) - return self._plot_slice(np.swapaxes(sslice, 0, 1), extent=self.extent(axis=0), volume=volume, **kwargs) - - def plot_top(self, volume='annotation', mapping=None, region_values=None, ax=None, **kwargs): - """ - Plot top view of atlas - :param volume: - - 'image' - allen image volume - - 'annotation' - allen annotation volume - - 'boundary' - outline of boundaries between all regions - - 'volume' - custom volume, must pass in volume of shape ba.image.shape as regions_value argument - - 'value' - custom value per allen region, must pass in array of shape ba.regions.id as regions_value argument - - :param mapping: mapping to use. Options can be found using ba.regions.mappings.keys() - :param region_values: - :param ax: - :param kwargs: - :return: - """ - - self.compute_surface() - ix, iy = np.meshgrid(np.arange(self.bc.nx), np.arange(self.bc.ny)) - iz = self.bc.z2i(self.top) - inds = self._lookup_inds(np.stack((ix, iy, iz), axis=-1)) - - regions = self._get_mapping(mapping=mapping)[self.label.flat[inds]] - - if volume == 'annotation': - im = self._label2rgb(regions) - elif volume == 'image': - im = self.top - elif volume == 'value': - im = region_values[regions] - elif volume == 'volume': - im = np.zeros((iz.shape)) - for x in range(im.shape[0]): - for y in range(im.shape[1]): - im[x, y] = region_values[x, y, iz[x, y]] - elif volume == 'boundary': - im = self.compute_boundaries(regions) - - return self._plot_slice(im, self.extent(axis=2), ax=ax, volume=volume, **kwargs) - - -@dataclass -class Trajectory: +class Trajectory(iblatlas.atlas.Trajectory): """ 3D Trajectory (usually for a linear probe), minimally defined by a vector and a point. @@ -991,111 +65,15 @@ class Trajectory: vector: np.ndarray point: np.ndarray - @staticmethod - def fit(xyz): - """ - Fits a line to a 3D cloud of points. - - Parameters - ---------- - xyz : numpy.array - An n by 3 array containing a cloud of points to fit a line to. - - Returns - ------- - Trajectory - A new trajectory object. - """ - xyz_mean = np.mean(xyz, axis=0) - return Trajectory(vector=np.linalg.svd(xyz - xyz_mean)[2][0], point=xyz_mean) - - def eval_x(self, x): - """ - given an array of x coordinates, returns the xyz array of coordinates along the insertion - :param x: n by 1 or numpy array containing x-coordinates - :return: n by 3 numpy array containing xyz-coordinates - """ - return self._eval(x, axis=0) - - def eval_y(self, y): - """ - given an array of y coordinates, returns the xyz array of coordinates along the insertion - :param y: n by 1 or numpy array containing y-coordinates - :return: n by 3 numpy array containing xyz-coordinates - """ - return self._eval(y, axis=1) - - def eval_z(self, z): - """ - given an array of z coordinates, returns the xyz array of coordinates along the insertion - :param z: n by 1 or numpy array containing z-coordinates - :return: n by 3 numpy array containing xyz-coordinates - """ - return self._eval(z, axis=2) - - def project(self, point): - """ - projects a point onto the trajectory line - :param point: np.array(x, y, z) coordinates - :return: - """ - # https://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html - if point.ndim == 1: - return self.project(point[np.newaxis])[0] - return (self.point + np.dot(point[:, np.newaxis] - self.point, self.vector) / - np.dot(self.vector, self.vector) * self.vector) - - def mindist(self, xyz, bounds=None): - """ - Computes the minimum distance to the trajectory line for one or a set of points. - If bounds are provided, computes the minimum distance to the segment instead of an - infinite line. - :param xyz: [..., 3] - :param bounds: defaults to None. np.array [2, 3]: segment boundaries, inf line if None - :return: minimum distance [...] - """ - proj = self.project(xyz) - d = np.sqrt(np.sum((proj - xyz) ** 2, axis=-1)) - if bounds is not None: - # project the boundaries and the points along the traj - b = np.dot(bounds, self.vector) - ob = np.argsort(b) - p = np.dot(xyz[:, np.newaxis], self.vector).squeeze() - # for points below and above boundaries, compute cartesian distance to the boundary - imin = p < np.min(b) - d[imin] = np.sqrt(np.sum((xyz[imin, :] - bounds[ob[0], :]) ** 2, axis=-1)) - imax = p > np.max(b) - d[imax] = np.sqrt(np.sum((xyz[imax, :] - bounds[ob[1], :]) ** 2, axis=-1)) - return d - - def _eval(self, c, axis): - # uses symmetric form of 3d line equation to get xyz coordinates given one coordinate - if not isinstance(c, np.ndarray): - c = np.array(c) - while c.ndim < 2: - c = c[..., np.newaxis] - # there are cases where it's impossible to project if a line is // to the axis - if self.vector[axis] == 0: - return np.nan * np.zeros((c.shape[0], 3)) - else: - return (c - self.point[axis]) * self.vector / self.vector[axis] + self.point - - def exit_points(self, bc): - """ - Given a Trajectory and a BrainCoordinates object, computes the intersection of the - trajectory with the brain coordinates bounding box - :param bc: BrainCoordinate objects - :return: np.ndarray 2 y 3 corresponding to exit points xyz coordinates - """ - bounds = np.c_[bc.xlim, bc.ylim, bc.zlim] - epoints = np.r_[self.eval_x(bc.xlim), self.eval_y(bc.ylim), self.eval_z(bc.zlim)] - epoints = epoints[~np.all(np.isnan(epoints), axis=1)] - ind = np.all(np.bitwise_and(bounds[0, :] <= epoints, epoints <= bounds[1, :]), axis=1) - return epoints[ind, :] + def __init_sublcall__(self): + warning_text = f"{dataclass.__module__}.{dataclass.__name__} is deprecated. " \ + f"Use iblatlas.{dataclass.__module__.split('.')[-1]}.{dataclass.__name__} instead" + warnings.warn(warning_text, DeprecationWarning) +@deprecated_dataclass @dataclass -class Insertion: +class Insertion(iblatlas.atlas.Insertion): """ Defines an ephys probe insertion in 3D coordinate. IBL conventions. @@ -1258,215 +236,12 @@ def get_brain_entry(traj, brain_atlas): return Insertion._get_surface_intersection(traj, brain_atlas, surface='top') -class AllenAtlas(BrainAtlas): - """ - The Allan Common Coordinate Framework (CCF) brain atlas. - - Instantiates an atlas.BrainAtlas corresponding to the Allen CCF at the given resolution - using the IBL Bregma and coordinate system. - """ - - """pathlib.PurePosixPath: The default relative path of the Allen atlas file.""" - atlas_rel_path = PurePosixPath('histology', 'ATLAS', 'Needles', 'Allen') - - """numpy.array: A diffusion weighted imaging (DWI) image volume. - - The Allen atlas DWI average template volume has with the shape (ap, ml, dv) and contains uint16 - values. FIXME What do the values represent? - """ - image = None - - """numpy.array: An annotation label volume. - - The Allen atlas label volume has with the shape (ap, ml, dv) and contains uint16 indices - of the Allen CCF brain regions to which each voxel belongs. - """ - label = None - - def __init__(self, res_um=25, scaling=(1, 1, 1), mock=False, hist_path=None): - """ - Instantiates an atlas.BrainAtlas corresponding to the Allen CCF at the given resolution - using the IBL Bregma and coordinate system. - - Parameters - ---------- - res_um : {10, 25, 50} int - The Atlas resolution in micrometres; one of 10, 25 or 50um. - scaling : float, numpy.array - Scale factor along ml, ap, dv for squeeze and stretch (default: [1, 1, 1]). - mock : bool - For testing purposes, return atlas object with image comprising zeros. - hist_path : str, pathlib.Path - The location of the image volume. May be a full file path or a directory. - - Examples - -------- - Instantiate Atlas from a non-default location, in this case the cache_dir of an ONE instance. - >>> target_dir = one.cache_dir / AllenAtlas.atlas_rel_path - ... ba = AllenAtlas(hist_path=target_dir) - """ - LUT_VERSION = 'v01' # version 01 is the lateralized version - regions = BrainRegions() - xyz2dims = np.array([1, 0, 2]) # this is the c-contiguous ordering - dims2xyz = np.array([1, 0, 2]) - # we use Bregma as the origin - self.res_um = res_um - ibregma = (ALLEN_CCF_LANDMARKS_MLAPDV_UM['bregma'] / self.res_um) - dxyz = self.res_um * 1e-6 * np.array([1, -1, -1]) * scaling - if mock: - image, label = [np.zeros((528, 456, 320), dtype=np.int16) for _ in range(2)] - label[:, :, 100:105] = 1327 # lookup index for retina, id 304325711 (no id 1327) - else: - # Hist path may be a full path to an existing image file, or a path to a directory - cache_dir = Path(one.params.get(silent=True).CACHE_DIR) - hist_path = Path(hist_path or cache_dir.joinpath(self.atlas_rel_path)) - if not hist_path.suffix: # check if folder - hist_path /= f'average_template_{res_um}.nrrd' - # get the image volume - if not hist_path.exists(): - hist_path = _download_atlas_allen(hist_path) - # get the remapped label volume - file_label = hist_path.with_name(f'annotation_{res_um}.nrrd') - if not file_label.exists(): - file_label = _download_atlas_allen(file_label) - file_label_remap = hist_path.with_name(f'annotation_{res_um}_lut_{LUT_VERSION}.npz') - if not file_label_remap.exists(): - label = self._read_volume(file_label).astype(dtype=np.int32) - _logger.info("Computing brain atlas annotations lookup table") - # lateralize atlas: for this the regions of the left hemisphere have primary - # keys opposite to to the normal ones - lateral = np.zeros(label.shape[xyz2dims[0]]) - lateral[int(np.floor(ibregma[0]))] = 1 - lateral = np.sign(np.cumsum(lateral)[np.newaxis, :, np.newaxis] - 0.5) - label = label * lateral.astype(np.int32) - # the 10 um atlas is too big to fit in memory so work by chunks instead - if res_um == 10: - first, ncols = (0, 10) - while True: - last = np.minimum(first + ncols, label.shape[-1]) - _logger.info(f"Computing... {last} on {label.shape[-1]}") - _, im = ismember(label[:, :, first:last], regions.id) - label[:, :, first:last] = np.reshape(im, label[:, :, first:last].shape) - if last == label.shape[-1]: - break - first += ncols - label = label.astype(dtype=np.uint16) - _logger.info("Saving npz, this can take a long time") - else: - _, im = ismember(label, regions.id) - label = np.reshape(im.astype(np.uint16), label.shape) - np.savez_compressed(file_label_remap, label) - _logger.info(f"Cached remapping file {file_label_remap} ...") - # loads the files - label = self._read_volume(file_label_remap) - image = self._read_volume(hist_path) - - super().__init__(image, label, dxyz, regions, ibregma, dims2xyz=dims2xyz, xyz2dims=xyz2dims) - - @staticmethod - def _read_volume(file_volume): - if file_volume.suffix == '.nrrd': - volume, _ = nrrd.read(file_volume, index_order='C') # ml, dv, ap - # we want the coronal slice to be the most contiguous - volume = np.transpose(volume, (2, 0, 1)) # image[iap, iml, idv] - elif file_volume.suffix == '.npz': - volume = np.load(file_volume)['arr_0'] - return volume - - def xyz2ccf(self, xyz, ccf_order='mlapdv', mode='raise'): - """ - Converts anatomical coordinates to CCF coordinates. - - Anatomical coordinates are in meters, relative to bregma, which CFF coordinates are - assumed to be the volume indices multiplied by the spacing in micormeters. - - Parameters - ---------- - xyz : numpy.array - An N by 3 array of anatomical coordinates in meters, relative to bregma. - ccf_order : {'mlapdv', 'apdvml'}, default='mlapdv' - The order of the CCF coordinates returned. For IBL (the default) this is (ML, AP, DV), - for Allen MCC vertices, this is (AP, DV, ML). - mode : {'raise', 'clip', 'wrap'}, default='raise' - How to behave if the coordinate lies outside of the volume: raise (default) will raise - a ValueError; 'clip' will replace the index with the closest index inside the volume; - 'wrap' will return the index as is. - - Returns - ------- - numpy.array - Coordinates in CCF space (um, origin is the front left top corner of the data - volume, order determined by ccf_order - """ - ordre = self._ccf_order(ccf_order) - ccf = self.bc.xyz2i(xyz, round=False, mode=mode) * float(self.res_um) - return ccf[..., ordre] - - def ccf2xyz(self, ccf, ccf_order='mlapdv'): - """ - Convert anatomical coordinates from CCF coordinates. - - Anatomical coordinates are in meters, relative to bregma, which CFF coordinates are - assumed to be the volume indices multiplied by the spacing in micormeters. - - Parameters - ---------- - ccf : numpy.array - An N by 3 array of coordinates in CCF space (atlas volume indices * um resolution). The - origin is the front left top corner of the data volume. - ccf_order : {'mlapdv', 'apdvml'}, default='mlapdv' - The order of the CCF coordinates given. For IBL (the default) this is (ML, AP, DV), - for Allen MCC vertices, this is (AP, DV, ML). - - Returns - ------- - numpy.array - The MLAPDV coordinates in meters, relative to bregma. - """ - ordre = self._ccf_order(ccf_order, reverse=True) - return self.bc.i2xyz((ccf[..., ordre] / float(self.res_um))) - - @staticmethod - def _ccf_order(ccf_order, reverse=False): - """ - Returns the mapping to go from CCF coordinates order to the brain atlas xyz - :param ccf_order: 'mlapdv' or 'apdvml' - :param reverse: defaults to False. - If False, returns from CCF to brain atlas - If True, returns from brain atlas to CCF - :return: - """ - if ccf_order == 'mlapdv': - return [0, 1, 2] - elif ccf_order == 'apdvml': - if reverse: - return [2, 0, 1] - else: - return [1, 2, 0] - else: - ValueError("ccf_order needs to be either 'mlapdv' or 'apdvml'") - - def compute_regions_volume(self, cumsum=False): - """ - Sums the number of voxels in the labels volume for each region. - Then compute volumes for all of the levels of hierarchy in cubic mm. - :param: cumsum: computes the cumulative sum of the volume as per the hierarchy (defaults to False) - :return: - """ - nr = self.regions.id.shape[0] - count = np.bincount(self.label.flatten(), minlength=nr) - if not cumsum: - self.regions.volume = count * (self.res_um / 1e3) ** 3 - else: - self.regions.compute_hierarchy() - self.regions.volume = np.zeros_like(count) - for i in np.arange(nr): - if count[i] == 0: - continue - self.regions.volume[np.unique(self.regions.hierarchy[:, i])] += count[i] - self.regions.volume = self.regions.volume * (self.res_um / 1e3) ** 3 +@deprecated_decorator +def AllenAtlas(*args, **kwargs): + return iblatlas.atlas.AllenAtlas(*args, **kwargs) +@deprecated_decorator def NeedlesAtlas(*args, **kwargs): """ Instantiates an atlas.BrainAtlas corresponding to the Allen CCF at the given resolution @@ -1500,12 +275,11 @@ def NeedlesAtlas(*args, **kwargs): three-dimensional brain atlas using an average magnetic resonance image of 40 adult C57Bl/6J mice. Neuroimage 42(1):60-9. [doi 10.1016/j.neuroimage.2008.03.037] """ - DV_SCALE = 0.952 # multiplicative factor on DV dimension, determined from MRI->CCF transform - AP_SCALE = 1.087 # multiplicative factor on AP dimension - kwargs['scaling'] = np.array([1, AP_SCALE, DV_SCALE]) - return AllenAtlas(*args, **kwargs) + + return iblatlas.atlas.NeedlesAtlas(*args, **kwargs) +@deprecated_decorator def MRITorontoAtlas(*args, **kwargs): """ The MRI Toronto brain atlas. @@ -1532,165 +306,9 @@ def MRITorontoAtlas(*args, **kwargs): relatively larger in males emerge before those larger in females. Nat Commun 9, 2615. [doi 10.1038/s41467-018-04921-2] """ - ML_SCALE = 0.952 - DV_SCALE = 0.885 # multiplicative factor on DV dimension, determined from MRI->CCF transform - AP_SCALE = 1.031 # multiplicative factor on AP dimension - kwargs['scaling'] = np.array([ML_SCALE, AP_SCALE, DV_SCALE]) - return AllenAtlas(*args, **kwargs) - - -def _download_atlas_allen(target_file_image): - """ - Download the Allen Atlas from the alleninstitute.org Website. - - Parameters - ---------- - target_file_image : str, pathlib.Path - The full target file path to which to download the file. The name of the image file name - must be either `average_template_.nrrd` or `annotation_.nrrd`, where is - one of {10, 25, 50}. - - Returns - ------- - pathlib.Path - The full path to the downloaded file. - - Notes - ----- - - © 2015 Allen Institute for Brain Science. Allen Mouse Brain Atlas (2015) with region annotations (2017). - - Available from: http://download.alleninstitute.org/informatics-archive/current-release/mouse_ccf/annotation/ - - See Allen Mouse Common Coordinate Framework Technical White Paper for details - http://help.brain-map.org/download/attachments/8323525/Mouse_Common_Coordinate_Framework.pdf - - """ - (target_file_image := Path(target_file_image)).parent.mkdir(exist_ok=True, parents=True) - ROOT_URL = 'http://download.alleninstitute.org/informatics-archive/' - - if target_file_image.name.split('_')[0] == 'average': - url = f'{ROOT_URL}current-release/mouse_ccf/average_template/' - elif target_file_image.name.split('_')[0] == 'annotation': - url = f'{ROOT_URL}current-release/mouse_ccf/annotation/ccf_2017/' - else: - raise ValueError('Unrecognized file image') - url += target_file_image.name - - return Path(http_download_file(url, target_dir=target_file_image.parent)) - - -class FranklinPaxinosAtlas(BrainAtlas): - - """pathlib.PurePosixPath: The default relative path of the atlas file.""" - atlas_rel_path = PurePosixPath('histology', 'ATLAS', 'Needles', 'FranklinPaxinos') - - def __init__(self, res_um=(10, 100, 10), scaling=(1, 1, 1), mock=False, hist_path=None): - """The Franklin & Paxinos brain atlas. + return iblatlas.atlas.MRITorontoAtlas(*args, **kwargs) - Instantiates an atlas.BrainAtlas corresponding to the Franklin & Paxinos atlas [1]_ at the - given resolution, matched to the Allen coordinate Framework [2]_ and using the IBL Bregma - and coordinate system. The Franklin Paxisnos volume has resolution of 10um in ML and DV - axis and 100 um in AP direction. - Parameters - ---------- - res_um : list, numpy.array - The Atlas resolution in micometres in each dimension. - scaling : float, numpy.array - Scale factor along ml, ap, dv for squeeze and stretch (default: [1, 1, 1]). - mock : bool - For testing purposes, return atlas object with image comprising zeros. - hist_path : str, pathlib.Path - The location of the image volume. May be a full file path or a directory. - - Examples - -------- - Instantiate Atlas from a non-default location, in this case the cache_dir of an ONE instance. - >>> target_dir = one.cache_dir / AllenAtlas.atlas_rel_path - ... ba = FranklinPaxinosAtlas(hist_path=target_dir) - - References - ---------- - .. [1] Paxinos G, and Franklin KBJ (2012) The Mouse Brain in Stereotaxic Coordinates, 4th - edition (Elsevier Academic Press) - .. [2] Chon U et al (2019) Enhanced and unified anatomical labeling for a common mouse - brain atlas [doi 10.1038/s41467-019-13057-w] - """ - # TODO interpolate? - LUT_VERSION = 'v01' # version 01 is the lateralized version - regions = FranklinPaxinosRegions() - xyz2dims = np.array([1, 0, 2]) # this is the c-contiguous ordering - dims2xyz = np.array([1, 0, 2]) - # we use Bregma as the origin - self.res_um = np.asarray(res_um) - ibregma = (PAXINOS_CCF_LANDMARKS_MLAPDV_UM['bregma'] / self.res_um) - dxyz = self.res_um * 1e-6 * np.array([1, -1, -1]) * scaling - if mock: - image, label = [np.zeros((528, 456, 320), dtype=np.int16) for _ in range(2)] - label[:, :, 100:105] = 1327 # lookup index for retina, id 304325711 (no id 1327) - else: - # Hist path may be a full path to an existing image file, or a path to a directory - cache_dir = Path(one.params.get(silent=True).CACHE_DIR) - hist_path = Path(hist_path or cache_dir.joinpath(self.atlas_rel_path)) - if not hist_path.suffix: # check if folder - hist_path /= f'average_template_{res_um[0]}_{res_um[1]}_{res_um[2]}.npz' - - # get the image volume - if not hist_path.exists(): - hist_path.parent.mkdir(exist_ok=True, parents=True) - aws.s3_download_file(f'atlas/FranklinPaxinos/{hist_path.name}', str(hist_path)) - # get the remapped label volume - file_label = hist_path.with_name(f'annotation_{res_um[0]}_{res_um[1]}_{res_um[2]}.npz') - if not file_label.exists(): - file_label.parent.mkdir(exist_ok=True, parents=True) - aws.s3_download_file(f'atlas/FranklinPaxinos/{file_label.name}', str(file_label)) - - file_label_remap = hist_path.with_name(f'annotation_{res_um[0]}_{res_um[1]}_{res_um[2]}_lut_{LUT_VERSION}.npz') - - if not file_label_remap.exists(): - label = self._read_volume(file_label).astype(dtype=np.int32) - _logger.info("computing brain atlas annotations lookup table") - # lateralize atlas: for this the regions of the left hemisphere have primary - # keys opposite to to the normal ones - lateral = np.zeros(label.shape[xyz2dims[0]]) - lateral[int(np.floor(ibregma[0]))] = 1 - lateral = np.sign(np.cumsum(lateral)[np.newaxis, :, np.newaxis] - 0.5) - label = label * lateral.astype(np.int32) - _, im = ismember(label, regions.id) - label = np.reshape(im.astype(np.uint16), label.shape) - np.savez_compressed(file_label_remap, label) - _logger.info(f"Cached remapping file {file_label_remap} ...") - # loads the files - label = self._read_volume(file_label_remap) - image = self._read_volume(hist_path) - - super().__init__(image, label, dxyz, regions, ibregma, dims2xyz=dims2xyz, xyz2dims=xyz2dims) - - @staticmethod - def _read_volume(file_volume): - """ - Loads an atlas image volume given a file path. - - Parameters - ---------- - file_volume : pathlib.Path - The file path of an image volume. Currently supports .nrrd and .npz files. - - Returns - ------- - numpy.array - The loaded image volume with dimensions (ap, ml, dv). - - Raises - ------ - ValueError - Unknown file extension, expects either '.nrrd' or '.npz'. - """ - if file_volume.suffix == '.nrrd': - volume, _ = nrrd.read(file_volume, index_order='C') # ml, dv, ap - # we want the coronal slice to be the most contiguous - volume = np.transpose(volume, (2, 0, 1)) # image[iap, iml, idv] - elif file_volume.suffix == '.npz': - volume = np.load(file_volume)['arr_0'] - else: - raise ValueError( - f'"{file_volume.suffix}" files not supported, must be either ".nrrd" or ".npz"') - return volume +@deprecated_decorator +def FranklinPaxinosAtlas(*args, **kwargs): + return iblatlas.atlas.FranklinPaxinosAtlas(*args, **kwargs) diff --git a/ibllib/atlas/flatmaps.py b/ibllib/atlas/flatmaps.py index 782fba4c6..4dcedc764 100644 --- a/ibllib/atlas/flatmaps.py +++ b/ibllib/atlas/flatmaps.py @@ -1,122 +1,15 @@ """Techniques to project the brain volume onto 2D images for visualisation purposes.""" -from functools import lru_cache -import logging -import json -import nrrd -import numpy as np -from scipy.interpolate import interp1d -import matplotlib.pyplot as plt +from ibllib.atlas import deprecated_decorator +from iblatlas import flatmaps -from iblutil.util import Bunch -from iblutil.io.hashfile import md5 -import one.remote.aws as aws -from ibllib.atlas.atlas import AllenAtlas +@deprecated_decorator +def FlatMap(**kwargs): + return flatmaps.FlatMap(**kwargs) -_logger = logging.getLogger(__name__) - - -class FlatMap(AllenAtlas): - """The Allen Atlas flatmap. - - FIXME Document! How are these flatmaps determined? Are they related to the Swansan atlas or is - that something else? - """ - - def __init__(self, flatmap='dorsal_cortex', res_um=25): - """ - Available flatmaps are currently 'dorsal_cortex', 'circles' and 'pyramid' - :param flatmap: - :param res_um: - """ - super().__init__(res_um=res_um) - self.name = flatmap - if flatmap == 'dorsal_cortex': - self._get_flatmap_from_file() - elif flatmap == 'circles': - if res_um != 25: - raise NotImplementedError('Pyramid circles not implemented for resolution other than 25um') - self.flatmap, self.ml_scale, self.ap_scale = circles(N=5, atlas=self, display='flat') - elif flatmap == 'pyramid': - if res_um != 25: - raise NotImplementedError('Pyramid circles not implemented for resolution other than 25um') - self.flatmap, self.ml_scale, self.ap_scale = circles(N=5, atlas=self, display='pyramid') - - def _get_flatmap_from_file(self): - # gets the file in the ONE cache for the flatmap name in the property, downloads it if needed - file_flatmap = self._get_cache_dir().joinpath(f'{self.name}_{self.res_um}.nrrd') - if not file_flatmap.exists(): - file_flatmap.parent.mkdir(exist_ok=True, parents=True) - aws.s3_download_file(f'atlas/{file_flatmap.name}', file_flatmap) - self.flatmap, _ = nrrd.read(file_flatmap) - - def plot_flatmap(self, depth=0, volume='annotation', mapping='Allen', region_values=None, ax=None, **kwargs): - """ - Displays the 2D image corresponding to the flatmap. - - If there are several depths, by default it will display the first one. - - Parameters - ---------- - depth : int - Index of the depth to display in the flatmap volume (the last dimension). - volume : {'image', 'annotation', 'boundary', 'value'} - - 'image' - Allen image volume. - - 'annotation' - Allen annotation volume. - - 'boundary' - outline of boundaries between all regions. - - 'volume' - custom volume, must pass in volume of shape BrainAtlas.image.shape as - regions_value argument. - mapping : str, default='Allen' - The brain region mapping to use. - region_values : numpy.array - An array the shape of the brain atlas image containing custom region values. Used when - `volume` value is 'volume'. - ax : matplotlib.pyplot.Axes, optional - A set of axes to plot to. - **kwargs - See matplotlib.pyplot.imshow. - - Returns - ------- - matplotlib.pyplot.Axes - The plotted image axes. - """ - if self.flatmap.ndim == 3: - inds = np.int32(self.flatmap[:, :, depth]) - else: - inds = np.int32(self.flatmap[:, :]) - regions = self._get_mapping(mapping=mapping)[self.label.flat[inds]] - if volume == 'annotation': - im = self._label2rgb(regions) - elif volume == 'value': - im = region_values[regions] - elif volume == 'boundary': - im = self.compute_boundaries(regions) - elif volume == 'image': - im = self.image.flat[inds] - else: - raise ValueError(f'Volume type "{volume}" not supported') - if not ax: - ax = plt.gca() - - return self._plot_slice(im, self.extent_flmap(), ax=ax, volume=volume, **kwargs) - - def extent_flmap(self): - """ - Returns the boundary coordinates of the flat map. - - Returns - ------- - numpy.array - The bounding coordinates of the flat map image, specified as (left, right, bottom, top). - """ - extent = np.r_[0, self.flatmap.shape[1], 0, self.flatmap.shape[0]] - return extent - - -@lru_cache(maxsize=1, typed=False) +@deprecated_decorator def circles(N=5, atlas=None, display='flat'): """ :param N: number of circles @@ -124,94 +17,11 @@ def circles(N=5, atlas=None, display='flat'): :param display: "flat" or "pyramid" :return: 2D map of indices, ap_coordinate, ml_coordinate """ - atlas = atlas if atlas else AllenAtlas() - - sz = np.array([]) - level = np.array([]) - for k in np.arange(N): - nlast = 2000 # 25 um for 5mm diameter - n = int((k + 1) * nlast / N) - r = .4 * (k + 1) / N - theta = (np.linspace(0, 2 * np.pi, n) + np.pi / 2) - sz = np.r_[sz, r * np.exp(1j * theta)] - level = np.r_[level, theta * 0 + k] - - atlas.compute_surface() - iy, ix = np.where(~np.isnan(atlas.top)) - centroid = np.array([np.mean(iy), np.mean(ix)]) - xlim = np.array([np.min(ix), np.max(ix)]) - ylim = np.array([np.min(iy), np.max(iy)]) - - s = Bunch( - x=np.real(sz) * np.diff(xlim) + centroid[1], - y=np.imag(sz) * np.diff(ylim) + centroid[0], - level=level, - distance=level * 0, - ) - - # compute the overall linear distance for each circle - d0 = 0 - for lev in np.unique(s['level']): - ind = s['level'] == lev - diff = np.abs(np.diff(s['x'][ind] + 1j * s['y'][ind])) - s['distance'][ind] = np.cumsum(np.r_[0, diff]) + d0 - d0 = s['distance'][ind][-1] - - fcn = interp1d(s['distance'], s['x'] + 1j * s['y'], fill_value='extrap') - d = np.arange(0, np.ceil(s['distance'][-1])) - - s_ = Bunch({ - 'x': np.real(fcn(d)), - 'y': np.imag(fcn(d)), - 'level': interp1d(s['distance'], level, kind='nearest')(d), - 'distance': d - }) - if display == 'flat': - ih = np.arange(atlas.bc.nz) - iw = np.arange(s_['distance'].size) - image_map = np.zeros((ih.size, iw.size), dtype=np.int32) - iw, ih = np.meshgrid(iw, ih) - # i2d = np.ravel_multi_index((ih[:], iw[:]), image_map.shape) - iml, _ = np.meshgrid(np.round(s_.x).astype(np.int32), np.arange(atlas.bc.nz)) - iap, idv = np.meshgrid(np.round(s_.y).astype(np.int32), np.arange(atlas.bc.nz)) - i3d = atlas._lookup_inds(np.c_[iml.flat, iap.flat, idv.flat]) - i3d = np.reshape(i3d, [atlas.bc.nz, s_['x'].size]) - image_map[ih, iw] = i3d - - elif display == 'pyramid': - for i in np.flipud(np.arange(N)): - ind = s_['level'] == i - dtot = s_['distance'][ind] - dtot = dtot - np.mean(dtot) - if i == N - 1: - ipx = np.arange(np.floor(dtot[0]), np.ceil(dtot[-1]) + 1) - nh = atlas.bc.nz * N - X0 = int(ipx[-1]) - image_map = np.zeros((nh, ipx.size), dtype=np.int32) - - iw = np.arange(np.sum(ind)) - iw = np.int32(iw - np.mean(iw) + X0) - ih = atlas.bc.nz * i + np.arange(atlas.bc.nz) - - iw, ih = np.meshgrid(iw, ih) - iml, _ = np.meshgrid(np.round(s_.x[ind]).astype(np.int32), np.arange(atlas.bc.nz)) - iap, idv = np.meshgrid(np.round(s_.y[ind]).astype(np.int32), np.arange(atlas.bc.nz)) - i3d = atlas._lookup_inds(np.c_[iml.flat, iap.flat, idv.flat]) - i3d = np.reshape(i3d, [atlas.bc.nz, s_['x'][ind].size]) - image_map[ih, iw] = i3d - x, y = (atlas.bc.i2x(s.x), atlas.bc.i2y(s.y)) - return image_map, x, y - # if display == 'flat': - # fig, ax = plt.subplots(2, 1, figsize=(16, 5)) - # elif display == 'pyramid': - # fig, ax = plt.subplots(1, 2, figsize=(14, 12)) - # ax[0].imshow(ba._label2rgb(ba.label.flat[image_map]), origin='upper') - # ax[1].imshow(ba.top) - # ax[1].plot(centroid[1], centroid[0], '*') - # ax[1].plot(s.x, s.y) + return flatmaps.circles(N=N, atlas=atlas, display=display) +@deprecated_decorator def swanson(filename="swanson2allen.npz"): """ FIXME Document! Which publication to reference? Are these specifically for flat maps? @@ -225,21 +35,11 @@ def swanson(filename="swanson2allen.npz"): ------- """ - # filename could be "swanson2allen_original.npz", or "swanson2allen.npz" for remapped indices to match - # existing labels in the brain atlas - OLD_MD5 = [ - 'bb0554ecc704dd4b540151ab57f73822', # version 2022-05-02 (remapped) - '7722c1307cf9a6f291ad7632e5dcc88b', # version 2022-05-09 (removed wolf pixels and 2 artefact regions) - ] - npz_file = AllenAtlas._get_cache_dir().joinpath(filename) - if not npz_file.exists() or md5(npz_file) in OLD_MD5: - npz_file.parent.mkdir(exist_ok=True, parents=True) - _logger.info(f'downloading swanson image from {aws.S3_BUCKET_IBL} s3 bucket...') - aws.s3_download_file(f'atlas/{npz_file.name}', npz_file) - s2a = np.load(npz_file)['swanson2allen'] # inds contains regions ids - return s2a + return flatmaps.swanson(filename=filename) + +@deprecated_decorator def swanson_json(filename="swansonpaths.json", remap=True): """ Vectorized version of the swanson bitmap file. The vectorized version was generated from swanson() using matlab @@ -255,98 +55,4 @@ def swanson_json(filename="swansonpaths.json", remap=True): ------- """ - OLD_MD5 = ['97ccca2b675b28ba9b15ca8af5ba4111', # errored map with FOTU and CUL4, 5 mixed up - '56daa7022b5e03080d8623814cda6f38', # old md5 of swanson json without CENT and PTLp - # and CUL4 split (on s3 called swansonpaths_56daa.json) - 'f848783954883c606ca390ceda9e37d2'] - - json_file = AllenAtlas._get_cache_dir().joinpath(filename) - if not json_file.exists() or md5(json_file) in OLD_MD5: - json_file.parent.mkdir(exist_ok=True, parents=True) - _logger.info(f'downloading swanson paths from {aws.S3_BUCKET_IBL} s3 bucket...') - aws.s3_download_file(f'atlas/{json_file.name}', json_file, overwrite=True) - - with open(json_file) as f: - sw_json = json.load(f) - - # The swanson contains regions that are children of regions contained within the Allen - # annotation volume. Here we remap these regions to the parent that is contained with the - # annotation volume - if remap: - id_map = {391: [392, 393, 394, 395, 396], - 474: [483, 487], - 536: [537, 541], - 601: [602, 603, 604, 608], - 622: [624, 625, 626, 627, 628, 629, 630, 631, 632, 634, 635, 636, 637, 638], - 686: [687, 688, 689], - 708: [709, 710], - 721: [723, 724, 726, 727, 729, 730, 731], - 740: [741, 742, 743], - 758: [759, 760, 761, 762], - 771: [772, 773], - 777: [778, 779, 780], - 788: [789, 790, 791, 792], - 835: [836, 837, 838], - 891: [894, 895, 896, 897, 898, 900, 901, 902], - 926: [927, 928], - 949: [950, 951, 952, 953, 954], - 957: [958, 959, 960, 961, 962], - 999: [1000, 1001], - 578: [579, 580]} - - rev_map = {} - for k, vals in id_map.items(): - for v in vals: - rev_map[v] = k - - for sw in sw_json: - sw['thisID'] = rev_map.get(sw['thisID'], sw['thisID']) - - return sw_json - - -@lru_cache(maxsize=None) -def _swanson_labels_positions(thres=20000): - """ - Computes label positions to overlay on the Swanson flatmap. - - Parameters - ---------- - thres : int, default=20000 - The number of pixels above which a region is labeled. - - Returns - ------- - dict of str - A map of brain acronym to a tuple of x y coordinates. - """ - s2a = swanson() - iw, ih = np.meshgrid(np.arange(s2a.shape[1]), np.arange(s2a.shape[0])) - # compute the center of mass of all regions (fast enough to do on the fly) - bc = np.maximum(1, np.bincount(s2a.flatten())) - cmw = np.bincount(s2a.flatten(), weights=iw.flatten()) / bc - cmh = np.bincount(s2a.flatten(), weights=ih.flatten()) / bc - bc[0] = 1 - - NWH, NWW = (200, 600) - h, w = s2a.shape - labels = {} - for ilabel in np.where(bc > thres)[0]: - x, y = (cmw[ilabel], cmh[ilabel]) - # the polygon is convex and the label is outside. Dammit !!! - if s2a[int(y), int(x)] != ilabel: - # find the nearest point to the center of mass - ih, iw = np.where(s2a == ilabel) - iimin = np.argmin(np.abs((x - iw) + 1j * (y - ih))) - # get the center of mass of a window around this point - sh = np.arange(np.maximum(0, ih[iimin] - NWH), np.minimum(ih[iimin] + NWH, h)) - sw = np.arange(np.maximum(0, iw[iimin] - NWW), np.minimum(iw[iimin] + NWW, w)) - roi = s2a[sh][:, sw] == ilabel - roi = roi / np.sum(roi) - # ax.plot(x, y, 'k+') - # ax.plot(iw[iimin], ih[iimin], '*k') - x = sw[np.searchsorted(np.cumsum(np.sum(roi, axis=0)), .5) - 1] - y = sh[np.searchsorted(np.cumsum(np.sum(roi, axis=1)), .5) - 1] - # ax.plot(x, y, 'r+') - labels[ilabel] = (x, y) - return labels + return flatmaps.swanson_json(filename=filename, remap=remap) diff --git a/ibllib/atlas/genes.py b/ibllib/atlas/genes.py index e3f32e378..c2261d5c5 100644 --- a/ibllib/atlas/genes.py +++ b/ibllib/atlas/genes.py @@ -1,18 +1,10 @@ """Gene expression maps.""" -import logging -from pathlib import Path -import numpy as np -import pandas as pd - -from iblutil.io.hashfile import md5 -import one.remote.aws as aws - -from ibllib.atlas.atlas import AllenAtlas - -_logger = logging.getLogger(__name__) +from iblatlas import genes +from ibllib.atlas import deprecated_decorator +@deprecated_decorator def allen_gene_expression(filename='gene-expression.pqt', folder_cache=None): """ Reads in the Allen gene expression experiments binary data. @@ -22,17 +14,6 @@ def allen_gene_expression(filename='gene-expression.pqt', folder_cache=None): and a memmap of all experiments volumes, size (4345, 58, 41, 67) corresponding to (nexperiments, ml, dv, ap). The spacing between slices is 200 um """ - OLD_MD5 = [] - DIM_EXP = (4345, 58, 41, 67) - folder_cache = folder_cache or AllenAtlas._get_cache_dir().joinpath(filename) - file_parquet = Path(folder_cache).joinpath('gene-expression.pqt') - file_bin = file_parquet.with_suffix(".bin") - if not file_parquet.exists() or md5(file_parquet) in OLD_MD5: - file_parquet.parent.mkdir(exist_ok=True, parents=True) - _logger.info(f'downloading gene expression data from {aws.S3_BUCKET_IBL} s3 bucket...') - aws.s3_download_file(f'atlas/{file_parquet.name}', file_parquet) - aws.s3_download_file(f'atlas/{file_bin.name}', file_bin) - df_genes = pd.read_parquet(file_parquet) - gexp_all = np.memmap(file_bin, dtype=np.float16, mode='r', offset=0, shape=DIM_EXP) - return df_genes, gexp_all + return genes.allen_gene_expression(filename=filename, folder_cache=folder_cache) + diff --git a/ibllib/atlas/plots.py b/ibllib/atlas/plots.py index 9c5926dcb..c6e691877 100644 --- a/ibllib/atlas/plots.py +++ b/ibllib/atlas/plots.py @@ -1,150 +1,12 @@ """ Module that has convenience plotting functions for 2D atlas slices and flatmaps. """ -import copy -import logging -import numpy as np -from scipy.ndimage import gaussian_filter -from scipy.stats import binned_statistic -import matplotlib.pyplot as plt -from matplotlib import cm, colors -from matplotlib.patches import Polygon, PathPatch -import matplotlib.path as mpath -from iblutil.io.hashfile import md5 -import one.remote.aws as aws - -from ibllib.atlas import AllenAtlas -from ibllib.atlas.flatmaps import FlatMap, _swanson_labels_positions, swanson, swanson_json -from ibllib.atlas.regions import BrainRegions -from iblutil.numerical import ismember -from ibllib.atlas.atlas import BrainCoordinates, ALLEN_CCF_LANDMARKS_MLAPDV_UM - -_logger = logging.getLogger(__name__) - - -def get_bc_10(): - """ - Get BrainCoordinates object for 10um Allen Atlas - - Returns - ------- - BrainCoordinates object - """ - dims2xyz = np.array([1, 0, 2]) - res_um = 10 - scaling = np.array([1, 1, 1]) - image_10 = np.array([1320, 1140, 800]) - - iorigin = (ALLEN_CCF_LANDMARKS_MLAPDV_UM['bregma'] / res_um) - dxyz = res_um * 1e-6 * np.array([1, -1, -1]) * scaling - nxyz = np.array(image_10)[dims2xyz] - bc = BrainCoordinates(nxyz=nxyz, xyz0=(0, 0, 0), dxyz=dxyz) - bc = BrainCoordinates(nxyz=nxyz, xyz0=-bc.i2xyz(iorigin), dxyz=dxyz) - - return bc - - -def plot_polygon(ax, xy, color, reg_id, edgecolor='k', linewidth=0.3, alpha=1): - """ - Function to plot matplotlib polygon on an axis - - Parameters - ---------- - ax : matplotlib.pyplot.Axes - An axis object to plot onto. - xy: numpy.array - 2D array of x and y coordinates of vertices of polygon - color: str, tuple of int - The color to fill the polygon - reg_id: str, int - An id to assign to the polygon - edgecolor: str, tuple of int - The color of the edge of the polgon - linewidth: int - The width of the edges of the polygon - alpha: float between 0 and 1 - The opacitiy of the polygon - - Returns - ------- - - """ - p = Polygon(xy, facecolor=color, edgecolor=edgecolor, linewidth=linewidth, alpha=alpha, gid=f'region_{reg_id}') - ax.add_patch(p) - - -def plot_polygon_with_hole(ax, vertices, codes, color, reg_id, edgecolor='k', linewidth=0.3, alpha=1): - """ - Function to plot matplotlib polygon that contains a hole on an axis - - Parameters - ---------- - ax : matplotlib.pyplot.Axes - An axis object to plot onto. - vertices: numpy.array - 2D array of x and y coordinates of vertices of polygon - codes: numpy.array - 1D array of path codes used to link the vertices - (https://matplotlib.org/stable/tutorials/advanced/path_tutorial.html) - color: str, tuple of int - The color to fill the polygon - reg_id: str, int - An id to assign to the polygon - edgecolor: str, tuple of int - The color of the edge of the polgon - linewidth: int - The width of the edges of the polygon - alpha: float between 0 and 1 - The opacitiy of the polygon - - Returns - ------- - - """ - - path = mpath.Path(vertices, codes) - patch = PathPatch(path, facecolor=color, edgecolor=edgecolor, linewidth=linewidth, alpha=alpha, gid=f'region_{reg_id}') - ax.add_patch(patch) - - -def coords_for_poly_hole(coords): - """ - Function to convert - - Parameters - ---------- - coords : dict - Dictionary containing keys x, y and invert. x and y contain numpy.array of x coordinates, y coordinates - for the vertices of the polgyon. The invert key is either 1 or -1 and deterimine how to assign the paths. - The value for invert for each polygon was assigned manually after looking at the result - - Returns - ------- - all_coords: numpy.array - 2D array of x and y coordinates of vertices of polygon - all_codes: numpy.array - 1D array of path codes used to link the vertices - (https://matplotlib.org/stable/tutorials/advanced/path_tutorial.html) - - """ - for i, c in enumerate(coords): - xy = np.c_[c['x'], c['y']] - codes = np.ones(len(xy), dtype=mpath.Path.code_type) * mpath.Path.LINETO - codes[0] = mpath.Path.MOVETO - if i == 0: - val = c.get('invert', 1) - all_coords = xy[::val] - all_codes = codes - else: - codes[-1] = mpath.Path.CLOSEPOLY - val = c.get('invert', -1) - all_coords = np.concatenate((all_coords, xy[::val])) - all_codes = np.concatenate((all_codes, codes)) - - return all_coords, all_codes +import iblatlas.plots as atlas_plots +from ibllib.atlas import deprecated_decorator +@deprecated_decorator def prepare_lr_data(acronyms_lh, values_lh, acronyms_rh, values_rh): """ Prepare data in format needed for plotting when providing different region values per hemisphere @@ -156,16 +18,10 @@ def prepare_lr_data(acronyms_lh, values_lh, acronyms_rh, values_rh): :return: combined acronyms and two column array of values """ - acronyms = np.unique(np.r_[acronyms_lh, acronyms_rh]) - values = np.nan * np.ones((acronyms.shape[0], 2)) - _, l_idx = ismember(acronyms_lh, acronyms) - _, r_idx = ismember(acronyms_rh, acronyms) - values[l_idx, 0] = values_lh - values[r_idx, 1] = values_rh - - return acronyms, values + return atlas_plots.prepare_lr_data(acronyms_lh, values_lh, acronyms_rh, values_rh) +@deprecated_decorator def reorder_data(acronyms, values, brain_regions=None): """ Reorder list of acronyms and values to match the Allen ordering. @@ -189,178 +45,10 @@ def reorder_data(acronyms, values, brain_regions=None): An ordered array of values. I don't know what those values are, not IDs, so maybe indices? """ - br = brain_regions or BrainRegions() - atlas_id = br.acronym2id(acronyms, hemisphere='right') - all_ids = br.id[br.order][:br.n_lr + 1] - ordered_ids = np.zeros_like(all_ids) * np.nan - ordered_values = np.zeros_like(all_ids) * np.nan - _, idx = ismember(atlas_id, all_ids) - ordered_ids[idx] = atlas_id - ordered_values[idx] = values - - ordered_ids = ordered_ids[~np.isnan(ordered_ids)] - ordered_values = ordered_values[~np.isnan(ordered_values)] - ordered_acronyms = br.id2acronym(ordered_ids) - - return ordered_acronyms, ordered_values - - -def load_slice_files(slice, mapping): - """ - Function to load in set of vectorised atlas slices for a given atlas axis and mapping. - - If the data does not exist locally, it will download the files automatically stored in a AWS S3 - bucket. - - Parameters - ---------- - slice : {'coronal', 'sagittal', 'horizontal', 'top'} - The axis of the atlas to load. - mapping : {'Allen', 'Beryl', 'Cosmos'} - The mapping to load. - - Returns - ------- - slice_data : numpy.array - A json containing the vertices to draw each region for each slice in the Allen annotation volume. - - """ - OLD_MD5 = { - 'coronal': [], - 'sagittal': [], - 'horizontal': [], - 'top': [] - } - - slice_file = AllenAtlas._get_cache_dir().parent.joinpath('svg', f'{slice}_{mapping}_paths.npy') - if not slice_file.exists() or md5(slice_file) in OLD_MD5[slice]: - slice_file.parent.mkdir(exist_ok=True, parents=True) - _logger.info(f'downloading swanson paths from {aws.S3_BUCKET_IBL} s3 bucket...') - aws.s3_download_file(f'atlas/{slice_file.name}', slice_file) - - slice_data = np.load(slice_file, allow_pickle=True) - - return slice_data - - -def _plot_slice_vector(coords, slice, values, mapping, empty_color='silver', clevels=None, cmap='viridis', show_cbar=False, - ba=None, ax=None, slice_json=None, **kwargs): - """ - Function to plot scalar value per allen region on vectorised version of histology slice. Do not use directly but use - through plot_scalar_on_slice function with vector=True. - - Parameters - ---------- - coords: float - Coordinate of slice in um (not needed when slice='top'). - slice: {'coronal', 'sagittal', 'horizontal', 'top'} - The axis through the atlas volume to display. - values: numpy.array - Array of values for each of the lateralised Allen regions found using BrainRegions().acronym. If no - value is assigned to the acronym, the value at corresponding to that index should be NaN. - mapping: {'Allen', 'Beryl', 'Cosmos'} - The mapping to use. - empty_color: str, tuple of int, default='silver' - The color used to fill the regions that do not have any values assigned (regions with NaN). - clevels: numpy.array, list or tuple - The min and max values to use for the colormap. - cmap: string - Colormap to use. - show_cbar: bool, default=False - Whether to display a colorbar. - ba : ibllib.atlas.AllenAtlas - A brain atlas object. - ax : matplotlib.pyplot.Axes - An axis object to plot onto. - slice_json: numpy.array - The set of vectorised slices for this slice, obtained using load_slice_files(slice, mapping). - **kwargs - Set of kwargs passed into matplotlib.patches.Polygon. - - Returns - ------- - fig: matplotlib.figure.Figure - The plotted figure. - ax: matplotlib.pyplot.Axes - The plotted axes. - cbar: matplotlib.pyplot.colorbar, optional - matplotlib colorbar object, only returned if show_cbar=True - - """ - ba = ba or AllenAtlas() - mapping = mapping.split('-')[0].lower() - if clevels is None: - clevels = (np.nanmin(values), np.nanmax(values)) - - if ba.res_um == 10: - bc10 = ba.bc - else: - bc10 = get_bc_10() - - if ax is None: - fig, ax = plt.subplots() - ax.set_axis_off() - else: - fig = ax.get_figure() - - colormap = cm.get_cmap(cmap) - norm = colors.Normalize(vmin=clevels[0], vmax=clevels[1]) - nan_vals = np.isnan(values) - rgba_color = np.full((values.size, 4), fill_value=np.nan) - rgba_color[~nan_vals] = colormap(norm(values[~nan_vals]), bytes=True) - - if slice_json is None: - slice_json = load_slice_files(slice, mapping) - - if slice == 'coronal': - idx = bc10.y2i(coords) - xlim = np.array([0, bc10.nx]) - ylim = np.array([0, bc10.nz]) - elif slice == 'sagittal': - idx = bc10.x2i(coords) - xlim = np.array([0, bc10.ny]) - ylim = np.array([0, bc10.nz]) - elif slice == 'horizontal': - idx = bc10.z2i(coords) - xlim = np.array([0, bc10.nx]) - ylim = np.array([0, bc10.ny]) - else: - # top case - xlim = np.array([0, bc10.nx]) - ylim = np.array([0, bc10.ny]) - - if slice != 'top': - slice_json = slice_json.item().get(str(int(idx))) - - for i, reg in enumerate(slice_json): - color = rgba_color[reg['thisID']] - if any(np.isnan(color)): - color = empty_color - else: - color = color / 255 - coords = reg['coordsReg'] - - if len(coords) == 0: - continue - - if isinstance(coords, (list, tuple)): - vertices, codes = coords_for_poly_hole(coords) - plot_polygon_with_hole(ax, vertices, codes, color, **kwargs) - else: - xy = np.c_[coords['x'], coords['y']] - plot_polygon(ax, xy, color, **kwargs) - - ax.set_xlim(xlim) - ax.set_ylim(ylim) - ax.invert_yaxis() - - if show_cbar: - cbar = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) - return fig, ax, cbar - else: - return fig, ax + return atlas_plots.reorder_data(acronyms, values, brain_regions=brain_regions) +@deprecated_decorator def plot_scalar_on_slice(regions, values, coord=-1000, slice='coronal', mapping=None, hemisphere='left', background='image', cmap='viridis', clevels=None, show_cbar=False, empty_color='silver', brain_atlas=None, ax=None, vector=False, slice_files=None, **kwargs): @@ -417,56 +105,13 @@ def plot_scalar_on_slice(regions, values, coord=-1000, slice='coronal', mapping= matplotlib colorbar object, only returned if show_cbar=True. """ - ba = brain_atlas or AllenAtlas() - br = ba.regions - mapping = mapping or br.default_mapping - - if clevels is None: - clevels = (np.nanmin(values), np.nanmax(values)) - - # Find the mapping to use - if '-lr' in mapping: - map = mapping - else: - map = mapping + '-lr' - - region_values = np.zeros_like(br.id) * np.nan - - if len(values.shape) == 2: - for r, vL, vR in zip(regions, values[:, 0], values[:, 1]): - idx = np.where(br.acronym[br.mappings[map]] == r)[0] - idx_lh = idx[idx > br.n_lr] - idx_rh = idx[idx <= br.n_lr] - region_values[idx_rh] = vR - region_values[idx_lh] = vL - else: - for r, v in zip(regions, values): - region_values[np.where(br.acronym[br.mappings[map]] == r)[0]] = v - if hemisphere == 'left': - region_values[0:(br.n_lr + 1)] = np.nan - elif hemisphere == 'right': - region_values[br.n_lr:] = np.nan - region_values[0] = np.nan - - if show_cbar: - if vector: - fig, ax, cbar = _plot_slice_vector(coord / 1e6, slice, region_values, map, clevels=clevels, cmap=cmap, ba=ba, - ax=ax, empty_color=empty_color, show_cbar=show_cbar, slice_json=slice_files, - **kwargs) - else: - fig, ax, cbar = _plot_slice(coord / 1e6, slice, region_values, 'value', background=background, map=map, - clevels=clevels, cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax, cbar - else: - if vector: - fig, ax = _plot_slice_vector(coord / 1e6, slice, region_values, map, clevels=clevels, cmap=cmap, ba=ba, - ax=ax, empty_color=empty_color, show_cbar=show_cbar, slice_json=slice_files, **kwargs) - else: - fig, ax = _plot_slice(coord / 1e6, slice, region_values, 'value', background=background, map=map, clevels=clevels, - cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax + return (atlas_plots.plot_scalar_on_slice(regions, values, coord=coord, slice=slice, mapping=mapping, + hemisphere=hemisphere, background=background, cmap=cmap, clevels=clevels, + show_cbar=show_cbar, empty_color=empty_color, brain_atlas=brain_atlas, + ax=ax, vector=vector, slice_files=slice_files, **kwargs)) +@deprecated_decorator def plot_scalar_on_flatmap(regions, values, depth=0, flatmap='dorsal_cortex', mapping='Allen', hemisphere='left', background='boundary', cmap='viridis', clevels=None, show_cbar=False, flmap_atlas=None, ax=None): """ @@ -488,71 +133,12 @@ def plot_scalar_on_flatmap(regions, values, depth=0, flatmap='dorsal_cortex', ma :return: """ - if clevels is None: - clevels = (np.nanmin(values), np.nanmax(values)) - - ba = flmap_atlas or FlatMap(flatmap=flatmap) - br = ba.regions - - # Find the mapping to use - if '-lr' in mapping: - map = mapping - else: - map = mapping + '-lr' - - region_values = np.zeros_like(br.id) * np.nan - - if len(values.shape) == 2: - for r, vL, vR in zip(regions, values[:, 0], values[:, 1]): - idx = np.where(br.acronym[br.mappings[map]] == r)[0] - idx_lh = idx[idx > br.n_lr] - idx_rh = idx[idx <= br.n_lr] - region_values[idx_rh] = vR - region_values[idx_lh] = vL - else: - for r, v in zip(regions, values): - region_values[np.where(br.acronym[br.mappings[map]] == r)[0]] = v - if hemisphere == 'left': - region_values[0:(br.n_lr + 1)] = np.nan - elif hemisphere == 'right': - region_values[br.n_lr:] = np.nan - region_values[0] = np.nan - - d_idx = int(np.round(depth / ba.res_um)) # need to find nearest to 25 - - if background == 'boundary': - cmap_bound = cm.get_cmap("bone_r").copy() - cmap_bound.set_under([1, 1, 1], 0) - - if ax: - fig = ax.get_figure() - else: - fig, ax = plt.subplots() - - if background == 'image': - ba.plot_flatmap(d_idx, volume='image', mapping=map, ax=ax) - ba.plot_flatmap(d_idx, volume='value', region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - else: - ba.plot_flatmap(d_idx, volume='value', region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - ba.plot_flatmap(d_idx, volume='boundary', mapping=map, ax=ax, cmap=cmap_bound, vmin=0.01, vmax=0.8) - - # For circle flatmap we don't want to cut the axis - if ba.name != 'circles': - if hemisphere == 'left': - ax.set_xlim(0, np.ceil(ba.flatmap.shape[1] / 2)) - elif hemisphere == 'right': - ax.set_xlim(np.ceil(ba.flatmap.shape[1] / 2), ba.flatmap.shape[1]) - - if show_cbar: - norm = colors.Normalize(vmin=clevels[0], vmax=clevels[1], clip=False) - cbar = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) - return fig, ax, cbar - else: - return fig, ax + return atlas_plots.plot_scalar_on_slice(regions, values, depth=depth, flatmap=flatmap, mapping=mapping, + hemisphere=hemisphere, background=background, cmap=cmap, clevels=clevels, + show_cbar=show_cbar, flmap_atlas=flmap_atlas, ax=ax) +@deprecated_decorator def plot_volume_on_slice(volume, coord=-1000, slice='coronal', mapping='Allen', background='boundary', cmap='Reds', clevels=None, show_cbar=False, brain_atlas=None, ax=None): """ @@ -571,25 +157,12 @@ def plot_volume_on_slice(volume, coord=-1000, slice='coronal', mapping='Allen', :return: """ - ba = brain_atlas or AllenAtlas() - assert volume.shape == ba.image.shape, 'Volume must have same shape as ba' - - # Find the mapping to use - if '-lr' in mapping: - map = mapping - else: - map = mapping + '-lr' - - if show_cbar: - fig, ax, cbar = _plot_slice(coord / 1e6, slice, volume, 'volume', background=background, map=map, clevels=clevels, - cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax, cbar - else: - fig, ax = _plot_slice(coord / 1e6, slice, volume, 'volume', background=background, map=map, clevels=clevels, - cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax + return atlas_plots.plot_volume_on_slice(volume, coord=coord, slice=slice, mapping=mapping, background=background, + cmap=cmap, clevels=clevels, show_cbar=show_cbar, brain_atlas=brain_atlas, + ax=ax) +@deprecated_decorator def plot_points_on_slice(xyz, values=None, coord=-1000, slice='coronal', mapping='Allen', background='boundary', cmap='Reds', clevels=None, show_cbar=False, aggr='mean', fwhm=100, brain_atlas=None, ax=None): """ @@ -615,170 +188,12 @@ def plot_points_on_slice(xyz, values=None, coord=-1000, slice='coronal', mapping :return: """ - ba = brain_atlas or AllenAtlas() - - # Find the mapping to use - if '-lr' in mapping: - map = mapping - else: - map = mapping + '-lr' - - region_values = compute_volume_from_points(xyz, values, aggr=aggr, fwhm=fwhm, ba=ba) - - if show_cbar: - fig, ax, cbar = _plot_slice(coord / 1e6, slice, region_values, 'volume', background=background, map=map, clevels=clevels, - cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax, cbar - else: - fig, ax = _plot_slice(coord / 1e6, slice, region_values, 'volume', background=background, map=map, clevels=clevels, - cmap=cmap, ba=ba, ax=ax, show_cbar=show_cbar) - return fig, ax - - -def compute_volume_from_points(xyz, values=None, aggr='sum', fwhm=100, ba=None): - """ - Creates a 3D volume with xyz points placed in corresponding voxel in volume. Points that fall into the same voxel within the - volume are aggregated according to the method specified in aggr. Gaussian smoothing with a 3D kernel with distance specified - by fwhm (full width half max) argument is applied. If fwhm = 0, no gaussian smoothing is applied. - - :param xyz: 3 column array of xyz coordinates of points in metres - :param values: 1 column array of values per xyz coordinates, if no values are given the sum of xyz points in each voxel is - returned - :param aggr: aggregation method. Options are sum, count, mean, std, median, min and max. Can also give in custom function - (https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.binned_statistic.html) - :param fwhm: full width at half maximum of gaussian kernel in um - :param ba: AllenAtlas object - :return: - """ - - ba = ba or AllenAtlas() - - idx = ba._lookup(xyz) - ba_shape = ba.image.shape[0] * ba.image.shape[1] * ba.image.shape[2] - - if values is not None: - volume = binned_statistic(idx, values, range=[0, ba_shape], statistic=aggr, bins=ba_shape).statistic - volume[np.isnan(volume)] = 0 - else: - volume = np.bincount(idx, minlength=ba_shape, weights=values) - - volume = volume.reshape(ba.image.shape[0], ba.image.shape[1], ba.image.shape[2]).astype(np.float32) - - if fwhm > 0: - # Compute sigma used for gaussian kernel - fwhm_over_sigma_ratio = np.sqrt(8 * np.log(2)) - sigma = fwhm / (fwhm_over_sigma_ratio * ba.res_um) - # TODO to speed up only apply gaussian filter on slices within distance of chosen coordinate - volume = gaussian_filter(volume, sigma=sigma) - - # Mask so that outside of the brain is set to nan - volume[ba.label == 0] = np.nan - - return volume - - -def _plot_slice(coord, slice, region_values, vol_type, background='boundary', map='Allen', clevels=None, cmap='viridis', - show_cbar=False, ba=None, ax=None): - """ - Function to plot scalar value per allen region on histology slice. - - Do not use directly but use through plot_scalar_on_slice function. - - Parameters - ---------- - coord: float - coordinate of slice in um (not needed when slice='top'). - slice: {'coronal', 'sagittal', 'horizontal', 'top'} - the axis through the atlas volume to display. - region_values: numpy.array - Array of values for each of the lateralised Allen regions found using BrainRegions().acronym. If no - value is assigned to the acronym, the value at corresponding to that index should be nan. - vol_type: 'value' - The type of volume to be displayed, should always be 'value' if values want to be displayed. - background: {'image', 'boundary'} - The background slice to overlay the values onto. When 'image' it uses the Allen dwi image, when - 'boundary' it displays the boundaries between regions. - map: {'Allen', 'Beryl', 'Cosmos'} - the mapping to use. - clevels: numpy.array, list or tuple - The min and max values to use for the colormap. - cmap: str, default='viridis' - Colormap to use. - show_cbar: bool, default=False - Whether to display a colorbar. - ba : ibllib.atlas.AllenAtlas - A brain atlas object. - ax : matplotlib.pyplot.Axes - An axis object to plot onto. - - Returns - ------- - fig: matplotlib.figure.Figure - The plotted figure - ax: matplotlib.pyplot.Axes - The plotted axes. - cbar: matplotlib.pyplot.colorbar - matplotlib colorbar object, only returned if show_cbar=True. - - """ - ba = ba or AllenAtlas() - - if clevels is None: - clevels = (np.nanmin(region_values), np.nanmax(region_values)) - - if ax: - fig = ax.get_figure() - else: - fig, ax = plt.subplots() - - if slice == 'coronal': - if background == 'image': - ba.plot_cslice(coord, volume='image', mapping=map, ax=ax) - ba.plot_cslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - else: - ba.plot_cslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - ba.plot_cslice(coord, volume='boundary', mapping=map, ax=ax) - - elif slice == 'sagittal': - if background == 'image': - ba.plot_sslice(coord, volume='image', mapping=map, ax=ax) - ba.plot_sslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - else: - ba.plot_sslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - ba.plot_sslice(coord, volume='boundary', mapping=map, ax=ax) - - elif slice == 'horizontal': - if background == 'image': - ba.plot_hslice(coord, volume='image', mapping=map, ax=ax) - ba.plot_hslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - else: - ba.plot_hslice(coord, volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - ba.plot_hslice(coord, volume='boundary', mapping=map, ax=ax) - - elif slice == 'top': - if background == 'image': - ba.plot_top(volume='image', mapping=map, ax=ax) - ba.plot_top(volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - else: - ba.plot_top(volume=vol_type, region_values=region_values, mapping=map, cmap=cmap, vmin=clevels[0], - vmax=clevels[1], ax=ax) - ba.plot_top(volume='boundary', mapping=map, ax=ax) - - if show_cbar: - norm = colors.Normalize(vmin=clevels[0], vmax=clevels[1], clip=False) - cbar = fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) - return fig, ax, cbar - else: - return fig, ax + return atlas_plots.plot_points_on_slice(xyz, values=values, coord=coord, slice=slice, mapping=mapping, + background=background, cmap=cmap, clevels=clevels, show_cbar=show_cbar, + aggr=aggr, fwhm=fwhm, brain_atlas=brain_atlas, ax=ax) +@deprecated_decorator def plot_scalar_on_barplot(acronyms, values, errors=None, order=True, ax=None, brain_regions=None): """ Function to plot scalar value per allen region on a bar plot. If order=True, the acronyms and values are reordered @@ -807,24 +222,12 @@ def plot_scalar_on_barplot(acronyms, values, errors=None, order=True, ax=None, b The plotted axes. """ - br = brain_regions or BrainRegions() - - if order: - acronyms, values = reorder_data(acronyms, values, brain_regions) - - _, idx = ismember(acronyms, br.acronym) - colours = br.rgb[idx] - - if ax: - fig = ax.get_figure() - else: - fig, ax = plt.subplots() - ax.bar(np.arange(acronyms.size), values, color=colours) - - return fig, ax + return atlas_plots.plot_scalar_on_barplot(acronyms, values, errors=errors, order=order, ax=ax, + brain_regions=brain_regions) +@deprecated_decorator def plot_swanson_vector(acronyms=None, values=None, ax=None, hemisphere=None, br=None, orientation='landscape', empty_color='silver', vmin=None, vmax=None, cmap='viridis', annotate=False, annotate_n=10, annotate_order='top', annotate_list=None, mask=None, mask_color='w', fontsize=10, **kwargs): @@ -878,173 +281,15 @@ def plot_swanson_vector(acronyms=None, values=None, ax=None, hemisphere=None, br The plotted axes. """ - br = BrainRegions() if br is None else br - br.compute_hierarchy() - sw_shape = (2968, 6820) - - if ax is None: - fig, ax = plt.subplots() - ax.set_axis_off() - - if hemisphere != 'both' and acronyms is not None and not isinstance(acronyms[0], str): - # If negative atlas ids are passed in and we are not going to lateralise (e.g hemisphere='both') - # transfer them over to one hemisphere - acronyms = np.abs(acronyms) - - if acronyms is not None: - ibr, vals = br.propagate_down(acronyms, values) - colormap = cm.get_cmap(cmap) - vmin = vmin or np.nanmin(vals) - vmax = vmax or np.nanmax(vals) - norm = colors.Normalize(vmin=vmin, vmax=vmax) - rgba_color = colormap(norm(vals), bytes=True) - - if mask is not None: - imr, _ = br.propagate_down(mask, np.ones_like(mask)) - else: - imr = [] - - sw_json = swanson_json() - if hemisphere == 'both': - sw_rev = copy.deepcopy(sw_json) - for sw in sw_rev: - sw['thisID'] = sw['thisID'] + br.n_lr - sw_json = sw_json + sw_rev - plot_idx = [] - plot_val = [] - for i, reg in enumerate(sw_json): - - coords = reg['coordsReg'] - reg_id = reg['thisID'] - - if acronyms is None: - color = br.rgba[br.mappings['Swanson'][reg['thisID']]] / 255 - if hemisphere is None: - col_l = None - col_r = color - elif hemisphere == 'left': - col_l = empty_color if orientation == 'portrait' else color - col_r = color if orientation == 'portrait' else empty_color - elif hemisphere == 'right': - col_l = color if orientation == 'portrait' else empty_color - col_r = empty_color if orientation == 'portrait' else color - elif hemisphere in ['both', 'mirror']: - col_l = color - col_r = color - else: - idx = np.where(ibr == reg['thisID'])[0] - idxm = np.where(imr == reg['thisID'])[0] - if len(idx) > 0: - plot_idx.append(ibr[idx[0]]) - plot_val.append(vals[idx[0]]) - color = rgba_color[idx[0]] / 255 - elif len(idxm) > 0: - color = mask_color - else: - color = empty_color - - if hemisphere is None: - col_l = None - col_r = color - elif hemisphere == 'left': - col_l = empty_color if orientation == 'portrait' else color - col_r = color if orientation == 'portrait' else empty_color - elif hemisphere == 'right': - col_l = color if orientation == 'portrait' else empty_color - col_r = empty_color if orientation == 'portrait' else color - elif hemisphere == 'mirror': - col_l = color - col_r = color - elif hemisphere == 'both': - if reg_id <= br.n_lr: - col_l = color if orientation == 'portrait' else None - col_r = None if orientation == 'portrait' else color - else: - col_l = None if orientation == 'portrait' else color - col_r = color if orientation == 'portrait' else None - - if reg['hole']: - vertices, codes = coords_for_poly_hole(coords) - if orientation == 'portrait': - vertices[:, [0, 1]] = vertices[:, [1, 0]] - if col_r is not None: - plot_polygon_with_hole(ax, vertices, codes, col_r, reg_id, **kwargs) - if col_l is not None: - vertices_inv = np.copy(vertices) - vertices_inv[:, 0] = -1 * vertices_inv[:, 0] + (sw_shape[0] * 2) - plot_polygon_with_hole(ax, vertices_inv, codes, col_l, reg_id, **kwargs) - else: - if col_r is not None: - plot_polygon_with_hole(ax, vertices, codes, col_r, reg_id, **kwargs) - if col_l is not None: - vertices_inv = np.copy(vertices) - vertices_inv[:, 1] = -1 * vertices_inv[:, 1] + (sw_shape[0] * 2) - plot_polygon_with_hole(ax, vertices_inv, codes, col_l, reg_id, **kwargs) - else: - coords = [coords] if isinstance(coords, dict) else coords - for c in coords: - if orientation == 'portrait': - xy = np.c_[c['y'], c['x']] - if col_r is not None: - plot_polygon(ax, xy, col_r, reg_id, **kwargs) - if col_l is not None: - xy_inv = np.copy(xy) - xy_inv[:, 0] = -1 * xy_inv[:, 0] + (sw_shape[0] * 2) - plot_polygon(ax, xy_inv, col_l, reg_id, **kwargs) - else: - xy = np.c_[c['x'], c['y']] - if col_r is not None: - plot_polygon(ax, xy, col_r, reg_id, **kwargs) - if col_l is not None: - xy_inv = np.copy(xy) - xy_inv[:, 1] = -1 * xy_inv[:, 1] + (sw_shape[0] * 2) - plot_polygon(ax, xy_inv, col_l, reg_id, **kwargs) - - if orientation == 'portrait': - ax.set_ylim(0, sw_shape[1]) - if hemisphere is None: - ax.set_xlim(0, sw_shape[0]) - else: - ax.set_xlim(0, 2 * sw_shape[0]) - else: - ax.set_xlim(0, sw_shape[1]) - if hemisphere is None: - ax.set_ylim(0, sw_shape[0]) - else: - ax.set_ylim(0, 2 * sw_shape[0]) - - if annotate: - if annotate_list is not None: - annotate_swanson(ax=ax, acronyms=annotate_list, orientation=orientation, br=br, thres=10, fontsize=fontsize) - elif acronyms is not None: - ids = br.index2id(np.array(plot_idx)) - _, indices, _ = np.intersect1d(br.id, br.remap(ids, 'Swanson-lr'), return_indices=True) - a, b = ismember(ids, br.id[indices]) - sorted_id = ids[a] - vals = np.array(plot_val)[a] - sort_vals = np.argsort(vals) if annotate_order == 'bottom' else np.argsort(vals)[::-1] - annotate_swanson(ax=ax, acronyms=sorted_id[sort_vals[:annotate_n]], orientation=orientation, br=br, - thres=10, fontsize=fontsize) - else: - annotate_swanson(ax=ax, orientation=orientation, br=br, fontsize=fontsize) - - def format_coord(x, y): - patch = next((p for p in ax.patches if p.contains_point(p.get_transform().transform(np.r_[x, y]))), None) - if patch is not None: - ind = int(patch.get_gid().split('_')[1]) - ancestors = br.ancestors(br.id[ind])['acronym'] - return f'sw-{ind}, {ancestors}, aid={br.id[ind]}-{br.acronym[ind]} \n {br.name[ind]}' - else: - return '' - - ax.format_coord = format_coord - - ax.invert_yaxis() - ax.set_aspect('equal') - return ax + return atlas_plots.plot_swanson_vector(acronyms=acronyms, values=values, ax=ax, hemisphere=hemisphere, br=br, + orientation=orientation, empty_color=empty_color, vmin=vmin, vmax=vmax, + cmap=cmap, annotate=annotate, annotate_n=annotate_n, + annotate_order=annotate_order, annotate_list=annotate_list, mask=mask, + mask_color=mask_color, fontsize=fontsize, **kwargs) +@deprecated_decorator def plot_swanson(acronyms=None, values=None, ax=None, hemisphere=None, br=None, orientation='landscape', annotate=False, empty_color='silver', **kwargs): """ @@ -1086,89 +331,6 @@ def plot_swanson(acronyms=None, values=None, ax=None, hemisphere=None, br=None, matplotlib.pyplot.Axes The plotted axes. """ - mapping = 'Swanson' - br = BrainRegions() if br is None else br - br.compute_hierarchy() - s2a = swanson() - # both hemispheres - if hemisphere == 'both': - _s2a = s2a + np.sum(br.id > 0) - _s2a[s2a == 0] = 0 - _s2a[s2a == 1] = 1 - s2a = np.r_[s2a, np.flipud(_s2a)] - mapping = 'Swanson-lr' - elif hemisphere == 'mirror': - s2a = np.r_[s2a, np.flipud(s2a)] - if orientation == 'portrait': - s2a = np.transpose(s2a) - if acronyms is None: - regions = br.mappings[mapping][s2a] - im = br.rgba[regions] - iswan = None - else: - ibr, vals = br.propagate_down(acronyms, values) - # we now have the mapped regions and aggregated values, map values onto swanson map - iswan, iv = ismember(s2a, ibr) - im = np.zeros_like(s2a, dtype=np.float32) - im[iswan] = vals[iv] - im[~iswan] = np.nan - if not ax: - ax = plt.gca() - ax.set_axis_off() # unless provided we don't need scales here - ax.imshow(im, **kwargs) - # overlay the boundaries if value plot - imb = np.zeros((*s2a.shape[:2], 4), dtype=np.uint8) - # fill in the empty regions with the blank regions colours if necessary - if iswan is not None: - imb[~iswan] = (np.array(colors.to_rgba(empty_color)) * 255).astype('uint8') - imb[s2a == 0] = 255 - # imb[s2a == 1] = np.array([167, 169, 172, 255]) - imb[s2a == 1] = np.array([0, 0, 0, 255]) - ax.imshow(imb) - if annotate: - annotate_swanson(ax=ax, orientation=orientation, br=br) - - # provides the mean to see the region on axis - def format_coord(x, y): - ind = s2a[int(y), int(x)] - ancestors = br.ancestors(br.id[ind])['acronym'] - return f'sw-{ind}, {ancestors}, aid={br.id[ind]}-{br.acronym[ind]} \n {br.name[ind]}' - ax.format_coord = format_coord - return ax - - -def annotate_swanson(ax, acronyms=None, orientation='landscape', br=None, thres=20000, **kwargs): - """ - Display annotations on a Swanson flatmap. - - Parameters - ---------- - ax : matplotlib.pyplot.Axes - An axis object to plot onto. - acronyms : array_like - A list or numpy array of acronyms or Allen region IDs. If None plot all acronyms. - orientation : {landscape', 'portrait'}, default='landscape' - The plot orientation. - br : ibllib.atlas.BrainRegions - A brain regions object. - thres : int, default=20000 - The number of pixels above which a region is labelled. - **kwargs - See matplotlib.pyplot.Axes.annotate. - - """ - br = br or BrainRegions() - if acronyms is None: - indices = np.arange(br.id.size) - else: # TODO we should in fact remap and compute labels for hierarchical regions - aids = br.parse_acronyms_argument(acronyms) - _, indices, _ = np.intersect1d(br.id, br.remap(aids, 'Swanson-lr'), return_indices=True) - labels = _swanson_labels_positions(thres=thres) - for ilabel in labels: - # do not display unwanted labels - if ilabel not in indices: - continue - # rotate the labels if the display is in portrait mode - xy = np.flip(labels[ilabel]) if orientation == 'portrait' else labels[ilabel] - ax.annotate(br.acronym[ilabel], xy=xy, ha='center', va='center', **kwargs) + return atlas_plots.plot_swanson(acronyms=acronyms, values=values, ax=ax, hemisphere=hemisphere, br=br, + orientation=orientation, annotate=annotate, empty_color=empty_color, **kwargs) diff --git a/ibllib/atlas/regions.py b/ibllib/atlas/regions.py index 85f94edbb..c5f3f609a 100644 --- a/ibllib/atlas/regions.py +++ b/ibllib/atlas/regions.py @@ -26,679 +26,24 @@ FIXME Document the two structure trees. Which Website did they come from, and which publication/edition? """ -from dataclasses import dataclass import logging -from pathlib import Path - -import numpy as np -import pandas as pd -from iblutil.util import Bunch -from iblutil.numerical import ismember +from iblatlas import regions +from ibllib.atlas import deprecated_decorator _logger = logging.getLogger(__name__) -FILE_MAPPINGS = str(Path(__file__).parent.joinpath('mappings.pqt')) -ALLEN_FILE_REGIONS = str(Path(__file__).parent.joinpath('allen_structure_tree.csv')) -FRANKLIN_FILE_REGIONS = str(Path(__file__).parent.joinpath('franklin_paxinos_structure_tree.csv')) - - -@dataclass -class _BrainRegions: - """A struct of brain regions, their names, IDs, relationships and associated plot colours.""" - - """numpy.array: An integer array of unique brain region IDs.""" - id: np.ndarray - """numpy.array: A str array of verbose brain region names.""" - name: object - """numpy.array: A str array of brain region acronyms.""" - acronym: object - """numpy.array: A, (n, 3) uint8 array of brain region RGB colour values.""" - rgb: np.uint8 - """numpy.array: An unsigned integer array indicating the number of degrees removed from root.""" - level: np.ndarray - """numpy.array: An integer array of parent brain region IDs.""" - parent: np.ndarray - """numpy.array: The position within the flattened graph.""" - order: np.uint16 - - def __post_init__(self): - self._compute_mappings() - - def _compute_mappings(self): - """Compute default mapping for the structure tree. - - Default mapping is identity. This method is intended to be overloaded by subclasses. - """ - self.default_mapping = None - self.mappings = dict(default_mapping=self.order) - # the number of lateralized regions (typically half the number of regions in a lateralized structure tree) - self.n_lr = 0 - - def to_df(self): - """ - Return dataclass as a pandas DataFrame. - - Returns - ------- - pandas.DataFrame - The object as a pandas DataFrame with attributes as columns. - """ - attrs = ['id', 'name', 'acronym', 'hexcolor', 'level', 'parent', 'order'] - d = dict(zip(attrs, list(map(self.__getattribute__, attrs)))) - return pd.DataFrame(d) - - @property - def rgba(self): - """numpy.array: An (n, 4) uint8 array of RGBA values for all n brain regions.""" - rgba = np.c_[self.rgb, self.rgb[:, 0] * 0 + 255] - rgba[0, :] = 0 # set the void to transparent - return rgba - - @property - def hexcolor(self): - """numpy.array of str: The RGB colour values as hexadecimal triplet strings.""" - return np.apply_along_axis(lambda x: "#{0:02x}{1:02x}{2:02x}".format(*x.astype(int)), 1, self.rgb) - - def get(self, ids) -> Bunch: - """ - Return a map of id, name, acronym, etc. for the provided IDs. - - Parameters - ---------- - ids : int, tuple of ints, numpy.array - One or more brain region IDs to get information for. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays the length of `ids`. - """ - uid, uind = np.unique(ids, return_inverse=True) - a, iself, _ = np.intersect1d(self.id, uid, assume_unique=False, return_indices=True) - b = Bunch() - for k in self.__dataclass_fields__.keys(): - b[k] = self.__getattribute__(k)[iself[uind]] - return b - - def _navigate_tree(self, ids, direction='down', return_indices=False): - """ - Navigate the tree and get all related objects either up, down or along the branch. - - By convention the provided id is returned in the list of regions. - - Parameters - ---------- - ids : int, array_like - One or more brain region IDs (int32). - direction : {'up', 'down'} - Whether to return ancestors ('up') or descendants ('down'). - return_indices : bool, default=False - If true returns a second argument with indices mapping to the current brain region - object. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays the length of `ids`. - """ - indices = ismember(self.id, ids)[0] - count = np.sum(indices) - while True: - if direction == 'down': - indices |= ismember(self.parent, self.id[indices])[0] - elif direction == 'up': - indices |= ismember(self.id, self.parent[indices])[0] - else: - raise ValueError("direction should be either 'up' or 'down'") - if count == np.sum(indices): # last iteration didn't find any match - break - else: - count = np.sum(indices) - if return_indices: - return self.get(self.id[indices]), np.where(indices)[0] - else: - return self.get(self.id[indices]) - - def subtree(self, scalar_id, return_indices=False): - """ - Given a node, returns the subtree containing the node along with ancestors. - - Parameters - ---------- - scalar_id : int - A brain region ID. - return_indices : bool, default=False - If true returns a second argument with indices mapping to the current brain region - object. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays the length of one. - """ - if not np.isscalar(scalar_id): - assert scalar_id.size == 1 - _, idown = self._navigate_tree(scalar_id, direction='down', return_indices=True) - _, iup = self._navigate_tree(scalar_id, direction='up', return_indices=True) - indices = np.unique(np.r_[idown, iup]) - if return_indices: - return self.get(self.id[indices]), np.where(indices)[0] - else: - return self.get(self.id[indices]) - - def descendants(self, ids, **kwargs): - """ - Get descendants from one or more IDs. - - Parameters - ---------- - ids : int, array_like - One or more brain region IDs. - return_indices : bool, default=False - If true returns a second argument with indices mapping to the current brain region - object. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays the length of `ids`. - """ - return self._navigate_tree(ids, direction='down', **kwargs) - - def ancestors(self, ids, **kwargs): - """ - Get ancestors from one or more IDs. - - Parameters - ---------- - ids : int, array_like - One or more brain region IDs. - return_indices : bool, default=False - If true returns a second argument with indices mapping to the current brain region - object. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays the length of `ids`. - """ - return self._navigate_tree(ids, direction='up', **kwargs) - - def leaves(self): - """ - Get all regions that do not have children. - - Returns - ------- - iblutil.util.Bunch[str, numpy.array] - A dict-like object containing the keys {'id', 'name', 'acronym', 'rgb', 'level', - 'parent', 'order'} with arrays of matching length. - """ - leaves = np.setxor1d(self.id, self.parent) - return self.get(np.int64(leaves[~np.isnan(leaves)])) - - def _mapping_from_regions_list(self, new_map, lateralize=False): - """ - From a vector of region IDs, creates a structure tree index mapping. - - For example, given a subset of brain region IDs, this returns an array the length of the - total number of brain regions, where each element is the structure tree index for that - region. The IDs in `new_map` and their descendants are given that ID's index and any - missing IDs are given the root index. - - - Parameters - ---------- - new_map : array_like of int - An array of atlas brain region IDs. - lateralize : bool - If true, lateralized indices are assigned to all IDs. If false, IDs are assigned to q - non-lateralized index regardless of their sign. - - Returns - ------- - numpy.array - A vector of brain region indices representing the structure tree order corresponding to - each input ID and its descendants. - """ - I_ROOT = 1 - I_VOID = 0 - # to lateralize we make sure all regions are represented in + and - - new_map = np.unique(np.r_[-new_map, new_map]) - assert np.all(np.isin(new_map, self.id)), \ - "All mapping ids should be represented in the Allen ids" - # with the lateralization, self.id may have duplicate values so ismember is necessary - iid, inm = ismember(self.id, new_map) - iid = np.where(iid)[0] - mapind = np.zeros_like(self.id) + I_ROOT # non assigned regions are root - # TODO should root be lateralised? - mapind[iid] = iid # regions present in the list have the same index - # Starting by the higher up levels in the hierarchy, assign all descendants to the mapping - for i in np.argsort(self.level[iid]): - descendants = self.descendants(self.id[iid[i]]).id - _, idesc, _ = np.intersect1d(self.id, descendants, return_indices=True) - mapind[idesc] = iid[i] - mapind[0] = I_VOID # void stays void - # to delateralize the regions, assign the positive index to all mapind elements - if lateralize is False: - _, iregion = ismember(np.abs(self.id), self.id) - mapind = mapind[iregion] - return mapind - - def acronym2acronym(self, acronym, mapping=None): - """ - Remap acronyms onto mapping - - :param acronym: list or array of acronyms - :param mapping: target map to remap acronyms - :return: array of remapped acronyms - """ - mapping = mapping or self.default_mapping - inds = self._find_inds(acronym, self.acronym) - return self.acronym[self.mappings[mapping]][inds] - - def acronym2id(self, acronym, mapping=None, hemisphere=None): - """ - Convert acronyms to atlas ids and remap - - :param acronym: list or array of acronyms - :param mapping: target map to remap atlas_ids - :param hemisphere: which hemisphere to return atlas ids for, options left or right - :return: array of remapped atlas ids - """ - mapping = mapping or self.default_mapping - inds = self._find_inds(acronym, self.acronym) - return self.id[self.mappings[mapping]][self._filter_lr(inds, mapping, hemisphere)] - - def acronym2index(self, acronym, mapping=None, hemisphere=None): - """ - Convert acronym to index and remap - :param acronym: - :param mapping: - :param hemisphere: - :return: array of remapped acronyms and list of indexes for each acronnym - """ - mapping = mapping or self.default_mapping - acronym = self.acronym2acronym(acronym, mapping=mapping) - index = list() - for id in acronym: - inds = np.where(self.acronym[self.mappings[mapping]] == id)[0] - index.append(self._filter_lr_index(inds, hemisphere)) - - return acronym, index - - def id2acronym(self, atlas_id, mapping=None): - """ - Convert atlas id to acronym and remap - - :param atlas_id: list or array of atlas ids - :param mapping: target map to remap acronyms - :return: array of remapped acronyms - """ - mapping = mapping or self.default_mapping - inds = self._find_inds(atlas_id, self.id) - return self.acronym[self.mappings[mapping]][inds] - - def id2id(self, atlas_id, mapping='Allen'): - """ - Remap atlas id onto mapping - - :param atlas_id: list or array of atlas ids - :param mapping: target map to remap acronyms - :return: array of remapped atlas ids - """ - - inds = self._find_inds(atlas_id, self.id) - return self.id[self.mappings[mapping]][inds] - - def id2index(self, atlas_id, mapping='Allen'): - """ - Convert atlas id to index and remap - - :param atlas_id: list or array of atlas ids - :param mapping: mapping to use - :return: dict of indices for each atlas_id - """ - - atlas_id = self.id2id(atlas_id, mapping=mapping) - index = list() - for id in atlas_id: - inds = np.where(self.id[self.mappings[mapping]] == id)[0] - index.append(inds) - - return atlas_id, index - - def index2acronym(self, index, mapping=None): - """ - Convert index to acronym and remap - - :param index: - :param mapping: - :return: - """ - mapping = mapping or self.default_mapping - inds = self.acronym[self.mappings[mapping]][index] - return inds - - def index2id(self, index, mapping=None): - """ - Convert index to atlas id and remap - - :param index: - :param mapping: - :return: - """ - mapping = mapping or self.default_mapping - inds = self.id[self.mappings[mapping]][index] - return inds - - def _filter_lr(self, values, mapping, hemisphere): - """ - Filter values by those on left or right hemisphere - :param values: array of index values - :param mapping: mapping to use - :param hemisphere: hemisphere - :return: - """ - if 'lr' in mapping: - if hemisphere == 'left': - return values + self.n_lr - elif hemisphere == 'right': - return values - else: - return np.c_[values + self.n_lr, values] - else: - return values - - def _filter_lr_index(self, values, hemisphere): - """ - Filter index values by those on left or right hemisphere - - :param values: array of index values - :param hemisphere: hemisphere - :return: - """ - if hemisphere == 'left': - return values[values > self.n_lr] - elif hemisphere == 'right': - return values[values <= self.n_lr] - else: - return values - - def _find_inds(self, values, all_values): - if not isinstance(values, list) and not isinstance(values, np.ndarray): - values = np.array([values]) - _, inds = ismember(np.array(values), all_values) - - return inds - - def parse_acronyms_argument(self, acronyms, mode='raise'): - """Parse input acronyms. - - Returns a numpy array of region IDs regardless of the input: list of acronyms, array of - acronym strings or region IDs. To be used by functions to provide flexible input type. - - Parameters - ---------- - acronyms : array_like - An array of region acronyms to convert to IDs. An array of region IDs may also be - provided, in which case they are simply returned. - mode : str, optional - If 'raise', asserts that all acronyms exist in the structure tree. - - Returns - ------- - numpy.array of int - An array of brain regions corresponding to `acronyms`. - """ - # first get the allen region ids regardless of the input type - acronyms = np.array(acronyms) - # if the user provides acronyms they're not signed by definition - if not np.issubdtype(acronyms.dtype, np.number): - user_aids = self.acronym2id(acronyms) - if mode == 'raise': - assert user_aids.size == acronyms.size, 'all acronyms must exist in the ontology' - else: - user_aids = acronyms - return user_aids - - -class FranklinPaxinosRegions(_BrainRegions): - """Mouse Brain in Stereotaxic Coordinates (MBSC). - - Paxinos G, and Franklin KBJ (2012). The Mouse Brain in Stereotaxic Coordinates, 4th edition (Elsevier Academic Press). - """ - def __init__(self): - df_regions = pd.read_csv(FRANKLIN_FILE_REGIONS) - # get rid of nan values, there are rows that are in Allen but are not in the Franklin Paxinos atlas - df_regions = df_regions[~df_regions['Structural ID'].isna()] - # add in root - root = [{'Structural ID': int(997), 'Franklin-Paxinos Full name': 'root', 'Franklin-Paxinos abbreviation': 'root', - 'structure Order': 50, 'red': 255, 'green': 255, 'blue': 255, 'Allen Full name': 'root', - 'Allen abbreviation': 'root'}] - df_regions = pd.concat([pd.DataFrame(root), df_regions], ignore_index=True) - - allen_regions = pd.read_csv(ALLEN_FILE_REGIONS) - - # Find the level of acronyms that are the same as Allen - a, b = ismember(df_regions['Allen abbreviation'].values, allen_regions['acronym'].values) - level = allen_regions['depth'].values[b] - df_regions['level'] = np.full(len(df_regions), np.nan) - df_regions['allen level'] = np.full(len(df_regions), np.nan) - df_regions.loc[a, 'level'] = level - df_regions.loc[a, 'allen level'] = level - - nan_idx = np.where(df_regions['Allen abbreviation'].isna())[0] - df_regions.loc[nan_idx, 'Allen abbreviation'] = df_regions['Franklin-Paxinos abbreviation'].values[nan_idx] - df_regions.loc[nan_idx, 'Allen Full name'] = df_regions['Franklin-Paxinos Full name'].values[nan_idx] - - # Now fill in the nan values with one level up from their parents we need to this multiple times - while np.sum(np.isnan(df_regions['level'].values)) > 0: - nan_loc = np.isnan(df_regions['level'].values) - parent_level = df_regions['Parent ID'][nan_loc].values - a, b = ismember(parent_level, df_regions['Structural ID'].values) - assert len(a) == len(b) == np.sum(nan_loc) - level = df_regions['level'].values[b] + 1 - df_regions.loc[nan_loc, 'level'] = level - - # lateralize - df_regions_left = df_regions.iloc[np.array(df_regions['Structural ID'] > 0), :].copy() - df_regions_left['Structural ID'] = - df_regions_left['Structural ID'] - df_regions_left['Parent ID'] = - df_regions_left['Parent ID'] - df_regions_left['Allen Full name'] = \ - df_regions_left['Allen Full name'].apply(lambda x: x + ' (left)') - df_regions = pd.concat((df_regions, df_regions_left), axis=0) - - # insert void - void = [{'Structural ID': int(0), 'Franklin-Paxinos Full Name': 'void', 'Franklin-Paxinos abbreviation': 'void', - 'Parent ID': int(0), 'structure Order': 0, 'red': 0, 'green': 0, 'blue': 0, 'Allen Full name': 'void', - 'Allen abbreviation': 'void'}] - df_regions = pd.concat([pd.DataFrame(void), df_regions], ignore_index=True) - - # converts colors to RGB uint8 array - c = np.c_[df_regions['red'], df_regions['green'], df_regions['blue']].astype(np.uint32) - - super().__init__(id=df_regions['Structural ID'].to_numpy().astype(np.int64), - name=df_regions['Allen Full name'].to_numpy(), - acronym=df_regions['Allen abbreviation'].to_numpy(), - rgb=c, - level=df_regions['level'].to_numpy().astype(np.uint16), - parent=df_regions['Parent ID'].to_numpy(), - order=df_regions['structure Order'].to_numpy().astype(np.uint16)) - - def _compute_mappings(self): - """ - Compute lateralized and non-lateralized mappings. - - This method is called by __post_init__. - """ - self.mappings = { - 'FranklinPaxinos': self._mapping_from_regions_list(np.unique(np.abs(self.id)), lateralize=False), - 'FranklinPaxinos-lr': np.arange(self.id.size), - } - self.default_mapping = 'FranklinPaxinos' - self.n_lr = int((len(self.id) - 1) / 2) # the number of lateralized regions - - -class BrainRegions(_BrainRegions): - """ - A struct of Allen brain regions, their names, IDs, relationships and associated plot colours. - - ibllib.atlas.regions.BrainRegions(brainmap='Allen') - - Notes - ----- - The Allen atlas IDs are kept intact but lateralized as follows: labels are duplicated - and IDs multiplied by -1, with the understanding that left hemisphere regions have negative - IDs. - """ - def __init__(self): - df_regions = pd.read_csv(ALLEN_FILE_REGIONS) - # lateralize - df_regions_left = df_regions.iloc[np.array(df_regions.id > 0), :].copy() - df_regions_left['id'] = - df_regions_left['id'] - df_regions_left['parent_structure_id'] = - df_regions_left['parent_structure_id'] - df_regions_left['name'] = df_regions_left['name'].apply(lambda x: x + ' (left)') - df_regions = pd.concat((df_regions, df_regions_left), axis=0) - # converts colors to RGB uint8 array - c = np.uint32(df_regions.color_hex_triplet.map( - lambda x: int(x, 16) if isinstance(x, str) else 256 ** 3 - 1)) - c = np.flip(np.reshape(c.view(np.uint8), (df_regions.id.size, 4))[:, :3], 1) - c[0, :] = 0 # set the void region to black - # For void assign the depth and level to avoid warnings of nan being converted to int - df_regions.loc[0, 'depth'] = 0 - df_regions.loc[0, 'graph_order'] = 0 - # creates the BrainRegion instance - super().__init__(id=df_regions.id.to_numpy(), - name=df_regions.name.to_numpy(), - acronym=df_regions.acronym.to_numpy(), - rgb=c, - level=df_regions.depth.to_numpy().astype(np.uint16), - parent=df_regions.parent_structure_id.to_numpy(), - order=df_regions.graph_order.to_numpy().astype(np.uint16)) - - def _compute_mappings(self): - """ - Recomputes the mapping indices for all mappings. - - Attempts to load mappings from the FILE_MAPPINGS file, otherwise generates from arrays of - brain IDs. In production, we use the MAPPING_FILES pqt to avoid recomputing at each - instantiation as this take a few seconds to execute. - - Currently there are 8 available mappings (Allen, Beryl, Cosmos, and Swanson), lateralized - (with suffix -lr) and non-lateralized. Each row contains the correspondence to the Allen - CCF structure tree order (i.e. index) for each mapping. - - This method is called by __post_init__. - """ - # mappings are indices not ids: they range from 0 to n regions -1 - if Path(FILE_MAPPINGS).exists(): - mappings = pd.read_parquet(FILE_MAPPINGS) - self.mappings = {k: mappings[k].to_numpy() for k in mappings} - else: - beryl = np.load(Path(__file__).parent.joinpath('beryl.npy')) - cosmos = np.load(Path(__file__).parent.joinpath('cosmos.npy')) - swanson = np.load(Path(__file__).parent.joinpath('swanson_regions.npy')) - self.mappings = { - 'Allen': self._mapping_from_regions_list(np.unique(np.abs(self.id)), lateralize=False), - 'Allen-lr': np.arange(self.id.size), - 'Beryl': self._mapping_from_regions_list(beryl, lateralize=False), - 'Beryl-lr': self._mapping_from_regions_list(beryl, lateralize=True), - 'Cosmos': self._mapping_from_regions_list(cosmos, lateralize=False), - 'Cosmos-lr': self._mapping_from_regions_list(cosmos, lateralize=True), - 'Swanson': self._mapping_from_regions_list(swanson, lateralize=False), - 'Swanson-lr': self._mapping_from_regions_list(swanson, lateralize=True), - } - pd.DataFrame(self.mappings).to_parquet(FILE_MAPPINGS) - self.default_mapping = 'Allen' - self.n_lr = int((len(self.id) - 1) / 2) # the number of lateralized regions - - def compute_hierarchy(self): - """ - Creates a self.hierarchy attribute that is an n_levels by n_region array - of indices. This is useful to perform fast vectorized computations of - ancestors and descendants. - :return: - """ - if hasattr(self, 'hierarchy'): - return - n_levels = np.max(self.level) - n_regions = self.id.size - # creates the parent index. Void and root are omitted from intersection - # as they figure as NaN - pmask, i_p = ismember(self.parent, self.id) - self.iparent = np.arange(n_regions) - self.iparent[pmask] = i_p - # the last level of the hierarchy is the actual mapping, then going up level per level - # we assign the parend index - self.hierarchy = np.tile(np.arange(n_regions), (n_levels, 1)) - _mask = np.zeros(n_regions, bool) - for lev in np.flipud(np.arange(n_levels)): - if lev < (n_levels - 1): - self.hierarchy[lev, _mask] = self.iparent[self.hierarchy[lev + 1, _mask]] - sel = self.level == (lev + 1) - self.hierarchy[lev, sel] = np.where(sel)[0] - _mask[sel] = True - - def propagate_down(self, acronyms, values): - """ - This function remaps a set of user specified acronyms and values to the - swanson map, by filling down the child nodes when higher up values are - provided. - :param acronyms: list or array of allen ids or acronyms - :param values: list or array of associated values - :return: - # FIXME Why only the swanson map? Also, how is this actually related to the Swanson map? - """ - user_aids = self.parse_acronyms_argument(acronyms) - _, user_indices = ismember(user_aids, self.id) - self.compute_hierarchy() - ia, ib = ismember(self.hierarchy, user_indices) - v = np.zeros_like(ia, dtype=np.float64) * np.NaN - v[ia] = values[ib] - all_values = np.nanmedian(v, axis=0) - indices = np.where(np.any(ia, axis=0))[0] - all_values = all_values[indices] - return indices, all_values - - def remap(self, region_ids, source_map='Allen', target_map='Beryl'): - """ - Remap atlas regions IDs from source map to target map. - Any NaNs in `region_ids` remain as NaN in the output array. - Parameters - ---------- - region_ids : array_like of int - The region IDs to remap. - source_map : str - The source map name, in `self.mappings`. - target_map : str - The target map name, in `self.mappings`. +@deprecated_decorator +def BrainRegions(): + return regions.BrainRegions() - Returns - ------- - numpy.array of int - The input IDs mapped to `target_map`. - """ - isnan = np.isnan(region_ids) - if np.sum(isnan) > 0: - # In case the user provides nans - nan_loc = np.where(isnan)[0] - _, inds = ismember(region_ids[~isnan], self.id[self.mappings[source_map]]) - mapped_ids = self.id[self.mappings[target_map][inds]].astype(float) - mapped_ids = np.insert(mapped_ids, nan_loc, np.full(nan_loc.shape, np.nan)) - else: - _, inds = ismember(region_ids, self.id[self.mappings[source_map]]) - mapped_ids = self.id[self.mappings[target_map][inds]] - return mapped_ids +@deprecated_decorator +def FranklinPaxinosRegions(): + return regions.FranklinPaxinosRegions() +@deprecated_decorator def regions_from_allen_csv(): """ (DEPRECATED) Reads csv file containing the ALlen Ontology and instantiates a BrainRegions object. From b0ee9cab5b189969b0a0f5d1523284c714b79929 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 14:30:35 +0100 Subject: [PATCH 3/9] clean up functions and remove files from manifest --- MANIFEST.in | 6 -- ibllib/atlas/__init__.py | 4 + ibllib/atlas/atlas.py | 192 +-------------------------------------- ibllib/atlas/genes.py | 1 - requirements.txt | 1 + 5 files changed, 6 insertions(+), 198 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 0a9960553..2082b46c3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,3 @@ -include ibllib/atlas/allen_structure_tree.csv -include ibllib/atlas/franklin_paxinos_structure_tree.csv -include ibllib/atlas/beryl.npy -include ibllib/atlas/cosmos.npy -include ibllib/atlas/swanson.npy -include ibllib/atlas/mappings.pqt include ibllib/io/extractors/extractor_types.json include ibllib/io/extractors/task_extractor_map.json include brainbox/tests/wheel_test.p diff --git a/ibllib/atlas/__init__.py b/ibllib/atlas/__init__.py index aaaaf24dd..22c7720bd 100644 --- a/ibllib/atlas/__init__.py +++ b/ibllib/atlas/__init__.py @@ -197,3 +197,7 @@ from .atlas import * # noqa from .regions import regions_from_allen_csv from .flatmaps import FlatMap +import warnings + +warnings.warn('ibllib.atlas is deprecated. Please install iblatlas using "pip install iblatlas" and use ' + 'this module instead', DeprecationWarning) diff --git a/ibllib/atlas/atlas.py b/ibllib/atlas/atlas.py index b21075c5c..428c2ca2a 100644 --- a/ibllib/atlas/atlas.py +++ b/ibllib/atlas/atlas.py @@ -1,13 +1,8 @@ """ Classes for manipulating brain atlases, insertions, and coordinates. """ -from pathlib import Path, PurePosixPath -from dataclasses import dataclass -import logging -import warnings - -import numpy as np +import warnings import iblatlas.atlas @@ -20,27 +15,6 @@ def deprecated_function(*args, **kwargs): return deprecated_function -def deprecated_dataclass(dataclass): - def deprecated_function(): - warning_text = f"{dataclass.__module__}.{dataclass.__name__} is deprecated. " \ - f"Use iblatlas.{dataclass.__module__.split('.')[-1]}.{dataclass.__name__} instead" - warnings.warn(warning_text, DeprecationWarning) - return dataclass - - return deprecated_function() - - -ALLEN_CCF_LANDMARKS_MLAPDV_UM = {'bregma': np.array([5739, 5400, 332])} -"""dict: The ML AP DV voxel coordinates of brain landmarks in the Allen atlas.""" - -PAXINOS_CCF_LANDMARKS_MLAPDV_UM = {'bregma': np.array([5700, 4300 + 160, 330])} -"""dict: The ML AP DV voxel coordinates of brain landmarks in the Franklin & Paxinos atlas.""" - -S3_BUCKET_IBL = 'ibl-brain-wide-map-public' -"""str: The name of the public IBL S3 bucket containing atlas data.""" - -_logger = logging.getLogger(__name__) - @deprecated_decorator def BrainCoordinates(*args, **kwargs): @@ -62,178 +36,14 @@ class Trajectory(iblatlas.atlas.Trajectory): >>> trj = Trajectory.fit(xyz) """ - vector: np.ndarray - point: np.ndarray - - def __init_sublcall__(self): - warning_text = f"{dataclass.__module__}.{dataclass.__name__} is deprecated. " \ - f"Use iblatlas.{dataclass.__module__.split('.')[-1]}.{dataclass.__name__} instead" - warnings.warn(warning_text, DeprecationWarning) -@deprecated_dataclass -@dataclass class Insertion(iblatlas.atlas.Insertion): """ Defines an ephys probe insertion in 3D coordinate. IBL conventions. To instantiate, use the static methods: `Insertion.from_track` and `Insertion.from_dict`. """ - x: float - y: float - z: float - phi: float - theta: float - depth: float - label: str = '' - beta: float = 0 - - @staticmethod - def from_track(xyzs, brain_atlas=None): - """ - Define an insersion from one or more trajectory. - - Parameters - ---------- - xyzs : numpy.array - An n by 3 array xyz coordinates representing an insertion trajectory. - brain_atlas : BrainAtlas - A brain atlas instance, used to attain the point of entry. - - Returns - ------- - Insertion - """ - assert brain_atlas, 'Input argument brain_atlas must be defined' - traj = Trajectory.fit(xyzs) - # project the deepest point into the vector to get the tip coordinate - tip = traj.project(xyzs[np.argmin(xyzs[:, 2]), :]) - # get intersection with the brain surface as an entry point - entry = Insertion.get_brain_entry(traj, brain_atlas) - # convert to spherical system to store the insertion - depth, theta, phi = cart2sph(*(entry - tip)) - insertion_dict = { - 'x': entry[0], 'y': entry[1], 'z': entry[2], 'phi': phi, 'theta': theta, 'depth': depth - } - return Insertion(**insertion_dict) - - @staticmethod - def from_dict(d, brain_atlas=None): - """ - Constructs an Insertion object from the json information stored in probes.description file. - - Parameters - ---------- - d : dict - A dictionary containing at least the following keys {'x', 'y', 'z', 'phi', 'theta', - 'depth'}. The depth and xyz coordinates must be in um. - brain_atlas : BrainAtlas, default=None - If provided, disregards the z coordinate and locks the insertion point to the z of the - brain surface. - - Returns - ------- - Insertion - - Examples - -------- - >>> tri = {'x': 544.0, 'y': 1285.0, 'z': 0.0, 'phi': 0.0, 'theta': 5.0, 'depth': 4501.0} - >>> ins = Insertion.from_dict(tri) - """ - assert brain_atlas, 'Input argument brain_atlas must be defined' - z = d['z'] / 1e6 - if not hasattr(brain_atlas, 'top'): - brain_atlas.compute_surface() - iy = brain_atlas.bc.y2i(d['y'] / 1e6) - ix = brain_atlas.bc.x2i(d['x'] / 1e6) - # Only use the brain surface value as z if it isn't NaN (this happens when the surface touches the edges - # of the atlas volume - if not np.isnan(brain_atlas.top[iy, ix]): - z = brain_atlas.top[iy, ix] - return Insertion(x=d['x'] / 1e6, y=d['y'] / 1e6, z=z, - phi=d['phi'], theta=d['theta'], depth=d['depth'] / 1e6, - beta=d.get('beta', 0), label=d.get('label', '')) - - @property - def trajectory(self): - """ - Gets the trajectory object matching insertion coordinates - :return: atlas.Trajectory - """ - return Trajectory.fit(self.xyz) - - @property - def xyz(self): - return np.c_[self.entry, self.tip].transpose() - - @property - def entry(self): - return np.array((self.x, self.y, self.z)) - - @property - def tip(self): - return sph2cart(- self.depth, self.theta, self.phi) + np.array((self.x, self.y, self.z)) - - @staticmethod - def _get_surface_intersection(traj, brain_atlas, surface='top'): - """ - TODO Document! - - Parameters - ---------- - traj - brain_atlas - surface - - Returns - ------- - - """ - brain_atlas.compute_surface() - - distance = traj.mindist(brain_atlas.srf_xyz) - dist_sort = np.argsort(distance) - # In some cases the nearest two intersection points are not the top and bottom of brain - # So we find all intersection points that fall within one voxel and take the one with - # highest dV to be entry and lowest dV to be exit - idx_lim = np.sum(distance[dist_sort] * 1e6 < np.max(brain_atlas.res_um)) - dist_lim = dist_sort[0:idx_lim] - z_val = brain_atlas.srf_xyz[dist_lim, 2] - if surface == 'top': - ma = np.argmax(z_val) - _xyz = brain_atlas.srf_xyz[dist_lim[ma], :] - _ixyz = brain_atlas.bc.xyz2i(_xyz) - _ixyz[brain_atlas.xyz2dims[2]] += 1 - elif surface == 'bottom': - ma = np.argmin(z_val) - _xyz = brain_atlas.srf_xyz[dist_lim[ma], :] - _ixyz = brain_atlas.bc.xyz2i(_xyz) - - xyz = brain_atlas.bc.i2xyz(_ixyz.astype(float)) - - return xyz - - @staticmethod - def get_brain_exit(traj, brain_atlas): - """ - Given a Trajectory and a BrainAtlas object, computes the brain exit coordinate as the - intersection of the trajectory and the brain surface (brain_atlas.surface) - :param brain_atlas: - :return: 3 element array x,y,z - """ - # Find point where trajectory intersects with bottom of brain - return Insertion._get_surface_intersection(traj, brain_atlas, surface='bottom') - - @staticmethod - def get_brain_entry(traj, brain_atlas): - """ - Given a Trajectory and a BrainAtlas object, computes the brain entry coordinate as the - intersection of the trajectory and the brain surface (brain_atlas.surface) - :param brain_atlas: - :return: 3 element array x,y,z - """ - # Find point where trajectory intersects with top of brain - return Insertion._get_surface_intersection(traj, brain_atlas, surface='top') @deprecated_decorator diff --git a/ibllib/atlas/genes.py b/ibllib/atlas/genes.py index c2261d5c5..34ad6c73e 100644 --- a/ibllib/atlas/genes.py +++ b/ibllib/atlas/genes.py @@ -16,4 +16,3 @@ def allen_gene_expression(filename='gene-expression.pqt', folder_cache=None): """ return genes.allen_gene_expression(filename=filename, folder_cache=folder_cache) - diff --git a/requirements.txt b/requirements.txt index 544601f8a..d6943fef0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,3 +30,4 @@ ONE-api>=2.2 slidingRP>=1.0.0 # steinmetz lab refractory period metrics wfield==0.3.7 # widefield extractor frozen for now (2023/07/15) until Joao fixes latest version psychofit +iblatlas From 5703b80b276285bd4e137de34a074db320f441e1 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 14:41:07 +0100 Subject: [PATCH 4/9] update tests --- ibllib/tests/test_atlas.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ibllib/tests/test_atlas.py b/ibllib/tests/test_atlas.py index f3d147c26..04273dc6e 100644 --- a/ibllib/tests/test_atlas.py +++ b/ibllib/tests/test_atlas.py @@ -2,9 +2,8 @@ import numpy as np import matplotlib.pyplot as plt - -from ibllib.atlas import (BrainCoordinates, cart2sph, sph2cart, Trajectory, - Insertion, ALLEN_CCF_LANDMARKS_MLAPDV_UM, AllenAtlas) +from iblatlas.atlas import cart2sph, sph2cart, ALLEN_CCF_LANDMARKS_MLAPDV_UM +from ibllib.atlas import (BrainCoordinates, Trajectory, Insertion, AllenAtlas) from ibllib.atlas.regions import BrainRegions, FranklinPaxinosRegions from ibllib.atlas.plots import prepare_lr_data, reorder_data from iblutil.numerical import ismember From bbd85dd78a6abb6e45d39eada6a8266bf47432f7 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 15:21:59 +0100 Subject: [PATCH 5/9] fix example imports --- examples/atlas/Working with ibllib atlas.ipynb | 8 ++++---- examples/atlas/atlas_circular_pyramidal_flatmap.ipynb | 6 +++--- examples/atlas/atlas_dorsal_cortex_flatmap.ipynb | 6 +++--- examples/atlas/atlas_mapping.ipynb | 4 ++-- examples/atlas/atlas_plotting_points_on_slice.ipynb | 6 +++--- examples/atlas/atlas_plotting_scalar_on_slice.ipynb | 8 ++++---- examples/atlas/atlas_swanson_flatmap.ipynb | 4 ++-- examples/atlas/atlas_working_with_ibllib_atlas.ipynb | 10 +++++----- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/examples/atlas/Working with ibllib atlas.ipynb b/examples/atlas/Working with ibllib atlas.ipynb index 9e435aea7..3e0319b8f 100644 --- a/examples/atlas/Working with ibllib atlas.ipynb +++ b/examples/atlas/Working with ibllib atlas.ipynb @@ -21,7 +21,7 @@ "id": "461b8f34", "metadata": {}, "source": [ - "The Allen atlas image and annotation volumes can be accessed using the `ibllib.atlas.AllenAtlas` class. Upon instantiating the class for the first time, the relevant files will be downloaded from the Allen database." + "The Allen atlas image and annotation volumes can be accessed using the `iblatlas.atlas.AllenAtlas` class. Upon instantiating the class for the first time, the relevant files will be downloaded from the Allen database." ] }, { @@ -31,7 +31,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "\n", "res = 25 # resolution of Atlas, available resolutions are 10, 25 (default) and 50\n", "brain_atlas = AllenAtlas(res_um=res)" @@ -139,7 +139,7 @@ "id": "a1802136", "metadata": {}, "source": [ - "The Allen brain region structure tree can be accessed through the class `ibllib.atlas.regions.BrainRegions`. " + "The Allen brain region structure tree can be accessed through the class `iblatlas.regions.BrainRegions`. " ] }, { @@ -149,7 +149,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.regions import BrainRegions\n", "\n", "brain_regions = BrainRegions()\n", "\n", diff --git a/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb b/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb index cb84f8019..03ab28c20 100644 --- a/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb +++ b/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb @@ -39,7 +39,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas import FlatMap\n", + "from iblatlas.flatmaps import FlatMap\n", "flmap_cr = FlatMap(flatmap='circles')" ] }, @@ -142,7 +142,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.regions import BrainRegions\n", "br = BrainRegions()\n", "# prepare array of acronyms with beryl mapping\n", "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", @@ -177,7 +177,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import plot_scalar_on_flatmap\n", + "from iblatlas.plots import plot_scalar_on_flatmap\n", "# Plot region values on the left hemisphere of circle flatmap overlaid on brain region boundaries using Allen mapping\n", "fig, ax = plt.subplots(figsize=(18,4))\n", "fig, ax = plot_scalar_on_flatmap(acronyms, values, hemisphere='left', mapping='Allen', flmap_atlas=flmap_cr, ax=ax)" diff --git a/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb b/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb index 5ac2d0f69..fd233d1b4 100644 --- a/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb +++ b/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb @@ -39,7 +39,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas import FlatMap\n", + "from iblatlas.flatmaps import FlatMap\n", "\n", "res = 25\n", "flmap = FlatMap(flatmap='dorsal_cortex', res_um=res)\n", @@ -100,7 +100,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.regions import BrainRegions\n", "br = BrainRegions()\n", "# prepare array of acronyms with beryl mapping\n", "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", @@ -135,7 +135,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import plot_scalar_on_flatmap\n", + "from iblatlas.plots import plot_scalar_on_flatmap\n", "\n", "# Plot region values on the left hemisphere at depth=0um overlaid on boundary image using Allen mapping\n", "fig, ax = plot_scalar_on_flatmap(acronyms, values, depth=0, mapping='Allen', hemisphere='left', background='boundary',\n", diff --git a/examples/atlas/atlas_mapping.ipynb b/examples/atlas/atlas_mapping.ipynb index 41a050f8a..a75818b02 100644 --- a/examples/atlas/atlas_mapping.ipynb +++ b/examples/atlas/atlas_mapping.ipynb @@ -16,8 +16,8 @@ "outputs": [], "source": [ "# import brain atlas and brain regions objects\n", - "from ibllib.atlas import AllenAtlas\n", - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.atlas import AllenAtlas\n", + "from iblatlas.regions import BrainRegions\n", "ba = AllenAtlas()\n", "br = BrainRegions() # br is also an attribute of ba so could to br = ba.regions" ] diff --git a/examples/atlas/atlas_plotting_points_on_slice.ipynb b/examples/atlas/atlas_plotting_points_on_slice.ipynb index d65e687f8..c013a97f8 100644 --- a/examples/atlas/atlas_plotting_points_on_slice.ipynb +++ b/examples/atlas/atlas_plotting_points_on_slice.ipynb @@ -41,7 +41,7 @@ "source": [ "from one.api import ONE\n", "from brainbox.io.one import SpikeSortingLoader\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "import numpy as np\n", "\n", "one = ONE()\n", @@ -80,7 +80,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import plot_points_on_slice\n", + "from iblatlas.plots import plot_points_on_slice\n", "import matplotlib.pyplot as plt\n", "\n", "fig, axs = plt.subplots(1, 2, figsize=(16,4))\n", @@ -173,7 +173,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import compute_volume_from_points, plot_volume_on_slice\n", + "from iblatlas.plots import compute_volume_from_points, plot_volume_on_slice\n", "\n", "# Extract xyz coords from clusters dict\n", "xyz = np.c_[clusters['x'], clusters['y'], clusters['z']]\n", diff --git a/examples/atlas/atlas_plotting_scalar_on_slice.ipynb b/examples/atlas/atlas_plotting_scalar_on_slice.ipynb index eddf024f1..a5a2c81ea 100644 --- a/examples/atlas/atlas_plotting_scalar_on_slice.ipynb +++ b/examples/atlas/atlas_plotting_scalar_on_slice.ipynb @@ -78,7 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import prepare_lr_data\n", + "from iblatlas.plots import prepare_lr_data\n", "\n", "acronyms_lh = np.array(['VPM', 'VPL', 'PO', 'LP', 'CA1', 'DG-mo'])\n", "values_lh = np.random.randint(0, 10, acronyms_lh.size)\n", @@ -104,7 +104,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.regions import BrainRegions\n", "br = BrainRegions()\n", "\n", "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", @@ -129,8 +129,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.plots import plot_scalar_on_slice\n", - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.plots import plot_scalar_on_slice\n", + "from iblatlas.atlas import AllenAtlas\n", "ba = AllenAtlas()" ] }, diff --git a/examples/atlas/atlas_swanson_flatmap.ipynb b/examples/atlas/atlas_swanson_flatmap.ipynb index 4f92ee3ae..c476d6973 100644 --- a/examples/atlas/atlas_swanson_flatmap.ipynb +++ b/examples/atlas/atlas_swanson_flatmap.ipynb @@ -23,8 +23,8 @@ "outputs": [], "source": [ "import numpy as np\n", - "from ibllib.atlas.plots import plot_swanson_vector\n", - "from ibllib.atlas import BrainRegions\n", + "from iblatlas.plots import plot_swanson_vector\n", + "from iblatlas.regions import BrainRegions\n", "\n", "br = BrainRegions()\n", "\n", diff --git a/examples/atlas/atlas_working_with_ibllib_atlas.ipynb b/examples/atlas/atlas_working_with_ibllib_atlas.ipynb index d56d21edd..ebbd874d2 100644 --- a/examples/atlas/atlas_working_with_ibllib_atlas.ipynb +++ b/examples/atlas/atlas_working_with_ibllib_atlas.ipynb @@ -21,7 +21,7 @@ "id": "461b8f34", "metadata": {}, "source": [ - "The Allen atlas image and annotation volumes can be accessed using the `ibllib.atlas.AllenAtlas` class. Upon instantiating the class for the first time, the relevant files will be downloaded from the Allen database." + "The Allen atlas image and annotation volumes can be accessed using the `iblatlas.atlas.AllenAtlas` class. Upon instantiating the class for the first time, the relevant files will be downloaded from the Allen database." ] }, { @@ -31,7 +31,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas import AllenAtlas\n", + "from iblatlas.atlas import AllenAtlas\n", "\n", "res = 25 # resolution of Atlas, available resolutions are 10, 25 (default) and 50\n", "brain_atlas = AllenAtlas(res_um=res)" @@ -142,7 +142,7 @@ "source": [ "### Index versus Allen ID\n", "\n", - "The Allen brain region structure tree can be accessed through the class `ibllib.atlas.regions.BrainRegions`." + "The Allen brain region structure tree can be accessed through the class `iblatlas.regions.BrainRegions`." ] }, { @@ -152,7 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ibllib.atlas.regions import BrainRegions\n", + "from iblatlas.regions import BrainRegions\n", "\n", "brain_regions = BrainRegions()\n", "\n", @@ -847,7 +847,7 @@ "execution_count": null, "outputs": [], "source": [ - "from ibllib.atlas import ALLEN_CCF_LANDMARKS_MLAPDV_UM\n", + "from iblatlas.atlas import ALLEN_CCF_LANDMARKS_MLAPDV_UM\n", "print(ALLEN_CCF_LANDMARKS_MLAPDV_UM)" ], "metadata": { From c0409c096b0e724f2a5e8241de69f5669c46698d Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 16:27:06 +0100 Subject: [PATCH 6/9] typo --- brainbox/atlas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brainbox/atlas.py b/brainbox/atlas.py index a28718b37..8c28feb89 100644 --- a/brainbox/atlas.py +++ b/brainbox/atlas.py @@ -7,7 +7,7 @@ import numpy as np import seaborn as sns import matplotlib.pyplot as plt -from ibatlas import atlas +from iblatlas import atlas def _label2values(imlabel, fill_values, ba): From e45e0762e619ec3c657e0409c5256ba388f94636 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Wed, 13 Sep 2023 16:55:51 +0100 Subject: [PATCH 7/9] remove atlas example notebooks --- .../atlas/Working with ibllib atlas.ipynb | 365 ------ .../atlas_circular_pyramidal_flatmap.ipynb | 248 ---- .../atlas/atlas_dorsal_cortex_flatmap.ipynb | 203 ---- examples/atlas/atlas_mapping.ipynb | 650 ----------- .../atlas_plotting_points_on_slice.ipynb | 251 ---- .../atlas_plotting_scalar_on_slice.ipynb | 277 ----- examples/atlas/atlas_swanson_flatmap.ipynb | 418 ------- .../atlas_working_with_ibllib_atlas.ipynb | 1016 ----------------- examples/atlas/images/brain_xyz.png | Bin 116535 -> 0 bytes 9 files changed, 3428 deletions(-) delete mode 100644 examples/atlas/Working with ibllib atlas.ipynb delete mode 100644 examples/atlas/atlas_circular_pyramidal_flatmap.ipynb delete mode 100644 examples/atlas/atlas_dorsal_cortex_flatmap.ipynb delete mode 100644 examples/atlas/atlas_mapping.ipynb delete mode 100644 examples/atlas/atlas_plotting_points_on_slice.ipynb delete mode 100644 examples/atlas/atlas_plotting_scalar_on_slice.ipynb delete mode 100644 examples/atlas/atlas_swanson_flatmap.ipynb delete mode 100644 examples/atlas/atlas_working_with_ibllib_atlas.ipynb delete mode 100644 examples/atlas/images/brain_xyz.png diff --git a/examples/atlas/Working with ibllib atlas.ipynb b/examples/atlas/Working with ibllib atlas.ipynb deleted file mode 100644 index 3e0319b8f..000000000 --- a/examples/atlas/Working with ibllib atlas.ipynb +++ /dev/null @@ -1,365 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b767b213", - "metadata": {}, - "source": [ - "# Working with IBL atlas object" - ] - }, - { - "cell_type": "markdown", - "id": "bba98311", - "metadata": {}, - "source": [ - "## Getting started" - ] - }, - { - "cell_type": "markdown", - "id": "461b8f34", - "metadata": {}, - "source": [ - "The Allen atlas image and annotation volumes can be accessed using the `iblatlas.atlas.AllenAtlas` class. Upon instantiating the class for the first time, the relevant files will be downloaded from the Allen database." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "df873343", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.atlas import AllenAtlas\n", - "\n", - "res = 25 # resolution of Atlas, available resolutions are 10, 25 (default) and 50\n", - "brain_atlas = AllenAtlas(res_um=res)" - ] - }, - { - "cell_type": "markdown", - "id": "95a8e4db", - "metadata": {}, - "source": [ - "## Exploring the volumes" - ] - }, - { - "cell_type": "markdown", - "id": "12f16b38", - "metadata": {}, - "source": [ - "The brain_atlas class contains two volumes, the dwi image volume and the annotation label volume" - ] - }, - { - "cell_type": "markdown", - "id": "5f34f56c", - "metadata": {}, - "source": [ - "### 1. Image Volume \n", - "Allen atlas dwi average template" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "769b4fd4", - "metadata": {}, - "outputs": [], - "source": [ - "# Access the image volume\n", - "im = brain_atlas.image\n", - "\n", - "# Explore the size of the image volume (ap, ml, dv)\n", - "print(f'Shape of image volume: {im.shape}')\n", - "\n", - "# Plot a coronal slice at ap = -1000um\n", - "ap = -1000 / 1e6 # input must be in metres\n", - "ax = brain_atlas.plot_cslice(ap, volume='image')\n" - ] - }, - { - "cell_type": "markdown", - "id": "1c46789b", - "metadata": {}, - "source": [ - "### Label Volume\n" - ] - }, - { - "cell_type": "markdown", - "id": "72bea21a", - "metadata": {}, - "source": [ - "The label volume contains information about which brain region each voxel in the volume belongs to." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ff7cb654", - "metadata": {}, - "outputs": [], - "source": [ - "# Access the image volume\n", - "lab = brain_atlas.label\n", - "\n", - "# Explore the size of the image volume (ap, ml, dv)\n", - "print(f'Shape of label volume: {lab.shape}')\n", - "\n", - "# Plot a coronal slice at ap = -1000um\n", - "ap = -1000 / 1e6 # input must be in metres\n", - "ax = brain_atlas.plot_cslice(ap, volume='annotation')" - ] - }, - { - "cell_type": "markdown", - "id": "8bd69066", - "metadata": {}, - "source": [ - "The label volume used in the IBL AllenAtlas class differs from the Allen annotation volume in two ways.\n", - "- Each voxel has information about the index of the Allen region rather than the Allen atlas id\n", - "- The volume has been lateralised to differentiate between the left and right hemisphere\n", - "\n", - "To understand this better let's explore the BrainRegions class that contains information about the Allen structure tree." - ] - }, - { - "cell_type": "markdown", - "id": "04f601ed", - "metadata": {}, - "source": [ - "## Exploring brain regions" - ] - }, - { - "cell_type": "markdown", - "id": "a1802136", - "metadata": {}, - "source": [ - "The Allen brain region structure tree can be accessed through the class `iblatlas.regions.BrainRegions`. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9c2d097f", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.regions import BrainRegions\n", - "\n", - "brain_regions = BrainRegions()\n", - "\n", - "# Alternatively if you already have the AllenAtlas instantiated you can access it as an attribute\n", - "brain_regions = brain_atlas.regions" - ] - }, - { - "cell_type": "markdown", - "id": "6cf9ab47", - "metadata": {}, - "source": [ - "The brain_regions class has the following data attributes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d078160", - "metadata": {}, - "outputs": [], - "source": [ - "brain_regions.__annotations__" - ] - }, - { - "cell_type": "markdown", - "id": "44339559", - "metadata": {}, - "source": [ - "These attributes are the same as the Allen structure tree and for example `id` corresponds to the Allen atlas id while the `name` represents the full anatomical brain region name." - ] - }, - { - "cell_type": "markdown", - "id": "fbe04558", - "metadata": {}, - "source": [ - "The index refers to the index in each of these attribute arrays. For example, index 1 corresponds to the `root` brain region with an atlas id of 977. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0c1fdf7c", - "metadata": {}, - "outputs": [], - "source": [ - "index = 1\n", - "print(brain_regions.id[index])\n", - "print(brain_regions.acronym[index])" - ] - }, - { - "cell_type": "markdown", - "id": "fd8e542c", - "metadata": {}, - "source": [ - "Alternatively, index 1000 corresponds to `PPYd` with an atlas id of 185" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cf56d8d9", - "metadata": {}, - "outputs": [], - "source": [ - "index = 1000\n", - "print(brain_regions.id[index])\n", - "print(brain_regions.acronym[index])" - ] - }, - { - "cell_type": "markdown", - "id": "4c3acedd", - "metadata": {}, - "source": [ - "In the label volume we described above, it is these indices that we are referring to. Therefore, we know all voxels in the volume with a value of 0 will be voxels that lie in `root`, while the voxels that have a value of 1000 will be in `PPYd`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b607f170", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "root_voxels = np.where(brain_atlas.label == 1)\n", - "ppyd_voxels = np.where(brain_atlas.label == 1000)" - ] - }, - { - "cell_type": "markdown", - "id": "474bb26b", - "metadata": {}, - "source": [ - "An additional nuance is the lateralisation. If you compare the size of the brain_regions data class to the Allen structure tree. You will see that it has double the number of columms. This is because the IBL brain regions encodes both the left and right hemisphere. We can understand this better by exploring the `brain_regions.id` and `brain_regions.name` at the indices where it transitions between hemispheres." - ] - }, - { - "cell_type": "markdown", - "id": "861fef87", - "metadata": {}, - "source": [ - "The `brain_region.id` go from positive Allen atlas ids (right hemisphere) to negative Allen atlas ids (left hemisphere)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31cceb95", - "metadata": {}, - "outputs": [], - "source": [ - "print(brain_regions.id[1320:1340])" - ] - }, - { - "cell_type": "markdown", - "id": "e2221959", - "metadata": {}, - "source": [ - "The `brain_region.name` go from right to left hemisphere descriptions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "97079539", - "metadata": {}, - "outputs": [], - "source": [ - "print(brain_regions.name[1320:1340])" - ] - }, - { - "cell_type": "markdown", - "id": "7f35aa26", - "metadata": {}, - "source": [ - "In the label volume, we can therefore differentiate between left and right hemisphere voxels for the same brain region. First we will use a method in the brain_region class to find out the index of left and right `CA1`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4c93c1a0", - "metadata": {}, - "outputs": [], - "source": [ - "brain_regions.acronym2index('CA1')" - ] - }, - { - "cell_type": "markdown", - "id": "d8bb5fc2", - "metadata": {}, - "source": [ - "The method `acronym2index` returns a tuple, with the first value being a list of acronyms passed in and the second value giving the indices in the array that correspond to the left and right hemispheres for this region. We can now use these indices to search in the label volume" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0680ca09", - "metadata": {}, - "outputs": [], - "source": [ - "CA1_right = np.where(brain_atlas.label == 458)\n", - "CA1_left = np.where(brain_atlas.label == 1785)" - ] - }, - { - "cell_type": "markdown", - "id": "42cc166b", - "metadata": {}, - "source": [ - "## Coordinate systems" - ] - }, - { - "cell_type": "markdown", - "id": "7ffcd53b", - "metadata": {}, - "source": [ - "The voxles can be translated to 3D space. In the IBL all xyz coordinates" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb b/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb deleted file mode 100644 index 03ab28c20..000000000 --- a/examples/atlas/atlas_circular_pyramidal_flatmap.ipynb +++ /dev/null @@ -1,248 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f199bec2", - "metadata": {}, - "source": [ - "# Plotting brain region values on circular flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "94c08e66", - "metadata": {}, - "source": [ - "This example walks through various ways to overlay brain region values on a circular flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "17fd07ec", - "metadata": {}, - "source": [ - "## The circular flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "3ca88864", - "metadata": {}, - "source": [ - "The circular flatmap is obtained by sampling the volume using concentric circles through the brain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1178246b", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.flatmaps import FlatMap\n", - "flmap_cr = FlatMap(flatmap='circles')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "490614c3", - "metadata": {}, - "outputs": [], - "source": [ - "# Display the concentric circles used in flatmap\n", - "ax = flmap_cr.plot_top(volume='image')\n", - "ax.plot(flmap_cr.ml_scale * 1e6, flmap_cr.ap_scale * 1e6)" - ] - }, - { - "cell_type": "markdown", - "id": "135dd187", - "metadata": {}, - "source": [ - "This results in a flatmap that can be displayed in the following way" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8b8c4223", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "fig, ax = plt.subplots(figsize=(18,4))\n", - "flmap_cr.plot_flatmap(ax)" - ] - }, - { - "cell_type": "markdown", - "id": "ec15f88c", - "metadata": {}, - "source": [ - "It is also possible to display this flatmap such that each circle is stacked on top of eachother. For this, the **pyramid** flatmap should be used" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7461e3f8", - "metadata": {}, - "outputs": [], - "source": [ - "# Instantiate flatmap with circles arranged vetically on top of eachother\n", - "flmap_py = FlatMap(flatmap='pyramid')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f78b2ab", - "metadata": {}, - "outputs": [], - "source": [ - "fig, ax = plt.subplots(figsize=(8, 8))\n", - "flmap_py.plot_flatmap(ax=ax)" - ] - }, - { - "cell_type": "markdown", - "id": "e7af738a", - "metadata": {}, - "source": [ - "## Data preparation" - ] - }, - { - "cell_type": "markdown", - "id": "40fa09d0", - "metadata": {}, - "source": [ - "In order to plot brain regions values on the flatmap an array of acronyms and an array of values corresponding to each acronym must be provided. A detailed overview of how to prepare your data can be found [here](https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_plotting_scalar_on_slice.html#Data-preparation)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "20a1db83", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "# prepare array of acronyms\n", - "acronyms = np.array(['VPM', 'PO', 'LP', 'CA1', 'DG-mo', 'VISa5', 'SSs5'])\n", - "# assign data to each acronym\n", - "values = np.arange(acronyms.size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e6ae51d0", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.regions import BrainRegions\n", - "br = BrainRegions()\n", - "# prepare array of acronyms with beryl mapping\n", - "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", - "values_beryl = np.arange(acronyms_beryl.size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3724b968", - "metadata": {}, - "outputs": [], - "source": [ - "# prepare different values for left and right hemipshere for Beryl acronyms\n", - "values_beryl_lh = np.random.randint(0, 10, acronyms_beryl.size)\n", - "values_beryl_rh = np.random.randint(0, 10, acronyms_beryl.size)\n", - "values_beryl_lr = np.c_[values_beryl_lh, values_beryl_rh]" - ] - }, - { - "cell_type": "markdown", - "id": "74fe528a", - "metadata": {}, - "source": [ - "## Examples" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfa2d623", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import plot_scalar_on_flatmap\n", - "# Plot region values on the left hemisphere of circle flatmap overlaid on brain region boundaries using Allen mapping\n", - "fig, ax = plt.subplots(figsize=(18,4))\n", - "fig, ax = plot_scalar_on_flatmap(acronyms, values, hemisphere='left', mapping='Allen', flmap_atlas=flmap_cr, ax=ax)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cc78a1c7", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the both hemispheres of circle flatmap overlaid on the dwi Allen image using Beryl mapping\n", - "fig, ax = plt.subplots(figsize=(18,4))\n", - "fig, ax = plot_scalar_on_flatmap(acronyms_beryl, values_beryl, hemisphere='both', mapping='Beryl', background='image', \n", - " cmap='Reds', flmap_atlas=flmap_cr, ax=ax)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "37bf7bd8", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the right hemisphere of pyramidal flatmap overlaid on the dwi Allen image using Allen mapping\n", - "fig, ax = plt.subplots(figsize=(8,8))\n", - "fig, ax = plot_scalar_on_flatmap(acronyms, values, hemisphere='right', mapping='Allen', background='image', \n", - " cmap='Reds', flmap_atlas=flmap_py, ax=ax)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28f7f30c", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot two column region values on the both hemispheres of pyramidal flatmap overlaid on brain region boundaries \n", - "# using Beryl mapping\n", - "fig, ax = plt.subplots(figsize=(8,8))\n", - "fig, ax = plot_scalar_on_flatmap(acronyms_beryl, values_beryl_lr, hemisphere='both', mapping='Beryl', \n", - " background='boundary', cmap='Blues', flmap_atlas=flmap_py, ax=ax)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb b/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb deleted file mode 100644 index fd233d1b4..000000000 --- a/examples/atlas/atlas_dorsal_cortex_flatmap.ipynb +++ /dev/null @@ -1,203 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "fdc19afd", - "metadata": {}, - "source": [ - "# Plotting brain region values on cortical flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "328a2d74", - "metadata": {}, - "source": [ - "This example walks through various ways to overlay brain region values on a cortical flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "bd3eecf6", - "metadata": {}, - "source": [ - "## The dorsal cortex flatmap" - ] - }, - { - "cell_type": "markdown", - "id": "8354c14e", - "metadata": {}, - "source": [ - "The **dorsal_cortex** flatmap comprises a flattened volume of cortex up to depth 2000 um" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33fa3ec7", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.flatmaps import FlatMap\n", - "\n", - "res = 25\n", - "flmap = FlatMap(flatmap='dorsal_cortex', res_um=res)\n", - "\n", - "# Plot flatmap at depth = 0 \n", - "flmap.plot_flatmap(int(0 / res))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dfdd67e8", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot flatmap at depth = 800 um\n", - "flmap.plot_flatmap(int(800 / res))" - ] - }, - { - "cell_type": "markdown", - "id": "6e4a49d8", - "metadata": {}, - "source": [ - "## Data preparation" - ] - }, - { - "cell_type": "markdown", - "id": "919372e5", - "metadata": {}, - "source": [ - "In order to plot brain regions values on the flatmap an array of acronyms and an array of values corresponding to each acronym must be provided. A detailed overview of how to prepare your data can be found [here](https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_plotting_scalar_on_slice.html#Data-preparation)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0de1b18b", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "# prepare array of acronyms\n", - "acronyms = np.array(['ACAd1', 'ACAv1', 'AId1', 'AIp1', 'AIv1', 'AUDd1', 'AUDp1','AUDpo1', 'AUDv1', \n", - " 'SSp-m1', 'SSp-n1', 'SSp-tr1', 'SSp-ul1','SSp-un1', 'SSs1', \n", - " 'VISC1', 'VISa1', 'VISal1', 'VISam1', 'VISl1', 'VISli1', 'VISp1', 'VISp2/3', 'VISpl1', 'VISpm1', \n", - " 'SSp-n2/3', 'SSp-tr2/3', 'SSp-ul2/3', 'SSp-un2/3', 'SSs2/3',\n", - " 'VISC2/3', 'VISa2/3', 'VISal2/3', 'VISam2/3', 'VISl2/3','VISli2/3', 'VISp2/3', 'VISpl1', 'VISpl2/3'])\n", - "# assign data to each acronym\n", - "values = np.arange(acronyms.size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b9eb9b3a", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.regions import BrainRegions\n", - "br = BrainRegions()\n", - "# prepare array of acronyms with beryl mapping\n", - "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", - "values_beryl = np.arange(acronyms_beryl.size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "251385a1", - "metadata": {}, - "outputs": [], - "source": [ - "# prepare different values for left and right hemipshere for Beryl acronyms\n", - "values_beryl_lh = np.random.randint(0, 10, acronyms_beryl.size)\n", - "values_beryl_rh = np.random.randint(0, 10, acronyms_beryl.size)\n", - "values_beryl_lr = np.c_[values_beryl_lh, values_beryl_rh]" - ] - }, - { - "cell_type": "markdown", - "id": "6ad92aca", - "metadata": {}, - "source": [ - "## Examples" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e26dcf4c", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import plot_scalar_on_flatmap\n", - "\n", - "# Plot region values on the left hemisphere at depth=0um overlaid on boundary image using Allen mapping\n", - "fig, ax = plot_scalar_on_flatmap(acronyms, values, depth=0, mapping='Allen', hemisphere='left', background='boundary',\n", - " cmap='viridis', flmap_atlas=flmap)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "de4a63a0", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the right hemisphere at depth=200um overlaid on boundary image using Allen mapping and show cbar\n", - "fig, ax, cbar = plot_scalar_on_flatmap(acronyms, values, depth=200, mapping='Allen', hemisphere='right', background='boundary',\n", - " cmap='Reds', flmap_atlas=flmap, show_cbar=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "da848f40", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot single column region values on the both hemisphere at depth=800um overlaid dwi Allen image using Beryl mapping\n", - "fig, ax = plot_scalar_on_flatmap(acronyms_beryl, values_beryl, depth=800, mapping='Beryl', hemisphere='both', \n", - " background='image', cmap='plasma', flmap_atlas=flmap)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "322f3a04", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot two column region values on the both hemispheres at depth=0um on boundary image using Allen mapping\n", - "fig, ax, cbar = plot_scalar_on_flatmap(acronyms_beryl, values_beryl_lr, depth=0, mapping='Beryl', hemisphere='both', \n", - " background='boundary', cmap='Blues', flmap_atlas=flmap, show_cbar=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/atlas/atlas_mapping.ipynb b/examples/atlas/atlas_mapping.ipynb deleted file mode 100644 index a75818b02..000000000 --- a/examples/atlas/atlas_mapping.ipynb +++ /dev/null @@ -1,650 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "b8ea3853", - "metadata": {}, - "source": [ - "# Atlas mapping" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2dae63ed", - "metadata": {}, - "outputs": [], - "source": [ - "# import brain atlas and brain regions objects\n", - "from iblatlas.atlas import AllenAtlas\n", - "from iblatlas.regions import BrainRegions\n", - "ba = AllenAtlas()\n", - "br = BrainRegions() # br is also an attribute of ba so could to br = ba.regions" - ] - }, - { - "cell_type": "markdown", - "id": "279114b7", - "metadata": {}, - "source": [ - "## Available Mappings\n", - "Four mappings are currently available within the IBL, these are:\n", - "\n", - "1. Allen Atlas - total of 1328 annotation regions provided by Allen Atlas\n", - "2. Beryl Atlas - total of 308 annotation regions\n", - "3. Cosmos Atlas - total of 12 annotation regions\n", - "4. Swanson Atlas - total of 319 annotation regions (*)\n", - "\n", - "(*) Note: The dedicated mapping for plotting on Swanson flatmap is explained in this [webpage](https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_swanson_flatmap.html)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3c9d922c", - "metadata": { - "pycharm": { - "is_executing": true - } - }, - "outputs": [], - "source": [ - "# create figure\n", - "import matplotlib.pyplot as plt\n", - "fig, axs = plt.subplots(1, 3, figsize=(15, 18))\n", - "\n", - "# plot coronal slice at ap = -2000 um\n", - "ap = -2000 / 1e6\n", - "# Allen mapping\n", - "ba.plot_cslice(ap, volume='annotation', mapping='Allen', ax=axs[0])\n", - "_ = axs[0].set_title('Allen')\n", - "# Beryl mapping\n", - "ba.plot_cslice(ap, volume='annotation', mapping='Beryl', ax=axs[1])\n", - "_ = axs[1].set_title('Beryl')\n", - "# Cosmos mapping\n", - "ba.plot_cslice(ap, volume='annotation', mapping='Cosmos', ax=axs[2])\n", - "_ = axs[2].set_title('Cosmos')" - ] - }, - { - "cell_type": "markdown", - "source": [ - "The `br.mappings` contains the `index` of the region for a given mapping.\n", - "You can use these indices to find for example the acronyms contained in Cosmos:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "import numpy as np\n", - "cosmos_indices = np.unique(br.mappings['Cosmos'])\n", - "br.acronym[cosmos_indices]" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "You can check that all brain regions within these 4 mappings are contained in the Allen parcellation, for example for Beryl:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "set(np.unique(br.mappings['Beryl'])).difference(set(np.unique(br.mappings['Allen'])))\n", - "# Expect to return an empty set" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "id": "a8460f28", - "metadata": {}, - "source": [ - "## Understanding the mappings\n", - "The mappings store the highest level annotation (or parent node) that should be considered. Any regions that are children of these nodes are assigned the same annotation as their parent. \n", - "\n", - "For example, consider the region with the acronym **MDm** (Mediodorsal nucleus of the thalamus, medial part). Firstly, to navigate ourselves, we can find the acronyms of all the ancestors to this region," - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "08393219", - "metadata": {}, - "outputs": [], - "source": [ - "# First find the atlas_id associated with acronym MDm\n", - "atlas_id = br.acronym2id('MDm')\n", - "# Then find the acronyms of the ancestors of this region\n", - "print(br.ancestors(ids=atlas_id)['acronym'])" - ] - }, - { - "cell_type": "markdown", - "id": "9081a1b3", - "metadata": {}, - "source": [ - "We can then take a look at what acronym this region will be assigned under the different mappings. Under the Allen mapping we expect it to be assigned the same acronym as this is the lowest level region parcelation that we use." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ef838f27", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.acronym2acronym('MDm', mapping='Allen'))" - ] - }, - { - "cell_type": "markdown", - "id": "2cc3d224", - "metadata": {}, - "source": [ - "Under the Beryl mapping, **MDm** is given the acronym of it's parent, **MD**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3aa34ba5", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.acronym2acronym('MDm', mapping='Beryl'))" - ] - }, - { - "cell_type": "markdown", - "id": "f574d115", - "metadata": {}, - "source": [ - "Under the Cosmos mapping, it is assigned to the region **TH**" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f74fa398", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.acronym2acronym('MDm', mapping='Cosmos'))" - ] - }, - { - "cell_type": "markdown", - "source": [ - "Under the Swanson mapping, it is assigned to the region **MD**" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(br.acronym2acronym('MDm', mapping='Swanson'))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "id": "23ed91cb", - "metadata": {}, - "source": [ - "Therefore any clusters that are assigned an acronym **MDm** in the Allen mapping, will instead be assigned to the region **MD** in the Beryl or Swanson mapping, and **TH** in the Cosmos mapping." - ] - }, - { - "cell_type": "markdown", - "id": "509bcee9", - "metadata": {}, - "source": [ - "If a region is above (i.e. parent) of what is included in a mapping, the value for this region is set to root. For example **TH** is not included in the Beryl nor Swanson mapping" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76962ae7", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.acronym2acronym('TH', mapping='Beryl'))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(br.acronym2acronym('TH', mapping='Swanson'))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "However, a child region that is not included in the mapping will be returned as its closest parent in the mapping. For example, VISa is not included in Swanson, but its parent PLTp is:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(br.acronym2acronym('VISa', mapping='Swanson'))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "id": "607f1e1b", - "metadata": {}, - "source": [ - "## Lateralisation\n", - "Lateralised versions of each of the three mappings are also available. This allow regions on the left and right hemispheres of the brain to be differentiated. \n", - "\n", - "The convention used is that regions in the left hemisphere have negative atlas ids whereas those on the right hemisphere have positive atlas ids.\n", - "\n", - "For the non lateralised mappings the atlas id is always positive regardless of whether the region lies in the left or right hemisphere. Aggregating values over regions could result, therefore, in values from different hemispheres being considered together.\n", - "\n", - "One thing to be aware of, is that while lateralised mappings return distinct atlas ids for the left and right hemispheres, the acronyms returned are not lateralised. \n", - "\n", - "For example consider finding the atlas id when mapping the acronym **MDm** onto the Beryl atlas. When specifying the left hemisphere, the returned atlas id is negative" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "30f5e956", - "metadata": {}, - "outputs": [], - "source": [ - "# Left hemisphere gives negative atlas id\n", - "print(br.acronym2id('MDm', mapping='Beryl-lr', hemisphere='left'))" - ] - }, - { - "cell_type": "markdown", - "id": "aa94a06e", - "metadata": {}, - "source": [ - "When specifying the right hemisphere, the returned atlas id is positive" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9125fc7c", - "metadata": {}, - "outputs": [], - "source": [ - "# Left hemisphere gives negative atlas id\n", - "print(br.acronym2id('MDm', mapping='Beryl-lr', hemisphere='right'))" - ] - }, - { - "cell_type": "markdown", - "id": "be5e0654", - "metadata": {}, - "source": [ - "However, when converting from atlas id to acronym, regardless of whether we specify a negative (left hemisphere) or positive (right hemisphere) value, the returned acronym is always the same" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "aaa8b3f5", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.id2acronym(-362, mapping='Beryl-lr'))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "77291362", - "metadata": {}, - "outputs": [], - "source": [ - "print(br.id2acronym(362, mapping='Beryl-lr'))" - ] - }, - { - "cell_type": "markdown", - "id": "6dc83925", - "metadata": {}, - "source": [ - "## How to map your data" - ] - }, - { - "cell_type": "markdown", - "id": "9606cc0b", - "metadata": {}, - "source": [ - "### Mapping from mlapdv coordinates\n", - "The recommended and most versatile way to find the locations of clusters under different mappings is to use the mlapdv coordinates of the clusters. Given a probe insertion id, the clusters object can be loaded in using the following code" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bc33122d", - "metadata": {}, - "outputs": [], - "source": [ - "from brainbox.io.one import SpikeSortingLoader\n", - "from one.api import ONE\n", - "one = ONE()\n", - "\n", - "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", - "\n", - "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", - "spikes, clusters, channels = sl.load_spike_sorting()\n", - "clusters = sl.merge_clusters(spikes, clusters, channels)" - ] - }, - { - "cell_type": "markdown", - "id": "2a274d98", - "metadata": {}, - "source": [ - "You will find that the cluster object returned already contains an atlas_id attribute. These are the atlas ids that are obtained using the default mapping - **non lateralised Allen**. For this mapping regardless of whether the clusters lie in the right or left hemisphere the clusters are assigned positive atlas ids" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "03881f96", - "metadata": {}, - "outputs": [], - "source": [ - "print(clusters['atlas_id'][0:10])" - ] - }, - { - "cell_type": "markdown", - "id": "2170a215", - "metadata": {}, - "source": [ - "We can obtain the mlapdv coordinates of the clusters and explore in which hemisphere the clusters lie, clusters with negative x lie on the left hemisphere." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "74617c4b", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "mlapdv = np.c_[clusters['x'], clusters['y'], clusters['z']] # x = ml, y = ap, z = dv\n", - "clus_LH = np.sum(mlapdv[:, 0] < 0)\n", - "clus_RH = np.sum(mlapdv[:, 0] > 0)\n", - "print(f'Total clusters = {len(mlapdv)}, LH clusters = {clus_LH}, RH clusters = {clus_RH}')" - ] - }, - { - "cell_type": "markdown", - "id": "ac3c4252", - "metadata": {}, - "source": [ - "To get a better understanding of the difference between using lateralised and non-lateralised mappings, let's also make a manipulated version of the mlapdv positions where the first 5 clusters have been moved into the right hemisphere and call this `mlapdv_rh`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e2c981f", - "metadata": {}, - "outputs": [], - "source": [ - "mlapdv_rh = np.copy(mlapdv)\n", - "mlapdv_rh[0:5, 0] = -1 * mlapdv_rh[0:5, 0]\n", - "clus_LH = np.sum(mlapdv_rh[:, 0] < 0)\n", - "clus_RH = np.sum(mlapdv_rh[:, 0] > 0)\n", - "print(f'Total clusters = {len(mlapdv_rh)}, LH clusters = {clus_LH}, RH clusters = {clus_RH}')" - ] - }, - { - "cell_type": "markdown", - "id": "a1323aed", - "metadata": {}, - "source": [ - "To find the locations of the clusters in the brain from the mlapdv position we can use the [get_labels](https://int-brain-lab.github.io/iblenv/_autosummary/ibllib.atlas.atlas.html#ibllib.atlas.atlas.BrainAtlas.get_labels) method in the AllenAtlas object. First let's explore the output of using a non lateralised mapping." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d332a29a", - "metadata": {}, - "outputs": [], - "source": [ - "atlas_id_Allen = ba.get_labels(mlapdv, mapping='Allen')\n", - "atlas_id_Allen_rh = ba.get_labels(mlapdv_rh, mapping='Allen')\n", - "print(f'Non-lateralised Allen mapping ids using mlapdv: {atlas_id_Allen[0:10]}')\n", - "print(f'Non-lateralised Allen mapping ids using mlapdv_rh: {atlas_id_Allen_rh[0:10]}')" - ] - }, - { - "cell_type": "markdown", - "id": "5a2a5225", - "metadata": {}, - "source": [ - "Notice that regardless of whether the clusters lie in the left or right hemisphere the sign of the atlas id is the same. The result of this mapping is also equivalent the default output from `clusters['atlas_id']`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "76664a2d", - "metadata": {}, - "outputs": [], - "source": [ - "np.array_equal(clusters['atlas_id'], atlas_id_Allen)" - ] - }, - { - "cell_type": "markdown", - "id": "185f9f6b", - "metadata": {}, - "source": [ - "Now if we use the lateralised mapping, we notice that the clusters in the left hemisphere have been assigned negative atlas ids whereas those in the right hemisphere have positive atlas ids" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "af431f8d", - "metadata": {}, - "outputs": [], - "source": [ - "atlas_id_Allen = ba.get_labels(mlapdv, mapping='Allen-lr')\n", - "atlas_id_Allen_rh = ba.get_labels(mlapdv_rh, mapping='Allen-lr')\n", - "print(f'Lateralised Allen mapping ids using mlapdv: {atlas_id_Allen[0:10]}')\n", - "print(f'Lateralised Allen mapping ids using mlapdv_rh: {atlas_id_Allen_rh[0:10]}')" - ] - }, - { - "cell_type": "markdown", - "id": "b0a09001", - "metadata": {}, - "source": [ - "By changing the mapping argument that we pass in, we can also easily obtain the atlas ids for the Beryl and Cosmos mappings" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "91143d6a", - "metadata": {}, - "outputs": [], - "source": [ - "atlas_id_Beryl = ba.get_labels(mlapdv_rh, mapping='Beryl-lr')\n", - "atlas_id_Cosmos = ba.get_labels(mlapdv_rh, mapping='Cosmos')\n", - "print(f'Lateralised Beryl mapping ids using mlapdv_rh: {atlas_id_Beryl[0:10]}')\n", - "print(f'Non-lateralised Cosmos mapping ids using mlapdv_rh: {atlas_id_Cosmos[0:10]}')" - ] - }, - { - "cell_type": "markdown", - "id": "c229e12d", - "metadata": {}, - "source": [ - "### Mapping from atlas ids\n", - "Methods are available that allow you to translate atlas ids from one mapping to another. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8be221d9", - "metadata": {}, - "outputs": [], - "source": [ - "# map atlas ids from lateralised Allen to lateralised Beryl\n", - "atlas_id_Allen = ba.get_labels(mlapdv_rh, mapping='Allen-lr') # lateralised Allen\n", - "\n", - "remap_beryl = br.id2id(atlas_id_Allen, mapping='Beryl-lr')\n", - "print(f'Lateralised Beryl mapping ids using remap: {remap_beryl[0:10]}')\n", - "\n", - "# map atlas ids from lateralised Allen to non-lateralised Cosmos\n", - "remap_cosmos = br.id2id(atlas_id_Allen_rh, mapping='Cosmos')\n", - "print(f'Non-lateralised Cosmos mapping ids using remap: {remap_cosmos[0:10]}')" - ] - }, - { - "cell_type": "markdown", - "id": "2cf81730", - "metadata": {}, - "source": [ - "When remapping with atlas ids it is not possible to map from \n", - "\n", - "1. A non-lateralised to a lateralised mapping. \n", - "2. From a higher mapping to a lower one (e.g cannot map from Beryl to Allen, or Cosmos to Allen)\n", - "3. From Beryl to Cosmos\n", - "\n", - "This is why it is recommened to use mlapdv coordinates for remappings as it allows complete flexibility" - ] - }, - { - "cell_type": "markdown", - "id": "ec294cf4", - "metadata": {}, - "source": [ - "### Converting to acronyms\n", - "Methods are available to convert between atlas ids and acronyms. Note that when converting to acronyms, even if the atlas ids are lateralised the returned acronyms are not" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7f9428b9", - "metadata": {}, - "outputs": [], - "source": [ - "atlas_id_Allen = ba.get_labels(mlapdv_rh, mapping='Allen-lr') # lateralised Allen\n", - "acronym_Allen = br.id2acronym(atlas_id_Allen, mapping='Allen-lr')\n", - "print(f'Acronyms of lateralised Allen ids: {acronym_Allen[0:10]}')\n", - "\n", - "atlas_id_Allen = ba.get_labels(mlapdv_rh, mapping='Allen-lr') # Non-ateralised Allen\n", - "acronym_Allen = br.id2acronym(atlas_id_Allen)\n", - "print(f'Acronyms of non-lateralised Allen: {acronym_Allen[0:10]}')" - ] - }, - { - "cell_type": "markdown", - "id": "7cd9eb73", - "metadata": {}, - "source": [ - "It is also possible to simultaneously remap the acronyms with these methods" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0f82948f", - "metadata": {}, - "outputs": [], - "source": [ - "acronym_Cosmos = br.id2acronym(atlas_id_Allen, mapping='Cosmos-lr')\n", - "print(acronym_Cosmos[0:10])" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/examples/atlas/atlas_plotting_points_on_slice.ipynb b/examples/atlas/atlas_plotting_points_on_slice.ipynb deleted file mode 100644 index c013a97f8..000000000 --- a/examples/atlas/atlas_plotting_points_on_slice.ipynb +++ /dev/null @@ -1,251 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "87d1873a", - "metadata": {}, - "source": [ - "# Plotting cluster locations on histology slices" - ] - }, - { - "cell_type": "markdown", - "id": "77834a05", - "metadata": {}, - "source": [ - "This example walks through various ways to display the 3D location of clusters on histology slices" - ] - }, - { - "cell_type": "markdown", - "id": "5011bf58", - "metadata": {}, - "source": [ - "## Data Preparation" - ] - }, - { - "cell_type": "markdown", - "id": "60f93667", - "metadata": {}, - "source": [ - "For all examples below the xyz coordinates of each point and an array of values must be provided. Here we load in the spikesorting data for an example probe insertion and set the xyz coordinates to the coordinates of the clusters and the array of values to the firing rate." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a5d22548", - "metadata": {}, - "outputs": [], - "source": [ - "from one.api import ONE\n", - "from brainbox.io.one import SpikeSortingLoader\n", - "from iblatlas.atlas import AllenAtlas\n", - "import numpy as np\n", - "\n", - "one = ONE()\n", - "ba = AllenAtlas()\n", - "pid = 'da8dfec1-d265-44e8-84ce-6ae9c109b8bd'\n", - "sl = SpikeSortingLoader(pid=pid, one=one, atlas=ba)\n", - "spikes, clusters, channels = sl.load_spike_sorting()\n", - "clusters = sl.merge_clusters(spikes, clusters, channels)\n", - "\n", - "# Extract xyz coords from clusters dict\n", - "# Here we will set all ap values to a chosen value for visualisation purposes\n", - "xyz = np.c_[clusters['x'], np.ones_like(clusters['x']) * 400 / 1e6, clusters['z']]\n", - "values = clusters['firing_rate']" - ] - }, - { - "cell_type": "markdown", - "id": "c84d11bd", - "metadata": {}, - "source": [ - "## Example 1: Aggregation methods" - ] - }, - { - "cell_type": "markdown", - "id": "b9340c33", - "metadata": {}, - "source": [ - "The values of points that lie within the same voxel within the volume can be aggregated together in different ways by changing the `aggr` argument. Below shows an example where values in each voxel have been aggregated according to the average firing rate of the clusters (left) and according to the count of clusters in each voxel (right)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f35b8827", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import plot_points_on_slice\n", - "import matplotlib.pyplot as plt\n", - "\n", - "fig, axs = plt.subplots(1, 2, figsize=(16,4))\n", - "\n", - "# Plot points on the coronal slice at ap=400um overlaid overlaid on brain region boundaries using Allen mapping \n", - "# and a 3D gaussian kernel with fwhm=100um is applied\n", - "\n", - "# Values in the same voxel are aggregated by the mean\n", - "fig, ax = plot_points_on_slice(xyz, values=values, coord=400, slice='coronal', mapping='Allen', background='boundary', \n", - " cmap='Reds', aggr='mean', fwhm=100, brain_atlas=ba, ax=axs[0])\n", - "\n", - "# Values in the same voxel are aggregated by the count\n", - "# N.B. can also pass values=None in this case as they are not used in the computation\n", - "fig, ax, cbar = plot_points_on_slice(xyz, values=values, coord=400, slice='coronal', mapping='Allen', background='boundary', \n", - " cmap='Reds', aggr='count', fwhm=100, brain_atlas=ba, ax=axs[1], show_cbar=True)" - ] - }, - { - "cell_type": "markdown", - "id": "54650899", - "metadata": {}, - "source": [ - "The different options for aggregation are listed in the docstring of the function" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0b6ad58e", - "metadata": {}, - "outputs": [], - "source": [ - "help(plot_points_on_slice)" - ] - }, - { - "cell_type": "markdown", - "id": "ea0cf9a4", - "metadata": {}, - "source": [ - "## Example 2: Applying gaussian kernels with varying FWHM" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ba3ca84", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot points on the coronal slice at ap=400um overlaid overlaid on Allen dwi image using Cosmos mapping with\n", - "# values aggregated by max\n", - "\n", - "figs, axs = plt.subplots(1, 3, figsize=(18,4))\n", - "\n", - "# FWHM of 100 um\n", - "fig, ax = plot_points_on_slice(xyz, values=values, coord=400, slice='coronal', background='image', \n", - " cmap='Purples', aggr='max', fwhm=100, brain_atlas=ba, ax=axs[0])\n", - "\n", - "# FWHM of 300 um\n", - "fig, ax = plot_points_on_slice(xyz, values=values, coord=400, slice='coronal', background='image', \n", - " cmap='Purples', aggr='max', fwhm=300, brain_atlas=ba, ax=axs[1])\n", - "\n", - "# FWHM of 0 um\n", - "# if fwhm=0 no gaussian kernal applied\n", - "fig, ax = plot_points_on_slice(xyz, values=values, coord=400, slice='coronal', background='image', \n", - " cmap='Purples', aggr='max', fwhm=0, brain_atlas=ba, ax=axs[2])" - ] - }, - { - "cell_type": "markdown", - "id": "a0aca146", - "metadata": {}, - "source": [ - "## Example 3: Precomputing Volume" - ] - }, - { - "cell_type": "markdown", - "id": "19edcc5b", - "metadata": {}, - "source": [ - "Convolving the 3D volume with the gaussian kernal can take some time to compute, particularly when using a large fwhm value. When exploring the same volume at different coordinates and using different slices it is recommended to precompute the volume and then plot the slices. Below shows an example of how to do this." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "81f6919c", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import compute_volume_from_points, plot_volume_on_slice\n", - "\n", - "# Extract xyz coords from clusters dict\n", - "xyz = np.c_[clusters['x'], clusters['y'], clusters['z']]\n", - "values = clusters['amp_max']\n", - "\n", - "# Compute volume\n", - "volume = compute_volume_from_points(xyz, values=values, aggr='mean', fwhm=250, ba=ba)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9d938d24", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot points on the coronal slices on brain region boundaries using Beryl maping\n", - "figs, axs = plt.subplots(1, 3, figsize=(18,4))\n", - "fig, ax = plot_volume_on_slice(volume, coord=300, slice='coronal', mapping='Beryl', background='boundary', \n", - " cmap='Oranges', brain_atlas=ba, ax=axs[0])\n", - "ax.set_title('ap = 300um')\n", - "fig, ax = plot_volume_on_slice(volume, coord=400, slice='coronal', mapping='Beryl', background='boundary', \n", - " cmap='Oranges', brain_atlas=ba, ax=axs[1])\n", - "ax.set_title('ap = 400um')\n", - "fig, ax = plot_volume_on_slice(volume, coord=500, slice='coronal', mapping='Beryl', background='boundary', \n", - " cmap='Oranges', brain_atlas=ba,ax=axs[2])\n", - "ax.set_title('ap = 500um')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0311b5ad", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot points on the saggital slice at ap=-800um overlaid on brain region boundaries using Cosmos mapping\n", - "fig, ax, cbar = plot_volume_on_slice(volume, coord=-800, slice='sagittal', mapping='Cosmos', background='boundary', \n", - " cmap='Blues', brain_atlas=ba, clevels=[0, 2e-7], show_cbar=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7835fa36", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot points on the horizontal slice at dv=-5000um overlaid on allen dwi image\n", - "fig, ax = plot_volume_on_slice(volume, coord=-5000, slice='horizontal', background='image', cmap='Greens', brain_atlas=ba)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/examples/atlas/atlas_plotting_scalar_on_slice.ipynb b/examples/atlas/atlas_plotting_scalar_on_slice.ipynb deleted file mode 100644 index a5a2c81ea..000000000 --- a/examples/atlas/atlas_plotting_scalar_on_slice.ipynb +++ /dev/null @@ -1,277 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "12df3a26", - "metadata": {}, - "source": [ - "# Plotting brain region values on histology slices" - ] - }, - { - "cell_type": "markdown", - "id": "a056f78f", - "metadata": {}, - "source": [ - "This example walks through various ways to overlay brain region values on histology slices" - ] - }, - { - "cell_type": "markdown", - "id": "8cef5812", - "metadata": {}, - "source": [ - "## Data preparation\n", - "For all the examples below, an array of acronyms and an array of values corresponding to each acronym must be provided. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "dd06e7ff", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "# prepare array of acronyms\n", - "acronyms = np.array(['VPM', 'VPL', 'PO', 'LP', 'CA1', 'DG-mo', 'SSs5', 'VISa5', 'AUDv6a', 'MOp5', 'FRP5'])\n", - "# assign data to each acronym\n", - "values = np.arange(len(acronyms))\n", - "\n", - "# acronyms and values must have the same number of rows\n", - "assert (acronyms.size == values.size)" - ] - }, - { - "cell_type": "markdown", - "id": "e0b26454", - "metadata": {}, - "source": [ - "If different values for each acronym want to be shown on each hemisphere, the array of values must contain two columns, the first corresponding to values on the left hemisphere and the second corresponding to values on the right hemisphere" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fa6bdd24", - "metadata": {}, - "outputs": [], - "source": [ - "# values to be used for left and right hemisphere\n", - "values_lh = np.random.randint(0, 10, acronyms.size)\n", - "values_rh = np.random.randint(0, 10, acronyms.size)\n", - "values_lr = np.c_[values_lh, values_rh]" - ] - }, - { - "cell_type": "markdown", - "id": "7115c423", - "metadata": {}, - "source": [ - "When providing values for each hemisphere, if a value for a given acronym has only been computed, for example, in the left hemisphere, the corresponding value in right hemisphere array should be set to NaN. A helper function is available that will prepare the array of values in the correct format" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "06c9e0c7", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import prepare_lr_data\n", - "\n", - "acronyms_lh = np.array(['VPM', 'VPL', 'PO', 'LP', 'CA1', 'DG-mo'])\n", - "values_lh = np.random.randint(0, 10, acronyms_lh.size)\n", - "\n", - "acronyms_rh = np.array(['VPM', 'PO', 'LP', 'CA1', 'DG-mo', 'VISa5', 'SSs5'])\n", - "values_rh = np.random.randint(0, 10, acronyms_rh.size)\n", - "\n", - "acronyms_lr, values_lr = prepare_lr_data(acronyms_lh, values_lh, acronyms_rh, values_rh)" - ] - }, - { - "cell_type": "markdown", - "id": "45aa4b07", - "metadata": {}, - "source": [ - "Different mappings can be used when plotting the images (see [here](https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_mapping.html) for more information on atlas mappings). The acronyms provided must correspond to the specified map" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7afc0de4", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.regions import BrainRegions\n", - "br = BrainRegions()\n", - "\n", - "acronyms_beryl = np.unique(br.acronym2acronym(acronyms, mapping='Beryl'))\n", - "values_beryl = np.arange(acronyms_beryl.size)\n", - "\n", - "acronyms_cosmos = np.unique(br.acronym2acronym(acronyms, mapping='Cosmos'))\n", - "values_cosmos = np.arange(acronyms_cosmos.size)" - ] - }, - { - "cell_type": "markdown", - "id": "9bcda7f3", - "metadata": {}, - "source": [ - "## Example 1: Coronal slices" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33270b88", - "metadata": {}, - "outputs": [], - "source": [ - "from iblatlas.plots import plot_scalar_on_slice\n", - "from iblatlas.atlas import AllenAtlas\n", - "ba = AllenAtlas()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0731a71d", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the left hemisphere of a coronal slice at ap=-2000um overlaid on the dwi Allen image\n", - "fig, ax = plot_scalar_on_slice(acronyms, values, coord=-2000, slice='coronal', mapping='Allen', hemisphere='left',\n", - " background='image', cmap='Reds', brain_atlas=ba)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "284efd17", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot single column region values on both hemispheres of a coronal slice at ap=-2000um overlaid on brain region boundaries\n", - "# using Beryl mapping\n", - "# If values only contains one column, the values will be mirrored on each hemisphere\n", - "fig, ax = plot_scalar_on_slice(acronyms_beryl, values_beryl, coord=-2000, slice='coronal', mapping='Beryl', \n", - " hemisphere='both', background='boundary', cmap='Blues', brain_atlas=ba)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2df60008", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot two column region values on both hemispheres of a coronal slice at ap=-1800um overlaid on brain region boundaries\n", - "# and display colorbar\n", - "# Values contains two columns, so each hemisphere has different values \n", - "fig, ax, cbar = plot_scalar_on_slice(acronyms_lr, values_lr, coord=-1800, slice='coronal', mapping='Allen', hemisphere='both', \n", - " background='boundary', cmap='viridis', brain_atlas=ba, show_cbar=True)" - ] - }, - { - "cell_type": "markdown", - "id": "cbdc5f5d", - "metadata": {}, - "source": [ - "## Example 2: Sagittal slices" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25fec19d", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the left hemisphere of a sagittal slice at ml=-2000um overlaid on the dwi Allen image \n", - "# using cosmos mapping\n", - "fig, ax = plot_scalar_on_slice(acronyms_cosmos, values_cosmos, coord=-2000, slice='sagittal', mapping='Cosmos', \n", - " hemisphere='left', background='image', cmap='Greens', brain_atlas=ba)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4ee0db12", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on the left hemisphere of a sagittal slice at ml=-1000um overlaid on brain region boundaries\n", - "# Create figure before and pass in axis on which to plot\n", - "import matplotlib.pyplot as plt\n", - "fig, ax = plt.subplots()\n", - "fig, ax = plot_scalar_on_slice(acronyms, values, coord=-1000, slice='sagittal', mapping='Allen', hemisphere='left', \n", - " background='boundary', cmap='plasma', brain_atlas=ba, ax=ax)" - ] - }, - { - "cell_type": "markdown", - "id": "b78a2be9", - "metadata": {}, - "source": [ - "## Example 3: Horizontal slices" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "11f7862e", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot two column region values on the both hemispheres of a horizontal slice at dv=--2500um overlaid on the dwi Allen image\n", - "# Pass in clevels min max values to cap the colormap\n", - "fig, ax = plot_scalar_on_slice(acronyms_lr, values_lr, coord=-2500, slice='horizontal', mapping='Allen', hemisphere='both', \n", - " background='image', cmap='Reds', brain_atlas=ba, clevels=[0, 5])" - ] - }, - { - "cell_type": "markdown", - "id": "dbfc97d2", - "metadata": {}, - "source": [ - "## Example 4: Top view" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "238294a4", - "metadata": {}, - "outputs": [], - "source": [ - "# Plot region values on left hemisphere of a top view overlaid on brain region boundaries using Beryl mapping and show cbar\n", - "\n", - "fig, ax, cbar = plot_scalar_on_slice(acronyms_beryl, values_beryl, slice='top', mapping='Beryl', hemisphere='left', \n", - " background='boundary', cmap='Purples', brain_atlas=ba, show_cbar=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:iblenv] *", - "language": "python", - "name": "conda-env-iblenv-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/atlas/atlas_swanson_flatmap.ipynb b/examples/atlas/atlas_swanson_flatmap.ipynb deleted file mode 100644 index c476d6973..000000000 --- a/examples/atlas/atlas_swanson_flatmap.ipynb +++ /dev/null @@ -1,418 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Plotting brain region values on the Swanson flat map" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The Swanson flatmap is a 2D representation of the mouse brain to facilitate comparative analysis of brain data.\n", - "We extended the mouse atlas presented by [Hahn et al.](https://onlinelibrary.wiley.com/doi/full/10.1002/cne.24966?casa_token=kRb4fuUae6wAAAAA%3AHoiNx1MNVgZNUXT-MZN_mU6LAjKBiz5OE5cFj2Aj-GUE9l-oBllFUaM11XwCtEbpJyxKrwaMRnXC7MjY)\n", - "to interface programmatically with the Allen Atlas regions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "from iblatlas.plots import plot_swanson_vector\n", - "from iblatlas.regions import BrainRegions\n", - "\n", - "br = BrainRegions()\n", - "\n", - "# Plot Swanson map will default colors and acronyms\n", - "plot_swanson_vector(br=br, annotate=True)" - ] - }, - { - "cell_type": "markdown", - "source": [ - "### What regions are represented in the Swanson flatmap" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "The Swanson map holds 323 brain region acronyms.\n", - "To find these acronyms, use the indices stored in the swanson mapping:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "swanson_indices = np.unique(br.mappings['Swanson'])\n", - "swanson_ac = np.sort(br.acronym[swanson_indices])\n", - "swanson_ac.size" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Regions which are \"children\" or \"parents\" of a Swanson region will not be included in the acronyms. For example `VISa` is in Swanson, but its parent `PTLp` or child `VISa2/3` are not:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Example: VISa is in Swanson\n", - "print(np.isin(['VISa'], swanson_ac))\n", - "\n", - "# Example child: VISa2/3 is not in Swanson\n", - "print(np.isin(['VISa2/3'], swanson_ac))\n", - "\n", - "# Example parent: PTLp is not in Swanson\n", - "print(np.isin(['PTLp'], swanson_ac))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Also, only the indices corresponding to one hemisphere are represented in Swanson. For example, for VISa:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# indices of VISa\n", - "indices = br.acronym2index('VISa')[1][0]\n", - "print(f'Index {indices[0]} in swanson? {indices[0] in swanson_indices}')\n", - "print(f'Index {indices[1]} in swanson? {indices[1] in swanson_indices}')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "### Selecting the brain regions for plotting" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "You can only plot value for a given region that is in Swanson, or a parent region (see below for detailed explanation on this latter point). You cannot plot value on children regions of those in the Swanson mapping. In other words, the brain regions contains in the Swanson mapping are the lowest hierarchical level you can plot onto.\n", - "\n", - "This was done to ensure there is no confusion about how data is aggregated and represented per region.\n", - "For example, if you were to input values for both `VISa1` and `VISa2/3`, it is unclear whether the mean, median or else should have been plotted onto the `VISa` area - instead, we ask you to do the aggregation yourself and pass this into the plotting function.\n", - "\n", - "For example," - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# 'VISa', 'CA1', 'VPM' are in Swanson and all 3 are plotted\n", - "acronyms = ['VISa', 'CA1', 'VPM']\n", - "values = np.array([1.5, 3, 4])\n", - "plot_swanson_vector(acronyms, values, annotate=True,\n", - " annotate_list=['VISa', 'CA1', 'VPM'],empty_color='silver')\n", - "\n", - "# 'VISa1','VISa2/3' are not in Swanson, only 'CA1', 'VPM' are plotted\n", - "acronyms = ['VISa1','VISa2/3', 'CA1', 'VPM']\n", - "values = np.array([1, 2, 3, 4])\n", - "plot_swanson_vector(acronyms, values, annotate=True,\n", - " annotate_list=['VISa1','VISa2/3', 'CA1', 'VPM'],empty_color='silver')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "You can plot onto the parent of a region in the Swanson mapping, for example you can plot over `PTLp` (which is the parent of `VISa` and `VISrl`). This paints the same value across all regions of the Swanson mapping contained in the parent region." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Plotting over a parent region (PTLp) paints the same value across all children (VISa and VISrl)\n", - "acronyms = ['PTLp', 'CA1', 'VPM']\n", - "values = np.array([1.5, 3, 4])\n", - "plot_swanson_vector(acronyms, values, annotate=True,\n", - " annotate_list=['PTLp', 'CA1', 'VPM'],empty_color='silver')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Plotting over a parent and child region simultaneously will overwrite the corresponding portion of the parent region:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Plotting over 'PTLp' and overwriting the 'VISrl' value\n", - "acronyms = ['PTLp','VISrl', 'CA1', 'VPM']\n", - "values = np.array([1, 2, 3, 4])\n", - "plot_swanson_vector(acronyms, values, annotate=True,\n", - " annotate_list=['PTLp','VISrl', 'CA1', 'VPM'],empty_color='silver')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "As such, you can easily fill in a whole top-hierarchy region, supplemented by one particular region of interest." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "acronyms = ['Isocortex', 'VISa']\n", - "values = np.array([1, 2])\n", - "plot_swanson_vector(acronyms, values, annotate=True,\n", - " annotate_list=['Isocortex', 'VISa'],empty_color='silver')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "## Mapping to the swanson brain regions\n", - "\n", - "Similarly as explained in this [page](https://int-brain-lab.github.io/iblenv/notebooks_external/atlas_mapping.html), you can map brain regions to those found in Swanson using `br.acronym2acronym`:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "br.acronym2acronym('MDm', mapping='Swanson')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Plotting values on the swanson flatmap\n", - "### Single hemisphere display\n", - "You simply need to provide an array of brain region acronyms, and an array of corresponding values." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# prepare array of acronyms\n", - "acronyms = np.array(\n", - " ['VPLpc', 'PO', 'LP', 'DG', 'CA1', 'PTLp', 'MRN', 'APN', 'POL',\n", - " 'VISam', 'MY', 'PGRNl', 'IRN', 'PARN', 'SPVI', 'NTS', 'SPIV',\n", - " 'NOD', 'IP', 'AON', 'ORBl', 'AId', 'MOs', 'GRN', 'P', 'CENT',\n", - " 'CUL', 'COApm', 'PA', 'CA2', 'CA3', 'HY', 'ZI', 'MGv', 'LGd',\n", - " 'LHA', 'SF', 'TRS', 'PVT', 'LSc', 'ACAv', 'ACAd', 'MDRNv', 'MDRNd',\n", - " 'COPY', 'PRM', 'DCO', 'DN', 'SIM', 'MEA', 'SI', 'RT', 'MOp', 'PCG',\n", - " 'ICd', 'CS', 'PAG', 'SCdg', 'SCiw', 'VCO', 'ANcr1', 'ENTm', 'ENTl',\n", - " 'NOT', 'VPM', 'VAL', 'VPL', 'CP', 'SSp-ul', 'MV', 'VISl', 'LGv',\n", - " 'SSp-bfd', 'ANcr2', 'DEC', 'LD', 'SSp-ll', 'V', 'SUT', 'PB', 'CUN',\n", - " 'ICc', 'PAA', 'EPv', 'BLAa', 'CEAl', 'GPe', 'PPN', 'SCig', 'SCop',\n", - " 'SCsg', 'RSPd', 'RSPagl', 'VISp', 'HPF', 'MGm', 'SGN', 'TTd', 'DP',\n", - " 'ILA', 'PL', 'RSPv', 'SSp-n', 'ORBm', 'ORBvl', 'PRNc', 'ACB',\n", - " 'SPFp', 'VM', 'SUV', 'OT', 'MA', 'BST', 'LSv', 'LSr', 'UVU',\n", - " 'SSp-m', 'LA', 'CM', 'MD', 'SMT', 'PFL', 'MARN', 'PRE', 'POST',\n", - " 'PRNr', 'SSp-tr', 'PIR', 'CTXsp', 'RN', 'PSV', 'SUB', 'LDT', 'PAR',\n", - " 'SPVO', 'TR', 'VISpm', 'MS', 'COApl', 'BMAp', 'AMd', 'ICe', 'TEa',\n", - " 'MOB', 'SNr', 'GU', 'VISC', 'SSs', 'AIp', 'NPC', 'BLAp', 'SPVC',\n", - " 'PYR', 'AV', 'EPd', 'NLL', 'AIv', 'CLA', 'AAA', 'AUDv', 'TRN'],\n", - " dtype='0)[0]\n", - "negative_id = np.where(brain_regions.id<0)[0]\n", - "void_id = np.where(brain_regions.id==0)[0]\n", - "\n", - "print(len(positive_id) + len(negative_id) + len(void_id))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "We can understand this better by exploring the `brain_regions.id` and `brain_regions.name` at the indices where it transitions between hemispheres." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "The first value of `brain_region.id` is `void` (Allen id `0`):" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(brain_regions.id[index_void])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "The point of change between right and left hemisphere is at the index:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(len(positive_id))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Around this index, the `brain_region.id` go from positive Allen atlas ids (right hemisphere) to negative Allen atlas ids (left hemisphere)." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(brain_regions.id[1320:1340])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Regions are organised following the same index ordering in left/right hemisphere.\n", - "For example, you will find the same acronym `PPYd` at the index 1000, and once you've passed the positive integers:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 1000\n", - "print(brain_regions.acronym[index])\n", - "print(brain_regions.acronym[index + len(positive_id)])\n", - "# Note: do not re-use this approach, this is for explanation only - you will see below a dedicated function" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "id": "0a1af738", - "metadata": {}, - "source": [ - "The `brain_region.name` also go from right to left hemisphere:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c99a6e89", - "metadata": {}, - "outputs": [], - "source": [ - "print(brain_regions.name[1320:1340])" - ] - }, - { - "cell_type": "markdown", - "id": "c144bdd2", - "metadata": {}, - "source": [ - "In the label volume, we can therefore differentiate between left and right hemisphere voxels for the same brain region. First we will use a method in the brain_region class to find out the index of left and right `CA1`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0b7d5209", - "metadata": {}, - "outputs": [], - "source": [ - "brain_regions.acronym2index('CA1')\n", - "# The first values are the acronyms, the second values are the indices" - ] - }, - { - "cell_type": "markdown", - "id": "607ae9b6", - "metadata": {}, - "source": [ - "The method `acronym2index` returns a tuple, with the first value being a list of acronyms passed in and the second value giving the indices in the array that correspond to the left and right hemispheres for this region. We can now use these indices to search in the label volume" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0680ca09", - "metadata": {}, - "outputs": [], - "source": [ - "CA1_right = np.where(brain_atlas.label == 458)\n", - "CA1_left = np.where(brain_atlas.label == 1785)" - ] - }, - { - "cell_type": "markdown", - "source": [ - "## Navigate the brain region hierarchy\n", - "The 1328 regions in the Allen parcelation are organised in a hierarchical tree.\n", - "For example, the region PPY encompasses both the regions PPYd and PPYs.\n", - "\n", - "You can visually explore the hierarchy through this [webpage](https://openalyx.internationalbrainlab.org/admin/experiments/brainregion/) (username: `intbrainlab`, password: `international`).\n", - "(TODO THIS IS NOT A GREAT WAY, CHANGE TO OTHER REFERENCE)\n" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "markdown", - "source": [ - "### Ancestors\n", - "\n", - "To find ancestors of a region, i.e. regions that are higher in the hierarchy tree, use `brain_regions.ancestors`.\n", - "\n", - "Let's use the region PPYd as an example:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 1000 # Remember the Allen id at this index is 185\n", - "brain_regions.ancestors(ids=brain_regions.id[index])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "All parents along the hierarchy tree are returned.\n", - "The parents are organised in increasing order of `level` (0-1-2...), i.e. the highest, all-encompassing level is first (`root` in the example above).\n", - "Note:\n", - "- The fields contain all the parents regions, including the one passed in (which is last).\n", - "- The field `parent` returns the parent region id of the regions in `id` (you can notice they are the same as in `id` but incremented by one level).\n", - "- The field `order` returns values used for plotting (Note: this is *not* the parent's index)\n", - "\n", - "For example, the last `parent` region is PPY (which is indeed the closest parent of PPYd):" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 999\n", - "print(brain_regions.id[index])\n", - "print(brain_regions.acronym[index])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "### Descendants\n", - "To find the descendants of a region, use `brain_regions.descendants`.\n", - "\n", - "Let's use the region PPY as an example:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 999\n", - "brain_regions.descendants(ids=brain_regions.id[index])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Note:\n", - "- The fields contain all the descendant regions, including the one passed in (which is first).\n", - "- The field `parent` returns the parent region id of the regions in `id`.\n", - "- The field `order` returns values used for plotting (Note: this is *not* the parent's index)\n", - "\n", - "Note also that the `descendants` methods will return all descendants from all the different branches down, for example for PTLp :" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "atlas_id = brain_regions.acronym2id('PTLp')\n", - "# Print the acronyms of the descendants of this region\n", - "print(brain_regions.descendants(ids=atlas_id)['acronym'])\n", - "# Print the levels of the descendants of this region\n", - "print(brain_regions.descendants(ids=atlas_id)['level'])" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "### Find region at a particular place in the hierarchy\n", - "\n", - "#### Leaf node\n", - "\n", - "If you need to check a region is a leaf node, i.e. that it has no descendant, you could use the `descendants` method and check that the returned length of the `id` is one (i.e. it only returns itself).\n", - "\n", - "For example, PPYd is a leaf node:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 1000\n", - "ppyd_desc = brain_regions.descendants(ids=brain_regions.id[index])\n", - "\n", - "len(ppyd_desc['id']) == 1" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "However, there is a faster method.\n", - "To find all the regions that are leaf nodes, use `brain_regions.leaves`:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "brain_regions.leaves()" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "It is recommended you use this function to check whether a region is a leaf node:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "index = 1000\n", - "brain_regions.id[index] in brain_regions.leaves().id" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "#### Find region at a given hierarchy level\n", - "\n", - "To find all the regions that are on a given level of the hierarchy, use `brain_regions.level`:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "print(f'brain_regions.level contains {brain_regions.level.size} values, which are either {np.unique(brain_regions.level)}')" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Example: find the index and acronyms of brain regions at level 0 (i.e. highest parents):\n", - "index = np.where(brain_regions.level == 0)[0]\n", - "print(index)\n", - "brain_regions.acronym[index] # Note that root appears twice because of the lateralisation" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "id": "89d7f7d8", - "metadata": {}, - "source": [ - "## Coordinate systems" - ] - }, - { - "cell_type": "markdown", - "id": "0c47c5c7", - "metadata": {}, - "source": [ - "The voxels can be translated to 3D space.\n", - "In the IBL, all xyz coordinates are referenced from Bregma, which point is set as xyz coordinate [0,0,0].\n", - "\n", - "![IBL coordinate system](https://github.com/int-brain-lab/ibllib/blob/atlas_docs/examples/atlas/images/brain_xyz.png?raw=true)\n", - "\n", - "In contrast, in the Allen coordinate framework, the [0,0,0] point corresponds to one of the cubic volume edge." - ] - }, - { - "cell_type": "markdown", - "source": [ - "Below we show the value of Bregma in the Allen CCF space (in micrometer um):" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "from iblatlas.atlas import ALLEN_CCF_LANDMARKS_MLAPDV_UM\n", - "print(ALLEN_CCF_LANDMARKS_MLAPDV_UM)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "To translate this into an index into the volume `brain_atlas`, you need to divide by the atlas resolution (also in micrometer):" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Find bregma position in indices\n", - "bregma_index = ALLEN_CCF_LANDMARKS_MLAPDV_UM['bregma'] / brain_atlas.res_um" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "This index can be passed into `brain_atlas.bc.i2xyz` that converts volume indices into IBL xyz coordinates (i.e. relative to Bregma):" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Find bregma position in xyz in m (expect this to be 0 0 0)\n", - "bregma_xyz = brain_atlas.bc.i2xyz(bregma_index)\n", - "print(bregma_xyz)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "Functions exist in both direction, i.e. from a volume index to IBL xyz, and from xyz to an index.\n", - "Note that the functions return/input values are in *meters*, not micrometers." - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Convert from arbitrary index to xyz position (m) position relative to Bregma\n", - "index = np.array([102, 234, 178]).astype(float)\n", - "xyz = brain_atlas.bc.i2xyz(index)\n", - "print(f'xyz values are in meters: {xyz}')\n", - "\n", - "# Convert from xyz position (m) to index in atlas\n", - "xyz = np.array([-325, 4000, 250]) / 1e6\n", - "index = brain_atlas.bc.xyz2i(xyz)" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "To know the sign and voxel resolution for each xyz axis, use:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Find the resolution (in meter) of each axis\n", - "res_xyz = brain_atlas.bc.dxyz\n", - "\n", - "# Find the sign of each axis\n", - "sign_xyz = np.sign(res_xyz)\n", - "\n", - "print(f\"Resolution xyz: {res_xyz} in meter \\nSign xyz:\\t\\t{sign_xyz}\")" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - }, - { - "cell_type": "markdown", - "source": [ - "To jump directly from an Allen xyz value to an IBL xyz value, use `brain_atlas.ccf2xyz`:" - ], - "metadata": { - "collapsed": false - } - }, - { - "cell_type": "code", - "execution_count": null, - "outputs": [], - "source": [ - "# Example: Where is the Allen 0 relative to IBL Bregma?\n", - "# This will give the Bregma value shown above (in meters), but with opposite axis sign value\n", - "brain_atlas.ccf2xyz(np.array([0, 0, 0]))" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/examples/atlas/images/brain_xyz.png b/examples/atlas/images/brain_xyz.png deleted file mode 100644 index 5d707848e4787dea6ac63f1058193fac01b16e35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 116535 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 z%0A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 Date: Mon, 25 Sep 2023 08:55:20 +0100 Subject: [PATCH 8/9] release notes --- release_notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/release_notes.md b/release_notes.md index 7e1552106..3e74a481a 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,3 +1,8 @@ +## Develop +### features +- Deprecate ibllib.atlas. Code now contained in package iblatlas + + ## Release Notes 2.25 ### features From e47b8eac4a5084c0707316ef1c979180dcc23e37 Mon Sep 17 00:00:00 2001 From: Mayo Faulkner Date: Mon, 25 Sep 2023 09:48:52 +0100 Subject: [PATCH 9/9] add belly camera, fix passive signatures --- ibllib/pipes/dynamic_pipeline.py | 2 +- ibllib/pipes/ephys_preprocessing.py | 3 +-- ibllib/pipes/video_tasks.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ibllib/pipes/dynamic_pipeline.py b/ibllib/pipes/dynamic_pipeline.py index be6505268..7c8fd6065 100644 --- a/ibllib/pipes/dynamic_pipeline.py +++ b/ibllib/pipes/dynamic_pipeline.py @@ -306,7 +306,7 @@ def make_pipeline(session_path, **pkwargs): # Video tasks if 'cameras' in devices: cams = list(devices['cameras'].keys()) - subset_cams = [c for c in cams if c in ('left', 'right', 'body')] + subset_cams = [c for c in cams if c in ('left', 'right', 'body', 'belly')] video_kwargs = {'device_collection': 'raw_video_data', 'cameras': cams} video_compressed = sess_params.get_video_compressed(acquisition_description) diff --git a/ibllib/pipes/ephys_preprocessing.py b/ibllib/pipes/ephys_preprocessing.py index 22fdfd6c5..9cef81a34 100644 --- a/ibllib/pipes/ephys_preprocessing.py +++ b/ibllib/pipes/ephys_preprocessing.py @@ -1283,8 +1283,7 @@ class EphysPassive(tasks.Task): ('_spikeglx_sync.times.*', 'raw_ephys_data*', True), ('*.meta', 'raw_ephys_data*', True), ('*wiring.json', 'raw_ephys_data*', False), - ('_iblrig_RFMapStim.raw*', 'raw_passive_data', True), - ('_iblrig_taskSettings.raw*.json', 'raw_passive_data', True)], + ('_iblrig_RFMapStim.raw*', 'raw_passive_data', True)], 'output_files': [('_ibl_passiveGabor.table.csv', 'alf', True), ('_ibl_passivePeriods.intervalsTable.csv', 'alf', True), ('_ibl_passiveRFM.times.npy', 'alf', True), diff --git a/ibllib/pipes/video_tasks.py b/ibllib/pipes/video_tasks.py index 270a9c2cc..eaf00aaa0 100644 --- a/ibllib/pipes/video_tasks.py +++ b/ibllib/pipes/video_tasks.py @@ -270,7 +270,7 @@ def _run(self, **kwargs): mp4_files = self.session_path.joinpath(self.device_collection).glob('*.mp4') labels = [label_from_path(x) for x in mp4_files] - labels = [lab for lab in labels if lab in ('left', 'right', 'body')] + labels = [lab for lab in labels if lab in ('left', 'right', 'body', 'belly')] kwargs = {} if self.sync_namespace == 'timeline':