Skip to content

Commit

Permalink
Merge pull request #399 from GSTT-CSC/378-geom-accuracy-wrong-slices
Browse files Browse the repository at this point in the history
This update corrects the use of a mask created for slices 1 and 5, and taking the phantom centre from these slices rather than 7 in order to more accurately calculate geometric accuracy.

Below functions updated in the ACRobject.py are updated to more flexibly take the slice as input rather implicitly use slice 7:
- find_phantom_centre
- get_mask_image
- measure_orthogonal_lengths

Furthermore, for the get_mask_image function the threshold was increased to overcome background noise in some images.
  • Loading branch information
sophie22 authored Jan 18, 2024
2 parents c8f540c + 2698256 commit 8cb6f66
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 200 deletions.
35 changes: 17 additions & 18 deletions hazenlib/ACRObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ def __init__(self, dcm_list):
self.orientation_checks()
# Determine whether image rotation is necessary
self.rot_angle = self.determine_rotation()
# Find the centre coordinates of the phantom (circle) on slice 7 only:
self.centre, self.radius = self.find_phantom_center(self.images[6])
# Store the DCM object of slice 7 as it is used often
self.slice7_dcm = self.dcms[6]
# Find the centre coordinates of the phantom (circle)
self.centre, self.radius = self.find_phantom_center()
# Store a mask image of slice 7 for reusability
self.mask_image = self.get_mask_image(self.images[6])

Expand Down Expand Up @@ -144,17 +144,15 @@ def rotate_images(self):
self.images, self.rot_angle, resize=False, preserve_range=True
)

def find_phantom_center(self):
def find_phantom_center(self, img):
"""
Find the center of the ACR phantom by filtering the uniformity slice and using the Hough circle detector.
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
Returns
-------
centre : tuple
Tuple of ints representing the (x, y) center of the image.
Returns:
tuple: Tuple of ints representing the (x, y) center of the image.
"""
img = self.images[6]
dx, dy = self.pixel_spacing

img_blur = cv2.GaussianBlur(img, (1, 1), 0)
Expand All @@ -174,14 +172,14 @@ def find_phantom_center(self):
radius = int(detected_circles[2])
return centre, radius

