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.
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/MANIFEST.in +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/PKG-INFO +1 -1
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/README.rst +11 -12
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/__init__.py +0 -6
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/cbct.py +129 -54
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/__init__.py +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/common_functions.py +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/decorators.py +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/geometry.py +7 -8
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/image.py +60 -8
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/io.py +97 -78
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/profile.py +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/core/utilities.py +0 -6
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/cbct/High quality head.zip +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/flatsym/flatsym_demo.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/AQA.dlg +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/BQA.dlg +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/Tlog.bin +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/log_reader/Tlog2.bin +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/picket_fence/EPID-PF-LR.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/starshot/10X_collimator.tif +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRGS_dmlc.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRGS_open.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRMLC_dmlc.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/demo_files/vmat/DRMLC_open.dcm +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/flatsym.py +117 -90
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/log_analyzer.py +175 -67
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/picketfence.py +16 -9
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac/starshot.py +57 -23
- pylinac-0.6.0.1/pylinac/vmat.py +437 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/PKG-INFO +1 -1
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/SOURCES.txt +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/dependency_links.txt +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/not-zip-safe +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/requires.txt +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/pylinac.egg-info/top_level.txt +0 -0
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/setup.cfg +4 -3
- {pylinac-0.5.0.2 → pylinac-0.6.0.1}/setup.py +2 -2
- pylinac-0.5.0.2/pylinac/vmat.py +0 -591
|
File without changes
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
Pylinac
|
|
2
2
|
=======
|
|
3
3
|
|
|
4
|
-
.. image:: https://
|
|
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://
|
|
9
|
-
:target: https://
|
|
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
|
|
33
|
-
(DRGS) and
|
|
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=
|
|
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=
|
|
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=
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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("
|
|
879
|
+
raise FileExistsError("Files given were not valid zip files")
|
|
880
880
|
else:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
filelist = self.
|
|
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
|
-
|
|
889
|
-
except IOError:
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1195
|
+
cbct = CBCT()
|
|
1122
1196
|
# cbct.load_zip_file(zip_file)
|
|
1123
|
-
|
|
1197
|
+
cbct.load_demo_images()
|
|
1124
1198
|
# cbct.algo_data.images = np.roll(cbct.algo_data.images, 30, axis=1)
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1199
|
+
cbct.analyze()
|
|
1200
|
+
print(cbct.return_results())
|
|
1201
|
+
cbct.plot_analyzed_subimage('mtf')
|
|
1202
|
+
# cbct.save_analyzed_image('ttt.png')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
-
|
|
82
|
+
try:
|
|
82
83
|
self._load_file(file_or_array, to_gray)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
+
|