Skip to content

Commit 07657e2

Browse files
author
Donglai Wei
committed
update fiber.yaml
1 parent a37e410 commit 07657e2

File tree

16 files changed

+1196
-527
lines changed

16 files changed

+1196
-527
lines changed

connectomics/data/io/__init__.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,23 @@
1414
# Core I/O functions
1515
from .io import (
1616
# HDF5 I/O
17-
read_hdf5, write_hdf5, list_hdf5_datasets,
18-
17+
read_hdf5,
18+
write_hdf5,
19+
list_hdf5_datasets,
1920
# Image I/O
20-
read_image, read_images, read_image_as_volume,
21-
save_image, save_images, SUPPORTED_IMAGE_FORMATS,
22-
21+
read_image,
22+
read_images,
23+
read_image_as_volume,
24+
save_image,
25+
save_images,
26+
SUPPORTED_IMAGE_FORMATS,
2327
# Pickle I/O
24-
read_pickle_file, write_pickle_file,
25-
28+
read_pickle_file,
29+
write_pickle_file,
2630
# High-level volume I/O
27-
read_volume, save_volume, get_vol_shape,
31+
read_volume,
32+
save_volume,
33+
get_vol_shape,
2834
)
2935

3036
# Tile operations
@@ -35,7 +41,8 @@
3541

