pylinac 0.5.0.2__zip → 0.6.0.1__zip

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/MANIFEST.in +0 -0
  2. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/PKG-INFO +1 -1
  3. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/README.rst +11 -12
  4. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/__init__.py +0 -6
  5. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/cbct.py +129 -54
  6. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/__init__.py +0 -0
  7. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/common_functions.py +0 -0
  8. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/decorators.py +0 -0
  9. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/geometry.py +7 -8
  10. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/image.py +60 -8
  11. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/io.py +97 -78
  12. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/profile.py +0 -0
  13. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/utilities.py +0 -6
  14. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/cbct/High quality head.zip +0 -0
  15. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/flatsym/flatsym_demo.dcm +0 -0
  16. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/AQA.dlg +0 -0
  17. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/BQA.dlg +0 -0
  18. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/Tlog.bin +0 -0
  19. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/Tlog2.bin +0 -0
  20. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/picket_fence/EPID-PF-LR.dcm +0 -0
  21. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/starshot/10X_collimator.tif +0 -0
  22. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRGS_dmlc.dcm +0 -0
  23. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRGS_open.dcm +0 -0
  24. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRMLC_dmlc.dcm +0 -0
  25. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRMLC_open.dcm +0 -0
  26. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/flatsym.py +117 -90
  27. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/log_analyzer.py +175 -67
  28. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/picketfence.py +16 -9
  29. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/starshot.py +57 -23
  30. pylinac-0.6.0.1/pylinac/vmat.py +437 -0
  31. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/PKG-INFO +1 -1
  32. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/SOURCES.txt +0 -0
  33. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/dependency_links.txt +0 -0
  34. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/not-zip-safe +0 -0
  35. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/requires.txt +0 -0
  36. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/top_level.txt +0 -0
  37. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/setup.cfg +4 -3
  38. {pylinac-0.5.0.2 → pylinac-0.6.0.1}/setup.py +2 -2
  39. pylinac-0.5.0.2/pylinac/vmat.py +0 -591
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.1
2
2
  Name: pylinac
3
- Version: 0.5.0.2
3
+ Version: 0.6.0.1
4
4
  Summary: A toolkit for performing TG-142 QA-related tasks on a linear accelerator
5
5
  Home-page: https://github.com/jrkerns/pylinac
6
6
  Author: James Kerns
@@ -1,17 +1,12 @@
1
1
  Pylinac
2
2
  =======
3
3
 
4
- .. image:: https://pypip.in/version/pylinac/badge.svg?style=flat
5
- :target: https://pypi.python.org/pypi/pylinac/
4
+ .. image:: https://img.shields.io/pypi/v/pylinac.svg
5
+ :target: https://pypi.python.org/pypi/pylinac
6
6
  :alt: Latest Version
7
7
 
8
- .. image:: https://pypip.in/py_versions/pylinac/badge.svg?style=flat
9
- :target: https://pypi.python.org/pypi/pylinac/
10
- :alt: Supported Python versions
11
-
12
- .. image:: https://pypip.in/license/pylinac/badge.svg?style=flat
13
- :target: https://pypi.python.org/pypi/pylinac/
14
- :alt: License
8
+ .. image:: https://travis-ci.org/jrkerns/pylinac.svg?branch=master
9
+ :target: https://travis-ci.org/jrkerns/pylinac
15
10
 
16
11
  .. image:: https://coveralls.io/repos/jrkerns/pylinac/badge.svg?branch=master
17
12
  :target: https://coveralls.io/r/jrkerns/pylinac
@@ -29,8 +24,8 @@ Below are the tools currently available; tools will be added one at a time as th
29
24
  the minimum circle that touches all the radiation spokes (wobble). Based on ideas from `Depuydt et al <http://iopscience.iop.org/0031-9155/57/10/2997>`_
30
25
  and `Gonzalez et al <http://dx.doi.org/10.1118/1.1755491>`_ and evolutionary optimization.
31
26
  * `VMAT QA <http://pylinac.readthedocs.org/en/latest/vmat_docs.html>`_ -
