diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 1ae8ae98..2bf9e976 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -65,11 +65,13 @@ jobs: - uses: neuroinformatics-unit/actions/test@v2 with: python-version: ${{ matrix.python-version }} + secret-codecov-token: ${{ secrets.CODECOV_TOKEN }} use-xvfb: true test_numba_disabled: needs: [linting, manifest] name: Run tests with numba disabled + timeout-minutes: 60 runs-on: ubuntu-latest env: NUMBA_DISABLE_JIT: "1" @@ -89,6 +91,7 @@ jobs: - uses: neuroinformatics-unit/actions/test@v2 with: python-version: "3.10" + secret-codecov-token: ${{ secrets.CODECOV_TOKEN }} codecov-flags: "numba" # Run brainglobe-workflows brainmapper-CLI tests to check for @@ -96,6 +99,7 @@ jobs: test_brainmapper_cli: needs: [linting, manifest] name: Run brainmapper tests to check for breakages + timeout-minutes: 60 runs-on: ubuntu-latest steps: - name: Cache tensorflow model diff --git a/cellfinder/core/download/download.py b/cellfinder/core/download/download.py index 00d6dee3..cd96616f 100644 --- a/cellfinder/core/download/download.py +++ b/cellfinder/core/download/download.py @@ -2,6 +2,7 @@ import shutil import tarfile import urllib.request +from pathlib import Path from brainglobe_utils.general.config import get_config_obj from brainglobe_utils.general.system import disk_free_gb @@ -124,5 +125,8 @@ def write_model_to_config(new_model_path, orig_config, custom_config): data[i] = line.replace( f"model_path = '{orig_path}", f"model_path = '{new_model_path}" ) + + custom_config_path = Path(custom_config) + custom_config_path.parent.mkdir(parents=True, exist_ok=True) with open(custom_config, "w") as out_conf: out_conf.writelines(data) diff --git a/cellfinder/napari/detect/detect.py b/cellfinder/napari/detect/detect.py index 396f90b6..eaaab20f 100644 --- a/cellfinder/napari/detect/detect.py +++ b/cellfinder/napari/detect/detect.py @@ -11,9 +11,8 @@ from cellfinder.core.classify.cube_generator import get_cube_depth_min_max from cellfinder.napari.utils import ( add_layers, - header_label_widget, + cellfinder_header, html_label_widget, - widget_header, ) from .detect_containers import ( @@ -39,8 +38,60 @@ def detect_widget() -> FunctionGui: """ progress_bar = ProgressBar() + # options that is filled in from the gui + options = {"signal_image": None, "background_image": None, "viewer": None} + + # signal and background images are separated out from the main magicgui + # parameter selections and are inserted as widget children in their own + # sub-containers of the root. Because if these image parameters are + # included in the root widget, every time *any* parameter updates, the gui + # freezes for a bit likely because magicgui is processing something for + # all the parameters when any parameter changes. And this processing takes + # particularly long for image parameters. Placing them as sub-containers + # alleviates this + @magicgui( + call_button=False, + persist=False, + scrollable=False, + labels=False, + auto_call=True, + ) + def signal_image_opt( + viewer: napari.Viewer, + signal_image: napari.layers.Image, + ): + """ + magicgui widget for setting the signal_image parameter. + + Parameters + ---------- + signal_image : napari.layers.Image + Image layer containing the labelled cells + """ + options["signal_image"] = signal_image + options["viewer"] = viewer + + @magicgui( + call_button=False, + persist=False, + scrollable=False, + labels=False, + auto_call=True, + ) + def background_image_opt( + background_image: napari.layers.Image, + ): + """ + magicgui widget for setting the background image parameter. + + Parameters + ---------- + background_image : napari.layers.Image + Image layer without labelled cells + """ + options["background_image"] = background_image + @magicgui( - header=header_label_widget, detection_label=html_label_widget("Cell detection", tag="h3"), **DataInputs.widget_representation(), **DetectionInputs.widget_representation(), @@ -52,12 +103,8 @@ def detect_widget() -> FunctionGui: scrollable=True, ) def widget( - header, detection_label, data_options, - viewer: napari.Viewer, - signal_image: napari.layers.Image, - background_image: napari.layers.Image, voxel_size_z: float, voxel_size_y: float, voxel_size_x: float, @@ -86,10 +133,6 @@ def widget( Parameters ---------- - signal_image : napari.layers.Image - Image layer containing the labelled cells - background_image : napari.layers.Image - Image layer without labelled cells voxel_size_z : float Size of your voxels in the axial dimension voxel_size_y : float @@ -132,9 +175,24 @@ def widget( reset_button : Reset parameters to default """ + # we must manually call so that the parameters of these functions are + # initialized and updated. Because, if the images are open in napari + # before we open cellfinder, then these functions may never be called, + # even though the image filenames are shown properly in the parameters + # in the gui. Likely auto_call doesn't make magicgui call the functions + # in this circumstance, only if the parameters are updated once + # cellfinder plugin is fully open and initialized + signal_image_opt() + background_image_opt() + + signal_image = options["signal_image"] + background_image = options["background_image"] + viewer = options["viewer"] + if signal_image is None or background_image is None: show_info("Both signal and background images must be specified.") return + data_inputs = DataInputs( signal_image.data, background_image.data, @@ -205,8 +263,7 @@ def update_progress_bar(label: str, max: int, value: int): worker.update_progress_bar.connect(update_progress_bar) worker.start() - widget.header.value = widget_header - widget.header.native.setOpenExternalLinks(True) + widget.native.layout().insertWidget(0, cellfinder_header()) @widget.reset_button.changed.connect def restore_defaults(): @@ -226,6 +283,20 @@ def restore_defaults(): # Insert progress bar before the run and reset buttons widget.insert(-3, progress_bar) + # add the signal and background image parameters + # make it look as if it's directly in the root container + signal_image_opt.margins = 0, 0, 0, 0 + # the parameters are updated using `auto_call` only. If False, magicgui + # passes these as args to widget(), which doesn't list them as args + signal_image_opt.gui_only = True + widget.insert(3, signal_image_opt) + widget.signal_image_opt.label = "Signal image" + + background_image_opt.margins = 0, 0, 0, 0 + background_image_opt.gui_only = True + widget.insert(4, background_image_opt) + widget.background_image_opt.label = "Background image" + scroll = QScrollArea() scroll.setWidget(widget._widget._qwidget) widget._widget._qwidget = scroll diff --git a/cellfinder/napari/images/brainglobe.png b/cellfinder/napari/images/brainglobe.png deleted file mode 100644 index 427bdaba..00000000 Binary files a/cellfinder/napari/images/brainglobe.png and /dev/null differ diff --git a/cellfinder/napari/train/train.py b/cellfinder/napari/train/train.py index 4d5e005b..d604bf4b 100644 --- a/cellfinder/napari/train/train.py +++ b/cellfinder/napari/train/train.py @@ -8,11 +8,7 @@ from qtpy.QtWidgets import QScrollArea from cellfinder.core.train.train_yml import run as train_yml -from cellfinder.napari.utils import ( - header_label_widget, - html_label_widget, - widget_header, -) +from cellfinder.napari.utils import cellfinder_header, html_label_widget from .train_containers import ( MiscTrainingInputs, @@ -41,7 +37,6 @@ def run_training( def training_widget() -> FunctionGui: @magicgui( - header=header_label_widget, training_label=html_label_widget("Network training", tag="h3"), **TrainingDataInputs.widget_representation(), **OptionalNetworkInputs.widget_representation(), @@ -52,7 +47,6 @@ def training_widget() -> FunctionGui: scrollable=True, ) def widget( - header: dict, training_label: dict, data_options: dict, yaml_files: Path, @@ -161,8 +155,7 @@ def widget( ) worker.start() - widget.header.value = widget_header - widget.header.native.setOpenExternalLinks(True) + widget.native.layout().insertWidget(0, cellfinder_header()) @widget.reset_button.changed.connect def restore_defaults(): diff --git a/cellfinder/napari/utils.py b/cellfinder/napari/utils.py index 689b2842..d48b81fb 100644 --- a/cellfinder/napari/utils.py +++ b/cellfinder/napari/utils.py @@ -4,21 +4,7 @@ import numpy as np import pandas as pd from brainglobe_utils.cells.cells import Cell -from pkg_resources import resource_filename - -brainglobe_logo = resource_filename( - "cellfinder", "napari/images/brainglobe.png" -) - - -widget_header = """ -
Efficient cell detection in large images.
- - - - -For help, hover the cursor over each parameter. -""" # noqa: E501 +from brainglobe_utils.qtpy.logo import header_widget def html_label_widget(label: str, *, tag: str = "b") -> dict: @@ -31,13 +17,18 @@ def html_label_widget(label: str, *, tag: str = "b") -> dict: ) -header_label_widget = html_label_widget( - f""" - -
cellfinder
-""", - tag="h1", -) +def cellfinder_header(): + """ + Create the header containing the brainglobe logo and documentation links + for all cellfinder widgets. + """ + return header_widget( + "cellfinder", + "Efficient cell detection in large images.", + documentation_path="cellfinder/user-guide/napari-plugin/index.html", + citation_doi="https://doi.org/10.1371/journal.pcbi.1009074", + help_text="For help, hover the cursor over each parameter.", + ) def add_layers(points: List[Cell], viewer: napari.Viewer) -> None: diff --git a/pyproject.toml b/pyproject.toml index efdb36f0..2eeeb61e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "brainglobe-utils>=0.4.2", + "brainglobe-utils>=0.4.3", "brainglobe-napari-io>=0.3.4", "dask[array]", "fancylog>=0.0.7", @@ -120,7 +120,7 @@ python = 3.10: py310 [testenv] -commands = python -m pytest -v --color=yes +commands = python -m pytest -v --color=yes --cov=cellfinder --cov-report=xml deps = pytest pytest-cov