From 5512d8d63365da7bd5cee32d210c1c47f75fc9e3 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Thu, 25 Jul 2024 16:30:29 +0100 Subject: [PATCH 01/10] Add transform widget --- .github/workflows/test_and_deploy.yml | 19 +- MANIFEST.in | 1 + brainglobe_utils/brainmapper/analysis.py | 47 +- .../brainmapper/transform_widget.py | 550 +++++++++++++++++ brainglobe_utils/general/system.py | 28 + brainglobe_utils/napari.yaml | 12 + brainglobe_utils/qtpy/table.py | 118 ++++ pyproject.toml | 7 +- tests/tests/conftest.py | 4 +- tests/tests/test_brainmapper/test_analysis.py | 7 +- .../test_brainmapper/test_transform_widget.py | 567 ++++++++++++++++++ tests/tests/test_brainreg/__init__.py | 0 tests/tests/test_general/test_system.py | 16 + tests/tests/test_qtpy/test_table.py | 46 ++ 14 files changed, 1380 insertions(+), 42 deletions(-) create mode 100644 brainglobe_utils/brainmapper/transform_widget.py create mode 100644 brainglobe_utils/napari.yaml create mode 100644 brainglobe_utils/qtpy/table.py create mode 100644 tests/tests/test_brainmapper/test_transform_widget.py create mode 100644 tests/tests/test_brainreg/__init__.py create mode 100644 tests/tests/test_qtpy/test_table.py diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 3ec7775..761547e 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -26,9 +26,6 @@ jobs: needs: [linting, manifest] name: ${{ matrix.os }} py${{ matrix.python-version }} runs-on: ${{ matrix.os }} - env: - # pooch cache dir - BRAINGLOBE_TEST_DATA_DIR: "~/.pooch_cache" strategy: matrix: @@ -45,12 +42,13 @@ jobs: python-version: "3.12" steps: - - name: Cache pooch data + - name: Cache data uses: actions/cache@v4 with: - path: "~/.pooch_cache" + path: "$HOME/.brainglobe" # hash on conftest in case url changes - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/conftest.py') }} + key: brainglobe-dir-${{ runner.os }}${{ hashFiles('**/conftest.py') }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -72,16 +70,15 @@ jobs: runs-on: ubuntu-latest env: NUMBA_DISABLE_JIT: "1" - # pooch cache dir - BRAINGLOBE_TEST_DATA_DIR: "~/.pooch_cache" + steps: - - name: Cache pooch data + - name: Cache data uses: actions/cache@v4 with: - path: "~/.pooch_cache" + path: "$HOME/.brainglobe" # hash on conftest in case url changes - key: ${{ runner.os }}-3.11-${{ hashFiles('**/conftest.py') }} + key: brainglobe-dir-${{ runner.os }}${{ hashFiles('**/conftest.py') }} - name: Set up Python uses: actions/setup-python@v4 diff --git a/MANIFEST.in b/MANIFEST.in index bc165f3..a7ba596 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include LICENSE include README.md graft brainglobe_utils *.py +include brainglobe_utils/napari.yaml include brainglobe_utils/qtpy/brainglobe.png prune tests diff --git a/brainglobe_utils/brainmapper/analysis.py b/brainglobe_utils/brainmapper/analysis.py index e78369a..60067f7 100644 --- a/brainglobe_utils/brainmapper/analysis.py +++ b/brainglobe_utils/brainmapper/analysis.py @@ -3,9 +3,10 @@ by Charly Rousseau (https://github.com/crousseau). """ +import os from dataclasses import dataclass from pathlib import Path -from typing import List, Set, Union +from typing import List, Optional, Set, Union import numpy as np import pandas as pd @@ -104,15 +105,10 @@ def combine_df_hemispheres(df: pd.DataFrame) -> pd.DataFrame: return both -def create_all_cell_csv( - points: List[Point], output_filename: Union[str, Path] -) -> None: +def create_all_cell_df(points: List[Point]) -> None: """ - Create a CSV file with cell data from a list of Point objects. - This function takes a list of Point objects, each representing cell coordinates and brain region and converts this into a pandas DataFrame. - The DataFrame is then saved to a CSV file at the specified filename. Parameters ---------- @@ -120,16 +116,12 @@ def create_all_cell_csv( A list of Point objects, each containing cell data such as raw and atlas coordinates, structure name, and hemisphere information. - output_filename : Union[str, Path] - The filename (including path) where the CSV file will be saved. - Can be a string or a Path object. Returns ------- - None + df: pd.DataFrame """ - ensure_directory_exists(Path(output_filename).parent) df = pd.DataFrame( columns=( "coordinate_raw_axis_0", @@ -155,14 +147,13 @@ def create_all_cell_csv( temp_matrix[i].append(point.hemisphere) df = pd.DataFrame(temp_matrix, columns=df.columns, index=None) - df.to_csv(output_filename, index=False) + return df def count_points_per_brain_region( points: List[Point], structures_with_points: Set[str], brainreg_volume_csv_path: Union[str, Path], - output_filename: Union[str, Path], ) -> None: """ Count the number of points per brain region. @@ -177,12 +168,11 @@ def count_points_per_brain_region( brainreg_volume_csv_path : Union[str, Path] The path to the CSV file containing volume information from the brainreg registration. - output_filename : Union[str, Path] - The path where the summary of points by atlas region will be saved. + Returns ------- - None + df: pd.DataFrame """ structures_with_points = list(structures_with_points) @@ -219,17 +209,16 @@ def count_points_per_brain_region( combined_hemispheres = combine_df_hemispheres(sorted_point_numbers) df = calculate_densities(combined_hemispheres, brainreg_volume_csv_path) df = sanitise_df(df) - - df.to_csv(output_filename, index=False) + return df def summarise_points_by_atlas_region( points_in_raw_data_space: np.ndarray, points_in_atlas_space: np.ndarray, atlas: BrainGlobeAtlas, - brainreg_volume_csv_path: Union[str, Path], - points_list_output_filename: Union[str, Path], - summary_filename: Union[str, Path], + brainreg_volume_csv_path: Optional[os.PathLike] = None, + points_list_output_filename: Optional[os.PathLike] = None, + summary_filename: Optional[os.PathLike] = None, ) -> None: """ Summarise points data by atlas region. @@ -282,11 +271,19 @@ def summarise_points_by_atlas_region( except Exception: continue - create_all_cell_csv(points, points_list_output_filename) + all_cell_df = create_all_cell_df(points) - count_points_per_brain_region( + if points_list_output_filename is not None: + ensure_directory_exists(Path(points_list_output_filename).parent) + all_cell_df.to_csv(points_list_output_filename, index=False) + + points_per_region_df = count_points_per_brain_region( points, structures_with_points, brainreg_volume_csv_path, - summary_filename, ) + + if summary_filename is not None: + points_per_region_df.to_csv(summary_filename, index=False) + + return all_cell_df, points_per_region_df diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py new file mode 100644 index 0000000..09d071c --- /dev/null +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -0,0 +1,550 @@ +import json +from pathlib import Path +from typing import Any, Dict, List, Union + +import napari +import pandas as pd +import tifffile +from brainglobe_atlasapi import BrainGlobeAtlas +from brainglobe_atlasapi.list_atlases import get_downloaded_atlases +from brainglobe_space import AnatomicalSpace +from qtpy import QtCore +from qtpy.QtWidgets import ( + QComboBox, + QFileDialog, + QGridLayout, + QLabel, + QTableView, + QWidget, +) + +from brainglobe_utils.brainmapper.analysis import ( + summarise_points_by_atlas_region, +) +from brainglobe_utils.brainreg.transform import ( + transform_points_from_downsampled_to_atlas_space, +) +from brainglobe_utils.general.system import ensure_extension +from brainglobe_utils.qtpy.dialog import display_info +from brainglobe_utils.qtpy.interaction import add_button, add_combobox +from brainglobe_utils.qtpy.logo import header_widget +from brainglobe_utils.qtpy.table import DataFrameModel + + +class TransformPoints(QWidget): + def __init__(self, viewer: napari.viewer.Viewer): + super(TransformPoints, self).__init__() + self.viewer = viewer + self.raw_data = None + self.points_layer = None + self.atlas = None + self.transformed_points = None + + self.image_layer_names = self._get_layer_names() + self.points_layer_names = self._get_layer_names( + layer_type=napari.layers.Points + ) + self.setup_main_layout() + + @self.viewer.layers.events.connect + def update_layer_list(v: napari.viewer.Viewer): + """ + Update internal list of layers whenever the napari layers list + is updated. + """ + self.image_layer_names = self._get_layer_names() + self.points_layer_names = self._get_layer_names( + layer_type=napari.layers.Points + ) + + self._update_combobox_options( + self.raw_data_choice, self.image_layer_names + ) + + self._update_combobox_options( + self.points_layer_choice, self.points_layer_names + ) + + @staticmethod + def _update_combobox_options(combobox: QComboBox, options_list: List[str]): + original_text = combobox.currentText() + combobox.clear() + combobox.addItems(options_list) + combobox.setCurrentText(original_text) + + def _get_layer_names( + self, + layer_type: napari.layers.Layer = napari.layers.Image, + default: str = "", + ) -> List[str]: + """ + Get list of layer names of a given layer type. + """ + layer_names = [ + layer.name + for layer in self.viewer.layers + if type(layer) == layer_type + ] + + if layer_names: + return [default] + layer_names + else: + return [default] + + def setup_main_layout(self): + self.layout = QGridLayout() + self.layout.setContentsMargins(10, 10, 10, 10) + self.layout.setAlignment(QtCore.Qt.AlignTop) + self.layout.setSpacing(4) + self.add_header() + self.add_points_combobox(row=1, column=0) + self.add_raw_data_combobox(row=2, column=0) + self.add_transform_button(row=3, column=0) + + self.add_points_summary_table(row=4, column=0) + self.add_save_all_points_button(row=6, column=0) + self.add_save_points_summary_button(row=6, column=1) + self.add_status_label(row=7, column=0) + + self.setLayout(self.layout) + + def add_header(self): + """ + Header including brainglobe logo and documentation links. + """ + #
is included in the package_name to make the label under the logo + # more compact, by splitting it onto two lines + header = header_widget( + package_name="brainglobe-
utils", + package_tagline="Transform points to atlas space", + github_repo_name="brainglobe-utils", + citation_doi="https://doi.org/10.1038/s41598-021-04676-9", + help_text="For help, hover the cursor over each parameter.", + ) + self.layout.addWidget(header, 0, 0, 1, 2) + + def add_points_combobox(self, row, column): + self.points_layer_choice, _ = add_combobox( + self.layout, + "Points layer", + self.points_layer_names, + column=column, + row=row, + callback=self.set_points_layer, + ) + + def add_raw_data_combobox(self, row, column): + self.raw_data_choice, _ = add_combobox( + self.layout, + "Raw data layer", + self.image_layer_names, + column=column, + row=row, + callback=self.set_raw_data_layer, + ) + + def add_transform_button(self, row, column): + self.transform_button = add_button( + "Transform points", + self.layout, + self.transform_points_to_atlas_space, + row=row, + column=column, + visibility=True, + tooltip="Transform points layer to atlas space", + ) + + def add_points_summary_table(self, row, column): + self.points_per_region_table_title = QLabel( + "Points distribution summary" + ) + self.points_per_region_table_title.setVisible(False) + self.layout.addWidget(self.points_per_region_table_title, row, column) + self.points_per_region_table = QTableView() + self.points_per_region_table.setVisible(False) + self.layout.addWidget(self.points_per_region_table, row + 1, column) + + def add_save_all_points_button(self, row, column): + self.save_all_points_button = add_button( + "Save all points information", + self.layout, + self.save_all_points_csv, + row=row, + column=column, + visibility=False, + tooltip="Save all points information as a csv file", + ) + + def add_save_points_summary_button(self, row, column): + self.save_points_summary_button = add_button( + "Save points summary", + self.layout, + self.save_points_summary_csv, + row=row, + column=column, + visibility=False, + tooltip="Save points summary as a csv file", + ) + + def add_status_label(self, row, column): + self.status_label = QLabel() + self.status_label.setText("Ready") + self.layout.addWidget(self.status_label, row, column) + + def set_raw_data_layer(self): + """ + Set background layer from current background text box selection. + """ + if self.raw_data_choice.currentText() != "": + self.raw_data = self.viewer.layers[ + self.raw_data_choice.currentText() + ] + + def set_points_layer(self): + """ + Set background layer from current background text box selection. + """ + if self.points_layer_choice.currentText() != "": + self.points_layer = self.viewer.layers[ + self.points_layer_choice.currentText() + ] + + def transform_points_to_atlas_space(self): + layers_in_place = self.check_layers() + if not layers_in_place: + return + + self.status_label.setText("Loading brainreg data ...") + data_loaded = self.load_brainreg_directory() + + if not data_loaded: + self.status_label.setText("Ready") + return + + self.status_label.setText("Transforming points ...") + + self.run_transform_points_to_downsampled_space() + self.run_transform_downsampled_points_to_atlas_space() + + self.status_label.setText("Analysing point distribution ...") + self.analyse_points() + self.status_label.setText("Ready") + + def check_layers(self): + if self.raw_data is None and self.points_layer is None: + display_info( + self, + "No layers selected", + "Please select the layers corresponding to the points " + "you would like to transform and the raw data (registered by " + "brainreg)", + ) + return False + + if self.raw_data is None: + display_info( + self, + "No raw data layer selected", + "Please select a layer that corresponds to the raw " + "data (registered by brainreg)", + ) + return False + + if self.points_layer is None: + display_info( + self, + "No points layer selected", + "Please select a points layer you would like to transform", + ) + return False + + return True + + def load_brainreg_directory(self): + brainreg_directory = QFileDialog.getExistingDirectory( + self, + "Select brainreg directory", + ) + if brainreg_directory == "": + return False + else: + self.brainreg_directory = Path(brainreg_directory) + + self.initialise_brainreg_data() + self.status_label.setText("Ready") + return True + + def initialise_brainreg_data(self): + self.get_brainreg_paths() + self.check_brainreg_directory() + self.get_registration_metadata() + self.load_atlas() + + def get_brainreg_paths(self): + self.paths = Paths(self.brainreg_directory) + + def check_brainreg_directory(self): + try: + with open(self.paths.brainreg_metadata_file) as json_file: + self.brainreg_metadata = json.load(json_file) + + if "atlas" not in self.brainreg_metadata: + self.display_brainreg_directory_warning() + + except FileNotFoundError: + self.display_brainreg_directory_warning() + + def display_brainreg_directory_warning(self): + display_info( + self, + "Not a brainreg directory", + "This directory does not appear to be a valid brainreg " + "directory. Please try loading another brainreg output directory.", + ) + + def get_registration_metadata(self): + self.metadata = Metadata(self.brainreg_metadata) + + def load_atlas(self): + if not self.is_atlas_installed(self.metadata.atlas_string): + display_info( + self, + "Atlas not downloaded", + f"Atlas: {self.metadata.atlas_string} needs to be " + f"downloaded. This may take some time depending on " + f"the size of the atlas and your network speed.", + ) + self.atlas = BrainGlobeAtlas(self.metadata.atlas_string) + + def run_transform_points_to_downsampled_space(self): + downsampled_space = self.get_downsampled_space() + raw_data_space = self.get_raw_data_space() + self.points_in_downsampled_space = raw_data_space.map_points_to( + downsampled_space, self.points_layer.data + ) + self.viewer.add_points( + self.points_in_downsampled_space, + name="Points in downsampled space", + visible=False, + ) + + def run_transform_downsampled_points_to_atlas_space(self): + deformation_field_paths = [ + self.paths.deformation_field_0, + self.paths.deformation_field_1, + self.paths.deformation_field_2, + ] + self.points_in_atlas_space, points_out_of_bounds = ( + transform_points_from_downsampled_to_atlas_space( + self.points_in_downsampled_space, + self.atlas, + deformation_field_paths, + warn_out_of_bounds=False, + ) + ) + self.viewer.add_points( + self.points_in_atlas_space, + name="Points in atlas space", + visible=True, + ) + + if len(points_out_of_bounds) > 0: + display_info( + self, + "Points outside atlas", + f"{len(points_out_of_bounds)} " + f"points fell outside the atlas space", + ) + + def get_downsampled_space(self): + target_shape = tifffile.imread(self.paths.downsampled_image).shape + + downsampled_space = AnatomicalSpace( + self.atlas.orientation, + shape=target_shape, + resolution=self.atlas.resolution, + ) + return downsampled_space + + def get_raw_data_space(self): + raw_data_space = AnatomicalSpace( + self.metadata.orientation, + shape=self.raw_data.data.shape, + resolution=[float(i) for i in self.metadata.voxel_sizes], + ) + return raw_data_space + + def analyse_points(self): + self.all_points_df, self.points_per_region_df = ( + summarise_points_by_atlas_region( + self.points_layer.data, + self.points_in_atlas_space, + self.atlas, + self.paths.volume_csv_path, + ) + ) + + self.populate_summary_table() + self.save_all_points_button.setVisible(True) + self.save_points_summary_button.setVisible(True) + + print("Analysing points") + + def populate_summary_table( + self, + columns_to_keep=[ + "structure_name", + "left_cell_count", + "right_cell_count", + ], + ): + summary_df = self.points_per_region_df[columns_to_keep] + self.points_per_region_table_model = DataFrameModel(summary_df) + self.points_per_region_table.setModel( + self.points_per_region_table_model + ) + self.points_per_region_table_title.setVisible(True) + self.points_per_region_table.setVisible(True) + + def save_all_points_csv(self): + self.save_df_to_csv(self.all_points_df) + + def save_points_summary_csv(self): + self.save_df_to_csv(self.points_per_region_df) + + def save_df_to_csv(self, df: pd.DataFrame) -> None: + """ + Save the given DataFrame to a CSV file. + + Prompts the user to choose a filename and ensures the file has a + .csv extension. + The DataFrame is then saved to the specified file. + + Parameters + ---------- + df : pd.DataFrame + The DataFrame to be saved. + + Returns + ------- + None + """ + path, _ = QFileDialog.getSaveFileName( + self, + "Choose filename", + "", + "CSV Files (*.csv)", + ) + + if path: + path = ensure_extension(path, ".csv") + df.to_csv(path, index=False) + + @staticmethod + def is_atlas_installed(atlas): + downloaded_atlases = get_downloaded_atlases() + if atlas in downloaded_atlases: + return True + else: + return False + + +class Paths: + """ + A class to hold all brainreg-related file paths. + + N.B. this could be imported from brainreg, but it is copied here to + prevent a circular dependency + + Attributes + ---------- + brainreg_directory : Path + Path to brainreg output directory (or brainmapper + "registration" directory) + brainreg_metadata_file : Path + The path to the brainreg metadata (brainreg.json) file + deformation_field_0 : Path + The path to the deformation field (0th dimension) + deformation_field_1 : Path + The path to the deformation field (1st dimension) + deformation_field_2 : Path + The path to the deformation field (2nd dimension) + downsampled_image : Path + The path to the downsampled.tiff image file + volume_csv_path : Path + The path to the csv file containing region volumes + + Parameters + ---------- + brainreg_directory : Union[str, Path] + Path to brainreg output directory (or brainmapper + "registration" directory) + """ + + def __init__(self, brainreg_directory: Union[str, Path]) -> None: + self.brainreg_directory: Path = Path(brainreg_directory) + self.brainreg_metadata_file: Path = self.make_filepaths( + "brainreg.json" + ) + self.deformation_field_0: Path = self.make_filepaths( + "deformation_field_0.tiff" + ) + self.deformation_field_1: Path = self.make_filepaths( + "deformation_field_1.tiff" + ) + self.deformation_field_2: Path = self.make_filepaths( + "deformation_field_2.tiff" + ) + self.downsampled_image: Path = self.make_filepaths("downsampled.tiff") + self.volume_csv_path: Path = self.make_filepaths("volumes.csv") + + def make_filepaths(self, filename: str) -> Path: + """ + Create a full file path by combining the directory with a filename. + + Parameters + ---------- + filename : str + The name of the file to create a path for. + + Returns + ------- + Path + The full path to the specified file. + """ + return self.brainreg_directory / filename + + +class Metadata: + """ + A class to represent brainreg registration metadata + (loaded from brainreg.json) + + Attributes + ---------- + orientation : str + The orientation of the input data (in brainglobe-space format) + atlas_string : str + The BrainGlobe atlas used for brain registration. + voxel_sizes : List[float] + The voxel sizes of the input data + + Parameters + ---------- + brainreg_metadata : Dict[str, Any] + A dictionary containing metadata information, + loaded from brainreg.json + """ + + def __init__(self, brainreg_metadata: Dict[str, Any]) -> None: + """ + Initialize the Metadata instance with brainreg metadata. + + Parameters + ---------- + brainreg_metadata : Dict[str, Any] + A dictionary containing metadata information from brainreg.json + """ + self.orientation: str = brainreg_metadata["orientation"] + self.atlas_string: str = brainreg_metadata["atlas"] + self.voxel_sizes: List[float] = brainreg_metadata["voxel_sizes"] diff --git a/brainglobe_utils/general/system.py b/brainglobe_utils/general/system.py index f33046f..ee4ba20 100644 --- a/brainglobe_utils/general/system.py +++ b/brainglobe_utils/general/system.py @@ -6,6 +6,7 @@ import subprocess from pathlib import Path from tempfile import gettempdir +from typing import Union import psutil from natsort import natsorted @@ -20,6 +21,33 @@ MAX_PROCESSES_WINDOWS = 61 +def ensure_extension( + file_path: Union[str, os.PathLike], extension: str +) -> Path: + """ + Ensure that the given file path has the specified extension. + + If the file path does not already have the specified extension, + it changes the file path to have that extension. + + Parameters + ---------- + file_path : Union[str, os.PathLike] + The path to the file. + extension : str + The desired file extension (should include the dot, e.g., '.txt'). + + Returns + ------- + Path + The Path object with the ensured extension. + """ + path = Path(file_path) + if path.suffix != extension: + path = path.with_suffix(extension) + return path + + def replace_extension(file, new_extension, check_leading_period=True): """ Replaces the file extension of a given file. diff --git a/brainglobe_utils/napari.yaml b/brainglobe_utils/napari.yaml new file mode 100644 index 0000000..638c257 --- /dev/null +++ b/brainglobe_utils/napari.yaml @@ -0,0 +1,12 @@ +name: brainglobe-utils +display_name: BrainGlobe + +contributions: + commands: + - id: brainglobe-utils.TransformPoints + title: Open points transformation widget + python_name: brainglobe_utils.brainmapper.transform_widget:TransformPoints + + widgets: + - command: brainglobe-utils.TransformPoints + display_name: Transform points to a BrainGlobe atlas diff --git a/brainglobe_utils/qtpy/table.py b/brainglobe_utils/qtpy/table.py new file mode 100644 index 0000000..95280d5 --- /dev/null +++ b/brainglobe_utils/qtpy/table.py @@ -0,0 +1,118 @@ +from typing import Any, Optional + +import pandas as pd +from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt + + +class DataFrameModel(QAbstractTableModel): + """ + A Qt table model that wraps a pandas DataFrame for use with Qt + view widgets. + + Parameters + ---------- + df : pd.DataFrame + The DataFrame to be displayed in the Qt view. + + """ + + def __init__(self, df: pd.DataFrame): + """ + Initialize the model with a DataFrame. + + Parameters + ---------- + df : pd.DataFrame + The DataFrame to be displayed. + """ + super().__init__() + self._df = df + + def rowCount(self, parent: Optional[QModelIndex] = None) -> int: + """ + Return the number of rows in the model. + + Parameters + ---------- + parent : Optional[QModelIndex], optional + The parent index, by default None. + + Returns + ------- + int + The number of rows in the DataFrame. + """ + return self._df.shape[0] + + def columnCount(self, parent: Optional[QModelIndex] = None) -> int: + """ + Return the number of columns in the model. + + Parameters + ---------- + parent : Optional[QModelIndex], optional + The parent index, by default None. + + Returns + ------- + int + The number of columns in the DataFrame. + """ + return self._df.shape[1] + + def data( + self, index: QModelIndex, role: int = Qt.DisplayRole + ) -> Optional[Any]: + """ + Return the data at the given index for the specified role. + + Parameters + ---------- + index : QModelIndex + The index of the data to be retrieved. + role : int, optional + The role for which the data is being requested, + by default Qt.DisplayRole. + + Returns + ------- + Optional[Any] + The data at the specified index, or None if the role + is not Qt.DisplayRole. + """ + if role == Qt.DisplayRole: + return str(self._df.iloc[index.row(), index.column()]) + return None + + def headerData( + self, + section: int, + orientation: Qt.Orientation, + role: int = Qt.DisplayRole, + ) -> Optional[Any]: + """ + Return the header data for the specified section and orientation. + + Parameters + ---------- + section : int + The section (column or row) for which the header data is requested. + orientation : Qt.Orientation + The orientation (horizontal or vertical) of the header. + role : int, optional + The role for which the header data is being requested, by + default Qt.DisplayRole. + + Returns + ------- + Optional[Any] + The header data for the specified section and orientation, or + None if the role is not Qt.DisplayRole. + """ + + if role == Qt.DisplayRole: + if orientation == Qt.Horizontal: + return self._df.columns[section] + if orientation == Qt.Vertical: + return self._df.index[section] + return None diff --git a/pyproject.toml b/pyproject.toml index a1a8140..485b4f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,12 @@ bug_tracker = "https://github.com/brainglobe/brainglobe-utils/issues" source_code = "https://github.com/brainglobe/brainglobe-utils" user_support = "https://github.com/brainglobe/brainglobe-utils/issues" +[project.entry-points."napari.manifest"] +brainglobe-utils = "brainglobe_utils:napari.yaml" + [project.optional-dependencies] qt = ["qtpy", "superqt"] +napari = ["brainglobe-utils[qt]", "napari[all]"] dev = [ "black", @@ -65,7 +69,7 @@ dev = [ "setuptools_scm", "tox", "pooch", - "brainglobe-utils[qt]", + "brainglobe-utils[napari]", ] @@ -125,5 +129,4 @@ passenv = DISPLAY XAUTHORITY PYVISTA_OFF_SCREEN - BRAINGLOBE_TEST_DATA_DIR """ diff --git a/tests/tests/conftest.py b/tests/tests/conftest.py index f0a78cc..a554762 100644 --- a/tests/tests/conftest.py +++ b/tests/tests/conftest.py @@ -20,12 +20,12 @@ def test_data_registry(): """ registry = pooch.create( - path=pooch.os_cache("brainglobe_test_data"), + path=Path.home() / ".brainglobe" / "test_data", base_url="https://gin.g-node.org/BrainGlobe/test-data/raw/master/", registry={ "cellfinder/cells-z-1000-1050.xml": None, "cellfinder/other-cells-z-1000-1050.xml": None, + "brainglobe-utils/points_transform_brainreg_directory.zip": "a1997f61a5efa752584ea91b7c479506343215bb91f5be09a72349f24e21fc54", # noqa: E501 }, - env="BRAINGLOBE_TEST_DATA_DIR", ) return registry diff --git a/tests/tests/test_brainmapper/test_analysis.py b/tests/tests/test_brainmapper/test_analysis.py index f20d9eb..356f953 100644 --- a/tests/tests/test_brainmapper/test_analysis.py +++ b/tests/tests/test_brainmapper/test_analysis.py @@ -169,9 +169,12 @@ def test_get_region_totals( ) output_path = Path(tmp_path / "tmp_region_totals.csv") - count_points_per_brain_region( - points, structures_with_points, volumes_path, output_path + points_per_region_df = count_points_per_brain_region( + points, + structures_with_points, + volumes_path, ) + points_per_region_df.to_csv(output_path, index=False) assert output_path.exists() # Read data back in, and sort rows by the structures for comparison. diff --git a/tests/tests/test_brainmapper/test_transform_widget.py b/tests/tests/test_brainmapper/test_transform_widget.py new file mode 100644 index 0000000..d3050d5 --- /dev/null +++ b/tests/tests/test_brainmapper/test_transform_widget.py @@ -0,0 +1,567 @@ +import json +from pathlib import Path +from typing import Any, Dict + +import numpy as np +import pandas as pd +import pooch +import pytest +from brainglobe_atlasapi import BrainGlobeAtlas + +from brainglobe_utils.brainmapper.transform_widget import ( + Metadata, + Paths, + TransformPoints, +) + +RAW_DATA_ORIENTATION = ATLAS_ORIENTATION = "asr" +points = np.array( + [ + [10, 68, 105], + [10, 90, 134], + [10, 105, 157], + [36, 69, 86], + [36, 72, 155], + [36, 112, 128], + [74, 54, 60], + [74, 121, 50], + [74, 87, 153], + [74, 84, 169], + [74, 108, 156], + [74, 75, 148], + [74, 98, 169], + [74, 76, 159], + [74, 99, 156], + [74, 91, 146], + [74, 87, 160], + [112, 44, 60], + [112, 76, 136], + [156, 77, 54], + [173, 126, 159], + [201, 66, 130], + [219, 132, 199], + [219, 1, 1], + ] +) + +points_outside_brain = np.array( + [ + [10000, 10000, 10000], + [100001, 100001, 100001], + ] +) + + +points_in_downsampled_space = np.array( + [ + [10.0, 68.0, 105.0], + [10.0, 90.0, 134.0], + [10.0, 105.0, 157.0], + [36.0, 69.0, 86.0], + [36.0, 72.0, 155.0], + [36.0, 112.0, 128.0], + [74.0, 54.0, 60.0], + [74.0, 121.0, 50.0], + [74.0, 87.0, 153.0], + [74.0, 84.0, 169.0], + [74.0, 108.0, 156.0], + [74.0, 75.0, 148.0], + [74.0, 98.0, 169.0], + [74.0, 76.0, 159.0], + [74.0, 99.0, 156.0], + [74.0, 91.0, 146.0], + [74.0, 87.0, 160.0], + [112.0, 44.0, 60.0], + [112.0, 76.0, 136.0], + [156.0, 77.0, 54.0], + [173.0, 126.0, 159.0], + [201.0, 66.0, 130.0], + [219.0, 132.0, 199.0], + [219.0, 1.0, 1.0], + ] +) + +points_in_atlas_space = np.array( + [ + [36, 54, 97], + [34, 76, 124], + [34, 90, 146], + [61, 58, 82], + [62, 60, 145], + [58, 101, 120], + [100, 47, 60], + [93, 113, 53], + [95, 80, 146], + [95, 76, 161], + [93, 100, 148], + [97, 67, 141], + [94, 90, 161], + [97, 68, 151], + [94, 92, 148], + [95, 84, 139], + [95, 80, 152], + [139, 42, 60], + [131, 72, 132], + [173, 81, 56], + [177, 135, 155], + [214, 79, 129], + [218, 150, 194], + [249, 17, 10], + ] +) + + +@pytest.fixture +def sample_dataframe(): + return pd.DataFrame({"column1": [1, 2, 3], "column2": ["a", "b", "c"]}) + + +@pytest.fixture +def random_json_path(tmp_path): + json_path = tmp_path / "random_json.json" + content = { + "name": "Pooh Bear", + "location": "100 acre wood", + "food": "Honey", + } + with open(json_path, "w") as f: + json.dump(content, f) + + return json_path + + +@pytest.fixture +def mock_display_info(mocker): + return mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.display_info" + ) + + +@pytest.fixture(scope="function") +def transformation_widget_with_transformed_points( + transformation_widget_with_data, +): + transformation_widget_with_data.run_transform_points_to_downsampled_space() + transformation_widget_with_data.run_transform_downsampled_points_to_atlas_space() + return transformation_widget_with_data + + +@pytest.fixture(scope="function") +def transformation_widget_with_data( + transformation_widget_with_napari_layers, brainreg_directory +): + transformation_widget_with_napari_layers.brainreg_directory = ( + brainreg_directory + ) + transformation_widget_with_napari_layers.initialise_brainreg_data() + return transformation_widget_with_napari_layers + + +@pytest.fixture(scope="function") +def transformation_widget(make_napari_viewer): + viewer = make_napari_viewer() + widget = TransformPoints(viewer) + viewer.window.add_dock_widget(widget) + return widget + + +@pytest.fixture(scope="function") +def transformation_widget_with_napari_layers( + transformation_widget, brainreg_directory +): + points_layer = transformation_widget.viewer.add_points(points) + transformation_widget.points_layer = points_layer + + raw_data = brainreg_directory / "downsampled.tiff" + raw_data_layer = transformation_widget.viewer.open(raw_data) + transformation_widget.raw_data = raw_data_layer[0] + return transformation_widget + + +@pytest.fixture +def brainreg_directory(test_data_registry) -> Path: + _ = test_data_registry.fetch( + "brainglobe-utils/points_transform_brainreg_directory.zip", + progressbar=True, + processor=pooch.Unzip(extract_dir=""), + ) + return ( + Path.home() + / ".brainglobe" + / "test_data" + / "brainglobe-utils" + / "points_transform_brainreg_directory" + ) + + +def test_download_brainreg_directory(brainreg_directory): + assert brainreg_directory.exists() + + +def test_atlas_download(): + atlas_name = "allen_mouse_50um" + atlas = BrainGlobeAtlas(atlas_name) + assert atlas.atlas_name == atlas_name + + +@pytest.fixture +def dummy_brainreg_directory() -> Path: + return Path("/path/to/brainreg_directory") + + +@pytest.fixture +def dummy_brainreg_file_paths(dummy_brainreg_directory) -> Paths: + return Paths(dummy_brainreg_directory) + + +def test_initialise_brainreg_data( + transformation_widget_with_data, brainreg_directory +): + + assert ( + transformation_widget_with_data.paths.brainreg_directory + == brainreg_directory + ) + assert ( + transformation_widget_with_data.metadata.orientation + == ATLAS_ORIENTATION + ) + assert ( + transformation_widget_with_data.atlas.atlas_name == "allen_mouse_50um" + ) + + +def test_get_downsampled_space(transformation_widget_with_data): + downsampled_space = transformation_widget_with_data.get_downsampled_space() + assert downsampled_space.origin_string == ATLAS_ORIENTATION + + +def test_get_raw_data_space(transformation_widget_with_data): + raw_data_space = transformation_widget_with_data.get_raw_data_space() + assert raw_data_space.origin_string == RAW_DATA_ORIENTATION + + +def test_call_transform_points_to_atlas_space( + mocker, transformation_widget_with_data +): + mock_load_brainreg = mocker.patch.object( + transformation_widget_with_data, "load_brainreg_directory" + ) + mock_transform_downsampled = mocker.patch.object( + transformation_widget_with_data, + "run_transform_points_to_downsampled_space", + ) + mock_transform_atlas = mocker.patch.object( + transformation_widget_with_data, + "run_transform_downsampled_points_to_atlas_space", + ) + mock_analyse_points = mocker.patch.object( + transformation_widget_with_data, "analyse_points" + ) + + transformation_widget_with_data.transform_points_to_atlas_space() + mock_load_brainreg.assert_called_once() + mock_transform_downsampled.assert_called_once() + mock_transform_atlas.assert_called_once() + mock_analyse_points.assert_called_once() + + +def test_transform_points_to_atlas_space( + transformation_widget_with_transformed_points, +): + np.testing.assert_array_equal( + transformation_widget_with_transformed_points.viewer.layers[ + "Points in downsampled space" + ].data, + points_in_downsampled_space, + ) + np.testing.assert_array_equal( + transformation_widget_with_transformed_points.viewer.layers[ + "Points in atlas space" + ].data, + points_in_atlas_space, + ) + + +def test_transformation_raises_info_points_out_of_bounds( + transformation_widget_with_data, mock_display_info +): + points_layer = transformation_widget_with_data.viewer.add_points( + points_outside_brain + ) + transformation_widget_with_data.points_layer = points_layer + transformation_widget_with_data.run_transform_points_to_downsampled_space() + transformation_widget_with_data.run_transform_downsampled_points_to_atlas_space() + mock_display_info.assert_called_once_with( + transformation_widget_with_data, + "Points outside atlas", + "2 points fell outside the atlas space", + ) + + +def test_check_layers(transformation_widget_with_data): + assert transformation_widget_with_data.check_layers() + + +def test_check_layers_no_layers(transformation_widget, mock_display_info): + transformation_widget.check_layers() + + mock_display_info.assert_called_once_with( + transformation_widget, + "No layers selected", + "Please select the layers corresponding to the points " + "you would like to transform and the raw data (registered by " + "brainreg)", + ) + + +def test_check_layers_no_raw_data(transformation_widget, mock_display_info): + points_layer = transformation_widget.viewer.add_points(points) + transformation_widget.points_layer = points_layer + + transformation_widget.check_layers() + + mock_display_info.assert_called_once_with( + transformation_widget, + "No raw data layer selected", + "Please select a layer that corresponds to the raw " + "data (registered by brainreg)", + ) + + +def test_check_layers_no_points_data( + transformation_widget, brainreg_directory, mock_display_info +): + raw_data = brainreg_directory / "downsampled.tiff" + raw_data_layer = transformation_widget.viewer.open(raw_data) + transformation_widget.raw_data = raw_data_layer[0] + + transformation_widget.check_layers() + + mock_display_info.assert_called_once_with( + transformation_widget, + "No points layer selected", + "Please select a points layer you would like to transform", + ) + + +def test_load_brainreg_directory( + transformation_widget_with_napari_layers, brainreg_directory, mocker +): + # Mock dialog to avoid need for UI + mock_get_save_file_name = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.QFileDialog.getExistingDirectory" + ) + mock_get_save_file_name.return_value = brainreg_directory + + transformation_widget_with_napari_layers.load_brainreg_directory() + assert ( + transformation_widget_with_napari_layers.atlas.atlas_name + == "allen_mouse_50um" + ) + + +def test_load_brainreg_directory_no_input( + transformation_widget_with_napari_layers, mocker +): + # Mock dialog to avoid need for UI + mock_get_save_file_name = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.QFileDialog.getExistingDirectory" + ) + mock_get_save_file_name.return_value = "" + + transformation_widget_with_napari_layers.load_brainreg_directory() + assert not hasattr( + transformation_widget_with_napari_layers.atlas, "atlas_name" + ) + + +def test_check_brainreg_directory_correct_metadata( + mocker, transformation_widget_with_data +): + mock_method = mocker.patch.object( + transformation_widget_with_data, "display_brainreg_directory_warning" + ) + + transformation_widget_with_data.check_brainreg_directory() + mock_method.assert_not_called() + + +def test_check_brainreg_directory_random_data( + mocker, transformation_widget_with_data, random_json_path +): + mock_method = mocker.patch.object( + transformation_widget_with_data, "display_brainreg_directory_warning" + ) + transformation_widget_with_data.paths.brainreg_metadata_file = ( + random_json_path + ) + transformation_widget_with_data.check_brainreg_directory() + mock_method.assert_called_once() + + +def test_check_brainreg_directory_false_path( + mocker, transformation_widget_with_data +): + mock_method = mocker.patch.object( + transformation_widget_with_data, "display_brainreg_directory_warning" + ) + + transformation_widget_with_data.paths.brainreg_metadata_file = "/some/file" + transformation_widget_with_data.check_brainreg_directory() + mock_method.assert_called_once() + + +def test_display_brainreg_directory_warning_calls_display_info( + transformation_widget_with_napari_layers, mock_display_info +): + transformation_widget_with_napari_layers.display_brainreg_directory_warning() + + # Assert display_info was called once with the expected arguments + mock_display_info.assert_called_once_with( + transformation_widget_with_napari_layers, + "Not a brainreg directory", + "This directory does not appear to be a valid brainreg directory. " + "Please try loading another brainreg output directory.", + ) + + +def test_analysis(transformation_widget_with_transformed_points): + transformation_widget_with_transformed_points.analyse_points() + + assert ( + transformation_widget_with_transformed_points.all_points_df.shape[0] + == 21 + ) + + df = transformation_widget_with_transformed_points.points_per_region_df + assert ( + df.loc[ + df["structure_name"] == "Caudoputamen", "left_cell_count" + ].values[0] + == 9 + ) + assert ( + df.loc[df["structure_name"] == "Pons", "left_cell_count"].values[0] + == 1 + ) + assert ( + df.loc[ + df["structure_name"] + == "Primary somatosensory area, upper limb, layer 5", + "left_cells_per_mm3", + ].values[0] + == 0 + ) + + +def test_save_df_to_csv( + mocker, + transformation_widget_with_transformed_points, + sample_dataframe, + tmp_path, +): + # Mock dialog to avoid need for UI + mock_get_save_file_name = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.QFileDialog.getSaveFileName" + ) + + save_path = tmp_path / "test.csv" + mock_get_save_file_name.return_value = (save_path, "CSV Files (*.csv)") + + transformation_widget_with_transformed_points.save_df_to_csv( + sample_dataframe + ) + + # Ensure the file dialog was called + mock_get_save_file_name.assert_called_once_with( + transformation_widget_with_transformed_points, + "Choose filename", + "", + "CSV Files (*.csv)", + ) + + assert save_path.exists() + + +def test_save_all_points_and_summary_csv( + mocker, + transformation_widget_with_transformed_points, + tmp_path, +): + transformation_widget_with_transformed_points.analyse_points() + + # Mock dialog to avoid need for UI + mock_get_save_file_name = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.QFileDialog.getSaveFileName" + ) + + save_path = tmp_path / "all_points.csv" + mock_get_save_file_name.return_value = (save_path, "CSV Files (*.csv)") + transformation_widget_with_transformed_points.save_all_points_csv() + assert save_path.exists() + + save_path = tmp_path / "points_per_region.csv" + mock_get_save_file_name.return_value = (save_path, "CSV Files (*.csv)") + transformation_widget_with_transformed_points.save_points_summary_csv() + assert save_path.exists() + + +def test_is_atlas_installed(mocker, transformation_widget): + mock_get_downloaded_atlases = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.get_downloaded_atlases" + ) + mock_get_downloaded_atlases.return_value = [ + "allen_mouse_10um", + "allen_mouse_50um", + ] + + assert transformation_widget.is_atlas_installed("allen_mouse_10um") + assert not transformation_widget.is_atlas_installed("allen_mouse_25um") + + +def test_paths_initialisation( + dummy_brainreg_file_paths, dummy_brainreg_directory +): + assert ( + dummy_brainreg_file_paths.brainreg_directory + == dummy_brainreg_directory + ) + assert ( + dummy_brainreg_file_paths.brainreg_metadata_file + == dummy_brainreg_directory / "brainreg.json" + ) + assert ( + dummy_brainreg_file_paths.deformation_field_0 + == dummy_brainreg_directory / "deformation_field_0.tiff" + ) + assert ( + dummy_brainreg_file_paths.downsampled_image + == dummy_brainreg_directory / "downsampled.tiff" + ) + assert ( + dummy_brainreg_file_paths.volume_csv_path + == dummy_brainreg_directory / "volumes.csv" + ) + + +def test_make_filepaths(dummy_brainreg_file_paths, dummy_brainreg_directory): + filename = "test_file.txt" + expected_path = dummy_brainreg_directory / filename + assert dummy_brainreg_file_paths.make_filepaths(filename) == expected_path + + +@pytest.fixture +def sample_metadata() -> Dict[str, Any]: + return { + "orientation": "prs", + "atlas": "allen_mouse_25um", + "voxel_sizes": [5, 2, 2], + } + + +def test_metadata_initialisation(sample_metadata): + metadata = Metadata(sample_metadata) + assert metadata.orientation == sample_metadata["orientation"] + assert metadata.atlas_string == sample_metadata["atlas"] + assert metadata.voxel_sizes == sample_metadata["voxel_sizes"] diff --git a/tests/tests/test_brainreg/__init__.py b/tests/tests/test_brainreg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/tests/test_general/test_system.py b/tests/tests/test_general/test_system.py index 16ef4cf..ea851b8 100644 --- a/tests/tests/test_general/test_system.py +++ b/tests/tests/test_general/test_system.py @@ -64,6 +64,22 @@ def mock_statvfs(): return mock_stats +def test_ensure_extension(): + assert system.ensure_extension("example.txt", ".txt") == Path( + "example.txt" + ) + assert system.ensure_extension(Path("example.txt"), ".txt") == Path( + "example.txt" + ) + + assert system.ensure_extension("example.md", ".txt") == Path("example.txt") + assert system.ensure_extension(Path("example.md"), ".txt") == Path( + "example.txt" + ) + + assert system.ensure_extension("example", ".txt") == Path("example.txt") + + def test_replace_extension(): test_file = "test_file.sh" test_ext = "txt" diff --git a/tests/tests/test_qtpy/test_table.py b/tests/tests/test_qtpy/test_table.py new file mode 100644 index 0000000..952e4d6 --- /dev/null +++ b/tests/tests/test_qtpy/test_table.py @@ -0,0 +1,46 @@ +import pandas as pd +import pytest +from qtpy.QtCore import Qt + +from brainglobe_utils.qtpy.table import DataFrameModel + + +@pytest.fixture +def sample_df(): + return pd.DataFrame( + {"A": [1, 2, 3], "B": ["cat", "dog", "rabbit"], "C": [7, 8, 9]} + ) + + +@pytest.fixture +def model(sample_df): + return DataFrameModel(sample_df) + + +def test_row_count(model, sample_df): + assert model.rowCount() == sample_df.shape[0] + + +def test_column_count(model, sample_df): + assert model.columnCount() == sample_df.shape[1] + + +def test_data(model): + index = model.index(0, 0) + assert model.data(index, Qt.DisplayRole) == "1" + index = model.index(1, 1) + assert model.data(index, Qt.DisplayRole) == "dog" + index = model.index(2, 2) + assert model.data(index, Qt.DisplayRole) == "9" + + +def test_header_data(model, sample_df): + assert ( + model.headerData(0, Qt.Vertical, Qt.DisplayRole) == sample_df.index[0] + ) + assert ( + model.headerData(1, Qt.Vertical, Qt.DisplayRole) == sample_df.index[1] + ) + assert ( + model.headerData(2, Qt.Vertical, Qt.DisplayRole) == sample_df.index[2] + ) From 5837ad6b923fd192db082c91b521e011a30a6282 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Thu, 25 Jul 2024 18:01:28 +0100 Subject: [PATCH 02/10] Improve testing --- .../test_brainmapper/test_transform_widget.py | 30 +++++++++++++++++++ tests/tests/test_qtpy/test_table.py | 3 ++ 2 files changed, 33 insertions(+) diff --git a/tests/tests/test_brainmapper/test_transform_widget.py b/tests/tests/test_brainmapper/test_transform_widget.py index d3050d5..83bbf89 100644 --- a/tests/tests/test_brainmapper/test_transform_widget.py +++ b/tests/tests/test_brainmapper/test_transform_widget.py @@ -426,6 +426,36 @@ def test_display_brainreg_directory_warning_calls_display_info( ) +def test_warn_user_about_atlas_download( + transformation_widget_with_data, mocker, mock_display_info +): + mock_is_atlas_installed = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.TransformPoints.is_atlas_installed" + ) + mock_is_atlas_installed.return_value = False + + transformation_widget_with_data.load_atlas() + mock_display_info.assert_called_once_with( + transformation_widget_with_data, + "Atlas not downloaded", + "Atlas: allen_mouse_50um needs to be " + "downloaded. This may take some time depending on " + "the size of the atlas and your network speed.", + ) + +def test_layers_not_in_place_return(mocker, transformation_widget): + mock_is_atlas_installed = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.TransformPoints.check_layers" + ) + mock_is_atlas_installed.return_value = False + assert transformation_widget.transform_points_to_atlas_space() is None + +def test_transform_points_return_if_no_brainreg(mocker, transformation_widget_with_data): + mock_is_atlas_installed = mocker.patch( + "brainglobe_utils.brainmapper.transform_widget.TransformPoints.load_brainreg_directory" + ) + mock_is_atlas_installed.return_value = False + assert transformation_widget_with_data.transform_points_to_atlas_space() is None def test_analysis(transformation_widget_with_transformed_points): transformation_widget_with_transformed_points.analyse_points() diff --git a/tests/tests/test_qtpy/test_table.py b/tests/tests/test_qtpy/test_table.py index 952e4d6..3de9490 100644 --- a/tests/tests/test_qtpy/test_table.py +++ b/tests/tests/test_qtpy/test_table.py @@ -33,6 +33,7 @@ def test_data(model): index = model.index(2, 2) assert model.data(index, Qt.DisplayRole) == "9" + assert model.data(index, Qt.EditRole) is None def test_header_data(model, sample_df): assert ( @@ -44,3 +45,5 @@ def test_header_data(model, sample_df): assert ( model.headerData(2, Qt.Vertical, Qt.DisplayRole) == sample_df.index[2] ) + + From 695b1b2f8957bf807c13ab8b8d3fc7a68a60ddde Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 17:04:37 +0000 Subject: [PATCH 03/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../tests/test_brainmapper/test_transform_widget.py | 13 +++++++++++-- tests/tests/test_qtpy/test_table.py | 3 +-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/tests/test_brainmapper/test_transform_widget.py b/tests/tests/test_brainmapper/test_transform_widget.py index 83bbf89..c4f8523 100644 --- a/tests/tests/test_brainmapper/test_transform_widget.py +++ b/tests/tests/test_brainmapper/test_transform_widget.py @@ -443,6 +443,7 @@ def test_warn_user_about_atlas_download( "the size of the atlas and your network speed.", ) + def test_layers_not_in_place_return(mocker, transformation_widget): mock_is_atlas_installed = mocker.patch( "brainglobe_utils.brainmapper.transform_widget.TransformPoints.check_layers" @@ -450,12 +451,20 @@ def test_layers_not_in_place_return(mocker, transformation_widget): mock_is_atlas_installed.return_value = False assert transformation_widget.transform_points_to_atlas_space() is None -def test_transform_points_return_if_no_brainreg(mocker, transformation_widget_with_data): + +def test_transform_points_return_if_no_brainreg( + mocker, transformation_widget_with_data +): mock_is_atlas_installed = mocker.patch( "brainglobe_utils.brainmapper.transform_widget.TransformPoints.load_brainreg_directory" ) mock_is_atlas_installed.return_value = False - assert transformation_widget_with_data.transform_points_to_atlas_space() is None + assert ( + transformation_widget_with_data.transform_points_to_atlas_space() + is None + ) + + def test_analysis(transformation_widget_with_transformed_points): transformation_widget_with_transformed_points.analyse_points() diff --git a/tests/tests/test_qtpy/test_table.py b/tests/tests/test_qtpy/test_table.py index 3de9490..2014d2c 100644 --- a/tests/tests/test_qtpy/test_table.py +++ b/tests/tests/test_qtpy/test_table.py @@ -35,6 +35,7 @@ def test_data(model): assert model.data(index, Qt.EditRole) is None + def test_header_data(model, sample_df): assert ( model.headerData(0, Qt.Vertical, Qt.DisplayRole) == sample_df.index[0] @@ -45,5 +46,3 @@ def test_header_data(model, sample_df): assert ( model.headerData(2, Qt.Vertical, Qt.DisplayRole) == sample_df.index[2] ) - - From b2d1efa8f5e9cc98b65e0bc5e95c2295ff588a98 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Mon, 29 Jul 2024 10:46:30 +0100 Subject: [PATCH 04/10] Update widget name and citation --- brainglobe_utils/brainmapper/transform_widget.py | 4 ++-- brainglobe_utils/napari.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py index 09d071c..ced1312 100644 --- a/brainglobe_utils/brainmapper/transform_widget.py +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -115,10 +115,10 @@ def add_header(self): #
is included in the package_name to make the label under the logo # more compact, by splitting it onto two lines header = header_widget( - package_name="brainglobe-
utils", + package_name="brainmapper", package_tagline="Transform points to atlas space", github_repo_name="brainglobe-utils", - citation_doi="https://doi.org/10.1038/s41598-021-04676-9", + citation_doi="https://doi.org/10.1371/journal.pcbi.1009074", help_text="For help, hover the cursor over each parameter.", ) self.layout.addWidget(header, 0, 0, 1, 2) diff --git a/brainglobe_utils/napari.yaml b/brainglobe_utils/napari.yaml index 638c257..61773c6 100644 --- a/brainglobe_utils/napari.yaml +++ b/brainglobe_utils/napari.yaml @@ -3,10 +3,10 @@ display_name: BrainGlobe contributions: commands: - - id: brainglobe-utils.TransformPoints + - id: brainglobe-utils.brainmapper title: Open points transformation widget python_name: brainglobe_utils.brainmapper.transform_widget:TransformPoints widgets: - - command: brainglobe-utils.TransformPoints + - command: brainglobe-utils.brainmapper display_name: Transform points to a BrainGlobe atlas From 6c0888d9d6f1594a40dab3da3091bfa69654bf07 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Mon, 29 Jul 2024 10:46:39 +0100 Subject: [PATCH 05/10] Add docstrings and typing --- .../brainmapper/transform_widget.py | 299 ++++++++++++++++-- 1 file changed, 265 insertions(+), 34 deletions(-) diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py index ced1312..5a1284b 100644 --- a/brainglobe_utils/brainmapper/transform_widget.py +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -33,6 +33,15 @@ class TransformPoints(QWidget): def __init__(self, viewer: napari.viewer.Viewer): + """ + Initialize the TransformPoints widget. + + Parameters + ---------- + viewer : napari.viewer.Viewer + The napari viewer instance. + This will be passed when opening the widget. + """ super(TransformPoints, self).__init__() self.viewer = viewer self.raw_data = None @@ -47,10 +56,15 @@ def __init__(self, viewer: napari.viewer.Viewer): self.setup_main_layout() @self.viewer.layers.events.connect - def update_layer_list(v: napari.viewer.Viewer): + def update_layer_list(v: napari.viewer.Viewer) -> None: """ Update internal list of layers whenever the napari layers list is updated. + + Parameters + ---------- + v : napari.viewer.Viewer + The napari viewer instance. """ self.image_layer_names = self._get_layer_names() self.points_layer_names = self._get_layer_names( @@ -66,7 +80,20 @@ def update_layer_list(v: napari.viewer.Viewer): ) @staticmethod - def _update_combobox_options(combobox: QComboBox, options_list: List[str]): + def _update_combobox_options( + combobox: QComboBox, options_list: List[str] + ) -> None: + """ + Update the options in a QComboBox. + + Parameters + ---------- + combobox : QComboBox + The combobox to update. + options_list : List[str] + The list of options to set in the combobox. + """ + original_text = combobox.currentText() combobox.clear() combobox.addItems(options_list) @@ -79,6 +106,18 @@ def _get_layer_names( ) -> List[str]: """ Get list of layer names of a given layer type. + + Parameters + ---------- + layer_type : napari.layers.Layer, optional + The type of layer to get names for. Default is napari.layers.Image. + default : str, optional + Default values to include in the list. Default is an empty string. + + Returns + ------- + List[str] + A list of layer names. """ layer_names = [ layer.name @@ -91,7 +130,10 @@ def _get_layer_names( else: return [default] - def setup_main_layout(self): + def setup_main_layout(self) -> None: + """ + Set up the main layout of the widget. + """ self.layout = QGridLayout() self.layout.setContentsMargins(10, 10, 10, 10) self.layout.setAlignment(QtCore.Qt.AlignTop) @@ -108,12 +150,10 @@ def setup_main_layout(self): self.setLayout(self.layout) - def add_header(self): + def add_header(self) -> None: """ Header including brainglobe logo and documentation links. """ - #
is included in the package_name to make the label under the logo - # more compact, by splitting it onto two lines header = header_widget( package_name="brainmapper", package_tagline="Transform points to atlas space", @@ -123,7 +163,18 @@ def add_header(self): ) self.layout.addWidget(header, 0, 0, 1, 2) - def add_points_combobox(self, row, column): + def add_points_combobox(self, row: int, column: int) -> None: + """ + Add a combobox for selecting the points layer containing + the points (e.g. cells) to transform to a BrainGlobe atlas. + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.points_layer_choice, _ = add_combobox( self.layout, "Points layer", @@ -133,7 +184,18 @@ def add_points_combobox(self, row, column): callback=self.set_points_layer, ) - def add_raw_data_combobox(self, row, column): + def add_raw_data_combobox(self, row: int, column: int) -> None: + """ + Add a combobox for selecting the raw data layer. This defines + the coordinate space for transforming points to a BrainGlobe atlas. + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.raw_data_choice, _ = add_combobox( self.layout, "Raw data layer", @@ -143,7 +205,17 @@ def add_raw_data_combobox(self, row, column): callback=self.set_raw_data_layer, ) - def add_transform_button(self, row, column): + def add_transform_button(self, row: int, column: int) -> None: + """ + Add a button to begin the transformation of points to atlas space. + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.transform_button = add_button( "Transform points", self.layout, @@ -154,7 +226,17 @@ def add_transform_button(self, row, column): tooltip="Transform points layer to atlas space", ) - def add_points_summary_table(self, row, column): + def add_points_summary_table(self, row: int, column: int) -> None: + """ + Add a table to display the summary of points per atlas region. + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.points_per_region_table_title = QLabel( "Points distribution summary" ) @@ -164,7 +246,19 @@ def add_points_summary_table(self, row, column): self.points_per_region_table.setVisible(False) self.layout.addWidget(self.points_per_region_table, row + 1, column) - def add_save_all_points_button(self, row, column): + def add_save_all_points_button(self, row: int, column: int) -> None: + """ + Add a button to save all points information (i.e. the list of + all points, and their coordinates in raw data and atlas + space, alongside assigned atlas region). + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.save_all_points_button = add_button( "Save all points information", self.layout, @@ -175,7 +269,17 @@ def add_save_all_points_button(self, row, column): tooltip="Save all points information as a csv file", ) - def add_save_points_summary_button(self, row, column): + def add_save_points_summary_button(self, row: int, column: int) -> None: + """ + Add a button to save points summary (i.e. points per atlas region). + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.save_points_summary_button = add_button( "Save points summary", self.layout, @@ -186,12 +290,22 @@ def add_save_points_summary_button(self, row, column): tooltip="Save points summary as a csv file", ) - def add_status_label(self, row, column): + def add_status_label(self, row: int, column: int) -> None: + """ + Add a status label to inform the user of progress. + + Parameters + ---------- + row : int + Row in the grid layout. + column : int + Column in the grid layout. + """ self.status_label = QLabel() self.status_label.setText("Ready") self.layout.addWidget(self.status_label, row, column) - def set_raw_data_layer(self): + def set_raw_data_layer(self) -> None: """ Set background layer from current background text box selection. """ @@ -200,7 +314,7 @@ def set_raw_data_layer(self): self.raw_data_choice.currentText() ] - def set_points_layer(self): + def set_points_layer(self) -> None: """ Set background layer from current background text box selection. """ @@ -209,7 +323,10 @@ def set_points_layer(self): self.points_layer_choice.currentText() ] - def transform_points_to_atlas_space(self): + def transform_points_to_atlas_space(self) -> None: + """ + Transform points layer to atlas space. + """ layers_in_place = self.check_layers() if not layers_in_place: return @@ -230,7 +347,17 @@ def transform_points_to_atlas_space(self): self.analyse_points() self.status_label.setText("Ready") - def check_layers(self): + def check_layers(self) -> bool: + """ + Check if the layers needed to begin the transformation + have been selected by the user. + + Returns + ------- + bool + True if both raw data and points layers are selected, + False otherwise. + """ if self.raw_data is None and self.points_layer is None: display_info( self, @@ -260,7 +387,16 @@ def check_layers(self): return True - def load_brainreg_directory(self): + def load_brainreg_directory(self) -> bool: + """ + Load the brainreg directory selected by the user. + Returns false if not selected, to abort analysis. + + Returns + ------- + bool + True if a directory was selected, False otherwise. + """ brainreg_directory = QFileDialog.getExistingDirectory( self, "Select brainreg directory", @@ -274,16 +410,28 @@ def load_brainreg_directory(self): self.status_label.setText("Ready") return True - def initialise_brainreg_data(self): + def initialise_brainreg_data(self) -> None: + """ + Initialize brainreg data by defining the paths, + then loading the brainreg metadata, and then the atlas. + """ self.get_brainreg_paths() self.check_brainreg_directory() self.get_registration_metadata() self.load_atlas() - def get_brainreg_paths(self): + def get_brainreg_paths(self) -> None: + """ + Get the relevant file paths from the brainreg + output directory chosen by the user + """ self.paths = Paths(self.brainreg_directory) - def check_brainreg_directory(self): + def check_brainreg_directory(self) -> None: + """ + Check if the selected directory is a valid brainreg directory + by checking for the existence of an "atlas" entry in the json + """ try: with open(self.paths.brainreg_metadata_file) as json_file: self.brainreg_metadata = json.load(json_file) @@ -294,7 +442,11 @@ def check_brainreg_directory(self): except FileNotFoundError: self.display_brainreg_directory_warning() - def display_brainreg_directory_warning(self): + def display_brainreg_directory_warning(self) -> None: + """ + Display a warning to the user if the selected directory + is not a valid brainreg directory. + """ display_info( self, "Not a brainreg directory", @@ -302,10 +454,17 @@ def display_brainreg_directory_warning(self): "directory. Please try loading another brainreg output directory.", ) - def get_registration_metadata(self): + def get_registration_metadata(self) -> None: + """ + Load registration metadata (atlas, orientation, voxel sizes) + from the brainreg directory. + """ self.metadata = Metadata(self.brainreg_metadata) - def load_atlas(self): + def load_atlas(self) -> None: + """ + Load the BrainGlobe atlas used for the initial brainreg registration. + """ if not self.is_atlas_installed(self.metadata.atlas_string): display_info( self, @@ -316,7 +475,13 @@ def load_atlas(self): ) self.atlas = BrainGlobeAtlas(self.metadata.atlas_string) - def run_transform_points_to_downsampled_space(self): + def run_transform_points_to_downsampled_space(self) -> None: + """ + Transform points fromm the raw data space (in which points + were detected) to the downsampled space defined by brainreg. + This space is the same as the raw data, but downsampled and realigned + to match the orientation and resolution of the atlas. + """ downsampled_space = self.get_downsampled_space() raw_data_space = self.get_raw_data_space() self.points_in_downsampled_space = raw_data_space.map_points_to( @@ -328,7 +493,11 @@ def run_transform_points_to_downsampled_space(self): visible=False, ) - def run_transform_downsampled_points_to_atlas_space(self): + def run_transform_downsampled_points_to_atlas_space(self) -> None: + """ + Transform points from the downsampled space to atlas space. Uses + the deformation fields output by NiftyReg (via brainreg) as a look up. + """ deformation_field_paths = [ self.paths.deformation_field_0, self.paths.deformation_field_1, @@ -356,7 +525,16 @@ def run_transform_downsampled_points_to_atlas_space(self): f"points fell outside the atlas space", ) - def get_downsampled_space(self): + def get_downsampled_space(self) -> AnatomicalSpace: + """ + Get the anatomical space (as defined by brainglobe-space) + for the downsampled data. + + Returns + ------- + AnatomicalSpace + The downsampled anatomical space (as defined by brainglobe-space). + """ target_shape = tifffile.imread(self.paths.downsampled_image).shape downsampled_space = AnatomicalSpace( @@ -366,7 +544,16 @@ def get_downsampled_space(self): ) return downsampled_space - def get_raw_data_space(self): + def get_raw_data_space(self) -> AnatomicalSpace: + """ + Get the anatomical space (as defined by brainglobe-space) + for the raw data. + + Returns + ------- + AnatomicalSpace + The raw data anatomical space (as defined by brainglobe-space). + """ raw_data_space = AnatomicalSpace( self.metadata.orientation, shape=self.raw_data.data.shape, @@ -374,7 +561,11 @@ def get_raw_data_space(self): ) return raw_data_space - def analyse_points(self): + def analyse_points(self) -> None: + """ + Analyse the distribution of points in the space + of the BrainGlobe Atlas. + """ self.all_points_df, self.points_per_region_df = ( summarise_points_by_atlas_region( self.points_layer.data, @@ -392,12 +583,22 @@ def analyse_points(self): def populate_summary_table( self, - columns_to_keep=[ + columns_to_keep: List[str] = [ "structure_name", "left_cell_count", "right_cell_count", ], - ): + ) -> None: + """ + Populate the table with the summary of points per atlas region. + + Parameters + ---------- + columns_to_keep : List[str], optional + Columns to keep in the summary table. + Default columns are ["structure_name", "left_cell_count", + "right_cell_count"]. + """ summary_df = self.points_per_region_df[columns_to_keep] self.points_per_region_table_model = DataFrameModel(summary_df) self.points_per_region_table.setModel( @@ -406,10 +607,18 @@ def populate_summary_table( self.points_per_region_table_title.setVisible(True) self.points_per_region_table.setVisible(True) - def save_all_points_csv(self): + def save_all_points_csv(self) -> None: + """ + Save the coordinate and atlas region + information for all points to a CSV file. + """ self.save_df_to_csv(self.all_points_df) - def save_points_summary_csv(self): + def save_points_summary_csv(self) -> None: + """ + Save the summary of the distribution of points + in the atlas space to a CSV file. + """ self.save_df_to_csv(self.points_per_region_df) def save_df_to_csv(self, df: pd.DataFrame) -> None: @@ -441,7 +650,20 @@ def save_df_to_csv(self, df: pd.DataFrame) -> None: df.to_csv(path, index=False) @staticmethod - def is_atlas_installed(atlas): + def is_atlas_installed(atlas: str) -> bool: + """ + Check if the specified BrainGlobe atlas is installed. + + Parameters + ---------- + atlas : str + The atlas name to check. + + Returns + ------- + bool + True if the atlas is installed, False otherwise. + """ downloaded_atlases = get_downloaded_atlases() if atlas in downloaded_atlases: return True @@ -482,6 +704,15 @@ class Paths: """ def __init__(self, brainreg_directory: Union[str, Path]) -> None: + """ + Set the paths based on the given brainreg directory + + Parameters + ---------- + brainreg_directory : Union[str, Path] + Path to brainreg output directory + (or brainmapper "registration" directory). + """ self.brainreg_directory: Path = Path(brainreg_directory) self.brainreg_metadata_file: Path = self.make_filepaths( "brainreg.json" From 0b324775d885dbc3427dd9df1195b1a80fd34009 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Mon, 29 Jul 2024 11:05:35 +0100 Subject: [PATCH 06/10] remove ref to tooltips --- brainglobe_utils/brainmapper/transform_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py index 5a1284b..d88bc24 100644 --- a/brainglobe_utils/brainmapper/transform_widget.py +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -159,7 +159,6 @@ def add_header(self) -> None: package_tagline="Transform points to atlas space", github_repo_name="brainglobe-utils", citation_doi="https://doi.org/10.1371/journal.pcbi.1009074", - help_text="For help, hover the cursor over each parameter.", ) self.layout.addWidget(header, 0, 0, 1, 2) From ed811eef2834474dae6e85db7e1b9cb2b2738993 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Tue, 30 Jul 2024 15:53:54 +0100 Subject: [PATCH 07/10] Add napari trove classifier --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 485b4f8..df51b93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ license = { file = "LICENSE" } requires-python = ">=3.10" classifiers = [ "Development Status :: 3 - Alpha", + "Framework :: napari", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: OS Independent", From bcfa6ca109cbaf1325b27896503a439b5ea80d69 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Tue, 30 Jul 2024 16:56:08 +0100 Subject: [PATCH 08/10] Update widget name --- brainglobe_utils/napari.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brainglobe_utils/napari.yaml b/brainglobe_utils/napari.yaml index 61773c6..ea81d41 100644 --- a/brainglobe_utils/napari.yaml +++ b/brainglobe_utils/napari.yaml @@ -9,4 +9,4 @@ contributions: widgets: - command: brainglobe-utils.brainmapper - display_name: Transform points to a BrainGlobe atlas + display_name: brainmapper From 815ccfab6a110449a66780f4e1978259f631af2c Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Wed, 31 Jul 2024 11:53:36 +0100 Subject: [PATCH 09/10] Remove unnecessary 1 line functions --- .../brainmapper/transform_widget.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py index d88bc24..ce8733b 100644 --- a/brainglobe_utils/brainmapper/transform_widget.py +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -414,18 +414,11 @@ def initialise_brainreg_data(self) -> None: Initialize brainreg data by defining the paths, then loading the brainreg metadata, and then the atlas. """ - self.get_brainreg_paths() + self.paths = Paths(self.brainreg_directory) self.check_brainreg_directory() - self.get_registration_metadata() + self.metadata = Metadata(self.brainreg_metadata) self.load_atlas() - def get_brainreg_paths(self) -> None: - """ - Get the relevant file paths from the brainreg - output directory chosen by the user - """ - self.paths = Paths(self.brainreg_directory) - def check_brainreg_directory(self) -> None: """ Check if the selected directory is a valid brainreg directory @@ -453,13 +446,6 @@ def display_brainreg_directory_warning(self) -> None: "directory. Please try loading another brainreg output directory.", ) - def get_registration_metadata(self) -> None: - """ - Load registration metadata (atlas, orientation, voxel sizes) - from the brainreg directory. - """ - self.metadata = Metadata(self.brainreg_metadata) - def load_atlas(self) -> None: """ Load the BrainGlobe atlas used for the initial brainreg registration. From 52aab7521597be0e389b8ebd09a2789f67bc2b03 Mon Sep 17 00:00:00 2001 From: Adam Tyson Date: Wed, 31 Jul 2024 12:05:49 +0100 Subject: [PATCH 10/10] Remove parameter default --- brainglobe_utils/brainmapper/transform_widget.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/brainglobe_utils/brainmapper/transform_widget.py b/brainglobe_utils/brainmapper/transform_widget.py index ce8733b..2fcceb5 100644 --- a/brainglobe_utils/brainmapper/transform_widget.py +++ b/brainglobe_utils/brainmapper/transform_widget.py @@ -49,7 +49,9 @@ def __init__(self, viewer: napari.viewer.Viewer): self.atlas = None self.transformed_points = None - self.image_layer_names = self._get_layer_names() + self.image_layer_names = self._get_layer_names( + layer_type=napari.layers.Image + ) self.points_layer_names = self._get_layer_names( layer_type=napari.layers.Points ) @@ -66,7 +68,9 @@ def update_layer_list(v: napari.viewer.Viewer) -> None: v : napari.viewer.Viewer The napari viewer instance. """ - self.image_layer_names = self._get_layer_names() + self.image_layer_names = self._get_layer_names( + layer_type=napari.layers.Image + ) self.points_layer_names = self._get_layer_names( layer_type=napari.layers.Points ) @@ -101,7 +105,7 @@ def _update_combobox_options( def _get_layer_names( self, - layer_type: napari.layers.Layer = napari.layers.Image, + layer_type: napari.layers.Layer, default: str = "", ) -> List[str]: """ @@ -110,7 +114,7 @@ def _get_layer_names( Parameters ---------- layer_type : napari.layers.Layer, optional - The type of layer to get names for. Default is napari.layers.Image. + The type of layer to get names for. default : str, optional Default values to include in the list. Default is an empty string.