Skip to content

Commit ced0e10

Browse files
author
Donglai Wei
committed
Add bbox arg to visualize command
1 parent 4df5095 commit ced0e10

2 files changed

Lines changed: 111 additions & 2 deletions

File tree

justfile

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,23 @@ sweep config:
200200
# Visualize volumes with Neuroglancer from config (e.g., just visualize tutorials/monai_lucchi.yaml test --volumes prediction:path.h5)
201201
# Port defaults to 9999. Override with: just visualize config mode --port 8080 --volumes ...
202202
# Default selects first file from globs. Use --select to change: --select 1, --select filename, --select all
203-
visualize config mode *ARGS='':
203+
# Optional bbox shortcut (auto-expands to --bbox): just visualize config mode 0,0,0,32,256,256
204+
visualize config mode bbox='' *ARGS='':
204205
#!/usr/bin/env bash
205206
args="--config {{config}} --mode {{mode}}"
207+
extra_args="{{bbox}} {{ARGS}}"
206208
# Check if --port is in ARGS, otherwise add default
207-
if [[ ! "{{ARGS}}" =~ --port ]]; then
209+
if [[ ! "$extra_args" =~ --port ]]; then
208210
args="$args --port 9999"
209211
fi
212+
if [ -n "{{bbox}}" ]; then
213+
if [[ "{{bbox}}" == --* ]]; then
214+
# Backward compatibility: first extra CLI flag may be captured in bbox slot
215+
args="$args {{bbox}}"
216+
else
217+
args="$args --bbox {{bbox}}"
218+
fi
219+
fi
210220
python -i scripts/visualize_neuroglancer.py $args {{ARGS}}
211221

212222
# Visualize specific image and label files (e.g., just visualize-files datasets/img.tif datasets/label.h5)

scripts/visualize_neuroglancer.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,101 @@ def parse_args():
312312
help="Select specific file from glob patterns by index (e.g., '0', '-1') or filename (e.g., 'volume_001'). "
313313
"Default: '0' (first file). Use 'all' to load all files.",
314314
)
315+
parser.add_argument(
316+
"--bbox",
317+
type=str,
318+
default=None,
319+
help=(
320+
"Crop all loaded volumes to a spatial bounding box before visualization. "
321+
"Format: 'zmin,ymin,xmin,zmax,ymax,xmax' (Python slicing, end-exclusive)."
322+
),
323+
)
315324

316325
return parser.parse_args()
317326

318327

328+
def parse_bbox_arg(bbox_str: str) -> Tuple[int, int, int, int, int, int]:
329+
"""Parse bbox string 'zmin,ymin,xmin,zmax,ymax,xmax' into integer coordinates."""
330+
parts = [p.strip() for p in bbox_str.split(",")]
331+
if len(parts) != 6:
332+
raise ValueError(
333+
"bbox must have 6 comma-separated integers: zmin,ymin,xmin,zmax,ymax,xmax"
334+
)
335+
336+
try:
337+
zmin, ymin, xmin, zmax, ymax, xmax = (int(p) for p in parts)
338+
except ValueError as e:
339+
raise ValueError(
340+
"bbox values must be integers in format zmin,ymin,xmin,zmax,ymax,xmax"
341+
) from e
342+
343+
if min(zmin, ymin, xmin, zmax, ymax, xmax) < 0:
344+
raise ValueError("bbox values must be non-negative")
345+
if not (zmin < zmax and ymin < ymax and xmin < xmax):
346+
raise ValueError("bbox min values must be strictly less than max values")
347+
348+
return zmin, ymin, xmin, zmax, ymax, xmax
349+
350+
351+
def crop_volumes_to_bbox(
352+
volumes: Dict[str, Tuple],
353+
bbox: Tuple[int, int, int, int, int, int],
354+
default_offset: Tuple[int, int, int],
355+
) -> Dict[str, Tuple]:
356+
"""
357+
Crop all volumes to the same bbox and update voxel offsets accordingly.
358+
359+
Args:
360+
volumes: Mapping of volume names to (data, type[, resolution, offset]) tuples.
361+
bbox: (zmin, ymin, xmin, zmax, ymax, xmax), end-exclusive.
362+
default_offset: Fallback offset used when a volume has no explicit offset.
363+
364+
Returns:
365+
New volume mapping with cropped arrays and adjusted offsets.
366+
"""
367+
zmin, ymin, xmin, zmax, ymax, xmax = bbox
368+
cropped_volumes: Dict[str, Tuple] = {}
369+
370+
print(f"\nApplying bbox crop to all volumes: {bbox} (end-exclusive)")
371+
372+
for name, vol_data in volumes.items():
373+
if len(vol_data) == 2:
374+
data, vol_type = vol_data
375+
vol_resolution = None
376+
vol_offset = None
377+
else:
378+
data, vol_type, vol_resolution, vol_offset = vol_data
379+
380+
if data.ndim == 3:
381+
spatial_shape = data.shape
382+
crop_slices = (slice(zmin, zmax), slice(ymin, ymax), slice(xmin, xmax))
383+
elif data.ndim == 4:
384+
spatial_shape = data.shape[-3:]
385+
crop_slices = (slice(None), slice(zmin, zmax), slice(ymin, ymax), slice(xmin, xmax))
386+
else:
387+
raise ValueError(
388+
f"Volume '{name}' has unsupported ndim={data.ndim} for bbox cropping (expected 3D or 4D)"
389+
)
390+
391+
if not (
392+
0 <= zmin < zmax <= spatial_shape[0]
393+
and 0 <= ymin < ymax <= spatial_shape[1]
394+
and 0 <= xmin < xmax <= spatial_shape[2]
395+
):
396+
raise ValueError(
397+
f"bbox {bbox} is out of bounds for volume '{name}' spatial shape {spatial_shape}"
398+
)
399+
400+
cropped_data = data[crop_slices]
401+
base_offset = vol_offset if vol_offset is not None else default_offset
402+
new_offset = (base_offset[0] + zmin, base_offset[1] + ymin, base_offset[2] + xmin)
403+
404+
print(f" {name}: {data.shape} -> {cropped_data.shape}, offset {base_offset} -> {new_offset}")
405+
cropped_volumes[name] = (cropped_data, vol_type, vol_resolution, new_offset)
406+
407+
return cropped_volumes
408+
409+
319410
def load_volumes_from_config(
320411
config_path: str, mode: str = "train", prediction_base_name: Optional[str] = None,
321412
select: str = "0"
@@ -1102,6 +1193,14 @@ def main():
11021193
print(f"ERROR: Invalid offset format '{args.offset}'. Expected format: 'z-y-x' or 'y-x' for 2D (e.g., '0-0-0' or '0-0')")
11031194
sys.exit(1)
11041195

1196+
if args.bbox:
1197+
try:
1198+
bbox = parse_bbox_arg(args.bbox)
1199+
volumes = crop_volumes_to_bbox(volumes, bbox, default_offset=offset)
1200+
except ValueError as e:
1201+
print(f"ERROR: Invalid bbox '{args.bbox}': {e}")
1202+
sys.exit(1)
1203+
11051204
# Start visualization (returns viewer for interactive access)
11061205
viewer = visualize_volumes(
11071206
volumes=volumes,

0 commit comments

Comments
 (0)