def test_store_image_as_short_nifti(test_output_dirs: TestOutputDirectories,
                                    norm_method: PhotometricNormalizationMethod,
                                    image_range: Any,
                                    window_level: Any) -> None:
    window, level = window_level if window_level else (400, 0)

    image = np.random.random_sample((1, 2, 3))
    image_shape = image.shape

    args = SegmentationModelBase(norm_method=norm_method, window=window, level=level, should_validate=False)

    # Get integer values that are in the image range
    image1 = LinearTransform.transform(data=image, input_range=(0, 1), output_range=args.output_range)
    image = image1.astype(np.short)  # type: ignore
    header = ImageHeader(origin=(1, 1, 1), direction=(1, 0, 0, 0, 1, 0, 0, 0, 1), spacing=(1, 1, 1))
    nifti_name = test_output_dirs.create_file_or_folder_path(default_image_name)
    io_util.store_image_as_short_nifti(image, header, nifti_name, args)

    if norm_method == PhotometricNormalizationMethod.CtWindow:
        output_range = get_range_for_window_level(args.level, args.window)
        image = LinearTransform.transform(data=image, input_range=args.output_range, output_range=output_range)
        image = image.astype(np.short)
    else:
        image = image * 1000

    t = np.unique(image)
    assert_nifti_content(nifti_name, image_shape, header, list(t), np.short)
Exemplo n.º 2
0
def to_unique_bytes(a: np.ndarray, input_range: Tuple[float, float]) -> Any:
    """Returns an array of unique ubytes after applying LinearTransform on the input array."""
    ubyte_range = (np.iinfo(np.ubyte).min, np.iinfo(np.ubyte).max)
    a = LinearTransform.transform(data=a,
                                  input_range=input_range,
                                  output_range=ubyte_range)
    return np.unique(a.astype(np.ubyte))
Exemplo n.º 3
0
 def _load_and_scale_image(name: str) -> ImageWithHeader:
     image_with_header = load_nifti_image(full_ml_test_data_path(name))
     return ImageWithHeader(image=LinearTransform.transform(
         data=image_with_header.image,
         input_range=(0, 255),
         output_range=(0, 1)),
                            header=image_with_header.header)
Exemplo n.º 4
0
def store_as_nifti(
        image: np.ndarray,
        header: ImageHeader,
        file_name: PathOrString,
        image_type: Union[str, type, np.dtype],
        scale: bool = False,
        input_range: Optional[Iterable[Union[int, float]]] = None,
        output_range: Optional[Iterable[Union[int, float]]] = None) -> Path:
    """
    Saves an image in nifti format (uploading to Azure also if an online Run), and performs the following operations:
    1) transpose the image back into X,Y,Z from Z,Y,X
    2) if scale is true, then performs a linear scaling to either the input/output range or
    byte range (0,255) as default.
    3) cast the image values to the given type before saving

    :param image: 3D image in shape: Z x Y x X.
    :param header: The image header
    :param file_name: The name of the file for this image.
    :param scale: Should perform linear scaling of the image, to desired output range or byte range by default.
    :param input_range: The input range the image belongs to.
    :param output_range: The output range to scale the image to.
    :param image_type: The type to save the image in.
    :return: the path to the saved image
    """
    if image.ndim != 3:
        raise Exception("Image must have 3 dimensions, found: {}".format(
            len(image.shape)))

    if image_type is None:
        raise Exception("You must specify a valid image type.")

    if scale and ((input_range is not None and output_range is None) or
                  (input_range is None and output_range is not None)):
        raise Exception(
            "You must provide both input and output ranges to apply custom linear scaling."
        )

    # rescale image for visualization in the app
    if scale:
        if input_range is not None and output_range is not None:
            # noinspection PyTypeChecker
            image = LinearTransform.transform(
                data=image,
                input_range=input_range,  # type: ignore
                output_range=tuple(output_range)  # type: ignore
            )
        else:
            image = (image + 1) * 255

    image = sitk.GetImageFromArray(image.astype(image_type))
    image.SetSpacing(sitk.VectorDouble(reverse_tuple_float3(
        header.spacing)))  # Spacing needs to be X Y Z
    image.SetOrigin(header.origin)
    image.SetDirection(header.direction)
    sitk.WriteImage(image, str(file_name))
    return Path(file_name)
