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