ocdkit 0.0.1__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.
- ocdkit-0.0.1/.github/workflows/test_and_deploy.yml +111 -0
- ocdkit-0.0.1/.gitignore +89 -0
- ocdkit-0.0.1/LICENSE +28 -0
- ocdkit-0.0.1/PKG-INFO +66 -0
- ocdkit-0.0.1/README.md +39 -0
- ocdkit-0.0.1/pyproject.toml +51 -0
- ocdkit-0.0.1/scripts/bench_colorize.py +104 -0
- ocdkit-0.0.1/scripts/coverage_cross_device.sh +41 -0
- ocdkit-0.0.1/setup.cfg +4 -0
- ocdkit-0.0.1/src/ocdkit/__init__.py +10 -0
- ocdkit-0.0.1/src/ocdkit/array/__init__.py +3 -0
- ocdkit-0.0.1/src/ocdkit/array/convert.py +121 -0
- ocdkit-0.0.1/src/ocdkit/array/filters.py +56 -0
- ocdkit-0.0.1/src/ocdkit/array/imports.py +4 -0
- ocdkit-0.0.1/src/ocdkit/array/index.py +242 -0
- ocdkit-0.0.1/src/ocdkit/array/morphology.py +194 -0
- ocdkit-0.0.1/src/ocdkit/array/normalize.py +425 -0
- ocdkit-0.0.1/src/ocdkit/array/ops.py +134 -0
- ocdkit-0.0.1/src/ocdkit/array/spatial.py +410 -0
- ocdkit-0.0.1/src/ocdkit/array/transform.py +261 -0
- ocdkit-0.0.1/src/ocdkit/array/union_find.py +52 -0
- ocdkit-0.0.1/src/ocdkit/array/warp.py +28 -0
- ocdkit-0.0.1/src/ocdkit/imports.py +8 -0
- ocdkit-0.0.1/src/ocdkit/io/__init__.py +3 -0
- ocdkit-0.0.1/src/ocdkit/io/files.py +141 -0
- ocdkit-0.0.1/src/ocdkit/io/image.py +138 -0
- ocdkit-0.0.1/src/ocdkit/io/imports.py +4 -0
- ocdkit-0.0.1/src/ocdkit/io/path.py +68 -0
- ocdkit-0.0.1/src/ocdkit/io/result.py +34 -0
- ocdkit-0.0.1/src/ocdkit/load/__init__.py +5 -0
- ocdkit-0.0.1/src/ocdkit/load/module.py +132 -0
- ocdkit-0.0.1/src/ocdkit/load/object.py +136 -0
- ocdkit-0.0.1/src/ocdkit/logging/__init__.py +3 -0
- ocdkit-0.0.1/src/ocdkit/logging/handler.py +206 -0
- ocdkit-0.0.1/src/ocdkit/measure/__init__.py +3 -0
- ocdkit-0.0.1/src/ocdkit/measure/bbox.py +188 -0
- ocdkit-0.0.1/src/ocdkit/measure/diameter.py +185 -0
- ocdkit-0.0.1/src/ocdkit/measure/imports.py +4 -0
- ocdkit-0.0.1/src/ocdkit/measure/medoid.py +181 -0
- ocdkit-0.0.1/src/ocdkit/measure/metrics.py +43 -0
- ocdkit-0.0.1/src/ocdkit/plot/__init__.py +5 -0
- ocdkit-0.0.1/src/ocdkit/plot/color.py +215 -0
- ocdkit-0.0.1/src/ocdkit/plot/contour.py +102 -0
- ocdkit-0.0.1/src/ocdkit/plot/defaults.py +147 -0
- ocdkit-0.0.1/src/ocdkit/plot/display.py +133 -0
- ocdkit-0.0.1/src/ocdkit/plot/export.py +108 -0
- ocdkit-0.0.1/src/ocdkit/plot/figure.py +24 -0
- ocdkit-0.0.1/src/ocdkit/plot/grid.py +306 -0
- ocdkit-0.0.1/src/ocdkit/plot/imports.py +9 -0
- ocdkit-0.0.1/src/ocdkit/plot/label.py +733 -0
- ocdkit-0.0.1/src/ocdkit/plot/ncolor.py +54 -0
- ocdkit-0.0.1/src/ocdkit/utils/__init__.py +3 -0
- ocdkit-0.0.1/src/ocdkit/utils/collections.py +97 -0
- ocdkit-0.0.1/src/ocdkit/utils/gpu.py +210 -0
- ocdkit-0.0.1/src/ocdkit/utils/kwargs.py +136 -0
- ocdkit-0.0.1/src/ocdkit.egg-info/PKG-INFO +66 -0
- ocdkit-0.0.1/src/ocdkit.egg-info/SOURCES.txt +76 -0
- ocdkit-0.0.1/src/ocdkit.egg-info/dependency_links.txt +1 -0
- ocdkit-0.0.1/src/ocdkit.egg-info/requires.txt +17 -0
- ocdkit-0.0.1/src/ocdkit.egg-info/top_level.txt +1 -0
- ocdkit-0.0.1/tests/fixtures/multichan_3c_4x4.czi +0 -0
- ocdkit-0.0.1/tests/fixtures/tiny_8x8.czi +0 -0
- ocdkit-0.0.1/tests/test_array.py +798 -0
- ocdkit-0.0.1/tests/test_gpu.py +243 -0
- ocdkit-0.0.1/tests/test_io.py +299 -0
- ocdkit-0.0.1/tests/test_measure.py +423 -0
- ocdkit-0.0.1/tests/test_morphology.py +184 -0
- ocdkit-0.0.1/tests/test_plot_color.py +194 -0
- ocdkit-0.0.1/tests/test_plot_contour.py +48 -0
- ocdkit-0.0.1/tests/test_plot_display.py +37 -0
- ocdkit-0.0.1/tests/test_plot_export.py +38 -0
- ocdkit-0.0.1/tests/test_plot_figure.py +55 -0
- ocdkit-0.0.1/tests/test_plot_grid.py +67 -0
- ocdkit-0.0.1/tests/test_plot_label.py +74 -0
- ocdkit-0.0.1/tests/test_plot_notebook.py +115 -0
- ocdkit-0.0.1/tests/test_registration.py +154 -0
- ocdkit-0.0.1/tests/test_slice.py +31 -0
- ocdkit-0.0.1/tests/test_spatial.py +259 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
name: CI/CD
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*" # e.g. v1.0, v2.0
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Test on ${{ matrix.os }} (Python ${{ matrix.python_version }})
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest, macos-latest] # macos-latest = Apple Silicon (MPS)
|
|
16
|
+
python_version: ['3.11', '3.12']
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0 # needed for setuptools_scm
|
|
21
|
+
|
|
22
|
+
- name: Set up Python
|
|
23
|
+
uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python_version }}
|
|
26
|
+
cache: pip
|
|
27
|
+
|
|
28
|
+
- name: Install package with dev deps
|
|
29
|
+
shell: bash
|
|
30
|
+
run: |
|
|
31
|
+
python -m pip install --upgrade pip
|
|
32
|
+
pip install --extra-index-url https://download.pytorch.org/whl/cpu \
|
|
33
|
+
-e . pytest pytest-cov
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
shell: bash
|
|
37
|
+
env:
|
|
38
|
+
PYTORCH_ENABLE_MPS_FALLBACK: "1"
|
|
39
|
+
run: |
|
|
40
|
+
python -m pytest tests/ \
|
|
41
|
+
--cov=ocdkit --cov-report=xml --cov-report=term \
|
|
42
|
+
--junitxml=junit.xml \
|
|
43
|
+
-v
|
|
44
|
+
|
|
45
|
+
- name: Upload coverage & junit artifacts
|
|
46
|
+
if: matrix.os == 'macos-latest' && matrix.python_version == '3.11'
|
|
47
|
+
uses: actions/upload-artifact@v4
|
|
48
|
+
with:
|
|
49
|
+
name: test-results
|
|
50
|
+
path: |
|
|
51
|
+
coverage.xml
|
|
52
|
+
junit.xml
|
|
53
|
+
|
|
54
|
+
deploy:
|
|
55
|
+
name: Deploy to PyPI
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
needs: test
|
|
58
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
with:
|
|
62
|
+
fetch-depth: 0
|
|
63
|
+
|
|
64
|
+
- name: Set up Python
|
|
65
|
+
uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: '3.11'
|
|
68
|
+
|
|
69
|
+
- name: Install build tools
|
|
70
|
+
run: |
|
|
71
|
+
python -m pip install --upgrade pip
|
|
72
|
+
pip install build twine setuptools setuptools_scm wheel
|
|
73
|
+
|
|
74
|
+
- name: Build distribution
|
|
75
|
+
run: python -m build --sdist --wheel
|
|
76
|
+
|
|
77
|
+
- name: Publish to PyPI
|
|
78
|
+
env:
|
|
79
|
+
TWINE_USERNAME: __token__
|
|
80
|
+
TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
|
|
81
|
+
run: python -m twine upload dist/*
|
|
82
|
+
|
|
83
|
+
badges:
|
|
84
|
+
name: Build and commit badges
|
|
85
|
+
runs-on: ubuntu-latest
|
|
86
|
+
needs: test
|
|
87
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
|
|
91
|
+
- name: Download test artifacts
|
|
92
|
+
uses: actions/download-artifact@v4
|
|
93
|
+
with:
|
|
94
|
+
name: test-results
|
|
95
|
+
|
|
96
|
+
- name: Install badge dependencies
|
|
97
|
+
run: |
|
|
98
|
+
python -m pip install --upgrade pip
|
|
99
|
+
pip install 'genbadge[coverage,tests]'
|
|
100
|
+
|
|
101
|
+
- name: Generate badges
|
|
102
|
+
run: |
|
|
103
|
+
mkdir -p badges
|
|
104
|
+
genbadge coverage -i coverage.xml -o badges/coverage.svg
|
|
105
|
+
genbadge tests -i junit.xml -o badges/tests.svg
|
|
106
|
+
|
|
107
|
+
- name: Commit badges
|
|
108
|
+
uses: EndBug/add-and-commit@v9
|
|
109
|
+
with:
|
|
110
|
+
add: 'badges/*.svg'
|
|
111
|
+
message: 'CI: update coverage and test badges'
|
ocdkit-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
**/__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# macOS resource fork / metadata junk
|
|
10
|
+
._*
|
|
11
|
+
.DS_Store
|
|
12
|
+
.AppleDouble
|
|
13
|
+
.AppleDB
|
|
14
|
+
|
|
15
|
+
# Distribution / packaging
|
|
16
|
+
.Python
|
|
17
|
+
build/
|
|
18
|
+
develop-eggs/
|
|
19
|
+
dist/
|
|
20
|
+
downloads/
|
|
21
|
+
eggs/
|
|
22
|
+
.eggs/
|
|
23
|
+
lib/
|
|
24
|
+
lib64/
|
|
25
|
+
parts/
|
|
26
|
+
sdist/
|
|
27
|
+
var/
|
|
28
|
+
wheels/
|
|
29
|
+
pip-wheel-metadata/
|
|
30
|
+
share/python-wheels/
|
|
31
|
+
*.egg-info/
|
|
32
|
+
.installed.cfg
|
|
33
|
+
*.egg
|
|
34
|
+
MANIFEST
|
|
35
|
+
*_version.py
|
|
36
|
+
|
|
37
|
+
# PyInstaller
|
|
38
|
+
*.manifest
|
|
39
|
+
*.spec
|
|
40
|
+
|
|
41
|
+
# Installer logs
|
|
42
|
+
pip-log.txt
|
|
43
|
+
pip-delete-this-directory.txt
|
|
44
|
+
|
|
45
|
+
# Unit test / coverage reports
|
|
46
|
+
htmlcov/
|
|
47
|
+
.tox/
|
|
48
|
+
.nox/
|
|
49
|
+
.coverage
|
|
50
|
+
.coverage.*
|
|
51
|
+
.cache
|
|
52
|
+
nosetests.xml
|
|
53
|
+
coverage.xml
|
|
54
|
+
*.cover
|
|
55
|
+
*.py,cover
|
|
56
|
+
.hypothesis/
|
|
57
|
+
.pytest_cache/
|
|
58
|
+
|
|
59
|
+
# Sphinx documentation
|
|
60
|
+
docs/_build/
|
|
61
|
+
|
|
62
|
+
# Jupyter Notebook
|
|
63
|
+
.ipynb_checkpoints
|
|
64
|
+
*/.ipynb_checkpoints/*
|
|
65
|
+
|
|
66
|
+
# IPython
|
|
67
|
+
profile_default/
|
|
68
|
+
ipython_config.py
|
|
69
|
+
|
|
70
|
+
# pyenv
|
|
71
|
+
.python-version
|
|
72
|
+
|
|
73
|
+
# Environments
|
|
74
|
+
.env
|
|
75
|
+
.venv
|
|
76
|
+
env/
|
|
77
|
+
venv/
|
|
78
|
+
ENV/
|
|
79
|
+
env.bak/
|
|
80
|
+
venv.bak/
|
|
81
|
+
|
|
82
|
+
# IDE
|
|
83
|
+
.idea/
|
|
84
|
+
.vscode/
|
|
85
|
+
|
|
86
|
+
# mypy
|
|
87
|
+
.mypy_cache/
|
|
88
|
+
.dmypy.json
|
|
89
|
+
dmypy.json
|
ocdkit-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Kevin Cutler
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
ocdkit-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ocdkit
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Obsessively precise utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
|
|
5
|
+
License: BSD-3-Clause
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Requires-Dist: scikit-image
|
|
12
|
+
Requires-Dist: tifffile
|
|
13
|
+
Requires-Dist: imagecodecs
|
|
14
|
+
Requires-Dist: matplotlib
|
|
15
|
+
Requires-Dist: fastremap
|
|
16
|
+
Requires-Dist: edt
|
|
17
|
+
Requires-Dist: torch>=1.12
|
|
18
|
+
Requires-Dist: dask[array]
|
|
19
|
+
Requires-Dist: natsort
|
|
20
|
+
Requires-Dist: numba
|
|
21
|
+
Requires-Dist: bioio
|
|
22
|
+
Requires-Dist: bioio-czi
|
|
23
|
+
Requires-Dist: ncolor>=1.5.1
|
|
24
|
+
Requires-Dist: cmap
|
|
25
|
+
Requires-Dist: tqdm
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# ocdkit
|
|
29
|
+
|
|
30
|
+
Obsessively precise utilities for scientific Python — array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install ocdkit # core (numpy, scipy, scikit-image, tifffile, matplotlib)
|
|
36
|
+
pip install ocdkit[torch] # + PyTorch GPU support
|
|
37
|
+
pip install ocdkit[plot] # + ncolor, cmap, opt_einsum
|
|
38
|
+
pip install ocdkit[spatial] # + numba, fastremap (contour extraction, skeletonization)
|
|
39
|
+
pip install ocdkit[all] # everything
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Modules
|
|
43
|
+
|
|
44
|
+
| Module | What's in it |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `ocdkit.array` | `rescale`, `safe_divide`, `is_integer`, `get_module`, `unique_nonzero` |
|
|
47
|
+
| `ocdkit.gpu` | `resolve_device`, `empty_cache`, `torch_GPU`, `torch_CPU` |
|
|
48
|
+
| `ocdkit.io` | `imread`, `imwrite`, `getname`, `check_dir` |
|
|
49
|
+
| `ocdkit.spatial` | `kernel_setup`, `get_neighbors`, `get_neigh_inds`, `masks_to_affinity`, `get_contour`, `boundary_to_masks` |
|
|
50
|
+
| `ocdkit.morphology` | `find_boundaries`, `skeletonize` |
|
|
51
|
+
| `ocdkit.measure` | `crop_bbox`, `bbox_to_slice`, `make_square`, `diameters` |
|
|
52
|
+
| `ocdkit.plot` | `figure`, `image_grid`, `split_list`, `colorize`, `rgb_flow`, `vector_contours`, `apply_ncolor`, `color_swatches`, `recolor_label`, `add_label_background` |
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from ocdkit.array import rescale
|
|
58
|
+
from ocdkit.gpu import resolve_device
|
|
59
|
+
from ocdkit.plot import figure, image_grid
|
|
60
|
+
|
|
61
|
+
device = resolve_device() # auto-detect CUDA / MPS / CPU
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
BSD-3-Clause
|
ocdkit-0.0.1/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ocdkit
|
|
2
|
+
|
|
3
|
+
Obsessively precise utilities for scientific Python — array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ocdkit # core (numpy, scipy, scikit-image, tifffile, matplotlib)
|
|
9
|
+
pip install ocdkit[torch] # + PyTorch GPU support
|
|
10
|
+
pip install ocdkit[plot] # + ncolor, cmap, opt_einsum
|
|
11
|
+
pip install ocdkit[spatial] # + numba, fastremap (contour extraction, skeletonization)
|
|
12
|
+
pip install ocdkit[all] # everything
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Modules
|
|
16
|
+
|
|
17
|
+
| Module | What's in it |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `ocdkit.array` | `rescale`, `safe_divide`, `is_integer`, `get_module`, `unique_nonzero` |
|
|
20
|
+
| `ocdkit.gpu` | `resolve_device`, `empty_cache`, `torch_GPU`, `torch_CPU` |
|
|
21
|
+
| `ocdkit.io` | `imread`, `imwrite`, `getname`, `check_dir` |
|
|
22
|
+
| `ocdkit.spatial` | `kernel_setup`, `get_neighbors`, `get_neigh_inds`, `masks_to_affinity`, `get_contour`, `boundary_to_masks` |
|
|
23
|
+
| `ocdkit.morphology` | `find_boundaries`, `skeletonize` |
|
|
24
|
+
| `ocdkit.measure` | `crop_bbox`, `bbox_to_slice`, `make_square`, `diameters` |
|
|
25
|
+
| `ocdkit.plot` | `figure`, `image_grid`, `split_list`, `colorize`, `rgb_flow`, `vector_contours`, `apply_ncolor`, `color_swatches`, `recolor_label`, `add_label_background` |
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from ocdkit.array import rescale
|
|
31
|
+
from ocdkit.gpu import resolve_device
|
|
32
|
+
from ocdkit.plot import figure, image_grid
|
|
33
|
+
|
|
34
|
+
device = resolve_device() # auto-detect CUDA / MPS / CPU
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
BSD-3-Clause
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "setuptools_scm>=6.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ocdkit"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Obsessively precise utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "BSD-3-Clause"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
"scipy",
|
|
15
|
+
"scikit-image",
|
|
16
|
+
"tifffile",
|
|
17
|
+
"imagecodecs",
|
|
18
|
+
"matplotlib",
|
|
19
|
+
"fastremap",
|
|
20
|
+
"edt",
|
|
21
|
+
"torch>=1.12",
|
|
22
|
+
"dask[array]",
|
|
23
|
+
"natsort",
|
|
24
|
+
"numba",
|
|
25
|
+
"bioio",
|
|
26
|
+
"bioio-czi",
|
|
27
|
+
"ncolor>=1.5.1",
|
|
28
|
+
"cmap",
|
|
29
|
+
"tqdm",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[tool.coverage.run]
|
|
39
|
+
source = ["ocdkit"]
|
|
40
|
+
|
|
41
|
+
[tool.coverage.paths]
|
|
42
|
+
source = [
|
|
43
|
+
"src/ocdkit",
|
|
44
|
+
"/Volumes/DataDrive/ocdkit/src/ocdkit",
|
|
45
|
+
"/home/kcutler/DataDrive/ocdkit/src/ocdkit",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.coverage.report]
|
|
49
|
+
show_missing = true
|
|
50
|
+
|
|
51
|
+
[tool.setuptools_scm]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Benchmark colorize: matmul vs opt_einsum across backends and shapes."""
|
|
2
|
+
|
|
3
|
+
import time, string
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
from opt_einsum import contract
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_colors(C):
|
|
10
|
+
angle = np.linspace(0, 1, C, endpoint=False) * 2 * np.pi
|
|
11
|
+
angles = np.stack((angle, angle + 2*np.pi/3, angle + 4*np.pi/3), axis=-1)
|
|
12
|
+
return ((np.cos(angles) + 1) / 2).astype(np.float32)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def bench(fn, n_warmup=3, n_iter=10):
|
|
16
|
+
for _ in range(n_warmup):
|
|
17
|
+
fn()
|
|
18
|
+
if torch.cuda.is_available():
|
|
19
|
+
torch.cuda.synchronize()
|
|
20
|
+
t0 = time.perf_counter()
|
|
21
|
+
for _ in range(n_iter):
|
|
22
|
+
fn()
|
|
23
|
+
if torch.cuda.is_available():
|
|
24
|
+
torch.cuda.synchronize()
|
|
25
|
+
return (time.perf_counter() - t0) / n_iter * 1000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
configs = [
|
|
29
|
+
(126, (2000, 2000), [126], '126ch 2kx2k single'),
|
|
30
|
+
(126, (2000, 2000), [42,42,42], '126ch 2kx2k 3-int'),
|
|
31
|
+
(126, (10000, 100), [126], '126ch 10kx100 single'),
|
|
32
|
+
(256, (5000, 50), [64]*4, '256ch 5kx50 4-int'),
|
|
33
|
+
(512, (1000, 1000), [512], '512ch 1kx1k single'),
|
|
34
|
+
(512, (1000, 1000), [128]*4, '512ch 1kx1k 4-int'),
|
|
35
|
+
(1024,(500, 500), [1024], '1024ch 500x500 single'),
|
|
36
|
+
(126, (100, 100, 100),[126], '126ch 100^3 single'),
|
|
37
|
+
(256, (32, 32), [256], '256ch 32x32 single'),
|
|
38
|
+
(3, (4000, 4000), [3], '3ch 4kx4k single'),
|
|
39
|
+
(8, (4000, 4000), [4,4], '8ch 4kx4k 2-int'),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
devices = ['cpu']
|
|
43
|
+
if torch.cuda.is_available():
|
|
44
|
+
devices.append('cuda')
|
|
45
|
+
if torch.backends.mps.is_available():
|
|
46
|
+
devices.append('mps')
|
|
47
|
+
|
|
48
|
+
for device_name in devices:
|
|
49
|
+
device = torch.device(device_name)
|
|
50
|
+
print(f"\n{'='*95}")
|
|
51
|
+
print(f" Device: {device_name.upper()}")
|
|
52
|
+
print(f"{'='*95}")
|
|
53
|
+
print(f"{'Config':<35} {'np mm':>8} {'np ein':>8} {'t mm':>8} {'t ein':>8} np torch")
|
|
54
|
+
print('-' * 95)
|
|
55
|
+
|
|
56
|
+
for C, spatial, intervals, label in configs:
|
|
57
|
+
N = len(intervals)
|
|
58
|
+
im_np = np.random.rand(C, *spatial).astype(np.float32)
|
|
59
|
+
colors = make_colors(C)
|
|
60
|
+
agg = np.zeros((C, N), dtype=np.float32)
|
|
61
|
+
s = 0
|
|
62
|
+
for i, sz in enumerate(intervals):
|
|
63
|
+
agg[s:s+sz, i] = 1.0/sz
|
|
64
|
+
s += sz
|
|
65
|
+
|
|
66
|
+
idx = ''.join(c for c in string.ascii_lowercase if c not in 'cl')
|
|
67
|
+
sp = idx[:len(spatial)]
|
|
68
|
+
eq = f'c{sp},cN,cl->N{sp}l'
|
|
69
|
+
|
|
70
|
+
# Numpy (always CPU)
|
|
71
|
+
def np_mm():
|
|
72
|
+
w = (agg[...,None]*colors[:,None,:]).reshape(C,N*3)
|
|
73
|
+
return w.T @ im_np.reshape(C,-1)
|
|
74
|
+
def np_ein():
|
|
75
|
+
return contract(eq, im_np, agg, colors)
|
|
76
|
+
|
|
77
|
+
t1 = bench(np_mm)
|
|
78
|
+
t2 = bench(np_ein)
|
|
79
|
+
|
|
80
|
+
# Torch on target device
|
|
81
|
+
im_t = torch.from_numpy(im_np).to(device)
|
|
82
|
+
agg_t = torch.from_numpy(agg).to(device)
|
|
83
|
+
col_t = torch.from_numpy(colors).to(device)
|
|
84
|
+
|
|
85
|
+
def t_mm():
|
|
86
|
+
w = (agg_t[...,None]*col_t[:,None,:]).reshape(C,N*3).float()
|
|
87
|
+
return w.T @ im_t.reshape(C,-1).float()
|
|
88
|
+
def t_ein():
|
|
89
|
+
return contract(eq, im_t.float(), agg_t, col_t)
|
|
90
|
+
|
|
91
|
+
t3 = bench(t_mm)
|
|
92
|
+
t4 = bench(t_ein)
|
|
93
|
+
|
|
94
|
+
np_w = 'ein' if t2 < t1 else 'mm'
|
|
95
|
+
t_w = 'ein' if t4 < t3 else 'mm'
|
|
96
|
+
np_r = max(t1,t2)/min(t1,t2)
|
|
97
|
+
t_r = max(t3,t4)/min(t3,t4)
|
|
98
|
+
|
|
99
|
+
print(f'{label:<35} {t1:>6.1f}ms {t2:>6.1f}ms {t3:>6.1f}ms {t4:>6.1f}ms {np_w}({np_r:.1f}x) {t_w}({t_r:.1f}x)')
|
|
100
|
+
|
|
101
|
+
# Free GPU memory
|
|
102
|
+
del im_t, agg_t, col_t
|
|
103
|
+
if device_name != 'cpu':
|
|
104
|
+
torch.cuda.empty_cache() if device_name == 'cuda' else torch.mps.empty_cache()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Run ocdkit tests on Mac (MPS), Threadripper (CUDA), and
|
|
4
|
+
# Threadripper with CUDA disabled (CPU-only), then combine coverage.
|
|
5
|
+
# Notebook-based tests (IPython/Jupyter-dependent code) run on Mac only.
|
|
6
|
+
#
|
|
7
|
+
# Usage: bash scripts/coverage_cross_device.sh
|
|
8
|
+
#
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
MAC_ROOT="/Volumes/DataDrive/ocdkit"
|
|
12
|
+
REMOTE_ROOT="/home/kcutler/DataDrive/ocdkit"
|
|
13
|
+
REMOTE="kcutler@threadripper.local"
|
|
14
|
+
PYENV='export PATH="$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH"'
|
|
15
|
+
COV_DIR="$MAC_ROOT/.coverage_combined"
|
|
16
|
+
|
|
17
|
+
rm -rf "$COV_DIR"
|
|
18
|
+
mkdir -p "$COV_DIR"
|
|
19
|
+
|
|
20
|
+
echo "=== Mac (MPS) ==="
|
|
21
|
+
cd "$MAC_ROOT"
|
|
22
|
+
python -m coverage run --data-file="$COV_DIR/.coverage.mac" -m pytest tests/ -q
|
|
23
|
+
|
|
24
|
+
echo ""
|
|
25
|
+
echo "=== Threadripper (CUDA) ==="
|
|
26
|
+
ssh "$REMOTE" "$PYENV && cd $REMOTE_ROOT && python -m coverage run --data-file=/tmp/.coverage.cuda -m pytest tests/ -q --ignore=tests/test_plot_notebook.py"
|
|
27
|
+
scp "$REMOTE":/tmp/.coverage.cuda "$COV_DIR/.coverage.cuda"
|
|
28
|
+
|
|
29
|
+
echo ""
|
|
30
|
+
echo "=== Threadripper (CPU-only) ==="
|
|
31
|
+
ssh "$REMOTE" "$PYENV && cd $REMOTE_ROOT && CUDA_VISIBLE_DEVICES='' python -m coverage run --data-file=/tmp/.coverage.cpu -m pytest tests/ -q --ignore=tests/test_plot_notebook.py"
|
|
32
|
+
scp "$REMOTE":/tmp/.coverage.cpu "$COV_DIR/.coverage.cpu"
|
|
33
|
+
|
|
34
|
+
echo ""
|
|
35
|
+
echo "=== Combining coverage ==="
|
|
36
|
+
cd "$MAC_ROOT"
|
|
37
|
+
python -m coverage combine --data-file="$COV_DIR/.coverage" "$COV_DIR"/.coverage.*
|
|
38
|
+
python -m coverage report --data-file="$COV_DIR/.coverage" --show-missing
|
|
39
|
+
echo ""
|
|
40
|
+
python -m coverage html --data-file="$COV_DIR/.coverage" -d "$COV_DIR/htmlcov"
|
|
41
|
+
echo "HTML report: file://$COV_DIR/htmlcov/index.html"
|
ocdkit-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Type detection, conversion, and rescaling utilities."""
|
|
2
|
+
|
|
3
|
+
from .imports import *
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_module(x):
|
|
7
|
+
"""Return ``np`` or ``torch`` depending on the type of *x*."""
|
|
8
|
+
if isinstance(x, da.Array):
|
|
9
|
+
return np
|
|
10
|
+
if isinstance(x, (np.ndarray, tuple, int, float)) or np.isscalar(x):
|
|
11
|
+
return np
|
|
12
|
+
if torch.is_tensor(x):
|
|
13
|
+
return torch
|
|
14
|
+
raise ValueError(
|
|
15
|
+
"Input must be a numpy array, a tuple, a torch tensor, "
|
|
16
|
+
"an integer, or a float"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def safe_divide(num, den, cutoff=0):
|
|
21
|
+
"""Division ignoring zeros and NaNs in the denominator."""
|
|
22
|
+
module = get_module(num)
|
|
23
|
+
valid_den = (den > cutoff) & module.isfinite(den)
|
|
24
|
+
|
|
25
|
+
if isinstance(num, da.Array) or isinstance(den, da.Array):
|
|
26
|
+
return da.where(valid_den, num / den, 0)
|
|
27
|
+
elif module == np:
|
|
28
|
+
r = num.astype(np.float32, copy=False)
|
|
29
|
+
r = np.divide(r, den, out=np.zeros_like(r), where=valid_den)
|
|
30
|
+
elif module == torch:
|
|
31
|
+
r = num.float()
|
|
32
|
+
den = den.float()
|
|
33
|
+
safe_den = torch.where(valid_den, den, torch.ones_like(den))
|
|
34
|
+
r = torch.where(valid_den, torch.div(r, safe_den), torch.zeros_like(r))
|
|
35
|
+
else:
|
|
36
|
+
raise TypeError("num must be a numpy array or a PyTorch tensor")
|
|
37
|
+
|
|
38
|
+
return r
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def rescale(T, floor=None, ceiling=None, exclude_dims=None):
|
|
42
|
+
"""Min-max rescale to [0, 1].
|
|
43
|
+
|
|
44
|
+
Works on numpy arrays and torch tensors. When *exclude_dims* is
|
|
45
|
+
given, normalization is applied independently along those axes.
|
|
46
|
+
"""
|
|
47
|
+
module = get_module(T)
|
|
48
|
+
if exclude_dims is not None:
|
|
49
|
+
if isinstance(exclude_dims, int):
|
|
50
|
+
exclude_dims = (exclude_dims,)
|
|
51
|
+
axes = tuple(i for i in range(T.ndim) if i not in exclude_dims)
|
|
52
|
+
newshape = [T.shape[i] if i in exclude_dims else 1 for i in range(T.ndim)]
|
|
53
|
+
else:
|
|
54
|
+
axes = None
|
|
55
|
+
newshape = T.shape
|
|
56
|
+
|
|
57
|
+
if ceiling is None:
|
|
58
|
+
ceiling = module.amax(T, axis=axes)
|
|
59
|
+
if exclude_dims is not None:
|
|
60
|
+
ceiling = ceiling.reshape(*newshape)
|
|
61
|
+
if floor is None:
|
|
62
|
+
floor = module.amin(T, axis=axes)
|
|
63
|
+
if exclude_dims is not None:
|
|
64
|
+
floor = floor.reshape(*newshape)
|
|
65
|
+
|
|
66
|
+
T = safe_divide(T - floor, ceiling - floor)
|
|
67
|
+
|
|
68
|
+
return T
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def to_16_bit(im):
|
|
72
|
+
"""Rescale image to [0, 2**16 - 1] and cast to uint16."""
|
|
73
|
+
return np.uint16(rescale(im) * (2 ** 16 - 1))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_8_bit(im):
|
|
77
|
+
"""Rescale image to [0, 2**8 - 1] and cast to uint8."""
|
|
78
|
+
return np.uint8(rescale(im) * (2 ** 8 - 1))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_integer(var):
|
|
82
|
+
"""Check whether *var* is an integer or integer-typed array/tensor."""
|
|
83
|
+
if isinstance(var, int):
|
|
84
|
+
return True
|
|
85
|
+
if isinstance(var, np.integer):
|
|
86
|
+
return True
|
|
87
|
+
if isinstance(var, (np.ndarray, np.memmap)) and np.issubdtype(var.dtype, np.integer):
|
|
88
|
+
return True
|
|
89
|
+
if isinstance(var, da.Array) and np.issubdtype(var.dtype, np.integer):
|
|
90
|
+
return True
|
|
91
|
+
if isinstance(var, torch.Tensor) and not var.is_floating_point():
|
|
92
|
+
return True
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def move_axis(img, axis=-1, pos="last"):
|
|
97
|
+
"""Move ndarray axis to a new location, preserving order of other axes."""
|
|
98
|
+
if axis == -1:
|
|
99
|
+
axis = img.ndim - 1
|
|
100
|
+
axis = min(img.ndim - 1, axis)
|
|
101
|
+
if pos in ("first", 0):
|
|
102
|
+
pos = 0
|
|
103
|
+
elif pos in ("last", -1):
|
|
104
|
+
pos = img.ndim - 1
|
|
105
|
+
perm = list(range(img.ndim))
|
|
106
|
+
perm.pop(axis)
|
|
107
|
+
perm.insert(pos, axis)
|
|
108
|
+
return np.transpose(img, perm)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def move_min_dim(img, force=False):
|
|
112
|
+
"""Move the minimum-sized dimension last (as channels) if < 10."""
|
|
113
|
+
if len(img.shape) > 2:
|
|
114
|
+
min_dim = min(img.shape)
|
|
115
|
+
if min_dim < 10 or force:
|
|
116
|
+
if img.shape[-1] == min_dim:
|
|
117
|
+
channel_axis = -1
|
|
118
|
+
else:
|
|
119
|
+
channel_axis = (img.shape).index(min_dim)
|
|
120
|
+
img = move_axis(img, axis=channel_axis, pos="last")
|
|
121
|
+
return img
|