32
- A module for analyzing EPID images after performing the `Jorgensen et al. <http://dx.doi.org/10.1118/1.3552922>`_ tests for VMAT QA, specifically the Dose Rate & Gantry Speed
33
- (DRGS) and Dose Rate MLC (DRMLC) tests. Can load the open and MLC field images and calculate segment ratios as per the Jorgensen implementation.
27
+ A module for analyzing EPID images after performing the Varian RapidArc QA tests, specifically the Dose Rate & Gantry Speed
28
+ (DRGS) and MLC Speed (MLCS) tests. Can load the open and MLC field images and calculate segment ratios as per the Varian specs.
34
29
  * `CBCT QA <http://pylinac.readthedocs.org/en/latest/cbct_docs.html>`_ -
35
30
  A module to automatically analyze DICOM images of a CatPhan 504 delivered on a Varian linac. Corrects for yaw, pitch, roll
36
31
  and left-right, up-down displacement. Analysis is based on test descriptions in the
@@ -50,11 +45,15 @@ Documentation
50
45
  To get started, install, run the demos, view the API docs, and learn the module design, visit the
51
46
  `Full Documentation <http://pylinac.readthedocs.org/en/latest/index.html>`_ on Read The Docs.
52
47
 
48
+ Discussion
49
+ ----------
50
+ Have questions? Ask them here on the `pylinac forum <https://groups.google.com/forum/#!forum/pylinac>`_.
51
+
53
52
  Contributing
54
53
  ------------
55
54
 
56
55
  Contributions to pylinac can be many. The most useful things a non-programmer can contribute are images to analyze and bug reports. If
57
- you have VMAT images, starshot images, machine log files, CBCT DICOM files, or anything else you want analyzed, email or share them via Dropbox, Google Drive, etc.
56
+ you have VMAT images, starshot images, machine log files, CBCT DICOM files, or anything else you want analyzed, email or share them via Dropbox, Google Drive, etc: jkerns at gmail.com
58
57
 
59
58
 
60
59
 
@@ -6,12 +6,6 @@ import sys
6
6
  if sys.version_info[0] < 3:
7
7
  raise ValueError("Pylinac is only supported on Python 3.x. It seems you are using Python 2; please use a different interpreter.")
8
8
 
9
- try:
10
- import pandas
11
- except ImportError:
12
- import warnings
13
- warnings.warn("Future versions of pylinac will use the pandas package, but it is not installed", FutureWarning)
14
-
15
9
  DEBUG = False
16
10
  MEMORY_PROFILE = False
17
11
 
@@ -15,6 +15,7 @@ import shutil
15
15
  import zipfile
16
16
  import time
17
17
  import math
18
+ from io import BytesIO
18
19
 
19
20
  import numpy as np
20
21
  from scipy import ndimage
@@ -86,7 +87,7 @@ class Algo_Data:
86
87
  """Determine the roll of the phantom by calculating the angle of the two
87
88
  air bubbles on the HU slice. Delegation method."""
88
89
  # TODO: need more efficient way of doing this w/o creating HU slice
89
- HU = HU_Slice(self)
90
+ HU = HU_Slice(self, tolerance=40)
90
91
  return HU.determine_phantom_roll()
91
92
 
92
93
  @lazyproperty
@@ -325,17 +326,17 @@ class HU_Slice(Base_HU_Slice):
325
326
  """Class for analysis of the HU slice of the CBCT dicom data set."""
326
327
  dist2objs = 120 # radius in pixels to the centers of the HU objects
327
328
  object_radius = 9 # radius of the HU ROIs themselves
328
- tolerance = 40 # standard tolerance of HU
329
+ # tolerance = 40 # standard tolerance of HU
329
330
  air_bubble_size = 450 #
330
331
 
331
332
 
332
- def __init__(self, algo_data):
333
+ def __init__(self, algo_data, tolerance):
333
334
  super().__init__(algo_data)
334
335
  self.scale_by_FOV()
335
336
  self.image = Image(combine_surrounding_slices(self.algo_data.images, self.algo_data.HU_slice_num))
336
337
 
337
338
  HU_ROIp = partial(HU_ROI, slice_array=self.image.array, radius=self.object_radius, dist_from_center=self.dist2objs,
338
- tolerance=self.tolerance)
339
+ tolerance=tolerance)
339
340
 
340
341
  air = HU_ROIp('Air', 90, -1000)
341
342
  pmp = HU_ROIp('PMP', 120, -200)
@@ -393,18 +394,17 @@ class UNIF_Slice(Base_HU_Slice):
393
394
  """Class for analysis of the Uniformity slice of the CBCT dicom data set."""
394
395
  dist2objs = 110
395
396
  obj_radius = 20
396
- tolerance = 40
397
397
 
