OpenREM 1.0.0b2__py3-none-any.whl → 1.0.0b3__py3-none-any.whl
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.
- openrem/locale/de/LC_MESSAGES/django.po +1060 -1059
- openrem/locale/django.pot +973 -972
- openrem/locale/es_MX/LC_MESSAGES/django.po +1049 -1048
- openrem/locale/it/LC_MESSAGES/django.po +1044 -1043
- openrem/locale/lt/LC_MESSAGES/django.po +989 -988
- openrem/locale/nb_NO/LC_MESSAGES/django.po +985 -984
- openrem/locale/pt_BR/LC_MESSAGES/django.po +1003 -1002
- openrem/manage.py +10 -10
- openrem/openremproject/__init__.py +1 -1
- openrem/openremproject/local_settings.py.linux +128 -128
- openrem/openremproject/local_settings.py.windows +144 -144
- openrem/openremproject/local_settings.py.windows-sqlite3 +129 -129
- openrem/openremproject/settings.py +278 -278
- openrem/openremproject/urls.py +32 -32
- openrem/openremproject/wsgi.py.example +28 -28
- openrem/remapp/__init__.py +2 -2
- openrem/remapp/admin.py +31 -31
- openrem/remapp/exports/ct_export.py +780 -753
- openrem/remapp/exports/dx_export.py +817 -805
- openrem/remapp/exports/export_common.py +931 -951
- openrem/remapp/exports/export_common_pandas.py +2422 -0
- openrem/remapp/exports/exportviews.py +815 -860
- openrem/remapp/exports/mg_csv_nhsbsp.py +292 -292
- openrem/remapp/exports/mg_export.py +673 -510
- openrem/remapp/exports/nm_export.py +796 -575
- openrem/remapp/exports/rf_export.py +1418 -1431
- openrem/remapp/extractors/ct_philips.py +424 -414
- openrem/remapp/extractors/ct_toshiba.py +2116 -2108
- openrem/remapp/extractors/dx.py +1033 -952
- openrem/remapp/extractors/extract_common.py +817 -817
- openrem/remapp/extractors/import_views.py +426 -426
- openrem/remapp/extractors/mam.py +685 -672
- openrem/remapp/extractors/nm_image.py +439 -431
- openrem/remapp/extractors/ptsizecsv2db.py +368 -368
- openrem/remapp/extractors/rdsr.py +667 -654
- openrem/remapp/extractors/rdsr_methods.py +1771 -1768
- openrem/remapp/extractors/rrdsr_methods.py +630 -622
- openrem/remapp/fixtures/openskin_safelist.json +11 -11
- openrem/remapp/forms.py +2286 -2277
- openrem/remapp/interface/chart_functions.py +2412 -2393
- openrem/remapp/interface/mod_filters.py +1241 -1243
- openrem/remapp/migrations/0001_initial.py.1-0-upgrade +1043 -1043
- openrem/remapp/models.py +3418 -3407
- openrem/remapp/netdicom/dicomviews.py +681 -683
- openrem/remapp/netdicom/qrscu.py +2646 -2646
- openrem/remapp/netdicom/tools.py +134 -134
- openrem/remapp/static/css/bootstrap-theme.css +587 -587
- openrem/remapp/static/css/bootstrap-theme.min.css +4 -4
- openrem/remapp/static/css/bootstrap.css +6800 -6800
- openrem/remapp/static/css/bootstrap.min.css +4 -4
- openrem/remapp/static/css/datepicker3.css +790 -790
- openrem/remapp/static/css/jquery.qtip.min.css +2 -2
- openrem/remapp/static/css/openrem-extra.css +442 -442
- openrem/remapp/static/css/openrem.css +96 -96
- openrem/remapp/static/css/registration.css +34 -34
- openrem/remapp/static/fonts/glyphicons-halflings-regular.svg +287 -287
- openrem/remapp/static/js/bootstrap-datepicker.js +1671 -1671
- openrem/remapp/static/js/bootstrap.js +2363 -2363
- openrem/remapp/static/js/bootstrap.min.js +6 -6
- openrem/remapp/static/js/charts/chartCommonFunctions.js +75 -75
- openrem/remapp/static/js/charts/chartFullScreen.js +41 -41
- openrem/remapp/static/js/charts/ctChartAjax.js +331 -331
- openrem/remapp/static/js/charts/dxChartAjax.js +290 -290
- openrem/remapp/static/js/charts/mgChartAjax.js +144 -144
- openrem/remapp/static/js/charts/nmChartAjax.js +64 -64
- openrem/remapp/static/js/charts/plotly-2.35.2.min.js +8 -0
- openrem/remapp/static/js/charts/rfChartAjax.js +128 -128
- openrem/remapp/static/js/chroma.min.js +32 -32
- openrem/remapp/static/js/datepicker.js +5 -5
- openrem/remapp/static/js/dicom.js +115 -115
- openrem/remapp/static/js/django_reverse/reverse.js +13 -13
- openrem/remapp/static/js/formatDate.js +7 -7
- openrem/remapp/static/js/html5shiv.min.js +8 -8
- openrem/remapp/static/js/jquery-1.11.0.min.js +4 -4
- openrem/remapp/static/js/npm.js +12 -12
- openrem/remapp/static/js/respond.min.js +4 -4
- openrem/remapp/static/js/skin-dose-maps/jquery.qtip.min.js +4 -4
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dHUDObject.js +112 -112
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dObject.js +367 -367
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dPersonObject.js +158 -158
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapColourScaleObject.js +153 -153
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapObject.js +367 -367
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping.js +584 -584
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping3d.js +255 -255
- openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMappingAjax.js +267 -212
- openrem/remapp/static/js/skin-dose-maps/three.min.js +835 -835
- openrem/remapp/static/js/sorttable.js +495 -495
- openrem/remapp/templates/base.html +253 -253
- openrem/remapp/templates/registration/changepassword.html +25 -25
- openrem/remapp/templates/registration/changepassworddone.html +12 -12
- openrem/remapp/templates/registration/login.html +42 -42
- openrem/remapp/templates/remapp/backgroundtaskmaximumrows_form.html +29 -29
- openrem/remapp/templates/remapp/base.html +1 -1
- openrem/remapp/templates/remapp/ctdetail.html +235 -235
- openrem/remapp/templates/remapp/ctfiltered.html +310 -310
- openrem/remapp/templates/remapp/dicomdeletesettings_form.html +31 -31
- openrem/remapp/templates/remapp/dicomqr.html +147 -147
- openrem/remapp/templates/remapp/dicomquerydetails.html +83 -83
- openrem/remapp/templates/remapp/dicomqueryimages.html +49 -49
- openrem/remapp/templates/remapp/dicomqueryseries.html +109 -109
- openrem/remapp/templates/remapp/dicomquerysummary.html +48 -48
- openrem/remapp/templates/remapp/dicomremoteqr_confirm_delete.html +60 -60
- openrem/remapp/templates/remapp/dicomremoteqr_form.html +32 -32
- openrem/remapp/templates/remapp/dicomstorescp_confirm_delete.html +53 -53
- openrem/remapp/templates/remapp/dicomstorescp_form.html +48 -48
- openrem/remapp/templates/remapp/dicomsummary.html +257 -257
- openrem/remapp/templates/remapp/displaychartoptions.html +184 -184
- openrem/remapp/templates/remapp/displayhomepageoptions.html +57 -57
- openrem/remapp/templates/remapp/displayname-count.html +6 -6
- openrem/remapp/templates/remapp/displayname-last-date.html +3 -3
- openrem/remapp/templates/remapp/displayname-modality.html +86 -105
- openrem/remapp/templates/remapp/displayname-skinmap.html +18 -18
- openrem/remapp/templates/remapp/displaynameupdate.html +100 -100
- openrem/remapp/templates/remapp/displaynameview.html +222 -219
- openrem/remapp/templates/remapp/dxdetail.html +176 -176
- openrem/remapp/templates/remapp/dxfiltered.html +324 -324
- openrem/remapp/templates/remapp/exports-active.html +25 -25
- openrem/remapp/templates/remapp/exports-complete.html +35 -35
- openrem/remapp/templates/remapp/exports-error.html +26 -26
- openrem/remapp/templates/remapp/exports-queue.html +18 -18
- openrem/remapp/templates/remapp/exports.html +191 -191
- openrem/remapp/templates/remapp/failed_summary_list.html +27 -27
- openrem/remapp/templates/remapp/filteredbase.html +162 -162
- openrem/remapp/templates/remapp/highdosemetricalertsettings_form.html +76 -76
- openrem/remapp/templates/remapp/home-list-modalities.html +94 -94
- openrem/remapp/templates/remapp/home.html +202 -202
- openrem/remapp/templates/remapp/list_filters.html +24 -24
- openrem/remapp/templates/remapp/mgdetail.html +160 -138
- openrem/remapp/templates/remapp/mgfiltered.html +311 -311
- openrem/remapp/templates/remapp/nmdetail.html +300 -300
- openrem/remapp/templates/remapp/nmfiltered.html +255 -255
- openrem/remapp/templates/remapp/notpatient.html +190 -190
- openrem/remapp/templates/remapp/notpatientindicators_form_base.html +81 -81
- openrem/remapp/templates/remapp/notpatientindicatorsid_confirm_delete.html +54 -54
- openrem/remapp/templates/remapp/notpatientindicatorsid_form.html +23 -23
- openrem/remapp/templates/remapp/notpatientindicatorsname_confirm_delete.html +54 -54
- openrem/remapp/templates/remapp/notpatientindicatorsname_form.html +23 -23
- openrem/remapp/templates/remapp/notpatientindicatorsname_form_base.html +85 -85
- openrem/remapp/templates/remapp/openskinsafelist_add.html +130 -130
- openrem/remapp/templates/remapp/openskinsafelist_confirm_delete.html +100 -100
- openrem/remapp/templates/remapp/openskinsafelist_form.html +207 -207
- openrem/remapp/templates/remapp/patientidsettings_form.html +83 -83
- openrem/remapp/templates/remapp/populate_summary_progress.html +83 -83
- openrem/remapp/templates/remapp/populate_summary_progress_error.html +36 -36
- openrem/remapp/templates/remapp/review_failed_imports.html +157 -157
- openrem/remapp/templates/remapp/review_failed_study.html +41 -41
- openrem/remapp/templates/remapp/review_studies_delete_button.html +20 -20
- openrem/remapp/templates/remapp/review_study.html +19 -19
- openrem/remapp/templates/remapp/review_summary_list.html +245 -245
- openrem/remapp/templates/remapp/rf_dose_alert_email_template.html +14 -1
- openrem/remapp/templates/remapp/rfalertnotificationsview.html +59 -59
- openrem/remapp/templates/remapp/rfdetail.html +547 -543
- openrem/remapp/templates/remapp/rfdetailbase.html +18 -18
- openrem/remapp/templates/remapp/rffiltered.html +404 -404
- openrem/remapp/templates/remapp/sizeimports.html +119 -119
- openrem/remapp/templates/remapp/sizeprocess.html +96 -96
- openrem/remapp/templates/remapp/sizeupload.html +110 -110
- openrem/remapp/templates/remapp/skindosemapcalcsettings_form.html +28 -28
- openrem/remapp/templates/remapp/standardname-modality.html +69 -69
- openrem/remapp/templates/remapp/standardnames_confirm_delete.html +71 -71
- openrem/remapp/templates/remapp/standardnames_form.html +87 -87
- openrem/remapp/templates/remapp/standardnamesettings_form.html +41 -41
- openrem/remapp/templates/remapp/standardnamesrefreshall.html +92 -92
- openrem/remapp/templates/remapp/standardnameview.html +103 -103
- openrem/remapp/templates/remapp/study_confirm_delete.html +147 -147
- openrem/remapp/templates/remapp/task_admin.html +265 -265
- openrem/remapp/templates/remapp/tasks.html +76 -76
- openrem/remapp/templatetags/formfilters.py +13 -13
- openrem/remapp/templatetags/proper_paginate.py +38 -38
- openrem/remapp/templatetags/remappduration.py +36 -36
- openrem/remapp/templatetags/sigdig.py +38 -38
- openrem/remapp/templatetags/sort_class_property_value.py +15 -15
- openrem/remapp/templatetags/update_variable.py +20 -20
- openrem/remapp/templatetags/url_replace.py +25 -25
- openrem/remapp/tests/test_charts_common.py +202 -202
- openrem/remapp/tests/test_charts_ct.py +7111 -7111
- openrem/remapp/tests/test_charts_dx.py +3513 -3513
- openrem/remapp/tests/test_charts_mg.py +1116 -1115
- openrem/remapp/tests/test_dcmdatetime.py +189 -189
- openrem/remapp/tests/test_dicom_qr.py +2580 -2580
- openrem/remapp/tests/test_display_name.py +274 -274
- openrem/remapp/tests/test_export_ct_xlsx.py +272 -248
- openrem/remapp/tests/test_export_dx_xlsx.py +137 -134
- openrem/remapp/tests/test_export_mammo_csv.py +242 -242
- openrem/remapp/tests/test_export_rf_xlsx.py +246 -246
- openrem/remapp/tests/test_files/DX-Im-DRGEM.dcm +0 -0
- openrem/remapp/tests/test_files/MG-RDSR-GEPristina-2D.dcm +0 -0
- openrem/remapp/tests/test_files/MG-RDSR-GEPristina-DBT.dcm +0 -0
- openrem/remapp/tests/test_files/MG-RDSR-Giotto-DBT.dcm +0 -0
- openrem/remapp/tests/test_files/skin_map_alphenix.py +590 -590
- openrem/remapp/tests/test_files/skin_map_zee.py +354 -354
- openrem/remapp/tests/test_filters_ct.py +321 -321
- openrem/remapp/tests/test_filters_dx.py +92 -92
- openrem/remapp/tests/test_filters_mammo.py +183 -183
- openrem/remapp/tests/test_filters_rf.py +118 -118
- openrem/remapp/tests/test_get_values.py +72 -72
- openrem/remapp/tests/test_hash_id.py +65 -65
- openrem/remapp/tests/test_import_ct_esr_ge.py +3034 -3034
- openrem/remapp/tests/test_import_ct_philips_rdsr.py +42 -42
- openrem/remapp/tests/test_import_ct_rdsr_multiple.py +256 -256
- openrem/remapp/tests/test_import_ct_rdsr_siemens.py +827 -827
- openrem/remapp/tests/test_import_ct_rdsr_spectrumdynamics.py +91 -91
- openrem/remapp/tests/test_import_ct_rdsr_toshiba_dosecheck.py +67 -67
- openrem/remapp/tests/test_import_ct_rdsr_toshiba_multivaluesd.py +33 -33
- openrem/remapp/tests/test_import_ct_rdsr_toshiba_pixelmed.py +118 -118
- openrem/remapp/tests/test_import_ct_sc_philips.py +44 -44
- openrem/remapp/tests/test_import_dual_rdsr.py +110 -110
- openrem/remapp/tests/test_import_dx.py +1267 -1191
- openrem/remapp/tests/test_import_dx_rdsr.py +1250 -1253
- openrem/remapp/tests/test_import_mam.py +438 -438
- openrem/remapp/tests/test_import_mg_im_hol_proj.py +46 -46
- openrem/remapp/tests/test_import_mg_rdsr.py +586 -586
- openrem/remapp/tests/test_import_nm_image.py +420 -420
- openrem/remapp/tests/test_import_nm_siemens_rdsr.py +396 -396
- openrem/remapp/tests/test_import_px.py +161 -161
- openrem/remapp/tests/test_import_rf_rdsr.py +420 -418
- openrem/remapp/tests/test_missing_date.py +42 -42
- openrem/remapp/tests/test_not_patient.py +60 -60
- openrem/remapp/tests/test_openskin.py +272 -272
- openrem/remapp/tests/test_patient_id_settings.py +72 -72
- openrem/remapp/tests/test_pt_size_import.py +232 -232
- openrem/remapp/tests/test_rf_detail.py +113 -113
- openrem/remapp/tests/test_rf_high_dose_alert.py +361 -361
- openrem/remapp/tools/background.py +361 -361
- openrem/remapp/tools/check_standard_name_status.py +47 -0
- openrem/remapp/tools/check_uid.py +70 -70
- openrem/remapp/tools/dcmdatetime.py +248 -248
- openrem/remapp/tools/default_import.py +44 -47
- openrem/remapp/tools/get_values.py +230 -230
- openrem/remapp/tools/hash_id.py +58 -58
- openrem/remapp/tools/make_skin_map.py +448 -406
- openrem/remapp/tools/not_patient_indicators.py +72 -72
- openrem/remapp/tools/openskin/calc_exp_map.py +173 -173
- openrem/remapp/tools/openskin/geomclass.py +475 -475
- openrem/remapp/tools/openskin/geomfunc.py +433 -432
- openrem/remapp/tools/openskin/skinmap.py +417 -417
- openrem/remapp/tools/populate_summary.py +185 -193
- openrem/remapp/tools/save_skin_map_structure.py +73 -73
- openrem/remapp/tools/send_high_dose_alert_emails.py +238 -207
- openrem/remapp/urls.py +456 -448
- openrem/remapp/version.py +11 -11
- openrem/remapp/views.py +1147 -1052
- openrem/remapp/views_admin.py +3876 -3936
- openrem/remapp/views_charts_ct.py +2110 -2058
- openrem/remapp/views_charts_dx.py +1906 -1836
- openrem/remapp/views_charts_mg.py +1349 -1196
- openrem/remapp/views_charts_nm.py +535 -535
- openrem/remapp/views_charts_rf.py +1219 -1241
- openrem/remapp/views_openskin.py +379 -384
- openrem/sample-config/openrem-consumer.service +12 -12
- openrem/sample-config/openrem-gunicorn.service +13 -13
- openrem/sample-config/openrem-server +14 -13
- openrem/sample-config/openrem_orthanc_config_linux.lua +454 -454
- openrem/sample-config/openrem_orthanc_config_windows.lua +455 -455
- openrem/sample-config/queue-init.bat +73 -73
- openrem/scripts/openrem_ctphilips.py +25 -25
- openrem/scripts/openrem_cttoshiba.py +28 -28
- openrem/scripts/openrem_dx.py +22 -22
- openrem/scripts/openrem_mg.py +22 -22
- openrem/scripts/openrem_nm.py +22 -22
- openrem/scripts/openrem_ptsizecsv.py +17 -17
- openrem/scripts/openrem_qr.py +12 -12
- openrem/scripts/openrem_rdsr.py +25 -25
- {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/METADATA +39 -29
- openrem-1.0.0b3.dist-info/RECORD +379 -0
- {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/WHEEL +1 -1
- {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/COPYING-GPLv3 +674 -674
- {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/LICENSE +22 -22
- OpenREM-1.0.0b2.dist-info/RECORD +0 -373
- openrem/remapp/static/js/charts/plotly-2.17.1.min.js +0 -8
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ctphilips.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_cttoshiba.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_dx.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_mg.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_nm.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ptsizecsv.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_qr.py +0 -0
- {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_rdsr.py +0 -0
- {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/top_level.txt +0 -0
|
@@ -1,2108 +1,2116 @@
|
|
|
1
|
-
# This Python file uses the following encoding: utf-8
|
|
2
|
-
# OpenREM - Radiation Exposure Monitoring tools for the physicist
|
|
3
|
-
# Copyright (C) 2017 The Royal Marsden NHS Foundation Trust
|
|
4
|
-
#
|
|
5
|
-
# This program is free software: you can redistribute it and/or modify
|
|
6
|
-
# it under the terms of the GNU General Public License as published by
|
|
7
|
-
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
-
# (at your option) any later version.
|
|
9
|
-
#
|
|
10
|
-
# This program is distributed in the hope that it will be useful,
|
|
11
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
-
# GNU General Public License for more details.
|
|
14
|
-
#
|
|
15
|
-
# Additional permission under section 7 of GPLv3:
|
|
16
|
-
# You shall not make any use of the name of The Royal Marsden NHS
|
|
17
|
-
# Foundation trust in connection with this Program in any press or
|
|
18
|
-
# other public announcement without the prior written consent of
|
|
19
|
-
# The Royal Marsden NHS Foundation Trust.
|
|
20
|
-
#
|
|
21
|
-
# You should have received a copy of the GNU General Public License
|
|
22
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
.. module:: rdsr_toshiba_fct_from_dose_images.
|
|
26
|
-
:synopsis: Module to create a radiation dose structured report from legacy Toshiba CT studies
|
|
27
|
-
|
|
28
|
-
.. moduleauthor:: David Platten
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
from builtins import str # pylint: disable=redefined-builtin
|
|
32
|
-
import sys
|
|
33
|
-
import os
|
|
34
|
-
import shutil
|
|
35
|
-
import logging
|
|
36
|
-
import traceback
|
|
37
|
-
|
|
38
|
-
import django
|
|
39
|
-
|
|
40
|
-
from .rdsr import rdsr
|
|
41
|
-
from openrem.openremproject.settings import (
|
|
42
|
-
JAVA_EXE,
|
|
43
|
-
JAVA_OPTIONS,
|
|
44
|
-
PIXELMED_JAR,
|
|
45
|
-
PIXELMED_JAR_OPTIONS,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
# setup django/OpenREM
|
|
49
|
-
base_path = os.path.dirname(__file__)
|
|
50
|
-
project_path = os.path.abspath(os.path.join(base_path, "..", ".."))
|
|
51
|
-
if project_path not in sys.path:
|
|
52
|
-
sys.path.insert(1, project_path)
|
|
53
|
-
os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
|
|
54
|
-
django.setup()
|
|
55
|
-
|
|
56
|
-
# logger is explicitly named so that it is still handled when using __main__
|
|
57
|
-
logger = logging.getLogger("remapp.extractors.ct_toshiba")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _find_dose_summary_objects(folder_path):
|
|
61
|
-
"""This function looks for objects with a SOPClassUID of "Secondary
|
|
62
|
-
Capture Image Storage" and an ImageType with a length of 2.
|
|
63
|
-
|
|
64
|
-
Dose summary objects have a 2-element ImageType:
|
|
65
|
-
ImageType is ['DERIVED', 'SECONDARY']
|
|
66
|
-
Other secondary capture storage objects have a 3-element ImageType:
|
|
67
|
-
ImageType is ['DERIVED', 'SECONDARY', 'MULTI_FORMAT_RASTER']
|
|
68
|
-
ImageType is ['DERIVED', 'SECONDARY', 'DISPLAY']
|
|
69
|
-
ImageType is ['DERIVED', 'SECONDARY', 'MPR']
|
|
70
|
-
|
|
71
|
-
The above are from a virtual colonoscopy study.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
folder_path: a string containing the path to the folder containing the
|
|
75
|
-
DICOM objects
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
A list of structures, each containing the elements "fileName",
|
|
79
|
-
"studyTime" and "instanceNumber".
|
|
80
|
-
"""
|
|
81
|
-
import pydicom
|
|
82
|
-
from pydicom.filereader import InvalidDicomError
|
|
83
|
-
|
|
84
|
-
sop_class_uid = "Secondary Capture Image Storage"
|
|
85
|
-
dose_summary_object_list = []
|
|
86
|
-
|
|
87
|
-
for file_name in os.listdir(folder_path):
|
|
88
|
-
if os.path.isfile(os.path.join(folder_path, file_name)):
|
|
89
|
-
try:
|
|
90
|
-
dcm = pydicom.dcmread(os.path.join(folder_path, file_name))
|
|
91
|
-
if (
|
|
92
|
-
str(dcm.SOPClassUID) == str(sop_class_uid)
|
|
93
|
-
and len(dcm.ImageType) == 2
|
|
94
|
-
):
|
|
95
|
-
dose_summary_object_list.append(
|
|
96
|
-
{
|
|
97
|
-
"fileName": file_name,
|
|
98
|
-
"studyTime": dcm.StudyTime,
|
|
99
|
-
"instanceNumber": dcm.InstanceNumber,
|
|
100
|
-
}
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
except InvalidDicomError as e:
|
|
104
|
-
logger.debug(
|
|
105
|
-
"Invalid DICOM error: {0} when trying to read {1}".format(
|
|
106
|
-
e.message, os.path.join(folder_path, file_name)
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
except Exception:
|
|
110
|
-
logger.debug(traceback.format_exc())
|
|
111
|
-
|
|
112
|
-
return dose_summary_object_list
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _copy_files_from_a_to_b(src_folder, dest_folder):
|
|
116
|
-
"""Copy files in src_folder to dest_folder"""
|
|
117
|
-
src_files = os.listdir(src_folder)
|
|
118
|
-
for file_name in src_files:
|
|
119
|
-
full_file_name = os.path.join(src_folder, file_name)
|
|
120
|
-
if os.path.isfile(full_file_name):
|
|
121
|
-
shutil.copy(full_file_name, dest_folder)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _split_by_studyinstanceuid(dicom_path):
|
|
125
|
-
"""Parse a folder of files, creating a sub-folder for each StudyInstanceUID found in any DICOM files. Each DICOM
|
|
126
|
-
file is copied into the folder that matches its StudyInstanceUID. The files are renamed to have integer names
|
|
127
|
-
starting with 0 and incrementing by 1 for each additional file copied.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
dicom_path (str): The full path to a folder containing DICOM objects.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
list: A list of paths that contain the DICOM objects corresponding to each StudyInstanceUID present in the
|
|
134
|
-
original dicom_path.
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
import pydicom
|
|
138
|
-
|
|
139
|
-
from pydicom.filereader import InvalidDicomError
|
|
140
|
-
|
|
141
|
-
folder_list = []
|
|
142
|
-
file_counter = 0
|
|
143
|
-
|
|
144
|
-
for filename in os.listdir(dicom_path):
|
|
145
|
-
if os.path.isfile(os.path.join(dicom_path, filename)):
|
|
146
|
-
try:
|
|
147
|
-
dcm = pydicom.dcmread(os.path.join(dicom_path, filename))
|
|
148
|
-
|
|
149
|
-
subfolder_path = os.path.join(dicom_path, dcm.StudyInstanceUID)
|
|
150
|
-
if not os.path.isdir(subfolder_path):
|
|
151
|
-
os.mkdir(subfolder_path)
|
|
152
|
-
folder_list.append(subfolder_path)
|
|
153
|
-
|
|
154
|
-
shutil.copy2(
|
|
155
|
-
os.path.join(dicom_path, filename),
|
|
156
|
-
os.path.join(subfolder_path, str(file_counter)),
|
|
157
|
-
)
|
|
158
|
-
file_counter += 1
|
|
159
|
-
except InvalidDicomError as e:
|
|
160
|
-
logger.debug(
|
|
161
|
-
"Invalid DICOM error: {0} when trying to read {1}".format(
|
|
162
|
-
e.message, os.path.join(dicom_path, filename)
|
|
163
|
-
)
|
|
164
|
-
)
|
|
165
|
-
except Exception:
|
|
166
|
-
logger.debug(traceback.format_exc())
|
|
167
|
-
|
|
168
|
-
return folder_list
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def _find_extra_info(dicom_path):
|
|
172
|
-
"""Parses a folder of files, obtaining DICOM tag information on each acquisition present.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
dicom_path (str): A string containing the full path to the folder.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
list: A two element list. The first element contains study-level information. The second element is a list of
|
|
179
|
-
dictionaries, one per acquisition found in the files. Each dictionary contains the following information about
|
|
180
|
-
each acquisition, if present:
|
|
181
|
-
|
|
182
|
-
AcquisitionNumber
|
|
183
|
-
ProtocolName
|
|
184
|
-
ExposureTime
|
|
185
|
-
SpiralPitchFactor
|
|
186
|
-
CTDIvol
|
|
187
|
-
ExposureModulationType
|
|
188
|
-
NominalTotalCollimationWidth
|
|
189
|
-
NominalSingleCollimationWidth
|
|
190
|
-
DeviceSerialNumber
|
|
191
|
-
StudyDescription
|
|
192
|
-
RequestedProcedureDescription
|
|
193
|
-
DLP
|
|
194
|
-
SoftwareVersions
|
|
195
|
-
DeviceSerialNumber
|
|
196
|
-
|
|
197
|
-
"""
|
|
198
|
-
import pydicom
|
|
199
|
-
|
|
200
|
-
from pydicom.filereader import InvalidDicomError
|
|
201
|
-
|
|
202
|
-
from struct import unpack
|
|
203
|
-
|
|
204
|
-
logger.debug(
|
|
205
|
-
"Starting _find_extra_info routine for images in {0}".format(dicom_path)
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
acquisition_info = []
|
|
209
|
-
acquisitions_collected = []
|
|
210
|
-
study_info = {}
|
|
211
|
-
|
|
212
|
-
for filename in os.listdir(dicom_path):
|
|
213
|
-
if os.path.isfile(os.path.join(dicom_path, filename)):
|
|
214
|
-
try:
|
|
215
|
-
dcm = pydicom.dcmread(os.path.join(dicom_path, filename))
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
# Only look at the tags if the combination of AcquisitionNumber and AcquisitionTime is new
|
|
219
|
-
acquisition_code = (
|
|
220
|
-
str(dcm.AcquisitionNumber) + "_" + dcm.AcquisitionTime
|
|
221
|
-
)
|
|
222
|
-
if acquisition_code not in acquisitions_collected:
|
|
223
|
-
logger.debug("acquisition_code is {0}".format(acquisition_code))
|
|
224
|
-
acquisitions_collected.append(acquisition_code)
|
|
225
|
-
|
|
226
|
-
info_dictionary = {}
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
info_dictionary["AcquisitionNumber"] = dcm.AcquisitionNumber
|
|
230
|
-
except AttributeError:
|
|
231
|
-
pass
|
|
232
|
-
except Exception:
|
|
233
|
-
logger.debug(traceback.format_exc())
|
|
234
|
-
|
|
235
|
-
try:
|
|
236
|
-
info_dictionary["AcquisitionTime"] = dcm.AcquisitionTime
|
|
237
|
-
except AttributeError:
|
|
238
|
-
pass
|
|
239
|
-
except Exception:
|
|
240
|
-
logger.debug(traceback.format_exc())
|
|
241
|
-
|
|
242
|
-
try:
|
|
243
|
-
info_dictionary["ProtocolName"] = dcm.ProtocolName
|
|
244
|
-
except AttributeError:
|
|
245
|
-
pass
|
|
246
|
-
except Exception:
|
|
247
|
-
logger.debug(traceback.format_exc())
|
|
248
|
-
|
|
249
|
-
try:
|
|
250
|
-
info_dictionary["ExposureTime"] = dcm.ExposureTime
|
|
251
|
-
except AttributeError:
|
|
252
|
-
pass
|
|
253
|
-
except Exception:
|
|
254
|
-
logger.debug(traceback.format_exc())
|
|
255
|
-
|
|
256
|
-
try:
|
|
257
|
-
info_dictionary["KVP"] = dcm.KVP
|
|
258
|
-
except AttributeError:
|
|
259
|
-
pass
|
|
260
|
-
except Exception:
|
|
261
|
-
logger.debug(traceback.format_exc())
|
|
262
|
-
|
|
263
|
-
try:
|
|
264
|
-
# For some Toshiba CT scanners there is information on the detector configuration
|
|
265
|
-
if dcm.Manufacturer.lower() == "toshiba":
|
|
266
|
-
info_dictionary[
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
info_dictionary["NominalTotalCollimationWidth"] = dcm[
|
|
270
|
-
0x7005, 0x1009
|
|
271
|
-
].value.count("1") * float(dcm[0x7005, 0x1008].value)
|
|
272
|
-
except AttributeError:
|
|
273
|
-
pass
|
|
274
|
-
except Exception:
|
|
275
|
-
logger.debug(traceback.format_exc())
|
|
276
|
-
|
|
277
|
-
try:
|
|
278
|
-
info_dictionary["SpiralPitchFactor"] = dcm.SpiralPitchFactor
|
|
279
|
-
except AttributeError:
|
|
280
|
-
try:
|
|
281
|
-
# For some Toshiba CT scanners, stored as a decimal string (DS)
|
|
282
|
-
info_dictionary["SpiralPitchFactor"] = dcm[
|
|
283
|
-
0x7005, 0x1023
|
|
284
|
-
].value
|
|
285
|
-
except KeyError:
|
|
286
|
-
pass
|
|
287
|
-
except Exception:
|
|
288
|
-
logger.debug(traceback.format_exc())
|
|
289
|
-
except Exception:
|
|
290
|
-
logger.debug(traceback.format_exc())
|
|
291
|
-
|
|
292
|
-
try:
|
|
293
|
-
# For some Toshiba CT scanners, stored as a floating point double (FD) by the
|
|
294
|
-
# scanner, but encoded by PACS as hex
|
|
295
|
-
if dcm[0x7005, 0x1063].VR == "FD":
|
|
296
|
-
info_dictionary["CTDIvol"] = dcm[0x7005, 0x1063].value
|
|
297
|
-
logger.debug(
|
|
298
|
-
"CTDIvol found in VR==FD format in dcm[0x7005,0x1063] ({0}).".format(
|
|
299
|
-
dcm[0x7005, 0x1063].value
|
|
300
|
-
)
|
|
301
|
-
)
|
|
302
|
-
else:
|
|
303
|
-
info_dictionary["CTDIvol"] = unpack(
|
|
304
|
-
"<d", "".join(dcm[0x7005, 0x1063])
|
|
305
|
-
)[0]
|
|
306
|
-
logger.debug(
|
|
307
|
-
"CTDIvol unpacked from dcm[0x7005,0x1063] ({0}).".format(
|
|
308
|
-
unpack("<d", "".join(dcm[0x7005, 0x1063]))[0]
|
|
309
|
-
)
|
|
310
|
-
)
|
|
311
|
-
except KeyError:
|
|
312
|
-
logger.debug(
|
|
313
|
-
"There was a key error when finding CTDIvol. Trying elsewhere."
|
|
314
|
-
)
|
|
315
|
-
try:
|
|
316
|
-
info_dictionary["CTDIvol"] = dcm.CTDIvol
|
|
317
|
-
logger.debug(
|
|
318
|
-
"CTDIvol found in dcm.CTDIvol ({0}).".format(
|
|
319
|
-
dcm.CTDIvol
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
except AttributeError:
|
|
323
|
-
pass
|
|
324
|
-
except Exception:
|
|
325
|
-
logger.debug(traceback.format_exc())
|
|
326
|
-
except TypeError:
|
|
327
|
-
logger.debug(
|
|
328
|
-
"There was a type error when finding CTDIvol. Trying elsewhere."
|
|
329
|
-
)
|
|
330
|
-
try:
|
|
331
|
-
info_dictionary["CTDIvol"] = dcm.CTDIvol
|
|
332
|
-
logger.debug(
|
|
333
|
-
"CTDIvol found in dcm.CTDIvol ({0}).".format(
|
|
334
|
-
dcm.CTDIvol
|
|
335
|
-
)
|
|
336
|
-
)
|
|
337
|
-
except AttributeError:
|
|
338
|
-
pass
|
|
339
|
-
except Exception:
|
|
340
|
-
logger.debug(traceback.format_exc())
|
|
341
|
-
except Exception:
|
|
342
|
-
logger.debug(traceback.format_exc())
|
|
343
|
-
|
|
344
|
-
try:
|
|
345
|
-
info_dictionary[
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
except AttributeError:
|
|
349
|
-
pass
|
|
350
|
-
except Exception:
|
|
351
|
-
logger.debug(traceback.format_exc())
|
|
352
|
-
|
|
353
|
-
try:
|
|
354
|
-
# For some Toshiba CT scanners, stored as a floating point double (FD) by the
|
|
355
|
-
# scanner, but encoded by PACS as hex
|
|
356
|
-
if dcm[0x7005, 0x1040].VR == "FD":
|
|
357
|
-
info_dictionary["DLP"] = dcm[0x7005, 0x1040].value
|
|
358
|
-
logger.debug(
|
|
359
|
-
"DLP found in VR==FD format in dcm[0x7005,0x1040] ({0}).".format(
|
|
360
|
-
dcm[0x7005, 0x1040].value
|
|
361
|
-
)
|
|
362
|
-
)
|
|
363
|
-
else:
|
|
364
|
-
info_dictionary["DLP"] = unpack(
|
|
365
|
-
"<d", "".join(dcm[0x7005, 0x1040])
|
|
366
|
-
)[0]
|
|
367
|
-
logger.debug(
|
|
368
|
-
"DLP unpacked from dcm[0x7005,0x1040] ({0}).".format(
|
|
369
|
-
unpack("<d", "".join(dcm[0x7005, 0x1040]))[0]
|
|
370
|
-
)
|
|
371
|
-
)
|
|
372
|
-
except KeyError:
|
|
373
|
-
logger.debug("There was a key error when finding DLP.")
|
|
374
|
-
except TypeError:
|
|
375
|
-
pass
|
|
376
|
-
except Exception:
|
|
377
|
-
logger.debug(traceback.format_exc())
|
|
378
|
-
|
|
379
|
-
acquisition_info.append(info_dictionary)
|
|
380
|
-
|
|
381
|
-
# Update the study-level information, whether this acquisition number has been seen yet or not
|
|
382
|
-
try:
|
|
383
|
-
if dcm.StudyDescription != "":
|
|
384
|
-
try:
|
|
385
|
-
if study_info["StudyDescription"] == "":
|
|
386
|
-
# Only update study_info['StudyDescription'] if it's empty
|
|
387
|
-
study_info[
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
except KeyError:
|
|
391
|
-
# study_info['StudyDescription'] isn't present, so add it
|
|
392
|
-
study_info["StudyDescription"] = dcm.StudyDescription
|
|
393
|
-
except Exception:
|
|
394
|
-
logger.debug(traceback.format_exc())
|
|
395
|
-
except AttributeError:
|
|
396
|
-
# dcm.StudyDescription isn't present. Try looking at the CodeMeaning of
|
|
397
|
-
# ProcedureCodeSequence instead
|
|
398
|
-
try:
|
|
399
|
-
if dcm.ProcedureCodeSequence[0].CodeMeaning != "":
|
|
400
|
-
try:
|
|
401
|
-
if study_info["StudyDescription"] == "":
|
|
402
|
-
# Only update study_info['StudyDescription'] if it's empty
|
|
403
|
-
study_info[
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
except KeyError:
|
|
407
|
-
# study_info['StudyDescription'] isn't present, so add it
|
|
408
|
-
study_info[
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
except Exception:
|
|
412
|
-
logger.debug(traceback.format_exc())
|
|
413
|
-
except AttributeError:
|
|
414
|
-
# dcm.ProcedureCodeSequence[0].CodeMeaning isn't present either
|
|
415
|
-
pass
|
|
416
|
-
except Exception:
|
|
417
|
-
logger.debug(traceback.format_exc())
|
|
418
|
-
except Exception:
|
|
419
|
-
logger.debug(traceback.format_exc())
|
|
420
|
-
|
|
421
|
-
try:
|
|
422
|
-
if dcm.RequestedProcedureDescription != "":
|
|
423
|
-
try:
|
|
424
|
-
if study_info["RequestedProcedureDescription"] == "":
|
|
425
|
-
# Only update study_info['RequestedProcedureDescription'] if it's empty
|
|
426
|
-
study_info[
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
except KeyError:
|
|
430
|
-
# study_info['RequestedProcedureDescription'] doesn't exist yet, so create it
|
|
431
|
-
study_info[
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
except Exception:
|
|
435
|
-
logger.debug(traceback.format_exc())
|
|
436
|
-
except AttributeError:
|
|
437
|
-
# dcm.RequestedProcedureDescription isn't present. Try looking at the
|
|
438
|
-
# RequestedProcedureDescription of RequestAttributesSequence instead
|
|
439
|
-
try:
|
|
440
|
-
if dcm.ProcedureCodeSequence[0].CodeMeaning != "":
|
|
441
|
-
try:
|
|
442
|
-
if (
|
|
443
|
-
study_info["RequestedProcedureDescription"]
|
|
444
|
-
== ""
|
|
445
|
-
):
|
|
446
|
-
# Only update study_info['RequestedProcedureDescription'] if it's empty
|
|
447
|
-
study_info[
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
except KeyError:
|
|
451
|
-
# study_info['RequestedProcedureDescription'] isn't present, so add it
|
|
452
|
-
study_info[
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
except Exception:
|
|
456
|
-
logger.debug(traceback.format_exc())
|
|
457
|
-
except AttributeError:
|
|
458
|
-
# dcm.ProcedureCodeSequence[0].CodeMeaning isn't present either
|
|
459
|
-
pass
|
|
460
|
-
except Exception:
|
|
461
|
-
logger.debug(traceback.format_exc())
|
|
462
|
-
|
|
463
|
-
try:
|
|
464
|
-
if dcm.SoftwareVersions != "":
|
|
465
|
-
try:
|
|
466
|
-
if study_info["SoftwareVersions"] == "":
|
|
467
|
-
# Only update study_info['SoftwareVersions'] if it's empty
|
|
468
|
-
study_info[
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
except KeyError:
|
|
472
|
-
# study_info['SoftwareVersions'] doesn't exist yet, so create it
|
|
473
|
-
study_info["SoftwareVersions"] = dcm.SoftwareVersions
|
|
474
|
-
except Exception:
|
|
475
|
-
logger.debug(traceback.format_exc())
|
|
476
|
-
except AttributeError:
|
|
477
|
-
pass
|
|
478
|
-
|
|
479
|
-
try:
|
|
480
|
-
if dcm.DeviceSerialNumber != "":
|
|
481
|
-
try:
|
|
482
|
-
if study_info["DeviceSerialNumber"] == "":
|
|
483
|
-
# Only update study_info['DeviceSerialNumber'] if it's empty
|
|
484
|
-
study_info[
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
except KeyError:
|
|
488
|
-
# study_info['DeviceSerialNumber'] doesn't exist yet, so create it
|
|
489
|
-
study_info[
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
except Exception:
|
|
493
|
-
logger.debug(traceback.format_exc())
|
|
494
|
-
except AttributeError:
|
|
495
|
-
pass
|
|
496
|
-
|
|
497
|
-
except AttributeError:
|
|
498
|
-
pass
|
|
499
|
-
except Exception:
|
|
500
|
-
logger.debug(traceback.format_exc())
|
|
501
|
-
|
|
502
|
-
except InvalidDicomError:
|
|
503
|
-
pass
|
|
504
|
-
except Exception:
|
|
505
|
-
logger.debug(traceback.format_exc())
|
|
506
|
-
|
|
507
|
-
logger.debug(
|
|
508
|
-
"Reached the end of _find_extra_info for images in {0}".format(dicom_path)
|
|
509
|
-
)
|
|
510
|
-
logger.debug("study_info is: {0}".format(study_info))
|
|
511
|
-
logger.debug("acquisition_info is: {0}".format(acquisition_info))
|
|
512
|
-
return [study_info, acquisition_info]
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _make_explicit_vr_little_endian(folder, dcmconv_exe):
|
|
516
|
-
"""Parse folder of files, making each DICOM file explicit VR little endian using the DICOM toolkit dcmconv.exe
|
|
517
|
-
command. See http://support.dcmtk.org/docs/dcmconv.html for documentation.
|
|
518
|
-
|
|
519
|
-
Args:
|
|
520
|
-
folder (string): Full path containing DICOM objects.
|
|
521
|
-
dcmconv_exe (str): A string containing the dcmconv command
|
|
522
|
-
|
|
523
|
-
"""
|
|
524
|
-
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
525
|
-
import subprocess # nosec
|
|
526
|
-
|
|
527
|
-
for filename in os.listdir(folder):
|
|
528
|
-
command = (
|
|
529
|
-
dcmconv_exe
|
|
530
|
-
+ " +te "
|
|
531
|
-
+ os.path.join(folder, filename)
|
|
532
|
-
+ " "
|
|
533
|
-
+ os.path.join(folder, filename)
|
|
534
|
-
)
|
|
535
|
-
subprocess.call(command.split()) # nosec
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def _make_dicomdir(folder, dcmmkdir_exe):
|
|
539
|
-
"""Parse folder of files, making a DICOMDIR for it using the DICOM toolkit dcmmkdir.exe command. See
|
|
540
|
-
http://support.dcmtk.org/docs/dcmmkdir.html for documentation.
|
|
541
|
-
|
|
542
|
-
Args:
|
|
543
|
-
folder (string): Full path containing DICOM objects.
|
|
544
|
-
dcmmkdir_exe (str): A string containing the dcmmkdir command
|
|
545
|
-
|
|
546
|
-
"""
|
|
547
|
-
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
548
|
-
import subprocess # nosec
|
|
549
|
-
|
|
550
|
-
command = (
|
|
551
|
-
dcmmkdir_exe
|
|
552
|
-
+ " --recurse --output-file "
|
|
553
|
-
+ os.path.join(folder, "DICOMDIR")
|
|
554
|
-
+ " --input-directory "
|
|
555
|
-
+ folder
|
|
556
|
-
)
|
|
557
|
-
subprocess.call(command.split()) # nosec
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def _make_dicom_rdsr(folder, pixelmed_jar_command, sr_filename):
|
|
561
|
-
"""Parse folder of files, making a DICOM RDSR using pixelmed.jar.
|
|
562
|
-
|
|
563
|
-
Args:
|
|
564
|
-
folder (string): Full path containing DICOM objects.
|
|
565
|
-
pixelmed_jar_command (str): A string containing the pixelmed_jar command and options.
|
|
566
|
-
sr_filename (str): A string containing the filename to use when creating the rdsr.
|
|
567
|
-
|
|
568
|
-
"""
|
|
569
|
-
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
570
|
-
import subprocess # nosec
|
|
571
|
-
|
|
572
|
-
command = (
|
|
573
|
-
pixelmed_jar_command + " " + folder + " " + os.path.join(folder, sr_filename)
|
|
574
|
-
)
|
|
575
|
-
subprocess.call(command.split()) # nosec
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
def _update_dicom_rdsr(
|
|
579
|
-
rdsr_file, additional_study_info, additional_acquisition_info, new_rdsr_file
|
|
580
|
-
):
|
|
581
|
-
"""Try to update information in an RDSR file using pydicom. Match the pair of CTDIvol and DLP values found in the
|
|
582
|
-
RDSR CT Acquisition with a pair of CTDIvol and DLP values in additional_info. If a match is found, use the other
|
|
583
|
-
information in the additional_info element to update the corresponding CT Acquisition in the RDSR.
|
|
584
|
-
|
|
585
|
-
Args:
|
|
586
|
-
rdsr_file (str): A fully qualified RDSR filename.
|
|
587
|
-
additional_study_info (list): A list of dictionaries containing information on the CT study.
|
|
588
|
-
additional_acquisition_info (list): A list of dictionaries containing information on each acquisition in the CT
|
|
589
|
-
study.
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
integer (int): 1 on success; 0 if the rdsr_file could not be read.
|
|
593
|
-
|
|
594
|
-
"""
|
|
595
|
-
import pydicom
|
|
596
|
-
from pydicom.dataset import Dataset
|
|
597
|
-
from pydicom.sequence import Sequence
|
|
598
|
-
|
|
599
|
-
logger.debug("Trying to open initial RDSR file")
|
|
600
|
-
try:
|
|
601
|
-
dcm = pydicom.dcmread(rdsr_file)
|
|
602
|
-
logger.debug("RDSR file opened: {0}".format(rdsr_file))
|
|
603
|
-
except IOError as e:
|
|
604
|
-
logger.debug(
|
|
605
|
-
"I/O error({0}): {1} when trying to read {2}".format(
|
|
606
|
-
e.errno, e.strerror, rdsr_file
|
|
607
|
-
)
|
|
608
|
-
)
|
|
609
|
-
return 0
|
|
610
|
-
except Exception:
|
|
611
|
-
logger.debug(traceback.format_exc())
|
|
612
|
-
|
|
613
|
-
# Update the study-level information if it does not exist, or is an empty string. Over-write DeviceSerialNumber
|
|
614
|
-
# even if it is already present (Dose Utility generates a unique DeviceSerialNumber, but I'd prefer to use the
|
|
615
|
-
# real one)
|
|
616
|
-
logger.debug("Updating study-level data")
|
|
617
|
-
for key, val in list(additional_study_info.items()):
|
|
618
|
-
try:
|
|
619
|
-
rdsr_val = getattr(dcm, key)
|
|
620
|
-
logger.debug("{0}: {1}".format(key, val))
|
|
621
|
-
if rdsr_val == "":
|
|
622
|
-
setattr(dcm, key, val)
|
|
623
|
-
if key == "DeviceSerialNumber":
|
|
624
|
-
setattr(dcm, key, val)
|
|
625
|
-
except AttributeError:
|
|
626
|
-
setattr(dcm, key, val)
|
|
627
|
-
except Exception:
|
|
628
|
-
logger.debug(traceback.format_exc())
|
|
629
|
-
logger.debug("Study level data updated")
|
|
630
|
-
|
|
631
|
-
# Now go through each CT Aquisition container in the rdsr file and see if any of the information should be updated.
|
|
632
|
-
logger.debug("Updating acquisition data")
|
|
633
|
-
for container in dcm.ContentSequence:
|
|
634
|
-
if container.ValueType == "CONTAINER":
|
|
635
|
-
if container.ConceptNameCodeSequence[0].CodeMeaning == "CT Acquisition":
|
|
636
|
-
for container2 in container.ContentSequence:
|
|
637
|
-
# The Acquisition protocol would go in at this level I think
|
|
638
|
-
if container2.ValueType == "CONTAINER":
|
|
639
|
-
if (
|
|
640
|
-
container2.ConceptNameCodeSequence[0].CodeMeaning
|
|
641
|
-
== "CT Dose"
|
|
642
|
-
):
|
|
643
|
-
logger.debug("Found CT Dose container")
|
|
644
|
-
current_dlp = "0"
|
|
645
|
-
current_ctdi_vol = "0"
|
|
646
|
-
for container3 in container2.ContentSequence:
|
|
647
|
-
if (
|
|
648
|
-
container3.ConceptNameCodeSequence[0].CodeMeaning
|
|
649
|
-
== "DLP"
|
|
650
|
-
):
|
|
651
|
-
current_dlp = container3.MeasuredValueSequence[
|
|
652
|
-
0
|
|
653
|
-
].NumericValue
|
|
654
|
-
logger.debug(
|
|
655
|
-
"Found DLP value: {0}".format(current_dlp)
|
|
656
|
-
)
|
|
657
|
-
if (
|
|
658
|
-
container3.ConceptNameCodeSequence[0].CodeMeaning
|
|
659
|
-
== "Mean CTDIvol"
|
|
660
|
-
):
|
|
661
|
-
current_ctdi_vol = container3.MeasuredValueSequence[
|
|
662
|
-
0
|
|
663
|
-
].NumericValue
|
|
664
|
-
logger.debug(
|
|
665
|
-
"Found CTDIvol value: {0}".format(
|
|
666
|
-
current_ctdi_vol
|
|
667
|
-
)
|
|
668
|
-
)
|
|
669
|
-
|
|
670
|
-
# Check to see if the current DLP and CTDIvol pair matches any of the acquisitions in
|
|
671
|
-
# additional_info
|
|
672
|
-
for acquisition in additional_acquisition_info:
|
|
673
|
-
try:
|
|
674
|
-
logger.debug(
|
|
675
|
-
"Trying to match DLP and CTDIvol. Current values: {0}, {1}".format(
|
|
676
|
-
float(acquisition["DLP"]),
|
|
677
|
-
float(acquisition["CTDIvol"]),
|
|
678
|
-
)
|
|
679
|
-
)
|
|
680
|
-
if float(acquisition["CTDIvol"]) == float(
|
|
681
|
-
current_ctdi_vol
|
|
682
|
-
) and float(acquisition["DLP"]) == float(
|
|
683
|
-
current_dlp
|
|
684
|
-
):
|
|
685
|
-
logger.debug("DLP and CTDIvol match")
|
|
686
|
-
# There's a match between CTDIvol and DLP, so see if things can be updated or added.
|
|
687
|
-
try:
|
|
688
|
-
for key, val in list(acquisition.items()):
|
|
689
|
-
if key != "CTDIvol" and key != "DLP":
|
|
690
|
-
logger.debug(
|
|
691
|
-
"{0} -> {1}".format(
|
|
692
|
-
key, str(val)
|
|
693
|
-
)
|
|
694
|
-
)
|
|
695
|
-
##############################################
|
|
696
|
-
# Code here to add / update the data...
|
|
697
|
-
coding = Dataset()
|
|
698
|
-
coding2 = Dataset()
|
|
699
|
-
if key == "ProtocolName":
|
|
700
|
-
# First, check if there is already a ProtocolName container that has a protocol in it.
|
|
701
|
-
data_exists = False
|
|
702
|
-
for (
|
|
703
|
-
container2b
|
|
704
|
-
) in container.ContentSequence:
|
|
705
|
-
for (
|
|
706
|
-
container3b
|
|
707
|
-
) in container2b:
|
|
708
|
-
try:
|
|
709
|
-
if (
|
|
710
|
-
container3b[
|
|
711
|
-
0
|
|
712
|
-
].CodeValue
|
|
713
|
-
== "125203"
|
|
714
|
-
):
|
|
715
|
-
data_exists = (
|
|
716
|
-
True
|
|
717
|
-
)
|
|
718
|
-
if (
|
|
719
|
-
container2b.TextValue
|
|
720
|
-
== ""
|
|
721
|
-
):
|
|
722
|
-
# Update the protocol if it is blank
|
|
723
|
-
container2b.TextValue = (
|
|
724
|
-
val
|
|
725
|
-
)
|
|
726
|
-
except AttributeError:
|
|
727
|
-
pass
|
|
728
|
-
except Exception:
|
|
729
|
-
logger.debug(
|
|
730
|
-
traceback.format_exc()
|
|
731
|
-
)
|
|
732
|
-
|
|
733
|
-
if not data_exists:
|
|
734
|
-
# If there is no protocol then add it
|
|
735
|
-
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
736
|
-
# (0040, a040) Value Type CS: 'TEXT'
|
|
737
|
-
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
738
|
-
# (0008, 0100) Code Value SH: '125203'
|
|
739
|
-
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
740
|
-
# (0008, 0104) Code Meaning LO: 'Acquisition Protocol'
|
|
741
|
-
# ---------
|
|
742
|
-
# (0040, a160) Text Value UT: 'TAP'
|
|
743
|
-
# Create the inner coding bit
|
|
744
|
-
coding.CodeValue = "125203"
|
|
745
|
-
coding.CodingSchemeDesignator = (
|
|
746
|
-
"DCM"
|
|
747
|
-
)
|
|
748
|
-
coding.CodeMeaning = (
|
|
749
|
-
"Acquisition Protocol"
|
|
750
|
-
)
|
|
751
|
-
# Create the outer container bit, including the protocol name
|
|
752
|
-
prot_container = Dataset()
|
|
753
|
-
prot_container.RelationshipType = (
|
|
754
|
-
"CONTAINS"
|
|
755
|
-
)
|
|
756
|
-
prot_container.ValueType = (
|
|
757
|
-
"TEXT"
|
|
758
|
-
)
|
|
759
|
-
prot_container.TextValue = (
|
|
760
|
-
val
|
|
761
|
-
)
|
|
762
|
-
# Add the coding sequence into the container.
|
|
763
|
-
# Sequences are lists.
|
|
764
|
-
prot_container.ConceptNameCodeSequence = Sequence(
|
|
765
|
-
[coding]
|
|
766
|
-
)
|
|
767
|
-
container.ContentSequence.append(
|
|
768
|
-
prot_container
|
|
769
|
-
)
|
|
770
|
-
|
|
771
|
-
if key == "SpiralPitchFactor":
|
|
772
|
-
# First, check if there is already a SpiralPitchFactor container that has a value in it.
|
|
773
|
-
data_exists = False
|
|
774
|
-
|
|
775
|
-
for (
|
|
776
|
-
container2b
|
|
777
|
-
) in container.ContentSequence:
|
|
778
|
-
if (
|
|
779
|
-
container2b.ValueType
|
|
780
|
-
== "CONTAINER"
|
|
781
|
-
):
|
|
782
|
-
if (
|
|
783
|
-
container2b.ConceptNameCodeSequence[
|
|
784
|
-
0
|
|
785
|
-
].CodeMeaning
|
|
786
|
-
== "CT Acquisition Parameters"
|
|
787
|
-
):
|
|
788
|
-
for (
|
|
789
|
-
container3b
|
|
790
|
-
) in container2b:
|
|
791
|
-
for (
|
|
792
|
-
container4b
|
|
793
|
-
) in (
|
|
794
|
-
container3b
|
|
795
|
-
):
|
|
796
|
-
try:
|
|
797
|
-
if (
|
|
798
|
-
container4b.ConceptNameCodeSequence[
|
|
799
|
-
0
|
|
800
|
-
].CodeValue
|
|
801
|
-
== "113828"
|
|
802
|
-
):
|
|
803
|
-
data_exists = True
|
|
804
|
-
except AttributeError:
|
|
805
|
-
pass
|
|
806
|
-
except Exception:
|
|
807
|
-
logger.debug(
|
|
808
|
-
traceback.format_exc()
|
|
809
|
-
)
|
|
810
|
-
|
|
811
|
-
if not data_exists:
|
|
812
|
-
# If there is no pitch then add it
|
|
813
|
-
# ---------
|
|
814
|
-
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
815
|
-
# (0040, a040) Value Type CS: 'NUM'
|
|
816
|
-
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
817
|
-
# (0008, 0100) Code Value SH: '113828'
|
|
818
|
-
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
819
|
-
# (0008, 0104) Code Meaning LO: 'Pitch Factor'
|
|
820
|
-
# ---------
|
|
821
|
-
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
822
|
-
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
823
|
-
# (0008, 0100) Code Value SH: '{ratio}'
|
|
824
|
-
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
825
|
-
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
826
|
-
# (0008, 0104) Code Meaning LO: 'ratio'
|
|
827
|
-
# ---------
|
|
828
|
-
# (0040, a30a) Numeric Value DS: '0.6'
|
|
829
|
-
# ---------
|
|
830
|
-
# ---------
|
|
831
|
-
# Create the first inner coding bit
|
|
832
|
-
coding.CodeValue = (
|
|
833
|
-
"113828"
|
|
834
|
-
)
|
|
835
|
-
coding.CodingSchemeDesignator = (
|
|
836
|
-
"DCM"
|
|
837
|
-
)
|
|
838
|
-
coding.CodeMeaning = "Pitch Factor"
|
|
839
|
-
# Create the second inner coding bit
|
|
840
|
-
coding2.CodeValue = (
|
|
841
|
-
"{ratio}"
|
|
842
|
-
)
|
|
843
|
-
coding2.CodingSchemeDesignator = (
|
|
844
|
-
"UCUM"
|
|
845
|
-
)
|
|
846
|
-
coding2.CodingSchemeVersion = (
|
|
847
|
-
"1.4"
|
|
848
|
-
)
|
|
849
|
-
coding2.CodeMeaning = (
|
|
850
|
-
"ratio"
|
|
851
|
-
)
|
|
852
|
-
measurement_units_container = (
|
|
853
|
-
Dataset()
|
|
854
|
-
)
|
|
855
|
-
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
856
|
-
[coding2]
|
|
857
|
-
)
|
|
858
|
-
measurement_units_container.NumericValue = (
|
|
859
|
-
val
|
|
860
|
-
)
|
|
861
|
-
measured_value_sequence = Sequence(
|
|
862
|
-
[
|
|
863
|
-
measurement_units_container
|
|
864
|
-
]
|
|
865
|
-
)
|
|
866
|
-
# Create the outer container bit
|
|
867
|
-
pitch_container = (
|
|
868
|
-
Dataset()
|
|
869
|
-
)
|
|
870
|
-
pitch_container.RelationshipType = (
|
|
871
|
-
"CONTAINS"
|
|
872
|
-
)
|
|
873
|
-
pitch_container.ValueType = (
|
|
874
|
-
"NUM"
|
|
875
|
-
)
|
|
876
|
-
# Add the coding sequence into the container.
|
|
877
|
-
# Sequences are lists.
|
|
878
|
-
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
879
|
-
[coding]
|
|
880
|
-
)
|
|
881
|
-
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
882
|
-
container2b.ContentSequence.append(
|
|
883
|
-
pitch_container
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
if (
|
|
887
|
-
key
|
|
888
|
-
== "NominalSingleCollimationWidth"
|
|
889
|
-
):
|
|
890
|
-
# First, check if there is already a NominalSingleCollimationWidth container that has a value in it.
|
|
891
|
-
data_exists = False
|
|
892
|
-
|
|
893
|
-
for (
|
|
894
|
-
container2b
|
|
895
|
-
) in container.ContentSequence:
|
|
896
|
-
if (
|
|
897
|
-
container2b.ValueType
|
|
898
|
-
== "CONTAINER"
|
|
899
|
-
):
|
|
900
|
-
if (
|
|
901
|
-
container2b.ConceptNameCodeSequence[
|
|
902
|
-
0
|
|
903
|
-
].CodeMeaning
|
|
904
|
-
== "CT Acquisition Parameters"
|
|
905
|
-
):
|
|
906
|
-
for (
|
|
907
|
-
container3b
|
|
908
|
-
) in container2b:
|
|
909
|
-
for (
|
|
910
|
-
container4b
|
|
911
|
-
) in (
|
|
912
|
-
container3b
|
|
913
|
-
):
|
|
914
|
-
try:
|
|
915
|
-
if (
|
|
916
|
-
container4b.ConceptNameCodeSequence[
|
|
917
|
-
0
|
|
918
|
-
].CodeValue
|
|
919
|
-
== "113826"
|
|
920
|
-
):
|
|
921
|
-
data_exists = True
|
|
922
|
-
except AttributeError:
|
|
923
|
-
pass
|
|
924
|
-
if not data_exists:
|
|
925
|
-
# If there is no NominalSingleCollimationWidth then add it
|
|
926
|
-
# ---------
|
|
927
|
-
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
928
|
-
# (0040, a040) Value Type CS: 'NUM'
|
|
929
|
-
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
930
|
-
# (0008, 0100) Code Value SH: '113826'
|
|
931
|
-
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
932
|
-
# (0008, 0104) Code Meaning LO: 'Nominal Single Collimation Width'
|
|
933
|
-
# ---------
|
|
934
|
-
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
935
|
-
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
936
|
-
# (0008, 0100) Code Value SH: 'mm'
|
|
937
|
-
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
938
|
-
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
939
|
-
# (0008, 0104) Code Meaning LO: 'mm'
|
|
940
|
-
# ---------
|
|
941
|
-
# (0040, a30a) Numeric Value DS: '0.6'
|
|
942
|
-
# ---------
|
|
943
|
-
# ---------
|
|
944
|
-
# Create the first inner coding bit
|
|
945
|
-
coding.CodeValue = (
|
|
946
|
-
"113826"
|
|
947
|
-
)
|
|
948
|
-
coding.CodingSchemeDesignator = (
|
|
949
|
-
"DCM"
|
|
950
|
-
)
|
|
951
|
-
coding.CodeMeaning = "Nominal Single Collimation Width"
|
|
952
|
-
# Create the second inner coding bit
|
|
953
|
-
coding2.CodeValue = (
|
|
954
|
-
"mm"
|
|
955
|
-
)
|
|
956
|
-
coding2.CodingSchemeDesignator = (
|
|
957
|
-
"UCUM"
|
|
958
|
-
)
|
|
959
|
-
coding2.CodingSchemeVersion = (
|
|
960
|
-
"1.4"
|
|
961
|
-
)
|
|
962
|
-
coding2.CodeMeaning = (
|
|
963
|
-
"mm"
|
|
964
|
-
)
|
|
965
|
-
measurement_units_container = (
|
|
966
|
-
Dataset()
|
|
967
|
-
)
|
|
968
|
-
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
969
|
-
[coding2]
|
|
970
|
-
)
|
|
971
|
-
measurement_units_container.NumericValue = (
|
|
972
|
-
val
|
|
973
|
-
)
|
|
974
|
-
measured_value_sequence = Sequence(
|
|
975
|
-
[
|
|
976
|
-
measurement_units_container
|
|
977
|
-
]
|
|
978
|
-
)
|
|
979
|
-
# Create the outer container bit
|
|
980
|
-
pitch_container = (
|
|
981
|
-
Dataset()
|
|
982
|
-
)
|
|
983
|
-
pitch_container.RelationshipType = (
|
|
984
|
-
"CONTAINS"
|
|
985
|
-
)
|
|
986
|
-
pitch_container.ValueType = (
|
|
987
|
-
"NUM"
|
|
988
|
-
)
|
|
989
|
-
# Add the coding sequence into the container.
|
|
990
|
-
# Sequences are lists.
|
|
991
|
-
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
992
|
-
[coding]
|
|
993
|
-
)
|
|
994
|
-
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
995
|
-
container2b.ContentSequence.append(
|
|
996
|
-
pitch_container
|
|
997
|
-
)
|
|
998
|
-
|
|
999
|
-
if (
|
|
1000
|
-
key
|
|
1001
|
-
== "NominalTotalCollimationWidth"
|
|
1002
|
-
):
|
|
1003
|
-
# First, check if there is already a NominalSingleCollimationWidth container that has a value in it.
|
|
1004
|
-
data_exists = False
|
|
1005
|
-
|
|
1006
|
-
for (
|
|
1007
|
-
container2b
|
|
1008
|
-
) in container.ContentSequence:
|
|
1009
|
-
if (
|
|
1010
|
-
container2b.ValueType
|
|
1011
|
-
== "CONTAINER"
|
|
1012
|
-
):
|
|
1013
|
-
if (
|
|
1014
|
-
container2b.ConceptNameCodeSequence[
|
|
1015
|
-
0
|
|
1016
|
-
].CodeMeaning
|
|
1017
|
-
== "CT Acquisition Parameters"
|
|
1018
|
-
):
|
|
1019
|
-
for (
|
|
1020
|
-
container3b
|
|
1021
|
-
) in container2b:
|
|
1022
|
-
for (
|
|
1023
|
-
container4b
|
|
1024
|
-
) in (
|
|
1025
|
-
container3b
|
|
1026
|
-
):
|
|
1027
|
-
try:
|
|
1028
|
-
if (
|
|
1029
|
-
container4b.ConceptNameCodeSequence[
|
|
1030
|
-
0
|
|
1031
|
-
].CodeValue
|
|
1032
|
-
== "113827"
|
|
1033
|
-
):
|
|
1034
|
-
data_exists = True
|
|
1035
|
-
except AttributeError:
|
|
1036
|
-
pass
|
|
1037
|
-
except Exception:
|
|
1038
|
-
logger.debug(
|
|
1039
|
-
traceback.format_exc()
|
|
1040
|
-
)
|
|
1041
|
-
|
|
1042
|
-
if not data_exists:
|
|
1043
|
-
# If there is no NominalSingleCollimationWidth then add it
|
|
1044
|
-
# ---------
|
|
1045
|
-
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
1046
|
-
# (0040, a040) Value Type CS: 'NUM'
|
|
1047
|
-
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
1048
|
-
# (0008, 0100) Code Value SH: '113827'
|
|
1049
|
-
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
1050
|
-
# (0008, 0104) Code Meaning LO: 'Nominal Total Collimation Width'
|
|
1051
|
-
# ---------
|
|
1052
|
-
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
1053
|
-
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
1054
|
-
# (0008, 0100) Code Value SH: 'mm'
|
|
1055
|
-
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
1056
|
-
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
1057
|
-
# (0008, 0104) Code Meaning LO: 'mm'
|
|
1058
|
-
# ---------
|
|
1059
|
-
# (0040, a30a) Numeric Value DS: '38.4'
|
|
1060
|
-
# ---------
|
|
1061
|
-
# ---------
|
|
1062
|
-
# Create the first inner coding bit
|
|
1063
|
-
coding.CodeValue = (
|
|
1064
|
-
"113827"
|
|
1065
|
-
)
|
|
1066
|
-
coding.CodingSchemeDesignator = (
|
|
1067
|
-
"DCM"
|
|
1068
|
-
)
|
|
1069
|
-
coding.CodeMeaning = "Nominal Total Collimation Width"
|
|
1070
|
-
# Create the second inner coding bit
|
|
1071
|
-
coding2.CodeValue = (
|
|
1072
|
-
"mm"
|
|
1073
|
-
)
|
|
1074
|
-
coding2.CodingSchemeDesignator = (
|
|
1075
|
-
"UCUM"
|
|
1076
|
-
)
|
|
1077
|
-
coding2.CodingSchemeVersion = (
|
|
1078
|
-
"1.4"
|
|
1079
|
-
)
|
|
1080
|
-
coding2.CodeMeaning = (
|
|
1081
|
-
"mm"
|
|
1082
|
-
)
|
|
1083
|
-
measurement_units_container = (
|
|
1084
|
-
Dataset()
|
|
1085
|
-
)
|
|
1086
|
-
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1087
|
-
[coding2]
|
|
1088
|
-
)
|
|
1089
|
-
measurement_units_container.NumericValue = (
|
|
1090
|
-
val
|
|
1091
|
-
)
|
|
1092
|
-
measured_value_sequence = Sequence(
|
|
1093
|
-
[
|
|
1094
|
-
measurement_units_container
|
|
1095
|
-
]
|
|
1096
|
-
)
|
|
1097
|
-
# Create the outer container bit
|
|
1098
|
-
pitch_container = (
|
|
1099
|
-
Dataset()
|
|
1100
|
-
)
|
|
1101
|
-
pitch_container.RelationshipType = (
|
|
1102
|
-
"CONTAINS"
|
|
1103
|
-
)
|
|
1104
|
-
pitch_container.ValueType = (
|
|
1105
|
-
"NUM"
|
|
1106
|
-
)
|
|
1107
|
-
# Add the coding sequence into the container.
|
|
1108
|
-
# Sequences are lists.
|
|
1109
|
-
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
1110
|
-
[coding]
|
|
1111
|
-
)
|
|
1112
|
-
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
1113
|
-
container2b.ContentSequence.append(
|
|
1114
|
-
pitch_container
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
if key == "ExposureModulationType":
|
|
1118
|
-
# First, check if there is already an ExposureModulationType container that has a value in it.
|
|
1119
|
-
data_exists = False
|
|
1120
|
-
for (
|
|
1121
|
-
container2b
|
|
1122
|
-
) in container.ContentSequence:
|
|
1123
|
-
for (
|
|
1124
|
-
container3b
|
|
1125
|
-
) in container2b:
|
|
1126
|
-
try:
|
|
1127
|
-
if (
|
|
1128
|
-
container3b[
|
|
1129
|
-
0
|
|
1130
|
-
].CodeValue
|
|
1131
|
-
== "113842"
|
|
1132
|
-
):
|
|
1133
|
-
data_exists = (
|
|
1134
|
-
True
|
|
1135
|
-
)
|
|
1136
|
-
if (
|
|
1137
|
-
container2b.TextValue
|
|
1138
|
-
== ""
|
|
1139
|
-
):
|
|
1140
|
-
# Update the X-Ray Modulation Type if it is blank
|
|
1141
|
-
container2b.TextValue = (
|
|
1142
|
-
val
|
|
1143
|
-
)
|
|
1144
|
-
except AttributeError:
|
|
1145
|
-
pass
|
|
1146
|
-
except Exception:
|
|
1147
|
-
logger.debug(
|
|
1148
|
-
traceback.format_exc()
|
|
1149
|
-
)
|
|
1150
|
-
|
|
1151
|
-
if not data_exists:
|
|
1152
|
-
# Create the inner coding bit
|
|
1153
|
-
coding.CodeValue = "113842"
|
|
1154
|
-
coding.CodingSchemeDesignator = (
|
|
1155
|
-
"DCM"
|
|
1156
|
-
)
|
|
1157
|
-
coding.CodeMeaning = (
|
|
1158
|
-
"X-Ray Modulation Type"
|
|
1159
|
-
)
|
|
1160
|
-
# Create the outer container bit, including the protocol name
|
|
1161
|
-
prot_container = Dataset()
|
|
1162
|
-
prot_container.RelationshipType = (
|
|
1163
|
-
"CONTAINS"
|
|
1164
|
-
)
|
|
1165
|
-
prot_container.ValueType = (
|
|
1166
|
-
"TEXT"
|
|
1167
|
-
)
|
|
1168
|
-
prot_container.TextValue = (
|
|
1169
|
-
val
|
|
1170
|
-
)
|
|
1171
|
-
# Add the coding sequence into the container.
|
|
1172
|
-
# Sequences are lists.
|
|
1173
|
-
prot_container.ConceptNameCodeSequence = Sequence(
|
|
1174
|
-
[coding]
|
|
1175
|
-
)
|
|
1176
|
-
container.ContentSequence.append(
|
|
1177
|
-
prot_container
|
|
1178
|
-
)
|
|
1179
|
-
|
|
1180
|
-
if key == "KVP":
|
|
1181
|
-
# First, check if there is already a kVp value in an x-ray source parameters container inside
|
|
1182
|
-
# a CT Acquisition Parameters container
|
|
1183
|
-
source_parameters_exists = False
|
|
1184
|
-
kvp_data_exists = False
|
|
1185
|
-
|
|
1186
|
-
for (
|
|
1187
|
-
container2b
|
|
1188
|
-
) in container.ContentSequence:
|
|
1189
|
-
if (
|
|
1190
|
-
container2b.ValueType
|
|
1191
|
-
== "CONTAINER"
|
|
1192
|
-
):
|
|
1193
|
-
if (
|
|
1194
|
-
container2b.ConceptNameCodeSequence[
|
|
1195
|
-
0
|
|
1196
|
-
].CodeMeaning
|
|
1197
|
-
== "CT Acquisition Parameters"
|
|
1198
|
-
):
|
|
1199
|
-
for (
|
|
1200
|
-
container3b
|
|
1201
|
-
) in container2b:
|
|
1202
|
-
for (
|
|
1203
|
-
container4b
|
|
1204
|
-
) in (
|
|
1205
|
-
container3b
|
|
1206
|
-
):
|
|
1207
|
-
try:
|
|
1208
|
-
if (
|
|
1209
|
-
container4b.ConceptNameCodeSequence[
|
|
1210
|
-
0
|
|
1211
|
-
].CodeMeaning
|
|
1212
|
-
== "CT X-Ray Source Parameters"
|
|
1213
|
-
):
|
|
1214
|
-
source_parameters_exists = True
|
|
1215
|
-
|
|
1216
|
-
for container5b in container4b:
|
|
1217
|
-
try:
|
|
1218
|
-
if (
|
|
1219
|
-
container5b[
|
|
1220
|
-
0
|
|
1221
|
-
]
|
|
1222
|
-
.ConceptNameCodeSequence[
|
|
1223
|
-
0
|
|
1224
|
-
]
|
|
1225
|
-
.CodeValue
|
|
1226
|
-
== "113733"
|
|
1227
|
-
):
|
|
1228
|
-
kvp_data_exists = True
|
|
1229
|
-
except AttributeError:
|
|
1230
|
-
pass
|
|
1231
|
-
except Exception:
|
|
1232
|
-
logger.debug(
|
|
1233
|
-
traceback.format_exc()
|
|
1234
|
-
)
|
|
1235
|
-
except AttributeError:
|
|
1236
|
-
# Likely there's no ConceptNameCodeSequence attribute
|
|
1237
|
-
pass
|
|
1238
|
-
except Exception:
|
|
1239
|
-
logger.debug(
|
|
1240
|
-
traceback.format_exc()
|
|
1241
|
-
)
|
|
1242
|
-
|
|
1243
|
-
if (
|
|
1244
|
-
not source_parameters_exists
|
|
1245
|
-
):
|
|
1246
|
-
# There is no x-ray source parameters section, so add it
|
|
1247
|
-
# Create the x-ray source container
|
|
1248
|
-
source_container = (
|
|
1249
|
-
Dataset()
|
|
1250
|
-
)
|
|
1251
|
-
source_container.RelationshipType = (
|
|
1252
|
-
"CONTAINS"
|
|
1253
|
-
)
|
|
1254
|
-
source_container.ValueType = (
|
|
1255
|
-
"CONTAINER"
|
|
1256
|
-
)
|
|
1257
|
-
coding = (
|
|
1258
|
-
Dataset()
|
|
1259
|
-
)
|
|
1260
|
-
coding.CodeValue = (
|
|
1261
|
-
"113831"
|
|
1262
|
-
)
|
|
1263
|
-
coding.CodingSchemeDesignator = (
|
|
1264
|
-
"DCM"
|
|
1265
|
-
)
|
|
1266
|
-
coding.CodeMeaning = "CT X-Ray Source Parameters"
|
|
1267
|
-
source_container.ConceptNameCodeSequence = Sequence(
|
|
1268
|
-
[coding]
|
|
1269
|
-
)
|
|
1270
|
-
|
|
1271
|
-
# Create the kVp container that will go in to the x-ray source container
|
|
1272
|
-
kvp_container = (
|
|
1273
|
-
Dataset()
|
|
1274
|
-
)
|
|
1275
|
-
kvp_container.RelationshipType = (
|
|
1276
|
-
"CONTAINS"
|
|
1277
|
-
)
|
|
1278
|
-
kvp_container.ValueType = (
|
|
1279
|
-
"NUM"
|
|
1280
|
-
)
|
|
1281
|
-
coding2 = (
|
|
1282
|
-
Dataset()
|
|
1283
|
-
)
|
|
1284
|
-
coding2.CodeValue = (
|
|
1285
|
-
"113733"
|
|
1286
|
-
)
|
|
1287
|
-
coding2.CodingSchemeDesignator = (
|
|
1288
|
-
"DCM"
|
|
1289
|
-
)
|
|
1290
|
-
coding2.CodeMeaning = (
|
|
1291
|
-
"KVP"
|
|
1292
|
-
)
|
|
1293
|
-
kvp_container.ConceptNameCodeSequence = Sequence(
|
|
1294
|
-
[coding2]
|
|
1295
|
-
)
|
|
1296
|
-
coding3 = (
|
|
1297
|
-
Dataset()
|
|
1298
|
-
)
|
|
1299
|
-
coding3.CodeValue = (
|
|
1300
|
-
"kV"
|
|
1301
|
-
)
|
|
1302
|
-
coding3.CodingSchemeDesignator = (
|
|
1303
|
-
"UCUM"
|
|
1304
|
-
)
|
|
1305
|
-
coding3.CodingSchemeVersion = (
|
|
1306
|
-
"1.4"
|
|
1307
|
-
)
|
|
1308
|
-
coding3.CodeMeaning = (
|
|
1309
|
-
"kV"
|
|
1310
|
-
)
|
|
1311
|
-
measurement_units_container = (
|
|
1312
|
-
Dataset()
|
|
1313
|
-
)
|
|
1314
|
-
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1315
|
-
[coding3]
|
|
1316
|
-
)
|
|
1317
|
-
measurement_units_container.NumericValue = (
|
|
1318
|
-
val
|
|
1319
|
-
)
|
|
1320
|
-
measured_value_sequence = Sequence(
|
|
1321
|
-
[
|
|
1322
|
-
measurement_units_container
|
|
1323
|
-
]
|
|
1324
|
-
)
|
|
1325
|
-
kvp_container.MeasuredValueSequence = measured_value_sequence
|
|
1326
|
-
|
|
1327
|
-
# Put the kVp container inside the x-ray source container
|
|
1328
|
-
source_container.ContentSequence = Sequence(
|
|
1329
|
-
[
|
|
1330
|
-
kvp_container
|
|
1331
|
-
]
|
|
1332
|
-
)
|
|
1333
|
-
|
|
1334
|
-
# Add the source_container to the rdsr contents
|
|
1335
|
-
try:
|
|
1336
|
-
# Append it to an existing ContentSequence
|
|
1337
|
-
container2b.ContentSequence.append(
|
|
1338
|
-
source_container
|
|
1339
|
-
)
|
|
1340
|
-
except
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
kvp_container
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
coding2
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
coding3
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
)
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
)
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
)
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
)
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
)
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
exposure_time_per_rotation_container.
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
)
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
measurement_units_container
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
"
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
""
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
for unique_study_time in unique_study_times:
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
content_sequence
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
)
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
item.
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
logger.debug(
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
logger.debug(
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
logger.debug("
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
logger.debug(
|
|
2094
|
-
"
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
1
|
+
# This Python file uses the following encoding: utf-8
|
|
2
|
+
# OpenREM - Radiation Exposure Monitoring tools for the physicist
|
|
3
|
+
# Copyright (C) 2017 The Royal Marsden NHS Foundation Trust
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# Additional permission under section 7 of GPLv3:
|
|
16
|
+
# You shall not make any use of the name of The Royal Marsden NHS
|
|
17
|
+
# Foundation trust in connection with this Program in any press or
|
|
18
|
+
# other public announcement without the prior written consent of
|
|
19
|
+
# The Royal Marsden NHS Foundation Trust.
|
|
20
|
+
#
|
|
21
|
+
# You should have received a copy of the GNU General Public License
|
|
22
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
.. module:: rdsr_toshiba_fct_from_dose_images.
|
|
26
|
+
:synopsis: Module to create a radiation dose structured report from legacy Toshiba CT studies
|
|
27
|
+
|
|
28
|
+
.. moduleauthor:: David Platten
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from builtins import str # pylint: disable=redefined-builtin
|
|
32
|
+
import sys
|
|
33
|
+
import os
|
|
34
|
+
import shutil
|
|
35
|
+
import logging
|
|
36
|
+
import traceback
|
|
37
|
+
|
|
38
|
+
import django
|
|
39
|
+
|
|
40
|
+
from .rdsr import rdsr
|
|
41
|
+
from openrem.openremproject.settings import (
|
|
42
|
+
JAVA_EXE,
|
|
43
|
+
JAVA_OPTIONS,
|
|
44
|
+
PIXELMED_JAR,
|
|
45
|
+
PIXELMED_JAR_OPTIONS,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# setup django/OpenREM
|
|
49
|
+
base_path = os.path.dirname(__file__)
|
|
50
|
+
project_path = os.path.abspath(os.path.join(base_path, "..", ".."))
|
|
51
|
+
if project_path not in sys.path:
|
|
52
|
+
sys.path.insert(1, project_path)
|
|
53
|
+
os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
|
|
54
|
+
django.setup()
|
|
55
|
+
|
|
56
|
+
# logger is explicitly named so that it is still handled when using __main__
|
|
57
|
+
logger = logging.getLogger("remapp.extractors.ct_toshiba")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _find_dose_summary_objects(folder_path):
|
|
61
|
+
"""This function looks for objects with a SOPClassUID of "Secondary
|
|
62
|
+
Capture Image Storage" and an ImageType with a length of 2.
|
|
63
|
+
|
|
64
|
+
Dose summary objects have a 2-element ImageType:
|
|
65
|
+
ImageType is ['DERIVED', 'SECONDARY']
|
|
66
|
+
Other secondary capture storage objects have a 3-element ImageType:
|
|
67
|
+
ImageType is ['DERIVED', 'SECONDARY', 'MULTI_FORMAT_RASTER']
|
|
68
|
+
ImageType is ['DERIVED', 'SECONDARY', 'DISPLAY']
|
|
69
|
+
ImageType is ['DERIVED', 'SECONDARY', 'MPR']
|
|
70
|
+
|
|
71
|
+
The above are from a virtual colonoscopy study.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
folder_path: a string containing the path to the folder containing the
|
|
75
|
+
DICOM objects
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
A list of structures, each containing the elements "fileName",
|
|
79
|
+
"studyTime" and "instanceNumber".
|
|
80
|
+
"""
|
|
81
|
+
import pydicom
|
|
82
|
+
from pydicom.filereader import InvalidDicomError
|
|
83
|
+
|
|
84
|
+
sop_class_uid = "Secondary Capture Image Storage"
|
|
85
|
+
dose_summary_object_list = []
|
|
86
|
+
|
|
87
|
+
for file_name in os.listdir(folder_path):
|
|
88
|
+
if os.path.isfile(os.path.join(folder_path, file_name)):
|
|
89
|
+
try:
|
|
90
|
+
dcm = pydicom.dcmread(os.path.join(folder_path, file_name))
|
|
91
|
+
if (
|
|
92
|
+
str(dcm.SOPClassUID) == str(sop_class_uid)
|
|
93
|
+
and len(dcm.ImageType) == 2
|
|
94
|
+
):
|
|
95
|
+
dose_summary_object_list.append(
|
|
96
|
+
{
|
|
97
|
+
"fileName": file_name,
|
|
98
|
+
"studyTime": dcm.StudyTime,
|
|
99
|
+
"instanceNumber": dcm.InstanceNumber,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
except InvalidDicomError as e:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"Invalid DICOM error: {0} when trying to read {1}".format(
|
|
106
|
+
e.message, os.path.join(folder_path, file_name)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
except Exception:
|
|
110
|
+
logger.debug(traceback.format_exc())
|
|
111
|
+
|
|
112
|
+
return dose_summary_object_list
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _copy_files_from_a_to_b(src_folder, dest_folder):
|
|
116
|
+
"""Copy files in src_folder to dest_folder"""
|
|
117
|
+
src_files = os.listdir(src_folder)
|
|
118
|
+
for file_name in src_files:
|
|
119
|
+
full_file_name = os.path.join(src_folder, file_name)
|
|
120
|
+
if os.path.isfile(full_file_name):
|
|
121
|
+
shutil.copy(full_file_name, dest_folder)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _split_by_studyinstanceuid(dicom_path):
|
|
125
|
+
"""Parse a folder of files, creating a sub-folder for each StudyInstanceUID found in any DICOM files. Each DICOM
|
|
126
|
+
file is copied into the folder that matches its StudyInstanceUID. The files are renamed to have integer names
|
|
127
|
+
starting with 0 and incrementing by 1 for each additional file copied.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
dicom_path (str): The full path to a folder containing DICOM objects.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
list: A list of paths that contain the DICOM objects corresponding to each StudyInstanceUID present in the
|
|
134
|
+
original dicom_path.
|
|
135
|
+
|
|
136
|
+
"""
|
|
137
|
+
import pydicom
|
|
138
|
+
|
|
139
|
+
from pydicom.filereader import InvalidDicomError
|
|
140
|
+
|
|
141
|
+
folder_list = []
|
|
142
|
+
file_counter = 0
|
|
143
|
+
|
|
144
|
+
for filename in os.listdir(dicom_path):
|
|
145
|
+
if os.path.isfile(os.path.join(dicom_path, filename)):
|
|
146
|
+
try:
|
|
147
|
+
dcm = pydicom.dcmread(os.path.join(dicom_path, filename))
|
|
148
|
+
|
|
149
|
+
subfolder_path = os.path.join(dicom_path, dcm.StudyInstanceUID)
|
|
150
|
+
if not os.path.isdir(subfolder_path):
|
|
151
|
+
os.mkdir(subfolder_path)
|
|
152
|
+
folder_list.append(subfolder_path)
|
|
153
|
+
|
|
154
|
+
shutil.copy2(
|
|
155
|
+
os.path.join(dicom_path, filename),
|
|
156
|
+
os.path.join(subfolder_path, str(file_counter)),
|
|
157
|
+
)
|
|
158
|
+
file_counter += 1
|
|
159
|
+
except InvalidDicomError as e:
|
|
160
|
+
logger.debug(
|
|
161
|
+
"Invalid DICOM error: {0} when trying to read {1}".format(
|
|
162
|
+
e.message, os.path.join(dicom_path, filename)
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
except Exception:
|
|
166
|
+
logger.debug(traceback.format_exc())
|
|
167
|
+
|
|
168
|
+
return folder_list
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _find_extra_info(dicom_path):
|
|
172
|
+
"""Parses a folder of files, obtaining DICOM tag information on each acquisition present.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
dicom_path (str): A string containing the full path to the folder.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
list: A two element list. The first element contains study-level information. The second element is a list of
|
|
179
|
+
dictionaries, one per acquisition found in the files. Each dictionary contains the following information about
|
|
180
|
+
each acquisition, if present:
|
|
181
|
+
|
|
182
|
+
AcquisitionNumber
|
|
183
|
+
ProtocolName
|
|
184
|
+
ExposureTime
|
|
185
|
+
SpiralPitchFactor
|
|
186
|
+
CTDIvol
|
|
187
|
+
ExposureModulationType
|
|
188
|
+
NominalTotalCollimationWidth
|
|
189
|
+
NominalSingleCollimationWidth
|
|
190
|
+
DeviceSerialNumber
|
|
191
|
+
StudyDescription
|
|
192
|
+
RequestedProcedureDescription
|
|
193
|
+
DLP
|
|
194
|
+
SoftwareVersions
|
|
195
|
+
DeviceSerialNumber
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
import pydicom
|
|
199
|
+
|
|
200
|
+
from pydicom.filereader import InvalidDicomError
|
|
201
|
+
|
|
202
|
+
from struct import unpack
|
|
203
|
+
|
|
204
|
+
logger.debug(
|
|
205
|
+
"Starting _find_extra_info routine for images in {0}".format(dicom_path)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
acquisition_info = []
|
|
209
|
+
acquisitions_collected = []
|
|
210
|
+
study_info = {}
|
|
211
|
+
|
|
212
|
+
for filename in os.listdir(dicom_path):
|
|
213
|
+
if os.path.isfile(os.path.join(dicom_path, filename)):
|
|
214
|
+
try:
|
|
215
|
+
dcm = pydicom.dcmread(os.path.join(dicom_path, filename))
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
# Only look at the tags if the combination of AcquisitionNumber and AcquisitionTime is new
|
|
219
|
+
acquisition_code = (
|
|
220
|
+
str(dcm.AcquisitionNumber) + "_" + dcm.AcquisitionTime
|
|
221
|
+
)
|
|
222
|
+
if acquisition_code not in acquisitions_collected:
|
|
223
|
+
logger.debug("acquisition_code is {0}".format(acquisition_code))
|
|
224
|
+
acquisitions_collected.append(acquisition_code)
|
|
225
|
+
|
|
226
|
+
info_dictionary = {}
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
info_dictionary["AcquisitionNumber"] = dcm.AcquisitionNumber
|
|
230
|
+
except AttributeError:
|
|
231
|
+
pass
|
|
232
|
+
except Exception:
|
|
233
|
+
logger.debug(traceback.format_exc())
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
info_dictionary["AcquisitionTime"] = dcm.AcquisitionTime
|
|
237
|
+
except AttributeError:
|
|
238
|
+
pass
|
|
239
|
+
except Exception:
|
|
240
|
+
logger.debug(traceback.format_exc())
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
info_dictionary["ProtocolName"] = dcm.ProtocolName
|
|
244
|
+
except AttributeError:
|
|
245
|
+
pass
|
|
246
|
+
except Exception:
|
|
247
|
+
logger.debug(traceback.format_exc())
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
info_dictionary["ExposureTime"] = dcm.ExposureTime
|
|
251
|
+
except AttributeError:
|
|
252
|
+
pass
|
|
253
|
+
except Exception:
|
|
254
|
+
logger.debug(traceback.format_exc())
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
info_dictionary["KVP"] = dcm.KVP
|
|
258
|
+
except AttributeError:
|
|
259
|
+
pass
|
|
260
|
+
except Exception:
|
|
261
|
+
logger.debug(traceback.format_exc())
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# For some Toshiba CT scanners there is information on the detector configuration
|
|
265
|
+
if dcm.Manufacturer.lower() == "toshiba":
|
|
266
|
+
info_dictionary["NominalSingleCollimationWidth"] = (
|
|
267
|
+
float(dcm[0x7005, 0x1008].value)
|
|
268
|
+
)
|
|
269
|
+
info_dictionary["NominalTotalCollimationWidth"] = dcm[
|
|
270
|
+
0x7005, 0x1009
|
|
271
|
+
].value.count("1") * float(dcm[0x7005, 0x1008].value)
|
|
272
|
+
except AttributeError:
|
|
273
|
+
pass
|
|
274
|
+
except Exception:
|
|
275
|
+
logger.debug(traceback.format_exc())
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
info_dictionary["SpiralPitchFactor"] = dcm.SpiralPitchFactor
|
|
279
|
+
except AttributeError:
|
|
280
|
+
try:
|
|
281
|
+
# For some Toshiba CT scanners, stored as a decimal string (DS)
|
|
282
|
+
info_dictionary["SpiralPitchFactor"] = dcm[
|
|
283
|
+
0x7005, 0x1023
|
|
284
|
+
].value
|
|
285
|
+
except KeyError:
|
|
286
|
+
pass
|
|
287
|
+
except Exception:
|
|
288
|
+
logger.debug(traceback.format_exc())
|
|
289
|
+
except Exception:
|
|
290
|
+
logger.debug(traceback.format_exc())
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# For some Toshiba CT scanners, stored as a floating point double (FD) by the
|
|
294
|
+
# scanner, but encoded by PACS as hex
|
|
295
|
+
if dcm[0x7005, 0x1063].VR == "FD":
|
|
296
|
+
info_dictionary["CTDIvol"] = dcm[0x7005, 0x1063].value
|
|
297
|
+
logger.debug(
|
|
298
|
+
"CTDIvol found in VR==FD format in dcm[0x7005,0x1063] ({0}).".format(
|
|
299
|
+
dcm[0x7005, 0x1063].value
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
else:
|
|
303
|
+
info_dictionary["CTDIvol"] = unpack(
|
|
304
|
+
"<d", "".join(dcm[0x7005, 0x1063])
|
|
305
|
+
)[0]
|
|
306
|
+
logger.debug(
|
|
307
|
+
"CTDIvol unpacked from dcm[0x7005,0x1063] ({0}).".format(
|
|
308
|
+
unpack("<d", "".join(dcm[0x7005, 0x1063]))[0]
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
except KeyError:
|
|
312
|
+
logger.debug(
|
|
313
|
+
"There was a key error when finding CTDIvol. Trying elsewhere."
|
|
314
|
+
)
|
|
315
|
+
try:
|
|
316
|
+
info_dictionary["CTDIvol"] = dcm.CTDIvol
|
|
317
|
+
logger.debug(
|
|
318
|
+
"CTDIvol found in dcm.CTDIvol ({0}).".format(
|
|
319
|
+
dcm.CTDIvol
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
except AttributeError:
|
|
323
|
+
pass
|
|
324
|
+
except Exception:
|
|
325
|
+
logger.debug(traceback.format_exc())
|
|
326
|
+
except TypeError:
|
|
327
|
+
logger.debug(
|
|
328
|
+
"There was a type error when finding CTDIvol. Trying elsewhere."
|
|
329
|
+
)
|
|
330
|
+
try:
|
|
331
|
+
info_dictionary["CTDIvol"] = dcm.CTDIvol
|
|
332
|
+
logger.debug(
|
|
333
|
+
"CTDIvol found in dcm.CTDIvol ({0}).".format(
|
|
334
|
+
dcm.CTDIvol
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
except AttributeError:
|
|
338
|
+
pass
|
|
339
|
+
except Exception:
|
|
340
|
+
logger.debug(traceback.format_exc())
|
|
341
|
+
except Exception:
|
|
342
|
+
logger.debug(traceback.format_exc())
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
info_dictionary["ExposureModulationType"] = (
|
|
346
|
+
dcm.ExposureModulationType
|
|
347
|
+
)
|
|
348
|
+
except AttributeError:
|
|
349
|
+
pass
|
|
350
|
+
except Exception:
|
|
351
|
+
logger.debug(traceback.format_exc())
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
# For some Toshiba CT scanners, stored as a floating point double (FD) by the
|
|
355
|
+
# scanner, but encoded by PACS as hex
|
|
356
|
+
if dcm[0x7005, 0x1040].VR == "FD":
|
|
357
|
+
info_dictionary["DLP"] = dcm[0x7005, 0x1040].value
|
|
358
|
+
logger.debug(
|
|
359
|
+
"DLP found in VR==FD format in dcm[0x7005,0x1040] ({0}).".format(
|
|
360
|
+
dcm[0x7005, 0x1040].value
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
info_dictionary["DLP"] = unpack(
|
|
365
|
+
"<d", "".join(dcm[0x7005, 0x1040])
|
|
366
|
+
)[0]
|
|
367
|
+
logger.debug(
|
|
368
|
+
"DLP unpacked from dcm[0x7005,0x1040] ({0}).".format(
|
|
369
|
+
unpack("<d", "".join(dcm[0x7005, 0x1040]))[0]
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
except KeyError:
|
|
373
|
+
logger.debug("There was a key error when finding DLP.")
|
|
374
|
+
except TypeError:
|
|
375
|
+
pass
|
|
376
|
+
except Exception:
|
|
377
|
+
logger.debug(traceback.format_exc())
|
|
378
|
+
|
|
379
|
+
acquisition_info.append(info_dictionary)
|
|
380
|
+
|
|
381
|
+
# Update the study-level information, whether this acquisition number has been seen yet or not
|
|
382
|
+
try:
|
|
383
|
+
if dcm.StudyDescription != "":
|
|
384
|
+
try:
|
|
385
|
+
if study_info["StudyDescription"] == "":
|
|
386
|
+
# Only update study_info['StudyDescription'] if it's empty
|
|
387
|
+
study_info["StudyDescription"] = (
|
|
388
|
+
dcm.StudyDescription
|
|
389
|
+
)
|
|
390
|
+
except KeyError:
|
|
391
|
+
# study_info['StudyDescription'] isn't present, so add it
|
|
392
|
+
study_info["StudyDescription"] = dcm.StudyDescription
|
|
393
|
+
except Exception:
|
|
394
|
+
logger.debug(traceback.format_exc())
|
|
395
|
+
except AttributeError:
|
|
396
|
+
# dcm.StudyDescription isn't present. Try looking at the CodeMeaning of
|
|
397
|
+
# ProcedureCodeSequence instead
|
|
398
|
+
try:
|
|
399
|
+
if dcm.ProcedureCodeSequence[0].CodeMeaning != "":
|
|
400
|
+
try:
|
|
401
|
+
if study_info["StudyDescription"] == "":
|
|
402
|
+
# Only update study_info['StudyDescription'] if it's empty
|
|
403
|
+
study_info["StudyDescription"] = (
|
|
404
|
+
dcm.ProcedureCodeSequence[0].CodeMeaning
|
|
405
|
+
)
|
|
406
|
+
except KeyError:
|
|
407
|
+
# study_info['StudyDescription'] isn't present, so add it
|
|
408
|
+
study_info["StudyDescription"] = (
|
|
409
|
+
dcm.ProcedureCodeSequence[0].CodeMeaning
|
|
410
|
+
)
|
|
411
|
+
except Exception:
|
|
412
|
+
logger.debug(traceback.format_exc())
|
|
413
|
+
except AttributeError:
|
|
414
|
+
# dcm.ProcedureCodeSequence[0].CodeMeaning isn't present either
|
|
415
|
+
pass
|
|
416
|
+
except Exception:
|
|
417
|
+
logger.debug(traceback.format_exc())
|
|
418
|
+
except Exception:
|
|
419
|
+
logger.debug(traceback.format_exc())
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
if dcm.RequestedProcedureDescription != "":
|
|
423
|
+
try:
|
|
424
|
+
if study_info["RequestedProcedureDescription"] == "":
|
|
425
|
+
# Only update study_info['RequestedProcedureDescription'] if it's empty
|
|
426
|
+
study_info["RequestedProcedureDescription"] = (
|
|
427
|
+
dcm.RequestedProcedureDescription
|
|
428
|
+
)
|
|
429
|
+
except KeyError:
|
|
430
|
+
# study_info['RequestedProcedureDescription'] doesn't exist yet, so create it
|
|
431
|
+
study_info["RequestedProcedureDescription"] = (
|
|
432
|
+
dcm.RequestedProcedureDescription
|
|
433
|
+
)
|
|
434
|
+
except Exception:
|
|
435
|
+
logger.debug(traceback.format_exc())
|
|
436
|
+
except AttributeError:
|
|
437
|
+
# dcm.RequestedProcedureDescription isn't present. Try looking at the
|
|
438
|
+
# RequestedProcedureDescription of RequestAttributesSequence instead
|
|
439
|
+
try:
|
|
440
|
+
if dcm.ProcedureCodeSequence[0].CodeMeaning != "":
|
|
441
|
+
try:
|
|
442
|
+
if (
|
|
443
|
+
study_info["RequestedProcedureDescription"]
|
|
444
|
+
== ""
|
|
445
|
+
):
|
|
446
|
+
# Only update study_info['RequestedProcedureDescription'] if it's empty
|
|
447
|
+
study_info["RequestedProcedureDescription"] = (
|
|
448
|
+
dcm.ProcedureCodeSequence[0].CodeMeaning
|
|
449
|
+
)
|
|
450
|
+
except KeyError:
|
|
451
|
+
# study_info['RequestedProcedureDescription'] isn't present, so add it
|
|
452
|
+
study_info["RequestedProcedureDescription"] = (
|
|
453
|
+
dcm.ProcedureCodeSequence[0].CodeMeaning
|
|
454
|
+
)
|
|
455
|
+
except Exception:
|
|
456
|
+
logger.debug(traceback.format_exc())
|
|
457
|
+
except AttributeError:
|
|
458
|
+
# dcm.ProcedureCodeSequence[0].CodeMeaning isn't present either
|
|
459
|
+
pass
|
|
460
|
+
except Exception:
|
|
461
|
+
logger.debug(traceback.format_exc())
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
if dcm.SoftwareVersions != "":
|
|
465
|
+
try:
|
|
466
|
+
if study_info["SoftwareVersions"] == "":
|
|
467
|
+
# Only update study_info['SoftwareVersions'] if it's empty
|
|
468
|
+
study_info["SoftwareVersions"] = (
|
|
469
|
+
dcm.SoftwareVersions
|
|
470
|
+
)
|
|
471
|
+
except KeyError:
|
|
472
|
+
# study_info['SoftwareVersions'] doesn't exist yet, so create it
|
|
473
|
+
study_info["SoftwareVersions"] = dcm.SoftwareVersions
|
|
474
|
+
except Exception:
|
|
475
|
+
logger.debug(traceback.format_exc())
|
|
476
|
+
except AttributeError:
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
if dcm.DeviceSerialNumber != "":
|
|
481
|
+
try:
|
|
482
|
+
if study_info["DeviceSerialNumber"] == "":
|
|
483
|
+
# Only update study_info['DeviceSerialNumber'] if it's empty
|
|
484
|
+
study_info["SoftwareVersions"] = (
|
|
485
|
+
dcm.DeviceSerialNumber
|
|
486
|
+
)
|
|
487
|
+
except KeyError:
|
|
488
|
+
# study_info['DeviceSerialNumber'] doesn't exist yet, so create it
|
|
489
|
+
study_info["DeviceSerialNumber"] = (
|
|
490
|
+
dcm.DeviceSerialNumber
|
|
491
|
+
)
|
|
492
|
+
except Exception:
|
|
493
|
+
logger.debug(traceback.format_exc())
|
|
494
|
+
except AttributeError:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
except AttributeError:
|
|
498
|
+
pass
|
|
499
|
+
except Exception:
|
|
500
|
+
logger.debug(traceback.format_exc())
|
|
501
|
+
|
|
502
|
+
except InvalidDicomError:
|
|
503
|
+
pass
|
|
504
|
+
except Exception:
|
|
505
|
+
logger.debug(traceback.format_exc())
|
|
506
|
+
|
|
507
|
+
logger.debug(
|
|
508
|
+
"Reached the end of _find_extra_info for images in {0}".format(dicom_path)
|
|
509
|
+
)
|
|
510
|
+
logger.debug("study_info is: {0}".format(study_info))
|
|
511
|
+
logger.debug("acquisition_info is: {0}".format(acquisition_info))
|
|
512
|
+
return [study_info, acquisition_info]
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _make_explicit_vr_little_endian(folder, dcmconv_exe):
|
|
516
|
+
"""Parse folder of files, making each DICOM file explicit VR little endian using the DICOM toolkit dcmconv.exe
|
|
517
|
+
command. See http://support.dcmtk.org/docs/dcmconv.html for documentation.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
folder (string): Full path containing DICOM objects.
|
|
521
|
+
dcmconv_exe (str): A string containing the dcmconv command
|
|
522
|
+
|
|
523
|
+
"""
|
|
524
|
+
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
525
|
+
import subprocess # nosec
|
|
526
|
+
|
|
527
|
+
for filename in os.listdir(folder):
|
|
528
|
+
command = (
|
|
529
|
+
dcmconv_exe
|
|
530
|
+
+ " +te "
|
|
531
|
+
+ os.path.join(folder, filename)
|
|
532
|
+
+ " "
|
|
533
|
+
+ os.path.join(folder, filename)
|
|
534
|
+
)
|
|
535
|
+
subprocess.call(command.split()) # nosec
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _make_dicomdir(folder, dcmmkdir_exe):
|
|
539
|
+
"""Parse folder of files, making a DICOMDIR for it using the DICOM toolkit dcmmkdir.exe command. See
|
|
540
|
+
http://support.dcmtk.org/docs/dcmmkdir.html for documentation.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
folder (string): Full path containing DICOM objects.
|
|
544
|
+
dcmmkdir_exe (str): A string containing the dcmmkdir command
|
|
545
|
+
|
|
546
|
+
"""
|
|
547
|
+
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
548
|
+
import subprocess # nosec
|
|
549
|
+
|
|
550
|
+
command = (
|
|
551
|
+
dcmmkdir_exe
|
|
552
|
+
+ " --recurse --output-file "
|
|
553
|
+
+ os.path.join(folder, "DICOMDIR")
|
|
554
|
+
+ " --input-directory "
|
|
555
|
+
+ folder
|
|
556
|
+
)
|
|
557
|
+
subprocess.call(command.split()) # nosec
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _make_dicom_rdsr(folder, pixelmed_jar_command, sr_filename):
|
|
561
|
+
"""Parse folder of files, making a DICOM RDSR using pixelmed.jar.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
folder (string): Full path containing DICOM objects.
|
|
565
|
+
pixelmed_jar_command (str): A string containing the pixelmed_jar command and options.
|
|
566
|
+
sr_filename (str): A string containing the filename to use when creating the rdsr.
|
|
567
|
+
|
|
568
|
+
"""
|
|
569
|
+
# Security implications of using subprocess have been considered - it is necessary for this function to work.
|
|
570
|
+
import subprocess # nosec
|
|
571
|
+
|
|
572
|
+
command = (
|
|
573
|
+
pixelmed_jar_command + " " + folder + " " + os.path.join(folder, sr_filename)
|
|
574
|
+
)
|
|
575
|
+
subprocess.call(command.split()) # nosec
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _update_dicom_rdsr(
|
|
579
|
+
rdsr_file, additional_study_info, additional_acquisition_info, new_rdsr_file
|
|
580
|
+
):
|
|
581
|
+
"""Try to update information in an RDSR file using pydicom. Match the pair of CTDIvol and DLP values found in the
|
|
582
|
+
RDSR CT Acquisition with a pair of CTDIvol and DLP values in additional_info. If a match is found, use the other
|
|
583
|
+
information in the additional_info element to update the corresponding CT Acquisition in the RDSR.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
rdsr_file (str): A fully qualified RDSR filename.
|
|
587
|
+
additional_study_info (list): A list of dictionaries containing information on the CT study.
|
|
588
|
+
additional_acquisition_info (list): A list of dictionaries containing information on each acquisition in the CT
|
|
589
|
+
study.
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
integer (int): 1 on success; 0 if the rdsr_file could not be read.
|
|
593
|
+
|
|
594
|
+
"""
|
|
595
|
+
import pydicom
|
|
596
|
+
from pydicom.dataset import Dataset
|
|
597
|
+
from pydicom.sequence import Sequence
|
|
598
|
+
|
|
599
|
+
logger.debug("Trying to open initial RDSR file")
|
|
600
|
+
try:
|
|
601
|
+
dcm = pydicom.dcmread(rdsr_file)
|
|
602
|
+
logger.debug("RDSR file opened: {0}".format(rdsr_file))
|
|
603
|
+
except IOError as e:
|
|
604
|
+
logger.debug(
|
|
605
|
+
"I/O error({0}): {1} when trying to read {2}".format(
|
|
606
|
+
e.errno, e.strerror, rdsr_file
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
return 0
|
|
610
|
+
except Exception:
|
|
611
|
+
logger.debug(traceback.format_exc())
|
|
612
|
+
|
|
613
|
+
# Update the study-level information if it does not exist, or is an empty string. Over-write DeviceSerialNumber
|
|
614
|
+
# even if it is already present (Dose Utility generates a unique DeviceSerialNumber, but I'd prefer to use the
|
|
615
|
+
# real one)
|
|
616
|
+
logger.debug("Updating study-level data")
|
|
617
|
+
for key, val in list(additional_study_info.items()):
|
|
618
|
+
try:
|
|
619
|
+
rdsr_val = getattr(dcm, key)
|
|
620
|
+
logger.debug("{0}: {1}".format(key, val))
|
|
621
|
+
if rdsr_val == "":
|
|
622
|
+
setattr(dcm, key, val)
|
|
623
|
+
if key == "DeviceSerialNumber":
|
|
624
|
+
setattr(dcm, key, val)
|
|
625
|
+
except AttributeError:
|
|
626
|
+
setattr(dcm, key, val)
|
|
627
|
+
except Exception:
|
|
628
|
+
logger.debug(traceback.format_exc())
|
|
629
|
+
logger.debug("Study level data updated")
|
|
630
|
+
|
|
631
|
+
# Now go through each CT Aquisition container in the rdsr file and see if any of the information should be updated.
|
|
632
|
+
logger.debug("Updating acquisition data")
|
|
633
|
+
for container in dcm.ContentSequence:
|
|
634
|
+
if container.ValueType == "CONTAINER":
|
|
635
|
+
if container.ConceptNameCodeSequence[0].CodeMeaning == "CT Acquisition":
|
|
636
|
+
for container2 in container.ContentSequence:
|
|
637
|
+
# The Acquisition protocol would go in at this level I think
|
|
638
|
+
if container2.ValueType == "CONTAINER":
|
|
639
|
+
if (
|
|
640
|
+
container2.ConceptNameCodeSequence[0].CodeMeaning
|
|
641
|
+
== "CT Dose"
|
|
642
|
+
):
|
|
643
|
+
logger.debug("Found CT Dose container")
|
|
644
|
+
current_dlp = "0"
|
|
645
|
+
current_ctdi_vol = "0"
|
|
646
|
+
for container3 in container2.ContentSequence:
|
|
647
|
+
if (
|
|
648
|
+
container3.ConceptNameCodeSequence[0].CodeMeaning
|
|
649
|
+
== "DLP"
|
|
650
|
+
):
|
|
651
|
+
current_dlp = container3.MeasuredValueSequence[
|
|
652
|
+
0
|
|
653
|
+
].NumericValue
|
|
654
|
+
logger.debug(
|
|
655
|
+
"Found DLP value: {0}".format(current_dlp)
|
|
656
|
+
)
|
|
657
|
+
if (
|
|
658
|
+
container3.ConceptNameCodeSequence[0].CodeMeaning
|
|
659
|
+
== "Mean CTDIvol"
|
|
660
|
+
):
|
|
661
|
+
current_ctdi_vol = container3.MeasuredValueSequence[
|
|
662
|
+
0
|
|
663
|
+
].NumericValue
|
|
664
|
+
logger.debug(
|
|
665
|
+
"Found CTDIvol value: {0}".format(
|
|
666
|
+
current_ctdi_vol
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Check to see if the current DLP and CTDIvol pair matches any of the acquisitions in
|
|
671
|
+
# additional_info
|
|
672
|
+
for acquisition in additional_acquisition_info:
|
|
673
|
+
try:
|
|
674
|
+
logger.debug(
|
|
675
|
+
"Trying to match DLP and CTDIvol. Current values: {0}, {1}".format(
|
|
676
|
+
float(acquisition["DLP"]),
|
|
677
|
+
float(acquisition["CTDIvol"]),
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
if float(acquisition["CTDIvol"]) == float(
|
|
681
|
+
current_ctdi_vol
|
|
682
|
+
) and float(acquisition["DLP"]) == float(
|
|
683
|
+
current_dlp
|
|
684
|
+
):
|
|
685
|
+
logger.debug("DLP and CTDIvol match")
|
|
686
|
+
# There's a match between CTDIvol and DLP, so see if things can be updated or added.
|
|
687
|
+
try:
|
|
688
|
+
for key, val in list(acquisition.items()):
|
|
689
|
+
if key != "CTDIvol" and key != "DLP":
|
|
690
|
+
logger.debug(
|
|
691
|
+
"{0} -> {1}".format(
|
|
692
|
+
key, str(val)
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
##############################################
|
|
696
|
+
# Code here to add / update the data...
|
|
697
|
+
coding = Dataset()
|
|
698
|
+
coding2 = Dataset()
|
|
699
|
+
if key == "ProtocolName":
|
|
700
|
+
# First, check if there is already a ProtocolName container that has a protocol in it.
|
|
701
|
+
data_exists = False
|
|
702
|
+
for (
|
|
703
|
+
container2b
|
|
704
|
+
) in container.ContentSequence:
|
|
705
|
+
for (
|
|
706
|
+
container3b
|
|
707
|
+
) in container2b:
|
|
708
|
+
try:
|
|
709
|
+
if (
|
|
710
|
+
container3b[
|
|
711
|
+
0
|
|
712
|
+
].CodeValue
|
|
713
|
+
== "125203"
|
|
714
|
+
):
|
|
715
|
+
data_exists = (
|
|
716
|
+
True
|
|
717
|
+
)
|
|
718
|
+
if (
|
|
719
|
+
container2b.TextValue
|
|
720
|
+
== ""
|
|
721
|
+
):
|
|
722
|
+
# Update the protocol if it is blank
|
|
723
|
+
container2b.TextValue = (
|
|
724
|
+
val
|
|
725
|
+
)
|
|
726
|
+
except AttributeError:
|
|
727
|
+
pass
|
|
728
|
+
except Exception:
|
|
729
|
+
logger.debug(
|
|
730
|
+
traceback.format_exc()
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
if not data_exists:
|
|
734
|
+
# If there is no protocol then add it
|
|
735
|
+
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
736
|
+
# (0040, a040) Value Type CS: 'TEXT'
|
|
737
|
+
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
738
|
+
# (0008, 0100) Code Value SH: '125203'
|
|
739
|
+
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
740
|
+
# (0008, 0104) Code Meaning LO: 'Acquisition Protocol'
|
|
741
|
+
# ---------
|
|
742
|
+
# (0040, a160) Text Value UT: 'TAP'
|
|
743
|
+
# Create the inner coding bit
|
|
744
|
+
coding.CodeValue = "125203"
|
|
745
|
+
coding.CodingSchemeDesignator = (
|
|
746
|
+
"DCM"
|
|
747
|
+
)
|
|
748
|
+
coding.CodeMeaning = (
|
|
749
|
+
"Acquisition Protocol"
|
|
750
|
+
)
|
|
751
|
+
# Create the outer container bit, including the protocol name
|
|
752
|
+
prot_container = Dataset()
|
|
753
|
+
prot_container.RelationshipType = (
|
|
754
|
+
"CONTAINS"
|
|
755
|
+
)
|
|
756
|
+
prot_container.ValueType = (
|
|
757
|
+
"TEXT"
|
|
758
|
+
)
|
|
759
|
+
prot_container.TextValue = (
|
|
760
|
+
val
|
|
761
|
+
)
|
|
762
|
+
# Add the coding sequence into the container.
|
|
763
|
+
# Sequences are lists.
|
|
764
|
+
prot_container.ConceptNameCodeSequence = Sequence(
|
|
765
|
+
[coding]
|
|
766
|
+
)
|
|
767
|
+
container.ContentSequence.append(
|
|
768
|
+
prot_container
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
if key == "SpiralPitchFactor":
|
|
772
|
+
# First, check if there is already a SpiralPitchFactor container that has a value in it.
|
|
773
|
+
data_exists = False
|
|
774
|
+
|
|
775
|
+
for (
|
|
776
|
+
container2b
|
|
777
|
+
) in container.ContentSequence:
|
|
778
|
+
if (
|
|
779
|
+
container2b.ValueType
|
|
780
|
+
== "CONTAINER"
|
|
781
|
+
):
|
|
782
|
+
if (
|
|
783
|
+
container2b.ConceptNameCodeSequence[
|
|
784
|
+
0
|
|
785
|
+
].CodeMeaning
|
|
786
|
+
== "CT Acquisition Parameters"
|
|
787
|
+
):
|
|
788
|
+
for (
|
|
789
|
+
container3b
|
|
790
|
+
) in container2b:
|
|
791
|
+
for (
|
|
792
|
+
container4b
|
|
793
|
+
) in (
|
|
794
|
+
container3b
|
|
795
|
+
):
|
|
796
|
+
try:
|
|
797
|
+
if (
|
|
798
|
+
container4b.ConceptNameCodeSequence[
|
|
799
|
+
0
|
|
800
|
+
].CodeValue
|
|
801
|
+
== "113828"
|
|
802
|
+
):
|
|
803
|
+
data_exists = True
|
|
804
|
+
except AttributeError:
|
|
805
|
+
pass
|
|
806
|
+
except Exception:
|
|
807
|
+
logger.debug(
|
|
808
|
+
traceback.format_exc()
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
if not data_exists:
|
|
812
|
+
# If there is no pitch then add it
|
|
813
|
+
# ---------
|
|
814
|
+
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
815
|
+
# (0040, a040) Value Type CS: 'NUM'
|
|
816
|
+
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
817
|
+
# (0008, 0100) Code Value SH: '113828'
|
|
818
|
+
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
819
|
+
# (0008, 0104) Code Meaning LO: 'Pitch Factor'
|
|
820
|
+
# ---------
|
|
821
|
+
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
822
|
+
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
823
|
+
# (0008, 0100) Code Value SH: '{ratio}'
|
|
824
|
+
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
825
|
+
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
826
|
+
# (0008, 0104) Code Meaning LO: 'ratio'
|
|
827
|
+
# ---------
|
|
828
|
+
# (0040, a30a) Numeric Value DS: '0.6'
|
|
829
|
+
# ---------
|
|
830
|
+
# ---------
|
|
831
|
+
# Create the first inner coding bit
|
|
832
|
+
coding.CodeValue = (
|
|
833
|
+
"113828"
|
|
834
|
+
)
|
|
835
|
+
coding.CodingSchemeDesignator = (
|
|
836
|
+
"DCM"
|
|
837
|
+
)
|
|
838
|
+
coding.CodeMeaning = "Pitch Factor"
|
|
839
|
+
# Create the second inner coding bit
|
|
840
|
+
coding2.CodeValue = (
|
|
841
|
+
"{ratio}"
|
|
842
|
+
)
|
|
843
|
+
coding2.CodingSchemeDesignator = (
|
|
844
|
+
"UCUM"
|
|
845
|
+
)
|
|
846
|
+
coding2.CodingSchemeVersion = (
|
|
847
|
+
"1.4"
|
|
848
|
+
)
|
|
849
|
+
coding2.CodeMeaning = (
|
|
850
|
+
"ratio"
|
|
851
|
+
)
|
|
852
|
+
measurement_units_container = (
|
|
853
|
+
Dataset()
|
|
854
|
+
)
|
|
855
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
856
|
+
[coding2]
|
|
857
|
+
)
|
|
858
|
+
measurement_units_container.NumericValue = (
|
|
859
|
+
val
|
|
860
|
+
)
|
|
861
|
+
measured_value_sequence = Sequence(
|
|
862
|
+
[
|
|
863
|
+
measurement_units_container
|
|
864
|
+
]
|
|
865
|
+
)
|
|
866
|
+
# Create the outer container bit
|
|
867
|
+
pitch_container = (
|
|
868
|
+
Dataset()
|
|
869
|
+
)
|
|
870
|
+
pitch_container.RelationshipType = (
|
|
871
|
+
"CONTAINS"
|
|
872
|
+
)
|
|
873
|
+
pitch_container.ValueType = (
|
|
874
|
+
"NUM"
|
|
875
|
+
)
|
|
876
|
+
# Add the coding sequence into the container.
|
|
877
|
+
# Sequences are lists.
|
|
878
|
+
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
879
|
+
[coding]
|
|
880
|
+
)
|
|
881
|
+
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
882
|
+
container2b.ContentSequence.append(
|
|
883
|
+
pitch_container
|
|
884
|
+
)
|
|
885
|
+
|
|
886
|
+
if (
|
|
887
|
+
key
|
|
888
|
+
== "NominalSingleCollimationWidth"
|
|
889
|
+
):
|
|
890
|
+
# First, check if there is already a NominalSingleCollimationWidth container that has a value in it.
|
|
891
|
+
data_exists = False
|
|
892
|
+
|
|
893
|
+
for (
|
|
894
|
+
container2b
|
|
895
|
+
) in container.ContentSequence:
|
|
896
|
+
if (
|
|
897
|
+
container2b.ValueType
|
|
898
|
+
== "CONTAINER"
|
|
899
|
+
):
|
|
900
|
+
if (
|
|
901
|
+
container2b.ConceptNameCodeSequence[
|
|
902
|
+
0
|
|
903
|
+
].CodeMeaning
|
|
904
|
+
== "CT Acquisition Parameters"
|
|
905
|
+
):
|
|
906
|
+
for (
|
|
907
|
+
container3b
|
|
908
|
+
) in container2b:
|
|
909
|
+
for (
|
|
910
|
+
container4b
|
|
911
|
+
) in (
|
|
912
|
+
container3b
|
|
913
|
+
):
|
|
914
|
+
try:
|
|
915
|
+
if (
|
|
916
|
+
container4b.ConceptNameCodeSequence[
|
|
917
|
+
0
|
|
918
|
+
].CodeValue
|
|
919
|
+
== "113826"
|
|
920
|
+
):
|
|
921
|
+
data_exists = True
|
|
922
|
+
except AttributeError:
|
|
923
|
+
pass
|
|
924
|
+
if not data_exists:
|
|
925
|
+
# If there is no NominalSingleCollimationWidth then add it
|
|
926
|
+
# ---------
|
|
927
|
+
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
928
|
+
# (0040, a040) Value Type CS: 'NUM'
|
|
929
|
+
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
930
|
+
# (0008, 0100) Code Value SH: '113826'
|
|
931
|
+
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
932
|
+
# (0008, 0104) Code Meaning LO: 'Nominal Single Collimation Width'
|
|
933
|
+
# ---------
|
|
934
|
+
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
935
|
+
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
936
|
+
# (0008, 0100) Code Value SH: 'mm'
|
|
937
|
+
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
938
|
+
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
939
|
+
# (0008, 0104) Code Meaning LO: 'mm'
|
|
940
|
+
# ---------
|
|
941
|
+
# (0040, a30a) Numeric Value DS: '0.6'
|
|
942
|
+
# ---------
|
|
943
|
+
# ---------
|
|
944
|
+
# Create the first inner coding bit
|
|
945
|
+
coding.CodeValue = (
|
|
946
|
+
"113826"
|
|
947
|
+
)
|
|
948
|
+
coding.CodingSchemeDesignator = (
|
|
949
|
+
"DCM"
|
|
950
|
+
)
|
|
951
|
+
coding.CodeMeaning = "Nominal Single Collimation Width"
|
|
952
|
+
# Create the second inner coding bit
|
|
953
|
+
coding2.CodeValue = (
|
|
954
|
+
"mm"
|
|
955
|
+
)
|
|
956
|
+
coding2.CodingSchemeDesignator = (
|
|
957
|
+
"UCUM"
|
|
958
|
+
)
|
|
959
|
+
coding2.CodingSchemeVersion = (
|
|
960
|
+
"1.4"
|
|
961
|
+
)
|
|
962
|
+
coding2.CodeMeaning = (
|
|
963
|
+
"mm"
|
|
964
|
+
)
|
|
965
|
+
measurement_units_container = (
|
|
966
|
+
Dataset()
|
|
967
|
+
)
|
|
968
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
969
|
+
[coding2]
|
|
970
|
+
)
|
|
971
|
+
measurement_units_container.NumericValue = (
|
|
972
|
+
val
|
|
973
|
+
)
|
|
974
|
+
measured_value_sequence = Sequence(
|
|
975
|
+
[
|
|
976
|
+
measurement_units_container
|
|
977
|
+
]
|
|
978
|
+
)
|
|
979
|
+
# Create the outer container bit
|
|
980
|
+
pitch_container = (
|
|
981
|
+
Dataset()
|
|
982
|
+
)
|
|
983
|
+
pitch_container.RelationshipType = (
|
|
984
|
+
"CONTAINS"
|
|
985
|
+
)
|
|
986
|
+
pitch_container.ValueType = (
|
|
987
|
+
"NUM"
|
|
988
|
+
)
|
|
989
|
+
# Add the coding sequence into the container.
|
|
990
|
+
# Sequences are lists.
|
|
991
|
+
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
992
|
+
[coding]
|
|
993
|
+
)
|
|
994
|
+
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
995
|
+
container2b.ContentSequence.append(
|
|
996
|
+
pitch_container
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
if (
|
|
1000
|
+
key
|
|
1001
|
+
== "NominalTotalCollimationWidth"
|
|
1002
|
+
):
|
|
1003
|
+
# First, check if there is already a NominalSingleCollimationWidth container that has a value in it.
|
|
1004
|
+
data_exists = False
|
|
1005
|
+
|
|
1006
|
+
for (
|
|
1007
|
+
container2b
|
|
1008
|
+
) in container.ContentSequence:
|
|
1009
|
+
if (
|
|
1010
|
+
container2b.ValueType
|
|
1011
|
+
== "CONTAINER"
|
|
1012
|
+
):
|
|
1013
|
+
if (
|
|
1014
|
+
container2b.ConceptNameCodeSequence[
|
|
1015
|
+
0
|
|
1016
|
+
].CodeMeaning
|
|
1017
|
+
== "CT Acquisition Parameters"
|
|
1018
|
+
):
|
|
1019
|
+
for (
|
|
1020
|
+
container3b
|
|
1021
|
+
) in container2b:
|
|
1022
|
+
for (
|
|
1023
|
+
container4b
|
|
1024
|
+
) in (
|
|
1025
|
+
container3b
|
|
1026
|
+
):
|
|
1027
|
+
try:
|
|
1028
|
+
if (
|
|
1029
|
+
container4b.ConceptNameCodeSequence[
|
|
1030
|
+
0
|
|
1031
|
+
].CodeValue
|
|
1032
|
+
== "113827"
|
|
1033
|
+
):
|
|
1034
|
+
data_exists = True
|
|
1035
|
+
except AttributeError:
|
|
1036
|
+
pass
|
|
1037
|
+
except Exception:
|
|
1038
|
+
logger.debug(
|
|
1039
|
+
traceback.format_exc()
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
if not data_exists:
|
|
1043
|
+
# If there is no NominalSingleCollimationWidth then add it
|
|
1044
|
+
# ---------
|
|
1045
|
+
# (0040, a010) Relationship Type CS: 'CONTAINS'
|
|
1046
|
+
# (0040, a040) Value Type CS: 'NUM'
|
|
1047
|
+
# (0040, a043) Concept Name Code Sequence 1 item(s) ----
|
|
1048
|
+
# (0008, 0100) Code Value SH: '113827'
|
|
1049
|
+
# (0008, 0102) Coding Scheme Designator SH: 'DCM'
|
|
1050
|
+
# (0008, 0104) Code Meaning LO: 'Nominal Total Collimation Width'
|
|
1051
|
+
# ---------
|
|
1052
|
+
# (0040, a300) Measured Value Sequence 1 item(s) ----
|
|
1053
|
+
# (0040, 08ea) Measurement Units Code Sequence 1 item(s) ----
|
|
1054
|
+
# (0008, 0100) Code Value SH: 'mm'
|
|
1055
|
+
# (0008, 0102) Coding Scheme Designator SH: 'UCUM'
|
|
1056
|
+
# (0008, 0103) Coding Scheme Version SH: '1.4'
|
|
1057
|
+
# (0008, 0104) Code Meaning LO: 'mm'
|
|
1058
|
+
# ---------
|
|
1059
|
+
# (0040, a30a) Numeric Value DS: '38.4'
|
|
1060
|
+
# ---------
|
|
1061
|
+
# ---------
|
|
1062
|
+
# Create the first inner coding bit
|
|
1063
|
+
coding.CodeValue = (
|
|
1064
|
+
"113827"
|
|
1065
|
+
)
|
|
1066
|
+
coding.CodingSchemeDesignator = (
|
|
1067
|
+
"DCM"
|
|
1068
|
+
)
|
|
1069
|
+
coding.CodeMeaning = "Nominal Total Collimation Width"
|
|
1070
|
+
# Create the second inner coding bit
|
|
1071
|
+
coding2.CodeValue = (
|
|
1072
|
+
"mm"
|
|
1073
|
+
)
|
|
1074
|
+
coding2.CodingSchemeDesignator = (
|
|
1075
|
+
"UCUM"
|
|
1076
|
+
)
|
|
1077
|
+
coding2.CodingSchemeVersion = (
|
|
1078
|
+
"1.4"
|
|
1079
|
+
)
|
|
1080
|
+
coding2.CodeMeaning = (
|
|
1081
|
+
"mm"
|
|
1082
|
+
)
|
|
1083
|
+
measurement_units_container = (
|
|
1084
|
+
Dataset()
|
|
1085
|
+
)
|
|
1086
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1087
|
+
[coding2]
|
|
1088
|
+
)
|
|
1089
|
+
measurement_units_container.NumericValue = (
|
|
1090
|
+
val
|
|
1091
|
+
)
|
|
1092
|
+
measured_value_sequence = Sequence(
|
|
1093
|
+
[
|
|
1094
|
+
measurement_units_container
|
|
1095
|
+
]
|
|
1096
|
+
)
|
|
1097
|
+
# Create the outer container bit
|
|
1098
|
+
pitch_container = (
|
|
1099
|
+
Dataset()
|
|
1100
|
+
)
|
|
1101
|
+
pitch_container.RelationshipType = (
|
|
1102
|
+
"CONTAINS"
|
|
1103
|
+
)
|
|
1104
|
+
pitch_container.ValueType = (
|
|
1105
|
+
"NUM"
|
|
1106
|
+
)
|
|
1107
|
+
# Add the coding sequence into the container.
|
|
1108
|
+
# Sequences are lists.
|
|
1109
|
+
pitch_container.ConceptNameCodeSequence = Sequence(
|
|
1110
|
+
[coding]
|
|
1111
|
+
)
|
|
1112
|
+
pitch_container.MeasuredValueSequence = measured_value_sequence
|
|
1113
|
+
container2b.ContentSequence.append(
|
|
1114
|
+
pitch_container
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
if key == "ExposureModulationType":
|
|
1118
|
+
# First, check if there is already an ExposureModulationType container that has a value in it.
|
|
1119
|
+
data_exists = False
|
|
1120
|
+
for (
|
|
1121
|
+
container2b
|
|
1122
|
+
) in container.ContentSequence:
|
|
1123
|
+
for (
|
|
1124
|
+
container3b
|
|
1125
|
+
) in container2b:
|
|
1126
|
+
try:
|
|
1127
|
+
if (
|
|
1128
|
+
container3b[
|
|
1129
|
+
0
|
|
1130
|
+
].CodeValue
|
|
1131
|
+
== "113842"
|
|
1132
|
+
):
|
|
1133
|
+
data_exists = (
|
|
1134
|
+
True
|
|
1135
|
+
)
|
|
1136
|
+
if (
|
|
1137
|
+
container2b.TextValue
|
|
1138
|
+
== ""
|
|
1139
|
+
):
|
|
1140
|
+
# Update the X-Ray Modulation Type if it is blank
|
|
1141
|
+
container2b.TextValue = (
|
|
1142
|
+
val
|
|
1143
|
+
)
|
|
1144
|
+
except AttributeError:
|
|
1145
|
+
pass
|
|
1146
|
+
except Exception:
|
|
1147
|
+
logger.debug(
|
|
1148
|
+
traceback.format_exc()
|
|
1149
|
+
)
|
|
1150
|
+
|
|
1151
|
+
if not data_exists:
|
|
1152
|
+
# Create the inner coding bit
|
|
1153
|
+
coding.CodeValue = "113842"
|
|
1154
|
+
coding.CodingSchemeDesignator = (
|
|
1155
|
+
"DCM"
|
|
1156
|
+
)
|
|
1157
|
+
coding.CodeMeaning = (
|
|
1158
|
+
"X-Ray Modulation Type"
|
|
1159
|
+
)
|
|
1160
|
+
# Create the outer container bit, including the protocol name
|
|
1161
|
+
prot_container = Dataset()
|
|
1162
|
+
prot_container.RelationshipType = (
|
|
1163
|
+
"CONTAINS"
|
|
1164
|
+
)
|
|
1165
|
+
prot_container.ValueType = (
|
|
1166
|
+
"TEXT"
|
|
1167
|
+
)
|
|
1168
|
+
prot_container.TextValue = (
|
|
1169
|
+
val
|
|
1170
|
+
)
|
|
1171
|
+
# Add the coding sequence into the container.
|
|
1172
|
+
# Sequences are lists.
|
|
1173
|
+
prot_container.ConceptNameCodeSequence = Sequence(
|
|
1174
|
+
[coding]
|
|
1175
|
+
)
|
|
1176
|
+
container.ContentSequence.append(
|
|
1177
|
+
prot_container
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
if key == "KVP":
|
|
1181
|
+
# First, check if there is already a kVp value in an x-ray source parameters container inside
|
|
1182
|
+
# a CT Acquisition Parameters container
|
|
1183
|
+
source_parameters_exists = False
|
|
1184
|
+
kvp_data_exists = False
|
|
1185
|
+
|
|
1186
|
+
for (
|
|
1187
|
+
container2b
|
|
1188
|
+
) in container.ContentSequence:
|
|
1189
|
+
if (
|
|
1190
|
+
container2b.ValueType
|
|
1191
|
+
== "CONTAINER"
|
|
1192
|
+
):
|
|
1193
|
+
if (
|
|
1194
|
+
container2b.ConceptNameCodeSequence[
|
|
1195
|
+
0
|
|
1196
|
+
].CodeMeaning
|
|
1197
|
+
== "CT Acquisition Parameters"
|
|
1198
|
+
):
|
|
1199
|
+
for (
|
|
1200
|
+
container3b
|
|
1201
|
+
) in container2b:
|
|
1202
|
+
for (
|
|
1203
|
+
container4b
|
|
1204
|
+
) in (
|
|
1205
|
+
container3b
|
|
1206
|
+
):
|
|
1207
|
+
try:
|
|
1208
|
+
if (
|
|
1209
|
+
container4b.ConceptNameCodeSequence[
|
|
1210
|
+
0
|
|
1211
|
+
].CodeMeaning
|
|
1212
|
+
== "CT X-Ray Source Parameters"
|
|
1213
|
+
):
|
|
1214
|
+
source_parameters_exists = True
|
|
1215
|
+
|
|
1216
|
+
for container5b in container4b:
|
|
1217
|
+
try:
|
|
1218
|
+
if (
|
|
1219
|
+
container5b[
|
|
1220
|
+
0
|
|
1221
|
+
]
|
|
1222
|
+
.ConceptNameCodeSequence[
|
|
1223
|
+
0
|
|
1224
|
+
]
|
|
1225
|
+
.CodeValue
|
|
1226
|
+
== "113733"
|
|
1227
|
+
):
|
|
1228
|
+
kvp_data_exists = True
|
|
1229
|
+
except AttributeError:
|
|
1230
|
+
pass
|
|
1231
|
+
except Exception:
|
|
1232
|
+
logger.debug(
|
|
1233
|
+
traceback.format_exc()
|
|
1234
|
+
)
|
|
1235
|
+
except AttributeError:
|
|
1236
|
+
# Likely there's no ConceptNameCodeSequence attribute
|
|
1237
|
+
pass
|
|
1238
|
+
except Exception:
|
|
1239
|
+
logger.debug(
|
|
1240
|
+
traceback.format_exc()
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
if (
|
|
1244
|
+
not source_parameters_exists
|
|
1245
|
+
):
|
|
1246
|
+
# There is no x-ray source parameters section, so add it
|
|
1247
|
+
# Create the x-ray source container
|
|
1248
|
+
source_container = (
|
|
1249
|
+
Dataset()
|
|
1250
|
+
)
|
|
1251
|
+
source_container.RelationshipType = (
|
|
1252
|
+
"CONTAINS"
|
|
1253
|
+
)
|
|
1254
|
+
source_container.ValueType = (
|
|
1255
|
+
"CONTAINER"
|
|
1256
|
+
)
|
|
1257
|
+
coding = (
|
|
1258
|
+
Dataset()
|
|
1259
|
+
)
|
|
1260
|
+
coding.CodeValue = (
|
|
1261
|
+
"113831"
|
|
1262
|
+
)
|
|
1263
|
+
coding.CodingSchemeDesignator = (
|
|
1264
|
+
"DCM"
|
|
1265
|
+
)
|
|
1266
|
+
coding.CodeMeaning = "CT X-Ray Source Parameters"
|
|
1267
|
+
source_container.ConceptNameCodeSequence = Sequence(
|
|
1268
|
+
[coding]
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
# Create the kVp container that will go in to the x-ray source container
|
|
1272
|
+
kvp_container = (
|
|
1273
|
+
Dataset()
|
|
1274
|
+
)
|
|
1275
|
+
kvp_container.RelationshipType = (
|
|
1276
|
+
"CONTAINS"
|
|
1277
|
+
)
|
|
1278
|
+
kvp_container.ValueType = (
|
|
1279
|
+
"NUM"
|
|
1280
|
+
)
|
|
1281
|
+
coding2 = (
|
|
1282
|
+
Dataset()
|
|
1283
|
+
)
|
|
1284
|
+
coding2.CodeValue = (
|
|
1285
|
+
"113733"
|
|
1286
|
+
)
|
|
1287
|
+
coding2.CodingSchemeDesignator = (
|
|
1288
|
+
"DCM"
|
|
1289
|
+
)
|
|
1290
|
+
coding2.CodeMeaning = (
|
|
1291
|
+
"KVP"
|
|
1292
|
+
)
|
|
1293
|
+
kvp_container.ConceptNameCodeSequence = Sequence(
|
|
1294
|
+
[coding2]
|
|
1295
|
+
)
|
|
1296
|
+
coding3 = (
|
|
1297
|
+
Dataset()
|
|
1298
|
+
)
|
|
1299
|
+
coding3.CodeValue = (
|
|
1300
|
+
"kV"
|
|
1301
|
+
)
|
|
1302
|
+
coding3.CodingSchemeDesignator = (
|
|
1303
|
+
"UCUM"
|
|
1304
|
+
)
|
|
1305
|
+
coding3.CodingSchemeVersion = (
|
|
1306
|
+
"1.4"
|
|
1307
|
+
)
|
|
1308
|
+
coding3.CodeMeaning = (
|
|
1309
|
+
"kV"
|
|
1310
|
+
)
|
|
1311
|
+
measurement_units_container = (
|
|
1312
|
+
Dataset()
|
|
1313
|
+
)
|
|
1314
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1315
|
+
[coding3]
|
|
1316
|
+
)
|
|
1317
|
+
measurement_units_container.NumericValue = (
|
|
1318
|
+
val
|
|
1319
|
+
)
|
|
1320
|
+
measured_value_sequence = Sequence(
|
|
1321
|
+
[
|
|
1322
|
+
measurement_units_container
|
|
1323
|
+
]
|
|
1324
|
+
)
|
|
1325
|
+
kvp_container.MeasuredValueSequence = measured_value_sequence
|
|
1326
|
+
|
|
1327
|
+
# Put the kVp container inside the x-ray source container
|
|
1328
|
+
source_container.ContentSequence = Sequence(
|
|
1329
|
+
[
|
|
1330
|
+
kvp_container
|
|
1331
|
+
]
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
# Add the source_container to the rdsr contents
|
|
1335
|
+
try:
|
|
1336
|
+
# Append it to an existing ContentSequence
|
|
1337
|
+
container2b.ContentSequence.append(
|
|
1338
|
+
source_container
|
|
1339
|
+
)
|
|
1340
|
+
except (
|
|
1341
|
+
TypeError
|
|
1342
|
+
):
|
|
1343
|
+
# ContentSequence doesn't exist, so add it
|
|
1344
|
+
container2b.ContentSequence = Sequence(
|
|
1345
|
+
[
|
|
1346
|
+
source_container
|
|
1347
|
+
]
|
|
1348
|
+
)
|
|
1349
|
+
except (
|
|
1350
|
+
Exception
|
|
1351
|
+
):
|
|
1352
|
+
logger.debug(
|
|
1353
|
+
traceback.format_exc()
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
source_parameters_exists = (
|
|
1357
|
+
True
|
|
1358
|
+
)
|
|
1359
|
+
kvp_data_exists = (
|
|
1360
|
+
True
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
elif (
|
|
1364
|
+
not kvp_data_exists
|
|
1365
|
+
):
|
|
1366
|
+
# CT X-ray Source Parameters exists, but there is no kVp data
|
|
1367
|
+
for (
|
|
1368
|
+
container3b
|
|
1369
|
+
) in (
|
|
1370
|
+
container2b
|
|
1371
|
+
):
|
|
1372
|
+
for container4b in container3b:
|
|
1373
|
+
try:
|
|
1374
|
+
if (
|
|
1375
|
+
container4b.ConceptNameCodeSequence[
|
|
1376
|
+
0
|
|
1377
|
+
].CodeMeaning
|
|
1378
|
+
== "CT X-Ray Source Parameters"
|
|
1379
|
+
):
|
|
1380
|
+
# Create the kVp container that will go in to the x-ray source container
|
|
1381
|
+
kvp_container = (
|
|
1382
|
+
Dataset()
|
|
1383
|
+
)
|
|
1384
|
+
kvp_container.RelationshipType = "CONTAINS"
|
|
1385
|
+
kvp_container.ValueType = "NUM"
|
|
1386
|
+
coding2 = (
|
|
1387
|
+
Dataset()
|
|
1388
|
+
)
|
|
1389
|
+
coding2.CodeValue = "113733"
|
|
1390
|
+
coding2.CodingSchemeDesignator = "DCM"
|
|
1391
|
+
coding2.CodeMeaning = "KVP"
|
|
1392
|
+
kvp_container.ConceptNameCodeSequence = Sequence(
|
|
1393
|
+
[
|
|
1394
|
+
coding2
|
|
1395
|
+
]
|
|
1396
|
+
)
|
|
1397
|
+
coding3 = (
|
|
1398
|
+
Dataset()
|
|
1399
|
+
)
|
|
1400
|
+
coding3.CodeValue = "kV"
|
|
1401
|
+
coding3.CodingSchemeDesignator = "UCUM"
|
|
1402
|
+
coding3.CodingSchemeVersion = "1.4"
|
|
1403
|
+
coding3.CodeMeaning = "kV"
|
|
1404
|
+
measurement_units_container = (
|
|
1405
|
+
Dataset()
|
|
1406
|
+
)
|
|
1407
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1408
|
+
[
|
|
1409
|
+
coding3
|
|
1410
|
+
]
|
|
1411
|
+
)
|
|
1412
|
+
measurement_units_container.NumericValue = val
|
|
1413
|
+
measured_value_sequence = Sequence(
|
|
1414
|
+
[
|
|
1415
|
+
measurement_units_container
|
|
1416
|
+
]
|
|
1417
|
+
)
|
|
1418
|
+
kvp_container.MeasuredValueSequence = measured_value_sequence
|
|
1419
|
+
|
|
1420
|
+
# Add the kVp container inside the x-ray source container
|
|
1421
|
+
container4b.ContentSequence.append(
|
|
1422
|
+
kvp_container
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
kvp_data_exists = True
|
|
1426
|
+
|
|
1427
|
+
except AttributeError:
|
|
1428
|
+
# Likely there's no ConceptNameCodeSequence attribute
|
|
1429
|
+
pass
|
|
1430
|
+
except Exception:
|
|
1431
|
+
logger.debug(
|
|
1432
|
+
traceback.format_exc()
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
if key == "ExposureTime":
|
|
1436
|
+
# First, check if there is already an exposure time per rotation value in an x-ray source parameters container inside
|
|
1437
|
+
# a CT Acquisition Parameters container.
|
|
1438
|
+
source_parameters_exists = False
|
|
1439
|
+
exposure_time_per_rotation_data_exists = (
|
|
1440
|
+
False
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
for (
|
|
1444
|
+
container2b
|
|
1445
|
+
) in container.ContentSequence:
|
|
1446
|
+
if (
|
|
1447
|
+
container2b.ValueType
|
|
1448
|
+
== "CONTAINER"
|
|
1449
|
+
):
|
|
1450
|
+
if (
|
|
1451
|
+
container2b.ConceptNameCodeSequence[
|
|
1452
|
+
0
|
|
1453
|
+
].CodeMeaning
|
|
1454
|
+
== "CT Acquisition Parameters"
|
|
1455
|
+
):
|
|
1456
|
+
for (
|
|
1457
|
+
container3b
|
|
1458
|
+
) in container2b:
|
|
1459
|
+
for (
|
|
1460
|
+
container4b
|
|
1461
|
+
) in (
|
|
1462
|
+
container3b
|
|
1463
|
+
):
|
|
1464
|
+
try:
|
|
1465
|
+
if (
|
|
1466
|
+
container4b.ConceptNameCodeSequence[
|
|
1467
|
+
0
|
|
1468
|
+
].CodeMeaning
|
|
1469
|
+
== "CT X-Ray Source Parameters"
|
|
1470
|
+
):
|
|
1471
|
+
source_parameters_exists = True
|
|
1472
|
+
|
|
1473
|
+
for container5b in container4b:
|
|
1474
|
+
try:
|
|
1475
|
+
if (
|
|
1476
|
+
container5b[
|
|
1477
|
+
0
|
|
1478
|
+
]
|
|
1479
|
+
.ConceptNameCodeSequence[
|
|
1480
|
+
0
|
|
1481
|
+
]
|
|
1482
|
+
.CodeValue
|
|
1483
|
+
== "113843"
|
|
1484
|
+
):
|
|
1485
|
+
exposure_time_per_rotation_data_exists = True
|
|
1486
|
+
except AttributeError:
|
|
1487
|
+
pass
|
|
1488
|
+
except Exception:
|
|
1489
|
+
logger.debug(
|
|
1490
|
+
traceback.format_exc()
|
|
1491
|
+
)
|
|
1492
|
+
except AttributeError:
|
|
1493
|
+
# Likely there's no ConceptNameCodeSequence attribute
|
|
1494
|
+
pass
|
|
1495
|
+
except Exception:
|
|
1496
|
+
logger.debug(
|
|
1497
|
+
traceback.format_exc()
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
if (
|
|
1501
|
+
not source_parameters_exists
|
|
1502
|
+
):
|
|
1503
|
+
# There is no x-ray source parameters section, so add it
|
|
1504
|
+
# Create the x-ray source container
|
|
1505
|
+
source_container = (
|
|
1506
|
+
Dataset()
|
|
1507
|
+
)
|
|
1508
|
+
source_container.RelationshipType = (
|
|
1509
|
+
"CONTAINS"
|
|
1510
|
+
)
|
|
1511
|
+
source_container.ValueType = (
|
|
1512
|
+
"CONTAINER"
|
|
1513
|
+
)
|
|
1514
|
+
coding = (
|
|
1515
|
+
Dataset()
|
|
1516
|
+
)
|
|
1517
|
+
coding.CodeValue = (
|
|
1518
|
+
"113831"
|
|
1519
|
+
)
|
|
1520
|
+
coding.CodingSchemeDesignator = (
|
|
1521
|
+
"DCM"
|
|
1522
|
+
)
|
|
1523
|
+
coding.CodeMeaning = "CT X-Ray Source Parameters"
|
|
1524
|
+
source_container.ConceptNameCodeSequence = Sequence(
|
|
1525
|
+
[coding]
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
# Create the kVp container that will go in to the x-ray source container
|
|
1529
|
+
exposure_time_per_rotation_container = (
|
|
1530
|
+
Dataset()
|
|
1531
|
+
)
|
|
1532
|
+
exposure_time_per_rotation_container.RelationshipType = (
|
|
1533
|
+
"CONTAINS"
|
|
1534
|
+
)
|
|
1535
|
+
exposure_time_per_rotation_container.ValueType = (
|
|
1536
|
+
"NUM"
|
|
1537
|
+
)
|
|
1538
|
+
coding2 = (
|
|
1539
|
+
Dataset()
|
|
1540
|
+
)
|
|
1541
|
+
coding2.CodeValue = (
|
|
1542
|
+
"113843"
|
|
1543
|
+
)
|
|
1544
|
+
coding2.CodingSchemeDesignator = (
|
|
1545
|
+
"DCM"
|
|
1546
|
+
)
|
|
1547
|
+
coding2.CodeMeaning = "Exposure Time per Rotation"
|
|
1548
|
+
exposure_time_per_rotation_container.ConceptNameCodeSequence = Sequence(
|
|
1549
|
+
[coding2]
|
|
1550
|
+
)
|
|
1551
|
+
coding3 = (
|
|
1552
|
+
Dataset()
|
|
1553
|
+
)
|
|
1554
|
+
coding3.CodeValue = (
|
|
1555
|
+
"s"
|
|
1556
|
+
)
|
|
1557
|
+
coding3.CodingSchemeDesignator = (
|
|
1558
|
+
"UCUM"
|
|
1559
|
+
)
|
|
1560
|
+
coding3.CodingSchemeVersion = (
|
|
1561
|
+
"1.4"
|
|
1562
|
+
)
|
|
1563
|
+
coding3.CodeMeaning = (
|
|
1564
|
+
"s"
|
|
1565
|
+
)
|
|
1566
|
+
measurement_units_container = (
|
|
1567
|
+
Dataset()
|
|
1568
|
+
)
|
|
1569
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1570
|
+
[coding3]
|
|
1571
|
+
)
|
|
1572
|
+
measurement_units_container.NumericValue = str(
|
|
1573
|
+
float(val)
|
|
1574
|
+
/ 1000
|
|
1575
|
+
)
|
|
1576
|
+
measured_value_sequence = Sequence(
|
|
1577
|
+
[
|
|
1578
|
+
measurement_units_container
|
|
1579
|
+
]
|
|
1580
|
+
)
|
|
1581
|
+
exposure_time_per_rotation_container.MeasuredValueSequence = measured_value_sequence
|
|
1582
|
+
|
|
1583
|
+
# Put the exposure time per rotation container inside the x-ray source container
|
|
1584
|
+
source_container.ContentSequence = Sequence(
|
|
1585
|
+
[
|
|
1586
|
+
exposure_time_per_rotation_container
|
|
1587
|
+
]
|
|
1588
|
+
)
|
|
1589
|
+
|
|
1590
|
+
# Add the source_container to the rdsr contents
|
|
1591
|
+
try:
|
|
1592
|
+
# Append it to an existing ContentSequence
|
|
1593
|
+
container2b.ContentSequence.append(
|
|
1594
|
+
source_container
|
|
1595
|
+
)
|
|
1596
|
+
except (
|
|
1597
|
+
TypeError
|
|
1598
|
+
):
|
|
1599
|
+
# ContentSequence doesn't exist, so add it
|
|
1600
|
+
container2b.ContentSequence = Sequence(
|
|
1601
|
+
[
|
|
1602
|
+
source_container
|
|
1603
|
+
]
|
|
1604
|
+
)
|
|
1605
|
+
except (
|
|
1606
|
+
Exception
|
|
1607
|
+
):
|
|
1608
|
+
logger.debug(
|
|
1609
|
+
traceback.format_exc()
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
source_parameters_exists = (
|
|
1613
|
+
True
|
|
1614
|
+
)
|
|
1615
|
+
exposure_time_per_rotation_data_exists = (
|
|
1616
|
+
True
|
|
1617
|
+
)
|
|
1618
|
+
|
|
1619
|
+
elif (
|
|
1620
|
+
not exposure_time_per_rotation_data_exists
|
|
1621
|
+
):
|
|
1622
|
+
# CT X-ray Source Parameters exists, but there is no exposure time per rotation data
|
|
1623
|
+
for (
|
|
1624
|
+
container3b
|
|
1625
|
+
) in (
|
|
1626
|
+
container2b
|
|
1627
|
+
):
|
|
1628
|
+
for container4b in container3b:
|
|
1629
|
+
try:
|
|
1630
|
+
if (
|
|
1631
|
+
container4b.ConceptNameCodeSequence[
|
|
1632
|
+
0
|
|
1633
|
+
].CodeMeaning
|
|
1634
|
+
== "CT X-Ray Source Parameters"
|
|
1635
|
+
):
|
|
1636
|
+
# Create the exposure time per rotation container that will go in to the x-ray source container
|
|
1637
|
+
exposure_time_per_rotation_container = (
|
|
1638
|
+
Dataset()
|
|
1639
|
+
)
|
|
1640
|
+
exposure_time_per_rotation_container.RelationshipType = "CONTAINS"
|
|
1641
|
+
exposure_time_per_rotation_container.ValueType = "NUM"
|
|
1642
|
+
coding2 = (
|
|
1643
|
+
Dataset()
|
|
1644
|
+
)
|
|
1645
|
+
coding2.CodeValue = "113843"
|
|
1646
|
+
coding2.CodingSchemeDesignator = "DCM"
|
|
1647
|
+
coding2.CodeMeaning = "Exposure Time per Rotation"
|
|
1648
|
+
exposure_time_per_rotation_container.ConceptNameCodeSequence = Sequence(
|
|
1649
|
+
[
|
|
1650
|
+
coding2
|
|
1651
|
+
]
|
|
1652
|
+
)
|
|
1653
|
+
coding3 = (
|
|
1654
|
+
Dataset()
|
|
1655
|
+
)
|
|
1656
|
+
coding3.CodeValue = "s"
|
|
1657
|
+
coding3.CodingSchemeDesignator = "UCUM"
|
|
1658
|
+
coding3.CodingSchemeVersion = "1.4"
|
|
1659
|
+
coding3.CodeMeaning = "s"
|
|
1660
|
+
measurement_units_container = (
|
|
1661
|
+
Dataset()
|
|
1662
|
+
)
|
|
1663
|
+
measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
|
|
1664
|
+
[
|
|
1665
|
+
coding3
|
|
1666
|
+
]
|
|
1667
|
+
)
|
|
1668
|
+
measurement_units_container.NumericValue = str(
|
|
1669
|
+
float(
|
|
1670
|
+
val
|
|
1671
|
+
)
|
|
1672
|
+
/ 1000
|
|
1673
|
+
)
|
|
1674
|
+
measured_value_sequence = Sequence(
|
|
1675
|
+
[
|
|
1676
|
+
measurement_units_container
|
|
1677
|
+
]
|
|
1678
|
+
)
|
|
1679
|
+
exposure_time_per_rotation_container.MeasuredValueSequence = measured_value_sequence
|
|
1680
|
+
|
|
1681
|
+
# Add the exposure time per rotation container inside the x-ray source container
|
|
1682
|
+
container4b.ContentSequence.append(
|
|
1683
|
+
exposure_time_per_rotation_container
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
exposure_time_per_rotation_data_exists = True
|
|
1687
|
+
|
|
1688
|
+
except AttributeError:
|
|
1689
|
+
# Likely there's no ConceptNameCodeSequence attribute
|
|
1690
|
+
pass
|
|
1691
|
+
except Exception:
|
|
1692
|
+
logger.debug(
|
|
1693
|
+
traceback.format_exc()
|
|
1694
|
+
)
|
|
1695
|
+
except KeyError as e:
|
|
1696
|
+
logger.debug(traceback.format_exc())
|
|
1697
|
+
except Exception as e:
|
|
1698
|
+
logger.debug(traceback.format_exc())
|
|
1699
|
+
# The end of updating the RDSR
|
|
1700
|
+
##############################################
|
|
1701
|
+
except KeyError:
|
|
1702
|
+
# Either CTDIvol or DLP data is not present
|
|
1703
|
+
pass
|
|
1704
|
+
except ValueError:
|
|
1705
|
+
# Perhaps the contents of the DLP or CTDIvol are not values
|
|
1706
|
+
pass
|
|
1707
|
+
except Exception:
|
|
1708
|
+
logger.debug(traceback.format_exc())
|
|
1709
|
+
|
|
1710
|
+
logger.debug("Updated acquisition data")
|
|
1711
|
+
logger.debug("Saving updated RDSR file")
|
|
1712
|
+
dcm.save_as(new_rdsr_file)
|
|
1713
|
+
logger.debug("Updated RDSR file saved")
|
|
1714
|
+
return 1
|
|
1715
|
+
|
|
1716
|
+
|
|
1717
|
+
def ct_toshiba(folder_name):
|
|
1718
|
+
"""Function to create radiation dose structured reports from a folder of dose images.
|
|
1719
|
+
|
|
1720
|
+
:param folder_name: Path to folder containing Toshiba DICOM objects - dose summary and images
|
|
1721
|
+
"""
|
|
1722
|
+
import pydicom
|
|
1723
|
+
|
|
1724
|
+
rdsr_name = "sr.dcm"
|
|
1725
|
+
updated_rdsr_name = "sr_updated.dcm"
|
|
1726
|
+
combined_rdsr_name = "sr_combined.dcm"
|
|
1727
|
+
|
|
1728
|
+
# Split the folder of images by StudyInstanceUID. This is required because pixelmed.jar will only process the
|
|
1729
|
+
# first dose summary image it finds. Splitting the files by StudyInstanceUID should mean that there is only one
|
|
1730
|
+
# dose summary per folder. N.B. I think Conquest may do this by default with incoming DICOM objects. This
|
|
1731
|
+
# routine also renames the files using integer file names to ensure that they are accepted by dcmmkdir later on.
|
|
1732
|
+
logger.debug(
|
|
1733
|
+
"Splitting into folders by StudyInstanceUID for {0}".format(folder_name)
|
|
1734
|
+
)
|
|
1735
|
+
folders = _split_by_studyinstanceuid(folder_name)
|
|
1736
|
+
logger.debug(
|
|
1737
|
+
"Splitting into folders by StudyInstanceUID complete for {0}".format(
|
|
1738
|
+
folder_name
|
|
1739
|
+
)
|
|
1740
|
+
)
|
|
1741
|
+
|
|
1742
|
+
# Obtain additional information from the image tags in each folder and add this information to the RDSR file.
|
|
1743
|
+
for folder in folders:
|
|
1744
|
+
|
|
1745
|
+
# Check to see if there's just one dose summary in the folder
|
|
1746
|
+
dose_summary_object_info = _find_dose_summary_objects(folder)
|
|
1747
|
+
logger.debug("dose_summary_object_info is {0}".format(dose_summary_object_info))
|
|
1748
|
+
|
|
1749
|
+
# For Toshiba scanners each dose summary consists of two objects
|
|
1750
|
+
number_of_dose_summary_objects = len(dose_summary_object_info) // 2
|
|
1751
|
+
|
|
1752
|
+
if number_of_dose_summary_objects > 1:
|
|
1753
|
+
# There's more than one pair of dose summary objects, so duplicate the
|
|
1754
|
+
# contents of folder number_of_dose_summary_objects times.
|
|
1755
|
+
unique_study_times = {
|
|
1756
|
+
item["studyTime"] for item in dose_summary_object_info
|
|
1757
|
+
}
|
|
1758
|
+
subfolder_paths = []
|
|
1759
|
+
for unique_study_time in unique_study_times:
|
|
1760
|
+
subfolder_path = os.path.join(folder, unique_study_time)
|
|
1761
|
+
subfolder_paths.append(subfolder_path)
|
|
1762
|
+
if not os.path.isdir(subfolder_path):
|
|
1763
|
+
os.mkdir(subfolder_path)
|
|
1764
|
+
_copy_files_from_a_to_b(folder, subfolder_path)
|
|
1765
|
+
|
|
1766
|
+
# Delete all but one pair of dose summary objects from each subfolder
|
|
1767
|
+
for unique_study_time in unique_study_times:
|
|
1768
|
+
for dose_summary_object in dose_summary_object_info:
|
|
1769
|
+
if dose_summary_object["studyTime"] != unique_study_time:
|
|
1770
|
+
# Delete the corresponding dose_summary_object file from the
|
|
1771
|
+
# x subfolder in folder
|
|
1772
|
+
os.remove(
|
|
1773
|
+
os.path.join(
|
|
1774
|
+
folder,
|
|
1775
|
+
unique_study_time,
|
|
1776
|
+
dose_summary_object["fileName"],
|
|
1777
|
+
)
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
# Now create an RDSR in each subfolder in subfolder_paths
|
|
1781
|
+
for sub_folder in subfolder_paths:
|
|
1782
|
+
# Create a DICOM RDSR for the sub-folder using pixelmed.jar.
|
|
1783
|
+
logger.debug(
|
|
1784
|
+
"Trying to make initial DICOM RDSR object in {0}".format(sub_folder)
|
|
1785
|
+
)
|
|
1786
|
+
combined_command = (
|
|
1787
|
+
JAVA_EXE
|
|
1788
|
+
+ " "
|
|
1789
|
+
+ JAVA_OPTIONS
|
|
1790
|
+
+ " "
|
|
1791
|
+
+ PIXELMED_JAR
|
|
1792
|
+
+ " "
|
|
1793
|
+
+ PIXELMED_JAR_OPTIONS
|
|
1794
|
+
)
|
|
1795
|
+
_make_dicom_rdsr(sub_folder, combined_command, rdsr_name)
|
|
1796
|
+
# Check that the initial RDSR exists
|
|
1797
|
+
initial_rdsr_name_and_path = os.path.join(sub_folder, rdsr_name)
|
|
1798
|
+
if not os.path.isfile(initial_rdsr_name_and_path):
|
|
1799
|
+
logger.debug(
|
|
1800
|
+
"Failed to create initial DICOM RDSR object created in {0}. Skipping.".format(
|
|
1801
|
+
sub_folder
|
|
1802
|
+
)
|
|
1803
|
+
)
|
|
1804
|
+
# Remove this sub_folder from subfolder_paths list as it can't be used
|
|
1805
|
+
subfolder_paths.remove(sub_folder)
|
|
1806
|
+
continue
|
|
1807
|
+
logger.debug(
|
|
1808
|
+
"Initial DICOM RDSR object created in {0}".format(sub_folder)
|
|
1809
|
+
)
|
|
1810
|
+
|
|
1811
|
+
logger.debug(
|
|
1812
|
+
"Gathering extra information from images in {0}".format(sub_folder)
|
|
1813
|
+
)
|
|
1814
|
+
extra_information = _find_extra_info(sub_folder)
|
|
1815
|
+
extra_study_information = extra_information[0]
|
|
1816
|
+
extra_acquisition_information = extra_information[1]
|
|
1817
|
+
logger.debug(
|
|
1818
|
+
"Gathered extra information from images in {0}".format(sub_folder)
|
|
1819
|
+
)
|
|
1820
|
+
|
|
1821
|
+
# Use the extra information to update the initial rdsr file created by DoseUtility
|
|
1822
|
+
logger.debug("Updating information in rdsr in {0}".format(sub_folder))
|
|
1823
|
+
updated_rdsr_name_and_path = os.path.join(sub_folder, updated_rdsr_name)
|
|
1824
|
+
result = _update_dicom_rdsr(
|
|
1825
|
+
initial_rdsr_name_and_path,
|
|
1826
|
+
extra_study_information,
|
|
1827
|
+
extra_acquisition_information,
|
|
1828
|
+
updated_rdsr_name_and_path,
|
|
1829
|
+
)
|
|
1830
|
+
# Check that the updated RDSR exists
|
|
1831
|
+
if result == 1:
|
|
1832
|
+
logger.debug("Updated information in rdsr")
|
|
1833
|
+
else:
|
|
1834
|
+
logger.debug(
|
|
1835
|
+
"Failed to update the initial DICOM RDSR object in {0}. Skipping.".format(
|
|
1836
|
+
sub_folder
|
|
1837
|
+
)
|
|
1838
|
+
)
|
|
1839
|
+
# Remove this sub_folder from subfolder_paths list as it can't be used
|
|
1840
|
+
subfolder_paths.remove(sub_folder)
|
|
1841
|
+
continue
|
|
1842
|
+
|
|
1843
|
+
# Now combine the data contained in the RDSRs
|
|
1844
|
+
first_subfolder = True
|
|
1845
|
+
for sub_folder in subfolder_paths:
|
|
1846
|
+
if first_subfolder == True:
|
|
1847
|
+
shutil.copy(
|
|
1848
|
+
os.path.join(sub_folder, updated_rdsr_name),
|
|
1849
|
+
os.path.join(folder, combined_rdsr_name),
|
|
1850
|
+
)
|
|
1851
|
+
combined_rdsr = pydicom.dcmread(
|
|
1852
|
+
os.path.join(folder, combined_rdsr_name)
|
|
1853
|
+
)
|
|
1854
|
+
for content_sequence in combined_rdsr.ContentSequence:
|
|
1855
|
+
if (
|
|
1856
|
+
content_sequence.ConceptNameCodeSequence[0].CodeMeaning
|
|
1857
|
+
== "CT Accumulated Dose Data"
|
|
1858
|
+
):
|
|
1859
|
+
combined_rdsr_accumulated_dose_data = content_sequence
|
|
1860
|
+
first_subfolder = False
|
|
1861
|
+
else:
|
|
1862
|
+
current_rdsr = pydicom.dcmread(
|
|
1863
|
+
os.path.join(sub_folder, updated_rdsr_name)
|
|
1864
|
+
)
|
|
1865
|
+
for content_sequence in current_rdsr.ContentSequence:
|
|
1866
|
+
if (
|
|
1867
|
+
content_sequence.ConceptNameCodeSequence[0].CodeMeaning
|
|
1868
|
+
== "CT Acquisition"
|
|
1869
|
+
):
|
|
1870
|
+
combined_rdsr.ContentSequence.append(content_sequence)
|
|
1871
|
+
if (
|
|
1872
|
+
content_sequence.ConceptNameCodeSequence[0].CodeMeaning
|
|
1873
|
+
== "CT Accumulated Dose Data"
|
|
1874
|
+
):
|
|
1875
|
+
# Total Number of Irradiation Events
|
|
1876
|
+
logger.debug(
|
|
1877
|
+
"Trying to update total number of irradiation events"
|
|
1878
|
+
)
|
|
1879
|
+
for item in content_sequence.ContentSequence:
|
|
1880
|
+
if (
|
|
1881
|
+
item.ConceptNameCodeSequence[0].CodeMeaning
|
|
1882
|
+
== "Total Number of Irradiation Events"
|
|
1883
|
+
):
|
|
1884
|
+
additional_amount = item.MeasuredValueSequence[
|
|
1885
|
+
0
|
|
1886
|
+
].NumericValue
|
|
1887
|
+
logger.debug(
|
|
1888
|
+
"Found extra events: {0}".format(
|
|
1889
|
+
additional_amount
|
|
1890
|
+
)
|
|
1891
|
+
)
|
|
1892
|
+
for (
|
|
1893
|
+
item
|
|
1894
|
+
) in combined_rdsr_accumulated_dose_data.ContentSequence:
|
|
1895
|
+
if (
|
|
1896
|
+
item.ConceptNameCodeSequence[0].CodeMeaning
|
|
1897
|
+
== "Total Number of Irradiation Events"
|
|
1898
|
+
):
|
|
1899
|
+
logger.debug(
|
|
1900
|
+
"Found current events: {0}".format(
|
|
1901
|
+
item.MeasuredValueSequence[0].NumericValue
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
item.MeasuredValueSequence[0].NumericValue = str(
|
|
1905
|
+
int(
|
|
1906
|
+
item.MeasuredValueSequence[0].NumericValue
|
|
1907
|
+
+ additional_amount
|
|
1908
|
+
)
|
|
1909
|
+
)
|
|
1910
|
+
logger.debug(
|
|
1911
|
+
"Updated to: {0}".format(
|
|
1912
|
+
item.MeasuredValueSequence[0].NumericValue
|
|
1913
|
+
)
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
# CT Dose Length Product Total
|
|
1917
|
+
logger.debug("Trying to update total DLP")
|
|
1918
|
+
extra_ct_dlp_total_content_sequence = None
|
|
1919
|
+
for content_seq in content_sequence.ContentSequence:
|
|
1920
|
+
if (
|
|
1921
|
+
content_seq.ConceptNameCodeSequence[0].CodeMeaning
|
|
1922
|
+
== "CT Dose Length Product Total"
|
|
1923
|
+
):
|
|
1924
|
+
additional_amount = (
|
|
1925
|
+
content_seq.MeasuredValueSequence[
|
|
1926
|
+
0
|
|
1927
|
+
].NumericValue
|
|
1928
|
+
)
|
|
1929
|
+
logger.debug(
|
|
1930
|
+
"Found extra DLP: {0}".format(additional_amount)
|
|
1931
|
+
)
|
|
1932
|
+
extra_ct_dlp_total_content_sequence = content_seq
|
|
1933
|
+
# If the total DLP is not zero
|
|
1934
|
+
if float(additional_amount):
|
|
1935
|
+
# Find out which dosimetry phantom this value is for
|
|
1936
|
+
for cont_seq in content_seq.ContentSequence:
|
|
1937
|
+
if (
|
|
1938
|
+
cont_seq.ConceptNameCodeSequence[
|
|
1939
|
+
0
|
|
1940
|
+
].CodeMeaning
|
|
1941
|
+
== "CTDIw Phantom Type"
|
|
1942
|
+
):
|
|
1943
|
+
additional_type = (
|
|
1944
|
+
cont_seq.ConceptCodeSequence[
|
|
1945
|
+
0
|
|
1946
|
+
].CodeMeaning
|
|
1947
|
+
)
|
|
1948
|
+
|
|
1949
|
+
for i, content_seq in enumerate(
|
|
1950
|
+
combined_rdsr_accumulated_dose_data.ContentSequence
|
|
1951
|
+
):
|
|
1952
|
+
if (
|
|
1953
|
+
content_seq.ConceptNameCodeSequence[0].CodeMeaning
|
|
1954
|
+
== "CT Dose Length Product Total"
|
|
1955
|
+
):
|
|
1956
|
+
current_amount = content_seq.MeasuredValueSequence[
|
|
1957
|
+
0
|
|
1958
|
+
].NumericValue
|
|
1959
|
+
logger.debug(
|
|
1960
|
+
"Found current total DLP: {0}".format(
|
|
1961
|
+
current_amount
|
|
1962
|
+
)
|
|
1963
|
+
)
|
|
1964
|
+
# If the total DLP is not zero
|
|
1965
|
+
total_type = None
|
|
1966
|
+
if float(current_amount):
|
|
1967
|
+
# Find out which dosimetry phantom this value is for
|
|
1968
|
+
for cont_seq in content_seq.ContentSequence:
|
|
1969
|
+
if (
|
|
1970
|
+
cont_seq.ConceptNameCodeSequence[
|
|
1971
|
+
0
|
|
1972
|
+
].CodeMeaning
|
|
1973
|
+
== "CTDIw Phantom Type"
|
|
1974
|
+
):
|
|
1975
|
+
total_type = (
|
|
1976
|
+
cont_seq.ConceptCodeSequence[
|
|
1977
|
+
0
|
|
1978
|
+
].CodeMeaning
|
|
1979
|
+
)
|
|
1980
|
+
|
|
1981
|
+
if current_amount and additional_amount:
|
|
1982
|
+
# If combined_rdsr total DLP and new one use the same dosimetry phantom then just add them together.
|
|
1983
|
+
if total_type == additional_type:
|
|
1984
|
+
content_seq.MeasuredValueSequence[
|
|
1985
|
+
0
|
|
1986
|
+
].NumericValue = "%.2f" % (
|
|
1987
|
+
content_seq.MeasuredValueSequence[
|
|
1988
|
+
0
|
|
1989
|
+
].NumericValue
|
|
1990
|
+
+ additional_amount
|
|
1991
|
+
)
|
|
1992
|
+
# If additional DLP is to 16 cm head phantom then divide it by 2 before adding.
|
|
1993
|
+
elif "head" in additional_type.lower():
|
|
1994
|
+
content_seq.MeasuredValueSequence[
|
|
1995
|
+
0
|
|
1996
|
+
].NumericValue = "%.2f" % (
|
|
1997
|
+
content_seq.MeasuredValueSequence[
|
|
1998
|
+
0
|
|
1999
|
+
].NumericValue
|
|
2000
|
+
+ (additional_amount / 2.0)
|
|
2001
|
+
)
|
|
2002
|
+
# If current total DLP is to 16 cm head phantom then divide it by 2 before adding the 32 cm additional.
|
|
2003
|
+
else:
|
|
2004
|
+
content_seq.MeasuredValueSequence[
|
|
2005
|
+
0
|
|
2006
|
+
].NumericValue = "%.2f" % (
|
|
2007
|
+
(
|
|
2008
|
+
content_seq.MeasuredValueSequence[
|
|
2009
|
+
0
|
|
2010
|
+
].NumericValue
|
|
2011
|
+
/ 2.0
|
|
2012
|
+
)
|
|
2013
|
+
+ additional_amount
|
|
2014
|
+
)
|
|
2015
|
+
logger.debug(
|
|
2016
|
+
"Updated to: {0}".format(
|
|
2017
|
+
content_seq.MeasuredValueSequence[
|
|
2018
|
+
0
|
|
2019
|
+
].NumericValue
|
|
2020
|
+
)
|
|
2021
|
+
)
|
|
2022
|
+
elif current_amount:
|
|
2023
|
+
# There is no additional DLP to add
|
|
2024
|
+
logger.debug("No additional DLP to add")
|
|
2025
|
+
else:
|
|
2026
|
+
# There is no current DLP, but we do have extra, so over-write the current content sequence with extra_ct_dlp_total_content_sequence
|
|
2027
|
+
logger.debug(
|
|
2028
|
+
"Current total DLP is zero. Replacing with extra {0} mGy.cm.".format(
|
|
2029
|
+
additional_amount
|
|
2030
|
+
)
|
|
2031
|
+
)
|
|
2032
|
+
combined_rdsr_accumulated_dose_data.ContentSequence[
|
|
2033
|
+
i
|
|
2034
|
+
] = extra_ct_dlp_total_content_sequence
|
|
2035
|
+
|
|
2036
|
+
if len(subfolder_paths) > 0:
|
|
2037
|
+
combined_rdsr.save_as(
|
|
2038
|
+
os.path.join(folder, combined_rdsr_name + "_updated")
|
|
2039
|
+
)
|
|
2040
|
+
logger.debug(
|
|
2041
|
+
"Importing updated combined rdsr in to OpenREM ({0})".format(
|
|
2042
|
+
os.path.join(folder, combined_rdsr_name + "_updated")
|
|
2043
|
+
)
|
|
2044
|
+
)
|
|
2045
|
+
rdsr(os.path.join(folder, combined_rdsr_name + "_updated"))
|
|
2046
|
+
logger.debug("Imported in to OpenREM")
|
|
2047
|
+
else:
|
|
2048
|
+
logger.debug("RDSRs could not be created for any subfolders")
|
|
2049
|
+
|
|
2050
|
+
else:
|
|
2051
|
+
# Create a DICOM RDSR for the sub-folder using pixelmed.jar.
|
|
2052
|
+
logger.debug(
|
|
2053
|
+
"Trying to make initial DICOM RDSR object in {0}".format(folder)
|
|
2054
|
+
)
|
|
2055
|
+
combined_command = (
|
|
2056
|
+
JAVA_EXE
|
|
2057
|
+
+ " "
|
|
2058
|
+
+ JAVA_OPTIONS
|
|
2059
|
+
+ " "
|
|
2060
|
+
+ PIXELMED_JAR
|
|
2061
|
+
+ " "
|
|
2062
|
+
+ PIXELMED_JAR_OPTIONS
|
|
2063
|
+
)
|
|
2064
|
+
_make_dicom_rdsr(folder, combined_command, rdsr_name)
|
|
2065
|
+
# Check that the initial RDSR exists
|
|
2066
|
+
initial_rdsr_name_and_path = os.path.join(folder, rdsr_name)
|
|
2067
|
+
if os.path.isfile(initial_rdsr_name_and_path):
|
|
2068
|
+
logger.debug("Initial DICOM RDSR object created in {0}".format(folder))
|
|
2069
|
+
|
|
2070
|
+
logger.debug(
|
|
2071
|
+
"Gathering extra information from images in {0}".format(folder)
|
|
2072
|
+
)
|
|
2073
|
+
extra_information = _find_extra_info(folder)
|
|
2074
|
+
extra_study_information = extra_information[0]
|
|
2075
|
+
extra_acquisition_information = extra_information[1]
|
|
2076
|
+
logger.debug(
|
|
2077
|
+
"Gathered extra information from images in {0}".format(folder)
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
# Use the extra information to update the initial rdsr file created by DoseUtility
|
|
2081
|
+
logger.debug("Updating information in rdsr in {0}".format(folder))
|
|
2082
|
+
updated_rdsr_name_and_path = os.path.join(folder, updated_rdsr_name)
|
|
2083
|
+
result = _update_dicom_rdsr(
|
|
2084
|
+
initial_rdsr_name_and_path,
|
|
2085
|
+
extra_study_information,
|
|
2086
|
+
extra_acquisition_information,
|
|
2087
|
+
updated_rdsr_name_and_path,
|
|
2088
|
+
)
|
|
2089
|
+
logger.debug("Updated information in rdsr")
|
|
2090
|
+
|
|
2091
|
+
# Now import the updated rdsr into OpenREM using the Toshiba extractor
|
|
2092
|
+
if result == 1:
|
|
2093
|
+
logger.debug(
|
|
2094
|
+
"Importing updated rdsr in to OpenREM ({0})".format(
|
|
2095
|
+
updated_rdsr_name_and_path
|
|
2096
|
+
)
|
|
2097
|
+
)
|
|
2098
|
+
rdsr(updated_rdsr_name_and_path)
|
|
2099
|
+
logger.debug("Imported in to OpenREM")
|
|
2100
|
+
else:
|
|
2101
|
+
logger.debug(
|
|
2102
|
+
"Not imported to OpenREM. Result is: {0}".format(result)
|
|
2103
|
+
)
|
|
2104
|
+
else:
|
|
2105
|
+
logger.debug(
|
|
2106
|
+
"Failed to create initial DICOM RDSR object created in {0}. Skipping.".format(
|
|
2107
|
+
folder
|
|
2108
|
+
)
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
# Now delete the image folder
|
|
2112
|
+
logger.debug("Removing study folder")
|
|
2113
|
+
shutil.rmtree(folder_name)
|
|
2114
|
+
logger.debug("Removing study folder complete")
|
|
2115
|
+
logger.debug("Reached end of ct_toshiba routine")
|
|
2116
|
+
return 0
|