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,952 +1,1033 @@
1
- # This Python file uses the following encoding: utf-8
2
- # OpenREM - Radiation Exposure Monitoring tools for the physicist
3
- # Copyright (C) 2012,2013 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
- # This file (dx.py) is intended to extract radiation dose related data from
26
- # DX images. It is based on mam.py.
27
- # David Platten, 28/3/2014
28
- #
29
- """
30
- .. module:: dx.
31
- :synopsis: Module to extract radiation dose related data from DX image objects.
32
-
33
- .. moduleauthor:: David Platten, Ed McDonagh
34
-
35
- """
36
- from datetime import datetime
37
- from decimal import Decimal, DecimalException
38
- import logging
39
- import os
40
- from random import random
41
- import sys
42
- from time import sleep
43
-
44
- import django
45
- from django.core.exceptions import ObjectDoesNotExist
46
- import pydicom
47
- from pydicom.valuerep import MultiValue
48
-
49
- from openrem.remapp.tools.background import (
50
- record_task_error_exit,
51
- record_task_related_query,
52
- record_task_info,
53
- )
54
-
55
- from ..tools import check_uid
56
- from ..tools.dcmdatetime import get_date, get_time, make_date_time
57
- from ..tools.get_values import (
58
- get_value_kw,
59
- get_value_num,
60
- get_or_create_cid,
61
- get_seq_code_value,
62
- get_seq_code_meaning,
63
- list_to_string,
64
- )
65
- from ..tools.hash_id import hash_id
66
-
67
- # setup django/OpenREM
68
- basepath = os.path.dirname(__file__)
69
- projectpath = os.path.abspath(os.path.join(basepath, "..", ".."))
70
- if projectpath not in sys.path:
71
- sys.path.insert(1, projectpath)
72
- os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
73
- django.setup()
74
-
75
- from .extract_common import ( # pylint: disable=wrong-import-order, wrong-import-position
76
- get_study_check_dup,
77
- populate_dx_rf_summary,
78
- patient_module_attributes,
79
- add_standard_names,
80
- )
81
- from remapp.models import ( # pylint: disable=wrong-import-order, wrong-import-position
82
- AccumXRayDose,
83
- AccumIntegratedProjRadiogDose,
84
- DicomDeleteSettings,
85
- DoseRelatedDistanceMeasurements,
86
- Exposure,
87
- GeneralEquipmentModuleAttr,
88
- GeneralStudyModuleAttr,
89
- IrradEventXRayData,
90
- IrradEventXRayDetectorData,
91
- IrradEventXRayMechanicalData,
92
- IrradEventXRaySourceData,
93
- Kvp,
94
- PatientIDSettings,
95
- PatientStudyModuleAttr,
96
- ProjectionXRayRadiationDose,
97
- UniqueEquipmentNames,
98
- XrayFilters,
99
- XrayGrid,
100
- )
101
-
102
- logger = logging.getLogger(
103
- "remapp.extractors.dx"
104
- ) # Explicitly named so that it is still handled when using __main__
105
-
106
-
107
- def _xrayfilters(filttype, material, thickmax, thickmin, source):
108
-
109
- filters = XrayFilters.objects.create(irradiation_event_xray_source_data=source)
110
- if filttype:
111
- filter_types = {
112
- "STRIP": {"code": "113650", "meaning": "Strip filter"},
113
- "WEDGE": {"code": "113651", "meaning": "Wedge filter"},
114
- "BUTTERFLY": {"code:": "113652", "meaning": "Butterfly filter"},
115
- "NONE": {"code": "111609", "meaning": "No filter"},
116
- "FLAT": {"code": "113653", "meaning": "Flat filter"},
117
- }
118
- if filttype in filter_types:
119
- filters.xray_filter_type = get_or_create_cid(
120
- filter_types[filttype]["code"], filter_types[filttype]["meaning"]
121
- )
122
- if material:
123
- logger.debug(
124
- f"In _xrayfilters, attempting to match material {material.strip().lower()}"
125
- )
126
- if material.strip().lower() == "molybdenum":
127
- filters.xray_filter_material = get_or_create_cid(
128
- "C-150F9", "Molybdenum or Molybdenum compound"
129
- )
130
- if material.strip().lower() == "rhodium":
131
- filters.xray_filter_material = get_or_create_cid(
132
- "C-167F9", "Rhodium or Rhodium compound"
133
- )
134
- if material.strip().lower() == "silver":
135
- filters.xray_filter_material = get_or_create_cid(
136
- "C-137F9", "Silver or Silver compound"
137
- )
138
- if material.strip().lower() in [
139
- "aluminum",
140
- "aluminium",
141
- ]: # Illegal spelling of Aluminium found in Philips DiDi
142
- filters.xray_filter_material = get_or_create_cid(
143
- "C-120F9", "Aluminum or Aluminum compound"
144
- )
145
- if material.strip().lower() == "copper":
146
- filters.xray_filter_material = get_or_create_cid(
147
- "C-127F9", "Copper or Copper compound"
148
- )
149
- if material.strip().lower() == "niobium":
150
- filters.xray_filter_material = get_or_create_cid(
151
- "C-1190E", "Niobium or Niobium compound"
152
- )
153
- if material.strip().lower() == "europium":
154
- filters.xray_filter_material = get_or_create_cid(
155
- "C-1190F", "Europium or Europium compound"
156
- )
157
- if material.strip().lower() == "lead":
158
- filters.xray_filter_material = get_or_create_cid(
159
- "C-132F9", "Lead or Lead compound"
160
- )
161
- if material.strip().lower() == "tantalum":
162
- filters.xray_filter_material = get_or_create_cid(
163
- "C-156F9", "Tantalum or Tantalum compound"
164
- )
165
- if thickmax is not None and thickmin is not None:
166
- if thickmax < thickmin:
167
- tempmin = thickmax
168
- thickmax = thickmin
169
- thickmin = tempmin
170
- if thickmax is not None:
171
- filters.xray_filter_thickness_maximum = thickmax
172
- if thickmin is not None:
173
- filters.xray_filter_thickness_minimum = thickmin
174
- filters.save()
175
-
176
-
177
- def _xrayfiltersnone(source):
178
- filters = XrayFilters.objects.create(irradiation_event_xray_source_data=source)
179
- filters.xray_filter_type = get_or_create_cid("111609", "No filter")
180
- filters.save()
181
-
182
-
183
- def _xray_filters_multiple(
184
- xray_filter_material,
185
- xray_filter_thickness_maximum,
186
- xray_filter_thickness_minimum,
187
- source,
188
- ):
189
- for i, material in enumerate(xray_filter_material):
190
- try:
191
- thickmax = None
192
- thickmin = None
193
- if isinstance(xray_filter_thickness_maximum, list):
194
- thickmax = xray_filter_thickness_maximum[i]
195
- if isinstance(xray_filter_thickness_minimum, list):
196
- thickmin = xray_filter_thickness_minimum[i]
197
- _xrayfilters("FLAT", material, thickmax, thickmin, source)
198
- except IndexError:
199
- pass
200
-
201
-
202
- def _xray_filters_prep(dataset, source):
203
- xray_filter_type = get_value_kw("FilterType", dataset)
204
- xray_filter_material = get_value_kw("FilterMaterial", dataset)
205
-
206
- # Explicit no filter, register as such
207
- if xray_filter_type == "NONE":
208
- _xrayfiltersnone(source)
209
- return
210
- # Implicit no filter, just ignore
211
- if xray_filter_type is None:
212
- return
213
-
214
- # Get multiple filters into pydicom MultiValue or lists
215
- if (
216
- xray_filter_material
217
- and "," in xray_filter_material
218
- and not isinstance(xray_filter_material, MultiValue)
219
- ):
220
- xray_filter_material = xray_filter_material.split(",")
221
-
222
- xray_filter_thickness_minimum = get_value_kw("FilterThicknessMinimum", dataset)
223
- xray_filter_thickness_maximum = get_value_kw("FilterThicknessMaximum", dataset)
224
- if xray_filter_thickness_minimum and not isinstance(
225
- xray_filter_thickness_minimum, (MultiValue, list)
226
- ):
227
- try:
228
- float(xray_filter_thickness_minimum)
229
- except ValueError:
230
- if "," in xray_filter_thickness_minimum:
231
- xray_filter_thickness_minimum = xray_filter_thickness_minimum.split(",")
232
- if xray_filter_thickness_maximum and not isinstance(
233
- xray_filter_thickness_maximum, (MultiValue, list)
234
- ):
235
- try:
236
- float(xray_filter_thickness_maximum)
237
- except ValueError:
238
- if "," in xray_filter_thickness_maximum:
239
- xray_filter_thickness_maximum = xray_filter_thickness_maximum.split(",")
240
-
241
- if xray_filter_material and isinstance(xray_filter_material, (MultiValue, list)):
242
- _xray_filters_multiple(
243
- xray_filter_material,
244
- xray_filter_thickness_maximum,
245
- xray_filter_thickness_minimum,
246
- source,
247
- )
248
- else:
249
- # deal with known Siemens filter records
250
- siemens_filters = ("CU_0.1_MM", "CU_0.2_MM", "CU_0.3_MM")
251
- if xray_filter_type in siemens_filters:
252
- if xray_filter_type == "CU_0.1_MM":
253
- thickmax = 0.1
254
- thickmin = 0.1
255
- elif xray_filter_type == "CU_0.2_MM":
256
- thickmax = 0.2
257
- thickmin = 0.2
258
- elif xray_filter_type == "CU_0.3_MM":
259
- thickmax = 0.3
260
- thickmin = 0.3
261
- _xrayfilters("FLAT", "COPPER", thickmax, thickmin, source)
262
- else:
263
- _xrayfilters(
264
- xray_filter_type,
265
- xray_filter_material,
266
- xray_filter_thickness_maximum,
267
- xray_filter_thickness_minimum,
268
- source,
269
- )
270
-
271
-
272
- def _kvp(dataset, source):
273
- kv = Kvp.objects.create(irradiation_event_xray_source_data=source)
274
- kv.kvp = get_value_kw("KVP", dataset)
275
- kv.save()
276
-
277
-
278
- def _exposure(dataset, source):
279
- exp = Exposure.objects.create(irradiation_event_xray_source_data=source)
280
-
281
- exp.exposure = get_value_kw("ExposureInuAs", dataset) # uAs
282
- if not exp.exposure:
283
- exposure = get_value_kw("Exposure", dataset)
284
- if exposure:
285
- exp.exposure = exposure * 1000
286
- exp.save()
287
-
288
-
289
- def _xraygrid(gridcode, source):
290
- grid = XrayGrid.objects.create(irradiation_event_xray_source_data=source)
291
- if gridcode == "111646":
292
- grid.xray_grid = get_or_create_cid("111646", "No grid")
293
- elif gridcode == "111641":
294
- grid.xray_grid = get_or_create_cid("111641", "Fixed grid")
295
- elif gridcode == "111642":
296
- grid.xray_grid = get_or_create_cid("111642", "Focused grid")
297
- elif gridcode == "111643":
298
- grid.xray_grid = get_or_create_cid("111643", "Reciprocating grid")
299
- elif gridcode == "111644":
300
- grid.xray_grid = get_or_create_cid("111644", "Parallel grid")
301
- elif gridcode == "111645":
302
- grid.xray_grid = get_or_create_cid("111645", "Crossed grid")
303
- grid.save()
304
-
305
-
306
- def _irradiationeventxraydetectordata(dataset, event):
307
- detector = IrradEventXRayDetectorData.objects.create(
308
- irradiation_event_xray_data=event
309
- )
310
- detector.exposure_index = get_value_kw("ExposureIndex", dataset)
311
- detector.relative_xray_exposure = get_value_kw("RelativeXRayExposure", dataset)
312
- manufacturer = detector.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
313
- 0
314
- ].manufacturer.lower()
315
- if "fuji" in manufacturer:
316
- detector.relative_exposure_unit = "S ()"
317
- elif "carestream" in manufacturer:
318
- detector.relative_exposure_unit = "EI (Mbels)"
319
- elif "kodak" in manufacturer:
320
- detector.relative_exposure_unit = "EI (Mbels)"
321
- elif "agfa" in manufacturer:
322
- detector.relative_exposure_unit = "lgM (Bels)"
323
- elif "konica" in manufacturer:
324
- detector.relative_exposure_unit = "S ()"
325
- elif "canon" in manufacturer:
326
- detector.relative_exposure_unit = "REX ()"
327
- elif "swissray" in manufacturer:
328
- detector.relative_exposure_unit = "DI ()"
329
- elif "philips" in manufacturer:
330
- detector.relative_exposure_unit = "EI ()"
331
- elif "siemens" in manufacturer:
332
- detector.relative_exposure_unit = "EXI (μGy)"
333
- detector.sensitivity = get_value_kw("Sensitivity", dataset)
334
- detector.target_exposure_index = get_value_kw("TargetExposureIndex", dataset)
335
- detector.deviation_index = get_value_kw("DeviationIndex", dataset)
336
- detector.save()
337
-
338
-
339
- def _irradiationeventxraysourcedata(dataset, event):
340
- # TODO: review model to convert to cid where appropriate, and add additional fields such as field height and width
341
- source = IrradEventXRaySourceData.objects.create(irradiation_event_xray_data=event)
342
- source.average_xray_tube_current = get_value_kw("XRayTubeCurrent", dataset)
343
- if not source.average_xray_tube_current:
344
- source.average_xray_tube_current = get_value_kw(
345
- "AverageXRayTubeCurrent", dataset
346
- )
347
- source.exposure_time = get_value_kw("ExposureTime", dataset)
348
- source.focal_spot_size = get_value_kw("FocalSpots", dataset)
349
- collimated_field_area = get_value_kw("FieldOfViewDimensions", dataset)
350
- if collimated_field_area:
351
- source.collimated_field_area = (
352
- float(collimated_field_area[0]) * float(collimated_field_area[1]) / 1000000
353
- )
354
- exp_ctrl_mode = get_value_kw("ExposureControlMode", dataset)
355
- if exp_ctrl_mode:
356
- source.exposure_control_mode = exp_ctrl_mode
357
- xray_grid = get_value_kw("Grid", dataset)
358
- if xray_grid:
359
- if xray_grid == "NONE":
360
- _xraygrid("111646", source)
361
- else:
362
- for gtype in xray_grid:
363
- if (
364
- "FI" in gtype
365
- ): # Fixed; abbreviated due to fitting two keywords in 16 characters
366
- _xraygrid("111641", source)
367
- elif "FO" in gtype: # Focused
368
- _xraygrid("111642", source)
369
- elif "RE" in gtype: # Reciprocating
370
- _xraygrid("111643", source)
371
- elif "PA" in gtype: # Parallel
372
- _xraygrid("111644", source)
373
- elif "CR" in gtype: # Crossed
374
- _xraygrid("111645", source)
375
- source.grid_absorbing_material = get_value_kw("GridAbsorbingMaterial", dataset)
376
- source.grid_spacing_material = get_value_kw("GridSpacingMaterial", dataset)
377
- source.grid_thickness = get_value_kw("GridThickness", dataset)
378
- source.grid_pitch = get_value_kw("GridPitch", dataset)
379
- source.grid_aspect_ratio = get_value_kw("GridAspectRatio", dataset)
380
- source.grid_period = get_value_kw("GridPeriod", dataset)
381
- source.grid_focal_distance = get_value_kw("GridFocalDistance", dataset)
382
- source.save()
383
- _xray_filters_prep(dataset, source)
384
- _kvp(dataset, source)
385
- _exposure(dataset, source)
386
-
387
-
388
- def _doserelateddistancemeasurements(dataset, mech):
389
- dist = DoseRelatedDistanceMeasurements.objects.create(
390
- irradiation_event_xray_mechanical_data=mech
391
- )
392
- manufacturer = dist.irradiation_event_xray_mechanical_data.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
393
- 0
394
- ].manufacturer
395
- model_name = dist.irradiation_event_xray_mechanical_data.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
396
- 0
397
- ].manufacturer_model_name
398
- dist.distance_source_to_detector = get_value_kw("DistanceSourceToDetector", dataset)
399
- if (
400
- dist.distance_source_to_detector
401
- and manufacturer
402
- and model_name
403
- and "kodak" in manufacturer.lower()
404
- and "dr 7500" in model_name.lower()
405
- ):
406
- dist.distance_source_to_detector *= 100 # convert dm to mm
407
- dist.distance_source_to_entrance_surface = get_value_kw(
408
- "DistanceSourceToPatient", dataset
409
- )
410
- dist.distance_source_to_isocenter = get_value_kw(
411
- "DistanceSourceToIsocenter", dataset
412
- )
413
- # DistanceSourceToReferencePoint isn't a DICOM tag. Same as DistanceSourceToPatient?
414
- # dist.distance_source_to_reference_point = get_value_kw('DistanceSourceToReferencePoint',dataset)
415
- # Table longitudinal and lateral positions not DICOM elements.
416
- # dist.table_longitudinal_position = get_value_kw('TableLongitudinalPosition',dataset)
417
- # dist.table_lateral_position = get_value_kw('TableLateralPosition',dataset)
418
- dist.table_height_position = get_value_kw("TableHeight", dataset)
419
- # DistanceSourceToTablePlane not a DICOM tag.
420
- # dist.distance_source_to_table_plane = get_value_kw('DistanceSourceToTablePlane',dataset)
421
- dist.radiological_thickness = get_value_num(0x00451049, dataset)
422
- dist.save()
423
-
424
-
425
- def _irradiationeventxraymechanicaldata(dataset, event):
426
- mech = IrradEventXRayMechanicalData.objects.create(
427
- irradiation_event_xray_data=event
428
- )
429
- mech.magnification_factor = get_value_kw(
430
- "EstimatedRadiographicMagnificationFactor", dataset
431
- )
432
- mech.primary_angle = get_value_kw("PositionerPrimaryAngle", dataset)
433
- mech.secondary_angle = get_value_kw("PositionerSecondaryAngle", dataset)
434
- mech.column_angulation = get_value_kw("ColumnAngulation", dataset)
435
- mech.table_head_tilt_angle = get_value_kw("TableHeadTiltAngle", dataset)
436
- mech.table_horizontal_rotation_angle = get_value_kw(
437
- "TableHorizontalRotationAngle", dataset
438
- )
439
- mech.table_cradle_tilt_angle = get_value_kw("TableCradleTiltAngle", dataset)
440
- mech.save()
441
- _doserelateddistancemeasurements(dataset, mech)
442
-
443
-
444
- def _irradiationeventxraydata(dataset, proj): # TID 10003
445
- # TODO: review model to convert to cid where appropriate, and add additional fields
446
-
447
- event = IrradEventXRayData.objects.create(projection_xray_radiation_dose=proj)
448
- event.acquisition_plane = get_or_create_cid("113622", "Single Plane")
449
- event.irradiation_event_uid = get_value_kw("SOPInstanceUID", dataset)
450
- event_time = get_value_kw("AcquisitionTime", dataset)
451
- if not event_time:
452
- event_time = get_value_kw("ContentTime", dataset)
453
- if not event_time:
454
- event_time = get_value_kw("StudyTime", dataset)
455
- event_date = get_value_kw("AcquisitionDate", dataset)
456
- if not event_date:
457
- event_date = get_value_kw("ContentDate", dataset)
458
- if not event_date:
459
- event_date = get_value_kw("StudyDate", dataset)
460
- event.date_time_started = make_date_time("{0}{1}".format(event_date, event_time))
461
- event.irradiation_event_type = get_or_create_cid("113611", "Stationary Acquisition")
462
- event.acquisition_protocol = get_value_kw("ProtocolName", dataset)
463
- if not event.acquisition_protocol:
464
- manufacturer = get_value_kw("Manufacturer", dataset)
465
- software_versions = get_value_kw("SoftwareVersions", dataset)
466
- if manufacturer == "TOSHIBA_MEC" and software_versions == "TM_TFD_1.0":
467
- event.acquisition_protocol = get_value_kw("ImageComments", dataset)
468
- if not event.acquisition_protocol:
469
- event.acquisition_protocol = get_value_kw("SeriesDescription", dataset)
470
- if not event.acquisition_protocol:
471
- event.acquisition_protocol = get_seq_code_meaning(
472
- "PerformedProtocolCodeSequence", dataset
473
- )
474
- series_description = get_value_kw("SeriesDescription", dataset)
475
- if series_description:
476
- event.comment = series_description
477
- event.anatomical_structure = get_or_create_cid(
478
- get_seq_code_value("AnatomicRegionSequence", dataset),
479
- get_seq_code_meaning("AnatomicRegionSequence", dataset),
480
- )
481
- laterality = get_value_kw("ImageLaterality", dataset)
482
- if laterality:
483
- if laterality.strip() == "R":
484
- event.laterality = get_or_create_cid("G-A100", "Right")
485
- if laterality.strip() == "L":
486
- event.laterality = get_or_create_cid("G-A101", "Left")
487
-
488
- event.image_view = get_or_create_cid(
489
- get_seq_code_value("ViewCodeSequence", dataset),
490
- get_seq_code_meaning("ViewCodeSequence", dataset),
491
- )
492
- if not event.image_view:
493
- projection = get_value_kw("ViewPosition", dataset)
494
- if projection == "AP":
495
- event.image_view = get_or_create_cid("R-10206", "antero-posterior")
496
- elif projection == "PA":
497
- event.image_view = get_or_create_cid("R-10214", "postero-anterior")
498
- elif projection == "LL":
499
- event.image_view = get_or_create_cid("R-10236", "left lateral")
500
- elif projection == "RL":
501
- event.image_view = get_or_create_cid("R-10232", "right lateral")
502
- # http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,5101) lists four other views: RLD (Right Lateral Decubitus),
503
- # LLD (Left Lateral Decubitus), RLO (Right Lateral Oblique) and LLO (Left Lateral Oblique). There isn't an exact
504
- # match for these views in the CID 4010 DX View (http://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_4010.html)
505
-
506
- # image view modifier?
507
- if event.anatomical_structure:
508
- event.target_region = event.anatomical_structure
509
- event.entrance_exposure_at_rp = get_value_kw("EntranceDoseInmGy", dataset)
510
- # reference point definition?
511
- pc_fibroglandular = get_value_kw("CommentsOnRadiationDose", dataset)
512
- if pc_fibroglandular:
513
- if "%" in pc_fibroglandular:
514
- event.percent_fibroglandular_tissue = pc_fibroglandular.replace(
515
- "%", ""
516
- ).strip()
517
- exposure_control = get_value_kw("ExposureControlModeDescription", dataset)
518
-
519
- if event.comment and exposure_control:
520
- event.comment = event.comment + ", " + exposure_control
521
-
522
- dap = get_value_kw("ImageAndFluoroscopyAreaDoseProduct", dataset)
523
- if dap:
524
- event.dose_area_product = (
525
- dap / 100000
526
- ) # Value of DICOM tag (0018,115e) in dGy.cm2, converted to Gy.m2
527
- event.save()
528
-
529
- _irradiationeventxraydetectordata(dataset, event)
530
- _irradiationeventxraysourcedata(dataset, event)
531
- _irradiationeventxraymechanicaldata(dataset, event)
532
- _accumulatedxraydose_update(event)
533
-
534
-
535
- def _accumulatedxraydose(proj):
536
- accum = AccumXRayDose.objects.create(projection_xray_radiation_dose=proj)
537
- accum.acquisition_plane = get_or_create_cid("113622", "Single Plane")
538
- accum.save()
539
- accumint = AccumIntegratedProjRadiogDose.objects.create(accumulated_xray_dose=accum)
540
- accumint.dose_area_product_total = 0.0
541
- accumint.total_number_of_radiographic_frames = 0
542
- accumint.save()
543
-
544
-
545
- def _accumulatedxraydose_update(event):
546
- accumint = (
547
- event.projection_xray_radiation_dose.accumxraydose_set.get().accumintegratedprojradiogdose_set.get()
548
- )
549
- if accumint.total_number_of_radiographic_frames is not None:
550
- accumint.total_number_of_radiographic_frames = (
551
- accumint.total_number_of_radiographic_frames + 1
552
- )
553
- else:
554
- accumint.total_number_of_radiographic_frames = 1
555
-
556
- if event.dose_area_product:
557
- accumint.dose_area_product_total += Decimal(event.dose_area_product)
558
- accumint.save()
559
-
560
-
561
- def _projectionxrayradiationdose(dataset, g):
562
- proj = ProjectionXRayRadiationDose.objects.create(general_study_module_attributes=g)
563
- proj.procedure_reported = get_or_create_cid("113704", "Projection X-Ray")
564
- proj.has_intent = get_or_create_cid("R-408C3", "Diagnostic Intent")
565
- proj.scope_of_accumulation = get_or_create_cid("113014", "Study")
566
- proj.source_of_dose_information = get_or_create_cid(
567
- "113866", "Copied From Image Attributes"
568
- )
569
- proj.xray_detector_data_available = get_or_create_cid("R-00339", "No")
570
- proj.xray_source_data_available = get_or_create_cid("R-0038D", "Yes")
571
- proj.xray_mechanical_data_available = get_or_create_cid("R-0038D", "Yes")
572
- proj.save()
573
- _accumulatedxraydose(proj)
574
- _irradiationeventxraydata(dataset, proj)
575
-
576
-
577
- def _generalequipmentmoduleattributes(dataset, study):
578
- equip = GeneralEquipmentModuleAttr.objects.create(
579
- general_study_module_attributes=study
580
- )
581
- equip.manufacturer = get_value_kw("Manufacturer", dataset)
582
- equip.institution_name = get_value_kw("InstitutionName", dataset)
583
- equip.institution_address = get_value_kw("InstitutionAddress", dataset)
584
- equip.station_name = get_value_kw("StationName", dataset)
585
- equip.institutional_department_name = get_value_kw(
586
- "InstitutionalDepartmentName", dataset
587
- )
588
- equip.manufacturer_model_name = get_value_kw("ManufacturerModelName", dataset)
589
- equip.device_serial_number = get_value_kw("DeviceSerialNumber", dataset)
590
- equip.software_versions = get_value_kw("SoftwareVersions", dataset)
591
- equip.gantry_id = get_value_kw("GantryID", dataset)
592
- equip.spatial_resolution = get_value_kw("SpatialResolution", dataset)
593
- equip.date_of_last_calibration = get_date("DateOfLastCalibration", dataset)
594
- equip.time_of_last_calibration = get_time("TimeOfLastCalibration", dataset)
595
-
596
- equip_display_name, created = UniqueEquipmentNames.objects.get_or_create(
597
- manufacturer=equip.manufacturer,
598
- manufacturer_hash=hash_id(equip.manufacturer),
599
- institution_name=equip.institution_name,
600
- institution_name_hash=hash_id(equip.institution_name),
601
- station_name=equip.station_name,
602
- station_name_hash=hash_id(equip.station_name),
603
- institutional_department_name=equip.institutional_department_name,
604
- institutional_department_name_hash=hash_id(equip.institutional_department_name),
605
- manufacturer_model_name=equip.manufacturer_model_name,
606
- manufacturer_model_name_hash=hash_id(equip.manufacturer_model_name),
607
- device_serial_number=equip.device_serial_number,
608
- device_serial_number_hash=hash_id(equip.device_serial_number),
609
- software_versions=equip.software_versions,
610
- software_versions_hash=hash_id(equip.software_versions),
611
- gantry_id=equip.gantry_id,
612
- gantry_id_hash=hash_id(equip.gantry_id),
613
- hash_generated=True,
614
- device_observer_uid=None,
615
- device_observer_uid_hash=None,
616
- )
617
- if created:
618
- if equip.institution_name and equip.station_name:
619
- equip_display_name.display_name = (
620
- equip.institution_name + " " + equip.station_name
621
- )
622
- elif equip.institution_name:
623
- equip_display_name.display_name = equip.institution_name
624
- elif equip.station_name:
625
- equip_display_name.display_name = equip.station_name
626
- else:
627
- equip_display_name.display_name = "Blank"
628
- equip_display_name.save()
629
-
630
- equip.unique_equipment_name = UniqueEquipmentNames(pk=equip_display_name.pk)
631
-
632
- equip.save()
633
-
634
-
635
- def _patientstudymoduleattributes(dataset, g): # C.7.2.2
636
- patientatt = PatientStudyModuleAttr.objects.create(
637
- general_study_module_attributes=g
638
- )
639
- patientatt.patient_age = get_value_kw("PatientAge", dataset)
640
- patientatt.patient_weight = get_value_kw("PatientWeight", dataset)
641
- patientatt.patient_size = get_value_kw("PatientSize", dataset)
642
- try:
643
- Decimal(patientatt.patient_size)
644
- except DecimalException:
645
- patientatt.patient_size = None
646
- except TypeError:
647
- pass
648
- patientatt.save()
649
-
650
-
651
- def _generalstudymoduleattributes(dataset, g):
652
- g.study_date = get_date("StudyDate", dataset)
653
- g.study_time = get_time("StudyTime", dataset)
654
- g.study_workload_chart_time = datetime.combine(
655
- datetime.date(datetime(1900, 1, 1)), datetime.time(g.study_time)
656
- )
657
- g.referring_physician_name = list_to_string(
658
- get_value_kw("ReferringPhysicianName", dataset)
659
- )
660
- g.study_id = get_value_kw("StudyID", dataset)
661
- accession_number = get_value_kw("AccessionNumber", dataset)
662
- patient_id_settings = PatientIDSettings.objects.get()
663
- if accession_number and patient_id_settings.accession_hashed:
664
- accession_number = hash_id(accession_number)
665
- g.accession_hashed = True
666
- g.accession_number = accession_number
667
- g.study_description = get_value_kw("StudyDescription", dataset)
668
- if not g.study_description:
669
- g.study_description = get_value_kw("SeriesDescription", dataset)
670
- if not g.study_description:
671
- g.study_description = get_seq_code_meaning("ProcedureCodeSequence", dataset)
672
- g.modality_type = get_value_kw("Modality", dataset)
673
- g.physician_of_record = list_to_string(get_value_kw("PhysiciansOfRecord", dataset))
674
- g.name_of_physician_reading_study = list_to_string(
675
- get_value_kw("NameOfPhysiciansReadingStudy", dataset)
676
- )
677
- g.performing_physician_name = list_to_string(
678
- get_value_kw("PerformingPhysicianName", dataset)
679
- )
680
- g.operator_name = list_to_string(get_value_kw("OperatorsName", dataset))
681
- # Being used to summarise protocol for study:
682
- g.procedure_code_meaning = get_seq_code_meaning("ProcedureCodeSequence", dataset)
683
- if not g.procedure_code_meaning:
684
- g.procedure_code_meaning = get_value_kw("ProtocolName", dataset)
685
- if not g.procedure_code_meaning:
686
- g.procedure_code_meaning = get_value_kw("StudyDescription", dataset)
687
- if not g.procedure_code_meaning:
688
- g.procedure_code_meaning = get_value_kw("SeriesDescription", dataset)
689
- g.requested_procedure_code_value = get_seq_code_value(
690
- "RequestedProcedureCodeSequence", dataset
691
- )
692
- g.requested_procedure_code_meaning = get_seq_code_meaning(
693
- "RequestedProcedureCodeSequence", dataset
694
- )
695
- if not g.requested_procedure_code_value:
696
- g.requested_procedure_code_value = get_seq_code_value(
697
- "RequestAttributesSequence", dataset
698
- )
699
- if not g.requested_procedure_code_value:
700
- g.requested_procedure_code_value = get_seq_code_value(
701
- "ProcedureCodeSequence", dataset
702
- )
703
- if not g.requested_procedure_code_value:
704
- g.requested_procedure_code_value = get_seq_code_value(
705
- "PerformedProtocolCodeSequence", dataset
706
- )
707
- if not g.requested_procedure_code_meaning:
708
- g.requested_procedure_code_meaning = get_seq_code_meaning(
709
- "RequestAttributesSequence", dataset
710
- )
711
- if not g.requested_procedure_code_meaning:
712
- g.requested_procedure_code_meaning = get_seq_code_meaning(
713
- "ProcedureCodeSequence", dataset
714
- )
715
- if not g.requested_procedure_code_meaning:
716
- g.requested_procedure_code_meaning = get_value_num(0x00321060, dataset)
717
- if not g.requested_procedure_code_meaning:
718
- g.requested_procedure_code_meaning = get_seq_code_meaning(
719
- "PerformedProtocolCodeSequence", dataset
720
- )
721
- if not g.requested_procedure_code_meaning:
722
- manufacturer = get_value_kw("Manufacturer", dataset)
723
- model = get_value_kw("ManufacturerModelName", dataset)
724
- if (
725
- manufacturer
726
- and model
727
- and "canon" in manufacturer.lower()
728
- and "cxdi" in model.lower()
729
- ):
730
- g.requested_procedure_code_meaning = get_value_num(0x00081030, dataset)
731
- if (
732
- manufacturer
733
- and model
734
- and "carestream health" in manufacturer.lower()
735
- and "drx-revolution" in model.lower()
736
- ):
737
- g.requested_procedure_code_meaning = get_value_num(0x00081030, dataset)
738
- g.save()
739
-
740
- _generalequipmentmoduleattributes(dataset, g)
741
- _projectionxrayradiationdose(dataset, g)
742
- _patientstudymoduleattributes(dataset, g)
743
- patient_module_attributes(dataset, g)
744
- populate_dx_rf_summary(g)
745
- g.number_of_events = (
746
- g.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
747
- )
748
- g.save()
749
-
750
- # Add standard names
751
- add_standard_names(g)
752
-
753
-
754
- # The routine will accept three types of image:
755
- # CR image storage (SOP UID = '1.2.840.10008.5.1.4.1.1.1')
756
- # Digital x-ray image storage - for presentation (SOP UID = '1.2.840.10008.5.1.4.1.1.1.1')
757
- # Digital x-ray image storage - for processing (SOP UID = '1.2.840.10008.5.1.4.1.1.1.1.1')
758
- # These SOP UIDs were taken from http://www.dicomlibrary.com/dicom/sop/
759
- def _test_if_dx(dataset):
760
- """Test if dicom object passed is a DX or CR radiographic file by looking at SOP Class UID"""
761
- if (
762
- dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1"
763
- and dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1.1"
764
- and dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1.1.1"
765
- ):
766
- return 0
767
- return 1
768
-
769
-
770
- def _dx2db(dataset):
771
- study_uid = get_value_kw("StudyInstanceUID", dataset)
772
- if not study_uid:
773
- error = "In dx import: No UID returned"
774
- logger.error(error)
775
- record_task_error_exit(error)
776
- return
777
- record_task_info(f"UID: {study_uid.replace('.', '. ')}")
778
- record_task_related_query(study_uid)
779
- study_in_db = check_uid.check_uid(study_uid)
780
-
781
- if study_in_db:
782
- sleep(
783
- 2.0
784
- ) # Give initial event a chance to get to save on _projectionxrayradiationdose
785
- this_study = get_study_check_dup(dataset, modality="DX")
786
- if this_study:
787
- _irradiationeventxraydata(
788
- dataset, this_study.projectionxrayradiationdose_set.get()
789
- )
790
- populate_dx_rf_summary(this_study)
791
- this_study.number_of_events = (
792
- this_study.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
793
- )
794
- this_study.save()
795
- else:
796
- error = f"Study {study_uid.replace('.', '. ')} already in DB"
797
- logger.error(error)
798
- record_task_error_exit(error)
799
- return
800
-
801
- if not study_in_db:
802
- # study doesn't exist, start from scratch
803
- g = GeneralStudyModuleAttr.objects.create()
804
- g.study_instance_uid = get_value_kw("StudyInstanceUID", dataset)
805
- g.save()
806
- logger.debug(
807
- "Started importing DX with Study Instance UID of {0}".format(
808
- g.study_instance_uid
809
- )
810
- )
811
- event_uid = get_value_kw("SOPInstanceUID", dataset)
812
- check_uid.record_sop_instance_uid(g, event_uid)
813
- # check study again
814
- study_in_db = check_uid.check_uid(study_uid)
815
- if study_in_db == 1:
816
- _generalstudymoduleattributes(dataset, g)
817
- elif not study_in_db:
818
- error = "Something went wrong, GeneralStudyModuleAttr wasn't created"
819
- record_task_error_exit(error)
820
- logger.error(error)
821
- return
822
- elif study_in_db > 1:
823
- sleep(random()) # nosec - not being used for cryptography
824
- # Check if other instance(s) has deleted the study yet
825
- study_in_db = check_uid.check_uid(study_uid)
826
- if study_in_db == 1:
827
- _generalstudymoduleattributes(dataset, g)
828
- elif study_in_db > 1:
829
- g.delete()
830
- study_in_db = check_uid.check_uid(study_uid)
831
- if not study_in_db:
832
- # both must have been deleted simultaneously!
833
- sleep(random()) # nosec - not being used for cryptography
834
- # Check if other instance has created the study again yet
835
- study_in_db = check_uid.check_uid(study_uid)
836
- if study_in_db == 1:
837
- sleep(
838
- 2.0
839
- ) # Give initial event a chance to get to save on _projectionxrayradiationdose
840
- this_study = get_study_check_dup(dataset, modality="DX")
841
- if this_study:
842
- _irradiationeventxraydata(
843
- dataset,
844
- this_study.projectionxrayradiationdose_set.get(),
845
- )
846
- while not study_in_db:
847
- g = GeneralStudyModuleAttr.objects.create()
848
- g.study_instance_uid = get_value_kw("StudyInstanceUID", dataset)
849
- g.save()
850
- check_uid.record_sop_instance_uid(g, event_uid)
851
- # check again
852
- study_in_db = check_uid.check_uid(study_uid)
853
- if study_in_db == 1:
854
- _generalstudymoduleattributes(dataset, g)
855
- elif study_in_db > 1:
856
- g.delete()
857
- sleep(random()) # nosec - not being used for cryptography
858
- study_in_db = check_uid.check_uid(study_uid)
859
- if study_in_db == 1:
860
- sleep(
861
- 2.0
862
- ) # Give initial event a chance to get to save on _projectionxrayradiationdose
863
- this_study = get_study_check_dup(dataset, modality="DX")
864
- if this_study:
865
- _irradiationeventxraydata(
866
- dataset,
867
- this_study.projectionxrayradiationdose_set.get(),
868
- )
869
- elif study_in_db == 1:
870
- sleep(
871
- 2.0
872
- ) # Give initial event a chance to get to save on _projectionxrayradiationdose
873
- this_study = get_study_check_dup(dataset, modality="DX")
874
- if this_study:
875
- _irradiationeventxraydata(
876
- dataset,
877
- this_study.projectionxrayradiationdose_set.get(),
878
- )
879
-
880
-
881
- def _fix_kodak_filters(dataset):
882
- """
883
- Replace floats with commas in with multivalue floats: as found in older Carestream/Kodak units such as the DR7500
884
- :param dataset: DICOM dataset
885
- :return: Repaired DICOM dataset
886
- """
887
-
888
- try: # Black magic pydicom method suggested by Darcy Mason: https://groups.google.com/forum/?hl=en-GB#!topic/pydicom/x_WsC2gCLck
889
- xray_filter_thickness_minimum = get_value_kw("FilterThicknessMinimum", dataset)
890
- except (
891
- ValueError
892
- ): # Assumes ValueError will be a comma separated pair of numbers, as per Kodak.
893
- thick = dict.__getitem__(
894
- dataset, 0x187052
895
- ) # pydicom black magic as suggested by
896
- thickval = thick.__getattribute__("value")
897
- if "," in thickval:
898
- thickval = thickval.replace(",", "\\")
899
- thick2 = thick._replace(value=thickval)
900
- dict.__setitem__(dataset, 0x187052, thick2)
901
-
902
- try:
903
- xray_filter_thickness_maximum = get_value_kw("FilterThicknessMaximum", dataset)
904
- except (
905
- ValueError
906
- ): # Assumes ValueError will be a comma separated pair of numbers, as per Kodak.
907
- thick = dict.__getitem__(
908
- dataset, 0x187054
909
- ) # pydicom black magic as suggested by
910
- thickval = thick.__getattribute__("value")
911
- if "," in thickval:
912
- thickval = thickval.replace(",", "\\")
913
- thick2 = thick._replace(value=thickval)
914
- dict.__setitem__(dataset, 0x187054, thick2)
915
-
916
-
917
- def dx(dig_file):
918
- """Extract radiation dose structured report related data from DX radiographic images
919
-
920
- :param filename: relative or absolute path to DICOM DX radiographic image file.
921
- :type filename: str.
922
-
923
- """
924
-
925
- try:
926
- del_settings = DicomDeleteSettings.objects.get()
927
- del_dx_im = del_settings.del_dx_im
928
- except ObjectDoesNotExist:
929
- del_dx_im = False
930
-
931
- logger.debug("About to read DX")
932
- dataset = pydicom.dcmread(dig_file)
933
- try:
934
- dataset.decode()
935
- except ValueError as err:
936
- if "could not convert string to float" in str(err):
937
- _fix_kodak_filters(dataset)
938
- dataset.decode()
939
- isdx = _test_if_dx(dataset)
940
- if not isdx:
941
- error = "{0} is not a DICOM DX radiographic image".format(dig_file)
942
- logger.error(error)
943
- record_task_error_exit(error)
944
- return 1
945
-
946
- logger.debug("About to launch _dx2db")
947
- _dx2db(dataset)
948
-
949
- if del_dx_im:
950
- os.remove(dig_file)
951
-
952
- return 0
1
+ # This Python file uses the following encoding: utf-8
2
+ # OpenREM - Radiation Exposure Monitoring tools for the physicist
3
+ # Copyright (C) 2012,2013 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
+ # This file (dx.py) is intended to extract radiation dose related data from
26
+ # DX images. It is based on mam.py.
27
+ # David Platten, 28/3/2014
28
+ #
29
+ """
30
+ .. module:: dx.
31
+ :synopsis: Module to extract radiation dose related data from DX image objects.
32
+
33
+ .. moduleauthor:: David Platten, Ed McDonagh
34
+
35
+ """
36
+ from datetime import datetime
37
+ from decimal import Decimal, DecimalException
38
+ import logging
39
+ import os
40
+ from random import random
41
+ import sys
42
+ from time import sleep
43
+
44
+ import django
45
+ from django.core.exceptions import ObjectDoesNotExist
46
+ import pydicom
47
+ from pydicom import config
48
+ from pydicom.valuerep import MultiValue
49
+
50
+ from openrem.remapp.tools.background import (
51
+ record_task_error_exit,
52
+ record_task_related_query,
53
+ record_task_info,
54
+ )
55
+
56
+ from ..tools import check_uid
57
+ from ..tools.dcmdatetime import get_date, get_time, make_date_time
58
+ from ..tools.get_values import (
59
+ get_value_kw,
60
+ get_value_num,
61
+ get_or_create_cid,
62
+ get_seq_code_value,
63
+ get_seq_code_meaning,
64
+ list_to_string,
65
+ )
66
+ from ..tools.hash_id import hash_id
67
+
68
+ # setup django/OpenREM
69
+ basepath = os.path.dirname(__file__)
70
+ projectpath = os.path.abspath(os.path.join(basepath, "..", ".."))
71
+ if projectpath not in sys.path:
72
+ sys.path.insert(1, projectpath)
73
+ os.environ["DJANGO_SETTINGS_MODULE"] = "openremproject.settings"
74
+ django.setup()
75
+
76
+ from .extract_common import ( # pylint: disable=wrong-import-order, wrong-import-position
77
+ get_study_check_dup,
78
+ populate_dx_rf_summary,
79
+ patient_module_attributes,
80
+ add_standard_names,
81
+ )
82
+ from remapp.models import ( # pylint: disable=wrong-import-order, wrong-import-position
83
+ AccumXRayDose,
84
+ AccumIntegratedProjRadiogDose,
85
+ DicomDeleteSettings,
86
+ DoseRelatedDistanceMeasurements,
87
+ Exposure,
88
+ GeneralEquipmentModuleAttr,
89
+ GeneralStudyModuleAttr,
90
+ IrradEventXRayData,
91
+ IrradEventXRayDetectorData,
92
+ IrradEventXRayMechanicalData,
93
+ IrradEventXRaySourceData,
94
+ Kvp,
95
+ PatientIDSettings,
96
+ PatientStudyModuleAttr,
97
+ ProjectionXRayRadiationDose,
98
+ UniqueEquipmentNames,
99
+ XrayFilters,
100
+ XrayGrid,
101
+ )
102
+
103
+ logger = logging.getLogger(
104
+ "remapp.extractors.dx"
105
+ ) # Explicitly named so that it is still handled when using __main__
106
+
107
+
108
+ def _xrayfilters(filttype, material, thickmax, thickmin, source):
109
+
110
+ filters = XrayFilters.objects.create(irradiation_event_xray_source_data=source)
111
+ if filttype:
112
+ filter_types = {
113
+ "STRIP": {"code": "113650", "meaning": "Strip filter"},
114
+ "WEDGE": {"code": "113651", "meaning": "Wedge filter"},
115
+ "BUTTERFLY": {"code:": "113652", "meaning": "Butterfly filter"},
116
+ "NONE": {"code": "111609", "meaning": "No filter"},
117
+ "FLAT": {"code": "113653", "meaning": "Flat filter"},
118
+ }
119
+ if filttype in filter_types:
120
+ filters.xray_filter_type = get_or_create_cid(
121
+ filter_types[filttype]["code"], filter_types[filttype]["meaning"]
122
+ )
123
+ if material:
124
+ logger.debug(
125
+ f"In _xrayfilters, attempting to match material {material.strip().lower()}"
126
+ )
127
+ if material.strip().lower() == "molybdenum":
128
+ filters.xray_filter_material = get_or_create_cid(
129
+ "C-150F9", "Molybdenum or Molybdenum compound"
130
+ )
131
+ if material.strip().lower() == "rhodium":
132
+ filters.xray_filter_material = get_or_create_cid(
133
+ "C-167F9", "Rhodium or Rhodium compound"
134
+ )
135
+ if material.strip().lower() == "silver":
136
+ filters.xray_filter_material = get_or_create_cid(
137
+ "C-137F9", "Silver or Silver compound"
138
+ )
139
+ if material.strip().lower() in [
140
+ "aluminum",
141
+ "aluminium",
142
+ ]: # Illegal spelling of Aluminium found in Philips DiDi
143
+ filters.xray_filter_material = get_or_create_cid(
144
+ "C-120F9", "Aluminum or Aluminum compound"
145
+ )
146
+ if material.strip().lower() == "copper":
147
+ filters.xray_filter_material = get_or_create_cid(
148
+ "C-127F9", "Copper or Copper compound"
149
+ )
150
+ if material.strip().lower() == "niobium":
151
+ filters.xray_filter_material = get_or_create_cid(
152
+ "C-1190E", "Niobium or Niobium compound"
153
+ )
154
+ if material.strip().lower() == "europium":
155
+ filters.xray_filter_material = get_or_create_cid(
156
+ "C-1190F", "Europium or Europium compound"
157
+ )
158
+ if material.strip().lower() == "lead":
159
+ filters.xray_filter_material = get_or_create_cid(
160
+ "C-132F9", "Lead or Lead compound"
161
+ )
162
+ if material.strip().lower() == "tantalum":
163
+ filters.xray_filter_material = get_or_create_cid(
164
+ "C-156F9", "Tantalum or Tantalum compound"
165
+ )
166
+ if thickmax is not None and thickmin is not None:
167
+ if thickmax < thickmin:
168
+ tempmin = thickmax
169
+ thickmax = thickmin
170
+ thickmin = tempmin
171
+ if thickmax is not None:
172
+ filters.xray_filter_thickness_maximum = thickmax
173
+ if thickmin is not None:
174
+ filters.xray_filter_thickness_minimum = thickmin
175
+ filters.save()
176
+
177
+
178
+ def _xrayfiltersnone(source):
179
+ filters = XrayFilters.objects.create(irradiation_event_xray_source_data=source)
180
+ filters.xray_filter_type = get_or_create_cid("111609", "No filter")
181
+ filters.save()
182
+
183
+
184
+ def _xray_filters_multiple(
185
+ xray_filter_material,
186
+ xray_filter_thickness_maximum,
187
+ xray_filter_thickness_minimum,
188
+ source,
189
+ ):
190
+ for i, material in enumerate(xray_filter_material):
191
+ try:
192
+ thickmax = None
193
+ thickmin = None
194
+ if isinstance(xray_filter_thickness_maximum, list):
195
+ thickmax = xray_filter_thickness_maximum[i]
196
+ if isinstance(xray_filter_thickness_minimum, list):
197
+ thickmin = xray_filter_thickness_minimum[i]
198
+ _xrayfilters("FLAT", material, thickmax, thickmin, source)
199
+ except IndexError:
200
+ pass
201
+
202
+
203
+ def _xray_filters_prep(dataset, source):
204
+ xray_filter_type = get_value_kw("FilterType", dataset)
205
+ xray_filter_material = get_value_kw("FilterMaterial", dataset)
206
+
207
+ # Explicit no filter, register as such
208
+ if xray_filter_type == "NONE":
209
+ _xrayfiltersnone(source)
210
+ return
211
+ # Implicit no filter, just ignore
212
+ if xray_filter_type is None:
213
+ return
214
+
215
+ # Get multiple filters into pydicom MultiValue or lists
216
+ if (
217
+ xray_filter_material
218
+ and "," in xray_filter_material
219
+ and not isinstance(xray_filter_material, MultiValue)
220
+ ):
221
+ xray_filter_material = xray_filter_material.split(",")
222
+
223
+ xray_filter_thickness_minimum = get_value_kw("FilterThicknessMinimum", dataset)
224
+ xray_filter_thickness_maximum = get_value_kw("FilterThicknessMaximum", dataset)
225
+ if xray_filter_thickness_minimum and not isinstance(
226
+ xray_filter_thickness_minimum, (MultiValue, list)
227
+ ):
228
+ try:
229
+ float(xray_filter_thickness_minimum)
230
+ except ValueError:
231
+ if "," in xray_filter_thickness_minimum:
232
+ xray_filter_thickness_minimum = xray_filter_thickness_minimum.split(",")
233
+ if xray_filter_thickness_maximum and not isinstance(
234
+ xray_filter_thickness_maximum, (MultiValue, list)
235
+ ):
236
+ try:
237
+ float(xray_filter_thickness_maximum)
238
+ except ValueError:
239
+ if "," in xray_filter_thickness_maximum:
240
+ xray_filter_thickness_maximum = xray_filter_thickness_maximum.split(",")
241
+
242
+ if xray_filter_material and isinstance(xray_filter_material, (MultiValue, list)):
243
+ _xray_filters_multiple(
244
+ xray_filter_material,
245
+ xray_filter_thickness_maximum,
246
+ xray_filter_thickness_minimum,
247
+ source,
248
+ )
249
+ else:
250
+ # deal with known Siemens filter records
251
+ siemens_filters = ("CU_0.1_MM", "CU_0.2_MM", "CU_0.3_MM")
252
+ if xray_filter_type in siemens_filters:
253
+ if xray_filter_type == "CU_0.1_MM":
254
+ thickmax = 0.1
255
+ thickmin = 0.1
256
+ elif xray_filter_type == "CU_0.2_MM":
257
+ thickmax = 0.2
258
+ thickmin = 0.2
259
+ elif xray_filter_type == "CU_0.3_MM":
260
+ thickmax = 0.3
261
+ thickmin = 0.3
262
+ _xrayfilters("FLAT", "COPPER", thickmax, thickmin, source)
263
+ else:
264
+ _xrayfilters(
265
+ xray_filter_type,
266
+ xray_filter_material,
267
+ xray_filter_thickness_maximum,
268
+ xray_filter_thickness_minimum,
269
+ source,
270
+ )
271
+
272
+
273
+ def _kvp(dataset, source):
274
+ kv = Kvp.objects.create(irradiation_event_xray_source_data=source)
275
+ kv.kvp = get_value_kw("KVP", dataset)
276
+ kv.save()
277
+
278
+
279
+ def _exposure(dataset, source):
280
+ exp = Exposure.objects.create(irradiation_event_xray_source_data=source)
281
+
282
+ exp.exposure = get_value_kw("ExposureInuAs", dataset) # uAs
283
+ if not exp.exposure:
284
+ exposure = get_value_kw("Exposure", dataset)
285
+ if exposure:
286
+ exp.exposure = exposure * 1000
287
+ exp.save()
288
+
289
+
290
+ def _xraygrid(gridcode, source):
291
+ grid = XrayGrid.objects.create(irradiation_event_xray_source_data=source)
292
+ if gridcode == "111646":
293
+ grid.xray_grid = get_or_create_cid("111646", "No grid")
294
+ elif gridcode == "111641":
295
+ grid.xray_grid = get_or_create_cid("111641", "Fixed grid")
296
+ elif gridcode == "111642":
297
+ grid.xray_grid = get_or_create_cid("111642", "Focused grid")
298
+ elif gridcode == "111643":
299
+ grid.xray_grid = get_or_create_cid("111643", "Reciprocating grid")
300
+ elif gridcode == "111644":
301
+ grid.xray_grid = get_or_create_cid("111644", "Parallel grid")
302
+ elif gridcode == "111645":
303
+ grid.xray_grid = get_or_create_cid("111645", "Crossed grid")
304
+ grid.save()
305
+
306
+
307
+ def _irradiationeventxraydetectordata(dataset, event):
308
+ detector = IrradEventXRayDetectorData.objects.create(
309
+ irradiation_event_xray_data=event
310
+ )
311
+ detector.exposure_index = get_value_kw("ExposureIndex", dataset)
312
+ detector.relative_xray_exposure = get_value_kw("RelativeXRayExposure", dataset)
313
+ manufacturer = detector.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
314
+ 0
315
+ ].manufacturer.lower()
316
+ if "fuji" in manufacturer:
317
+ detector.relative_exposure_unit = "S ()"
318
+ elif "carestream" in manufacturer:
319
+ detector.relative_exposure_unit = "EI (Mbels)"
320
+ elif "kodak" in manufacturer:
321
+ detector.relative_exposure_unit = "EI (Mbels)"
322
+ elif "agfa" in manufacturer:
323
+ detector.relative_exposure_unit = "lgM (Bels)"
324
+ elif "konica" in manufacturer:
325
+ detector.relative_exposure_unit = "S ()"
326
+ elif "canon" in manufacturer:
327
+ detector.relative_exposure_unit = "REX ()"
328
+ elif "swissray" in manufacturer:
329
+ detector.relative_exposure_unit = "DI ()"
330
+ elif "philips" in manufacturer:
331
+ detector.relative_exposure_unit = "EI ()"
332
+ elif "siemens" in manufacturer:
333
+ detector.relative_exposure_unit = "EXI (μGy)"
334
+ detector.sensitivity = get_value_kw("Sensitivity", dataset)
335
+ detector.target_exposure_index = get_value_kw("TargetExposureIndex", dataset)
336
+ detector.deviation_index = get_value_kw("DeviationIndex", dataset)
337
+ detector.save()
338
+
339
+
340
+ def _irradiationeventxraysourcedata(dataset, event):
341
+ # TODO: review model to convert to cid where appropriate, and add additional fields such as field height and width
342
+ source = IrradEventXRaySourceData.objects.create(irradiation_event_xray_data=event)
343
+ source.average_xray_tube_current = get_value_kw("XRayTubeCurrent", dataset)
344
+ if not source.average_xray_tube_current:
345
+ source.average_xray_tube_current = get_value_kw(
346
+ "AverageXRayTubeCurrent", dataset
347
+ )
348
+ source.exposure_time = get_value_kw("ExposureTime", dataset)
349
+ source.focal_spot_size = get_value_kw("FocalSpots", dataset)
350
+ collimated_field_area = get_value_kw("FieldOfViewDimensions", dataset)
351
+ if collimated_field_area:
352
+ source.collimated_field_area = (
353
+ float(collimated_field_area[0]) * float(collimated_field_area[1]) / 1000000
354
+ )
355
+ exp_ctrl_mode = get_value_kw("ExposureControlMode", dataset)
356
+ if exp_ctrl_mode:
357
+ source.exposure_control_mode = exp_ctrl_mode
358
+ xray_grid = get_value_kw("Grid", dataset)
359
+ if xray_grid:
360
+ if xray_grid == "NONE":
361
+ _xraygrid("111646", source)
362
+ else:
363
+ for gtype in xray_grid:
364
+ if (
365
+ "FI" in gtype
366
+ ): # Fixed; abbreviated due to fitting two keywords in 16 characters
367
+ _xraygrid("111641", source)
368
+ elif "FO" in gtype: # Focused
369
+ _xraygrid("111642", source)
370
+ elif "RE" in gtype: # Reciprocating
371
+ _xraygrid("111643", source)
372
+ elif "PA" in gtype: # Parallel
373
+ _xraygrid("111644", source)
374
+ elif "CR" in gtype: # Crossed
375
+ _xraygrid("111645", source)
376
+ source.grid_absorbing_material = get_value_kw("GridAbsorbingMaterial", dataset)
377
+ source.grid_spacing_material = get_value_kw("GridSpacingMaterial", dataset)
378
+ source.grid_thickness = get_value_kw("GridThickness", dataset)
379
+ source.grid_pitch = get_value_kw("GridPitch", dataset)
380
+ source.grid_aspect_ratio = get_value_kw("GridAspectRatio", dataset)
381
+ source.grid_period = get_value_kw("GridPeriod", dataset)
382
+ source.grid_focal_distance = get_value_kw("GridFocalDistance", dataset)
383
+ source.save()
384
+ _xray_filters_prep(dataset, source)
385
+ _kvp(dataset, source)
386
+ _exposure(dataset, source)
387
+
388
+
389
+ def _doserelateddistancemeasurements(dataset, mech):
390
+ dist = DoseRelatedDistanceMeasurements.objects.create(
391
+ irradiation_event_xray_mechanical_data=mech
392
+ )
393
+ manufacturer = dist.irradiation_event_xray_mechanical_data.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
394
+ 0
395
+ ].manufacturer
396
+ model_name = dist.irradiation_event_xray_mechanical_data.irradiation_event_xray_data.projection_xray_radiation_dose.general_study_module_attributes.generalequipmentmoduleattr_set.all()[
397
+ 0
398
+ ].manufacturer_model_name
399
+ dist.distance_source_to_detector = get_value_kw("DistanceSourceToDetector", dataset)
400
+ if (
401
+ dist.distance_source_to_detector
402
+ and manufacturer
403
+ and model_name
404
+ and "kodak" in manufacturer.lower()
405
+ and "dr 7500" in model_name.lower()
406
+ ):
407
+ dist.distance_source_to_detector *= 100 # convert dm to mm
408
+ dist.distance_source_to_entrance_surface = get_value_kw(
409
+ "DistanceSourceToPatient", dataset
410
+ )
411
+ dist.distance_source_to_isocenter = get_value_kw(
412
+ "DistanceSourceToIsocenter", dataset
413
+ )
414
+ # DistanceSourceToReferencePoint isn't a DICOM tag. Same as DistanceSourceToPatient?
415
+ # dist.distance_source_to_reference_point = get_value_kw('DistanceSourceToReferencePoint',dataset)
416
+ # Table longitudinal and lateral positions not DICOM elements.
417
+ # dist.table_longitudinal_position = get_value_kw('TableLongitudinalPosition',dataset)
418
+ # dist.table_lateral_position = get_value_kw('TableLateralPosition',dataset)
419
+ dist.table_height_position = get_value_kw("TableHeight", dataset)
420
+ # DistanceSourceToTablePlane not a DICOM tag.
421
+ # dist.distance_source_to_table_plane = get_value_kw('DistanceSourceToTablePlane',dataset)
422
+ dist.radiological_thickness = get_value_num(0x00451049, dataset)
423
+ dist.save()
424
+
425
+
426
+ def _irradiationeventxraymechanicaldata(dataset, event):
427
+ mech = IrradEventXRayMechanicalData.objects.create(
428
+ irradiation_event_xray_data=event
429
+ )
430
+ mech.magnification_factor = get_value_kw(
431
+ "EstimatedRadiographicMagnificationFactor", dataset
432
+ )
433
+ mech.primary_angle = get_value_kw("PositionerPrimaryAngle", dataset)
434
+ mech.secondary_angle = get_value_kw("PositionerSecondaryAngle", dataset)
435
+ mech.column_angulation = get_value_kw("ColumnAngulation", dataset)
436
+ mech.table_head_tilt_angle = get_value_kw("TableHeadTiltAngle", dataset)
437
+ mech.table_horizontal_rotation_angle = get_value_kw(
438
+ "TableHorizontalRotationAngle", dataset
439
+ )
440
+ mech.table_cradle_tilt_angle = get_value_kw("TableCradleTiltAngle", dataset)
441
+ mech.save()
442
+ _doserelateddistancemeasurements(dataset, mech)
443
+
444
+
445
+ def _irradiationeventxraydata(dataset, proj): # TID 10003
446
+ # TODO: review model to convert to cid where appropriate, and add additional fields
447
+
448
+ event = IrradEventXRayData.objects.create(projection_xray_radiation_dose=proj)
449
+ event.acquisition_plane = get_or_create_cid("113622", "Single Plane")
450
+ event.irradiation_event_uid = get_value_kw("SOPInstanceUID", dataset)
451
+ event_time = get_value_kw("AcquisitionTime", dataset)
452
+ if not event_time:
453
+ event_time = get_value_kw("ContentTime", dataset)
454
+ if not event_time:
455
+ event_time = get_value_kw("StudyTime", dataset)
456
+ event_date = get_value_kw("AcquisitionDate", dataset)
457
+ if not event_date:
458
+ event_date = get_value_kw("ContentDate", dataset)
459
+ if not event_date:
460
+ event_date = get_value_kw("StudyDate", dataset)
461
+ event.date_time_started = make_date_time("{0}{1}".format(event_date, event_time))
462
+ event.irradiation_event_type = get_or_create_cid("113611", "Stationary Acquisition")
463
+ event.acquisition_protocol = get_value_kw("ProtocolName", dataset)
464
+ if not event.acquisition_protocol:
465
+ manufacturer = get_value_kw("Manufacturer", dataset)
466
+ software_versions = get_value_kw("SoftwareVersions", dataset)
467
+ if manufacturer == "TOSHIBA_MEC" and software_versions == "TM_TFD_1.0":
468
+ event.acquisition_protocol = get_value_kw("ImageComments", dataset)
469
+ if not event.acquisition_protocol:
470
+ event.acquisition_protocol = get_value_kw("SeriesDescription", dataset)
471
+ if not event.acquisition_protocol:
472
+ event.acquisition_protocol = get_seq_code_meaning(
473
+ "PerformedProtocolCodeSequence", dataset
474
+ )
475
+ series_description = get_value_kw("SeriesDescription", dataset)
476
+ if series_description:
477
+ event.comment = series_description
478
+ event.anatomical_structure = get_or_create_cid(
479
+ get_seq_code_value("AnatomicRegionSequence", dataset),
480
+ get_seq_code_meaning("AnatomicRegionSequence", dataset),
481
+ )
482
+ laterality = get_value_kw("ImageLaterality", dataset)
483
+ if laterality:
484
+ if laterality.strip() == "R":
485
+ event.laterality = get_or_create_cid("G-A100", "Right")
486
+ if laterality.strip() == "L":
487
+ event.laterality = get_or_create_cid("G-A101", "Left")
488
+
489
+ event.image_view = get_or_create_cid(
490
+ get_seq_code_value("ViewCodeSequence", dataset),
491
+ get_seq_code_meaning("ViewCodeSequence", dataset),
492
+ )
493
+ if not event.image_view:
494
+ projection = get_value_kw("ViewPosition", dataset)
495
+ if projection == "AP":
496
+ event.image_view = get_or_create_cid("R-10206", "antero-posterior")
497
+ elif projection == "PA":
498
+ event.image_view = get_or_create_cid("R-10214", "postero-anterior")
499
+ elif projection == "LL":
500
+ event.image_view = get_or_create_cid("R-10236", "left lateral")
501
+ elif projection == "RL":
502
+ event.image_view = get_or_create_cid("R-10232", "right lateral")
503
+ # http://dicomlookup.com/lookup.asp?sw=Tnumber&q=(0018,5101) lists four other views: RLD (Right Lateral Decubitus),
504
+ # LLD (Left Lateral Decubitus), RLO (Right Lateral Oblique) and LLO (Left Lateral Oblique). There isn't an exact
505
+ # match for these views in the CID 4010 DX View (http://dicom.nema.org/medical/dicom/current/output/chtml/part16/sect_CID_4010.html)
506
+
507
+ # image view modifier?
508
+ if event.anatomical_structure:
509
+ event.target_region = event.anatomical_structure
510
+ event.entrance_exposure_at_rp = get_value_kw("EntranceDoseInmGy", dataset)
511
+ # reference point definition?
512
+ pc_fibroglandular = get_value_kw("CommentsOnRadiationDose", dataset)
513
+ if pc_fibroglandular:
514
+ if "%" in pc_fibroglandular:
515
+ event.percent_fibroglandular_tissue = pc_fibroglandular.replace(
516
+ "%", ""
517
+ ).strip()
518
+ exposure_control = get_value_kw("ExposureControlModeDescription", dataset)
519
+
520
+ if event.comment and exposure_control:
521
+ event.comment = event.comment + ", " + exposure_control
522
+
523
+ dap = get_value_kw("ImageAndFluoroscopyAreaDoseProduct", dataset)
524
+ if dap:
525
+ event.dose_area_product = (
526
+ dap / 100000
527
+ ) # Value of DICOM tag (0018,115e) in dGy.cm2, converted to Gy.m2
528
+ event.save()
529
+
530
+ _irradiationeventxraydetectordata(dataset, event)
531
+ _irradiationeventxraysourcedata(dataset, event)
532
+ _irradiationeventxraymechanicaldata(dataset, event)
533
+ _accumulatedxraydose_update(event)
534
+
535
+
536
+ def _accumulatedxraydose(proj):
537
+ accum = AccumXRayDose.objects.create(projection_xray_radiation_dose=proj)
538
+ accum.acquisition_plane = get_or_create_cid("113622", "Single Plane")
539
+ accum.save()
540
+ accumint = AccumIntegratedProjRadiogDose.objects.create(accumulated_xray_dose=accum)
541
+ accumint.dose_area_product_total = 0.0
542
+ accumint.total_number_of_radiographic_frames = 0
543
+ accumint.save()
544
+
545
+
546
+ def _accumulatedxraydose_update(event):
547
+ accumint = (
548
+ event.projection_xray_radiation_dose.accumxraydose_set.get().accumintegratedprojradiogdose_set.get()
549
+ )
550
+ if accumint.total_number_of_radiographic_frames is not None:
551
+ accumint.total_number_of_radiographic_frames = (
552
+ accumint.total_number_of_radiographic_frames + 1
553
+ )
554
+ else:
555
+ accumint.total_number_of_radiographic_frames = 1
556
+
557
+ if event.dose_area_product:
558
+ accumint.dose_area_product_total += Decimal(event.dose_area_product)
559
+ accumint.save()
560
+
561
+
562
+ def _projectionxrayradiationdose(dataset, g):
563
+ proj = ProjectionXRayRadiationDose.objects.create(general_study_module_attributes=g)
564
+ proj.procedure_reported = get_or_create_cid("113704", "Projection X-Ray")
565
+ proj.has_intent = get_or_create_cid("R-408C3", "Diagnostic Intent")
566
+ proj.scope_of_accumulation = get_or_create_cid("113014", "Study")
567
+ proj.source_of_dose_information = get_or_create_cid(
568
+ "113866", "Copied From Image Attributes"
569
+ )
570
+ proj.xray_detector_data_available = get_or_create_cid("R-00339", "No")
571
+ proj.xray_source_data_available = get_or_create_cid("R-0038D", "Yes")
572
+ proj.xray_mechanical_data_available = get_or_create_cid("R-0038D", "Yes")
573
+ proj.save()
574
+ _accumulatedxraydose(proj)
575
+ _irradiationeventxraydata(dataset, proj)
576
+
577
+
578
+ def _generalequipmentmoduleattributes(dataset, study):
579
+ equip = GeneralEquipmentModuleAttr.objects.create(
580
+ general_study_module_attributes=study
581
+ )
582
+ equip.manufacturer = get_value_kw("Manufacturer", dataset)
583
+ equip.institution_name = get_value_kw("InstitutionName", dataset)
584
+ equip.institution_address = get_value_kw("InstitutionAddress", dataset)
585
+ equip.station_name = get_value_kw("StationName", dataset)
586
+ equip.institutional_department_name = get_value_kw(
587
+ "InstitutionalDepartmentName", dataset
588
+ )
589
+ equip.manufacturer_model_name = get_value_kw("ManufacturerModelName", dataset)
590
+ equip.device_serial_number = get_value_kw("DeviceSerialNumber", dataset)
591
+ equip.software_versions = get_value_kw("SoftwareVersions", dataset)
592
+ equip.gantry_id = get_value_kw("GantryID", dataset)
593
+ equip.spatial_resolution = get_value_kw("SpatialResolution", dataset)
594
+ equip.date_of_last_calibration = get_date("DateOfLastCalibration", dataset)
595
+ equip.time_of_last_calibration = get_time("TimeOfLastCalibration", dataset)
596
+
597
+ equip_display_name, created = UniqueEquipmentNames.objects.get_or_create(
598
+ manufacturer=equip.manufacturer,
599
+ manufacturer_hash=hash_id(equip.manufacturer),
600
+ institution_name=equip.institution_name,
601
+ institution_name_hash=hash_id(equip.institution_name),
602
+ station_name=equip.station_name,
603
+ station_name_hash=hash_id(equip.station_name),
604
+ institutional_department_name=equip.institutional_department_name,
605
+ institutional_department_name_hash=hash_id(equip.institutional_department_name),
606
+ manufacturer_model_name=equip.manufacturer_model_name,
607
+ manufacturer_model_name_hash=hash_id(equip.manufacturer_model_name),
608
+ device_serial_number=equip.device_serial_number,
609
+ device_serial_number_hash=hash_id(equip.device_serial_number),
610
+ software_versions=equip.software_versions,
611
+ software_versions_hash=hash_id(equip.software_versions),
612
+ gantry_id=equip.gantry_id,
613
+ gantry_id_hash=hash_id(equip.gantry_id),
614
+ hash_generated=True,
615
+ device_observer_uid=None,
616
+ device_observer_uid_hash=None,
617
+ )
618
+ if created:
619
+ if equip.institution_name and equip.station_name:
620
+ equip_display_name.display_name = (
621
+ equip.institution_name + " " + equip.station_name
622
+ )
623
+ elif equip.institution_name:
624
+ equip_display_name.display_name = equip.institution_name
625
+ elif equip.station_name:
626
+ equip_display_name.display_name = equip.station_name
627
+ else:
628
+ equip_display_name.display_name = "Blank"
629
+ equip_display_name.save()
630
+
631
+ equip.unique_equipment_name = UniqueEquipmentNames(pk=equip_display_name.pk)
632
+
633
+ equip.save()
634
+
635
+
636
+ def _patientstudymoduleattributes(dataset, g): # C.7.2.2
637
+ patientatt = PatientStudyModuleAttr.objects.create(
638
+ general_study_module_attributes=g
639
+ )
640
+ patientatt.patient_age = get_value_kw("PatientAge", dataset)
641
+ patientatt.patient_weight = get_value_kw("PatientWeight", dataset)
642
+ patientatt.patient_size = get_value_kw("PatientSize", dataset)
643
+ try:
644
+ Decimal(patientatt.patient_size)
645
+ except DecimalException:
646
+ patientatt.patient_size = None
647
+ except TypeError:
648
+ pass
649
+ patientatt.save()
650
+
651
+
652
+ def _generalstudymoduleattributes(dataset, g):
653
+ g.study_date = get_date("StudyDate", dataset)
654
+ g.study_time = get_time("StudyTime", dataset)
655
+ g.study_workload_chart_time = datetime.combine(
656
+ datetime.date(datetime(1900, 1, 1)), datetime.time(g.study_time)
657
+ )
658
+ g.referring_physician_name = list_to_string(
659
+ get_value_kw("ReferringPhysicianName", dataset)
660
+ )
661
+ g.study_id = get_value_kw("StudyID", dataset)
662
+ accession_number = get_value_kw("AccessionNumber", dataset)
663
+ patient_id_settings = PatientIDSettings.objects.get()
664
+ if accession_number and patient_id_settings.accession_hashed:
665
+ accession_number = hash_id(accession_number)
666
+ g.accession_hashed = True
667
+ g.accession_number = accession_number
668
+ g.study_description = get_value_kw("StudyDescription", dataset)
669
+ if not g.study_description:
670
+ g.study_description = get_value_kw("SeriesDescription", dataset)
671
+ if not g.study_description:
672
+ g.study_description = get_seq_code_meaning("ProcedureCodeSequence", dataset)
673
+ g.modality_type = get_value_kw("Modality", dataset)
674
+ g.physician_of_record = list_to_string(get_value_kw("PhysiciansOfRecord", dataset))
675
+ g.name_of_physician_reading_study = list_to_string(
676
+ get_value_kw("NameOfPhysiciansReadingStudy", dataset)
677
+ )
678
+ g.performing_physician_name = list_to_string(
679
+ get_value_kw("PerformingPhysicianName", dataset)
680
+ )
681
+ g.operator_name = list_to_string(get_value_kw("OperatorsName", dataset))
682
+ # Being used to summarise protocol for study:
683
+ g.procedure_code_meaning = get_seq_code_meaning("ProcedureCodeSequence", dataset)
684
+ if not g.procedure_code_meaning:
685
+ g.procedure_code_meaning = get_value_kw("ProtocolName", dataset)
686
+ if not g.procedure_code_meaning:
687
+ g.procedure_code_meaning = get_value_kw("StudyDescription", dataset)
688
+ if not g.procedure_code_meaning:
689
+ g.procedure_code_meaning = get_value_kw("SeriesDescription", dataset)
690
+ g.requested_procedure_code_value = get_seq_code_value(
691
+ "RequestedProcedureCodeSequence", dataset
692
+ )
693
+ g.requested_procedure_code_meaning = get_seq_code_meaning(
694
+ "RequestedProcedureCodeSequence", dataset
695
+ )
696
+ if not g.requested_procedure_code_value:
697
+ g.requested_procedure_code_value = get_seq_code_value(
698
+ "RequestAttributesSequence", dataset
699
+ )
700
+ if not g.requested_procedure_code_value:
701
+ g.requested_procedure_code_value = get_seq_code_value(
702
+ "ProcedureCodeSequence", dataset
703
+ )
704
+ if not g.requested_procedure_code_value:
705
+ g.requested_procedure_code_value = get_seq_code_value(
706
+ "PerformedProtocolCodeSequence", dataset
707
+ )
708
+ if not g.requested_procedure_code_meaning:
709
+ g.requested_procedure_code_meaning = get_seq_code_meaning(
710
+ "RequestAttributesSequence", dataset
711
+ )
712
+ if not g.requested_procedure_code_meaning:
713
+ g.requested_procedure_code_meaning = get_seq_code_meaning(
714
+ "ProcedureCodeSequence", dataset
715
+ )
716
+ if not g.requested_procedure_code_meaning:
717
+ g.requested_procedure_code_meaning = get_value_num(0x00321060, dataset)
718
+ if not g.requested_procedure_code_meaning:
719
+ g.requested_procedure_code_meaning = get_seq_code_meaning(
720
+ "PerformedProtocolCodeSequence", dataset
721
+ )
722
+ if not g.requested_procedure_code_meaning:
723
+ manufacturer = get_value_kw("Manufacturer", dataset)
724
+ model = get_value_kw("ManufacturerModelName", dataset)
725
+ if (
726
+ manufacturer
727
+ and model
728
+ and "canon" in manufacturer.lower()
729
+ and "cxdi" in model.lower()
730
+ ):
731
+ g.requested_procedure_code_meaning = get_value_num(0x00081030, dataset)
732
+ if (
733
+ manufacturer
734
+ and model
735
+ and "carestream health" in manufacturer.lower()
736
+ and "drx-revolution" in model.lower()
737
+ ):
738
+ g.requested_procedure_code_meaning = get_value_num(0x00081030, dataset)
739
+ g.save()
740
+
741
+ _generalequipmentmoduleattributes(dataset, g)
742
+ _projectionxrayradiationdose(dataset, g)
743
+ _patientstudymoduleattributes(dataset, g)
744
+ patient_module_attributes(dataset, g)
745
+ populate_dx_rf_summary(g)
746
+ g.number_of_events = (
747
+ g.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
748
+ )
749
+ g.save()
750
+
751
+ # Add standard names
752
+ add_standard_names(g)
753
+
754
+
755
+ # The routine will accept three types of image:
756
+ # CR image storage (SOP UID = '1.2.840.10008.5.1.4.1.1.1')
757
+ # Digital x-ray image storage - for presentation (SOP UID = '1.2.840.10008.5.1.4.1.1.1.1')
758
+ # Digital x-ray image storage - for processing (SOP UID = '1.2.840.10008.5.1.4.1.1.1.1.1')
759
+ # These SOP UIDs were taken from http://www.dicomlibrary.com/dicom/sop/
760
+ def _test_if_dx(dataset):
761
+ """Test if dicom object passed is a DX or CR radiographic file by looking at SOP Class UID"""
762
+ if (
763
+ dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1"
764
+ and dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1.1"
765
+ and dataset.SOPClassUID != "1.2.840.10008.5.1.4.1.1.1.1.1"
766
+ ):
767
+ return 0
768
+ return 1
769
+
770
+
771
+ def _dx2db(dataset):
772
+ study_uid = get_value_kw("StudyInstanceUID", dataset)
773
+ if not study_uid:
774
+ error = "In dx import: No UID returned"
775
+ logger.error(error)
776
+ record_task_error_exit(error)
777
+ return
778
+ record_task_info(f"UID: {study_uid.replace('.', '. ')}")
779
+ record_task_related_query(study_uid)
780
+ study_in_db = check_uid.check_uid(study_uid)
781
+
782
+ if study_in_db:
783
+ sleep(
784
+ 2.0
785
+ ) # Give initial event a chance to get to save on _projectionxrayradiationdose
786
+ this_study = get_study_check_dup(dataset, modality="DX")
787
+ if this_study:
788
+ _irradiationeventxraydata(
789
+ dataset, this_study.projectionxrayradiationdose_set.get()
790
+ )
791
+ populate_dx_rf_summary(this_study)
792
+ this_study.number_of_events = (
793
+ this_study.projectionxrayradiationdose_set.get().irradeventxraydata_set.count()
794
+ )
795
+ this_study.save()
796
+
797
+ # Update any matching standard names
798
+ add_standard_names(this_study)
799
+
800
+ else:
801
+ error = f"Study {study_uid.replace('.', '. ')} already in DB"
802
+ logger.error(error)
803
+ record_task_error_exit(error)
804
+ return
805
+
806
+ if not study_in_db:
807
+ # study doesn't exist, start from scratch
808
+ g = GeneralStudyModuleAttr.objects.create()
809
+ g.study_instance_uid = get_value_kw("StudyInstanceUID", dataset)
810
+ g.save()
811
+ logger.debug(
812
+ "Started importing DX with Study Instance UID of {0}".format(
813
+ g.study_instance_uid
814
+ )
815
+ )
816
+ event_uid = get_value_kw("SOPInstanceUID", dataset)
817
+ check_uid.record_sop_instance_uid(g, event_uid)
818
+ # check study again
819
+ study_in_db = check_uid.check_uid(study_uid)
820
+ if study_in_db == 1:
821
+ _generalstudymoduleattributes(dataset, g)
822
+ elif not study_in_db:
823
+ error = "Something went wrong, GeneralStudyModuleAttr wasn't created"
824
+ record_task_error_exit(error)
825
+ logger.error(error)
826
+ return
827
+ elif study_in_db > 1:
828
+ sleep(random()) # nosec - not being used for cryptography
829
+ # Check if other instance(s) has deleted the study yet
830
+ study_in_db = check_uid.check_uid(study_uid)
831
+ if study_in_db == 1:
832
+ _generalstudymoduleattributes(dataset, g)
833
+ elif study_in_db > 1:
834
+ g.delete()
835
+ study_in_db = check_uid.check_uid(study_uid)
836
+ if not study_in_db:
837
+ # both must have been deleted simultaneously!
838
+ sleep(random()) # nosec - not being used for cryptography
839
+ # Check if other instance has created the study again yet
840
+ study_in_db = check_uid.check_uid(study_uid)
841
+ if study_in_db == 1:
842
+ sleep(
843
+ 2.0
844
+ ) # Give initial event a chance to get to save on _projectionxrayradiationdose
845
+ this_study = get_study_check_dup(dataset, modality="DX")
846
+ if this_study:
847
+ _irradiationeventxraydata(
848
+ dataset,
849
+ this_study.projectionxrayradiationdose_set.get(),
850
+ )
851
+ while not study_in_db:
852
+ g = GeneralStudyModuleAttr.objects.create()
853
+ g.study_instance_uid = get_value_kw("StudyInstanceUID", dataset)
854
+ g.save()
855
+ check_uid.record_sop_instance_uid(g, event_uid)
856
+ # check again
857
+ study_in_db = check_uid.check_uid(study_uid)
858
+ if study_in_db == 1:
859
+ _generalstudymoduleattributes(dataset, g)
860
+ elif study_in_db > 1:
861
+ g.delete()
862
+ sleep(random()) # nosec - not being used for cryptography
863
+ study_in_db = check_uid.check_uid(study_uid)
864
+ if study_in_db == 1:
865
+ sleep(
866
+ 2.0
867
+ ) # Give initial event a chance to get to save on _projectionxrayradiationdose
868
+ this_study = get_study_check_dup(dataset, modality="DX")
869
+ if this_study:
870
+ _irradiationeventxraydata(
871
+ dataset,
872
+ this_study.projectionxrayradiationdose_set.get(),
873
+ )
874
+ elif study_in_db == 1:
875
+ sleep(
876
+ 2.0
877
+ ) # Give initial event a chance to get to save on _projectionxrayradiationdose
878
+ this_study = get_study_check_dup(dataset, modality="DX")
879
+ if this_study:
880
+ _irradiationeventxraydata(
881
+ dataset,
882
+ this_study.projectionxrayradiationdose_set.get(),
883
+ )
884
+
885
+
886
+ def _fix_kodak_filters(dataset):
887
+ """
888
+ Replace floats with commas in with multivalue floats: as found in older Carestream/Kodak units such as the DR7500
889
+ :param dataset: DICOM dataset
890
+ :return: Repaired DICOM dataset
891
+ """
892
+
893
+ try: # Black magic pydicom method suggested by Darcy Mason: https://groups.google.com/forum/?hl=en-GB#!topic/pydicom/x_WsC2gCLck
894
+ xray_filter_thickness_minimum = get_value_kw("FilterThicknessMinimum", dataset)
895
+ except (
896
+ ValueError
897
+ ): # Assumes ValueError will be a comma separated pair of numbers, as per Kodak.
898
+ thick = dict.__getitem__(
899
+ dataset, 0x187052
900
+ ) # pydicom black magic as suggested by
901
+ thickval = thick.__getattribute__("value")
902
+ if "," in thickval:
903
+ thickval = thickval.replace(",", "\\")
904
+ thick2 = thick._replace(value=thickval)
905
+ dict.__setitem__(dataset, 0x187052, thick2)
906
+
907
+ try:
908
+ xray_filter_thickness_maximum = get_value_kw("FilterThicknessMaximum", dataset)
909
+ except (
910
+ ValueError
911
+ ): # Assumes ValueError will be a comma separated pair of numbers, as per Kodak.
912
+ thick = dict.__getitem__(
913
+ dataset, 0x187054
914
+ ) # pydicom black magic as suggested by
915
+ thickval = thick.__getattribute__("value")
916
+ if "," in thickval:
917
+ thickval = thickval.replace(",", "\\")
918
+ thick2 = thick._replace(value=thickval)
919
+ dict.__setitem__(dataset, 0x187054, thick2)
920
+
921
+
922
+ def _remove_spaces_decimal(value):
923
+ """Remove padding and convert to decimal"""
924
+ while value and value.endswith((" ", "\x00")):
925
+ value = value[:-1]
926
+ return Decimal(value)
927
+
928
+
929
+ def _fix_exposure_values(dataset):
930
+ """
931
+ Replace decimal values in ExposureTime, XRayTubeCurrent and Exposure with integer values; move original values to
932
+ appropriate fields
933
+ :param dataset: DICOM dataset
934
+ :return: Repaired DICOM dataset
935
+ """
936
+ try:
937
+ dataset.ExposureTime
938
+ except TypeError:
939
+ exposure_time = dataset.get_item("ExposureTime").value.decode()
940
+ exposure_time = _remove_spaces_decimal(exposure_time)
941
+ del dataset.ExposureTime
942
+ dataset.ExposureTime = round(exposure_time, 0)
943
+ if "ExposureTimeInuS" not in dataset:
944
+ dataset.ExposureTimeInuS = round(exposure_time * 1000, 0)
945
+ logger.warning(
946
+ f"ExposureTime (VR=IS) contained illegal value {exposure_time}. Now set "
947
+ f"to {dataset.ExposureTime}. ExposureTimeInuS now set to "
948
+ f"{dataset.ExposureTimeInuS}."
949
+ )
950
+ try:
951
+ dataset.XRayTubeCurrent
952
+ except TypeError:
953
+ xray_tube_current = dataset.get_item("XRayTubeCurrent").value.decode()
954
+ xray_tube_current = _remove_spaces_decimal(xray_tube_current)
955
+ del dataset.XRayTubeCurrent
956
+ dataset.XRayTubeCurrent = round(xray_tube_current, 0)
957
+ if "XRayTubeCurrentInuA" not in dataset:
958
+ dataset.XRayTubeCurrentInuA = round(xray_tube_current * 1000, 0)
959
+ logger.warning(
960
+ f"ExposureTime (VR=IS) contained illegal value {xray_tube_current}. Now set "
961
+ f"to {dataset.XRayTubeCurrent}. ExposureTimeInuS now set to "
962
+ f"{dataset.XRayTubeCurrentInuA}."
963
+ )
964
+ try:
965
+ dataset.Exposure
966
+ except TypeError:
967
+ exposure = dataset.get_item("Exposure").value.decode()
968
+ exposure = _remove_spaces_decimal(exposure)
969
+ del dataset.Exposure
970
+ dataset.Exposure = round(exposure, 0)
971
+ if "ExposureInuAs" not in dataset:
972
+ dataset.ExposureInuAs = round(exposure * 1000, 0)
973
+ logger.warning(
974
+ f"Exposure (VR=IS) contained illegal value {exposure}. Now set "
975
+ f"to {dataset.Exposure}. ExposureTimeInuS now set to "
976
+ f"{dataset.ExposureInuAs}."
977
+ )
978
+
979
+
980
+ def dx(dig_file):
981
+ """Extract radiation dose structured report related data from DX radiographic images
982
+
983
+ :param filename: relative or absolute path to DICOM DX radiographic image file.
984
+ :type filename: str.
985
+
986
+ """
987
+
988
+ try:
989
+ del_settings = DicomDeleteSettings.objects.get()
990
+ del_dx_im = del_settings.del_dx_im
991
+ except ObjectDoesNotExist:
992
+ del_dx_im = False
993
+
994
+ # Set convert_wrong_length_to_UN = True to prevent the wrong length causing an error.
995
+ config.convert_wrong_length_to_UN = True
996
+
997
+ logger.debug("About to read DX")
998
+
999
+ try:
1000
+ dataset = pydicom.dcmread(dig_file)
1001
+ except FileNotFoundError:
1002
+ logger.warning(
1003
+ f"dx.py not attempting to extract from {dig_file}, the file does not exist"
1004
+ )
1005
+ record_task_error_exit(
1006
+ f"Not attempting to extract from {dig_file}, the file does not exist"
1007
+ )
1008
+ return 1
1009
+
1010
+ try:
1011
+ dataset.decode()
1012
+ except ValueError as err:
1013
+ if "could not convert string to float" in str(err):
1014
+ _fix_kodak_filters(dataset)
1015
+ dataset.decode()
1016
+ except TypeError as err:
1017
+ if "Could not convert value to integer without loss" in str(err):
1018
+ _fix_exposure_values(dataset)
1019
+ dataset.decode()
1020
+ isdx = _test_if_dx(dataset)
1021
+ if not isdx:
1022
+ error = "{0} is not a DICOM DX radiographic image".format(dig_file)
1023
+ logger.error(error)
1024
+ record_task_error_exit(error)
1025
+ return 1
1026
+
1027
+ logger.debug("About to launch _dx2db")
1028
+ _dx2db(dataset)
1029
+
1030
+ if del_dx_im:
1031
+ os.remove(dig_file)
1032
+
1033
+ return 0