def init_brickwall(self, volume_service, subset_labels, roi): sbm = None if roi["name"]: base_service = volume_service.base_service if not roi["server"] or not roi["uuid"]: assert isinstance(base_service, DvidVolumeService), \ "Since you aren't using a DVID input source, you must specify the ROI server and uuid." roi["server"] = (roi["server"] or volume_service.server) roi["uuid"] = (roi["uuid"] or volume_service.uuid) if roi["scale"] is not None: scale = roi["scale"] elif isinstance(volume_service, ScaledVolumeService): scale = volume_service.scale_delta assert scale <= 5, \ "The 'roi' option doesn't support volumes downscaled beyond level 5" else: scale = 0 brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**(5-scale)).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, 2**(5-scale)) seg_box_s0 = seg_box * 2**scale seg_box_s5 = seg_box // 2**(5-scale) with Timer(f"Fetching mask for ROI '{roi['name']}' ({seg_box_s0[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(roi["server"], roi["uuid"], roi["name"], format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask(roi_mask_s5, seg_box, 2**(5-scale)) elif subset_labels: try: sbm = volume_service.sparse_block_mask_for_labels([*subset_labels]) if ((sbm.box[1] - sbm.box[0]) == 0).any(): raise RuntimeError("Could not find sparse masks for any of the subset-labels") except NotImplementedError: sbm = None with Timer("Initializing BrickWall", logger): # Aim for 2 GB RDD partitions when loading segmentation GB = 2**30 target_partition_size_voxels = 2 * GB // np.uint64().nbytes # Apply halo WHILE downloading the data. # TODO: Allow the user to configure whether or not the halo should # be fetched from the outset, or added after the blocks are loaded. halo = self.config["connectedcomponents"]["halo"] brickwall = BrickWall.from_volume_service(volume_service, 0, None, self.client, target_partition_size_voxels, halo, sbm, compression='lz4_2x') return brickwall
def init_boxes(self, volume_service, subset_labels, roi): sbm = None if roi: base_service = volume_service.base_service assert isinstance(base_service, DvidVolumeService), \ "Can't specify an ROI unless you're using a dvid input" assert isinstance(volume_service, (ScaledVolumeService, DvidVolumeService)), \ "The 'roi' option doesn't support adapters other than 'rescale-level'" scale = 0 if isinstance(volume_service, ScaledVolumeService): scale = volume_service.scale_delta assert scale <= 5, \ "The 'roi' option doesn't support volumes downscaled beyond level 5" server, uuid, _seg_instance = base_service.instance_triple brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**(5-scale)).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, brick_shape) seg_box_s0 = seg_box * 2**scale seg_box_s5 = seg_box // 2**(5 - scale) with Timer( f"Fetching mask for ROI '{roi}' ({seg_box_s0[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(server, uuid, roi, format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask.create_from_highres_mask( roi_mask_s5, 2**(5 - scale), seg_box, brick_shape) elif subset_labels: try: sbm = volume_service.sparse_block_mask_for_labels( [*subset_labels]) if ((sbm.box[1] - sbm.box[0]) == 0).any(): raise RuntimeError( "Could not find sparse masks for any of the subset-labels" ) except NotImplementedError: sbm = None if sbm is None: boxes = boxes_from_grid(volume_service.bounding_box_zyx, volume_service.preferred_message_shape, clipped=True) return np.array([*boxes]) else: return sbm.sparse_boxes(brick_shape)
def init_boxes(self, volume_service, roi): if not roi["name"]: boxes = boxes_from_grid(volume_service.bounding_box_zyx, volume_service.preferred_message_shape, clipped=True) return np.array([*boxes]) base_service = volume_service.base_service if not roi["server"] or not roi["uuid"]: assert isinstance(base_service, DvidVolumeService), \ "Since you aren't using a DVID input source, you must specify the ROI server and uuid." roi["server"] = (roi["server"] or volume_service.server) roi["uuid"] = (roi["uuid"] or volume_service.uuid) if roi["scale"] is not None: scale = roi["scale"] elif isinstance(volume_service, ScaledVolumeService): scale = volume_service.scale_delta assert scale <= 5, \ "The 'roi' option doesn't support volumes downscaled beyond level 5" else: scale = 0 brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**(5-scale)).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, 2**(5 - scale)) seg_box_s0 = seg_box * 2**scale seg_box_s5 = seg_box // 2**(5 - scale) with Timer( f"Fetching mask for ROI '{roi['name']}' ({seg_box_s0[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(roi["server"], roi["uuid"], roi["name"], format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask(roi_mask_s5, seg_box, 2**(5 - scale)) boxes = sbm.sparse_boxes(brick_shape) # Clip boxes to the true (not rounded) bounding box boxes[:, 0] = np.maximum(boxes[:, 0], volume_service.bounding_box_zyx[0]) boxes[:, 1] = np.minimum(boxes[:, 1], volume_service.bounding_box_zyx[1]) return boxes
def init_boxes(self, volume_service, roi): if not roi: boxes = boxes_from_grid(volume_service.bounding_box_zyx, volume_service.preferred_message_shape, clipped=True) return np.array([*boxes]) base_service = volume_service.base_service assert isinstance(base_service, DvidVolumeService), \ "Can't specify an ROI unless you're using a dvid input" assert isinstance(volume_service, (ScaledVolumeService, DvidVolumeService)), \ "The 'roi' option doesn't support adapters other than 'rescale-level'" scale = 0 if isinstance(volume_service, ScaledVolumeService): scale = volume_service.scale_delta assert scale <= 5, \ "The 'roi' option doesn't support volumes downscaled beyond level 5" server, uuid, _seg_instance = base_service.instance_triple brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**(5-scale)).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, 2**(5 - scale)) seg_box_s0 = seg_box * 2**scale seg_box_s5 = seg_box // 2**(5 - scale) with Timer( f"Fetching mask for ROI '{roi}' ({seg_box_s0[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(server, uuid, roi, format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask(roi_mask_s5, seg_box, 2**(5 - scale)) boxes = sbm.sparse_boxes(brick_shape) # Clip boxes to the true (not rounded) bounding box boxes[:, 0] = np.maximum(boxes[:, 0], volume_service.bounding_box_zyx[0]) boxes[:, 1] = np.minimum(boxes[:, 1], volume_service.bounding_box_zyx[1]) return boxes
def init_boxes(self, volume_service, roi, chunk_shape_s0): """ Return a set of bounding boxes to tile the given ROI. Scale 0 of the volume service should correspond to full-res data, which is 32x higher-res than ROI resolution. """ if not roi["name"]: boxes = boxes_from_grid(volume_service.bounding_box_zyx, chunk_shape_s0, clipped=True) return np.array([*boxes]) base_service = volume_service.base_service if not roi["server"] or not roi["uuid"]: assert isinstance(base_service, DvidVolumeService), \ "Since you aren't using a DVID input source, you must specify the ROI server and uuid." roi["server"] = (roi["server"] or volume_service.server) roi["uuid"] = (roi["uuid"] or volume_service.uuid) assert not (chunk_shape_s0 % 2**5).any(), \ "If using an ROI, select a chunk shape that is divisible by 32" seg_box_s0 = volume_service.bounding_box_zyx seg_box_s0 = round_box(seg_box_s0, 2**5) seg_box_s5 = seg_box_s0 // 2**5 with Timer( f"Fetching mask for ROI '{roi['name']}' ({seg_box_s0[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(roi["server"], roi["uuid"], roi["name"], format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask(roi_mask_s5, seg_box_s0, 2**5) boxes = sbm.sparse_boxes(chunk_shape_s0) # Clip boxes to the true (not rounded) bounding box boxes[:, 0] = np.maximum(boxes[:, 0], volume_service.bounding_box_zyx[0]) boxes[:, 1] = np.minimum(boxes[:, 1], volume_service.bounding_box_zyx[1]) return boxes
def init_boxes(self, volume_service, roi): if not roi["name"]: boxes = boxes_from_grid(volume_service.bounding_box_zyx, volume_service.preferred_message_shape, clipped=True) return np.array([*boxes]) server, uuid, roi_name = roi["server"], roi["uuid"], roi["name"] roi_scale = roi["relative-scale"] brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**roi_scale).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, 2**roi_scale) seg_box_s5 = seg_box // 2**roi_scale with Timer( f"Fetching mask for ROI '{roi_name}' ({seg_box[:, ::-1].tolist()})", logger): roi_mask_s5, _ = fetch_roi(server, uuid, roi_name, format='mask', mask_box=seg_box_s5) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask(roi_mask_s5, seg_box, 2**roi_scale) boxes = sbm.sparse_boxes(brick_shape) # Clip boxes to the true (not rounded) bounding box boxes[:, 0] = np.maximum(boxes[:, 0], volume_service.bounding_box_zyx[0]) boxes[:, 1] = np.minimum(boxes[:, 1], volume_service.bounding_box_zyx[1]) return boxes
def main(): configure_default_logging() parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('--no-downres', action='store_true') parser.add_argument('--only-within-roi') parser.add_argument('--not-within-roi') parser.add_argument('dvid_server') parser.add_argument('uuid') parser.add_argument('labelmap_instance') parser.add_argument('sparsevol_files', nargs='+') args = parser.parse_args() instance_info = (args.dvid_server, args.uuid, args.labelmap_instance) assert not args.only_within_roi or not args.not_within_roi, \ "Can't supply both --only-within-roi and --not-within-roi. Pick one or the other (or neither)." roi = args.only_within_roi or args.not_within_roi invert_roi = (args.not_within_roi is not None) if roi: roi_mask, mask_box = fetch_roi(args.dvid_server, args.uuid, roi, format='mask') roi_sbm = SparseBlockMask(roi_mask, mask_box * (2**5), 2**5) # ROIs are provided at scale 5 else: roi_sbm = None # Ideally, we would choose the max label for the node we're writing to, # but the /maxlabel endpoint doesn't work for all nodes # instead, we'll use the repo-wide maxlabel from the /info JSON. #maxlabel = fetch_maxlabel(args.dvid_server, args.uuid, args.labelmap_instance) maxlabel = fetch_instance_info( args.dvid_server, args.uuid, args.labelmap_instance)["Extended"]["MaxRepoLabel"] for i, path in enumerate(args.sparsevol_files): maxlabel += 1 name = os.path.split(path)[1] prefix_logger = PrefixedLogger(logger, f"Vol #{i:02d} {name}: ") with Timer(f"Pasting {name} as {maxlabel}", logger): overwritten_labels = overwrite_sparsevol(*instance_info, maxlabel, path, roi_sbm, invert_roi, args.no_downres, prefix_logger) results_path = os.path.splitext(path)[0] + '.json' with open(results_path, 'w') as f: results = { 'new-label': maxlabel, 'overwritten_labels': sorted(overwritten_labels) } json.dump(results, f, indent=2, cls=NumpyConvertingEncoder) logger.info(f"Done.")
def _init_mask(self): """ - read the mask ROI as a volume - dilate/erode it if necessary - invert it if necessary - save to .h5 (just for offline debug) - return the scale-5 mask and its scale-5 bounding-box """ options = self.config["masksegmentation"] roi = options["mask-roi"] invert_mask = options["invert-mask"] max_scale = options["max-pyramid-scale"] roi_dilation = options["dilate-roi"] roi_erosion = options["erode-roi"] seg_dilation = options["dilate-segmentation"] block_width = self.output_service.block_width # Select a mask_box that's large enough to divide evenly into the # block width even when reduced to the highest scale we'll be processing. seg_box = round_box(self.input_service.bounding_box_zyx, block_width * 2**max_scale) seg_box_s5 = round_box(seg_box, 2**5) // (2**5) with Timer(f"Loading ROI '{roi}'", logger): roi_mask, _ = fetch_roi(self.input_service.server, self.input_service.uuid, roi, format='mask', mask_box=seg_box_s5) with h5py.File('roi-mask.h5', 'w') as f: f.create_dataset('mask', data=roi_mask.view(np.uint8), chunks=(128, 128, 128)) assert not (roi_dilation and roi_erosion) if roi_dilation > 0: with Timer(f"Dilating ROI by {roi_dilation}", logger): roi_mask = vigra.filters.multiBinaryDilation( roi_mask, roi_dilation) with h5py.File('dilated-roi-mask.h5', 'w') as f: f.create_dataset('mask', data=roi_mask.view(np.uint8), chunks=(128, 128, 128)) if roi_erosion > 0: with Timer(f"Eroding ROI by {roi_erosion}", logger): roi_mask = vigra.filters.multiBinaryErosion( roi_mask, roi_erosion) with h5py.File('eroded-roi-mask.h5', 'w') as f: f.create_dataset('mask', data=roi_mask.view(np.uint8), chunks=(128, 128, 128)) assert not seg_dilation or invert_mask, \ "Can't use 'dilate-segmentation'. The segmentation isn't downloaded unless 'invert-mask' is used." if invert_mask: with Timer("Inverting mask", logger): # Initialize the mask with entire segmentation at scale 5, # then subtract the roi from it. boxes = [ *boxes_from_grid(seg_box_s5, (64, 64, 2048), clipped=True) ] input_service = self.input_service def fetch_seg_mask_s5(box_s5): seg_s5 = input_service.get_subvolume(box_s5, scale=5) return box_s5, (seg_s5 != 0) boxes_and_mask = dask.bag.from_sequence( boxes, 1).map(fetch_seg_mask_s5).compute() seg_mask = np.zeros(box_shape(seg_box_s5), bool) for box_s5, box_mask in boxes_and_mask: overwrite_subvol(seg_mask, box_s5, box_mask) if seg_dilation == 0: with h5py.File('segmentation-mask.h5', 'w') as f: f.create_dataset('mask', data=seg_mask.view(np.uint8), chunks=(128, 128, 128)) else: with Timer(f"Dilating segmentation by {seg_dilation}", logger): seg_mask = vigra.filters.multiBinaryDilation( seg_mask, seg_dilation) with h5py.File('dilated-segmentation-mask.h5', 'w') as f: f.create_dataset('mask', data=seg_mask.view(np.uint8), chunks=(128, 128, 128)) seg_mask[roi_mask] = False roi_mask = seg_mask with h5py.File('final-mask.h5', 'w') as f: f.create_dataset('mask', data=roi_mask.view(np.uint8), chunks=(128, 128, 128)) # Downsample the roi_mask to dvid-block resolution, just to see how many blocks it touches. block_mask = view_as_blocks(roi_mask, (2, 2, 2)).any(axis=(3, 4, 5)) blocks_touched = block_mask.sum() voxel_total = blocks_touched * (block_width**3) logger.info( f"Mask touches {blocks_touched} blocks ({voxel_total / 1e9:.1f} Gigavoxels)" ) return roi_mask, seg_box_s5
def export_roi(server, uuid, roi_name, scale, scaled_shape_zyx, parent_output_dir): """ Export the ROI to a PNG stack (as binary images) at the requested scale. Args: server, uuid, roi_name: ROI instance to read scale: What scale to export as, relative to the full-res grayscale. Must be no greater than 5. (ROIs are natively at scale=5, so using that scale will result in no upscaling.) scaled_shape_zyx: The max shape of the exported volume, in scaled coordinates. The PNG stack files always start at (0,0,0), and extend to this shape. Any ROI blocks below 0 or above this shape are silently ignored. parent_output_dir: Where to write the directory of PNG images. (A child directory will be created here and named after the ROI instance.) """ from neuclease.util import view_as_blocks assert not ((scaled_shape_zyx * 2**scale) % 64).any(), \ "The code below assumes that the volume shape is block aligned" # Fetch the ROI-block coords (always scale 5) roi_coords = fetch_roi((server, uuid, roi_name), format='coords') if len(roi_coords) == 0: return output_dir = f'{parent_output_dir}/{roi_name}-mask-scale-{scale}' os.makedirs(output_dir, exist_ok=True) # Create a mask for the scale we're using (hopefully it fits in RAM...) scaled_mask = np.zeros(scaled_shape_zyx, np.uint8) # Create a view of the scaled mask that allows us to broadcast on a per-block basis, # indexed as follows: scaled_mask_view[Bz,By,Bx,f,f,f], # where f = scale_diff_factor = 32 / (2**SCALE) # (ROIs are returned at scale 5!!) scale_diff_factor = (2**5) // (2**scale) scaled_mask_view = view_as_blocks(scaled_mask, 3*(scale_diff_factor,)) roi_box = np.array([roi_coords.min(axis=0), 1+roi_coords.max(axis=0)]) if (roi_box[0] < 0).any() or (roi_box[1] > scaled_mask_view.shape[:3]).any(): # Drop coordinates outside the volume. # (Some ROIs extend beyond our sample.) (Z, Y, X) = scaled_mask_view.shape[:3] #@UnusedVariable roi_coords_df = pd.DataFrame(roi_coords, columns=list('zyx')) roi_coords_df.query('x >= 0 and y >= 0 and z >= 0 and x < @X and y < @Y and z < @Z', inplace=True) roi_coords = roi_coords_df.values roi_box = np.array([roi_coords.min(axis=0), 1+roi_coords.max(axis=0)]) # Apply to the mask scaled_mask_view[tuple(roi_coords.transpose())] = 1 scaled_mask = vigra.taggedView(scaled_mask, 'zyx') for z, z_slice in enumerate(tqdm(scaled_mask, leave=False)): vigra.impex.writeImage(z_slice, f'{output_dir}/{z:05d}.png', 'UINT8')
def init_boxes(self, volume_service, subset_labels, roi): sbm = None if roi: base_service = volume_service.base_service assert isinstance(base_service, DvidVolumeService), \ "Can't specify an ROI unless you're using a dvid input" assert isinstance(volume_service, (ScaledVolumeService, DvidVolumeService)), \ "The 'roi' option doesn't support adapters other than 'rescale-level'" scale = 0 if isinstance(volume_service, ScaledVolumeService): scale = volume_service.scale_delta assert scale <= 5, \ "The 'roi' option doesn't support volumes downscaled beyond level 5" server, uuid, _seg_instance = base_service.instance_triple brick_shape = volume_service.preferred_message_shape assert not (brick_shape % 2**(5-scale)).any(), \ "If using an ROI, select a brick shape that is divisible by 32" seg_box = volume_service.bounding_box_zyx seg_box = round_box(seg_box, brick_shape) seg_box_s5 = seg_box // 2**(5 - scale) with Timer(f"Fetching mask for ROI '{roi}'", logger): roi_mask_s5, roi_box_s5 = fetch_roi(server, uuid, roi, format='mask') # Restrict to input bounding box clipped_roi_box_s5 = box_intersection(seg_box_s5, roi_box_s5) clipped_roi_mask_s5 = extract_subvol( roi_mask_s5, clipped_roi_box_s5 - roi_box_s5[0]) # Align to brick grid aligned_roi_box_s5 = round_box(clipped_roi_box_s5, brick_shape // 2**5, 'out') padding = (aligned_roi_box_s5 - clipped_roi_box_s5) padding[0] *= -1 aligned_roi_mask_s5 = np.pad(clipped_roi_mask_s5, padding.transpose()) # At the service native scale aligned_roi_box = (2**(5 - scale) * aligned_roi_box_s5) logger.info( f"Brick-aligned ROI '{roi}' has bounding-box {aligned_roi_box[:, ::-1].tolist()}" ) # SBM 'full-res' corresponds to the input service voxels, not necessarily scale-0. sbm = SparseBlockMask.create_from_highres_mask( aligned_roi_mask_s5, 2**(5 - scale), aligned_roi_box, brick_shape) elif subset_labels: try: sbm = volume_service.sparse_block_mask_for_labels( [*subset_labels]) if ((sbm.box[1] - sbm.box[0]) == 0).any(): raise RuntimeError( "Could not find sparse masks for any of the subset-labels" ) except NotImplementedError: sbm = None if sbm is None: boxes = boxes_from_grid(volume_service.bounding_box_zyx, volume_service.preferred_message_shape, clipped=True) return np.array([*boxes]) else: boxes = sbm.sparse_boxes(brick_shape) boxes = np.array(boxes) # Clip boxes[:, 0, :] = np.maximum(volume_service.bounding_box_zyx[0], boxes[:, 0, :]) boxes[:, 1, :] = np.minimum(volume_service.bounding_box_zyx[1], boxes[:, 1, :]) assert (boxes[:,0,:] < boxes[:,1,:]).all(), \ "After cropping to input volume, some bricks disappeared." return boxes
def check_in_rois(server, uuid, synapse_df, rois): """ Adds a column 'in_roi' to the given points dataframe indicating whether or not each point is covered by a ROI in the given list of ROIs. Adds the column (IN-PLACE). Args: server: dvid server uuid: Where to pull the rois from synapse_df: A DataFrame with at least columns ['x', 'y', 'z', 'body'] rois: list of strings (roi instance names) Returns: None (Operates in-place) """ num_bodies = len(pd.unique(synapse_df['body'])) logging.info( f"Checking in {len(rois)} ROIs for {len(synapse_df)} synapses from {num_bodies} bodies" ) masks_and_boxes = [] for roi in rois: logger.info(f"Fetching ROI '{roi}'") mask, box = fetch_roi(server, uuid, roi, format='mask') masks_and_boxes.append((mask, box)) _masks, boxes = zip(*masks_and_boxes) boxes = np.array(boxes) # box/shape is in scale-5 coordinates logger.info("Combining ROIs into a single mask") combined_box = (boxes[:, 0, :].min(axis=0), boxes[:, 1, :].max(axis=0)) combined_shape = (combined_box[1] - combined_box[0]) combined_mask = np.zeros(combined_shape, dtype=bool) for mask, box in masks_and_boxes: offset_box = box - combined_box[0] combined_mask[box_to_slicing(*offset_box)] |= mask # Rescale points to scale 5 (ROIs are given at scale 5) logger.info("Scaling points") downsampled_coords_zyx = synapse_df[['z', 'y', 'x']] // (2**5) # Drop everything outside the combined_box logger.info("Excluding OOB points") min_z, min_y, min_x = combined_box[0] #@UnusedVariable max_z, max_y, max_x = combined_box[1] #@UnusedVariable q = 'z >= @min_z and y >= @min_y and x >= @min_x and z < @max_z and y < @max_y and x < @max_x' downsampled_coords_zyx.query(q, inplace=True) logging.info("Extracting mask values") synapse_df['in_roi'] = False downsampled_coords_zyx -= combined_box[0] synapse_df.loc[downsampled_coords_zyx.index, 'in_roi'] = combined_mask[tuple( downsampled_coords_zyx.values.transpose())] roi_synapses = synapse_df['in_roi'].sum() roi_bodies = len(pd.unique(synapse_df['body'][synapse_df['in_roi']])) logging.info( f"Found {roi_synapses} synapses in the ROI from {roi_bodies} bodies")