def test_store_as_scaled_ubyte_nifti(test_output_dirs: TestOutputDirectories, input_range: Any) -> None:
    image = np.random.random_sample((dim_z, dim_y, dim_x))
    header = ImageHeader(origin=(1, 1, 1), direction=(1, 0, 0, 0, 1, 0, 0, 0, 1), spacing=(1, 2, 4))
    io_util.store_as_scaled_ubyte_nifti(image, header,
                                        test_output_dirs.create_file_or_folder_path(default_image_name),
                                        input_range)
    image = LinearTransform.transform(data=image, input_range=input_range, output_range=(0, 255))
    t = np.unique(image.astype(np.ubyte))
    assert_nifti_content(test_output_dirs.create_file_or_folder_path(default_image_name), image.shape, header, list(t),
                         np.ubyte)
Exemplo n.º 6
0
def test_scale_and_unscale_image(
        test_output_dirs: TestOutputDirectories) -> None:
    """
    Test if an image in the CT value range can be recovered when we save dataset examples
    (undoing the effects of CT Windowing)
    """
    image_size = (5, 5, 5)
    spacing = (1, 2, 3)
    header = ImageHeader(origin=(0, 1, 0),
                         direction=(-1, 0, 0, 0, -1, 0, 0, 0, -1),
                         spacing=spacing)
    np.random.seed(0)
    # Random image values with mean -100, std 100. This will cover a range
    # from -400 to +200 HU
    image = np.random.normal(-100, 100, size=image_size)
    window = 200
    level = -100
    # Lower and upper bounds of the interval of raw CT values that will be retained.
    lower = level - window / 2
    upper = level + window / 2
    # Create a copy of the image with all values outside of the (Window, Level) range set to the boundaries.
    # When saving and loading back in, we will not be able to recover any values that fell outside those boundaries.
    image_restricted = image.copy()
    image_restricted[image < lower] = lower
    image_restricted[image > upper] = upper
    # The image will be saved with voxel type short
    image_restricted = image_restricted.astype(int)
    # Apply window and level, mapping to the usual CNN input value range
    cnn_input_range = (-1, +1)
    image_windowed = LinearTransform.transform(data=image,
                                               input_range=(lower, upper),
                                               output_range=cnn_input_range)
    args = SegmentationModelBase(
        norm_method=PhotometricNormalizationMethod.CtWindow,
        output_range=cnn_input_range,
        window=window,
        level=level,
        should_validate=False)

    file_name = test_output_dirs.create_file_or_folder_path(
        "scale_and_unscale_image.nii.gz")
    io_util.store_image_as_short_nifti(image_windowed, header, file_name, args)
    image_from_disk = io_util.load_nifti_image(file_name)
    # noinspection PyTypeChecker
    assert_nifti_content(file_name, image_size, header,
                         np.unique(image_restricted).tolist(), np.short)
    assert np.array_equal(image_from_disk.image, image_restricted)
def test_store_as_nifti(test_output_dirs: TestOutputDirectories, image_type: Any, scale: Any, input_range: Any,
                        output_range: Any) \
        -> None:
    image = np.random.random_sample((dim_z, dim_y, dim_x))
    spacingzyx = (1, 2, 3)
    path_image = test_output_dirs.create_file_or_folder_path(default_image_name)
    header = ImageHeader(origin=(1, 1, 1), direction=(1, 0, 0, 0, 1, 0, 0, 0, 1), spacing=spacingzyx)
    io_util.store_as_nifti(image, header, path_image,
                           image_type, scale, input_range, output_range)
    if scale:
        linear_transform = LinearTransform.transform(data=image, input_range=input_range, output_range=output_range)
        image = linear_transform.astype(image_type)  # type: ignore
    assert_nifti_content(test_output_dirs.create_file_or_folder_path(default_image_name),
                         image.shape, header, list(np.unique(image.astype(image_type))), image_type)

    loaded_image = io_util.load_nifti_image(path_image, image_type)
    assert loaded_image.header.spacing == spacingzyx
