{"name":"napari-persistent-homology","display_name":"Persistent Homology 3D","visibility":"public","icon":"","categories":[],"schema_version":"0.2.1","on_activate":null,"on_deactivate":null,"contributions":{"commands":[{"id":"napari-persistent-homology.make_widget","title":"Persistent Homology Analysis","python_name":"napari_persistent_homology._widget:PersistentHomologyWidget","short_title":null,"category":null,"icon":null,"enablement":null},{"id":"napari-persistent-homology.cristae_binary_mask_3d","title":"Load cristae binary mask 3D","python_name":"napari_persistent_homology._sample_data:load_cristae_binary_mask_3d","short_title":null,"category":null,"icon":null,"enablement":null}],"readers":null,"writers":null,"widgets":[{"command":"napari-persistent-homology.make_widget","display_name":"Persistent Homology","autogenerate":false}],"sample_data":[{"command":"napari-persistent-homology.cristae_binary_mask_3d","key":"cristae_binary_mask_3d","display_name":"Cristae binary mask 3D"}],"themes":null,"menus":{"napari/layers/measure":[{"command":"napari-persistent-homology.make_widget","when":null,"group":null,"alt":null}]},"submenus":null,"keybindings":null,"configuration":[]},"package_metadata":{"metadata_version":"2.4","name":"napari-persistent-homology","version":"0.1.5","dynamic":["license-file"],"platform":null,"supported_platform":null,"summary":"3D shape analysis of binary segmentations using persistent homology","description":"# napari-persistent-homology\n\n[![License BSD-3](https://img.shields.io/pypi/l/napari-persistent-homology.svg?color=green&v=2)](https://github.com/mertesdorfj/napari-persistent-homology/raw/main/LICENSE)\n[![PyPI](https://img.shields.io/pypi/v/napari-persistent-homology.svg?color=green&v=2)](https://pypi.org/project/napari-persistent-homology)\n[![Python Version](https://img.shields.io/pypi/pyversions/napari-persistent-homology.svg?color=green&v=2)](https://python.org)\n[![tests](https://github.com/mertesdorfj/napari-persistent-homology/actions/workflows/test_and_deploy.yml/badge.svg)](https://github.com/mertesdorfj/napari-persistent-homology/actions/workflows/test_and_deploy.yml)\n[![codecov](https://codecov.io/gh/mertesdorfj/napari-persistent-homology/branch/main/graph/badge.svg?v=2)](https://codecov.io/gh/mertesdorfj/napari-persistent-homology)\n[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-persistent-homology)](https://napari-hub.org/plugins/napari-persistent-homology)\n[![npe2](https://img.shields.io/badge/plugin-npe2-blue?link=https://napari.org/stable/plugins/index.html)](https://napari.org/stable/plugins/index.html)\n\n**3D shape analysis of binary segmentations using persistent homology.**\n\n----------------------------------\n\n## Overview\n\n`napari-persistent-homology` is an interactive [napari] dock widget for measuring the **size and spacing of structures in 3D binary segmentations** — directly, reproducibly, and without manual point-picking.\n\nGiven a segmented 3D volume (a `Labels` mask), the plugin quantifies properties such as the thickness of sheet-like structures, the radius of tubular ones, the distance between separate objects, and the regularity of their spacing inside a parent compartment. Each of these questions is reduced to a single characteristic length. The measurement is based on **persistent homology**: The structure is gradually eroded or dilated and the number of objects / enclosed holes is counted at each step. The shape of the resulting count curve encodes a characteristic length scale (a radius, thickness, or spacing) together with a spread (FWHM) that reflects how uniform or variable that length is. Because morphology grows or shrinks the structure uniformly in every direction, the result is **directionally unbiased** and independent of where measurement endpoints would have been placed by hand, making it well suited to large, automated 3D analyses.\n\nThe method is not specific to any particular kind of data: Any 3D binary mask is supported, whether the structures are sheet-like, tubular, or blob-like (e.g. membranes, fibres, vesicles, pores, cells). It was introduced for characterising **mitochondrial cristae** in FIB-SEM electron-microscopy data by [Wang et al., 2024](https://doi.org/10.1038/s42003-024-06045-4) (*Communications Biology* **7**:377); this plugin is based on their work and provides a point-and-click napari interface to their analysis code, removing the need for scripting.\n\nThe plugin works on any 3D binary `Labels` layer and supports three analysis modes:\n\n| Mode | What it measures | Example use cases |\n|---|---|---|\n| **Object radius / half-thickness (erosion)** | The typical radius of solid objects, or half-thickness of sheet/slab structures | Cristae half-thickness, fibre radius, membrane half-width. The plugin also reports the full width / diameter. |\n| **Object spacing (dilation)** | The typical distance between separate objects | Inter-mitochondria spacing, fibre-to-fibre distance |\n| **Internal spacing (dilation in container)** | The typical spacing between objects inside a parent compartment | Cristae spacing inside a mitochondrion |\n\nIn every mode, the plugin builds a **count curve**: At each erosion or dilation step, it records how many separate objects (or enclosed holes) currently exist. This curve rises to a peak and then falls, and two numbers summarise it:\n\n- **Peak location**: *Where* the curve peaks, which corresponds to the characteristic length that is measured. As shown in the paper, this peak sits at *half* the relevant distance, so the plugin reports both the raw peak (a radius / half-thickness, or a half-spacing) and twice it (the full width / diameter, or the full inter-object distance).\n- **FWHM (full-width at half-maximum)**: *How wide* the peak is, which indicates how much that length varies across the structure: A sharp peak means a uniform size, a broad peak means a mix of sizes.\n\nResults appear in an embedded plot and can be exported to CSV together with all the parameters used. Under the hood, the analysis runs on a background thread, so the napari UI stays responsive even on large volumes.\n\n## How it works\n\nThe method combines mathematical morphology with persistent homology, following Wang et al. (2024):\n\n![Persistent homology standardises distance measurements](https://raw.githubusercontent.com/mertesdorfj/napari-persistent-homology/main/docs/images/Wang_et_al_figure3.jpg)\n\n*The idea behind the method, illustrated on mitochondrial cristae (figure from [Wang et al., 2024](https://doi.org/10.1038/s42003-024-06045-4), CC BY 4.0).* **(a–c)** A distance such as a crista width (purple) or the gap between cristae (yellow) can be drawn in many equally plausible ways — the choice of start- and end-point is subjective, so manual measurements are hard to reproduce. **(d–e)** Persistent homology removes that ambiguity. The structure is grown step by step (red arrows); as two opposing surfaces approach, the gap between them closes into an enclosed **hole** (yellow hatching) that later vanishes once the surfaces merge. The step at which the most holes are open (here, dilation round 2) is the moment most surfaces just touch — and since each surface has grown by the same amount, that step equals **half** the gap between them, independent of orientation. **(d vs. e)** A smoother surface (d) opens and closes its holes over a narrow range of steps, whereas a more curved or rough surface (e) keeps them open for longer; this shows up as a **wider count-curve peak (larger FWHM)**, which is why the FWHM acts as a measure of surface curvature / roughness.\n\nThe method by Chenhao Wang et al. turns this idea into three steps:\n\n1. **Subpixel morphology**: The binary mask is repeatedly dilated or eroded by a *fraction* of a voxel per step. Rather than the one-voxel-at-a-time classical operators, the volume is evolved under the level-set PDE `dU/dt = ±|∇U|` using a first-order Osher–Sethian upwind scheme (paper eqs. 3–6). With the default step `λ = 0.1`, ten steps equal one full voxel layer added (dilation) or removed (erosion).\n\n2. **Counting**: After each step, the (soft) mask is binarised at 0.5 and passed to a 26-connected 3D connected-components labeller:\n   - **Erosion → object count**: As the object shrinks, thin regions pinch off and the foreground briefly splits into more components before vanishing.\n   - **Dilation → hole count**: As objects grow, the gaps between them close into enclosed holes that later disappear when surfaces merge. (The outer background is always one extra component, so it is subtracted off). In *internal-spacing* mode, holes are counted only inside the container compartment: The dilated mask is restricted to the container first, so gaps outside it are ignored. In Wang et al. (2024), this is used to confine the analysis to the interior of each mitochondrion.\n\n3. **Feature extraction**: The resulting count curve is Gaussian-smoothed and summarised by two numbers (the first few steps are skipped according to a user-defined offset value, since the curve is noisiest there):\n   - **Peak location**: The step at which the count is highest. This is the step where the most surfaces are simultaneously touching, which happens when each surface has moved inward (erosion) or outward (dilation) by half the distance separating it from its neighbour. The peak therefore measures *half* of that distance - a radius / half-thickness (erosion) or a half-gap (dilation) - and the plugin reports both this value and twice it (the full thickness or full spacing).\n   - **FWHM**: The width of the count-curve peak at half its height, a proxy for surface roughness / curvature: Rougher, more curved surfaces keep holes and objects alive over more rounds, widening the peak.\n\nAll step-unit results are divided by `ceil(1/λ)` to convert back to voxel units, and then, optionally, to nm / µm using the physical voxel size entered in the widget.\n\n## Installation\n\nYou can install `napari-persistent-homology` via [pip]:\n\n```bash\npip install napari-persistent-homology\n```\n\nIf napari is not already installed, you can install the plugin together with napari and a Qt backend via:\n\n```bash\npip install \"napari-persistent-homology[all]\"\n```\n\nTo install the latest development version straight from GitHub:\n\n```bash\npip install git+https://github.com/mertesdorfj/napari-persistent-homology.git\n```\n\nThe plugin requires Python ≥ 3.10.\n\n## Quick start\n\n1. Launch napari (`napari` from your terminal, or from your IDE).\n2. Open the widget from the menu bar under either:\n   - **Plugins → Persistent Homology (Persistent Homology 3D)**, or\n   - **Layers → Measure → Persistent Homology (Persistent Homology 3D)**.\n3. Load a 3D binary segmentation (your own `.tif` / `.npy` file, or the bundled sample under **File → Open Sample → Cristae binary mask 3D (Persistent Homology 3D)**).\n4. Choose an analysis mode and click **Run Analysis**.\n5. Inspect the count curve and the resulting measurements (radius / spacing and the full-width at half-maximum, FWHM) in the **Results** section.\n6. Click **Save Results** to export the summary values to CSV, or **Save Curve & Plot** to export the count-curve data (CSV) plus the embedded plot (PNG).\n\n## How to use the widget\n\n![The widget on startup](https://raw.githubusercontent.com/mertesdorfj/napari-persistent-homology/main/docs/images/plugin_screenshot_start.png)\n\n*The widget docked in napari on startup, before any analysis has been run.*\n\nThe widget is laid out top to bottom:\n\n1. **Input** — pick the `Labels` layer that contains your binary segmentation. In *Internal spacing* mode, a second dropdown appears for selecting the container layer.\n2. **Analysis** — pick one of the three modes (see table above).\n3. **Parameters** — basic parameters are always visible; click **▶ Advanced mode** to reveal three more advanced options.\n4. **Physical Scale** — enter your voxel size and physical unit if you want results in nm / µm in addition to voxels.\n5. **Run Analysis** — starts the computation on a background thread.\n6. **Plot** — the raw and smoothed count curve, with the detected peak marked (dashed vertical line) and the FWHM shown as a dashed horizontal bar.\n7. **Results** — the raw peak (radius / half-spacing), twice that (full width / inter-object spacing), and the full-width at half-maximum (FWHM) in voxels (and, if voxel size is set, in physical units too).\n8. **Save Results** and **Save Curve & Plot** — two separate export buttons. The first writes a small CSV with just the summary values from the Results section; the second writes the raw + smoothed count curve to CSV *and* saves the count curve plot as a PNG. See **Outputs** below.\n\n### Input parameters\n\n| Parameter | Where | Default | Description |\n|---|---|---|---|\n| **Segmentation layer** | Input | — | The `Labels` layer containing your binary segmentation (all non-zero voxels are treated as foreground). |\n| **Container layer** | Input (internal-spacing mode only) | — | A second `Labels` layer defining the parent compartment for *Internal spacing* mode. |\n| **Mode** | Analysis | Object radius / half-thickness | One of the three analysis modes described above. |\n| **Lambda** | Parameters | 0.1 (minimum) | Subpixel step size in voxel-length units, in the range `0.1`–`1.0`. `0.1` means 10 morphology steps per voxel. Smaller values are more accurate but slower; the minimum is set to `0.1` because smaller steps quickly become impractical without offering further accuracy gains. |\n| **Max steps** | Parameters | 100 | Total number of morphology steps. Limits the maximum measurable distance to `max_steps × Lambda` voxels (e.g. 100 × 0.1 = 10 voxels). |\n| **Connectivity** | Advanced | 26 | 3D connected-component neighbourhood used by the counters (options: `6`, `18`, `26`). `26` is the full 3D neighbourhood (face + edge + corner) and is recommended for most datasets. |\n| **Sigma** | Advanced | 3.0 | Standard deviation of the Gaussian smoothing applied to the count curve before peak-finding. Larger values give smoother curves and alleviate more noise, but can also blur close peaks and lead to more inaccurate / shifted peak results. |\n| **Offset** | Advanced | `int(1 / Lambda)` | Number of initial steps to skip when searching for the peak. The very first steps are dominated by noise and small irregularities in the surface rather than real structure, which can create a misleading spike near step 0; ignoring them keeps the search on the true peak. The default tracks `Lambda` so that one full voxel layer is always skipped (e.g. `10` at the default `Lambda = 0.1`); whenever `Lambda` is changed in the widget, the offset is updated automatically. The user is free to override it manually afterwards — the next `Lambda` change resets it again. |\n| **Voxel size X / Y / Z** | Physical Scale | `—` in `vox` mode, `1.0` in `nm` / `µm` mode | The physical voxel size along each axis, used to convert results from voxels to nm / µm. Defaults to `1.0` the first time you switch the unit to `nm` or `µm` - adjust to your actual voxel size. Stays at `—` and is skipped while the selected unit is `vox`. All three values must be greater than 0 when the unit is `nm` or `µm`; clicking 'Run Analysis' with a 0 in any axis raises an error and blocks the run. |\n| **Unit** | Physical Scale | vox | The physical unit in which results are reported (available options: `vox`, `nm`, `µm`). |\n\n> **Anisotropic voxels.** The analysis pipeline operates on the binary volume isotropically - it has no internal knowledge of physical voxel anisotropy. When you provide the voxel size, the plugin converts the voxel-unit results to physical units using the **arithmetic mean** of the X / Y / Z voxel sizes. For datasets with strong anisotropy, the physical-unit result is therefore an approximation and should be treated with caution; consider resampling to an isotropic grid first if exact physical sizes matter (might be added in a future version of this plugin).\n\n### Outputs\n\n![The widget displaying analysis results](https://raw.githubusercontent.com/mertesdorfj/napari-persistent-homology/main/docs/images/plugin_screenshot_result.png)\n\n*The widget following a completed run in **Object radius / half-thickness (erosion)** mode, showing the count curve with the detected peak and FWHM bar, and the corresponding measurements in the Results section below.*\n\nAfter a successful run, the widget displays:\n\n- **Plot** — the raw count curve (light blue), the Gaussian-smoothed curve (darker blue), a dashed vertical line marking the detected peak, and a dashed orange horizontal bar at half-peak height spanning the FWHM. A legend reports the peak and FWHM values in voxels. The x-axis is the morphology-step index (not voxels).\n- **Radius / half-thickness (erosion)** — *shown only in erosion mode.* The peak location of the object-count curve: The typical radius of solid objects, or the half-thickness of sheet-like structures.\n- **Width / thickness (erosion)** — *shown only in erosion mode.* Exactly twice the radius / half-thickness. For solid objects this is the full diameter; for sheet/slab structures it is the full thickness.\n- **Half-spacing (dilation)** — *shown only in dilation modes.* The peak location of the hole-count curve. Per the paper this peak is *half* the average gap between objects, so it is reported as a half-spacing.\n- **Inter-object spacing (dilation)** — *shown only in dilation modes.* Exactly twice the half-spacing result: The full typical distance between separate objects.\n- **Full-width at half-maximum (FWHM)** — *shown in every mode.* The spread of the count curve around its peak, measured at half of the peak height. Larger values indicate more variability in the size distribution of the analysed structures (a proxy for surface roughness / curvature).\n\nIn every mode, values are shown in the chosen physical unit first with the voxel value in brackets (e.g. `25.00 nm  (5.00 vox)`) when a physical unit is selected and the voxel sizes are set. With unit `vox`, only the voxel value is shown.\n\n> **Note on switching modes.** The Results section is \"frozen\" - it always shows the labels and values from the *last* run. The labels only update when you click 'Run Analysis' again.\n\nThere are **two export buttons** below the Results section:\n\n**Save Results**: Prompts for a CSV path and writes only the summary values shown in the Results compartment:\n\n- A title line — `# napari-persistent-homology — Summary Results`.\n- A comment header — `# Mode: …`, `# Parameters: …`, and `# Voxel size: …` (or a note that it was not set).\n- A `Metric` / `Value_vox` (and `Value_<unit>` when a physical voxel size is set, e.g. `Value_nm` or `Value_um`) table whose row labels match the results in the widget:\n  - In erosion mode: `Radius / half-thickness`, `Width / thickness`, `Full-width at half-maximum`\n  - In dilation modes: `Half-spacing`, `Inter-object spacing`, `Full-width at half-maximum`\n\n**Save Curve & Plot**: Prompts for a single path and writes two siblings derived from it:\n\n- `<base>.csv` — title line `# napari-persistent-homology — Object Count Curve` (erosion mode) or `# napari-persistent-homology — Hole Count Curve` (dilation modes), the same metadata header as the summary file, plus the full per-round count curve. Columns are `Erosion_round` or `Dilation_round`, followed by `Count_raw` and `Count_smoothed`.\n- `<base>.png` — the count curve plot generated with matplotlib as it appears in the widget, rendered at 150 DPI with a tight bounding box.\n\nIf you pick e.g. the name `result.csv`, the PNG is saved alongside as `result.png`. Any `.csv` or `.png` extension you type is stripped first so the two files always share a base name.\n\n\n### Sample data\n\nA small 3D cristae binary mask is bundled with the plugin and is available via **File → Open Sample → Cristae binary mask 3D (Persistent Homology 3D)**. It is a 114 × 163 × 234 `uint8` volume from a FIB-SEM acquisition and is a quick way to confirm that the plugin is working and to explore the '*Object radius / half-thickness (erosion)*' and '*Object spacing (dilation)*' modes.\n\n## Citation\n\nIf you use this plugin in your research, please cite the original paper that the analysis pipeline is based on:\n\n> Wang, C., Østergaard, L., Hasselholt, S., & Sporring, J. (2024). *A semi-automatic method for extracting mitochondrial cristae characteristics from 3D focused ion beam scanning electron microscopy data*. Communications Biology, 7, 377. https://doi.org/10.1038/s42003-024-06045-4\n\n## Contributing\n\nContributions are very welcome. Please:\n\n1. Fork the repository and create a feature branch.\n2. Install the development environment (the `--group` flag needs pip ≥ 25.1, which is the first version to support PEP 735 dependency groups):\n   ```bash\n   pip install -e . --group dev\n   ```\n   On older pip, install the dev tools by hand instead:\n   ```bash\n   pip install -e .\n   pip install \"napari[qt]\" pytest pytest-cov pytest-qt\n   ```\n3. Make sure the test suite still passes and add tests for any new behaviour:\n   ```bash\n   python -m pytest tests/ -v\n   ```\n4. Open a pull request describing your change.\n\nPlease ensure the coverage at least stays the same before you submit a pull request.\n\n## License\n\nDistributed under the terms of the [BSD-3] license, `napari-persistent-homology` is free and open source software.\n\n## Issues\n\nIf you encounter any problems, please [file an issue] along with a detailed description (napari version, plugin version, OS, and the smallest input that reproduces the problem if possible).\n\n## Acknowledgements\n\nThis plugin wraps the research code originally written by **Chenhao Wang** and colleagues. The napari plugin scaffolding was generated from the official [napari plugin template](https://github.com/napari/napari-plugin-template).\n\n[napari]: https://github.com/napari/napari\n[BSD-3]: http://opensource.org/licenses/BSD-3-Clause\n[file an issue]: https://github.com/mertesdorfj/napari-persistent-homology/issues\n[pip]: https://pypi.org/project/pip/\n","description_content_type":"text/markdown","keywords":null,"home_page":null,"download_url":null,"author":null,"author_email":"Julia Mertesdorf <jume@di.ku.dk>","maintainer":null,"maintainer_email":null,"license":null,"classifier":["Development Status :: 2 - Pre-Alpha","Framework :: napari","Intended Audience :: Developers","Operating System :: OS Independent","Programming Language :: Python","Programming Language :: Python :: 3","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","Topic :: Scientific/Engineering :: Image Processing"],"requires_dist":["numpy","magicgui","qtpy","scikit-image","scipy","tqdm","connected-components-3d","matplotlib","napari[all]; extra == \"all\""],"requires_python":">=3.10","requires_external":null,"project_url":["Bug Tracker, https://github.com/mertesdorfj/napari-persistent-homology/issues","Documentation, https://github.com/mertesdorfj/napari-persistent-homology#README.md","Source Code, https://github.com/mertesdorfj/napari-persistent-homology","User Support, https://github.com/mertesdorfj/napari-persistent-homology/issues"],"provides_extra":["all"],"provides_dist":null,"obsoletes_dist":null},"npe1_shim":false}