3642
# Utilities
3743
from .utils import (
38-
vast_to_segmentation,
44+
seg_to_rgb,
45+
rgb_to_seg,
3946
normalize_data_range,
4047
convert_to_uint8,
4148
split_multichannel_mask,
@@ -51,25 +58,35 @@
5158

5259
__all__ = [
5360
# HDF5 I/O
54-
'read_hdf5', 'write_hdf5', 'list_hdf5_datasets',
55-
61+
"read_hdf5",
62+
"write_hdf5",
63+
"list_hdf5_datasets",
5664
# Image I/O
57-
'read_image', 'read_images', 'read_image_as_volume',
58-
'save_image', 'save_images', 'SUPPORTED_IMAGE_FORMATS',
59-
65+
"read_image",
66+
"read_images",
67+
"read_image_as_volume",
68+
"save_image",
69+
"save_images",
70+
"SUPPORTED_IMAGE_FORMATS",
6071
# Pickle I/O
61-
'read_pickle_file', 'write_pickle_file',
62-
72+
"read_pickle_file",
73+
"write_pickle_file",
6374
# High-level volume I/O
64-
'read_volume', 'save_volume', 'get_vol_shape',
65-
75+
"read_volume",
76+
"save_volume",
77+
"get_vol_shape",
6678
# Tile operations
67-
'create_tile_metadata', 'reconstruct_volume_from_tiles',
68-
79+
"create_tile_metadata",
80+
"reconstruct_volume_from_tiles",
6981
# Utilities
70-
'vast_to_segmentation', 'normalize_data_range', 'convert_to_uint8',
71-
'split_multichannel_mask', 'squeeze_arrays',
72-
82+
"seg_to_rgb",
83+
"rgb_to_seg",
84+
"normalize_data_range",
85+
"convert_to_uint8",
86+
"split_multichannel_mask",
87+
"squeeze_arrays",
7388
# MONAI transforms
74-
'LoadVolumed', 'SaveVolumed', 'TileLoaderd',
89+
"LoadVolumed",
90+
"SaveVolumed",
91+
"TileLoaderd",
7592
]

connectomics/data/io/tiles.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from scipy.ndimage import zoom
1313

1414
from .io import read_image
15-
from .utils import vast_to_segmentation
15+
from .utils import rgb_to_seg
1616

1717

1818
def create_tile_metadata(
@@ -170,8 +170,8 @@ def reconstruct_volume_from_tiles(
170170
result[z - z0,
171171
y_actual_start - y0:y_actual_end - y0,
172172
x_actual_start - x0:x_actual_end - x0] = \
173-
vast_to_segmentation(patch[y_actual_start - y_patch_start:y_actual_end - y_patch_start,
174-
x_actual_start - x_patch_start:x_actual_end - x_patch_start])
173+
rgb_to_seg(patch[y_actual_start - y_patch_start:y_actual_end - y_patch_start,
174+
x_actual_start - x_patch_start:x_actual_end - x_patch_start])
175175

176176
# Apply padding for chunks touching the border of the large input volume
177177
if max(boundary_diffs) > 0:

connectomics/data/io/utils.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,24 @@
99
import numpy as np
1010

1111

12-
def vast_to_segmentation(segmentation_data: np.ndarray) -> np.ndarray:
12+
def seg_to_rgb(seg):
13+
"""
14+
Convert a segmentation map to an RGB image.
15+
16+
Args:
17+
seg (numpy.ndarray): The input segmentation map.
18+
19+
Returns:
20+
numpy.ndarray: The RGB image representation of the segmentation map.
21+
22+
Notes:
23+
- The function converts a segmentation map to an RGB image, where each unique segment ID is assigned a unique color.
24+
- The RGB image is represented as a numpy array.
25+
"""
26+
return np.stack([seg // 65536, seg // 256, seg % 256], axis=2).astype(np.uint8)
27+
28+
29+
def rgb_to_seg(rgb: np.ndarray) -> np.ndarray:
1330
"""Convert VAST segmentation format to standard format.
1431
1532
VAST format uses RGB encoding where each pixel's RGB values are combined
@@ -22,20 +39,25 @@ def vast_to_segmentation(segmentation_data: np.ndarray) -> np.ndarray:
2239
Converted segmentation with proper ID encoding
2340
"""
2441
# Convert to 24 bits
25-
if segmentation_data.ndim == 2 or segmentation_data.shape[-1] == 1:
26-
return np.squeeze(segmentation_data)
27-
elif segmentation_data.ndim == 3: # Single RGB image
28-
return (segmentation_data[:, :, 0].astype(np.uint32) * 65536 +
29-
segmentation_data[:, :, 1].astype(np.uint32) * 256 +
30-
segmentation_data[:, :, 2].astype(np.uint32))
31-
elif segmentation_data.ndim == 4: # Multiple RGB images
32-
return (segmentation_data[:, :, :, 0].astype(np.uint32) * 65536 +
33-
segmentation_data[:, :, :, 1].astype(np.uint32) * 256 +
34-
segmentation_data[:, :, :, 2].astype(np.uint32))
35-
36-
37-
def normalize_data_range(data: np.ndarray, target_min: float = 0.0,
38-
target_max: float = 1.0, ignore_uint8: bool = True) -> np.ndarray:
42+
if rgb.ndim == 2 or rgb.shape[-1] == 1:
43+
return np.squeeze(rgb)
44+
elif rgb.ndim == 3: # Single RGB image
45+
return (
46+
rgb[:, :, 0].astype(np.uint32) * 65536
47+
+ rgb[:, :, 1].astype(np.uint32) * 256
48+
+ rgb[:, :, 2].astype(np.uint32)
49+
)
50+
elif rgb.ndim == 4: # Multiple RGB images
51+
return (
52+
rgb[:, :, :, 0].astype(np.uint32) * 65536
53+
+ rgb[:, :, :, 1].astype(np.uint32) * 256
54+
+ rgb[:, :, :, 2].astype(np.uint32)
55+
)
56+
57+
58+
def normalize_data_range(
59+
data: np.ndarray, target_min: float = 0.0, target_max: float = 1.0, ignore_uint8: bool = True
60+
) -> np.ndarray:
3961
"""Normalize array values to a target range.
4062
4163
Args:
@@ -119,9 +141,10 @@ def squeeze_arrays(*arrays):
119141

120142

121143
__all__ = [
122-
'vast_to_segmentation',
123-
'normalize_data_range',
124-
'convert_to_uint8',
125-
'split_multichannel_mask',
126-
'squeeze_arrays',
127-
]
144+
"seg_to_rgb",
145+
"rgb_to_seg",
146+
"normalize_data_range",
147+
"convert_to_uint8",
148+
"split_multichannel_mask",
149+
"squeeze_arrays",
150+
]

connectomics/data/process/monai_transforms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ class MultiTaskLabelTransformd(MapTransform):
602602
}
603603
_TASK_DEFAULTS: Dict[str, Dict[str, Any]] = {
604604
"binary": {},
605-
"affinity": {"offsets": ["1-1-0", "1-0-0", "0-1-0", "0-0-1"]},
605+
"affinity": {"offsets": ["1-0-0", "0-1-0", "0-0-1"]}, # Default: 3 short-range affinities (z, y, x)
606606
"instance_boundary": {"thickness": 1, "edge_mode": "seg-all", "mode": "3d"},
607607
"instance_edt": {"mode": "2d", "quantize": False},
608608
"skeleton_aware_edt": {

connectomics/data/process/target.py

Lines changed: 88 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -244,74 +244,111 @@ def seg_to_binary(label, segment_id=[]):
244244
return fg_mask
245245

246246

247-
def seg_to_affinity(seg: np.ndarray, target_opt: List[str]) -> np.ndarray:
247+
def seg_to_affinity(
248+
seg: np.ndarray,
249+
offsets: List[str] = None,
250+
long_range: int = None,
251+
) -> np.ndarray:
248252
"""
249-
Compute affinities from a segmentation based on target options.
253+
Compute affinity maps from segmentation.
254+
255+
Supports two modes:
256+
1. DeepEM/SNEMI style: Provide `offsets` as list of strings (e.g., ["0-0-1", "0-1-0", "1-0-0"])
257+
2. BANIS style: Provide `long_range` as int for 6-channel output (3 short + 3 long range)
250258
251259
Args:
252260
seg: The segmentation to compute affinities from. Shape: (z, y, x).
253-
target_opt: List of strings defining affinity offsets.
254-
Can be either:
255-
- Legacy format: ['1', '0-0-1', '0-1-0', ...] (first element is type indicator)
256-
- Modern format: ['0-0-1', '0-1-0', ...] (direct offset list)
261+
0 indicates background.
262+
offsets: List of offset strings in "z-y-x" format (e.g., ["0-0-1", "0-1-0", "1-0-0"]).
263+
Each string defines one affinity channel.
264+
long_range: BANIS-style: offset for long-range affinities. Produces 6 channels:
265+
- Channel 0-2: Short-range (offset 1) for z, y, x
266+
- Channel 3-5: Long-range (offset long_range) for z, y, x
257267
258268
Returns:
259-
The affinities. Shape: (num_offsets, z, y, x).
269+
The affinities. Shape: (num_channels, z, y, x).
260270
"""
261-
if len(target_opt) == 0:
262-
# Default short-range affinities
263-
offsets = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
264-
else:
265-
# Detect format: check if first element is a type indicator or an offset
266-
start_idx = 0
267-
if len(target_opt) > 0 and "-" not in target_opt[0]:
268-
# Legacy format: first element is type indicator (e.g., '1')
269-
start_idx = 1
270-
271-
# Parse offsets from target_opt
272-
offsets = []
273-
for opt_str in target_opt[start_idx:]:
274-
if "-" in opt_str:
275-
offset = [int(x) for x in opt_str.split("-")]
276-
offsets.append(offset)
277-
278-
# Fallback to default if no valid offsets found
279-
if len(offsets) == 0:
280-
offsets = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
281-
282-
num_offsets = len(offsets)
283-
affinities = np.zeros((num_offsets, *seg.shape), dtype=np.float32)
284-
285-
for i, offset in enumerate(offsets):
286-
dz, dy, dx = offset
287-
288-
# Create slices for the offset
271+
# BANIS mode: use long_range parameter (takes precedence if specified)
272+
if long_range is not None:
273+
affinities = np.zeros((6, *seg.shape), dtype=np.float32)
274+
275+
# Short range affinities (offset 1)
276+
affinities[0, :-1] = (seg[:-1] == seg[1:]) & (seg[1:] > 0)
277+
affinities[1, :, :-1] = (seg[:, :-1] == seg[:, 1:]) & (seg[:, 1:] > 0)
278+
affinities[2, :, :, :-1] = (seg[:, :, :-1] == seg[:, :, 1:]) & (seg[:, :, 1:] > 0)
279+
280+
# Long range affinities
281+
affinities[3, :-long_range] = (seg[:-long_range] == seg[long_range:]) & (seg[long_range:] > 0)
282+
affinities[4, :, :-long_range] = (seg[:, :-long_range] == seg[:, long_range:]) & (seg[:, long_range:] > 0)
283+
affinities[5, :, :, :-long_range] = (seg[:, :, :-long_range] == seg[:, :, long_range:]) & (seg[:, :, long_range:] > 0)
284+
285+
return affinities
286+
287+
# DeepEM/SNEMI mode: use offsets parameter
288+
if offsets is None:
289+
# Default: short-range affinities for z, y, x
290+
offsets = ["1-0-0", "0-1-0", "0-0-1"]
291+
292+
# Parse offsets from strings
293+
parsed_offsets = []
294+
for offset_str in offsets:
295+
parts = offset_str.split("-")
296+
if len(parts) == 3:
297+
parsed_offsets.append([int(parts[0]), int(parts[1]), int(parts[2])])
298+
else:
299+
raise ValueError(f"Invalid offset format: {offset_str}. Expected 'z-y-x' format.")
300+
301+
num_channels = len(parsed_offsets)
302+
affinities = np.zeros((num_channels, *seg.shape), dtype=np.float32)
303+
304+
for i, (dz, dy, dx) in enumerate(parsed_offsets):
305+
# Handle each axis independently
306+
# For positive offset: compare seg[:-offset] with seg[offset:]
307+
# For negative offset: compare seg[-offset:] with seg[:offset]
308+
309+
if dz == 0 and dy == 0 and dx == 0:
310+
# Zero offset: all foreground pixels are 1
311+
affinities[i] = (seg > 0).astype(np.float32)
312+
continue
313+
314+
# Build source and destination slices for each axis
289315
if dz > 0:
290-
src_slice = (slice(None, -dz), slice(None), slice(None))
291-
dst_slice = (slice(dz, None), slice(None), slice(None))
316+
z_src = slice(None, -dz)
317+
z_dst = slice(dz, None)
292318
elif dz < 0:
293-
src_slice = (slice(-dz, None), slice(None), slice(None))
294-
dst_slice = (slice(None, dz), slice(None), slice(None))
319+
z_src = slice(-dz, None)
320+
z_dst = slice(None, dz)
295321
else:
296-
src_slice = (slice(None), slice(None), slice(None))
297-
dst_slice = (slice(None), slice(None), slice(None))
322+
z_src = slice(None)
323+
z_dst = slice(None)
298324

299325
if dy > 0:
300-
src_slice = (src_slice[0], slice(None, -dy), src_slice[2])
301-
dst_slice = (dst_slice[0], slice(dy, None), dst_slice[2])
326+
y_src = slice(None, -dy)
327+
y_dst = slice(dy, None)
302328
elif dy < 0:
303-
src_slice = (src_slice[0], slice(-dy, None), src_slice[2])
304-
dst_slice = (dst_slice[0], slice(None, dy), dst_slice[2])
329+
y_src = slice(-dy, None)
330+
y_dst = slice(None, dy)
331+
else:
332+
y_src = slice(None)
333+
y_dst = slice(None)
305334

306335
if dx > 0:
307-
src_slice = (src_slice[0], src_slice[1], slice(None, -dx))
308-
dst_slice = (dst_slice[0], dst_slice[1], slice(dx, None))
336+
x_src = slice(None, -dx)
337+
x_dst = slice(dx, None)
309338
elif dx < 0:
310-
src_slice = (src_slice[0], src_slice[1], slice(-dx, None))
311-
dst_slice = (dst_slice[0], dst_slice[1], slice(None, dx))
339+
x_src = slice(-dx, None)
340+
x_dst = slice(None, dx)
341+
else:
342+
x_src = slice(None)
343+
x_dst = slice(None)
344+
345+
src_slice = (z_src, y_src, x_src)
346+
dst_slice = (z_dst, y_dst, x_dst)
312347

313-
# Compute affinity
314-
affinities[i][dst_slice] = (seg[src_slice] == seg[dst_slice]) & (seg[dst_slice] > 0)
348+
# Compute affinity: same segment ID and not background
349+
affinities[i][dst_slice] = (
350+
(seg[src_slice] == seg[dst_slice]) & (seg[dst_slice] > 0)
351+
).astype(np.float32)
315352

316353
return affinities
317354

0 commit comments

Comments
 (0)