예제 #1
0
    def from_narrow_png(self, file_name, step_size=0.1):
        """ import from png file

        Source file is a full color PNG, sufficiently narrow that
        density is uniform along its short dimension. The image density along
        its long dimension is reflective of a dose distribution.

        Parameters
        ----------
        file_name : str
        step-size : float, optional

        Returns
        -------
        Profile

        Raises
        ------
        ValueError
            if aspect ratio <= 5, i.e. not narrow
        AssertionError
            if step_size <= 12.7 over dpi, i.e. small

        """
        image_file = PIL.Image.open(file_name)
        assert image_file.mode == "RGB"
        dpi_horiz, dpi_vert = image_file.info["dpi"]

        image_array = mpimg.imread(file_name)

        # DIMENSIONS TO AVG ACROSS DIFFERENT FOR HORIZ VS VERT IMG
        if image_array.shape[0] > 5 * image_array.shape[1]:  # VERT
            image_vector = np.average(image_array, axis=(1, 2))
            pixel_size_in_cm = 2.54 / dpi_vert
        elif image_array.shape[1] > 5 * image_array.shape[0]:  # HORIZ
            image_vector = np.average(image_array, axis=(0, 2))
            pixel_size_in_cm = 2.54 / dpi_horiz
        else:
            raise ValueError("The PNG file is not a narrow strip.")
        assert step_size > 5 * pixel_size_in_cm, "step size too small"

        if image_vector.shape[0] % 2 == 0:
            image_vector = image_vector[:-1]  # SO ZERO DISTANCE IS MID-PIXEL

        length_in_cm = image_vector.shape[0] * pixel_size_in_cm
        full_resolution_distances = np.arange(-length_in_cm / 2,
                                              length_in_cm / 2,
                                              pixel_size_in_cm)

        # TO MOVE FROM FILM RESOLUTION TO DESIRED PROFILE RESOLUTION
        num_pixels_to_avg_over = int(step_size / pixel_size_in_cm)
        sample_indices = np.arange(
            num_pixels_to_avg_over / 2,
            len(full_resolution_distances),
            num_pixels_to_avg_over,
        ).astype(int)
        downsampled_distances = list(full_resolution_distances[sample_indices])

        downsampled_density = []
        for idx in sample_indices:  # AVERAGE OVER THE SAMPLING WINDOW
            avg_density = np.average(
                image_vector[int(idx - num_pixels_to_avg_over /
                                 2):int(idx + num_pixels_to_avg_over / 2)])
            downsampled_density.append(avg_density)

        zipped_profile = list(zip(downsampled_distances, downsampled_density))
        return Profile().from_tuples(zipped_profile)
