diff --git a/FastSurferCNN/data_loader/conform.py b/FastSurferCNN/data_loader/conform.py index 840dd22f2..1e3316095 100644 --- a/FastSurferCNN/data_loader/conform.py +++ b/FastSurferCNN/data_loader/conform.py @@ -1566,10 +1566,15 @@ def conformed_vox_img_size( target_img_size: IntVector3d | None MAX_VOX_SIZE = 1.0 MAX_DIMENSION = 256 + if vox_eps <= 0: + raise ValueError(f"vox_eps must be > 0, got {vox_eps}.") + # number of decimals to round voxel sizes to, so that vox_eps-sized float noise does not affect + # results; clamp at 0 so that vox_eps > 1 does not round voxel sizes to tens of mm + decimals = max(0, int(np.ceil(-np.log10(vox_eps)))) # this is similar to mri_convert --conform_min, note, vox_size == 'auto' is extra, but not covered by VoxSizeOption if isinstance(vox_size, str) and (vox_size := cast(VoxSizeOption, vox_size.lower())) in ["min", "auto"]: # find minimal voxel side length - min_vox_size = np.round(np.min(img.header.get_zooms()[:3]), decimals=int(np.ceil(-np.log10(vox_eps)))) + min_vox_size = np.round(np.min(img.header.get_zooms()[:3]), decimals=decimals) # set to 1 mm if larger than that _conformed_vox_size = min(min_vox_size, MAX_VOX_SIZE) if threshold_1mm and _conformed_vox_size > threshold_1mm: @@ -1600,9 +1605,18 @@ def conformed_vox_img_size( if target_vox_size is not None: # correct sizes for changing voxel size (if voxel size is changing) # compute field of view dimensions in mm (in native orientation) - fov = np.array(img.header.get_zooms()[:3]) * target_img_size - # compute number of voxels needed to cover field of view - target_img_size = np.ceil((fov / target_vox_size * 10000).astype(int).astype(float) / 10000).astype(int) + fov = np.array(np.round(img.header.get_zooms()[:3], decimals=decimals)) * target_img_size + # number of voxels needed to cover the field of view + n_vox = fov / target_vox_size + # n_vox is integer when the fov is a multiple of the voxel size, but floating-point + # error makes it only approximately so. Storing the zoom as float32 leaves a relative + # residual of ~1e-8 that scales with the voxel count, i.e. machine precision rather + # than a voxel-size tolerance. Snap counts that are integer within that relative error + # and round up only genuine partial voxels. + rounded = np.rint(n_vox) + target_img_size = np.where( + np.isclose(n_vox, rounded, rtol=1e-6, atol=0.0), rounded, np.ceil(n_vox) + ).astype(int) # use cube (same size in all directions) with MAX_DIMENSION in each direction as minimum if _img_size == "auto": target_img_size = np.full_like(np.maximum(MAX_DIMENSION, target_img_size), np.amax(target_img_size))