cellfinder 1.8.0__py3-none-any.whl → 1.9.0__py3-none-any.whl

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.
@@ -54,6 +54,8 @@ def main(
54
54
  split_ball_z_size: float = 15,
55
55
  split_ball_overlap_fraction: float = 0.8,
56
56
  n_splitting_iter: int = 10,
57
+ n_sds_above_mean_tiled_thresh: float = 10,
58
+ tiled_thresh_tile_size: float | None = None,
57
59
  *,
58
60
  callback: Optional[Callable[[int], None]] = None,
59
61
  ) -> List[Cell]:
@@ -96,8 +98,8 @@ def main(
96
98
  Gaussian filter width (as a fraction of soma diameter) used during
97
99
  2d in-plane Laplacian of Gaussian filtering.
98
100
  n_sds_above_mean_thresh : float
99
- Intensity threshold (the number of standard deviations above
100
- the mean) of the filtered 2d planes used to mark pixels as
101
+ Per-plane intensity threshold (the number of standard deviations
102
+ above the mean) of the filtered 2d planes used to mark pixels as
101
103
  foreground or background.
102
104
  outlier_keep : bool, optional
103
105
  Whether to keep outliers during detection. Defaults to False.
@@ -136,6 +138,20 @@ def main(
136
138
  The number of iterations to run the 3d filtering on a cluster. Each
137
139
  iteration reduces the cluster size by the voxels not retained in
138
140
  the previous iteration.
141
+ n_sds_above_mean_tiled_thresh : float
142
+ Per-plane, per-tile intensity threshold (the number of standard
143
+ deviations above the mean) for the filtered 2d planes used to mark
144
+ pixels as foreground or background. When used, (tile size is not zero)
145
+ a pixel is marked as foreground if its intensity is above both the
146
+ per-plane and per-tile threshold. I.e. it's above the set number of
147
+ standard deviations of the per-plane average and of the per-plane
148
+ per-tile average for the tile that contains it.
149
+ tiled_thresh_tile_size : float
150
+ The tile size used to tile the x, y plane to calculate the local
151
+ average intensity for the tiled threshold. The value is multiplied
152
+ by soma diameter (i.e. 1 means one soma diameter). If zero or None, the
153
+ tiled threshold is disabled and only the per-plane threshold is used.
154
+ Tiling is done with 50% overlap when striding.
139
155
  callback : Callable[int], optional
140
156
  A callback function that is called every time a plane has finished
141
157
  being processed. Called with the plane number that has finished.
@@ -193,6 +209,8 @@ def main(
193
209
  ball_overlap_fraction=ball_overlap_fraction,
194
210
  log_sigma_size=log_sigma_size,
195
211
  n_sds_above_mean_thresh=n_sds_above_mean_thresh,
212
+ n_sds_above_mean_tiled_thresh=n_sds_above_mean_tiled_thresh,
213
+ tiled_thresh_tile_size=tiled_thresh_tile_size,
196
214
  outlier_keep=outlier_keep,
197
215
  artifact_keep=artifact_keep,
198
216
  save_planes=save_planes,
@@ -228,7 +246,9 @@ def main(
228
246
  plane_shape=settings.plane_shape,
229
247
  clipping_value=settings.clipping_value,
230
248
  threshold_value=settings.threshold_value,
231
- n_sds_above_mean_thresh=n_sds_above_mean_thresh,
249
+ n_sds_above_mean_thresh=settings.n_sds_above_mean_thresh,
250
+ n_sds_above_mean_tiled_thresh=settings.n_sds_above_mean_tiled_thresh,
251
+ tiled_thresh_tile_size=settings.tiled_thresh_tile_size,
232
252
  log_sigma_size=log_sigma_size,
233
253
  soma_diameter=settings.soma_diameter,
234
254
  torch_device=torch_device,
@@ -1,13 +1,12 @@
1
- from dataclasses import dataclass, field
2
1
  from typing import Tuple
3
2
 
4
3
  import torch
4
+ import torch.nn.functional as F
5
5
 
6
6
  from cellfinder.core.detect.filters.plane.classical_filter import PeakEnhancer
7
7
  from cellfinder.core.detect.filters.plane.tile_walker import TileWalker
8
8
 
9
9
 
10
- @dataclass
11
10
  class TileProcessor:
12
11
  """
13
12
  Processor that filters each plane to highlight the peaks and also
@@ -63,12 +62,22 @@ class TileProcessor:
63
62
  # voxels who are this many std above mean or more are set to
64
63
  # threshold_value
65
64
  n_sds_above_mean_thresh: float
65
+ # If used, voxels who are this many or more std above mean of the
66
+ # containing tile as well as above n_sds_above_mean_thresh for the plane
67
+ # average are set to threshold_value.
68
+ n_sds_above_mean_tiled_thresh: float
69
+ # the tile size, in pixels, that will be used to tile the x, y plane when
70
+ # we calculate the per-tile mean / std for use with
71
+ # n_sds_above_mean_tiled_thresh. We use 50% overlap when tiling.
72
+ local_threshold_tile_size_px: int = 0
73
+ # the torch device name
74
+ torch_device: str = ""
66
75
 
67
76
  # filter that finds the peaks in the planes
68
- peak_enhancer: PeakEnhancer = field(init=False)
77
+ peak_enhancer: PeakEnhancer = None
69
78
  # generates tiles of the planes, with each tile marked as being inside
70
79
  # or outside the brain based on brightness
71
- tile_walker: TileWalker = field(init=False)
80
+ tile_walker: TileWalker = None
72
81
 
73
82
  def __init__(
74
83
  self,
@@ -76,6 +85,8 @@ class TileProcessor:
76
85
  clipping_value: int,
77
86
  threshold_value: int,
78
87
  n_sds_above_mean_thresh: float,
88
+ n_sds_above_mean_tiled_thresh: float,
89
+ tiled_thresh_tile_size: float | None,
79
90
  log_sigma_size: float,
80
91
  soma_diameter: int,
81
92
  torch_device: str,
@@ -85,6 +96,12 @@ class TileProcessor:
85
96
  self.clipping_value = clipping_value
86
97
  self.threshold_value = threshold_value
87
98
  self.n_sds_above_mean_thresh = n_sds_above_mean_thresh
99
+ self.n_sds_above_mean_tiled_thresh = n_sds_above_mean_tiled_thresh
100
+ if tiled_thresh_tile_size:
101
+ self.local_threshold_tile_size_px = int(
102
+ round(soma_diameter * tiled_thresh_tile_size)
103
+ )
104
+ self.torch_device = torch_device
88
105
 
89
106
  laplace_gaussian_sigma = log_sigma_size * soma_diameter
90
107
  self.peak_enhancer = PeakEnhancer(
@@ -131,7 +148,10 @@ class TileProcessor:
131
148
  planes,
132
149
  enhanced_planes,
133
150
  self.n_sds_above_mean_thresh,
151
+ self.n_sds_above_mean_tiled_thresh,
152
+ self.local_threshold_tile_size_px,
134
153
  self.threshold_value,
154
+ self.torch_device,
135
155
  )
136
156
 
137
157
  return planes, inside_brain_tiles
@@ -145,21 +165,98 @@ def _threshold_planes(
145
165
  planes: torch.Tensor,
146
166
  enhanced_planes: torch.Tensor,
147
167
  n_sds_above_mean_thresh: float,
168
+ n_sds_above_mean_tiled_thresh: float,
169
+ local_threshold_tile_size_px: int,
148
170
  threshold_value: int,
171
+ torch_device: str,
149
172
  ) -> None:
150
173
  """
151
174
  Sets each plane (in-place) to threshold_value, where the corresponding
152
175
  enhanced_plane > mean + n_sds_above_mean_thresh*std. Each plane will be
153
176
  set to zero elsewhere.
154
177
  """
155
- planes_1d = enhanced_planes.view(enhanced_planes.shape[0], -1)
178
+ z, y, x = enhanced_planes.shape
156
179
 
180
+ # ---- get per-plane global threshold ----
181
+ planes_1d = enhanced_planes.view(z, -1)
157
182
  # add back last dim
158
- avg = torch.mean(planes_1d, dim=1, keepdim=True).unsqueeze(2)
159
- sd = torch.std(planes_1d, dim=1, keepdim=True).unsqueeze(2)
160
- threshold = avg + n_sds_above_mean_thresh * sd
183
+ std, mean = torch.std_mean(planes_1d, dim=1, keepdim=True)
184
+ threshold = mean.unsqueeze(2) + n_sds_above_mean_thresh * std.unsqueeze(2)
185
+ above_global = enhanced_planes > threshold
186
+
187
+ # ---- calculate the local tiled threshold ----
188
+ # we do 50% overlap so there's no jumps at boundaries
189
+ stride = local_threshold_tile_size_px // 2
190
+ # make tile even for ease of computation
191
+ tile_size = stride * 2
192
+ # Due to 50% overlap, to get tiles we move the tile by half tile (stride).
193
+ # Total moves will be y // stride - 2 (we start already with mask on first
194
+ # tile). So add back 1 for the first tile. Partial tiles are dropped
195
+ n_y_tiles = max(y // stride - 1, 1) if stride else 1
196
+ n_x_tiles = max(x // stride - 1, 1) if stride else 1
197
+ do_tile_y = n_y_tiles >= 2
198
+ do_tile_x = n_x_tiles >= 2
199
+ # we want at least one axis to have at least two tiles
200
+ if local_threshold_tile_size_px >= 2 and (do_tile_y or do_tile_x):
201
+ # num edge pixels dropped b/c moving by stride would move tile off edge
202
+ y_rem = y % stride
203
+ x_rem = x % stride
204
+ enhanced_planes_raw = enhanced_planes
205
+ if do_tile_y:
206
+ enhanced_planes = enhanced_planes[:, y_rem // 2 :, :]
207
+ if do_tile_x:
208
+ enhanced_planes = enhanced_planes[:, :, x_rem // 2 :]
209
+
210
+ # add empty channel dim after z "batch" dim -> zcyx
211
+ enhanced_planes = enhanced_planes.unsqueeze(1)
212
+ # unfold makes it 3 dim, z, M, L. L is number of tiles, M is tile area
213
+ unfolded = F.unfold(
214
+ enhanced_planes,
215
+ (tile_size if do_tile_y else y, tile_size if do_tile_x else x),
216
+ stride=stride,
217
+ )
218
+ # average the tile areas, for each tile
219
+ std, mean = torch.std_mean(unfolded, dim=1, keepdim=True)
220
+ threshold = mean + n_sds_above_mean_tiled_thresh * std
221
+
222
+ # reshape it back into Y by X tiles, instead of YX being one dim
223
+ threshold = threshold.reshape((z, n_y_tiles, n_x_tiles))
224
+
225
+ # we need total size of n_tiles * stride + stride + rem for the
226
+ # original size. So we add 2 strides and then chop off the excess above
227
+ # rem. We center it because of 50% overlap, the first tile is actually
228
+ # centered in between the first two strides
229
+ offsets = [(0, y), (0, x)]
230
+ for dim, do_tile, n_tiles, n, rem in [
231
+ (1, do_tile_y, n_y_tiles, y, y_rem),
232
+ (2, do_tile_x, n_x_tiles, x, x_rem),
233
+ ]:
234
+ if do_tile:
235
+ repeats = (
236
+ torch.ones(n_tiles, dtype=torch.int, device=torch_device)
237
+ * stride
238
+ )
239
+ # add total of 2 additional strides
240
+ repeats[0] = 2 * stride
241
+ repeats[-1] = 2 * stride
242
+ output_size = (n_tiles + 2) * stride
243
+
244
+ threshold = threshold.repeat_interleave(
245
+ repeats, dim=dim, output_size=output_size
246
+ )
247
+ # drop the excess we gained from padding rem to whole stride
248
+ offset = (stride - rem) // 2
249
+ offsets[dim - 1] = offset, n + offset
250
+
251
+ # can't use slice(...) objects in jit code so use actual indices
252
+ (a, b), (c, d) = offsets
253
+ threshold = threshold[:, a:b, c:d]
254
+
255
+ above_local = enhanced_planes_raw > threshold
256
+ above = torch.logical_and(above_global, above_local)
257
+ else:
258
+ above = above_global
161
259
 
162
- above = enhanced_planes > threshold
163
260
  planes[above] = threshold_value
164
261
  # subsequent steps only care about the values that are set to threshold or
165
262
  # above in planes. We set values in *planes* to threshold based on the
@@ -133,11 +133,31 @@ class DetectionSettings:
133
133
 
134
134
  n_sds_above_mean_thresh: float = 10
135
135
  """
136
- Intensity threshold (the number of standard deviations above
137
- the mean) of the filtered 2d planes used to mark pixels as
136
+ Per-plane intensity threshold (the number of standard deviations
137
+ above the mean) of the 2d filtered planes used to mark pixels as
138
138
  foreground or background.
139
139
  """
140
140
 
141
+ n_sds_above_mean_tiled_thresh: float = 10
142
+ """
143
+ Per-plane, per-tile intensity threshold (the number of standard deviations
144
+ above the mean) for the filtered 2d planes used to mark pixels as
145
+ foreground or background. When used, (tile size is not zero) a pixel is
146
+ marked as foreground if its intensity is above both the per-plane and
147
+ per-tile threshold. I.e. it's above the set number of standard deviations
148
+ of the per-plane average and of the per-plane per-tile average for the tile
149
+ that contains it.
150
+ """
151
+
152
+ tiled_thresh_tile_size: float | None = None
153
+ """
154
+ The tile size used to tile the x, y plane to calculate the local average
155
+ intensity for the tiled threshold. The value is multiplied by soma
156
+ diameter (i.e. 1 means one soma diameter). If zero or None, the tiled
157
+ threshold is disabled and only the per-plane threshold is used. Tiling is
158
+ done with 50% overlap when striding.
159
+ """
160
+
141
161
  outlier_keep: bool = False
142
162
  """Whether to keep outlier structures during detection."""
143
163
 
@@ -222,6 +222,11 @@ class CellDetector:
222
222
  neighbour_ids[2] = previous_plane[y, x]
223
223
 
224
224
  if is_new_structure(neighbour_ids):
225
+ if self.next_structure_id > self.soma_centre_value:
226
+ raise ValueError(
227
+ "label overflow: number of connected "
228
+ "components exceeds label capacity"
229
+ )
225
230
  neighbour_ids[0] = self.next_structure_id
226
231
  self.next_structure_id += 1
227
232
  struct_id = self.add(x, y, self.z, neighbour_ids)
@@ -66,8 +66,7 @@ def coords_to_volume(
66
66
  relative_zs = np.array((zs - z_min + ball_radius), dtype=np.int64)
67
67
 
68
68
  # set each point as the center with a value of threshold
69
- for rel_x, rel_y, rel_z in zip(relative_xs, relative_ys, relative_zs):
70
- volume[rel_x, rel_y, rel_z] = threshold_value
69
+ volume[relative_xs, relative_ys, relative_zs] = threshold_value
71
70
 
72
71
  volume = volume.swapaxes(0, 2)
73
72
  return torch.from_numpy(volume)
cellfinder/core/main.py CHANGED
@@ -42,6 +42,8 @@ def main(
42
42
  split_ball_z_size: float = 15,
43
43
  split_ball_overlap_fraction: float = 0.8,
44
44
  n_splitting_iter: int = 10,
45
+ n_sds_above_mean_tiled_thresh: float = 10,
46
+ tiled_thresh_tile_size: float | None = None,
45
47
  *,
46
48
  detect_callback: Optional[Callable[[int], None]] = None,
47
49
  classify_callback: Optional[Callable[[int], None]] = None,
@@ -94,8 +96,8 @@ def main(
94
96
  Gaussian filter width (as a fraction of soma diameter) used during
95
97
  2d in-plane Laplacian of Gaussian filtering.
96
98
  n_sds_above_mean_thresh : float
97
- Intensity threshold (the number of standard deviations above
98
- the mean) of the filtered 2d planes used to mark pixels as
99
+ Per-plane intensity threshold (the number of standard deviations
100
+ above the mean) of the filtered 2d planes used to mark pixels as
99
101
  foreground or background.
100
102
  soma_spread_factor : float
101
103
  Cell spread factor for determining the largest cell volume before
@@ -155,6 +157,20 @@ def main(
155
157
  The number of iterations to run the 3d filtering on a cluster. Each
156
158
  iteration reduces the cluster size by the voxels not retained in
157
159
  the previous iteration.
160
+ n_sds_above_mean_tiled_thresh : float
161
+ Per-plane, per-tile intensity threshold (the number of standard
162
+ deviations above the mean) for the filtered 2d planes used to mark
163
+ pixels as foreground or background. When used, (tile size is not zero)
164
+ a pixel is marked as foreground if its intensity is above both the
165
+ per-plane and per-tile threshold. I.e. it's above the set number of
166
+ standard deviations of the per-plane average and of the per-plane
167
+ per-tile average for the tile that contains it.
168
+ tiled_thresh_tile_size : float
169
+ The tile size used to tile the x, y plane to calculate the local
170
+ average intensity for the tiled threshold. The value is multiplied
171
+ by soma diameter (i.e. 1 means one soma diameter). If zero or None, the
172
+ tiled threshold is disabled and only the per-plane threshold is used.
173
+ Tiling is done with 50% overlap when striding.
158
174
  detect_callback : Callable[int], optional
159
175
  Called every time a plane has finished being processed during the
160
176
  detection stage. Called with the plane number that has finished.
@@ -172,19 +188,21 @@ def main(
172
188
  logger.info("Detecting cell candidates")
173
189
 
174
190
  points = detect.main(
175
- signal_array,
176
- start_plane,
177
- end_plane,
178
- voxel_sizes,
179
- soma_diameter,
180
- max_cluster_size,
181
- ball_xy_size,
182
- ball_z_size,
183
- ball_overlap_fraction,
184
- soma_spread_factor,
185
- n_free_cpus,
186
- log_sigma_size,
187
- n_sds_above_mean_thresh,
191
+ signal_array=signal_array,
192
+ start_plane=start_plane,
193
+ end_plane=end_plane,
194
+ voxel_sizes=voxel_sizes,
195
+ soma_diameter=soma_diameter,
196
+ max_cluster_size=max_cluster_size,
197
+ ball_xy_size=ball_xy_size,
198
+ ball_z_size=ball_z_size,
199
+ ball_overlap_fraction=ball_overlap_fraction,
200
+ soma_spread_factor=soma_spread_factor,
201
+ n_free_cpus=n_free_cpus,
202
+ log_sigma_size=log_sigma_size,
203
+ n_sds_above_mean_thresh=n_sds_above_mean_thresh,
204
+ n_sds_above_mean_tiled_thresh=n_sds_above_mean_tiled_thresh,
205
+ tiled_thresh_tile_size=tiled_thresh_tile_size,
188
206
  batch_size=detection_batch_size,
189
207
  torch_device=torch_device,
190
208
  pin_memory=pin_memory,
@@ -261,7 +261,7 @@ def random_bool(likelihood: Optional[float] = None) -> bool:
261
261
  if likelihood is None:
262
262
  return bool(getrandbits(1))
263
263
  else:
264
- if uniform(0, 1) > likelihood:
264
+ if uniform(0, 1) < likelihood:
265
265
  return True
266
266
  else:
267
267
  return False
@@ -29,7 +29,7 @@ from fancylog import fancylog
29
29
  from keras.callbacks import CSVLogger, ModelCheckpoint, TensorBoard
30
30
  from sklearn.model_selection import train_test_split
31
31
 
32
- import cellfinder.core as program_for_log
32
+ import cellfinder.core as package_for_log
33
33
  from cellfinder.core import logger
34
34
  from cellfinder.core.classify.cube_generator import CubeGeneratorFromDisk
35
35
  from cellfinder.core.classify.resnet import layer_type
@@ -269,7 +269,7 @@ def cli():
269
269
 
270
270
  fancylog.start_logging(
271
271
  args.output_dir,
272
- program_for_log,
272
+ package=package_for_log,
273
273
  variables=[args],
274
274
  log_header="CELLFINDER TRAINING LOG",
275
275
  )
@@ -11,7 +11,7 @@ from brainglobe_utils.IO.yaml import save_yaml
11
11
  from magicgui.widgets import ProgressBar
12
12
  from napari.qt.threading import thread_worker
13
13
  from napari.utils.notifications import show_info
14
- from qt_niu.dialog import display_warning
14
+ from qt_niu.dialog import display_info, display_warning
15
15
  from qt_niu.interaction import add_button, add_combobox
16
16
  from qtpy import QtCore
17
17
  from qtpy.QtWidgets import (
@@ -495,26 +495,61 @@ class CurationWidget(QWidget):
495
495
  return False
496
496
 
497
497
  def check_training_data_exists(self) -> bool:
498
- if not (
499
- self.training_data_cell_layer or self.training_data_non_cell_layer
500
- ):
501
- show_info(
502
- "No training data layers have been added. "
503
- "Please add a layer and annotate some points.",
498
+ """
499
+ Checks that
500
+ - both training data layers exists
501
+ - at least one of them is not empty.
502
+
503
+ Will display a popup dialog and return False if these conditions
504
+ are not both fulfilled.
505
+
506
+ Will show a notification if only one layer is non-empty, but this
507
+ is considered valid.
508
+
509
+ Returns
510
+ -------
511
+ bool
512
+ True if both training layers exists and at least one
513
+ of them contains some data. False otherwise.
514
+ """
515
+ both_training_layers_exist = (
516
+ self.training_data_cell_layer and self.training_data_non_cell_layer
517
+ )
518
+
519
+ if not both_training_layers_exist:
520
+ display_info(
521
+ self,
522
+ "No training data layers have been added.",
523
+ "Please add layers for both cells and non-cells,"
524
+ "and annotate some points.",
504
525
  )
505
526
  return False
506
- else:
507
- if (
508
- len(self.training_data_cell_layer.data) > 0
509
- or len(self.training_data_non_cell_layer.data) > 0
510
- ):
511
- return True
512
- else:
527
+
528
+ at_least_one_training_layer_contains_data = (
529
+ len(self.training_data_cell_layer.data) > 0
530
+ or len(self.training_data_non_cell_layer.data) > 0
531
+ )
532
+
533
+ both_training_layers_contain_data = (
534
+ len(self.training_data_cell_layer.data) > 0
535
+ and len(self.training_data_non_cell_layer.data) > 0
536
+ )
537
+
538
+ if at_least_one_training_layer_contains_data:
539
+ if not both_training_layers_contain_data:
513
540
  show_info(
514
- "No training data points have been added. "
515
- "Please annotate some points.",
541
+ "One of the training layers is empty. This is OK, but"
542
+ "For optimal (re-)training ensure you have roughly equal "
543
+ "number of points in each of your training points layers."
516
544
  )
517
- return False
545
+ return True
546
+ else:
547
+ display_info(
548
+ self,
549
+ "No training data points have been added.",
550
+ "Please annotate points in the training data layers.",
551
+ )
552
+ return False
518
553
 
519
554
  def get_output_directory(self):
520
555
  """
@@ -148,7 +148,7 @@ def restore_options_defaults(widget: FunctionGui) -> None:
148
148
 
149
149
 
150
150
  def get_results_callback(
151
- skip_classification: bool, viewer: napari.Viewer
151
+ skip_classification: bool, viewer: napari.Viewer, scale
152
152
  ) -> Callable:
153
153
  """
154
154
  Returns the callback that is connected to output of the pipeline.
@@ -162,6 +162,7 @@ def get_results_callback(
162
162
  viewer=viewer,
163
163
  name="Cell candidates",
164
164
  cell_type=Cell.UNKNOWN,
165
+ scale=scale,
165
166
  )
166
167
 
167
168
  else:
@@ -172,6 +173,7 @@ def get_results_callback(
172
173
  viewer=viewer,
173
174
  unknown_name="Rejected",
174
175
  cell_name="Detected",
176
+ scale=scale,
175
177
  )
176
178
 
177
179
  return done_func
@@ -246,6 +248,8 @@ def detect_widget() -> FunctionGui:
246
248
  soma_diameter: float,
247
249
  log_sigma_size: float,
248
250
  n_sds_above_mean_thresh: float,
251
+ n_sds_above_mean_tiled_thresh: float,
252
+ tiled_thresh_tile_size: float,
249
253
  ball_xy_size: float,
250
254
  ball_z_size: float,
251
255
  ball_overlap_fraction: float,
@@ -288,9 +292,23 @@ def detect_widget() -> FunctionGui:
288
292
  Gaussian filter width (as a fraction of soma diameter) used during
289
293
  2d in-plane Laplacian of Gaussian filtering
290
294
  n_sds_above_mean_thresh : float
291
- Intensity threshold (the number of standard deviations above
292
- the mean) of the filtered 2d planes used to mark pixels as
295
+ Per-plane intensity threshold (the number of standard deviations
296
+ above the mean) of the filtered 2d planes used to mark pixels as
293
297
  foreground or background
298
+ n_sds_above_mean_tiled_thresh : float
299
+ Per-plane, per-tile intensity threshold (the number of standard
300
+ deviations above the mean) for the filtered 2d planes used to mark
301
+ pixels as foreground or background. When used, (tile size is not
302
+ zero) a pixel is marked as foreground if its intensity is above
303
+ both the per-plane and per-tile threshold. I.e. it's above the set
304
+ number of standard deviations of the per-plane average and of the
305
+ per-plane per-tile average for the tile that contains it.
306
+ tiled_thresh_tile_size : float
307
+ The tile size used to tile the x, y plane to calculate the local
308
+ average intensity for the tiled threshold. The value is multiplied
309
+ by soma diameter (i.e. 1 means one soma diameter). If zero, the
310
+ tiled threshold is disabled and only the per-plane threshold is
311
+ used. Tiling is done with 50% overlap when striding.
294
312
  ball_xy_size : float
295
313
  3d filter's in-plane (xy) filter ball size (microns)
296
314
  ball_z_size : float
@@ -396,6 +414,8 @@ def detect_widget() -> FunctionGui:
396
414
  ball_overlap_fraction,
397
415
  log_sigma_size,
398
416
  n_sds_above_mean_thresh,
417
+ n_sds_above_mean_tiled_thresh,
418
+ tiled_thresh_tile_size,
399
419
  soma_spread_factor,
400
420
  max_cluster_size,
401
421
  detection_batch_size,
@@ -435,7 +455,11 @@ def detect_widget() -> FunctionGui:
435
455
  )
436
456
 
437
457
  worker.returned.connect(
438
- get_results_callback(skip_classification, options["viewer"])
458
+ get_results_callback(
459
+ skip_classification,
460
+ options["viewer"],
461
+ options["signal_image"].scale,
462
+ )
439
463
  )
440
464
  # Make sure if the worker emits an error, it is propagated to this
441
465
  # thread
@@ -69,6 +69,8 @@ class DetectionInputs(InputContainer):
69
69
  ball_overlap_fraction: float = 0.6
70
70
  log_sigma_size: float = 0.2
71
71
  n_sds_above_mean_thresh: float = 10
72
+ n_sds_above_mean_tiled_thresh: float = 10
73
+ tiled_thresh_tile_size: float = 0
72
74
  soma_spread_factor: float = 1.4
73
75
  max_cluster_size: float = 100000
74
76
  detection_batch_size: int = 1
@@ -95,7 +97,14 @@ class DetectionInputs(InputContainer):
95
97
  "log_sigma_size", custom_label="Filter width"
96
98
  ),
97
99
  n_sds_above_mean_thresh=cls._custom_widget(
98
- "n_sds_above_mean_thresh", custom_label="Threshold"
100
+ "n_sds_above_mean_thresh", custom_label="Plane threshold"
101
+ ),
102
+ n_sds_above_mean_tiled_thresh=cls._custom_widget(
103
+ "n_sds_above_mean_tiled_thresh", custom_label="Tiled threshold"
104
+ ),
105
+ tiled_thresh_tile_size=cls._custom_widget(
106
+ "tiled_thresh_tile_size",
107
+ custom_label="Thresholding tile size",
99
108
  ),
100
109
  soma_spread_factor=cls._custom_widget(
101
110
  "soma_spread_factor", custom_label="Split cell spread"
@@ -56,28 +56,35 @@ class Worker(WorkerBase):
56
56
  self.update_progress_bar.connect(update_progress_bar)
57
57
 
58
58
  def work(self) -> list:
59
- self.update_progress_bar.emit("Setting up detection...", 1, 0)
59
+ if not self.detection_inputs.skip_detection:
60
+ self.update_progress_bar.emit("Setting up detection...", 1, 0)
60
61
 
61
62
  def detect_callback(plane: int) -> None:
62
- self.update_progress_bar.emit(
63
- "Detecting cells",
64
- self.data_inputs.nplanes,
65
- plane + 1,
66
- )
63
+ if not self.detection_inputs.skip_detection:
64
+ self.update_progress_bar.emit(
65
+ "Detecting cells",
66
+ self.data_inputs.nplanes,
67
+ plane + 1,
68
+ )
67
69
 
68
70
  def detect_finished_callback(points: list) -> None:
69
71
  self.npoints_detected = len(points)
70
- self.update_progress_bar.emit("Setting up classification...", 1, 0)
72
+ if not self.classification_inputs.skip_classification:
73
+ self.update_progress_bar.emit(
74
+ "Setting up classification...", 1, 0
75
+ )
71
76
 
72
77
  def classify_callback(batch: int) -> None:
73
- self.update_progress_bar.emit(
74
- "Classifying cells",
75
- # Default cellfinder-core batch size is 64. This seems to give
76
- # a slight underestimate of the number of batches though, so
77
- # allow for batch number to go over this
78
- max(self.npoints_detected // 64 + 1, batch + 1),
79
- batch + 1,
80
- )
78
+ if not self.classification_inputs.skip_classification:
79
+ self.update_progress_bar.emit(
80
+ "Classifying cells",
81
+ # Default cellfinder-core batch size is 64.
82
+ # This seems to give a slight
83
+ # underestimate of the number of batches though,
84
+ # so allow for batch number to go over this
85
+ max(self.npoints_detected // 64 + 1, batch + 1),
86
+ batch + 1,
87
+ )
81
88
 
82
89
  result = cellfinder_run(
83
90
  **self.data_inputs.as_core_arguments(),
@@ -88,5 +95,8 @@ class Worker(WorkerBase):
88
95
  classify_callback=classify_callback,
89
96
  detect_finished_callback=detect_finished_callback,
90
97
  )
91
- self.update_progress_bar.emit("Finished classification", 1, 1)
98
+ if not self.classification_inputs.skip_classification:
99
+ self.update_progress_bar.emit("Finished classification", 1, 1)
100
+ else:
101
+ self.update_progress_bar.emit("Finished detection", 1, 1)
92
102
  return result
@@ -1,4 +1,4 @@
1
- from typing import List, Tuple
1
+ from typing import List, Optional, Tuple
2
2
 
3
3
  import napari
4
4
  import napari.layers
@@ -44,6 +44,7 @@ def add_classified_layers(
44
44
  viewer: napari.Viewer,
45
45
  unknown_name: str = "Rejected",
46
46
  cell_name: str = "Detected",
47
+ scale: Optional[Tuple[float, float, float]] = None,
47
48
  ) -> None:
48
49
  """
49
50
  Adds cell candidates as two separate point layers - unknowns and cells, to
@@ -60,6 +61,7 @@ def add_classified_layers(
60
61
  face_color="lightskyblue",
61
62
  visible=False,
62
63
  metadata=dict(point_type=Cell.UNKNOWN),
64
+ scale=scale,
63
65
  )
64
66
  viewer.add_points(
65
67
  cells_to_array(points, Cell.CELL, napari_order=True),
@@ -70,6 +72,7 @@ def add_classified_layers(
70
72
  symbol="ring",
71
73
  face_color="lightgoldenrodyellow",
72
74
  metadata=dict(point_type=Cell.CELL),
75
+ scale=scale,
73
76
  )
74
77
 
75
78
 
@@ -78,6 +81,7 @@ def add_single_layer(
78
81
  viewer: napari.Viewer,
79
82
  name: str,
80
83
  cell_type: int,
84
+ scale: Optional[Tuple[float, float, float]] = None,
81
85
  ) -> None:
82
86
  """
83
87
  Adds all cells of cell_type Cell.TYPE to a new point layer in the napari
@@ -93,6 +97,7 @@ def add_single_layer(
93
97
  face_color="lightskyblue",
94
98
  visible=True,
95
99
  metadata=dict(point_type=cell_type),
100
+ scale=scale,
96
101
  )
97
102
 
98
103
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cellfinder
3
- Version: 1.8.0
3
+ Version: 1.9.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
@@ -26,7 +26,7 @@ License-File: LICENSE
26
26
  Requires-Dist: brainglobe-utils>=0.5.0
27
27
  Requires-Dist: brainglobe-napari-io>=0.3.4
28
28
  Requires-Dist: dask[array]
29
- Requires-Dist: fancylog>=0.0.7
29
+ Requires-Dist: fancylog>=0.6.0
30
30
  Requires-Dist: natsort
31
31
  Requires-Dist: numba
32
32
  Requires-Dist: numpy
@@ -53,24 +53,26 @@ Requires-Dist: brainglobe-napari-io; extra == "napari"
53
53
  Requires-Dist: magicgui; extra == "napari"
54
54
  Requires-Dist: napari-ndtiffs; extra == "napari"
55
55
  Requires-Dist: napari-plugin-engine>=0.1.4; extra == "napari"
56
- Requires-Dist: napari[pyqt5]>=0.6.1; extra == "napari"
56
+ Requires-Dist: napari[pyqt5]>=0.6.5; extra == "napari"
57
57
  Requires-Dist: pooch>=1; extra == "napari"
58
58
  Requires-Dist: qtpy; extra == "napari"
59
59
  Dynamic: license-file
60
60
 
61
61
  [![Python Version](https://img.shields.io/pypi/pyversions/cellfinder.svg)](https://pypi.org/project/cellfinder)
62
62
  [![PyPI](https://img.shields.io/pypi/v/cellfinder.svg)](https://pypi.org/project/cellfinder)
63
- [![Downloads](https://pepy.tech/badge/cellfinder)](https://pepy.tech/project/cellfinder)
63
+ [![Anaconda version](https://anaconda.org/conda-forge/cellfinder/badges/version.svg)](https://anaconda.org/conda-forge/cellfinder)
64
+ [![Napari hub](https://img.shields.io/endpoint?url=https://npe2api-git-add-shields-napari.vercel.app/api/shields/cellfinder)](https://napari-hub.org/plugins/cellfinder.html)
65
+ [![PyPI Downloads](https://pepy.tech/badge/cellfinder)](https://pepy.tech/project/cellfinder)
64
66
  [![Wheel](https://img.shields.io/pypi/wheel/cellfinder.svg)](https://pypi.org/project/cellfinder)
65
67
  [![Development Status](https://img.shields.io/pypi/status/cellfinder.svg)](https://github.com/brainglobe/cellfinder)
66
68
  [![Tests](https://img.shields.io/github/actions/workflow/status/brainglobe/cellfinder/test_and_deploy.yml?branch=main)](https://github.com/brainglobe/cellfinder/actions)
67
69
  [![codecov](https://codecov.io/gh/brainglobe/cellfinder/branch/main/graph/badge.svg?token=nx1lhNI7ox)](https://codecov.io/gh/brainglobe/cellfinder)
68
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
69
- [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
70
+ [![Code style: Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/format.json)](https://github.com/astral-sh/ruff)[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
70
71
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
71
72
  [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://brainglobe.info/community/developers/index.html)
72
- [![Twitter](https://img.shields.io/twitter/follow/brain_globe?style=social)](https://twitter.com/brain_globe)
73
-
73
+ [![image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftags%2Fbrainglobe.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/brainglobe)
74
+ [![Bluesky](https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff)](https://bsky.app/profile/brainglobe.info)
75
+ [![Mastodon](https://img.shields.io/badge/Mastodon-6364FF?logo=mastodon&logoColor=fff)](https://mastodon.online/@brainglobe)
74
76
  # cellfinder
75
77
 
76
78
  cellfinder is software for automated 3D cell detection in very large 3D images (e.g., serial two-photon or lightsheet volumes of whole mouse brains).
@@ -1,7 +1,7 @@
1
1
  cellfinder/__init__.py,sha256=S5oQ3EORuyQTMYC4uUuzGKZ23J3Ya6q-1DOBib1KfiA,1166
2
2
  cellfinder/cli_migration_warning.py,sha256=u4nKQiPYmpx0HRqm0PI8wBx78rNiiBSQSGciDoXEq78,1623
3
3
  cellfinder/core/__init__.py,sha256=pRFuQsl78HEK0S6gvhJw70QLbjjSBzP-GFO0AtVaGtk,62
4
- cellfinder/core/main.py,sha256=QaangjXVi5Sq_fCvMs_PGS-qiFZAsTwymnJ7lhggSno,9497
4
+ cellfinder/core/main.py,sha256=8VRM05-FvUPLjcBFFYruV_0Yct-UQOW2a0u5ubjBDnw,10875
5
5
  cellfinder/core/types.py,sha256=lTqWE4v0SMM0qLAZJdyAzqV1nLgDtobEpglNJcXt160,106
6
6
  cellfinder/core/classify/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  cellfinder/core/classify/augment.py,sha256=8dMbM7KhimM6NMgdMC53oHoCfYj1CIB-h3Yk8CZAxPw,6321
@@ -12,17 +12,17 @@ cellfinder/core/classify/tools.py,sha256=gdWE8cBMlT1pqxBKt6j2az7i7FOMR4N0ds4w9Yn
12
12
  cellfinder/core/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  cellfinder/core/config/cellfinder.conf,sha256=5i8axif7ekMutKDiVnZRs-LiJrgVQljg_beltidqtNk,56
14
14
  cellfinder/core/detect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- cellfinder/core/detect/detect.py,sha256=jtttIndsVsT0Ww_Dz41GgcVan0nlGzeq24Aulf8g6GQ,9919
15
+ cellfinder/core/detect/detect.py,sha256=DZxots88sG7GgKjXT-35TecQtRXGVEB-tRx12fTZjXU,11245
16
16
  cellfinder/core/detect/filters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- cellfinder/core/detect/filters/setup_filters.py,sha256=DIOQe2bvfSTRLPLgDJdzmaPQIgLpmS4lNxihHCRnU5Y,15252
17
+ cellfinder/core/detect/filters/setup_filters.py,sha256=-OFQsvMhxRhzHHATghQWjTUNudvo64uY27CHd7FSIk4,16210
18
18
  cellfinder/core/detect/filters/plane/__init__.py,sha256=lybcPbVDnurEQeq2FiLq0zR8p_ztarQOlajhdh1Q2-4,40
19
19
  cellfinder/core/detect/filters/plane/classical_filter.py,sha256=X5k266tbl9EHRVY5dls53B5IZlmP7U0UB9BsZ1ey_pc,13250
20
- cellfinder/core/detect/filters/plane/plane_filter.py,sha256=4ByEQkF73W5oK_YyrdUwW8GxD-XSrCTtLo4SOT8NCdQ,6127
20
+ cellfinder/core/detect/filters/plane/plane_filter.py,sha256=G22c48ecK7cCQWOutqmy0o5Y06rKTm0dM66-c7qUndk,10503
21
21
  cellfinder/core/detect/filters/plane/tile_walker.py,sha256=IiQibvWKnYlgl9h414fRklV7C2xZ0vXNmJ9t89DhYuI,4863
22
22
  cellfinder/core/detect/filters/volume/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  cellfinder/core/detect/filters/volume/ball_filter.py,sha256=DSBaHXtJ8TM71xT4fgzwL9E1MTE1euNv0vZ6Ozcx5cQ,14491
24
- cellfinder/core/detect/filters/volume/structure_detection.py,sha256=AIHq-5u5VFpKBBLEsE1Py-MlndbL8T0zXu0Bq2CI16Y,12916
25
- cellfinder/core/detect/filters/volume/structure_splitting.py,sha256=AcQTi_Gddj0vxSVz4JhZz_z16mVBOfCHQlkv8y3uPl4,10190
24
+ cellfinder/core/detect/filters/volume/structure_detection.py,sha256=jibzhm5WDKDQMO2vMRiqDwxoMKsjNYzUVfp5Dla2sBU,13207
25
+ cellfinder/core/detect/filters/volume/structure_splitting.py,sha256=C4YFamJtJuHZJwC5IJfrFG6TBIUh20kvSyvVR67SEOo,10129
26
26
  cellfinder/core/detect/filters/volume/volume_filter.py,sha256=G9uK6rALRTZufrihzz5TVtE1oGpkXNktrFA4nnOIEaM,20573
27
27
  cellfinder/core/download/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  cellfinder/core/download/cli.py,sha256=X9L9ZIkWqs58hX8G8q_0AKN4gk8BhydGxI9nLpdHQQE,1764
@@ -37,25 +37,25 @@ cellfinder/core/tools/source_files.py,sha256=vvwsIMe1ULKvXg_x22L75iqvCyMjEbUjJsD
37
37
  cellfinder/core/tools/system.py,sha256=WvEzPr7v-ohLDnzf4n13TMcN5OYIAXOEkaSmrHzdnwc,2438
38
38
  cellfinder/core/tools/threading.py,sha256=mc-XVEfLHj3AFWYcK1Qh4TKmVuW4mNaxgLBlQmBVViU,14154
39
39
  cellfinder/core/tools/tiff.py,sha256=NzIz6wq2GzxmcIhawFMwZADe-uQO2rIG46H7xkpGKLs,2899
40
- cellfinder/core/tools/tools.py,sha256=opMGC5GBBsId0dmL8V0KQrI4U70w_D_KtGQYpZNeHYQ,9390
40
+ cellfinder/core/tools/tools.py,sha256=fh8PAcoIGh9U5ChUUPl5a8JY2orR1DqZ3qPGHNxi-zI,9390
41
41
  cellfinder/core/train/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- cellfinder/core/train/train_yaml.py,sha256=tG5FXHZj26PqPUaZCVmFgnvACvIh23mCwEDXwoXv2Hc,13104
42
+ cellfinder/core/train/train_yaml.py,sha256=_6XsW82npFRcwHTJK3nUUlQfRbY7IVVAgoXYR2-_IBc,13112
43
43
  cellfinder/napari/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
- cellfinder/napari/curation.py,sha256=jItls1SmD5GWNzPlSlF-H73AGgmmBk-PwFO7_gZBY40,22207
44
+ cellfinder/napari/curation.py,sha256=XxrsdK6gHmxgOtmxAJaq-JmheeB4nFJNMrl-0yH--Dc,23464
45
45
  cellfinder/napari/input_container.py,sha256=upTkufWF3aOr9sPPK13C2YwqQ6tygULym8oDXrRLXJs,2510
46
46
  cellfinder/napari/napari.yaml,sha256=WMR1CIAmYIVyQngbdbomTRZLvlDbb6LxsXsvTRClQnE,921
47
47
  cellfinder/napari/sample_data.py,sha256=oUST23q09MM8dxHbUCmO0AjtXG6OlR_32LLqP0EU2UA,732
48
- cellfinder/napari/utils.py,sha256=AwTs76M9azutHhHj2yuaKErDEQ5F6pFbIIakBfzen6M,3824
48
+ cellfinder/napari/utils.py,sha256=pXuEoDIUeqgDtEsQg1U7OM3ie0_9rCmxAOZgJOwU-mQ,4009
49
49
  cellfinder/napari/detect/__init__.py,sha256=BD9Bg9NTAr6yRTq2A_p58U6j4w5wbY0sdXwhPJ3MSMY,34
50
- cellfinder/napari/detect/detect.py,sha256=nFpoxXnDuKTa2nHj62ACvYsiIHzVFXJ6qczomAaBr1g,15723
51
- cellfinder/napari/detect/detect_containers.py,sha256=rxvSiRszyaVFevJAi8bLMoUog51McfiXWvaQkr-fZQA,6429
52
- cellfinder/napari/detect/thread_worker.py,sha256=PWM3OE-FpK-dpdhaE_Gi-2lD3u8sL-SJ13mp0pMhTyI,3078
50
+ cellfinder/napari/detect/detect.py,sha256=A0FLHnWSEx1h4Lrkx5kmQY4fvgBVYoyla51kWdXcg98,17049
51
+ cellfinder/napari/detect/detect_containers.py,sha256=K4TO7KFac6jzjTt6Mvh6ey4zpO-RHzzs6a8SOfByAfY,6843
52
+ cellfinder/napari/detect/thread_worker.py,sha256=dQitk83sbY8MSx0iSROub3Wnp6nR0HlPOkFvjESm1dQ,3593
53
53
  cellfinder/napari/train/__init__.py,sha256=xo4CK-DvSecInGEc2ohcTgQYlH3iylFnGvKTCoq2WkI,35
54
54
  cellfinder/napari/train/train.py,sha256=tXrB8j_c293mzhWX5iEs4FvIwV1FpZuaX1rsSP4dgDQ,5830
55
55
  cellfinder/napari/train/train_containers.py,sha256=ovPl4ZiH6DUr-CGIYu-iju05z3_rtomyYnCWI6fURKc,4296
56
- cellfinder-1.8.0.dist-info/licenses/LICENSE,sha256=Tw8iMytIDXLSmcIUsbQmRWojstl9yOWsPCx6ZT6dZLY,1564
57
- cellfinder-1.8.0.dist-info/METADATA,sha256=m_i5i-J0oyEdjkcipTpZIdcpWjjnxfdfIaqNN6CO4zw,7165
58
- cellfinder-1.8.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
- cellfinder-1.8.0.dist-info/entry_points.txt,sha256=n2n3muDgifENJtTRz1JqYStxvuprROCwmKLt-VxvEHk,248
60
- cellfinder-1.8.0.dist-info/top_level.txt,sha256=jyTQzX-tDjbsMr6s-E71Oy0IKQzmHTXSk4ZhpG5EDSE,11
61
- cellfinder-1.8.0.dist-info/RECORD,,
56
+ cellfinder-1.9.0.dist-info/licenses/LICENSE,sha256=Tw8iMytIDXLSmcIUsbQmRWojstl9yOWsPCx6ZT6dZLY,1564
57
+ cellfinder-1.9.0.dist-info/METADATA,sha256=EfJdieyhSLKELsEcDv6Go1w7sLfmxZ0n18fQESBQm88,8452
58
+ cellfinder-1.9.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
59
+ cellfinder-1.9.0.dist-info/entry_points.txt,sha256=n2n3muDgifENJtTRz1JqYStxvuprROCwmKLt-VxvEHk,248
60
+ cellfinder-1.9.0.dist-info/top_level.txt,sha256=jyTQzX-tDjbsMr6s-E71Oy0IKQzmHTXSk4ZhpG5EDSE,11
61
+ cellfinder-1.9.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5