organella 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. organella-0.1.0/.github/workflows/ci.yml +85 -0
  2. organella-0.1.0/.github/workflows/deploy-pages.yml +93 -0
  3. organella-0.1.0/.github/workflows/release.yml +80 -0
  4. organella-0.1.0/.gitignore +17 -0
  5. organella-0.1.0/LICENSE +21 -0
  6. organella-0.1.0/PKG-INFO +330 -0
  7. organella-0.1.0/README.md +287 -0
  8. organella-0.1.0/docs/_legal.css +17 -0
  9. organella-0.1.0/docs/impressum.html +47 -0
  10. organella-0.1.0/docs/organella-icon.png +0 -0
  11. organella-0.1.0/docs/organella.png +0 -0
  12. organella-0.1.0/docs/privacy.html +79 -0
  13. organella-0.1.0/docs/report-3d.png +0 -0
  14. organella-0.1.0/docs/report-distributions.png +0 -0
  15. organella-0.1.0/docs/report-instances.png +0 -0
  16. organella-0.1.0/docs/report-voxel-distances.png +0 -0
  17. organella-0.1.0/geometry_to_blender.py +343 -0
  18. organella-0.1.0/pyproject.toml +89 -0
  19. organella-0.1.0/src/organella/__init__.py +31 -0
  20. organella-0.1.0/src/organella/analysis/__init__.py +13 -0
  21. organella-0.1.0/src/organella/analysis/cache.py +162 -0
  22. organella-0.1.0/src/organella/analysis/distances.py +136 -0
  23. organella-0.1.0/src/organella/analysis/gaps.py +102 -0
  24. organella-0.1.0/src/organella/analysis/meshes.py +738 -0
  25. organella-0.1.0/src/organella/analysis/parallel.py +176 -0
  26. organella-0.1.0/src/organella/analysis/primitives.py +230 -0
  27. organella-0.1.0/src/organella/analysis/shapes.py +364 -0
  28. organella-0.1.0/src/organella/cli.py +650 -0
  29. organella-0.1.0/src/organella/column_schema.py +129 -0
  30. organella-0.1.0/src/organella/config.py +363 -0
  31. organella-0.1.0/src/organella/measure/__init__.py +35 -0
  32. organella-0.1.0/src/organella/measure/contacts.py +91 -0
  33. organella-0.1.0/src/organella/measure/discovery.py +410 -0
  34. organella-0.1.0/src/organella/measure/geometry.py +171 -0
  35. organella-0.1.0/src/organella/measure/instances.py +482 -0
  36. organella-0.1.0/src/organella/measure/loading.py +406 -0
  37. organella-0.1.0/src/organella/measure/morphology.py +176 -0
  38. organella-0.1.0/src/organella/measure/readers.py +341 -0
  39. organella-0.1.0/src/organella/model/__init__.py +31 -0
  40. organella-0.1.0/src/organella/model/measurement.py +26 -0
  41. organella-0.1.0/src/organella/model/object.py +182 -0
  42. organella-0.1.0/src/organella/model/report.py +26 -0
  43. organella-0.1.0/src/organella/model/rows.py +38 -0
  44. organella-0.1.0/src/organella/pipeline/__init__.py +43 -0
  45. organella-0.1.0/src/organella/pipeline/batch.py +155 -0
  46. organella-0.1.0/src/organella/pipeline/objects.py +203 -0
  47. organella-0.1.0/src/organella/pipeline/parts.py +112 -0
  48. organella-0.1.0/src/organella/pipeline/pool.py +167 -0
  49. organella-0.1.0/src/organella/pipeline/table.py +128 -0
  50. organella-0.1.0/src/organella/report/organella_report.html +5731 -0
  51. organella-0.1.0/src/organella/report_io.py +213 -0
  52. organella-0.1.0/src/organella/report_page.py +222 -0
  53. organella-0.1.0/tests/conftest.py +164 -0
  54. organella-0.1.0/tests/report_page_check.mjs +574 -0
  55. organella-0.1.0/tests/synthetic.py +165 -0
  56. organella-0.1.0/tests/test_2d_objects.py +396 -0
  57. organella-0.1.0/tests/test_blender_export.py +195 -0
  58. organella-0.1.0/tests/test_cli.py +404 -0
  59. organella-0.1.0/tests/test_contacts.py +102 -0
  60. organella-0.1.0/tests/test_contacts_view.py +373 -0
  61. organella-0.1.0/tests/test_end_to_end_against_itk.py +94 -0
  62. organella-0.1.0/tests/test_foreign_layouts.py +339 -0
  63. organella-0.1.0/tests/test_geometry_views.py +451 -0
  64. organella-0.1.0/tests/test_group_baseline.py +178 -0
  65. organella-0.1.0/tests/test_instances.py +221 -0
  66. organella-0.1.0/tests/test_mesh.py +380 -0
  67. organella-0.1.0/tests/test_metric_help.py +184 -0
  68. organella-0.1.0/tests/test_morphology.py +98 -0
  69. organella-0.1.0/tests/test_object_loader.py +238 -0
  70. organella-0.1.0/tests/test_object_noun.py +157 -0
  71. organella-0.1.0/tests/test_object_stack.py +68 -0
  72. organella-0.1.0/tests/test_overview.py +251 -0
  73. organella-0.1.0/tests/test_pipeline.py +420 -0
  74. organella-0.1.0/tests/test_reference_agreement.py +163 -0
  75. organella-0.1.0/tests/test_report_page.py +686 -0
  76. organella-0.1.0/tests/test_self_driven_pipeline.py +101 -0
  77. organella-0.1.0/tests/test_significance.py +100 -0
  78. organella-0.1.0/tests/test_skeleton_metrics.py +128 -0
  79. organella-0.1.0/tests/test_skeletons.py +241 -0
  80. organella-0.1.0/tests/test_surface_kinds.py +600 -0
