dacapo-toolbox 1.0.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.
File without changes
@@ -0,0 +1,483 @@
1
+ import random
2
+ import logging
3
+ from collections.abc import Sequence
4
+
5
+ import gunpowder as gp
6
+ from gunpowder.nodes.gp_graph_source import GraphSource as GPGraphSource
7
+ import networkx as nx
8
+ import dask.array as da
9
+ import numpy as np
10
+
11
+ from funlib.geometry import Coordinate, Roi
12
+ from funlib.persistence import Array
13
+
14
+ import torch
15
+
16
+ import time
17
+ import functools
18
+ from dataclasses import dataclass
19
+ from .tmp import gcd
20
+ from typing import Callable
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def interpolatable_dtypes(dtype) -> bool:
26
+ return dtype in [np.float32, np.float64, np.uint8, np.uint16]
27
+
28
+
29
+ def nx_to_gp_graph(
30
+ graph: nx.Graph,
31
+ scale: Sequence[float],
32
+ ) -> gp.Graph:
33
+ """
34
+ Convert a NetworkX graph to a gunpowder Graph.
35
+ """
36
+ graph: gp.Graph = gp.Graph(
37
+ [
38
+ gp.Node(
39
+ node,
40
+ np.array(attrs.pop("position")) / np.array(scale),
41
+ attrs=attrs,
42
+ )
43
+ for node, attrs in graph.nodes(data=True)
44
+ ], # type: ignore[arg-type]
45
+ [gp.Edge(u, v, attrs) for u, v, attrs in graph.edges(data=True)], # type: ignore[arg-type]
46
+ gp.GraphSpec(Roi((None,) * len(scale), (None,) * len(scale))),
47
+ )
48
+ return graph
49
+
50
+
51
+ def gp_to_nx_graph(
52
+ graph: gp.Graph,
53
+ ) -> nx.Graph:
54
+ """
55
+ Convert a gunpowder Graph to a NetworkX graph.
56
+ """
57
+ g = nx.Graph()
58
+ for node in graph.nodes:
59
+ g.add_node(node.id, position=node.location, **node.attrs)
60
+ for edge in graph.edges:
61
+ g.add_edge(edge.u, edge.v, **edge.attrs)
62
+ return g
63
+
64
+
65
+ class PipelineDataset(torch.utils.data.IterableDataset):
66
+ """
67
+ A torch dataset that wraps a gunpowder pipeline and provides batches of data.
68
+ It has support for applying torchvision style transforms to the resulting batches.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ pipeline: gp.Pipeline,
74
+ request: gp.BatchRequest,
75
+ keys: list[gp.ArrayKey],
76
+ transforms: dict[
77
+ str | tuple[str | tuple[str, ...], str | tuple[str, ...]], Callable
78
+ ]
79
+ | None = None,
80
+ ):
81
+ self.pipeline = pipeline
82
+ self.request = request
83
+ self.keys = keys
84
+ self.transforms = transforms
85
+
86
+ def __iter__(self):
87
+ while True:
88
+ t1 = time.time()
89
+ batch_request = self.request.copy()
90
+ batch_request._random_seed = random.randint(0, 2**32 - 1)
91
+ batch = self.pipeline.request_batch(batch_request)
92
+ # TODO: Throw warning on incorrect byteorder!
93
+ torch_batch = {
94
+ str(key): torch.from_numpy(
95
+ batch[key]
96
+ .data.astype(batch[key].data.dtype.newbyteorder("="))
97
+ .copy()
98
+ )
99
+ if isinstance(key, gp.ArrayKey)
100
+ else gp_to_nx_graph(batch[key])
101
+ for key in self.keys
102
+ }
103
+ torch_batch["metadata"] = {
104
+ str(key): (batch[key].spec.roi.offset, batch[key].spec.voxel_size)
105
+ for key in self.keys
106
+ if isinstance(key, gp.ArrayKey)
107
+ }
108
+
109
+ if self.transforms is not None:
110
+ for transform_signature, transform_func in self.transforms.items():
111
+ if isinstance(transform_signature, tuple):
112
+ in_key, out_key = transform_signature
113
+ else:
114
+ in_key, out_key = transform_signature, transform_signature
115
+
116
+ if isinstance(in_key, str):
117
+ in_keys = [in_key]
118
+ elif isinstance(in_key, tuple):
119
+ in_keys = list(in_key)
120
+
121
+ for in_key in in_keys:
122
+ assert in_key in torch_batch, (
123
+ f"Can only process keys that are in the batch. Please ensure that {in_key} "
124
+ f"is either provided as a dataset or created as the result of a transform "
125
+ f"of the form ({{in_key}}, {in_key})) *before* the transform ({in_key})."
126
+ )
127
+ in_tensors = [torch_batch[in_key] for in_key in in_keys]
128
+ out_tensor = transform_func(*in_tensors)
129
+ if isinstance(out_key, str):
130
+ torch_batch[out_key] = out_tensor
131
+ else:
132
+ out_keys = out_key
133
+ out_tensors = out_tensor
134
+ for out_key, out_tensor in zip(out_keys, out_tensors):
135
+ torch_batch[out_key] = out_tensor
136
+
137
+ t2 = time.time()
138
+ logger.debug(f"Batch generated in {t2 - t1:.4f} seconds")
139
+ yield torch_batch
140
+
141
+
142
+ @dataclass
143
+ class SimpleAugmentConfig:
144
+ """
145
+ The simple augment handles non-interpolating geometric transformations.
146
+ This includes mirroring and transposing in n-dimensional space.
147
+ See https://github.com/funkelab/gunpowder/blob/main/gunpowder/nodes/simple_augment.py
148
+ for more details.
149
+
150
+ Parameters:
151
+ :param p: Probability of applying the augmentations.
152
+ :param mirror_only: List of axes to mirror. If None, all axes may be mirrored.
153
+ :param transpose_only: List of axes to transpose. If None, all axes may be transposed.
154
+ :param mirror_probs: List of probabilities for each axis in `mirror_only`.
155
+ If None, uses equal probability for all axes.
156
+ :param transpose_probs: Dictionary mapping tuples of axes to probabilities for transposing.
157
+ If None, uses equal probability for all axes.
158
+ """
159
+
160
+ p: float = 0.0
161
+ mirror_only: Sequence[int] | None = None
162
+ transpose_only: Sequence[int] | None = None
163
+ mirror_probs: Sequence[float] | None = None
164
+ transpose_probs: dict[tuple[int, ...], float] | Sequence[float] | None = None
165
+
166
+
167
+ @dataclass
168
+ class DeformAugmentConfig:
169
+ """
170
+ The deform augment handles interpolating geometric transformations.
171
+ This includes scaling, rotation, and elastic deformations.
172
+ See https://github.com/funkelab/gunpowder/blob/main/gunpowder/nodes/deform_augment.py
173
+ for more details.
174
+
175
+ Parameters:
176
+ :param p: Probability of applying the augmentations.
177
+ :param control_point_spacing: Spacing of the control points for the elastic deformation.
178
+ :param jitter_sigma: Standard deviation of the Gaussian noise added to the control points.
179
+ :param scale_interval: Interval for scaling the input data.
180
+ :param rotate: Whether to apply random rotations.
181
+ :param subsample: Subsampling factor for the control points.
182
+ :param spatial_dims: Number of spatial dimensions.
183
+ :param rotation_axes: Axes around which to rotate. If None, rotates around all axes.
184
+ """
185
+
186
+ p: float = 0.0
187
+ control_point_spacing: Sequence[int] | None = None
188
+ jitter_sigma: Sequence[float] | None = None
189
+ scale_interval: tuple[float, float] | None = None
190
+ rotate: bool = False
191
+ subsample: int = 4
192
+ spatial_dims: int = 3
193
+ rotation_axes: Sequence[int] | None = None
194
+
195
+
196
+ @dataclass
197
+ class MaskedSampling:
198
+ """
199
+ Sampling strategy that uses a mask to determine which samples to include.
200
+
201
+ :param mask_key: The key of the mask array in the dataset.
202
+ :param min_masked: Minimum fraction of samples that must be masked in to include the
203
+ sample. If less than this fraction is masked in, the sample is skipped.
204
+ :param strategy: Optional strategy to apply to the mask. by default generates an integral
205
+ mask for quick sampling at the cost of extra memory usage. If your dataset is large, you
206
+ may want to use "reject".
207
+ """
208
+
209
+ mask_key: str
210
+ min_masked: float = 1.0
211
+ strategy: str = "integral_mask"
212
+
213
+
214
+ @dataclass
215
+ class PointSampling:
216
+ """
217
+ Sampling strategy that uses a set of points to determine which samples to include.
218
+
219
+ :param sample_points_key: The key of the sample points array in the dataset.
220
+ """
221
+
222
+ sample_points_key: str
223
+
224
+
225
+ def iterable_dataset(
226
+ datasets: dict[str, Array | nx.Graph | Sequence[Array] | Sequence[nx.Graph]],
227
+ shapes: dict[str, Sequence[int]],
228
+ weights: Sequence[float] | None = None,
229
+ transforms: dict[
230
+ str | tuple[str | tuple[str, ...], str | tuple[str, ...]], Callable
231
+ ]
232
+ | None = None,
233
+ sampling_strategies: MaskedSampling
234
+ | PointSampling
235
+ | Sequence[MaskedSampling | PointSampling]
236
+ | None = None,
237
+ trim: int | Sequence[int] | None = None,
238
+ simple_augment_config: SimpleAugmentConfig | None = None,
239
+ deform_augment_config: DeformAugmentConfig | None = None,
240
+ interpolatable: dict[str, bool] | None = None,
241
+ ) -> torch.utils.data.IterableDataset:
242
+ """
243
+ Builds a gunpowder pipeline and wraps it in a torch IterableDataset.
244
+ See https://pytorch.org/docs/stable/data.html#torch.utils.data.IterableDataset for more info
245
+ """
246
+
247
+ # Check the validity of the inputs
248
+ assert len(datasets) >= 1, "Expected at least one dataset, got an empty dictionary"
249
+ assert "ROI_MASK" not in datasets, (
250
+ "The key 'ROI_MASK' is reserved for internal use. "
251
+ "Please use a different key for your dataset."
252
+ )
253
+
254
+ if interpolatable is None:
255
+ interpolatable = {}
256
+
257
+ # convert single arrays to lists
258
+ datasets: dict[str, list[Array] | list[nx.Graph]] = {
259
+ name: [ds] if isinstance(ds, (Array, nx.Graph)) else list(ds)
260
+ for name, ds in datasets.items()
261
+ }
262
+
263
+ # define keys:
264
+ keys = [
265
+ gp.ArrayKey(name) if isinstance(dataset[0], Array) else gp.GraphKey(name)
266
+ for name, dataset in datasets.items()
267
+ ]
268
+ array_keys = [key for key in keys if isinstance(key, gp.ArrayKey)]
269
+ graph_keys = [key for key in keys if isinstance(key, gp.GraphKey)]
270
+
271
+ roi_mask_key = gp.ArrayKey("ROI_MASK")
272
+
273
+ # reorganize from raw: [a,b,c], gt: [a,b,c] to (raw,gt): [(a,a), (b,b), (c,c)]
274
+ crops_datasets: list[tuple[Array | nx.Graph, ...]] = list(zip(*datasets.values()))
275
+ crops_scale = [
276
+ functools.reduce(
277
+ lambda x, y: gcd(x, y),
278
+ [array.voxel_size for array in crop_datasets if isinstance(array, Array)],
279
+ )
280
+ for crop_datasets in crops_datasets
281
+ ]
282
+
283
+ # check that for each key, all arrays have the same voxel size when scaled
284
+ # by the crop scale
285
+ assert all(
286
+ functools.reduce(
287
+ lambda x, y: x * 0 if x != y else x,
288
+ [
289
+ array.voxel_size / scale
290
+ for array, scale in zip(datasets[key], crops_scale)
291
+ if isinstance(array, Array)
292
+ ],
293
+ )
294
+ == datasets[key][0].voxel_size / crops_scale[0]
295
+ for key in datasets
296
+ if isinstance(datasets[key][0], Array)
297
+ )
298
+
299
+ if isinstance(sampling_strategies, (MaskedSampling, PointSampling)):
300
+ sampling_strategies = [sampling_strategies] * len(crops_datasets)
301
+ elif sampling_strategies is None:
302
+ sampling_strategies = [None] * len(crops_datasets)
303
+
304
+ # Get source nodes
305
+ dataset_sources = []
306
+
307
+ # type hints since zip seems to get rid of the type info
308
+ # crop_datasets: list[Array | nx.Graph]
309
+ # crop_scale: Coordinate
310
+ # sampling_strategy: MaskedSampling | PointSampling | None
311
+
312
+ for crop_datasets, crop_scale, sampling_strategy in zip(
313
+ crops_datasets, crops_scale, sampling_strategies
314
+ ):
315
+ crop_arrays = [array for array in crop_datasets if isinstance(array, Array)]
316
+
317
+ for dataset in crop_datasets:
318
+ assert dataset is None or isinstance(dataset, (Array, nx.Graph)), (
319
+ f"Dataset {dataset} is not an Array or a NetworkX graph. "
320
+ f"{type(dataset)} is not supported. Please provide a valid dataset."
321
+ )
322
+
323
+ # smallest roi
324
+ crop_roi = (
325
+ functools.reduce(
326
+ lambda x, y: x.intersect(y), [array.roi for array in crop_arrays]
327
+ )
328
+ / crop_scale
329
+ )
330
+ crop_voxel_size = (
331
+ functools.reduce(
332
+ lambda x, y: Coordinate(*map(min, x, y)),
333
+ [array.voxel_size for array in crop_arrays],
334
+ )
335
+ / crop_scale
336
+ )
337
+ crop_roi.snap_to_grid(crop_voxel_size, mode="grow")
338
+ if trim is not None:
339
+ if isinstance(trim, int):
340
+ trim = [trim] * crop_roi.dims()
341
+ trim = Coordinate(trim)
342
+ crop_roi = crop_roi.grow(-trim * crop_voxel_size, -trim * crop_voxel_size)
343
+
344
+ crop_graphs = [
345
+ nx_to_gp_graph(graph, crop_scale)
346
+ if graph is not None
347
+ else nx_to_gp_graph(nx.Graph(), crop_scale)
348
+ for graph in crop_datasets
349
+ if isinstance(graph, nx.Graph) or graph is None
350
+ ]
351
+
352
+ crop_sources = tuple(
353
+ gp.ArraySource(
354
+ key,
355
+ Array(
356
+ array.data,
357
+ offset=array.roi.offset / crop_scale,
358
+ voxel_size=array.voxel_size / crop_scale,
359
+ units=array.units,
360
+ axis_names=array.axis_names,
361
+ types=array.types,
362
+ ),
363
+ interpolatable=interpolatable.get(
364
+ str(key), interpolatable_dtypes(array.dtype)
365
+ ),
366
+ )
367
+ + gp.Pad(
368
+ key,
369
+ None
370
+ if not (
371
+ isinstance(sampling_strategy, MaskedSampling)
372
+ and sampling_strategy.mask_key == str(key)
373
+ )
374
+ else Coordinate((0,) * len(crop_scale)),
375
+ )
376
+ for key, array in zip(array_keys, crop_arrays)
377
+ ) + tuple(
378
+ GPGraphSource(key, graph) for key, graph in zip(graph_keys, crop_graphs)
379
+ )
380
+
381
+ crop_sources += (
382
+ gp.ArraySource( # a dummy array for consisntency
383
+ roi_mask_key,
384
+ Array(
385
+ da.ones(crop_roi.shape),
386
+ offset=crop_roi.offset,
387
+ voxel_size=crop_voxel_size,
388
+ ),
389
+ ),
390
+ )
391
+
392
+ dataset_source = crop_sources + gp.MergeProvider()
393
+
394
+ if sampling_strategy is None:
395
+ # If no sampling strategy is provided, use random sampling
396
+ dataset_source += gp.RandomLocation()
397
+ elif isinstance(sampling_strategy, PointSampling):
398
+ assert gp.GraphKey(sampling_strategy.sample_points_key) in keys, (
399
+ f"Sample points key {sampling_strategy.sample_points_key} must be one of the dataset keys: {keys}. "
400
+ "Please ensure that the sample points are provided as part of the dataset."
401
+ )
402
+ dataset_source += gp.RandomLocation(
403
+ ensure_nonempty=gp.GraphKey(sampling_strategy.sample_points_key),
404
+ ensure_centered=True,
405
+ )
406
+ elif (
407
+ isinstance(sampling_strategy, MaskedSampling)
408
+ and sampling_strategy.strategy == "integral_mask"
409
+ ):
410
+ assert gp.ArrayKey(sampling_strategy.mask_key) in keys, (
411
+ f"Mask key {sampling_strategy.mask_key} must be one of the dataset keys: {keys}. "
412
+ "Please ensure that the mask is provided as part of the dataset."
413
+ )
414
+ dataset_source += gp.RandomLocation(
415
+ min_masked=sampling_strategy.min_masked,
416
+ mask=gp.ArrayKey(sampling_strategy.mask_key),
417
+ )
418
+ elif sampling_strategy == "reject":
419
+ dataset_source += gp.RandomLocation()
420
+ dataset_source += gp.Reject(roi_mask_key, 1.0)
421
+ else:
422
+ raise ValueError(
423
+ f"Unsupported sampling strategy: {sampling_strategy}. "
424
+ "Please use either `None`, or provide a PointSampling/MaskSampling instance."
425
+ )
426
+
427
+ dataset_sources.append(dataset_source)
428
+
429
+ pipeline = tuple(dataset_sources) + gp.RandomProvider(weights)
430
+
431
+ if deform_augment_config is not None:
432
+ pipeline += gp.DeformAugment(
433
+ control_point_spacing=Coordinate(
434
+ deform_augment_config.control_point_spacing
435
+ or (1,) * crop_scale[0].dims()
436
+ ),
437
+ jitter_sigma=deform_augment_config.jitter_sigma
438
+ or (0,) * crop_scale[0].dims(),
439
+ scale_interval=deform_augment_config.scale_interval,
440
+ rotate=deform_augment_config.rotate,
441
+ subsample=deform_augment_config.subsample,
442
+ spatial_dims=deform_augment_config.spatial_dims,
443
+ rotation_axes=deform_augment_config.rotation_axes,
444
+ use_fast_points_transform=True,
445
+ p=deform_augment_config.p,
446
+ )
447
+ if simple_augment_config is not None:
448
+ pipeline += gp.SimpleAugment(
449
+ mirror_only=simple_augment_config.mirror_only,
450
+ transpose_only=simple_augment_config.transpose_only,
451
+ mirror_probs=simple_augment_config.mirror_probs,
452
+ transpose_probs=simple_augment_config.transpose_probs,
453
+ p=simple_augment_config.p,
454
+ )
455
+
456
+ # generate request for all necessary inputs to training
457
+ request = gp.BatchRequest()
458
+ for key in array_keys:
459
+ crop_scale = crops_scale[0]
460
+ data_shape = shapes.get(str(key), None)
461
+ assert data_shape is not None, (
462
+ f"Shape for key {key} not provided. Please provide a shape for all keys."
463
+ )
464
+ request.add(
465
+ key, Coordinate(data_shape) * datasets[str(key)][0].voxel_size / crop_scale
466
+ )
467
+ for key in graph_keys:
468
+ data_shape = shapes.get(str(key), None)
469
+ assert data_shape is not None, (
470
+ f"Shape for key {key} not provided. Please provide a shape for all keys."
471
+ )
472
+ request.add(key, Coordinate(data_shape))
473
+
474
+ # Add mask placeholder to guarantee center voxel is contained in
475
+ # the mask, and to be used for some sampling strategies.
476
+ request.add(
477
+ roi_mask_key,
478
+ crop_voxel_size,
479
+ )
480
+
481
+ # Build the pipeline
482
+ gp.build(pipeline).__enter__()
483
+ return PipelineDataset(pipeline, request=request, keys=keys, transforms=transforms)
File without changes
@@ -0,0 +1,29 @@
1
+ import torch
2
+
3
+
4
+ class DistanceHead(torch.nn.Module):
5
+ def __init__(self, in_channels: int = 12, incr_factor: int = 12, dims: int = 3):
6
+ super().__init__()
7
+ self.dims = dims
8
+ self.conv1 = torch.nn.Conv3d(
9
+ in_channels, in_channels * incr_factor, kernel_size=1
10
+ )
11
+ self.relu = torch.nn.LeakyReLU()
12
+ self.conv2 = torch.nn.Conv3d(in_channels * incr_factor, 1, kernel_size=1)
13
+ self.sigmoid = torch.nn.Sigmoid()
14
+
15
+ torch.nn.init.kaiming_normal_(
16
+ self.conv1.weight, mode="fan_out", nonlinearity="relu"
17
+ )
18
+ torch.nn.init.kaiming_normal_(
19
+ self.conv2.weight, mode="fan_out", nonlinearity="sigmoid"
20
+ )
21
+
22
+ def forward(self, x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
23
+ out = self.sigmoid(self.conv2(self.relu(self.conv1(x - y))))
24
+ # remove channel dimension
25
+ if out.ndim == self.dims + 1:
26
+ out = out.unsqueeze(0)
27
+ elif out.ndim == self.dims + 2:
28
+ out = out.squeeze(1)
29
+ return out