def read_narrow_png(file_name, step_size=0.1):
    """  Extract a an relative-density profilee from a narrow png file.

    Source file is a full color PNG that is sufficiently narrow that
    density uniform along its short dimension. The image density along
    its long dimension is reflective of a dose distribution. Requires
    Python PIL.

    Parameters
    ----------
    file_name : str
    step-size : float, optional
        Distance output increment in cm, defaults to 1 mm

    Returns
    -------
    array_like
        Image profile as a list of (distance, density) tuples, where density
        is an average color intensity value as represented in the source file.

    Raises
    ------
    ValueError
        If image is not narrow, i.e. aspect ratio <= 5
    AssertionError
        If step_size is too small, i.e. step_size <= 12.7 / dpi

    """

    image_file = Image.open(file_name)
    assert image_file.mode == "RGB"
    dpi_horiz, dpi_vert = image_file.info["dpi"]

    image_array = mpimg.imread(file_name)

    # DIMENSIONS TO AVG ACROSS DIFFERENT FOR HORIZ VS VERT IMG
    if image_array.shape[0] > 5 * image_array.shape[1]:  # VERT
        image_vector = np.average(image_array, axis=(1, 2))
        pixel_size_in_cm = 2.54 / dpi_vert
    elif image_array.shape[1] > 5 * image_array.shape[0]:  # HORIZ
        image_vector = np.average(image_array, axis=(0, 2))
        pixel_size_in_cm = 2.54 / dpi_horiz
    else:
        raise ValueError("The PNG file is not a narrow strip.")
    assert step_size > 5 * pixel_size_in_cm, "step size too small"

    if image_vector.shape[0] % 2 == 0:
        image_vector = image_vector[:-1]  # SO ZERO DISTANCE IS MID-PIXEL

    length_in_cm = image_vector.shape[0] * pixel_size_in_cm
    full_resolution_distances = np.arange(-length_in_cm / 2, length_in_cm / 2,
                                          pixel_size_in_cm)

    # TO MOVE FROM FILM RESOLUTION TO DESIRED PROFILE RESOLUTION
    num_pixels_to_avg_over = int(step_size / pixel_size_in_cm)
    sample_indices = np.arange(
        num_pixels_to_avg_over / 2,
        len(full_resolution_distances),
        num_pixels_to_avg_over,
    ).astype(int)
    downsampled_distances = list(full_resolution_distances[sample_indices])

    downsampled_density = []
    for idx in sample_indices:  # AVERAGE OVER THE SAMPLING WINDOW
        avg_density = np.average(
            image_vector[int(idx - num_pixels_to_avg_over /
                             2):int(idx + num_pixels_to_avg_over / 2)])
        downsampled_density.append(avg_density)

    zipped_profile = list(zip(downsampled_distances, downsampled_density))
    return zipped_profile
def normalise_profile(
    distance,
    relative_dose,
    pdd_distance=None,
    pdd_relative_dose=None,
    scan_depth=None,
    normalisation_position="cra",
    scale_to_pdd=False,
    smoothed_normalisation=False,
):
    """Normalise a profile given a defined normalisation position and
    normalisation scaling
    """
    # If scaling is to PDD interpolate along the PDD to find the scaling,
    # otherwise set scaling to 100.
    if scale_to_pdd:
        # If insufficient information has been supplies raise a meaningful
        # error
        if pdd_distance is None or pdd_relative_dose is None or scan_depth is None:
            raise Exception(
                "Scaling to PDD requires pdd_distance, pdd_relative_dose, "
                "and scan_depth to be defined.")

        pdd_interpolation = interp1d(pdd_distance, pdd_relative_dose)
        scaling = pdd_interpolation(scan_depth)
    else:
        scaling = 100

    # Linear interpolation function
    if smoothed_normalisation:
        filtered = savgol_filter(relative_dose, 21, 2)
        interpolation = interp1d(distance, filtered)
    else:
        interpolation = interp1d(distance, relative_dose)

    try:
        # Check if user wrote a number for normalisation position
        float_position = float(normalisation_position)
    except ValueError:
        # If text was written the conversion to float will fail
        float_position = None

    # If position was given by the user as a number then define the
    # normalisation to that position
    if float_position is not None:
        normalisation = scaling / interpolation(float_position)

    # Otherwise if the user gave 'cra' (case independent) normalise at 0
    elif normalisation_position.lower() == "cra":
        normalisation = scaling / interpolation(0)

    # Otherwise if the user gave 'cm' (case independent) normalise to the
    # centre of mass
    elif normalisation_position.lower() == "cm":
        threshold = 0.5 * np.max(relative_dose)
        weights = relative_dose.copy()
        weights[weights < threshold] = 0

        centre_of_mass = np.average(distance, weights=weights)
        normalisation = scaling / interpolation(centre_of_mass)

    # Otherwise if the user gave 'max' (case independent) normalise to the
    # point of dose maximum
    elif normalisation_position.lower() == "max":
        normalisation = scaling / np.max(relative_dose)

    else:
        raise TypeError("Expected either a float for `normalisation_position` "
                        "or one of 'cra', 'cm', or 'max'")

    return relative_dose * normalisation