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.
- organella-0.1.0/.github/workflows/ci.yml +85 -0
- organella-0.1.0/.github/workflows/deploy-pages.yml +93 -0
- organella-0.1.0/.github/workflows/release.yml +80 -0
- organella-0.1.0/.gitignore +17 -0
- organella-0.1.0/LICENSE +21 -0
- organella-0.1.0/PKG-INFO +330 -0
- organella-0.1.0/README.md +287 -0
- organella-0.1.0/docs/_legal.css +17 -0
- organella-0.1.0/docs/impressum.html +47 -0
- organella-0.1.0/docs/organella-icon.png +0 -0
- organella-0.1.0/docs/organella.png +0 -0
- organella-0.1.0/docs/privacy.html +79 -0
- organella-0.1.0/docs/report-3d.png +0 -0
- organella-0.1.0/docs/report-distributions.png +0 -0
- organella-0.1.0/docs/report-instances.png +0 -0
- organella-0.1.0/docs/report-voxel-distances.png +0 -0
- organella-0.1.0/geometry_to_blender.py +343 -0
- organella-0.1.0/pyproject.toml +89 -0
- organella-0.1.0/src/organella/__init__.py +31 -0
- organella-0.1.0/src/organella/analysis/__init__.py +13 -0
- organella-0.1.0/src/organella/analysis/cache.py +162 -0
- organella-0.1.0/src/organella/analysis/distances.py +136 -0
- organella-0.1.0/src/organella/analysis/gaps.py +102 -0
- organella-0.1.0/src/organella/analysis/meshes.py +738 -0
- organella-0.1.0/src/organella/analysis/parallel.py +176 -0
- organella-0.1.0/src/organella/analysis/primitives.py +230 -0
- organella-0.1.0/src/organella/analysis/shapes.py +364 -0
- organella-0.1.0/src/organella/cli.py +650 -0
- organella-0.1.0/src/organella/column_schema.py +129 -0
- organella-0.1.0/src/organella/config.py +363 -0
- organella-0.1.0/src/organella/measure/__init__.py +35 -0
- organella-0.1.0/src/organella/measure/contacts.py +91 -0
- organella-0.1.0/src/organella/measure/discovery.py +410 -0
- organella-0.1.0/src/organella/measure/geometry.py +171 -0
- organella-0.1.0/src/organella/measure/instances.py +482 -0
- organella-0.1.0/src/organella/measure/loading.py +406 -0
- organella-0.1.0/src/organella/measure/morphology.py +176 -0
- organella-0.1.0/src/organella/measure/readers.py +341 -0
- organella-0.1.0/src/organella/model/__init__.py +31 -0
- organella-0.1.0/src/organella/model/measurement.py +26 -0
- organella-0.1.0/src/organella/model/object.py +182 -0
- organella-0.1.0/src/organella/model/report.py +26 -0
- organella-0.1.0/src/organella/model/rows.py +38 -0
- organella-0.1.0/src/organella/pipeline/__init__.py +43 -0
- organella-0.1.0/src/organella/pipeline/batch.py +155 -0
- organella-0.1.0/src/organella/pipeline/objects.py +203 -0
- organella-0.1.0/src/organella/pipeline/parts.py +112 -0
- organella-0.1.0/src/organella/pipeline/pool.py +167 -0
- organella-0.1.0/src/organella/pipeline/table.py +128 -0
- organella-0.1.0/src/organella/report/organella_report.html +5731 -0
- organella-0.1.0/src/organella/report_io.py +213 -0
- organella-0.1.0/src/organella/report_page.py +222 -0
- organella-0.1.0/tests/conftest.py +164 -0
- organella-0.1.0/tests/report_page_check.mjs +574 -0
- organella-0.1.0/tests/synthetic.py +165 -0
- organella-0.1.0/tests/test_2d_objects.py +396 -0
- organella-0.1.0/tests/test_blender_export.py +195 -0
- organella-0.1.0/tests/test_cli.py +404 -0
- organella-0.1.0/tests/test_contacts.py +102 -0
- organella-0.1.0/tests/test_contacts_view.py +373 -0
- organella-0.1.0/tests/test_end_to_end_against_itk.py +94 -0
- organella-0.1.0/tests/test_foreign_layouts.py +339 -0
- organella-0.1.0/tests/test_geometry_views.py +451 -0
- organella-0.1.0/tests/test_group_baseline.py +178 -0
- organella-0.1.0/tests/test_instances.py +221 -0
- organella-0.1.0/tests/test_mesh.py +380 -0
- organella-0.1.0/tests/test_metric_help.py +184 -0
- organella-0.1.0/tests/test_morphology.py +98 -0
- organella-0.1.0/tests/test_object_loader.py +238 -0
- organella-0.1.0/tests/test_object_noun.py +157 -0
- organella-0.1.0/tests/test_object_stack.py +68 -0
- organella-0.1.0/tests/test_overview.py +251 -0
- organella-0.1.0/tests/test_pipeline.py +420 -0
- organella-0.1.0/tests/test_reference_agreement.py +163 -0
- organella-0.1.0/tests/test_report_page.py +686 -0
- organella-0.1.0/tests/test_self_driven_pipeline.py +101 -0
- organella-0.1.0/tests/test_significance.py +100 -0
- organella-0.1.0/tests/test_skeleton_metrics.py +128 -0
- organella-0.1.0/tests/test_skeletons.py +241 -0
- 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/
|
organella-0.1.0/LICENSE
ADDED
|
@@ -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.
|
organella-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
| [](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. | [](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
|
+
| [](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. | [](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`.
|