def mri_window(image_in: np.ndarray,
               mask: Optional[np.ndarray],
               output_range: Tuple[float, float] = (-1.0, 1.0),
               sharpen: float = 1.9,
               tail: Union[List[float], float] = 1.0,
               debug_mode: bool = False) -> Tuple[np.array, str]:
    """
    This function takes an MRI Image,  removes to first peak of values (air). Then a window range is found centered
    around the mean of the remaining values and with a range controlled by the standard deviation and the sharpen
    input parameter. The larger sharpen is, the wider the range. The resulting values are the normalised to the given
    output_range, with values below and above the range being set the the boundary values.
    :param image_in: The image to normalize.
    :param mask: Consider only pixel values of the input image for which the mask is non-zero. If None the whole
    image is considered.
    :param output_range: The desired value range of the result image.
    :param sharpen: number of standard deviation either side of mean to include in the window
    :param tail: Default 1, allow window range to include more of tail of distribution.
    :param debug_mode: If true, create diagnostic plots.
    :return: normalized image
    """
    nchannel = image_in.shape[0]
    imout = np.zeros_like(image_in)
    if isinstance(tail, int):
        tail = float(tail)
    if isinstance(tail, float):
        tail = [tail] * nchannel
    status = ""
    for ichannel in range(nchannel):
        if ichannel > 0:
            status += "Channel {}: ".format(ichannel)
        # Flatten to apply Otsu_thresholding
        imflat = image_in[ichannel, ...].flatten()
        if mask is None:
            maflat = None
            in_mask = False
        else:
            maflat = mask.flatten()
            in_mask = mask > 0
        # Find Otsu's threshold for the values of the input image
        threshold = threshold_otsu(imflat)
        # Find window level
        level, std_i, _, max_foreground = robust_mean_std(
            imflat[imflat > threshold])
        # If lower value of window is below threshold replace lower value with threshold
        input_range = (max(level - std_i * sharpen, threshold),
                       min(max_foreground,
                           level + tail[ichannel] * std_i * sharpen))
        im_thresh = image_in[ichannel, ...]
        im_thresh[image_in[ichannel, ...] < threshold] = 0
        # Use Polynomial transform function to convert data to output range
        imout[ichannel,
              ...] = LinearTransform.transform(im_thresh, input_range,
                                               output_range)
        status += f"Otsu {threshold:0.0f}, level {level:0.0f}, range ({input_range[0]:0.0f}, {input_range[1]:0.0f}) "
        logging.debug(status)
        if debug_mode:
            print('Otsu {}, range {}'.format(threshold, input_range))
            if mask is None:
                no_thresh = np.sum(imflat < threshold)
                no_high = np.sum(imout == output_range[1])
                pc_thresh = no_thresh / np.numel(imflat) * 100
                pc_high = no_high / np.numel(imflat) * 100
            else:
                no_thresh = np.sum(imflat[maflat == 1] < threshold)
                no_high = np.sum(imout == output_range[1])
                pc_thresh = no_thresh / np.sum(in_mask) * 100
                pc_high = no_high / np.sum(in_mask) * 100

            print('Percent of values outside window range: low,high',
                  pc_thresh, pc_high, no_high)

            with open("channels_trim.txt", 'a') as fileout:
                fileout.write(
                    "Thresholded: {ich:d}, {pl:4.2f}, {ph:4.2f} \n".format(
                        ich=ichannel, pl=pc_thresh, ph=pc_high))

            # Plot input distribution
            fig, axs = plt.subplots(2, 2, figsize=(9, 9))
            axs[0, 0].set_title("Original Image")
            axs[0, 0].imshow(image_in[ichannel, :, :, 70], cmap='gray')
            #    axs[1,0].hist(image.flatten(), 100)
            axs[1, 0].set_title("Original Image - Histogram with Mask")
            if mask is None:
                axs[1, 0].hist(image_in[ichannel, ...].flatten(), 200)
            else:
                axs[1, 0].hist(image_in[ichannel, ...][in_mask].flatten(), 200)
            axs[0, 1].set_title(
                "Normalised Image, Level= {level:4.1f},\n "
                "Window range {in1:4.1f} to {in2:4.1f}, \n"
                "{pt:4.1f} % below threshold, {ph:4.1f} % above window \n"
                "Threshold= {th:4.1f}".format(level=level,
                                              in1=input_range[0],
                                              in2=input_range[1],
                                              pt=pc_thresh,
                                              ph=pc_high,
                                              th=threshold))
            axs[0, 1].imshow(imout[ichannel, :, :, 70], cmap='gray')
            axs[1, 1].set_title("Normalised Image - Histogram with Mask")
            if mask is None:
                axs[1, 1].hist(imout[ichannel, ...].flatten(), 200)
            else:
                axs[1, 1].hist(imout[ichannel, ...][in_mask].flatten(), 200)
            plt.show()

    return imout, status
