From 2a3ccc31df7ca7e201435c5d335100df010ca827 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 16:05:15 +0000 Subject: [PATCH 01/30] Google-style docstring --- hazenlib/ACRObject.py | 85 ++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/hazenlib/ACRObject.py b/hazenlib/ACRObject.py index da9b6eda..de10d868 100644 --- a/hazenlib/ACRObject.py +++ b/hazenlib/ACRObject.py @@ -47,14 +47,12 @@ def sort_images(self): return img_stack, dicom_stack def orientation_checks(self): - """ - Perform orientation checks on a set of images to determine if slice order inversion or an + """Perform orientation checks on a set of images to determine if slice order inversion or an LR orientation swap is required. - Description - ----------- - This function analyzes the given set of images and their associated DICOM objects to determine if any - adjustments are needed to restore the correct slice order and view orientation. + Note: + This function analyzes the given set of images and their associated DICOM objects to determine if any + adjustments are needed to restore the correct slice order and view orientation. """ test_images = (self.images[0], self.images[-1]) dx = self.pixel_spacing[0] @@ -241,26 +239,21 @@ def circular_mask(centre, radius, dims): return mask def measure_orthogonal_lengths(self, mask): - """ - Compute the horizontal and vertical lengths of a mask, based on the centroid. + """Compute the horizontal and vertical lengths of a mask, based on the centroid. - Parameters: - ---------- - mask : ndarray of bool - Boolean array of the image. + Args: + mask (np.array): Boolean array of the image where pixel values meet threshold Returns: - ---------- - length_dict : dict - A dictionary containing the following information for both horizontal and vertical line profiles: - 'Horizontal Start' | 'Vertical Start' : tuple of int - Horizontal/vertical starting point of the object. - 'Horizontal End' | 'Vertical End' : tuple of int - Horizontal/vertical ending point of the object. - 'Horizontal Extent' | 'Vertical Extent' : ndarray of int - Indices of the non-zero elements of the horizontal/vertical line profile. - 'Horizontal Distance' | 'Vertical Distance' : float - The horizontal/vertical length of the object. + dict: A dictionary containing the following information for both horizontal and vertical line profiles: + 'Horizontal Start' | 'Vertical Start' : tuple of int + Horizontal/vertical starting point of the object. + 'Horizontal End' | 'Vertical End' : tuple of int + Horizontal/vertical ending point of the object. + 'Horizontal Extent' | 'Vertical Extent' : ndarray of int + Indices of the non-zero elements of the horizontal/vertical line profile. + 'Horizontal Distance' | 'Vertical Distance' : float + The horizontal/vertical length of the object. """ dims = mask.shape dx, dy = self.pixel_spacing @@ -296,24 +289,16 @@ def measure_orthogonal_lengths(self, mask): @staticmethod def rotate_point(origin, point, angle): - """ - Compute the horizontal and vertical lengths of a mask, based on the centroid. + """Compute the horizontal and vertical lengths of a mask, based on the centroid. - Parameters: - ---------- - origin : tuple - The coordinates of the point around which the rotation is performed. - point : tuple - The coordinates of the point to rotate. - angle : int - Angle in degrees. + Args: + origin (tuple): The coordinates of the point around which the rotation is performed. + point (tuple): The coordinates of the point to rotate. + angle (int): Angle in degrees. Returns: - ---------- - x_prime : float - A float representing the x coordinate of the desired point after being rotated around an origin. - y_prime : float - A float representing the y coordinate of the desired point after being rotated around an origin. + tuple of float: x_prime and y_prime - A float representing the x and y + coordinates of the desired point after being rotated around an origin. """ theta = np.radians(angle) c, s = np.cos(theta), np.sin(theta) @@ -324,24 +309,18 @@ def rotate_point(origin, point, angle): @staticmethod def find_n_highest_peaks(data, n, height=1): - """ - Find the indices and amplitudes of the N highest peaks within a 1D array. + """Find the indices and amplitudes of the N highest peaks within a 1D array. - Parameters: - ---------- - data : np.array - The array containing the data to perform peak extraction on. - n : int - The coordinates of the point to rotate. - height : int or float - The amplitude threshold for peak identification. + Args: + data (np.array): pixel array containing the data to perform peak extraction on + n (int): The coordinates of the point to rotate + height (int, optional): The amplitude threshold for peak identification. Defaults to 1. Returns: - ---------- - peak_locs : np.array - A numpy array containing the indices of the N highest peaks identified. - peak_heights : np.array - A numpy array containing the amplitudes of the N highest peaks identified. + tuple of np.array: peak_locs and peak_heights + peak_locs: A numpy array containing the indices of the N highest peaks identified. + peak_heights: A numpy array containing the amplitudes of the N highest peaks identified. + """ peaks = scipy.signal.find_peaks(data, height) pk_heights = peaks[1]["peak_heights"] From a8966014fed3bf407bd527e6a8e06c347e33ae34 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 17:18:57 +0000 Subject: [PATCH 02/30] correct formatting for sphinx compatibility --- hazenlib/tasks/slice_width.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/hazenlib/tasks/slice_width.py b/hazenlib/tasks/slice_width.py index 70d59f59..dfd3b300 100644 --- a/hazenlib/tasks/slice_width.py +++ b/hazenlib/tasks/slice_width.py @@ -851,13 +851,12 @@ def get_slice_width(self, dcm): Returns: dict: including - - slice_width_mm: float - calculated slice width (top, bottom, combined; various methods) in mm - - horizontal_linearity_mm, vertical_linearity_mm : float - calculated average rod distance in mm - - horz_distortion_mm, vert_distortion_mm : float - calculated rod distance distortion in mm - + - slice_width_mm (float): + calculated slice width (top, bottom, combined; various methods) in mm + - horizontal_linearity_mm, vertical_linearity_mm (float): + calculated average rod distance in mm + - horz_distortion_mm, vert_distortion_mm (float) + calculated rod distance distortion in mm """ slice_width_mm = {"top": {}, "bottom": {}, "combined": {}} arr = dcm.pixel_array From bf5cc49ea236d528c7183e8be1285126ddfaa161 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:04:32 +0000 Subject: [PATCH 03/30] add Google-style docstrings --- hazenlib/ACRObject.py | 80 +++++++++++++++++--------------------- hazenlib/HazenTask.py | 24 ++++++++++++ hazenlib/logger.py | 1 + hazenlib/tasks/ghosting.py | 22 +++++------ 4 files changed, 72 insertions(+), 55 deletions(-) diff --git a/hazenlib/ACRObject.py b/hazenlib/ACRObject.py index de10d868..4d697840 100644 --- a/hazenlib/ACRObject.py +++ b/hazenlib/ACRObject.py @@ -5,7 +5,16 @@ class ACRObject: + """Base class for performing tasks on image sets of the ACR phantom. + acquired following the ACR Large phantom guidelines + """ + def __init__(self, dcm_list): + """Initialise an ACR object instance + + Args: + dcm_list (list): list of pydicom.Dataset objects - DICOM files loaded + """ # Initialise an ACR object from a stack of images of the ACR phantom self.dcm_list = dcm_list # Load files as DICOM and their pixel arrays into 'images' @@ -24,17 +33,15 @@ def __init__(self, dcm_list): self.mask_image = self.get_mask_image(self.images[6]) def sort_images(self): - """ - Sort a stack of images based on slice position. - - Returns - ------- - img_stack : np.array - A sorted stack of images, where each image is represented as a 2D numpy array. - dcm_stack : pyd - A sorted stack of dicoms - """ + """Sort a stack of images based on slice position. + + Returns: + tuple of lists: + img_stack - list of np.array of dcm.pixel_array + A sorted stack of images, where each image is represented as a 2D numpy array. + dcm_stack - list of pydicom.Dataset objects + """ # TODO: implement a check if phantom was placed in other than axial position # This is to be able to flag to the user the caveat of measurments if deviating from ACR guidance @@ -47,8 +54,8 @@ def sort_images(self): return img_stack, dicom_stack def orientation_checks(self): - """Perform orientation checks on a set of images to determine if slice order inversion or an - LR orientation swap is required. + """Perform orientation checks on a set of images to determine if slice order inversion + or an LR orientation swap is required. Note: This function analyzes the given set of images and their associated DICOM objects to determine if any @@ -105,13 +112,10 @@ def orientation_checks(self): print("LR orientation swap not required.") def determine_rotation(self): - """ - Determine the rotation angle of the phantom using edge detection and the Hough transform. + """Determine the rotation angle of the phantom using edge detection and the Hough transform. - Returns - ------ - rot_angle : float - The rotation angle in degrees. + Returns: + float: The rotation angle in degrees. """ thresh = cv2.threshold(self.images[0], 127, 255, cv2.THRESH_BINARY)[1] @@ -129,13 +133,10 @@ def determine_rotation(self): return rot_angle def rotate_images(self): - """ - Rotate the images by a specified angle. The value range and dimensions of the image are preserved. + """Rotate the images by a specified angle. The value range and dimensions of the image are preserved. - Returns - ------- - np.array: - The rotated images. + Returns: + np.array: The rotated images. """ return skimage.transform.rotate( @@ -147,10 +148,8 @@ def find_phantom_center(self): Find the center of the ACR phantom by filtering the uniformity slice and using the Hough circle detector. - Returns - ------- - centre : tuple - Tuple of ints representing the (x, y) center of the image. + Returns: + tuple of ints: representing the (x, y) center of the image. """ img = self.images[6] dx, dy = self.pixel_spacing @@ -212,22 +211,15 @@ def get_mask_image(self, image, mag_threshold=0.05, open_threshold=500): @staticmethod def circular_mask(centre, radius, dims): - """ - Sort a stack of images based on slice position. - - Parameters - ---------- - centre : tuple - The centre coordinates of the circular mask. - radius : int - The radius of the circular mask. - dims : tuple - The dimensions of the circular mask. - - Returns - ------- - img_stack : np.array - A sorted stack of images, where each image is represented as a 2D numpy array. + """Sort a stack of images based on slice position. + + Args: + centre (tuple): centre coordinates of the circular mask. + radius (int): radius of the circular mask. + dims (tuple): dimensions of the circular mask. + + Returns: + np.array: A sorted stack of images, where each image is represented as a 2D numpy array. """ # Define a circular logical mask x = np.linspace(1, dims[0], dims[0]) diff --git a/hazenlib/HazenTask.py b/hazenlib/HazenTask.py index d86aaa58..0c53a110 100644 --- a/hazenlib/HazenTask.py +++ b/hazenlib/HazenTask.py @@ -9,9 +9,18 @@ class HazenTask: + """Base class for performing tasks on image sets""" + def __init__( self, input_data: list, report: bool = False, report_dir=None, **kwargs ): + """Initialise a HazenTask instance + + Args: + input_data (list): list of filepaths to DICOM images + report (bool, optional): Whether to create measurement visualisation diagrams. Defaults to False. + report_dir (string, optional): Path to output report images. Defaults to None. + """ data_paths = sorted(input_data) self.dcm_list = [dcmread(dicom) for dicom in data_paths] self.report: bool = report @@ -28,6 +37,11 @@ def __init__( self.report_files = [] def init_result_dict(self) -> dict: + """Initialise the dictionary that holds measurement results and input description + + Returns: + dict: holds measurement results and task input description + """ result_dict = { "task": f"{type(self).__name__}", "file": None, @@ -36,6 +50,16 @@ def init_result_dict(self) -> dict: return result_dict def img_desc(self, dcm, properties=None) -> str: + """Obtain values from the DICOM header to identify input series + + Args: + dcm (pydicom.Dataset): DICOM image object + properties (list, optional): list of DICOM header field names supported by pydicom + that shuld be used to generate sereis identifier. Defaults to None. + + Returns: + str: contatenation of the specified DICOM header property values + """ if properties is None: properties = ["SeriesDescription", "SeriesNumber", "InstanceNumber"] try: diff --git a/hazenlib/logger.py b/hazenlib/logger.py index 7888ded6..74439dbd 100644 --- a/hazenlib/logger.py +++ b/hazenlib/logger.py @@ -4,6 +4,7 @@ def configure_logger(): + """Configure logger for the standard out (command line) stream and save logs to file""" # make log formatters stream_formatter = colorlog.ColoredFormatter( "%(log_color)s%(asctime)-15s %(levelname).1s " diff --git a/hazenlib/tasks/ghosting.py b/hazenlib/tasks/ghosting.py index 33d67134..f9e5f8a2 100644 --- a/hazenlib/tasks/ghosting.py +++ b/hazenlib/tasks/ghosting.py @@ -114,11 +114,11 @@ def get_signal_slice(self, bounding_box, slice_radius=5): """_summary_ Args: - bounding_box (_type_): _description_ + bounding_box (tuple): left_column, right_column, upper_row, lower_row slice_radius (int, optional): _description_. Defaults to 5. Returns: - _type_: _description_ + tuple of np.array: _description_ """ left_column, right_column, upper_row, lower_row = bounding_box centre_row = (upper_row + lower_row) // 2 @@ -139,14 +139,14 @@ def get_pe_direction(self, dcm): return dcm.InPlanePhaseEncodingDirection def get_background_rois(self, dcm, signal_centre): - """_summary_ + """Create pixel arrays of the selected regions of interest from the background Args: - dcm (_type_): _description_ - signal_centre (_type_): _description_ + dcm (pydicom.Dataset): DICOM image object + signal_centre (list): x, y coordinates of the centre Returns: - _type_: _description_ + list: pixel arrays of the background regions of interest """ background_rois = [] @@ -205,11 +205,11 @@ def get_background_slices(self, background_rois, slice_radius=5): """_summary_ Args: - background_rois (_type_): _description_ + background_rois (list): list of pixel arrays (np.array) slice_radius (int, optional): _description_. Defaults to 5. Returns: - _type_: _description_ + list: _description_ """ slices = [ ( @@ -230,11 +230,11 @@ def get_eligible_area(self, signal_bounding_box, dcm, slice_radius=5): Args: signal_bounding_box (_type_): _description_ - dcm (_type_): _description_ + dcm (pydicom.Dataset): DICOM image object slice_radius (int, optional): _description_. Defaults to 5. Returns: - _type_: _description_ + tuple of lists: corresponding to eligible_columns, eligible_rows """ left_column, right_column, upper_row, lower_row = signal_bounding_box @@ -352,7 +352,7 @@ def get_ghosting(self, dcm) -> float: img = img.astype("float64") # print('this is img',img) img *= 255.0 / img.max() - # img = hazenlib.utils.rescale_to_byte(dcm.pixel_array) + img = hazenlib.utils.rescale_to_byte(dcm.pixel_array) img = cv.rectangle(img.copy(), (x1, y1), (x2, y2), (255, 0, 0), 1) for roi in background_rois: From 0e28382ef19ce4c9860c7f46bd253510911aaa67 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:04:39 +0000 Subject: [PATCH 04/30] add docstrings --- hazenlib/utils.py | 205 ++++++++++++++++++++++++++++++---------------- 1 file changed, 136 insertions(+), 69 deletions(-) diff --git a/hazenlib/utils.py b/hazenlib/utils.py index bc029779..76bca648 100644 --- a/hazenlib/utils.py +++ b/hazenlib/utils.py @@ -14,6 +14,15 @@ def get_dicom_files(folder: str, sort=False) -> list: + """Collect files in the folder into a list if they are parsable DICOMs + + Args: + folder (str): path to folder + sort (bool, optional): whether to sort file list based on InstanceNumber. Defaults to False. + + Returns: + list: full path to DICOM files found within a folder + """ if sort: file_list = [ os.path.join(folder, x) @@ -31,15 +40,16 @@ def get_dicom_files(folder: str, sort=False) -> list: def is_dicom_file(filename): - """ - Util function to check if file is a dicom file - the first 128 bytes are preamble + """Check if file is a DICOM file, using the the first 128 bytes are preamble the next 4 bytes should contain DICM otherwise it is not a dicom - :param filename: file to check for the DICM header block - :type filename: str - :returns: True if it is a dicom file + Args: + filename (str): path to file to be checked for the DICM header block + + Returns: + bool: True or False whether file is a DICOM """ + # TODO: make it more robust, ensure that file contains a pixel_array file_stream = open(filename, "rb") file_stream.seek(128) data = file_stream.read(4) @@ -51,23 +61,17 @@ def is_dicom_file(filename): def is_enhanced_dicom(dcm: pydicom.Dataset) -> bool: - """ - - Parameters - ---------- - dcm + """Check if file is an enhanced DICOM file - Returns - ------- - bool + Args: + dcm (pydicom.Dataset): DICOM image object - Raises - ------ - Exception - Unrecognised SOPClassUID + Raises: + Exception: Unrecognised_SOPClassUID + Returns: + bool: True or False whether file is an enhanced DICOM """ - if dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.4.1": return True elif dcm.SOPClassUID == "1.2.840.10008.5.1.4.1.1.4": @@ -77,6 +81,17 @@ def is_enhanced_dicom(dcm: pydicom.Dataset) -> bool: def get_manufacturer(dcm: pydicom.Dataset) -> str: + """Get the manufacturer field from the DICOM header + + Args: + dcm (pydicom.Dataset): DICOM image object + + Raises: + Exception: _description_ + + Returns: + str: manufacturer of the scanner used to obtain the DICOM image + """ supported = ["ge", "siemens", "philips", "toshiba", "canon"] manufacturer = dcm.Manufacturer.lower() for item in supported: @@ -87,6 +102,14 @@ def get_manufacturer(dcm: pydicom.Dataset) -> str: def get_average(dcm: pydicom.Dataset) -> float: + """Get the NumberOfAverages field from the DICOM header + + Args: + dcm (pydicom.Dataset): DICOM image object + + Returns: + float: value of the NumberOfAverages field from the DICOM header + """ if is_enhanced_dicom(dcm): averages = ( dcm.SharedFunctionalGroupsSequence[0].MRAveragesSequence[0].NumberOfAverages @@ -98,34 +121,28 @@ def get_average(dcm: pydicom.Dataset) -> float: def get_bandwidth(dcm: pydicom.Dataset) -> float: - """ - Returns PixelBandwidth + """Get the PixelBandwidth field from the DICOM header - Parameters - ---------- - dcm: pydicom.Dataset + Args: + dcm (pydicom.Dataset): DICOM image object - Returns - ------- - bandwidth: float + Returns: + float: value of the PixelBandwidth field from the DICOM header """ bandwidth = dcm.PixelBandwidth return bandwidth def get_num_of_frames(dcm: pydicom.Dataset) -> int: - """ - Returns number of frames of dicom object - - Parameters - ---------- - dcm: pydicom.Dataset - DICOM object + """Get the number of frames from the DICOM pixel_array - Returns - ------- + Args: + dcm (pydicom.Dataset): DICOM image object + Returns: + float: value of the PixelBandwidth field from the DICOM header """ + # TODO: investigate what values could the dcm.pixel_array.shape be and what that means if len(dcm.pixel_array.shape) > 2: return dcm.pixel_array.shape[0] elif len(dcm.pixel_array.shape) == 2: @@ -133,6 +150,14 @@ def get_num_of_frames(dcm: pydicom.Dataset) -> int: def get_slice_thickness(dcm: pydicom.Dataset) -> float: + """Get the SliceThickness field from the DICOM header + + Args: + dcm (pydicom.Dataset): DICOM image object + + Returns: + float: value of the SliceThickness field from the DICOM header + """ if is_enhanced_dicom(dcm): try: slice_thickness = ( @@ -155,6 +180,14 @@ def get_slice_thickness(dcm: pydicom.Dataset) -> float: def get_pixel_size(dcm: pydicom.Dataset) -> (float, float): + """Get the PixelSpacing field from the DICOM header + + Args: + dcm (pydicom.Dataset): DICOM image object + + Returns: + tuple of float: x and y values of the PixelSpacing field from the DICOM header + """ manufacturer = get_manufacturer(dcm) try: if is_enhanced_dicom(dcm): @@ -166,7 +199,7 @@ def get_pixel_size(dcm: pydicom.Dataset) -> (float, float): else: dx, dy = dcm.PixelSpacing except: - print("Warning: Could not find PixelSpacing..") + print("Warning: Could not find PixelSpacing.") if "ge" in manufacturer: fov = get_field_of_view(dcm) dx = fov / dcm.Columns @@ -178,18 +211,16 @@ def get_pixel_size(dcm: pydicom.Dataset) -> (float, float): def get_TR(dcm: pydicom.Dataset) -> float: - """ - Returns Repetition Time (TR) + """Get the RepetitionTime field from the DICOM header - Parameters - ---------- - dcm: pydicom.Dataset + Args: + dcm (pydicom.Dataset): DICOM image object - Returns - ------- - TR: float + Returns: + float: value of the RepetitionTime field from the DICOM header, or defaults to 1000 """ - + # TODO: explore what type of DICOM files do not have RepetitionTime in DICOM header + # check with physicists whether 1000 is an appropriate default value try: TR = dcm.RepetitionTime except: @@ -199,17 +230,16 @@ def get_TR(dcm: pydicom.Dataset) -> float: def get_rows(dcm: pydicom.Dataset) -> float: - """ - Returns number of image rows (rows) + """Get the Rows field from the DICOM header - Parameters - ---------- - dcm: pydicom.Dataset + Args: + dcm (pydicom.Dataset): DICOM image object - Returns - ------- - rows: float + Returns: + float: value of the Rows field from the DICOM header, or defaults to 256 """ + # TODO: explore what type of DICOM files do not have Rows in DICOM header + # check with physicists whether 256 is an appropriate default value try: rows = dcm.Rows except: @@ -222,17 +252,16 @@ def get_rows(dcm: pydicom.Dataset) -> float: def get_columns(dcm: pydicom.Dataset) -> float: - """ - Returns number of image columns (columns) + """Get the Columns field from the DICOM header - Parameters - ---------- - dcm: pydicom.Dataset + Args: + dcm (pydicom.Dataset): DICOM image object - Returns - ------- - columns: float + Returns: + float: value of the Columns field from the DICOM header, or defaults to 256 """ + # TODO: explore what type of DICOM files do not have Columns in DICOM header + # check with physicists whether 256 is an appropriate default value try: columns = dcm.Columns except: @@ -244,6 +273,17 @@ def get_columns(dcm: pydicom.Dataset) -> float: def get_field_of_view(dcm: pydicom.Dataset): + """Get Field of View value from DICOM header depending on manufacturer encoding + + Args: + dcm (pydicom.Dataset): DICOM image object + + Raises: + NotImplementedError: Manufacturer not GE, Siemens, Toshiba or Philips so FOV cannot be calculated. + + Returns: + float: value of the Field of View (calculated as Columns * PixelSpacing[0]) + """ # assumes square pixels manufacturer = get_manufacturer(dcm) @@ -265,7 +305,7 @@ def get_field_of_view(dcm: pydicom.Dataset): fov = dcm.Columns * dcm.PixelSpacing[0] else: raise NotImplementedError( - "Manufacturer not ge,siemens, toshiba or philips so FOV cannot be calculated." + "Manufacturer not GE, Siemens, Toshiba or Philips so FOV cannot be calculated." ) return fov @@ -274,12 +314,14 @@ def get_field_of_view(dcm: pydicom.Dataset): def get_image_orientation(iop): """ From http://dicomiseasy.blogspot.com/2013/06/getting-oriented-using-image-plane.html + Args: - iop: + iop (list): values of dcm.ImageOrientationPatient - list of float Returns: - + str: Sagittal, Coronal or Transverse """ + # TODO: check that ImageOrientationPatient field is always available (every mannufacturer and enhanced) iop_round = [round(x) for x in iop] plane = np.cross(iop_round[0:3], iop_round[3:6]) plane = [abs(x) for x in plane] @@ -294,11 +336,12 @@ def get_image_orientation(iop): def rescale_to_byte(array): """ WARNING: This function normalises/equalises the histogram. This might have unintended consequences. + Args: - array: + array (np.array): dcm.pixel_array Returns: - + np.array: normalised pixel values as 8-bit (byte) integer """ image_histogram, bins = np.histogram(array.flatten(), 255) cdf = image_histogram.cumsum() # cumulative distribution function @@ -311,6 +354,8 @@ def rescale_to_byte(array): class Rod: + """Class for rods detected in the image""" + def __init__(self, x, y): self.x = x self.y = y @@ -339,9 +384,8 @@ def __eq__(self, other): class ShapeDetector: - """ + """Class for the detection of shapes in pixel arrays This class is largely adapted from https://www.pyimagesearch.com/2016/02/08/opencv-shape-detection/ - """ def __init__(self, arr): @@ -352,6 +396,7 @@ def __init__(self, arr): self.thresh = None def find_contours(self): + """Find contours in pixel array""" # convert the resized image to grayscale, blur it slightly, and threshold it self.blurred = cv.GaussianBlur(self.arr.copy(), (5, 5), 0) # magic numbers @@ -372,6 +417,14 @@ def find_contours(self): # plt.show() def detect(self): + """Detect specified shapes in pixel array + + Currently supported shapes: + - circle + - triangle + - rectangle + - pentagon + """ for c in self.contours: # initialize the shape name and approximate the contour peri = cv.arcLength(c, True) @@ -401,6 +454,20 @@ def detect(self): self.shapes[shape].append(c) def get_shape(self, shape): + """Identify shapes in pixel array + + Args: + shape (_type_): _description_ + + Raises: + exc.ShapeDetectionError: ensure that only expected shapes are detected + exc.MultipleShapesError: ensure that only 1 shape is detected + + Returns: + tuple: varies depending on shape detected + - circle: x, y, r - corresponding to x,y coords of centre and radius + - rectangle/square: (x, y), size, angle - corresponding to x,y coords of centre, size (tuple) and angle in degrees + """ self.find_contours() self.detect() From 3599cee456e00dc1c2fea383df8261fd1de0bccc Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:05:50 +0000 Subject: [PATCH 05/30] remove CLI diagram as it doesn't render correctly on RTD --- hazenlib/__init__.py | 72 +------------------------------------------- 1 file changed, 1 insertion(+), 71 deletions(-) diff --git a/hazenlib/__init__.py b/hazenlib/__init__.py index e91b401d..de511dd0 100644 --- a/hazenlib/__init__.py +++ b/hazenlib/__init__.py @@ -1,75 +1,4 @@ """ -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWWWNNNNNNNWWWWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMWWNNNNXXXXXXXXXXXXXXXKKKXNNWWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMWWNNXKK0OOO0XNXK0KKXXXXKXNNX0OOOO0KKXXNNWMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWNXXK0kddxOkxO0KXXKXXXXXXXXXXXNXXXKOxdxkOO00KXWMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMWWNKOxdxxxxxxxO0KNXKXNXKKK0KKKXKKKKKK0kkOkxolodxkk0XWMMMMMMMMMMMMMMM -MMMMMMMMMWNXXKOOkkxxxkOddOXNXKOOO0KKXXXNNWWNXK00OxdkXNX0kdc:coox0WMMMMMMMMMMMMMM -MMMMMMMWNKOOkxxxxxxkOOOOO00kkO0KNNWWNX0OkO0OxxO000KXXNNNWNXOdc:ldkNMMMMMMMMMMMMM -MMMMMMWKOxoc:lddxxxxkO0KKK0OkxkO00Okxlccclooooddooodxxk0KXXNWXkxddxKWMMMMMMMMMMM -MMMMMNklc:;:clooxxxkkkkO00OOOxc,,;;,,,,;;;;::looddxxkdoloxk0KXXNX0xx0WMMMMMMMMMM -MMMMNx:,;coxOXNNWWNNXXXXKOkxdl:,...';:clodkOKXNNNNNNWNXOdoodkkOKNWN0kONMMMMMMMMM -MMMNx:coxO0KKK0OkkkO0KXXKOkxoc;'.',;coxkO0KKKKKKKKKXNWWWX0kdddxkOKNWWKKXWMMMMMMM -MMW0odkOkkkxddxO0XXXXXKK0Odoooc'...,:ccccccccloodxxkOO0KXNNKkxxxxkOKXNXXXNMMMMMM -MNOdxxxdodxk0XNNXKKKXXX0OO00kc......,coxkkOkxxddoooddxxkk0XNNXOxddxkOO0KXXXWMMMM -XxloddoodOXNNXKKKXNWXOkOKX0l'.........;clldkO0KKKKKK0OOkkkO0KNNXOxddxkkO0KXKNWMM -OolllldOKXX0O0XWMWKxoxKNXk;............',;llcldxk0KNWWWWNK0OOOKXNXOxddxkkOKXKXWM -dc:cldk0OkkKNWWWXkox0XXKd'................;oxdlccldxOXNWMMWNX0OOKXNXOxddxxk0XKXM -kc:oxdxddOXWNXKOdx0NNXKl....................lKXKOkdoodxOKXNWMWX0OOKXNKOkxdxOXNXN -Kloxllld0K0KK0doONWNNKl......................:ONWWWNXKOkk0KKXWMNKOOOKNX0kxxk0NXX -W0o:,:xOxdk00xxKWWWWXl........................,o0XNWWMWNXXXKKNWMWKOkkOKNX0Ok0NXX -MWKl:ldlcx0OkOXWMWMXl....,:loxkxdlcc:::::;,'....,lkKXNWNNNXKKXNWMN0kxkOXWX00KNXX -MMMXxc;;cxkxkKWMMMXl..',:okOO0KKKKK00KXXXXK0Okdc,.'lkKKKKXNXKKXNMMXOxxkOXXOO0NXX -MMMMXl,;codx0WMMMXl'',:ldO0KXXNWWNNNNNNXXNNNNNNX0kl,,lk0OO0XXKKXWMWKkxxk0KOOOKXX -MMMMWO:;lookXMWWNd,;loxOXNNNNNKKNWWNXKKXXXNNWWNNNX0d,.'lxkkOKK0KNWMXkxkkOOkOOKKX -MMMMMXoclcxKWWNNk;';:clodk0KNWNXNWNK000kkdoddkKXXXOoc,..,lkkO000XNWKkxkkOkkOO00K -MMMMMXocclOXWWNO:'.',,,'.',:xXWNXOkkko:,''.'':ooloxxol:...cxkOOOKNW0dxxkOkxkO00K -MMMMMXdc:oO0XNXo.,:ccldoc,..,d0KOdodko:;::::;:lxklcdOOkc...:xkkkOXN0dxxkOxxkO0OK -MMMMMWOc;ok0KXKl'::;:oddol;..cOOkdokOd:cccclooooxd:;dKX0l'..:dxdkKX0ddxxkdxkOOON -MMMMMMNx;;oxk00l',;;,..;ddl,.;xOkdodOkdc;,,locxOdldOKWWNO:...:ol:oOkodkxddxOOOKW -MMMMMMMNx;;loxkd;,;;:cldkdc;'lKNN0xOXKOkkk0K000K0OKWNNXX0c.. 'c:';xxlododk0OO0WM -MMMMMMMMNOo:;:colodc:lddl;''cKWWNKkxO0OxxkKXNNNXKXNNNNNXkc.. .,;.'cc::cok0OkONMM -MMMMMMMMMMNOo;,;:xXKOxdoc;,c0WWWNX000KKK0KXXNXXXNWNNXXN0c.....,,..'..;lxxdoONMMM -MMMMMMMMMMMMW0d:;dKXNX0kdll0WMWWNXKXXXXXXXKKKXNNWWWWNXK0o...........;:c::dKWMMMM -MMMMMMMMMMMMMMW0:cOKNWXOllONWWNKK0KXXNNNNNNNXXKKXXXXXKKx:........',,;;:dKWMMMMMM -MMMMMMMMMMMMMMMWx:oO0Kk:,oKNWWXOkO00O0XNWWWWNXK000000Od:'.......'';ld0NMMMMMMMMM -MMMMMMMMMMMMMMMM0;,lxxc',o0XNNKdlOKOxxxOKXNNNNX0Okolxo,....',''cdkKWMMMMMMMMMMMM -MMMMMMMMMMMMMMMXo..:ddc,',lkkxl;,:lldK0xxk0XXXK0Odc',;'''...'',xWMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMNk:.'dko:'',;;;:;',cloxO0K0OO000Odcc:....'....''oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMNo..coc;''',;;;:,,,,,,;lxxolodxoc;;c,..;,'.''..oNMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMWO;.';:;:;;:od:cc:;;::,',,;,;;;;,,,c;.'::;'.,,..ckXMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMM0:..,:;:clccl;,:,,,,''...':lc::loclc',::,..'.. .,oKWMMMMMMMMMM -MMMMMMMMMMMMMMMMWk'':lc,,,,coooooddl;......:occlllld:.,:,...... 'l0WMMMMMMMM -MMMMMMMMMMMMMMMMNd';loc:;'',:cccclc:,.......:;,cc:cl,'cc;....... .lONMMMMMM -MMMMMMMMMMMMMMMMXl';cccc;;:,'.. ..''''.....,c;:l:cc,,:c:,'.''..... .;d0NMMM -MMMMMMMMMMMMMMMMKc'',:loc::;,...'c;''...'',:;';lcc;,c:,,,.''.. .... ....;d0W -MMMMMMMMMMMMMMWXxlc;',ll:;;:;...;l;''',;:c:,..':;,;;;'.'..''. ............ .: -MMMMMMMMMMMMN0o;:do;',;,;ooc:,',,:;',:::cc;.'',:,.,,,'..................... . -MMMMMMMMMN0d;...,lc,'.':odl::;;:,';;;clc:;,'..,,''''.'...................... . -MMMMMMWKd;. ....;cc'.:odollc;::'.';,;l:,,;,.',.'',,.',,,'...................... -MMMMWKd,..........;;..,:ooloccol;',:;,lc,,',';:,;:'...''.........''''''''''..''' -MMMW0d:'..........',..';cclooxxdooll::oc;,...:c',:,.,:;'.''''..''',;;,,,,,,,;ccl -MMMWN0d:;,'............';:clodxxxxlc:;:;:,..;;'''::':c,,,;;,',,,;;,;;,'',;ldk00K -MMMMMWN0koc:,,'''..''''.';;;lldkxo::;;;,''.','..,:,''',,,,',,,:::::;;;;;cdKWWMMM -MMMMMMMMMWXKOxlc;,...','...,:;coc;;;;'''''.....,:,...''''',;::clcclddxk0XWMMMMMM -MMMMMMMMMMMMMWNN0d:,........''';;..,,........';;,'','',;;:codxkOO0KNWWMMMMMMMMMM -MMMMMMMMMMMMMMMMMWX0xl;,...........',...,...':;;;;;::codk0XNWWMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMWXOdc,'..',,,,,;;,,,'.,;ccldkOOKXNWMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMWN0xoc:cccccldkxxkOOO0KXNWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM -MMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xc;;::cxXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM - - -`7MMF' `7MMF' - MM MM - MM MM ,6"Yb. M""MMV .gP"Ya 7MMpMMMb. - MMmmmmmmMM 8) MM ' AMV ,M' Yb MM MM - MM MM ,pm9MM AMV 8M~~~~~' MM MM - MM MM 8M MM AMV ,YM. , MM MM -.JMML. .JMML.`Moo9^Yo.AMMmmmM `Mbmmd'.JMML JMML. - - - Welcome to the hazen Command Line Interface The following Tasks are available: @@ -178,6 +107,7 @@ def init_task(selected_task, files, report, report_dir, **kwargs): def main(): + """Main entrypoint to hazen""" arguments = docopt(__doc__, version=__version__) files = get_dicom_files(arguments[""]) From 82d938cd067508a285555e7a3e223dcc0c89fe5e Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:10:29 +0000 Subject: [PATCH 06/30] move relaxometry params to data folder --- hazenlib/{ => data}/relaxometry_params.py | 0 hazenlib/tasks/relaxometry.py | 2 +- tests/test_relaxometry.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename hazenlib/{ => data}/relaxometry_params.py (100%) diff --git a/hazenlib/relaxometry_params.py b/hazenlib/data/relaxometry_params.py similarity index 100% rename from hazenlib/relaxometry_params.py rename to hazenlib/data/relaxometry_params.py diff --git a/hazenlib/tasks/relaxometry.py b/hazenlib/tasks/relaxometry.py index 00a4adf9..1e048de5 100644 --- a/hazenlib/tasks/relaxometry.py +++ b/hazenlib/tasks/relaxometry.py @@ -137,7 +137,7 @@ import hazenlib.exceptions from hazenlib.HazenTask import HazenTask -from hazenlib.relaxometry_params import ( +from hazenlib.data.relaxometry_params import ( MAX_RICIAN_NOISE, SEED_RICIAN_NOISE, TEMPLATE_VALUES, diff --git a/tests/test_relaxometry.py b/tests/test_relaxometry.py index a2fd5c8d..d0003eb3 100644 --- a/tests/test_relaxometry.py +++ b/tests/test_relaxometry.py @@ -20,7 +20,7 @@ from hazenlib.utils import get_dicom_files from hazenlib.exceptions import ArgumentCombinationError from tests import TEST_DATA_DIR, TEST_REPORT_DIR -from hazenlib.relaxometry_params import TEMPLATE_VALUES +from hazenlib.data.relaxometry_params import TEMPLATE_VALUES class TestRelaxometry(unittest.TestCase): From 12d0dcfcda2b4aa78cfa1ba8d8bcb9a6298425f6 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:20:26 +0000 Subject: [PATCH 07/30] create rst files for RTD --- docs/source/hazenlib.rst | 69 +++++++++++++++++ docs/source/hazenlib.tasks.rst | 133 +++++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + docs/source/modules.rst | 7 ++ 4 files changed, 210 insertions(+) create mode 100644 docs/source/hazenlib.rst create mode 100644 docs/source/hazenlib.tasks.rst create mode 100644 docs/source/modules.rst diff --git a/docs/source/hazenlib.rst b/docs/source/hazenlib.rst new file mode 100644 index 00000000..b22bb714 --- /dev/null +++ b/docs/source/hazenlib.rst @@ -0,0 +1,69 @@ +hazenlib package +================ + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + hazenlib.tasks + +Submodules +---------- + +hazenlib.ACRObject module +------------------------- + +.. automodule:: hazenlib.ACRObject + :members: + :undoc-members: + :show-inheritance: + +hazenlib.HazenTask module +------------------------- + +.. automodule:: hazenlib.HazenTask + :members: + :undoc-members: + :show-inheritance: + +hazenlib.exceptions module +-------------------------- + +.. automodule:: hazenlib.exceptions + :members: + :undoc-members: + :show-inheritance: + +hazenlib.logger module +---------------------- + +.. automodule:: hazenlib.logger + :members: + :undoc-members: + :show-inheritance: + +.. hazenlib.relaxometry\_params module +.. ----------------------------------- + +.. .. automodule:: hazenlib.relaxometry_params +.. :members: +.. :undoc-members: +.. :show-inheritance: + +hazenlib.utils module +--------------------- + +.. automodule:: hazenlib.utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: hazenlib + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/hazenlib.tasks.rst b/docs/source/hazenlib.tasks.rst new file mode 100644 index 00000000..bbb48ca6 --- /dev/null +++ b/docs/source/hazenlib.tasks.rst @@ -0,0 +1,133 @@ +hazenlib.tasks package +====================== + +Submodules +---------- + +hazenlib.tasks.acr\_geometric\_accuracy module +---------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_geometric_accuracy + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_ghosting module +----------------------------------- + +.. automodule:: hazenlib.tasks.acr_ghosting + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_slice\_position module +------------------------------------------ + +.. automodule:: hazenlib.tasks.acr_slice_position + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_slice\_thickness module +------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_slice_thickness + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_snr module +------------------------------ + +.. automodule:: hazenlib.tasks.acr_snr + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_spatial\_resolution module +---------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_spatial_resolution + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_uniformity module +------------------------------------- + +.. automodule:: hazenlib.tasks.acr_uniformity + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.ghosting module +------------------------------ + +.. automodule:: hazenlib.tasks.ghosting + :members: + :undoc-members: + :show-inheritance: + +.. hazenlib.tasks.relaxometry module +.. --------------------------------- + +.. .. automodule:: hazenlib.tasks.relaxometry +.. :members: +.. :undoc-members: +.. :show-inheritance: + +hazenlib.tasks.slice\_position module +------------------------------------- + +.. automodule:: hazenlib.tasks.slice_position + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.slice\_width module +---------------------------------- + +.. automodule:: hazenlib.tasks.slice_width + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.snr module +------------------------- + +.. automodule:: hazenlib.tasks.snr + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.snr\_map module +------------------------------ + +.. automodule:: hazenlib.tasks.snr_map + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.spatial\_resolution module +----------------------------------------- + +.. automodule:: hazenlib.tasks.spatial_resolution + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.uniformity module +-------------------------------- + +.. automodule:: hazenlib.tasks.uniformity + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: hazenlib.tasks + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst index 9774ed9d..a3540baa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,6 +32,7 @@ We have also built an interactive web-based implementation of *hazen*, which is gettingstarted tasks + modules getinvolved contributors diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 00000000..a3db5c22 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +hazenlib +======== + +.. toctree:: + :maxdepth: 4 + + hazenlib From 5485a4e371aba5e4ba5842867a30f3c8ef9905ef Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:20:59 +0000 Subject: [PATCH 08/30] add instructions for updating auto-documentation --- CONTRIBUTING.md | 16 ++++++++++ docs/source/conf.py | 71 +++++++++++++++++++++++++-------------------- 2 files changed, 55 insertions(+), 32 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0ecaca9..eedf39be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,8 @@ - [2) How to make and test code changes](#2-how-to-make-and-test-code-changes) - [3) Developer Process for Contributing](#3-developer-process-for-contributing) - [4) Release Process](#4-release-process) +- [5) Update Documentation](#5-update-documentation) + ## 1) Introduction @@ -148,4 +150,18 @@ For a new release:
> - Updated cli-test.yml by @laurencejackson in #195 > - Release/0.5.2 by @tomaroberts in #196 +## 5) Update Documentation +Create rst files describing the structure of the hazen Python Package +``` +# in an active hazen virtual environment in the root of the project +# the command below specifies that sphinx should look for scripts in the hazenlib folder +# and output rst files into the docs/source folder +sphinx-apidoc -o docs/source hazenlib + +# next, from within the docs/ folder +cd docs/ +# create/update the html files for the documentation +make html -f Makefile +# opening the docs/source/index.html in a web browser allows a preview of the generated docs +``` \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index c825a69a..93128284 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,9 +19,9 @@ # -- Project information ----------------------------------------------------- -project = 'hazen' -copyright = '2022, Haris Shuaib' -author = 'Haris Shuaib' +project = "hazen" +copyright = "2022, Haris Shuaib" +author = "Haris Shuaib" # The full version, including alpha/beta/rc tags release = __version__ @@ -32,22 +32,29 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', # NumPy and Google docstrings - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.ifconfig', - 'sphinxcontrib.bibtex' - ] -napoleon_google_docstring = False -napoleon_use_param = False +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", # NumPy and Google docstrings + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.ifconfig", + "sphinxcontrib.bibtex", +] + +napoleon_google_docstring = True +napoleon_use_param = True napoleon_use_ivar = True -bibtex_bibfiles = ['references.bib'] +napoleon_use_rtype = True +napoleon_use_keyword = True +napoleon_custom_sections = None + +bibtex_bibfiles = ["references.bib"] + # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -60,25 +67,25 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" html_theme_options = { - 'analytics_id': '', # Provided by Google in your dashboard - 'analytics_anonymize_ip': False, - 'logo_only': False, - 'display_version': True, - 'prev_next_buttons_location': 'bottom', - 'style_external_links': False, - 'vcs_pageview_mode': '', - 'style_nav_header_background': '', + "analytics_id": "", # Provided by Google in your dashboard + "analytics_anonymize_ip": False, + "logo_only": False, + "display_version": True, + "prev_next_buttons_location": "bottom", + "style_external_links": False, + "vcs_pageview_mode": "", + "style_nav_header_background": "", # Toc options - 'collapse_navigation': True, - 'sticky_navigation': True, - 'navigation_depth': 4, - 'includehidden': True, - 'titles_only': False + "collapse_navigation": True, + "sticky_navigation": True, + "navigation_depth": 4, + "includehidden": True, + "titles_only": False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ["_static"] From 411cab5705b9a637d8ee0fa7ec986dedeaa4dfd9 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:32:53 +0000 Subject: [PATCH 09/30] add new lines for better sphinx parsing --- hazenlib/ACRObject.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/hazenlib/ACRObject.py b/hazenlib/ACRObject.py index 4d697840..c46d08c0 100644 --- a/hazenlib/ACRObject.py +++ b/hazenlib/ACRObject.py @@ -35,11 +35,9 @@ def __init__(self, dcm_list): def sort_images(self): """Sort a stack of images based on slice position. - Returns: - tuple of lists: - img_stack - list of np.array of dcm.pixel_array - A sorted stack of images, where each image is represented as a 2D numpy array. + tuple of lists: img_stack and dcm_stack + img_stack - list of np.array of dcm.pixel_array: A sorted stack of images, where each image is represented as a 2D numpy array. \n dcm_stack - list of pydicom.Dataset objects """ # TODO: implement a check if phantom was placed in other than axial position @@ -55,11 +53,10 @@ def sort_images(self): def orientation_checks(self): """Perform orientation checks on a set of images to determine if slice order inversion - or an LR orientation swap is required. + or an LR orientation swap is required. \n - Note: - This function analyzes the given set of images and their associated DICOM objects to determine if any - adjustments are needed to restore the correct slice order and view orientation. + This function analyzes the given set of images and their associated DICOM objects to determine if any + adjustments are needed to restore the correct slice order and view orientation. """ test_images = (self.images[0], self.images[-1]) dx = self.pixel_spacing[0] @@ -149,7 +146,7 @@ def find_phantom_center(self): Returns: - tuple of ints: representing the (x, y) center of the image. + tuple of ints: representing the (x, y) coordinates of the center of the image """ img = self.images[6] dx, dy = self.pixel_spacing @@ -172,7 +169,7 @@ def find_phantom_center(self): return centre, radius def get_mask_image(self, image, mag_threshold=0.05, open_threshold=500): - """Create a masked pixel array + """Create a masked pixel array \n Mask an image by magnitude threshold before applying morphological opening to remove small unconnected features. The convex hull is calculated in order to accommodate for potential air bubbles. @@ -182,8 +179,7 @@ def get_mask_image(self, image, mag_threshold=0.05, open_threshold=500): open_threshold (int, optional): open threshold. Defaults to 500. Returns: - np.array: - The masked image. + np.array: the masked image """ test_mask = self.circular_mask( self.centre, (80 // self.pixel_spacing[0]), image.shape @@ -237,7 +233,7 @@ def measure_orthogonal_lengths(self, mask): mask (np.array): Boolean array of the image where pixel values meet threshold Returns: - dict: A dictionary containing the following information for both horizontal and vertical line profiles: + dict: a dictionary with the following 'Horizontal Start' | 'Vertical Start' : tuple of int Horizontal/vertical starting point of the object. 'Horizontal End' | 'Vertical End' : tuple of int @@ -289,8 +285,9 @@ def rotate_point(origin, point, angle): angle (int): Angle in degrees. Returns: - tuple of float: x_prime and y_prime - A float representing the x and y - coordinates of the desired point after being rotated around an origin. + tuple of float: x_prime and y_prime + Floats representing the x and y coordinates of the input point + after being rotated around an origin. """ theta = np.radians(angle) c, s = np.cos(theta), np.sin(theta) @@ -310,7 +307,7 @@ def find_n_highest_peaks(data, n, height=1): Returns: tuple of np.array: peak_locs and peak_heights - peak_locs: A numpy array containing the indices of the N highest peaks identified. + peak_locs: A numpy array containing the indices of the N highest peaks identified. \n peak_heights: A numpy array containing the amplitudes of the N highest peaks identified. """ From 6e149d28b2892393995dd22f7611aa2f67a5ba36 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Wed, 17 Jan 2024 18:41:56 +0000 Subject: [PATCH 10/30] correct the relative import path --- hazenlib/data/relaxometry_params.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hazenlib/data/relaxometry_params.py b/hazenlib/data/relaxometry_params.py index 18393f96..287569fc 100644 --- a/hazenlib/data/relaxometry_params.py +++ b/hazenlib/data/relaxometry_params.py @@ -1,9 +1,7 @@ import os.path import numpy as np -TEMPLATE_DIR = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "data", "relaxometry" -) +TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "relaxometry") TEMPLATE_VALUES = { "plate3": { "sphere_centres_row_col": (), From f4c9289807426d9d8071baa9b328d75134ad86c0 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 22 Jan 2024 10:19:02 +0000 Subject: [PATCH 11/30] split task docs for ACR and MagNet --- docs/source/hazenlib.rst | 49 ++++------ docs/source/hazenlib.tasks.acr.rst | 58 +++++++++++ docs/source/hazenlib.tasks.magnet.rst | 66 +++++++++++++ docs/source/hazenlib.tasks.rst | 133 -------------------------- 4 files changed, 145 insertions(+), 161 deletions(-) create mode 100644 docs/source/hazenlib.tasks.acr.rst create mode 100644 docs/source/hazenlib.tasks.magnet.rst delete mode 100644 docs/source/hazenlib.tasks.rst diff --git a/docs/source/hazenlib.rst b/docs/source/hazenlib.rst index b22bb714..87b63370 100644 --- a/docs/source/hazenlib.rst +++ b/docs/source/hazenlib.rst @@ -1,34 +1,43 @@ hazenlib package ================ -Subpackages ------------ +Core functions +----------------- + +.. automodule:: hazenlib + :members: + :undoc-members: + :show-inheritance: + +Task-specific functions +------------------------ .. toctree:: :maxdepth: 4 - hazenlib.tasks + hazenlib.tasks.acr + hazenlib.tasks.magnet -Submodules ----------- +Utility functions: +------------------- -hazenlib.ACRObject module -------------------------- +hazenlib.ACRObject +------------------- .. automodule:: hazenlib.ACRObject :members: :undoc-members: :show-inheritance: -hazenlib.HazenTask module -------------------------- +hazenlib.HazenTask +------------------- .. automodule:: hazenlib.HazenTask :members: :undoc-members: :show-inheritance: -hazenlib.exceptions module +hazenlib.exceptions -------------------------- .. automodule:: hazenlib.exceptions @@ -36,7 +45,7 @@ hazenlib.exceptions module :undoc-members: :show-inheritance: -hazenlib.logger module +hazenlib.logger ---------------------- .. automodule:: hazenlib.logger @@ -44,26 +53,10 @@ hazenlib.logger module :undoc-members: :show-inheritance: -.. hazenlib.relaxometry\_params module -.. ----------------------------------- - -.. .. automodule:: hazenlib.relaxometry_params -.. :members: -.. :undoc-members: -.. :show-inheritance: - -hazenlib.utils module +hazenlib.utils --------------------- .. automodule:: hazenlib.utils :members: :undoc-members: :show-inheritance: - -Module contents ---------------- - -.. automodule:: hazenlib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/hazenlib.tasks.acr.rst b/docs/source/hazenlib.tasks.acr.rst new file mode 100644 index 00000000..b00375fc --- /dev/null +++ b/docs/source/hazenlib.tasks.acr.rst @@ -0,0 +1,58 @@ +hazenlib ACR tasks +=================== + +hazenlib.tasks.acr\_geometric\_accuracy task +---------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_geometric_accuracy + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_ghosting task +----------------------------------- + +.. automodule:: hazenlib.tasks.acr_ghosting + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_slice\_position task +------------------------------------------ + +.. automodule:: hazenlib.tasks.acr_slice_position + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_slice\_thickness task +------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_slice_thickness + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_snr task +------------------------------ + +.. automodule:: hazenlib.tasks.acr_snr + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_spatial\_resolution task +---------------------------------------------- + +.. automodule:: hazenlib.tasks.acr_spatial_resolution + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.acr\_uniformity task +------------------------------------- + +.. automodule:: hazenlib.tasks.acr_uniformity + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/hazenlib.tasks.magnet.rst b/docs/source/hazenlib.tasks.magnet.rst new file mode 100644 index 00000000..892de068 --- /dev/null +++ b/docs/source/hazenlib.tasks.magnet.rst @@ -0,0 +1,66 @@ +hazenlib MagNet tasks +======================= + +hazenlib.tasks.ghosting task +------------------------------ + +.. automodule:: hazenlib.tasks.ghosting + :members: + :undoc-members: + :show-inheritance: + +.. hazenlib.tasks.relaxometry task +.. --------------------------------- + +.. .. automodule:: hazenlib.tasks.relaxometry +.. :members: +.. :undoc-members: +.. :show-inheritance: + +hazenlib.tasks.slice\_position task +------------------------------------- + +.. automodule:: hazenlib.tasks.slice_position + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.slice\_width task +---------------------------------- + +.. automodule:: hazenlib.tasks.slice_width + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.snr task +------------------------- + +.. automodule:: hazenlib.tasks.snr + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.snr\_map task +------------------------------ + +.. automodule:: hazenlib.tasks.snr_map + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.spatial\_resolution task +----------------------------------------- + +.. automodule:: hazenlib.tasks.spatial_resolution + :members: + :undoc-members: + :show-inheritance: + +hazenlib.tasks.uniformity task +-------------------------------- + +.. automodule:: hazenlib.tasks.uniformity + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/hazenlib.tasks.rst b/docs/source/hazenlib.tasks.rst deleted file mode 100644 index bbb48ca6..00000000 --- a/docs/source/hazenlib.tasks.rst +++ /dev/null @@ -1,133 +0,0 @@ -hazenlib.tasks package -====================== - -Submodules ----------- - -hazenlib.tasks.acr\_geometric\_accuracy module ----------------------------------------------- - -.. automodule:: hazenlib.tasks.acr_geometric_accuracy - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_ghosting module ------------------------------------ - -.. automodule:: hazenlib.tasks.acr_ghosting - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_slice\_position module ------------------------------------------- - -.. automodule:: hazenlib.tasks.acr_slice_position - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_slice\_thickness module -------------------------------------------- - -.. automodule:: hazenlib.tasks.acr_slice_thickness - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_snr module ------------------------------- - -.. automodule:: hazenlib.tasks.acr_snr - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_spatial\_resolution module ----------------------------------------------- - -.. automodule:: hazenlib.tasks.acr_spatial_resolution - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.acr\_uniformity module -------------------------------------- - -.. automodule:: hazenlib.tasks.acr_uniformity - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.ghosting module ------------------------------- - -.. automodule:: hazenlib.tasks.ghosting - :members: - :undoc-members: - :show-inheritance: - -.. hazenlib.tasks.relaxometry module -.. --------------------------------- - -.. .. automodule:: hazenlib.tasks.relaxometry -.. :members: -.. :undoc-members: -.. :show-inheritance: - -hazenlib.tasks.slice\_position module -------------------------------------- - -.. automodule:: hazenlib.tasks.slice_position - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.slice\_width module ----------------------------------- - -.. automodule:: hazenlib.tasks.slice_width - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.snr module -------------------------- - -.. automodule:: hazenlib.tasks.snr - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.snr\_map module ------------------------------- - -.. automodule:: hazenlib.tasks.snr_map - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.spatial\_resolution module ------------------------------------------ - -.. automodule:: hazenlib.tasks.spatial_resolution - :members: - :undoc-members: - :show-inheritance: - -hazenlib.tasks.uniformity module --------------------------------- - -.. automodule:: hazenlib.tasks.uniformity - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: hazenlib.tasks - :members: - :undoc-members: - :show-inheritance: From 5582d1478b6147e173cf3e0136767c0cca0837fe Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 22 Jan 2024 10:19:28 +0000 Subject: [PATCH 12/30] directly list hazenlib docs --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index a3540baa..18875be4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -32,7 +32,7 @@ We have also built an interactive web-based implementation of *hazen*, which is gettingstarted tasks - modules + hazenlib getinvolved contributors From 167214a08ce205607589f813a4f834a79d5e5fd8 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 22 Jan 2024 10:21:03 +0000 Subject: [PATCH 13/30] fix version import for publishing --- docs/source/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 93128284..400e6121 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,7 @@ # import os import sys -from hazenlib import __version__ +from hazenlib._version import __version__ sys.path.insert(0, os.path.abspath("../..")) @@ -50,6 +50,7 @@ napoleon_use_rtype = True napoleon_use_keyword = True napoleon_custom_sections = None +autodoc_member_order = "bysource" bibtex_bibfiles = ["references.bib"] From 589d3388fa1169488ce0f2fe103182f2d436ca35 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Mon, 22 Jan 2024 11:28:04 +0000 Subject: [PATCH 14/30] add Molly to contributors --- docs/source/contributors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/contributors.rst b/docs/source/contributors.rst index 2f03d639..f275bf42 100644 --- a/docs/source/contributors.rst +++ b/docs/source/contributors.rst @@ -16,3 +16,4 @@ Contributors * `Elizabeth Stamou `_ * `Francesco Padormo `_ * `Sian Culley `_ +* `Molly Buckley `_ From e31aa1dc1bf87041792291b94d846c3a151c75cf Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Thu, 25 Jan 2024 11:39:21 +0000 Subject: [PATCH 15/30] Convert all docstrings in acr_geometric_accuracy to google style --- hazenlib/tasks/acr_geometric_accuracy.py | 30 ++++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/hazenlib/tasks/acr_geometric_accuracy.py b/hazenlib/tasks/acr_geometric_accuracy.py index baf462c5..bbe7c888 100644 --- a/hazenlib/tasks/acr_geometric_accuracy.py +++ b/hazenlib/tasks/acr_geometric_accuracy.py @@ -36,9 +36,9 @@ class ACRGeometricAccuracy(HazenTask): - """Geometric accuracy measurement class for DICOM images of the ACR phantom + """Geometric accuracy measurement class for DICOM images of the ACR phantom. - Inherits from HazenTask class + Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -50,7 +50,9 @@ def run(self) -> dict: using the first and fifth slices from the ACR phantom image set Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Initialise results dictionary @@ -98,13 +100,13 @@ def run(self) -> dict: def get_geometric_accuracy(self, slice_index): - """Measure geometric accuracy for input slice + """Measure geometric accuracy for input slice. Args: - dcm (pydicom.Dataset): DICOM image object + slice_index (int): the index of the slice position, for example slice 5 would have an index of 4. Returns: - tuple of float: horizontal and vertical distances + tuple of float: horizontal and vertical distances. """ img_dcm = self.ACR_obj.dcms[slice_index] img = img_dcm.pixel_array @@ -186,15 +188,17 @@ def get_geometric_accuracy(self, slice_index): return length_dict['Horizontal Distance'], length_dict['Vertical Distance'] def diagonal_lengths(self, img, cxy, slice_index): - """Measure diagonal lengths + """Measure diagonal lengths. Args: - img (np.array): dcm.pixel_array - cxy (list): x,y coordinates and radius of the circle - slice_index (int): index of the slice number + img (np.array): pixel array of the slice (dcm.pixel_array). + cxy (list): x,y coordinates and radius of the circle. + slice_index (int): index of the slice number. Returns: - tuple of dictionaries: _description_ + tuple of dictionaries: for both the south-east (SE) diagonal length and the south-west (SW) diagonal length: + "start" and "end" indicate the start and end x and y positions of the lengths; "Extent" is the distance (in + pixels) of the lengths; "Distance" is "Extent" with factors applied to convert from pixels to mm. """ res = self.ACR_obj.pixel_spacing eff_res = np.sqrt(np.mean(np.square(res))) @@ -242,10 +246,10 @@ def diagonal_lengths(self, img, cxy, slice_index): @staticmethod def distortion_metric(L): - """Calculate the distortion metric based on length + """Calculate the distortion metric based on length. Args: - L (tuple): horizontal and vertical distances from slices 1 and 5 + L (tuple): horizontal and vertical distances from slices 1 and 5. Returns: tuple of floats: mean_err, max_err, cov_l From d16ac0c0129f3938c88a3e9e578195a03aefba0e Mon Sep 17 00:00:00 2001 From: Sophie Ratkai Date: Thu, 25 Jan 2024 11:48:33 +0000 Subject: [PATCH 16/30] Update import path --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 400e6121..c44a4f00 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,7 @@ # import os import sys -from hazenlib._version import __version__ +from ../../hazenlib._version import __version__ sys.path.insert(0, os.path.abspath("../..")) From d2496af78518fe78820a32b32e49abf97c94755f Mon Sep 17 00:00:00 2001 From: Sophie Ratkai Date: Thu, 25 Jan 2024 12:24:26 +0000 Subject: [PATCH 17/30] update import paths --- docs/source/conf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c44a4f00..b6d947a3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,9 +12,8 @@ # import os import sys -from ../../hazenlib._version import __version__ - sys.path.insert(0, os.path.abspath("../..")) +from hazenlib._version import __version__ # -- Project information ----------------------------------------------------- From c005cc529e2ac011fc543474efb478da917c70e6 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Thu, 25 Jan 2024 16:37:21 +0000 Subject: [PATCH 18/30] Google style docstrings for acr_ghosting --- hazenlib/tasks/acr_ghosting.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/hazenlib/tasks/acr_ghosting.py b/hazenlib/tasks/acr_ghosting.py index d26b22dc..1b08da64 100644 --- a/hazenlib/tasks/acr_ghosting.py +++ b/hazenlib/tasks/acr_ghosting.py @@ -25,9 +25,9 @@ class ACRGhosting(HazenTask): - """Ghosting measurement class for DICOM images of the ACR phantom + """Ghosting measurement class for DICOM images of the ACR phantom. - Inherits from HazenTask class + Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -36,11 +36,12 @@ def __init__(self, **kwargs): self.ACR_obj = ACRObject(self.dcm_list) def run(self) -> dict: - """Main function for performing ghosting measurement - using slice 7 from the ACR phantom image set + """Main function for performing ghosting measurement using slice 7 from the ACR phantom image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Initialise results dictionary results = self.init_result_dict() @@ -62,13 +63,13 @@ def run(self) -> dict: return results def get_signal_ghosting(self, dcm): - """Calculate signal ghosting + """Calculate signal ghosting. Args: - dcm (pydicom.Dataset): DICOM image object + dcm (pydicom.Dataset): DICOM image object. Returns: - float: percentage ghosting value + float: percentage ghosting value. """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata From 8ee486066bcaba554edcd63d43765422045e0900 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Sun, 28 Jan 2024 12:21:05 +0000 Subject: [PATCH 19/30] Google-style docstrings for acr_slice_position --- hazenlib/tasks/acr_slice_position.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/hazenlib/tasks/acr_slice_position.py b/hazenlib/tasks/acr_slice_position.py index cddc16fd..3ec99127 100644 --- a/hazenlib/tasks/acr_slice_position.py +++ b/hazenlib/tasks/acr_slice_position.py @@ -49,11 +49,13 @@ def __init__(self, **kwargs): self.ACR_obj = ACRObject(self.dcm_list) def run(self) -> dict: - """Main function for performing slice position measurement - using the first and last slices from the ACR phantom image set + """Main function for performing slice position measurement using the first and last slices from the ACR phantom + image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Identify relevant slices dcms = [self.ACR_obj.dcms[0], self.ACR_obj.dcms[-1]] @@ -83,15 +85,15 @@ def run(self) -> dict: return results def find_wedges(self, img, mask, res): - """Find wedges in the pixel array + """Find wedges in the pixel array. Args: - img (np.array): dcm.pixel_array - mask (np.array): dcm.pixel_array of the image mask - res (float): dcm.PixelSpacing + img (np.array): dcm.pixel_array. + mask (np.array): dcm.pixel_array of the image mask. + res (float): dcm.PixelSpacing. Returns: - tuple: arrays of x and y coordinates of wedges + tuple: arrays of x and y coordinates of wedges. """ # X COORDINATES x_investigate_region = np.ceil(35 / res[0]).astype( @@ -200,13 +202,13 @@ def find_wedges(self, img, mask, res): return x_pts, y_pts def get_slice_position(self, dcm): - """Measure slice position + """Measure slice position. Args: - dcm (pydicom.Dataset): DICOM image object + dcm (pydicom.Dataset): DICOM image object. Returns: - float: bar length difference + float: bar length difference. """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata From cd7ef6a90440582d830e465ebbbc06fb1eb1ef62 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Tue, 30 Jan 2024 10:20:24 +0000 Subject: [PATCH 20/30] Google style docstrings for acr_slice_thickness --- hazenlib/tasks/acr_slice_thickness.py | 41 ++++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/hazenlib/tasks/acr_slice_thickness.py b/hazenlib/tasks/acr_slice_thickness.py index ac7429b4..57b7944b 100644 --- a/hazenlib/tasks/acr_slice_thickness.py +++ b/hazenlib/tasks/acr_slice_thickness.py @@ -27,9 +27,9 @@ class ACRSliceThickness(HazenTask): - """Slice width measurement class for DICOM images of the ACR phantom + """Slice width measurement class for DICOM images of the ACR phantom. - Inherits from HazenTask class + Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -38,11 +38,12 @@ def __init__(self, **kwargs): self.ACR_obj = ACRObject(self.dcm_list) def run(self) -> dict: - """Main function for performing slice width measurement - using slice 1 from the ACR phantom image set + """Main function for performing slice width measurement using slice 1 from the ACR phantom image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Identify relevant slice slice_thickness_dcm = self.ACR_obj.dcms[0] @@ -67,15 +68,15 @@ def run(self) -> dict: return results def find_ramps(self, img, centre, res): - """Find ramps in the pixel array + """Find ramps in the pixel array. Args: - img (np.array): dcm.pixel_array - centre (list): x,y coordinates of the phantom centre - res (float): dcm.PixelSpacing + img (np.array): dcm.pixel_array. + centre (list): x,y coordinates of the phantom centre. + res (float): dcm.PixelSpacing. Returns: - tuple: x and y coordinates of ramp + tuple: x and y coordinates of ramp. """ # X investigate_region = int(np.ceil(5.5 / res[1]).item()) @@ -129,13 +130,13 @@ def find_ramps(self, img, centre, res): return x, y def FWHM(self, data): - """Calculate full width at half maximum + """Calculate full width at half maximum. Args: - data (np.array): curve + data (np.array): curve. Returns: - tuple: simple interpolation of half max points + tuple: simple interpolation of half max points. """ baseline = np.min(data) data -= baseline @@ -148,14 +149,14 @@ def FWHM(self, data): # Interpolation def simple_interp(x_start, ydata): - """Simple interpolation + """Simple interpolation. Args: - x_start (int or float): x coordinate of the half maximum - ydata (np.array): y coordinates + x_start (int or float): x coordinate of the half maximum. + ydata (np.array): y coordinates. Returns: - float: true x coordinate of the half maximum + float: true x coordinate of the half maximum. """ x_init = x_start - 5 x_pts = np.arange(x_start - 5, x_start + 5) @@ -175,13 +176,13 @@ def simple_interp(x_start, ydata): return FWHM_pts def get_slice_thickness(self, dcm): - """Measure slice thickness + """Measure slice thickness. Args: - dcm (pydicom.Dataset): DICOM image object + dcm (pydicom.Dataset): DICOM image object. Returns: - float: measured slice thickness + float: measured slice thickness. """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata From e431f9d1437206f4cdd2873191cc841da773936a Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Tue, 30 Jan 2024 12:04:03 +0000 Subject: [PATCH 21/30] Google style docstrings for acr_geometric_accuracy more descriptive detail in the docstrings --- hazenlib/tasks/acr_geometric_accuracy.py | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/hazenlib/tasks/acr_geometric_accuracy.py b/hazenlib/tasks/acr_geometric_accuracy.py index bbe7c888..5c4a8369 100644 --- a/hazenlib/tasks/acr_geometric_accuracy.py +++ b/hazenlib/tasks/acr_geometric_accuracy.py @@ -37,8 +37,6 @@ class ACRGeometricAccuracy(HazenTask): """Geometric accuracy measurement class for DICOM images of the ACR phantom. - - Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -46,13 +44,12 @@ def __init__(self, **kwargs): self.ACR_obj = ACRObject(self.dcm_list) def run(self) -> dict: - """Main function for performing geometric accuracy measurement - using the first and fifth slices from the ACR phantom image set + """Main function for performing geometric accuracy measurement using the first and fifth slices from the ACR phantom image set. Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Initialise results dictionary @@ -100,7 +97,9 @@ def run(self) -> dict: def get_geometric_accuracy(self, slice_index): - """Measure geometric accuracy for input slice. + """Measure geometric accuracy for input slice. Creates a mask over the phantom from the pixel array of the DICOM + image. Uses the centre and shape of the mask to determine horizontal and vertical lengths, and also diagonal lengths + in the case of slice 5. Args: slice_index (int): the index of the slice position, for example slice 5 would have an index of 4. @@ -188,7 +187,7 @@ def get_geometric_accuracy(self, slice_index): return length_dict['Horizontal Distance'], length_dict['Vertical Distance'] def diagonal_lengths(self, img, cxy, slice_index): - """Measure diagonal lengths. + """Measure diagonal lengths. Rotates the pixel array by 45° and measures the horizontal and vertical distances. Args: img (np.array): pixel array of the slice (dcm.pixel_array). @@ -197,10 +196,13 @@ def diagonal_lengths(self, img, cxy, slice_index): Returns: tuple of dictionaries: for both the south-east (SE) diagonal length and the south-west (SW) diagonal length: - "start" and "end" indicate the start and end x and y positions of the lengths; "Extent" is the distance (in - pixels) of the lengths; "Distance" is "Extent" with factors applied to convert from pixels to mm. + "start" and "end" indicate the start and end x and y positions of the lengths; "Extent" is the distance (in + pixels) of the lengths; "Distance" is "Extent" with factors applied to convert from pixels to mm. """ res = self.ACR_obj.pixel_spacing + # getting the geometric mean of the x and y pixel spacing components, as the pixel_spacing DICOM attribute is in + #co-ordinate form due to the possibility of pixels being rectangular, ie. the length and width of pixels can + #differ. eff_res = np.sqrt(np.mean(np.square(res))) img_rotate = skimage.transform.rotate(img, 45, center=(cxy[0], cxy[1])) @@ -246,7 +248,8 @@ def diagonal_lengths(self, img, cxy, slice_index): @staticmethod def distortion_metric(L): - """Calculate the distortion metric based on length. + """Calculates the mean error, the maximum error and the coefficient of variation between the horizontal and vertical + distances measured on slices 1 and 5. Args: L (tuple): horizontal and vertical distances from slices 1 and 5. @@ -254,6 +257,7 @@ def distortion_metric(L): Returns: tuple of floats: mean_err, max_err, cov_l """ + #TODO change function name to eg. get_distortion_metrics err = [x - 190 for x in L] mean_err = np.mean(err) From ead4dc9a6a7710ee35d20a581632d6ca32f7f2e8 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Tue, 30 Jan 2024 12:15:40 +0000 Subject: [PATCH 22/30] Google style docstrings for acr_ghosting more detailed description in docstrings --- hazenlib/tasks/acr_ghosting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hazenlib/tasks/acr_ghosting.py b/hazenlib/tasks/acr_ghosting.py index 1b08da64..411092d9 100644 --- a/hazenlib/tasks/acr_ghosting.py +++ b/hazenlib/tasks/acr_ghosting.py @@ -26,8 +26,6 @@ class ACRGhosting(HazenTask): """Ghosting measurement class for DICOM images of the ACR phantom. - - Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -40,8 +38,8 @@ def run(self) -> dict: Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Initialise results dictionary results = self.init_result_dict() @@ -63,7 +61,9 @@ def run(self) -> dict: return results def get_signal_ghosting(self, dcm): - """Calculate signal ghosting. + """Draws four ellipses outside the phantom in four directions and calculates the mean + signal value within each of these. Calculates the percentage signal ghosting (PSG): the mean signal in these + four ROIs as a percentage of a ROI in the centre of the phantom. Args: dcm (pydicom.Dataset): DICOM image object. From 35954da671febca058284d4c3eb56e5ddcf3c48c Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Tue, 30 Jan 2024 13:02:00 +0000 Subject: [PATCH 23/30] google style docstrings for acr_slice_position more detailed descriptions in docstrings --- hazenlib/tasks/acr_slice_position.py | 47 +++++++++++++--------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/hazenlib/tasks/acr_slice_position.py b/hazenlib/tasks/acr_slice_position.py index 3ec99127..3dbd16c4 100644 --- a/hazenlib/tasks/acr_slice_position.py +++ b/hazenlib/tasks/acr_slice_position.py @@ -38,9 +38,7 @@ class ACRSlicePosition(HazenTask): - """Slice position measurement class for DICOM images of the ACR phantom - - Inherits from HazenTask class + """Slice position measurement class for DICOM images of the ACR phantom. """ def __init__(self, **kwargs): @@ -54,8 +52,8 @@ def run(self) -> dict: Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Identify relevant slices dcms = [self.ACR_obj.dcms[0], self.ACR_obj.dcms[-1]] @@ -85,7 +83,8 @@ def run(self) -> dict: return results def find_wedges(self, img, mask, res): - """Find wedges in the pixel array. + """Investigates the top half of the phantom to locate where the wedges pass through the slice, and calculates the + co-ordinates of these locations. Args: img (np.array): dcm.pixel_array. @@ -104,13 +103,12 @@ def find_wedges(self, img, mask, res): # we want an odd number to see -N to N points in the x direction x_investigate_region = x_investigate_region + 1 - w_point = np.argwhere(np.sum(mask, 0) > 0)[0].item() # westmost point of object - e_point = np.argwhere(np.sum(mask, 0) > 0)[ - -1 - ].item() # eastmost point of object - n_point = np.argwhere(np.sum(mask, 1) > 0)[ - 0 - ].item() # northmost point of object + # westmost point of object + w_point = np.argwhere(np.sum(mask, 0) > 0)[0].item() + # eastmost point of object + e_point = np.argwhere(np.sum(mask, 0) > 0)[-1].item() + # northmost point of object + n_point = np.argwhere(np.sum(mask, 1) > 0)[0].item() invest_x = [] for k in range(x_investigate_region): @@ -126,19 +124,15 @@ def find_wedges(self, img, mask, res): invest_x.append(t * line_prof_x) # mask unwanted values out and append invest_x = np.array(invest_x).T # transpose array - mean_x_profile = np.mean( - invest_x, 1 - ) # mean of horizontal projections of phantom - abs_diff_x_profile = np.abs( - np.diff(mean_x_profile) - ) # absolute first derivative of mean + # mean of horizontal projections of phantom + mean_x_profile = np.mean(invest_x, 1) + # absolute first derivative of mean + abs_diff_x_profile = np.abs(np.diff(mean_x_profile)) - x_peaks, _ = self.ACR_obj.find_n_highest_peaks( - abs_diff_x_profile, 2 - ) # find two highest peaks - x_locs = ( - w_point + x_peaks - ) # x coordinates of these peaks in image coordinate system(before diff operation) + # find two highest peaks + x_peaks, _ = self.ACR_obj.find_n_highest_peaks(abs_diff_x_profile, 2) + # x coordinates of these peaks in image coordinate system(before diff operation) + x_locs = (w_point + x_peaks) width_pts = [x_locs[0], x_locs[1]] # width of wedges width = np.max(width_pts) - np.min(width_pts) # width @@ -202,7 +196,7 @@ def find_wedges(self, img, mask, res): return x_pts, y_pts def get_slice_position(self, dcm): - """Measure slice position. + """Locates the two opposing wedges and calculates the height difference. Args: dcm (pydicom.Dataset): DICOM image object. @@ -222,6 +216,7 @@ def get_slice_position(self, dcm): img, (y_pts[0], x_pts[1]), (y_pts[1], x_pts[1]), mode="constant" ).flatten() # line profile through right wedge + #interpolation interp_factor = 5 x = np.arange(1, len(line_prof_L) + 1) new_x = np.arange( From 79c167d2d44f230859d1129e74d865b9c9f67f75 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Tue, 30 Jan 2024 14:30:19 +0000 Subject: [PATCH 24/30] google style docstrings for acr_slice_thickness --- hazenlib/tasks/acr_slice_thickness.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/hazenlib/tasks/acr_slice_thickness.py b/hazenlib/tasks/acr_slice_thickness.py index 57b7944b..2720fbc3 100644 --- a/hazenlib/tasks/acr_slice_thickness.py +++ b/hazenlib/tasks/acr_slice_thickness.py @@ -28,8 +28,6 @@ class ACRSliceThickness(HazenTask): """Slice width measurement class for DICOM images of the ACR phantom. - - Inherits from HazenTask class. """ def __init__(self, **kwargs): @@ -42,8 +40,8 @@ def run(self) -> dict: Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the + generated images for visualisation. """ # Identify relevant slice slice_thickness_dcm = self.ACR_obj.dcms[0] @@ -68,7 +66,7 @@ def run(self) -> dict: return results def find_ramps(self, img, centre, res): - """Find ramps in the pixel array. + """Find ramps in the pixel array and returns co-ordinates of their location. Args: img (np.array): dcm.pixel_array. @@ -130,26 +128,25 @@ def find_ramps(self, img, centre, res): return x, y def FWHM(self, data): - """Calculate full width at half maximum. + """Calculate full width at half maximum of the line profile. Args: - data (np.array): curve. + data (np.array): slice profile curve. Returns: - tuple: simple interpolation of half max points. + tuple: co-ordinates of the half-maximum points on the line profile. """ baseline = np.min(data) data -= baseline + #TODO create separate variable so that data value isn't being rewritten half_max = np.max(data) * 0.5 # Naive attempt - half_max_crossing_indices = np.argwhere( - np.diff(np.sign(data - half_max)) - ).flatten() + half_max_crossing_indices = np.argwhere(np.diff(np.sign(data - half_max))).flatten() # Interpolation def simple_interp(x_start, ydata): - """Simple interpolation. + """Simple interpolation - obtaining more accurate x co-ordinates. Args: x_start (int or float): x coordinate of the half maximum. @@ -159,7 +156,6 @@ def simple_interp(x_start, ydata): float: true x coordinate of the half maximum. """ x_init = x_start - 5 - x_pts = np.arange(x_start - 5, x_start + 5) x_pts = np.arange(x_init, x_init + 11) y_pts = ydata[x_pts] @@ -172,11 +168,10 @@ def simple_interp(x_start, ydata): FWHM_pts = simple_interp(half_max_crossing_indices[0], data), simple_interp( half_max_crossing_indices[-1], data ) - return FWHM_pts def get_slice_thickness(self, dcm): - """Measure slice thickness. + """Identify the ramps, measure the line profile, measure the FWHM, and use this to calculate the slice thickness. Args: dcm (pydicom.Dataset): DICOM image object. @@ -186,6 +181,7 @@ def get_slice_thickness(self, dcm): """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata + #TODO define object centre for slice 1 (not slice 7 as the default) cxy = self.ACR_obj.centre x_pts, y_pts = self.find_ramps(img, cxy, res) @@ -221,6 +217,7 @@ def get_slice_thickness(self, dcm): scipy.interpolate.interp1d(sample, line)(new_sample) for line in lines ] fwhm = [self.FWHM(interp_line) for interp_line in interp_lines] + ramp_length[0, i] = (1 / interp_factor) * np.diff(fwhm[0]) * res[0] ramp_length[1, i] = (1 / interp_factor) * np.diff(fwhm[1]) * res[0] @@ -231,6 +228,7 @@ def get_slice_thickness(self, dcm): dz = 0.2 * (np.prod(ramp_length, axis=0)) / np.sum(ramp_length, axis=0) dz = dz[~np.isnan(dz)] + #TODO check this - if it's taking the value closest to the DICOM slice thickness this is potentially not accurate? z_ind = np.argmin(np.abs(dcm.SliceThickness - dz)) slice_thickness = dz[z_ind] From be56ad9a1e3a9ba67acc949bb442a684e3b1ab85 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Thu, 1 Feb 2024 09:49:58 +0000 Subject: [PATCH 25/30] Google style docstrings for ACR_SNR --- hazenlib/tasks/acr_snr.py | 66 +++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/hazenlib/tasks/acr_snr.py b/hazenlib/tasks/acr_snr.py index 76f3d515..e3257744 100644 --- a/hazenlib/tasks/acr_snr.py +++ b/hazenlib/tasks/acr_snr.py @@ -27,9 +27,7 @@ class ACRSNR(HazenTask): - """Signal-to-noise ratio measurement class for DICOM images of the ACR phantom - - Inherits from HazenTask class + """Signal-to-noise ratio measurement class for DICOM images of the ACR phantom. """ def __init__(self, **kwargs): @@ -49,14 +47,14 @@ def __init__(self, **kwargs): self.subtract = None def run(self) -> dict: - """Main function for performing SNR measurement - using slice 7 from the ACR phantom image set + """Main function for performing SNR measurement using slice 7 from the ACR phantom image set. Performs either + smoothing or subtraction method depending on preferences set by user. Notes: - using the smoothing method by default or the subtraction method if a second set of images are provided (in a separate folder) + using the smoothing method by default or the subtraction method if a second set of images are provided (in a separate folder). Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. """ # Identify relevant slice snr_dcm = self.ACR_obj.slice7_dcm @@ -113,14 +111,15 @@ def run(self) -> dict: return results def get_normalised_snr_factor(self, dcm, measured_slice_width=None) -> float: - """Calculate the normalisation factor to be applied + """Calculates the normalisation factor to be applied to the SNR in order to obtain the absolute SNR (ASNR). The + normalisation factor depends on voxel size, bandwidth, number of averages and number of phase encoding steps. Args: dcm (pydicom.Dataset): DICOM image object measured_slice_width (float, optional): Provide the true slice width for the set of images. Defaults to None. Returns: - float: normalisation factor + float: normalisation factor. """ dx, dy = hazenlib.utils.get_pixel_size(dcm) bandwidth = hazenlib.utils.get_bandwidth(dcm) @@ -145,36 +144,37 @@ def get_normalised_snr_factor(self, dcm, measured_slice_width=None) -> float: return normalised_snr_factor def filtered_image(self, dcm: pydicom.Dataset) -> np.array: - """Apply filtering to a pixel array (image) + """Apply filtering to a pixel array (image), as per the single image SNR method outlined in McCann et al, 2013. Notes: - Performs a 2D convolution (for filtering images) - uses uniform_filter SciPy function + Performs a 2D convolution (for filtering images), using uniform_filter (SciPy function). Args: - dcm (pydicom.Dataset): DICOM image object + dcm (pydicom.Dataset): DICOM image object. Returns: - np.array: pixel array of the filtered image + np.array: pixel array of the filtered image. """ a = dcm.pixel_array.astype("int") # filter size = 9, following MATLAB code and McCann 2013 paper for head coil, although note McCann 2013 # recommends 25x25 for body coil. + #TODO add coil options, same as with MagNet SNR filtered_array = ndimage.uniform_filter(a, 25, mode="constant") return filtered_array def get_noise_image(self, dcm: pydicom.Dataset) -> np.array: - """Get noise image by subtracting the filtered image from the original pixel array + """Get a noise image when only one set of DICOM data is available. Notes: - Separates the image noise by smoothing the image and subtracting the smoothed image from the original. + Separates the image noise by smoothing the pixel array and subtracting the smoothed pixel array from the + original, leaving only the noise. Args: dcm (pydicom.Dataset): DICOM image object Returns: - np.array: pixel array representing the image noise + np.array: pixel array representing the image noise. """ a = dcm.pixel_array.astype("int") @@ -189,16 +189,16 @@ def get_noise_image(self, dcm: pydicom.Dataset) -> np.array: def get_roi_samples( self, ax, dcm: pydicom.Dataset or np.ndarray, centre_col: int, centre_row: int ) -> list: - """Identify regions of interest + """Takes the pixel array and divides it into several rectangular regions of interest (ROIs). If 'ax' is provided, then a plot of the ROIs is generated. Args: - ax (matplotlib.pyplot.subplots): matplotlib axis for visualisation - dcm (pydicom.Dataset or np.ndarray): DICOM image object, or its pixel array - centre_col (int): x coordinate of the centre - centre_row (int): y coordinate of the centre + ax (matplotlib.pyplot.subplots): matplotlib axis for visualisation. + dcm (pydicom.Dataset or np.ndarray): DICOM image object, or its pixel array. + centre_col (int): x coordinate of the centre. + centre_row (int): y coordinate of the centre. Returns: - list of np.array: subsets of the original pixel array + list of np.array: subsets of the original pixel array. """ if type(dcm) == np.ndarray: data = dcm @@ -246,14 +246,17 @@ def get_roi_samples( def snr_by_smoothing( self, dcm: pydicom.Dataset, measured_slice_width=None ) -> float: - """Calculate signal to noise ratio based on smoothing method + """Obtains a noise image using the single-image smoothing technique. Generates a ROI within the phantom region + of the pixel array. Then measures the mean signal within the ROI on the original pixel array, and the standard + deviation within the ROI on the noise image. Calculates SNR using these values and multiplies the SNR by the + normalisation factor. Args: - dcm (pydicom.Dataset): DICOM image object + dcm (pydicom.Dataset): DICOM image object. measured_slice_width (float, optional): Provide the true slice width for the set of images. Defaults to None. Returns: - float: normalised_snr + float: normalised_snr. """ centre = self.ACR_obj.centre col, row = centre @@ -305,15 +308,18 @@ def snr_by_smoothing( def snr_by_subtraction( self, dcm1: pydicom.Dataset, dcm2: pydicom.Dataset, measured_slice_width=None ) -> float: - """Calculate signal to noise ratio based on subtraction method + """Calculates signal to noise ratio using the two image subtraction method. Obtains a noise image by subtracting + the two pixel arrays. Obtains ROIs within the phantom region of the pixel arrays. Calculates the mean within + the ROI on one of the pixel arrays, and the standard deviation within the ROIs on the noise image. + Calculates the SNR with these measurements and multiplies by the normalisation factor. Args: - dcm1 (pydicom.Dataset): DICOM image object to calculate signal - dcm2 (pydicom.Dataset): DICOM image object to calculate noise + dcm1 (pydicom.Dataset): DICOM image object to calculate signal. + dcm2 (pydicom.Dataset): DICOM image object to calculate noise. measured_slice_width (float, optional): Provide the true slice width for the set of images. Defaults to None. Returns: - float: normalised_snr + float: normalised_snr. """ centre = self.ACR_obj.centre From 82c22b64a571f0c47a3aae65bb96111beaca5e43 Mon Sep 17 00:00:00 2001 From: mollybuckley Date: Mon, 12 Feb 2024 14:22:38 +0000 Subject: [PATCH 26/30] Google style docstrings for acr_uniformity --- hazenlib/tasks/acr_uniformity.py | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/hazenlib/tasks/acr_uniformity.py b/hazenlib/tasks/acr_uniformity.py index f7caec92..3e2c8e2d 100644 --- a/hazenlib/tasks/acr_uniformity.py +++ b/hazenlib/tasks/acr_uniformity.py @@ -26,9 +26,7 @@ class ACRUniformity(HazenTask): - """Uniformity measurement class for DICOM images of the ACR phantom - - Inherits from HazenTask class + """Uniformity measurement class for DICOM images of the ACR phantom. """ def __init__(self, **kwargs): @@ -37,8 +35,7 @@ def __init__(self, **kwargs): self.ACR_obj = ACRObject(self.dcm_list) def run(self) -> dict: - """Main function for performing uniformity measurement - using slice 7 from the ACR phantom image set + """Main function for performing uniformity measurement using slice 7 from the ACR phantom image set. Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation @@ -64,13 +61,21 @@ def run(self) -> dict: return results def get_integral_uniformity(self, dcm): - """Calculate the integral uniformity in accordance with ACR guidance. + """ + Calculates the percent integral uniformity (PIU) of a DICOM pixel array. Iterates with a ~1 cm^2 ROI through a + ~200 cm^2 ROI inside the phantom region, and calculates the mean non-zero pixel value inside each iteration of + the ~1 cm^2 ROI. The PIU is defined as: + + PIU = 100 * (1 - (max - min) / (max + min)) + + where 'max' and 'min' represent the maximum and minimum of the mean non-zero pixel values of each iteration + of the ~1 cm^2 ROI. Args: - dcm (pydicom.Dataset): DICOM image object to calculate uniformity from + dcm (pydicom.Dataset): DICOM image object to calculate uniformity from. Returns: - int or float: value of integral unformity + int or float: value of integral uniformity. """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata @@ -105,16 +110,17 @@ def get_integral_uniformity(self, dcm): mean_array = np.zeros(img_masked.shape) def uniformity_iterator(masked_image, sample_mask, rows, cols): - """Iterate through a pixel array and determine mean value + """Iterates spatially through the pixel array with a circular ROI and calculates the mean non-zero pixel + value within the circular ROI at each iteration. Args: - masked_image (np.array): subset of pixel array - sample_mask (np.array): _description_ - rows (np.array): 1D array - cols (np.array): 1D array + masked_image (np.array): subset of pixel array. + sample_mask (np.array): _description_. + rows (np.array): 1D array. + cols (np.array): 1D array. Returns: - np.array: array of mean values + np.array: array of mean values. """ coords = np.nonzero(sample_mask) # Coordinates of mask for idx, (row, col) in enumerate(zip(rows, cols)): From ba662fa42b690f12055c0cd6278f2e96b6e4178e Mon Sep 17 00:00:00 2001 From: sophie22 Date: Thu, 15 Feb 2024 13:20:18 +0000 Subject: [PATCH 27/30] minor stylistic changes, some typo fixes --- hazenlib/ACRObject.py | 40 +++++---- hazenlib/tasks/acr_ghosting.py | 123 ++++++++++++-------------- hazenlib/tasks/acr_slice_position.py | 103 +++++++++++---------- hazenlib/tasks/acr_slice_thickness.py | 23 +++-- hazenlib/tasks/acr_snr.py | 20 ++--- hazenlib/tasks/acr_uniformity.py | 47 ++++------ 6 files changed, 169 insertions(+), 187 deletions(-) diff --git a/hazenlib/ACRObject.py b/hazenlib/ACRObject.py index 3982fbff..563f8863 100644 --- a/hazenlib/ACRObject.py +++ b/hazenlib/ACRObject.py @@ -5,7 +5,7 @@ class ACRObject: - """Base class for performing tasks on image sets of the ACR phantom. + """Base class for performing tasks on image sets of the ACR phantom. \n acquired following the ACR Large phantom guidelines """ @@ -36,8 +36,8 @@ def sort_images(self): """Sort a stack of images based on slice position. Returns: - tuple of lists: img_stack and dcm_stack - img_stack - list of np.array of dcm.pixel_array: A sorted stack of images, where each image is represented as a 2D numpy array. \n + tuple of lists: + img_stack - list of np.ndarray of dcm.pixel_array: A sorted stack of images, where each image is represented as a 2D numpy array. \n dcm_stack - list of pydicom.Dataset objects """ # TODO: implement a check if phantom was placed in other than axial position @@ -133,7 +133,7 @@ def rotate_images(self): """Rotate the images by a specified angle. The value range and dimensions of the image are preserved. Returns: - np.array: The rotated images. + np.ndarray: The rotated images. """ return skimage.transform.rotate( @@ -143,11 +143,12 @@ def rotate_images(self): def find_phantom_center(self, img): """ Find the center of the ACR phantom by filtering the input slice and using the Hough circle detector. + Args: - img (np.array): pixel array of the dicom + img (np.ndarray): pixel array of the dicom Returns: - tuple of ints: representing the (x, y) coordinates of the center of the image + tuple of ints: (x, y) coordinates of the center of the image """ dx, dy = self.pixel_spacing @@ -169,17 +170,17 @@ def find_phantom_center(self, img): return centre, radius def get_mask_image(self, image, mag_threshold=0.07, open_threshold=500): - """Create a masked pixel array \n + """Create a masked pixel array. \n Mask an image by magnitude threshold before applying morphological opening to remove small unconnected features. The convex hull is calculated in order to accommodate for potential air bubbles. Args: - image (np.array): pixel array of the dicom + image (np.ndarray): pixel array of the dicom mag_threshold (float, optional): magnitude threshold. Defaults to 0.07. open_threshold (int, optional): open threshold. Defaults to 500. Returns: - np.array: the masked image + np.ndarray: the masked image """ test_mask = self.circular_mask( self.centre, (80 // self.pixel_spacing[0]), image.shape @@ -215,7 +216,7 @@ def circular_mask(centre, radius, dims): dims (tuple): dimensions of the circular mask. Returns: - np.array: A sorted stack of images, where each image is represented as a 2D numpy array. + np.ndarray: A sorted stack of images, where each image is represented as a 2D numpy array. """ # Define a circular logical mask x = np.linspace(1, dims[0], dims[0]) @@ -230,22 +231,24 @@ def measure_orthogonal_lengths(self, mask, slice_index): """Compute the horizontal and vertical lengths of a mask, based on the centroid. Args: - mask (np.array): Boolean array of the image where pixel values meet threshold + mask (np.ndarray): Boolean array of the image where pixel values meet threshold Returns: - dict: a dictionary with the following + dict: a dictionary with the following: 'Horizontal Start' | 'Vertical Start' : tuple of int Horizontal/vertical starting point of the object. 'Horizontal End' | 'Vertical End' : tuple of int Horizontal/vertical ending point of the object. - 'Horizontal Extent' | 'Vertical Extent' : ndarray of int + 'Horizontal Extent' | 'Vertical Extent' : np.ndarray of int Indices of the non-zero elements of the horizontal/vertical line profile. 'Horizontal Distance' | 'Vertical Distance' : float The horizontal/vertical length of the object. """ dims = mask.shape dx, dy = self.pixel_spacing - [(vertical, horizontal), radius] = self.find_phantom_center(self.images[slice_index]) + [(vertical, horizontal), radius] = self.find_phantom_center( + self.images[slice_index] + ) horizontal_start = (horizontal, 0) horizontal_end = (horizontal, dims[0] - 1) @@ -286,9 +289,8 @@ def rotate_point(origin, point, angle): angle (int): Angle in degrees. Returns: - tuple of float: x_prime and y_prime - Floats representing the x and y coordinates of the input point - after being rotated around an origin. + tuple of float: Floats representing the x and y coordinates of the input point + after being rotated around an origin. """ theta = np.radians(angle) c, s = np.cos(theta), np.sin(theta) @@ -302,12 +304,12 @@ def find_n_highest_peaks(data, n, height=1): """Find the indices and amplitudes of the N highest peaks within a 1D array. Args: - data (np.array): pixel array containing the data to perform peak extraction on + data (np.ndarray): pixel array containing the data to perform peak extraction on n (int): The coordinates of the point to rotate height (int, optional): The amplitude threshold for peak identification. Defaults to 1. Returns: - tuple of np.array: peak_locs and peak_heights + tuple of np.ndarray: peak_locs: A numpy array containing the indices of the N highest peaks identified. \n peak_heights: A numpy array containing the amplitudes of the N highest peaks identified. diff --git a/hazenlib/tasks/acr_ghosting.py b/hazenlib/tasks/acr_ghosting.py index 411092d9..441a5c76 100644 --- a/hazenlib/tasks/acr_ghosting.py +++ b/hazenlib/tasks/acr_ghosting.py @@ -25,8 +25,7 @@ class ACRGhosting(HazenTask): - """Ghosting measurement class for DICOM images of the ACR phantom. - """ + """Ghosting measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -37,9 +36,7 @@ def run(self) -> dict: """Main function for performing ghosting measurement using slice 7 from the ACR phantom image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. """ # Initialise results dictionary results = self.init_result_dict() @@ -61,9 +58,9 @@ def run(self) -> dict: return results def get_signal_ghosting(self, dcm): - """Draws four ellipses outside the phantom in four directions and calculates the mean - signal value within each of these. Calculates the percentage signal ghosting (PSG): the mean signal in these - four ROIs as a percentage of a ROI in the centre of the phantom. + """Calculates the percentage signal ghosting (PSG) \n + Draws four ellipses outside the phantom in four directions and calculates the mean + signal value within each of these, then expresses it as a percentage of a ROI in the centre of the phantom. Args: dcm (pydicom.Dataset): DICOM image object. @@ -72,10 +69,10 @@ def get_signal_ghosting(self, dcm): float: percentage ghosting value. """ img = dcm.pixel_array - res = dcm.PixelSpacing # In-plane resolution from metadata - r_large = np.ceil(80 / res[0]).astype( - int - ) # Required pixel radius to produce ~200cm2 ROI + # In-plane resolution from metadata + res = dcm.PixelSpacing + # Required pixel radius to produce ~200cm2 ROI + r_large = np.ceil(80 / res[0]).astype(int) dims = img.shape mask = self.ACR_obj.mask_image @@ -89,107 +86,101 @@ def get_signal_ghosting(self, dcm): lroi = np.square(x - cxy[0]) + np.square( y - cxy[1] - np.divide(5, res[1]) ) <= np.square(r_large) - sad = 2 * np.ceil( - np.sqrt(1000 / (4 * np.pi)) / res[0] - ) # Short axis diameter for an ellipse of 10cm2 with a 1:4 axis ratio + # Short axis diameter for an ellipse of 10cm2 with a 1:4 axis ratio + sad = 2 * np.ceil(np.sqrt(1000 / (4 * np.pi)) / res[0]) # WEST ELLIPSE - w_point = np.argwhere(np.sum(mask, 0) > 0)[0] # find first column in mask - w_centre = [cxy[1], np.floor(w_point / 2)] # initialise centre of ellipse - left_fov_to_centre = ( - w_centre[1] - sad / 2 - 5 - ) # edge of ellipse towards left FoV (+ tolerance) - centre_to_left_phantom = ( - w_centre[1] + sad / 2 + 5 - ) # edge of ellipse towards left side of phantom (+ tolerance) + # find first column in mask + w_point = np.argwhere(np.sum(mask, 0) > 0)[0] + # initialise centre of ellipse + w_centre = [cxy[1], np.floor(w_point / 2)] + # edge of ellipse towards left FoV (+ tolerance) + left_fov_to_centre = w_centre[1] - sad / 2 - 5 + # edge of ellipse towards left side of phantom (+ tolerance) + centre_to_left_phantom = w_centre[1] + sad / 2 + 5 if left_fov_to_centre < 0 or centre_to_left_phantom > w_point: diffs = [left_fov_to_centre, centre_to_left_phantom - w_point] ind = diffs.index(max(diffs, key=abs)) - w_factor = (sad / 2) / ( - sad / 2 - np.absolute(diffs[ind]) - ) # ellipse scaling factor + # ellipse scaling factor + w_factor = (sad / 2) / (sad / 2 - np.absolute(diffs[ind])) else: w_factor = 1 + # generate ellipse mask w_ellipse = np.square((y - w_centre[0]) / (4 * w_factor)) + np.square( (x - w_centre[1]) * w_factor - ) <= np.square( - 10 / res[0] - ) # generate ellipse mask + ) <= np.square(10 / res[0]) # EAST ELLIPSE - e_point = np.argwhere(np.sum(mask, 0) > 0)[-1] # find last column in mask + # find last column in mask + e_point = np.argwhere(np.sum(mask, 0) > 0)[-1] + # initialise centre of ellipse e_centre = [ cxy[1], e_point + np.ceil((dims[1] - e_point) / 2), - ] # initialise centre of ellipse - right_fov_to_centre = ( - e_centre[1] + sad / 2 + 5 - ) # edge of ellipse towards right FoV (+ tolerance) - centre_to_right_phantom = ( - e_centre[1] - sad / 2 - 5 - ) # edge of ellipse towards right side of phantom (+ tolerance) + ] + # edge of ellipse towards right FoV (+ tolerance) + right_fov_to_centre = e_centre[1] + sad / 2 + 5 + # edge of ellipse towards right side of phantom (+ tolerance) + centre_to_right_phantom = e_centre[1] - sad / 2 - 5 if right_fov_to_centre > dims[1] - 1 or centre_to_right_phantom < e_point: diffs = [ dims[1] - 1 - right_fov_to_centre, centre_to_right_phantom - e_point, ] ind = diffs.index(max(diffs, key=abs)) - e_factor = (sad / 2) / ( - sad / 2 - np.absolute(diffs[ind]) - ) # ellipse scaling factor + # ellipse scaling factor + e_factor = (sad / 2) / (sad / 2 - np.absolute(diffs[ind])) else: e_factor = 1 + # generate ellipse mask e_ellipse = np.square((y - e_centre[0]) / (4 * e_factor)) + np.square( (x - e_centre[1]) * e_factor - ) <= np.square( - 10 / res[0] - ) # generate ellipse mask + ) <= np.square(10 / res[0]) # NORTH ELLIPSE - n_point = np.argwhere(np.sum(mask, 1) > 0)[0] # find first row in mask - n_centre = [np.round(n_point / 2), cxy[0]] # initialise centre of ellipse - top_fov_to_centre = ( - n_centre[0] - sad / 2 - 5 - ) # edge of ellipse towards top FoV (+ tolerance) - centre_to_top_phantom = ( - n_centre[0] + sad / 2 + 5 - ) # edge of ellipse towards top side of phantom (+ tolerance) + # find first row in mask + n_point = np.argwhere(np.sum(mask, 1) > 0)[0] + # initialise centre of ellipse + n_centre = [np.round(n_point / 2), cxy[0]] + # edge of ellipse towards top FoV (+ tolerance) + top_fov_to_centre = n_centre[0] - sad / 2 - 5 + # edge of ellipse towards top side of phantom (+ tolerance) + centre_to_top_phantom = n_centre[0] + sad / 2 + 5 if top_fov_to_centre < 0 or centre_to_top_phantom > n_point: diffs = [top_fov_to_centre, centre_to_top_phantom - n_point] ind = diffs.index(max(diffs, key=abs)) - n_factor = (sad / 2) / ( - sad / 2 - np.absolute(diffs[ind]) - ) # ellipse scaling factor + # ellipse scaling factor + n_factor = (sad / 2) / (sad / 2 - np.absolute(diffs[ind])) else: n_factor = 1 + # generate ellipse mask n_ellipse = np.square((y - n_centre[0]) * n_factor) + np.square( (x - n_centre[1]) / (4 * n_factor) - ) <= np.square( - 10 / res[0] - ) # generate ellipse mask + ) <= np.square(10 / res[0]) # SOUTH ELLIPSE - s_point = np.argwhere(np.sum(mask, 1) > 0)[-1] # find last row in mask + # find last row in mask + s_point = np.argwhere(np.sum(mask, 1) > 0)[-1] + # initialise centre of ellipse s_centre = [ s_point + np.round((dims[1] - s_point) / 2), cxy[0], - ] # initialise centre of ellipse - bottom_fov_to_centre = ( - s_centre[0] + sad / 2 + 5 - ) # edge of ellipse towards bottom FoV (+ tolerance) - centre_to_bottom_phantom = s_centre[0] - sad / 2 - 5 # edge of ellipse towards + ] + # edge of ellipse towards bottom FoV (+ tolerance) + bottom_fov_to_centre = s_centre[0] + sad / 2 + 5 + # edge of ellipse towards + centre_to_bottom_phantom = s_centre[0] - sad / 2 - 5 if bottom_fov_to_centre > dims[0] - 1 or centre_to_bottom_phantom < s_point: diffs = [ dims[0] - 1 - bottom_fov_to_centre, centre_to_bottom_phantom - s_point, ] ind = diffs.index(max(diffs, key=abs)) - s_factor = (sad / 2) / ( - sad / 2 - np.absolute(diffs[ind]) - ) # ellipse scaling factor + # ellipse scaling factor + s_factor = (sad / 2) / (sad / 2 - np.absolute(diffs[ind])) else: s_factor = 1 diff --git a/hazenlib/tasks/acr_slice_position.py b/hazenlib/tasks/acr_slice_position.py index 3dbd16c4..55dbc31b 100644 --- a/hazenlib/tasks/acr_slice_position.py +++ b/hazenlib/tasks/acr_slice_position.py @@ -9,13 +9,13 @@ vertically through the left and right wedges. The right wedge's line profile is shifted and wrapped round before being subtracted from the left wedge's line profile, e.g.: -Right line profile: [1, 2, 3, 4, 5] +Right line profile: [1, 2, 3, 4, 5] \n Right line profile wrapped round by 1: [2, 3, 4, 5, 1] -This wrapping process, from hereon referred to as circular shifting, is then used for subtractions. +This wrapping process, from hereon referred to as 'circular shifting', is then used for subtractions. The shift used to produce the minimum difference between the circularly shifted right line profile and the static left -one is used to determine the bar length difference, which is twice the slice position displacement. +one is used to determine the bar length difference, which is twice the slice position displacement. \n The results are also visualised. Created by Yassine Azma @@ -38,8 +38,7 @@ class ACRSlicePosition(HazenTask): - """Slice position measurement class for DICOM images of the ACR phantom. - """ + """Slice position measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -51,9 +50,7 @@ def run(self) -> dict: image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. """ # Identify relevant slices dcms = [self.ACR_obj.dcms[0], self.ACR_obj.dcms[-1]] @@ -83,12 +80,12 @@ def run(self) -> dict: return results def find_wedges(self, img, mask, res): - """Investigates the top half of the phantom to locate where the wedges pass through the slice, and calculates the - co-ordinates of these locations. + """Investigates the top half of the phantom to locate where the wedges pass through the slice, + and calculates the co-ordinates of these locations. Args: - img (np.array): dcm.pixel_array. - mask (np.array): dcm.pixel_array of the image mask. + img (np.ndarray): dcm.pixel_array. + mask (np.ndarray): dcm.pixel_array of the image mask. res (float): dcm.PixelSpacing. Returns: @@ -112,18 +109,20 @@ def find_wedges(self, img, mask, res): invest_x = [] for k in range(x_investigate_region): - y_loc = n_point + k # add n_point to ensure in image's coordinate system - t = mask[ - y_loc, np.arange(w_point, e_point + 1, 1) - ] # mask for resultant line profile + # add n_point to ensure in image's coordinate system + y_loc = n_point + k + # mask for resultant line profile + t = mask[y_loc, np.arange(w_point, e_point + 1, 1)] # line profile at varying y positions from west to east line_prof_x = skimage.measure.profile_line( img, (y_loc, w_point), (y_loc, e_point), mode="constant" ).flatten() - invest_x.append(t * line_prof_x) # mask unwanted values out and append + # mask unwanted values out and append + invest_x.append(t * line_prof_x) - invest_x = np.array(invest_x).T # transpose array + # transpose array + invest_x = np.array(invest_x).T # mean of horizontal projections of phantom mean_x_profile = np.mean(invest_x, 1) # absolute first derivative of mean @@ -132,10 +131,12 @@ def find_wedges(self, img, mask, res): # find two highest peaks x_peaks, _ = self.ACR_obj.find_n_highest_peaks(abs_diff_x_profile, 2) # x coordinates of these peaks in image coordinate system(before diff operation) - x_locs = (w_point + x_peaks) + x_locs = w_point + x_peaks - width_pts = [x_locs[0], x_locs[1]] # width of wedges - width = np.max(width_pts) - np.min(width_pts) # width + # width of wedges + width_pts = [x_locs[0], x_locs[1]] + # width + width = np.max(width_pts) - np.min(width_pts) # rough midpoints of wedges x_pts = np.round( @@ -158,40 +159,36 @@ def find_wedges(self, img, mask, res): x_loc = ( m - np.floor(y_investigate_region / 2) + np.floor(np.mean(x_pts)) ).astype(int) - c = mask[ - np.arange(n_point, end_point + 1, 1), x_loc - ] # mask for resultant line profile + # mask for resultant line profile + c = mask[np.arange(n_point, end_point + 1, 1), x_loc] line_prof_y = skimage.measure.profile_line( img, (n_point, x_loc), (end_point, x_loc), mode="constant" ).flatten() invest_y.append(c * line_prof_y) - invest_y = np.array(invest_y).T # transpose array - mean_y_profile = np.mean(invest_y, 1) # mean of vertical projections of phantom - abs_diff_y_profile = np.abs( - np.diff(mean_y_profile) - ) # absolute first derivative of mean + # transpose array + invest_y = np.array(invest_y).T + # mean of vertical projections of phantom + mean_y_profile = np.mean(invest_y, 1) + # absolute first derivative of mean + abs_diff_y_profile = np.abs(np.diff(mean_y_profile)) - y_peaks, _ = self.ACR_obj.find_n_highest_peaks( - abs_diff_y_profile, 2 - ) # find two highest peaks - y_locs = ( - w_point + y_peaks - 1 - ) # y coordinates of these peaks in image coordinate system(before diff operation) + # find two highest peaks + y_peaks, _ = self.ACR_obj.find_n_highest_peaks(abs_diff_y_profile, 2) + # y coordinates of these peaks in image coordinate system(before diff operation) + y_locs = w_point + y_peaks - 1 if y_locs[1] - y_locs[0] < 5 / res[1]: - y = [ - n_point + round(10 / res[1]) - ] # if peaks too close together, use phantom geometry + # if peaks too close together, use phantom geometry + y = [n_point + round(10 / res[1])] else: - y = np.round( - np.min(y_locs) + 0.25 * np.abs(np.diff(y_locs)) - ) # define y coordinate + # define y coordinate + y = np.round(np.min(y_locs) + 0.25 * np.abs(np.diff(y_locs))) - dist_to_y = np.abs(n_point - y[0]) * res[1] # distance to y from top of phantom - y_pts = np.append(y, np.round(y[0] + (47 - dist_to_y) / res[1])).astype( - int - ) # place 2nd y point 47mm from top of phantom + # distance to y from top of phantom + dist_to_y = np.abs(n_point - y[0]) * res[1] + # place 2nd y point 47mm from top of phantom + y_pts = np.append(y, np.round(y[0] + (47 - dist_to_y) / res[1])).astype(int) return x_pts, y_pts @@ -209,14 +206,16 @@ def get_slice_position(self, dcm): mask = self.ACR_obj.mask_image x_pts, y_pts = self.find_wedges(img, mask, res) + # line profile through left wedge line_prof_L = skimage.measure.profile_line( img, (y_pts[0], x_pts[0]), (y_pts[1], x_pts[0]), mode="constant" - ).flatten() # line profile through left wedge + ).flatten() + # line profile through right wedge line_prof_R = skimage.measure.profile_line( img, (y_pts[0], x_pts[1]), (y_pts[1], x_pts[1]), mode="constant" - ).flatten() # line profile through right wedge + ).flatten() - #interpolation + # interpolation interp_factor = 5 x = np.arange(1, len(line_prof_L) + 1) new_x = np.arange( @@ -231,9 +230,10 @@ def get_slice_position(self, dcm): # difference of line profiles delta = interp_line_prof_L - interp_line_prof_R + # find two highest peaks peaks, _ = ACRObject.find_n_highest_peaks( abs(delta), 2, 0.5 * np.max(abs(delta)) - ) # find two highest peaks + ) # if only one peak, set dummy range if len(peaks) == 1: @@ -257,9 +257,8 @@ def get_slice_position(self, dcm): err = np.zeros(len(lag)) for k, lag_val in enumerate(lag): - difference = static_line_R - np.roll( - static_line_L, lag_val - ) # difference of L and circularly shifted R + # difference of L and circularly shifted R + difference = static_line_R - np.roll(static_line_L, lag_val) # set wrapped values to nan if lag_val > 0: difference[:lag_val] = np.nan diff --git a/hazenlib/tasks/acr_slice_thickness.py b/hazenlib/tasks/acr_slice_thickness.py index 2720fbc3..e20cc46c 100644 --- a/hazenlib/tasks/acr_slice_thickness.py +++ b/hazenlib/tasks/acr_slice_thickness.py @@ -27,8 +27,7 @@ class ACRSliceThickness(HazenTask): - """Slice width measurement class for DICOM images of the ACR phantom. - """ + """Slice width measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -39,9 +38,7 @@ def run(self) -> dict: """Main function for performing slice width measurement using slice 1 from the ACR phantom image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. """ # Identify relevant slice slice_thickness_dcm = self.ACR_obj.dcms[0] @@ -69,7 +66,7 @@ def find_ramps(self, img, centre, res): """Find ramps in the pixel array and returns co-ordinates of their location. Args: - img (np.array): dcm.pixel_array. + img (np.ndarray): dcm.pixel_array. centre (list): x,y coordinates of the phantom centre. res (float): dcm.PixelSpacing. @@ -131,18 +128,20 @@ def FWHM(self, data): """Calculate full width at half maximum of the line profile. Args: - data (np.array): slice profile curve. + data (np.ndarray): slice profile curve. Returns: tuple: co-ordinates of the half-maximum points on the line profile. """ baseline = np.min(data) data -= baseline - #TODO create separate variable so that data value isn't being rewritten + # TODO create separate variable so that data value isn't being rewritten half_max = np.max(data) * 0.5 # Naive attempt - half_max_crossing_indices = np.argwhere(np.diff(np.sign(data - half_max))).flatten() + half_max_crossing_indices = np.argwhere( + np.diff(np.sign(data - half_max)) + ).flatten() # Interpolation def simple_interp(x_start, ydata): @@ -150,7 +149,7 @@ def simple_interp(x_start, ydata): Args: x_start (int or float): x coordinate of the half maximum. - ydata (np.array): y coordinates. + ydata (np.ndarray): y coordinates. Returns: float: true x coordinate of the half maximum. @@ -181,7 +180,7 @@ def get_slice_thickness(self, dcm): """ img = dcm.pixel_array res = dcm.PixelSpacing # In-plane resolution from metadata - #TODO define object centre for slice 1 (not slice 7 as the default) + # TODO define object centre for slice 1 (not slice 7 as the default) cxy = self.ACR_obj.centre x_pts, y_pts = self.find_ramps(img, cxy, res) @@ -228,7 +227,7 @@ def get_slice_thickness(self, dcm): dz = 0.2 * (np.prod(ramp_length, axis=0)) / np.sum(ramp_length, axis=0) dz = dz[~np.isnan(dz)] - #TODO check this - if it's taking the value closest to the DICOM slice thickness this is potentially not accurate? + # TODO check this - if it's taking the value closest to the DICOM slice thickness this is potentially not accurate? z_ind = np.argmin(np.abs(dcm.SliceThickness - dz)) slice_thickness = dz[z_ind] diff --git a/hazenlib/tasks/acr_snr.py b/hazenlib/tasks/acr_snr.py index e3257744..82cacc1d 100644 --- a/hazenlib/tasks/acr_snr.py +++ b/hazenlib/tasks/acr_snr.py @@ -3,14 +3,15 @@ Calculates the SNR for slice 7 (the uniformity slice) of the ACR phantom. -This script utilises the smoothed subtraction method described in McCann 2013: -A quick and robust method for measurement of signal-to-noise ratio in MRI, Phys. Med. Biol. 58 (2013) 3775:3790 +This script utilises the smoothed subtraction method described in McCann 2013 [1], and a standard subtraction SNR. -and a standard subtraction SNR. - -Created by Neil Heraghty (Adapted by Yassine Azma) +Created by Neil Heraghty (Adapted by Yassine Azma, yassine.azma@rmh.nhs.uk) 09/01/2023 + +[1] McCann, A. J., Workman, A., & McGrath, C. (2013). A quick and robust +method for measurement of signal-to-noise ratio in MRI. Physics in Medicine +& Biology, 58(11), 3775. """ import os @@ -27,8 +28,7 @@ class ACRSNR(HazenTask): - """Signal-to-noise ratio measurement class for DICOM images of the ACR phantom. - """ + """Signal-to-noise ratio measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -48,10 +48,10 @@ def __init__(self, **kwargs): def run(self) -> dict: """Main function for performing SNR measurement using slice 7 from the ACR phantom image set. Performs either - smoothing or subtraction method depending on preferences set by user. + smoothing or subtraction method depending on user-provided input. Notes: - using the smoothing method by default or the subtraction method if a second set of images are provided (in a separate folder). + Uses the smoothing method by default or the subtraction method if a second set of images are provided (using the --subtract option with dataset in a separate folder). Returns: dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. @@ -159,7 +159,7 @@ def filtered_image(self, dcm: pydicom.Dataset) -> np.array: # filter size = 9, following MATLAB code and McCann 2013 paper for head coil, although note McCann 2013 # recommends 25x25 for body coil. - #TODO add coil options, same as with MagNet SNR + # TODO add coil options, same as with MagNet SNR filtered_array = ndimage.uniform_filter(a, 25, mode="constant") return filtered_array diff --git a/hazenlib/tasks/acr_uniformity.py b/hazenlib/tasks/acr_uniformity.py index 3e2c8e2d..9c2b0c74 100644 --- a/hazenlib/tasks/acr_uniformity.py +++ b/hazenlib/tasks/acr_uniformity.py @@ -26,8 +26,7 @@ class ACRUniformity(HazenTask): - """Uniformity measurement class for DICOM images of the ACR phantom. - """ + """Uniformity measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -61,41 +60,32 @@ def run(self) -> dict: return results def get_integral_uniformity(self, dcm): - """ - Calculates the percent integral uniformity (PIU) of a DICOM pixel array. Iterates with a ~1 cm^2 ROI through a - ~200 cm^2 ROI inside the phantom region, and calculates the mean non-zero pixel value inside each iteration of - the ~1 cm^2 ROI. The PIU is defined as: - - PIU = 100 * (1 - (max - min) / (max + min)) - - where 'max' and 'min' represent the maximum and minimum of the mean non-zero pixel values of each iteration - of the ~1 cm^2 ROI. + """Calculates the percent integral uniformity (PIU) of a DICOM pixel array. \n + Iterates with a ~1 cm^2 ROI through a ~200 cm^2 ROI inside the phantom region, + and calculates the mean non-zero pixel value inside each ~1 cm^2 ROI. \n + The PIU is defined as: `PIU = 100 * (1 - (max - min) / (max + min))`, where \n + 'max' and 'min' represent the maximum and minimum of the mean non-zero pixel values of each ~1 cm^2 ROI. Args: dcm (pydicom.Dataset): DICOM image object to calculate uniformity from. Returns: - int or float: value of integral uniformity. + float: value of integral uniformity. """ img = dcm.pixel_array - res = dcm.PixelSpacing # In-plane resolution from metadata - r_large = np.ceil(80 / res[0]).astype( - int - ) # Required pixel radius to produce ~200cm2 ROI - r_small = np.ceil(np.sqrt(100 / np.pi) / res[0]).astype( - int - ) # Required pixel radius to produce ~1cm2 ROI - d_void = np.ceil(5 / res[0]).astype( - int - ) # Offset distance for rectangular void at top of phantom + # In-plane resolution from metadata + res = dcm.PixelSpacing + # Required pixel radius to produce ~200cm2 ROI + r_large = np.ceil(80 / res[0]).astype(int) + # Required pixel radius to produce ~1cm2 ROI + r_small = np.ceil(np.sqrt(100 / np.pi) / res[0]).astype(int) + # Offset distance for rectangular void at top of phantom + d_void = np.ceil(5 / res[0]).astype(int) dims = img.shape # Dimensions of image cxy = self.ACR_obj.centre - base_mask = ACRObject.circular_mask( - (cxy[0], cxy[1] + d_void), r_small, dims - ) # Dummy circular mask at - # centroid - coords = np.nonzero(base_mask) # Coordinates of mask + # Dummy circular mask at + base_mask = ACRObject.circular_mask((cxy[0], cxy[1] + d_void), r_small, dims) lroi = self.ACR_obj.circular_mask([cxy[0], cxy[1] + d_void], r_large, dims) img_masked = lroi * img @@ -122,7 +112,8 @@ def uniformity_iterator(masked_image, sample_mask, rows, cols): Returns: np.array: array of mean values. """ - coords = np.nonzero(sample_mask) # Coordinates of mask + # Coordinates of mask + coords = np.nonzero(sample_mask) for idx, (row, col) in enumerate(zip(rows, cols)): centre = [row, col] translate_mask = [ From 6072a8c93c5c4d0035cde969cc294b4cf96702c3 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Thu, 15 Feb 2024 13:20:45 +0000 Subject: [PATCH 28/30] blacks formatting, docstring styling --- hazenlib/tasks/acr_geometric_accuracy.py | 194 +++++++++++++++-------- 1 file changed, 128 insertions(+), 66 deletions(-) diff --git a/hazenlib/tasks/acr_geometric_accuracy.py b/hazenlib/tasks/acr_geometric_accuracy.py index 5c4a8369..ef3b4567 100644 --- a/hazenlib/tasks/acr_geometric_accuracy.py +++ b/hazenlib/tasks/acr_geometric_accuracy.py @@ -36,8 +36,7 @@ class ACRGeometricAccuracy(HazenTask): - """Geometric accuracy measurement class for DICOM images of the ACR phantom. - """ + """Geometric accuracy measurement class for DICOM images of the ACR phantom.""" def __init__(self, **kwargs): super().__init__(**kwargs) @@ -47,35 +46,40 @@ def run(self) -> dict: """Main function for performing geometric accuracy measurement using the first and fifth slices from the ACR phantom image set. Returns: - dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM - Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the - generated images for visualisation. + dict: results are returned in a standardised dictionary structure specifying the task name, input DICOM Series Description + SeriesNumber + InstanceNumber, task measurement key-value pairs, optionally path to the generated images for visualisation. """ # Initialise results dictionary results = self.init_result_dict() - results['file'] = [self.img_desc(self.ACR_obj.dcms[0]), self.img_desc(self.ACR_obj.dcms[4])] + results["file"] = [ + self.img_desc(self.ACR_obj.dcms[0]), + self.img_desc(self.ACR_obj.dcms[4]), + ] try: lengths_1 = self.get_geometric_accuracy(0) - results['measurement'][self.img_desc(self.ACR_obj.dcms[0])] = { + results["measurement"][self.img_desc(self.ACR_obj.dcms[0])] = { "Horizontal distance": round(lengths_1[0], 2), - "Vertical distance": round(lengths_1[1], 2) + "Vertical distance": round(lengths_1[1], 2), } except Exception as e: - print(f"Could not calculate the geometric accuracy for {self.img_desc(self.ACR_obj.dcms[0])} because of : {e}") + print( + f"Could not calculate the geometric accuracy for {self.img_desc(self.ACR_obj.dcms[0])} because of : {e}" + ) traceback.print_exc(file=sys.stdout) try: lengths_5 = self.get_geometric_accuracy(4) - results['measurement'][self.img_desc(self.ACR_obj.dcms[4])] = { + results["measurement"][self.img_desc(self.ACR_obj.dcms[4])] = { "Horizontal distance": round(lengths_5[0], 2), "Vertical distance": round(lengths_5[1], 2), "Diagonal distance SW": round(lengths_5[2], 2), - "Diagonal distance SE": round(lengths_5[3], 2) + "Diagonal distance SE": round(lengths_5[3], 2), } except Exception as e: - print(f"Could not calculate the geometric accuracy for {self.img_desc(self.ACR_obj.dcms[4])} because of : {e}") + print( + f"Could not calculate the geometric accuracy for {self.img_desc(self.ACR_obj.dcms[4])} because of : {e}" + ) traceback.print_exc(file=sys.stdout) @@ -95,11 +99,11 @@ def run(self) -> dict: return results - def get_geometric_accuracy(self, slice_index): - """Measure geometric accuracy for input slice. Creates a mask over the phantom from the pixel array of the DICOM - image. Uses the centre and shape of the mask to determine horizontal and vertical lengths, and also diagonal lengths - in the case of slice 5. + """Measure geometric accuracy for input slice. \n + Creates a mask over the phantom from the pixel array of the DICOM image. + Uses the centre and shape of the mask to determine horizontal and vertical lengths, + and also diagonal lengths in slice 5. Args: slice_index (int): the index of the slice position, for example slice 5 would have an index of 4. @@ -125,69 +129,127 @@ def get_geometric_accuracy(self, slice_index): if slice_index == 0: axes[0].imshow(img) - axes[0].scatter(cxy[0], cxy[1], c='red') - axes[0].set_title('Centroid Location') + axes[0].scatter(cxy[0], cxy[1], c="red") + axes[0].set_title("Centroid Location") axes[1].imshow(mask) - axes[1].set_title('Thresholding Result') + axes[1].set_title("Thresholding Result") axes[2].imshow(img) - axes[2].arrow(length_dict['Horizontal Extent'][0], cxy[1], - length_dict['Horizontal Extent'][-1] - length_dict['Horizontal Extent'][0], 1, - color='blue', - length_includes_head=True, head_width=5) - axes[2].arrow(cxy[0], length_dict['Vertical Extent'][0], 1, length_dict['Vertical Extent'][-1] - - length_dict['Vertical Extent'][0], color='orange', length_includes_head=True, - head_width=5) - axes[2].legend([str(np.round(length_dict['Horizontal Distance'], 2)) + 'mm', - str(np.round(length_dict['Vertical Distance'], 2)) + 'mm']) - axes[2].axis('off') - axes[2].set_title('Geometric Accuracy for Slice 1') - - img_path = os.path.realpath(os.path.join(self.report_path, f'{self.img_desc(img_dcm)}.png')) + axes[2].arrow( + length_dict["Horizontal Extent"][0], + cxy[1], + length_dict["Horizontal Extent"][-1] + - length_dict["Horizontal Extent"][0], + 1, + color="blue", + length_includes_head=True, + head_width=5, + ) + axes[2].arrow( + cxy[0], + length_dict["Vertical Extent"][0], + 1, + length_dict["Vertical Extent"][-1] + - length_dict["Vertical Extent"][0], + color="orange", + length_includes_head=True, + head_width=5, + ) + axes[2].legend( + [ + str(np.round(length_dict["Horizontal Distance"], 2)) + "mm", + str(np.round(length_dict["Vertical Distance"], 2)) + "mm", + ] + ) + axes[2].axis("off") + axes[2].set_title("Geometric Accuracy for Slice 1") + + img_path = os.path.realpath( + os.path.join(self.report_path, f"{self.img_desc(img_dcm)}.png") + ) fig.savefig(img_path) self.report_files.append(img_path) if slice_index == 4: axes[0].imshow(img) - axes[0].scatter(cxy[0], cxy[1], c='red') - axes[0].axis('off') - axes[0].set_title('Centroid Location') + axes[0].scatter(cxy[0], cxy[1], c="red") + axes[0].axis("off") + axes[0].set_title("Centroid Location") axes[1].imshow(mask) - axes[1].axis('off') - axes[1].set_title('Thresholding Result') + axes[1].axis("off") + axes[1].set_title("Thresholding Result") axes[2].imshow(img) - axes[2].arrow(length_dict['Horizontal Extent'][0], cxy[1], length_dict['Horizontal Extent'][-1] - - length_dict['Horizontal Extent'][0], 1, color='blue', length_includes_head=True, - head_width=5) - axes[2].arrow(cxy[0], length_dict['Vertical Extent'][0], 1, length_dict['Vertical Extent'][-1] - - length_dict['Vertical Extent'][0], color='orange', length_includes_head=True, head_width=5) - axes[2].arrow(se_dict['Start'][0], se_dict['Start'][1], se_dict['Extent'][0], se_dict['Extent'][1], - color='purple', length_includes_head=True, head_width=5) - axes[2].arrow(sw_dict['Start'][0], sw_dict['Start'][1], sw_dict['Extent'][0], sw_dict['Extent'][1], - color='yellow', length_includes_head=True, head_width=5) - - axes[2].legend([str(np.round(length_dict['Horizontal Distance'], 2)) + 'mm', - str(np.round(length_dict['Vertical Distance'], 2)) + 'mm', - str(np.round(sw_dict['Distance'], 2)) + 'mm', - str(np.round(se_dict['Distance'], 2)) + 'mm']) - axes[2].axis('off') - axes[2].set_title('Geometric Accuracy for Slice 5') - - img_path = os.path.realpath(os.path.join(self.report_path, f'{self.img_desc(img_dcm)}.png')) + axes[2].arrow( + length_dict["Horizontal Extent"][0], + cxy[1], + length_dict["Horizontal Extent"][-1] + - length_dict["Horizontal Extent"][0], + 1, + color="blue", + length_includes_head=True, + head_width=5, + ) + axes[2].arrow( + cxy[0], + length_dict["Vertical Extent"][0], + 1, + length_dict["Vertical Extent"][-1] + - length_dict["Vertical Extent"][0], + color="orange", + length_includes_head=True, + head_width=5, + ) + axes[2].arrow( + se_dict["Start"][0], + se_dict["Start"][1], + se_dict["Extent"][0], + se_dict["Extent"][1], + color="purple", + length_includes_head=True, + head_width=5, + ) + axes[2].arrow( + sw_dict["Start"][0], + sw_dict["Start"][1], + sw_dict["Extent"][0], + sw_dict["Extent"][1], + color="yellow", + length_includes_head=True, + head_width=5, + ) + + axes[2].legend( + [ + str(np.round(length_dict["Horizontal Distance"], 2)) + "mm", + str(np.round(length_dict["Vertical Distance"], 2)) + "mm", + str(np.round(sw_dict["Distance"], 2)) + "mm", + str(np.round(se_dict["Distance"], 2)) + "mm", + ] + ) + axes[2].axis("off") + axes[2].set_title("Geometric Accuracy for Slice 5") + + img_path = os.path.realpath( + os.path.join(self.report_path, f"{self.img_desc(img_dcm)}.png") + ) fig.savefig(img_path) self.report_files.append(img_path) if slice_index == 4: - return length_dict['Horizontal Distance'], length_dict['Vertical Distance'], \ - sw_dict['Distance'], se_dict['Distance'] + return ( + length_dict["Horizontal Distance"], + length_dict["Vertical Distance"], + sw_dict["Distance"], + se_dict["Distance"], + ) else: - return length_dict['Horizontal Distance'], length_dict['Vertical Distance'] + return length_dict["Horizontal Distance"], length_dict["Vertical Distance"] def diagonal_lengths(self, img, cxy, slice_index): - """Measure diagonal lengths. Rotates the pixel array by 45° and measures the horizontal and vertical distances. + """Measure diagonal lengths by rotating the pixel array by 45° and measure the horizontal and vertical distances. Args: img (np.array): pixel array of the slice (dcm.pixel_array). @@ -195,19 +257,19 @@ def diagonal_lengths(self, img, cxy, slice_index): slice_index (int): index of the slice number. Returns: - tuple of dictionaries: for both the south-east (SE) diagonal length and the south-west (SW) diagonal length: - "start" and "end" indicate the start and end x and y positions of the lengths; "Extent" is the distance (in - pixels) of the lengths; "Distance" is "Extent" with factors applied to convert from pixels to mm. + tuple of dictionaries: for both the south-east (SE) diagonal length and the south-west (SW) diagonal length: \n + "start" and "end" indicate the start and end x and y positions of the lengths; "Extent" is the distance (in + pixels) of the lengths; "Distance" is "Extent" with factors applied to convert from pixels to mm. """ res = self.ACR_obj.pixel_spacing # getting the geometric mean of the x and y pixel spacing components, as the pixel_spacing DICOM attribute is in - #co-ordinate form due to the possibility of pixels being rectangular, ie. the length and width of pixels can - #differ. + # co-ordinate form due to the possibility of pixels being rectangular, ie. the length and width of pixels can + # differ. eff_res = np.sqrt(np.mean(np.square(res))) img_rotate = skimage.transform.rotate(img, 45, center=(cxy[0], cxy[1])) length_dict = self.ACR_obj.measure_orthogonal_lengths(img_rotate, slice_index) - extent_h = length_dict['Horizontal Extent'] + extent_h = length_dict["Horizontal Extent"] origin = (cxy[0], cxy[1]) start = (extent_h[0], cxy[1]) @@ -257,7 +319,7 @@ def distortion_metric(L): Returns: tuple of floats: mean_err, max_err, cov_l """ - #TODO change function name to eg. get_distortion_metrics + # TODO change function name to eg. get_distortion_metrics err = [x - 190 for x in L] mean_err = np.mean(err) From 5c88b0f7f3bc77fb0667c14bf01c2a707943edb9 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 27 Feb 2024 10:07:12 +0000 Subject: [PATCH 29/30] resolve some TODOs --- hazenlib/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hazenlib/utils.py b/hazenlib/utils.py index 76bca648..9352a0b3 100644 --- a/hazenlib/utils.py +++ b/hazenlib/utils.py @@ -220,7 +220,6 @@ def get_TR(dcm: pydicom.Dataset) -> float: float: value of the RepetitionTime field from the DICOM header, or defaults to 1000 """ # TODO: explore what type of DICOM files do not have RepetitionTime in DICOM header - # check with physicists whether 1000 is an appropriate default value try: TR = dcm.RepetitionTime except: @@ -238,8 +237,6 @@ def get_rows(dcm: pydicom.Dataset) -> float: Returns: float: value of the Rows field from the DICOM header, or defaults to 256 """ - # TODO: explore what type of DICOM files do not have Rows in DICOM header - # check with physicists whether 256 is an appropriate default value try: rows = dcm.Rows except: @@ -260,8 +257,6 @@ def get_columns(dcm: pydicom.Dataset) -> float: Returns: float: value of the Columns field from the DICOM header, or defaults to 256 """ - # TODO: explore what type of DICOM files do not have Columns in DICOM header - # check with physicists whether 256 is an appropriate default value try: columns = dcm.Columns except: From 1fec22f4c2493e2b3271bfbfb5f978a832054c88 Mon Sep 17 00:00:00 2001 From: sophie22 Date: Tue, 27 Feb 2024 10:11:19 +0000 Subject: [PATCH 30/30] revert img value --- hazenlib/tasks/ghosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hazenlib/tasks/ghosting.py b/hazenlib/tasks/ghosting.py index f9e5f8a2..a20ab718 100644 --- a/hazenlib/tasks/ghosting.py +++ b/hazenlib/tasks/ghosting.py @@ -352,7 +352,7 @@ def get_ghosting(self, dcm) -> float: img = img.astype("float64") # print('this is img',img) img *= 255.0 / img.max() - img = hazenlib.utils.rescale_to_byte(dcm.pixel_array) + # img = hazenlib.utils.rescale_to_byte(dcm.pixel_array) img = cv.rectangle(img.copy(), (x1, y1), (x2, y2), (255, 0, 0), 1) for roi in background_rois: