def _img_convert_type(img: sitk.Image, output_type) -> sitk.Image: """ Convert the img into the desired pixel type. The method safely ( without overflow/underflow ) converts from one pixel range to another. :param img: a SimpleITK Image object :param output_type: a SimpleITK PixelID such as sitkUInt8, sitkFloat32 for the pixel type of the returned image """ # the sub_volume_execute if img.GetPixelID() == sitk.sitkInt8 and output_type == sitk.sitkUInt8: img = sitk.Cast(img, sitk.sitkInt16) img += 128 return sitk.Cast(img, output_type) elif img.GetPixelID() == sitk.sitkInt16 and output_type == sitk.sitkUInt8: img = sitk.Cast(img, sitk.sitkInt32) img += 32768 img /= 256 return sitk.Cast(img, output_type) elif img.GetPixelID() == sitk.sitkInt16 and output_type == sitk.sitkUInt16: img = sitk.Cast(img, sitk.sitkInt32) img += 32768 return sitk.Cast(img, output_type) else: raise Exception( f"Converting from {img.GetPixelIDTypeAsString()} to " f"{sitk.GetPixelIDValueAsString(output_type)} is not implemented.")
def execute(self, image: sitk.Image, params: ImageRegistrationParameters = None) -> sitk.Image: """Registers an image. Args: image (sitk.Image): The image. params (ImageRegistrationParameters): The registration parameters. Returns: sitk.Image: The registered image. """ # todo: replace this filter by a registration. Registration can be costly, therefore, we provide you the # transformation, which you only need to apply to the image! atlas = params.atlas transform = params.transformation is_ground_truth = params.is_ground_truth # the ground truth will be handled slightly different # note: if you are interested in registration, and want to test it, have a look at # pymia.filtering.registration.MultiModalRegistration. Think about the type of registration, i.e. # do you want to register to an atlas or inter-subject? Or just ask us, we can guide you ;-) # interpolator = sitk.sitkCosineWindowedSinc or sitk.sitkAffine if is_ground_truth: image = sitk.Resample(image, atlas, transform, sitk.sitkLinear, 0.0, image.GetPixelID()) else: image = sitk.Resample(image, atlas, transform, sitk.sitkLinear, 0.0, image.GetPixelID()) return image
def execute(self, image: sitk.Image, params: CmdlineExecutorParams = None) -> sitk.Image: """Executes a command line program. Args: image (sitk.Image): The image to filter. params (CmdlineExecutorParams): The execution specific command line parameters. Returns: sitk.Image: The filtered image. """ temp_dir = tempfile.gettempdir() temp_in = os.path.join(temp_dir, 'in.nii') sitk.WriteImage(image, temp_in) temp_out = os.path.join(temp_dir, 'out.nii') cmd = [self.executable_path, temp_in, temp_out] if params is not None: cmd = cmd + params.arguments subprocess.run(cmd, check=True) out_image = sitk.ReadImage(temp_out, image.GetPixelID()) # clean up os.remove(temp_in) os.remove(temp_out) return out_image
def apply_transform_pandas(fixed_image: sitk.Image, moving_image: sitk.Image, reference_path, index=None): transform_path = os.path.join(reference_path.parent, 'Transforms.csv') if index is None: transform_params = blk.read_pandas_row(transform_path, reference_path.name, 'Image') else: transform_params = blk.read_pandas_row(transform_path, index, 'Image') transform = sitk.AffineTransform(2) transform.Rotate(0, 1, transform_params['Rotation'], pre=True) matrix = [ transform_params['Matrix Top Left'], transform_params['Matrix Top Right'], transform_params['Matrix Bottom Left'], transform_params['Matrix Bottom Right'] ] transform.SetMatrix(matrix) transform.SetTranslation( [transform_params['X Translation'], transform_params['Y Translation']]) origin = (int(transform_params['X Origin']), int(transform_params['Y Origin'])) moving_image.SetOrigin(origin) return sitk.Resample(moving_image, fixed_image, transform, sitk.sitkLinear, 0.0, moving_image.GetPixelID())
def clip_intensity(image: sitk.Image, lower: float, upper: float): """Clip image grey level intensities to specified range. The grey level intensities in the resulting image will fall in the range [lower, upper]. Parameters ---------- image The intensity image to clip. lower The lower bound on grey level intensity. Voxels with lower intensity will be set to this value. upper The upper bound on grey level intensity. Voxels with higer intensity will be set to this value. Returns ------- sitk.Image The clipped intensity image. """ return sitk.Clamp(image, image.GetPixelID(), lower, upper)
def float_dilate(image: sitk.Image, dilate: int) -> sitk.Image: r""" Dilate a float valued image. Parameters ---------- image : sitk.Image Image to be dilated. dilate : int Radius of the structuring element. Returns ------- sitk.Image Dilated image. """ if type(dilate) is not int or dilate < 1: raise Exception('float_dilate: invalid dilation value') original_type = image.GetPixelID() image = sitk.Cast(image, sitk.sitkUInt8) image = sitk.BinaryDilate(image, dilate) image = sitk.Cast(image, original_type) return image
def fitting_index(image: sitk.Image, norm: Union[int, str] = 2.0, centre: Tuple[float, float, float] = None, radius: float = None, padding: bool = True) -> float: r""" Compute the fitting index of an input object. The fitting index of order `p` is defined as the Jaccard coefficient computed between the input object and a p-ball centred in the object's centre of mass. Parameters ---------- image : sitk.Image Input binary image of the object. norm : Union[int,str] Order of the Minkowski norm ('inf' or 'max' to use the Chebyshev norm). centre : Tuple[float, float, float] Forces the p-ball to be centred in a specific point. radius : float Force the radius of the p-ball. padding : bool If `True`, add enough padding to be sure that the ball will entirely fit within the volume. Returns ------- float Value of the fitting index. """ if image.GetPixelID() != sitk.sitkUInt8: raise Exception('Unsupported %s pixel type' % image.GetPixelIDTypeAsString()) if centre is None: # Use the centroid as centre lssif = sitk.LabelShapeStatisticsImageFilter() lssif.Execute(image) centre = lssif.GetCentroid(1) if padding: # Add some padding to be sure that an isovolumetric 1-ball can fit # within the same volume of a sphere touching the boundary pad = tuple([x // 4 for x in image.GetSize()]) image = sitk.ConstantPad(image, pad, pad, 0) image.SetOrigin((0, 0, 0)) centre = tuple([x + y for x, y in zip(centre, pad)]) if radius is None: radius = isovolumteric_radius(image, norm) size = image.GetSize() sphere = drawing.create_sphere(radius, size=size, centre=centre, norm=norm) > 0 return jaccard(image, sphere)
def apply_transform_fromfile(fixed_image: sitk.Image, moving_image: sitk.Image, transform_path): transform = sitk.ReadTransform(str(transform_path)) registered_image = sitk.Resample(moving_image, fixed_image, transform, sitk.sitkLinear, 0.0, moving_image.GetPixelID()) meta.copy_relevant_metadata(registered_image, moving_image) return registered_image
def apply_bias_correction(img:sitk.Image): # print('working on N4') initial_img = img img_size = initial_img.GetSize() img_spacing = initial_img.GetSpacing() img_pixel_ID = img.GetPixelID() # Cast to float to enable bias correction to be used image = sitk.Cast(img, sitk.sitkFloat64) image = sitk.GetArrayFromImage(image) image[image == 0] = np.finfo(float).eps image = sitk.GetImageFromArray(image) # reset the origin and direction to what it was initially image.SetOrigin(initial_img.GetOrigin()) image.SetDirection(initial_img.GetDirection()) image.SetSpacing(initial_img.GetSpacing()) maskImage = sitk.OtsuThreshold(image, 0, 1) # Calculating a shrink factor that will be used to reduce image size and increase N4BC speed shrink_factor = [(img_size[0] // 64 if img_size[0] % 128 is not img_size[0] else 1), (img_size[1] // 64 if img_size[1] % 128 is not img_size[1] else 1), (img_size[2] // 64 if img_size[2] % 128 is not img_size[2] else 1)] # shrink the image and the otsu masked filter shrink_filter = sitk.ShrinkImageFilter() image_shr = shrink_filter.Execute(image, shrink_factor) maskImage_shr = shrink_filter.Execute(maskImage, shrink_factor) # apply image bias correction using N4 bias correction corrector = sitk.N4BiasFieldCorrectionImageFilter() corrected_image_shr = corrector.Execute(image_shr, maskImage_shr) # extract the bias field by dividing the shrunk image by the corrected shrunk image exp_logBiasField = image_shr / corrected_image_shr # resample the bias field to match original image reference_image2 = sitk.Image(img_size, exp_logBiasField.GetPixelIDValue()) reference_image2.SetOrigin(initial_img.GetOrigin()) reference_image2.SetDirection(initial_img.GetDirection()) reference_image2.SetSpacing(img_spacing) resampled_exp_logBiasField = sitk.Resample(exp_logBiasField, reference_image2) # extract the corrected image by dividing the initial image by the resampled bias field that was calculated earlier divide_filter2 = sitk.DivideImageFilter() corrected_image = divide_filter2.Execute(image, resampled_exp_logBiasField) # cast back to initial type to allow for further processing corrected_image = sitk.Cast(corrected_image, img_pixel_ID) return corrected_image
def mask(image: sitk.Image, mask: sitk.Image, jacobian: bool = False) -> sitk.Image: r""" Mask an image (special meaning for Jacobian maps). Parameters ---------- image : sitk.Image Input image to be masked (possibly float). mask : sitk.Image Mask (possibly float). jacobian : bool If true, the background after masking is set to one, if false to zero. Returns ------- sitk.Image The masked image. """ if jacobian: background = np.logical_not( sitk.GetArrayViewFromImage(mask)).astype(np_float_type) background = sitk.GetImageFromArray(background) background = sitk.Cast(background, image.GetPixelID()) background.CopyInformation(image) cast_mask = sitk.Cast(mask, image.GetPixelID()) cast_mask.CopyInformation(image) result = sitk.Multiply(image, cast_mask) result = sitk.Add(result, background) else: cast_mask = sitk.Cast(mask, image.GetPixelID()) cast_mask.CopyInformation(image) result = sitk.Multiply(image, cast_mask) return result
def supervised_register_images(fixed_image: sitk.Image, moving_image: sitk.Image, initial_transform: sitk.Transform = None, moving_path=None, registration_parameters: dict = None): """Register two images :param fixed_image: image that is being registered to :param moving_image: image that is being transformed and registered :param initial_transform: the type of registration/transform, e.g. affine or euler :param registration_parameters: dictionary of registration key/value arguments :return: Registered image, corresponding transform, metric, and stop """ # todo: Re-enable registering for RGB images while True: registration_method = define_registration_method( registration_parameters) fixed_final, moving_final, region_extracted = query_for_changes( fixed_image, moving_image, initial_transform, registration_method, moving_path) reg_plot = RegistrationPlot(fixed_final, moving_final, transform=initial_transform) (transform, metric, stop) = register(fixed_final, moving_final, reg_plot, registration_method=registration_method, initial_transform=initial_transform) if region_extracted: itkplt.plot_overlay(fixed_image, moving_image, transform, downsample=False) if query_good_registration(transform, metric, stop): break # todo: change registration method query here registered_image = sitk.Resample(moving_image, fixed_image, transform, sitk.sitkLinear, 0.0, moving_image.GetPixelID()) meta.copy_relevant_metadata(registered_image, moving_image) plt.close('all') return registered_image, transform, metric, stop
def level_set_cut_v2(image: sitk.Image, seed: list, pred_image_name: str) -> (sitk.Image, int): assert isinstance(image, sitk.Image) and image.GetPixelID() == sitk.sitkUInt8 seed = map(lambda each: list(map(int, each)), seed) ft = sitk.Image(image.GetSize(), sitk.sitkUInt8) ft.CopyInformation(image) logger.info(pred_image_name + ' have ' + str(len(list(seed))) + ' lesion region(s)') stats = sitk.LabelStatisticsImageFilter() factor = 1.8 lsFilter = sitk.ThresholdSegmentationLevelSetImageFilter() lsFilter.SetMaximumRMSError(0.02) lsFilter.SetNumberOfIterations(500) lsFilter.SetCurvatureScaling(.5) lsFilter.SetPropagationScaling(1) lsFilter.ReverseExpansionDirectionOn() ex_flag = False for each in seed: tmp_seg = sitk.Image(image.GetSize(), sitk.sitkUInt8) tmp_seg.CopyInformation(image) tmp_seg[each] = 1 tmp_seg = sitk.BinaryDilate(tmp_seg, 3) assert isinstance(tmp_seg, sitk.Image) init_ls = sitk.SignedMaurerDistanceMap(tmp_seg, insideIsPositive=True, useImageSpacing=True) stats.Execute(image, tmp_seg) lower_threshold = stats.GetMean(1) - factor * stats.GetSigma( 1) # - math.log(stats.GetMean(1)) upper_threshold = stats.GetMean(1) + factor * stats.GetSigma( 1) # + math.log(stats.GetMean(1)) logger.info('the lower_threshold and upper_threshold :' + \ str(lower_threshold) + ' ' + str(upper_threshold)) if lower_threshold == 0 or upper_threshold == 0: logger.warn('Threshold Error. Ignoring...') continue ex_flag = True lsFilter.SetLowerThreshold(lower_threshold) lsFilter.SetUpperThreshold(upper_threshold) ls = lsFilter.Execute(init_ls, sitk.Cast(image, sitk.sitkFloat32)) assert isinstance(ls, sitk.Image) ft += ls if ex_flag == True: return ft, 1 return ft, -1
def apply_transform_params(fixed_image: sitk.Image, moving_image: sitk.Image, transform_params, transform_type): """ Apply a transform based on a list of parameters associated to that transform :param fixed_image: Image whose origin/FOV/Spacing the transform will map to :param moving_image: Image the transform is being applied to :param transform_params: List of transform parameters :param transform_type: A blank transform. :return: """ transform_type.SetParameters(transform_params) return sitk.Resample(moving_image, fixed_image, transform_type, sitk.sitkLinear, 0.0, fixed_image.GetPixelID())
def __init__(self, image: sitk.Image): """Initializes a new instance of the ImageInformation class. Args: image (sitk.Image): The image whose properties to hold. """ self.size = image.GetSize() self.origin = image.GetOrigin() self.spacing = image.GetSpacing() self.direction = image.GetDirection() self.dimensions = image.GetDimension() self.number_of_components_per_pixel = image.GetNumberOfComponentsPerPixel() self.pixel_id = image.GetPixelID()
def compute_dice(b1: sitk.Image, b2: sitk.Image): b2 = sitk.Resample( b2, b1.GetSize(), sitk.Transform(), sitk.sitkNearestNeighbor, b1.GetOrigin(), b1.GetSpacing(), b1.GetDirection(), 0, b2.GetPixelID(), ) labstats = sitk.LabelOverlapMeasuresImageFilter() labstats.Execute(b1, b2) return labstats.GetDiceCoefficient()
def blend_images(fixed_sitk: sitk.Image, moving_sitk: sitk.Image, transform: sitk.Transform = None) -> sitk.Image: if not transform: transform = sitk.Transform() moving_sitk_resampled = sitk.Resample( moving_sitk, fixed_sitk, transform, sitk.sitkLinear, 0.0, moving_sitk.GetPixelID(), ) resulting_image = sitk.Compose(moving_sitk_resampled, fixed_sitk, fixed_sitk) # red and cyan colors return resulting_image
def execute(self, image: sitk.Image, params: fltr.IFilterParams=None) -> sitk.Image: """Executes a command line program. Args: image (sitk.Image): The image. params (fltr.IFilterParams): The parameters (unused). Returns: sitk.Image: The filtered image. """ temp_dir = tempfile.gettempdir() temp_in = os.path.join(temp_dir, 'in.nii') sitk.WriteImage(image, temp_in) temp_out = os.path.join(temp_dir, 'out.nii') subprocess.run([self.executable_path, temp_in, temp_out], check=True) out_image = sitk.ReadImage(temp_out, image.GetPixelID()) # clean up os.remove(temp_in) os.remove(temp_out) return out_image
def __init__(self, image: sitk.Image): """Represents ITK image properties. Holds common ITK image meta-data such as the size, origin, spacing, and direction. See Also: SimpleITK provides `itk::simple::Image::CopyInformation`_ to copy image information. .. _itk::simple::Image::CopyInformation: https://itk.org/SimpleITKDoxygen/html/classitk_1_1simple_1_1Image.html#afa8a4757400c414e809d1767ee616bd0 Args: image (sitk.Image): The image whose properties to hold. """ self.size = image.GetSize() self.origin = image.GetOrigin() self.spacing = image.GetSpacing() self.direction = image.GetDirection() self.dimensions = image.GetDimension() self.number_of_components_per_pixel = image.GetNumberOfComponentsPerPixel() self.pixel_id = image.GetPixelID()
def minkowski_compactness(image: sitk.Image, norm: Union[int, str] = 1.0) -> float: r""" Compute the Minkovski compactness of a binary image. Minkowski compactness of an object is defined as the ratio between the volume of the object and the volume of a p-ball centred on the object's centre of mass, maximised agaist spatial rotations of the ball. Parameters ---------- image : sitk.Image Input binary image. norm : Union[int,str] Order of the Minkowski norm ('inf' or 'max' to use the Chebyshev norm). Returns ------- float Value of the Minkowski norm. """ if image.GetPixelID() != sitk.sitkUInt8: raise Exception('Unsupported %s pixel type' % image.GetPixelIDTypeAsString()) if norm == 'inf' or norm == 'max': descriptor = 'cubeness' elif norm == 1.0: descriptor = 'octahedroness' else: raise Exception('Unsupported value %s for the norm parameter' % str(norm)) lssif = sitk.LabelShapeStatisticsImageFilter() lssif.Execute(image) a = sitk.GetArrayViewFromImage(image) > 0 return _disptools.shape_descriptor(a, lssif.GetCentroid(1), image.GetSpacing(), descriptor)
def get_reference_image( floating_sitk: sitk.Image, spacing: TypeTripletFloat, ) -> sitk.Image: old_spacing = np.array(floating_sitk.GetSpacing()) new_spacing = np.array(spacing) old_size = np.array(floating_sitk.GetSize()) new_size = old_size * old_spacing / new_spacing new_size = np.ceil(new_size).astype(np.uint16) new_size[old_size == 1] = 1 # keep singleton dimensions new_origin_index = 0.5 * (new_spacing / old_spacing - 1) new_origin_lps = floating_sitk.TransformContinuousIndexToPhysicalPoint( new_origin_index) reference = sitk.Image( new_size.tolist(), floating_sitk.GetPixelID(), floating_sitk.GetNumberOfComponentsPerPixel(), ) reference.SetDirection(floating_sitk.GetDirection()) reference.SetSpacing(new_spacing.tolist()) reference.SetOrigin(new_origin_lps) return reference
def sphericity(image: sitk.Image) -> float: r""" Measure the sphericity of a object. The sphericity is defined [5]_ [6]_ as the ratio between the surface of a sphere with the same volume of the object and the surface of the object itself. .. math:: \frac{\pi^{\frac{1}{3}}(6V)^{\frac{2}{3}}}{A} A sphere has sphericity equal to 1, non-spherical objects have sphericity strictly lesser than 1. References ---------- .. [5] Wadell, Hakon. "Volume, shape, and roundness of quartz particles." The Journal of Geology 43.3 (1935): 250-280. .. [6] Lehmann, Gaëthan. "Label object representation and manipulation with ITK" Insight Journal, July-December 2007 Parameters ---------- image : sitk.Image Input binary (sitkUInt8) image. Returns ------- float A floating point value of sphericity in the interval [0, 1]. """ if image.GetPixelID() != sitk.sitkUInt8: raise Exception('Unsupported %s pixel type' % image.GetPixelIDTypeAsString()) lssif = sitk.LabelShapeStatisticsImageFilter() lssif.Execute(image) return lssif.GetRoundness(1)
def resample(fixed_image: sitk.Image, moving_image: sitk.Image, transform: sitk.Transform = None, *, fusion=False, projection=False, combine=False, invert=False ) -> sitk.Image: """Resample fixed_image onto the coordinates of moving_image with transform results from registration. The registration process results in a transform which maps points from the coordinates of the fixed_image to the moving_image. This method is next used to resample the the fixed_image with the transform to produce an image with the fixed_image aligned with the moving_image. If no transform is provided, then an identity transform is assumed and the moving_image is still resampled onto the fixed_image. This operation is useful to see alignment of images before registration. :param fixed_image: A 3D SimpleITK Image whose pixel values are resampled :param moving_image: A 3D SimpleITK Image whose coordinates are used for the output image :param transform: (optional) A 3D SimpleITK Transform mapping from points from the fixed_image to the moving_image. :param fusion: Enable fusing the resampled moving_image and the fixed_image into a RGB image. :param projection: Enable perform a z-projection to reduce the dimensionality to 2D. :param combine: Enable combining the resampled moving_image and the fixed_image into a 2-channel vector image. :param invert: Invert the input transform. :return: The processed SimpleITK Image. """ if not transform: transform = sitk.Transform(3, sitk.sitkIdentity) if invert: transform = transform.GetInverse() if fusion or combine: _logger.info("Fusing images...") resampled_image = _combine_images(fixed_image, moving_image, transform, fusion=fusion) else: output_pixel_type = moving_image.GetPixelID() _logger.info("Resampling image...") resampler = sitk.ResampleImageFilter() resampler.SetOutputDirection(fixed_image.GetDirection()) resampler.SetOutputOrigin(fixed_image.GetOrigin()) resampler.SetOutputSpacing(fixed_image.GetSpacing()) resampler.SetSize(fixed_image.GetSize()) resampler.SetOutputPixelType(output_pixel_type) resampler.SetDefaultPixelValue(0) resampler.SetInterpolator(sitk.sitkLinear) resampler.SetTransform(transform) resampled_image = resampler.Execute(moving_image) if projection: _logger.info("Projecting image...") proj_size = resampled_image.GetSize()[:2] + (0,) output_pixel_type = resampled_image.GetPixelID() projection_image = sitk.Cast(sitk.MeanProjection(resampled_image, projectionDimension=2), output_pixel_type) resampled_image = sitk.Extract(projection_image, size=proj_size, directionCollapseToStrategy=sitk.ExtractImageFilter.DIRECTIONCOLLAPSETOIDENTITY) return resampled_image
def registration(fixed_image: sitk.Image, # noqa: C901 moving_image: sitk.Image, *, do_fft_initialization=True, do_affine2d=False, do_affine3d=True, ignore_spacing=True, sigma=1.0, auto_mask=False, samples_per_parameter=5000, expand=None) -> sitk.Transform: """Robust multi-phase registration for multi-panel confocal microscopy images. The fixed and moving image are expected to be the same molecular labeling, and the same imaged regioned. The phase available are: - fft initialization for translation estimation - 2D affine which can correct rotational acquisition problems, this phase is done on z-projections to optimize a \ 2D similarity transform followed by 2D affine - 3D affine robust mulit-level registration :param fixed_image: a scalar SimpleITK 3D Image :param moving_image: a scalar SimpleITK 3D Image :param do_fft_initialization: perform FFT based cross correlation for initialize translation :param do_affine2d: perform registration on 2D images from z-projection :param do_affine3d: multi-level affine transform :param ignore_spacing: internally adjust spacing magnitude to be near 1 to avoid numeric stability issues with \ micro sized spacing :param sigma: scalar to change the amount of Gaussian smoothing performed :param auto_mask: ignore zero valued pixels connected to the image boarder :param samples_per_parameter: the number of image samples to used per transform parameter at full resolution :param expand: Perform super-sampling to increase number of z-slices by an integer factor. Super-sampling is \ automatically performed when the number of z-slices is less than 5. :return: A SimpleITK transform mapping points from the fixed image to the moving. This may be a CompositeTransform. """ # Identity transform will be returned if all registration steps are disabled by # the calling function. result = sitk.Transform() initial_translation_3d = True moving_mask = None fixed_mask = None number_of_samples_per_parameter = samples_per_parameter expand_factors = None if expand: expand_factors = [1, 1, expand] if fixed_image.GetPixelID() != sitk.sitkFloat32: fixed_image = sitk.Cast(fixed_image, sitk.sitkFloat32) # expand the image if at least 5 in any dimension if not expand_factors: expand_factors = [-(-5//s) for s in fixed_image.GetSize()] if any([e != 1 for e in expand_factors]): _logger.warning("Fixed image under sized in at lease one dimension!" "\tApplying expand factors {0} to image size.".format(expand_factors)) fixed_image = sitk.Expand(fixed_image, expandFactors=expand_factors) if moving_image.GetPixelID() != sitk.sitkFloat32: moving_image = sitk.Cast(moving_image, sitk.sitkFloat32) expand_factors = [-(-5//s) for s in moving_image.GetSize()] if any([e != 1 for e in expand_factors]): _logger.warning("WARNING: Moving image under sized in at lease one dimension!" "\tApplying expand factors {0} to image size.".format(expand_factors)) moving_image = sitk.Expand(moving_image, expandFactors=expand_factors) if auto_mask: fixed_mask = imgf.make_auto_mask(fixed_image) moving_mask = imgf.make_auto_mask(moving_image) if ignore_spacing: # # FORCE THE SPACING magnitude to be normalized near 1.0 # spacing_magnitude = imgf.spacing_average_magnitude(fixed_image) _logger.info("Adjusting image spacing by {0}...".format(1.0/spacing_magnitude)) new_spacing = [s/spacing_magnitude for s in fixed_image.GetSpacing()] _logger.info("\tFixed Image Spacing: {0}->{1}".format(fixed_image.GetSpacing(), new_spacing)) fixed_image.SetSpacing(new_spacing) fixed_image.SetOrigin([o/spacing_magnitude for o in fixed_image.GetOrigin()]) new_spacing = [s / spacing_magnitude for s in moving_image.GetSpacing()] _logger.info("\tMoving Image Spacing: {0}->{1}".format(moving_image.GetSpacing(), new_spacing)) moving_image.SetSpacing(new_spacing) moving_image.SetOrigin([o/spacing_magnitude for o in moving_image.GetOrigin()]) if moving_mask: moving_mask.SetSpacing(new_spacing) moving_mask.SetOrigin([o/spacing_magnitude for o in moving_mask.GetOrigin()]) if fixed_mask: fixed_mask.SetSpacing(new_spacing) fixed_mask.SetOrigin([o / spacing_magnitude for o in fixed_mask.GetOrigin()]) # # # Do FFT based translation initialization # # initial_translation = None if do_fft_initialization: initial_translation = imgf.fft_initialization(moving_image, fixed_image, bin_shrink=8, projection=(not initial_translation_3d)) result = sitk.TranslationTransform(len(initial_translation), initial_translation) # # Do 2D registration first # if do_affine2d: result = register_as_2d_affine(fixed_image, moving_image, sigma_base=sigma, initial_translation=initial_translation, fixed_image_mask=fixed_mask, moving_image_mask=moving_mask) if do_affine3d: _logger.info("Initializing Affine Registration...") if do_affine2d: # set the FFT xcoor initial z translation if do_fft_initialization and len(initial_translation) >= 3: # take the z-translation from the FFT translation = list(result.GetTranslation()) translation[2] = initial_translation[2] result.SetTranslation(translation) _logger.info("Initialized 3D affine with z-translation... {0}".format(translation)) affine = result else: affine = sitk.CenteredTransformInitializer(fixed_image, moving_image, sitk.AffineTransform(3), sitk.CenteredTransformInitializerFilter.GEOMETRY) affine = sitk.AffineTransform(affine) if do_fft_initialization: if len(initial_translation) >= 3: affine.SetTranslation(list(initial_translation)) _logger.info("Initialized 3D affine with z-translation... {0}".format(initial_translation)) else: affine.SetTranslation(list(initial_translation)+[0, ]) affine_result = register_3d(fixed_image, moving_image, initial_transform=affine, sigma_base=sigma, fixed_image_mask=fixed_mask, moving_image_mask=moving_mask, number_of_samples_per_parameter=number_of_samples_per_parameter) result = affine_result if ignore_spacing: # Compose the scaling Transform into a single affine transform # The spacing of the image was modified to do registration, so we need to apply the appropriate scaling to # transform to the space the registration was done in.r scale = spacing_magnitude scale_transform = sitk.ScaleTransform(3) scale_transform.SetScale([scale]*3) result = sitk.CompositeTransform([sitk.Transform(scale_transform), result, scale_transform.GetInverse()]) # if result was a composite transform then we have nested composite # transforms white need to be flattened for writing. result.FlattenTransform() _logger.info(result) return result
def write(self, segmentation: sitk.Image, source_images: List[pydicom.Dataset]) -> pydicom.Dataset: """Writes a DICOM-SEG dataset from a segmentation image and the corresponding DICOM source images. Args: segmentation: A `SimpleITK.Image` with integer labels and a single component per spatial location. source_images: A list of `pydicom.Dataset` which are the source images for the segmentation image. Returns: A `pydicom.Dataset` instance with all necessary information and meta information for writing the dataset to disk. """ if segmentation.GetDimension() != 3: raise ValueError("Only 3D segmentation data is supported") if segmentation.GetNumberOfComponentsPerPixel() > 1: raise ValueError("Multi-class segmentations can only be " "represented with a single component per voxel") if segmentation.GetPixelID() not in [ sitk.sitkUInt8, sitk.sitkUInt16, sitk.sitkUInt32, sitk.sitkUInt64, ]: raise ValueError("Unsigned integer data type required") # TODO Add further checks if source images are from the same series slice_to_source_images = self._map_source_images_to_segmentation( segmentation, source_images) # Compute unique labels and their respective bounding boxes label_statistics_filter = sitk.LabelStatisticsImageFilter() label_statistics_filter.Execute(segmentation, segmentation) unique_labels = set( [x for x in label_statistics_filter.GetLabels() if x != 0]) if len(unique_labels) == 0: raise ValueError("Segmentation does not contain any labels") # Check if all present labels where declared in the DICOM template declared_segments = set( [x.SegmentNumber for x in self._template.SegmentSequence]) missing_declarations = unique_labels.difference(declared_segments) if missing_declarations: missing_segment_numbers = ", ".join( [str(x) for x in missing_declarations]) message = ( f"Skipping segment(s) {missing_segment_numbers}, since their " "declaration is missing in the DICOM template") if not self._skip_missing_segment: raise ValueError(message) logger.warning(message) labels_to_process = unique_labels.intersection(declared_segments) if not labels_to_process: raise ValueError("No segments found for encoding as DICOM-SEG") # Compute bounding boxes for each present label and optionally restrict # the volume to serialize to the joined maximum extent bboxs = { x: label_statistics_filter.GetBoundingBox(x) for x in labels_to_process } if self._inplane_cropping: min_x, min_y, _ = np.min([x[::2] for x in bboxs.values()], axis=0).tolist() max_x, max_y, _ = ( np.max([x[1::2] for x in bboxs.values()], axis=0) + 1).tolist() logger.info( "Serializing cropped image planes starting at coordinates " f"({min_x}, {min_y}) with size ({max_x - min_x}, {max_y - min_y})" ) else: min_x, min_y = 0, 0 max_x, max_y = segmentation.GetWidth(), segmentation.GetHeight() logger.info( f"Serializing image planes at full size ({max_x}, {max_y})") # Create target dataset for storing serialized data result = SegmentationDataset( reference_dicom=source_images[0] if source_images else None, rows=max_y - min_y, columns=max_x - min_x, segmentation_type=SegmentationType.BINARY, ) dimension_organization = DimensionOrganizationSequence() dimension_organization.add_dimension("ReferencedSegmentNumber", "SegmentIdentificationSequence") dimension_organization.add_dimension("ImagePositionPatient", "PlanePositionSequence") result.add_dimension_organization(dimension_organization) writer_utils.copy_segmentation_template( target=result, template=self._template, segments=labels_to_process, skip_missing_segment=self._skip_missing_segment, ) writer_utils.set_shared_functional_groups_sequence( target=result, segmentation=segmentation) # FIX - Use ImageOrientationPatient value from DICOM source rather than the segmentation result.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence[ 0].ImageOrientationPatient = source_images[ 0].ImageOrientationPatient buffer = sitk.GetArrayFromImage(segmentation) for segment in labels_to_process: logger.info(f"Processing segment {segment}") if self._skip_empty_slices: bbox = bboxs[segment] min_z, max_z = bbox[4], bbox[5] + 1 else: min_z, max_z = 0, segmentation.GetDepth() logger.info( "Total number of slices that will be processed for segment " f"{segment} is {max_z - min_z} (inclusive from {min_z} to {max_z})" ) skipped_slices = [] for slice_idx in range(min_z, max_z): frame_index = (min_x, min_y, slice_idx) frame_position = segmentation.TransformIndexToPhysicalPoint( frame_index) frame_data = np.equal( buffer[slice_idx, min_y:max_y, min_x:max_x], segment) if self._skip_empty_slices and not frame_data.any(): skipped_slices.append(slice_idx) continue frame_fg_item = result.add_frame( data=frame_data.astype(np.uint8), referenced_segment=segment, referenced_images=slice_to_source_images[slice_idx], ) frame_fg_item.FrameContentSequence = [pydicom.Dataset()] frame_fg_item.FrameContentSequence[0].DimensionIndexValues = [ segment, # Segment number slice_idx - min_z + 1, # Slice index within cropped volume ] frame_fg_item.PlanePositionSequence = [pydicom.Dataset()] frame_fg_item.PlanePositionSequence[0].ImagePositionPatient = [ f"{x:e}" for x in frame_position ] if skipped_slices: logger.info(f"Skipped empty slices for segment {segment}: " f'{", ".join([str(x) for x in skipped_slices])}') # Encode all frames into a bytearray if self._inplane_cropping or self._skip_empty_slices: num_encoded_bytes = len(result.PixelData) max_encoded_bytes = (segmentation.GetWidth() * segmentation.GetHeight() * segmentation.GetDepth() * len(result.SegmentSequence) // 8) savings = (1 - num_encoded_bytes / max_encoded_bytes) * 100 logger.info( f"Optimized frame data length is {num_encoded_bytes:,}B " f"instead of {max_encoded_bytes:,}B (saved {savings:.2f}%)") result.SegmentsOverlap = "NO" return result
def cubicity(image: sitk.Image) -> float: r""" Measure the cubicity of an object. The cubicity is defined [4]_ as the ratio between the volume of the object and the volume of its bounding cube. A cube has cubicity equal to 1, a sphere has cubicity pi/6, and in general non-cubic objects have cubicity strictly lesser than 1. Here the bounding cube is estimated as the cube whose side is equal to the longest side of the oriented bounding box of the object. References ---------- .. [4] O'Flannery, LJ and O'Mahony, MM, "Precise shape grading of coarse aggregate". Magazine of Concrete Research 51.5 (1999), pp. 319-324. Parameters ---------- image : sitk.Image Input binary (sitkUInt8) image. Returns ------- float A floating point value of cubicity in the interval [0, 1]. """ if image.GetPixelID() != sitk.sitkUInt8: raise Exception('Unsupported %s pixel type' % image.GetPixelIDTypeAsString()) # NOTE: the size of the bounding box is already in image space # units, while the volume (number of voxels) needs to be multiplied # by the voxel size dv = functools.reduce(lambda x, y: x * y, image.GetSpacing(), 1.0) try: lssif = sitk.LabelShapeStatisticsImageFilter() lssif.ComputeOrientedBoundingBoxOn() lssif.Execute(image) (s1, s2, s3) = lssif.GetOrientedBoundingBoxSize(1) volume = lssif.GetNumberOfPixels(1) * dv except AttributeError: # Use ITK as a fallback if the method is not available in # SimpleITK if 'itk' not in sys.modules: raise Exception( 'sitk_to_itk: itk module is required to use this feature.') itk_image = drawing.sitk_to_itk(image) li2slmf = itk.LabelImageToShapeLabelMapFilter.IUC3LM3.New(itk_image) li2slmf.ComputeOrientedBoundingBoxOn() statistics = li2slmf()[0][1] # FIXME GetOrientedBoundingBoxSize() seems to be broken # (s1, s2, s3) = statistics.GetOrientedBoundingBoxSize() # (s1, s2, s3) = statistics.GetBoundingBox().GetSize() # FIXME GetBoundingBox is in voxels, GetOrientedBoundingBox is # in image size instead, so multiply by the spacing when using # GetBoundingBox (s1, s2, s3) = [x * s for x, s in zip([s1, s2, s3], image.GetSpacing())] volume = statistics.GetNumberOfPixels() * dv return volume / (max(s1, s2, s3)**3)