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.
Files changed (279) hide show
  1. openrem/locale/de/LC_MESSAGES/django.po +1060 -1059
  2. openrem/locale/django.pot +973 -972
  3. openrem/locale/es_MX/LC_MESSAGES/django.po +1049 -1048
  4. openrem/locale/it/LC_MESSAGES/django.po +1044 -1043
  5. openrem/locale/lt/LC_MESSAGES/django.po +989 -988
  6. openrem/locale/nb_NO/LC_MESSAGES/django.po +985 -984
  7. openrem/locale/pt_BR/LC_MESSAGES/django.po +1003 -1002
  8. openrem/manage.py +10 -10
  9. openrem/openremproject/__init__.py +1 -1
  10. openrem/openremproject/local_settings.py.linux +128 -128
  11. openrem/openremproject/local_settings.py.windows +144 -144
  12. openrem/openremproject/local_settings.py.windows-sqlite3 +129 -129
  13. openrem/openremproject/settings.py +278 -278
  14. openrem/openremproject/urls.py +32 -32
  15. openrem/openremproject/wsgi.py.example +28 -28
  16. openrem/remapp/__init__.py +2 -2
  17. openrem/remapp/admin.py +31 -31
  18. openrem/remapp/exports/ct_export.py +780 -753
  19. openrem/remapp/exports/dx_export.py +817 -805
  20. openrem/remapp/exports/export_common.py +931 -951
  21. openrem/remapp/exports/export_common_pandas.py +2422 -0
  22. openrem/remapp/exports/exportviews.py +815 -860
  23. openrem/remapp/exports/mg_csv_nhsbsp.py +292 -292
  24. openrem/remapp/exports/mg_export.py +673 -510
  25. openrem/remapp/exports/nm_export.py +796 -575
  26. openrem/remapp/exports/rf_export.py +1418 -1431
  27. openrem/remapp/extractors/ct_philips.py +424 -414
  28. openrem/remapp/extractors/ct_toshiba.py +2116 -2108
  29. openrem/remapp/extractors/dx.py +1033 -952
  30. openrem/remapp/extractors/extract_common.py +817 -817
  31. openrem/remapp/extractors/import_views.py +426 -426
  32. openrem/remapp/extractors/mam.py +685 -672
  33. openrem/remapp/extractors/nm_image.py +439 -431
  34. openrem/remapp/extractors/ptsizecsv2db.py +368 -368
  35. openrem/remapp/extractors/rdsr.py +667 -654
  36. openrem/remapp/extractors/rdsr_methods.py +1771 -1768
  37. openrem/remapp/extractors/rrdsr_methods.py +630 -622
  38. openrem/remapp/fixtures/openskin_safelist.json +11 -11
  39. openrem/remapp/forms.py +2286 -2277
  40. openrem/remapp/interface/chart_functions.py +2412 -2393
  41. openrem/remapp/interface/mod_filters.py +1241 -1243
  42. openrem/remapp/migrations/0001_initial.py.1-0-upgrade +1043 -1043
  43. openrem/remapp/models.py +3418 -3407
  44. openrem/remapp/netdicom/dicomviews.py +681 -683
  45. openrem/remapp/netdicom/qrscu.py +2646 -2646
  46. openrem/remapp/netdicom/tools.py +134 -134
  47. openrem/remapp/static/css/bootstrap-theme.css +587 -587
  48. openrem/remapp/static/css/bootstrap-theme.min.css +4 -4
  49. openrem/remapp/static/css/bootstrap.css +6800 -6800
  50. openrem/remapp/static/css/bootstrap.min.css +4 -4
  51. openrem/remapp/static/css/datepicker3.css +790 -790
  52. openrem/remapp/static/css/jquery.qtip.min.css +2 -2
  53. openrem/remapp/static/css/openrem-extra.css +442 -442
  54. openrem/remapp/static/css/openrem.css +96 -96
  55. openrem/remapp/static/css/registration.css +34 -34
  56. openrem/remapp/static/fonts/glyphicons-halflings-regular.svg +287 -287
  57. openrem/remapp/static/js/bootstrap-datepicker.js +1671 -1671
  58. openrem/remapp/static/js/bootstrap.js +2363 -2363
  59. openrem/remapp/static/js/bootstrap.min.js +6 -6
  60. openrem/remapp/static/js/charts/chartCommonFunctions.js +75 -75
  61. openrem/remapp/static/js/charts/chartFullScreen.js +41 -41
  62. openrem/remapp/static/js/charts/ctChartAjax.js +331 -331
  63. openrem/remapp/static/js/charts/dxChartAjax.js +290 -290
  64. openrem/remapp/static/js/charts/mgChartAjax.js +144 -144
  65. openrem/remapp/static/js/charts/nmChartAjax.js +64 -64
  66. openrem/remapp/static/js/charts/plotly-2.35.2.min.js +8 -0
  67. openrem/remapp/static/js/charts/rfChartAjax.js +128 -128
  68. openrem/remapp/static/js/chroma.min.js +32 -32
  69. openrem/remapp/static/js/datepicker.js +5 -5
  70. openrem/remapp/static/js/dicom.js +115 -115
  71. openrem/remapp/static/js/django_reverse/reverse.js +13 -13
  72. openrem/remapp/static/js/formatDate.js +7 -7
  73. openrem/remapp/static/js/html5shiv.min.js +8 -8
  74. openrem/remapp/static/js/jquery-1.11.0.min.js +4 -4
  75. openrem/remapp/static/js/npm.js +12 -12
  76. openrem/remapp/static/js/respond.min.js +4 -4
  77. openrem/remapp/static/js/skin-dose-maps/jquery.qtip.min.js +4 -4
  78. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dHUDObject.js +112 -112
  79. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dObject.js +367 -367
  80. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMap3dPersonObject.js +158 -158
  81. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapColourScaleObject.js +153 -153
  82. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapObject.js +367 -367
  83. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping.js +584 -584
  84. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMapping3d.js +255 -255
  85. openrem/remapp/static/js/skin-dose-maps/rfSkinDoseMappingAjax.js +267 -212
  86. openrem/remapp/static/js/skin-dose-maps/three.min.js +835 -835
  87. openrem/remapp/static/js/sorttable.js +495 -495
  88. openrem/remapp/templates/base.html +253 -253
  89. openrem/remapp/templates/registration/changepassword.html +25 -25
  90. openrem/remapp/templates/registration/changepassworddone.html +12 -12
  91. openrem/remapp/templates/registration/login.html +42 -42
  92. openrem/remapp/templates/remapp/backgroundtaskmaximumrows_form.html +29 -29
  93. openrem/remapp/templates/remapp/base.html +1 -1
  94. openrem/remapp/templates/remapp/ctdetail.html +235 -235
  95. openrem/remapp/templates/remapp/ctfiltered.html +310 -310
  96. openrem/remapp/templates/remapp/dicomdeletesettings_form.html +31 -31
  97. openrem/remapp/templates/remapp/dicomqr.html +147 -147
  98. openrem/remapp/templates/remapp/dicomquerydetails.html +83 -83
  99. openrem/remapp/templates/remapp/dicomqueryimages.html +49 -49
  100. openrem/remapp/templates/remapp/dicomqueryseries.html +109 -109
  101. openrem/remapp/templates/remapp/dicomquerysummary.html +48 -48
  102. openrem/remapp/templates/remapp/dicomremoteqr_confirm_delete.html +60 -60
  103. openrem/remapp/templates/remapp/dicomremoteqr_form.html +32 -32
  104. openrem/remapp/templates/remapp/dicomstorescp_confirm_delete.html +53 -53
  105. openrem/remapp/templates/remapp/dicomstorescp_form.html +48 -48
  106. openrem/remapp/templates/remapp/dicomsummary.html +257 -257
  107. openrem/remapp/templates/remapp/displaychartoptions.html +184 -184
  108. openrem/remapp/templates/remapp/displayhomepageoptions.html +57 -57
  109. openrem/remapp/templates/remapp/displayname-count.html +6 -6
  110. openrem/remapp/templates/remapp/displayname-last-date.html +3 -3
  111. openrem/remapp/templates/remapp/displayname-modality.html +86 -105
  112. openrem/remapp/templates/remapp/displayname-skinmap.html +18 -18
  113. openrem/remapp/templates/remapp/displaynameupdate.html +100 -100
  114. openrem/remapp/templates/remapp/displaynameview.html +222 -219
  115. openrem/remapp/templates/remapp/dxdetail.html +176 -176
  116. openrem/remapp/templates/remapp/dxfiltered.html +324 -324
  117. openrem/remapp/templates/remapp/exports-active.html +25 -25
  118. openrem/remapp/templates/remapp/exports-complete.html +35 -35
  119. openrem/remapp/templates/remapp/exports-error.html +26 -26
  120. openrem/remapp/templates/remapp/exports-queue.html +18 -18
  121. openrem/remapp/templates/remapp/exports.html +191 -191
  122. openrem/remapp/templates/remapp/failed_summary_list.html +27 -27
  123. openrem/remapp/templates/remapp/filteredbase.html +162 -162
  124. openrem/remapp/templates/remapp/highdosemetricalertsettings_form.html +76 -76
  125. openrem/remapp/templates/remapp/home-list-modalities.html +94 -94
  126. openrem/remapp/templates/remapp/home.html +202 -202
  127. openrem/remapp/templates/remapp/list_filters.html +24 -24
  128. openrem/remapp/templates/remapp/mgdetail.html +160 -138
  129. openrem/remapp/templates/remapp/mgfiltered.html +311 -311
  130. openrem/remapp/templates/remapp/nmdetail.html +300 -300
  131. openrem/remapp/templates/remapp/nmfiltered.html +255 -255
  132. openrem/remapp/templates/remapp/notpatient.html +190 -190
  133. openrem/remapp/templates/remapp/notpatientindicators_form_base.html +81 -81
  134. openrem/remapp/templates/remapp/notpatientindicatorsid_confirm_delete.html +54 -54
  135. openrem/remapp/templates/remapp/notpatientindicatorsid_form.html +23 -23
  136. openrem/remapp/templates/remapp/notpatientindicatorsname_confirm_delete.html +54 -54
  137. openrem/remapp/templates/remapp/notpatientindicatorsname_form.html +23 -23
  138. openrem/remapp/templates/remapp/notpatientindicatorsname_form_base.html +85 -85
  139. openrem/remapp/templates/remapp/openskinsafelist_add.html +130 -130
  140. openrem/remapp/templates/remapp/openskinsafelist_confirm_delete.html +100 -100
  141. openrem/remapp/templates/remapp/openskinsafelist_form.html +207 -207
  142. openrem/remapp/templates/remapp/patientidsettings_form.html +83 -83
  143. openrem/remapp/templates/remapp/populate_summary_progress.html +83 -83
  144. openrem/remapp/templates/remapp/populate_summary_progress_error.html +36 -36
  145. openrem/remapp/templates/remapp/review_failed_imports.html +157 -157
  146. openrem/remapp/templates/remapp/review_failed_study.html +41 -41
  147. openrem/remapp/templates/remapp/review_studies_delete_button.html +20 -20
  148. openrem/remapp/templates/remapp/review_study.html +19 -19
  149. openrem/remapp/templates/remapp/review_summary_list.html +245 -245
  150. openrem/remapp/templates/remapp/rf_dose_alert_email_template.html +14 -1
  151. openrem/remapp/templates/remapp/rfalertnotificationsview.html +59 -59
  152. openrem/remapp/templates/remapp/rfdetail.html +547 -543
  153. openrem/remapp/templates/remapp/rfdetailbase.html +18 -18
  154. openrem/remapp/templates/remapp/rffiltered.html +404 -404
  155. openrem/remapp/templates/remapp/sizeimports.html +119 -119
  156. openrem/remapp/templates/remapp/sizeprocess.html +96 -96
  157. openrem/remapp/templates/remapp/sizeupload.html +110 -110
  158. openrem/remapp/templates/remapp/skindosemapcalcsettings_form.html +28 -28
  159. openrem/remapp/templates/remapp/standardname-modality.html +69 -69
  160. openrem/remapp/templates/remapp/standardnames_confirm_delete.html +71 -71
  161. openrem/remapp/templates/remapp/standardnames_form.html +87 -87
  162. openrem/remapp/templates/remapp/standardnamesettings_form.html +41 -41
  163. openrem/remapp/templates/remapp/standardnamesrefreshall.html +92 -92
  164. openrem/remapp/templates/remapp/standardnameview.html +103 -103
  165. openrem/remapp/templates/remapp/study_confirm_delete.html +147 -147
  166. openrem/remapp/templates/remapp/task_admin.html +265 -265
  167. openrem/remapp/templates/remapp/tasks.html +76 -76
  168. openrem/remapp/templatetags/formfilters.py +13 -13
  169. openrem/remapp/templatetags/proper_paginate.py +38 -38
  170. openrem/remapp/templatetags/remappduration.py +36 -36
  171. openrem/remapp/templatetags/sigdig.py +38 -38
  172. openrem/remapp/templatetags/sort_class_property_value.py +15 -15
  173. openrem/remapp/templatetags/update_variable.py +20 -20
  174. openrem/remapp/templatetags/url_replace.py +25 -25
  175. openrem/remapp/tests/test_charts_common.py +202 -202
  176. openrem/remapp/tests/test_charts_ct.py +7111 -7111
  177. openrem/remapp/tests/test_charts_dx.py +3513 -3513
  178. openrem/remapp/tests/test_charts_mg.py +1116 -1115
  179. openrem/remapp/tests/test_dcmdatetime.py +189 -189
  180. openrem/remapp/tests/test_dicom_qr.py +2580 -2580
  181. openrem/remapp/tests/test_display_name.py +274 -274
  182. openrem/remapp/tests/test_export_ct_xlsx.py +272 -248
  183. openrem/remapp/tests/test_export_dx_xlsx.py +137 -134
  184. openrem/remapp/tests/test_export_mammo_csv.py +242 -242
  185. openrem/remapp/tests/test_export_rf_xlsx.py +246 -246
  186. openrem/remapp/tests/test_files/DX-Im-DRGEM.dcm +0 -0
  187. openrem/remapp/tests/test_files/MG-RDSR-GEPristina-2D.dcm +0 -0
  188. openrem/remapp/tests/test_files/MG-RDSR-GEPristina-DBT.dcm +0 -0
  189. openrem/remapp/tests/test_files/MG-RDSR-Giotto-DBT.dcm +0 -0
  190. openrem/remapp/tests/test_files/skin_map_alphenix.py +590 -590
  191. openrem/remapp/tests/test_files/skin_map_zee.py +354 -354
  192. openrem/remapp/tests/test_filters_ct.py +321 -321
  193. openrem/remapp/tests/test_filters_dx.py +92 -92
  194. openrem/remapp/tests/test_filters_mammo.py +183 -183
  195. openrem/remapp/tests/test_filters_rf.py +118 -118
  196. openrem/remapp/tests/test_get_values.py +72 -72
  197. openrem/remapp/tests/test_hash_id.py +65 -65
  198. openrem/remapp/tests/test_import_ct_esr_ge.py +3034 -3034
  199. openrem/remapp/tests/test_import_ct_philips_rdsr.py +42 -42
  200. openrem/remapp/tests/test_import_ct_rdsr_multiple.py +256 -256
  201. openrem/remapp/tests/test_import_ct_rdsr_siemens.py +827 -827
  202. openrem/remapp/tests/test_import_ct_rdsr_spectrumdynamics.py +91 -91
  203. openrem/remapp/tests/test_import_ct_rdsr_toshiba_dosecheck.py +67 -67
  204. openrem/remapp/tests/test_import_ct_rdsr_toshiba_multivaluesd.py +33 -33
  205. openrem/remapp/tests/test_import_ct_rdsr_toshiba_pixelmed.py +118 -118
  206. openrem/remapp/tests/test_import_ct_sc_philips.py +44 -44
  207. openrem/remapp/tests/test_import_dual_rdsr.py +110 -110
  208. openrem/remapp/tests/test_import_dx.py +1267 -1191
  209. openrem/remapp/tests/test_import_dx_rdsr.py +1250 -1253
  210. openrem/remapp/tests/test_import_mam.py +438 -438
  211. openrem/remapp/tests/test_import_mg_im_hol_proj.py +46 -46
  212. openrem/remapp/tests/test_import_mg_rdsr.py +586 -586
  213. openrem/remapp/tests/test_import_nm_image.py +420 -420
  214. openrem/remapp/tests/test_import_nm_siemens_rdsr.py +396 -396
  215. openrem/remapp/tests/test_import_px.py +161 -161
  216. openrem/remapp/tests/test_import_rf_rdsr.py +420 -418
  217. openrem/remapp/tests/test_missing_date.py +42 -42
  218. openrem/remapp/tests/test_not_patient.py +60 -60
  219. openrem/remapp/tests/test_openskin.py +272 -272
  220. openrem/remapp/tests/test_patient_id_settings.py +72 -72
  221. openrem/remapp/tests/test_pt_size_import.py +232 -232
  222. openrem/remapp/tests/test_rf_detail.py +113 -113
  223. openrem/remapp/tests/test_rf_high_dose_alert.py +361 -361
  224. openrem/remapp/tools/background.py +361 -361
  225. openrem/remapp/tools/check_standard_name_status.py +47 -0
  226. openrem/remapp/tools/check_uid.py +70 -70
  227. openrem/remapp/tools/dcmdatetime.py +248 -248
  228. openrem/remapp/tools/default_import.py +44 -47
  229. openrem/remapp/tools/get_values.py +230 -230
  230. openrem/remapp/tools/hash_id.py +58 -58
  231. openrem/remapp/tools/make_skin_map.py +448 -406
  232. openrem/remapp/tools/not_patient_indicators.py +72 -72
  233. openrem/remapp/tools/openskin/calc_exp_map.py +173 -173
  234. openrem/remapp/tools/openskin/geomclass.py +475 -475
  235. openrem/remapp/tools/openskin/geomfunc.py +433 -432
  236. openrem/remapp/tools/openskin/skinmap.py +417 -417
  237. openrem/remapp/tools/populate_summary.py +185 -193
  238. openrem/remapp/tools/save_skin_map_structure.py +73 -73
  239. openrem/remapp/tools/send_high_dose_alert_emails.py +238 -207
  240. openrem/remapp/urls.py +456 -448
  241. openrem/remapp/version.py +11 -11
  242. openrem/remapp/views.py +1147 -1052
  243. openrem/remapp/views_admin.py +3876 -3936
  244. openrem/remapp/views_charts_ct.py +2110 -2058
  245. openrem/remapp/views_charts_dx.py +1906 -1836
  246. openrem/remapp/views_charts_mg.py +1349 -1196
  247. openrem/remapp/views_charts_nm.py +535 -535
  248. openrem/remapp/views_charts_rf.py +1219 -1241
  249. openrem/remapp/views_openskin.py +379 -384
  250. openrem/sample-config/openrem-consumer.service +12 -12
  251. openrem/sample-config/openrem-gunicorn.service +13 -13
  252. openrem/sample-config/openrem-server +14 -13
  253. openrem/sample-config/openrem_orthanc_config_linux.lua +454 -454
  254. openrem/sample-config/openrem_orthanc_config_windows.lua +455 -455
  255. openrem/sample-config/queue-init.bat +73 -73
  256. openrem/scripts/openrem_ctphilips.py +25 -25
  257. openrem/scripts/openrem_cttoshiba.py +28 -28
  258. openrem/scripts/openrem_dx.py +22 -22
  259. openrem/scripts/openrem_mg.py +22 -22
  260. openrem/scripts/openrem_nm.py +22 -22
  261. openrem/scripts/openrem_ptsizecsv.py +17 -17
  262. openrem/scripts/openrem_qr.py +12 -12
  263. openrem/scripts/openrem_rdsr.py +25 -25
  264. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/METADATA +39 -29
  265. openrem-1.0.0b3.dist-info/RECORD +379 -0
  266. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info}/WHEEL +1 -1
  267. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/COPYING-GPLv3 +674 -674
  268. {OpenREM-1.0.0b2.dist-info → openrem-1.0.0b3.dist-info/licenses}/LICENSE +22 -22
  269. OpenREM-1.0.0b2.dist-info/RECORD +0 -373
  270. openrem/remapp/static/js/charts/plotly-2.17.1.min.js +0 -8
  271. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ctphilips.py +0 -0
  272. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_cttoshiba.py +0 -0
  273. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_dx.py +0 -0
  274. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_mg.py +0 -0
  275. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_nm.py +0 -0
  276. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_ptsizecsv.py +0 -0
  277. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_qr.py +0 -0
  278. {OpenREM-1.0.0b2.data → openrem-1.0.0b3.data}/scripts/openrem_rdsr.py +0 -0
  279. {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
- "NominalSingleCollimationWidth"
268
- ] = float(dcm[0x7005, 0x1008].value)
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
- "ExposureModulationType"
347
- ] = dcm.ExposureModulationType
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
- "StudyDescription"
389
- ] = dcm.StudyDescription
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
- "StudyDescription"
405
- ] = dcm.ProcedureCodeSequence[0].CodeMeaning
406
- except KeyError:
407
- # study_info['StudyDescription'] isn't present, so add it
408
- study_info[
409
- "StudyDescription"
410
- ] = dcm.ProcedureCodeSequence[0].CodeMeaning
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
- "RequestedProcedureDescription"
428
- ] = dcm.RequestedProcedureDescription
429
- except KeyError:
430
- # study_info['RequestedProcedureDescription'] doesn't exist yet, so create it
431
- study_info[
432
- "RequestedProcedureDescription"
433
- ] = dcm.RequestedProcedureDescription
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
- "RequestedProcedureDescription"
449
- ] = dcm.ProcedureCodeSequence[0].CodeMeaning
450
- except KeyError:
451
- # study_info['RequestedProcedureDescription'] isn't present, so add it
452
- study_info[
453
- "RequestedProcedureDescription"
454
- ] = dcm.ProcedureCodeSequence[0].CodeMeaning
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
- "SoftwareVersions"
470
- ] = dcm.SoftwareVersions
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
- "SoftwareVersions"
486
- ] = dcm.DeviceSerialNumber
487
- except KeyError:
488
- # study_info['DeviceSerialNumber'] doesn't exist yet, so create it
489
- study_info[
490
- "DeviceSerialNumber"
491
- ] = dcm.DeviceSerialNumber
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 TypeError:
1341
- # ContentSequence doesn't exist, so add it
1342
- container2b.ContentSequence = Sequence(
1343
- [
1344
- source_container
1345
- ]
1346
- )
1347
- except Exception:
1348
- logger.debug(
1349
- traceback.format_exc()
1350
- )
1351
-
1352
- source_parameters_exists = (
1353
- True
1354
- )
1355
- kvp_data_exists = (
1356
- True
1357
- )
1358
-
1359
- elif (
1360
- not kvp_data_exists
1361
- ):
1362
- # CT X-ray Source Parameters exists, but there is no kVp data
1363
- for (
1364
- container3b
1365
- ) in (
1366
- container2b
1367
- ):
1368
- for container4b in container3b:
1369
- try:
1370
- if (
1371
- container4b.ConceptNameCodeSequence[
1372
- 0
1373
- ].CodeMeaning
1374
- == "CT X-Ray Source Parameters"
1375
- ):
1376
- # Create the kVp container that will go in to the x-ray source container
1377
- kvp_container = (
1378
- Dataset()
1379
- )
1380
- kvp_container.RelationshipType = "CONTAINS"
1381
- kvp_container.ValueType = "NUM"
1382
- coding2 = (
1383
- Dataset()
1384
- )
1385
- coding2.CodeValue = "113733"
1386
- coding2.CodingSchemeDesignator = "DCM"
1387
- coding2.CodeMeaning = "KVP"
1388
- kvp_container.ConceptNameCodeSequence = Sequence(
1389
- [
1390
- coding2
1391
- ]
1392
- )
1393
- coding3 = (
1394
- Dataset()
1395
- )
1396
- coding3.CodeValue = "kV"
1397
- coding3.CodingSchemeDesignator = "UCUM"
1398
- coding3.CodingSchemeVersion = "1.4"
1399
- coding3.CodeMeaning = "kV"
1400
- measurement_units_container = (
1401
- Dataset()
1402
- )
1403
- measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
1404
- [
1405
- coding3
1406
- ]
1407
- )
1408
- measurement_units_container.NumericValue = val
1409
- measured_value_sequence = Sequence(
1410
- [
1411
- measurement_units_container
1412
- ]
1413
- )
1414
- kvp_container.MeasuredValueSequence = measured_value_sequence
1415
-
1416
- # Add the kVp container inside the x-ray source container
1417
- container4b.ContentSequence.append(
1418
- kvp_container
1419
- )
1420
-
1421
- kvp_data_exists = True
1422
-
1423
- except AttributeError:
1424
- # Likely there's no ConceptNameCodeSequence attribute
1425
- pass
1426
- except Exception:
1427
- logger.debug(
1428
- traceback.format_exc()
1429
- )
1430
-
1431
- if key == "ExposureTime":
1432
- # First, check if there is already an exposure time per rotation value in an x-ray source parameters container inside
1433
- # a CT Acquisition Parameters container.
1434
- source_parameters_exists = False
1435
- exposure_time_per_rotation_data_exists = (
1436
- False
1437
- )
1438
-
1439
- for (
1440
- container2b
1441
- ) in container.ContentSequence:
1442
- if (
1443
- container2b.ValueType
1444
- == "CONTAINER"
1445
- ):
1446
- if (
1447
- container2b.ConceptNameCodeSequence[
1448
- 0
1449
- ].CodeMeaning
1450
- == "CT Acquisition Parameters"
1451
- ):
1452
- for (
1453
- container3b
1454
- ) in container2b:
1455
- for (
1456
- container4b
1457
- ) in (
1458
- container3b
1459
- ):
1460
- try:
1461
- if (
1462
- container4b.ConceptNameCodeSequence[
1463
- 0
1464
- ].CodeMeaning
1465
- == "CT X-Ray Source Parameters"
1466
- ):
1467
- source_parameters_exists = True
1468
-
1469
- for container5b in container4b:
1470
- try:
1471
- if (
1472
- container5b[
1473
- 0
1474
- ]
1475
- .ConceptNameCodeSequence[
1476
- 0
1477
- ]
1478
- .CodeValue
1479
- == "113843"
1480
- ):
1481
- exposure_time_per_rotation_data_exists = True
1482
- except AttributeError:
1483
- pass
1484
- except Exception:
1485
- logger.debug(
1486
- traceback.format_exc()
1487
- )
1488
- except AttributeError:
1489
- # Likely there's no ConceptNameCodeSequence attribute
1490
- pass
1491
- except Exception:
1492
- logger.debug(
1493
- traceback.format_exc()
1494
- )
1495
-
1496
- if (
1497
- not source_parameters_exists
1498
- ):
1499
- # There is no x-ray source parameters section, so add it
1500
- # Create the x-ray source container
1501
- source_container = (
1502
- Dataset()
1503
- )
1504
- source_container.RelationshipType = (
1505
- "CONTAINS"
1506
- )
1507
- source_container.ValueType = (
1508
- "CONTAINER"
1509
- )
1510
- coding = (
1511
- Dataset()
1512
- )
1513
- coding.CodeValue = (
1514
- "113831"
1515
- )
1516
- coding.CodingSchemeDesignator = (
1517
- "DCM"
1518
- )
1519
- coding.CodeMeaning = "CT X-Ray Source Parameters"
1520
- source_container.ConceptNameCodeSequence = Sequence(
1521
- [coding]
1522
- )
1523
-
1524
- # Create the kVp container that will go in to the x-ray source container
1525
- exposure_time_per_rotation_container = (
1526
- Dataset()
1527
- )
1528
- exposure_time_per_rotation_container.RelationshipType = (
1529
- "CONTAINS"
1530
- )
1531
- exposure_time_per_rotation_container.ValueType = (
1532
- "NUM"
1533
- )
1534
- coding2 = (
1535
- Dataset()
1536
- )
1537
- coding2.CodeValue = (
1538
- "113843"
1539
- )
1540
- coding2.CodingSchemeDesignator = (
1541
- "DCM"
1542
- )
1543
- coding2.CodeMeaning = "Exposure Time per Rotation"
1544
- exposure_time_per_rotation_container.ConceptNameCodeSequence = Sequence(
1545
- [coding2]
1546
- )
1547
- coding3 = (
1548
- Dataset()
1549
- )
1550
- coding3.CodeValue = (
1551
- "s"
1552
- )
1553
- coding3.CodingSchemeDesignator = (
1554
- "UCUM"
1555
- )
1556
- coding3.CodingSchemeVersion = (
1557
- "1.4"
1558
- )
1559
- coding3.CodeMeaning = (
1560
- "s"
1561
- )
1562
- measurement_units_container = (
1563
- Dataset()
1564
- )
1565
- measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
1566
- [coding3]
1567
- )
1568
- measurement_units_container.NumericValue = str(
1569
- float(val)
1570
- / 1000
1571
- )
1572
- measured_value_sequence = Sequence(
1573
- [
1574
- measurement_units_container
1575
- ]
1576
- )
1577
- exposure_time_per_rotation_container.MeasuredValueSequence = measured_value_sequence
1578
-
1579
- # Put the exposure time per rotation container inside the x-ray source container
1580
- source_container.ContentSequence = Sequence(
1581
- [
1582
- exposure_time_per_rotation_container
1583
- ]
1584
- )
1585
-
1586
- # Add the source_container to the rdsr contents
1587
- try:
1588
- # Append it to an existing ContentSequence
1589
- container2b.ContentSequence.append(
1590
- source_container
1591
- )
1592
- except TypeError:
1593
- # ContentSequence doesn't exist, so add it
1594
- container2b.ContentSequence = Sequence(
1595
- [
1596
- source_container
1597
- ]
1598
- )
1599
- except Exception:
1600
- logger.debug(
1601
- traceback.format_exc()
1602
- )
1603
-
1604
- source_parameters_exists = (
1605
- True
1606
- )
1607
- exposure_time_per_rotation_data_exists = (
1608
- True
1609
- )
1610
-
1611
- elif (
1612
- not exposure_time_per_rotation_data_exists
1613
- ):
1614
- # CT X-ray Source Parameters exists, but there is no exposure time per rotation data
1615
- for (
1616
- container3b
1617
- ) in (
1618
- container2b
1619
- ):
1620
- for container4b in container3b:
1621
- try:
1622
- if (
1623
- container4b.ConceptNameCodeSequence[
1624
- 0
1625
- ].CodeMeaning
1626
- == "CT X-Ray Source Parameters"
1627
- ):
1628
- # Create the exposure time per rotation container that will go in to the x-ray source container
1629
- exposure_time_per_rotation_container = (
1630
- Dataset()
1631
- )
1632
- exposure_time_per_rotation_container.RelationshipType = "CONTAINS"
1633
- exposure_time_per_rotation_container.ValueType = "NUM"
1634
- coding2 = (
1635
- Dataset()
1636
- )
1637
- coding2.CodeValue = "113843"
1638
- coding2.CodingSchemeDesignator = "DCM"
1639
- coding2.CodeMeaning = "Exposure Time per Rotation"
1640
- exposure_time_per_rotation_container.ConceptNameCodeSequence = Sequence(
1641
- [
1642
- coding2
1643
- ]
1644
- )
1645
- coding3 = (
1646
- Dataset()
1647
- )
1648
- coding3.CodeValue = "s"
1649
- coding3.CodingSchemeDesignator = "UCUM"
1650
- coding3.CodingSchemeVersion = "1.4"
1651
- coding3.CodeMeaning = "s"
1652
- measurement_units_container = (
1653
- Dataset()
1654
- )
1655
- measurement_units_container.MeasurementUnitsCodeSequence = Sequence(
1656
- [
1657
- coding3
1658
- ]
1659
- )
1660
- measurement_units_container.NumericValue = str(
1661
- float(
1662
- val
1663
- )
1664
- / 1000
1665
- )
1666
- measured_value_sequence = Sequence(
1667
- [
1668
- measurement_units_container
1669
- ]
1670
- )
1671
- exposure_time_per_rotation_container.MeasuredValueSequence = measured_value_sequence
1672
-
1673
- # Add the exposure time per rotation container inside the x-ray source container
1674
- container4b.ContentSequence.append(
1675
- exposure_time_per_rotation_container
1676
- )
1677
-
1678
- exposure_time_per_rotation_data_exists = True
1679
-
1680
- except AttributeError:
1681
- # Likely there's no ConceptNameCodeSequence attribute
1682
- pass
1683
- except Exception:
1684
- logger.debug(
1685
- traceback.format_exc()
1686
- )
1687
- except KeyError as e:
1688
- logger.debug(traceback.format_exc())
1689
- except Exception as e:
1690
- logger.debug(traceback.format_exc())
1691
- # The end of updating the RDSR
1692
- ##############################################
1693
- except KeyError:
1694
- # Either CTDIvol or DLP data is not present
1695
- pass
1696
- except ValueError:
1697
- # Perhaps the contents of the DLP or CTDIvol are not values
1698
- pass
1699
- except Exception:
1700
- logger.debug(traceback.format_exc())
1701
-
1702
- logger.debug("Updated acquisition data")
1703
- logger.debug("Saving updated RDSR file")
1704
- dcm.save_as(new_rdsr_file)
1705
- logger.debug("Updated RDSR file saved")
1706
- return 1
1707
-
1708
-
1709
- def ct_toshiba(folder_name):
1710
- """Function to create radiation dose structured reports from a folder of dose images.
1711
-
1712
- :param folder_name: Path to folder containing Toshiba DICOM objects - dose summary and images
1713
- """
1714
- import pydicom
1715
-
1716
- rdsr_name = "sr.dcm"
1717
- updated_rdsr_name = "sr_updated.dcm"
1718
- combined_rdsr_name = "sr_combined.dcm"
1719
-
1720
- # Split the folder of images by StudyInstanceUID. This is required because pixelmed.jar will only process the
1721
- # first dose summary image it finds. Splitting the files by StudyInstanceUID should mean that there is only one
1722
- # dose summary per folder. N.B. I think Conquest may do this by default with incoming DICOM objects. This
1723
- # routine also renames the files using integer file names to ensure that they are accepted by dcmmkdir later on.
1724
- logger.debug(
1725
- "Splitting into folders by StudyInstanceUID for {0}".format(folder_name)
1726
- )
1727
- folders = _split_by_studyinstanceuid(folder_name)
1728
- logger.debug(
1729
- "Splitting into folders by StudyInstanceUID complete for {0}".format(
1730
- folder_name
1731
- )
1732
- )
1733
-
1734
- # Obtain additional information from the image tags in each folder and add this information to the RDSR file.
1735
- for folder in folders:
1736
-
1737
- # Check to see if there's just one dose summary in the folder
1738
- dose_summary_object_info = _find_dose_summary_objects(folder)
1739
- logger.debug("dose_summary_object_info is {0}".format(dose_summary_object_info))
1740
-
1741
- # For Toshiba scanners each dose summary consists of two objects
1742
- number_of_dose_summary_objects = len(dose_summary_object_info) // 2
1743
-
1744
- if number_of_dose_summary_objects > 1:
1745
- # There's more than one pair of dose summary objects, so duplicate the
1746
- # contents of folder number_of_dose_summary_objects times.
1747
- unique_study_times = {
1748
- item["studyTime"] for item in dose_summary_object_info
1749
- }
1750
- subfolder_paths = []
1751
- for unique_study_time in unique_study_times:
1752
- subfolder_path = os.path.join(folder, unique_study_time)
1753
- subfolder_paths.append(subfolder_path)
1754
- if not os.path.isdir(subfolder_path):
1755
- os.mkdir(subfolder_path)
1756
- _copy_files_from_a_to_b(folder, subfolder_path)
1757
-
1758
- # Delete all but one pair of dose summary objects from each subfolder
1759
- for unique_study_time in unique_study_times:
1760
- for dose_summary_object in dose_summary_object_info:
1761
- if dose_summary_object["studyTime"] != unique_study_time:
1762
- # Delete the corresponding dose_summary_object file from the
1763
- # x subfolder in folder
1764
- os.remove(
1765
- os.path.join(
1766
- folder,
1767
- unique_study_time,
1768
- dose_summary_object["fileName"],
1769
- )
1770
- )
1771
-
1772
- # Now create an RDSR in each subfolder in subfolder_paths
1773
- for sub_folder in subfolder_paths:
1774
- # Create a DICOM RDSR for the sub-folder using pixelmed.jar.
1775
- logger.debug(
1776
- "Trying to make initial DICOM RDSR object in {0}".format(sub_folder)
1777
- )
1778
- combined_command = (
1779
- JAVA_EXE
1780
- + " "
1781
- + JAVA_OPTIONS
1782
- + " "
1783
- + PIXELMED_JAR
1784
- + " "
1785
- + PIXELMED_JAR_OPTIONS
1786
- )
1787
- _make_dicom_rdsr(sub_folder, combined_command, rdsr_name)
1788
- # Check that the initial RDSR exists
1789
- initial_rdsr_name_and_path = os.path.join(sub_folder, rdsr_name)
1790
- if not os.path.isfile(initial_rdsr_name_and_path):
1791
- logger.debug(
1792
- "Failed to create initial DICOM RDSR object created in {0}. Skipping.".format(
1793
- sub_folder
1794
- )
1795
- )
1796
- # Remove this sub_folder from subfolder_paths list as it can't be used
1797
- subfolder_paths.remove(sub_folder)
1798
- continue
1799
- logger.debug(
1800
- "Initial DICOM RDSR object created in {0}".format(sub_folder)
1801
- )
1802
-
1803
- logger.debug(
1804
- "Gathering extra information from images in {0}".format(sub_folder)
1805
- )
1806
- extra_information = _find_extra_info(sub_folder)
1807
- extra_study_information = extra_information[0]
1808
- extra_acquisition_information = extra_information[1]
1809
- logger.debug(
1810
- "Gathered extra information from images in {0}".format(sub_folder)
1811
- )
1812
-
1813
- # Use the extra information to update the initial rdsr file created by DoseUtility
1814
- logger.debug("Updating information in rdsr in {0}".format(sub_folder))
1815
- updated_rdsr_name_and_path = os.path.join(sub_folder, updated_rdsr_name)
1816
- result = _update_dicom_rdsr(
1817
- initial_rdsr_name_and_path,
1818
- extra_study_information,
1819
- extra_acquisition_information,
1820
- updated_rdsr_name_and_path,
1821
- )
1822
- # Check that the updated RDSR exists
1823
- if result == 1:
1824
- logger.debug("Updated information in rdsr")
1825
- else:
1826
- logger.debug(
1827
- "Failed to update the initial DICOM RDSR object in {0}. Skipping.".format(
1828
- sub_folder
1829
- )
1830
- )
1831
- # Remove this sub_folder from subfolder_paths list as it can't be used
1832
- subfolder_paths.remove(sub_folder)
1833
- continue
1834
-
1835
- # Now combine the data contained in the RDSRs
1836
- first_subfolder = True
1837
- for sub_folder in subfolder_paths:
1838
- if first_subfolder == True:
1839
- shutil.copy(
1840
- os.path.join(sub_folder, updated_rdsr_name),
1841
- os.path.join(folder, combined_rdsr_name),
1842
- )
1843
- combined_rdsr = pydicom.dcmread(
1844
- os.path.join(folder, combined_rdsr_name)
1845
- )
1846
- for content_sequence in combined_rdsr.ContentSequence:
1847
- if (
1848
- content_sequence.ConceptNameCodeSequence[0].CodeMeaning
1849
- == "CT Accumulated Dose Data"
1850
- ):
1851
- combined_rdsr_accumulated_dose_data = content_sequence
1852
- first_subfolder = False
1853
- else:
1854
- current_rdsr = pydicom.dcmread(
1855
- os.path.join(sub_folder, updated_rdsr_name)
1856
- )
1857
- for content_sequence in current_rdsr.ContentSequence:
1858
- if (
1859
- content_sequence.ConceptNameCodeSequence[0].CodeMeaning
1860
- == "CT Acquisition"
1861
- ):
1862
- combined_rdsr.ContentSequence.append(content_sequence)
1863
- if (
1864
- content_sequence.ConceptNameCodeSequence[0].CodeMeaning
1865
- == "CT Accumulated Dose Data"
1866
- ):
1867
- # Total Number of Irradiation Events
1868
- logger.debug(
1869
- "Trying to update total number of irradiation events"
1870
- )
1871
- for item in content_sequence.ContentSequence:
1872
- if (
1873
- item.ConceptNameCodeSequence[0].CodeMeaning
1874
- == "Total Number of Irradiation Events"
1875
- ):
1876
- additional_amount = item.MeasuredValueSequence[
1877
- 0
1878
- ].NumericValue
1879
- logger.debug(
1880
- "Found extra events: {0}".format(
1881
- additional_amount
1882
- )
1883
- )
1884
- for (
1885
- item
1886
- ) in combined_rdsr_accumulated_dose_data.ContentSequence:
1887
- if (
1888
- item.ConceptNameCodeSequence[0].CodeMeaning
1889
- == "Total Number of Irradiation Events"
1890
- ):
1891
- logger.debug(
1892
- "Found current events: {0}".format(
1893
- item.MeasuredValueSequence[0].NumericValue
1894
- )
1895
- )
1896
- item.MeasuredValueSequence[0].NumericValue = str(
1897
- int(
1898
- item.MeasuredValueSequence[0].NumericValue
1899
- + additional_amount
1900
- )
1901
- )
1902
- logger.debug(
1903
- "Updated to: {0}".format(
1904
- item.MeasuredValueSequence[0].NumericValue
1905
- )
1906
- )
1907
-
1908
- # CT Dose Length Product Total
1909
- logger.debug("Trying to update total DLP")
1910
- extra_ct_dlp_total_content_sequence = None
1911
- for content_seq in content_sequence.ContentSequence:
1912
- if (
1913
- content_seq.ConceptNameCodeSequence[0].CodeMeaning
1914
- == "CT Dose Length Product Total"
1915
- ):
1916
- additional_amount = (
1917
- content_seq.MeasuredValueSequence[
1918
- 0
1919
- ].NumericValue
1920
- )
1921
- logger.debug(
1922
- "Found extra DLP: {0}".format(additional_amount)
1923
- )
1924
- extra_ct_dlp_total_content_sequence = content_seq
1925
- # If the total DLP is not zero
1926
- if float(additional_amount):
1927
- # Find out which dosimetry phantom this value is for
1928
- for cont_seq in content_seq.ContentSequence:
1929
- if (
1930
- cont_seq.ConceptNameCodeSequence[
1931
- 0
1932
- ].CodeMeaning
1933
- == "CTDIw Phantom Type"
1934
- ):
1935
- additional_type = (
1936
- cont_seq.ConceptCodeSequence[
1937
- 0
1938
- ].CodeMeaning
1939
- )
1940
-
1941
- for i, content_seq in enumerate(
1942
- combined_rdsr_accumulated_dose_data.ContentSequence
1943
- ):
1944
- if (
1945
- content_seq.ConceptNameCodeSequence[0].CodeMeaning
1946
- == "CT Dose Length Product Total"
1947
- ):
1948
- current_amount = content_seq.MeasuredValueSequence[
1949
- 0
1950
- ].NumericValue
1951
- logger.debug(
1952
- "Found current total DLP: {0}".format(
1953
- current_amount
1954
- )
1955
- )
1956
- # If the total DLP is not zero
1957
- total_type = None
1958
- if float(current_amount):
1959
- # Find out which dosimetry phantom this value is for
1960
- for cont_seq in content_seq.ContentSequence:
1961
- if (
1962
- cont_seq.ConceptNameCodeSequence[
1963
- 0
1964
- ].CodeMeaning
1965
- == "CTDIw Phantom Type"
1966
- ):
1967
- total_type = (
1968
- cont_seq.ConceptCodeSequence[
1969
- 0
1970
- ].CodeMeaning
1971
- )
1972
-
1973
- if current_amount and additional_amount:
1974
- # If combined_rdsr total DLP and new one use the same dosimetry phantom then just add them together.
1975
- if total_type == additional_type:
1976
- content_seq.MeasuredValueSequence[
1977
- 0
1978
- ].NumericValue = "%.2f" % (
1979
- content_seq.MeasuredValueSequence[
1980
- 0
1981
- ].NumericValue
1982
- + additional_amount
1983
- )
1984
- # If additional DLP is to 16 cm head phantom then divide it by 2 before adding.
1985
- elif "head" in additional_type.lower():
1986
- content_seq.MeasuredValueSequence[
1987
- 0
1988
- ].NumericValue = "%.2f" % (
1989
- content_seq.MeasuredValueSequence[
1990
- 0
1991
- ].NumericValue
1992
- + (additional_amount / 2.0)
1993
- )
1994
- # If current total DLP is to 16 cm head phantom then divide it by 2 before adding the 32 cm additional.
1995
- else:
1996
- content_seq.MeasuredValueSequence[
1997
- 0
1998
- ].NumericValue = "%.2f" % (
1999
- (
2000
- content_seq.MeasuredValueSequence[
2001
- 0
2002
- ].NumericValue
2003
- / 2.0
2004
- )
2005
- + additional_amount
2006
- )
2007
- logger.debug(
2008
- "Updated to: {0}".format(
2009
- content_seq.MeasuredValueSequence[
2010
- 0
2011
- ].NumericValue
2012
- )
2013
- )
2014
- elif current_amount:
2015
- # There is no additional DLP to add
2016
- logger.debug("No additional DLP to add")
2017
- else:
2018
- # There is no current DLP, but we do have extra, so over-write the current content sequence with extra_ct_dlp_total_content_sequence
2019
- logger.debug(
2020
- "Current total DLP is zero. Replacing with extra {0} mGy.cm.".format(
2021
- additional_amount
2022
- )
2023
- )
2024
- combined_rdsr_accumulated_dose_data.ContentSequence[
2025
- i
2026
- ] = extra_ct_dlp_total_content_sequence
2027
-
2028
- if len(subfolder_paths) > 0:
2029
- combined_rdsr.save_as(
2030
- os.path.join(folder, combined_rdsr_name + "_updated")
2031
- )
2032
- logger.debug(
2033
- "Importing updated combined rdsr in to OpenREM ({0})".format(
2034
- os.path.join(folder, combined_rdsr_name + "_updated")
2035
- )
2036
- )
2037
- rdsr(os.path.join(folder, combined_rdsr_name + "_updated"))
2038
- logger.debug("Imported in to OpenREM")
2039
- else:
2040
- logger.debug("RDSRs could not be created for any subfolders")
2041
-
2042
- else:
2043
- # Create a DICOM RDSR for the sub-folder using pixelmed.jar.
2044
- logger.debug(
2045
- "Trying to make initial DICOM RDSR object in {0}".format(folder)
2046
- )
2047
- combined_command = (
2048
- JAVA_EXE
2049
- + " "
2050
- + JAVA_OPTIONS
2051
- + " "
2052
- + PIXELMED_JAR
2053
- + " "
2054
- + PIXELMED_JAR_OPTIONS
2055
- )
2056
- _make_dicom_rdsr(folder, combined_command, rdsr_name)
2057
- # Check that the initial RDSR exists
2058
- initial_rdsr_name_and_path = os.path.join(folder, rdsr_name)
2059
- if os.path.isfile(initial_rdsr_name_and_path):
2060
- logger.debug("Initial DICOM RDSR object created in {0}".format(folder))
2061
-
2062
- logger.debug(
2063
- "Gathering extra information from images in {0}".format(folder)
2064
- )
2065
- extra_information = _find_extra_info(folder)
2066
- extra_study_information = extra_information[0]
2067
- extra_acquisition_information = extra_information[1]
2068
- logger.debug(
2069
- "Gathered extra information from images in {0}".format(folder)
2070
- )
2071
-
2072
- # Use the extra information to update the initial rdsr file created by DoseUtility
2073
- logger.debug("Updating information in rdsr in {0}".format(folder))
2074
- updated_rdsr_name_and_path = os.path.join(folder, updated_rdsr_name)
2075
- result = _update_dicom_rdsr(
2076
- initial_rdsr_name_and_path,
2077
- extra_study_information,
2078
- extra_acquisition_information,
2079
- updated_rdsr_name_and_path,
2080
- )
2081
- logger.debug("Updated information in rdsr")
2082
-
2083
- # Now import the updated rdsr into OpenREM using the Toshiba extractor
2084
- if result == 1:
2085
- logger.debug(
2086
- "Importing updated rdsr in to OpenREM ({0})".format(
2087
- updated_rdsr_name_and_path
2088
- )
2089
- )
2090
- rdsr(updated_rdsr_name_and_path)
2091
- logger.debug("Imported in to OpenREM")
2092
- else:
2093
- logger.debug(
2094
- "Not imported to OpenREM. Result is: {0}".format(result)
2095
- )
2096
- else:
2097
- logger.debug(
2098
- "Failed to create initial DICOM RDSR object created in {0}. Skipping.".format(
2099
- folder
2100
- )
2101
- )
2102
-
2103
- # Now delete the image folder
2104
- logger.debug("Removing study folder")
2105
- shutil.rmtree(folder_name)
2106
- logger.debug("Removing study folder complete")
2107
- logger.debug("Reached end of ct_toshiba routine")
2108
- return 0
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