Skip to content

Commit

Permalink
Display BrainGlobe atlases
Browse files Browse the repository at this point in the history
Integrate BrainGlobe atlases so that after downloading an atlas, the GUI displays it. Extend the handler for opening BrainGlobe atlases to set up the atlas for use throughout the app and display it.

- Reset registered image suffixes even if ignoring filename changes if specified to reset
- Simplify checks before opening and removing atlases
- Provide feedback on fetching the atlas listing
- Fix to still show local atlases when unable to fetch cloud atlases
- Provide debugging when removing an atlas
- Add type hints to the BrainGlobe model module
  • Loading branch information
yoda-vid committed Aug 19, 2021
1 parent 1ace774 commit 0c60b4c
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 42 deletions.
59 changes: 50 additions & 9 deletions magmap/atlas/brain_globe.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""BrainGlobe integration in MageallanMapper"""
"""BrainGlobe model for integration in MagellanMapper"""

from requests.exceptions import ConnectionError
import shutil
from typing import Dict
from typing import Dict, Optional

from bg_atlasapi import list_atlases, bg_atlas

Expand All @@ -12,40 +12,81 @@


class BrainGlobeMM:
"""Model for BrainGlobe-MagellanMapper interactions.
Attributes:
atlases_avail: Dictionary of the names of available atlases from
BrainGlobe to their latest version string.
atlases_local: Dictionary of the names of locally downloaded BrainGlobe
atlases to their version string.
"""
def __init__(self):
self.atlases_avail: Dict[str, str] = {}
self.atlases_local: Dict[str, str] = {}

def get_avail_atlases(self):
def get_avail_atlases(self) -> Dict[str, str]:
"""Fetch the available atlases from BrainGlobe.
Returns:
Dictionary of the names of available atlases from BrainGlobe to
their latest version string.
"""
try:
self.atlases_avail = list_atlases.get_all_atlases_lastversions()
except ConnectionError:
_logger.warn("Unable to get BrainGlobe available atlases")
return self.atlases_avail

def get_local_atlases(self):
def get_local_atlases(self) -> Dict[str, str]:
"""Get local, downloaded BrainGlobe atlases.
Returns:
Dictionary of the names of locally downloaded BrainGlobe atlases
to their version string.
"""
self.atlases_local = {
a: list_atlases.get_local_atlas_version(a)
for a in list_atlases.get_downloaded_atlases()
}
return self.atlases_local

def get_atlas(self, name, download=True):
def get_atlas(
self, name: str, download: bool = True
) -> Optional[bg_atlas.BrainGlobeAtlas]:
"""Get a BrainGlobe atlas.
Args:
name: Name of atlas to retrieve.
download: True to download the atlas if not available locally;
False to return None if the atlas is not present.
Returns:
The BrainGlobe atlas instance.
"""
if not download and name not in self.atlases_local:
return None
atlas = bg_atlas.BrainGlobeAtlas(name)
return atlas

def remove_local_atlas(self, name):
def remove_local_atlas(self, name: str):
"""Remove local copy of downloaded BrainGlobe atlas.
Args:
name: Name of atlas to remove
"""
atlas = self.get_atlas(name, False)
print("atlas to remove", name, atlas)
if not atlas:
_logger.warn("'%s' atlas not found", name)
return
try:
print("path", atlas.root_dir)
if atlas.root_dir.is_dir():
print("removing")
shutil.rmtree(atlas.root_dir)
_logger.debug(
"Removed '%s' atlas from '%s'", name, atlas.root_dir)
except FileExistsError as e:
_logger.warn(e)
76 changes: 50 additions & 26 deletions magmap/gui/bg_panel.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,97 @@
"""Panel for BrainGlobe access"""
"""Panel controller for BrainGlobe access"""

from typing import Callable, Optional, Sequence, TYPE_CHECKING

from PyQt5 import QtCore

from magmap.atlas import brain_globe

if TYPE_CHECKING:
from bg_atlasapi import BrainGlobeAtlas


class SetupAtlasesThread(QtCore.QThread):
"""Thread for setting up file import by extracting image metadata.
"""Thread for setting atlases by fetching the BrainGlobe atlas listing.
Attributes:
fn_success (func): Signal taking
no arguments, to be emitted upon successfull import; defaults
to None.
brain_globe_mm: BrainGlobe-MagellanMapper model.
fn_success: Signal function taking no arguments, to be emitted upon
successfull import.
fn_progress: Signal function taking a string argument to emit feedback.
"""

signal = QtCore.pyqtSignal()
progress = QtCore.pyqtSignal(str)

def __init__(self, brain_globe_mm, fn_success):
"""Initialize the import thread."""
def __init__(
self, brain_globe_mm: brain_globe.BrainGlobeMM,
fn_success: Callable[[], None], fn_progress: Callable[[str], None]):
"""Initialize the setup thread."""
super().__init__()
self.bg_mm: brain_globe.BrainGlobeMM = brain_globe_mm
self.signal.connect(fn_success)
self.progress.connect(fn_progress)

def run(self):
"""Set up image import metadata."""
print("running")
self.bg_mm.get_avail_atlases()
"""Fetch the atlas listing."""
atlases = self.bg_mm.get_avail_atlases()
msg = ("Fetched atlases available from BrainGlobe" if atlases
else "Unable to access atlas listing from BrainGlobe. "
"Showing atlases dowloaded from BrainGlobe.")
self.progress.emit(msg)
self.signal.emit()