398
- def __init__(self, algo_data):
398
+ def __init__(self, algo_data, tolerance):
399
399
  super().__init__(algo_data)
400
400
  self.scale_by_FOV()
401
401
  self.image = Image(combine_surrounding_slices(self.algo_data.images, self.algo_data.UN_slice_num))
402
402
 
403
- HU_ROIp = partial(HU_ROI, slice_array=self.image.array, tolerance=self.tolerance, radius=self.obj_radius,
403
+ HU_ROIp = partial(HU_ROI, slice_array=self.image.array, tolerance=tolerance, radius=self.obj_radius,
404
404
  dist_from_center=self.dist2objs)
405
405
 
406
406
  # center has distance of 0, thus doesn't use partial
407
- center = HU_ROI('Center', 0, 0, self.image.array, self.obj_radius, dist_from_center=0, tolerance=self.tolerance)
407
+ center = HU_ROI('Center', 0, 0, self.image.array, self.obj_radius, dist_from_center=0, tolerance=tolerance)
408
408
  right = HU_ROIp('Right', 0, 0)
409
409
  top = HU_ROIp('Top', -90, 0)
410
410
  left = HU_ROIp('Left', 180, 0)
@@ -417,6 +417,7 @@ class UNIF_Slice(Base_HU_Slice):
417
417
  self.dist2objs /= self.algo_data.fov_ratio
418
418
  self.obj_radius /= self.algo_data.fov_ratio
419
419
 
420
+
420
421
  class Locon_Slice(Slice):
421
422
  """Class for analysis of the low contrast slice of the CBCT dicom data set."""
422
423
  # TODO: work on this
@@ -454,7 +455,7 @@ class SR_Slice(Slice):
454
455
  self.scale_by_FOV()
455
456
  self.image = Image(combine_surrounding_slices(self.algo_data.images, self.algo_data.SR_slice_num, mode='max'))
456
457
 
457
- self.LP_MTF = {} # holds lp:mtf data
458
+ self.LP_MTF = OrderedDict() # holds lp:mtf data
458
459
  for idx, radius in enumerate(self.radius2profs):
459
460
  c = SR_Circle_ROI(idx, self.image.array, radius=radius)
460
461
  self.add_ROI(c)
@@ -704,10 +705,10 @@ class GEO_Slice(Slice):
704
705
  line_nominal_value = 50
705
706
  dist2objs = 72
706
707
  obj_radius = 20
707
- tolerance = 1
708
708
 
709
- def __init__(self, algo_data):
709
+ def __init__(self, algo_data, tolerance):
710
710
  super().__init__(algo_data)
711
+ self.tolerance = tolerance
711
712
  self.scale_by_FOV()
712
713
  self.image = Image(combine_surrounding_slices(self.algo_data.images, self.algo_data.HU_slice_num, mode='median'))
713
714
 
@@ -819,20 +820,21 @@ class CBCT:
819
820
  demo_zip = osp.join(cbct_demo_dir, 'High quality head.zip')
820
821
  demo_folder = osp.join(cbct_demo_dir, 'High quality head')
821
822
 
823
+ self.load_zip_file(demo_zip)
822
824
  # unpack demo folder if not already around
823
- if not osp.isdir(demo_folder) or len(os.listdir(demo_folder)) == 0:
824
- shutil.unpack_archive(demo_zip, cbct_demo_dir)
825
-
826
- filelist = self._get_CT_filenames_from_folder(demo_folder)
827
- self._load_files(filelist)
828
- # delete the unpacked demo folder
829
- if cleanup:
830
- try:
831
- shutil.rmtree(demo_folder)
832
- time.sleep(0.1) # sleep timer added because fast repeats of loading/dumping caused errors
833
- except IOError:
834
- print("Extracted demo images were not able to be deleted. You can manually delete them if you "
835
- "like from %s" % demo_folder)
825
+ # if not osp.isdir(demo_folder) or len(os.listdir(demo_folder)) == 0:
826
+ # shutil.unpack_archive(demo_zip, cbct_demo_dir)
827
+ #
828
+ # filelist = self._get_CT_filenames_from_folder(demo_folder)
829
+ # self._load_files(filelist)
830
+ # # delete the unpacked demo folder
831
+ # if cleanup:
832
+ # try:
833
+ # shutil.rmtree(demo_folder)
834
+ # time.sleep(0.1) # sleep timer added because fast repeats of loading/dumping caused errors
835
+ # except IOError:
836
+ # print("Extracted demo images were not able to be deleted. You can manually delete them if you "
837
+ # "like from %s" % demo_folder)
836
838
 
837
839
  def load_folder_UI(self):
838
840
  """Load the CT DICOM files from a folder using a UI."""
@@ -873,22 +875,20 @@ class CBCT:
873
875
  FileExistsError : If zip_file passed was not a legitimate zip file.
874
876
  FileNotFoundError : If no CT images are found in the folder
875
877
  """
876
- zip_folder, _ = osp.splitext(zip_file)
877
-
878
878
  if not zipfile.is_zipfile(zip_file):
879
- raise FileExistsError("Path given was not a valid zip file")
879
+ raise FileExistsError("Files given were not valid zip files")
880
880
  else:
881
- shutil.unpack_archive(zip_file, osp.dirname(zip_file))
882
-
883
- filelist = self._get_CT_filenames_from_folder(zip_folder)
884
- self._load_files(filelist)
881
+ zfs = zipfile.ZipFile(zip_file)
882
+ # shutil.unpack_archive(zip_file, osp.dirname(zip_file))
883
+ filelist = self._get_CT_filenames_from_zip(zfs)
884
+ self._load_files(filelist, is_zip=True, zfiles=zfs)
885
885
 
886
886
  # delete the unpacked folder
887
- try:
888
- shutil.rmtree(zip_folder)
889
- except IOError:
890
- print("Extracted demo images were not able to be deleted. You can manually delete them if you "
891
- "like from %s" % zip_folder)
887
+ # try:
888
+ # shutil.rmtree(zip_folder)
889
+ # except IOError:
890
+ # print("Extracted demo images were not able to be deleted. You can manually delete them if you "
891
+ # "like from %s" % zip_folder)
892
892
 
893
893
  def _get_CT_filenames_from_folder(self, folder):
894
894
  """Walk through a folder to find DICOM CT images.
@@ -908,7 +908,16 @@ class CBCT:
908
908
  return filelist
909
909
  raise FileNotFoundError("CT images were not found in the specified folder.")
910
910
 
911
- def _load_files(self, file_list, im_size=512):
911
+ def _get_CT_filenames_from_zip(self, zfile):
912
+ """Get the CT image file names from a zip file."""
913
+ allnames = zfile.namelist()
914
+ filelist = [item for item in allnames if os.path.basename(item).endswith('.dcm') and os.path.basename(item).startswith('CT')]
915
+ if filelist:
916
+ return filelist
917
+ raise FileNotFoundError("CT images were not found in the specified folder.")
918
+
919
+
920
+ def _load_files(self, file_list, im_size=512, is_zip=False, zfiles=None):
912
921
  """Load CT DICOM files given a list of image paths.
913
922
 
914
923
  Parameters
@@ -931,6 +940,8 @@ class CBCT:
931
940
  # load dicom files from list names and get the image slice position
932
941
  # TODO: figure out more memory-efficient way to sort images; maybe based on number of slices and slice thickness
933
942
  for idx, item in enumerate(file_list):
943
+ if is_zip:
944
+ item = BytesIO(zfiles.read(item))
934
945
  dcm = dicom.read_file(item)
935
946
  im_order[idx] = np.round(dcm.ImagePositionPatient[-1]/dcm.SliceThickness)
936
947
  # resize image if need be
@@ -946,7 +957,7 @@ class CBCT:
946
957
  sorted_images = self._sort_images(im_order, images)
947
958
 
948
959
  # determine settings needed for given CBCT.
949
- self._get_settings(file_list[0], sorted_images)
960
+ self._get_settings(item, sorted_images)
950
961
  # convert images from CT# to HU using dicom tags
951
962
  self._convert2HU()
952
963
 
@@ -970,6 +981,10 @@ class CBCT:
970
981
  images : numpy.ndarray
971
982
  3D numpy array of the CT images.
972
983
  """
984
+ try:
985
+ dicom_file_path.seek(0)
986
+ except AttributeError:
987
+ pass
973
988
  dcm = dicom.read_file(dicom_file_path, stop_before_pixels=True)
974
989
  self.algo_data = Algo_Data(images, dcm)
975
990
 
@@ -978,23 +993,23 @@ class CBCT:
978
993
  self.algo_data.images *= self.algo_data.dicom_metadata.RescaleSlope
979
994
  self.algo_data.images += self.algo_data.dicom_metadata.RescaleIntercept
980
995
 
981
- def construct_HU(self):
996
+ def construct_HU(self, tolerance):
982
997
  """Construct the Houndsfield Unit Slice and its ROIs."""
983
- self.HU = HU_Slice(self.algo_data)
998
+ self.HU = HU_Slice(self.algo_data, tolerance)
984
999
 
985
1000
  def construct_SR(self):
986
1001
  """Construct the Spatial Resolution Slice and its ROIs so MTF can be calculated."""
987
1002
  self.SR = SR_Slice(self.algo_data)
988
1003
  self.SR.calc_MTF()
989
1004
 
990
- def construct_GEO(self):
1005
+ def construct_GEO(self, tolerance):
991
1006
  """Construct the Geometry Slice and find the node centers."""
992
- self.GEO = GEO_Slice(self.algo_data)
1007
+ self.GEO = GEO_Slice(self.algo_data, tolerance)
993
1008
  self.GEO.calc_node_centers()
994
1009
 
995
- def construct_UNIF(self):
1010
+ def construct_UNIF(self, tolerance):
996
1011
  """Construct the Uniformity Slice and its ROIs."""
997
- self.UN = UNIF_Slice(self.algo_data)
1012
+ self.UN = UNIF_Slice(self.algo_data, tolerance)
998
1013
 
999
1014
  def construct_Locon(self):
1000
1015
  """Construct the Low Contrast Slice."""
@@ -1041,6 +1056,65 @@ class CBCT:
1041
1056
  if show:
1042
1057
  plt.show()
1043
1058
 
1059
+ def save_analyzed_image(self, filename, **kwargs):
1060
+ """Save the analyzed plot."""
1061
+ self.plot_analyzed_image(show=False)
1062
+ plt.savefig(filename, **kwargs)
1063
+
1064
+ def plot_analyzed_subimage(self, subimage='hu', show=True):
1065
+ """Plot a specific component of the HU analysis."""
1066
+ subimage = subimage.lower()
1067
+ plt.clf()
1068
+ plt.axis('off')
1069
+
1070
+ if subimage == 'hu':
1071
+ # HU objects
1072
+ plt.imshow(self.HU.image.array, cmap=plt.cm.Greys)
1073
+ for roi in self.HU.ROIs.values():
1074
+ color = roi.get_pass_fail_color()
1075
+ roi.add_to_axes(plt.gca(), edgecolor=color)
1076
+ # GEO objects
1077
+ for line in self.GEO.lines.values():
1078
+ line.add_to_axes(plt.gca(), color='blue')
1079
+ # plt.title('HU & Geometric Slice')
1080
+ plt.autoscale(tight=True)
1081
+ elif subimage == 'unif':
1082
+ plt.imshow(self.UN.image.array, cmap=plt.cm.Greys)
1083
+ for roi in self.UN.ROIs.values():
1084
+ color = roi.get_pass_fail_color()
1085
+ roi.add_to_axes(plt.gca(), edgecolor=color)
1086
+ plt.autoscale(tight=True)
1087
+ # plt.title('Uniformity Slice')
1088
+ elif subimage == 'sr':
1089
+ # SR objects
1090
+ plt.imshow(self.SR.image.array, cmap=plt.cm.Greys)
1091
+ last_roi = len(self.SR.ROIs) - 1
1092
+ for roi in [self.SR.ROIs[0], self.SR.ROIs[last_roi]]:
1093
+ roi.add_to_axes(plt.gca(), edgecolor='blue')
1094
+ plt.autoscale(tight=True)
1095
+
1096
+ # plt.title('Spatial Resolution Slice')
1097
+ elif subimage == 'mtf':
1098
+ plt.axis('on')
1099
+ x = list(self.SR.LP_MTF.keys())
1100
+ y = list(self.SR.LP_MTF.values())
1101
+ plt.grid('on')
1102
+ plt.plot(x, y, marker='o')
1103
+ plt.ylim([0, 1.1])
1104
+ plt.xlim([0.1, 1.3])
1105
+ plt.xlabel('Line pairs / mm')
1106
+ plt.ylabel('Relative MTF function')
1107
+ else:
1108
+ raise ValueError("Subimage parameter {} not understood".format(subimage))
1109
+
1110
+ if show:
1111
+ plt.show()
1112
+
1113
+ def save_analyzed_subimage(self, filename, subimage='hu', **kwargs):
1114
+ """Save a component image to file."""
1115
+ self.plot_analyzed_subimage(subimage, show=False)
1116
+ plt.savefig(filename, **kwargs)
1117
+
1044
1118
  def return_results(self):
1045
1119
  """Return the results of the analysis as a string."""
1046
1120
  #TODO: make prettier
@@ -1056,13 +1130,13 @@ class CBCT:
1056
1130
  self.GEO.get_line_lengths(), self.GEO.overall_passed)
