{"name":"pentachrome-plugin","display_name":"Pentachrome Pipeline","visibility":"public","icon":null,"categories":[],"schema_version":"0.2.1","on_activate":null,"on_deactivate":null,"contributions":{"commands":[{"id":"pentachrome-plugin.guided","title":"Guided Analysis","python_name":"pentachrome_plugin._guided_widget:GuidedWidget","short_title":null,"category":null,"icon":null,"enablement":null}],"readers":null,"writers":null,"widgets":[{"command":"pentachrome-plugin.guided","display_name":"Guided Analysis","autogenerate":false}],"sample_data":null,"themes":null,"menus":{},"submenus":null,"keybindings":null,"configuration":[]},"package_metadata":{"metadata_version":"2.4","name":"pentachrome-plugin","version":"0.9.1","dynamic":["license-file"],"platform":null,"supported_platform":null,"summary":"Napari plugin for the Pentachrome histology pipeline: VSI extraction, nnUNet inference, statistics.","description":"# pentachrome-plugin\n\nNapari plugin for the Pentachrome histology pipeline. The public plugin exposes a\nsingle clinician-facing widget, **Guided Analysis**, that walks through the whole\nworkflow in one pane:\n\n1. **Extract** — pull tissue-region TIFFs out of Olympus `.vsi` files.\n2. **Detect** — run the trained nnUNet Epithelium / MultiStructure models on those\n   images and load colorized masks back into the viewer.\n3. **Measure** — per-region statistics (thickness, composition, cell densities),\n   with CSV export.\n\nEach phase also exists as a separate advanced widget (`VsiExtractorWidget`,\n`NnUnetInferenceWidget`, `AnalysisWidget`) used during development. These are **not**\nregistered in the public menu, see [From source (development)](#from-source-development).\n\nSource and issues: https://github.com/dtsilis7/Pentrachrome-Pipeline\n\n**Requirements:** Windows, Python 3.10, napari >= 0.4.18, and the nnUNet model\nweights. VSI reading uses `bioio-bioformats` (a plugin dependency) which\nauto-downloads a JDK on first use — **no Java install, no javabridge**. Only the\nmodel weights are installed separately (see below).\n\n## Installation (Windows, PowerShell)\n\nA working install has **three parts**, in this order:\n\n1. A conda environment (Python 3.10) with NumPy<2\n2. The plugin itself (pulls in the JDK-free `bioio-bioformats` VSI reader)\n3. The nnUNet model weights (downloaded separately)\n\n> ⚠️ **`pip install pentachrome-plugin` gives you the UI *and* VSI extraction, but\n> inference still needs the model weights** (Step 3) in the *same* env. Installing\n> through napari's plugin manager only does the pip part.\n\n### Step 1 — environment\n\nCreate a Python 3.10 env pinned to `numpy<2` (the `bioio-bioformats` VSI reader is\npinned <2, and nnU-Net / the imaging stack share the 1.x ABI).\n\n```powershell\nconda create -n pentachrome python=3.10 \"numpy<2\" -y\nconda activate pentachrome\n```\n\n**No JDK or javabridge to install.** VSI reading goes through `bioio-bioformats`\n(pulled in by Step 2), which provisions the Bio-Formats JVM via scyjava + cjdk and\n**auto-downloads a JDK the first time you extract** (cached afterward).\n\n### Step 2 — the plugin\n\n```powershell\npip install pentachrome-plugin   # also installs the bioio-bioformats VSI reader\npip install nnunetv2             # required for the Detect step\n```\n\nYou can also install the plugin through napari's **Plugins -> Install/Uninstall\nPlugins** dialog (search \"pentachrome\"), but that only covers this step, you still\nneed Step 1 and Step 3 in the same environment.\n\n**Already installed?** To pull a newer release into the same env, upgrade just the\nplugin (leaves PyTorch, nnU-Net, and weights untouched); restart napari afterwards:\n\n```powershell\npip install --upgrade pentachrome-plugin\npip show pentachrome-plugin        # check the \"Version:\" line\n```\n\n### Step 3 — model weights\n\nDownload the nnUNet weights and point the widget at them — see\n[Model weights](#model-weights) below.\n\n### Step 4 — GPU (CUDA) acceleration (optional, NVIDIA only)\n\n> ⚠️ **`pip install nnunetv2` installs the CPU-only PyTorch wheel.** On a machine\n> with an NVIDIA GPU, the Guided Analysis **Check GPU** button will still report\n> *\"No CUDA GPU available to PyTorch\"* and inference runs on the (much slower) CPU,\n> because the default PyTorch wheel from PyPI (`torch ...+cpu`) contains no CUDA at\n> all. Installing the NVIDIA CUDA toolkit system-wide does **not** fix this — the\n> PyTorch wheel bundles its own CUDA runtime.\n\nTo use the GPU, reinstall PyTorch from the matching CUDA index **after** nnU-Net,\nso the CUDA wheel wins:\n\n```powershell\nconda activate pentachrome\n# Do this AFTER `pip install nnunetv2` — otherwise nnU-Net drags the +cpu wheel back in.\npip install --force-reinstall torch --index-url https://download.pytorch.org/whl/cu124\n```\n\nPick the CUDA line your **driver** supports (run `nvidia-smi`; the top-right\n\"CUDA Version\" is the max your driver allows — a newer driver runs older wheels):\n\n| GPU generation | Use index |\n| --- | --- |\n| Most RTX 20/30/40-series | `.../whl/cu124` (or `cu121`) |\n| **RTX 50-series (Blackwell, e.g. 5060 Ti)** | `.../whl/cu128` — needs PyTorch ≥ 2.7; earlier CUDA wheels have no kernels for these cards |\n| Older driver that can't do 12.x | `.../whl/cu118` |\n\nVerify (in the same env):\n\n```powershell\npython -c \"import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())\"\n```\n\nYou want a version ending in `+cuXXX` (not `+cpu`), a CUDA number, and `True`.\nThen **Check GPU** in the widget turns green. If you ever reinstall or upgrade\n`nnunetv2`, redo this step — it can pull the `+cpu` wheel back in.\n\n> On managed/locked-down Windows machines, inference is launched through the env's\n> `python.exe` (not the generated `nnUNetv2_predict.exe`), so AppLocker / group-policy\n> `.exe` restrictions don't block it (fixed in 0.7.1).\n\n### Verify\n\n```powershell\npython -m napari\n```\n\nIn napari, open **Plugins -> Guided Analysis** — the widget should load and show\nits Extract / Analyze / Statistics steps.\n\n### From source (development)\n\nFor working on the plugin itself, do Step 1 above, then install editable from a\ncheckout instead of from PyPI (`cd` into the plugin directory first, or pass the\nabsolute path):\n\n```powershell\nconda activate pentachrome\ncd \"...\\pentachrome_plugin\"\npip install -e .\n```\n\nThe three phase widgets are de-registered from the public menu. To open them\nstandalone during development, run the repo's `dev_widgets.py` — it docks the\nExtractor / Inference / Statistics widgets as tabs:\n\n```powershell\npython dev_widgets.py\n```\n\n## Launch\n\n```powershell\nconda activate pentachrome   # or whichever env you installed into\npython -m napari\n```\n\nIn napari: **Plugins -> Guided Analysis**.\n\n> The per-phase sections below (nnUNet Inference, Mask Statistics, etc.) describe\n> the underlying widgets, which the Guided Analysis pane drives end-to-end. Their\n> **Plugins -> ...** menu references apply only to the dev widgets opened via\n> `dev_widgets.py`; end users reach the same functionality through Guided Analysis.\n\n## Model weights\n\nThe nnUNet weights (~900 MB) aren't bundled in the PyPI package. Download\n[`nnunet_results.zip`]\nfrom the [releases page](https://github.com/dtsilis7/Pentrachrome-Pipeline/releases/tag/weights-v1),\nunzip it, and point the inference widget's **nnUNet results** field at the\nextracted folder (the one containing `Dataset001_Epithelium` and\n`Dataset002_MultiStructure`).\n\n## nnUNet Inference (Phase 2)\n\nRequires `nnunetv2` installed in the same environment (the `nnUNetv2_predict` CLI must be on PATH) — this is covered by Step 2 of [Installation](#installation-windows-powershell) above.\n\nWorkflow:\n\n1. Load TIFFs into napari (e.g. via Phase 1's auto-load checkbox, or drag-and-drop).\n2. Open **Plugins -> nnUNet Inference**.\n3. Select one or more image layers in the list.\n4. Tick **Epithelium**, **MultiStructure**, or both.\n5. Set **Output folder** (where raw + colorized masks go) and **nnUNet results** (folder containing `Dataset001_Epithelium` and `Dataset002_MultiStructure`). The results path auto-fills if `nnUNet_Training/nnUNet_results/results` is found.\n6. Pick **Device** (`cpu` or `cuda`) and click **Analyze**.\n\n### Speed vs quality (important on CPU)\n\nnnUNet inference on a laptop CPU is slow because every image goes through *folds × mirror augmentations × sliding-window patches* forward passes. With defaults that can be 20+ passes per image. The widget exposes three knobs in the **Speed / quality** group:\n\n| Knob | Default | What it does |\n| --- | --- | --- |\n| Epithelium folds | `Fold 0 only` | Use 1 of the 5 trained folds for Dataset001. All 5 ensembled is best quality but ~5x slower. Dataset002 only has fold 0 trained, so it's always 1 fold. |\n| Disable test-time mirroring | on | Passes `--disable_tta`. Skips the 4 mirror augmentations the model normally averages over. ~4x faster, small accuracy hit. |\n| Sliding-window step | `0.5` | Passes `-step_size`. Larger = fewer overlapping patches = faster but rougher tile borders. Try `0.7` for a middle ground. |\n\nWith all three defaults on a CPU laptop, one ROI tile should take a few minutes instead of 30+. Switch to `All 5 folds` + TTA on once you've moved to a GPU box.\n\n### Continuing from the extractor\n\nThe two widgets are linked through two small bridges, so you can run **Extract -> Analyze** in a single napari session without re-picking files:\n\n- When the extractor auto-loads a TIFF as a viewer layer, it stashes the on-disk path on `layer.metadata['source_tiff']`. The inference widget reads that during staging and **copies the original file** into `_staging_input/` rather than re-saving the in-memory array, important for 15k x 15k tiles.\n- When an extraction completes, the inference widget's **\"Use last extractor output\"** button pre-fills the output folder to `<extractor_output_root>/_inference`, so masks land next to the per-VSI subfolders the extractor created.\n\nBoth bridges are in-process only (see `_session.py`); they reset when napari closes.\n\nOutputs land in:\n\n```\n<output_folder>/\n  _staging_input/            # nnUNet-named (_0000.tif) copies of the selected layers\n  epithelium_raw/            # binary masks from Dataset001\n  epithelium_colored/        # RGB colorized masks (red epithelium)\n  multistructure_raw/        # 6-class masks from Dataset002\n  multistructure_colored/    # RGB colorized masks (Elastin/Collagen/Nuclei/Mucins/Membrane/Goblets)\n```\n\nColorized masks are added to the viewer as RGB image layers when the run finishes.\n\n### nnUNet inference architecture\n\nSame subprocess pattern as Phase 1. The widget never imports torch or nnUNetv2 directly; it spawns `_inference_worker.py` which:\n\n- sets `nnUNet_results` to the configured results dir,\n- calls `nnUNetv2_predict` once per enabled model (folds 0-4 for Epithelium, fold 0 for MultiStructure, matching `run_inference.py`),\n- colorizes the resulting integer masks with the palettes from `colorize_masks.py` / `compare_grid.py`,\n- streams JSON-line events on stdout for the widget's progress bar and log.\n\n## How it works\n\n- The widget itself never touches the JVM. When you click **Extract ROIs**, it spawns `_vsi_worker_bioio.py` as a separate Python process.\n- That worker starts the Bio-Formats JVM through `bioio-bioformats` (scyjava + cjdk — no user JDK), loops over the VSI files using `TileMaskStitcher` (`vsi_handler/tile_mask_stitcher_bioio.py`, which drives `loci.formats.ImageReader` directly via `vsi_handler/_bioio_reader.py`), writes numbered TIFFs into `<output_root>/<vsi_basename>/`, and emits JSON-line progress events on stdout.\n- The widget streams those events on a background thread and updates the progress bar / log without blocking the UI.\n- When the worker exits, the JVM dies with it. The next extraction batch starts a fresh JVM in a fresh process - this avoids the \"JVM cannot be restarted\" pitfall during a long napari session.\n\n## Defaults\n\nThe parameter defaults mirror `Processing_VSI_Files.py`:\n\n| Parameter | Default |\n| --- | --- |\n| Series | 6 |\n| Tile width / height | 15000 |\n| Threshold | 50 |\n| Min ROI area | 150000 |\n| Merge margin | 1000 |\n| Extra crop margin | 100 |\n\n## Layout\n\n```\npentachrome_plugin/\n  pyproject.toml\n  README.md\n  src/pentachrome_plugin/\n    __init__.py\n    napari.yaml             # napari manifest\n    _session.py             # in-process cross-widget state (extractor -> inference -> analysis)\n    _widget.py              # VsiExtractorWidget (Phase 1)\n    _vsi_worker_bioio.py    # VSI subprocess entrypoint (bioio-bioformats)\n    vsi_handler/            # bioio VSI reader (_bioio_reader.py) + tile/mask stitcher\n    _inference_widget.py    # NnUnetInferenceWidget (Phase 2)\n    _inference_worker.py    # nnUNet subprocess entrypoint\n    _analysis_widget.py     # AnalysisWidget (Phase 3, in-process)\n```\n\nPhase 3 (Mask Statistics) lives alongside these and registers through `napari.yaml`.\n\n## Mask Statistics (Phase 3)\n\nPure in-process; no subprocess needed (no JVM, no torch). Reuses\n`EpithelialAnalysis/Analyzers/` (`Descriptors.py`, `Thickness.py`), so the\nsame metrics that fed the original `region_summary.csv` show up in the\nwidget.\n\nWorkflow:\n\n1. Run Phase 2 first so `epithelium_raw/` and `multistructure_raw/` exist.\n2. Open **Plugins -> Mask Statistics**.\n3. Select one or more image layers in the list (their names must match the\n   mask filenames in `epithelium_raw/` / `multistructure_raw/`; if the\n   inference widget staged them, that's already true).\n4. Click **Use last inference output** (or browse).\n5. Tweak **Pixel size**, **Region dilation**, **Min epithelium area** if\n   needed (defaults match `Main.py`).\n6. Click **Analyze**.\n\nFor each detected epithelial region the widget reports:\n\n| Column | What it is |\n| --- | --- |\n| Area (mm^2) | Region area after the 50 um dilation |\n| Thickness mean/std (um) | Medial-axis thickness of (membrane within eroded region) U goblets U nuclei |\n| Elastin / Collagen / Other % | Fraction of stained structure pixels, same definition as `compute_structure_percentages` |\n| Elastin/Collagen | Ratio of elastin to collagen structure pixels (`n/a` when the region has no collagen). In the sub-epithelial band this reads as the elastic-vs-fibrotic character of the remodelled layer; a falling ratio tracks airway-remodelling severity in the COPD literature. *(Added in 0.7.3.)* |\n| Mucin % | Mucin pixels as a fraction of the epithelium area (not of total structure pixels) |\n| Nuclei / mm^2 and Goblets / mm^2 | Density per mm^2 of epithelium, goblet hyperplasia is a classic COPD readout |\n| Nuclei (n), Goblets (n) | Raw counts inside the region |\n\nA bold **(all regions)** row appended per image gives area-weighted means\nof the percentages / thickness and totals for the counts. **Export CSV...**\nsaves the whole table (per-region rows + aggregate rows).\n\nThe exported CSV carries one extra column that the on-screen table deliberately\nhides: **`log2(E/C)`**, the base-2 log of the Elastin/Collagen ratio (0 = balanced,\n+1 = elastin twice collagen, −1 = the reverse). It's symmetric and so averages and\nstatistically tests correctly across regions/patients, where the raw ratio — bounded\nat 0, unbounded above — is skewed. It's kept out of the viewer because it's a stats\naid, not an at-a-glance number, and it's a pure function of the raw column so nothing\nis lost. *(Added in 0.7.3.)*\n\nThe elastin organization score (`ElastinAnalyzer.determine_organized_region`)\nfrom `Main.py` is intentionally not yet exposed, it's much heavier (skan +\nshapely + ROI polygons) and will land as a separate toggle.\n\n### Class isolation\n\nA \"Class isolation\" group at the top of the widget lets you view a single\nclass (or a combination) without rerunning anything:\n\n1. Pick a source layer (the **original** TIFF, not a colorized mask).\n2. Tick one or more of **Elastin**, **Collagen**, **Nuclei**, **Mucins**,\n   **Cell Membrane**, **Goblets**, **Epithelium**.\n3. Click one of:\n   - **Show as mask** — adds a new layer that's white everywhere except the\n     ticked classes, colored with the same palette as the inference widget.\n   - **Show on original** — adds a copy of the original image with all\n     pixels outside the ticked classes turned white. Useful for sanity-\n     checking the segmentation against the stain.\n4. **Clear isolated layers** removes everything this panel added in one go.\n\nMasks are read on demand from the inference output folder; the original\nlayer's pixels are taken from the viewer.\n\n## License\n\nThis project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details.\n","description_content_type":"text/markdown","keywords":"napari,histology,nnunet,bioformats,segmentation","home_page":null,"download_url":null,"author":"Dimitrios Tsilis","author_email":null,"maintainer":null,"maintainer_email":null,"license":"MIT License\n\nCopyright (c) 2026 Dimitrios Tsilis\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","classifier":["Framework :: napari","Development Status :: 4 - Beta","Intended Audience :: Science/Research","Programming Language :: Python :: 3.10","Operating System :: Microsoft :: Windows","License :: OSI Approved :: MIT License"],"requires_dist":["napari[all]>=0.4.18","qtpy","tifffile","numpy<2","opencv-python","scipy","scikit-image","skan","imagecodecs","bioio-bioformats<2","matplotlib","shapely","pandas"],"requires_python":">=3.10","requires_external":null,"project_url":["Homepage, https://github.com/dtsilis7/Pentrachrome-Pipeline","Bug Tracker, https://github.com/dtsilis7/Pentrachrome-Pipeline/issues","Changelog, https://github.com/dtsilis7/Pentrachrome-Pipeline/blob/main/NapariInterface/pentachrome_plugin/CHANGELOG.md"],"provides_extra":null,"provides_dist":null,"obsoletes_dist":null},"npe1_shim":false}