From c9d8e761ee6a6f230f93785668f860c8d88abc24 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 14:25:05 +0200 Subject: [PATCH 1/5] Consolidate project config into pyproject and modernize CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove legacy packaging and test config files (setup.cfg, requirements.txt, .isort.cfg, napari_cellseg3d/_tests/pytest.ini, and its conftest). Migrate and update metadata in pyproject.toml: bump supported Python classifiers to 3.10–3.12, add napari manifest entry-point, include napari.yaml in package data, simplify dynamic fields, adjust optional dependencies (add PyQt6, pyside6, move pydensecrf2 to crf extra) and streamline dev dependencies (remove black/isort as direct dev deps). Update tooling rules (ruff token change) and remove redundant tool configs. Update tox.ini to target py310/311/312 across linux/windows/macos, expose platform mappings for GH Actions, and switch to using extras for test/crf/pyqt6 while keeping usedevelop and running pytest as the test command. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d2f79565..85ff3638 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,9 @@ pyside6 = [ pyqt6 = [ "napari[pyqt6]", ] +pyqt6 = [ + "PyQt6", +] onnx-cpu = [ "onnx", "onnxruntime" From 9febc81f2b29576b9799d99a3273d536715d12c5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 14:47:35 +0200 Subject: [PATCH 2/5] Modernize pyproject and update pre-commit hooks Bump and extend pre-commit configuration and migrate pyproject to a PEP 621 style layout. .pre-commit-config.yaml: update pre-commit-hooks rev, replace/update ruff hook to astral-sh/ruff-pre-commit with auto-fix and unsafe-fixes, add ruff-format, mdformat (with mdformat-myst), pyproject-fmt and validate-pyproject hooks; keep napari-plugin-checks. pyproject.toml: add [build-system], normalize project metadata (name, license, requires-python, classifiers), move authors to [[project.authors]], declare dynamic version, reorganize and deduplicate dependencies and optional-dependencies, add project.urls and napari entry-point, add [tool.setuptools] packaging settings, move and refine ruff configuration under [tool.ruff.lint], and add [tool.pyproject-fmt] settings. Overall this modernizes packaging and enables enhanced linting/formatting via pre-commit hooks. --- .pre-commit-config.yaml | 47 ++++++++++++++++++++++------------------- pyproject.toml | 8 ++----- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cec0a8d7..76414d68 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,37 +1,40 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: -# - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - id: check-yaml - id: check-toml # - id: check-added-large-files - # args: [--maxkb=5000] -# - repo: https://github.com/pycqa/isort -# rev: 5.12.0 -# hooks: -# - id: isort -# args: ["--profile", "black", --line-length=79] - - repo: https://github.com/charliermarsh/ruff-pre-commit - # Ruff version. - rev: 'v0.5.6' + # args: [--maxkb=5000] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.13 hooks: - id: ruff - args: [ --fix, --exit-non-zero-on-fix ] - - repo: https://github.com/psf/black - rev: 24.8.0 + args: [--fix, --unsafe-fixes, --exit-non-zero-on-fix] + + - id: ruff-format + + - repo: https://github.com/executablebooks/mdformat + rev: 1.0.0 hooks: - - id: black - args: [--line-length=79] + - id: mdformat + additional_dependencies: + - mdformat-myst + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: v2.21.2 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.25 + hooks: + - id: validate-pyproject + - repo: https://github.com/tlambert03/napari-plugin-checks rev: v0.3.0 hooks: - id: napari-plugin-checks - # https://mypy.readthedocs.io/en/stable/introduction.html - # you may wish to add this as well! - # - repo: https://github.com/pre-commit/mirrors-mypy - # rev: v0.910-1 - # hooks: - # - id: mypy diff --git a/pyproject.toml b/pyproject.toml index 85ff3638..d7884e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ # "nibabel", # "pillow", "pyclesperanto", - "matplotlib", "pydensecrf2", ] dynamic = ["version"] @@ -131,9 +130,6 @@ pyside6 = [ pyqt6 = [ "napari[pyqt6]", ] -pyqt6 = [ - "PyQt6", -] onnx-cpu = [ "onnx", "onnxruntime" @@ -155,7 +151,7 @@ docs = [ ] test = [ "pytest", - "pytest_qt", + "pytest-qt", "pytest-cov", "coverage", "tox", @@ -165,4 +161,4 @@ test = [ ] crf = [ "pydensecrf2", -] +] \ No newline at end of file From f49880d7a36a22752704f71508abdcd0b38f876a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 14:47:51 +0200 Subject: [PATCH 3/5] Run updated pre-commit --- .napari/DESCRIPTION.md | 12 +- CODE_OF_CONDUCT.md | 30 +- README.md | 77 +- docs/TODO.md | 5 +- docs/source/guides/training_wnet.rst | 2 +- examples/README.md | 2 +- napari_cellseg3d/_tests/test_inference.py | 4 +- .../_tests/test_labels_correction.py | 8 +- .../_tests/test_model_framework.py | 21 +- .../_tests/test_plugin_training.py | 8 +- napari_cellseg3d/_tests/test_plugin_utils.py | 4 +- napari_cellseg3d/_tests/test_review.py | 4 +- napari_cellseg3d/_tests/test_training.py | 12 +- napari_cellseg3d/_tests/test_utils.py | 20 +- napari_cellseg3d/code_models/crf.py | 5 +- .../code_models/instance_segmentation.py | 52 +- .../code_models/model_framework.py | 13 +- .../code_models/models/TEMPLATE_model.py | 4 +- .../code_models/models/model_SegResNet.py | 4 +- .../code_models/models/model_TRAILMAP.py | 1 + .../code_models/models/model_TRAILMAP_MS.py | 4 +- .../code_models/models/unet/buildingblocks.py | 85 +- .../code_models/models/unet/model.py | 10 +- .../code_models/models/wnet/model.py | 8 +- .../code_models/models/wnet/soft_Ncuts.py | 24 +- .../code_models/worker_inference.py | 96 +- .../code_models/worker_training.py | 157 +- napari_cellseg3d/code_models/workers_utils.py | 25 +- napari_cellseg3d/code_plugins/plugin_base.py | 33 +- .../code_plugins/plugin_convert.py | 91 +- napari_cellseg3d/code_plugins/plugin_crf.py | 14 +- napari_cellseg3d/code_plugins/plugin_crop.py | 32 +- .../code_plugins/plugin_helper.py | 8 +- .../code_plugins/plugin_metrics.py | 99 +- .../code_plugins/plugin_model_inference.py | 70 +- .../code_plugins/plugin_model_training.py | 116 +- .../code_plugins/plugin_review.py | 33 +- .../code_plugins/plugin_review_dock.py | 15 +- .../code_plugins/plugin_utilities.py | 24 +- napari_cellseg3d/config.py | 13 +- .../dev_scripts/artefact_labeling.py | 25 +- .../dev_scripts/classifier_test.ipynb | 71 +- .../dev_scripts/correct_labels.py | 28 +- napari_cellseg3d/dev_scripts/crop_data.py | 1 + .../dev_scripts/evaluate_labels.py | 16 +- .../dev_scripts/remote_inference.py | 25 +- .../dev_scripts/remote_training.py | 11 +- .../dev_scripts/sliding_window_voronoi.py | 1 + .../dev_scripts/test_new_evaluation.ipynb | 105 +- .../dev_scripts/whole_brain_utils.py | 1 + napari_cellseg3d/interface.py | 90 +- napari_cellseg3d/plugins.py | 1 + napari_cellseg3d/utils.py | 45 +- notebooks/Colab_WNet3D_training.ipynb | 191 +- notebooks/Colab_inference_demo.ipynb | 2624 +++++++++-------- notebooks/assess_instance.ipynb | 50 +- notebooks/label_stats_csv_plot.ipynb | 31 +- notebooks/labels_plot.ipynb | 15 +- notebooks/view_wnet.ipynb | 14 +- 59 files changed, 2095 insertions(+), 2495 deletions(-) diff --git a/.napari/DESCRIPTION.md b/.napari/DESCRIPTION.md index d379ed68..0c4fbc77 100644 --- a/.napari/DESCRIPTION.md +++ b/.napari/DESCRIPTION.md @@ -1,5 +1,5 @@ - + + ## Description A napari plugin for 3D cell segmentation: training, inference, and data review. In particular, this project was developed for analysis of mesoSPIM-acquired (cleared tissue + lightsheet) datasets. @@ -46,6 +47,7 @@ Here is an example of an mp4 video embedded this way. https://user-images.githubusercontent.com/17995243/120088305-6c093380-c132-11eb-822d-620e81eb5f0e.mp4 --> + ## Intended Audience & Supported Data This plugin requires basic knowledge in machine learning; @@ -68,6 +70,7 @@ make sure to mention this. If you know of researchers, groups or labs using your plugin, or if it has been cited anywhere, feel free to also include this information here. --> + ## Quickstart Install from pip with `pip install napari-cellseg3d` @@ -75,9 +78,10 @@ Install from pip with `pip install napari-cellseg3d` OR - Install napari from pip with `pip install "napari[all]"`, -then from the “Plugins” menu within the napari application, select “Install/Uninstall Package(s)...” + then from the “Plugins” menu within the napari application, select “Install/Uninstall Package(s)...” - Copy `napari-cellseg3d` and paste it where it says “Install by name/url…” - Click “Install” + + ## Additional Install Steps **Python >= 3.8 required** @@ -114,10 +119,12 @@ over in `setup.cfg`. However, if your plugin has any more complex dependencies, requires any additional preparation before (or after) installation, you should add this information here. --> + ## Getting Help If you would like to report an issue with the plugin, please open an [issue on Github](https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues) + + [![PyPI](https://img.shields.io/pypi/v/napari-cellseg3d.svg?color=green)](https://pypi.org/project/napari-cellseg3d) [![Downloads](https://static.pepy.tech/badge/napari-cellseg3d)](https://pepy.tech/project/napari-cellseg3d) [![Downloads](https://static.pepy.tech/badge/napari-cellseg3d/month)](https://pepy.tech/project/napari-cellseg3d) @@ -9,16 +11,13 @@ cellseg3d logo - **A package for 3D cell segmentation with deep learning, including a napari plugin**: training, inference, and data review. In particular, this project was developed for analysis of confocal and mesoSPIM-acquired (cleared tissue + lightsheet) tissue datasets, but is not limited to this type of data. [Check out our publication for more information!](https://elifesciences.org/articles/99848) - ![demo](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/0d16a71b-3ff2-477a-9d83-18d96cb1ce28/full_demo.gif?format=500w) - ## Installation - 💻 See the [Installation page](https://adaptivemotorcontrollab.github.io/CellSeg3D/welcome.html) in the documentation for detailed instructions. +💻 See the [Installation page](https://adaptivemotorcontrollab.github.io/CellSeg3D/welcome.html) in the documentation for detailed instructions. ## Documentation @@ -33,21 +32,22 @@ pip install napari_cellseg3d ``` To use the plugin, please run: + ``` napari ``` + Then go into `Plugins > napari_cellseg3d`, and choose which tool to use. - **Review (label)**: This module allows you to review your labels, from predictions or manual labeling, and correct them if needed. It then saves the status of each file in a csv, for easier monitoring. - **Inference**: This module allows you to use pre-trained segmentation algorithms on volumes to automatically label cells and compute statistics. -- **Train**: This module allows you to train segmentation algorithms from labeled volumes. +- **Train**: This module allows you to train segmentation algorithms from labeled volumes. - **Utilities**: This module allows you to perform several actions like cropping your volumes and labels dynamically, by selecting a fixed size volume and moving it around the image; fragment images into smaller cubes for training; or converting labels from instance to segmentation and the opposite. ## Why use CellSeg3D? The strength of our approach is we can match supervised model performance with purely self-supervised learning, meaning users don't need to spend (hundreds) of hours on annotation. Here is a quick look of our key results. TL;DR see panel **f**, which shows that with minmal input data we can outperform supervised models: - ![FIG1 (1)](https://github.com/user-attachments/assets/0d970b45-79ff-4c58-861f-e1e7dc9abc65) **Figure 1. Performance of 3D Semantic and Instance Segmentation Models.** @@ -59,7 +59,6 @@ F1-score is computed from the Intersection over Union (IoU) with ground truth la **c:** View of 3D instance labels from supervised models, as noted, for visual cortex volume in b evaluation. **d:** Illustration of our WNet3D architecture showcasing the dual 3D U-Net structure with our modifications. - ## News ### **CellSeg3D now published at eLife** @@ -69,15 +68,19 @@ Read the [article here !](https://elifesciences.org/articles/99848) ### **New version: v0.2.2** - v0.2.2: + - Updated the Colab Notebooks for training and inference - New models available in the inference demo notebook - CRF optional post-processing adjustments (and pip install directly) + - v0.2.1: + - Updated plugin default behaviors across the board to be more readily applicable to demo data - Threshold value in inference is now automatically set by default according to performance on demo data on a per-model basis - Added a grid search utility to find best thresholds for supervised models - v0.2.0: + - Changed project name to "napari_cellseg3d" to avoid setuptools deprecation - Small API changes for training/inference from a script - Some fixes to WandB integration and csv saving after training @@ -97,7 +100,6 @@ Previous additions: - New utilities - Many small improvements and many bug fixes - ## Requirements **Compatible with Python 3.8 to 3.10.** @@ -118,39 +120,38 @@ Please reach out if you have any issues with the installation, we will be happy To avoid issues when installing on the ARM64 architecture, please follow these steps. -1) Create a new conda env using the provided conda/napari_CellSeg3D_ARM64.yml file : +1. Create a new conda env using the provided conda/napari_CellSeg3D_ARM64.yml file : - git clone https://github.com/AdaptiveMotorControlLab/CellSeg3d.git - cd CellSeg3d - conda env create -f conda/napari_CellSeg3D_ARM64.yml - conda activate napari_CellSeg3D_ARM64 - - -2) Install a Qt backend (PySide or PyQt5) -3) Launch napari, the plugin should be available in the plugins menu. + ``` + git clone https://github.com/AdaptiveMotorControlLab/CellSeg3d.git + cd CellSeg3d + conda env create -f conda/napari_CellSeg3D_ARM64.yml + conda activate napari_CellSeg3D_ARM64 + ``` +1. Install a Qt backend (PySide or PyQt5) +1. Launch napari, the plugin should be available in the plugins menu. ## Issues **Help us make the code better by reporting issues and adding your feature requests!** - If you encounter any problems, please [file an issue] along with a detailed description. ## Testing -You can generate docs locally by running ``make html`` in the docs/ folder. +You can generate docs locally by running `make html` in the docs/ folder. -Before testing, install all requirements using ``pip install napari-cellseg3d[test]``. +Before testing, install all requirements using `pip install napari-cellseg3d[test]`. -``pydensecrf`` is also required for testing. +`pydensecrf` is also required for testing. To run tests locally: -- Locally : run ``pytest napari_cellseg3d\_tests`` in the plugin folder. -- Locally with coverage : In the plugin folder, run ``coverage run --source=napari_cellseg3d -m pytest`` then ``coverage xml`` to generate a .xml coverage file. -- With tox : run ``tox`` in the plugin folder (will simulate tests with several python and OS configs, requires substantial storage space) +- Locally : run `pytest napari_cellseg3d\_tests` in the plugin folder. +- Locally with coverage : In the plugin folder, run `coverage run --source=napari_cellseg3d -m pytest` then `coverage xml` to generate a .xml coverage file. +- With tox : run `tox` in the plugin folder (will simulate tests with several python and OS configs, requires substantial storage space) ## Contributing @@ -170,23 +171,6 @@ Distributed under the terms of the [MIT] license. "napari-cellseg3d" is free and open source software. -[napari-hub]: https://www.napari-hub.org/plugins/napari-cellseg3d - -[file an issue]: https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues -[napari]: https://github.com/napari/napari -[Cookiecutter]: https://github.com/audreyr/cookiecutter -[@napari]: https://github.com/napari -[MIT]: http://opensource.org/licenses/MIT -[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin -[tox]: https://tox.readthedocs.io/en/latest/ -[pip]: https://pypi.org/project/pip/ -[PyPI]: https://pypi.org/ -[Installation page]: https://adaptivemotorcontrollab.github.io/CellSeg3D/source/guides/installation_guide.html -[the PyTorch website for installation instructions]: https://pytorch.org/get-started/locally/ -[PyTorch]: https://pytorch.org/get-started/locally/ -[MONAI's optional dependencies]: https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies -[MONAI]: https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies - ## Citation ``` @@ -220,3 +204,14 @@ Please refer to the documentation for full acknowledgements. ## Plugin base This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template. + +[@napari]: https://github.com/napari +[cookiecutter]: https://github.com/audreyr/cookiecutter +[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin +[file an issue]: https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues +[mit]: http://opensource.org/licenses/MIT +[monai]: https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies +[monai's optional dependencies]: https://docs.monai.io/en/stable/installation.html#installing-the-recommended-dependencies +[napari]: https://github.com/napari/napari +[pytorch]: https://pytorch.org/get-started/locally/ +[the pytorch website for installation instructions]: https://pytorch.org/get-started/locally/ diff --git a/docs/TODO.md b/docs/TODO.md index 88aafee7..f2906380 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,8 +1,9 @@ -[//]: # ( +\[//\]: # ( TODO: + - [ ] Add a way to get the current version of the library - [x] Update all modules - [x] Better WNet3D tutorial - [x] Setup GH Actions - [ ] Add a bibliography -) + ) diff --git a/docs/source/guides/training_wnet.rst b/docs/source/guides/training_wnet.rst index 359dc321..429f0d59 100644 --- a/docs/source/guides/training_wnet.rst +++ b/docs/source/guides/training_wnet.rst @@ -18,7 +18,7 @@ The WNet3D **does not require a large amount of data to train**, but **choosing You may find below some guidelines, based on our own data and testing. -The WNet3D is a self-supervised learning approach for 3D cell segmentation, and relies on the assumption that structural and morphological features of cells can be inferred directly from unlabeled data. This involves leveraging inherent properties such as spatial coherence and local contrast in imaging volumes to distinguish cellular structures. This approach assumes that meaningful representations of cellular boundaries and nuclei can emerge solely from raw 3D volumes. Thus, we strongly recommend that you use WNet3D on stacks that have clear foreground/background segregation and limited noise. Even if your final samples have noise, it is best to train on data that is as clean as you can. +The WNet3D is a self-supervised learning approach for 3D cell segmentation, and relies on the assumption that structural and morphological features of cells can be inferred directly from unlabeled data. This involves leveraging inherent properties such as spatial coherence and local contrast in imaging volumes to distinguish cellular structures. This approach assumes that meaningful representations of cellular boundaries and nuclei can emerge solely from raw 3D volumes. Thus, we strongly recommend that you use WNet3D on stacks that have clear foreground/background segregation and limited noise. Even if your final samples have noise, it is best to train on data that is as clean as you can. .. important:: diff --git a/examples/README.md b/examples/README.md index be6ea1c4..ab67956e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,4 +5,4 @@ All credits to the original authors of the data. You can install, launch `napari`, activate the CellSeg3D plugin app, and drag & drop this volume into the canvas. Then, for example, run the `Inference` module with one of our models. -See [CellSeg3D documentation](https://adaptivemotorcontrollab.github.io/CellSeg3D/welcome.html) for more details. \ No newline at end of file +See [CellSeg3D documentation](https://adaptivemotorcontrollab.github.io/CellSeg3D/welcome.html) for more details. diff --git a/napari_cellseg3d/_tests/test_inference.py b/napari_cellseg3d/_tests/test_inference.py index 62972ba1..4bdc51fa 100644 --- a/napari_cellseg3d/_tests/test_inference.py +++ b/napari_cellseg3d/_tests/test_inference.py @@ -63,9 +63,7 @@ def test_load_folder(): def test_inference_on_folder(): config = InferenceWorkerConfig() config.filetype = ".tif" - config.images_filepaths = [ - str(Path(__file__).resolve().parent / "res/test.tif") - ] + config.images_filepaths = [str(Path(__file__).resolve().parent / "res/test.tif")] config.sliding_window_config.window_size = 8 diff --git a/napari_cellseg3d/_tests/test_labels_correction.py b/napari_cellseg3d/_tests/test_labels_correction.py index bd1b0412..4e5ff1a3 100644 --- a/napari_cellseg3d/_tests/test_labels_correction.py +++ b/napari_cellseg3d/_tests/test_labels_correction.py @@ -32,9 +32,7 @@ def test_artefact_labeling_utils(): def test_correct_labels(): output_path = res_folder / "test_correct" output_path.mkdir(exist_ok=True, parents=True) - cl.relabel_non_unique_i( - labels, str(output_path / "corrected.tif"), go_fast=True - ) + cl.relabel_non_unique_i(labels, str(output_path / "corrected.tif"), go_fast=True) def test_relabel(make_napari_viewer_proxy): @@ -49,6 +47,4 @@ def test_relabel(make_napari_viewer_proxy): def test_evaluate_model_performance(): - el.evaluate_model_performance( - labels, labels, print_details=True, visualize=False - ) + el.evaluate_model_performance(labels, labels, print_details=True, visualize=False) diff --git a/napari_cellseg3d/_tests/test_model_framework.py b/napari_cellseg3d/_tests/test_model_framework.py index 1cb86569..475c334d 100644 --- a/napari_cellseg3d/_tests/test_model_framework.py +++ b/napari_cellseg3d/_tests/test_model_framework.py @@ -58,9 +58,7 @@ def test_create_train_dataset_dict(make_napari_viewer_proxy): def test_log(make_napari_viewer_proxy): mock_test = "test" - framework = model_framework.ModelFramework( - viewer=make_napari_viewer_proxy() - ) + framework = model_framework.ModelFramework(viewer=make_napari_viewer_proxy()) framework.log.print_and_log(mock_test) assert len(framework.log.toPlainText()) != 0 assert framework.log.toPlainText() == "\n" + mock_test @@ -83,9 +81,7 @@ def test_log(make_napari_viewer_proxy): def test_display_elements(make_napari_viewer_proxy): - framework = model_framework.ModelFramework( - viewer=make_napari_viewer_proxy() - ) + framework = model_framework.ModelFramework(viewer=make_napari_viewer_proxy()) framework.display_status_report() framework.display_status_report() @@ -96,20 +92,13 @@ def test_display_elements(make_napari_viewer_proxy): def test_available_models_retrieval(make_napari_viewer_proxy): - framework = model_framework.ModelFramework( - viewer=make_napari_viewer_proxy() - ) + framework = model_framework.ModelFramework(viewer=make_napari_viewer_proxy()) assert framework.get_available_models() == MODEL_LIST def test_update_weights_path(make_napari_viewer_proxy): - framework = model_framework.ModelFramework( - viewer=make_napari_viewer_proxy() - ) - assert ( - framework._update_weights_path(framework._default_weights_folder) - is None - ) + framework = model_framework.ModelFramework(viewer=make_napari_viewer_proxy()) + assert framework._update_weights_path(framework._default_weights_folder) is None name = str(Path.home() / "test/weight.pth") framework._update_weights_path([name]) assert framework.weights_config.path == name diff --git a/napari_cellseg3d/_tests/test_plugin_training.py b/napari_cellseg3d/_tests/test_plugin_training.py index 32edd0e0..ff56f0f7 100644 --- a/napari_cellseg3d/_tests/test_plugin_training.py +++ b/napari_cellseg3d/_tests/test_plugin_training.py @@ -32,9 +32,7 @@ def test_worker_configs(make_napari_viewer_proxy): ] for attr in dir(default_config): if not attr.startswith("__") and attr not in excluded: - assert getattr(default_config, attr) == getattr( - worker.config, attr - ) + assert getattr(default_config, attr) == getattr(worker.config, attr) # test unsupervised config and worker widget.model_choice.setCurrentText("WNet3D") widget._toggle_unsupervised_mode(enabled=True) @@ -43,9 +41,7 @@ def test_worker_configs(make_napari_viewer_proxy): excluded = ["results_path_folder", "sample_size", "weights_info"] for attr in dir(default_config): if not attr.startswith("__") and attr not in excluded: - assert getattr(default_config, attr) == getattr( - worker.config, attr - ) + assert getattr(default_config, attr) == getattr(worker.config, attr) widget.unsupervised_images_filewidget.text_field.setText( str((im_path.parent / "wnet_test").resolve()) ) diff --git a/napari_cellseg3d/_tests/test_plugin_utils.py b/napari_cellseg3d/_tests/test_plugin_utils.py index 1e0e01c2..4aa74bf1 100644 --- a/napari_cellseg3d/_tests/test_plugin_utils.py +++ b/napari_cellseg3d/_tests/test_plugin_utils.py @@ -21,9 +21,7 @@ def test_utils_plugin(make_napari_viewer_proxy): view.dims.ndisplay = 3 for i, utils_name in enumerate(UTILITIES_WIDGETS.keys()): widget.utils_choice.setCurrentIndex(i) - assert isinstance( - widget.utils_widgets[i], UTILITIES_WIDGETS[utils_name] - ) + assert isinstance(widget.utils_widgets[i], UTILITIES_WIDGETS[utils_name]) if utils_name == "Convert to instance labels": # to avoid issues with Voronoi-Otsu missing runtime menu = widget.utils_widgets[i].instance_widgets.method_choice diff --git a/napari_cellseg3d/_tests/test_review.py b/napari_cellseg3d/_tests/test_review.py index 98f4f682..52209bed 100644 --- a/napari_cellseg3d/_tests/test_review.py +++ b/napari_cellseg3d/_tests/test_review.py @@ -15,9 +15,7 @@ def test_launch_review(make_napari_viewer_proxy): widget.folder_choice.setChecked(True) widget.image_filewidget.text_field = im_path widget.labels_filewidget.text_field = lab_path - widget.results_filewidget.text_field = str( - Path(__file__).resolve().parent / "res" - ) + widget.results_filewidget.text_field = str(Path(__file__).resolve().parent / "res") widget.run_review() widget._viewer.close() diff --git a/napari_cellseg3d/_tests/test_training.py b/napari_cellseg3d/_tests/test_training.py index 15ec119e..b5277c5b 100644 --- a/napari_cellseg3d/_tests/test_training.py +++ b/napari_cellseg3d/_tests/test_training.py @@ -52,12 +52,8 @@ def test_supervised_training(make_napari_viewer_proxy): worker_config = widget._set_worker_config() assert worker_config.model_info.name == "test" worker = widget._create_supervised_worker_from_config(worker_config) - worker.config.train_data_dict = [ - {"image": im_path_str, "label": im_path_str} - ] - worker.config.val_data_dict = [ - {"image": im_path_str, "label": im_path_str} - ] + worker.config.train_data_dict = [{"image": im_path_str, "label": im_path_str}] + worker.config.val_data_dict = [{"image": im_path_str, "label": im_path_str}] worker.config.max_epochs = 2 worker.config.validation_interval = 2 @@ -97,9 +93,7 @@ def test_unsupervised_training(make_napari_viewer_proxy): ) # widget.start() widget.data = widget.create_dataset_dict_no_labs() - widget.worker = widget._create_worker( - additional_results_description="wnet_test" - ) + widget.worker = widget._create_worker(additional_results_description="wnet_test") assert widget.worker.config.train_data_dict is not None widget.worker.config.max_epochs = 1 for res_i in widget.worker.train( diff --git a/napari_cellseg3d/_tests/test_utils.py b/napari_cellseg3d/_tests/test_utils.py index 57f7ec7f..57d60bbb 100644 --- a/napari_cellseg3d/_tests/test_utils.py +++ b/napari_cellseg3d/_tests/test_utils.py @@ -29,9 +29,7 @@ def test_save_folder(): images = [rand_gen.random((5, 5, 5)).astype(np.float32) for _ in range(10)] images_paths = [f"{i}.tif" for i in range(10)] - utils.save_folder( - test_path, folder_name, images, images_paths, exist_ok=True - ) + utils.save_folder(test_path, folder_name, images, images_paths, exist_ok=True) assert (test_path / folder_name).is_dir() for i in range(10): assert (test_path / folder_name / images_paths[i]).is_file() @@ -51,9 +49,7 @@ def test_sphericities(): mock_surface = random.randint( 100, 1000 ) # assuming surface is always larger than volume - sphericity_vol = utils.sphericity_volume_area( - mock_volume, mock_surface - ) + sphericity_vol = utils.sphericity_volume_area(mock_volume, mock_surface) assert 0 <= sphericity_vol <= 1 semi_major = random.randint(10, 100) @@ -65,9 +61,7 @@ def test_sphericities(): except ValueError: sphericity_axes = 0 if sphericity_axes is None: - sphericity_axes = ( - 0 # errors already handled in function, returns None - ) + sphericity_axes = 0 # errors already handled in function, returns None assert 0 <= sphericity_axes <= 1 @@ -215,15 +209,11 @@ def test_parse_default_path(): test_path = (Path.home() / "test" / "test" / "test" / "test").as_posix() path = [test_path, None, None] - assert utils.parse_default_path(path, check_existence=False) == str( - test_path - ) + assert utils.parse_default_path(path, check_existence=False) == str(test_path) test_path = (Path.home() / "test" / "does" / "not" / "exist").as_posix() path = [test_path, None, None] - assert utils.parse_default_path(path, check_existence=True) == str( - Path.home() - ) + assert utils.parse_default_path(path, check_existence=True) == str(Path.home()) long_path = Path.home() long_path = ( diff --git a/napari_cellseg3d/code_models/crf.py b/napari_cellseg3d/code_models/crf.py index 14a8a373..928cc8f6 100644 --- a/napari_cellseg3d/code_models/crf.py +++ b/napari_cellseg3d/code_models/crf.py @@ -38,6 +38,7 @@ unary_from_softmax, ) + def correct_shape_for_crf(image, desired_dims=4): """Corrects the shape of the image to be compatible with the CRF post-processing step.""" logger.debug(f"Correcting shape for CRF, desired_dims={desired_dims}") @@ -104,9 +105,7 @@ def crf(image, prob, sa, sb, sg, w1, w2, n_iter=5): ) return None - d = dcrf.DenseCRF( - image.shape[1] * image.shape[2] * image.shape[3], prob.shape[0] - ) + d = dcrf.DenseCRF(image.shape[1] * image.shape[2] * image.shape[3], prob.shape[0]) # Get unary potentials from softmax probabilities U = unary_from_softmax(prob) diff --git a/napari_cellseg3d/code_models/instance_segmentation.py b/napari_cellseg3d/code_models/instance_segmentation.py index 754a996f..d5efec29 100644 --- a/napari_cellseg3d/code_models/instance_segmentation.py +++ b/napari_cellseg3d/code_models/instance_segmentation.py @@ -59,9 +59,7 @@ def __init__( self.function = function self.counters: List[ui.DoubleIncrementCounter] = [] self.sliders: List[ui.Slider] = [] - self._setup_widgets( - num_counters, num_sliders, widget_parent=widget_parent - ) + self._setup_widgets(num_counters, num_sliders, widget_parent=widget_parent) self.recorded_parameters = {} """Stores the parameters when calling self.record_parameters()""" @@ -97,9 +95,7 @@ def _setup_widgets(self, num_counters, num_sliders, widget_parent=None): setattr( self, widget, - ui.DoubleIncrementCounter( - text_label="", parent=widget_parent - ), + ui.DoubleIncrementCounter(text_label="", parent=widget_parent), ) self.counters.append(getattr(self, widget)) @@ -129,14 +125,10 @@ def record_parameters(self): """Records all the parameters of the instance segmentation method from the current values of the widgets.""" if len(self.sliders) > 0: for slider in self.sliders: - self.recorded_parameters[slider.label.text()] = ( - slider.slider_value - ) + self.recorded_parameters[slider.label.text()] = slider.slider_value if len(self.counters) > 0: for counter in self.counters: - self.recorded_parameters[counter.label.text()] = ( - counter.value() - ) + self.recorded_parameters[counter.label.text()] = counter.value() def run_method_from_params(self, image): """Runs the method on the image with the RECORDED parameters set in the widget. @@ -149,14 +141,10 @@ def run_method_from_params(self, image): Returns: processed image from self._method """ if len(self.recorded_parameters) == 0: - logger.warning( - "No parameters recorded, running with values from widgets" - ) + logger.warning("No parameters recorded, running with values from widgets") self.record_parameters() - parameters = [ - self.recorded_parameters[key] for key in self.recorded_parameters - ] + parameters = [self.recorded_parameters[key] for key in self.recorded_parameters] assert len(parameters) == len(self.sliders) + len(self.counters), ( f"Number of parameters recorded ({len(parameters)}) " @@ -187,9 +175,7 @@ def run_method_on_channels_from_params(self, image): Returns: processed image from self._method """ image_list = self._make_list_from_channels(image) - result = np.array( - [self.run_method_from_params(im) for im in image_list] - ) + result = np.array([self.run_method_from_params(im) for im in image_list]) return result.squeeze() @staticmethod @@ -389,9 +375,7 @@ def clear_large_objects(image, large_label_size=200, use_window=True): thres_small=large_label_size, rem_seed_thres=0, ) - res = InstanceMethod.sliding_window( - image, func, increment_labels=False - ) + res = InstanceMethod.sliding_window(image, func, increment_labels=False) return np.where(res > 0, 0, image) labeled = binary_watershed( @@ -545,9 +529,7 @@ def __init__(self, widget_parent=None): ) self.sliders[0].label.setText("Foreground probability threshold") - self.sliders[0].tooltips = ( - "Probability threshold for foreground object" - ) + self.sliders[0].tooltips = "Probability threshold for foreground object" self.sliders[0].setValue(500) self.sliders[1].label.setText("Seed probability threshold") @@ -646,9 +628,7 @@ def __init__(self, widget_parent=None): ) self.sliders[0].label.setText("Foreground probability threshold") - self.sliders[0].tooltips = ( - "Probability threshold for foreground object" - ) + self.sliders[0].tooltips = "Probability threshold for foreground object" self.sliders[0].setValue(800) self.counters[0].label.setText("Small objects removal") @@ -709,16 +689,12 @@ def __init__(self, widget_parent=None): widget_parent=widget_parent, ) self.counters[0].label.setText("Spot sigma") # closeness - self.counters[0].tooltips = ( - "Determines how close detected objects can be" - ) + self.counters[0].tooltips = "Determines how close detected objects can be" self.counters[0].setMaximum(100) self.counters[0].setValue(0.65) self.counters[1].label.setText("Outline sigma") # smoothness - self.counters[1].tooltips = ( - "Determines the smoothness of the segmentation" - ) + self.counters[1].tooltips = "Determines the smoothness of the segmentation" self.counters[1].setMaximum(100) self.counters[1].setValue(0.65) @@ -823,9 +799,7 @@ def _build(self): group.layout.addWidget(counter) self.instance_widgets[name].append(counter) except RuntimeError as e: - logger.debug( - f"Caught runtime error {e}, most likely during testing" - ) + logger.debug(f"Caught runtime error {e}, most likely during testing") self.setLayout(group.layout) self._set_visibility() diff --git a/napari_cellseg3d/code_models/model_framework.py b/napari_cellseg3d/code_models/model_framework.py index 7caff7b6..fadf9c40 100644 --- a/napari_cellseg3d/code_models/model_framework.py +++ b/napari_cellseg3d/code_models/model_framework.py @@ -1,4 +1,5 @@ """Basic napari plugin framework for inference and training.""" + from pathlib import Path from typing import TYPE_CHECKING @@ -48,9 +49,7 @@ def __init__( loads_labels: if True, will contain UI elements used to load napari label layers has_results: if True, will add UI to choose a results path """ - super().__init__( - viewer, parent, loads_images, loads_labels, has_results - ) + super().__init__(viewer, parent, loads_images, loads_labels, has_results) self._viewer = viewer """Viewer to display the widget in""" @@ -107,9 +106,7 @@ def __init__( self.report_container = ui.ContainerWidget(l=10, t=5, r=5, b=5) - self.report_container.setSizePolicy( - QSizePolicy.Fixed, QSizePolicy.Minimum - ) + self.report_container.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum) self.container_docked = False # check if already docked self.progress = QProgressBar(self.report_container) @@ -240,9 +237,7 @@ def display_status_report(self): def _toggle_weights_path(self): """Toggle visibility of weight path.""" - ui.toggle_visibility( - self.custom_weights_choice, self.weights_filewidget - ) + ui.toggle_visibility(self.custom_weights_choice, self.weights_filewidget) def get_unsupervised_image_filepaths(self): """Returns a list of filepaths to images in the unsupervised images folder.""" diff --git a/napari_cellseg3d/code_models/models/TEMPLATE_model.py b/napari_cellseg3d/code_models/models/TEMPLATE_model.py index 7c33adf4..0cd08141 100644 --- a/napari_cellseg3d/code_models/models/TEMPLATE_model.py +++ b/napari_cellseg3d/code_models/models/TEMPLATE_model.py @@ -15,9 +15,7 @@ class ModelTemplate_(ABC): default_threshold = 0.5 # specify the default threshold for the model @abstractmethod - def __init__( - self, input_image_size, in_channels=1, out_channels=1, **kwargs - ): + def __init__(self, input_image_size, in_channels=1, out_channels=1, **kwargs): """Reimplement this as needed; only include input_image_size if necessary. For now only in/out channels = 1 is supported.""" pass diff --git a/napari_cellseg3d/code_models/models/model_SegResNet.py b/napari_cellseg3d/code_models/models/model_SegResNet.py index ef1e7492..4e447a01 100644 --- a/napari_cellseg3d/code_models/models/model_SegResNet.py +++ b/napari_cellseg3d/code_models/models/model_SegResNet.py @@ -9,9 +9,7 @@ class SegResNet_(SegResNetVAE): weights_file = "SegResNet_latest.pth" default_threshold = 0.3 - def __init__( - self, input_img_size, out_channels=1, dropout_prob=0.3, **kwargs - ): + def __init__(self, input_img_size, out_channels=1, dropout_prob=0.3, **kwargs): """Create a SegResNet model. Args: diff --git a/napari_cellseg3d/code_models/models/model_TRAILMAP.py b/napari_cellseg3d/code_models/models/model_TRAILMAP.py index 6673d1d1..1389b1a1 100644 --- a/napari_cellseg3d/code_models/models/model_TRAILMAP.py +++ b/napari_cellseg3d/code_models/models/model_TRAILMAP.py @@ -1,4 +1,5 @@ """Legacy version of adapted TRAILMAP model, not used in the current version of the plugin.""" + # import torch # from torch import nn # diff --git a/napari_cellseg3d/code_models/models/model_TRAILMAP_MS.py b/napari_cellseg3d/code_models/models/model_TRAILMAP_MS.py index 2735c871..e95bcaef 100644 --- a/napari_cellseg3d/code_models/models/model_TRAILMAP_MS.py +++ b/napari_cellseg3d/code_models/models/model_TRAILMAP_MS.py @@ -26,9 +26,7 @@ def __init__(self, in_channels=1, out_channels=1, **kwargs): ) except TypeError as e: logger.warning(f"Caught TypeError: {e}") - super().__init__( - in_channels=in_channels, out_channels=out_channels - ) + super().__init__(in_channels=in_channels, out_channels=out_channels) # def get_output(self, input): # out = self(input) diff --git a/napari_cellseg3d/code_models/models/unet/buildingblocks.py b/napari_cellseg3d/code_models/models/unet/buildingblocks.py index ce7d378f..d9c68e47 100644 --- a/napari_cellseg3d/code_models/models/unet/buildingblocks.py +++ b/napari_cellseg3d/code_models/models/unet/buildingblocks.py @@ -6,16 +6,11 @@ def conv3d(in_channels, out_channels, kernel_size, bias, padding): - return nn.Conv3d( - in_channels, out_channels, kernel_size, padding=padding, bias=bias - ) + return nn.Conv3d(in_channels, out_channels, kernel_size, padding=padding, bias=bias) -def create_conv( - in_channels, out_channels, kernel_size, order, num_groups, padding -): - """ - Create a list of modules with together constitute a single conv layer with non-linearity +def create_conv(in_channels, out_channels, kernel_size, order, num_groups, padding): + """Create a list of modules with together constitute a single conv layer with non-linearity and optional batchnorm/groupnorm. Args: @@ -35,9 +30,9 @@ def create_conv( list of tuple (name, module) """ assert "c" in order, "Conv layer MUST be present" - assert ( - order[0] not in "rle" - ), "Non-linearity cannot be the first operation in the layer" + assert order[0] not in "rle", ( + "Non-linearity cannot be the first operation in the layer" + ) modules = [] for i, char in enumerate(order): @@ -70,15 +65,13 @@ def create_conv( if num_channels < num_groups: num_groups = 1 - assert ( - num_channels % num_groups == 0 - ), f"Expected number of channels in input to be divisible by num_groups. num_channels={num_channels}, num_groups={num_groups}" + assert num_channels % num_groups == 0, ( + f"Expected number of channels in input to be divisible by num_groups. num_channels={num_channels}, num_groups={num_groups}" + ) modules.append( ( "groupnorm", - nn.GroupNorm( - num_groups=num_groups, num_channels=num_channels - ), + nn.GroupNorm(num_groups=num_groups, num_channels=num_channels), ) ) elif char == "b": @@ -96,8 +89,7 @@ def create_conv( class SingleConv(nn.Sequential): - """ - Basic convolutional module consisting of a Conv3d, non-linearity and optional batchnorm/groupnorm. The order + """Basic convolutional module consisting of a Conv3d, non-linearity and optional batchnorm/groupnorm. The order of operations can be specified via the `order` parameter Args: @@ -136,8 +128,7 @@ def __init__( class DoubleConv(nn.Sequential): - """ - A module consisting of two consecutive convolution layers (e.g. BatchNorm3d+ReLU+Conv3d). + """A module consisting of two consecutive convolution layers (e.g. BatchNorm3d+ReLU+Conv3d). We use (Conv3d+ReLU+GroupNorm3d) by default. This can be changed however by providing the 'order' argument, e.g. in order to change to Conv3d+BatchNorm3d+ELU use order='cbe'. @@ -211,8 +202,7 @@ def __init__( class ExtResNetBlock(nn.Module): - """ - Basic UNet block consisting of a SingleConv followed by the residual block. + """Basic UNet block consisting of a SingleConv followed by the residual block. The SingleConv takes care of increasing/decreasing the number of channels and also ensures that the number of output channels is compatible with the residual block that follows. This block can be used instead of standard DoubleConv in the Encoder module. @@ -278,18 +268,16 @@ def forward(self, x): out = self.conv3(out) out += residual - out = self.non_linearity(out) - - return out + return self.non_linearity(out) class Encoder(nn.Module): - """ - A single module from the encoder path consisting of the optional max + """A single module from the encoder path consisting of the optional max pooling layer (one may specify the MaxPool kernel_size to be different than the standard (2,2,2), e.g. if the volumetric data is anisotropic (make sure to use complementary scale_factor in the decoder path) followed by a DoubleConv module. + Args: in_channels (int): number of input channels out_channels (int): number of output channels @@ -340,14 +328,13 @@ def __init__( def forward(self, x): if self.pooling is not None: x = self.pooling(x) - x = self.basic_module(x) - return x + return self.basic_module(x) class Decoder(nn.Module): - """ - A single module for decoder path consisting of the upsampling layer + """A single module for decoder path consisting of the upsampling layer (either learned ConvTranspose3d or nearest neighbor interpolation) followed by a basic module (DoubleConv or ExtResNetBlock). + Args: in_channels (int): number of input channels out_channels (int): number of output channels @@ -415,8 +402,7 @@ def __init__( def forward(self, encoder_features, x): x = self.upsampling(encoder_features=encoder_features, x=x) x = self.joining(encoder_features, x) - x = self.basic_module(x) - return x + return self.basic_module(x) @staticmethod def _joining(encoder_features, x, concat): @@ -510,8 +496,7 @@ def create_decoders( class AbstractUpsampling(nn.Module): - """ - Abstract class for upsampling. A given implementation should upsample a given 5D input tensor using either + """Abstract class for upsampling. A given implementation should upsample a given 5D input tensor using either interpolation or learned transposed convolution. """ @@ -527,11 +512,10 @@ def forward(self, encoder_features, x): class InterpolateUpsampling(AbstractUpsampling): - """ - Args: - mode (str): algorithm used for upsampling: - 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'. Default: 'nearest' - used only if transposed_conv is False + """Args: + mode (str): algorithm used for upsampling: + 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'. Default: 'nearest' + used only if transposed_conv is False """ def __init__(self, mode="nearest"): @@ -544,16 +528,15 @@ def _interpolate(x, size, mode): class TransposeConvUpsampling(AbstractUpsampling): - """ - Args: - in_channels (int): number of input channels for transposed conv - used only if transposed_conv is True - out_channels (int): number of output channels for transpose conv - used only if transposed_conv is True - kernel_size (int or tuple): size of the convolving kernel - used only if transposed_conv is True - scale_factor (int or tuple): stride of the convolution - used only if transposed_conv is True + """Args: + in_channels (int): number of input channels for transposed conv + used only if transposed_conv is True + out_channels (int): number of output channels for transpose conv + used only if transposed_conv is True + kernel_size (int or tuple): size of the convolving kernel + used only if transposed_conv is True + scale_factor (int or tuple): stride of the convolution + used only if transposed_conv is True """ diff --git a/napari_cellseg3d/code_models/models/unet/model.py b/napari_cellseg3d/code_models/models/unet/model.py index 9591d054..d4d4deb3 100644 --- a/napari_cellseg3d/code_models/models/unet/model.py +++ b/napari_cellseg3d/code_models/models/unet/model.py @@ -12,8 +12,7 @@ def number_of_features_per_level(init_channel_number, num_levels): class Abstract3DUNet(nn.Module): - """ - Base class for standard and residual UNet. + """Base class for standard and residual UNet. Args: in_channels (int): number of input channels @@ -60,9 +59,7 @@ def __init__( super(Abstract3DUNet, self).__init__() if isinstance(f_maps, int): - f_maps = number_of_features_per_level( - f_maps, num_levels=num_levels - ) + f_maps = number_of_features_per_level(f_maps, num_levels=num_levels) assert isinstance(f_maps, (list, tuple)) assert len(f_maps) > 1, "Required at least 2 levels in the U-Net" @@ -132,8 +129,7 @@ def forward(self, x): class UNet3D(Abstract3DUNet): - """ - 3DUnet model from + """3DUnet model from `"3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation" `. diff --git a/napari_cellseg3d/code_models/models/wnet/model.py b/napari_cellseg3d/code_models/models/wnet/model.py index e0e5b764..df8f670a 100644 --- a/napari_cellseg3d/code_models/models/wnet/model.py +++ b/napari_cellseg3d/code_models/models/wnet/model.py @@ -55,12 +55,8 @@ def __init__( ): """Initialize the W-Net model.""" super(WNet, self).__init__() - self.encoder = UNet( - in_channels, num_classes, softmax=True, dropout=dropout - ) - self.decoder = UNet( - num_classes, out_channels, softmax=False, dropout=dropout - ) + self.encoder = UNet(in_channels, num_classes, softmax=True, dropout=dropout) + self.decoder = UNet(num_classes, out_channels, softmax=False, dropout=dropout) def forward(self, x): """Forward pass of the W-Net model. Returns the segmentation and the reconstructed image.""" diff --git a/napari_cellseg3d/code_models/models/wnet/soft_Ncuts.py b/napari_cellseg3d/code_models/models/wnet/soft_Ncuts.py index 48cf905e..2abe1ea3 100644 --- a/napari_cellseg3d/code_models/models/wnet/soft_Ncuts.py +++ b/napari_cellseg3d/code_models/models/wnet/soft_Ncuts.py @@ -37,9 +37,7 @@ class SoftNCutsLoss(nn.Module): radius (scalar): radius of pixels for which we compute the weights """ - def __init__( - self, data_shape, device, intensity_sigma, spatial_sigma, radius=None - ): + def __init__(self, data_shape, device, intensity_sigma, spatial_sigma, radius=None): """Initialize the Soft N-Cuts loss. Args: @@ -86,24 +84,18 @@ def forward(self, labels, inputs): loss = 0 - kernel = self.gaussian_kernel(self.radius, self.spatial_sigma).to( - self.device - ) + kernel = self.gaussian_kernel(self.radius, self.spatial_sigma).to(self.device) for k in range(K): # Compute the average pixel value for this class, and the difference from each pixel class_probs = labels[:, k].unsqueeze(1) class_mean = torch.mean( inputs * class_probs, dim=(2, 3, 4), keepdim=True - ) / torch.add( - torch.mean(class_probs, dim=(2, 3, 4), keepdim=True), 1e-5 - ) + ) / torch.add(torch.mean(class_probs, dim=(2, 3, 4), keepdim=True), 1e-5) diff = (inputs - class_mean).pow(2).sum(dim=1).unsqueeze(1) # Weight the loss by the difference from the class average. - weights = torch.exp( - diff.pow(2).mul(-1 / self.intensity_sigma**2) - ) + weights = torch.exp(diff.pow(2).mul(-1 / self.intensity_sigma**2)) numerator = torch.sum( class_probs @@ -134,14 +126,10 @@ def gaussian_kernel(self, radius, sigma): x_2 = np.linspace(-radius, radius, 2 * radius + 1) ** 2 dist = ( np.sqrt( - x_2.reshape(-1, 1, 1) - + x_2.reshape(1, -1, 1) - + x_2.reshape(1, 1, -1) + x_2.reshape(-1, 1, 1) + x_2.reshape(1, -1, 1) + x_2.reshape(1, 1, -1) ) / sigma ) kernel = norm.pdf(dist) / norm.pdf(0) kernel = torch.from_numpy(kernel.astype(np.float32)) - return kernel.view( - (1, 1, kernel.shape[0], kernel.shape[1], kernel.shape[2]) - ) + return kernel.view((1, 1, kernel.shape[0], kernel.shape[1], kernel.shape[2])) diff --git a/napari_cellseg3d/code_models/worker_inference.py b/napari_cellseg3d/code_models/worker_inference.py index 65bf0d24..444856a5 100644 --- a/napari_cellseg3d/code_models/worker_inference.py +++ b/napari_cellseg3d/code_models/worker_inference.py @@ -174,17 +174,11 @@ def log_parameters(self): f"Thresholding is enabled at {config.post_process_config.thresholding.threshold_value}" ) - status = ( - "enabled" - if config.sliding_window_config.is_enabled() - else "disabled" - ) + status = "enabled" if config.sliding_window_config.is_enabled() else "disabled" self.log(f"Window inference is {status}") if status == "enabled": - self.log( - f"Window size is {self.config.sliding_window_config.window_size}" - ) + self.log(f"Window size is {self.config.sliding_window_config.window_size}") self.log( f"Window overlap is {self.config.sliding_window_config.window_overlap}" ) @@ -210,9 +204,7 @@ def load_folder(self): """Loads the folder specified in :py:attr:`~self.images_filepaths` and returns a MONAI DataLoader.""" images_dict = self.create_inference_dict(self.config.images_filepaths) - data_check = LoadImaged(keys=["image"], image_only=True)( - images_dict[0] - ) + data_check = LoadImaged(keys=["image"], image_only=True)(images_dict[0]) check = data_check["image"].shape pad = utils.get_padding_dim(check) @@ -249,9 +241,7 @@ def load_folder(self): self.log("Loading dataset...") inference_ds = Dataset(data=images_dict, transform=load_transforms) - inference_loader = DataLoader( - inference_ds, batch_size=1, num_workers=2 - ) + inference_loader = DataLoader(inference_ds, batch_size=1, num_workers=2) self.log("Done") return inference_loader @@ -337,9 +327,7 @@ def model_output( aniso_transform (monai.transforms.Zoom): the anisotropic transform to apply to the output """ inputs = inputs.to("cpu") - dataset_device = ( - "cpu" if self.config.keep_on_cpu else self.config.device - ) + dataset_device = "cpu" if self.config.keep_on_cpu else self.config.device if self.config.sliding_window_config.is_enabled(): window_size = self.config.sliding_window_config.window_size @@ -352,10 +340,7 @@ def model_output( # logger.debug(f"model : {model}") logger.debug(f"inputs shape : {inputs.shape}") logger.debug(f"inputs type : {inputs.dtype}") - if ( - self.config.layer is None - and self.config.images_filepaths is not None - ): + if self.config.layer is None and self.config.images_filepaths is not None: normalization = QuantileNormalization() else: @@ -376,15 +361,11 @@ def model_output_wrapper(inputs): check_result = result.detach().cpu().numpy() for i in range(check_result.shape[0]): for j in range(check_result.shape[1]): - fraction_labeled = ( - utils.fraction_above_threshold( - check_result[i, j], - EXPERIMENTAL_AUTO_DISCARD_VALUE, - ) - ) - logger.debug( - f"Fraction labeled: {fraction_labeled}" + fraction_labeled = utils.fraction_above_threshold( + check_result[i, j], + EXPERIMENTAL_AUTO_DISCARD_VALUE, ) + logger.debug(f"Fraction labeled: {fraction_labeled}") if ( fraction_labeled > EXPERIMENTAL_AUTO_DISCARD_FRACTION_THRESHOLD @@ -392,9 +373,7 @@ def model_output_wrapper(inputs): logger.debug( f"Discarding empty region with fraction {fraction_labeled}" ) - result[i, j] = torch.zeros_like( - result[i, j] - ) + result[i, j] = torch.zeros_like(result[i, j]) return result ########################################## return post_process_transforms(result) @@ -431,8 +410,7 @@ def model_output_wrapper(inputs): if aniso_transform is not None: out = aniso_transform(out) out = np.array(out).astype(np.float32) - out = np.squeeze(out) - return out + return np.squeeze(out) except Exception as e: logger.exception(e) self._raise_error(e, "Error during sliding window inference") @@ -449,9 +427,7 @@ def _correct_results_rotation(self, array, shape): f"Correcting rotation due to results shape mismatch: target {shape}, got {array.shape}" ) array = utils.correct_rotation(array) - if ( - array.shape[-3:] != shape[-3:] - ): # check only non-channel dimensions + if array.shape[-3:] != shape[-3:]: # check only non-channel dimensions logger.warning( f"Results shape mismatch: target {shape}, got {array.shape}" ) @@ -527,9 +503,7 @@ def get_instance_result(self, semantic_labels, from_layer=False, i=-1): * the stats of the instance labels """ if not from_layer and i == -1: - raise ValueError( - "An ID should be provided when running from a file" - ) + raise ValueError("An ID should be provided when running from a file") # old_stderr = sys.stderr # sys.stderr = TqdmToLogSignal(self.log_w_replacement) if self.config.post_process_config.instance.enabled: @@ -585,7 +559,7 @@ def save_image( + f"{additional_info}" + original_filename + self.config.model_info.name - + f"_pred_{i+1}" + + f"_pred_{i + 1}" + f"_{time}" + filetype ) @@ -600,7 +574,7 @@ def save_image( if from_layer: self.log(f"Layer prediction saved as : {filename}") else: - self.log(f"File n°{i+1} saved as : {filename}") + self.log(f"File n°{i + 1} saved as : {filename}") def aniso_transform(self, image): """Applies an anisotropic transform to the image.""" @@ -614,9 +588,7 @@ def aniso_transform(self, image): return anisotropic_transform(image[0]) return image - def instance_seg( - self, semantic_labels, image_id=0, original_filename="layer" - ): + def instance_seg(self, semantic_labels, image_id=0, original_filename="layer"): """Runs the instance segmentation on the semantic labels. Args: @@ -628,16 +600,10 @@ def instance_seg( self.log(f"Running instance segmentation for image n°{image_id}") method = self.config.post_process_config.instance.method - instance_labels = method.run_method_on_channels_from_params( - semantic_labels - ) + instance_labels = method.run_method_on_channels_from_params(semantic_labels) logger.debug(f"DEBUG instance results shape : {instance_labels.shape}") - filetype = ( - ".tif" - if self.config.filetype == "" - else "_" + self.config.filetype - ) + filetype = ".tif" if self.config.filetype == "" else "_" + self.config.filetype instance_filepath = ( self.config.results_path @@ -673,9 +639,7 @@ def inference_on_folder(self, inf_data, i, model, post_process_transforms): out = utils.correct_rotation(out) extra_dims = len(inputs.shape) - 3 - inputs_shape_corrected = np.swapaxes( - inputs, extra_dims, 2 + extra_dims - ).shape + inputs_shape_corrected = np.swapaxes(inputs, extra_dims, 2 + extra_dims).shape if out.shape[-3:] != inputs_shape_corrected[-3:]: logger.debug( f"Output shape {out.shape[-3:]} does not match input shape {inputs_shape_corrected[-3:]} on HWD dims even after rotation" @@ -699,7 +663,7 @@ def inference_on_folder(self, inf_data, i, model, post_process_transforms): crf_results = None original = np.array(inf_data["image"]).astype(np.float32) - self.log(f"Inference completed on image n°{i+1}") + self.log(f"Inference completed on image n°{i + 1}") return self.create_inference_result( out, @@ -770,9 +734,7 @@ def inference_on_layer(self, image, model, post_process_transforms): logger.debug(f"Inference on layer result shape : {out.shape}") out = utils.correct_rotation(out) extra_dims = len(image.shape) - 3 - layer_shape_corrected = np.swapaxes( - image, extra_dims, 2 + extra_dims - ).shape + layer_shape_corrected = np.swapaxes(image, extra_dims, 2 + extra_dims).shape if out.shape[-3:] != layer_shape_corrected[-3:]: logger.debug( f"Output shape {out.shape[-3:]} does not match input shape {layer_shape_corrected[-3:]} on HWD dims even after rotation" @@ -881,12 +843,14 @@ def inference(self): PRETRAINED_WEIGHTS_DIR / Path(model_class.weights_file) ) try: - missing = model.load_state_dict( # note that this is redefined in WNet_ - torch.load( - weights, - map_location=self.config.device, - ), - strict=False, # True, # TODO(cyril): change to True + missing = ( + model.load_state_dict( # note that this is redefined in WNet_ + torch.load( + weights, + map_location=self.config.device, + ), + strict=False, # True, # TODO(cyril): change to True + ) ) self.log(f"Weights status : {missing}") except Exception as e: diff --git a/napari_cellseg3d/code_models/worker_training.py b/napari_cellseg3d/code_models/worker_training.py index e6d3173b..10441aa8 100644 --- a/napari_cellseg3d/code_models/worker_training.py +++ b/napari_cellseg3d/code_models/worker_training.py @@ -217,9 +217,7 @@ def get_patch_dataset(self, train_transforms): Orientationd(keys=["image"], axcodes="PLI"), SpatialPadd( keys=["image"], - spatial_size=( - utils.get_padding_dim(self.config.sample_size) - ), + spatial_size=(utils.get_padding_dim(self.config.sample_size)), ), EnsureTyped(keys=["image"]), ] @@ -331,9 +329,7 @@ def _get_data(self): if self.config.sampling: logger.debug("Loading patch dataset") - (self.data_shape, dataset) = self.get_patch_dataset( - train_transforms - ) + (self.data_shape, dataset) = self.get_patch_dataset(train_transforms) else: logger.debug("Loading volume dataset") (self.data_shape, dataset) = self.get_dataset(train_transforms) @@ -425,9 +421,7 @@ def log_parameters(self): ] self.log("*" * 20) - def train( - self, provided_model=None, provided_optimizer=None, provided_loss=None - ): + def train(self, provided_model=None, provided_optimizer=None, provided_loss=None): """Main training function. Note : args are mainly used for testing purposes. Model is otherwise initialized in the function. @@ -617,9 +611,7 @@ def train( epoch_rec_loss += reconstruction_loss.item() if WANDB_INSTALLED: wandb.log( - { - "Train/Reconstruction loss": reconstruction_loss.item() - } + {"Train/Reconstruction loss": reconstruction_loss.item()} ) # Backward pass for the reconstruction loss @@ -633,9 +625,7 @@ def train( epoch_loss += loss.item() if WANDB_INSTALLED: - wandb.log( - {"Train/Weighted sum of losses": loss.item()} - ) + wandb.log({"Train/Weighted sum of losses": loss.item()}) loss.backward(loss) optimizer.step() @@ -664,9 +654,7 @@ def train( if WANDB_INSTALLED: wandb.finish() - self.ncuts_losses.append( - epoch_ncuts_loss / len(self.dataloader) - ) + self.ncuts_losses.append(epoch_ncuts_loss / len(self.dataloader)) self.rec_losses.append(epoch_rec_loss / len(self.dataloader)) self.total_losses.append(epoch_loss / len(self.dataloader)) @@ -709,25 +697,15 @@ def train( if WANDB_INSTALLED: wandb.log({"Ncuts loss for epoch": self.ncuts_losses[-1]}) + wandb.log({"Reconstruction loss for epoch": self.rec_losses[-1]}) + wandb.log({"Sum of losses for epoch": self.total_losses[-1]}) wandb.log( - {"Reconstruction loss for epoch": self.rec_losses[-1]} - ) - wandb.log( - {"Sum of losses for epoch": self.total_losses[-1]} - ) - wandb.log( - { - "LR/Model learning rate": optimizer.param_groups[ - 0 - ]["lr"] - } + {"LR/Model learning rate": optimizer.param_groups[0]["lr"]} ) self.log(f"Ncuts loss: {self.ncuts_losses[-1]:.5f}") self.log(f"Reconstruction loss: {self.rec_losses[-1]:.5f}") - self.log( - f"Weighted sum of losses: {self.total_losses[-1]:.5f}" - ) + self.log(f"Weighted sum of losses: {self.total_losses[-1]:.5f}") if epoch > 0: self.log( f"Ncuts loss difference: {self.ncuts_losses[-1] - self.ncuts_losses[-2]:.5f}" @@ -774,8 +752,7 @@ def train( if epoch % 5 == 0: torch.save( model.state_dict(), - self.config.results_path_folder - + "/wnet_checkpoint.pth", + self.config.results_path_folder + "/wnet_checkpoint.pth", ) self.log("Training finished") @@ -858,9 +835,7 @@ def eval(self, model, epoch) -> TrainingReport: # normalize val_inputs across channels for i in range(val_inputs.shape[0]): for j in range(val_inputs.shape[1]): - val_inputs[i][j] = self.normalize_function( - val_inputs[i][j] - ) + val_inputs[i][j] = self.normalize_function(val_inputs[i][j]) logger.debug(f"Val inputs shape: {val_inputs.shape}") val_outputs = sliding_window_inference( val_inputs, @@ -885,9 +860,7 @@ def eval(self, model, epoch) -> TrainingReport: val_outputs = AsDiscrete(threshold=0.5)(val_outputs) logger.debug(f"Val outputs shape: {val_outputs.shape}") logger.debug(f"Val labels shape: {val_labels.shape}") - logger.debug( - f"Val decoder outputs shape: {val_decoder_outputs.shape}" - ) + logger.debug(f"Val decoder outputs shape: {val_decoder_outputs.shape}") max_dice_channel = utils.seek_best_dice_coeff_channel( y_pred=val_outputs, y_true=val_labels @@ -913,8 +886,7 @@ def eval(self, model, epoch) -> TrainingReport: # save_best_path.mkdir(parents=True, exist_ok=True) save_best_name = "wnet" save_path = ( - str(Path(save_best_path) / save_best_name) - + "_best_metric.pth" + str(Path(save_best_path) / save_best_name) + "_best_metric.pth" ) self.log(f"Saving new best model to {save_path}") torch.save(model.state_dict(), save_path) @@ -1056,10 +1028,7 @@ def log_parameters(self): ] # self.log("-" * 10) self.log("Validation files :\n") - [ - self.log(f"- {Path(val_file['image']).name}\n") - for val_file in self.val_files - ] + [self.log(f"- {Path(val_file['image']).name}\n") for val_file in self.val_files] # self.log("-" * 10) if self.config.deterministic_config.enabled: @@ -1198,9 +1167,7 @@ def train( model_class = model_config.get_model() ######## Check that labels are semantic, not instance - check_labels = LoadImaged(keys=["label"])( - self.config.train_data_dict[0] - ) + check_labels = LoadImaged(keys=["label"])(self.config.train_data_dict[0]) if check_labels["label"].max() > 1: self.warn( "Labels are not semantic, but instance. Converting to semantic, this might cause errors." @@ -1209,9 +1176,7 @@ def train( ######## if not self.config.sampling: - data_check = LoadImaged(keys=["image"])( - self.config.train_data_dict[0] - ) + data_check = LoadImaged(keys=["image"])(self.config.train_data_dict[0]) check = data_check["image"].shape do_sampling = self.config.sampling size = self.config.sample_size if do_sampling else check @@ -1260,9 +1225,7 @@ def train( logger.warning(msg) - logger.debug( - f"Data dict from config is {self.config.train_data_dict}" - ) + logger.debug(f"Data dict from config is {self.config.train_data_dict}") logger.debug(f"Train files : {self.train_files}") logger.debug(f"Val. files : {self.val_files}") @@ -1272,23 +1235,21 @@ def train( raise ValueError("Validation dataset is empty") if self.config.do_augmentation: - train_transforms = ( - Compose( # TODO : figure out which ones and values ? - [ - RandShiftIntensityd(keys=["image"], offsets=0.7), - Rand3DElasticd( - keys=["image", "label"], - sigma_range=(0.3, 0.7), - magnitude_range=(0.3, 0.7), - ), - RandFlipd(keys=["image", "label"]), - RandRotate90d(keys=["image", "label"]), - RandAffined( - keys=["image"], - ), - EnsureTyped(keys=["image"]), - ] - ) + train_transforms = Compose( # TODO : figure out which ones and values ? + [ + RandShiftIntensityd(keys=["image"], offsets=0.7), + Rand3DElasticd( + keys=["image", "label"], + sigma_range=(0.3, 0.7), + magnitude_range=(0.3, 0.7), + ), + RandFlipd(keys=["image", "label"]), + RandRotate90d(keys=["image", "label"]), + RandAffined( + keys=["image"], + ), + EnsureTyped(keys=["image"]), + ] ) else: train_transforms = EnsureTyped(keys=["image", "label"]) @@ -1334,8 +1295,7 @@ def get_patch_loader_func(num_samples): self.config.num_samples * self.config.training_percent ) num_val_samples = ceil( - self.config.num_samples - * (1 - self.config.training_percent) + self.config.num_samples * (1 - self.config.training_percent) ) if num_train_samples < 2: self.log( @@ -1348,24 +1308,16 @@ def get_patch_loader_func(num_samples): ) num_val_samples = 2 - sample_loader_train = get_patch_loader_func( - num_train_samples - ) + sample_loader_train = get_patch_loader_func(num_train_samples) sample_loader_eval = get_patch_loader_func(num_val_samples) else: - num_train_samples = num_val_samples = ( - self.config.num_samples - ) + num_train_samples = num_val_samples = self.config.num_samples - sample_loader_train = get_patch_loader_func( - num_train_samples - ) + sample_loader_train = get_patch_loader_func(num_train_samples) sample_loader_eval = get_patch_loader_func(num_val_samples) logger.debug(f"AMOUNT of train samples : {num_train_samples}") - logger.debug( - f"AMOUNT of validation samples : {num_val_samples}" - ) + logger.debug(f"AMOUNT of validation samples : {num_val_samples}") logger.debug("train_ds") train_dataset = PatchDataset( @@ -1518,9 +1470,7 @@ def get_patch_loader_func(num_samples): self.log(f"Epoch {epoch + 1}/{self.config.max_epochs}") if device.type == "cuda": self.log("Memory Usage:") - alloc_mem = round( - torch.cuda.memory_allocated(device) / 1024**3, 1 - ) + alloc_mem = round(torch.cuda.memory_allocated(device) / 1024**3, 1) reserved_mem = round( torch.cuda.memory_reserved(device) / 1024**3, 1 ) @@ -1591,11 +1541,7 @@ def get_patch_loader_func(num_samples): if WANDB_INSTALLED: wandb.log({"Training/Epoch loss": epoch_loss / step}) wandb.log( - { - "LR/Model learning rate": optimizer.param_groups[ - 0 - ]["lr"] - } + {"LR/Model learning rate": optimizer.param_groups[0]["lr"]} ) epoch_loss /= step @@ -1604,9 +1550,7 @@ def get_patch_loader_func(num_samples): self.log("Updating scheduler...") scheduler.step(epoch_loss) - self.log( - f"Current learning rate: {optimizer.param_groups[0]['lr']}" - ) + self.log(f"Current learning rate: {optimizer.param_groups[0]['lr']}") checkpoint_output = [] eta = ( @@ -1646,9 +1590,7 @@ def get_patch_loader_func(num_samples): except Exception as e: self.raise_error(e, "Error during validation") - logger.debug( - f"val_outputs shape : {val_outputs.shape}" - ) + logger.debug(f"val_outputs shape : {val_outputs.shape}") # val_outputs = model(val_inputs) pred = decollate_batch(val_outputs) @@ -1664,18 +1606,13 @@ def get_patch_loader_func(num_samples): post_label = EnsureType() output_raw = [ - RemapTensor(new_max=1, new_min=0)(t) - for t in pred + RemapTensor(new_max=1, new_min=0)(t) for t in pred ] # output_raw = pred - val_outputs = [ - post_pred(res_tensor) for res_tensor in pred - ] + val_outputs = [post_pred(res_tensor) for res_tensor in pred] - val_labels = [ - post_label(res_tensor) for res_tensor in labs - ] + val_labels = [post_label(res_tensor) for res_tensor in labs] # logger.debug(len(val_outputs)) # logger.debug(len(val_labels)) @@ -1704,9 +1641,7 @@ def get_patch_loader_func(num_samples): for channel in checkpoint_output for item in channel ] - checkpoint_output[3] = checkpoint_output[3].astype( - np.uint16 - ) + checkpoint_output[3] = checkpoint_output[3].astype(np.uint16) metric = dice_metric.aggregate().detach().item() diff --git a/napari_cellseg3d/code_models/workers_utils.py b/napari_cellseg3d/code_models/workers_utils.py index a68b0976..587daa4a 100644 --- a/napari_cellseg3d/code_models/workers_utils.py +++ b/napari_cellseg3d/code_models/workers_utils.py @@ -1,4 +1,5 @@ """Several worker-related utilities for inference and training.""" + import typing as t from dataclasses import dataclass from pathlib import Path @@ -19,9 +20,7 @@ if TYPE_CHECKING: from napari_cellseg3d.code_models.instance_segmentation import ImageStats -PRETRAINED_WEIGHTS_DIR = Path(__file__).parent.resolve() / Path( - "models/pretrained" -) +PRETRAINED_WEIGHTS_DIR = Path(__file__).parent.resolve() / Path("models/pretrained") class WeightsDownloader: @@ -84,9 +83,7 @@ def show_progress(_, block_size, __): # count, block_size, total_size file=self.log_widget, ) - filename, _ = urllib.request.urlretrieve( - url, reporthook=show_progress - ) + filename, _ = urllib.request.urlretrieve(url, reporthook=show_progress) with tarfile.open(filename, mode="r:gz") as tar: def is_within_directory(directory, target): @@ -99,15 +96,11 @@ def is_within_directory(directory, target): return abs_directory in abs_target.parents - def safe_extract( - tar, path=".", members=None, *, numeric_owner=False - ): + def safe_extract(tar, path=".", members=None, *, numeric_owner=False): for member in tar.getmembers(): member_path = str(Path(path) / member.name) if not is_within_directory(path, member_path): - raise Exception( - "Attempted Path Traversal in Tar File" - ) + raise Exception("Attempted Path Traversal in Tar File") tar.extractall(path, members, numeric_owner=numeric_owner) @@ -186,9 +179,7 @@ def __init__(self, file_location): def forward(self, modeL_input): """Wraps ONNX output in a torch tensor.""" - outputs = self.ort_session.run( - None, {"input": modeL_input.cpu().numpy()} - ) + outputs = self.ort_session.run(None, {"input": modeL_input.cpu().numpy()}) return torch.tensor(outputs[0]) def eval(self): @@ -318,9 +309,7 @@ class TrainingReport: loss_1_values: t.Dict = None # example : {"Loss" : [0.1, 0.2, 0.3]} loss_2_values: t.List = None weights: np.array = None - images_dict: t.Dict = ( - None # output, discrete output, target, target labels - ) + images_dict: t.Dict = None # output, discrete output, target, target labels supervised: bool = True # OR decoder output, encoder output, target, target labels # format : {"Layer name" : {"data" : np.array, "cmap" : "turbo"}} diff --git a/napari_cellseg3d/code_plugins/plugin_base.py b/napari_cellseg3d/code_plugins/plugin_base.py index b89856f6..0aefd1a8 100644 --- a/napari_cellseg3d/code_plugins/plugin_base.py +++ b/napari_cellseg3d/code_plugins/plugin_base.py @@ -1,4 +1,5 @@ """Base classes for napari_cellseg3d plugins.""" + from functools import partial from pathlib import Path @@ -62,9 +63,7 @@ def __init__( self.layer_choice = ui.RadioButton("Layer", parent=self) self.folder_choice = ui.RadioButton("Folder", parent=self) self.filetype = None - self.radio_buttons = ui.combine_blocks( - self.folder_choice, self.layer_choice - ) + self.radio_buttons = ui.combine_blocks(self.folder_choice, self.layer_choice) self.io_panel = None # call self._build_io_panel to build ################ # Image widgets @@ -194,9 +193,7 @@ def _show_io_element(widget: QWidget, toggle: QWidget = None): widget.setVisible(True) if toggle is not None: - toggle.toggled.connect( - partial(ui.toggle_visibility, toggle, widget) - ) + toggle.toggled.connect(partial(ui.toggle_visibility, toggle, widget)) @staticmethod def _hide_io_element(widget: QWidget, toggle: QWidget = None): @@ -263,9 +260,7 @@ def _check_results_path(self, folder: str): if not Path(folder).is_dir(): Path(folder).mkdir(parents=True, exist_ok=True) if not Path(folder).is_dir(): - logger.info( - f"Could not create missing results folder : {folder}" - ) + logger.info(f"Could not create missing results folder : {folder}") return False logger.info(f"Created missing results folder : {folder}") return True @@ -305,9 +300,7 @@ def _make_prev_button(self): ) def _make_next_button(self): - return ui.Button( - "Next", lambda: self.setCurrentIndex(self.currentIndex() + 1) - ) + return ui.Button("Next", lambda: self.setCurrentIndex(self.currentIndex() + 1)) def remove_from_viewer(self): """Removes the widget from the napari window. @@ -354,9 +347,7 @@ def __init__( * A dropdown menu to select the file extension to be loaded from the folders """ - super().__init__( - viewer, parent, loads_images, loads_labels, has_results - ) + super().__init__(viewer, parent, loads_images, loads_labels, has_results) self.images_filepaths = [] """array(str): paths to images for training or inference""" @@ -377,15 +368,11 @@ def __init__( ####################################################### # interface self.image_filewidget.text_field = "Images directory" - self.image_filewidget.button.clicked.disconnect( - self._show_dialog_images - ) + self.image_filewidget.button.clicked.disconnect(self._show_dialog_images) self.image_filewidget.button.clicked.connect(self.load_image_dataset) self.labels_filewidget.text_field = "Labels directory" - self.labels_filewidget.button.clicked.disconnect( - self._show_dialog_labels - ) + self.labels_filewidget.button.clicked.disconnect(self._show_dialog_labels) self.labels_filewidget.button.clicked.connect(self.load_label_dataset) ################ # Validation images widget @@ -440,9 +427,7 @@ def load_unsup_images_dataset(self): logger.info("Images loaded (unsupervised training) :") for f in filenames: logger.info(f"{str(Path(f).name)}") - self.validation_filepaths = [ - str(path) for path in sorted(filenames) - ] + self.validation_filepaths = [str(path) for path in sorted(filenames)] path = str(Path(filenames[0]).parent) self.unsupervised_images_filewidget.text_field.setText(path) self.unsupervised_images_filewidget.check_ready() diff --git a/napari_cellseg3d/code_plugins/plugin_convert.py b/napari_cellseg3d/code_plugins/plugin_convert.py index c57c61c0..c7c743ab 100644 --- a/napari_cellseg3d/code_plugins/plugin_convert.py +++ b/napari_cellseg3d/code_plugins/plugin_convert.py @@ -120,9 +120,7 @@ def _start(self): existing_layer=self.layer, add_as_labels=True, ) - elif ( - self.folder_choice.isChecked() and len(self.labels_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.labels_filepaths) != 0: images = [ self._remove_large(imread(file), remove_size) for file in self.labels_filepaths @@ -201,9 +199,7 @@ def _build(self): ) self._set_io_visibility() - self.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding - ) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def _fragment(self, crops, name): dir_name = f"/{name}_fragmented_{utils.get_time_filepath()}" @@ -280,9 +276,7 @@ def _build(self): ) self._set_io_visibility() - self.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding - ) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) def _start(self): utils.mkdir_from_str(self.results_path) @@ -312,9 +306,7 @@ def _start(self): existing_layer=self.layer, ) - elif ( - self.folder_choice.isChecked() and len(self.images_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.images_filepaths) != 0: images = [ utils.resize(np.array(imread(file)), zoom) for file in self.images_filepaths @@ -384,9 +376,7 @@ def _build(self): ) container.layout.addWidget(self.data_panel) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() container.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding @@ -417,9 +407,7 @@ def _start(self): existing_layer=self.layer, add_as_labels=True, ) - elif ( - self.folder_choice.isChecked() and len(self.images_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.images_filepaths) != 0: images = [ clear_small_objects(file, remove_size, is_file_path=True) for file in self.images_filepaths @@ -470,9 +458,7 @@ def _build(self): ) container.layout.addWidget(self.data_panel) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() container.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding @@ -505,12 +491,9 @@ def _start(self): f"semantic_{layer.name}", existing_layer=self.layer, ) - elif ( - self.folder_choice.isChecked() and len(self.labels_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.labels_filepaths) != 0: images = [ - to_semantic(file, is_file_path=True) - for file in self.labels_filepaths + to_semantic(file, is_file_path=True) for file in self.labels_filepaths ] utils.save_folder( self.results_path, @@ -568,9 +551,7 @@ def _build(self): ui.add_widgets(self.instance_widgets.layout(), [self.start_btn]) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() container.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding @@ -600,9 +581,7 @@ def _start(self): add_as_labels=True, ) - elif ( - self.folder_choice.isChecked() and len(self.images_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.images_filepaths) != 0: images = [ self.instance_widgets.run_method(imread(file)) for file in self.images_filepaths @@ -676,9 +655,7 @@ def _build(self): ) container.layout.addWidget(self.data_panel) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() container.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding @@ -709,9 +686,7 @@ def _start(self): f"threshold_{layer.name}", existing_layer=self.layer, ) - elif ( - self.folder_choice.isChecked() and len(self.images_filepaths) != 0 - ): + elif self.folder_choice.isChecked() and len(self.images_filepaths) != 0: images = [ self.function(imread(file), remove_size) for file in self.images_filepaths @@ -771,9 +746,7 @@ def _build(self): ) container.layout.addWidget(self.data_panel) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() container.setSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding @@ -797,12 +770,8 @@ def _start(self): stats_df = pd.DataFrame(stats.get_dict()) csv_name = self.csv_name.text() + ".csv" logger.info(f"Saving stats to {self.results_path}/{csv_name}") - stats_df.to_csv( - self.results_path + "/" + csv_name, index=False - ) - elif ( - self.folder_choice.isChecked() and len(self.labels_filepaths) != 0 - ): + stats_df.to_csv(self.results_path + "/" + csv_name, index=False) + elif self.folder_choice.isChecked() and len(self.labels_filepaths) != 0: for i, image_path in enumerate(self.labels_filepaths): logger.debug(f"Loading image {i} : {image_path}") image = imread(image_path) @@ -821,21 +790,15 @@ def _start(self): try: stats = volume_stats(image) except Exception as e: - logger.warning( - f"Error computing stats for image {i} : {e}" - ) + logger.warning(f"Error computing stats for image {i} : {e}") warn(f"Error computing stats for image {i}", stacklevel=0) logger.debug("Computing stats") stats_df = pd.DataFrame(stats.get_dict()) - csv_name = ( - str(image_name) + "_" + self.csv_name.text() + f"_{i}.csv" - ) + csv_name = str(image_name) + "_" + self.csv_name.text() + f"_{i}.csv" logger.info( f"Saving stats to {(Path(self.results_path) / Path(csv_name)).resolve()}" ) - stats_df.to_csv( - Path(self.results_path) / Path(csv_name), index=False - ) + stats_df.to_csv(Path(self.results_path) / Path(csv_name), index=False) else: logger.warning("Please specify a layer or a folder") @@ -875,12 +838,8 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): self.start_btn = ui.Button("Start", self._start) self.result_display = ui.make_label(self.result_text, self) - self.image_layer_loader.layer_list.currentIndexChanged.connect( - self._reset - ) - self.label_layer_loader.layer_list.currentIndexChanged.connect( - self._reset - ) + self.image_layer_loader.layer_list.currentIndexChanged.connect(self._reset) + self.label_layer_loader.layer_list.currentIndexChanged.connect(self._reset) self.container = self._build() @@ -896,9 +855,7 @@ def _build(self): ], ) - ui.ScrollArea.make_scrollable( - container.layout, self, max_wh=[MAX_W, MAX_H] - ) + ui.ScrollArea.make_scrollable(container.layout, self, max_wh=[MAX_W, MAX_H]) self._set_io_visibility() return container @@ -977,6 +934,8 @@ def _start(self): "binarized", existing_layer=self.layer, ) - self.result_test = f"Best threshold : {best_threshold}, Dice : {self.values[best_threshold]}\n" + self.result_test = ( + f"Best threshold : {best_threshold}, Dice : {self.values[best_threshold]}\n" + ) self._get_dice_graph() self.result_display.setText(self.result_text) diff --git a/napari_cellseg3d/code_plugins/plugin_crf.py b/napari_cellseg3d/code_plugins/plugin_crf.py index 0421ba5b..6bc41428 100644 --- a/napari_cellseg3d/code_plugins/plugin_crf.py +++ b/napari_cellseg3d/code_plugins/plugin_crf.py @@ -1,4 +1,5 @@ """CRF plugin for napari_cellseg3d.""" + import contextlib from functools import partial from pathlib import Path @@ -97,12 +98,8 @@ def _set_tooltips(self): self.sg_choice.setToolTip( "SG : Standard deviation of the Gaussian kernel in the gradient term." ) - self.w1_choice.setToolTip( - "W1 : Weight of the appearance term in the CRF." - ) - self.w2_choice.setToolTip( - "W2 : Weight of the smoothness term in the CRF." - ) + self.w1_choice.setToolTip("W1 : Weight of the appearance term in the CRF.") + self.w2_choice.setToolTip("W2 : Weight of the smoothness term in the CRF.") self.n_iter_choice.setToolTip("Number of iterations of the CRF.") def make_config(self): @@ -265,10 +262,7 @@ def _start(self): image_list = [self.image_layer_loader.layer_data()] labels_list = [self.label_layer_loader.layer_data()] [logger.debug(f"Image shape: {image.shape}") for image in image_list] - [ - logger.debug(f"Label shape: {labels.shape}") - for labels in labels_list - ] + [logger.debug(f"Label shape: {labels.shape}") for labels in labels_list] self._prepare_worker(image_list, labels_list) diff --git a/napari_cellseg3d/code_plugins/plugin_crop.py b/napari_cellseg3d/code_plugins/plugin_crop.py index 93f5680f..47f99f6b 100644 --- a/napari_cellseg3d/code_plugins/plugin_crop.py +++ b/napari_cellseg3d/code_plugins/plugin_crop.py @@ -1,4 +1,5 @@ """Crop utility plugin for napari_cellseg3d.""" + from math import floor from pathlib import Path @@ -80,12 +81,8 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): self._viewer.layers.events.inserted.connect(self._check_image_list) # TODO(cyril) : fix layer removal (issue with cropping layer? ) - self.folder_choice.clicked.connect( - self._toggle_second_image_io_visibility - ) - self.layer_choice.clicked.connect( - self._toggle_second_image_io_visibility - ) + self.folder_choice.clicked.connect(self._toggle_second_image_io_visibility) + self.layer_choice.clicked.connect(self._toggle_second_image_io_visibility) self.results_filewidget.text_field.setText(str(self.save_path)) self.results_filewidget.check_ready() @@ -153,9 +150,7 @@ def _auto_set_dims(self): logger.debug(f"auto_set_dims : {data.shape}") if len(data.shape) == 3: for i, box in enumerate(self.crop_size_widgets): - logger.debug( - f"setting dim {i} to {floor(data.shape[i]/2)}" - ) + logger.debug(f"setting dim {i} to {floor(data.shape[i] / 2)}") box.setValue(floor(data.shape[i] / 2)) def _build(self): @@ -182,9 +177,7 @@ def _build(self): dim_group_l.addWidget(self.aniso_widgets) [ dim_group_l.addWidget(widget, alignment=ui.ABS_AL) - for widget_list in zip( - self.crop_size_labels, self.crop_size_widgets - ) + for widget_list in zip(self.crop_size_labels, self.crop_size_widgets) for widget in widget_list ] dim_group_w.setLayout(dim_group_l) @@ -242,8 +235,7 @@ def quicksave(self): time = utils.get_date_time() im1_path = str( - self.results_path - / Path("cropped_" + self.image_layer1.name + time) + self.results_path / Path("cropped_" + self.image_layer1.name + time) ) viewer.layers[f"cropped_{self.image_layer1.name}"].save(im1_path) @@ -252,8 +244,7 @@ def quicksave(self): if self.crop_second_image: im2_path = str( - self.results_path - / Path("cropped_" + self.image_layer2.name + time) + self.results_path / Path("cropped_" + self.image_layer2.name + time) ) viewer.layers[f"cropped_{self.image_layer2.name}"].save(im2_path) @@ -323,9 +314,7 @@ def _start(self): self.image_layer1 = self._add_isotropic_layer(self.image_layer1) if self.crop_second_image: - self.image_layer2 = self._add_isotropic_layer( - self.image_layer2 - ) + self.image_layer2 = self._add_isotropic_layer(self.image_layer2) self.image_layer2.visible = False else: self.image_layer1.opacity = 0.7 @@ -475,10 +464,7 @@ def _add_crop_sliders( # logger.debug(crop_sizes) # logger.debug(ends) # logger.debug(stepsizes) - if ( - self.im1_crop_layer is not None - and self.create_new_layer.isChecked() - ): + if self.im1_crop_layer is not None and self.create_new_layer.isChecked(): self.im1_crop_layer.translate = [0, 0, 0] if self.im2_crop_layer is not None: self.im2_crop_layer.translate = [0, 0, 0] diff --git a/napari_cellseg3d/code_plugins/plugin_helper.py b/napari_cellseg3d/code_plugins/plugin_helper.py index 27a90214..de76d326 100644 --- a/napari_cellseg3d/code_plugins/plugin_helper.py +++ b/napari_cellseg3d/code_plugins/plugin_helper.py @@ -24,7 +24,9 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.help_url = "https://adaptivemotorcontrollab.github.io/CellSeg3D/" - self.about_url = "https://wysscenter.ch/advances/3d-computer-vision-for-brain-analysis" + self.about_url = ( + "https://wysscenter.ch/advances/3d-computer-vision-for-brain-analysis" + ) self.repo_url = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" self._viewer = viewer @@ -38,9 +40,7 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self.logo_label.setIcon(QIcon(image)) self.logo_label.setMinimumSize(200, 200) self.logo_label.setIconSize(QSize(200, 200)) - self.logo_label.setStyleSheet( - "QPushButton { background-color: transparent }" - ) + self.logo_label.setStyleSheet("QPushButton { background-color: transparent }") self.logo_label.setToolTip("Open Github page") self.info_label = ui.make_label( diff --git a/napari_cellseg3d/code_plugins/plugin_metrics.py b/napari_cellseg3d/code_plugins/plugin_metrics.py index ae58314e..ca37cfb1 100644 --- a/napari_cellseg3d/code_plugins/plugin_metrics.py +++ b/napari_cellseg3d/code_plugins/plugin_metrics.py @@ -1,4 +1,5 @@ """CURRENTLY UNUSED.""" + from typing import TYPE_CHECKING import matplotlib.pyplot as plt @@ -111,9 +112,7 @@ def _build(self): self.threshold_box, self.lbl_threshold_box, horizontal=False, l=2 ) - ui.add_widgets( - param_group_l, [thresh_container, self.rotate_choice], None - ) + ui.add_widgets(param_group_l, [thresh_container, self.rotate_choice], None) param_group_w.setLayout(param_group_l) ############################## @@ -173,7 +172,7 @@ def plot_dice(self, dice_coeffs, threshold=DEFAULT_THRESHOLD): def remove_plots(self): """Clears plots from window view.""" - if len(self.plots) != 0: + if self.plots: for p in self.plots: p.setVisible(False) self.plots = [] @@ -191,14 +190,12 @@ def compute_dice(self): rotate = self.rotate_choice.isChecked() total_metrics = [] - self.canvas = ( - None # kind of terrible way to stack plots... but it works. - ) - image_id = 0 - for ground_path, pred_path in zip( - self.images_filepaths, self.labels_filepaths + self.canvas = None # kind of terrible way to stack plots... but it works. + + for image_id, (ground_path, pred_path) in enumerate( + zip(self.images_filepaths, self.labels_filepaths), + start=1, ): - image_id += 1 ground = imread(ground_path) pred = imread(pred_path) @@ -206,97 +203,55 @@ def compute_dice(self): pred = to_semantic(pred).astype(np.int8) pred_dims = pred.shape[-3:] - # ground_dims = ground.shape[-3:] - # print(pred_dims) - # print(ground_dims) pad_pred = utils.get_padding_dim(pred_dims) - # pad_ground = utils.get_padding_dim(ground_dims) - - # origin, target = utils.align_array_sizes(array_shape=pad_ground, target_shape=pad_pred) - - # ground = np.moveaxis(ground, origin, target) - # print(ground.shape) - # print(pred.shape) while len(pred.shape) < 5: pred = np.expand_dims(pred, axis=0) - # print("-") + while len(ground.shape) < 4: ground = np.expand_dims(ground, axis=0) + ground = (SpatialPad(pad_pred)(ToTensor()(ground))).numpy() + while len(ground.shape) < len(pred.shape): ground = np.expand_dims(ground, axis=0) - # print("&") - - # print(ground.shape) - # print(pred.shape) if ground.shape != pred.shape: raise ValueError( - f"Padded sizes of images do not match ! Padded ground label : {ground.shape} Padded pred label : {pred.shape}" + "Padded sizes of images do not match! " + f"Padded ground label: {ground.shape}. " + f"Padded pred label: {pred.shape}." ) - # if u < 1: - # self._viewer.add_image( - # ground, name="ground", colormap="blue", opacity=0.7 - # ) - # self._viewer.add_image(pred, name="pred", colormap="red") - # self._viewer.add_image( - # np.rot90(pred[0][0], axes=(0, 1)), - # name="pred flip 0", - # colormap="red", - # opacity=0.7, - # ) - # self._viewer.add_image( - # np.rot90(pred[0][0], axes=(1, 2)), - # name="pred flip 1", - # colormap="red", - # opacity=0.7, - # ) - # self._viewer.add_image( - # np.rot90(pred[0][0], axes=(0, 2)), - # name="pred flip 2", - # colormap="red", - # opacity=0.7, - # ) - # u += 1 scores = [] - if rotate: # TODO : recored best rotation for display + + if rotate: pred_flip_x = np.rot90(pred[0][0], axes=(0, 1)) pred_flip_y = np.rot90(pred[0][0], axes=(1, 2)) pred_flip_z = np.rot90(pred[0][0], axes=(0, 2)) - for p in [pred[0][0], pred_flip_x, pred_flip_y, pred_flip_z]: - scores.append(utils.dice_coeff(p, ground)) - scores.append(utils.dice_coeff(np.flip(p), ground)) - for i in range(3): + for rotated_pred in [pred[0][0], pred_flip_x, pred_flip_y, pred_flip_z]: + scores.append(utils.dice_coeff(rotated_pred, ground)) + scores.append(utils.dice_coeff(np.flip(rotated_pred), ground)) + + for axis in range(3): scores.append( - utils.dice_coeff(np.flip(p, axis=i), ground) + utils.dice_coeff(np.flip(rotated_pred, axis=axis), ground) ) else: - i = 0 scores.append(utils.dice_coeff(pred, ground)) - # if t < 1: - # for i in range(3): - # self._viewer.add_image( - # np.flip(pred_flip_x, axis=i), - # name=f"flip", - # colormap="green", - # opacity=0.7, - # ) - # t += 1 - - # print(scores) + score = max(scores) + if score < threshold: - # TODO add filename ? self._viewer.dims.ndisplay = 3 self._viewer.add_image( - ground, name=f"ground_{i+1}", colormap="blue", opacity=0.7 + ground, name=f"ground_{image_id}", colormap="blue", opacity=0.7 ) self._viewer.add_image( - pred, name=f"pred_{i+1}", colormap="red", opacity=0.7 + pred, name=f"pred_{image_id}", colormap="red", opacity=0.7 ) + total_metrics.append(score) # print(f"DICE METRIC :{total_metrics}") self.plot_dice(total_metrics, threshold) diff --git a/napari_cellseg3d/code_plugins/plugin_model_inference.py b/napari_cellseg3d/code_plugins/plugin_model_inference.py index 089a3db3..ee990bbd 100644 --- a/napari_cellseg3d/code_plugins/plugin_model_inference.py +++ b/napari_cellseg3d/code_plugins/plugin_model_inference.py @@ -87,9 +87,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): """InferenceWorkerConfig class from config.py""" self.instance_config: InstanceMethod """InstanceSegConfig class from config.py""" - self.post_process_config: config.PostProcessConfig = ( - config.PostProcessConfig() - ) + self.post_process_config: config.PostProcessConfig = config.PostProcessConfig() """PostProcessConfig class from config.py""" ########################### @@ -145,18 +143,14 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): ) self.thresholding_slider = ui.Slider( - default=config.MODEL_LIST[ - self.model_choice.currentText() - ].default_threshold + default=config.MODEL_LIST[self.model_choice.currentText()].default_threshold * 100, divide_factor=100.0, parent=self, ) self.use_window_choice = ui.CheckBox("Use window inference") - self.use_window_choice.toggled.connect( - self._toggle_display_window_size - ) + self.use_window_choice.toggled.connect(self._toggle_display_window_size) sizes_window = ["8", "16", "32", "64", "128", "256", "512"] self._default_window_size = sizes_window.index("64") @@ -200,9 +194,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): func=self._toggle_display_artifact_size_thresh, parent=self, ) - self.remove_artifacts_label = ui.make_label( - "Remove labels larger than :" - ) + self.remove_artifacts_label = ui.make_label("Remove labels larger than :") self.artifact_removal_size = ui.IntIncrementCounter( lower=1, upper=10000, @@ -225,18 +217,14 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): func=self._toggle_display_instance, parent=self, ) - self.use_instance_choice.toggled.connect( - self._toggle_artifact_removal_widgets - ) + self.use_instance_choice.toggled.connect(self._toggle_artifact_removal_widgets) self.use_crf = ui.CheckBox( "Use CRF post-processing", func=self._toggle_display_crf, parent=self, ) - self.save_stats_to_csv_box = ui.CheckBox( - "Save stats to csv", parent=self - ) + self.save_stats_to_csv_box = ui.CheckBox("Save stats to csv", parent=self) ################## ################## @@ -299,7 +287,9 @@ def _set_tooltips(self): self.window_size_choice.setToolTip( "Size of the window to run inference with (in pixels)" ) - self.window_overlap_slider.tooltips = "Percentage of overlap between windows to use when using sliding window" + self.window_overlap_slider.tooltips = ( + "Percentage of overlap between windows to use when using sliding window" + ) self.keep_data_on_cpu_box.setToolTip( "If enabled, data will be kept on the RAM rather than the VRAM.\nCan avoid out of memory issues with CUDA" @@ -328,10 +318,7 @@ def check_ready(self): if self.layer_choice.isChecked(): if self.image_layer_loader.layer_data() is not None: return True - elif ( - self.folder_choice.isChecked() - and self.image_filewidget.check_ready() - ): + elif self.folder_choice.isChecked() and self.image_filewidget.check_ready(): return True return False @@ -430,27 +417,21 @@ def _build(self): alignment=None, ) - self.view_results_container.setLayout( - self.view_results_container.layout - ) + self.view_results_container.setLayout(self.view_results_container.layout) self.anisotropy_wdgt.build() ###### ############ ################## - tab = ui.ContainerWidget( - b=1, parent=self - ) # tab that will contain all widgets + tab = ui.ContainerWidget(b=1, parent=self) # tab that will contain all widgets L, T, R, B = 7, 20, 7, 11 # margins for group boxes ################################# ################################# # self.image_filewidget.update_field_color("black") - self.results_filewidget.text_field.setText( - self.worker_config.results_path - ) + self.results_filewidget.text_field.setText(self.worker_config.results_path) self.results_filewidget.check_ready() tab.layout.addWidget(self.data_panel) @@ -551,9 +532,7 @@ def _build(self): self.thresholding_checkbox.setChecked(True) self._toggle_crf_choice() self.model_choice.currentIndexChanged.connect(self._toggle_crf_choice) - self.model_choice.currentIndexChanged.connect( - self._set_default_threshold - ) + self.model_choice.currentIndexChanged.connect(self._set_default_threshold) ModelFramework._show_io_element( self.save_stats_to_csv_box, self.use_instance_choice ) @@ -689,9 +668,7 @@ def _display_results(self, result: InferenceResult): and not isinstance(result.instance_labels, Exception) and self.worker_config.post_process_config.instance.enabled ): - method_name = ( - self.worker_config.post_process_config.instance.method.name - ) + method_name = self.worker_config.post_process_config.instance.method.name if len(result.instance_labels.shape) >= 4: channels_by_labels = np.argsort( @@ -702,9 +679,7 @@ def _display_results(self, result: InferenceResult): for i in range(1, len(channels_by_labels)): if ( np.unique( - result.instance_labels[ - channels_by_labels[i] - ].flatten() + result.instance_labels[channels_by_labels[i]].flatten() ).size > 1 ): @@ -712,9 +687,7 @@ def _display_results(self, result: InferenceResult): break number_cells = ( - np.unique( - result.instance_labels[min_objs_channel].flatten() - ).size + np.unique(result.instance_labels[min_objs_channel].flatten()).size - 1 ) else: @@ -833,9 +806,7 @@ def start(self): self.worker.start() self.btn_start.setText("Running... Click to stop") - def _create_worker_from_config( - self, worker_config: config.InferenceWorkerConfig - ): + def _create_worker_from_config(self, worker_config: config.InferenceWorkerConfig): if isinstance(worker_config, config.InfererConfig): raise TypeError("Please provide a valid worker config object") return InferenceWorker(worker_config=worker_config) @@ -972,10 +943,7 @@ def on_yield(self, result: InferenceResult): total = 1 self._update_progress_bar(image_id, total) - if ( - self.config.show_results - and image_id <= self.config.show_results_count - ): + if self.config.show_results and image_id <= self.config.show_results_count: self._display_results(result) except Exception as e: self.on_error(e) diff --git a/napari_cellseg3d/code_plugins/plugin_model_training.py b/napari_cellseg3d/code_plugins/plugin_model_training.py index 67af40e8..da1a77b7 100644 --- a/napari_cellseg3d/code_plugins/plugin_model_training.py +++ b/napari_cellseg3d/code_plugins/plugin_model_training.py @@ -230,14 +230,10 @@ def __init__( ) self.epoch_choice.valueChanged.connect(self._update_validation_choice) - self.val_interval_choice.valueChanged.connect( - self._update_validation_choice - ) + self.val_interval_choice.valueChanged.connect(self._update_validation_choice) self.learning_rate_choice = LearningRateWidget(parent=self) - self.lbl_learning_rate_choice = ( - self.learning_rate_choice.lr_value_choice.label - ) + self.lbl_learning_rate_choice = self.learning_rate_choice.lr_value_choice.label self.scheduler_patience_choice = ui.IntIncrementCounter( 1, @@ -253,9 +249,7 @@ def __init__( self.augment_choice = ui.CheckBox("Augment data") - self.close_buttons = [ - self._make_close_button() for i in range(NUMBER_TABS) - ] + self.close_buttons = [self._make_close_button() for i in range(NUMBER_TABS)] """Close buttons list for each tab""" self.patch_size_widgets = ui.IntIncrementCounter.make_n( @@ -328,9 +322,7 @@ def __init__( def _set_tooltips(self): # tooltips - self.zip_choice.setToolTip( - "Save a copy of the results as a zip folder" - ) + self.zip_choice.setToolTip("Save a copy of the results as a zip folder") self.train_split_percent_choice.tooltips = "The percentage of images to retain for training.\nThe remaining images will be used for validation" self.epoch_choice.tooltips = "The number of epochs to train for.\nThe more you train, the better the model will fit the training data" self.loss_choice.setToolTip( @@ -360,10 +352,7 @@ def _set_tooltips(self): "Check this to enable data augmentation, which will randomly deform, flip and shift the intensity in images" " to provide a more diverse dataset" ) - [ - w.setToolTip("Size of the sample to extract") - for w in self.patch_size_widgets - ] + [w.setToolTip("Size of the sample to extract") for w in self.patch_size_widgets] self.patch_choice.setToolTip( "Check this to automatically crop your images into smaller, cubic images for training." "\nShould be used if you have a few large images" @@ -752,9 +741,7 @@ def _build(self): ui.add_blank(self, train_tab.layout) ################## # deterministic choice group - seed_w, seed_l = ui.make_group( - "Deterministic training", r=1, b=5, t=11 - ) + seed_w, seed_l = ui.make_group("Deterministic training", r=1, b=5, t=11) ui.add_widgets( seed_l, [self.use_deterministic_choice, self.container_seed], @@ -970,9 +957,7 @@ def start(self): self._reset_loss_plot() - self.config = config.TrainerConfig( - save_as_zip=self.zip_choice.isChecked() - ) + self.config = config.TrainerConfig(save_as_zip=self.zip_choice.isChecked()) if self.unsupervised_mode: try: @@ -1030,13 +1015,9 @@ def _create_unsupervised_worker_from_config( return WNetTrainingWorker(worker_config=worker_config) def _create_worker(self, additional_results_description=None): - self._set_worker_config( - additional_description=additional_results_description - ) + self._set_worker_config(additional_description=additional_results_description) if self.unsupervised_mode: - return self._create_unsupervised_worker_from_config( - self.worker_config - ) + return self._create_unsupervised_worker_from_config(self.worker_config) return self._create_supervised_worker_from_config(self.worker_config) def _set_worker_config( @@ -1069,14 +1050,10 @@ def _set_worker_config( ) loss_name = ( - (f"{self.loss_choice.currentText()}_") - if not self.unsupervised_mode - else "" + (f"{self.loss_choice.currentText()}_") if not self.unsupervised_mode else "" ) additional_description = ( - (f"{additional_description}_") - if additional_description is not None - else "" + (f"{additional_description}_") if additional_description is not None else "" ) results_path_folder = Path( self.results_path @@ -1175,9 +1152,7 @@ def _set_unsupervised_worker_config( if eval_volume_dict is None: eval_batch_size = 1 else: - eval_batch_size = ( - 1 if len(eval_volume_dict) < batch_size else batch_size - ) + eval_batch_size = 1 if len(eval_volume_dict) < batch_size else batch_size self.worker_config = config.WNetTrainingWorkerConfig( device=self.check_device_choice(), weights_info=self.weights_config, @@ -1192,9 +1167,7 @@ def _set_unsupervised_worker_config( sample_size=patch_size, do_augmentation=self.augment_choice.isChecked(), deterministic_config=deterministic_config, - num_classes=int( - self.wnet_widgets.num_classes_choice.currentText() - ), + num_classes=int(self.wnet_widgets.num_classes_choice.currentText()), reconstruction_loss=self.wnet_widgets.loss_choice.currentText(), n_cuts_weight=self.wnet_widgets.ncuts_weight_choice.value(), rec_loss_weight=self.wnet_widgets.get_reconstruction_weight(), @@ -1207,9 +1180,7 @@ def _set_unsupervised_worker_config( def _is_current_job_supervised( self, ): # TODO(cyril) rework for better check and _make_csv - if isinstance(self.worker, WNetTrainingWorker): - return False - return True + return not isinstance(self.worker, WNetTrainingWorker) def on_start(self): """Catches started signal from worker.""" @@ -1226,9 +1197,7 @@ def on_finish(self): self.log.print_and_log("*" * 20) self.log.print_and_log(f"\nWorker finished at {utils.get_time()}") - self.log.print_and_log( - f"Saving in {self.worker_config.results_path_folder}" - ) + self.log.print_and_log(f"Saving in {self.worker_config.results_path_folder}") self.log.print_and_log("Saving last loss plot") plot_name = self.worker_config.results_path_folder / Path( @@ -1301,9 +1270,7 @@ def _display_results(self, images_dict, complete_missing=False): self._viewer.reset_view() else: for i, layer_name in enumerate(list(images_dict.keys())): - if layer_name not in [ - layer.name for layer in self._viewer.layers - ]: + if layer_name not in [layer.name for layer in self._viewer.layers]: logger.debug(f"Adding missing layer {layer_name}") layer = self._viewer.add_image( data=images_dict[layer_name]["data"], @@ -1313,9 +1280,7 @@ def _display_results(self, images_dict, complete_missing=False): layer_list[i] = layer else: logger.debug(f"Refreshing layer {layer_name}") - self.result_layers[i].data = images_dict[layer_name][ - "data" - ] + self.result_layers[i].data = images_dict[layer_name]["data"] self.result_layers[i].refresh() self.result_layers[i].reset_contrast_limits() @@ -1330,15 +1295,12 @@ def on_yield(self, report: TrainingReport): if ( report.epoch == 0 - or report.epoch + 1 - == self.worker_config.validation_interval + or report.epoch + 1 == self.worker_config.validation_interval ) and len(self.result_layers) == 0: self.result_layers = [] self._display_results(report.images_dict) else: - self._display_results( - report.images_dict, complete_missing=True - ) + self._display_results(report.images_dict, complete_missing=True) except Exception as e: logger.exception(e) @@ -1400,9 +1362,7 @@ def _make_csv(self): # TDOD(cyril) design could use a good rework self.loss_2_values, self.worker_config.validation_interval - 1, "", - )[ - : len(size_column) - ] + )[: len(size_column)] self.df = pd.DataFrame( { @@ -1441,9 +1401,7 @@ def _make_csv(self): # TDOD(cyril) design could use a good rework } ) - path = Path(self.worker_config.results_path_folder) / Path( - "training.csv" - ) + path = Path(self.worker_config.results_path_folder) / Path("training.csv") self.df.to_csv(path, index=False) def _show_plot_max(self, plot, y): @@ -1464,11 +1422,7 @@ def _plot_loss( show_plot_2_max: bool = True, ): """Creates two subplots to plot the training loss and validation metric.""" - plot_key = ( - "supervised" - if self._is_current_job_supervised() - else "unsupervised" - ) + plot_key = "supervised" if self._is_current_job_supervised() else "unsupervised" with plt.style.context("dark_background"): # update loss self.plot_1.set_title(self.plot_1_labels["title"][plot_key]) @@ -1488,9 +1442,7 @@ def _plot_loss( if metric_name == "Dice metric": self._show_plot_max(self.plot_1, y) if len(loss_values_1.keys()) > 1: - self.plot_1.legend( - loc="lower left", fontsize="10", markerscale=0.6 - ) + self.plot_1.legend(loc="lower left", fontsize="10", markerscale=0.6) # update plot 2 if self._is_current_job_supervised(): @@ -1691,14 +1643,12 @@ def __init__(self, parent): text_label="Reconstruction weight", ) self.reconstruction_weight_choice.setMaximumWidth(20) - self.reconstruction_weight_divide_factor_choice = ( - ui.DoubleIncrementCounter( - lower=0.01, - upper=10000.0, - default=1.0, - parent=parent, - text_label="Reconstruction weight divide factor", - ) + self.reconstruction_weight_divide_factor_choice = ui.DoubleIncrementCounter( + lower=0.01, + upper=10000.0, + default=1.0, + parent=parent, + text_label="Reconstruction weight divide factor", ) self.reconstruction_weight_divide_factor_choice.setMaximumWidth(20) @@ -1706,12 +1656,8 @@ def __init__(self, parent): def _set_tooltips(self): self.num_classes_choice.setToolTip("Number of classes to segment") - self.intensity_sigma_choice.setToolTip( - "Intensity sigma for the NCuts loss" - ) - self.spatial_sigma_choice.setToolTip( - "Spatial sigma for the NCuts loss" - ) + self.intensity_sigma_choice.setToolTip("Intensity sigma for the NCuts loss") + self.spatial_sigma_choice.setToolTip("Spatial sigma for the NCuts loss") self.radius_choice.setToolTip("Radius of NCuts loss region") self.loss_choice.setToolTip("Loss function to use for reconstruction") self.ncuts_weight_choice.setToolTip("Weight of the NCuts loss") diff --git a/napari_cellseg3d/code_plugins/plugin_review.py b/napari_cellseg3d/code_plugins/plugin_review.py index 7fbfc5d1..1b1957cc 100644 --- a/napari_cellseg3d/code_plugins/plugin_review.py +++ b/napari_cellseg3d/code_plugins/plugin_review.py @@ -1,4 +1,5 @@ """Review plugin for 3D labeling of volumes.""" + from pathlib import Path import matplotlib.pyplot as plt @@ -91,9 +92,7 @@ def __init__(self, viewer: "napari.viewer.Viewer", parent=None): self._build() - self.image_filewidget.text_field.textChanged.connect( - self._update_results_path - ) + self.image_filewidget.text_field.textChanged.connect(self._update_results_path) print(f"{self}") def _update_results_path(self): @@ -275,9 +274,7 @@ def launch_review(self): contrast_limits=[200, 1000], scale=self.config.zoom_factor, ) # anything bigger than 255 will get mapped to 255... they did it like this because it must have rgb images - viewer.add_labels( - base_label, name="labels", scale=self.config.zoom_factor - ) + viewer.add_labels(base_label, name="labels", scale=self.config.zoom_factor) @magicgui( dirname={"mode": "d", "label": "Save labels in... "}, @@ -322,9 +319,7 @@ def quicksave(): canvas = FigureCanvas(Figure(figsize=(3, 15))) xy_axes = canvas.figure.add_subplot(3, 1, 1) - canvas.figure.suptitle( - "Shift-click on image for plot \n", fontsize=8 - ) + canvas.figure.suptitle("Shift-click on image for plot \n", fontsize=8) xy_axes.imshow(np.zeros((100, 100), np.int16)) xy_axes.scatter(50, 50, s=30, c="green", alpha=0.6, marker="+") xy_axes.set_xlabel("X axis") @@ -347,18 +342,14 @@ def quicksave(): canvas.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) - canvas_dock = viewer.window.add_dock_widget( - canvas, name=" ", area="right" - ) + canvas_dock = viewer.window.add_dock_widget(canvas, name=" ", area="right") canvas_dock._close_btn = False @viewer.mouse_drag_callbacks.append def update_canvas(viewer, event): if "shift" in event.modifiers: try: - cursor_position = np.round(viewer.cursor.position).astype( - int - ) + cursor_position = np.round(viewer.cursor.position).astype(int) logger.debug(f"plot @ {cursor_position}") cropped_volume = crop_volume_around_point( @@ -403,9 +394,7 @@ def update_canvas(viewer, event): self.config.model_name, self.config.new_csv, ) - datamananger = viewer.window.add_dock_widget( - dmg, name=" ", area="left" - ) + datamananger = viewer.window.add_dock_widget(dmg, name=" ", area="left") datamananger._close_btn = False def update_button(axis_event): @@ -437,9 +426,11 @@ def crop_volume_around_point(points, layer, zoom_factor): for min_coordinate in min_coordinates ] superior_bound = [ - max_coordinate - volume.shape[i] - if volume.shape[i] < max_coordinate - else 0 + ( + max_coordinate - volume.shape[i] + if volume.shape[i] < max_coordinate + else 0 + ) for i, max_coordinate in enumerate(max_coordinates) ] diff --git a/napari_cellseg3d/code_plugins/plugin_review_dock.py b/napari_cellseg3d/code_plugins/plugin_review_dock.py index e485b0a5..a8856aa3 100644 --- a/napari_cellseg3d/code_plugins/plugin_review_dock.py +++ b/napari_cellseg3d/code_plugins/plugin_review_dock.py @@ -1,4 +1,5 @@ """Widget opened when a new Review session is started.""" + from datetime import datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING @@ -43,9 +44,7 @@ def __init__(self, parent: "napari.viewer.Viewer"): """napari.viewer.Viewer: viewer in which the widget is displayed""" # add some buttons - self.button = ui.Button( - "1", self._button_func, parent=self, fixed=True - ) + self.button = ui.Button("1", self._button_func, parent=self, fixed=True) self.time_label = ui.make_label("", self) self.time_label.setVisible(False) @@ -97,7 +96,7 @@ def pause_timer(self): self.time_elapsed += self.pause_start - self.start_time self.pause_box.setText("Resume timer") self.time_label.setText( - f"({utils.time_difference(timedelta(),self.time_elapsed)})" + f"({utils.time_difference(timedelta(), self.time_elapsed)})" ) self.is_paused = True else: @@ -185,9 +184,7 @@ def load_csv(self, label_dir, model_type, checkbox): # logger.debug("csv load time") # logger.debug(recorded_time) t = datetime.strptime(recorded_time, TIMER_FORMAT) - self.time_elapsed = timedelta( - hours=t.hour, minutes=t.minute, seconds=t.second - ) + self.time_elapsed = timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) # logger.debug(self.time_elapsed) return df, csv_path @@ -204,9 +201,7 @@ def create_csv(self, label_dir, model_type): labels = sorted( list( path.name - for path in Path(str(label_dir)).glob( - "./*" + self.filetype - ) + for path in Path(str(label_dir)).glob("./*" + self.filetype) ) ) else: diff --git a/napari_cellseg3d/code_plugins/plugin_utilities.py b/napari_cellseg3d/code_plugins/plugin_utilities.py index 9ca16c6e..01d83b64 100644 --- a/napari_cellseg3d/code_plugins/plugin_utilities.py +++ b/napari_cellseg3d/code_plugins/plugin_utilities.py @@ -75,17 +75,13 @@ def __init__(self, viewer: "napari.viewer.Viewer"): self._build() self.utils_choice.currentIndexChanged.connect(self._update_visibility) - self.utils_choice.currentIndexChanged.connect( - self._update_current_widget - ) + self.utils_choice.currentIndexChanged.connect(self._update_current_widget) # self._dock_util() self._update_visibility() qInstallMessageHandler(ui.handle_adjust_errors_wrapper(self)) def _update_current_widget(self): - self.current_widget = self.utils_widgets[ - self.utils_choice.currentIndex() - ] + self.current_widget = self.utils_widgets[self.utils_choice.currentIndex()] def _update_results_path(self, widget): self.results_filewidget.text_field.setText(str(widget.save_path)) @@ -106,9 +102,7 @@ def _build(self): # layout.setSizeConstraint(QLayout.SetFixedSize) self.setLayout(layout) # self.setMinimumHeight(2000) - self.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding - ) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self._update_visibility() def _create_utils_widgets(self, names): @@ -123,9 +117,7 @@ def _create_utils_widgets(self, names): self.current_widget = self.utils_widgets[0] if len(self.utils_widgets) != len(UTILITIES_WIDGETS.keys()): - raise RuntimeError( - "One or several utility widgets are missing/erroneous" - ) + raise RuntimeError("One or several utility widgets are missing/erroneous") def _update_layers(self, current_loader, new_loader): current_layer = current_loader.layer() @@ -136,9 +128,7 @@ def _update_layers(self, current_loader, new_loader): and current_layer.name in new_loader.layer_list.get_items() ): index = new_loader.layer_list.get_items().index(current_layer.name) - logger.debug( - f"Index of layer {current_layer.name} in new loader : {index}" - ) + logger.debug(f"Index of layer {current_layer.name} in new loader : {index}") new_loader.layer_list.setCurrentIndex(index) def _update_fields(self, widget: BasePluginUtils): @@ -164,9 +154,7 @@ def _update_fields(self, widget: BasePluginUtils): except KeyError: pass - logger.debug( - f"Current widget save path : {self.current_widget.save_path}" - ) + logger.debug(f"Current widget save path : {self.current_widget.save_path}") logger.debug( f"Current widget text field : {self.current_widget.results_filewidget.text_field.text()}" ) diff --git a/napari_cellseg3d/config.py b/napari_cellseg3d/config.py index eb3a090b..67d25d37 100644 --- a/napari_cellseg3d/config.py +++ b/napari_cellseg3d/config.py @@ -1,4 +1,5 @@ """Module to store configuration parameters for napari_cellseg3d.""" + import datetime from dataclasses import dataclass, field from pathlib import Path @@ -101,9 +102,7 @@ class ModelInfo: """ name: str = next(iter(MODEL_LIST)) - model_input_size: Optional[ - List[int] - ] = None # only used by SegResNet and SwinUNETR + model_input_size: Optional[List[int]] = None # only used by SegResNet and SwinUNETR num_classes: int = 2 # only used by WNets def get_model(self): @@ -272,7 +271,9 @@ class InferenceWorkerConfig: keep_on_cpu: bool = False compute_stats: bool = False post_process_config: PostProcessConfig = field(default_factory=PostProcessConfig) - sliding_window_config: SlidingWindowConfig = field(default_factory=SlidingWindowConfig) + sliding_window_config: SlidingWindowConfig = field( + default_factory=SlidingWindowConfig + ) use_crf: bool = False crf_config: CRFConfig = field(default_factory=CRFConfig) @@ -329,7 +330,9 @@ class TrainingWorkerConfig: learning_rate: np.float64 = 1e-3 validation_interval: int = 2 batch_size: int = 1 - deterministic_config: DeterministicConfig = field(default_factory=DeterministicConfig) + deterministic_config: DeterministicConfig = field( + default_factory=DeterministicConfig + ) scheduler_factor: float = 0.5 scheduler_patience: int = 10 weights_info: WeightsInfo = field(default_factory=WeightsInfo) diff --git a/napari_cellseg3d/dev_scripts/artefact_labeling.py b/napari_cellseg3d/dev_scripts/artefact_labeling.py index a2935354..8ce2a41a 100644 --- a/napari_cellseg3d/dev_scripts/artefact_labeling.py +++ b/napari_cellseg3d/dev_scripts/artefact_labeling.py @@ -1,4 +1,3 @@ -import os # TODO(cyril): remove os from pathlib import Path import napari @@ -48,9 +47,7 @@ def map_labels(labels, artefacts): unique = np.flip(unique[np.argsort(counts)]) counts = np.flip(counts[np.argsort(counts)]) if unique[0] != 0: - map_labels_existing.append( - np.array([i, unique[np.argmax(counts)]]) - ) + map_labels_existing.append(np.array([i, unique[np.argmax(counts)]])) elif ( counts[0] < np.sum(counts) * 2 / 3.0 ): # the artefact is connected to multiple neurons @@ -116,9 +113,7 @@ def make_labels( else: labels = ndimage.label(image_contrasted)[0] - labels = select_artefacts_by_size( - labels, min_size=threshold_size, is_labeled=True - ) + labels = select_artefacts_by_size(labels, min_size=threshold_size, is_labeled=True) if not do_multi_label: labels = np.where(labels > 0, label_value, 0) @@ -226,9 +221,7 @@ def make_artefact_labels( # calculate the percentile of the intensity of all the pixels that are labeled as neurons # check if the neurons are not empty if np.sum(neurons) > 0: - threshold = np.percentile( - image[neurons], threshold_artefact_brightness_percent - ) + threshold = np.percentile(image[neurons], threshold_artefact_brightness_percent) else: # take the percentile of the non neurons if the neurons are empty threshold = np.percentile(image[non_neurons], 90) @@ -259,9 +252,7 @@ def make_artefact_labels( # calculate the percentile of the size of the neurons if np.sum(neurons) > 0: sizes = ndimage.sum_labels(labels > 0, labels, np.unique(labels)) - neurone_size_percentile = np.percentile( - sizes, threshold_artefact_size_percent - ) + neurone_size_percentile = np.percentile(sizes, threshold_artefact_size_percent) else: # find the size of each connected component sizes = ndimage.sum_labels(labels > 0, labels, np.unique(labels)) @@ -385,12 +376,8 @@ def create_artefact_labels_from_folder( Power for contrast enhancement. """ # find all the images in the folder and create a list - path_labels = [ - f for f in os.listdir(path + "/labels") if f.endswith(".tif") - ] - path_images = [ - f for f in os.listdir(path + "/volumes") if f.endswith(".tif") - ] + path_labels = [f.name for f in Path(path / "labels").glob("*.tif")] + path_images = [f.name for f in Path(path / "volumes").glob("*.tif")] # sort the list path_labels.sort() path_images.sort() diff --git a/napari_cellseg3d/dev_scripts/classifier_test.ipynb b/napari_cellseg3d/dev_scripts/classifier_test.ipynb index 75f3ba41..71bb12ab 100644 --- a/napari_cellseg3d/dev_scripts/classifier_test.ipynb +++ b/napari_cellseg3d/dev_scripts/classifier_test.ipynb @@ -6,13 +6,14 @@ "metadata": {}, "outputs": [], "source": [ - "from tifffile import imread\n", - "import pandas as pd\n", - "import numpy as np\n", - "from sklearn.ensemble import RandomForestClassifier\n", "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", "import napari\n", - "import matplotlib.pyplot as plt" + "import numpy as np\n", + "import pandas as pd\n", + "from sklearn.ensemble import RandomForestClassifier\n", + "from tifffile import imread" ] }, { @@ -40,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -248,12 +249,12 @@ " data_df = pd.DataFrame([i.name[:-4] for i in images_p], columns=[\"image_path\"])\n", " # add 100 bins of the hist of each image\n", " hists = []\n", - " for i, image in enumerate(images):\n", + " for _i, image in enumerate(images):\n", " # hists.append(np.histogram(image, bins=100)[0])\n", " hist = np.histogram(image, bins=100)[0]\n", " hist = hist / hist.sum()\n", " hists.append(hist)\n", - " \n", + "\n", " data_df = pd.concat([data_df, pd.DataFrame(hists)], axis=1)\n", "else:\n", " data_df = pd.read_csv(DATA_DF_PATH)\n", @@ -295,6 +296,7 @@ "outputs": [], "source": [ "from napari.settings import get_settings\n", + "\n", "settings = get_settings()\n", "settings.application.ipy_interactive = False\n", "\n", @@ -302,7 +304,7 @@ "\n", "# for i, im in enumerate(images):\n", "# viewer = napari.Viewer()\n", - " \n", + "\n", "# @viewer.bind_key(\"o\")\n", "# def next_label(event):\n", "# if len(labels) == i+1:\n", @@ -310,7 +312,7 @@ "# return\n", "# labels.append(0)\n", "# print(\"Added label 0\")\n", - " \n", + "\n", "# @viewer.bind_key(\"p\")\n", "# def next_label(event):\n", "# if len(labels) == i+1:\n", @@ -318,7 +320,7 @@ "# return\n", "# labels.append(1)\n", "# print(\"Added label 1\")\n", - " \n", + "\n", "# @viewer.bind_key(\"q\")\n", "# def next_label(event):\n", "# if len(labels) != i+1:\n", @@ -331,7 +333,7 @@ "# labels.pop()\n", "# print(\"Removed last label\")\n", "# print(labels)\n", - " \n", + "\n", "# print(f\"Image {i+1}/{len(images)} :\")\n", "# viewer.add_image(im, colormap=\"turbo\")\n", "# viewer.dims.ndisplay = 3\n", @@ -407,8 +409,9 @@ "source": [ "# plot decision tree\n", "from sklearn import tree\n", + "\n", "plt.figure(figsize=(20, 20))\n", - "tree.plot_tree(classifier.estimators_[5], filled=True,class_names=[\"0\", \"1\"])\n", + "tree.plot_tree(classifier.estimators_[5], filled=True, class_names=[\"0\", \"1\"])\n", "plt.show()" ] }, @@ -466,7 +469,10 @@ "metadata": {}, "outputs": [], "source": [ - "TEST_DATA_PATH = Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/classifier_test/TEST/TEST_preds_128_overlap_0.tif\"\n", + "TEST_DATA_PATH = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/classifier_test/TEST/TEST_preds_128_overlap_0.tif\"\n", + ")\n", "test_image = imread(str(TEST_DATA_PATH))\n", "test_image = test_image.swapaxes(0, 2)" ] @@ -508,7 +514,7 @@ " for j in range(0, test_image.shape[1], cube_size):\n", " for k in range(0, test_image.shape[2], cube_size):\n", " if i + cube_size >= test_image.shape[0]:\n", - " crop_location_i = test_image.shape[0] - cube_size\n", + " crop_location_i = test_image.shape[0] - cube_size\n", " else:\n", " crop_location_i = i\n", " if j + cube_size >= test_image.shape[1]:\n", @@ -519,19 +525,39 @@ " crop_location_k = test_image.shape[2] - cube_size\n", " else:\n", " crop_location_k = k\n", - " \n", - " crop = test_image[crop_location_i:crop_location_i+cube_size, crop_location_j:crop_location_j+cube_size, crop_location_k:crop_location_k+cube_size]\n", + "\n", + " crop = test_image[\n", + " crop_location_i : crop_location_i + cube_size,\n", + " crop_location_j : crop_location_j + cube_size,\n", + " crop_location_k : crop_location_k + cube_size,\n", + " ]\n", " # crop_normalized = (crop - crop.min()) / (crop.max() - crop.min())\n", " hist = np.histogram(crop, bins=100)[0]\n", " hist = hist / hist.sum()\n", " pred = classifier.predict(hist.reshape(1, -1))[0]\n", - " \n", + "\n", " if pred == 0:\n", - " preds[crop_location_i:crop_location_i+cube_size, crop_location_j:crop_location_j+cube_size, crop_location_k:crop_location_k+cube_size] = crop\n", - " rejected[crop_location_i:crop_location_i+cube_size, crop_location_j:crop_location_j+cube_size, crop_location_k:crop_location_k+cube_size] = 0\n", + " preds[\n", + " crop_location_i : crop_location_i + cube_size,\n", + " crop_location_j : crop_location_j + cube_size,\n", + " crop_location_k : crop_location_k + cube_size,\n", + " ] = crop\n", + " rejected[\n", + " crop_location_i : crop_location_i + cube_size,\n", + " crop_location_j : crop_location_j + cube_size,\n", + " crop_location_k : crop_location_k + cube_size,\n", + " ] = 0\n", " else:\n", - " preds[crop_location_i:crop_location_i+cube_size, crop_location_j:crop_location_j+cube_size, crop_location_k:crop_location_k+cube_size] = 0\n", - " rejected[crop_location_i:crop_location_i+cube_size, crop_location_j:crop_location_j+cube_size, crop_location_k:crop_location_k+cube_size] = crop\n", + " preds[\n", + " crop_location_i : crop_location_i + cube_size,\n", + " crop_location_j : crop_location_j + cube_size,\n", + " crop_location_k : crop_location_k + cube_size,\n", + " ] = 0\n", + " rejected[\n", + " crop_location_i : crop_location_i + cube_size,\n", + " crop_location_j : crop_location_j + cube_size,\n", + " crop_location_k : crop_location_k + cube_size,\n", + " ] = crop\n", "\n", "view = napari.view_image(preds, colormap=\"turbo\")\n", "view.add_image(test_image, colormap=\"turbo\", blending=\"additive\")\n", @@ -559,6 +585,7 @@ ], "source": [ "import joblib\n", + "\n", "joblib.dump(classifier, DATA_PATH / \"classifier_new.joblib\")" ] } diff --git a/napari_cellseg3d/dev_scripts/correct_labels.py b/napari_cellseg3d/dev_scripts/correct_labels.py index 694c9102..b0a6811e 100644 --- a/napari_cellseg3d/dev_scripts/correct_labels.py +++ b/napari_cellseg3d/dev_scripts/correct_labels.py @@ -40,9 +40,7 @@ def relabel_non_unique_i(label, save_path, go_fast=False): new_labels = np.zeros_like(label) map_labels_existing = [] unique_label = np.unique(label) - for i_label in tqdm( - range(len(unique_label)), desc="relabeling", ncols=100 - ): + for i_label in tqdm(range(len(unique_label)), desc="relabeling", ncols=100): i = unique_label[i_label] if i == 0: continue @@ -183,9 +181,7 @@ def relabel( "visualize the relabeld image in white the previous labels and in red the new labels" ) if not test: - visualize_map( - map_labels_existing, label_path, new_label_path, delay=delay - ) + visualize_map(map_labels_existing, label_path, new_label_path, delay=delay) label_path = new_label_path # detect artefact print("detection of potential neurons (in progress)") @@ -209,9 +205,7 @@ def relabel( target=partial(_ask_labels, test=test), args=(unique_artefact,) ) t.start() - artefact_copy = np.where( - np.isin(artefact, i_labels_to_add), 0, artefact - ) + artefact_copy = np.where(np.isin(artefact, i_labels_to_add), 0, artefact) if viewer is None: viewer = napari.Viewer() viewer.add_image(image, name="image") @@ -228,9 +222,7 @@ def relabel( for i in i_labels_to_add: if i not in i_labels_to_add_tmp: i_labels_to_add_tmp.append(i) - artefact_copy = np.where( - np.isin(artefact, i_labels_to_add_tmp), artefact, 0 - ) + artefact_copy = np.where(np.isin(artefact, i_labels_to_add_tmp), artefact, 0) print("these labels will be added") if test: viewer.close() @@ -307,15 +299,11 @@ def to_show(map_labels_existing, delay=0.5): time.sleep(delay) -def create_connected_widget( - old_label, new_label, map_labels_existing, delay=0.5 -): +def create_connected_widget(old_label, new_label, map_labels_existing, delay=0.5): """Builds a widget that can control a function in another thread.""" worker = to_show(map_labels_existing, delay) worker.start() - worker.yielded.connect( - lambda arg: modify_viewer(old_label, new_label, arg) - ) + worker.yielded.connect(lambda arg: modify_viewer(old_label, new_label, arg)) def visualize_map(map_labels_existing, label_path, relabel_path, delay=0.5): @@ -347,9 +335,7 @@ def visualize_map(map_labels_existing, label_path, relabel_path, delay=0.5): old_label.show_selected_label = True new_label.show_selected_label = True - create_connected_widget( - old_label, new_label, map_labels_existing, delay=delay - ) + create_connected_widget(old_label, new_label, map_labels_existing, delay=delay) napari.run() diff --git a/napari_cellseg3d/dev_scripts/crop_data.py b/napari_cellseg3d/dev_scripts/crop_data.py index 0484c66a..a45bdaad 100644 --- a/napari_cellseg3d/dev_scripts/crop_data.py +++ b/napari_cellseg3d/dev_scripts/crop_data.py @@ -1,4 +1,5 @@ """Simple script to fragment a 3d image into smaller 3d images of size roi_size.""" + from pathlib import Path import numpy as np diff --git a/napari_cellseg3d/dev_scripts/evaluate_labels.py b/napari_cellseg3d/dev_scripts/evaluate_labels.py index 48861dcd..c3e78188 100644 --- a/napari_cellseg3d/dev_scripts/evaluate_labels.py +++ b/napari_cellseg3d/dev_scripts/evaluate_labels.py @@ -69,9 +69,7 @@ def evaluate_model_performance( artefacts_found = len(new_labels) if len(map_labels_existing) > 0: # calculate the mean true positive ratio of the model - mean_true_positive_ratio_model = np.mean( - [i[3] for i in map_labels_existing] - ) + mean_true_positive_ratio_model = np.mean([i[3] for i in map_labels_existing]) # calculate the mean ratio of the neurons pixels correctly labelled mean_ratio_pixel_found = np.mean([i[2] for i in map_labels_existing]) else: @@ -80,9 +78,7 @@ def evaluate_model_performance( if len(map_fused_neurons) > 0: # calculate the mean ratio of the neurons pixels correctly labelled for the fused neurons - mean_ratio_pixel_found_fused = np.mean( - [i[2] for i in map_fused_neurons] - ) + mean_ratio_pixel_found_fused = np.mean([i[2] for i in map_fused_neurons]) # calculate the mean true positive ratio of the model for the fused neurons mean_true_positive_ratio_model_fused = np.mean( [i[3] for i in map_fused_neurons] @@ -150,9 +146,7 @@ def evaluate_model_performance( neurones_not_found_labels = neurones_not_found_labels[ neurones_not_found_labels != 0 ] - not_found = np.where( - np.isin(labels, neurones_not_found_labels), labels, 0 - ) + not_found = np.where(np.isin(labels, neurones_not_found_labels), labels, 0) viewer.add_labels(not_found, name="ground truth not found") artefacts_found = np.where( np.isin(model_labels, [i[0] for i in new_labels]), @@ -229,9 +223,7 @@ def map_labels(gt_labels, model_labels, threshold_correct=PERCENT_CORRECT): else: # if >50% of the pixels of the label unique[ii] of the true label map to the same label i of the model, # the label i is considered either as a fused neurons, if it the case for multiple unique[ii] or as neurone found - ratio_pixel_found = counts[ii] / np.sum( - gt_labels == unique[ii] - ) + ratio_pixel_found = counts[ii] / np.sum(gt_labels == unique[ii]) if ratio_pixel_found > threshold_correct: total_pixel_found += np.sum(counts[ii]) tmp_map.append( diff --git a/napari_cellseg3d/dev_scripts/remote_inference.py b/napari_cellseg3d/dev_scripts/remote_inference.py index 7a28bf51..bf7c5b38 100644 --- a/napari_cellseg3d/dev_scripts/remote_inference.py +++ b/napari_cellseg3d/dev_scripts/remote_inference.py @@ -1,4 +1,5 @@ """Script to perform inference on a single image and run post-processing on the results, withot napari.""" + import logging from dataclasses import dataclass from pathlib import Path @@ -75,16 +76,14 @@ class PostProcessConfig: outline_sigma: float = 0.55 isotropic_spot_sigma: float = 0.2 isotropic_outline_sigma: float = 0.2 - anisotropy_correction: List[ - float - ] = None # TODO change to actual values, should be a ratio like [1,1/5,1] + anisotropy_correction: List[float] = ( + None # TODO change to actual values, should be a ratio like [1,1/5,1] + ) clear_small_size: int = 5 clear_large_objects: int = 500 -def inference_on_images( - image: np.array, config: InferenceWorkerConfig = CONFIG -): +def inference_on_images(image: np.array, config: InferenceWorkerConfig = CONFIG): """This function provides inference on an image with minimal config. Args: @@ -148,12 +147,10 @@ def post_processing(semantic_segmentation, config: PostProcessConfig = None): ) # clear small objects logger.info(f"Clearing small objects with {config.clear_small_size}") - labels = clear_small_objects(labels, config.clear_small_size).astype( - np.uint16 - ) + labels = clear_small_objects(labels, config.clear_small_size).astype(np.uint16) logger.debug(f"Labels shape: {labels.shape}") # get volume stats WITH ANISOTROPY - logger.debug(f"NUMBER OF OBJECTS: {np.max(np.unique(labels))-1}") + logger.debug(f"NUMBER OF OBJECTS: {np.max(np.unique(labels)) - 1}") stats_not_resized = volume_stats(labels) ######## RUN WITH ANISOTROPY ######## result_dict = {} @@ -172,15 +169,11 @@ def post_processing(semantic_segmentation, config: PostProcessConfig = None): spot_sigma=config.isotropic_spot_sigma, outline_sigma=config.isotropic_outline_sigma, ) - logger.info( - f"Clearing small objects with {config.clear_large_objects}" - ) + logger.info(f"Clearing small objects with {config.clear_large_objects}") labels_resized = clear_small_objects( labels_resized, config.clear_small_size ).astype(np.uint16) - logger.debug( - f"NUMBER OF OBJECTS: {np.max(np.unique(labels_resized))-1}" - ) + logger.debug(f"NUMBER OF OBJECTS: {np.max(np.unique(labels_resized)) - 1}") logger.info("Getting volume stats without anisotropy") stats_resized = volume_stats(labels_resized) return labels_resized, stats_resized diff --git a/napari_cellseg3d/dev_scripts/remote_training.py b/napari_cellseg3d/dev_scripts/remote_training.py index 4113b211..1121465b 100644 --- a/napari_cellseg3d/dev_scripts/remote_training.py +++ b/napari_cellseg3d/dev_scripts/remote_training.py @@ -18,13 +18,10 @@ RESULTS_PATH = ( Path("/data/cyril") / "CELLSEG_BENCHMARK/cellseg3d_train" - / f"{MODEL_NAME}_{SPLIT_FOLDER}_{int(TRAINING_SPLIT*100)}" + / f"{MODEL_NAME}_{SPLIT_FOLDER}_{int(TRAINING_SPLIT * 100)}" ) -IMAGES = ( - Path("/data/cyril") - / f"CELLSEG_BENCHMARK/TPH2_mesospim/SPLITS/{SPLIT_FOLDER}" -) +IMAGES = Path("/data/cyril") / f"CELLSEG_BENCHMARK/TPH2_mesospim/SPLITS/{SPLIT_FOLDER}" LABELS = ( Path("/data/cyril") / f"CELLSEG_BENCHMARK/TPH2_mesospim/SPLITS/{SPLIT_FOLDER}/labels/semantic" @@ -75,9 +72,7 @@ def prepare_data(images_path, labels_path): for file in images: logger.info(Path(file).name) - assert len(images) == len( - labels - ), "Number of images and labels must be the same" + assert len(images) == len(labels), "Number of images and labels must be the same" return [ {"image": str(image_path), "label": str(label_path)} diff --git a/napari_cellseg3d/dev_scripts/sliding_window_voronoi.py b/napari_cellseg3d/dev_scripts/sliding_window_voronoi.py index 2585d900..f0f1234c 100644 --- a/napari_cellseg3d/dev_scripts/sliding_window_voronoi.py +++ b/napari_cellseg3d/dev_scripts/sliding_window_voronoi.py @@ -1,4 +1,5 @@ """Test script for sliding window Voronoi-Otsu segmentation."".""" + import numpy as np import pyclesperanto as cle from tqdm import tqdm diff --git a/napari_cellseg3d/dev_scripts/test_new_evaluation.ipynb b/napari_cellseg3d/dev_scripts/test_new_evaluation.ipynb index dcb7ace9..b8a90683 100644 --- a/napari_cellseg3d/dev_scripts/test_new_evaluation.ipynb +++ b/napari_cellseg3d/dev_scripts/test_new_evaluation.ipynb @@ -8,10 +8,11 @@ }, "outputs": [], "source": [ - "import evaluate_labels as evl\n", - "from tifffile import imread\n", "import time\n", - "import numpy as np" + "\n", + "import evaluate_labels as evl\n", + "import numpy as np\n", + "from tifffile import imread" ] }, { @@ -34,6 +35,7 @@ ], "source": [ "from importlib import reload\n", + "\n", "reload(evl)" ] }, @@ -44,7 +46,11 @@ "outputs": [], "source": [ "from pathlib import Path\n", - "path_true_labels=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/LABELS/relabel_gt.tif\"" + "\n", + "path_true_labels = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/LABELS/relabel_gt.tif\"\n", + ")" ] }, { @@ -69,10 +75,19 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/isotropic_visual_cp_masks(1).tif\"\n", + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/isotropic_visual_cp_masks(1).tif\"\n", + ")\n", "labels = imread(path_model_label)\n", "# labels.shape\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), labels,visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " labels,\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -94,8 +109,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_pred_WNet.tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_pred_WNet.tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -117,8 +141,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/stardist_labels.tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/stardist_labels.tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -140,8 +173,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_Swin_Generalized_latest(1).tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_Swin_Generalized_latest(1).tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -163,8 +205,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_TRAILMAP_DiceCE_best_metric(1).tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_TRAILMAP_DiceCE_best_metric(1).tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -186,8 +237,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_VNet_Generalized_latest(1).tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_VNet_Generalized_latest(1).tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { @@ -209,8 +269,17 @@ } ], "source": [ - "path_model_label=Path.home() / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_SegResNet_Generalized_latest.tif\"\n", - "res = evl.evaluate_model_performance(imread(path_true_labels), imread(path_model_label),visualize=False, return_graphical_summary=True,plot_according_to_gt_label=False)" + "path_model_label = (\n", + " Path.home()\n", + " / \"Desktop/Code/CELLSEG_BENCHMARK/RESULTS/full data/instance/instance_threshold_pred_SegResNet_Generalized_latest.tif\"\n", + ")\n", + "res = evl.evaluate_model_performance(\n", + " imread(path_true_labels),\n", + " imread(path_model_label),\n", + " visualize=False,\n", + " return_graphical_summary=True,\n", + " plot_according_to_gt_label=False,\n", + ")" ] }, { diff --git a/napari_cellseg3d/dev_scripts/whole_brain_utils.py b/napari_cellseg3d/dev_scripts/whole_brain_utils.py index e36be7cf..d06c97e2 100644 --- a/napari_cellseg3d/dev_scripts/whole_brain_utils.py +++ b/napari_cellseg3d/dev_scripts/whole_brain_utils.py @@ -1,4 +1,5 @@ """Utilities to improve whole-brain regions segmentation.""" + import numpy as np from skimage.measure import label from skimage.segmentation import find_boundaries diff --git a/napari_cellseg3d/interface.py b/napari_cellseg3d/interface.py index d06a5554..125e8af4 100644 --- a/napari_cellseg3d/interface.py +++ b/napari_cellseg3d/interface.py @@ -1,4 +1,5 @@ """User interface functions and aliases.""" + import contextlib import threading from functools import partial @@ -88,9 +89,7 @@ class QWidgetSingleton(type(QObject)): def __call__(cls, *args, **kwargs): """Ensure only one instance of a QWidget with QWidgetSingleton as a metaclass exists at a time.""" if cls not in cls._instances: - cls._instances[cls] = super(QWidgetSingleton, cls).__call__( - *args, **kwargs - ) + cls._instances[cls] = super(QWidgetSingleton, cls).__call__(*args, **kwargs) return cls._instances[cls] @@ -282,9 +281,7 @@ def print_and_log(self, text, printing=True): # causes issue if you clik on terminal (tied to CMD QuickEdit mode on Windows) self.moveCursor(QTextCursor.End) self.insertPlainText(f"\n{text}") - self.verticalScrollBar().setValue( - self.verticalScrollBar().maximum() - ) + self.verticalScrollBar().setValue(self.verticalScrollBar().maximum()) finally: self.lock.release() @@ -354,9 +351,7 @@ def add_label(widget, label, label_before=True, horizontal=True): class ContainerWidget(QWidget): """Class for a container widget that can contain other widgets.""" - def __init__( - self, l=0, t=0, r=1, b=11, vertical=True, parent=None, fixed=True - ): + def __init__(self, l=0, t=0, r=1, b=11, vertical=True, parent=None, fixed=True): """Creates a container widget that can contain other widgets. Args: @@ -498,9 +493,7 @@ def __init__( super().__init__(orientation, parent) if upper <= lower: - raise ValueError( - "The minimum value cannot be below the maximum one" - ) + raise ValueError("The minimum value cannot be below the maximum one") self.setMaximum(upper) self.setMinimum(lower) @@ -690,9 +683,7 @@ def __init__( self._layout.setContentsMargins(0, 0, 0, 0) self.container = ContainerWidget(t=7, parent=parent) - self.checkbox = CheckBox( - "Anisotropic data", self._toggle_display_aniso, parent - ) + self.checkbox = CheckBox("Anisotropic data", self._toggle_display_aniso, parent) if use_integer_counter: self.box_widgets = IntIncrementCounter.make_n( @@ -799,9 +790,7 @@ def __init__( self._viewer = viewer self.layer_type = layer_type - self.layer_list = DropdownMenu( - parent=self, text_label=name, fixed=False - ) + self.layer_list = DropdownMenu(parent=self, text_label=name, fixed=False) self.layer_description = make_label("Shape:", parent=self) self.layer_description.setVisible(False) # self.layer_list.setSizeAdjustPolicy(QComboBox.AdjustToContents) # use tooltip instead ? @@ -822,9 +811,7 @@ def __init__( self._check_for_layers() def _get_all_layers(self): - return [ - self.layer_list.itemText(i) for i in range(self.layer_list.count()) - ] + return [self.layer_list.itemText(i) for i in range(self.layer_list.count())] def _check_for_layers(self): """Check for layers of the correct type and update the dropdown menu. @@ -838,9 +825,7 @@ def _check_for_layers(self): isinstance(layer, self.layer_type) and layer.name not in self._get_all_layers() ): - logger.debug( - f"Layer {layer.name} - List : {self._get_all_layers()}" - ) + logger.debug(f"Layer {layer.name} - List : {self._get_all_layers()}") # add new layers of correct type self.layer_list.addItem(layer.name) logger.debug(f"Layer {layer.name} has been added to the menu") @@ -854,9 +839,7 @@ def _check_for_layers(self): # remove layers of incorrect type index = self.layer_list.findText(layer.name) self.layer_list.removeItem(index) - logger.debug( - f"Layer {layer.name} has been removed from the menu" - ) + logger.debug(f"Layer {layer.name} has been removed from the menu") self._check_for_removed_layers() self._update_tooltip() @@ -917,9 +900,7 @@ def _rename_layer(self, _): def _remove_layer(self, event): removed_layer = event.value - if isinstance( - removed_layer, self.layer_type - ) and removed_layer.name in [ + if isinstance(removed_layer, self.layer_type) and removed_layer.name in [ self.layer_list.itemText(i) for i in range(self.layer_list.count()) ]: index = self.layer_list.findText(removed_layer.name) @@ -1044,10 +1025,7 @@ def button(self): def check_ready(self): """Check if a path is correctly set.""" - if ( - self.text_field.text() in ["", self._initial_desc] - and self.required - ): + if self.text_field.text() in ["", self._initial_desc] and self.required: self.update_field_color("indianred") self.text_field.setToolTip("Mandatory field !") return False @@ -1095,20 +1073,14 @@ def __init__( """ super().__init__(parent) - self._container_widget = ( - QWidget() - ) # required to use QScrollArea.setWidget() - self._container_widget.setSizePolicy( - QSizePolicy.Fixed, QSizePolicy.Maximum - ) + self._container_widget = QWidget() # required to use QScrollArea.setWidget() + self._container_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Maximum) self._container_widget.setLayout(contained_layout) self._container_widget.adjustSize() self.setWidget(self._container_widget) self.setWidgetResizable(True) - self.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding - ) + self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) if base_wh is not None: self.setBaseSize(base_wh[0], base_wh[1]) @@ -1118,9 +1090,7 @@ def __init__( self.setMinimumSize(min_wh[0], min_wh[1]) self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) - self.setHorizontalScrollBarPolicy( - Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @classmethod def make_scrollable( @@ -1281,9 +1251,7 @@ def make_n( fixed: Optional[bool] = True, ): """Creates n increment counters with the specified parameters.""" - return make_n_spinboxes( - cls, n, lower, upper, default, step, parent, fixed - ) + return make_n_spinboxes(cls, n, lower, upper, default, step, parent, fixed) def set_visibility(self, visible: bool): """Sets the visibility of the counter and its label.""" @@ -1347,9 +1315,7 @@ def make_n( fixed: Optional[bool] = True, ): """Creates n increment counters with the specified parameters.""" - return make_n_spinboxes( - cls, n, lower, upper, default, step, parent, fixed - ) + return make_n_spinboxes(cls, n, lower, upper, default, step, parent, fixed) def add_blank(widget, layout=None): @@ -1410,7 +1376,9 @@ def open_folder_dialog( logger.debug(f"Default : {default_path}") return QFileDialog.getExistingDirectory( - widget, "Open directory", default_path # + "/.." + widget, + "Open directory", + default_path, # + "/.." ) @@ -1543,23 +1511,15 @@ def combine_blocks( # TODO FIXME PLEASE this is a horrible design temp_layout = QGridLayout() if horizontal: - temp_widget.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.Maximum - ) + temp_widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) temp_layout.setColumnMinimumWidth(0, min_spacing) c1, c2, r1, r2 = 0, 1, 0, 0 - temp_layout.setContentsMargins( - l, t, r, b - ) # determines spacing between widgets + temp_layout.setContentsMargins(l, t, r, b) # determines spacing between widgets else: - temp_widget.setSizePolicy( - QSizePolicy.Maximum, QSizePolicy.MinimumExpanding - ) + temp_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.MinimumExpanding) temp_layout.setRowMinimumHeight(0, min_spacing) c1, c2, r1, r2 = 0, 0, 0, 1 - temp_layout.setContentsMargins( - l, t, r, b - ) # determines spacing between widgets + temp_layout.setContentsMargins(l, t, r, b) # determines spacing between widgets # temp_layout.setColumnMinimumWidth(1,100) # temp_layout.setSizeConstraint(QLayout.SetMinAndMaxSize) diff --git a/napari_cellseg3d/plugins.py b/napari_cellseg3d/plugins.py index 74d867c7..ee5c149f 100644 --- a/napari_cellseg3d/plugins.py +++ b/napari_cellseg3d/plugins.py @@ -2,6 +2,7 @@ Main plugins menu for napari-cellseg3d. """ + from napari_cellseg3d.code_plugins.plugin_helper import Helper from napari_cellseg3d.code_plugins.plugin_model_inference import Inferer from napari_cellseg3d.code_plugins.plugin_model_training import Trainer diff --git a/napari_cellseg3d/utils.py b/napari_cellseg3d/utils.py index 45f50582..4f3a0f1a 100644 --- a/napari_cellseg3d/utils.py +++ b/napari_cellseg3d/utils.py @@ -1,4 +1,5 @@ """Utilities functions, classes, and variables.""" + import logging import math from datetime import datetime @@ -30,9 +31,7 @@ #################### # viewer utils -def save_folder( - results_path, folder_name, images, image_paths, exist_ok=False -): +def save_folder(results_path, folder_name, images, image_paths, exist_ok=False): """Saves a list of images in a folder. Args: @@ -98,18 +97,14 @@ def show_result( if existing_layer is None: if add_as_image: LOGGER.info("Added resulting image layer") - results_layer = viewer.add_image( - image, name=name, colormap=colormap - ) + results_layer = viewer.add_image(image, name=name, colormap=colormap) elif add_as_labels: LOGGER.info("Added resulting label layer") results_layer = viewer.add_labels(image, name=name) else: if isinstance(layer, napari.layers.Image): LOGGER.info("Added resulting image layer") - results_layer = viewer.add_image( - image, name=name, colormap=colormap - ) + results_layer = viewer.add_image(image, name=name, colormap=colormap) elif isinstance(layer, napari.layers.Labels): LOGGER.info("Added resulting label layer") results_layer = viewer.add_labels(image, name=name) @@ -126,9 +121,7 @@ def show_result( f"Results not shown, layer {existing_layer.name} not found" "Showing new layer instead" ) - results_layer = show_result( - viewer, layer, image, name, existing_layer=None - ) + results_layer = show_result(viewer, layer, image, name, existing_layer=None) return results_layer @@ -202,9 +195,7 @@ def sphericity_axis(semi_major, semi_minor): root = np.sqrt(a**2 - b**2) try: result = ( - 2 - * (a * (b**2)) ** (1 / 3) - / (a + (b**2) / root * np.log((a + root) / b)) + 2 * (a * (b**2)) ** (1 / 3) / (a + (b**2) / root * np.log((a + root) / b)) ) except ZeroDivisionError: # LOGGER.debug( @@ -287,8 +278,7 @@ def normalize_max(image): shape = image.shape image = image.flatten() image = (image - image.min()) / (image.max() - image.min()) - image = image.reshape(shape) - return image + return image.reshape(shape) def remap_image( @@ -305,8 +295,7 @@ def remap_image( im_min = prev_min if prev_min is not None else image.min() image = (image - im_min) / (im_max - im_min) image = image * (new_max - new_min) + new_min - image = image.reshape(shape) - return image + return image.reshape(shape) def resize(image, zoom_factors): @@ -376,11 +365,7 @@ def time_difference(time_start, time_finish, as_string=True): minutes = f"{int(minutes[0])}".zfill(2) seconds = f"{int(seconds[0])}".zfill(2) - return ( - f"{hours}:{minutes}:{seconds}" - if as_string - else [hours, minutes, seconds] - ) + return f"{hours}:{minutes}:{seconds}" if as_string else [hours, minutes, seconds] def get_padding_dim(image_shape, anisotropy_factor=None): @@ -618,14 +603,10 @@ def channels_fraction_above_threshold(volume: np.array, threshold=0.5) -> list: list: List of length C containing the fraction of pixels above the threshold for each channel """ if len(volume.shape) != 4: - raise ValueError( - f"Volume shape {volume.shape} is not 4D. Expecting CxHxWxD." - ) + raise ValueError(f"Volume shape {volume.shape} is not 4D. Expecting CxHxWxD.") fractions = [] for _i, channel in enumerate(volume): - fractions.append( - fraction_above_threshold(channel, threshold=threshold) - ) + fractions.append(fraction_above_threshold(channel, threshold=threshold)) return fractions @@ -641,7 +622,5 @@ def fraction_above_threshold(volume: np.array, threshold=0.5) -> float: """ flattened = volume.flatten() above_thresh = np.where(flattened > threshold, 1, 0) - LOGGER.debug( - f"non zero in above_thresh : {np.count_nonzero(above_thresh)}" - ) + LOGGER.debug(f"non zero in above_thresh : {np.count_nonzero(above_thresh)}") return np.count_nonzero(above_thresh) / np.size(flattened) diff --git a/notebooks/Colab_WNet3D_training.ipynb b/notebooks/Colab_WNet3D_training.ipynb index 334be037..5aa955cc 100644 --- a/notebooks/Colab_WNet3D_training.ipynb +++ b/notebooks/Colab_WNet3D_training.ipynb @@ -43,7 +43,7 @@ }, "outputs": [], "source": [ - "#@markdown ##Play to install CellSeg3D and WNet3D dependencies:\n", + "# @markdown ##Play to install CellSeg3D and WNet3D dependencies:\n", "!pip install -q napari-cellseg3d\n", "print(\"Dependencies installed\")" ] @@ -81,8 +81,14 @@ "source": [ "# @title\n", "from pathlib import Path\n", - "from napari_cellseg3d.dev_scripts import colab_training as c\n", - "from napari_cellseg3d.config import WNetTrainingWorkerConfig, WandBConfig, WeightsInfo, PRETRAINED_WEIGHTS_DIR" + "\n", + "from napari_cellseg3d.config import (\n", + " PRETRAINED_WEIGHTS_DIR,\n", + " WandBConfig,\n", + " WeightsInfo,\n", + " WNetTrainingWorkerConfig,\n", + ")\n", + "from napari_cellseg3d.dev_scripts import colab_training as c" ] }, { @@ -179,18 +185,21 @@ } ], "source": [ - "#@markdown ##Execute the cell below to verify if GPU access is available.\n", + "# @markdown ##Execute the cell below to verify if GPU access is available.\n", "\n", "import torch\n", + "\n", "if not torch.cuda.is_available():\n", - " print('You do not have GPU access.')\n", - " print('Did you change your runtime?')\n", - " print('If the runtime setting is correct then Google did not allocate a GPU for your session')\n", - " print('Expect slow performance. To access GPU try reconnecting later')\n", + " print(\"You do not have GPU access.\")\n", + " print(\"Did you change your runtime?\")\n", + " print(\n", + " \"If the runtime setting is correct then Google did not allocate a GPU for your session\"\n", + " )\n", + " print(\"Expect slow performance. To access GPU try reconnecting later\")\n", "\n", "else:\n", - " print('You have GPU access')\n", - " !nvidia-smi\n" + " print(\"You have GPU access\")\n", + " !nvidia-smi" ] }, { @@ -234,7 +243,8 @@ "source": [ "# mount user's Google Drive to Google Colab.\n", "from google.colab import drive\n", - "drive.mount('/content/gdrive')" + "\n", + "drive.mount(\"/content/gdrive\")" ] }, { @@ -312,39 +322,43 @@ }, "outputs": [], "source": [ - "#@markdown ###Path to the training data:\n", - "training_source = \"./gdrive/MyDrive/path/to/data\" #@param {type:\"string\"}\n", - "#@markdown ###Path to save the weights (make sure to have enough space in your drive):\n", - "model_save_path = \"./gdrive/MyDrive/WNET_TRAINING_RESULTS\" #@param {type:\"string\"}\n", - "#@markdown ---\n", - "#@markdown ###Perform validation on a test dataset (optional):\n", - "do_validation = False #@param {type:\"boolean\"}\n", - "#@markdown ###Path to evaluation data (optional, use if checked above):\n", - "eval_source = \"./gdrive/MyDrive/CELLSEG_BENCHMARK/DATA/WNET/eval/vol/\" #@param {type:\"string\"}\n", - "eval_target = \"./gdrive/MyDrive/CELLSEG_BENCHMARK/DATA/WNET/eval/lab/\" #@param {type:\"string\"}\n", - "#@markdown ---\n", - "#@markdown ###Training parameters\n", - "number_of_epochs = 50 #@param {type:\"number\"}\n", - "#@markdown ###Default advanced parameters\n", - "use_default_advanced_parameters = False #@param {type:\"boolean\"}\n", - "#@markdown If not, please change:\n", - "\n", - "#@markdown Training parameters:\n", - "batch_size = 4 #@param {type:\"number\"}\n", - "learning_rate = 2e-5 #@param {type:\"number\"}\n", - "num_classes = 2 #@param {type:\"number\"}\n", - "weight_decay = 0.01 #@param {type:\"number\"}\n", - "#@markdown Validation parameters:\n", - "validation_frequency = 2 #@param {type:\"number\"}\n", - "#@markdown SoftNCuts parameters:\n", - "intensity_sigma = 1.0 #@param {type:\"number\"}\n", - "spatial_sigma = 4.0 #@param {type:\"number\"}\n", - "ncuts_radius = 2 #@param {type:\"number\"}\n", - "#@markdown Reconstruction loss:\n", - "rec_loss = \"MSE\" #@param[\"MSE\", \"BCE\"]\n", - "#@markdown Weighted sum of losses:\n", - "n_cuts_weight = 0.5 #@param {type:\"number\"}\n", - "rec_loss_weight = 0.005 #@param {type:\"number\"}" + "# @markdown ###Path to the training data:\n", + "training_source = \"./gdrive/MyDrive/path/to/data\" # @param {type:\"string\"}\n", + "# @markdown ###Path to save the weights (make sure to have enough space in your drive):\n", + "model_save_path = \"./gdrive/MyDrive/WNET_TRAINING_RESULTS\" # @param {type:\"string\"}\n", + "# @markdown ---\n", + "# @markdown ###Perform validation on a test dataset (optional):\n", + "do_validation = False # @param {type:\"boolean\"}\n", + "# @markdown ###Path to evaluation data (optional, use if checked above):\n", + "eval_source = (\n", + " \"./gdrive/MyDrive/CELLSEG_BENCHMARK/DATA/WNET/eval/vol/\" # @param {type:\"string\"}\n", + ")\n", + "eval_target = (\n", + " \"./gdrive/MyDrive/CELLSEG_BENCHMARK/DATA/WNET/eval/lab/\" # @param {type:\"string\"}\n", + ")\n", + "# @markdown ---\n", + "# @markdown ###Training parameters\n", + "number_of_epochs = 50 # @param {type:\"number\"}\n", + "# @markdown ###Default advanced parameters\n", + "use_default_advanced_parameters = False # @param {type:\"boolean\"}\n", + "# @markdown If not, please change:\n", + "\n", + "# @markdown Training parameters:\n", + "batch_size = 4 # @param {type:\"number\"}\n", + "learning_rate = 2e-5 # @param {type:\"number\"}\n", + "num_classes = 2 # @param {type:\"number\"}\n", + "weight_decay = 0.01 # @param {type:\"number\"}\n", + "# @markdown Validation parameters:\n", + "validation_frequency = 2 # @param {type:\"number\"}\n", + "# @markdown SoftNCuts parameters:\n", + "intensity_sigma = 1.0 # @param {type:\"number\"}\n", + "spatial_sigma = 4.0 # @param {type:\"number\"}\n", + "ncuts_radius = 2 # @param {type:\"number\"}\n", + "# @markdown Reconstruction loss:\n", + "rec_loss = \"MSE\" # @param[\"MSE\", \"BCE\"]\n", + "# @markdown Weighted sum of losses:\n", + "n_cuts_weight = 0.5 # @param {type:\"number\"}\n", + "rec_loss_weight = 0.005 # @param {type:\"number\"}" ] }, { @@ -391,49 +405,58 @@ "eval_image_folder = Path(eval_source)\n", "eval_label_folder = Path(eval_target)\n", "\n", - "eval_dict = c.create_eval_dataset_dict(\n", + "eval_dict = (\n", + " c.create_eval_dataset_dict(\n", " eval_image_folder,\n", " eval_label_folder,\n", - " ) if do_validation else None\n", + " )\n", + " if do_validation\n", + " else None\n", + ")\n", "\n", "try:\n", - " import wandb\n", - " WANDB_INSTALLED = True\n", + " import wandb\n", + "\n", + " WANDB_INSTALLED = True\n", "except ImportError:\n", - " WANDB_INSTALLED = False\n", - "\n", - "\n", - "train_config = WNetTrainingWorkerConfig(\n", - " device=\"cuda:0\",\n", - " max_epochs=number_of_epochs,\n", - " learning_rate=2e-5,\n", - " validation_interval=2,\n", - " batch_size=4,\n", - " num_workers=2,\n", - " weights_info=WeightsInfo(),\n", - " results_path_folder=str(results_path),\n", - " train_data_dict=c.create_dataset_dict_no_labs(train_data_folder),\n", - " eval_volume_dict=eval_dict,\n", - ") if use_default_advanced_parameters else WNetTrainingWorkerConfig(\n", - " device=\"cuda:0\",\n", - " max_epochs=number_of_epochs,\n", - " learning_rate=learning_rate,\n", - " validation_interval=validation_frequency,\n", - " batch_size=batch_size,\n", - " num_workers=2,\n", - " weights_info=WeightsInfo(),\n", - " results_path_folder=str(results_path),\n", - " train_data_dict=c.create_dataset_dict_no_labs(train_data_folder),\n", - " eval_volume_dict=eval_dict,\n", - " # advanced\n", - " num_classes=num_classes,\n", - " weight_decay=weight_decay,\n", - " intensity_sigma=intensity_sigma,\n", - " spatial_sigma=spatial_sigma,\n", - " radius=ncuts_radius,\n", - " reconstruction_loss=rec_loss,\n", - " n_cuts_weight=n_cuts_weight,\n", - " rec_loss_weight=rec_loss_weight,\n", + " WANDB_INSTALLED = False\n", + "\n", + "\n", + "train_config = (\n", + " WNetTrainingWorkerConfig(\n", + " device=\"cuda:0\",\n", + " max_epochs=number_of_epochs,\n", + " learning_rate=2e-5,\n", + " validation_interval=2,\n", + " batch_size=4,\n", + " num_workers=2,\n", + " weights_info=WeightsInfo(),\n", + " results_path_folder=str(results_path),\n", + " train_data_dict=c.create_dataset_dict_no_labs(train_data_folder),\n", + " eval_volume_dict=eval_dict,\n", + " )\n", + " if use_default_advanced_parameters\n", + " else WNetTrainingWorkerConfig(\n", + " device=\"cuda:0\",\n", + " max_epochs=number_of_epochs,\n", + " learning_rate=learning_rate,\n", + " validation_interval=validation_frequency,\n", + " batch_size=batch_size,\n", + " num_workers=2,\n", + " weights_info=WeightsInfo(),\n", + " results_path_folder=str(results_path),\n", + " train_data_dict=c.create_dataset_dict_no_labs(train_data_folder),\n", + " eval_volume_dict=eval_dict,\n", + " # advanced\n", + " num_classes=num_classes,\n", + " weight_decay=weight_decay,\n", + " intensity_sigma=intensity_sigma,\n", + " spatial_sigma=spatial_sigma,\n", + " radius=ncuts_radius,\n", + " reconstruction_loss=rec_loss,\n", + " n_cuts_weight=n_cuts_weight,\n", + " rec_loss_weight=rec_loss_weight,\n", + " )\n", ")\n", "wandb_config = WandBConfig(\n", " mode=\"disabled\" if not WANDB_INSTALLED else \"online\",\n", @@ -461,8 +484,8 @@ "source": [ "# @title\n", "worker = c.get_colab_worker(worker_config=train_config, wandb_config=wandb_config)\n", - "for epoch_loss in worker.train():\n", - " continue" + "for _epoch_loss in worker.train():\n", + " continue" ] }, { diff --git a/notebooks/Colab_inference_demo.ipynb b/notebooks/Colab_inference_demo.ipynb index ff673d4f..85d7f4a4 100644 --- a/notebooks/Colab_inference_demo.ipynb +++ b/notebooks/Colab_inference_demo.ipynb @@ -1,1349 +1,1371 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PoYFnmmQAm-x" - }, - "source": [ - "# **CellSeg3D : inference demo notebook**\n", - "\n", - "---\n", - "This notebook is part of the [CellSeg3D project](https://github.com/AdaptiveMotorControlLab/CellSeg3d) in the [Mathis Lab of Adaptive Intelligence](https://www.mackenziemathislab.org/).\n", - "\n", - "- 💜 The foundation of this notebook owes much to the **[ZeroCostDL4Mic](https://github.com/HenriquesLab/ZeroCostDL4Mic)** project and to the **[DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)** team." - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PoYFnmmQAm-x" + }, + "source": [ + "# **CellSeg3D : inference demo notebook**\n", + "\n", + "---\n", + "This notebook is part of the [CellSeg3D project](https://github.com/AdaptiveMotorControlLab/CellSeg3d) in the [Mathis Lab of Adaptive Intelligence](https://www.mackenziemathislab.org/).\n", + "\n", + "- 💜 The foundation of this notebook owes much to the **[ZeroCostDL4Mic](https://github.com/HenriquesLab/ZeroCostDL4Mic)** project and to the **[DeepLabCut](https://github.com/DeepLabCut/DeepLabCut)** team." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "vJLmPrWhAm-z" + }, + "source": [ + "# **1. Installing dependencies**\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "CUNTOWCnAm-z" + }, + "source": [ + "## **1.1 Installing CellSeg3D**\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true, + "id": "bnFKu6uFAm-z" + }, + "outputs": [], + "source": [ + "# @markdown ##Install CellSeg3D and grab demo data\n", + "!git clone https://github.com/AdaptiveMotorControlLab/CellSeg3d.git --branch main --single-branch ./CellSeg3D\n", + "!pip install napari-cellseg3d" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pSVZsebrAm-0" + }, + "source": [ + "## **1.3 Load key dependencies**\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "vzm75tE_Am-0" + }, + "outputs": [], + "source": [ + "# @title Load libraries\n", + "import logging\n", + "from pathlib import Path\n", + "\n", + "from tifffile import imread\n", + "\n", + "import napari_cellseg3d\n", + "from napari_cellseg3d.dev_scripts import remote_inference as cs3d\n", + "from napari_cellseg3d.utils import LOGGER as logger\n", + "\n", + "logger.setLevel(logging.INFO)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "XLEJsiVNAm-0" + }, + "source": [ + "# **2. Inference**\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "gjp4WL40Am-0" + }, + "source": [ + "\n", + "## **2.1. Check for GPU access**\n", + "---\n", + "\n", + "By default, this session is configured to use Python 3 and GPU acceleration. To verify or adjust these settings:\n", + "\n", + "Navigate to Runtime and select Change the Runtime type.\n", + "\n", + "For Runtime type, ensure it's set to Python 3 (the programming language this program is written in).\n", + "\n", + "Under Accelerator, choose GPU (Graphics Processing Unit).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Fe8hNkOpAm-0" + }, + "outputs": [], + "source": [ + "# @markdown This cell verifies if GPU access is available.\n", + "\n", + "import torch\n", + "\n", + "if not torch.cuda.is_available():\n", + " print(\"You do not have GPU access.\")\n", + " print(\"Did you change your runtime?\")\n", + " print(\n", + " \"If the runtime setting is correct then Google did not allocate a GPU for your session\"\n", + " )\n", + " print(\"Expect slow performance. To access GPU try reconnecting later\")\n", + "\n", + "else:\n", + " print(\"You have GPU access\")\n", + " !nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JBCyJAGsAm-0" + }, + "source": [ + "## **2.2 Run inference**\n", + "---" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 35 }, + "id": "O0jLRpARAm-0", + "outputId": "e4e8549c-7100-4c0c-bc30-505c0dfeb138" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "vJLmPrWhAm-z" + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" }, - "source": [ - "# **1. Installing dependencies**\n", - "---" + "text/plain": [ + "'cupy backend (experimental)'" ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "demo_image_path = \"/content/CellSeg3D/examples/c5image.tif\"\n", + "demo_image = imread(demo_image_path)\n", + "inference_config = cs3d.CONFIG\n", + "post_process_config = cs3d.PostProcessConfig()\n", + "# select cle device for colab\n", + "import pyclesperanto as cle\n", + "\n", + "cle.select_device()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "b6vIW_oDlpok" + }, + "source": [ + "### Select the pretrained model" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "5tkEI1q-loqB", + "outputId": "d41875da-3879-4158-8a0f-6330afe442af" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "CUNTOWCnAm-z" - }, - "source": [ - "## **1.1 Installing CellSeg3D**\n", - "---" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Selected model: SwinUNetR\n" + ] + } + ], + "source": [ + "model_selection = \"SwinUNetR\" # @param [\"SwinUNetR\", \"WNet3D\", \"SegResNet\"]\n", + "print(f\"Selected model: {model_selection}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "aPFS4WTdmPo3" + }, + "outputs": [], + "source": [ + "from napari_cellseg3d.config import ModelInfo\n", + "\n", + "model_info = ModelInfo(\n", + " name=model_selection,\n", + " model_input_size=64 if model_selection == \"SegResNet\" else [64, 64, 64],\n", + " num_classes=2,\n", + ")\n", + "inference_config.model_info = model_info" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "hIEKoyEGAm-0", + "outputId": "2103baf6-8875-433b-8799-41e0d1f3c7f0" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true, - "id": "bnFKu6uFAm-z" - }, - "outputs": [], - "source": [ - "#@markdown ##Install CellSeg3D and grab demo data\n", - "!git clone https://github.com/AdaptiveMotorControlLab/CellSeg3d.git --branch main --single-branch ./CellSeg3D\n", - "!pip install napari-cellseg3d" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "--------------------\n", + "Parameters summary :\n", + "Model is : SwinUNetR\n", + "Window inference is enabled\n", + "Window size is 64\n", + "Window overlap is 0.25\n", + "Dataset loaded on cuda device\n", + "--------------------\n", + "MODEL DIMS : [64, 64, 64]\n", + "Model name : SwinUNetR\n", + "Instantiating model...\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "pSVZsebrAm-0" - }, - "source": [ - "## **1.3 Load key dependencies**\n", - "---" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "monai.networks.nets.swin_unetr SwinUNETR.__init__:img_size: Argument `img_size` has been deprecated since version 1.3. It will be removed in version 1.5. The img_size argument is not required anymore and checks on the input size are run during forward().\n", + "INFO:napari_cellseg3d.utils:********************\n" + ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "vzm75tE_Am-0" - }, - "outputs": [], - "source": [ - "# @title Load libraries\n", - "import napari_cellseg3d\n", - "from tifffile import imread\n", - "from pathlib import Path\n", - "from napari_cellseg3d.dev_scripts import remote_inference as cs3d\n", - "from napari_cellseg3d.utils import LOGGER as logger\n", - "\n", - "import logging\n", - "\n", - "logger.setLevel(logging.INFO)" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading weights...\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "XLEJsiVNAm-0" - }, - "source": [ - "# **2. Inference**\n", - "---" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:napari_cellseg3d.utils:Downloading the model from HuggingFace https://huggingface.co/C-Achard/cellseg3d/resolve/main/SwinUNetR_latest.tar.gz....\n", + "270729216B [00:10, 26012663.01B/s] \n", + "You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "gjp4WL40Am-0" - }, - "source": [ - "\n", - "## **2.1. Check for GPU access**\n", - "---\n", - "\n", - "By default, this session is configured to use Python 3 and GPU acceleration. To verify or adjust these settings:\n", - "\n", - "Navigate to Runtime and select Change the Runtime type.\n", - "\n", - "For Runtime type, ensure it's set to Python 3 (the programming language this program is written in).\n", - "\n", - "Under Accelerator, choose GPU (Graphics Processing Unit).\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Weights status : \n", + "Done\n", + "--------------------\n", + "Parameters summary :\n", + "Model is : SwinUNetR\n", + "Window inference is enabled\n", + "Window size is 64\n", + "Window overlap is 0.25\n", + "Dataset loaded on cuda device\n", + "--------------------\n", + "Loading layer\n", + "2024-12-22 18:58:42,183 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'QuantileNormalization', transform is not lazy\n", + "2024-12-22 18:58:42,279 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'ToTensor', transform is not lazy\n", + "2024-12-22 18:58:42,290 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'EnsureType', transform is not lazy\n", + "Done\n", + "----------\n", + "Inference started on layer...\n", + "Post-processing...\n", + "Layer prediction saved as : volume_SwinUNetR_pred_1_2024_12_22_18_58_48\n" + ] + } + ], + "source": [ + "result = cs3d.inference_on_images(\n", + " demo_image,\n", + " config=inference_config,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "IFbmZ3_zAm-1", + "outputId": "bde6a6c5-f47f-4164-9e1c-3bf5a94dd00d" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Fe8hNkOpAm-0" - }, - "outputs": [], - "source": [ - "#@markdown This cell verifies if GPU access is available.\n", - "\n", - "import torch\n", - "if not torch.cuda.is_available():\n", - " print('You do not have GPU access.')\n", - " print('Did you change your runtime?')\n", - " print('If the runtime setting is correct then Google did not allocate a GPU for your session')\n", - " print('Expect slow performance. To access GPU try reconnecting later')\n", - "\n", - "else:\n", - " print('You have GPU access')\n", - " !nvidia-smi\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 9.61it/s]\n", + "clesperanto's cupy / CUDA backend is experimental. Please use it with care. The following functions are known to cause issues in the CUDA backend:\n", + "affine_transform, apply_vector_field, create(uint64), create(int32), create(int64), resample, scale, spots_to_pointlist\n", + "divide by zero encountered in scalar divide\n", + "invalid value encountered in scalar multiply\n", + "WARNING:napari_cellseg3d.utils:0 invalid sphericities were set to NaN. This occurs for objects with a volume of 1 pixel.\n" + ] + } + ], + "source": [ + "# @title Post-process the result\n", + "# @markdown This cell post-processes the result of the inference : thresholding, instance segmentation, and statistics.\n", + "\n", + "if model_selection == \"WNet3D\":\n", + " result[0].semantic_segmentation = result[0].semantic_segmentation[1]\n", + "\n", + "instance_segmentation, stats = cs3d.post_processing(\n", + " result[0].semantic_segmentation,\n", + " config=post_process_config,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 496, + "referenced_widgets": [ + "7a72ee57e14c440bb2ce281da67e1311", + "2692114df7304a1cbc703e8bd0f848c3", + "697f7288fff64aefbee1a5c0d4894987", + "1e79fde882a44cd984a54a71c3337759", + "9cb58613b1a74eaeb285f6d2d77d567b", + "a1d487697e4b4ea6b897f380c2b112cc", + "10441f745a6f41cf8655b2fafbb8204f" + ] }, + "id": "TMRiQ-m4Am-1", + "outputId": "2d819126-5478-4d98-a5e2-ecacb7872465" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "JBCyJAGsAm-0" + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7a72ee57e14c440bb2ce281da67e1311", + "version_major": 2, + "version_minor": 0 }, - "source": [ - "## **2.2 Run inference**\n", - "---" + "text/plain": [ + "interactive(children=(IntSlider(value=62, description='z', max=123), Output()), _dom_classes=('widget-interact…" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 35 - }, - "id": "O0jLRpARAm-0", - "outputId": "e4e8549c-7100-4c0c-bc30-505c0dfeb138" - }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'cupy backend (experimental)'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } + "data": { + "text/html": [ + "
\n", + "
update_plot
def update_plot(z)
/content/<ipython-input-9-245acde924e0><no docstring>
" ], - "source": [ - "demo_image_path = \"/content/CellSeg3D/examples/c5image.tif\"\n", - "demo_image = imread(demo_image_path)\n", - "inference_config = cs3d.CONFIG\n", - "post_process_config = cs3d.PostProcessConfig()\n", - "# select cle device for colab\n", - "import pyclesperanto as cle\n", - "cle.select_device()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "b6vIW_oDlpok" - }, - "source": [ - "### Select the pretrained model" + "text/plain": [ + "" ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# @title Display the result\n", + "# @markdown This cell displays the result of the inference and post-processing. Use the slider to navigate through the z-stack.\n", + "# @markdown *KNOWN ISSUE* : The colormap of the labels is not consistent between the z-stacks.\n", + "import colorsys\n", + "\n", + "import ipywidgets as widgets\n", + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from IPython.display import display as ipy_display\n", + "\n", + "rng = np.random.default_rng(seed=0) # for reproducibility\n", + "\n", + "\n", + "def random_label_cmap(n=2**16, h=(0, 1), l=(0.4, 1), s=(0.2, 0.8)):\n", + " \"\"\"FUNCTION TAKEN FROM STARDIST REPO : https://github.com/stardist/stardist/blob/c6c261081c6f9717fa9f5c47720ad2d5a9153224/stardist/plot/plot.py#L8\"\"\"\n", + " h, l, s = (\n", + " rng.uniform(*h, n),\n", + " rng.uniform(*l, n),\n", + " rng.uniform(*s, n),\n", + " )\n", + " cols = np.stack(\n", + " [colorsys.hls_to_rgb(_h, _l, _s) for _h, _l, _s in zip(h, l, s)], axis=0\n", + " )\n", + " cols[0] = 0\n", + " # reset the random generator to the first draw to keep the colormap consistent\n", + "\n", + " return matplotlib.colors.ListedColormap(cols)\n", + "\n", + "\n", + "label_cmap = random_label_cmap(n=instance_segmentation.max() + 1)\n", + "\n", + "\n", + "def update_plot(z):\n", + " plt.figure(figsize=(15, 15))\n", + " plt.subplot(1, 3, 1)\n", + " plt.imshow(demo_image[z], cmap=\"gray\")\n", + " plt.subplot(1, 3, 2)\n", + " plt.imshow(result[0].semantic_segmentation[z], cmap=\"turbo\")\n", + " plt.subplot(1, 3, 3)\n", + " plt.imshow(instance_segmentation[z], cmap=label_cmap)\n", + " plt.show()\n", + "\n", + "\n", + "# Create a slider\n", + "z_slider = widgets.IntSlider(\n", + " min=0, max=demo_image.shape[0] - 1, step=1, value=demo_image.shape[0] // 2\n", + ")\n", + "\n", + "# Display the slider and update the plot when the slider is changed\n", + "widgets.interact(update_plot, z=z_slider)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 424 }, + "id": "Tw5exJ5EAm-1", + "outputId": "3aa36115-0b22-495b-b7e9-3eba7c06069a" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "5tkEI1q-loqB", - "outputId": "d41875da-3879-4158-8a0f-6330afe442af" + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "summary": "{\n \"name\": \"data\",\n \"rows\": 322,\n \"fields\": [\n {\n \"column\": \"Volume\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 54.38970963263131,\n \"min\": 8.0,\n \"max\": 252.0,\n \"num_unique_values\": 157,\n \"samples\": [\n 14.0,\n 124.0,\n 169.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid x\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 31.084053076294715,\n \"min\": 5.405263157894737,\n \"max\": 122.52380952380952,\n \"num_unique_values\": 321,\n \"samples\": [\n 73.65806451612903,\n 60.0,\n 81.18303571428571\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid y\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 25.419664210044758,\n \"min\": 0.0,\n \"max\": 85.0,\n \"num_unique_values\": 320,\n \"samples\": [\n 0.6310679611650486,\n 1.7452229299363058,\n 13.709401709401709\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid z\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 27.685581861438635,\n \"min\": 0.12903225806451613,\n \"max\": 93.0,\n \"num_unique_values\": 320,\n \"samples\": [\n 12.174757281553399,\n 10.108695652173912,\n 70.51282051282051\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Sphericity (axes)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.12590741777097128,\n \"min\": 5.583882595237912e-06,\n \"max\": 0.9900749841550203,\n \"num_unique_values\": 318,\n \"samples\": [\n 0.8007911710122612,\n 0.8283576063212563,\n 0.7547372074750549\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Image size\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n [\n 124,\n 86,\n 94\n ]\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Total image volume\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.001002416\",\n \"max\": \"1970-01-01 00:00:00.001002416\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 1002416\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Total object volume (pixels)\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.000033504\",\n \"max\": \"1970-01-01 00:00:00.000033504\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 33504.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Filling ratio\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00\",\n \"max\": \"1970-01-01 00:00:00\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 0.03342324942937862\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Number objects\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.000000322\",\n \"max\": \"1970-01-01 00:00:00.000000322\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 322\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", + "type": "dataframe", + "variable_name": "data" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Selected model: SwinUNetR\n" - ] - } + "text/html": [ + "\n", + "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VolumeCentroid xCentroid yCentroid zSphericity (axes)Image sizeTotal image volumeTotal object volume (pixels)Filling ratioNumber objects
0190.05.40526369.15789536.2105260.778113(124, 86, 94)100241633504.00.033423322
118.05.83333385.00000083.9444440.000007
267.07.28358265.49253792.0597010.867751
3108.010.32407484.34259368.8611110.672490
435.09.42857184.31428692.6000000.649649
.................................
31711.0122.36363614.72727325.0000000.951651
31824.0122.16666726.08333338.0833330.990075
31916.0122.12500034.12500036.5000000.944672
32013.0122.07692343.53846253.6153850.939852
32121.0122.52381049.66666736.2380950.895437
\n", + "

322 rows × 10 columns

\n", + "
\n", + "
\n", + "\n", + "
\n", + " \n", + "\n", + " \n", + "\n", + " \n", + "
\n", + "\n", + "\n", + "
\n", + " \n", + "\n", + "\n", + "\n", + " \n", + "
\n", + "\n", + "
\n", + " \n", + " \n", + " \n", + "
\n", + "\n", + "
\n", + "
\n" ], - "source": [ - "model_selection = \"SwinUNetR\" #@param [\"SwinUNetR\", \"WNet3D\", \"SegResNet\"]\n", - "print(f\"Selected model: {model_selection}\")" + "text/plain": [ + " Volume Centroid x Centroid y Centroid z Sphericity (axes) \\\n", + "0 190.0 5.405263 69.157895 36.210526 0.778113 \n", + "1 18.0 5.833333 85.000000 83.944444 0.000007 \n", + "2 67.0 7.283582 65.492537 92.059701 0.867751 \n", + "3 108.0 10.324074 84.342593 68.861111 0.672490 \n", + "4 35.0 9.428571 84.314286 92.600000 0.649649 \n", + ".. ... ... ... ... ... \n", + "317 11.0 122.363636 14.727273 25.000000 0.951651 \n", + "318 24.0 122.166667 26.083333 38.083333 0.990075 \n", + "319 16.0 122.125000 34.125000 36.500000 0.944672 \n", + "320 13.0 122.076923 43.538462 53.615385 0.939852 \n", + "321 21.0 122.523810 49.666667 36.238095 0.895437 \n", + "\n", + " Image size Total image volume Total object volume (pixels) \\\n", + "0 (124, 86, 94) 1002416 33504.0 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + ".. ... ... ... \n", + "317 \n", + "318 \n", + "319 \n", + "320 \n", + "321 \n", + "\n", + " Filling ratio Number objects \n", + "0 0.033423 322 \n", + "1 \n", + "2 \n", + "3 \n", + "4 \n", + ".. ... ... \n", + "317 \n", + "318 \n", + "319 \n", + "320 \n", + "321 \n", + "\n", + "[322 rows x 10 columns]" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title Display the statistics\n", + "# @markdown This cell displays the statistics of the post-processed result.\n", + "import pandas as pd\n", + "\n", + "data = pd.DataFrame(stats.get_dict())\n", + "ipy_display(data)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 617 }, + "id": "0NhZ-YksAm-1", + "outputId": "15904f15-5b1c-4b04-8b09-265c42a20e3a" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "aPFS4WTdmPo3" - }, - "outputs": [], - "source": [ - "from napari_cellseg3d.config import ModelInfo\n", - "\n", - "model_info = ModelInfo(\n", - " name=model_selection,\n", - " model_input_size=64 if model_selection == \"SegResNet\" else [64,64,64],\n", - " num_classes=2,\n", - ")\n", - "inference_config.model_info = model_info" + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + "\n", + "" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# @title Plot the a 3D view, with statistics\n", + "# @markdown This cell plots a 3D view of the cells, with the volume as the size of the points and the sphericity as the color.\n", + "import plotly.graph_objects as go\n", + "\n", + "\n", + "def plotly_cells_stats(data):\n", + "\n", + " x = data[\"Centroid x\"]\n", + " y = data[\"Centroid y\"]\n", + " z = data[\"Centroid z\"]\n", + "\n", + " fig = go.Figure(\n", + " data=go.Scatter3d(\n", + " x=np.floor(x),\n", + " y=np.floor(y),\n", + " z=np.floor(z),\n", + " mode=\"markers\",\n", + " marker=dict(\n", + " sizemode=\"diameter\",\n", + " sizeref=30,\n", + " sizemin=20,\n", + " size=data[\"Volume\"],\n", + " color=data[\"Sphericity (axes)\"],\n", + " colorscale=\"Turbo_r\",\n", + " colorbar_title=\"Sphericity\",\n", + " line_color=\"rgb(140, 140, 170)\",\n", + " ),\n", + " )\n", + " )\n", + "\n", + " fig.update_layout(\n", + " height=600,\n", + " width=600,\n", + " title=f\"Total number of cells : {int(data['Number objects'][0])}\",\n", + " )\n", + "\n", + " fig.show()\n", + "\n", + "\n", + "plotly_cells_stats(data)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "include_colab_link": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "10441f745a6f41cf8655b2fafbb8204f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "hIEKoyEGAm-0", - "outputId": "2103baf6-8875-433b-8799-41e0d1f3c7f0" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--------------------\n", - "Parameters summary :\n", - "Model is : SwinUNetR\n", - "Window inference is enabled\n", - "Window size is 64\n", - "Window overlap is 0.25\n", - "Dataset loaded on cuda device\n", - "--------------------\n", - "MODEL DIMS : [64, 64, 64]\n", - "Model name : SwinUNetR\n", - "Instantiating model...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "monai.networks.nets.swin_unetr SwinUNETR.__init__:img_size: Argument `img_size` has been deprecated since version 1.3. It will be removed in version 1.5. The img_size argument is not required anymore and checks on the input size are run during forward().\n", - "INFO:napari_cellseg3d.utils:********************\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading weights...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:napari_cellseg3d.utils:Downloading the model from HuggingFace https://huggingface.co/C-Achard/cellseg3d/resolve/main/SwinUNetR_latest.tar.gz....\n", - "270729216B [00:10, 26012663.01B/s] \n", - "You are using `torch.load` with `weights_only=False` (the current default value), which uses the default pickle module implicitly. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling (See https://github.com/pytorch/pytorch/blob/main/SECURITY.md#untrusted-models for more details). In a future release, the default value for `weights_only` will be flipped to `True`. This limits the functions that could be executed during unpickling. Arbitrary objects will no longer be allowed to be loaded via this mode unless they are explicitly allowlisted by the user via `torch.serialization.add_safe_globals`. We recommend you start setting `weights_only=True` for any use case where you don't have full control of the loaded file. Please open an issue on GitHub for any issues related to this experimental feature.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Weights status : \n", - "Done\n", - "--------------------\n", - "Parameters summary :\n", - "Model is : SwinUNetR\n", - "Window inference is enabled\n", - "Window size is 64\n", - "Window overlap is 0.25\n", - "Dataset loaded on cuda device\n", - "--------------------\n", - "Loading layer\n", - "2024-12-22 18:58:42,183 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'QuantileNormalization', transform is not lazy\n", - "2024-12-22 18:58:42,279 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'ToTensor', transform is not lazy\n", - "2024-12-22 18:58:42,290 - INFO - Apply pending transforms - lazy: False, pending: 0, upcoming 'EnsureType', transform is not lazy\n", - "Done\n", - "----------\n", - "Inference started on layer...\n", - "Post-processing...\n", - "Layer prediction saved as : volume_SwinUNetR_pred_1_2024_12_22_18_58_48\n" - ] - } - ], - "source": [ - "result = cs3d.inference_on_images(\n", - " demo_image,\n", - " config=inference_config,\n", - ")" - ] + "1e79fde882a44cd984a54a71c3337759": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "IFbmZ3_zAm-1", - "outputId": "bde6a6c5-f47f-4164-9e1c-3bf5a94dd00d" - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "1it [00:00, 9.61it/s]\n", - "clesperanto's cupy / CUDA backend is experimental. Please use it with care. The following functions are known to cause issues in the CUDA backend:\n", - "affine_transform, apply_vector_field, create(uint64), create(int32), create(int64), resample, scale, spots_to_pointlist\n", - "divide by zero encountered in scalar divide\n", - "invalid value encountered in scalar multiply\n", - "WARNING:napari_cellseg3d.utils:0 invalid sphericities were set to NaN. This occurs for objects with a volume of 1 pixel.\n" - ] - } - ], - "source": [ - "# @title Post-process the result\n", - "# @markdown This cell post-processes the result of the inference : thresholding, instance segmentation, and statistics.\n", - "\n", - "if model_selection == \"WNet3D\":\n", - " result[0].semantic_segmentation = result[0].semantic_segmentation[1]\n", - "\n", - "instance_segmentation,stats = cs3d.post_processing(\n", - " result[0].semantic_segmentation,\n", - " config=post_process_config,\n", - ")" - ] + "2692114df7304a1cbc703e8bd0f848c3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "IntSliderModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "IntSliderModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "IntSliderView", + "continuous_update": true, + "description": "z", + "description_tooltip": null, + "disabled": false, + "layout": "IPY_MODEL_9cb58613b1a74eaeb285f6d2d77d567b", + "max": 123, + "min": 0, + "orientation": "horizontal", + "readout": true, + "readout_format": "d", + "step": 1, + "style": "IPY_MODEL_a1d487697e4b4ea6b897f380c2b112cc", + "value": 62 + } }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 496, - "referenced_widgets": [ - "7a72ee57e14c440bb2ce281da67e1311", - "2692114df7304a1cbc703e8bd0f848c3", - "697f7288fff64aefbee1a5c0d4894987", - "1e79fde882a44cd984a54a71c3337759", - "9cb58613b1a74eaeb285f6d2d77d567b", - "a1d487697e4b4ea6b897f380c2b112cc", - "10441f745a6f41cf8655b2fafbb8204f" - ] - }, - "id": "TMRiQ-m4Am-1", - "outputId": "2d819126-5478-4d98-a5e2-ecacb7872465" - }, + "697f7288fff64aefbee1a5c0d4894987": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_10441f745a6f41cf8655b2fafbb8204f", + "msg_id": "", "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7a72ee57e14c440bb2ce281da67e1311", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "interactive(children=(IntSlider(value=62, description='z', max=123), Output()), _dom_classes=('widget-interact…" - ] - }, - "metadata": {}, - "output_type": "display_data" + { + "data": { + "image/png": "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\n", + "text/plain": "
" }, - { - "data": { - "text/html": [ - "
\n", - "
update_plot
def update_plot(z)
/content/<ipython-input-9-245acde924e0><no docstring>
" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# @title Display the result\n", - "#@markdown This cell displays the result of the inference and post-processing. Use the slider to navigate through the z-stack.\n", - "# @markdown *KNOWN ISSUE* : The colormap of the labels is not consistent between the z-stacks.\n", - "import matplotlib.pyplot as plt\n", - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "import matplotlib\n", - "import colorsys\n", - "import numpy as np\n", - "\n", - "def random_label_cmap(n=2**16, h = (0,1), l = (.4,1), s =(.2,.8)):\n", - " \"\"\"FUNCTION TAKEN FROM STARDIST REPO : https://github.com/stardist/stardist/blob/c6c261081c6f9717fa9f5c47720ad2d5a9153224/stardist/plot/plot.py#L8\"\"\"\n", - " h,l,s = np.random.uniform(*h,n), np.random.uniform(*l,n), np.random.uniform(*s,n)\n", - " cols = np.stack([colorsys.hls_to_rgb(_h,_l,_s) for _h,_l,_s in zip(h,l,s)],axis=0)\n", - " cols[0] = 0\n", - " # reset the random generator to the first draw to keep the colormap consistent\n", - "\n", - " return matplotlib.colors.ListedColormap(cols)\n", - "\n", - "label_cmap = random_label_cmap(n=instance_segmentation.max()+1)\n", - "\n", - "def update_plot(z):\n", - " plt.figure(figsize=(15, 15))\n", - " plt.subplot(1, 3, 1)\n", - " plt.imshow(demo_image[z], cmap='gray')\n", - " plt.subplot(1, 3, 2)\n", - " plt.imshow(result[0].semantic_segmentation[z], cmap='turbo')\n", - " plt.subplot(1, 3, 3)\n", - " plt.imshow(instance_segmentation[z], cmap=label_cmap)\n", - " plt.show()\n", - "\n", - "# Create a slider\n", - "z_slider = widgets.IntSlider(min=0, max=demo_image.shape[0]-1, step=1, value=demo_image.shape[0] // 2)\n", - "\n", - "# Display the slider and update the plot when the slider is changed\n", - "widgets.interact(update_plot, z=z_slider)" + "metadata": {}, + "output_type": "display_data" + } ] + } }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 424 - }, - "id": "Tw5exJ5EAm-1", - "outputId": "3aa36115-0b22-495b-b7e9-3eba7c06069a" - }, - "outputs": [ - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "summary": "{\n \"name\": \"data\",\n \"rows\": 322,\n \"fields\": [\n {\n \"column\": \"Volume\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 54.38970963263131,\n \"min\": 8.0,\n \"max\": 252.0,\n \"num_unique_values\": 157,\n \"samples\": [\n 14.0,\n 124.0,\n 169.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid x\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 31.084053076294715,\n \"min\": 5.405263157894737,\n \"max\": 122.52380952380952,\n \"num_unique_values\": 321,\n \"samples\": [\n 73.65806451612903,\n 60.0,\n 81.18303571428571\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid y\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 25.419664210044758,\n \"min\": 0.0,\n \"max\": 85.0,\n \"num_unique_values\": 320,\n \"samples\": [\n 0.6310679611650486,\n 1.7452229299363058,\n 13.709401709401709\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Centroid z\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 27.685581861438635,\n \"min\": 0.12903225806451613,\n \"max\": 93.0,\n \"num_unique_values\": 320,\n \"samples\": [\n 12.174757281553399,\n 10.108695652173912,\n 70.51282051282051\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Sphericity (axes)\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.12590741777097128,\n \"min\": 5.583882595237912e-06,\n \"max\": 0.9900749841550203,\n \"num_unique_values\": 318,\n \"samples\": [\n 0.8007911710122612,\n 0.8283576063212563,\n 0.7547372074750549\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Image size\",\n \"properties\": {\n \"dtype\": \"category\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n [\n 124,\n 86,\n 94\n ]\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Total image volume\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.001002416\",\n \"max\": \"1970-01-01 00:00:00.001002416\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 1002416\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Total object volume (pixels)\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.000033504\",\n \"max\": \"1970-01-01 00:00:00.000033504\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 33504.0\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Filling ratio\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00\",\n \"max\": \"1970-01-01 00:00:00\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 0.03342324942937862\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"Number objects\",\n \"properties\": {\n \"dtype\": \"date\",\n \"min\": \"1970-01-01 00:00:00.000000322\",\n \"max\": \"1970-01-01 00:00:00.000000322\",\n \"num_unique_values\": 2,\n \"samples\": [\n \"\",\n 322\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}", - "type": "dataframe", - "variable_name": "data" - }, - "text/html": [ - "\n", - "
\n", - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
VolumeCentroid xCentroid yCentroid zSphericity (axes)Image sizeTotal image volumeTotal object volume (pixels)Filling ratioNumber objects
0190.05.40526369.15789536.2105260.778113(124, 86, 94)100241633504.00.033423322
118.05.83333385.00000083.9444440.000007
267.07.28358265.49253792.0597010.867751
3108.010.32407484.34259368.8611110.672490
435.09.42857184.31428692.6000000.649649
.................................
31711.0122.36363614.72727325.0000000.951651
31824.0122.16666726.08333338.0833330.990075
31916.0122.12500034.12500036.5000000.944672
32013.0122.07692343.53846253.6153850.939852
32121.0122.52381049.66666736.2380950.895437
\n", - "

322 rows × 10 columns

\n", - "
\n", - "
\n", - "\n", - "
\n", - " \n", - "\n", - " \n", - "\n", - " \n", - "
\n", - "\n", - "\n", - "
\n", - " \n", - "\n", - "\n", - "\n", - " \n", - "
\n", - "\n", - "
\n", - " \n", - " \n", - " \n", - "
\n", - "\n", - "
\n", - "
\n" - ], - "text/plain": [ - " Volume Centroid x Centroid y Centroid z Sphericity (axes) \\\n", - "0 190.0 5.405263 69.157895 36.210526 0.778113 \n", - "1 18.0 5.833333 85.000000 83.944444 0.000007 \n", - "2 67.0 7.283582 65.492537 92.059701 0.867751 \n", - "3 108.0 10.324074 84.342593 68.861111 0.672490 \n", - "4 35.0 9.428571 84.314286 92.600000 0.649649 \n", - ".. ... ... ... ... ... \n", - "317 11.0 122.363636 14.727273 25.000000 0.951651 \n", - "318 24.0 122.166667 26.083333 38.083333 0.990075 \n", - "319 16.0 122.125000 34.125000 36.500000 0.944672 \n", - "320 13.0 122.076923 43.538462 53.615385 0.939852 \n", - "321 21.0 122.523810 49.666667 36.238095 0.895437 \n", - "\n", - " Image size Total image volume Total object volume (pixels) \\\n", - "0 (124, 86, 94) 1002416 33504.0 \n", - "1 \n", - "2 \n", - "3 \n", - "4 \n", - ".. ... ... ... \n", - "317 \n", - "318 \n", - "319 \n", - "320 \n", - "321 \n", - "\n", - " Filling ratio Number objects \n", - "0 0.033423 322 \n", - "1 \n", - "2 \n", - "3 \n", - "4 \n", - ".. ... ... \n", - "317 \n", - "318 \n", - "319 \n", - "320 \n", - "321 \n", - "\n", - "[322 rows x 10 columns]" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "7a72ee57e14c440bb2ce281da67e1311": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "VBoxModel", + "state": { + "_dom_classes": [ + "widget-interact" ], - "source": [ - "# @title Display the statistics\n", - "# @markdown This cell displays the statistics of the post-processed result.\n", - "import pandas as pd\n", - "data = pd.DataFrame(stats.get_dict())\n", - "display(data)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 617 - }, - "id": "0NhZ-YksAm-1", - "outputId": "15904f15-5b1c-4b04-8b09-265c42a20e3a" - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "
\n", - "
\n", - "\n", - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "VBoxModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "1.5.0", + "_view_name": "VBoxView", + "box_style": "", + "children": [ + "IPY_MODEL_2692114df7304a1cbc703e8bd0f848c3", + "IPY_MODEL_697f7288fff64aefbee1a5c0d4894987" ], - "source": [ - "# @title Plot the a 3D view, with statistics\n", - "# @markdown This cell plots a 3D view of the cells, with the volume as the size of the points and the sphericity as the color.\n", - "import plotly.graph_objects as go\n", - "import numpy as np\n", - "\n", - "def plotly_cells_stats(data):\n", - "\n", - " x = data[\"Centroid x\"]\n", - " y = data[\"Centroid y\"]\n", - " z = data[\"Centroid z\"]\n", - "\n", - " fig = go.Figure(\n", - " data=go.Scatter3d(\n", - " x=np.floor(x),\n", - " y=np.floor(y),\n", - " z=np.floor(z),\n", - " mode=\"markers\",\n", - " marker=dict(\n", - " sizemode=\"diameter\",\n", - " sizeref=30,\n", - " sizemin=20,\n", - " size=data[\"Volume\"],\n", - " color=data[\"Sphericity (axes)\"],\n", - " colorscale=\"Turbo_r\",\n", - " colorbar_title=\"Sphericity\",\n", - " line_color=\"rgb(140, 140, 170)\",\n", - " ),\n", - " )\n", - " )\n", - "\n", - " fig.update_layout(\n", - " height=600,\n", - " width=600,\n", - " title=f'Total number of cells : {int(data[\"Number objects\"][0])}',\n", - " )\n", - "\n", - " fig.show()\n", - "\n", - "plotly_cells_stats(data)" - ] - } - ], - "metadata": { - "accelerator": "GPU", - "colab": { - "gpuType": "T4", - "include_colab_link": true, - "provenance": [] - }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "layout": "IPY_MODEL_1e79fde882a44cd984a54a71c3337759" + } }, - "language_info": { - "name": "python" + "9cb58613b1a74eaeb285f6d2d77d567b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "10441f745a6f41cf8655b2fafbb8204f": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "1e79fde882a44cd984a54a71c3337759": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2692114df7304a1cbc703e8bd0f848c3": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "IntSliderModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "IntSliderModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "IntSliderView", - "continuous_update": true, - "description": "z", - "description_tooltip": null, - "disabled": false, - "layout": "IPY_MODEL_9cb58613b1a74eaeb285f6d2d77d567b", - "max": 123, - "min": 0, - "orientation": "horizontal", - "readout": true, - "readout_format": "d", - "step": 1, - "style": "IPY_MODEL_a1d487697e4b4ea6b897f380c2b112cc", - "value": 62 - } - }, - "697f7288fff64aefbee1a5c0d4894987": { - "model_module": "@jupyter-widgets/output", - "model_module_version": "1.0.0", - "model_name": "OutputModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/output", - "_model_module_version": "1.0.0", - "_model_name": "OutputModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/output", - "_view_module_version": "1.0.0", - "_view_name": "OutputView", - "layout": "IPY_MODEL_10441f745a6f41cf8655b2fafbb8204f", - "msg_id": "", - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABLkAAAFoCAYAAAC7YSngAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAADbmUlEQVR4nOy9eZikV132/629urq6a7pnn2QmTDYmQBYIMRkIiBCJyOsLEhB5UVDy6gsGlAS3qCAoEsUFRAMoPwyyySKyKiAGCVsSQgAJhMlGwiSZfaa36upauqp+fwx5nvt7Vz+npifTk66e+3Ndc13PmXOec75n76mp++5Ut9vtmhBCCCGEEEIIIYQQA0z6kQ5ACCGEEEIIIYQQQoiHiz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADz5J9yHXttdfaox71KCsWi3bhhRfaN77xjaVqSgghxAmI7hkhhBBLie4ZIYQYPFLdbrd7rCv98Ic/bC95yUvsne98p1144YX21re+1T760Y/aHXfcYevWrQu+2+l0bNeuXTYyMmKpVOpYhyaEECcc3W7XZmZmbNOmTZZOr4wv8D6ce8ZMd40QQhxrVtpdo3tGCCGWF0d8z3SXgJ/4iZ/oXnHFFVG63W53N23a1L3mmmv6vnv//fd3zUx/9Ed/9Ed/jvGf+++/fymO/EeEh3PPdLu6a/RHf/RHf5bqz0q5a3TP6I/+6I/+LM8//e6ZrB1jms2m3XrrrXb11VdHf5dOp+2SSy6xG2+8sad8o9GwRqMRpbs//mJZpVKJ/tej0+n05D8E/89IJpNJjA3fxToXqif0ySC/W6lUouds1g/p9PS0SzebzcR6Qm0w2A6PCaexL9xPLou02+3EeszCY43xz8/PH3GbrVYr2GY+n0+MD9cRv5vL5RLjwzrNevuFZXH+FiK0VpmhoaHEsnNzc4nxcF+wn6VSKTEeM78e+/WlWCxGzzyHOPaFQiEYH84p18NgvaF1bObHj/Pq9bpL437hPYrwePHYh+Z0MfuZ1xyCc89lefy4nzgXGGu327VGo2EjIyOJ7Q4Si71nzJLvmov+bKdli6NmZjZfSJ7fbsbnZevxmmvnfF6mFZ+z3eRj0szMUu3kvPmiXzeN4XjPzo7WXN7oIT+3qXYcX7bh9wf2Bfth1tuXUF6+xvXGz520Lxt6l8eonUveS6m2v8NC8XYpBhwHfo/LpjpxO2kKp0M/HuC7+B7nZeYp9iytm/kj/6YHvstthuB+tnPxAkxTPdkG3YXQ706uTXm8B+J3uxn6eavtBxDjD80D580XWlT2yL851M4lbzyM/XC7cfydTPheR3INf9ekod7ZsVkfT9af7aG+ZFtZSsf18nvcF9cmjQGWbZb8zwcd2gTZhHrbczN22/87Y0XcNcfynhFCCHFs6XfPHPMPuQ4cOGDtdtvWr1/v/n79+vW2Y8eOnvLXXHONveENb+j5+1QqFX0gE/qKL+cd6deB+72H/2ju98EaluV/bHMa311MvxabfzRt9mtjMWN9PNo8VmUfTj3Mw+nrsYivnzzgaPuyHMbveMRwtOfJsaz3WPVzsTEOEou9Z8yS75pscdSyQ4c/5LLFfMiVgv884A+5ssfmQy6jD7nm4R+emWFfcaY+6tJp/JArHfiQK0V3VOBDI87Ldo/8Q67QuzxGqUV8yBWKt2fO0slzxmWxnb4fcsG7HB/m4YefC8XA+SHwXW4zBPfTQh9ypZM/5Grn/cJN8QeK8CFIhz7kSvOHXG3cL8nzwHndh/Ehlx3lh1ypRXzIlcn4H7EzzbjeTIkWfY4+5Gon9yVDH3JlAh9yJX0YdbhNPwZYtk0fcqVoE4Q+PDNbGXfNsbxnhBBCHFv63TOPuGD+6quvtqmpqejP/fff/0iHJIQQYoWhu0YIIcRSontGCCGWB8f8m1xr1qyxTCZje/fudX+/d+9e27BhQ0/5QqHQI3cy89+eQokOy3X4UzyUWLEkCCVDLB/ib8CgpInLstwJv5pcq3kJCYN95b6gfIyleNwm1sOSqpA8kMeL68V3Z2f91+lD32jjvJDEj8ca+9rvU9lQWZ7vUF04Zv3WFOaHpIyhNsx6+40yPh4/lOL1k9Dh+FarVZcXmrPQmud2WQqK64/HiyWnobEOjV9oTXE+rzGOKbRusF4eAzaWxfHleLjfLBdMyuO6QvHxGg/Je0P7c9BZ7D1jlnzXzBe60Te4SvfEZ17z5LIr1yiT3G4q8G2jRjwv7TLJi1uhbx7R+Vzz6aEDcfyjhaLLS1E42A594crmS/DtY7+EeqRvuSnYOy1/L3XTtFZLIKOnvmTq1O9OXNd8mb+VRt+Ihr40Ki7LhibSkEeyuBZLt5LvhDRJOvGbaPxezzfPQt/GA1giWSS5J8plsw0aW7+MLEPzFoon9A27wkycyWshWyVJYgG/fecDatPWwjbbufC9noF5Yqkq5nG/eF6wbKvg6+Fvnlngm0i5ur8HcrA2miX6Bht9iw6/8BSSAq+518ssOi/7K5fe8cBToud1dz3Gl6VvxhUn4p8Xciwhpr3fLIGlQoFlpHG/hw/586U57O9u/jZeVEcjt+DfDyLH8p4RQohHmu/c/wOXPm/zWY9QJMeHY/5Nrnw+b+eff75df/310d91Oh27/vrrbfv27ce6OSGEECcYumeEEEIsJbpnhBBicDnm3+QyM7vqqqvspS99qT3xiU+0n/iJn7C3vvWtNjs7a7/6q7+6FM0JIYQ4wdA9I4QQYinRPSOEEIPJknzI9cIXvtD2799vr3vd62zPnj123nnn2ec+97ke88YQnU5nQeP5xfymudBvj+v3W95Q3hNqw8z/NrR+v4EQ81mSGJJNhaRkTMiMmt9jGRqOy2IkThwfjn1INmoWNvnn8Qv9RkeeU6yX20RZaz85ILIYaR7PL4Px8Rjh+HE83CamQ/GY+XnCMeB4zHz8vAfw6/ihdcwx9PutoaE54zHCdkNySobrxbFGmahZb783btwYPd97770uL/TbIHmMQuuc5yEk/Q399seVYPwb4ljcM2aHzbYfMtxubYwlio0yy6b8eNbWJxt/Z1rxWp1b5fcky8Xcb0ajI6O8OznubJXkWGRknWrH7cx61a2Tus2O+/jmyv63e65+MNYH5qdobZIMDaVRzRGS865P/m18WZLJsRwrV4dzlSR/zeG4LP/GSf4tdEh6ESblaZpf/g17mM95+Fvp2AScy+Jv45sdD/+MktTGgvkwnvm6P9PQqJyN5zvr+WcA+O231GZoTJpF3gP+3aEq/KZhypsHc3ReF62CPw8LNfiNwGTkzvONcjsu2yrQb0XsJJfl8cS+sqSvMBefC81hl2V/X/+wS9976sej53f//cdc3uSTV/t4i+0Fn83M2lmSV0JfeF4wb3qNPweYfCPuC851ux3Q0Q4gx+qeEUKI5QbKF1eidHFJPuQyM3vlK19pr3zlK5eqeiGEECc4umeEEEIsJbpnhBBi8HjEf7uiEEIIIYQQQgghhBAPF33IJYQQQgghhBBCCCEGniWTKz5cUqlU5CmDvjPsZcNeN+gzxGXr9Xr0zF477GWE7/bzBkLYC4h9cWq1WmIexhRqg2EPH+ynmfeq6ucpFfJE4jHDejmG0HscA441ew6F/J24TfZTCnk2YR77GnHZUOycDnlOsW/ZkXrNzc7OHnE9HE/I36nfWsD80H4J+c6Z+fnt56mHa7dSqbg83Dtmfp64Ht772BcePywbOjPMzKrVavQc8pbjNvv5s2FdIV8/bJ/bMPPnRsjrTixMppE8TuxXlAKPrgwdGR1YjvlZWm/kXZRpgUdOya+TZoU81yCZn/L11Nb4d/M1uDczPi9Ti9vM1X185QN+32Xq4D1WJ58o9qtsxWUL5N+Vrft0YyR5TwxN0l1ThHOL/LFy4HM1bCWXl2km+ysy7TyNEbzLllfsC4YxtYp+jAqt5Lsc/aYY9OdaCPQ0yxitE/bLAq+lfC05Hh6DQs3HgP1OtcPx4TrPNej+KPi7Br3K5n1RS3e6C5Yz6/W84nxkqOo9KOfK8dk+emDE5TVL9HMI9KVQ8z/j8VhjPp8ZSHHGd/R9q8516X/b87zo+bFrx3085OuHMaTIe4y90gzOAvasw3rQc2shkjzX2p2V5cklhBArlZXow4Xom1xCCCGEEEIIIYQQYuDRh1xCCCGEEEIIIYQQYuDRh1xCCCGEEEIIIYQQYuBZtp5cnU4n8pth3xkEPbjMvG8Pe9ugXxbnsQ8T+uCwVxGD8bGHD3vhoPcO+25hHr/HXl9Ytp/PUajfExMTlkTIZ8ssPC4YP48tey0dOnQoeubYuW/Y77m5OZfHY4QxcB6+Ozo66vLK5bJLow8SzwvHgGuB55fHC/vKaxz7zWPC/k4hn7LQ3mFCXlqhPI4ntK55Hnh+EY69VPJ+O9gunwOhMeI2MSZ+b9++fYn18loI+ehxHvct5BGH4zcy4r1jQusGPfNEMp10N/K4aRfi8exmkv31zLw/UTdDfl0BCzb2czJL9pti3yr0cGoV/PWNHlwcQ2EmeZ/la+QBV0/2JZuvkL8iDVFtTfxuccr3JVvz9c6OozeQj69Z8hWzV5QvnNy3TCuVmOY2GPSUauX7rIVCHB/7T+GcsUcTe1OVpoagrG+jVvH3aAcKpNmHiV7G8Q15j/XzMMP1yWOLvmlMyHuM603TXjKInccrRGHOn7kcA45ZdZx8L1tH/qMxe2Ah7Wzy/uWxvuMP/8alH1OO18L0ab6Nds57hiG5ed53yePAvmD1kXh8GyN+TDI0JsM1fx9HsdVzC/69EEKIR5aV7sHF6JtcQgghhBBCCCGEEGLg0YdcQgghhBBCCCGEEGLgWbZyxVQqFUlxUHaD0h0zs2Yz+WvbLOWZnp6OnlnexJIwfJdlPxxDNhsPI8u6OI1yIpZuodSSpW0s1RoaGkosG5K3cV9Ymod1heLjulhihePH88CSzvHx+Ndjz86SbCDrlyi2wzJIlpqF5gUlijwmPJ5YD8sTuV4sy/WwvA3b5X7iGHG/QmssFDunuR7eSyFpXkjKGJJI8ljzHIYkfwxK93hPhvYs9yXUT17XPJ5ISKbZb4+GpMEYXz9pKNaDbfD4iJh0JxXJyLow3Swd7JEgwpRl6pZIusX7IXmuM9RGturLtkpxXSxfy9eGXBqlhI2yn/+hibjezAxJwDIkTc7F6WzN18MyzeJUXDbdovOuRBLdgIzPSDWXbh+b/4+bWxX3letsFejMAMkfS98YlCh2AjJXlq+xzBBplHw86TZLL+M2WcbH8s/MPEjaqd8oy+xd8z6dNpCp98huPSEJIk8nqit5m3Uhk/vFEkTsJ8PvNkrx/inUyM6g7O/5ViG+G7Pz/g4oTQ27NK7lTs9eB7lx0c/vzmf4ORyeitdcruHrKU35eEPwEiuA7BDliWZmhVqcN706LHfHsa/DWLatsVBxIYQQ4riib3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFn2XpyZbPZyNsHvYL6eeSgHxD766B/DXvgsE8UlmUfHPbHwnfZIyfkrcUeSBg7txHy1OExYZ8j9OZhL7KZmRmXxr5y7NwOcqx8mULeaAz3JeSJxPVgWe5XyH+KYd8yrIvXJs9hyLcMx4HHhOcF11GoDY6P80Jprhfnm8eH48Oy/drE+NA3zax3ftG/rVqturxQTDyeIb8uBtcu95PHCPvC9fLeR489XlO4P7hNLovthNa/iEm3zNI/3qqdXHK5FB3BuVrymM6ujecpPxu+ZjPg2cU+R61S8rmfa/h60W/KzGxkV5yfp/O5C8nmGt/pDFnqpNpxTN00eTTVyLenENdVH+e95NMjB+I13yz5O5a9oFwtGV9PyM9pvpg8fuydxZ5X6XbyfZdp0p0BMXFeayQe0PkceWIe8n5OIdD3y4w8rzoce/L/X/LYuthbyXlmfn2yHxZ6Z5l5fyz2IutQva0iz1xyWZ/n28R2eLzmyv5nPPTZ2n/aHS6vvPZuX++OJ0fP3Yyvt1Hy+3Co6n8uQdAXjH3Ucg1/vzWLcT6PQYb8xdBLi88Qnu/pNfG9ue+M213e2M7To+dV+1f5NmldT6+Jf3ZslmIPs3Y3YFAohBBCHCf0TS4hhBBCCCGEEEIIMfDoQy4hhBBCCCGEEEIIMfAsW7liu92OJDYo+2GpUUiWNDzspQBYD8vrWFqGsiSWs4WkeSzFC0nouE2sl6VPIRkV18NjhLKvyclJl8cyJoyB+8L9LpfLiXkheSXnYTso2zLrlYri/HK/OY0yL24T8/g9nm98l8c2BNcbWqs81qH3Qu2wfC0kmeSx5TnEtcvxheaX4w1J5Xissc1169Yl5pmZ7d+/PzEejhf7yv3GGPrJPXGM+kmTj1YuyPOA0uVarebyWPaYtH9D83Wi003H8r0MKG1ahYXLPwRKG1nKiHKx4oTPmy/5tVCvxOsxX6Ozh2MF2VKuTtL9ul836RasP5KzYbwcO8oTD7cZv5upU+EM3YWQXZjyefNege+rIZlcSHLF/UQ6aZIykgoO5WzDEywrI1l4IXDG9cjHQHpO8rDSVLyQ2jl/ZqQD25IliCEJJ/e7MeQlYyjrYzkdrlXuF4MxtMr+vGsV/VlUmop//kp1aG3Sysb4WfYYItsiuwpoh8eL49t8c/xz0b895gqXt/5+X+85W+OyhdsvdHmd9JFLJuchme6Ex7owB5YKJBVs5/3C5v0SAtdGobLH5c0+bl9c5x0XuLzVD651aRz7OozBYuZPCCFOFL5800ww/6kXjRynSE4c9E0uIYQQQgghhBBCCDHw6EMuIYQQQgghhBBCCDHw6EMuIYQQQgghhBBCCDHwLFtPLvT1QV8h9tNhrxv0nmGPnMV4cmE97HMUqpfjQT8dLsv1IP08fDA+9h/ieOv12IOhn6cU1sUxcBrr6udlhBSL3hMEx6Sf9xPWy22wP1HIkwjbDPmmmfm1yOPHZTHNayyU5nWN9fTzUwp5cvFY4xjNzs66PO4bjhGuIY6JY2cwv9++q1Qq0fO2bdsS88zMvve97yXGF/LNC8W7mPOF/cR4/HD9sU9ZyD+OwXe5HMeAZXkMxMK0c11L/djTBr2rilPJflNmYY8p9NZKtens8UvVsgU4B3LhvY4+Q+zZlKsm+/KkwlvU0S76ejP1uN5mhXz7phbhn+Tt5JynWYZ8ttpkl5WGq7JVIg+kXLKfU2HGn+1Dh2Be+sxnvhbH1Bihc6Gd7CGWom2HPlfsPZZp0Rpr43PYp6xZis/yfM33k2mUwJ8yzesEByJ8ZqC3V63i/UXyNe+n2SrEk8beVJl58pgCH6v5XPJizbb8e1wP+ovVR/xZObbb3x87X/Ev0fMpf7jT5U39qOrS9/7mn0bPF9a/6PKaBf9zx8xYPC6r9q9yeSG/Ku6bmyfy4GI/vkYpvmv4XGgX/Lvje8aj551rTnN5hZl4jIpV72k7V/YHF8ZbmIvnvj2X/HOtEEKcSPTz4UoqK3+uY4P+FSSEEEIIIYQQQgghBh59yCWEEEIIIYQQQgghBh59yCWEEEIIIYQQQgghBp5l68mVzWYX9HXq51WFsH8NvsveRextg2n26WH/n1B8c3NzLo3tsk8Pvst+OlwW/ZTYU4r7gvWyvxPHh/nsG8R9w/HlMRoaij0aOL6Q/1RoHsy8p1Q/LyiEY0BqNW8WEyrLnmEMjhl7SIXa5XpxjfHccxrhMWHft5BPFHuaYZrXI84L1xPyKeu3VsvlcvR8zjnnuLyzzz7bpUdHR6PngwcPurypqSmXxnlZjH/cYvy7QmV5/DgGnCdeCyGfLZ4zLIt7sJ+v24lMppWyTPbwGkBPpK5fms5Dysx75mR6/JPAM7FEc99jixjX06H/dgpY+Fhuxu+79CHvsddeE3vqcF9CpMlSp5sBH8QeHys6FxrQlxx7e3Fn4ndTbT7T/LsYP3tVFcDPqzBDPnnkUza3Bryz2tyGL5uB+NCL6seZDlwLhRb9WAUeSezBVR/xg41eS1w23SC/zwPxzxIPvPSDLq/V8efs+s9eFj1zP9EDK9OkdZxPPtPKE+H7Db2q2G8q3fKbAL20GBfffHgh98wTMHTA9+2TtbdFz3u+N+nyvvJ9f64+K3t79Pz6j/6Uy/vTT33Zpdu5+PxuDPl60rDB0+0j/1mWfdTqZV9vCurlsqkO+77F7W6863SXNzcS/zw4tXa/y1t7/yaXLlbjNdaBNdWe87EJIVYG03fdFswfPePsYL44ctjLSx5dR8eiv8n15S9/2X7u537ONm3aZKlUyj7xiU+4/G63a6973ets48aNNjQ0ZJdccondddddxypeIYQQKxzdM0IIIZYS3TNCCLFyWfSHXLOzs3buuefatddeu2D+m9/8Znvb295m73znO+3mm2+24eFhu/TSS4PffhJCCCEeQveMEEKIpUT3jBBCrFwWLVd81rOeZc961rMWzOt2u/bWt77V/uiP/sie85znmJnZe9/7Xlu/fr194hOfsF/8xV884nZGRkYiaU61Gv86Z5YgstwJ5TwsQ8Ky/eQ7WA+XDcnHQlI3My9p6idhCoFlWbLEEjWEy/IYsYwzVBYlYCyxwn6G4uGyPCahGFhOyWC7LCXL5WK5BPern+QvBM4/rwWeX5wLXtehtRCqNzQPXLaf3DM0vqHxC8XOZbkv+MPjjh07XB6vTZTshuSUTCiP2+B5wTS3yX3BukISRK4rJEnkejhdKpUWfC8kcV2OHK97xsxsZt2cZUqH1/PYzljiydK8Zjl5DFkOiFIzlieyHDADMrR2gWWPtE46KB2keFie34nliina6hgTt5ElqSDHm1QPl023SBZH0kbM57KcRsnkPF1RHL+rp8CSMJBVBd4zM8tBX1BGaNYrJWyW4gEO1dsh/SlLy1rFuB6WU+Lcm5nNl+L0kyo3u7w761tdenhHLI2f2VZyeWkY2x7JZtP3G+WLLLfr0LvFGpx/uXawbKEWn50sr0vzOsd4sr7ekJxx19mTLn3V2mdHz/920W6X94TqXpce3RpLRfbl/PiN7PXn/lw5+d4MSRR5jLDfvG566oWyuYZvn6WgPMfI0Ex8/s1n/d0yW/FyaC+R7Cz4vNw5nveMEEKI48sxNZ6/9957bc+ePXbJJZdEf1epVOzCCy+0G2+88Vg2JYQQ4gRE94wQQoilRPeMEEIMNsfUeH7Pnj1mZrZ+/Xr39+vXr4/ymEaj4b4dMT09fSxDEkIIsYI4mnvGTHeNEEKII0P3jBBCDDbH9JtcR8M111xjlUol+rN58+ZHOiQhhBArDN01QgghlhLdM0IIsTw4pt/k2rBhg5mZ7d271zZu3Bj9/d69e+28885b8J2rr77arrrqqig9PT1tmzdvtuHh4cg7p1aL/ST6eV4hXBZ9adibKuSPxR5D7OkT8i5iDyJMo6+RmffXYb8kbhPr4fi4L+zbE8rDd/v5E+F4sucP+lgtxmuMYw95dPXzGcJ89tXCNM8Dz2dorDmN48n/g8f14pzy/OL6DM29me8nr5vQOgqtTbPwvsN6eGy5zcXsWRyzr3zlKy7v7rvvTiyLvn1mYY8ujgfXNfcltNdD88Dv8phwfCF/tKGh2CeF9yDvX/Q0w/YHzZMrxNHcM2bJd82ec26x9Mhh/6o137848f1MwZ8T6L0U8q1i2L/rSPOYdM032j243+c34/z6ef7bCLkaeFwVeW3Quoalyv1Mtf276GPG/mLcty56TJEHV7vo9we2w55C6F1VH/EBzhcD3nwtOv8CfklMO0f3QCZOs48VxoueVma9PlvsnxRqM5WO3/3STb9DZf04bD4z9pEKeTL1A/vG/axV5ly6UCvGsZLPVrbl38Xx47yjjY/9upjWW/4wej7rub/q8r5++Qtd+p8eXY6e/+rr73Z5a9f6sW6W4nFIV4ddngX6hmNgZpZrxOdNxmhNsbcbzCmvIa4XfeDqJX+fDFXjuyY7H/7nwVw57mc7Bz+7dhsLFR84jvU9I8SgM3rG2Y90CAPHUy8aScz78k0zR/WeOHKO6Te5tm7dahs2bLDrr78++rvp6Wm7+eabbfv27Qu+UygUbHR01P0RQgghFuJo7hkz3TVCCCGODN0zQggx2Cz6m1zVatV9o+Lee++173znOzY+Pm5btmyxV7/61fbGN77RzjjjDNu6dau99rWvtU2bNtlzn/vcYxm3EEKIFYruGSGEEEuJ7hkhhFi5LPpDrm9+85v2Uz/1U1H6oa/lvvSlL7X3vOc99ru/+7s2Oztrv/7rv26Tk5N28cUX2+c+9zkrFotJVQohhBARumeEEEIsJbpnhBBi5ZLqLjOzlunpaatUKnb66adHnjKHDh2K8tmDhn1xEO5ayN8p5MkV8qLid9nfib+qjJ4+5XLZ5U1OTkbP7A3E/UbPJvYY4ncxPu4ne5OhDxeXRb8fMz9G7CmE8fIcoccQx8AeV+xjhePLsYeW8mLyuE30pmKPJo4X+7YY7zb2lMJ+8jzwesTx5Xno57uF8BoLeTrhuuZ4cLzMzP1AyG0MD3vPEuw3rzf2iEP6+eZhjPwDKo49753x8XGXxnHgOeMxwr6Expbj7zeHSKgse+bV63WbmpqSfOLHPHTXbPvXH1mmdHhMNt0W+yCkpvw6ngVfIzOzTCvelymy/0HvqhRZEnbpGurAlTFfJN+lNvtaxXM6fK8//3Zf4Nfu0551ZfS8vbPD5b39vbHfHXs0Yb/MzHIzcT73heG+Ie2i70u2BuuzZ0zo3IJ2q+vpfgMPomydPB3J8wr9iApTvs35UvI+q67xZ1FmPtlbKeQFNZ/zeei7ZGbWhvzSlD/vyNbKzdvIAz529jSbOimOP1+jnxeK8dnE48V9wX73835aDFgX14MxcOzNkt8DOJ481sUZP9ZD++J2Dv7tc1xe5Wzv0ZU786LoubXtJJc3ebL3IkPStH8xpqGqv4cm1026dKYV/7wwMuF/VuS+4byFPOHM/Pi2CnQvgWcY+35xm41SvKbSsDjbs9N2x/O26q75MQ/dMyuVXV96S2LepqddmZgnhBAPl373zCP+2xWFEEIIIYQQQgghhHi46EMuIYQQQgghhBBCCDHwLNqT63hRrVYjmRZKcli6wxIhTLP0CCVCLCtkuVPSe2ZhySTXy/GVSrHkheVrKMfiNliyhm1yHreJsj7uC4PvhsaEY2AJIs4Zy85YYoVjwnPGZVF2xv0OjUNIqtpvrENyz9C8cL0M9o3HGueM5XUsD8SxZwknjq2ZH5N+awHXJ8eHksR+UsGkOs3CkkT+CiqnUS7I/WYJL8L7IxQv9w3f7XeG4FrmdcyEJIlYL68prhfXSkhuKmLKE2XLNg7LgVojID23UtIrZmbWzsXzkqH/L2rlYJ+1kqV3h+sBOSDJm/hdnO1u0a+/bMvP8SWt70XP4w2/Th44a2f0fNKd/tfbtwo+wE4a7reajydNbWLfWILIks5UG99lWaYlplmOhaoqlmalaPy6MNY412Zeymjm5yUkT+R3WYK4GFDehu2bhSVq1Y08JnQmQ/y5hu93pgUS8RF//6ZYIwm0Cr4sSwnb+TheljZyXzJNkNu1qS/wLs8vsxjJZPUkaPOun3V5106/2aWfu+Mz0fPa+30b06snXLpYjSX4w9P+DMF+815fd9/qxFgPbPFtzGf9PVCYi8/9fJ3uJRqzXCO+g+dz9LMPxMRz1Cr6O3aouvD52J5rLvj3QghxIvPgTdPB/JMukrz7WKNvcgkhhBBCCCGEEEKIgUcfcgkhhBBCCCGEEEKIgUcfcgkhhBBCCCGEEEKIgWfZenK1Wq3I7wh9j9g/h/2J0HuG/X/QI4frYX8d9L5hrx32z0HfI44n5KHD8eG7HB97DIU8r9ifCMePPXxCXj3sNxXyJwr5Go2MjATrCXl78XhimseW5zDkc4RjxD5gHB/Wy3MW8qPiPO4LzimPNbbD8bFHF+aHPMLM/JxxHreD+bz+sG+L8bfj+LheTG/YsCFYFtd9P/8zbJfjC3nh8d7CvvLeCY09r5vQ3gp5hvE+4/2c5AknT65kMvOpyPuqg0u55ce2k/Zjj/5AWb9MLNNOPnsydZ6LuGyH7Jzmi3SGlOI5zVf9flj72T0u/bc7b4qeH3jcfpd3xjfGo+fqSb5N9j3K1OK1Wq/4eIpT5IMYsK5iTy7rwFlEWd00e38lx5etxY2yjxXHgxZTWYqnXSBPLvCUalNh9qrqgjFYJ0M+atxQAKyXPZvSVC/6gIUdw8zys3CfFJLP52yL/EfJlwn7wnk5mpdQv3G8zPxYh2BvL/Y/wzFrVOaCdT1w9nej551ffZ/Ly0z7eM5/3l/EZT/g/brKExWXRp8rHoORA/HPiuipZmZW3O1/bmuuiff3pttW+djPPeTSbWiz0/J3TYbmFNdNmzy55mF8eWzZvwtBL69u4OwTK49NT7vykQ5BiBUBenbJn+vYoG9yCSGEEEIIIYQQQoiBRx9yCSGEEEIIIYQQQoiBRx9yCSGEEEIIIYQQQoiBZ9l6cnU6nchHhv1sQqBHDfvVoN9TP4+aUD0M+vawzxG/OzMzEz2zlxHGx3k8BujNs2/fvmDZI/V+MjObno41weVy2eWF/JS4n1hvyMvLLOxFFvIn4ja5HYyBvZaw3n6eYQj7VvH4hXyYQrDfFPaTvby4XhwH9JfqB48J14vph+NNhfH1K4scOHDApblvOEacF/JZ4xgwvn77A/3QeK+zF16IUAych/PUzxMO3016Fp5OOvZqCtknPeTb9RBYNO2Xm/OuKlTJq42Oly5kt3Pk8dfy76KPz9wYVXTxKS45vC+e883/s8bldWC7DE34ft158R0uPbZ7U/S88Vt+nx3a5kNAT6ehQ/4sT7f8GkxNzUbP3cqwz0s+gnvwfl3JvlVmZnnwFwv5h5mZ5ergr1giry/ylEJ/KvaqSoPPFvsjsS8Tek6xT1WmSe9mkvc0e5N1iskLG32jMvN0/1I/O9BmYY58GjPktdlJPtu5Hewb9xvHpGcP5pLHgOPJ1ek+hvg+8tM+74m7yXsuFZ/t+zcfdHnr7vN7qz4CHpmd5HjTDfLEHPPjOXL566LnF6W/6PJee+NHXHrt/evjegr+MGqUyAu0Ht/dGfLvSgfmjPPS8t4SQoiB5uDcTGLe6qGRxLxBQd/kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADz7KVK5rFciCU5LBEiOVtCMr/zLxkh/NYEobtsHSwVqsllmVYRoXSI5Y3lUrxr5fetGmTyzvrrLNces+e+NfFs2Tp4EH/dXpsB+VWZr0yptHR+NeWcr3cF5R2cVnsZz9ZHErLeD557LGdftI8LMsyNBwHjj0kvQzJE/ldbpP7hmmeB5wzzpubS/7V6NzmkcrZFooPxz4kk+M2Q2PST46Kskiul9dRaKxDskOWMobkgLzGUNrKZavVqkvjPPXbAzieIdkt53E6aS9JrphMN9217o9lWM1SPJ55KpcleVEXpFstkrOhrCo3RVKxNNcTp1me2CNfPEJZl5nZHKiohu/1d0337h3R8z/+6ctd3uYPHfJlzx6Pnp9y2tdcXuUB32ajAu/R1dwxuifXxXuJ5YnZWvJ6TZFMqjmSrG3k8cR2OD6UMpqZzY7HY8YSsPkcyd9BGscSMCybTYflYYVavOpQLmlmlq37shh/ipYYy/hwjXEerptGKSy5RokaS0F7pJedIy+Le4nLNobiOyHXIBsHkn9iPcNTJZfH83vaLedFz3959+dc3p5t33Pp1defGT0XeiR+vi8YE8srkeJuP9Y7Ln3ApW948D+j5+FZ389rn/Bhlx76xquj54lH+Xb4XOi4sU7eO22S3bZpzRvIHl2dARmtOHH51st2HnHZJ/zTliWMRIhHhpMuGu1faBnBUsZBlC/qm1xCCCGEEEIIIYQQYuDRh1xCCCGEEEIIIYQQYuDRh1xCCCGEEEIIIYQQYuBZtp5c+Xy+x1fHrNfTh/2TQl5Q6BXEvjzsc4Q+ONxmPu/dWjCffbbYMwfLsncR+oRdeumlLu9FL3qRS3/1q1+NntkLiNtEv7FQnpn3HOIx4fHE+Qn5koV8l8y891M/zyEc+37+WNgO9xP7wvPL9WI/uS+8jkLeR7yeee0eaT0cH9YTWm/92uB4cMy4zZBPGe8BnJeQhxTXFdrbXC/PPXu5YTtcL77LedwX3Gv9PMNCe4LfxXi5HlzH/fqJHmJYttvt2sxM8q8KPpHJNtKW/fF8lO6ZjTN4L5XovAGvG/Z3mi/Ee6uxJnkPMuytZH56DS100uTZxF5LjVK8jkpFX9FdV4xFz+N/fI/L+9wXvSfXuWftip4/89dPdXkXbv6oS6/aF/t3lQ94/8fcIb/XW+NxTLkZ8nQskY8QhI/+V2ZmmQbcqX28qVrlOM2eZvlZP7/FmbhR9uRihmbiu7te8ndNFjyc2FOKfY/mIZ2tc9mA1xH9qDRfpDECbyj29kIa3sbKcg26C2HNszdal7yYOpmAV1ozeU8U68m+W72+ZNRm4L9tefywLHujbf6fx/uYYC00h8mbisBx4THyAfnxOf2mU136ly5/YfRcnfdeKI0bf96l15ehTepLuuP7jXPaLPj7jb3m3Ht1/3MvrmXcH91Qn4U4Ati/Sx5dQhx/BtGDi9E3uYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwLNsPblGR0cX9BNiPyf2+EEfGva2wbLstcNgPntwhTyI+nk0lUqx6UWx6D1L0JNr7dq1Lu+0005z6fvuuy96Hh0ddXns01Or1aJnHhNmamoqeu7nPxVqM+R/xvOKPkIMzy+O0erVq4MxTE5ORs/YLzM/JuzDxH5JIV8r7lvIZ4vBenlN4dizNxqD8fbz5Ar5Y/F8Y5r7GdpnIU8uHh/uN8eE4Nxzvf36gjEuZo547NGnjNdbqN9cNjRPfN7g2PfzrMOyXI9YmHue8jVLjwybmdm69U+I/n7jf9WpZLI/QW6KfN7K8Xy2/TFvnTR54YFXUDp5+f/43fi5UPXnVKrg652txGfc1FYfxN894Rei54n7Z13eD7wll528P17XZ+33ebX1/i/W3r8+em6WaE+upzsChqxZIR8humrQ84x9jrAe9qJico24HfSpMjMb2uffbY7Fjb73wp9yeaM1H+8zRl4T13PLM10e+hwV5vyeTAc8r4IeXNbrR3WkpDsBr0AaW4zHzI89e5o1hpL9ndjzqksLHX2kunRneT80v4Z4DrFa9ufi8cTxK874n0G43mwjTqc7FAMdE81SXO98wKesfZpfC+WdftEPv/Hy6Dl1hj97xms+vsmT43uKfdSs5ceT5/RIYb8unBf0kmOfOXHiwt5aIszEO999xGXHXn55/0JC9GEl+G6F0De5hBBCCCGEEEIIIcTAow+5hBBCCCGEEEIIIcTAs2zliuVyOZLwoNSIZVMs10E5EYOSIJYssbwpJFHjsihLYokQy5RQnsX1YF9uueWWxPbNzH74wx9GzwcOHHB5PEYhqRZL80LSNy6L8YekoSxJY3ki1suSNG7zlFNOiZ6f+UwvCxkZ8V+7vOGGG6Lnm2++2eWh7BXnj2M3C0s8Q/I6jp3HAfO5LMbHayi0drkvvI4xhn57JyTrwzHhdcx9wZhCe6dffCFJIveTpYPYFy6L9fB7oXOBx4vrxbMgJPVdDDy2fN5gup80WRzmzSe/wUqjh9fdh056WvT3s1/9PVcus9NL8zLjq6LndtnvUZTXsTyxRy62iKWRgS3Zpf+imqv49VjZW46eWYr3R/Vfi56//eK/c3nPm7/TpTddsCZ6/tJ2H+y2fzrXxzAOsiWSs3E6PxvvdZaHkSLMwTI9lKG1ir6N4oyvqHAoHocHz/MS9kqu4tL/efrF0fPcy+9wea2ab+euX3tz9PzE7P9yeZlW8s8SITkgw+O3OOBeJ6lgqxifE5l5krcH4uGxLe/16UYlbqc6Fpbce0nikcOSxMVQnIr7ynuJpY24HlM0Dd3k6e0Zv3Tg/5XnNvqfD9q5eP82S35vHzrZj2dpajg5iAD5hr8/cG1kad2mgoMtiWI/nv3yHZYr9EqDPvG3Jz0C0YjlwmIkikKIxaFvcgkhhBBCCCGEEEKIgWdRH3Jdc801dsEFF9jIyIitW7fOnvvc59odd/j/4azX63bFFVfY6tWrrVwu22WXXWZ79+49pkELIYRYmeieEUIIsdTorhFCiJXLoj7kuuGGG+yKK66wm266yb7whS9Yq9WyZz7zmTY7G/92piuvvNI+/elP20c/+lG74YYbbNeuXfa85z3vmAcuhBBi5aF7RgghxFKju0YIIVYuqS6bziyC/fv327p16+yGG26wpz71qTY1NWVr1661D37wg/b85z/fzMx27NhhZ511lt1444120UUX9a1zenraKpWKnXrqqZH/DPrM1Go1V35uznsToG8Pe/ig/w975LD/EHoDsW9QyBOJPXPYpwf9ldhrCWGvnbGxMZdGv51qteryeIywr+yHxYT8z9AnistyX3Dsi0X/6+vL5bJLo5cW95t9rC644ILo+bd+67dcHrfzd38Xe8185jOfcXn79u1LbIPHL+TnFPIt47XAYF+53pCPWmjt8vxWKt5nBucJf5AzC+8l9qoKrd2QDxjvJU5jvf2OJsznMeIYQl59HAPC/cZ54XXD9aC/XMgjjOPlslhvyEPPzK8NXF/dbtemp6dtamrKRkdHbZBYinvGLL5r1t32KUuPHPa0SWXi8Xvc+5/hX0j7+W2NxGMf8ulplJN9+8zM8jWoh4qyN1WrEPAApKwc1Fuv+ABLB+IA58b8i9c890KX/kz5rOj5a+9/j8vr8d3KxunR3f48DvlszRfJf4+OTuwb+24hmabf99Vxf8Zd8bM/Hz0/7cGZYJunvmV39HzDRx50eTP+KrSffcH66Pnxf/KXLm/831+UHC95keVqcRr9zRYCfazYL4n9lLL1OL857L36cM7Yk4s9w+ZzcdmRvf6uvucFn3Tp31t3bfR83b/6+5f9vA6dPBk9D814X05sM9fw77GXF+bzGPBYZ1rJ+3dula8X/eMYrre6Jl4cHfKPQ3+2XN3XyV5znUy86FsFP2dp8vrCeQvNmZkfFx4/XEc8fqGymNeuTdv3XnSS7pof89A9IzzfetnOxLwn/NOW4xjJseeLb/n/EvMeP5T88+bDYezlly9JvcuN1//BDcl5b/rJ4xiJWA70u2celifX1NRh49bx8XEzM7v11lut1WrZJZdcEpXZtm2bbdmyxW688cYF62g0GjY9Pe3+CCGEEGbH5p4x010jhBAiGf2bRgghVg5H/SFXp9OxV7/61fbkJz/ZHve4x5mZ2Z49eyyfz9uqVatc2fXr19uePXsWrOeaa66xSqUS/dm8efPRhiSEEGIFcazuGTPdNUIIIRZG/6YRQoiVRfJ3sPtwxRVX2Pe+9z376le/+rACuPrqq+2qq66K0tPT07Z582abmpqKpDghORHLxRCWBKG0JyS3MlucBBHjYwkTl0UJEfcL5WIsJeM0tsP95HpxjFhyyPJAhMcIpVrcLrcZkl+x3A7bGR72vwqbJWv4g8X111/v8liueM899xxRfNyvUqmUGAOPH48R1htat5zP6xjXAs8Rjx/mc9nVq1e7NI4RSjYXiveh/9U06x0jbIdjX4wCmtvE8eV1w33DdrkejgHTPH44Z7xfee/jvkOZrVnvHsW+hCScHB+vKVwLofPOzEsk+62/QeBY3TNmyXfNWZ9+imWLh7/unLkXDIWH/XzWzvDzDcpGS/sl5WSHxSmSlJaOfH9kSBZXH4nP3MaQ3x8TG7wZ8nOe+Kbo+bv1bS6v+aHXRM/l3S7LXvfhW3y8IJEc3ufX1PTJ/l2UME1v9MGP7PPnM0rEOv7I7ZFupVrJ/x+H8qzSAR9febe/T7Y/Mf5WxdY7aJ9VSDp4UiyrP/VkPnt8fIUnb4yeq3VvLbAmsGVZppnuZBJKmqXayfuZpW8sXcXxZKlgOwd3FrXRzvl+Zg1kcS2fd+8Nf+rS9f+OJZ7r3+ZlJH/1Ub+fUaLIEr9WMV7nhTmah4zvaKEG0niW9AVkhcWqrzdX9/MwV0mWuw9NeYsAlA52Mv7ebJTievrJAb0EMVmeeDh/Yeng4Rj8eKY7cV0sc23nkiWyLHssTYEcHhZcqpW8hpczS/1vmpXK03/0rcS8L57yhMS8QZckPtKcKPLExcBSRskXxVF9yPXKV77SPvOZz9iXv/xlO/nk+KfcDRs2WLPZtMnJSfc/H3v37rUNGzYsWFehUOjrEyWEEOLE4ljeM2a6a4QQQvSif9MIIcTKY1FyxW63a6985Svt4x//uH3xi1+0rVu3uvzzzz/fcrmc+4bNHXfcYTt37rTt27cfm4iFEEKsWHTPCCGEWGp01wghxMplUd/kuuKKK+yDH/ygffKTn7SRkZFIOlapVGxoaMgqlYpdfvnldtVVV9n4+LiNjo7aq171Ktu+ffsR/8YrIYQQJy66Z4QQQiw1umuEEGLlsqgPud7xjneYmdnTnvY09/fXXXed/cqv/IqZmb3lLW+xdDptl112mTUaDbv00kvt7W9/+6IDa7fbkf8M+uKEPHLMvLcNe1VhHnsMcb38LhLyuuE8/tWWqM2vVqsub/fu2Bwl5H/F9PMMwzFinyP+zS9YF8fOXkFYF/scoa9VP8+mQ4cORc/oA8Wxm5kdPHgwesbxMuv1bHrwwdgTZGbG/7r4RiP2xuB4uJ84TxwPg2Pfb02FYkDvLP7qeyhejo/TGBN7j9VqtcR3eWwxhn4eXDj/XJbXKpbldcN+aNgXHiNej0jIs65fX3Bf4ro16/WT4z2M8LzgXno45wvSz79ruXI87xkzs3YhZani4XHtnBlLUDpk29jO+fFEj5958pRaDOjLlGHvKYoh04z3Sy7tM//m8S926Z+/Nj7ba0/c4fLOO+P/RM9ri+tdXt4fA86zKzXhz9FSya+/LoQ/udF7ms2sIz/IGngJ1ny/2asK0500nSHNeB5qayiPfJiee/fH4rJb7nd52dlVLt1+Zhzvx37l/7m8St3He9q9H4meH/UF779TL8d7m72UcD7NzJql+I7g2NkfC8cv12BvpQ6l4eci8lbCdtCvycxsnjyvstDv+riPZ9fj/XrcUDolep7O+PN5eK+Pt7Ym7ks7zz/rxHchz/3wlN94aDHVzfEa8rWiH1Wj5M9q9svKNeCOpblvDief87lG8s8Lc2W/H7jNdhY8T8k7i9cNjhmXLTR8WfTW4vnugvdXl4zd2A8Nfd7cup4fHE+u433XrARCHlziME+/8v8e1XsT73x3MF8+XEIsjkV9yHUkhtLFYtGuvfZau/baa486KCGEECcmumeEEEIsNbprhBBi5bIoTy4hhBBCCCGEEEIIIZYj+pBLCCGEEEIIIYQQQgw8i5IrHk9SqVTkuYN+QP2+Xoz57HuDHjUhXx6GfazYa4nTCPswTU5OLhgrw/GxdxGm8Vcbm/V6LR04cCAxjz2wEPZAQp8oM9839h8K+RzxeOK7IT8xMz8u7LPFZdFvjNvEsuzBxfViv3leuF6OH0EPLm6X68UxmZiYcHlDQ0OJbXA87PuG8zI3N+fyeJ5wjLhfuOZ5jYe8qDiP+40xsA8Y+2xhDOwtxx5dIS81nAfeyzyeOPbcby6L8ffzckN4rHH9cRs8Z0l78khkGScqqfbhP2ZmuQfjddRZM+LKtXO0VmGaMv6otBRcJ+mWH/v6mC+LFjp8eqTJWg5tuAo1v6Z2ZSs+3jPi5+lxf7+N/iAOYuRen9cp+T3Q/fa3o+fiP3/S5f3lxOdc+lduvCF6nit7c69cw+9nJNPy+zU/42OoV+IYszRKQxNx2WY57Ml1+pc2QOYml9eq+HrRX+w1Ize5vMl1ky59yj2ro+fGiD8XilXwziLvsVaJPK9q4OlIcz837ustVJN/7qhTDLl6XC/7vqHPVauQ7MF1+N14PAu3e0/Cy1/4v1y6cVI8p/Pvu8rllWhPoP1Tuk5+WJDu9PlvWfQmY18yfhfXUart10mr6O+pXD3ea+wXl6N4cXw5BgQ9t8y8V5aZ2VA1Pvd5HbOHGMY3RGuMvdycJ1c72eurdwx8P3HMmsOD6f8olid37N/n0n97w//n0m9//h8cz3COG/Lceni8/k0/+UiHIJYZ+iaXEEIIIYQQQgghhBh49CGXEEIIIYQQQgghhBh4Ut1lpmOZnp62SqViY2NjkXQIJXb9JEwo0WFpFJZleWJIFseyLm4T5U5cD8umMH6uJySnZKnb8PBw9Hzuuee6vHXr1rn0jh3xr49H6aJZr8wLY2LpFsvkcOn0k24hLIPEMWLpYK3mJS8hOSDHi33hOcTx47XAcxZaN7weMc3rj+cwJHPF+ee1ENoDHB/LU5HQ/uB2OFZsh+eE68F0P+lvSOoYkvyFzgEz3xeOF8eI1zGD8fKaYnAt87rmeQrJpTGPy3FfMH4ck263a41Gw6ampmx0dDQY94nCQ3fNk942admhw2NS2AvrKEfreoTWaiE+/1hOhFJGlIMtBL7bJPka14vSI85jGdWPzvle9HzaN85xefkqtEkSvxLJ0PZd99zo+dbX+7w6lT101aOi5xffcKPLKxzy8d3/hFiGPTw17PKGJ7w0PgXbkscoRJqKosxr+JA/R1ke2CrF4zKx0d+TTLEGd1jD73WU/HUzLKdMlg7yfHYyPj00GbfTpWuxPuI7gzI0BseIpW28xlDyN3IfWRSUfL/nS/G7LMvkfqOsj6WDOGb8HvcTJYAdGus01cvyQITHOtuK7x6uh8nXkmW5zVJ8/w1N+Z8zuC/FGT+eCK8jXNeVB/x79XGStMMcz5X9z2K4drtpXn++nuJMHH87H49luzZtt75sre6aH/PQPSOOnN/41zcdcdmVKl0UQvSn3z2jb3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIHnyH+v/XEmnU5H/jfokcR+SexXhJ4/nIe+OOy9w2XZNwphbyCsiz2GuB70mGKfHoyB4+N0yEqN80ZGRhZsY6F6q9Vq9NzPUwrrYq8g9APq1ya+y95K3BfOR0L+ROjBZRZeC+wbFRprnu/QHHJZTHNZ7Es/fzaE/bp4/WFfuF5OhzzisB5eJ7wWsG/cT34X2+G+8L7DcejnsYft8joJ9ZPnDOsJ+fgt9G5Sm1xvqCyv/5BHHXtyiYXJ1M0yP5669kiyD07+APkMrkn23kHfm2yD9hU1EfKYSnfI/wemv9cHzKe3fSn2ajy0ZdblZSvxusnMk0cdxZArb46eU5Wqy5uf9fv3pK/GPnV/+5yfcHm/e91NLr355thHoXayj6FZovOwFfZBegj2KjIav2wrbqfH84o6jnWt2jfi8kKeUpwXiof92nrmG9sM+Gq1CuR5RWXzNfQF8++iF1mq4OPh8UTvpdpJuWBZsziNnlFmZrk6nc9QL8eOeczsqPfszDfAe7GVPF5mZrlGPCY87jwv7E+F8P5B+DX0AeM22YML83l+mUItHk8+B2qV5J9lGbc/6JwimzI33zhe/TwIhTiWsH+XPLqEEA+hb3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFn2XpypVKpHo8bs14/IvaowXeKxaLLm5mZiZ7Zo4Y9ckJ5XC96OLFPD3sOYdm5uTmXVy6XE99jfx+Maffu3S6P/X22bt0aPY+Njbm822+/3aV37NgRPU9OTro8jgnHkL2BMM3jNzQ05NI4h9zPkM8WrwX2cEL/Np5vTHOboTUV8lky82PEbYZ8rbgs9o2920J+UyG/M05zPDzWGBOPCdYTamOhNBLyzeN+h/youJ7QHPI6xjS3GfKl47XA6w/hMQjFsNC5l9RmyPcN10m32+3xmhOH6WZinyL0rho54E2aUm3yUIShZ5+tkLcS+00hHTK+YU+fVDu53g4dTfVK/HJln/ckxBjmyt6zp3HGapcePuN/R8+Pf87TXd637VqXrn01vou23eYH5VPPeYpLP2niv6Lntd/xYzJ9ZrI3Gns/Zetxx+tFv68yrWxi2Z45ytFgQz56rP04MzE+9k/CeHn+uC+Yz2shRx5T6CnW64flwbI9ayiX3E/0rTLrXWNHmsc0S/4syjWS5xupl/176UCjIR8tM7N8Lfm8NvYB6yT7brWzyfuZc+Zz8fqsj/g7qlUg37JGvHbT7fDgtiG82hq6EwJnRrpN/pSw5rK03loF+hkAvL9wrPuNuxBLiTy6hBAPoW9yCSGEEEIIIYQQQoiBRx9yCSGEEEIIIYQQQoiBZ9nKFbvdbiSXQpkNy3VY5oVMT0+7NEqCuB6WKYXqZakRyqb4PZYwoRRpMfI1jg9hWeGqVatc+glPeEL0/KQnPcnl/ed//qdLHzhwIHqu172MpVr1vz4eY+S+LEZyNTwcS2lYZsaSxFAbIXkg56HMq5+sEOcwJOnj9GLWamg98nshKSvL1zgdioHLYgwskQzJUUMx9BtrLDs7O2shQtJRjhcJxcvxhKSWIfmumZc1cz1cNnSGYExcz2LWmFiY+ULXrHB43FCiyHK2brJiydL+2HJiti4t03yNJELpeM7aOVp/9G7a4rLNHgmdByVsLI3KNOMYWCrWGPH7d/jZL4ye1/76N1zegdd6SWf6k/Gdse/bEy5va9vH+4Hfe1r0/OuHbqLo/TpHiV2W5Fc4TyyxYvldCsYz3QiPdaYFd3Wf/w5sFeMxw/nk+IzWEMvQsE2WWmKeme9boZ5OzOMYUqSh82uXpPuFZPksSyRD0tpuhmVxyT8f1Ef8nd8sxD//5Rv+56l8nSTEMIntnO9ohuV3IG0NyYsZnt9UhyWdyfsS42d5IkoZzcwKIKdk6SruXzM/9j1r/iglnQ2SlLLsEdduYyguy+MuxGJhiSFLEBcDvivpohAnFvomlxBCCCGEEEIIIYQYePQhlxBCCCGEEEIIIYQYePQhlxBCCCGEEEIIIYQYeJatJ1cmk4n8ZtCXqZ/nDPpIsY9VqRT7h7D3E6fRB4d9b9hzKORPFEqHPIXYG4j9f7BvnMceXXfeeWf0jD5BZmZ79uxJjIF9jTiNXmns34XzxPPA/cb4Q75QZn5cQn5nHAPHjmW5TZ6zkJdbaG3wvDC4brgvuB55bYb82fp5NmGay4b2wNDQkMvDseW5D40ne5pxfBxDUjxmfhz4PR579H0LeZxxXmgPFAoFl4f7gel3bmFfQj5q3Cb7loU8/8TC5Gopy3YPj1W6BZ56Lb8/5tYkj2dhyqdT4PHToe3K3l7ODyh5+ZuZ99thTyQGPZEybd8o+ju1875frQLdjSPxnVF89Nku7ymbfsOlv//EN0TPzbndLi+3wft3HSzH4zu13vs9sldVuRWfP+w5hPTzLsIxY++kXIN9rZL9ijgGrCtXP/Ifq9p58i/MJa8xnm/0AePxSpF/Uhu83NiGKZNBHysaP/IBmy/G+TzWTDsLXofkCzWy19932WpcduZcf45Or90fPQ9NV1ze6EGfDsHrGj2wijV/rnZorJ0/VprnzKfRu4r7zb5WyHDN7w8c+2yfNYXzwnNvfP6kcQ7pLsyB/y3F3iySr998wKRQCCGEeITRN7mEEEIIIYQQQgghxMCjD7mEEEIIIYQQQgghxMCjD7mEEEIIIYQQQgghxMCzbD25ut1u5EXTz88GCXkiYT3sTcVlQz5MHA96DnEeexChp0/Ir4vf43rR/4c9fNiT68Ybb4yev/3tb7u8AwcOuPT09HT0HBq/hdII9pN9jfg9jD/kq2Xmvau4bMhTiutBH6l+3kU4Djxn7KWF/nHs0cRziunQWHJ8HAOOL5dlDydcyzy/XBbTXBb7xrFzDJhmPzEeI8wPeaNxu4vpd8hni+thvzGsh/ddyE8sNGcM52G9i6mH1xeuTRHTzcQ+WZl9c9Hft8fIh478a9D7pkHWQF3wK8rP+PsjV6dzNAPnVJrOv4B/V6vg1wJ7Sh0p+Vk6n+mcQr+pbsnvq1P++jku/Z+vfVv0/BPPXuvyTl71Bpc+543nR89D5/v7OOQxlaJuosdUX+8imMMej6sce17F502h5uslizPLtuK/YM+rTmBacnUea2iD/Loys8l9Yx8mjgE9xDh2M/Booli7AY+uubI/u0tTfr/gunG+cwvQLsVBDU97b6pNH4j92qqXnuryptZ7M7xcHbyzyDNseMrXO1upRc/Fqr/HeRxwLnCuzXo9udDnKjdPP6PAPGSonrmyv2uwnR5vLxpP3C+8brq0GOYhXvbd8uXCnqLYJo51P682IRbL25//By79G//6piMuK4Q4cVnUT8XveMc77JxzzrHR0VEbHR217du322c/+9kov16v2xVXXGGrV6+2crlsl112me3du/eYBy2EEGJlontGCCHEUqO7RgghVi6L+pDr5JNPtj//8z+3W2+91b75zW/a05/+dHvOc55j3//+983M7Morr7RPf/rT9tGPftRuuOEG27Vrlz3vec9bksCFEEKsPHTPCCGEWGp01wghxMol1V2MFnABxsfH7S//8i/t+c9/vq1du9Y++MEP2vOf/3wzM9uxY4edddZZduONN9pFF110RPVNT09bpVKx008/PZLboIRodnbWla/Vai6N8iLuGkp7WIIYkqxxHsuUQkPI7WAMLIVCKRLL4EKwhImlWsPDw4mxLkZyxXIxbhfBfveT283NxRKhfnJP7Fs/aSO+y/3CGLhfPCaLkZEuZi1g/CyfxXZY0sdgPTwmpRL9WnKol+PhdtavXx89r1mzxuXt3r07et6/f7/LC0kbWa7I8ZbL5eiZ9zqPbUhSzGmcU44hFHsof2jIy3NwHXObXO+qVasS6+W9H5r/I13X3W7XWq2WTU1N2ejoaGJ9y5Vjfc+YxXfNuu993NIjh8/IsZFdUf7aj73ElWd5IMrdsvV0Yl5IrmZmlq/BWUlHapdlU7mARLxB5yFK3+g9lDul/HHXA0om81NUT82vv9ktIMen2LGfZmaHTp6Jnoeq/vxjCSXK5OaLLKED2VQ6/OMMjgPLO9skzWs7aVlYbodywR4ZZBHkYSTD7Fcv0iMlzCT3leWLobK4dnmd8HsoReM5Kk74eh8491D03BjyP6ed+lf3uPTr//l3oucnvd//XHTHc+M77NVv+jeXt+MlXoI9tme9JbFq97BLz7zoH6Ln9pd+KfE9My/raxbC9zHC0stWASWw/pzP1/zPFrUK3Jskw0UprZmXNvZbU6H12CrE+7le8mPL0sZCLf5ZDN9r16btey86SXfNj3nonhFCPLLcdc9c/0JmdsZpQ/0LHSHf/OyO6PmJz9p2zOoVh+l3zxy18Xy73bYPfehDNjs7a9u3b7dbb73VWq2WXXLJJVGZbdu22ZYtW5wnFNNoNGx6etr9EUIIIY7VPWOmu0YIIcTC6N80Qgixslj0h1y33XablctlKxQK9vKXv9w+/vGP22Me8xjbs2eP5fP5nm8orF+/3vbs2ZNY3zXXXGOVSiX6s3nz5kV3QgghxMrhWN8zZrprhBBCePRvGiGEWJks+kOuRz/60fad73zHbr75ZnvFK15hL33pS+32228/6gCuvvpqm5qaiv7cf//9R12XEEKIwedY3zNmumuEEEJ49G8aIYRYmYR/3/YC5PN5O/30083M7Pzzz7dbbrnF/vZv/9Ze+MIXWrPZtMnJSfc/H3v37rUNGzYk1lcoFHo8pMwOexQ95B+EHjrs08P+SejFwx4+6G2D3lhmYa8lzmNPKawr5JHD8XIM7KGTFI+Z72fIf8jMe3+xB1LIg4jz+F1sl/sZ8ktiLyOsl8cv5MPEPlbcTigGHvtQPSHvsdD4cXzcN1xX3M9GI/bD4HrYyw3r4bW6bt06l0bPK27z4MGDLo2eXFu2bHF5+C6/x+uP08imTZsS07ze2PsLfcFwvMzC3l88Z7gW2P+K1wJ6nPXzj8N1zpIFPsdC+5nTSGg/47p9mNaLx51jfc+YJd81Lzv5OiuOHl4va20q+vtPPvAL/v2cn9+JM+MxbZT8uinOxO2wRw77HqHHFHspMVhXTz2FZD8l9K1aLOjZ1Sr7+LJp9reL22Q/J453bPcI5PkzFj3DzHq9yhD04eL3mHnwlGqWyHsxk9zI0CHfz65fCtYog29jwBMJvYsO4+vFd3nOOgX2Igv4iLInF7yKnkxmZu0ctunfq5P3U3Emjrf4hW+5vIs/+OcuffV/PRg971vn5/7yx3zBpZ/0z7Fn19TXd7u80/esihN0ro9hnnlvqrH7/F7/41+7wKXPeeV90fPQZh/7zb/nPSh/95M3R8+ZFv2smPbzkmvEMfJ887tIfcSXxbHmdV0d9/dddiq+l3if8ZyiL1ia1kkH9mw75+c+3fAeYtmEvqQbyZ6Xy5Hj9W+avd//ro2OjPT8/dCWrQ+/E0KcIPzmD951xGXfdtavufTfNd8fPb8qH/ZhPFrQg2sxeWby7FoKjv4n3x/T6XSs0WjY+eefb7lczq6//voo74477rCdO3fa9u3bH24zQgghTlB0zwghhFhqdNcIIcTKYFHf5Lr66qvtWc96lm3ZssVmZmbsgx/8oH3pS1+yz3/+81apVOzyyy+3q666ysbHx210dNRe9apX2fbt2xf1G6+EEEKcuOieEUIIsdTorhFCiJXLoj7k2rdvn73kJS+x3bt3W6VSsXPOOcc+//nP20//9E+bmdlb3vIWS6fTdtlll1mj0bBLL73U3v72tx9VYLVaLZLpoAyIpUb8tWCU6IRkSSwBCsnFmJAsjqVtLCdKipVj4Hi47GLkTSxvQ+bm/K9UxTHi2IeH/a/gxjEK9SWf919zD8m6eM4WIzvjelEmx5I/jI/rQUma2eFfUfoQLDnk+LCukLyT4djxXZb7cb2Y5n6ef/75Lv3sZz87en7wwQdd3n/8x3+49MRE/Dvh2VcCJYr95LwYX2g/mPl54XU7MzPj0jjWHAPLUXHseb6xTR5rXruh9cd9C8lcQ2PE6wT7xmMSOqcw9m6327PXlyvH854xM7vuB79j6R/LeItT49Hfb/3mf7tymVUbXXp8IjYUnrzA/wrj+kg8L7mGX4u5erJkiSV+2TrdU/AqS5hQtmfmJWutgNQtS3KmbuD73WlS23Uz/t1cDfZ6ISybQpkmS6xyNZd0MXUovhz2hd6b90e5G5NeuR+PdRxThxRYmeRtZ9kGnfvwcrPk22A5Wwf2OssK87NkbwDjl6/R3ZdLPnt4/YUksrm6bzNfhbIdf/790V/e5tI/+vSPoud1j/b74xMv/RmXvvWx8USdv8bLt+65OB6/xn94+X1pyvezCbLhdsH3q9QMyDuHfD9/uX61S59Si+dtYqO/h0YP+r7hXMzn/ByixI/3dn3El8X116ZzYahKVgg9MtiYVJt+/oIYeH6ReZYrtunnXugbSjZZvrmcOZ53zfrHnnMsQxdC9GEx0kaxMlnUh1zvfve7g/nFYtGuvfZau/baax9WUEIIIU5MdM8IIYRYanTXCCHEyuVhe3IJIYQQQgghhBBCCPFIow+5hBBCCCGEEEIIIcTAsyi54vFkcnIy8qZBzyb2QBofH3dp9Kw5cOCAywt55LAXT4iQ9xN7hIV8mNjDB+thrx2uB8tyv3iMMN3PlyfkXcR+VNgueyCF/MU4BoyPx5bbRM8pnrOQnxd7K2G9nMe+RyHPppBnGPeT5xDXSrPZdHk4njx+DI5ZpVJxeaOj3i/kggviX6OO+8rMbN0673eyf//+6Pnee+91eeiPxbGH1jWPweTkpEtjXTyf6I1m5ueQ9x3PU2gMsWw/fzGcb+4Llw2tsZBfW8gvkOPj+cXxwzOk3xo6kTn9prMtO3R4HAs79kV/35je7cq97JNvcukb7OzoufvSX3d5h34+/lXQ7axfi+0yeS3V4rXBdjbsgdUEHyb29GFvrRQ2Q95U6EfFHlzsN4V+VN0Ml/XrCuPt5ML+e4Wp5LuRvbRcmz1eWoE2DtGeBFu1kEeYWe/4Im1/3FgePJt4PHFMCjPJfmxmZp1MHAN7rHF8Lh7y4GLfMlxX7H+Wb8WF2bsNPa7MzPK1+P4tvvfzLq/xan8+7zuAXqXTLu/U23zZX3j0G6LnLbPPc3nFv4i9I3NbfewHtnh/rJGJchz7iO/Lr9x8vUtve/efRc833varLu+s3/PtzPx0vJnaOb8pG0N+jHC/4942I58tmgf2ssrPwLyUfNnh/f7njnoFvdzo5zZKN0rxBh8+5ONz+B+trVX0/fS+YHH7qQYZ2IlF0fjO9xPzCuc99jhGIsQjz1L5ap1x2lD/QkfBE5+1zaW/+dkdiXli6dE3uYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwLNsPbk6nU7kI4OeOexBc/HFF7v07Oxs9HzjjTe6vOnp2Bein0cN5rN/Dr+L/knsvcO+QujbE6q3XC67PPamqlariW2WSt7QJOSJxPWGvMnYdwvT3E+sh32rQjH0KxvyT+J5wXEJ9ZPrYX8nfJfHFj3CzLyfF49JyKsq5CnFHmGh+HD9m5nddtttLv3hD384ep6YmHB5hw4dcmlsl8cP54X7xesx5GnG3nPoY9avXsznejleLMvzjX3hseU1xfUmtcGwXxeva4yJ28R1FPL4M/P7B+Ppdrs93mniMLWxpmV+7D2Un4m9gu58+/mu3E/97G+69E92vh09X/Uf/l7qvu2a6Lm60c99qk3nQB7O5zr5PfrjxcHeWY1KcjvoBWRmNp8JeNSRl1a2kezf1S5Q2Rp6SvmyafNlm+U4Bmzj8Lvk9dWBvU5eRm0Dzzry0WoXfToHY8b+YjwvCPthcQzFqbiyTN3noTcZj1+v71ecro/4s2Y+58+4bCtuk+NhWvlkr69UIbnfmXk6KyGk6vte5/I6L/lPl35cK9lDzNb4c3ZuJvaDLH7nQZfXOPukuNwqPybdjG+jAR5iw4f85inUfJuTb/7T6PlRG/3PNne8wntQFqvx3Vie8L6XaVo3Hdhb7GmG45lp+7FN0YZBLzz2jwudC7hXFgK9tNjzD72/vOdWr7dgJ7Pw/PKciGMH+3XJo0usdN521q+59FJ5dC0V8uF6ZNE3uYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwLNs5YoISpHWrFnj8jZu3OjSKLlatWqVy0N5FkvAWE6E8iGWCIUkdAznYTshWRfLojiNZVnix2Cb/eRYKI3qVy/KFUPjxzJHlnVhembG/0rwoSH/a15DEkSeF5TCheahXz8xn/vC44dl+0liMaZQX1gSGaqXx++uu+5yaZS58thyO5s3b06sd9++fYnxcBrXQq1Wc3k8L5jmNcXyT5yLfjFgvSFZK8Mx4JrqJ2PGeENjYubnO3Se8BrnMcF6h4eHo+dOp+Pk2iIm28haNn14Lc3v/G709/ff82ZX7hPfmXPpIhwb/3zTV13ec9rxfI884NfJ3BqfTneSZWcZkhOxBBBplPxa6KbjtVus+jWeqx35/2+lYDl26KgkxZVlqyAdbNFaLSbLmDokkeSyw3thfwSOa5YgNkd8PfmZuJ7cbV6SZiNeclrftjp65r60STo4X8B5SyXmsSyTwX6jHHGxsPTSSePyPnaUsuZr4TbrIIkduc+vt8t2f9GlX/s3T46ez3jAT1pur4/vwe9eFT1/9Rq/B37jxlgGue6eMZfXGPLrugNrnvdSq+A30/0Xxj8DpmljVfavdel8PY4/1/B9YSlhqwj3esGPkRnYJDTJbiHt12p1TRwf718+B+oj8b2UoXWTq/ufWdr55PsF+8Ky1tAZUq/Avd3W/50vBpYgCiGSOVbyxS9/519d+qnnPf+oYxLLF91GQgghhBBCCCGEEGLg0YdcQgghhBBCCCGEEGLg0YdcQgghhBBCCCGEEGLgWbaeXO12O/K8Qb8i9vT53ve+59LoOcRl0UOHPXKaTf+rnsvlcvQc8psyC/v0sIcT9oX9dbCdfn5TpVIpMY99erBezuN40Z+I+xnqG+ehrxDnsQcW+qOxZ9jIyIhLh/yduG84pzzfOCYhXy1uk8vyPIW8n0LebiFvKs5jD6mQTxnuBzOzXbt2Rc/syTU25v1OcOxDayq0js3CHmIhrzSuh/cS7jvuN5fFGEOeehxPyL+L6+EYMP5+/l2htRqC9yim8YzgsRQxnVw78qnpduI5/NZT/JhdfMGwS5fWxh52b7r4ZJfX/UY8360y+SORz1a3E+ez11PWH3HWLMVrg/2x0h3fTgf+D4s9fLqB/95K0VJBn6uMt7K0trfxs/lysqdTtp7s0cX+P1w2XYvPn8xIsikX94u9x1owfrnKKpf33V/xHl3d7N3R86O/fGEwPvR/4nlAz6t5f731lEUvLfZ6ag6zv1MMj186Q3duPZ6XzCydjZ1kHyaOAfs5u8XXU9rrY/r9j98cPU9snHJ573vCpS79uP+J57Q4738WQ5+t3jWfvJCbJV8Pe5xlp/x+RjLzviyOC3t7zeeSY+iQz1YavNHYG4vbRNhjrZujn0mhb+zBleb9DJ5Zs2t9X3Cd8NrkvYR7bXLdZPTcnvX+nUIIsVSwRxfCfl3Pb4wllBQrFX2TSwghhBBCCCGEEEIMPPqQSwghhBBCCCGEEEIMPPqQSwghhBBCCCGEEEIMPMvWk6vT6UQ+NuiTg55CZt6Xx8x7B01OTrq8kO9NP18rju1oy6I3T8iPKOTfxGlun+tFf6KQh5SZ98TiNpmQFxnGxGPNaWwz5E3FMbEfEY8D9jvkExXy6+I0jwnHEPJTYq8vLMv95LKh+EL+Z+w1F8pjj7PQWsB1xPWgxxrD8XE6NL+hsuzlFtov/dYNwvXimlrMmcGE5jC0xvqNH6dFfxpDTcsMHV7Dq37yedHf/8EVZ7hyz/j0n7j0B6aeGz1vevnTXN78efE8oPfUYmEPIvTmaZR9va2CX3+5BqzVQAjdTDidbiXn9XhpdZLXH3t9occPe2lxO23w4WqTH5HzsSLvIh6/bAP8p9avcnlDE2tdem5sf/Rcq5Bn5wHvi8j+RQj6WKWon+lGsn+XtZJ9tZh62cfXypG3VhM8ufxV4+Y3leZ4fNluB7ypir6iQ6f5dPlAbNi2/k7vhfL7t3zNpb/3nNvj97673uWt3rdqwVjNzEYOeF/J2bH47mGPq3aWOg6kyNuLy2I+l2Wvr06mk5jnyrGPWmAN1UfIc5L9xQLtsJ+XwdrAdcFl0226Q2ns58biftaHY+/PjnkfUBGmcN5jXbrxne8fcVkhRDLs1/W2RygO8cihb3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFn2coV0+n0gtIblkLt27fPpVECxvIhlFhxXkhKxnGEJHQMS6OQkMwQZVEcD6e5/ZAUiuvh+I5UNsV1hSR+/WRnITkgzzem+8k08/lYUsLjiW0upp5+fRkaiuUTLP/jdrAu7vfc3FxiXrFYdGlc1xxfSGbIa4HjQzkq7w8cI5bAcpuhvcR9Ca0FliYfaXxmfl54reK7PAahvTU7O+vyeBwWI/3FGLjsYqSruM5xDXE5EZNr5CyTOTzGBx8Tj9PqmVNduf/+439x6fEtpeh5/7PmXF4nHa+NkQMll9fOJUuas3VabzT1IdlhoeYldE7G1/L1onSQZYQsHZwvJMfr5HVm1i6AHJ8lc61kKVzWH5VBeurFOkny1SMJM4iv4Ms+6tubfb2Nk+N6cmEZpJN00vjhnOarvp7slJcZ5ktxxXNrePx8xSihHN/hz/1D2/z51yzFZXMN+lknc2RSS2aunCxLNzObWxXHVJzxbWYa/j4+82vnRs8s78UY6hXfr1bBn7l52AO8VzolWqu5uK7cPP2Mx7LSgJRwPpcsg2wW6U6ogTUDv0eSQ2yzS52ZZxkuvMv95j2KtPNkk9BIvmvaXrnv6q3sXxOXo3NILA5JEoUQ4tigb3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFHH3IJIYQQQgghhBBCiIFn2XpypVKpyL8n5D/FvkIhzyH0A0KfpYVAbx5uMxQD+/KgF1A/ML5+Hjohnx72I8L40CeI2zTzfeN62BMJx5DjxfHjvGbT+5CEfNRCnmZc70Iebkl52M+RkZHEPIbzeDxxvjn2ajX5V2tz2ZBPFK9r9LVi7zH0ZTLzc8rzy4TKYnz9POtwnjh23oehdc9rHseM32MvtyOd05DvnJn3WWM/MS4b6guXxRh4THD+eX55rHmtHEksJzrpTiryv0EvntaWMVeuS0fRzJp4b7EfFvowsZ9Pt3PkHlfpBntMxelen6DkOW7nOA/WfNhaybXD9aDvF5Ol2NP+auzxtUKaZTonqugv5utFr6oUHWnowWXm/cU4Po6nXsEYKJ5a8nnCPlbeW8mXnd3i93OjFA/S9Oop3+YTPuPSvzPynuj5D//nH13eGV9/tEtPnhyvVfaQmivHbXYyfn5HDwy7dPFQ3JdWgc7jeb9B0A9tap33LyyM5BPL5up+TAozcb3s19Wh8xrXRn3ELzj270IaQ/5nkvmcP0fzDfDlJO+sLKVnK0dmMNco+Y2XaVG/wb+Lx5bBfndojfGZguNQrLKPX/zM6yTPfqSw93EMUq1wrEKI5ctH/+fqxLwXnHvNcYxEiIfPw/om15//+Z9bKpWyV7/61dHf1et1u+KKK2z16tVWLpftsssus7179z7cOIUQQpyA6J4RQgixlOieEUKIlcVRf8h1yy232D/8wz/YOeec4/7+yiuvtE9/+tP20Y9+1G644QbbtWuXPe95z3vYgQohhDix0D0jhBBiKdE9I4QQK4+j+pCrWq3ai1/8YnvXu95lY2OxpGNqasre/e5329/8zd/Y05/+dDv//PPtuuuus69//et20003HbOghRBCrGx0zwghhFhKdM8IIcTK5Kg8ua644gp79rOfbZdccom98Y1vjP7+1ltvtVarZZdcckn0d9u2bbMtW7bYjTfeaBdddNERt5HJZCKfGvSvYU8aBsuy9xPCHj3sK4TpkMcVE/J+MvPxh+rlNthrB/2xQr5VHFM/Dx/0lGIPJH435N+FcF9Cnk08XuythH5F3G9eG5gOzS+3yb5qGEOpVHJ57MmFbXJeqF4G+8YeTTwv4+PjC7ZvZnb//fe7NI49+zTxu7g2eM5w/Pg97ifWE/LVMvPrmseP1yp6nPH88jxhO+Vy2eXh/LPvHJ8hWJb99jhefJfHhMcB9wj3BWPnekKehDheoTNruXI87hkzs1wta9nu4TWcBmOmFNlYtUp+DNGbh72p0AeH3bDYswn9uzgPPaT6wV48rs2837/1crw28+QnVqj6irxnl89jj64u+k/RtRTy4MrWaEwayV5fPf2EGDp+C/Z4Z2UCHmLsTYb+Zz1l/TFhjUocQ63iM3GdlFt+rPMzPr7SF++Mnled81gfz/Z9Lr1qPm7n8Y/7gMs78P3Xu3SuEZ/RmaafmF5vt5jpNd5L6zNn/0xi2ed862su3SrEY9Imf6d0m9ZRNj63eK3iCZymzTQ06RdVsxQXKNT8vVSa8mNf3B3XPPMoX7Y6Th5itWQP17ky+T9C39jjbK4ce6ONHhh1eSEvr1AbZmaddDwOPJ+lQ7TxKvFjjvYHnjeju8O+tbNj8filYFOmQgfRMuR43TNCDDrs17VUHl3v/+XbEvN+6X1nL0mbYmWy6A+5PvShD9m3vvUtu+WWW3ry9uzZY/l83latWuX+fv369bZnz54F62s0Gu4fltPT04sNSQghxAriWN8zZrprhBBCxOieEUKIlcui/svl/vvvt9/6rd+yD3zgAz2/Wexoueaaa6xSqUR/Nm/efEzqFUIIMXgsxT1jprtGCCHEYXTPCCHEymZR3+S69dZbbd++ffaEJzwh+rt2u21f/vKX7e///u/t85//vDWbTZucnHT/+7F3717bsGHDgnVeffXVdtVVV0Xp6elp27x5s2Wz2QXliizlYRkVSrtC0kF+LyQDYjkbE5K+cRolYixZwrIcD0vLUArFcjaWJmG9IdmemZc4cZuLkWmihI3HgPuN7bB8jeViPC6hejHNeSgVDNXJ7/K6YckhjlFItmfmxyUkOeU21q5d69KPfvSjE8tOTEwkxhCSmJr5eeH5DUnxeN2MjsayDF5DPC8o+esnTcYzhvvC44kxcr04DxwP/yYlHD/ud+gM4XpD8kUeI/wBfG5uzuXx/qhUYi3K5ORkYp3LmaW4Z8yS7xoEJWuZOsl5cyQt68TnLsv2UILIssduJlkexnIslD0yLINslpLl+SxJrI7FUt/pNZMub91961wa+5KmozLd8mPSXcR/m6GcsV1IHpN+9WYghk6B7qyAkp/npU3/zkXpaK5B9gZtPy93X/z16HloxMsK19zwv6LnfvLT7kWPi9uc8mda9VNXufRvPCWWbaz6nye7vLEWy2fhrqG8dCcepOInvuzy3vze33HpmefeHre5Zdjl3fNn/+HSj3l/LG1Mk46UZbmZ+TgGllPiusZyZn5tmnmpY2bWn/P1ETpzZ3KQR7YIdboHTtkVtznv8yr7xl06C/LU/Zv9/dFJx/E1S/4sL09UXBrrsRrpcIkuHhwdsgAo+4XeKsTjUGr5svU1IHcv+ryRe309PBdRLAH563LikbxnhFiuLJUEMURInijEw2FRH3I94xnPsNtu84vxV3/1V23btm32e7/3e7Z582bL5XJ2/fXX22WXXWZmZnfccYft3LnTtm/fvmCdhUKhx8tGCCHEiclS3DNmumuEEEIcRveMEEKsbBb1IdfIyIg97nGPc383PDxsq1evjv7+8ssvt6uuusrGx8dtdHTUXvWqV9n27dtl0iiEEKIvumeEEEIsJbpnhBBiZXNUv10xxFve8hZLp9N22WWXWaPRsEsvvdTe/va3H+tmhBBCnKDonhFCCLGU6J4RQojB5WF/yPWlL33JpYvFol177bV27bXXPqx6k/yA2O+H0+iZwx5I6IPD77GvFXr8sB8R+/1g2VA8ZmalUil6Zp3+8HDscXHPPfcktsEx9BuTkPcTe/pgOyEPKbOwzxGm2UeI+4Jl8bfSmPV6CeG7IQ8uM7OhoaHomcdkdjb+FeGhued8HhP2wMIx4djZhwm/0s5thvrJ4G/v4fkM+dLxWsAx4RhCe4D7yeO5mP2Le4DrYbCvvG54zWEMtZr/Ve0h/z2uF+upVqsuj8cTvbR4XrgdjKHfGYLwvpuamkosO8gs1T3DoIdTJ+fXfL1CZzB4G6Xa5G1YjOewQ35S7ImEvlsZ8rjqpLuJZdPkfZOihvZu3Rk9/9V5L3V5rVRc9jU//AuXV5oaoXR8jvZ6U1ki7IfFfl6Y3yqRjyT1rQNeViGfMoZ9mNJt8O/K+H2Wq/uAcZ4Kh3zZia1+T25Z/93o+WDdeyaO37A/em5tO8nlzY3zmopjaKzx8ZT3+vSj//Vno+f5ksvq8YjL1+J3Wz2+ZbD+Npzq46GhRh+u0lnei4ppF8FjtEi+jenk+eb5Dfl1VdfUE8vyGirO+Hv0vifGPlv/9+JXuLyC+Xj/4od/ED1XfvRol9clI73pNfE9mmt42Vp5It5bjZK/WzLkj4VnSqHlz/laxb9bAM+9BnnztQrJ9wd73aEPGHp3mZlNnU4+l1Avxs5zO0gcr3tmpfLvD74wMe/ZJ334OEay8vmTr/yyS7/uKe97hCIRR8r7PvmdYP4vP+e84xLHicSifruiEEIIIYQQQgghhBDLEX3IJYQQQgghhBBCCCEGnmPuyXWsyGQykaQHJUIs6wrJqLgsSoJYAsbyIcwPyYW4TZZ8cXwoYUJplplZuVyOnvm3s7DECuPlPG4T48f2zXqlUSy5QjgmrJdleyij4nhY3oZplvRxPFhXSPJl5iVioTnkMeB4sS/cT24z1A7Hi30LjRGv1b17/a8lx35yPLw2sK+heTDzckGUfnK93C9O4z7sN1441txmSC7L64b7jXXxHsV6++11jI/3EksHk9owC489n1uhOWMZJK6pkHRWxGQbKcumD++/TD0ep5Q/FnokiZhmCSLOYIYkfSyjSjdAzhtWJjs5I0sFud4sSKVKbb+uD2XjtTsyvtPlrdvhTZXn1kB8JEXK0JggHB9LozJwlKZyyfWYmaUtWRqKMshCi+WUdGaUUBqafNeZmbVz0NeW7wxL3x647dnR87qdm1xeZ2t8LrT9kdEDrrlmyS/ANq0NlraG8txY05g0RuK+1c/f4vL+5Dfe5dI/+b4/iZ6/WL/Q5T36XT/r0nNjcfztLMky50ka2kSZocuyVBuk8SRlZIluiOI90y5dXleJnn/xPn+nFkmW+1cjsTR97f2rXV62ThLjgJwW92i6HY69CbLDmbEZlzd6sMLFI4oz9LNjxUv3c414IbFMGGWtsxV/hxpJOIeq8WIeZImiEIPISpIn/tL7znbp9//ybQklhVgc+iaXEEIIIYQQQgghhBh49CGXEEIIIYQQQgghhBh49CGXEEIIIYQQQgghhBh4lq0nV6vVinyK0OuG/ZNCXjPoccWwn87MjPc8QD+ikO8Xl2UvIC47PR37QvzgBz9weegbxN5PIUK+VZzP3kXsC4bxcV7ITynkf8ZeXuwxhfX083dCRkZGEvPMzKrV2EeDxyjk3cbjh3MRqsfMrNGIf7V3Py8yLBsao8nJSZc3Ojrq0lNTU5YEe0xhDJzH84JlMVaG+xnyieLx4v2Mc8F7gN/FdjgG3vvYl9AZwnPEPluYxn2/EJjP65jHfm4u9k3h9Rh6j/uNyJPryOjkYi+sNHhrdcmyKdfwY90qkHkQgN5B7CHV418DflT1EX8+M61CPP+je71nXX7Wr9XVu9ZHz6/4/nt9PZX90fOG7z/e5VU3+jYzEFLpPu/v09xYcun5AnjL5Xw/c+RzhKOXpm6zdxWOGXs24bvZQ/7MOPRYuk8CnkidjK94ZFc8ntUtfjF0qewZX4x9uJpr/Dk6uy4um+4keyAx+Rr5hFb9u/UxHJOwpxmOZ4Z+tChOxTHkd/ufX2YuPcelr7/236Ln5ogfg/pJvmL0purQpKEvVD/Yhwsp1PyabxWTz87umL8TKvvi8f38o3zexmbVpU/5yDMT25he4z0e8/W4b+zPFfKumivPJeYNT/v4WgW/Yarg2VUf9rGzRxyeE81hf5/g+cK0c8ll+/nbiRODZ5/04Uc6BLECYI8uIY4WfZNLCCGEEEIIIYQQQgw8+pBLCCGEEEIIIYQQQgw8+pBLCCGEEEIIIYQQQgw8y9aTa2RkJPKxQY+akP8Ql2UfGvTF6eedxWmEvYvQFwd9tRaKAf2AOAb0PeL3Qj5g7A20GP8k9ugqlWKPFY6BvZZCXloYE3tncbzoh8beVNwmtsP+RAyOdWg82aOJ0yHfI44BPZtCHmacDnkm8XqbnfUeIBgfl2V/p5DfU8iPivOwTc7jNcUxhcB32SeP5wHXecjbyyzs7RYaE16PvL8Rnl9skz0AeV1jv7kvIe+7UL/7ecKJwzxw1k5LDx8+o7beekr097l79rly1YvGXDrTBK+5Fp0RkE6Rh1SXliLmV+6lzLSvd2JrPIdzq2if1f0dUT4Qn7NjJe/L0z6wLnouzPnzeHifb7O6MQ5w/xN83vi9fk11IV725Ar5jZUO+TOCPbrQt6xF9WYPxPfo/Vf+q8u7qvIel377p/49MYbijN/r7SL2Nbx32IcLQW8t9nljX7cc/L8j+zfNr6F+15PrbeeS/eLaFGoHllzrNH9O9axroFHy511mnn6Ggux2NvmMNfN+bdxvrJX97Tg+XHO1ij9jO2nf8eJEXPYjf/dfviyNUakelz1wmq8XPbjMzFIwoGmj8zkXjwN7wGVbyT+Op6nf84H5tdNudcnZyVUuverW+N3m+d7rq1CLz4JMjnzo0sn97ILnWpdN84QQQtgvP+e8RzqEEw59k0sIIYQQQgghhBBCDDz6kEsIIYQQQgghhBBCDDzLVq546qmnRnKp/fvjX3fOUqh63f/aapTrsCQI5Tos82FJFcqUWG7HkiWUVbEsKiSLZFkh5qFUjGM381IkllRxDDgm3E8eB2yH5Vc89liW5ZTYDku1UBLJVKv+119zfBhDSI660LsIjl9oDBhugyVh/SSUR1ovpvvJ9nB8+0kvQ22G5je0pkLx8Lsh2SDH268vGG9ICmrm12dIbryY+Q1JDs38vhwfH3d5o6OjLn3o0KHo+YEHHnB5ExMTiW2GpLTiyLjR/q+N/vgq/LdXxOvkfb/0N65cq+DvAZRgZVr+DEYZFcsTGyNeulWcitv8f6+52OX91Pf9y+es+8PoecONT3d5OfNn8Hwh3r/lQ8M+9nwcQ6tA53zZ74/Sgbje1kkkDyv6NEovC1Uf+3zBl20Ox+3WK35MWIaGMrVWkc7YXHxXvmLVB13W2ZP+7HzUz74xej743j+zIwUlh2a9Ukyc05DEL9NKU5rldh147gbLolwVpbNmXv5n5uWfrZI/R4cm4rJzY3RfkHSwOxKnsy3fZqvgzz+UszEsO0SJIve7VpmLnktTyXJxMz9Gqbb/GSrdofFbA3cN9ZPXGMoM09QvlmliXSEJ53zOtzFU9fcmyhnzs74v45+626UtF58//7Lur11W5Ru/69J7fm1t9PxLX7zR5VXX+J+nfTz08xZIMTGPy4nD/NnHnmXF4V5Z82t+5lPHPZaP/fu3g/mXPfvxxykSIYRYOvRNLiGEEEIIIYQQQggx8OhDLiGEEEIIIYQQQggx8OhDLiGEEEIIIYQQQggx8CxbT64zzzzT8vnDv84YfWjYn4i9d9BHKORj1c/bBn2jHvOYx7i84WHvb3L33bE/wq5du1xeyKOLfawQjp19tzCf+8JgDDxe7HOE+Twm7BOGvkych2nuC3uRhXzU2HMN6w35RJn5voXK8piwZxOmeUx4/HBOuWzIC4r7jTHxezzWSe8tFEPIly4UL69VjIHnk+cbx5rrCcXXb/1hXzmP28GyHB+W7Rcf9oXPgYfOq4dAL7+zzz7b5Z122mkujWfI5OSky8O1wfHxusFx6HcuiMOUvjVppaHD4/qc3Fj09x849dGu3NhO7wc0vTEe3/kinQPgV8R52bo/i8Yuj322zn6q90rZccjv5x/+fewp9ZT5n3Z57GWUCvwXVq6eWfDZzKxJnk3lqfh5/C5/FrXJXjHdAm8l8uDK1Xw63YnrQi+qheiCzw97K3ULcfzvnn6+y7urcpNL33jnz0fPZx/ybTYrfsAa5eSYQp5hIdj7icmB91eG7HvqIy1Lgv3PmJl18T3KXlntXDx+6ANl1uv11SjBOZqm+4PqxXnK1POUlxwr5+Ua8UB0ye/J3/jea459rJhWEXy22j529hvDvvbznOL1mUSh5n+m43MhV4tj2HfalMub+MXNLv3MS18dPecvv8vlffGLB136/O8eiJ5/9I5/c3kjt8RnSps8wwo17xk2n+PRP0wnNLniEaOfD1dSWflzCSEGFX2TSwghhBBCCCGEEEIMPPqQSwghhBBCCCGEEEIMPPqQSwghhBBCCCGEEEIMPMvWk+uHP/xh5AlUrVajv2fPIfZaQg8d9t5BD6J+fj/oycVeO/xuKC/k8cNlsS+hNszCnlKhmEIeUma+362W9wDhdvBdHmssy55NnEafMvYR4hgwfp4X7hvGFPLkCvmJ8bvcBvtlhfydcGw5vunpaZc3NzeX+B7PA44R95Ph+JHQu/3qRXhMsE32TeMxCnlyhTzEQv0y83Ma8kbrFx+uOfbk4rWKaxnn06x3D+C7vP7Qj4896tirb2go9o06dOiQif7M/MQqS5UPj/m7zo89ubrXPuDKzW/d5tIh7x32xwrlvXz+C9Fzq8l3ln+3m4W1mvVrldPFarxWe/y6WvF+Zq+sDnlBza2J3820wudANw17kupJt5LTKfJEYl8wtPlh/6RuJm6z/ImXubybiy916bP2x0E1xn087AuG/k5MvuY7x15Rvl4YP8pDjyszs0Im3vs81uydFvIBy//qn7n0+lTcl6kP/ZHLc1ZaFGCukTwv6JV1OF7yL4R+8/hkGr6hViGul+vJ1eMxqY/4c7M0RV6lsDbYC68w49tkvzGE2wnB+y5fi/ddaF2Yhe+smfXx/XHo7Jtd3tjtF7j0vzzw4uj5TzL/4fLqtO+ac3G8v7T2Iy7v/ePnR8/F2bJ/r0D+j614XtDLjX3dhBBCiEcCfZNLCCGEEEIIIYQQQgw8+pBLCCGEEEIIIYQQQgw8y1aueMcdd0QSKZQQsSSIJU0oJ2LpG8q8WJbEErCpqfhXNt9zzz0uj+VEExMT0TPLibhejI/rwb5wPUxI9hhKs8SPZVMoS2OpVkiuyGVRPsZthPrdTzqI7bDsjEHpWyg+luKFpI2cxzJNjHfDhg0ub8uWLS6NEkVeY9gOr3mWvqFsjucoJMvlstwX3D88fhhDseh/tTjPN9bL88ltYpr7zWVRqsf7ObS/FyO95DaxXt6js7OzLo1jf9NNN7k8nm+sl2PHeeDx4znkd0V//vf9n7JMadTMzFZ/IN6TqbXh82VoMs7vkvKpk06W+OXpaL/yq5+Pnq//wLNd3um7/f7desYfR89nX+frrZ105BI6lIRx7ClSG4WklyzZbENduRnaO0Vfdr4AcnKqhyVrDoqnWYnLFkmhW2r5zqFEEefocDq5SZa28Zih7LA05e/Ydg72NsWOUjzOz5DMjGkOx21+4Cef6vJGL7/Tp5+wLnre8oznu7yT7jgjbp+kd520jy8kgeXxQzllWLZHclRaCzOV2M6gss9LxOsjZGeQTl5TzRKtVZCj8vxmaN3gPLWK/szNtpJlj62Cjw+ljKUpv19xP5iZFWfi/ML9j3Z5J3/W1zv6qk9Fz6d/+lMub/eT/5+P6eTY/uC9+17k8rb8IP4ZZfcZu11eruHXNfa7KYliX/7wss8+ou1f9uzHR88f+/dvH3FZIYQYVPRNLiGEEEIIIYQQQggx8CzqQ67Xv/71lkql3J9t22Iz3nq9bldccYWtXr3ayuWyXXbZZbZ3795jHrQQQoiVie4ZIYQQS43uGiGEWLks+ptcj33sY2337t3Rn69+9atR3pVXXmmf/vSn7aMf/ajdcMMNtmvXLnve8553TAMWQgixstE9I4QQYqnRXSOEECuTRXtyZbPZHq8hs8MeVu9+97vtgx/8oD396U83M7PrrrvOzjrrLLvpppvsoosuWlQ709PTkacN+kh16Heqs2cO+uBwWYS9qbieWi32gXjwwQddHvsThXx6OAb0EQrFzm2EvJXYY4i9qrgdhD19EB6j0HiGfMDYW4nTCPeF68Vx6ecphWVD9fTzicL5Zd+loaEhly6X41+7vXnzZpf3Mz/zMy69Z8+e6Bk94MzM7rvvvuiZ1xR6cJl5DyzuC89ZyJMrNPbs9YRjxPHx+kNPqdBa5Hd5nbDHHvYbx32hmEJrF8eTy3G/cf+G1rGZ7+vBgwddHvuqYb85BmyHz4WQL1ipFHuvdLtdd6Ytd47XPWNmVt5ftOzQYV+5+a2xv1yq7ddqq+DnJV9LQ1mqNPDfRx2y+tqwYyx6funXv+jbXOP97h71QBzD5Jm+0XSb9nod/BWTw+mJJwR7K6Ub/lzFutiDi5kvxvEWZtgDyb/bKsXtoteTmVk7F9eTKvHAB8pSPdy3uXK8t4aqfh7Qz8nM+xOl6VjI15JHn33BDLzIuJ7CXt/md1/wreh5zV/d5/I+84VJlz7jtpnoeceTft3lPe/u/46em7TemOnxeExGDvi7D8eW6Vh4LbiyNCaFOfBZLfkxyNXJaxOmn+eTfd7SHfAUbfG6Ia80mO8eHzVaC2nY/OhhZmZWmAocDAWfRL+xk3/g/Txnz/N9m3/SX0XP5/yZ92N7+kXfcen7yj+KnjsP+vsDfdZaBc7z8231RRwcy5jjedcsF+S5JYQ4EVj0N7nuuusu27Rpk5166qn24he/2Hbu3GlmZrfeequ1Wi275JJLorLbtm2zLVu22I033phYX6PRsOnpafdHCCHEicuxvmfMdNcIIYTw6N80QgixMlnUh1wXXnihvec977HPfe5z9o53vMPuvfdee8pTnmIzMzO2Z88ey+fztmrVKvfO+vXr3TdWmGuuucYqlUr0h7/9IoQQ4sRhKe4ZM901QgghYvRvGiGEWLksSq74rGc9K3o+55xz7MILL7RTTjnFPvKRj/TIto6Uq6++2q666qooPT09rUtBCCFOUJbinjHTXSOEECJG/6YRQoiVy6I9uZBVq1bZmWeeaXfffbf99E//tDWbTZucnHT/87F3794F9e4PUSgUnM/NQ6TT6cgjCD1q2EeIfaNCHlPsOYSwjxC2yV427HuEsE9PyCeKvZXQX4cv2JB/EsfOaXyXPaU4vpB3EdeLcL+xn+ylxOD881oI+TtxvSH/KZ4zjI9j53WCMXEe14seTpOTky6P0xgv93sx/cQ9wB5S7EWG8x3yluP8kDcV5/FaxTEKeXuZ+b7x2PI8Yf7MzIzL4/EMeeFhvNwmz3fIf4/rxTZ5DkP+YjxnITi+YjH21MF+9fNCW84ci3vGLPmuaZU61h06vO7QmydDvlDFKZ9ujsBarft5QD+gVs6PPXp5mZk1y3F+ukXnwIjf681SvG7Y76c3hviZT/W5VfFeamfJ56jhz1z0y2qWfE13Pvl7Lr3pjjPj+GaO3LOH6+31T4rT8xkq24jjw36ZmWWa/ozDsec20YPLzGz0QHw/t4p0NlK9rSKeeeSnCa+2c2GfKIS90uY2+rPp5NvOjZ6Hzl/n876436WLhbidbJO8QPfGfoG54nqXV93ox2j4UHy+sEcd94U9sRD23UJ/tB4vLRhrP85m8zRG7JcVisfvdV+WtmEwhtKU34c8Lkgb6u2SVRvHh/uH8zr0oyyWrV7yGJeXa/iyW289JXqe3ug7PrU+9gYtVkdcXqqd/PNzHvy52gPs1bWU/6YRQghxfFm0JxdSrVbtnnvusY0bN9r5559vuVzOrr/++ij/jjvusJ07d9r27dsfdqBCCCFOPHTPCCGEWGp01wghxMphUd/k+u3f/m37uZ/7OTvllFNs165d9sd//MeWyWTsRS96kVUqFbv88svtqquusvHxcRsdHbVXvepVtn379oH+LSRCCCGOH7pnhBBCLDW6a4QQYuWyqA+5HnjgAXvRi15kBw8etLVr19rFF19sN910k61du9bMzN7ylrdYOp22yy67zBqNhl166aX29re//agCS6VSkWwHJUws5WH5E6ZDEjqWOYbqCUm+uGxIvsb5LDsrlUqJbYTkivjeQulqtRo9s/QyJB/jGFAKZebHF+VW/C7L4kIyNB5rbhPHgaVaITkq1xuqh9dN0nsLlcVx2LFjh8tjuSJ+pZ1lpDhm5XLZ5fEcIjxn3G9cc7z+WEKHMfCchaSrvLdwjLhNlvxhvLxuuF6MgcvOzc25NK5zrgfb5PnlekNy6FAe1xOKNyTh5P0akibjuA+SXPF43jNmh2VLmWzv2ZFp0BlcorsHpGdZkjuhnAglhmZmtYqfw9JULE2fPpnmt07rZj5Oo9zKzGy+6PdWoxSvhcoDfp20c3E9Bzd5advGuzb6NuFov/TXX+jyfu7gj1z6N097UfTc/MwrfT0k8UN5W48csEXychjfDkk4WyDF4/HKz9DZXornbGjC59UqLmnD90CjdEfMnEZaM4BlhijNY0kfSyZ9Hp3H877Nsfvi++Nnare4vPd/7KdcurI3fn7bU8ddXuu2WHp1YKuXfedJepZpgTQ+H54zlCCmze8llJ8ybZLmoTyQ32OpLcr6OiRrTbVpznJwF6bDEnHsa6EW/rE5Dc2yrDBbg/hyfJ74stgXHEszs26OpLawVlLcKP2YXx+J+zI34u/JXD2e31SOrBlayf3uuHHXXSMeHv/+uf854rLP/plz+xcSQpyQLOpDrg996EPB/GKxaNdee61de+21DysoIYQQJya6Z4QQQiw1umuEEGLl8rA8uYQQQgghhBBCCCGEWA7oQy4hhBBCCCGEEEIIMfAsSq54PJmfn4+8ckI+VuwjhP467OeEfjZDQ0Muj32ZsF722gn5d/Xz3kG/LPbOwnhDnkIcL8fOv74YvaC4bMhzKORNtVBMSfWyLxTPGfab4+F3V69eHT2znxOPWcjnCNM8JtyvkDcVrw1Moxeamdk999zj0sPD8a+o5zaxb/084bBN3h9cFvvSz/MK87lebJPXG5fFNMfDnlKhsjwOmA6tRbPwesT559i53pA/YKhehtdGyAsv5PXFbeL6rFRik6FOp2MTExOJ9ZzIpNqH/5iFPXO6NJ3or5Sf8uum8K3Yq2r6J7cE22dvIx+bjwF9rdrky8NWPNOrp6Ln33/u/3Z5W2biNfV/uq/xbdz+iy6dPRB7AJ7UPejyRuZ8oz/c8/To+VTyx0IPMzMztEhiv67mMHlQgtFVuu3bzDXidJbsCqvr/R1WOgQ+fjU/7q2iv5f+8opLo+e/Wv1klzf9kb9y6S4YMfG8IKkGnRFUFOd7aKpAZf27s+vil/M1f3b/0ns+5dIz547Fiff7Nusnx/ck+y6xD1iP3RPA8xvypmqX2M8LzvJcsqdTftbHNztO928ueS91ir4semt1yUeK07g+e8YgsA9b1GZ9PG4zFb6ynOceryk+F3D+e3zfhpPvj9LUsEvjGsu26GdDGttmEbw2cf23/T4SJw537vpeYt6Zmx53zNqRD5cQ4kjQN7mEEEIIIYQQQgghxMCjD7mEEEIIIYQQQgghxMCjD7mEEEIIIYQQQgghxMCzbD25EPQnYh8h9r1BXyH2q1m/fn1iPbOzsy6N3jbsTcXeRdgO++kw6OmzdetWl4d9Yf+mkDcQ+/uw51Wj0VjwvYXqxX6zF1DIIynU734+TAj6VJl5XyEzs3K5HD3znO3atSuxXe5nyJMr5FvGa4H7hu9yWfbzqtXIQCahHl7HPH6Y5jb5XZ5/hD3sEPbdwrXBXmi8t5B+Yx3yvAr55vH89vMJO1JCa77f/GJ8XDbkRRbyNOs3JlgW56jfuSQOM18CT5oaj5kf++IhOP++/S2Xl3vPv0fP41TLgU9c7dJDk/FamFvl10nIn4i9gQpV/xen3Lgqen7OV/Yn1jP2mkMunT9AZ1wp3kv/8LHPubw/OvfbLv2YL8VeKZMn07lAHj/s+ePKNtkLCrzSMnSHgXdalv7frjhD+wyS9233Y/K11Ze59Pg/TUbPf/fTt7i85+/27cytiX8m4HlBfye2X+OyGchnX6jQCdYskVcpenBRXXNl72U5VI3P/XwtedzNzLLgKTZfIM+6zJGfMYUZ8nsqor8YrRM44+YqDZeXa/gzN1cPeGcF6PG4avmX0ROL92SKyqbt2Jy16NnVLvk6i1N+jGrjuGfJCy/rF10KBqbLpnCdzILlzMxaRb+fU230DIvvGm5PLB0/fP7fJOad+q9XHcdI+sN+XSGPLnluCSGOBfomlxBCCCGEEEIIIYQYePQhlxBCCCGEEEIIIYQYeJatXLHVavVIc8z6S7dQvsOyQpQTsSSIJVchqU9I4sf1suQPpZcsVyuVSon1cL+xTZbtsTQK40Xp4kL1Yj6PLfcFxzck1eJ5YDkb9nVmZsblsRwrFA+DfeO+hMaEpW44/1yWwXdDUjfOX2itLxTrQuA48Hhxv3FthGRx/drFeDl2fi9UlgnJNENlmcXIU0NSPi4bkuhyGsd6MXJBngeMgfsVig/3oOSKyXQzh/+YmaXh6Ozk/Por7PMycKtDesNmlzXbjs/y+2unuLzTv7Tbpdtnxu9mSv5szLR8DM3h5DOvS2th7pNvixOvX+vy0tW4ozt3ne/yVlG9jTVxTOUDPr7Hfu5C3+ZYvHaLM/4cZWVUqxiv5VxAumjmZXLtHMmxQD42X3JZbj7NzFog69v6MZ/3z3/rhaWvPBg/33mqP/eb36EzD+RsPXJKkG+lKvTzSyNZFofjY2aWbtNeB4ldq+jXRUgKivJEBtvnNsy83JPJ1n18KEFkuhQeKuNSJJHE+c3Mk11FYEyM6umRU3ZwTVHZDt3dUG+Krp2ed6GddCd8hyE8fsh8zq+FundxcLLSdsmfUyw7xJg69P/cWE9pyv/cNrrbp7vw6tx4HN98Tf93LnoJyROFEGIp0G0khBBCCCGEEEIIIQYefcglhBBCCCGEEEIIIQYefcglhBBCCCGEEEIIIQaeVHeZmbVMT09bpVKxsbGxyF8GQ2TvJwbzh4aGXB565LDfD/tYoUdXyM+J2wz58jBcL3pVsS8Pe0FhWfYT4zHCePv5CBWLsV8H9xM9fjgG9t06Wp+jUB6n0cPMLOyzFlo33C/2+grNC5fFfO4Le5GFfKKwHp4HbhP7xn5i3CbSz+sL80PzwPCaxxj61YNjwmuIxyjkyRby6wrl9WsT56WfZx2mQ2cGw/VgDDyfo6OjLo37ENd/p9OxPXv22NTUVM87JyoP3TVPefOkZYcOj0kKtnfbbyUr3DPh0u2Nq6Lney7a6fLO/AjcPeOrXN7sFj/36PfDPkbsiZQHv5tmyZdlzyH08yrtJo+herxHW2u8R9Ps2uQ7i8k0w15awXchPo6dbIScd1WrQJ51kOT32AcM6RmTXfsTy97zAl/Rqn3evwt9j9gPC/uZafkA6yN+rHP1+F32emJfJoS9n3he2vnAu+Brxeutt51gtiPkMcVeae1kmzA3Rv36xR5dCI8fzhP7qHE9mM957JuH85av+XibpTgGbpP71uMhBnBfnM8WeZGFfMFCZVd930/S91/+JZd+0/q/iJ5/78HXxnXOzNq+x/287pof89A9I4QQYnE8+N6bo+eTXnJhT36/e0bf5BJCCCGEEEIIIYQQA48+5BJCCCGEEEIIIYQQA09Y+/cIkkqlIpkOSppY3sSyH0yzDK1er0fPLEPiNMq+WKIWkqz1k5ah3IgliAjLprjfWC/Lprgvi1GkhiRqPNY8Dkion6GxDsnX+N2QVNDMS7e4TVwbLAHj8WTZF8JjglK9ox13Mz8OIZkjl+U1z+CYcF8WIyPFfobWOMfbb39gOyidXQiMbzGS4tBYL0ZOyWO9GJlwqF7uN87T+LiXSZ111lkujRLtHTt2uFj27Nljopdu5vAfM7NMA+SyaTpzTx5z6bsv+mH0fNo3TvV1TsZj39myllt0qWwD5WwsWeqN9SFYOtYxHy9K93adW3V5xVp85s7nZl1eruH3UkjWxWBMLH1j+VVIjsUSMJZV+bJxR9slkuWxjA/KNtZQRWs2uGQ7F3fmpDvobKRm3LyQFK/jh9OB8kQzs1YxrrhV8GdjrkH3b2BMeGzb2bA0/SHSlizTMzNLQ7wsDe2Vy0L7ObqbW0f+/6sYO/c5V/djEpTAZmhNBSSIITlgT3wkK8V1X6sk38ccO+/nNiyNfvHgGPH+De3DxlDd5Y09OBw9r37d1S7vK894u0s/uCvu213vfX30PD07byeZEEIIEQbliIspOzNXtW3/7xl939E3uYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwKMPuYQQQgghhBBCCCHEwLNsPbkajUbkW4M+Pf38k9C/Bn2D+F32z2HPHPQK6uddFPJeYu8d9OnhejGP2+B+Y5rzGK4rlId96ddvTHM/Q++FvL5CHmtmfoz6+U/x2kiC5489m9AjiWPnGEJt8jxhXdwm1st1cgxYL48114t7gseW/bGwHR4jbJPXSWiMOC+0R/v5syEcA89LaD9jup8PGJbl8avVai6NHoAcO+aZeS+t0H7m+NijC+cf4wvtzxOdRrlj7aHD89rJxeOXm/FzNrver5sN926OnrNVv8/mnvLo6Jn9c3K15LlIdcLp+QKcz3Sss6cPMnpgxKXb+TjezLxfb+zfhF5GmWb4rnExpdmj6cjXIJdFz6tuh7yWoB32H0KPKzOz/AzcWTy2JZ/O1uOyIW80s14fLgT9qPrNWXEmPut5rHs82NLJeUynz88ID7H3lF0uPTRb9umZ+JzK1/IWAn3LsuR51RzpEzDWw4MGcL/ni/Ff8Bpi3y305GoV/ARym7gneF5wL5mZtUr+HkUKMGb8Xsg1LUtefezRhfH2+HeRFxl6vc3nfKz53fEd9obdX3V5Ox/0d+o9B+N99xO1+DlfO3IvUiGEEGKp0De5hBBCCCGEEEIIIcTAow+5hBBCCCGEEEIIIcTAow+5hBBCCCGEEEIIIcTAs2w9uTqdzoKeXOwbNDc359LoX8R57E+EhLyg+L2QVxV733DZkOcQ9y2pDa6nn6dUqJ6QZxj7BoW8oUJt9huTkGdTyFsp5DXG8PxiDCGvJ46Bx4D9k0L1cjqfj/05OL6QL1OhUEhss9FouDyON+TJFRqH0DxwPDwvGBPHg2Ng5vvCbbJ/F/pTcVnuC7Yb8uRiQnMYmnszP74hPzYzP4ZcD7Y5MTHh8u68806Xnp2djZ4nJycT6xQx6U7s7YM+Qu2in6Pybr+uW2WcU/Lxg2nqkjdVo+Lbz1fB+46OtDZZxKG/V7rBXjt+jnMN8Bcjzyj0HkvR0T1fTD57FuMDxv5YIU8u9FIyM7NF+Hch7HPEPkw4nuyjxePQzsXzxr5qXBbnLUueRPMFmDNjnzLyqiqB91OL26QxgXWFsZp5vykz71vG/lPjO4ej59KhzS7v7gvvcOlWIV68lX3eDzBXJ//MwPhxv9G3jPuCsDfVYugdT/A1JZ+tZsnfJ+hbV6v4nytLU0O+bDO57Fw5/pmqMBf2NHN19uyHZI8unoduJnk88w2KIR2388Jb/9tl3fq6J7n0k9fEm+nUyj9Hz51M1cyekNimEEIIYWZ20ksudOkH33vzEZc9Ehb9Ta4HH3zQfumXfslWr15tQ0NDdvbZZ9s3v/nNKL/b7drrXvc627hxow0NDdkll1xid91116IDE0IIcWKie0YIIcRSo7tGCCFWJov6kGtiYsKe/OQnWy6Xs89+9rN2++2321//9V/b2NhYVObNb36zve1tb7N3vvOddvPNN9vw8LBdeumlPd8KEkIIIRjdM0IIIZYa3TVCCLFySXX7abWA3//937evfe1r9pWvfGXB/G63a5s2bbLXvOY19tu//dtmZjY1NWXr16+397znPfaLv/iLfduYnp62SqViIyMjkaQHpT0s6WPZD0qYQjJDlg+xbArf5SHiyw3r4nr5XYyX5ZQhSRH2y8xLwrifIQkYwzIvrIulZVw2JL3E+FgWx6AMbWhoKDHPzI8ny+I4Poyfy2K9PEc8hzgmIUka1xWSqpp5idqpp57q8jZvjmUj1WrV5f3gBz9waZQD8privuH+KZf9r4ev1Woujf3mNYVlec54LWB8XA/vZyzL9fBY4/iFpKD9wBi4zZAUuJ/cE+eC83jMcH2G5J5cz6pVqxLLcvuzs7M2NTVlo6Ojtpw5HveMWXzXPPFdBy1bOjwm2TrMA0ms8lMkCcslS+o6cHSyLI5BqRvLEx8OTnpJEjCUMPWTFaKUsFWku2aWZNYBmSHHgOl8za95li+ivA0lfWY+fpZmFWboLA9I6EJyT57D+UJAUkcyUizL8fGY4Pj1k4biuyxB7FA7w4fihfUvT/Gys8Lv3h49j5231uWdeYmXrHUhiMr+NS5vqOoXL459t4/KsJNOHs/6SNw3njOWo4ZkewxLEl08NH5z5fi+W7VvFZX1EzO9ejp6zrR8DCgPzJD0slHyVgNZeJfL9q7d+cSyLPGcrcDdTXNWOhQfXPl7vTT+3mf7+DKtuOzGHauj5/m5afvK767SXfNjHrpnhDiR+PZv/d0Rl338375qCSMRK5l+98yivsn1qU99yp74xCfaC17wAlu3bp09/vGPt3e9611R/r333mt79uyxSy65JPq7SqViF154od14441HEb4QQogTCd0zQgghlhrdNUIIsXJZ1IdcP/zhD+0d73iHnXHGGfb5z3/eXvGKV9hv/uZv2j//82HTyT179piZ2fr1691769evj/KYRqNh09PT7o8QQogTk6W4Z8x01wghhIjRv2mEEGLlsqjfrtjpdOyJT3yivelNbzIzs8c//vH2ve99z975znfaS1/60qMK4JprrrE3vOENR/WuEEKIlcVS3DNmumuEEELE6N80QgixclnUh1wbN260xzzmMe7vzjrrLPvYxz5mZmYbNmwwM7O9e/faxo0bozJ79+618847b8E6r776arvqqqui9PT0tG3evNmGhoYibxr0wWHPHE6zxw+CHjrs9RTyfmJC3k9MyDeKPX1CPmDsuxWKNeQFhZ49C4FluR72BcN8jg/HhN8LeZGx3xmPNfY15FNmduRebtzmmjXea2RmZiZ6Zq8nXjfoc8X18njiu4961KNc3jOe8Yzoed++fS5vamrKpfE3/bDvF6+x2dnZ6JnHlscP4w35sYX8zjiGUBtcL4817zPM77dfVq+OPUN4/HBtcD0cb8iHjscI+8YeXOx/hv5i3M/Q/mW/NgT7sgjrxUecpbhnzJLvmsx8KvJCylfjOW0X/PutcrLfVIr8kpwPU4k8Hct+neRr6NNDnkPkvRPyLuJ3Q/5TxmmA/ZPQp6y3LHl9Qb3scRV6l72p2CcMYQ+kYi3eL7WKH9tUibyLivFe7+fZ5GJdxHfeG2UfX6Eav8z96tCPDs1S8pixH1qmCV6g837SUh0/nsXPfyN6XvuDB13eh26Iz5Azbp91eT94qvccesbO2LuI1zGD64jXLa8bHJce/zjwAev0iA86iWXbeT9e7Sx5tEIM7GM1O+bviLX3r7Ykhg75Sfy1F7wgev6je//E5T36vy+OntlbLjtVcmkcE/YPmyvRHQv+XV0yb2tQO/l6HC/PS3UN/Hxa8T4nxaq/s1qFuOyhLfG6adf8GlrOHM9/0wghFob9u+TRJY4Vi5IrPvnJT7Y77rjD/d2dd95pp5xyipmZbd261TZs2GDXX399lD89PW0333yzbd++fcE6C4WCjY6Ouj9CCCFOTJbinjHTXSOEECJG/6YRQoiVy6K+yXXllVfak570JHvTm95kv/ALv2Df+MY37B//8R/tH//xH83s8DcbXv3qV9sb3/hGO+OMM2zr1q322te+1jZt2mTPfe5zlyJ+IYQQKwjdM0IIIZYa3TVCCLFyWdSHXBdccIF9/OMft6uvvtr+5E/+xLZu3Wpvfetb7cUvfnFU5nd/93dtdnbWfv3Xf90mJyft4osvts997nM90jIhhBCC0T0jhBBiqdFdI4QQK5dUd5mZtUxPT1ulUrGTTz458sNBrxv2vUEvG7Owvw566LAnDnsX4bvsGxSqt59nGKbZNwj9dvrVg/3u50c0PDy8YBsLvYtprodjQL8i9k/CsjxHk5OTiWW5Tf5BAueJ+8JeRjhvHDu2wz5WnA55woXWRmhszbx/1wUXXODyHv/4x0fP/Nt5vvKVr7j0rl27omce28Wsa55D7DePdcgPi+cBPbp4bPldLMv1hPrCnlchnzxeY3imsJ8Yr12Mn2NnD7bQWcTt4LoK+YAtxjsQ57fb7Vqr1bKpqSnJJ37MQ3fNk98yadmhw2OSn4rHbL4UVvOjD9d8KblcYa+fs9rJfg90oJlcgzzrAh5cIV8jM+/DlfYhLMpjKluDMy1DPnRlHwP6grG3F8eAvmU9bTaSvavaOX9uYb95TEI+Vgy/i33p0Hjla+S1CV5aZIlkjVJ8DmTJ+6l3zpLzOD6OCWE/qvxsfHbeMP1Yl/eVv78zej5tq79v9/2L9yt62df/O3oeOeAXPceHXm7ss8Vp7Df3i+tNasPMbL4I/l2ZsCccwvtsrux9Q7tQ19juisvb+dgfunT5tK9Hz5MPnuvyTr317Oi59ICfo7mN/n6rj8QbpjTl72b2nmMfLqRYpXsdxr5V8Jsy10i+NxulZA+2NHqq1abtOy/ZoLvmxzx0zwhxIsE+W4tBnlziSOl3zyzKk0sIIYQQQgghhBBCiOWIPuQSQgghhBBCCCGEEAPPojy5jifz8/ORbAflOv3kTixpSirLX2+bnfW/9hjrYYlaSIbGciKWHoVkSSjNm5vzX5dnaRnKsULyJjMvz2IZF8s2MQaWCnJM2G8eo5BUC/PMzEqlWPbAsYfkgNzvI5Vu8bshKeNC8R4toXWzc+dOlzc1Ff8K84MHD7o8liTimusnc8Vx6Dd+uFZ4DHiMkuLhevutVYyP2+D4sC7ev1wvyg5De5IlmyHfDd4PvLcwzVJGlkEi3E8cE17HTJJM+CG5ouilNta0TCTFieef5XUdUvKgHC/jp9eyU/HLLEMa3jHj0jOPiWXLs2O+opDUiOVYLMXrdkB6TrFnWsn/v8XSrVQR5PjJ297MvFwsR5I+lkiiDC0kvWNYgtjOwv0WkFuZeVkc18Ok2vF4svxvPrANuV6UknVI7mmknuy2UbuaLP8zM0tbnM/zyfHOrInv+Wt/dYvL+7XHj0XP73iql1V95u/+w6WH1sT1zlW8hD20VnFdmJnN53x8KK3l9deGshmSe/IewHdZwpfq8HpMnsR8w/elWYilejXq98Z7TnHp1gMnRc8nUbxOKjju1+rBn3+vS0/OboieT/nyM1wejpeZWQf+v5oliLwe07DGOizvLRz5HdGB8cu04r7wOAshRAjJE8VSodtICCGEEEIIIYQQQgw8+pBLCCGEEEIIIYQQQgw8+pBLCCGEEEIIIYQQQgw8A+HJhSzGg4vf37RpU/TMnjjVatWl9+zZEz2zl03Is4m9gEJwDCGfI/ZEwnHgPPbtwTSPF8eLaY5naGjIpRuNxoLPHF/IT4zT3Bf2lArFHvIr4jkMea6F+sLjF/KNCnmjmXlPJ/bdwvU4M+M9fJpN/6u8sW8cX8hLq5+/3ZH2m/sVmu9+/myheELzy15aXBbHkOcb9yF7cHG/Q/5i7LuFMSzG14/Bd0PlGPTbW8x7JxrpTiryuEEfrtw+71c4v6bk0ukWjOk3vuHy/r83vTp6Pvk/pl3e/l/2vkcv/vzXoufqmf6cYs+cXCNej+zBxRZD6NPEnmHsL4ZkW36tsjcZkq/6ss3YXszmC+ShRz5CLj6ql32Z0L8LPYWYTIvb8DWzT1gIFx/V284l7yeOHX3CuB72S0q1A/cdvZtqxOlM3beZqdMEr40f3/UfH3VZf3bG7dHz1n95on9vDXl7jcX30uiBkcRYzfx8s59YaByyLV7X4Jk4S/cQzUO75O/GpHrMzNKt5Hlhj7P6SNwuesCZmbUK/g4bORD/nMT7LtOI4737ST9yeb+56kMu/ZXhx0fPB2vPdHnoF2dmNjse93uoWqSytHbzyR5nOEa8z9j/LIluJ3kNCyFODOSzJZYD+iaXEEIIIYQQQgghhBh49CGXEEIIIYQQQgghhBh49CGXEEIIIYQQQgghhBh4lq0nV6PRiLxo0JOGPXLYQwd9hthDatu2bQs+m5nt2LHDpdFnaGJiwuWF/LH6+QihPxHnoTcQ9xP9mxj2m+I0tsPxhXyjOC/kTcZlMX5uk/2J0MuI80JeRpwX8k/i+NBPifOGh4cTY+C+sH8X+mWFPNbM/LzMzs66PJzv0HyahX3pFuMRx33D+HmscQ/we+yPFVoL3BesdzE+VqH9wXWFYuA5Y/+zkEcce82hJxafGeznhWnOw3r7+Z8hOPfdbjdYVhymDddJ9v6dLq9x2pku/eCZ90fP//eqv3B5I//njuj569/0nlyb/83vj3e8d3v0/MwHPuzy1u3c5NLow5Vr+HXSKvj5zYMfEHtwZeDYqq7358vQpC/cDfxXWJeOlyz4RPF780XyioT84X3k0VSmfeY8k3w96E9UL/v9in5YZmb1dXDXdHyA7AXVAZOzLnmjsccUehl16czN15LPYPYIw3mqVXxfuN7CDPhnFsM+ajin7LV06s0XRM/1EX+fMSMTsekae8BxX9iTDWGfKPaXc/XCvPAams+F79gQGEPIC83MLFeH+PyPnDaf8/M0tS6+y3lNjeyLXx7bu87lvXnn7/g292+Onk8hn7e5MT9P+Zo/UxCepxbU1aXMDvy/dyd9dGPLnnRCCCHEI4G+ySWEEEIIIYQQQgghBh59yCWEEEIIIYQQQgghBp5lK1dMp9ORNAjlRSzlYRlQSLqF9aCUyKxXdoZlWSLEbYZkexwvlmVZV4hQPSHpk5mXWHFfuCz2m6VaLJvDsizVwr7xnPD4oVyRYXkYttNPBol9C8nMWMbFY4J94dh5vlE+20+ihjHx2IbklAyPAxLqWz9pKK4VXjcsEw4RkiCG1h+XDe0tjo/TIckzluVzgNcmxtBvr2NfeH5D48dzhrHzexwvSp6xzW632xOD6KWdi8c+tXa9y5te4yXF84V4bdzTOdnlddvx3Odzfh1XNnkZ/X+vj/PXfd3LE1n6gysuRWqiQjVZ+pY/QGd3Md4Dow/4/TC7zq+/DEgFUY64EBgTyxV7JGHQt7k1dJZT31AOyGB8M2Mz9J4PYtW+VXGbZb+32yR9Gz4U77UuSb5C48DyROwLy+34/xkzENJw2/980BihMy4XjxnHw/LUFDTL84CKtdJUweXR8DnZJks2Q/JEjNXMz5mZWR6kjiyBrY/Ea7edDUvoUIIaWjOH24GYWuGymVby/wdnmn4/Y70sp6yuiX+mGt3r3xv75E+5dLMMdgsUH8twW8X4js3V/X7meeqk4/xmieS98C5LdBtcthGXlURRCCHEckPf5BJCCCGEEEIIIYQQA48+5BJCCCGEEEIIIYQQA48+5BJCCCGEEEIIIYQQA8+y9eRCP5uQt83srPdJQX8d9iq66667oucHH3zQ5R04cMClDx06FD2zfxd7LSH9fI7Qb4e9d8bGxqJn9hSanva/hn5ubi565n6GPLrYRyjk51Qul12axx7T7Mm1atWq6Hl8fNzl8fhNTExEzyFvNDPvkbQYTykmaX0tVA/Gyz5l/C6WDY2tWdhTitdGqM2k9s165wXhtRDyEOM1he+ytxz3pVqtRs8hjzWuq59/HMLrhmPC8eR6MK/fXsL80Dlg5vvK/eR2cDw5D/c6e3Kxvxim8bnT6bh5EP3pjo+69Mavz7n0hgfi8++Bx77V5eVe863o+YnfmHB5f/rKx7j013/r76Lnka1+PqdOPnIftdwhX7Y9Eq+5VM3vjz3nJfsgrrvTn/szm+BMozMjVzs2/09WmCJ/p5LPr1Xic3dkr9/bDz5mf/T8mTN+zuXNkgfWC2/797ieiRGXNzfi5xc9hzLNsC9iCPTDYm+nrP/RwtIt8CRkTykfrvO56uc/xR5nPi/gd8beaA24E2hpdjO+nmYpfpnbYG+ybCN5fFd9N167u5/kz8bRA8MujetoeqO/q0NjxP5T9WHy3oT5R/8rs14PrE4m7lu25fuFPnDzRfIqJZtG3Fvsd8bjWWgl/yjPY819Rdp58Hpth/c25rez4P+YTv7ZRYiVwPSDB4P5oyetPk6RCCFC6JtcQgghhBBCCCGEEGLg0YdcQgghhBBCCCGEEGLgWbZyxU6nE8l9UDLEUp5QmqVRKDVjmeO+fftcGiWKLEsKSZpY0seEpIMoz2JZHMumWKYUig/hNrkd7CtKA816JXRY1+iol/Y85jGxJOdpT3taMIabbropev7+97/v8lDKaBYea5a34fxzm6F4GJSI8ZpCKdlC+QjPIZYNSfH6ge+yLI7TOL8ca2hNcVkcE5TZmvWOCa4bzgu1E5JaMizjY4kxSm953RQKhcR6eUxwjXF8LJnE/cNthCTFXC/KDEOSUjM/fhhPP+nsiUyhmrNsu3etpXbt8X9RpHWSjtfGHU+7z2XdszU+V3/v3P/j8rovu8ynH7c+ej5wml+3Q1W/rjOteH5RrmZm1ljv+5CB47u+laTnhVhukWt4+V9rxO91J0uiNjtlfycUQS6W8duhV86Wj9dydT1Jk0kqhRKoyZP9GXJb97nRc+3373J5q1s+vh/8ws9Gz8//3890ed+78VUu3UmDFC5PEuw1yVJllqihtAznz8ysk0tOd+lKYIkaqsI6afo5iCRpWRjPXIPOnsCPLCGZI8MxYF8XI2vldTP92KHo+XOnP8Xl3fVUH/yb3vq16LlQ8Wd3o8RzFg/gPK3rds53vFWYh7LzwbJIoeb3Vmkq7gvvpeKUXzcN2Ft5Gj+WIFofuSqC66hYJVk/NMNriPvSKsRj34GynYAcUohBpZ9E8UjLSsq4/Nk58aNg/paxU45TJOLhom9yCSGEEEIIIYQQQoiBRx9yCSGEEEIIIYQQQoiBRx9yCSGEEEIIIYQQQoiBZ9l6cqGHDHom5fP5hYov+B57SqEvTj9vIGyHfW9CvlvsfcNeRujpwzGghw77RHE9GN/Q0JDLY98ebIfrCflRcXzsFRTyF1u3bl30/PM///Mub3x83KVLpfj3xd97770ub9euXS4d8k8K+Q6xtxKWZS8vrgf9nbgs+5ThmPA8hPzQQvXwnPE84DyFvJ4YXsehdcR+U8PDsV8N+2Gxzxv6YYX2A6d5/Hjuccx4bLkszj/Xi/Hy2PK8hGJnsN8Mx4D7mfMwJm6T13XSngz140Qn1Y69hwo7Y/8zG1/lCzb8uk6NxPmnfWODy/vABdui5we+8Bsub82TvYfd7Fh8T7EHF/swoU9Oj0cTXUttqKq428e+5Ttxm92N3k+x5Y9nGzoUr6NMw5+NzYqPoV5J9hFif6dMM643zRZDdGyhd9A9F37L1/Op2P/k9m9OubwDMz7ep+Z+GD2//ilfcHmvuuvPXLpVgPOllTwPZr1zgTSH4/3cLPlByDX8ecO+TC6eIp2HtXjvz1X8+Tw05c+/ZinuC491Cvy6eA2lW+RVVYz72SEbu9CYVNf49Vd5gM5Z8CLLzPgg7nvm/8TvHfAL47S0P/+++9IvR8/b/v0Sl1cd8/Xi2DdKfvzYE65VhDuizj5WfkCx3k6GBjtg/9nJ+UystjFCfqOB9dYq0s8o5JWGnnG1ET8vuUbyPwlydR9fphWPA7aZDtQhhBBCHC/0TS4hhBBCCCGEEEIIMfAs6kOuRz3qUZZKpXr+XHHFFWZ2+JtTV1xxha1evdrK5bJddtlltnfv3iUJXAghxMpEd40QQoilRPeMEEKsXBb1Idctt9xiu3fvjv584QuHv+7/ghe8wMzMrrzySvv0pz9tH/3oR+2GG26wXbt22fOe97xjH7UQQogVi+4aIYQQS4nuGSGEWLksSjy/du1al/7zP/9zO+200+wnf/InbWpqyt797nfbBz/4QXv6059uZmbXXXednXXWWXbTTTfZRRddtKjACoVC5C+D3jbs/8P+Sehnw55DExMT0fPs7GywffTpYV+jxfjb8LsYH8eO/jrsJ8aeQyMjI9FzPx8hjIHHhONDbyD2VuJ30feIvdIefPDB6PkHP/iByzvttNNcev/+/dFztVp1eSEfK84L+RXxGOE88HixNxW2yWPA72JMvFYZfJdjD/nQ8RihzxX3k8vimHA/uS8YU8hLa2ZmJjEeM7OTTz45er7vvvtc3tSU99BBeG2GPLnYx4rfDfWFfa2S2uB3eY9ym4tZq1gXt4nnBMeOfnGcxvdCfnXLkeN517RzXUvlDo/PPT8zHf39R856rit3xyq/D9/6sf+Onkcf8OP71X+5Lno++VuTLu/gdr9uEPbwKdbobAd/nXbOt8m+UeivVDvJ15Ndc1L0nJvxbWZr5MNUSC34fLisS1onDWUpvnyVfCXh/9gy3hLJ5ks+XauAJ9Kq3S6vuX1N9Pyo6x/w8d3vKy5tju/N/1y33uUdPMmfRcVafN6UD/izPL/bnzepA/HPFs0z17m8+kg8DuzB1Sr4+yRbAH+sjh+v0lSyHynHl2IrKFgb7LsVeg89uMy8rxr/D2m65f+mC0n0DzMzmy+xxxnG4Mfo9K+fHz1f+sLLXN5rch9x6X+e/4Po+bfOeLzLO+k2bzZXWxPPC89DmkzhxnZX4vfI/yzTorM9l+xPhT507TzdASPkGQYeWI2S/1msOJPsTcp+YmnywpvPJXtJ4vnSZfM2+nGmOAPeY9BktxUwHltmHM97RgixMtg58aPoecvYKY9gJKIfR+3J1Ww27f3vf7+97GUvs1QqZbfeequ1Wi275JLY7HPbtm22ZcsWu/HGGxPraTQaNj097f4IIYQQZrprhBBCLC26Z4QQYmVx1B9yfeITn7DJyUn7lV/5FTMz27Nnj+XzeVu1apUrt379etuzZ09iPddcc41VKpXoz+bNm482JCGEECsM3TVCCCGWEt0zQgixsjjq3/X77ne/2571rGfZpk2bHlYAV199tV111VVRenp62jZv3mzFYjGS/6BMCaWLZr2SIZQBsewHpVssUZqcnHRplIixDInTKCFiGRKDsiqOAeVN/SSIjUb8lXmWFbI0CePtJ5tCuRNLo1guhn1hWdzdd98dPf/TP/2Ty9u6datLf/vb316w/YXSOC88fqOjoy6NckGW/OGYzc3NJbZh5iWKY2NjLo9NSENrlf9HD9vhdcz9RngOcZ54nfA6wjSvk5BENrQeDx065PJ4/HCN8VjjOjbzc8rzG5KjslyR+4bSTJYqh+SAXC+OCcfHZbFNjofbwfEMyaM5j9cCx7ASWOq7Jt1JRdKw6ob7ovxNB/z8zhT8Wp1eHe/n4sxqlze8N56n6Ses8nkTLG+K5zdDki+UDx0ue3Sy02yD1hts59ZIOjHPzEvUWFbYpf8mQ1kcx8plUaLWLJPtQNGfY8OH4jNl3dd+2uU9vfGz0fMNv/0Ul7fxR36vP/iTsQ7y2pv+3uWdelfFpRsVOONO9pLs0eKwS1fPjtNP/1+/6vI+ePfLo+dt/32hy2OZHFI4QD9LtEk63QLJ+Gn+zB3e5+d7DqR5HZKC5hrxxOSneH2RTB1+BOiSKo3XKstnXTyrfL+HJuNFh+NuZlYdi++M9W/4FZf3pguu8DFcGsuEN3/L39XW4rMx7gDLEws1P57FQ3DHpv3PQU2SEg4fin/umB2nO7WZLOXj8UNKU77NHqlyI1na2CZ5Isorc43kfwKQ6tHaWV9Ps7Tw/5F3A/1Yziz1PSMGm9GT4jt++sGDR/XeseR/Hvgflz735HOXpJ0TkX4SRJQriuXNUX3I9aMf/cj+67/+y/7t3/4t+rsNGzZYs9m0yclJ9z8fe/futQ0bNiTWVSgUej48EUIIIXTXCCGEWEp0zwghxMrjqOSK1113na1bt86e/exnR393/vnnWy6Xs+uvvz76uzvuuMN27txp27dvf/iRCiGEOKHQXSOEEGIp0T0jhBArj0V/k6vT6dh1111nL33pS51cp1Kp2OWXX25XXXWVjY+P2+joqL3qVa+y7du367eQCCGEWBS6a4QQQiwlumeEEGJlsugPuf7rv/7Ldu7caS972ct68t7ylrdYOp22yy67zBqNhl166aX29re//agCQx8a9DJiXx729EFfIfarwTrZE4d9hND7hn1uQp5c3Can+d0k2Gcr5P8T8iriNtlbid/FvrCnFPs94bvsIYXz8s1vftPl3X777S6Nfl4zM977hMcevwY+PDwcLLtmzZoFn83MGYdyv9CDy8z3c9++fYnxmPmx77fGMF70b+KY2CsrtP5C9XCa+xkitO+4X9wmzil7U/G72JeQbxWneUy4bxgD7w8sy/PJbXL8SbH3i4/TOL5cD8bL8YT82fC52+32nCnLneN112TqZpkfD9W57zsv+vvtr/xtV27rPz/Ppbfsjs+t2ml+XUxuifdH+UDR5TVLfg7ztXgt9PPcQp+jboa8F2k7o7cW5+HqS7WT85h58nPK+KPJtdMmb69W6ci9ekoHkr2LilV/ZuRg/J6c+5Jv8wXvden5r78gel437TtTXe8HKQ2mRBMbd1EU3rfnK+ufFT1nX363y/u9V98RPf986as+9oYfJPRO6xT8THTZf68Uv5vmszHHfljJY98qwM9FZTqnaN2wV1qIbifZkytX931hf68kDl3g7/xsy784+jcvjZ5nTuO3fdlWMb5/020fa6rNHnZxOl/jfvn1WB+JB429tFrFeLMVZ8jnkryscH/je4fjTd6luTr9/Efz0CpAv8mTC+vFcguB3l84fu32kf9csRw4XveMWDkslc9WP9iHKylP/lxLSz/PLrF8WPSHXM985jMT/6FXLBbt2muvtWuvvfZhByaEEOLERXeNEEKIpUT3jBBCrEyOypNLCCGEEEIIIYQQQojlhD7kEkIIIYQQQgghhBADz6LliseLbrcbfYUYPXMmJiaC76FnDfsI4VeSuZ4j9bZZqF7M7+eZg2n2yGF/IqRYJF8XeJe/as3xLsZHCL2C+D3uC8Y0Nzfn8nDO2DcNPbjM/Jhxm+yRhD5hXC+/i2XXrl3r8tCj6eDBgy4v5OXG88Djh8zOzro0e4ZhOuTDxP5SXA++y2uT+4L5vFbZrw0J+bHxOg75TfXzrDvSejjNc8/v4lrgviA81qG9sxi/PfZVY0L7LuT5x+sG48d6Qv040WmOdKwzdHhNZBqxp92Zf3KhK9fd6NfG7JmxpxN6ZZl5r6WMP6ZsqOHXCXpncT3sVYQeWOy7lG7xHAc8kWbiPcD1zOeSY2APrhRtpWY5eZ2xn1NxKq44N0V7u+Q73qjAvU6+ZRkYv1U7/RlW/9jlLj07HndgdtT7SOYb/v6dWB/7L66/13twrP6mP/NG1sR3yJ3/M+nyzrx+KnqeevSUyytNeY+pTi7uN3uY1ct+IQ1PxHdRYcqvqbmx5DMux+svnTxnbX/dWbYev9sYobuG1m6o3p52crAe6RgdORDvM/S7MjObz/kYJs/CdePz2AMr04rTzWF/p7LfXXMkOT7ud7oDdzd5abWzcP+mw95ZbYghP+vL9uyBOub5+LoB/y4GvcgyrbBRWjd95P5sQgghxPFG3+QSQgghhBBCCCGEEAOPPuQSQgghhBBCCCGEEAPPspUrplKpSIozNBRLSA4dOuTKhSRWLGdDSRDLzkKwDIllaBhDP7kdSqU49pBEkiVqKG0MSeY4nyVf3DeMv1Tyv2Kd42WJYlKb/B6PPdbDUjJOo8yUY2e5J87TgQMHXB6uDa6HpWXYF5az8bs4TzxnXBb79nBkozhPPL8Mvstj228dITinoTHg/JDskeNDyeZC9eJccFmOiSWySfBYhySdPA8cH77L8XAax55jQDko95PTi5lDcZhOOpYgza2Jx354asyVmzqd34vnu1CjeQFZ17w/Rp00y4wlQuH5Q+kgSxlZOoj0ShljeuSKJCscOhDnz5dIHkuSOpRccT9Rnmhmlr83Psvvfba/q2ce9QOXfvTnfip6zpJkMj8BsuUcneUkb8s14nlqFrzkcD7n929l/5roefwuf4e1vvOfLv3A36+Kns/MnOryDv5UnFe42999pSl/Z9UqcUy8poam6GcJkAO2C+F1k6/BfULrJleDOevzYxGvDYQldPlW3GYrF5YuovKNVXC4pni8eua3niyxC8kn+b1eSSL8RS4s00OJIsoTGR5L3vvYJu8lLhuSU+L5Yub3QKtAdxaMUaNEPz+T7LEwF89FqJ9CCCHEI4G+ySWEEEIIIYQQQgghBh59yCWEEEIIIYQQQgghBh59yCWEEEIIIYQQQgghBp5l68nVarUi35qQt03Iwynkc4Q+N2be98vM++uwbxB7SqGHDrfJ7WBd7OGDMbCPFfv/YJucxzHgmHBfOI0eP+wnxvVi2eFh/6vQcazZKyvkRVar+V/rHvKN4nq47P333x89sycXjh97t/GYhOaXwbkIzQOX5XWNMfTzzkJvKq6H1wbC48f9Du27UHxcb2iseQ+EvKk4BlyfIW85zmdftdBe4vhwP/PeDvmCcTwh7zSOIeRpxvFhvRgfty8WJgfeRTNn+HM+46fbcJbYBycX8NZi76JsO7lsqp2c5v+h6gb+y6qb8W20M8k+TMVDdL6UwSuS7IgKU9wOJEp9/g8N9mF19V5fzzydIVAv+xPNb4zju+eCb/s2hn2Ao/c8Lno+6fa1Lm/yZO8x2QFzqInT/ESsyv2cS//a318UPU9fsNrH8N34sUK+ZI1ysr8TeymF6OfvlMYjzx9/1KZPZ/xxbe1K/NzJUJvN5LuGaeepIXiXPbmc9xjtHW4zPwPnfMAvzozGxHwez0urAD9DUewcA/p7tcu+bB18rjLz9HNb26fR5iqV9vHNVfzENItxZzo0gF2ap1zd/zzm8jrxHcYeXNmWjw/7LU8uIZaec08+95EOQYiBQt/kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADjz7kEkIIIYQQQgghhBADz7L15JqdnY38edCThv2I2GsG80N+ROyrFaqXPYY4Xa/Xo+eQRxgTqpf9sNhzaG4u9g9hXx6uFwn5/Zh5XzD2T2IPIowx5N/F8bBHF441+yXh2Jr58eS553HAslwPx4BwvIvxvEJPpH6eUiF/L8wLecuZ9XpBITyeSD9PM6w3tI557nlsca1yX3j8MM2+VRwvjgPPL7eDayPUb4495MnFbYTGIeTBxXVxvbgPuZ7QuRXyBBMxnVw78tlplOO/Z2+gVtGPPXrWZOt+T9ZH4n3XKvg1xLQK8RwWq379ZVrkQRmop5M7cg+nZjleD0P7qFby/0E/ry5dqWnyfkJfMPZ36tC1VH/suuj5lO96H6vRHf7uqW+Jn6tj3jtrrgw+juTBVS57r6+pzfEe3fBD78k1utf7cjZGwHeQfI3m1vj9NFxbFT2P3OvLVrck/xxSqPr5zTXi+Z9Z4/tZnClQ2fjddobuwiL7MsXzxGsVvanYc43nzL1Hnk2t4jyl4+dCzd9R3faR//9qF/rGXmPs0YXxcl63Q1544L/X9kPr2jQzy9fgXiJPLi5brcR30VDV/5yJ3ln9zoV5mAv2/Du4ya/rdi6ud9Xe9b5wh9dYPEjdNHuPzS9YzqzX0wznOwVtpDhYIYQQ4hFAt5EQQgghhBBCCCGEGHj0IZcQQgghhBBCCCGEGHiWrVwxm81GUhyU5LDsJiTVCkl0WFrEciyUM3I9LAFbjJwSJUwsmUQZFb8XkotxmyFpFNfLZVEaxbI4Bt9laSO2w3ksUcN8lLYtFC++y3mLkWRh7DwP5XLZpVHCOTk56fJqtZpLh+YwJM0LxY7tcxtm/ecJQfldPwkdthOSPfLanJ2ddWlsJyQFNfPjwGPCclncS/0knNhv3vsodeS9FJLWcptcFvtdKpVcHvcFy4Ykzzxn3M+k81ByxWTSrYxlmof3IkrAahU/RyzXQVgahVKu4UP+fCl4RZ3VQPp28KQJl1eeGHHpXD2eX4zVzKwb0DLOjft1U5gB+VXJn0vdwHGSpmMAZY9mXk7WK2306dp4/Bf5Wb9uGyf5/dIqxJ1jCdiq3cOQ9xMub2KDl3Wd/oNHRc/F3X5+6xv9/nUSNZKCNkpeatbYFj8Xar7jKEnMTfl52PcYL7Ou7Iv7wvLJZomlZXAukISunfXpDtw19bLv9/BEPJ4sMWVwneepn5kcy9niynh/pNrJe4nVbqio4z3IEkQeo1CbKFHktdoq+MWKfWWZ5tAh/3InAz/jTfm8ViG+/1iazHJoHjNkbsN9Ll3YvzmxbPnQsEujzBDlk2ZmhVo8KHjWmPWuMR9re8FnIYQQ4pFC3+QSQgghhBBCCCGEEAOPPuQSQgghhBBCCCGEEAOPPuQSQgghhBBCCCGEEAPPsvXkyuVykT8OetKwtwz74qDPEfrwcB778rDvEXrfTE9Puzyul318QvFhX0LxcT+5DayX2+B6Q35e7BMVio/rwZjYYyjkrcT14liz51DIh4nnkD2RsJ3QWIfG1sz7HHF8XBZjCrVp5v2pQmPC8YU82PqtG4TnM+Q/xfVwXxAeE0xzP9mjK7QH+nm7Iez1hfPCawrjC+WZ+bXAeSG/QPT9WqgdjJfnJeT5x2VDcyYWZr4wb93IpwY8r8iTpnSA/IAycXpuzM8Del7lp3zebb/4RZdOTa+Nnk/7xjkub2Kjv3vSHe9VhWTSHF+8FthjCL2z2G+KyTbifPbVSgfe5bLs9/SQDxrHamZWuMf7+tmWMpT1Wblq/O6ae/z4rL7vVJdGH6aZR/n5zfnjxeojsCfTPr7hQ+S/B/5JPf5sMA7Vk+iuIQMqbKf0AAW00XuRNUbiAcWxNDMrzvhzFcd+bpWvFr3n2POKvdI6mXgtp9q+zVQ6eS3Mk0/T8IwfP+xLtk7nqlvHdDbSjza4rueLPpOdorBe9uvKzPu+sT8fMruW/Fxh7OcLfi3kGuBzSR5cvMbMOsl5835+G2N7ouf0nvUur1bxPqfZVtw39rfD2Ns5Gus6+YtB/F0Yv27Ab00IIYQ4XuibXEIIIYQQQgghhBBi4NGHXEIIIYQQQgghhBBi4NGHXEIIIYQQQgghhBBi4Fm2nlwI+sywD8/IyIhLF4uxx8DMzExiPWvXrnV5Z5xxhkvXarXo+bvf/W5iHsPxsW9PyD8p5GPFHj7lcuxRwvGw71EoPq4XvYvY74d9y0JeVbOzsadKyPeL01jnQmn0LmLfpVC9obHGWM16xwR9o/r5HM3Nxf4X7MM0PDycWC/Hh33r1ybWMzk56fJwnZiFvdx4DkNjjeuE4+P1h3PI/eQ5w7XC8THoIVatVhPbNPN9C/Wb42HPMOwrl+V9iOuIx4j3Eo4Le49hvHi+mfXOC7YTOmtETLqTiryI0ONneKdfx9//ubtd+rXn/mb0/J5/+bzL6773ndHzZV/9V5f3yVfd7tKFJ2+Mnp+aucXlbbx91KUPnhafKek2eRf1+BPF+ewxhL5R+VZ4n6GnVNvbIPZ4BXVzR77Ocv9/e3cfHFV97gH8yW6STTYJSSCQF0IgCl5E8Q0qBa12ahzHUsWXseMULV7mlqo4hXYuSm1DryINWsei6OjVUdQWpHqv7zNX5Qar5YK8ChahgAV5TxAh2YTNy2b3d/+InPM8T7LnZJGwb9/PDDPn5Hf2nN/5nd85T7Ls82wHm9d7Wh22JGorsjuh609lkz3n2wfK47fnqzp+bBz0flpKZO0iL6tdZFTxp06/ek6wczER2Qc+ZgX/lLHFXyjvZy5YpQabVL1FVvtI14sLR99tj/POil6ys0d9sbyvsqJsSRRWTXx+elRBLF1Xjdfh4nW1iIg6HeaUrqXF+8vrhxEReVUNMV4OLbtd1UpT91beV/Zrj1XJ3xd0XStey8qr7i1dm4zjtbKIZP91rbTKzy4R6xnsZHQtwVCOisfsOHr8+P2cFZR9D+k5z2p0RfysvlkE/3d+Oq04+LK1fPXQn0bd7sD8xj7vs7K21H0jAIAkF1M0CofDVFtbS9XV1ZSbm0tnn302zZ8/v8ebM/PmzaPy8nLKzc2lmpoa2rVr12nvOAAApB7EGQAA6G+INQAAqSumN7kefvhhevrpp+nJJ5+k7du308MPP0yPPPIILV682NrmkUceoSeeeIKeeeYZWrt2LeXl5dE111zT41MtAAAAGuIMAAD0N8QaAIDUFVO64urVq2nKlCk0efJkIiIaMWIEvfLKK7Ru3Toi6v4fj0WLFtFvf/tbmjJlChERvfzyy1RaWkpvvvkm3XrrrX0+Vnt7u5V6w1NwdLrOwIEDo65/9dVXoo2nko0YMUK0fe973xPrW7dutZa3bZPpJTr9jqdnuaXmObXx9EqeikXUM21KpzQ5cUoB0/vl46vTzkpKSqJue+jQIdHGz02fi06L46lluj96W96u0wx1f/m5OqV/6tfpX154u07/0+fG+6Tb9NjzPujz5HSb3i9PWeNphEQ9z0X3n9Ppdry/eq46zRO9Ld+P7p9ODdXpd5y+t/hzQacn6jGK1h8iOTecUl41fUw9x3hKolPaLZE8N70tH1/+DHM7Jl82xsT0zIinMxlniIja8tvJ6z85X+x5bfbuFdt9OGi6WB/5J3s8z7lpomhbuPoNa3nGn/5TtG3++KhYrz5u76ft6edEm3+xPGbBUfuadvqjPzOIZNpU9gk1V72m1+2IeqYpRdgjWaeZ9Txm9HQxndrob7bv0WMvTRNt735wWKyX7LLPdRKtEm2eA8et5YxiWYYg4lX3R7M9Dscqm0RbyCfvjwhLURy8X6b3eBxS/HjqHZEck45y+ftLdrPcUajAfm17gXw25rTI2MhTJDsK5Xl25agYy/pk1JhkhKK36fMM+aOnDmYFdVp99P9D1XOOp83pFEmevqjT9jRPB99WtuksOr5fPa87/J1qWztu+dpkbOmZ8sdW1Hny/nfkymM46ZGC6JMxt6PA/r2jrUKm/uaekDHf32yXTfB4Zd+Nl3c++jwm6nl/J6MzHWv6gqcnajol8VTTDk/XfpLd6u0ro7ZNOvcHZ7AnANAfYvok16RJk6i+vp527txJRERbtmyhVatW0bXXXktERHv27KGGhgaqqamxXlNYWEgTJkygNWvW9LrPjo4OCgQC4h8AAKSn/ogzRIg1AABgw980AACpK6ZPcs2dO5cCgQCNHj2avF4vhcNhWrBgAU2dOpWIiBoaGoiIqLRU/q9AaWmp1abV1dXRAw88cCp9BwCAFNMfcYYIsQYAAGz4mwYAIHXF9EmuV199lZYuXUrLli2jTZs20UsvvUSPPvoovfTSS6fcgV//+tfU3Nxs/du/f/8p7wsAAJJbf8QZIsQaAACw4W8aAIDUFdMnuebMmUNz58618tDHjh1Le/fupbq6Opo2bRqVlZUREVFjYyOVl9tfi97Y2EgXXXRRr/v0+Xw96vQQERUXF1u1c3itKl3Lxqn+D6/ZQyTr/+xV9VY+/vhjsc7rE+m6QXq//Ji63o+uV6Rfy/G6PPp1TnV6dB0rp23danLx/o0cOVK0XXHFFWKd1wOqr68Xbbz+jz5n3T9eJ2rAgAGiTdej4tdF1+TSeP/0ePLz1rWedJ0jPj+d6mrpdj1XdT05/lqnOaXb9DrvL6/DpNv0utO9Q9TzOnH83PTY6vHk+9HXM5Yadnqu8m31eevj8FpW+jz5eOo2p/44jS1R3+ts6dfqPvB1t7GOth+n80g0/RFniKLHGk/YQ56wx1o+6fh7c8V2nQ+pekRhe31Iq6rL89UBa3nZVFkT54ZVso5k7kV2Hani52eItozWFrEeKbfnEa8p1BteK0hvy+8O3eZtl+cZKmD7UbWWM1WNJG+IxbAcdUxV08f3if0NZW+9LT8V8emP1on1LDbNH9k0SbT9fuf/sePLYw76p3zmhvLt9tuvvkO0/a1znFjfvOMma7nj62LZ96Cu+WffX5nh6P932FYkY0JXjnr+sUJSvmD0OmpERBn8XNUhs4NyvyeK21mb7LuszyaPofcT8rE6b6p2W2eBrsHGuudSH8uw2lW6xpXTPNfzMcwutz6G1l4Qvf5jRHU4WGj/PuNR/fOo692VZd9dIZ/6fZXVscoMOf/6zV8bLJS/6/C6WkREg/fY83Pf+bIml9aWb8fCnKB8HvJz0fNN4zX2wuyc+XKiO5N/0yQS1OCKfdtTrdH14pHony4nIrpjSNkp7RcA3MX0Sa5gMNjjDzCv12v9AVVdXU1lZWXiDY9AIEBr166liRNlYV4AAAANcQYAAPobYg0AQOqK6ZNc1113HS1YsICqqqrovPPOo08//ZQee+wxmj69+xugMjIyaPbs2fTQQw/RqFGjqLq6mmpra6miooJuuOGG/ug/AACkEMQZAADob4g1AACpK6Y3uRYvXky1tbV0991305EjR6iiooJ+/vOf07x586xt7r33Xjpx4gTNmDGDmpqa6PLLL6f33nuvR6qWm4yMDCuNKDvb/ni9Tg9rbZUfzeZpVDqdjb/28GH5FeVNTU1inadIOqUPEck0Kp2ypNOL+GudUt38fr9o06lvnFsqEk/d0n13SrnS2xYWFop1Pva6vzyNSve9pKRErI8fP95aLi6WaSHbt28X619++WXUY/IUSSJ53jrVjYtlTPQ103PMKR1Vv5ZfN90/p+ug98PvD7d0Nr7ulJapj6vPSx+H4ymlRPI6xXJMt+vC+6DTE/U6HyM9JnzeuN1LTqmN+hnH57LuD3++6D44pTjrtEen1FW+Hz12iexMxhkiora8NvLmdY9rVod9HxaM+YnYbmrucbFee9vz1vK/b1gu2kaNG2Ytv/zA+6LtP+reFutn3T/BWm6fJK9TsFo+F7zsWZoZUve2Tjtk60ZuSh6WZpgZVGlw6hbgKWEhv3o2OqQ06dQ3nbpF7fazM7ND9t2nHtf8tjy7Qd4DoWL73vbt+lq0bb9VXrMv/sv+lrTj1x0Tbff+9KBYH589x1ru9MlnrsbTF73qNPkYeTtV+l+OfC7wdLGwumb6+nblRFib+jSKSuPTKYpch589J1Sanr5m/P7QqZdZ7bLDvla7Tx35cr+5x2V/gyUsxS9HPv86/fZrC47I+zui5wlLoev0y2vWUiyfuXkBO424tbhZtOW0ynTA/ON2OvzXQ+Wc8gVln/h9yV/XfZw2isajUn/b8oOsTY7X4VFfiPWPi6fb/SuQc+rGfbK+lKfLngueRpm25jRPeKolkZwrPIXThJ3TqBPJmY4135ZTmqFuOzC/sc+vhdPLLUURIBl1NEcvEeQrzIvaFk8xvclVUFBAixYtokWLFkXdJiMjgx588EF68MEHv23fAAAgzSDOAABAf0OsAQBIXTHV5AIAAAAAAAAAAEhEeJMLAAAAAAAAAACSXoZJsGItgUCACgsLadiwYVbNm/Z2u8CEU20qop51cjin1zrV/3Gr08Nf69Y/vl9dT4fX23GrrcT3o+v08PHSx9E1kPS58dpQuuZAZWWlWM/Ls3NwDxw4INp4vS5dt2rkyJFifc4cu/bJ8OHDRdtf/vIXsb5ixQpr+eBBWUNFH4ePmdNY61sglrngNDf0MXVdJt6en58v2vj1d7r2els3/Nz0vNHH4f116ruuEeY0d/WY6DpvJ7+ym0jOL6Ke9fe+/tquv6NrXOnrwsdMj59THTWnxyOv80VENHToULE+atSoqK/dtWuXWOc1AnVNM/5M0ddBz1V+TXWtwObmZmpubqYBAwZE7Vc6ORlrLlh6mLz+7jEZ0GjX0PEdUXWYvpI1nCjPrjXXVS7HtIuVCwz55FwMZ8trGPLZ9wuvw0PUs95Pbqu9ntmu6jCF+l4Lh9fd0nWNslrknA/nsHo7KrzqWl/8XPV56ppNeZuP2tvu/ky0/Xftf4j1gi/tmnUV4x4VbWNfv8JaDjw1XbQ9t+ZzsZ59vX2c+m0yVv9ovIyNq1dcYi0/v/hvoq2pStZ/zDtuXxePw68AGXJIHGucqTJMPa4v3zYrqGKWem2g1K4FFVF1t3idLV0XStcQk3XeZN95bS8iWZvK1yL3k9UqX9tWYq871XLLPyrvBz3/2guiDz6/z4iIPiq/2lrOXSHrF4WulPWLJh636+oN3Cfj0rEq+XsHv0d1TbN2vz1vcoJyvvHroF9beFCe92tXXCbWj9610VouGiz3e+j5fxHr9z32v9by1+Oj1+DKUBMwnKXrx/X+vAkHA7T5p2WINd84GWcg8azevjJq26Rzf9Dn/ZyuGlx3DClz3wiSyuGmZveNvlFelFjPCacaXG7OVI0utziDT3IBAAAAAAAAAEDSw5tcAAAAAAAAAACQ9PAmFwAAAAAAAAAAJL1M903io6Ojw6o3w2v86JpSukYNrw+kawxxuh5RR4esseFU58ipRpJ+Ha+Lo+m6Qfpc+tofvR9dY4iv6xo+uq4Qr/+ja1zt3btXrDvVteJ90uOlaw59/rldN+X48eOiTffBiT6OU70sp9poGm93q8/mVL/LqS6Y7jtv069z6oOuj+V0XXSb07rT3HTrH9+PrvOmc6kvvPBCa/mcc84RbYcOHRLr69evt5b37dsn2pqbZR48778ea7frH40eL/1M4feWvi5+v1+s8+eaU009p5qDRPI8na412AbtzKbMnO5rlRHqjL7h4IFyndXo8gZl/YFIlj3HvB49v1Ttu0z7muU1y/3oOj05zfZr3epjRTysbmOHqufkMI2MVz2nguyZliXbuuQ0pqwOT6/Luj9ERC2XlFjLBYWXi7YfP79M7rioyFoc06DrL9j7Lbhwmmj5t7xtYv2dwXOt5YE5sl5S/kAZC//Y/hNruVA9crODclteC8oTjl4rza3OlpOuHIfY41d1tlSNruJ9dq25zGY5x0PF9rno+my+w/JZRCH795CuUllH0huSY8Kvt66zRfmq9pcYB3kuXVn2uegx8IbkeeY32ieQ+YWsk3Pp47PF+udj1lrL73wh+zdh8G6xvmnRldby1ZENoq3gqLwJWkrs3294XTIioryAva2ud9Y6UP6u42M1uzIPB0TbgENyHLYdta9LoEXG6gs3yuOEhtr3z8BdctvWofZ10DX1SJ2LYbXdOtm9FI4416UFSBSx1N0C6ItYanA5vTbR6nMlK3ySCwAAAAAAAAAAkh7e5AIAAAAAAAAAgKSXsOmKXV1dVmoOTwPSaT469Yin9ekUq7Y2+2u0dfqQUwqYThXU6U08hUinJzqlFzmlr+n+6LRCvl/dP36eRH1POyNyTvfUr+XrOo2U90/3vbW1Vax/8MEH1nJBQYFo0+mLgYD9sX23c+F9iCWdUl8znsKpj6m35e1uaXxO83rw4MFR21paWsR6U1OTtczHh6hnuqfTmLiNJ8fTe/X46XuAn7dO6dPpihUVFdbyyJEjRZvub36+TJfhnOaqbuPjq4+hnxN8/PT15NeBiGjHjh3Wsj5vnZLI7xF9LzmlGjq1IV2xb5qqw+T1d88Jnl7U4ZfX/njpEbEe8drzKKujUbQN+LrYWs5pUdfTK6+Ft8s+Jk9dJCLKCMtnSEe+fU09KmQ5pcKFVDpbZjubxyqNMOxTxyy023V6Xac/en91/7RAif0cO1olY1bhEflV6vK6yG2zOux7K++q74u23OVyfdeyJdbyjz+UA7bkOpkqWvjHu6zl1nKdQhf9WenRWV45YdYmj2nUGPG0vpBPpn3pFEmeTpYRViUBBsrXZrXb7XvGybm6aPwt1vIFMiOc3hlRJNb/uPo5a3nYdvn85SmbRPJcI16XyRDldUQ9x4HT6YAhNndzR8o59K/7vhTrBaPt5/6PfXJOlVXKtPoultK5v1SmQea0Rv+q9Ii6KTMi9vX1qOdARN0wngi7f4fKOFnz/ZvF+v+8wNKjO+V+n/mB/J2qpcV+rvkKVFwqkGU7OK8aa56iyPuuzwMg1d0xRD5rXjzSEGVL99emqmf3PdnnbWdU3dOPPYF0gk9yAQAAAAAAAABA0sObXAAAAAAAAAAAkPQSLl3xZFqNMcZKt4nl2+14u972VL9Z7nRue6r76es3BcZ6TKdvwoulD05j7dRGJNMtdeqlTi2L5Ro6tfXX9T3VPpyuMXG6nrH2r6/7cUuF6+t5Esk0SP0tnDrFj7/2dJ33t5kLsXxb6re5hqfSxp+r0O3kWITbWPovS8kJk0ppPyHThCMsLy3SKVOqwkF7P11tKl1RpQcalsql0xW72mSKGueWrmhYaqFOkSSWrhjuUv1RKYldLKfOqG+z6yKVrhjpe7piOGiPZ9i0qzb1q4nDdfGwdMWuNpkqHVanfSJg9zcQlI1tAfks6mq3U7+7guredkhX7PEtdOz6RnS6Ypcc63CIlxaQc0rPhUiX3QejjsnbiIgyOux2PY+DbExUJnyPMeGv7WqTqfFhr+wvP1cTQ7qiHqOwx54bHvVto3qs+fzrapf7aWnReaT2WLeqidISkuveE6wMRkgOUjjo8M3DoejzWPc97JNlHMJB9juAmteRgLxfAqx/Ol1RX8NwkM3rNtm/cFb0dMUe/SV7W56iePK+RqzphnFIP236QQrU1tLmvlEKaFElY5KZLn+TiNyerxkmwZ7ABw4coGHDhsW7GwAAKWf//v1UWVkZ724kBMQaAID+gVjTDXEGAKB/uMWZhHuTKxKJ0KFDh8gYQ1VVVbR///4examhWyAQoGHDhmGMHGCMnGF83KXCGBljqKWlhSoqKhy/DCOdRCIR2rFjB40ZMyapr21/S4X5398wRu4wRs5SZXwQayT8TdN3qXIP9BeMjzuMkbtUGKO+xpmES1f0eDxUWVlpfUxuwIABSXsRzhSMkTuMkTOMj7tkH6PCwsJ4dyGheDweGjp0KBEl/7U9EzBG7jBG7jBGzlJhfBBrbPibJnYYI2cYH3cYI3fJPkZ9iTP4bxYAAAAAAAAAAEh6eJMLAAAAAAAAAACSXsK+yeXz+eh3v/sd+Xw+943TFMbIHcbIGcbHHcYodeHausMYucMYucMYOcP4pDZcX3cYI2cYH3cYI3fpNEYJV3geAAAAAAAAAAAgVgn7SS4AAAAAAAAAAIC+wptcAAAAAAAAAACQ9PAmFwAAAAAAAAAAJD28yQUAAAAAAAAAAEkvYd/keuqpp2jEiBGUk5NDEyZMoHXr1sW7S3FRV1dH3/nOd6igoICGDBlCN9xwA+3YsUNs097eTjNnzqRBgwZRfn4+3XzzzdTY2BinHsffwoULKSMjg2bPnm39DGNEdPDgQbrtttto0KBBlJubS2PHjqUNGzZY7cYYmjdvHpWXl1Nubi7V1NTQrl274tjjMyccDlNtbS1VV1dTbm4unX322TR//nzi38uRzuOTqhBnuiHOxA5xpneIM84Qa9IP4owNsSY2iDO9Q5xxhjjzDZOAli9fbrKzs80LL7xgPv/8c/Ozn/3MFBUVmcbGxnh37Yy75pprzJIlS8zWrVvN5s2bzQ9/+ENTVVVlWltbrW3uvPNOM2zYMFNfX282bNhgvvvd75pJkybFsdfxs27dOjNixAhzwQUXmFmzZlk/T/cxOnbsmBk+fLi54447zNq1a83u3bvN+++/b7744gtrm4ULF5rCwkLz5ptvmi1btpjrr7/eVFdXm7a2tjj2/MxYsGCBGTRokHn33XfNnj17zGuvvWby8/PN448/bm2TzuOTihBnbIgzsUGc6R3ijDvEmvSCOCMh1vQd4kzvEGfcIc50S8g3uS699FIzc+ZMaz0cDpuKigpTV1cXx14lhiNHjhgiMh999JExxpimpiaTlZVlXnvtNWub7du3GyIya9asiVc346KlpcWMGjXKrFixwlx55ZVWUMAYGXPfffeZyy+/PGp7JBIxZWVl5g9/+IP1s6amJuPz+cwrr7xyJroYV5MnTzbTp08XP7vpppvM1KlTjTEYn1SEOBMd4kx0iDPRIc64Q6xJL4gzzhBreoc4Ex3ijDvEmW4Jl67Y2dlJGzdupJqaGutnHo+HampqaM2aNXHsWWJobm4mIqKBAwcSEdHGjRspFAqJ8Ro9ejRVVVWl3XjNnDmTJk+eLMaCCGNERPT222/T+PHj6ZZbbqEhQ4bQxRdfTM8995zVvmfPHmpoaBBjVFhYSBMmTEiLMZo0aRLV19fTzp07iYhoy5YttGrVKrr22muJCOOTahBnnCHORIc4Ex3ijDvEmvSBOOMOsaZ3iDPRIc64Q5zplhnvDmhHjx6lcDhMpaWl4uelpaX0j3/8I069SgyRSIRmz55Nl112GZ1//vlERNTQ0EDZ2dlUVFQkti0tLaWGhoY49DI+li9fTps2baL169f3aMMYEe3evZuefvpp+tWvfkX3338/rV+/nn7xi19QdnY2TZs2zRqH3u67dBijuXPnUiAQoNGjR5PX66VwOEwLFiygqVOnEhGl/fikGsSZ6BBnokOccYY44w6xJn0gzjhDrOkd4owzxBl3iDPdEu5NLohu5syZtHXrVlq1alW8u5JQ9u/fT7NmzaIVK1ZQTk5OvLuTkCKRCI0fP55+//vfExHRxRdfTFu3bqVnnnmGpk2bFufexd+rr75KS5cupWXLltF5551HmzdvptmzZ1NFRQXGB9IK4kzvEGfcIc64Q6wB6IZY0xPijDvEGXeIM90SLl2xpKSEvF5vj2+KaGxspLKysjj1Kv7uueceevfdd+nDDz+kyspK6+dlZWXU2dlJTU1NYvt0Gq+NGzfSkSNH6JJLLqHMzEzKzMykjz76iJ544gnKzMyk0tLStB+j8vJyGjNmjPjZueeeS/v27SMissYhXe+7OXPm0Ny5c+nWW2+lsWPH0u23306//OUvqa6ujogwPqkGcaZ3iDPRIc64Q5xxh1iTPhBnokOs6R3ijDvEGXeIM90S7k2u7OxsGjduHNXX11s/i0QiVF9fTxMnToxjz+LDGEP33HMPvfHGG7Ry5Uqqrq4W7ePGjaOsrCwxXjt27KB9+/alzXhdddVV9Pe//502b95s/Rs/fjxNnTrVWk73Mbrssst6fE3zzp07afjw4UREVF1dTWVlZWKMAoEArV27Ni3GKBgMkscjH4der5cikQgRYXxSDeKMhDjjDnHGHeKMO8Sa9IE40xNijTPEGXeIM+4QZ74R58L3vVq+fLnx+XzmxRdfNNu2bTMzZswwRUVFpqGhId5dO+PuuusuU1hYaP7617+aw4cPW/+CwaC1zZ133mmqqqrMypUrzYYNG8zEiRPNxIkT49jr+OPfRmIMxmjdunUmMzPTLFiwwOzatcssXbrU+P1+8+c//9naZuHChaaoqMi89dZb5rPPPjNTpkxJua+TjWbatGlm6NCh1tftvv7666akpMTce++91jbpPD6pCHHGhjhzahBnJMQZd4g16QVxRkKsiR3ijIQ44w5xpltCvslljDGLFy82VVVVJjs721x66aXmk08+iXeX4oKIev23ZMkSa5u2tjZz9913m+LiYuP3+82NN95oDh8+HL9OJwAdFDBGxrzzzjvm/PPPNz6fz4wePdo8++yzoj0SiZja2lpTWlpqfD6fueqqq8yOHTvi1NszKxAImFmzZpmqqiqTk5NjzjrrLPOb3/zGdHR0WNuk8/ikKsSZbogzpwZxpifEGWeINekHccaGWBM7xJmeEGecIc50yzDGmDP96TEAAAAAAAAAAIDTKeFqcgEAAAAAAAAAAMQKb3IBAAAAAAAAAEDSw5tcAAAAAAAAAACQ9PAmFwAAAAAAAAAAJD28yQUAAAAAAAAAAEkPb3IBAAAAAAAAAEDSw5tcAAAAAAAAAACQ9PAmFwAAAAAAAAAAJD28yQUAAAAAAAAAAEkPb3IBAAAAAAAAAEDSw5tcAAAAAAAAAACQ9PAmFwAAAAAAAAAAJL3/B+GqamBl0g1ZAAAAAElFTkSuQmCC\n", - "text/plain": "
" - }, - "metadata": {}, - "output_type": "display_data" - } - ] - } - }, - "7a72ee57e14c440bb2ce281da67e1311": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "VBoxModel", - "state": { - "_dom_classes": [ - "widget-interact" - ], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "VBoxModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "1.5.0", - "_view_name": "VBoxView", - "box_style": "", - "children": [ - "IPY_MODEL_2692114df7304a1cbc703e8bd0f848c3", - "IPY_MODEL_697f7288fff64aefbee1a5c0d4894987" - ], - "layout": "IPY_MODEL_1e79fde882a44cd984a54a71c3337759" - } - }, - "9cb58613b1a74eaeb285f6d2d77d567b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "1.2.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "1.2.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "overflow_x": null, - "overflow_y": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "a1d487697e4b4ea6b897f380c2b112cc": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "1.5.0", - "model_name": "SliderStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "1.5.0", - "_model_name": "SliderStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "1.2.0", - "_view_name": "StyleView", - "description_width": "", - "handle_color": null - } - } - } + "a1d487697e4b4ea6b897f380c2b112cc": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.5.0", + "model_name": "SliderStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "1.5.0", + "_model_name": "SliderStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "StyleView", + "description_width": "", + "handle_color": null + } } - }, - "nbformat": 4, - "nbformat_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/notebooks/assess_instance.ipynb b/notebooks/assess_instance.ipynb index 0dec4543..dd921e33 100644 --- a/notebooks/assess_instance.ipynb +++ b/notebooks/assess_instance.ipynb @@ -2,25 +2,26 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "tags": [] }, "outputs": [], "source": [ - "import napari\n", - "import numpy as np\n", "from pathlib import Path\n", - "from tifffile import imread\n", "\n", - "from napari_cellseg3d.dev_scripts import evaluate_labels as eval\n", - "from napari_cellseg3d.utils import resize\n", + "import napari\n", + "import numpy as np\n", "from napari_cellseg3d.code_models.model_instance_seg import (\n", " binary_connected,\n", " binary_watershed,\n", - " voronoi_otsu,\n", " to_semantic,\n", - ")" + " voronoi_otsu,\n", + ")\n", + "from tifffile import imread\n", + "\n", + "from napari_cellseg3d.dev_scripts import evaluate_labels as csg3d_eval\n", + "from napari_cellseg3d.utils import resize" ] }, { @@ -101,13 +102,10 @@ "semantic_gt = to_semantic(gt_labels_resized.copy())\n", "semantic_pred = to_semantic(prediction_resized.copy())\n", "\n", - "viewer.add_image(semantic_gt, colormap='bop blue')\n", - "viewer.add_image(semantic_pred, colormap='red')\n", + "viewer.add_image(semantic_gt, colormap=\"bop blue\")\n", + "viewer.add_image(semantic_pred, colormap=\"red\")\n", "\n", - "dice_coeff(\n", - " semantic_gt,\n", - " prediction_resized\n", - ")" + "dice_coeff(semantic_gt, prediction_resized)" ] }, { @@ -128,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { @@ -137,7 +135,7 @@ }, "outputs": [], "source": [ - "# eval.evaluate_model_performance(gt_labels_resized, gt_labels_resized)" + "# csg3d_eval.evaluate_model_performance(gt_labels_resized, gt_labels_resized)" ] }, { @@ -194,7 +192,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { @@ -253,12 +251,12 @@ } ], "source": [ - "eval.evaluate_model_performance(gt_labels_resized, connected)" + "csg3d_eval.evaluate_model_performance(gt_labels_resized, connected)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { @@ -317,11 +315,9 @@ } ], "source": [ - "watershed = binary_watershed(\n", - " prediction_resized, thres_small=2, rem_seed_thres=1\n", - ")\n", + "watershed = binary_watershed(prediction_resized, thres_small=2, rem_seed_thres=1)\n", "viewer.add_labels(watershed)\n", - "eval.evaluate_model_performance(gt_labels_resized, watershed)" + "csg3d_eval.evaluate_model_performance(gt_labels_resized, watershed)" ] }, { @@ -444,7 +440,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { @@ -503,12 +499,12 @@ } ], "source": [ - "eval.evaluate_model_performance(gt_labels_resized, voronoi)" + "csg3d_eval.evaluate_model_performance(gt_labels_resized, voronoi)" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": { "collapsed": false, "jupyter": { @@ -525,7 +521,7 @@ } ], "source": [ - "# eval.evaluate_model_performance(gt_labels_resized, voronoi)" + "# csg3d_eval.evaluate_model_performance(gt_labels_resized, voronoi)" ] }, { diff --git a/notebooks/label_stats_csv_plot.ipynb b/notebooks/label_stats_csv_plot.ipynb index b2c612c4..c9ad28fb 100644 --- a/notebooks/label_stats_csv_plot.ipynb +++ b/notebooks/label_stats_csv_plot.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", "metadata": {}, "source": [ "# Plotting cell ditribution from csv files\n", @@ -17,11 +18,11 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt\n", "import matplotlib.cm as cm\n", - "from matplotlib.colors import Normalize\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import pandas as pd" + "import pandas as pd\n", + "from matplotlib.colors import Normalize" ] }, { @@ -206,6 +207,7 @@ { "cell_type": "code", "execution_count": 14, + "id": "acae54e37e7d407bbb7b55eff062a284", "metadata": {}, "outputs": [ { @@ -224,11 +226,22 @@ } ], "source": [ - "display(data[[\"Image size\", \"Total image volume\", \"Total object volume (pixels)\", \"Filling ratio\", \"Number objects\"]].loc[0])" + "display(\n", + " data[\n", + " [\n", + " \"Image size\",\n", + " \"Total image volume\",\n", + " \"Total object volume (pixels)\",\n", + " \"Filling ratio\",\n", + " \"Number objects\",\n", + " ]\n", + " ].loc[0]\n", + ")" ] }, { "cell_type": "markdown", + "id": "9a63283cbaf04dbcab1f6479b197f3a8", "metadata": {}, "source": [ "### Plotting function\n", @@ -238,7 +251,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "4074f947-4b61-45c9-bc83-d644250c5bc0", "metadata": {}, "outputs": [], @@ -270,9 +283,7 @@ " )\n", "\n", " handles, labels = scatter.legend_elements(prop=\"sizes\", alpha=0.6)\n", - " legend2 = ax.legend(\n", - " handles, labels, loc=\"center left\", title=\"Volume\", labelspacing=1.7\n", - " )\n", + " ax.legend(handles, labels, loc=\"center left\", title=\"Volume\", labelspacing=1.7)\n", "\n", " ax.set_title(\n", " f\"Cell location from centroids\\nTotal number of cells : {int(data['Number objects'][0])}\",\n", @@ -330,7 +341,7 @@ "outputs": [], "source": [ "import plotly.graph_objects as go\n", - "from plotly.offline import iplot, init_notebook_mode" + "from plotly.offline import init_notebook_mode, iplot" ] }, { @@ -369,7 +380,7 @@ " fig.update_layout(\n", " height=600,\n", " width=600,\n", - " title=f'Total number of cells : {int(data[\"Number objects\"][0])}',\n", + " title=f\"Total number of cells : {int(data['Number objects'][0])}\",\n", " )\n", "\n", " fig.show()" diff --git a/notebooks/labels_plot.ipynb b/notebooks/labels_plot.ipynb index 4564fd93..785c8bee 100644 --- a/notebooks/labels_plot.ipynb +++ b/notebooks/labels_plot.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", "metadata": {}, "source": [ "# Labels plot\n", @@ -17,8 +18,10 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib.pyplot as plt\n", "import os\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "from tifffile import imread" ] @@ -26,12 +29,14 @@ { "cell_type": "code", "execution_count": null, + "id": "acae54e37e7d407bbb7b55eff062a284", "metadata": { "collapsed": false }, "outputs": [], "source": [ "import sys\n", + "\n", "!{sys.executable} -m pip install plotly" ] }, @@ -61,8 +66,8 @@ "pred_path = \"C:/Users/Cyril/Desktop/test/pred/large/\"\n", "\n", "pred_images = []\n", - "for filename in sorted(os.listdir(pred_path)):\n", - " img = imread(os.path.join(pred_path, filename))\n", + "for filename in sorted(Path(pred_path).iterdir()):\n", + " img = imread(Path(pred_path) / filename)\n", " pred_images.append(np.array(img.compute()))" ] }, @@ -91,7 +96,7 @@ "outputs": [], "source": [ "import plotly.graph_objects as go\n", - "from plotly.offline import iplot, init_notebook_mode" + "from plotly.offline import init_notebook_mode, iplot" ] }, { @@ -143,8 +148,6 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", "os.system(\"jupyter nbconvert --to html full_plot.ipynb\")" ] }, diff --git a/notebooks/view_wnet.ipynb b/notebooks/view_wnet.ipynb index 0c6c91fd..d5504d28 100644 --- a/notebooks/view_wnet.ipynb +++ b/notebooks/view_wnet.ipynb @@ -12,9 +12,11 @@ }, "outputs": [], "source": [ - "from torchview import draw_graph\n", "import os\n", - "os.environ[\"PATH\"] += os.pathsep + 'C:/Program Files/Graphviz/bin/'\n", + "\n", + "from torchview import draw_graph\n", + "\n", + "os.environ[\"PATH\"] += os.pathsep + \"C:/Program Files/Graphviz/bin/\"\n", "from napari_cellseg3d.code_models.models.wnet.model import WNet" ] }, @@ -59,7 +61,13 @@ "metadata": {}, "outputs": [], "source": [ - "model_graph = draw_graph(model, input_size=(batch_size, 1, 64,64,64), device='cpu', expand_nested=True, depth=5)\n", + "model_graph = draw_graph(\n", + " model,\n", + " input_size=(batch_size, 1, 64, 64, 64),\n", + " device=\"cpu\",\n", + " expand_nested=True,\n", + " depth=5,\n", + ")\n", "model_graph.visual_graph" ] } From 21e529bf12d5643f424d9b3fd16324f036a5e0d6 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 20 May 2026 16:19:20 +0200 Subject: [PATCH 4/5] Re-run pre-commit on all --- napari_cellseg3d/_tests/test_base_plugin.py | 36 +-- .../code_models/model_framework.py | 2 +- .../code_models/models/unet/buildingblocks.py | 8 +- .../code_models/models/unet/model.py | 4 +- .../code_plugins/plugin_convert.py | 4 +- napari_cellseg3d/code_plugins/plugin_crf.py | 2 +- napari_cellseg3d/code_plugins/plugin_crop.py | 6 +- .../code_plugins/plugin_metrics.py | 2 +- .../code_plugins/plugin_model_training.py | 4 +- .../code_plugins/plugin_review.py | 2 +- .../code_plugins/plugin_utilities.py | 2 +- .../dev_scripts/colab_training.py | 4 +- .../dev_scripts/remote_training.py | 2 +- napari_cellseg3d/interface.py | 4 +- napari_cellseg3d/utils.py | 2 +- notebooks/Colab_inference_demo.ipynb | 5 +- pyproject.toml | 268 ++++++++---------- 17 files changed, 166 insertions(+), 191 deletions(-) diff --git a/napari_cellseg3d/_tests/test_base_plugin.py b/napari_cellseg3d/_tests/test_base_plugin.py index 188a9135..99ee1a35 100644 --- a/napari_cellseg3d/_tests/test_base_plugin.py +++ b/napari_cellseg3d/_tests/test_base_plugin.py @@ -28,9 +28,7 @@ def test_base_single_image_update_default_paths(make_napari_viewer_proxy): assert plugin._default_path == [test_image, None, None] -def test_check_results_path_creates_missing_folder( - make_napari_viewer_proxy, tmp_path -): +def test_check_results_path_creates_missing_folder(make_napari_viewer_proxy, tmp_path): plugin = BasePluginSingleImage(make_napari_viewer_proxy()) results_dir = tmp_path / "new" / "results" @@ -58,9 +56,7 @@ def test_check_results_path_rejects_non_string(make_napari_viewer_proxy): def test_single_image_build_not_implemented(make_napari_viewer_proxy): plugin = BasePluginSingleImage(make_napari_viewer_proxy()) - with pytest.raises( - NotImplementedError, match="To be defined in child classes" - ): + with pytest.raises(NotImplementedError, match="To be defined in child classes"): plugin._build() @@ -109,9 +105,7 @@ def test_remove_docked_widgets_handles_lookup_error( def raise_lookup_error(_dock_widget): raise LookupError - monkeypatch.setattr( - viewer.window, "remove_dock_widget", raise_lookup_error - ) + monkeypatch.setattr(viewer.window, "remove_dock_widget", raise_lookup_error) assert plugin.remove_docked_widgets() is False @@ -235,9 +229,7 @@ def test_load_image_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): image_0.write_text("fake") image_1.write_text("fake") - monkeypatch.setattr( - plugin, "load_dataset_paths", lambda: [image_0, image_1] - ) + monkeypatch.setattr(plugin, "load_dataset_paths", lambda: [image_0, image_1]) plugin.load_image_dataset() @@ -253,9 +245,7 @@ def test_load_label_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): label_0.write_text("fake") label_1.write_text("fake") - monkeypatch.setattr( - plugin, "load_dataset_paths", lambda: [label_0, label_1] - ) + monkeypatch.setattr(plugin, "load_dataset_paths", lambda: [label_0, label_1]) plugin.load_label_dataset() @@ -263,9 +253,7 @@ def test_load_label_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): assert plugin.labels_filewidget.text_field.text() == str(tmp_path) -def test_load_unsup_images_dataset( - make_napari_viewer_proxy, monkeypatch, tmp_path -): +def test_load_unsup_images_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): plugin = BasePluginFolder(make_napari_viewer_proxy()) image_0 = tmp_path / "b.tif" @@ -273,16 +261,12 @@ def test_load_unsup_images_dataset( image_0.write_text("fake") image_1.write_text("fake") - monkeypatch.setattr( - plugin, "load_dataset_paths", lambda: [image_0, image_1] - ) + monkeypatch.setattr(plugin, "load_dataset_paths", lambda: [image_0, image_1]) plugin.load_unsup_images_dataset() assert plugin.validation_filepaths == [str(image_1), str(image_0)] - assert plugin.unsupervised_images_filewidget.text_field.text() == str( - tmp_path - ) + assert plugin.unsupervised_images_filewidget.text_field.text() == str(tmp_path) def test_show_file_dialog_updates_filetype( @@ -359,9 +343,7 @@ def test_load_results_path_sets_results_path( plugin._load_results_path() assert plugin.results_path == str(tmp_path.resolve()) - assert plugin.results_filewidget.text_field.text() == str( - tmp_path.resolve() - ) + assert plugin.results_filewidget.text_field.text() == str(tmp_path.resolve()) def test_show_and_hide_io_element_without_toggle(qtbot): diff --git a/napari_cellseg3d/code_models/model_framework.py b/napari_cellseg3d/code_models/model_framework.py index fadf9c40..933bb412 100644 --- a/napari_cellseg3d/code_models/model_framework.py +++ b/napari_cellseg3d/code_models/model_framework.py @@ -293,7 +293,7 @@ def create_train_dataset_dict(self): data_dicts = [ {"image": image_name, "label": label_name} for image_name, label_name in zip( - self.images_filepaths, self.labels_filepaths + self.images_filepaths, self.labels_filepaths, strict=False ) ] logger.debug(f"Training data dict : {data_dicts}") diff --git a/napari_cellseg3d/code_models/models/unet/buildingblocks.py b/napari_cellseg3d/code_models/models/unet/buildingblocks.py index d9c68e47..d5bc223b 100644 --- a/napari_cellseg3d/code_models/models/unet/buildingblocks.py +++ b/napari_cellseg3d/code_models/models/unet/buildingblocks.py @@ -90,7 +90,7 @@ def create_conv(in_channels, out_channels, kernel_size, order, num_groups, paddi class SingleConv(nn.Sequential): """Basic convolutional module consisting of a Conv3d, non-linearity and optional batchnorm/groupnorm. The order - of operations can be specified via the `order` parameter + of operations can be specified via the `order` parameter. Args: in_channels (int): number of input channels @@ -206,7 +206,7 @@ class ExtResNetBlock(nn.Module): The SingleConv takes care of increasing/decreasing the number of channels and also ensures that the number of output channels is compatible with the residual block that follows. This block can be used instead of standard DoubleConv in the Encoder module. - Motivated by: https://arxiv.org/pdf/1706.00120.pdf + Motivated by: https://arxiv.org/pdf/1706.00120.pdf. Notice we use ELU instead of ReLU (order='cge') and put non-linearity after the groupnorm. """ @@ -515,7 +515,7 @@ class InterpolateUpsampling(AbstractUpsampling): """Args: mode (str): algorithm used for upsampling: 'nearest' | 'linear' | 'bilinear' | 'trilinear' | 'area'. Default: 'nearest' - used only if transposed_conv is False + used only if transposed_conv is False. """ def __init__(self, mode="nearest"): @@ -536,7 +536,7 @@ class TransposeConvUpsampling(AbstractUpsampling): kernel_size (int or tuple): size of the convolving kernel used only if transposed_conv is True scale_factor (int or tuple): stride of the convolution - used only if transposed_conv is True + used only if transposed_conv is True. """ diff --git a/napari_cellseg3d/code_models/models/unet/model.py b/napari_cellseg3d/code_models/models/unet/model.py index d4d4deb3..1adefa79 100644 --- a/napari_cellseg3d/code_models/models/unet/model.py +++ b/napari_cellseg3d/code_models/models/unet/model.py @@ -114,7 +114,9 @@ def forward(self, x): encoders_features = encoders_features[1:] # decoder part - for decoder, encoder_features in zip(self.decoders, encoders_features): + for decoder, encoder_features in zip( + self.decoders, encoders_features, strict=False + ): # pass the output from the corresponding encoder and the output # of the previous decoder x = decoder(encoder_features, x) diff --git a/napari_cellseg3d/code_plugins/plugin_convert.py b/napari_cellseg3d/code_plugins/plugin_convert.py index c7c743ab..4b301199 100644 --- a/napari_cellseg3d/code_plugins/plugin_convert.py +++ b/napari_cellseg3d/code_plugins/plugin_convert.py @@ -164,11 +164,11 @@ def __init__(self, viewer: "napari.Viewer.viewer", parent=None): ) [ lbl.setText(f"Size in {ax} (pixels):") - for lbl, ax in zip(self.size_selection.box_widgets_lbl, "xyz") + for lbl, ax in zip(self.size_selection.box_widgets_lbl, "xyz", strict=False) ] [ w.setToolTip(f"Size of crop for {dim} axis") - for w, dim in zip(self.size_selection.box_widgets, "xyz") + for w, dim in zip(self.size_selection.box_widgets, "xyz", strict=False) ] self.image_layer_loader.layer_list.label.setText("Layer :") diff --git a/napari_cellseg3d/code_plugins/plugin_crf.py b/napari_cellseg3d/code_plugins/plugin_crf.py index 6bc41428..461644d4 100644 --- a/napari_cellseg3d/code_plugins/plugin_crf.py +++ b/napari_cellseg3d/code_plugins/plugin_crf.py @@ -226,7 +226,7 @@ def _check_ready(self): def run_crf_on_batch(self, images_list: list, labels_list: list, log=None): """Run CRF on a batch of images and labels.""" self.crf_results = [] - for image, label in zip(images_list, labels_list): + for image, label in zip(images_list, labels_list, strict=False): tqdm( unit="B", total=len(images_list), diff --git a/napari_cellseg3d/code_plugins/plugin_crop.py b/napari_cellseg3d/code_plugins/plugin_crop.py index 47f99f6b..b0961f1e 100644 --- a/napari_cellseg3d/code_plugins/plugin_crop.py +++ b/napari_cellseg3d/code_plugins/plugin_crop.py @@ -177,7 +177,9 @@ def _build(self): dim_group_l.addWidget(self.aniso_widgets) [ dim_group_l.addWidget(widget, alignment=ui.ABS_AL) - for widget_list in zip(self.crop_size_labels, self.crop_size_widgets) + for widget_list in zip( + self.crop_size_labels, self.crop_size_widgets, strict=False + ) for widget in widget_list ] dim_group_w.setLayout(dim_group_l) @@ -546,7 +548,7 @@ def set_slice( sliders = [ ui.Slider(text_label=axis, lower=0, upper=end, step=step) - for axis, end, step in zip("zyx", ends, stepsizes) + for axis, end, step in zip("zyx", ends, stepsizes, strict=False) ] self.sliders = sliders for axis, slider in enumerate(sliders): diff --git a/napari_cellseg3d/code_plugins/plugin_metrics.py b/napari_cellseg3d/code_plugins/plugin_metrics.py index ca37cfb1..b8a07b39 100644 --- a/napari_cellseg3d/code_plugins/plugin_metrics.py +++ b/napari_cellseg3d/code_plugins/plugin_metrics.py @@ -193,7 +193,7 @@ def compute_dice(self): self.canvas = None # kind of terrible way to stack plots... but it works. for image_id, (ground_path, pred_path) in enumerate( - zip(self.images_filepaths, self.labels_filepaths), + zip(self.images_filepaths, self.labels_filepaths, strict=False), start=1, ): ground = imread(ground_path) diff --git a/napari_cellseg3d/code_plugins/plugin_model_training.py b/napari_cellseg3d/code_plugins/plugin_model_training.py index da1a77b7..b1b52949 100644 --- a/napari_cellseg3d/code_plugins/plugin_model_training.py +++ b/napari_cellseg3d/code_plugins/plugin_model_training.py @@ -625,7 +625,9 @@ def _build(self): patch_size_l = patch_size_w.layout [ patch_size_l.addWidget(widget, alignment=ui.LEFT_AL) - for widgts in zip(self.patch_size_lbl, self.patch_size_widgets) + for widgts in zip( + self.patch_size_lbl, self.patch_size_widgets, strict=False + ) for widget in widgts ] # patch sizes patch_size_w.setLayout(patch_size_l) diff --git a/napari_cellseg3d/code_plugins/plugin_review.py b/napari_cellseg3d/code_plugins/plugin_review.py index 1b1957cc..0f4e7671 100644 --- a/napari_cellseg3d/code_plugins/plugin_review.py +++ b/napari_cellseg3d/code_plugins/plugin_review.py @@ -437,7 +437,7 @@ def crop_volume_around_point(points, layer, zoom_factor): crop_slice = tuple( slice(np.maximum(0, min_coordinate), max_coordinate) for min_coordinate, max_coordinate in zip( - min_coordinates, max_coordinates + min_coordinates, max_coordinates, strict=False ) ) diff --git a/napari_cellseg3d/code_plugins/plugin_utilities.py b/napari_cellseg3d/code_plugins/plugin_utilities.py index 01d83b64..86a1c43d 100644 --- a/napari_cellseg3d/code_plugins/plugin_utilities.py +++ b/napari_cellseg3d/code_plugins/plugin_utilities.py @@ -106,7 +106,7 @@ def _build(self): self._update_visibility() def _create_utils_widgets(self, names): - for key, name in zip(UTILITIES_WIDGETS, names): + for key, name in zip(UTILITIES_WIDGETS, names, strict=False): logger.debug(f"Creating {name} widget") setattr(self, name, UTILITIES_WIDGETS[key](self._viewer)) diff --git a/napari_cellseg3d/dev_scripts/colab_training.py b/napari_cellseg3d/dev_scripts/colab_training.py index 79bcfdbb..0109f60e 100644 --- a/napari_cellseg3d/dev_scripts/colab_training.py +++ b/napari_cellseg3d/dev_scripts/colab_training.py @@ -203,5 +203,7 @@ def create_eval_dataset_dict(image_directory, label_directory): logger.info(Path(file).name) return [ {"image": image_name, "label": label_name} - for image_name, label_name in zip(images_filepaths, labels_filepaths) + for image_name, label_name in zip( + images_filepaths, labels_filepaths, strict=False + ) ] diff --git a/napari_cellseg3d/dev_scripts/remote_training.py b/napari_cellseg3d/dev_scripts/remote_training.py index 1121465b..cced0c63 100644 --- a/napari_cellseg3d/dev_scripts/remote_training.py +++ b/napari_cellseg3d/dev_scripts/remote_training.py @@ -76,7 +76,7 @@ def prepare_data(images_path, labels_path): return [ {"image": str(image_path), "label": str(label_path)} - for image_path, label_path in zip(images, labels) + for image_path, label_path in zip(images, labels, strict=False) ] diff --git a/napari_cellseg3d/interface.py b/napari_cellseg3d/interface.py index 125e8af4..50cc7020 100644 --- a/napari_cellseg3d/interface.py +++ b/napari_cellseg3d/interface.py @@ -712,7 +712,7 @@ def __init__( ) [ w.setToolTip(f"Anisotropic resolution in microns for {dim} axis") - for w, dim in zip(self.box_widgets, "xyz") + for w, dim in zip(self.box_widgets, "xyz", strict=False) ] ################## @@ -729,7 +729,7 @@ def build(self): """Builds the layout of the widget.""" [ self.container.layout.addWidget(widget, alignment=HCENTER_AL) - for widgets in zip(self.box_widgets_lbl, self.box_widgets) + for widgets in zip(self.box_widgets_lbl, self.box_widgets, strict=False) for widget in widgets ] # anisotropy diff --git a/napari_cellseg3d/utils.py b/napari_cellseg3d/utils.py index 4f3a0f1a..4458c38d 100644 --- a/napari_cellseg3d/utils.py +++ b/napari_cellseg3d/utils.py @@ -44,7 +44,7 @@ def save_folder(results_path, folder_name, images, image_paths, exist_ok=False): results_folder = results_path / Path(folder_name) results_folder.mkdir(exist_ok=exist_ok, parents=True) - for file, image in zip(image_paths, images): + for file, image in zip(image_paths, images, strict=False): path = results_folder / Path(file).name imwrite( diff --git a/notebooks/Colab_inference_demo.ipynb b/notebooks/Colab_inference_demo.ipynb index 85d7f4a4..c31206da 100644 --- a/notebooks/Colab_inference_demo.ipynb +++ b/notebooks/Colab_inference_demo.ipynb @@ -442,14 +442,15 @@ "\n", "\n", "def random_label_cmap(n=2**16, h=(0, 1), l=(0.4, 1), s=(0.2, 0.8)):\n", - " \"\"\"FUNCTION TAKEN FROM STARDIST REPO : https://github.com/stardist/stardist/blob/c6c261081c6f9717fa9f5c47720ad2d5a9153224/stardist/plot/plot.py#L8\"\"\"\n", + " \"\"\"FUNCTION TAKEN FROM STARDIST REPO : https://github.com/stardist/stardist/blob/c6c261081c6f9717fa9f5c47720ad2d5a9153224/stardist/plot/plot.py#L8.\"\"\"\n", " h, l, s = (\n", " rng.uniform(*h, n),\n", " rng.uniform(*l, n),\n", " rng.uniform(*s, n),\n", " )\n", " cols = np.stack(\n", - " [colorsys.hls_to_rgb(_h, _l, _s) for _h, _l, _s in zip(h, l, s)], axis=0\n", + " [colorsys.hls_to_rgb(_h, _l, _s) for _h, _l, _s in zip(h, l, s, strict=False)],\n", + " axis=0,\n", " )\n", " cols[0] = 0\n", " # reset the random generator to the first draw to keep the colormap consistent\n", diff --git a/pyproject.toml b/pyproject.toml index d7884e5f..ec2dd166 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,164 +1,148 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=64", + "setuptools-scm>=8", + "wheel", +] + [project] -name = "napari_cellseg3d" +name = "napari-cellseg3d" +description = "Plugin for cell segmentation in 3D" +readme = "README.md" +license = { text = "MIT" } authors = [ - {name = "Cyril Achard", email = "cyril.achard@epfl.ch"}, - {name = "Maxime Vidal", email = "maxime.vidal@epfl.ch"}, - {name = "Mackenzie Mathis", email = "mackenzie@post.harvard.edu"}, + { name = "Cyril Achard", email = "cyril.achard@epfl.ch" }, + { name = "Maxime Vidal", email = "maxime.vidal@epfl.ch" }, + { name = "Mackenzie Mathis", email = "mackenzie@post.harvard.edu" }, ] -readme = "README.md" -description = "Plugin for cell segmentation in 3D" +requires-python = ">=3.10" classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "Framework :: napari", - "Topic :: Software Development :: Testing", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Scientific/Engineering :: Image Processing", - "Topic :: Scientific/Engineering :: Visualization", + "Development Status :: 4 - Beta", + "Framework :: napari", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Image Processing", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Software Development :: Testing", ] -license = {text = "MIT"} -requires-python = ">=3.10" +dynamic = [ "version" ] dependencies = [ - "numpy", - "napari[all]>=0.4.14", - "QtPy", -# "opencv-python>=4.5.5", -# "dask-image>=0.6.0", - "scikit-image>=0.19.2", - "matplotlib>=3.4.1", -# "tifffile>=2022.2.9", -# "imageio-ffmpeg>=0.4.5", - "imagecodecs>=2023.3.16", - "torch>=1.11", - "monai[nibabel,einops, tifffile]>=0.9.0", - "itk", - "tqdm", -# "nibabel", -# "pillow", - "pyclesperanto", - "pydensecrf2", + # "tifffile>=2022.2.9", + # "imageio-ffmpeg>=0.4.5", + "imagecodecs>=2023.3.16", + "itk", + "matplotlib>=3.4.1", + "monai[einops,nibabel,tifffile]>=0.9", + "napari[all]>=0.4.14", + "numpy", + # "nibabel", + # "pillow", + "pyclesperanto", + "pydensecrf2", + "qtpy", + # "opencv-python>=4.5.5", + # "dask-image>=0.6.0", + "scikit-image>=0.19.2", + "torch>=1.11", + "tqdm", ] -dynamic = ["version"] - -[project.urls] -Homepage = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" -Documentation = "https://adaptivemotorcontrollab.github.io/cellseg3d-docs/res/welcome.html" -Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" - -[project.entry-points."napari.manifest"] -"napari_cellseg3d" = "napari_cellseg3d:napari.yaml" - -[build-system] -requires = [ - "setuptools>=64", - "setuptools-scm>=8", - "wheel", +optional-dependencies.crf = [ + "pydensecrf2", ] -build-backend = "setuptools.build_meta" +optional-dependencies.dev = [ + "pre-commit", + "ruff", + "twine", +] +optional-dependencies.docs = [ + "jupyter-book", +] +optional-dependencies.onnx-cpu = [ "onnx", "onnxruntime" ] +optional-dependencies.onnx-gpu = [ "onnx", "onnxruntime-gpu" ] +optional-dependencies.pyqt6 = [ + "napari[pyqt6]", +] +optional-dependencies.pyside6 = [ + "napari[pyside6]", +] +optional-dependencies.test = [ + "coverage", + "onnx", + "onnxruntime", + "pytest", + "pytest-cov", + "pytest-qt", + "tox", + "twine", +] +optional-dependencies.wandb = [ "wandb" ] +urls.Documentation = "https://adaptivemotorcontrollab.github.io/cellseg3d-docs/res/welcome.html" +urls.Homepage = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" +urls.Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" +entry-points."napari.manifest".napari_cellseg3d = "napari_cellseg3d:napari.yaml" [tool.setuptools] include-package-data = true +package-data."*" = [ "res/*.png", "code_models/models/pretrained/*.json", "*.yaml", "napari.yaml" ] +packages.find.where = [ "." ] [tool.setuptools_scm] version_file = "napari_cellseg3d/_version.py" -[tool.setuptools.packages.find] -where = ["."] - -[tool.setuptools.package-data] -"*" = ["res/*.png", "code_models/models/pretrained/*.json", "*.yaml", "napari.yaml"] - [tool.ruff] -select = [ - "E", "F", "W", - "A", - "B", - "D", - "G", - "I", - "PT", - "PTH", - "RET", - "SIM", - "NPY", +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "docs/conf.py", + "napari_cellseg3d/_tests/conftest.py", + "node_modules", + "venv", ] # Never enforce `E501` (line length violations) and 'E741' (ambiguous variable names) # and 'G004' (do not use f-strings in logging) # and 'A003' (Shadowing python builtins) # and 'F401' (imported but unused) -ignore = ["E501", "E741", "G004", "A003", "F401"] -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", - "docs/conf.py", - "napari_cellseg3d/_tests/conftest.py", -] - -[tool.ruff.pydocstyle] -convention = "google" - -[project.optional-dependencies] -pyside6 = [ - "napari[pyside6]", -] -pyqt6 = [ - "napari[pyqt6]", -] -onnx-cpu = [ - "onnx", - "onnxruntime" -] -onnx-gpu = [ - "onnx", - "onnxruntime-gpu" -] -wandb = [ - "wandb" -] -dev = [ - "ruff", - "pre-commit", - "twine", -] -docs = [ - "jupyter-book", -] -test = [ - "pytest", - "pytest-qt", - "pytest-cov", - "coverage", - "tox", - "twine", - "onnx", - "onnxruntime", +# and 'E402' (module level import not at top of file) +ignore = [ "E501", "E741", "G004", "A003", "F401", "E402" ] +pydocstyle.convention = "google" +select = [ + "E", + "F", + "W", + "A", + "B", + "G", + "I", + "PT", + "PTH", + "RET", + "SIM", + "NPY", ] -crf = [ - "pydensecrf2", -] \ No newline at end of file From f64f467879d9e56d2e4830e9e4d89f1a3d3efa19 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 20 May 2026 16:24:18 +0200 Subject: [PATCH 5/5] Migrate pyproject.toml to PEP 621 tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework pyproject.toml to use canonical PEP 621 / table-based metadata and tidy tool configs. Authors moved to [[project.authors]], entry points to [project.entry-points], and optional dependencies to [project.optional-dependencies]; project.urls was added. Setuptools package-data and packages entries were converted to table form. Cleaned up classifiers (removed 3.13/3.14) and relocated ruff/pydocstyle settings under [tool.ruff.*]. Added [tool.pyproject-fmt] formatting options. No functional dependency versions were changed—this is metadata/formatting only to improve packaging/tooling compatibility. --- pyproject.toml | 73 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec2dd166..edc9afbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,11 +11,6 @@ name = "napari-cellseg3d" description = "Plugin for cell segmentation in 3D" readme = "README.md" license = { text = "MIT" } -authors = [ - { name = "Cyril Achard", email = "cyril.achard@epfl.ch" }, - { name = "Maxime Vidal", email = "maxime.vidal@epfl.ch" }, - { name = "Mackenzie Mathis", email = "mackenzie@post.harvard.edu" }, -] requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", @@ -28,8 +23,6 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Image Processing", "Topic :: Scientific/Engineering :: Visualization", @@ -56,26 +49,38 @@ dependencies = [ "torch>=1.11", "tqdm", ] -optional-dependencies.crf = [ +[[project.authors]] +name = "Cyril Achard" +email = "cyril.achard@epfl.ch" +[[project.authors]] +name = "Maxime Vidal" +email = "maxime.vidal@epfl.ch" +[[project.authors]] +name = "Mackenzie Mathis" +email = "mackenzie@post.harvard.edu" +[project.entry-points] +"napari.manifest".napari_cellseg3d = "napari_cellseg3d:napari.yaml" +[project.optional-dependencies] +crf = [ "pydensecrf2", ] -optional-dependencies.dev = [ +dev = [ "pre-commit", "ruff", "twine", ] -optional-dependencies.docs = [ +docs = [ "jupyter-book", ] -optional-dependencies.onnx-cpu = [ "onnx", "onnxruntime" ] -optional-dependencies.onnx-gpu = [ "onnx", "onnxruntime-gpu" ] -optional-dependencies.pyqt6 = [ +onnx-cpu = [ "onnx", "onnxruntime" ] +onnx-gpu = [ "onnx", "onnxruntime-gpu" ] +pyqt6 = [ "napari[pyqt6]", ] -optional-dependencies.pyside6 = [ +pyside6 = [ "napari[pyside6]", ] -optional-dependencies.test = [ +test = [ "coverage", "onnx", "onnxruntime", @@ -85,16 +90,18 @@ optional-dependencies.test = [ "tox", "twine", ] -optional-dependencies.wandb = [ "wandb" ] -urls.Documentation = "https://adaptivemotorcontrollab.github.io/cellseg3d-docs/res/welcome.html" -urls.Homepage = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" -urls.Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" -entry-points."napari.manifest".napari_cellseg3d = "napari_cellseg3d:napari.yaml" +wandb = [ "wandb" ] +[project.urls] +Documentation = "https://adaptivemotorcontrollab.github.io/cellseg3d-docs/res/welcome.html" +Homepage = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" +Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" [tool.setuptools] include-package-data = true -package-data."*" = [ "res/*.png", "code_models/models/pretrained/*.json", "*.yaml", "napari.yaml" ] -packages.find.where = [ "." ] +[tool.setuptools.package-data] +"*" = [ "res/*.png", "code_models/models/pretrained/*.json", "*.yaml", "napari.yaml" ] +[tool.setuptools.packages] +find.where = [ "." ] [tool.setuptools_scm] version_file = "napari_cellseg3d/_version.py" @@ -125,13 +132,7 @@ exclude = [ "node_modules", "venv", ] -# Never enforce `E501` (line length violations) and 'E741' (ambiguous variable names) -# and 'G004' (do not use f-strings in logging) -# and 'A003' (Shadowing python builtins) -# and 'F401' (imported but unused) -# and 'E402' (module level import not at top of file) -ignore = [ "E501", "E741", "G004", "A003", "F401", "E402" ] -pydocstyle.convention = "google" +[tool.ruff.lint] select = [ "E", "F", @@ -146,3 +147,17 @@ select = [ "SIM", "NPY", ] +# Never enforce `E501` (line length violations) and 'E741' (ambiguous variable names) +# and 'G004' (do not use f-strings in logging) +# and 'A003' (Shadowing python builtins) +# and 'F401' (imported but unused) +# and 'E402' (module level import not at top of file) +ignore = [ "E501", "E741", "G004", "A003", "F401", "E402" ] +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.pyproject-fmt] +max_supported_python = "3.12" +generate_python_version_classifiers = true +# Avoid collapsing tables to field.key = value format (less readable) +table_format = "long"