cellfinder 1.1.3__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.

Files changed (79) hide show
  1. {cellfinder-1.1.3 → cellfinder-1.2.0}/.github/workflows/test_and_deploy.yml +14 -8
  2. {cellfinder-1.1.3 → cellfinder-1.2.0}/.github/workflows/test_include_guard.yaml +6 -1
  3. {cellfinder-1.1.3 → cellfinder-1.2.0}/.gitignore +0 -5
  4. {cellfinder-1.1.3 → cellfinder-1.2.0}/PKG-INFO +9 -9
  5. {cellfinder-1.1.3 → cellfinder-1.2.0}/README.md +7 -7
  6. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/__init__.py +3 -0
  7. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/detect.py +12 -1
  8. cellfinder-1.2.0/cellfinder/core/detect/filters/volume/ball_filter.py +417 -0
  9. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/structure_detection.py +105 -41
  10. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/structure_splitting.py +1 -1
  11. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/volume_filter.py +48 -49
  12. cellfinder-1.2.0/cellfinder/core/download/cli.py +79 -0
  13. cellfinder-1.2.0/cellfinder/core/download/download.py +120 -0
  14. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/main.py +52 -42
  15. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/prep.py +11 -10
  16. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/source_files.py +5 -3
  17. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/train/train_yml.py +4 -6
  18. cellfinder-1.2.0/cellfinder/napari/detect/detect.py +428 -0
  19. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/detect/detect_containers.py +9 -1
  20. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/detect/thread_worker.py +14 -0
  21. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/train/train.py +2 -9
  22. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/train/train_containers.py +3 -3
  23. cellfinder-1.2.0/cellfinder/napari/utils.py +133 -0
  24. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/PKG-INFO +9 -9
  25. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/SOURCES.txt +0 -3
  26. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/requires.txt +1 -1
  27. {cellfinder-1.1.3 → cellfinder-1.2.0}/pyproject.toml +2 -2
  28. cellfinder-1.1.3/cellfinder/core/detect/filters/volume/ball_filter.py +0 -332
  29. cellfinder-1.1.3/cellfinder/core/download/cli.py +0 -72
  30. cellfinder-1.1.3/cellfinder/core/download/download.py +0 -132
  31. cellfinder-1.1.3/cellfinder/core/download/models.py +0 -49
  32. cellfinder-1.1.3/cellfinder/core/tools/IO.py +0 -48
  33. cellfinder-1.1.3/cellfinder/napari/detect/detect.py +0 -233
  34. cellfinder-1.1.3/cellfinder/napari/images/brainglobe.png +0 -0
  35. cellfinder-1.1.3/cellfinder/napari/utils.py +0 -92
  36. {cellfinder-1.1.3 → cellfinder-1.2.0}/.napari/config.yml +0 -0
  37. {cellfinder-1.1.3 → cellfinder-1.2.0}/CITATION.cff +0 -0
  38. {cellfinder-1.1.3 → cellfinder-1.2.0}/LICENSE +0 -0
  39. {cellfinder-1.1.3 → cellfinder-1.2.0}/MANIFEST.in +1 -1
  40. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/cli_migration_warning.py +0 -0
  41. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/__init__.py +0 -0
  42. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/__init__.py +0 -0
  43. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/augment.py +0 -0
  44. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/classify.py +0 -0
  45. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/cube_generator.py +0 -0
  46. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/resnet.py +0 -0
  47. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/classify/tools.py +0 -0
  48. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/config/__init__.py +0 -0
  49. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/config/cellfinder.conf +0 -0
  50. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/__init__.py +0 -0
  51. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/__init__.py +0 -0
  52. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/__init__.py +0 -0
  53. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/classical_filter.py +0 -0
  54. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/plane_filter.py +0 -0
  55. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/plane/tile_walker.py +0 -0
  56. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/setup_filters.py +0 -0
  57. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/detect/filters/volume/__init__.py +0 -0
  58. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/download/__init__.py +0 -0
  59. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/__init__.py +0 -0
  60. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/array_operations.py +0 -0
  61. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/geometry.py +0 -0
  62. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/image_processing.py +0 -0
  63. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/system.py +0 -0
  64. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/tf.py +0 -0
  65. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/tiff.py +0 -0
  66. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/tools/tools.py +0 -0
  67. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/train/__init__.py +0 -0
  68. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/core/types.py +0 -0
  69. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/__init__.py +0 -0
  70. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/curation.py +0 -0
  71. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/detect/__init__.py +0 -0
  72. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/input_container.py +0 -0
  73. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/napari.yaml +0 -0
  74. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/sample_data.py +0 -0
  75. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder/napari/train/__init__.py +0 -0
  76. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/dependency_links.txt +0 -0
  77. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/entry_points.txt +0 -0
  78. {cellfinder-1.1.3 → cellfinder-1.2.0}/cellfinder.egg-info/top_level.txt +0 -0
  79. {cellfinder-1.1.3 → 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
- - os: macos-latest
48
- python-version: "3.10"
49
- - os: windows-latest
50
- python-version: "3.10"
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('~/.cellfinder/**') }}
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('~/.cellfinder/**') }}
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('~/.cellfinder/**') }}
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
@@ -130,11 +130,6 @@ mprofile*.dat
130
130
 