@@ -0,0 +1,85 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+ workflow_dispatch:
7
+
8
+ # A newer push cancels the run it superseded: the suite is the slow part of a review.
9
+ concurrency:
10
+ group: ci-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ # Both versions report, rather than the second being cancelled by the first's failure.
18
+ fail-fast: false
19
+ matrix:
20
+ # 3.11 is what pyproject asks for at the lowest; 3.12 is what it is developed on.
21
+ python: ['3.11', '3.12']
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - uses: astral-sh/setup-uv@v5
26
+ with:
27
+ enable-cache: true
28
+
29
+ # The report page is JavaScript, and parts of the suite run it through node: the way a
30
+ # report is read into the page, the geometry decoder, the merge arithmetic, the contact
31
+ # clustering, every query the geometry sections build, and every section drawn against a
32
+ # stub DOM. Without node those tests fail rather than skip.
33
+ - uses: actions/setup-node@v4
34
+ with:
35
+ node-version: '20'
36
+
37
+ # With the remote extra, so the manifest reader is installed rather than absent: a
38
+ # folder holding one source.json is an object too, and nothing else covers that path.
39
+ - name: Install
40
+ run: |
41
+ uv venv --python ${{ matrix.python }} .venv
42
+ VIRTUAL_ENV=$PWD/.venv uv pip install -e '.[remote,test]'
43
+
44
+ - name: Test
45
+ run: .venv/bin/python -m pytest -q
46
+
47
+ lint:
48
+ runs-on: ubuntu-latest
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+
52
+ - uses: astral-sh/setup-uv@v5
53
+ with:
54
+ enable-cache: true
55
+
56
+ # Only the rules that catch a mistake rather than a preference: an undefined name, an
57
+ # unused import, a syntax error. The prose in this package is deliberate and the
58
+ # docstring and complexity rules would argue with it on every file.
59
+ - name: Ruff
60
+ run: uvx ruff check --select F,E9 src tests
61
+
62
+ package:
63
+ runs-on: ubuntu-latest
64
+ steps:
65
+ - uses: actions/checkout@v4
66
+
67
+ - uses: astral-sh/setup-uv@v5
68
+ with:
69
+ enable-cache: true
70
+
71
+ - name: Build
72
+ run: uv build
73
+
74
+ # The report page is a data file inside the package, and the suite reads it out of the
75
+ # source tree, so a wheel built without it would pass every test and still install a
76
+ # CLI whose `view` and `page` have nothing to open. Checked from a fresh environment,
77
+ # the way someone installing it gets it.
78
+ - name: The wheel carries the page, and the CLI can find it
79
+ run: |
80
+ unzip -l dist/*.whl | grep -q 'organella/report/organella_report.html'
81
+ uv venv --python 3.12 fresh
82
+ VIRTUAL_ENV=$PWD/fresh uv pip install dist/*.whl
83
+ page=$(fresh/bin/organella page)
84
+ test -f "$page"
85
+ echo "the installed CLI finds its page at $page"
@@ -0,0 +1,93 @@
1
+ name: Deploy the report page to GitHub Pages
2
+
3
+ # The report is read by one standalone HTML page: it loads a report.parquet in the browser
4
+ # and draws every chart from it, so publishing it is a copy, not a build. It is served at
5
+ # / and at /organella_report.html. The standalone prototype it grew out of keeps the URLs it
6
+ # was published under (/stats_viewer.html, /mesh_viewer.html) so existing links work.
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ paths:
12
+ - 'src/organella/report/**'
13
+ - 'docs/**'
14
+ - '.github/workflows/deploy-pages.yml'
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: read
19
+ pages: write
20
+ id-token: write
21
+
22
+ concurrency:
23
+ group: pages
24
+ cancel-in-progress: true
25
+
26
+ env:
27
+ # The last commit that changed the standalone prototype, before the package replaced it.
28
+ # Those files are frozen, so the site takes them straight out of history rather than
29
+ # keeping a copy in the tree.
30
+ PROTOTYPE_REF: 42c08030eca7b771844345c517d1e2cc904f4660
31
+
32
+ jobs:
33
+ build:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ with:
38
+ fetch-depth: 0 # history back to PROTOTYPE_REF
39
+
40
+ # Node, because the page is checked before it is published: it is one file with no
41
+ # build step and nothing imports it, so this is the only thing standing between a
42
+ # typo in it and whoever opens the published copy.
43
+ - uses: actions/setup-node@v4
44
+ with:
45
+ node-version: '20'
46
+
47
+ - uses: astral-sh/setup-uv@v5
48
+
49
+ - name: Install
50
+ run: |
51
+ uv venv --python 3.12 .venv
52
+ VIRTUAL_ENV=$PWD/.venv uv pip install -e '.[test]'
53
+
54
+ - name: Check the page reads a report and draws every section
55
+ run: .venv/bin/python -m pytest -q tests/test_report_page.py
56
+
57
+ - name: Publish the page
58
+ run: |
59
+ mkdir -p _site
60
+ page=$(.venv/bin/organella page)
61
+ cp "$page" _site/organella_report.html
62
+ cp "$page" _site/index.html
63
+
64
+ - name: Publish the imprint and the privacy policy
65
+ run: cp docs/impressum.html docs/privacy.html docs/_legal.css _site/
66
+
67
+ # Skipped rather than fatal when the commit is not there: a repository the history
68
+ # was not carried into still deploys, just without the prototype's URLs.
69
+ - name: Add the standalone prototype at its original URLs
70
+ run: |
71
+ if git cat-file -e "$PROTOTYPE_REF^{commit}" 2>/dev/null; then
72
+ git show "$PROTOTYPE_REF:stats_viewer.html" > _site/stats_viewer.html
73
+ git show "$PROTOTYPE_REF:mesh_viewer.html" > _site/mesh_viewer.html
74
+ else
75
+ echo "::notice::$PROTOTYPE_REF is not in this repository; publishing without the prototype URLs"
76
+ fi
77
+
78
+ - name: Show what will be published
79
+ run: ls -lh _site
80
+
81
+ - uses: actions/upload-pages-artifact@v3
82
+ with:
83
+ path: _site
84
+
85
+ deploy:
86
+ needs: build
87
+ runs-on: ubuntu-latest
88
+ environment:
89
+ name: github-pages
90
+ url: ${{ steps.deployment.outputs.page_url }}
91
+ steps:
92
+ - id: deployment
93
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,80 @@
1
+ name: Release
2
+
3
+ # A tag is the trigger, so publishing is deliberate: `git tag v0.1.0 && git push --tags`.
4
+ on:
5
+ push:
6
+ tags: ['v*']
7
+ workflow_dispatch:
8
+
9
+ # Nothing is granted by default; the publish job asks for the one token it needs.
10
+ permissions: {}
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - uses: astral-sh/setup-uv@v5
19
+ with:
20
+ enable-cache: true
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20'
25
+
26
+ # The tag names the version that will be on PyPI forever, and pyproject names the
27
+ # version that is actually built. A mismatch means tagging v0.2.0 and shipping 0.1.0,
28
+ # or failing at the upload because 0.1.0 is already there.
29
+ - name: The tag and the version agree
30
+ if: github.ref_type == 'tag'
31
+ run: |
32
+ built=$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)
33
+ tagged="${GITHUB_REF_NAME#v}"
34
+ test "$built" = "$tagged" || {
35
+ echo "::error::tag $GITHUB_REF_NAME says $tagged, pyproject says $built"
36
+ exit 1
37
+ }
38
+ echo "releasing $built"
39
+
40
+ # A release should not be the first place a failure turns up, so the suite runs here
41
+ # too rather than trusting that CI ran on this commit.
42
+ - name: Test
43
+ run: |
44
+ uv venv --python 3.12 .venv
45
+ VIRTUAL_ENV=$PWD/.venv uv pip install -e '.[remote,test]'
46
+ .venv/bin/python -m pytest -q
47
+
48
+ - name: Build
49
+ run: uv build
50
+
51
+ # The report page is a data file inside the package: a wheel without it installs a CLI
52
+ # with nothing to open, and no test would notice.
53
+ - name: The wheel carries the page
54
+ run: unzip -l dist/*.whl | grep -q 'organella/report/organella_report.html'
55
+
56
+ - uses: actions/upload-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+
61
+ publish:
62
+ needs: build
63
+ runs-on: ubuntu-latest
64
+ # Named so the publisher on PyPI can be pinned to it, and so a release can be gated on
65
+ # a review in the repository's settings.
66
+ environment: pypi
67
+ permissions:
68
+ id-token: write # the only thing trusted publishing needs: no token is stored
69
+ steps:
70
+ - uses: actions/download-artifact@v4
71
+ with:
72
+ name: dist
73
+ path: dist/
74
+
75
+ - uses: astral-sh/setup-uv@v5
76
+
77
+ # `always`, so a misconfigured publisher fails loudly instead of quietly asking for a
78
+ # password. --check-url lets a re-run skip what is already uploaded rather than error.
79
+ - name: Publish
80
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/organella/
@@ -0,0 +1,17 @@
1
+ # Editors
2
+ .idea/
3
+ .vscode/
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ .venv/
9
+ *.egg-info/
10
+ .pytest_cache/
11
+
12
+ # What a run writes: the report, and the geometry that goes beside it
13
+ report.parquet
14
+ *_meshes/
15
+
16
+ # What `uv build` leaves behind
17
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ida-mdc / Organella contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.5
2
+ Name: organella
3
+ Version: 0.1.0
4
+ Summary: Spatial analysis of segmented objects: measure a batch of them, and read the result in one standalone page
5
+ Project-URL: Homepage, https://ida-mdc.github.io/organella/
6
+ Project-URL: Repository, https://github.com/ida-mdc/organella
7
+ Project-URL: Issues, https://github.com/ida-mdc/organella/issues
8
+ Author: Helmholtz Imaging Engineering & Support Unit MDC
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: FIB-SEM,electron microscopy,image analysis,microscopy,morphometry,organelles,parquet,segmentation
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
18
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
19
+ Classifier: Topic :: Scientific/Engineering :: Visualization
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: click>=8.1
22
+ Requires-Dist: crackle-codec
23
+ Requires-Dist: edt>=2.4.0
24
+ Requires-Dist: fast-simplification>=0.1.6
25
+ Requires-Dist: imagecodecs
26
+ Requires-Dist: kimimaro>=4.0.0
27
+ Requires-Dist: numpy>=2.0.0
28
+ Requires-Dist: polars>=1.0.0
29
+ Requires-Dist: posix-ipc
30
+ Requires-Dist: psutil
31
+ Requires-Dist: pyarrow
32
+ Requires-Dist: scikit-image>=0.23.0
33
+ Requires-Dist: scipy>=1.13.0
34
+ Requires-Dist: simpleitk>=2.3
35
+ Requires-Dist: tifffile>=2024.5.0
36
+ Provides-Extra: remote
37
+ Requires-Dist: s3fs>=2023.6.0; extra == 'remote'
38
+ Requires-Dist: zarr<3,>=2.16; extra == 'remote'
39
+ Provides-Extra: test
40
+ Requires-Dist: duckdb>=1.0; extra == 'test'
41
+ Requires-Dist: pytest>=8.3.5; extra == 'test'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # Organella: measuring label relationships in 3D
45
+
46
+ <img src="https://raw.githubusercontent.com/ida-mdc/organella/main/docs/organella.png" alt="A figure drawn entirely out of organelles, holding a measuring tape" align="right" width="190">
47
+
48
+ Organella measures segmented objects - 2D or 3D, one or a batch of them - and produces a
49
+ single report you read in one standalone page: distributions, distances, contacts, and the
50
+ objects themselves in 3D.
51
+
52
+ ### [Open the viewer](https://ida-mdc.github.io/organella/)
53
+
54
+ Drop a `report.parquet` on it and every chart is drawn in your browser. Nothing is uploaded
55
+ and no server runs, so a report can be mailed to a collaborator with a link to that page.
56
+
57
+ Or read one that is already up: **[seven mouse β cells, from Müller et al.](https://ida-mdc.github.io/organella/?data=https%3A%2F%2Fdcache-doma-door01.desy.de%2FHelmholtz%2FHIP%2Fcollaborations%2FOrganella%2Freports%2Fmueller-betacells.parquet)** - FIB-SEM,
58
+ eight structures, with its geometry beside it so the 3D sections draw.
59
+
60
+ An **object** is one segmented thing measured as a whole, given as a folder: a source image,
61
+ one mask that bounds the object, and the label/mask volumes inside it. For example, object can be a cell
62
+ bounded by its plasma membrane (`--object-mask pm`). Specifying the object bound is optional.
63
+ Everything inside is clipped to it by default, because a field of view often holds
64
+ neighbouring cells. `--no-clip` measures them anyway.
65
+
66
+ <br clear="right">
67
+
68
+ ## Screenshots from the reports
69
+
70
+ | | |
71
+ | --- | --- |
72
+ | [![The 3D view of a cell drawn from its geometry](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-3d.png)](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-3d.png)<br>**The object in 3D,** from the geometry a `--with-mesh` run wrote. Structures switch on and off, colour by a metric, and explode pushes every instance out along its own direction from the centre. | [![Composition per object and boxes per group](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-distributions.png)](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-distributions.png)<br>**Composition, and groups compared.** How much of each structure there is in each object, and a box per group with Mann-Whitney brackets between whatever the charts are faceted by. |
73
+ | [![The instance gallery, microtubules sorted by skeleton length](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-instances.png)](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-instances.png)<br>**The instances behind a distribution:** the highest, the lowest, or a fair sample of any metric. Click one to look at it properly. | [![Voxel-distance histograms, one panel per target structure](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-voxel-distances.png)](https://raw.githubusercontent.com/ida-mdc/organella/main/docs/report-voxel-distances.png)<br>**Every voxel, by distance.** A structure's voxels binned by how far each one is from another structure, one panel per target. |
74
+
75
+ ## Workflow
76
+
77
+ 1. Organise your images in one of the [input layouts](#your-input).
78
+ 2. `organella dry-run` to check what will be analysed.
79
+ 3. `organella process` to write `report.parquet` and, with `--with-mesh`, the geometry.
80
+ 4. `organella view` to open the report page on it.
81
+ 5. Optionally import an object's geometry into [Blender](#blender).
82
+
83
+ ## Try it
84
+
85
+ First, [install uv](https://docs.astral.sh/uv/getting-started/installation/) (or install the package via pip without uv, but uv is cool).
86
+
87
+ Next, install Organella:
88
+
89
+ ```bash
90
+ uv pip install "organella @ git+https://github.com/ida-mdc/organella.git@main"
91
+ ```
92
+
93
+ Then test, measure, and view your label dataset:
94
+
95
+ ```bash
96
+ organella dry-run experiment/
97
+ organella process experiment/ -o report.parquet --with-mesh
98
+ organella view report.parquet
99
+ ```
100
+
101
+ The handful of arguments worth knowing from the start:
102
+
103
+ | | |
104
+ | --- | --- |
105
+ | `--object-mask pm` | the mask that bounds each object. It decides the origin of every distance and polarity, and everything inside it is clipped to it, so it is never guessed - `dry-run` lists the masks each folder has |
106
+ | `-p control -p treated` | subdirectories to import as groups. That grouping becomes the default comparison in every chart |
107
+ | `--voxel-size-um 0.1,0.02,0.02` | needed when the images carry no calibration, since a size is never invented. `y,x` for a plane |
108
+ | `--skeletons mito,ER` | branches, length and tortuosity for the structures worth it. Opt-in: it is the most expensive thing in a run |
109
+ | `--entities mito,ER` | measure only these. Each entity is another full-size channel, so a subject carrying 117 structures needs this to fit in memory |
110
+ | `--with-mesh` | also write the geometry the 3D sections and Blender read, to `<output>_meshes/` |
111
+
112
+ Every argument is listed under [Parameters of `process`](#parameters-of-process).
113
+
114
+ **Check the input first.** `dry-run` reads image headers only - no analysis, no output -
115
+ and prints per object the source image, the label and mask entities
116
+ found (`*` marks the object mask), and anything that looks wrong. Then which entities are
117
+ missing in which objects, and a suggested `--max-workers`. Exit code is `1` if any object
118
+ cannot be analysed.
119
+
120
+ ```text
121
+ control/cell_b
122
+ source sample_b.tif [12.4 MB stacked]
123
+ labels mito
124
+ masks nucleus, pm*
125
+ warn ignored readme_overlay.tif: not <prefix>_<name>_label|labels|mask
126
+
127
+ ===== 3 object folder(s) ===== (* = object mask)
128
+ label:mito 2/3 ← missing in some objects
129
+ mask:pm 3/3
130
+ ```
131
+
132
+ ## Reading the report
133
+
134
+ `organella view report.parquet` serves the report, its geometry and the page from one
135
+ localhost origin and opens it. Or open **<https://ida-mdc.github.io/organella/>** and drop
136
+ `report.parquet` on it: the page parses the parquet in the browser, so nothing is uploaded and
137
+ no server runs.
138
+
139
+ **The 3D sections need the geometry too**. Press **Add geometry** and pick either the `report_meshes` folder or the
140
+ `geometry.parquet` files themselves; The `organelle view` command attaches them for you and you don't need to do anything else.
141
+
142
+ In the page, **Charts** switches every panel between boxes and histograms, and
143
+ **Significance** puts Mann-Whitney brackets between whatever the charts are faceted by. Every
144
+ column carries its own description in the report, so the page explains each metric under the
145
+ chart of it.
146
+
147
+ ## Your input
148
+
149
+ Entity files must match `<prefix>_<name>_label.tif` (or `_labels.tif`) and
150
+ `<prefix>_<name>_mask.tif`, where `<prefix>` is the source image basename. The prefix is taken
151
+ off the front, so the entity name is whatever is left and may have underscores in it:
152
+ `s0011_rib_left_11_mask.tif` is the entity `rib_left_11`.
153
+
154
+ ```text
155
+ my_cell/
156
+ sample.tif
157
+ sample_mito_label.tif
158
+ sample_nucleus_mask.tif
159
+ sample_membrane_mask.tif
160
+ ```
161
+
162
+ An object folder can sit on its own, in a flat batch (`cells/cell_a/`, `cells/cell_b/`), or in
163
+ a grouped batch (`experiment/control/cell_a/`, `experiment/treated/cell_b/`), where the group
164
+ folder is what `-p` imports.
165
+
166
+ **Other formats.** NIfTI (`.nii`, `.nii.gz`), NRRD and MetaImage are read through SimpleITK.
167
+ Their headers carry a reliable voxel size, which TIFF often does not; spacing is read as
168
+ millimetres, the convention every reader of these files uses.
169
+
170
+ **Entities in a subfolder** named by nothing but the structure - a `segmentations/`, `masks/`
171
+ or `labels/` folder beside the source image. There is no prefix to strip, and label-or-mask
172
+ is read off the content rather than guessed from the name.
173
+
174
+ **A remote store, read as a crop.** A folder holding one `source.json` and no images names a
175
+ chunked store (N5 or Zarr, local or on S3), the arrays in it, and the window to read. A 512³
176
+ crop of OpenOrganelle's 122-gigavoxel HeLa cell is 0.64% of it, in under five seconds, at
177
+ full 4 nm resolution. Needs the `remote` extra: `pip install 'organella[remote]'`.
178
+
179
+ ```json
180
+ {
181
+ "store": "s3://janelia-cosem-datasets/jrc_hela-2/jrc_hela-2.n5",
182
+ "scale": "s0",
183
+ "crop": "3712:4224,256:768,5760:6272",
184
+ "source": "em/fibsem-uint16",
185
+ "entities": { "mito": "labels/mito_seg", "er": "labels/er_seg" }
186
+ }
187
+ ```
188
+
189
+ ## Output
190
+
191
+ ```text
192
+ report.parquet # rows per object, structure, instance and contact
193
+ report_meshes/<object>/geometry.parquet # only with --with-mesh
194
+ ```
195
+
196
+ Everything measured lands in `report.parquet`, as rows at four depths told apart by `row_type`:
197
+
198
+ | `row_type` | `obs_level` | one row per |
199
+ | --- | --- | --- |
200
+ | `object` | 0 | object folder: extent, provenance, and the whole-object totals |
201
+ | `entity` | 1 | structure, by name: volume, surface area, sphericity, instance counts |
202
+ | `instance` | 2 | labelled instance: its own size, shape, polarity and nearest neighbour |
203
+ | `distance` | 2 | instance × target structure: how far that instance is from it |
204
+ | `contact` | 2 | touching pair of instances of one structure, with their gap |
205
+
206
+ Geometry goes beside the report rather than in it, because meshes would multiply the size of a
207
+ table every query loads. One `geometry.parquet` per object holds a surface per instance (an
208
+ outline, for a 2D object), the skeletons, and the touching pairs, so it stands on its own for
209
+ Blender and for a hosted copy. The object row records where, in `mesh_geometry_file`.
210
+
211
+ ## Commands
212
+
213
+ | | |
214
+ | --- | --- |
215
+ | `organella dry-run DIR` | what would be analysed, from headers only |
216
+ | `organella process DIR -o REPORT` | measure a batch, write the report |
217
+ | `organella mesh DIR -o OUTDIR` | geometry only, for a report you already have |
218
+ | `organella view REPORT` | serve the report, its geometry and the page, and open it |
219
+ | `organella page` | print the path of the standalone page |
220
+ | `organella colours REPORT PALETTE` | recolour a report in about a second |
221
+ | `organella describe REPORT TEXT` | say what the data is: credits, a citation, a licence. `-` reads it from standard input |
222
+
223
+ `dry-run` takes `--object-mask NAME`, to check every folder has it rather than only listing
224
+ what they have. `view` takes `--port` (default 8052) and `--no-browser`. `mesh` takes
225
+ `-o, --out-dir` for where to write `<object>/geometry.parquet`, plus the input, geometry and
226
+ run parameters below and `--no-contacts`.
227
+
228
+ ## Parameters of `process`
229
+
230
+ **Input**
231
+
232
+ | | |
233
+ | --- | --- |
234
+ | `-o, --output FILE` | where to write the report. Required |
235
+ | `-p, --paths TEXT` | subdirectory to import as its own group, repeatable. Becomes the default grouping in every chart |
236
+ | `--object-mask NAME` | the mask that bounds each object. Never guessed: it decides the origin of every distance and polarity. Left out, entities are measured where they lie |
237
+ | `--description TEXT` | what this data is and who it credits. It travels in the report, and the page shows it above the first section, so a report you send arrives with its provenance |
238
+ | `--object-noun WORD` | what one measured thing is called in the report, e.g. `cell`, or `nucleus/nuclei` for an irregular plural. Presentation only |
239
+ | `--voxel-size-um Z,Y,X` | voxel size in µm; `y,x` for a plane. Inferred from the source metadata when omitted, and refused rather than invented if there is none |
240
+ | `--entities NAMES` | measure only these, plus the object mask. Each entity is another full-size channel, so a 117-structure subject needs selecting down before it fits in memory |
241
+ | `--label-map FILE` | JSON of `{"1": "liver"}`, splitting one volume whose ids each mean a different structure into an entity per id. Only named ids become entities |
242
+ | `--label-map-entity NAME` | which entity `--label-map` splits. Needed only when a folder has more than one label entity, where leaving it out is an error rather than a guess |
243
+ | `--auto-label-masks` | promote masks with several connected components to label entities |
244
+
245
+ **What gets measured**
246
+
247
+ | | |
248
+ | --- | --- |
249
+ | `--no-clip` | measure outside the object mask too. Clipped to it by default, since that is what naming a bounding mask means |
250
+ | `--no-instances` | entity-level morphology only: no per-instance rows |
251
+ | `--no-contacts` | skip the contact rows |
252
+ | `--contact-max-um T` | largest gap between two instances of one structure that still counts as a contact. Default 0.5 |
253
+ | `--skeletons NAMES` | structures to skeletonise, for branches, length and tortuosity. Opt-in: it is the most expensive thing in a run, and a granule's skeleton is one branch the length of its diameter |
254
+ | `--max-skeleton-voxels N` | skip skeletons for instances above this voxel count. Default 500000 |
255
+ | `--polarity-spread` | also measure each instance's angular spread on the polarity sphere |
256
+ | `--distance-histograms` | also measure per-instance distance distributions, not just the minimum |
257
+ | `--colours FILE` | also `--colors`. JSON of `{"mito": "#d62728"}`. Lands in the report, so every chart draws that structure the same. Unnamed structures keep the built-in palette |
258
+
259
+ **Geometry**, all of it only with `--with-mesh`
260
+
261
+ | | |
262
+ | --- | --- |
263
+ | `--with-mesh` | also write per-object geometry for the 3D views and Blender. Goes to `<output>_meshes/`, never into the parquet |
264
+ | `--geometry-as NAME=KIND,...` | how a structure's surface is stored: `mesh`, `ellipsoid` or `tube`, e.g. `vesicle=ellipsoid`. Decided from the measured shape when unnamed - a round instance becomes a 60-byte ellipsoid, which is what makes tens of thousands drawable. No measurement changes; the surface is only ever drawn |
265
+ | `--mesh-max-vertices N` | most vertices one surface keeps; 0 lifts the cap. Default 200000. A decimation *fraction* bounds nothing: at 0.5 one ER sheet was still 2.87 million vertices and 86 MB |
266
+ | `--mesh-smooth-sigma SIGMA` | Gaussian sigma before marching cubes. Default 0.7; 0 disables |
267
+ | `--mesh-step-size N` | marching-cubes step size; 1 is full resolution. Default 2 |
268
+ | `--mesh-target-reduction F` | decimation fraction. Default 0.8, keeping ~20% of faces |
269
+ | `--mesh-level L` | iso-surface level on the signed distance field. Default 0 |
270
+ | `--mesh-surface-method` | `marching-cubes` or `surface-nets`. The dual method has ~30% less staircase noise, no fewer vertices, and is 66% slower |
271
+ | `--mesh-workers N` | processes meshing one object's instances. Default: the cores the object pool is not using |
272
+ | `--reuse-geometry` | keep any `geometry.parquet` an object already has. Meshing dominates a run, so a batch that died partway finishes in minutes |
273
+
274
+ **The run**
275
+
276
+ | | |
277
+ | --- | --- |
278
+ | `--max-workers N` | worker processes. Default: worked out from memory, since one object can need gigabytes |
279
+ | `--num-threads N` | kimimaro worker count. Default 1, because objects already run in parallel |
280
+ | `--resume` | skip objects an interrupted run already measured. Each object's rows go to `<output>_parts/` as it finishes and are removed once the report is written |
281
+
282
+ ## Sharing a report
283
+
284
+ The geometry stays a folder of one file per object, so it travels with
285
+ the report in one of two shapes:
286
+
287
+ - hand over `report.parquet` and its `report_meshes/` folder, and the reader runs
288
+ `organella view report.parquet`
289
+ - or upload the two together, unchanged, and open the page with `?data=<url of the parquet>`.
290
+ The geometry is looked for beside the parquet as `<name>_meshes/`, so there is nothing else
291
+ to pass. If the files sit on a different origin than the page, that server has to allow
292
+ cross-origin requests - GitHub Pages does, a bare `python -m http.server` does not.
293
+
294
+
295
+ ## Development
296
+
297
+ ```bash
298
+ uv venv --python 3.12 .venv
299
+ source .venv/bin/activate
300
+ uv pip install -e '.[test]' # the suite needs pytest and duckdb
301
+ .venv/bin/python -m pytest
302
+ ```
303
+
304
+ The report page is one file with no build step, edit
305
+ [`src/organella/report/organella_report.html`](src/organella/report/organella_report.html)
306
+ and reload the browser. The test suite runs it through
307
+ node - how a report is read into it, the maths its panels draw, the queries its 3D
308
+ sections build, and every section drawn against a stub DOM:
309
+
310
+ ```bash
311
+ .venv/bin/python -m pytest tests/test_report_page.py
312
+ ```
313
+
314
+ That harness never runs the real load path - DuckDB-WASM, the parquet read, WebGL - so a
315
+ change to how the page opens a report has to be checked in a browser.
316
+
317
+ ## Blender
318
+
319
+ `geometry_to_blender.py` imports an object's `geometry.parquet`. It needs Blender with `pandas`
320
+ and `pyarrow` in Blender's own Python
321
+ (`<blender>/python/bin/python3 -m pip install pandas pyarrow`), and geometry from a **3D**
322
+ object - a 2D object carries outlines, which the report's gallery draws and Blender has no use
323
+ for.
324
+
325
+ ```bash
326
+ blender --background --python geometry_to_blender.py -- geometry.parquet [out.blend] [out.png]
327
+ ```
328
+
329
+ Or open the script in Blender's Script Editor, set `GEOMETRY_PATH` (optionally `OUT_BLEND`,
330
+ `OUT_RENDER`) and run it with `Alt+P`.