cellfinder 1.1.2__tar.gz → 1.2.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.
Potentially problematic release.
This version of cellfinder might be problematic. Click here for more details.
- {cellfinder-1.1.2 → cellfinder-1.2.0}/.github/workflows/test_and_deploy.yml +14 -8
- {cellfinder-1.1.2 → cellfinder-1.2.0}/.github/workflows/test_include_guard.yaml +6 -1
- {cellfinder-1.1.2 → cellfinder-1.2.0}/.gitignore +0 -5
- {cellfinder-1.1.2 → cellfinder-1.2.0}/PKG-INFO +9 -9
- {cellfinder-1.1.2 → cellfinder-1.2.0}/README.md +7 -7
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/__init__.py +3 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/detect.py +12 -1
- cellfinder-1.2.0/cellfinder/core/detect/filters/volume/ball_filter.py +417 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/structure_detection.py +105 -41
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/structure_splitting.py +1 -1
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/volume_filter.py +48 -49
- cellfinder-1.2.0/cellfinder/core/download/cli.py +79 -0
- cellfinder-1.2.0/cellfinder/core/download/download.py +120 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/main.py +52 -42
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/prep.py +11 -10
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/source_files.py +5 -3
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/train/train_yml.py +4 -6
- cellfinder-1.2.0/cellfinder/napari/detect/detect.py +428 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/detect/detect_containers.py +9 -1
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/detect/thread_worker.py +14 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/train/train.py +2 -9
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/train/train_containers.py +3 -3
- cellfinder-1.2.0/cellfinder/napari/utils.py +133 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/PKG-INFO +9 -9
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/SOURCES.txt +0 -3
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/requires.txt +1 -1
- {cellfinder-1.1.2 → cellfinder-1.2.0}/pyproject.toml +2 -2
- cellfinder-1.1.2/cellfinder/core/detect/filters/volume/ball_filter.py +0 -332
- cellfinder-1.1.2/cellfinder/core/download/cli.py +0 -72
- cellfinder-1.1.2/cellfinder/core/download/download.py +0 -128
- cellfinder-1.1.2/cellfinder/core/download/models.py +0 -49
- cellfinder-1.1.2/cellfinder/core/tools/IO.py +0 -48
- cellfinder-1.1.2/cellfinder/napari/detect/detect.py +0 -233
- cellfinder-1.1.2/cellfinder/napari/images/brainglobe.png +0 -0
- cellfinder-1.1.2/cellfinder/napari/utils.py +0 -92
- {cellfinder-1.1.2 → cellfinder-1.2.0}/.napari/config.yml +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/CITATION.cff +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/LICENSE +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/MANIFEST.in +1 -1
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/cli_migration_warning.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/augment.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/classify.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/cube_generator.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/resnet.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/classify/tools.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/config/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/config/cellfinder.conf +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/classical_filter.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/plane_filter.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/tile_walker.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/setup_filters.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/download/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/array_operations.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/geometry.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/image_processing.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/system.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/tf.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/tiff.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/tools/tools.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/train/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/core/types.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/curation.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/detect/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/input_container.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/napari.yaml +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/sample_data.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder/napari/train/__init__.py +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/dependency_links.txt +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/entry_points.txt +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/cellfinder.egg-info/top_level.txt +0 -0
- {cellfinder-1.1.2 → cellfinder-1.2.0}/setup.cfg +0 -0
|
@@ -42,12 +42,14 @@ jobs:
|
|
|
42
42
|
# Run all supported Python versions on linux
|
|
43
43
|
os: [ubuntu-latest]
|
|
44
44
|
python-version: ["3.9", "3.10"]
|
|
45
|
-
# Include one windows, one macos run
|
|
45
|
+
# Include one windows, one macos run each for M1 (latest) and Intel (13)
|
|
46
46
|
include:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
- os: macos-13
|
|
48
|
+
python-version: "3.10"
|
|
49
|
+
- os: macos-latest
|
|
50
|
+
python-version: "3.10"
|
|
51
|
+
- os: windows-latest
|
|
52
|
+
python-version: "3.10"
|
|
51
53
|
|
|
52
54
|
steps:
|
|
53
55
|
# Cache the tensorflow model so we don't have to remake it every time
|
|
@@ -55,7 +57,7 @@ jobs:
|
|
|
55
57
|
uses: actions/cache@v3
|
|
56
58
|
with:
|
|
57
59
|
path: "~/.cellfinder"
|
|
58
|
-
key: models-${{ hashFiles('~/.
|
|
60
|
+
key: models-${{ hashFiles('~/.brainglobe/**') }}
|
|
59
61
|
# Setup pyqt libraries
|
|
60
62
|
- name: Setup qtpy libraries
|
|
61
63
|
uses: tlambert03/setup-qt-libs@v1
|
|
@@ -65,11 +67,13 @@ jobs:
|
|
|
65
67
|
- uses: neuroinformatics-unit/actions/test@v2
|
|
66
68
|
with:
|
|
67
69
|
python-version: ${{ matrix.python-version }}
|
|
70
|
+
secret-codecov-token: ${{ secrets.CODECOV_TOKEN }}
|
|
68
71
|
use-xvfb: true
|
|
69
72
|
|
|
70
73
|
test_numba_disabled:
|
|
71
74
|
needs: [linting, manifest]
|
|
72
75
|
name: Run tests with numba disabled
|
|
76
|
+
timeout-minutes: 60
|
|
73
77
|
runs-on: ubuntu-latest
|
|
74
78
|
env:
|
|
75
79
|
NUMBA_DISABLE_JIT: "1"
|
|
@@ -79,7 +83,7 @@ jobs:
|
|
|
79
83
|
uses: actions/cache@v3
|
|
80
84
|
with:
|
|
81
85
|
path: "~/.cellfinder"
|
|
82
|
-
key: models-${{ hashFiles('~/.
|
|
86
|
+
key: models-${{ hashFiles('~/.brainglobe/**') }}
|
|
83
87
|
# Setup pyqt libraries
|
|
84
88
|
- name: Setup qtpy libraries
|
|
85
89
|
uses: tlambert03/setup-qt-libs@v1
|
|
@@ -89,6 +93,7 @@ jobs:
|
|
|
89
93
|
- uses: neuroinformatics-unit/actions/test@v2
|
|
90
94
|
with:
|
|
91
95
|
python-version: "3.10"
|
|
96
|
+
secret-codecov-token: ${{ secrets.CODECOV_TOKEN }}
|
|
92
97
|
codecov-flags: "numba"
|
|
93
98
|
|
|
94
99
|
# Run brainglobe-workflows brainmapper-CLI tests to check for
|
|
@@ -96,13 +101,14 @@ jobs:
|
|
|
96
101
|
test_brainmapper_cli:
|
|
97
102
|
needs: [linting, manifest]
|
|
98
103
|
name: Run brainmapper tests to check for breakages
|
|
104
|
+
timeout-minutes: 60
|
|
99
105
|
runs-on: ubuntu-latest
|
|
100
106
|
steps:
|
|
101
107
|
- name: Cache tensorflow model
|
|
102
108
|
uses: actions/cache@v3
|
|
103
109
|
with:
|
|
104
110
|
path: "~/.cellfinder"
|
|
105
|
-
key: models-${{ hashFiles('~/.
|
|
111
|
+
key: models-${{ hashFiles('~/.brainglobe/**') }}
|
|
106
112
|
|
|
107
113
|
- name: Checkout brainglobe-workflows
|
|
108
114
|
uses: actions/checkout@v3
|
|
@@ -35,7 +35,12 @@ jobs:
|
|
|
35
35
|
import cellfinder.core
|
|
36
36
|
import cellfinder.napari
|
|
37
37
|
|
|
38
|
-
- name: Uninstall tensorflow
|
|
38
|
+
- name: Uninstall tensorflow-macos on Mac M1
|
|
39
|
+
if: matrix.os == 'macos-latest'
|
|
40
|
+
run: python -m pip uninstall -y tensorflow-macos
|
|
41
|
+
|
|
42
|
+
- name: Uninstall tensorflow on Ubuntu
|
|
43
|
+
if: matrix.os == 'ubuntu-latest'
|
|
39
44
|
run: python -m pip uninstall -y tensorflow
|
|
40
45
|
|
|
41
46
|
- name: Test (broken) import
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cellfinder
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Automated 3D cell detection in large microscopy images
|
|
5
5
|
Author-email: "Adam Tyson, Christian Niedworok, Charly Rousseau" <code@adamltyson.com>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: brainglobe-utils>=0.
|
|
25
|
+
Requires-Dist: brainglobe-utils>=0.5.0
|
|
26
26
|
Requires-Dist: brainglobe-napari-io>=0.3.4
|
|
27
27
|
Requires-Dist: dask[array]
|
|
28
28
|
Requires-Dist: fancylog>=0.0.7
|
|
@@ -54,17 +54,17 @@ Requires-Dist: napari[pyqt5]; extra == "napari"
|
|
|
54
54
|
Requires-Dist: pooch>=1; extra == "napari"
|
|
55
55
|
Requires-Dist: qtpy; extra == "napari"
|
|
56
56
|
|
|
57
|
-
[](https://pypi.org/project/cellfinder)
|
|
58
|
+
[](https://pypi.org/project/cellfinder)
|
|
59
|
+
[](https://pepy.tech/project/cellfinder)
|
|
60
|
+
[](https://pypi.org/project/cellfinder)
|
|
61
|
+
[](https://github.com/brainglobe/cellfinder)
|
|
62
62
|
[](https://github.com/brainglobe/cellfinder/actions)
|
|
63
|
-
[](https://codecov.io/gh/brainglobe/cellfinder)
|
|
64
64
|
[](https://github.com/python/black)
|
|
65
65
|
[](https://pycqa.github.io/isort/)
|
|
66
66
|
[](https://github.com/pre-commit/pre-commit)
|
|
67
|
-
[](https://brainglobe.info/developers/index.html)
|
|
67
|
+
[](https://brainglobe.info/community/developers/index.html)
|
|
68
68
|
[](https://twitter.com/brain_globe)
|
|
69
69
|
|
|
70
70
|
# cellfinder
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
[](https://pypi.org/project/cellfinder)
|
|
2
|
+
[](https://pypi.org/project/cellfinder)
|
|
3
|
+
[](https://pepy.tech/project/cellfinder)
|
|
4
|
+
[](https://pypi.org/project/cellfinder)
|
|
5
|
+
[](https://github.com/brainglobe/cellfinder)
|
|
6
6
|
[](https://github.com/brainglobe/cellfinder/actions)
|
|
7
|
-
[](https://codecov.io/gh/brainglobe/cellfinder)
|
|
8
8
|
[](https://github.com/python/black)
|
|
9
9
|
[](https://pycqa.github.io/isort/)
|
|
10
10
|
[](https://github.com/pre-commit/pre-commit)
|
|
11
|
-
[](https://brainglobe.info/developers/index.html)
|
|
11
|
+
[](https://brainglobe.info/community/developers/index.html)
|
|
12
12
|
[](https://twitter.com/brain_globe)
|
|
13
13
|
|
|
14
14
|
# cellfinder
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
try:
|
|
4
5
|
__version__ = version("cellfinder")
|
|
@@ -22,3 +23,5 @@ except PackageNotFoundError as e:
|
|
|
22
23
|
|
|
23
24
|
__author__ = "Adam Tyson, Christian Niedworok, Charly Rousseau"
|
|
24
25
|
__license__ = "BSD-3-Clause"
|
|
26
|
+
|
|
27
|
+
DEFAULT_CELLFINDER_DIRECTORY = Path.home() / ".brainglobe" / "cellfinder"
|
|
@@ -22,6 +22,7 @@ from typing import Callable, List, Optional, Sequence, Tuple, TypeVar
|
|
|
22
22
|
import numpy as np
|
|
23
23
|
from brainglobe_utils.cells.cells import Cell
|
|
24
24
|
from brainglobe_utils.general.system import get_num_processes
|
|
25
|
+
from numba import set_num_threads
|
|
25
26
|
|
|
26
27
|
from cellfinder.core import logger, types
|
|
27
28
|
from cellfinder.core.detect.filters.plane import TileProcessor
|
|
@@ -157,6 +158,13 @@ def main(
|
|
|
157
158
|
)
|
|
158
159
|
n_processes = get_num_processes(min_free_cpu_cores=n_free_cpus)
|
|
159
160
|
n_ball_procs = max(n_processes - 1, 1)
|
|
161
|
+
|
|
162
|
+
# we parallelize 2d filtering, which typically lags behind the 3d
|
|
163
|
+
# processing so for n_ball_procs 2d filtering threads, ball_z_size will
|
|
164
|
+
# typically be in use while the others stall waiting for 3d processing
|
|
165
|
+
# so we can use those for other things, such as numba threading
|
|
166
|
+
set_num_threads(max(n_ball_procs - int(ball_z_size), 1))
|
|
167
|
+
|
|
160
168
|
start_time = datetime.now()
|
|
161
169
|
|
|
162
170
|
(
|
|
@@ -236,7 +244,10 @@ def main(
|
|
|
236
244
|
# then 3D filtering has finished. As batches of planes are filtered
|
|
237
245
|
# by the 3D filter, it releases the locks of subsequent 2D filter
|
|
238
246
|
# processes.
|
|
239
|
-
|
|
247
|
+
mp_3d_filter.process(async_results, locks, callback=callback)
|
|
248
|
+
|
|
249
|
+
# it's now done filtering, get results with pool
|
|
250
|
+
cells = mp_3d_filter.get_results(worker_pool)
|
|
240
251
|
|
|
241
252
|
time_elapsed = datetime.now() - start_time
|
|
242
253
|
logger.debug(
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numba import njit, objmode, prange
|
|
5
|
+
from numba.core import types
|
|
6
|
+
from numba.experimental import jitclass
|
|
7
|
+
|
|
8
|
+
from cellfinder.core.tools.array_operations import bin_mean_3d
|
|
9
|
+
from cellfinder.core.tools.geometry import make_sphere
|
|
10
|
+
|
|
11
|
+
DEBUG = False
|
|
12
|
+
|
|
13
|
+
uint32_3d_type = types.uint32[:, :, :]
|
|
14
|
+
bool_3d_type = types.bool_[:, :, :]
|
|
15
|
+
float_3d_type = types.float64[:, :, :]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@lru_cache(maxsize=50)
|
|
19
|
+
def get_kernel(ball_xy_size: int, ball_z_size: int) -> np.ndarray:
|
|
20
|
+
# Create a spherical kernel.
|
|
21
|
+
#
|
|
22
|
+
# This is done by:
|
|
23
|
+
# 1. Generating a binary sphere at a resolution *upscale_factor* larger
|
|
24
|
+
# than desired.
|
|
25
|
+
# 2. Downscaling the binary sphere to get a 'fuzzy' sphere at the
|
|
26
|
+
# original intended scale
|
|
27
|
+
upscale_factor: int = 7
|
|
28
|
+
upscaled_kernel_shape = (
|
|
29
|
+
upscale_factor * ball_xy_size,
|
|
30
|
+
upscale_factor * ball_xy_size,
|
|
31
|
+
upscale_factor * ball_z_size,
|
|
32
|
+
)
|
|
33
|
+
upscaled_ball_centre_position = (
|
|
34
|
+
np.floor(upscaled_kernel_shape[0] / 2),
|
|
35
|
+
np.floor(upscaled_kernel_shape[1] / 2),
|
|
36
|
+
np.floor(upscaled_kernel_shape[2] / 2),
|
|
37
|
+
)
|
|
38
|
+
upscaled_ball_radius = upscaled_kernel_shape[0] / 2.0
|
|
39
|
+
|
|
40
|
+
sphere_kernel = make_sphere(
|
|
41
|
+
upscaled_kernel_shape,
|
|
42
|
+
upscaled_ball_radius,
|
|
43
|
+
upscaled_ball_centre_position,
|
|
44
|
+
)
|
|
45
|
+
sphere_kernel = sphere_kernel.astype(np.float64)
|
|
46
|
+
kernel = bin_mean_3d(
|
|
47
|
+
sphere_kernel,
|
|
48
|
+
bin_height=upscale_factor,
|
|
49
|
+
bin_width=upscale_factor,
|
|
50
|
+
bin_depth=upscale_factor,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert (
|
|
54
|
+
kernel.shape[2] == ball_z_size
|
|
55
|
+
), "Kernel z dimension should be {}, got {}".format(
|
|
56
|
+
ball_z_size, kernel.shape[2]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return kernel
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# volume indices/size is 64 bit for very large brains(!)
|
|
63
|
+
spec = [
|
|
64
|
+
("ball_xy_size", types.uint32),
|
|
65
|
+
("ball_z_size", types.uint32),
|
|
66
|
+
("tile_step_width", types.uint64),
|
|
67
|
+
("tile_step_height", types.uint64),
|
|
68
|
+
("THRESHOLD_VALUE", types.uint32),
|
|
69
|
+
("SOMA_CENTRE_VALUE", types.uint32),
|
|
70
|
+
("overlap_fraction", types.float64),
|
|
71
|
+
("overlap_threshold", types.float64),
|
|
72
|
+
("middle_z_idx", types.uint32),
|
|
73
|
+
("_num_z_added", types.uint32),
|
|
74
|
+
("kernel", float_3d_type),
|
|
75
|
+
("volume", uint32_3d_type),
|
|
76
|
+
("inside_brain_tiles", bool_3d_type),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@jitclass(spec=spec)
|
|
81
|
+
class BallFilter:
|
|
82
|
+
"""
|
|
83
|
+
A 3D ball filter.
|
|
84
|
+
|
|
85
|
+
This runs a spherical kernel across the (x, y) dimensions
|
|
86
|
+
of a *ball_z_size* stack of planes, and marks pixels in the middle
|
|
87
|
+
plane of the stack that have a high enough intensity within the
|
|
88
|
+
spherical kernel.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
plane_width: int,
|
|
94
|
+
plane_height: int,
|
|
95
|
+
ball_xy_size: int,
|
|
96
|
+
ball_z_size: int,
|
|
97
|
+
overlap_fraction: float,
|
|
98
|
+
tile_step_width: int,
|
|
99
|
+
tile_step_height: int,
|
|
100
|
+
threshold_value: int,
|
|
101
|
+
soma_centre_value: int,
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
plane_width, plane_height :
|
|
107
|
+
Width/height of the planes.
|
|
108
|
+
ball_xy_size :
|
|
109
|
+
Diameter of the spherical kernel in the x/y dimensions.
|
|
110
|
+
ball_z_size :
|
|
111
|
+
Diameter of the spherical kernel in the z dimension.
|
|
112
|
+
Equal to the number of planes that stacked to filter
|
|
113
|
+
the central plane of the stack.
|
|
114
|
+
overlap_fraction :
|
|
115
|
+
The fraction of pixels within the spherical kernel that
|
|
116
|
+
have to be over *threshold_value* for a pixel to be marked
|
|
117
|
+
as having a high intensity.
|
|
118
|
+
tile_step_width, tile_step_height :
|
|
119
|
+
Width/height of individual tiles in the mask generated by
|
|
120
|
+
2D filtering.
|
|
121
|
+
threshold_value :
|
|
122
|
+
Value above which an individual pixel is considered to have
|
|
123
|
+
a high intensity.
|
|
124
|
+
soma_centre_value :
|
|
125
|
+
Value used to mark pixels with a high enough intensity.
|
|
126
|
+
"""
|
|
127
|
+
self.ball_xy_size = ball_xy_size
|
|
128
|
+
self.ball_z_size = ball_z_size
|
|
129
|
+
self.overlap_fraction = overlap_fraction
|
|
130
|
+
self.tile_step_width = tile_step_width
|
|
131
|
+
self.tile_step_height = tile_step_height
|
|
132
|
+
|
|
133
|
+
self.THRESHOLD_VALUE = threshold_value
|
|
134
|
+
self.SOMA_CENTRE_VALUE = soma_centre_value
|
|
135
|
+
|
|
136
|
+
# getting kernel is not jitted
|
|
137
|
+
with objmode(kernel=float_3d_type):
|
|
138
|
+
kernel = get_kernel(ball_xy_size, ball_z_size)
|
|
139
|
+
self.kernel = kernel
|
|
140
|
+
|
|
141
|
+
self.overlap_threshold = np.sum(self.overlap_fraction * self.kernel)
|
|
142
|
+
|
|
143
|
+
# Stores the current planes that are being filtered
|
|
144
|
+
# first axis is z for faster rotating the z-axis
|
|
145
|
+
self.volume = np.empty(
|
|
146
|
+
(ball_z_size, plane_width, plane_height),
|
|
147
|
+
dtype=np.uint32,
|
|
148
|
+
)
|
|
149
|
+
# Index of the middle plane in the volume
|
|
150
|
+
self.middle_z_idx = int(np.floor(ball_z_size / 2))
|
|
151
|
+
self._num_z_added = 0
|
|
152
|
+
|
|
153
|
+
# first axis is z
|
|
154
|
+
self.inside_brain_tiles = np.empty(
|
|
155
|
+
(
|
|
156
|
+
ball_z_size,
|
|
157
|
+
int(np.ceil(plane_width / tile_step_width)),
|
|
158
|
+
int(np.ceil(plane_height / tile_step_height)),
|
|
159
|
+
),
|
|
160
|
+
dtype=np.bool_,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def ready(self) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Return `True` if enough planes have been appended to run the filter.
|
|
167
|
+
"""
|
|
168
|
+
return self._num_z_added >= self.ball_z_size
|
|
169
|
+
|
|
170
|
+
def append(self, plane: np.ndarray, mask: np.ndarray) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Add a new 2D plane to the filter.
|
|
173
|
+
"""
|
|
174
|
+
if DEBUG:
|
|
175
|
+
assert [e for e in plane.shape[:2]] == [
|
|
176
|
+
e for e in self.volume.shape[1:]
|
|
177
|
+
], 'plane shape mismatch, expected "{}", got "{}"'.format(
|
|
178
|
+
[e for e in self.volume.shape[1:]],
|
|
179
|
+
[e for e in plane.shape[:2]],
|
|
180
|
+
)
|
|
181
|
+
assert [e for e in mask.shape[:2]] == [
|
|
182
|
+
e for e in self.inside_brain_tiles.shape[1:]
|
|
183
|
+
], 'mask shape mismatch, expected"{}", got {}"'.format(
|
|
184
|
+
[e for e in self.inside_brain_tiles.shape[1:]],
|
|
185
|
+
[e for e in mask.shape[:2]],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if self.ready:
|
|
189
|
+
# Shift everything down by one to make way for the new plane
|
|
190
|
+
# this is faster than np.roll, especially with z-axis first
|
|
191
|
+
self.volume[:-1, :, :] = self.volume[1:, :, :]
|
|
192
|
+
self.inside_brain_tiles[:-1, :, :] = self.inside_brain_tiles[
|
|
193
|
+
1:, :, :
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
# index for *next* slice is num we added *so far* until max
|
|
197
|
+
idx = min(self._num_z_added, self.ball_z_size - 1)
|
|
198
|
+
self._num_z_added += 1
|
|
199
|
+
|
|
200
|
+
# Add the new plane to the top of volume and inside_brain_tiles
|
|
201
|
+
self.volume[idx, :, :] = plane
|
|
202
|
+
self.inside_brain_tiles[idx, :, :] = mask
|
|
203
|
+
|
|
204
|
+
def get_middle_plane(self) -> np.ndarray:
|
|
205
|
+
"""
|
|
206
|
+
Get the plane in the middle of self.volume.
|
|
207
|
+
"""
|
|
208
|
+
return self.volume[self.middle_z_idx, :, :].copy()
|
|
209
|
+
|
|
210
|
+
def walk(self, parallel: bool = False) -> None:
|
|
211
|
+
# **don't** pass parallel as keyword arg - numba struggles with it
|
|
212
|
+
# Highly optimised because most time critical
|
|
213
|
+
ball_radius = self.ball_xy_size // 2
|
|
214
|
+
# Get extents of image that are covered by tiles
|
|
215
|
+
tile_mask_covered_img_width = (
|
|
216
|
+
self.inside_brain_tiles.shape[1] * self.tile_step_width
|
|
217
|
+
)
|
|
218
|
+
tile_mask_covered_img_height = (
|
|
219
|
+
self.inside_brain_tiles.shape[2] * self.tile_step_height
|
|
220
|
+
)
|
|
221
|
+
# Get maximum offsets for the ball
|
|
222
|
+
max_width = tile_mask_covered_img_width - self.ball_xy_size
|
|
223
|
+
max_height = tile_mask_covered_img_height - self.ball_xy_size
|
|
224
|
+
|
|
225
|
+
# we have to pass the raw volume so walk doesn't use its edits as it
|
|
226
|
+
# processes the volume. self.volume is the one edited in place
|
|
227
|
+
input_volume = self.volume.copy()
|
|
228
|
+
|
|
229
|
+
if parallel:
|
|
230
|
+
_walk_parallel(
|
|
231
|
+
max_height,
|
|
232
|
+
max_width,
|
|
233
|
+
self.tile_step_width,
|
|
234
|
+
self.tile_step_height,
|
|
235
|
+
self.inside_brain_tiles,
|
|
236
|
+
input_volume,
|
|
237
|
+
self.volume,
|
|
238
|
+
self.kernel,
|
|
239
|
+
ball_radius,
|
|
240
|
+
self.middle_z_idx,
|
|
241
|
+
self.overlap_threshold,
|
|
242
|
+
self.THRESHOLD_VALUE,
|
|
243
|
+
self.SOMA_CENTRE_VALUE,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
_walk_single(
|
|
247
|
+
max_height,
|
|
248
|
+
max_width,
|
|
249
|
+
self.tile_step_width,
|
|
250
|
+
self.tile_step_height,
|
|
251
|
+
self.inside_brain_tiles,
|
|
252
|
+
input_volume,
|
|
253
|
+
self.volume,
|
|
254
|
+
self.kernel,
|
|
255
|
+
ball_radius,
|
|
256
|
+
self.middle_z_idx,
|
|
257
|
+
self.overlap_threshold,
|
|
258
|
+
self.THRESHOLD_VALUE,
|
|
259
|
+
self.SOMA_CENTRE_VALUE,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@njit(cache=True)
|
|
264
|
+
def _cube_overlaps(
|
|
265
|
+
volume: np.ndarray,
|
|
266
|
+
x_start: int,
|
|
267
|
+
x_end: int,
|
|
268
|
+
y_start: int,
|
|
269
|
+
y_end: int,
|
|
270
|
+
overlap_threshold: float,
|
|
271
|
+
threshold_value: int,
|
|
272
|
+
kernel: np.ndarray,
|
|
273
|
+
) -> bool: # Highly optimised because most time critical
|
|
274
|
+
"""
|
|
275
|
+
For each pixel in cube in volume that is greater than THRESHOLD_VALUE, sum
|
|
276
|
+
up the corresponding pixels in *kernel*. If the total is less than
|
|
277
|
+
overlap_threshold, return False, otherwise return True.
|
|
278
|
+
|
|
279
|
+
Halfway through scanning the z-planes, if the total overlap is
|
|
280
|
+
less than 0.4 * overlap_threshold, this will return False early
|
|
281
|
+
without scanning the second half of the z-planes.
|
|
282
|
+
|
|
283
|
+
Parameters
|
|
284
|
+
----------
|
|
285
|
+
volume :
|
|
286
|
+
3D array.
|
|
287
|
+
x_start, x_end, y_start, y_end :
|
|
288
|
+
The start and end indices in volume that form the cube. End is
|
|
289
|
+
exclusive
|
|
290
|
+
overlap_threshold :
|
|
291
|
+
Threshold above which to return True.
|
|
292
|
+
threshold_value :
|
|
293
|
+
Value above which a pixel is marked as being part of a cell.
|
|
294
|
+
kernel :
|
|
295
|
+
3D array, with the same shape as *cube* in the volume.
|
|
296
|
+
"""
|
|
297
|
+
current_overlap_value = 0.0
|
|
298
|
+
|
|
299
|
+
middle = np.floor(volume.shape[0] / 2) + 1
|
|
300
|
+
halfway_overlap_thresh = (
|
|
301
|
+
overlap_threshold * 0.4
|
|
302
|
+
) # FIXME: do not hard code value
|
|
303
|
+
|
|
304
|
+
for z in range(volume.shape[0]):
|
|
305
|
+
# TODO: OPTIMISE: step from middle to outer boundaries to check
|
|
306
|
+
# more data first
|
|
307
|
+
#
|
|
308
|
+
# If halfway through the array, and the overlap value isn't more than
|
|
309
|
+
# 0.4 * the overlap threshold, return
|
|
310
|
+
if z == middle and current_overlap_value < halfway_overlap_thresh:
|
|
311
|
+
return False # DEBUG: optimisation attempt
|
|
312
|
+
|
|
313
|
+
for y in range(y_start, y_end):
|
|
314
|
+
for x in range(x_start, x_end):
|
|
315
|
+
# includes self.SOMA_CENTRE_VALUE
|
|
316
|
+
if volume[z, x, y] >= threshold_value:
|
|
317
|
+
# x/y must be shifted in kernel because we x/y is relative
|
|
318
|
+
# to the full volume, so shift it to relative to the cube
|
|
319
|
+
current_overlap_value += kernel[
|
|
320
|
+
x - x_start, y - y_start, z
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
return current_overlap_value > overlap_threshold
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@njit
|
|
327
|
+
def _is_tile_to_check(
|
|
328
|
+
x: int,
|
|
329
|
+
y: int,
|
|
330
|
+
middle_z: int,
|
|
331
|
+
tile_step_width: int,
|
|
332
|
+
tile_step_height: int,
|
|
333
|
+
inside_brain_tiles: np.ndarray,
|
|
334
|
+
) -> bool: # Highly optimised because most time critical
|
|
335
|
+
"""
|
|
336
|
+
Check if the tile containing pixel (x, y) is a tile that needs checking.
|
|
337
|
+
"""
|
|
338
|
+
x_in_mask = x // tile_step_width # TEST: test bounds (-1 range)
|
|
339
|
+
y_in_mask = y // tile_step_height # TEST: test bounds (-1 range)
|
|
340
|
+
return inside_brain_tiles[middle_z, x_in_mask, y_in_mask]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _walk_base(
|
|
344
|
+
max_height: int,
|
|
345
|
+
max_width: int,
|
|
346
|
+
tile_step_width: int,
|
|
347
|
+
tile_step_height: int,
|
|
348
|
+
inside_brain_tiles: np.ndarray,
|
|
349
|
+
input_volume: np.ndarray,
|
|
350
|
+
volume: np.ndarray,
|
|
351
|
+
kernel: np.ndarray,
|
|
352
|
+
ball_radius: int,
|
|
353
|
+
middle_z: int,
|
|
354
|
+
overlap_threshold: float,
|
|
355
|
+
threshold_value: int,
|
|
356
|
+
soma_centre_value: int,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Scan through *volume*, and mark pixels where there are enough surrounding
|
|
360
|
+
pixels with high enough intensity.
|
|
361
|
+
|
|
362
|
+
The surrounding area is defined by the *kernel*.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
max_height, max_width :
|
|
367
|
+
Maximum offsets for the ball filter.
|
|
368
|
+
inside_brain_tiles :
|
|
369
|
+
3d array containing information on whether a tile is
|
|
370
|
+
inside the brain or not. Tiles outside the brain are skipped.
|
|
371
|
+
input_volume :
|
|
372
|
+
3D array containing the plane-filtered data passed to the function
|
|
373
|
+
before walking. volume is edited in place, so this is the original
|
|
374
|
+
volume to prevent the changes for some cubes affective other cubes
|
|
375
|
+
during a single walk call.
|
|
376
|
+
volume :
|
|
377
|
+
3D array containing the plane-filtered data - edited in place.
|
|
378
|
+
kernel :
|
|
379
|
+
3D array
|
|
380
|
+
ball_radius :
|
|
381
|
+
Radius of the ball in the xy plane.
|
|
382
|
+
soma_centre_value :
|
|
383
|
+
Value that is used to mark pixels in *volume*.
|
|
384
|
+
|
|
385
|
+
Notes
|
|
386
|
+
-----
|
|
387
|
+
Warning: modifies volume in place!
|
|
388
|
+
"""
|
|
389
|
+
for y in prange(max_height):
|
|
390
|
+
for x in prange(max_width):
|
|
391
|
+
ball_centre_x = x + ball_radius
|
|
392
|
+
ball_centre_y = y + ball_radius
|
|
393
|
+
if _is_tile_to_check(
|
|
394
|
+
ball_centre_x,
|
|
395
|
+
ball_centre_y,
|
|
396
|
+
middle_z,
|
|
397
|
+
tile_step_width,
|
|
398
|
+
tile_step_height,
|
|
399
|
+
inside_brain_tiles,
|
|
400
|
+
):
|
|
401
|
+
if _cube_overlaps(
|
|
402
|
+
input_volume,
|
|
403
|
+
x,
|
|
404
|
+
x + kernel.shape[0],
|
|
405
|
+
y,
|
|
406
|
+
y + kernel.shape[1],
|
|
407
|
+
overlap_threshold,
|
|
408
|
+
threshold_value,
|
|
409
|
+
kernel,
|
|
410
|
+
):
|
|
411
|
+
volume[middle_z, ball_centre_x, ball_centre_y] = (
|
|
412
|
+
soma_centre_value
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
_walk_parallel = njit(parallel=True)(_walk_base)
|
|
417
|
+
_walk_single = njit(parallel=False)(_walk_base)
|