def get_mask_image(self, image, mag_threshold=0.05, open_threshold=500):
def get_mask_image(self, image, mag_threshold=0.07, open_threshold=500):
"""Create a masked pixel array
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 (_type_): _description_
mag_threshold (float, optional): magnitude threshold. Defaults to 0.05.
image (np.array): 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:
Expand Down Expand Up @@ -240,7 +238,7 @@ def circular_mask(centre, radius, dims):

return mask

def measure_orthogonal_lengths(self, mask):
def measure_orthogonal_lengths(self, mask, slice_index):
"""
Compute the horizontal and vertical lengths of a mask, based on the centroid.
Expand All @@ -264,17 +262,18 @@ def measure_orthogonal_lengths(self, mask):
"""
dims = mask.shape
dx, dy = self.pixel_spacing
[(vertical, horizontal), radius] = self.find_phantom_center(self.images[slice_index])

horizontal_start = (self.centre[1], 0)
horizontal_end = (self.centre[1], dims[0] - 1)
horizontal_start = (horizontal, 0)
horizontal_end = (horizontal, dims[0] - 1)
horizontal_line_profile = skimage.measure.profile_line(
mask, horizontal_start, horizontal_end
)
horizontal_extent = np.nonzero(horizontal_line_profile)[0]
horizontal_distance = (horizontal_extent[-1] - horizontal_extent[0]) * dx

vertical_start = (0, self.centre[0])
vertical_end = (dims[1] - 1, self.centre[0])
vertical_start = (0, vertical)
vertical_end = (dims[1] - 1, vertical)
vertical_line_profile = skimage.measure.profile_line(
mask, vertical_start, vertical_end
)
Expand Down
Loading

4 comments on commit 8cb6f66

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
hazenlib
   ACRObject.py1071190%92, 95–97, 102–105, 143, 197–200
   HazenTask.py28389%43–47
   __init__.py561573%173, 205–214, 216–225, 227–229, 246–250, 254
   exceptions.py21576%19–23, 42
   utils.py1894377%72, 76, 86, 91, 130, 137–150, 161, 168–175, 195–197, 215–219, 238–242, 251, 256, 267, 319, 322, 330–335, 338, 385, 394, 409
hazenlib/tasks
   acr_geometric_accuracy.py1125848%57–97, 119–180
   acr_ghosting.py1074261%46–62, 105–107, 159–161, 211–293
   acr_slice_position.py1364865%59–83, 286–353
   acr_slice_thickness.py1366056%48–67, 238–322
   acr_snr.py1345857%47, 62–113, 132, 227–242, 283–301, 348–373
   acr_spatial_resolution.py2076867%69–100, 188, 286, 303–314, 460–539
   acr_uniformity.py813260%47–64, 148–200
   ghosting.py1495166%28–47, 67, 171–172, 179, 196–197, 252–256, 271–275, 346–387
   relaxometry.py2908969%210–211, 213, 226–231, 238–246, 277–326, 375, 409–431, 609, 655–659, 726, 811–833, 851–866
   slice_position.py1244068%30, 43–71, 129–130, 157, 273, 283–306
   slice_width.py3515385%44–48, 52, 123, 188–213, 383, 555, 560–561, 567, 572, 648–649, 1021–1085
   snr.py1736960%45–48, 87, 103–113, 206–225, 237–247, 287–302, 330–340, 345–361, 399–415, 428–434, 477–495
   snr_map.py107199%157
   spatial_resolution.py2464582%50–54, 58, 90, 191, 213, 294, 460–503
   uniformity.py801976%59–63, 67, 118–119, 126, 175–205
TOTAL285981072% 

Tests Skipped Failures Errors Time
198 0 💤 0 ❌ 0 🔥 2m 18s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
hazenlib
   ACRObject.py1071190%92, 95–97, 102–105, 143, 197–200
   HazenTask.py29390%43–47
   __init__.py571574%173, 205–214, 216–225, 227–229, 246–250, 254
   exceptions.py21576%19–23, 42
   utils.py1894377%72, 76, 86, 91, 130, 137–150, 161, 168–175, 195–197, 215–219, 238–242, 251, 256, 267, 319, 322, 330–335, 338, 385, 394, 409
hazenlib/tasks
   acr_geometric_accuracy.py1125848%57–97, 119–180
   acr_ghosting.py1074261%46–62, 105–107, 159–161, 211–293
   acr_slice_position.py1364865%59–83, 286–353
   acr_slice_thickness.py1366056%48–67, 238–322
   acr_snr.py1345857%47, 62–113, 132, 227–242, 283–301, 348–373
   acr_spatial_resolution.py2076867%69–100, 188, 286, 303–314, 460–539
   acr_uniformity.py813260%47–64, 148–200
   ghosting.py1495166%28–47, 67, 171–172, 179, 196–197, 252–256, 271–275, 346–387
   relaxometry.py2918969%210–211, 213, 226–231, 238–246, 277–326, 375, 409–431, 609, 655–659, 726, 811–833, 851–866
   slice_position.py1244068%30, 43–71, 129–130, 157, 273, 283–306
   slice_width.py3525285%44–48, 52, 123, 188–213, 555, 560–561, 567, 572, 648–649, 1021–1085
   snr.py1736960%45–48, 87, 103–113, 206–225, 237–247, 287–302, 330–340, 345–361, 399–415, 428–434, 477–495
   snr_map.py108199%157
   spatial_resolution.py2464482%50–54, 58, 90, 213, 294, 460–503
   uniformity.py801976%59–63, 67, 118–119, 126, 175–205
TOTAL286480872% 

Tests Skipped Failures Errors Time
198 0 💤 0 ❌ 0 🔥 2m 18s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
hazenlib
   ACRObject.py1071190%92, 95–97, 102–105, 143, 197–200
   HazenTask.py28389%43–47
   __init__.py561573%173, 205–214, 216–225, 227–229, 246–250, 254
   exceptions.py21576%19–23, 42
   utils.py1894377%72, 76, 86, 91, 130, 137–150, 161, 168–175, 195–197, 215–219, 238–242, 251, 256, 267, 319, 322, 330–335, 338, 385, 394, 409
hazenlib/tasks
   acr_geometric_accuracy.py1125848%57–97, 119–180
   acr_ghosting.py1074261%46–62, 105–107, 159–161, 211–293
   acr_slice_position.py1364865%59–83, 286–353
   acr_slice_thickness.py1366056%48–67, 238–322
   acr_snr.py1345857%47, 62–113, 132, 227–242, 283–301, 348–373
   acr_spatial_resolution.py2076867%69–100, 188, 286, 303–314, 460–539
   acr_uniformity.py813260%47–64, 148–200
   ghosting.py1495166%28–47, 67, 171–172, 179, 196–197, 252–256, 271–275, 346–387
   relaxometry.py2908969%210–211, 213, 226–231, 238–246, 277–326, 375, 409–431, 609, 655–659, 726, 811–833, 851–866
   slice_position.py1244068%30, 43–71, 129–130, 157, 273, 283–306
   slice_width.py3515385%44–48, 52, 123, 188–213, 383, 555, 560–561, 567, 572, 648–649, 1021–1085
   snr.py1736960%45–48, 87, 103–113, 206–225, 237–247, 287–302, 330–340, 345–361, 399–415, 428–434, 477–495
   snr_map.py107199%157
   spatial_resolution.py2464582%50–54, 58, 90, 191, 213, 294, 460–503
   uniformity.py801976%59–63, 67, 118–119, 126, 175–205
TOTAL285981072% 

Tests Skipped Failures Errors Time
198 0 💤 0 ❌ 0 🔥 2m 17s ⏱️

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
hazenlib
   ACRObject.py1071190%92, 95–97, 102–105, 143, 197–200
   HazenTask.py29390%43–47
   __init__.py571574%173, 205–214, 216–225, 227–229, 246–250, 254
   exceptions.py21576%19–23, 42
   utils.py1894377%72, 76, 86, 91, 130, 137–150, 161, 168–175, 195–197, 215–219, 238–242, 251, 256, 267, 319, 322, 330–335, 338, 385, 394, 409
hazenlib/tasks
   acr_geometric_accuracy.py1125848%57–97, 119–180
   acr_ghosting.py1074261%46–62, 105–107, 159–161, 211–293
   acr_slice_position.py1364865%59–83, 286–353
   acr_slice_thickness.py1366056%48–67, 238–322
   acr_snr.py1345857%47, 62–113, 132, 227–242, 283–301, 348–373
   acr_spatial_resolution.py2076867%69–100, 188, 286, 303–314, 460–539
   acr_uniformity.py813260%47–64, 148–200
   ghosting.py1495166%28–47, 67, 171–172, 179, 196–197, 252–256, 271–275, 346–387
   relaxometry.py2918969%210–211, 213, 226–231, 238–246, 277–326, 375, 409–431, 609, 655–659, 726, 811–833, 851–866
   slice_position.py1244068%30, 43–71, 129–130, 157, 273, 283–306
   slice_width.py3525285%44–48, 52, 123, 188–213, 555, 560–561, 567, 572, 648–649, 1021–1085
   snr.py1736960%45–48, 87, 103–113, 206–225, 237–247, 287–302, 330–340, 345–361, 399–415, 428–434, 477–495
   snr_map.py108199%157
   spatial_resolution.py2464482%50–54, 58, 90, 213, 294, 460–503
   uniformity.py801976%59–63, 67, 118–119, 126, 175–205
TOTAL286480872% 

Tests Skipped Failures Errors Time
198 0 💤 0 ❌ 0 🔥 2m 17s ⏱️

Please sign in to comment.