def normalize_trim(image: np.ndarray,
                   mask: np.ndarray,
                   output_range: Tuple[float, float] = (-1.0, 1.0),
                   sharpen: float = 1.9,
                   trim_percentiles: Tuple[float, float] = (2.0, 98.0),
                   debug_mode: bool = False) -> np.array:
    """
    Normalizes a single image to have mean 0 and standard deviation 1
    Normalising occurs after percentile thresholds have been applied to strip out extreme values

    :param image: The image to normalize, size Channels x Z x Y x X
    :param mask: Consider only pixel values of the input image for which the mask is non-zero. Size Z x Y x X
    :param output_range: The desired value range of the result image.
    :param sharpen: number of standard deviation either side of mean to include in the window.
    :param trim_percentiles: Only consider voxel values between those two percentiles when computing mean and std.
    :param debug_mode: If true, create a diagnostic plot (interactive)
    :return: trimmed-normalized image
    """

    image_shape = image.shape
    imout = np.zeros_like(image)
    in_mask = mask > 0.5
    status = ""
    for ichannel in range(image_shape[0]):
        if ichannel > 0:
            status += "Channel {}: ".format(ichannel)
        channel_image = image[ichannel, ...]
        pixels_inside_mask = channel_image[in_mask].flatten().astype(float)
        # First remove all values that fall outside the trim_percentiles
        thresholds = np.percentile(pixels_inside_mask,
                                   trim_percentiles,
                                   interpolation='midpoint')
        lower_threshold = thresholds[0]
        upper_threshold = thresholds[1]
        above_lower = pixels_inside_mask > lower_threshold
        below_upper = pixels_inside_mask < upper_threshold
        inside_thresholds = np.logical_and(above_lower, below_upper)
        # Compute robust statistics off the pixel values that are inside the trim values
        median, estimated_std, min_value, max_value = robust_mean_std(
            pixels_inside_mask[inside_thresholds])
        # Compute an input value range from median and robust std, going as many standard deviations
        # as specified by the sharpen parameter
        input_range = (max(median - estimated_std * sharpen, min_value),
                       min(median + estimated_std * sharpen, max_value))
        # Use Polynomial transform function to convert data to output range. This also sets values outside
        # the input_range to the boundary values.
        channel_output = LinearTransform.transform(data=channel_image,
                                                   input_range=input_range,
                                                   output_range=output_range)
        channel_output[np.logical_not(in_mask)] = output_range[0]
        imout[ichannel, ...] = channel_output
        status += "Range ({0:0.0f}, {1:0.0f}) ".format(input_range[0],
                                                       input_range[1])
        logging.info(status)
        if debug_mode:
            print('median, estimated_std', median, estimated_std)
            #
            # Normalise values to zero mean and unit variance
            #
            fig, axs = plt.subplots(2, 2, figsize=(9, 9))
            axs[0, 0].set_title("Original Image")
            axs[0, 0].imshow(image[0, :, :, 70], cmap='gray')
            # axs[1,0].hist(image.flatten(), 100)
            axs[1, 0].set_title("Original Image - Histogram with Mask")
            axs[1, 0].set_xlim(lower_threshold, upper_threshold)
            axs[1, 0].hist(channel_image[in_mask].flatten(), 20)
            axs[0, 1].set_title("Normalised Image, Level= {level:4.1f},\n "
                                "Window range {in1} to {in2}".format(
                                    level=median,
                                    in1=lower_threshold,
                                    in2=upper_threshold))
            axs[0, 1].imshow(imout[0, :, :, 70], cmap='gray')
            axs[1, 1].set_title("Normalised Image - Histogram with Mask")
            axs[1, 1].hist(channel_image[in_mask], 20)
            plt.show()
    return imout, status