131
131
  *.DS_Store
132
132
 
133
- # asv
134
- .asv
135
- benchmarks/results
136
- benchmarks/html
137
- benchmarks/env
138
133
 
139
134
  # OS
140
135
  .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cellfinder
3
- Version: 1.1.3
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.4.2
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
- [![Python Version](https://img.shields.io/pypi/pyversions/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
58
- [![PyPI](https://img.shields.io/pypi/v/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
59
- [![Downloads](https://pepy.tech/badge/cellfinder-core)](https://pepy.tech/project/cellfinder)
60
- [![Wheel](https://img.shields.io/pypi/wheel/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
61
- [![Development Status](https://img.shields.io/pypi/status/cellfinder-core.svg)](https://github.com/brainglobe/cellfinder)
57
+ [![Python Version](https://img.shields.io/pypi/pyversions/cellfinder.svg)](https://pypi.org/project/cellfinder)
58
+ [![PyPI](https://img.shields.io/pypi/v/cellfinder.svg)](https://pypi.org/project/cellfinder)
59
+ [![Downloads](https://pepy.tech/badge/cellfinder)](https://pepy.tech/project/cellfinder)
60
+ [![Wheel](https://img.shields.io/pypi/wheel/cellfinder.svg)](https://pypi.org/project/cellfinder)
61
+ [![Development Status](https://img.shields.io/pypi/status/cellfinder.svg)](https://github.com/brainglobe/cellfinder)
62
62
  [![Tests](https://img.shields.io/github/actions/workflow/status/brainglobe/cellfinder/test_and_deploy.yml?branch=main)](https://github.com/brainglobe/cellfinder/actions)
63
- [![codecov](https://codecov.io/gh/brainglobe/cellfinder-core/branch/main/graph/badge.svg?token=nx1lhNI7ox)](https://codecov.io/gh/brainglobe/cellfinder)
63
+ [![codecov](https://codecov.io/gh/brainglobe/cellfinder/branch/main/graph/badge.svg?token=nx1lhNI7ox)](https://codecov.io/gh/brainglobe/cellfinder)
64
64
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
65
65
  [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
66
66
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
67
- [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://brainglobe.info/developers/index.html)
67
+ [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://brainglobe.info/community/developers/index.html)
68
68
  [![Twitter](https://img.shields.io/twitter/follow/brain_globe?style=social)](https://twitter.com/brain_globe)
69
69
 
70
70
  # cellfinder
@@ -1,14 +1,14 @@
1
- [![Python Version](https://img.shields.io/pypi/pyversions/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
2
- [![PyPI](https://img.shields.io/pypi/v/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
3
- [![Downloads](https://pepy.tech/badge/cellfinder-core)](https://pepy.tech/project/cellfinder)
4
- [![Wheel](https://img.shields.io/pypi/wheel/cellfinder-core.svg)](https://pypi.org/project/cellfinder)
5
- [![Development Status](https://img.shields.io/pypi/status/cellfinder-core.svg)](https://github.com/brainglobe/cellfinder)
1
+ [![Python Version](https://img.shields.io/pypi/pyversions/cellfinder.svg)](https://pypi.org/project/cellfinder)
2
+ [![PyPI](https://img.shields.io/pypi/v/cellfinder.svg)](https://pypi.org/project/cellfinder)
3
+ [![Downloads](https://pepy.tech/badge/cellfinder)](https://pepy.tech/project/cellfinder)
4
+ [![Wheel](https://img.shields.io/pypi/wheel/cellfinder.svg)](https://pypi.org/project/cellfinder)
5
+ [![Development Status](https://img.shields.io/pypi/status/cellfinder.svg)](https://github.com/brainglobe/cellfinder)
6
6
  [![Tests](https://img.shields.io/github/actions/workflow/status/brainglobe/cellfinder/test_and_deploy.yml?branch=main)](https://github.com/brainglobe/cellfinder/actions)
7
- [![codecov](https://codecov.io/gh/brainglobe/cellfinder-core/branch/main/graph/badge.svg?token=nx1lhNI7ox)](https://codecov.io/gh/brainglobe/cellfinder)
7
+ [![codecov](https://codecov.io/gh/brainglobe/cellfinder/branch/main/graph/badge.svg?token=nx1lhNI7ox)](https://codecov.io/gh/brainglobe/cellfinder)
8
8
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
9
9
  [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
10
10
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
11
- [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://brainglobe.info/developers/index.html)
11
+ [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://brainglobe.info/community/developers/index.html)
12
12
  [![Twitter](https://img.shields.io/twitter/follow/brain_globe?style=social)](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
- cells = mp_3d_filter.process(async_results, locks, callback=callback)
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)