Source code for napari_dmc_brainmap.padding.padding

"""
DMC-BrainMap widget for padding .tif files to match atlas resolution.

2024 - FJ
"""

from qtpy.QtCore import Signal
from qtpy.QtWidgets import QPushButton, QWidget, QVBoxLayout, QMessageBox, QProgressBar
from napari import Viewer
from napari.qt.threading import thread_worker
from napari.utils.notifications import show_info
from magicgui import magicgui
from magicgui.widgets import FunctionGui
import tifffile
from natsort import natsorted
import pandas as pd
from pathlib import Path
from napari_dmc_brainmap.stitching.stitching_tools import padding_for_atlas
from napari_dmc_brainmap.utils.path_utils import get_info, get_image_list
from napari_dmc_brainmap.utils.general_utils import get_animal_id
from napari_dmc_brainmap.utils.params_utils import load_params
from napari_dmc_brainmap.utils.gui_utils import check_input_path
from typing import List, Tuple, Generator


[docs] @thread_worker(progress={"total": 100}) def do_padding(input_path: Path, channels: List[str], pad_folder: str, resolution: Tuple[int, int]) -> Generator[int, None, str]: """ Pad .tif images to match the atlas resolution. Parameters: input_path (Path): Path to the input directory containing subfolders for images. channels (List[str]): List of channels to process. pad_folder (str): Name of the folder containing images to be padded. resolution (Tuple[int, int]): The desired resolution for padding. Yields: int: Progress value during padding. Returns: str: The animal ID of the processed images. """ if pad_folder == "confocal": raise NotImplementedError( "'confocal' is reserved for CZI file format. " "Rename the folder for .tif files (e.g., 'to_pad'). " "For CZI files, use the 'Stitch czi images' function." ) animal_id = get_animal_id(input_path) progress_value = 0 image_count = count_images(input_path, pad_folder, channels) progress_step = 100 / image_count for chan in channels: tif_files = list(input_path.joinpath(pad_folder, chan).glob("*.tif")) try: if not tif_files[0].name.endswith("_stitched.tif"): rename_image_files(tif_files, input_path, pad_folder, chan) get_image_list(input_path, chan, folder_id=pad_folder) # save_image_names_csv(tif_files, input_path) pad_dir, pad_im_list, _ = get_info(input_path, pad_folder, channel=chan) for im in pad_im_list: im_fn = pad_dir.joinpath(im) try: im_array = tifffile.imread(str(im_fn)) except Exception as e: show_info(f"Failed to read {im_fn}: {e}") continue im_padded = padding_for_atlas(im_array, resolution) try: tifffile.imwrite(str(im_fn), im_padded) except Exception as e: show_info(f"Failed to write {im_fn}: {e}") continue progress_value += progress_step yield int(progress_value) except IndexError: show_info(f"No images found for channel {chan}. Skipping padding...") yield 100 return animal_id
[docs] def count_images(input_path: Path, pad_folder: str, channels: List[str]) -> int: """ Count the number of images in the specified folder and channels. Parameters: input_path (Path): Path to the input directory. pad_folder (str): Name of the folder containing images to be padded. channels (List[str]): List of channels to count images for. Returns: int: The total count of images in the specified channels. """ image_count = sum(len(list(input_path.joinpath(pad_folder, chan).glob("*.tif"))) for chan in channels) return max(image_count, 1)
[docs] def rename_image_files(tif_files: List[Path], input_path: Path, pad_folder: str, chan: str) -> None: """ Rename image files to add '_stitched' suffix if missing. Parameters: tif_files (List[Path]): List of tif files to rename. input_path (Path): Path to the input directory. pad_folder (str): Name of the folder containing images to be renamed. chan (str): Channel name for which images are being renamed. """ for im in tif_files: im_old = input_path.joinpath(pad_folder, chan, im.name) im_new = input_path.joinpath(pad_folder, chan, f"{im.stem}_stitched.tif") im_old.rename(im_new)
[docs] def save_image_names_csv(tif_files: List[Path], input_path: Path) -> None: """ Save the list of image names to 'image_names.csv' if it does not already exist. Parameters: tif_files (List[Path]): List of tif files whose names are to be saved. input_path (Path): Path to the input directory. """ image_names_csv = input_path.joinpath("image_names.csv") print(tif_files) if not image_names_csv.exists(): image_list = natsorted([tif.name.split("_stitched.tif")[0] for tif in tif_files]) print(image_list) pd.DataFrame(image_list).to_csv(image_names_csv, index=False)
[docs] def initialize_widget() -> FunctionGui: """ Initialize the MagicGUI widget for padding configuration. Returns: FunctionGui: The initialized MagicGUI widget. """ @magicgui(layout='vertical', input_path=dict(widget_type='FileEdit', label='Input Path (Animal ID):', mode='d', tooltip='Directory containing subfolders with images or segmentation results.'), pad_folder=dict(widget_type='LineEdit', label='Folder Name for Images to Pad:', value='stitched', tooltip='Name of the folder containing the stitched images to be padded. ' '(e.g., animal_id/pad_folder/chan1)'), channels=dict(widget_type='Select', label='Imaged Channels:', value=['green', 'cy3'], choices=['dapi', 'green', 'n3', 'cy3', 'cy5'], tooltip='Select the imaged channels. Use Ctrl/Shift for multiple selections.'), call_button=False) def padding_widget(viewer: Viewer, input_path: Path, channels: List[str], pad_folder: str): """ Padding configuration widget. Parameters: viewer (Viewer): The Napari viewer instance. input_path (Path): Path to the folder containing experimental data (animal ID level). channels (List[str]): List of imaged channels. pad_folder (str): Name of the folder containing images to be padded. """ pass return padding_widget
[docs] class PaddingWidget(QWidget): """ QWidget for configuring and initiating the padding process for images. """ progress_signal = Signal(int) """Signal emitted to update the progress bar with an integer value.""" def __init__(self, napari_viewer: Viewer) -> None: """ Initialize the PaddingWidget instance. Parameters: napari_viewer (Viewer): The Napari viewer instance. """ super().__init__() self.viewer = napari_viewer self.setLayout(QVBoxLayout()) self.padding = initialize_widget() self.progress_bar = QProgressBar(self) self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) self.btn = QPushButton("Do the Padding (WARNING: Overwrites Existing Files!)") self.btn.clicked.connect(self._do_padding) self.layout().addWidget(self.padding.native) self.layout().addWidget(self.btn) self.layout().addWidget(self.progress_bar) self.progress_signal.connect(self.progress_bar.setValue) def _show_success_message(self, animal_id: str) -> None: """ Display a success message after the padding process is complete. Parameters: animal_id (str): The Animal ID for which padding was performed. """ msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Information) msg_box.setText(f"Padding finished successfully for {animal_id}!") msg_box.setWindowTitle("Padding Successful") msg_box.exec_() self.btn.setText("Do the Padding (WARNING: Overwrites Existing Files!)") self.progress_signal.emit(0) def _update_progress(self, value: int) -> None: """ Update the progress bar with the given value. Parameters: value (int): The progress value to set. """ self.progress_signal.emit(value) def _do_padding(self) -> None: """ Execute the padding operation with user confirmation. """ reply = QMessageBox.question( self, "Warning", "This operation will overwrite existing files. Do you want to continue?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: input_path = self.padding.input_path.value if not check_input_path(input_path): return channels = self.padding.channels.value pad_folder = self.padding.pad_folder.value params_dict = load_params(input_path) resolution = params_dict['atlas_info']['resolution'] # [x, y] padding_worker = do_padding(input_path, channels, pad_folder, resolution) padding_worker.yielded.connect(self._update_progress) padding_worker.started.connect(lambda: self.btn.setText("Padding Images...")) padding_worker.returned.connect(self._show_success_message) padding_worker.start()