1057
1131
  return string
1058
1132
 
1059
- def analyze(self):
1133
+ def analyze(self, hu_tolerance=40, scaling_tolerance=1):
1060
1134
  """Single-method full analysis of CBCT DICOM files."""
1061
1135
  if not self.images_loaded:
1062
1136
  raise AttributeError("Images not yet loaded")
1063
- self.construct_HU()
1064
- self.construct_UNIF()
1065
- self.construct_GEO()
1137
+ self.construct_HU(hu_tolerance)
1138
+ self.construct_UNIF(hu_tolerance)
1139
+ self.construct_GEO(scaling_tolerance)
1066
1140
  self.construct_SR()
1067
1141
  self.construct_Locon()
1068
1142
 
@@ -1116,12 +1190,13 @@ def combine_surrounding_slices(slice_array, nominal_slice_num, slices_plusminus=
1116
1190
  # CBCT Demo
1117
1191
  # ----------------------------------------
1118
1192
  if __name__ == '__main__':
1119
- CBCT().run_demo()
1193
+ # CBCT().run_demo()
1120
1194
  # zip_file = r"D:\Users\James\Dropbox\Programming\Python\Projects\PyCharm Projects\pylinac\tests\test_files\CBCT\Varian\Low dose thorax.zip"
1121
- # cbct = CBCT()
1195
+ cbct = CBCT()
1122
1196
  # cbct.load_zip_file(zip_file)
1123
- # cbct.load_demo_images()
1197
+ cbct.load_demo_images()
1124
1198
  # cbct.algo_data.images = np.roll(cbct.algo_data.images, 30, axis=1)
1125
- # cbct.analyze()
1126
- # print(cbct.return_results())
1127
- # cbct.plot_analyzed_image()
1199
+ cbct.analyze()
1200
+ print(cbct.return_results())
1201
+ cbct.plot_analyzed_subimage('mtf')
1202
+ # cbct.save_analyzed_image('ttt.png')
@@ -53,6 +53,8 @@ class Point:
53
53
  value = input_coords[4]
54
54
  except IndexError:
55
55
  pass
56
+ # else:
57
+ # raise TypeError("Point inputs not understood")
56
58
 
57
59
  if as_int:
58
60
  x = int(round(x))
@@ -105,15 +107,12 @@ class Circle:
105
107
  radius : float, optional
106
108
  Radius of the wobble circle.
107
109
  """
108
-
109
- if center_point is not None:
110
- if is_iterable(center_point):
111
- # if iterable, convert to Point because Point will convert
112
- center_point = Point(center_point)
113
- elif not isinstance(center_point, Point):
114
- raise TypeError("Circle center must be of type Point or iterable")
115
- elif center_point is None:
110
+ if center_point is None:
116
111
  center_point = Point()
112
+ elif isinstance(center_point, Point) or is_iterable(center_point):
113
+ center_point = Point(center_point)
114
+ else:
115
+ raise TypeError("Circle center must be of type Point or iterable")
117
116
 
118
117
  self.center = center_point
119
118
  self.radius = radius
@@ -11,7 +11,7 @@ from scipy.misc import imresize
11
11
 
12
12
  from pylinac.core.decorators import type_accept
13
13
  from pylinac.core.geometry import Point
14
- from pylinac.core.io import get_filepath_UI
14
+ from pylinac.core.io import get_filepath_UI, get_filenames_UI
15
15
  from pylinac.core.utilities import array2logical, typed_property
16
16
 
17
17
 
@@ -39,7 +39,8 @@ class Image:
39
39
  dpi : int, float
40
40
  The Dots-per-inch of the image.
41
41
  dpmm : int, float
42
- The Dots-per-mm of the image.
42
+ The Dots-per-mm of the image, defined at isocenter. E.g. if an EPID image is taken at 150cm SID,
43
+ the dpmm will scale back to 100cm.
43
44
  SID : int, float
44
45
  The Source-to-Image distance in cm.
45
46
  im_type : {'DICOM', 'Image', 'Array'}
@@ -78,12 +79,16 @@ class Image:
78
79
  If True (default), will convert RGB and HSV type images to greyscale.
79
80
  If False, will not do any conversion.
80
81
  """
81
- if isinstance(file_or_array, str):
82
+ try:
82
83
  self._load_file(file_or_array, to_gray)
83
- elif isinstance(file_or_array, np.ndarray):
84
- self._load_array(file_or_array)
85
- else:
84
+ except (IOError, AttributeError):
85
+ try:
86
+ self._load_array(file_or_array)
87
+ except:
88
+ raise TypeError("Image input type not understood")
89
+ except:
86
90
  raise TypeError("Image input type not understood")
91
+
87
92
  # @property
88
93
  # def array(self):
89
94
  # return self._array
@@ -148,6 +153,20 @@ class Image:
148
153
  obj = cls(file_path, to_gray)
149
154
  return obj
150
155
 
156
+ @classmethod
157
+ def open_multiple_UI(cls, caption='', to_gray=True):
158
+ """Load multiple images using a UI dialog.
159
+
160
+ .. versionadded:: 0.5.1
161
+
162
+ All files must be images, and must be the same size and shape.
163
+ Image metadata, e.g. DPI, is all based on the first image selected.
164
+ """
165
+ file_list = get_filenames_UI()
166
+ if file_list:
167
+ obj = cls.combine_multiples(file_list)
168
+ return obj
169
+
151
170
  def _load_array(self, array):
152
171
  """Load an array."""
153
172
  self.array = array
@@ -165,6 +184,10 @@ class Image:
165
184
 
166
185
  def _construct_image(self, file_path, to_gray):
167
186
  """Construct an object from an image file (TIF, JPEG, etc)."""
187
+ try:
188
+ file_path.seek(0)
189
+ except AttributeError:
190
+ pass
168
191
  img = pImage.open(file_path)
169
192
  if to_gray:
170
193
  if img.mode == 'RGB' or img.mode == 'HSV':
@@ -176,16 +199,20 @@ class Image:
176
199
  if 'dpi' in img.info:
177
200
  if len(img.info['dpi']) > 1:
178
201
  if img.info['dpi'][0] != img.info['dpi'][1]:
179
- raise TypeError("Input image has different DPI ratios for horizontal and vertical")
202
+ raise TypeError("Input image has different DPI values for horizontal and vertical")
180
203
  self.dpi = img.info['dpi'][0]
181
204
  else:
182
205
  self.dpi = img.info['dpi']
183
206
  else:
184
- warnings.warn("Pixel distance information was not available. You can set the attributed"
207
+ warnings.warn("Pixel distance information was not available. You can set the attribute"
185
208
  "explicitly if you know it.")
186
209
 
187
210
  def _construct_dicom(self, file_path):
188
211
  """Construct an object from a DICOM file (.dcm)."""
212
+ try:
213
+ file_path.seek(0)
214
+ except AttributeError:
215
+ pass
189
216
  dcm = dicom.read_file(file_path)
190
217
  self.array = dcm.pixel_array
191
218
  self.im_type = DICOM
@@ -301,7 +328,32 @@ class Image:
301
328
  self.array -= min_val
302
329
  return min_val
303
330
 
331
+ @classmethod
332
+ def combine_multiples(cls, image_file_list):
333
+ """Combine multiple image files into one superimposed image.
334
+
335
+ .. versionadded:: 0.5.1
336
+ """
337
+ # open first one to get initial settings
338
+ init_obj = cls(image_file_list[0])
339
+ concat_arr = init_obj.array
340
+ initial_shape = init_obj.shape
341
+ for img_file in image_file_list[1:]:
342
+ obj = cls(img_file)
343
+ if obj.shape != initial_shape:
344
+ raise AttributeError("Images must be the same size when combining.")
345
+ concat_arr = np.dstack((concat_arr, obj.array))
346
+
347
+ # create new mean array
348
+ combined_arr = np.mean(concat_arr, axis=2)
349
+ # use the initial Image object and replace its array (thus keeping all the other properties)
350
+ init_obj.array = combined_arr
351
+ return init_obj
352
+
304
353
  def __getattr__(self, item):
305
354
  """Set the Attribute getter to grab from the array if possible (for things like .shape, .size, etc)."""
306
355
  return getattr(self.array, item)
307
356
 
357
+ if __name__ == '__main__':
358
+ img = Image.open_multiple_UI()
359
+