Skip to content

Commit

Permalink
Merge branch 'pull/200'
Browse files Browse the repository at this point in the history
# Conflicts:
#	pylinac/flatsym.py
#	tests_basic/test_flatsym.py
  • Loading branch information
jrkerns committed May 6, 2020
2 parents 458b297 + 872a2ae commit 72b7b83
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 39 deletions.
69 changes: 52 additions & 17 deletions pylinac/flatsym.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Tuple, Union

import matplotlib.pyplot as plt
import numpy as np

from pylinac.core.utilities import open_path
from .core.exceptions import NotAnalyzed
Expand Down Expand Up @@ -96,6 +97,8 @@ class FlatSym:
Contains the method of calculation and the vertical and horizontal flatness data including the value.
positions : dict
The position ratio used for analysis for vertical and horizontal.
widths : dict
The width ratios used for analysis for vertical and horizontal.
"""

def __init__(self, path: str):
Expand All @@ -109,6 +112,7 @@ def __init__(self, path: str):
self.symmetry: dict = {}
self.flatness: dict = {}
self.positions: dict = {}
self.widths: dict = {}
self._is_analyzed: bool = False
self.image.check_inversion_by_histogram()

Expand All @@ -126,7 +130,8 @@ def run_demo():
print(fs.results())
fs.plot()

def analyze(self, flatness_method: str, symmetry_method: str, vert_position: float=0.5, horiz_position: float=0.5, invert=False):
def analyze(self, flatness_method: str, symmetry_method: str, vert_position: float=0.5, horiz_position: float=0.5,
vert_width=0, horiz_width=0):
"""Analyze the image to determine flatness & symmetry.
Parameters
Expand All @@ -141,15 +146,17 @@ def analyze(self, flatness_method: str, symmetry_method: str, vert_position: flo
horiz_position : float (0.0-1.0)
The distance ratio of the image to sample. E.g. at the default of 0.5 the profile is extracted
in the middle of the image. 0.0 is at the top edge of the image and 1.0 is at the bottom edge of the image.
invert : bool
Whether to invert the image. Setting this to True will override the default inversion. This is useful if
pylinac's automatic inversion is incorrect.
vert_width : float (0.0-1.0)
The width ratio of the image to sample. E.g. at the default of 0.0 a 1 pixel wide profile is extracted
in the middle of the image. 0.0 is 1 pixel wide and 1.0 is the image width.
horiz_width : float (0.0-1.0)
The width ratio of the image to sample. E.g. at the default of 0.0 a 1 pixel wide profile is extracted
in the middle of the image. 0.0 is 1 pixel wide and 1.0 is the image width.
"""
if invert:
self.image.invert()
self.symmetry = self._calc_symmetry(symmetry_method, vert_position, horiz_position)
self.flatness = self._calc_flatness(flatness_method, vert_position, horiz_position)
self.symmetry = self._calc_symmetry(symmetry_method, vert_position, horiz_position, vert_width, horiz_width)
self.flatness = self._calc_flatness(flatness_method, vert_position, horiz_position, vert_width, horiz_width)
self.positions = {'vertical': vert_position, 'horizontal': horiz_position}
self.widths = {'vertical': vert_width, 'horizontal': horiz_width}
self._is_analyzed = True

def results(self, as_str=True) -> Union[str, list]:
Expand Down Expand Up @@ -204,9 +211,28 @@ def results(self, as_str=True) -> Union[str, list]:
results = '\n'.join(result for result in results)
return results

def _calc_symmetry(self, method: str, vert_position: float, horiz_position: float):
vert_profile = SingleProfile(self.image.array[:, int(round(self.image.array.shape[1]*vert_position))])
horiz_profile = SingleProfile(self.image.array[int(round(self.image.array.shape[0]*horiz_position)), :])
def _get_vert_profile(self, vert_position: float, vert_width: float):
left_width = int(round(self.array.shape[1]*vert_position - self.array.shape[1]*vert_width/2))
if left_width < 0:
left_width = 0
right_width = int(round(self.array.shape[1]*vert_position + self.array.shape[1]*vert_width/2) + 1)
if right_width > self.array.shape[1]:
right_width = self.array.shape[1]
return SingleProfile(np.sum(self.array[:, left_width:right_width], 1))

def _get_horiz_profile(self, horiz_position: float, horiz_width: float):
bottom_width = int(round(self.array.shape[0] * horiz_position - self.array.shape[0] * horiz_width / 2))
if bottom_width < 0:
bottom_width = 0
top_width = int(round(self.array.shape[0] * horiz_position + self.array.shape[0] * horiz_width / 2) + 1)
if top_width > self.array.shape[0]:
top_width = self.array.shape[0]
return SingleProfile(np.sum(self.array[bottom_width:top_width, :], 0))

def _calc_symmetry(self, method: str, vert_position: float, horiz_position: float, vert_width: float, horiz_width: float):
vert_profile = self._get_vert_profile(vert_position, vert_width)
horiz_profile = self._get_horiz_profile(horiz_position, horiz_width)

# calc sym from profile
symmetry_calculation = SYMMETRY_EQUATIONS[method.lower()]
vert_sym, vert_sym_array, vert_lt, vert_rt = symmetry_calculation(vert_profile)
Expand All @@ -221,9 +247,10 @@ def _calc_symmetry(self, method: str, vert_position: float, horiz_position: floa
},
}

def _calc_flatness(self, method: str, vert_position: float, horiz_position: float):
vert_profile = SingleProfile(self.image.array[:, int(round(self.image.array.shape[1] * vert_position))])
horiz_profile = SingleProfile(self.image.array[int(round(self.image.array.shape[0] * horiz_position)), :])
def _calc_flatness(self, method: str, vert_position: float, horiz_position: float, vert_width: float, horiz_width: float):
vert_profile = self._get_vert_profile(vert_position, vert_width)
horiz_profile = self._get_horiz_profile(horiz_position, horiz_width)

# calc flatness from profile
flatness_calculation = FLATNESS_EQUATIONS[method.lower()]
vert_flatness, vert_max, vert_min, vert_lt, vert_rt = flatness_calculation(vert_profile)
Expand Down Expand Up @@ -320,9 +347,17 @@ def _plot_image(self, axis: plt.Axes=None, title: str=''):
plt.ioff()
if axis is None:
fig, axis = plt.subplots()
axis.imshow(self.image.array, cmap=get_dicom_cmap())
axis.axhline(self.positions['horizontal']*self.image.array.shape[0], color='r') # y
axis.axvline(self.positions['vertical']*self.image.array.shape[1], color='r') # x
axis.imshow(self.array, cmap=get_dicom_cmap())
#show horizontal profiles
left_profile = (self.positions['horizontal'] - self.widths['horizontal']/2)*self.array.shape[0]
right_profile = (self.positions['horizontal'] + self.widths['horizontal']/2)*self.array.shape[0]
axis.axhline(left_profile, color='r') # X
axis.axhline(right_profile, color='r') # X
#show vertical profiles
bottom_profile = (self.positions['vertical'] - self.widths['vertical']/2)*self.array.shape[1]
top_profile = (self.positions['vertical'] + self.widths['vertical']/2)*self.array.shape[1]
axis.axvline(bottom_profile, color='r') # Y
axis.axvline(top_profile, color='r') # Y
_remove_ticklabels(axis)
axis.set_title(title)

Expand Down
52 changes: 30 additions & 22 deletions tests_basic/test_flatsym.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ def test_analyze_works(self):
self.assertEqual(fs.symmetry['method'], 'varian')
self.assertEqual(fs.flatness['method'], 'varian')
self.assertEqual(fs.positions['vertical'], 0.5)
self.assertEqual(fs.positions['horizontal'], 0.5)
self.assertEqual(fs.widths['vertical'], 0.0)
self.assertEqual(fs.widths['horizontal'], 0.0)

def test_profile_limits(self):
"""Extreme profile limits should not raise an error"""
fs = FlatSym.from_demo_image()
fs.analyze(flatness_method="varian", symmetry_method="varian", vert_position=0.5, horiz_position=0.5, vert_width=0, horiz_width=0)
fs.analyze(flatness_method="varian", symmetry_method="varian", vert_position=0.0, horiz_position=0.0, vert_width=1, horiz_width=1)
fs.analyze(flatness_method="varian", symmetry_method="varian", vert_position=1.0, horiz_position=1.0, vert_width=1, horiz_width=1)

def test_analyze_sets_analyzed_flag(self):
fs = create_instance()
Expand Down Expand Up @@ -93,27 +103,30 @@ class FlatSymBase(LocationMixin):
flatness_method = 'varian'
vert_position = 0.5
horiz_position = 0.5
vert_width = 0
horiz_width = 0
print_results = False

@classmethod
def setUpClass(cls):
cls.fs = FlatSym(cls.get_filename())
cls.fs.analyze(flatness_method=cls.flatness_method, symmetry_method=cls.symmetry_method,
vert_position=cls.vert_position, horiz_position=cls.horiz_position)
vert_position=cls.vert_position, horiz_position=cls.horiz_position,
vert_width=cls.vert_width, horiz_width=cls.horiz_width)
if cls.print_results:
print(cls.fs.results())

def test_vert_symmetry(self):
self.assertAlmostEqual(self.fs.symmetry['vertical']['value'], self.vert_symmetry, delta=0.2)
self.assertAlmostEqual(self.fs.symmetry['vertical']['value'], self.vert_symmetry, delta=0.02)

def test_horiz_symmetry(self):
self.assertAlmostEqual(self.fs.symmetry['horizontal']['value'], self.horiz_symmetry, delta=0.2)
self.assertAlmostEqual(self.fs.symmetry['horizontal']['value'], self.horiz_symmetry, delta=0.02)

def test_vert_flatness(self):
self.assertAlmostEqual(self.fs.flatness['vertical']['value'], self.vert_flatness, delta=0.2)
self.assertAlmostEqual(self.fs.flatness['vertical']['value'], self.vert_flatness, delta=0.02)

def test_horiz_flatness(self):
self.assertAlmostEqual(self.fs.flatness['horizontal']['value'], self.horiz_flatness, delta=0.2)
self.assertAlmostEqual(self.fs.flatness['horizontal']['value'], self.horiz_flatness, delta=0.02)


class FlatSymDemo(FlatSymBase, TestCase):
Expand Down Expand Up @@ -143,20 +156,15 @@ class FlatSym18X(FlatSymBase, TestCase):
horiz_symmetry = 1.16


class FlatSym18XElekta(FlatSymBase, TestCase):
file_path = ['18x auto bulb2.dcm']
flatness_method = 'elekta'
symmetry_method = 'elekta'
vert_flatness = 103.3
vert_symmetry = 101.2
horiz_flatness = 103.6
horiz_symmetry = 101


class TIF1(FlatSymBase, TestCase):
"""This is to test loading from a TIF file"""
file_path = ['F&S.tif']
vert_flatness = 1.4
vert_symmetry = 0.64
horiz_flatness = 2.4
horiz_symmetry = 1.6
class FlatSymWideDemo(FlatSymBase, TestCase):
vert_width = 0.025
horiz_width = 0.025
vert_flatness = 1.84
vert_symmetry = 2.47
horiz_flatness = 1.84
horiz_symmetry = 2.96

@classmethod
def get_filename(cls):
return retrieve_demo_file(url='flatsym_demo.dcm')

0 comments on commit 72b7b83

Please sign in to comment.