class AccessAtlasThread(QtCore.QThread):
"""Thread for setting up file import by extracting image metadata.
"""Thread for setting up a specific access.
Attributes:
fn_success (func): Signal taking
no arguments, to be emitted upon successfull import; defaults
to None.
fn_success: Signal function taking no arguments, to be emitted upon
successfull import.
fn_progress: Signal function taking a string argument to emit feedback.
"""

signal = QtCore.pyqtSignal()
signal = QtCore.pyqtSignal(object)
progress = QtCore.pyqtSignal(str)

def __init__(self, brain_globe_mm, name, fn_success, fn_progress):
"""Initialize the import thread."""
def __init__(
self, brain_globe_mm: brain_globe.BrainGlobeMM, name: str,
fn_success: Callable[[], None], fn_progress: Callable[[str], None]):
"""Initialize the atlas access thread."""
super().__init__()
self.bg_mm: brain_globe.BrainGlobeMM = brain_globe_mm
self.name = name
print("gonna get atlases")
self.signal.connect(fn_success)
self.progress.connect(fn_progress)
print("connected")

def run(self):
"""Set up image import metadata."""
print("running")
"""Access the atlas, including download if necessary."""
self.progress.emit(
f"Accessing atlas '{self.name}', downloading if necessary...")
atlas = self.bg_mm.get_atlas(self.name)
self.progress.emit(f"Atlas '{self.name}' accessed:\n{atlas}")
self.signal.emit()
self.signal.emit(atlas)


class BrainGlobePanel:
def __init__(self, fn_set_atlases_table, fn_set_feedback):
def __init__(
self, fn_set_atlases_table: Callable[[Sequence], None],
fn_set_feedback: Callable[[str], None],
fn_opened_atlas: Optional[Callable[
["BrainGlobeAtlas"], None]] = None):
# set up attributes
self.fn_set_atlases_table = fn_set_atlases_table
self.fn_set_feedback = fn_set_feedback
self.fn_opened_atlas = fn_opened_atlas

# set up BrainGlobe-MagellanMapper interface
self.bg_mm = brain_globe.BrainGlobeMM()

# fetch listing of available atlases
self._thread = SetupAtlasesThread(self.bg_mm, self.update_atlas_panel)
self._thread = SetupAtlasesThread(
self.bg_mm, self.update_atlas_panel, self.fn_set_feedback)
self._thread.start()

def update_atlas_panel(self):
Expand All @@ -89,13 +108,18 @@ def update_atlas_panel(self):
installed = "No"
data.append([name, ver, installed])
for name, ver in atlases_local.items():
if atlases and name not in atlases:
if not atlases or name not in atlases:
data.append([name, ver, "Yes"])
self.fn_set_atlases_table(data)

def _open_atlas_handler(self, atlas):
self.update_atlas_panel()
if self.fn_opened_atlas:
self.fn_opened_atlas(atlas)

def open_atlas(self, name):
self._thread = AccessAtlasThread(
self.bg_mm, name, self.update_atlas_panel, self.fn_set_feedback)
self.bg_mm, name, self._open_atlas_handler, self.fn_set_feedback)
self._thread.start()

def remove_atlas(self, name):
Expand Down
27 changes: 20 additions & 7 deletions magmap/gui/visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1769,17 +1769,20 @@ def _image_path_updated(self):
from the Numpy image filename. Processed files (eg ROIs, blobs)
will not be loaded for now.
"""
if self._ignore_filename or not self._filename:
ignore_filename = self._ignore_filename or not self._filename
reset_filename = self._reset_filename
if ignore_filename:
# avoid triggering file load, eg if only updating widget value;
# reset flags
self._ignore_filename = False
self._reset_filename = True
return

if self._reset_filename:

if reset_filename:
# reset registered suffixes
config.reg_suffixes = dict.fromkeys(config.RegSuffixes, None)
self._reset_filename = True

if ignore_filename or reset_filename: return

# load image if possible without allowing import, deconstructing
# filename from the selected imported image
Expand Down Expand Up @@ -3247,18 +3250,28 @@ def _set_bg_feedback(self, val, append=True):
else:
self._bg_feedback = val

def _bg_open_handler(self, bg_atlas):
config.filename = str(bg_atlas.root_dir)
self.update_filename(config.filename, True)
np_io.setup_images(
config.filename, allow_import=False, bg_atlas=bg_atlas)
self._setup_for_image()
self.redraw_selected_viewer()
self.update_imgadj_for_img()

def _setup_brain_globe(self):
panel = bg_panel.BrainGlobePanel(self._set_bg_atlases, self._set_bg_feedback)
panel = bg_panel.BrainGlobePanel(
self._set_bg_atlases, self._set_bg_feedback, self._bg_open_handler)
return panel

@on_trait_change("_bg_access_btn")
def _open_brain_globe_atlas(self):
if self._bg_atlases and self._bg_atlases_sel:
if self._bg_atlases_sel:
self._brain_globe_panel.open_atlas(self._bg_atlases_sel[0])

@on_trait_change("_bg_remove_btn")
def _remove_brain_globe_atlas(self):
if self._bg_atlases and self._bg_atlases_sel:
if self._bg_atlases_sel:
self._brain_globe_panel.remove_atlas(self._bg_atlases_sel[0])


Expand Down

0 comments on commit 0c60b4c

Please sign in to comment.