dicom 0.9.4 → 0.9.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.rdoc +44 -0
- data/CONTRIBUTING.rdoc +83 -0
- data/README.rdoc +3 -2
- data/dicom.gemspec +12 -13
- data/lib/dicom/anonymizer.rb +649 -670
- data/lib/dicom/audit_trail.rb +7 -0
- data/lib/dicom/constants.rb +1 -1
- data/lib/dicom/d_library.rb +46 -31
- data/lib/dicom/d_object.rb +38 -24
- data/lib/dicom/d_read.rb +20 -6
- data/lib/dicom/d_server.rb +3 -3
- data/lib/dicom/d_write.rb +7 -2
- data/lib/dicom/deprecated.rb +318 -0
- data/lib/dicom/image_item.rb +62 -42
- data/lib/dicom/image_processor.rb +2 -2
- data/lib/dicom/image_processor_mini_magick.rb +6 -6
- data/lib/dicom/image_processor_r_magick.rb +6 -6
- data/lib/dicom/link.rb +7 -11
- data/lib/dicom/logging.rb +2 -1
- data/lib/dicom/variables.rb +36 -1
- data/lib/dicom/version.rb +1 -1
- data/rakefile.rb +3 -1
- metadata +46 -63
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 39b5531cc2e35e76e34a22584cd3543462c371c6
|
4
|
+
data.tar.gz: d4c4309d054a1c2495c746956c5aa5a67536edcb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 68a07acd231106b00284009a3799621f44fa050944ea0076367545f807b76e5b57c1771cca985879951c80f0004ada6615e9c2a7bd9170d07fffe6dae62a57d1
|
7
|
+
data.tar.gz: 28f1655dea19ba8530a4f6aacaf307e7734ea153f0a546248289b43ccf949c8734179bb768db8bc57418ce25e1e112e918099bdfc03023d01b4110e0f36d0d2c
|
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,47 @@
|
|
1
|
+
= 0.9.5
|
2
|
+
|
3
|
+
=== 26th March, 2013
|
4
|
+
|
5
|
+
* DICOM module:
|
6
|
+
* Use hyphen instead of underscore in ruby-dicom application title.
|
7
|
+
* Added the DICOM load module method, a flexible method for loading DICOM data:
|
8
|
+
* Accepts one or more directories (in which all files are scanned).
|
9
|
+
* Accepts one or more file paths (which are loaded as DICOM objects).
|
10
|
+
* Accepts one or more DICOM objects, which are called with #to_dcm and returned.
|
11
|
+
* Network:
|
12
|
+
* Bind the DServer to '0.0.0.0' by default to avoid Econnrefused issues.
|
13
|
+
* Significantly improved network receive performance.
|
14
|
+
* Enable adding private UIDs to ruby-dicom's DICOM library.
|
15
|
+
* Read element and UID dictionaries with UTF-8 encoding.
|
16
|
+
* DObject (/Item):
|
17
|
+
* Added DObject#anonymize for easy anonymization of a single DICOM object.
|
18
|
+
* Added add_element and add_sequence methods for conveniently creating new elements/sequences belonging to a specific DObject or Item.
|
19
|
+
* Fixed an issue where the NArray library where needed when trying to pass an array to the pixels= method.
|
20
|
+
* Fixed an issue where both Magick libraries where needed when trying to pass an image object to the image= method.
|
21
|
+
* Added DObject#was_dcm_on_input attribute to separate between file and DObject elements given to DICOM::load.
|
22
|
+
* Added option :include_empty_parents to DObject#write.
|
23
|
+
* Removed the deprecated DObjet#write :add_meta option.
|
24
|
+
* Added DObject#source attribute for keeping track of a DICOM object's origin.
|
25
|
+
* Defaults to ignoring duplicate data elements and sequences instead of replacing the original element.
|
26
|
+
* Added the :overwrite option to DObject#read and #parse which makes ruby-dicom overwrite elements when duplicates are encountered.
|
27
|
+
* Anonymizer:
|
28
|
+
* Added Anonymizer#to_anonymizer, as well as equality and state methods.
|
29
|
+
* Use 'O' instead of 'N' as the default replacement value for Patient's Sex.
|
30
|
+
* Added :random_file_name option for increased security in the case of sensitive file name information.
|
31
|
+
* Make all Anonymizer attributes accessible through options with Anonymizer#new.
|
32
|
+
* Removed the deprecated identity_file feature.
|
33
|
+
* Deprecated Anonymizer#execute, along with its accompanying methods.
|
34
|
+
* Added the Anonymizer#anonymize method, which is intended to replace the execute method.
|
35
|
+
* Improved the Anonymizer's conformance with the guidelines in the DICOM standard:
|
36
|
+
* Delete the File Meta Information group (0002) on anonymization.
|
37
|
+
* Add the Patient Identity Removed element with value 'YES'.
|
38
|
+
* Add de-identification method code sequence.
|
39
|
+
* Added 7 new elements to the default anonymization list.
|
40
|
+
* Enabled complete remapping of UIDs in the Anonymizer (keeping all references between files/series/studies valid).
|
41
|
+
* Added Anonymizer :recursive option, for anonymizing entire element trees (not just the top level).
|
42
|
+
* Added Anonymizer :encryption option, which allows the simulatenous preservation of privacy and key/value relations in the audit file.
|
43
|
+
|
44
|
+
|
1
45
|
= 0.9.4
|
2
46
|
|
3
47
|
=== 10th September, 2012
|
data/CONTRIBUTING.rdoc
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
= Contributing Code
|
2
|
+
|
3
|
+
So you want to contribute to ruby-dicom? That's great! Thank you! If you are a
|
4
|
+
first time committer however, you must to take a moment to read these instructions.
|
5
|
+
|
6
|
+
The preferred method, by far, is to convey your code contribution in the form
|
7
|
+
of a pull request on {github}[https://github.com/dicom/ruby-dicom].
|
8
|
+
|
9
|
+
== Committer's Recipe
|
10
|
+
|
11
|
+
* Fork the repository (for bonus points, use a topical branch name).
|
12
|
+
* Execute the specification (rspec tests) to verify that all spec examples pass
|
13
|
+
(if in doubt, check out the rakefile for instructions).
|
14
|
+
* Add a spec example for your change. Only refactoring and documentation changes
|
15
|
+
require no new tests. If you are adding functionality or fixing a bug, a test is mandatory!
|
16
|
+
* Alter the code to make the new spec example(s) pass.
|
17
|
+
* Keep it simple! One issue per pull request. Mixing two or more independent
|
18
|
+
issues in the same pull request will complicate the review of your request and
|
19
|
+
may result in a rejection (even if independent parts of the commit are sound).
|
20
|
+
* Don't modify the version or changelog.
|
21
|
+
* Push to your fork and submit a pull request.
|
22
|
+
* Wait for feedback (this shouldn't take too long). The pull request may be accepted right
|
23
|
+
away, it may be rejected (with a reason specified), or it may spark a discussion where
|
24
|
+
changes/improvements are suggested in order for the pull request to be accepted.
|
25
|
+
|
26
|
+
== Guidelines
|
27
|
+
|
28
|
+
In order to increase the chances of your pull request being accepted,
|
29
|
+
please follow the project's coding guidelines. Ideally, your contribution must not
|
30
|
+
add {technical debt}[http://en.wikipedia.org/wiki/Technical_debt] to the project.
|
31
|
+
|
32
|
+
* Provide thorough documentation. It should follow the format used by this project
|
33
|
+
and give information on parameters, exceptions and return values where relevant.
|
34
|
+
Provide examples for non-trivial use cases.
|
35
|
+
* Read the excellent {Github Ruby Styleguide}[https://github.com/styleguide/ruby]
|
36
|
+
if you are new to collaborative Ruby development. Do note though, that we actually
|
37
|
+
don't follow all styles listed yet (perhaps we should?!).
|
38
|
+
* Some sample patterns of ours:
|
39
|
+
* Indentation: Two spaces (no tabs).
|
40
|
+
* No trailing whitespace. Blank lines should not have any space.
|
41
|
+
* Method parameters: my_method(my_arg) is preferred instead of my_method( my_arg ) or my_method my_arg
|
42
|
+
* Assignment: a = b and not a=b
|
43
|
+
* In general: Follow the conventions you see used in the source code already.
|
44
|
+
|
45
|
+
== Contribution Agreement
|
46
|
+
|
47
|
+
ruby-dicom is licensed under the {GPL v3}[http://www.gnu.org/licenses/gpl.html],
|
48
|
+
and to be in the best position to enforce the GPL, the copyright status of ruby-dicom
|
49
|
+
needs to be as simple as possible. To achieve this, contributors should only provide
|
50
|
+
contributions which are their own work, and either:
|
51
|
+
|
52
|
+
a) Assign the copyright on the contribution to myself, Christoffer Lervåg
|
53
|
+
|
54
|
+
or
|
55
|
+
|
56
|
+
b) Disclaim copyright on it and thus put it in the public domain
|
57
|
+
|
58
|
+
Copyright assignment (a) is the preferred and encouraged option for larger
|
59
|
+
code contributions, and is assumed unless otherwise is specified.
|
60
|
+
|
61
|
+
Please see the {GNU FAQ}[http://www.gnu.org/licenses/gpl-faq.html#AssignCopyright]
|
62
|
+
for a fuller explanation of the need for this.
|
63
|
+
|
64
|
+
== Credits
|
65
|
+
|
66
|
+
All contributors are credited, with full name and link to their github account,
|
67
|
+
in the README file. If such an accreditation is not wanted (for whatever reason),
|
68
|
+
please let me know so, either in the pull request or in private.
|
69
|
+
|
70
|
+
|
71
|
+
= Other ways to contribute
|
72
|
+
|
73
|
+
Don't want to get your hands dirty with source code and git? Don't worry,
|
74
|
+
there are other ways in which you can contribute to the project as well!
|
75
|
+
|
76
|
+
* Create an issue on github for feature requests or bug reports.
|
77
|
+
* Weigh in with your opinion on existing issues.
|
78
|
+
* Write a tutorial.
|
79
|
+
* Answer questions, or tell the community about your exciting ruby-dicom projects in the
|
80
|
+
{mailing list}[http://groups.google.com/group/ruby-dicom].
|
81
|
+
* Academic works: Properly reference ruby-dicom in your work and tell us about it.
|
82
|
+
* Spread the word: Tell your colleagues about ruby-dicom.
|
83
|
+
* Make a donation.
|
data/README.rdoc
CHANGED
@@ -110,7 +110,7 @@ Example:
|
|
110
110
|
|
111
111
|
== COPYRIGHT
|
112
112
|
|
113
|
-
Copyright 2008-
|
113
|
+
Copyright 2008-2013 Christoffer Lervåg
|
114
114
|
|
115
115
|
This program is free software: you can redistribute it and/or modify
|
116
116
|
it under the terms of the GNU General Public License as published by
|
@@ -143,6 +143,7 @@ Please don't hesitate to email me if you have any feedback related to this proje
|
|
143
143
|
* {Jeff Miller}[https://github.com/jeffmax]
|
144
144
|
* {Donnie Millar}[https://github.com/dmillar]
|
145
145
|
* {Björn Albers}[https://github.com/bjoernalbers]
|
146
|
-
* {Lars Benner}[https://github.com/Maturin]
|
147
146
|
* {Felix Petriconi}[https://github.com/FelixPetriconi]
|
148
147
|
* {Steven Bedrick}[https://github.com/stevenbedrick]
|
148
|
+
* {Lars Benner}[https://github.com/Maturin]
|
149
|
+
* {Brett Goulder}[https://github.com/brettgoulder]
|
data/dicom.gemspec
CHANGED
@@ -9,23 +9,22 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.date = Time.now
|
10
10
|
s.summary = "Library for handling DICOM files and DICOM network communication."
|
11
11
|
s.require_paths = ['lib']
|
12
|
-
s.author =
|
13
|
-
s.email =
|
14
|
-
s.homepage =
|
15
|
-
s.license =
|
12
|
+
s.author = 'Christoffer Lervag'
|
13
|
+
s.email = 'chris.lervag@gmail.com'
|
14
|
+
s.homepage = 'http://dicom.rubyforge.org/'
|
15
|
+
s.license = 'GPLv3'
|
16
16
|
s.description = "DICOM is a standard widely used throughout the world to store and transfer medical image data. This library enables efficient and powerful handling of DICOM in Ruby, to the benefit of any student or professional who would like to use their favorite language to process DICOM files and communicate across the network."
|
17
17
|
s.files = Dir["{lib}/**/*", "[A-Z]*"]
|
18
18
|
s.rubyforge_project = 'dicom'
|
19
19
|
|
20
20
|
s.required_ruby_version = '>= 1.9.2'
|
21
|
-
s.required_rubygems_version = '>= 1.8.6'
|
22
21
|
|
23
|
-
s.add_development_dependency('bundler', '
|
24
|
-
s.add_development_dependency('
|
25
|
-
s.add_development_dependency('
|
26
|
-
s.add_development_dependency('
|
27
|
-
s.add_development_dependency('
|
28
|
-
s.add_development_dependency('rmagick', '
|
29
|
-
s.add_development_dependency('
|
30
|
-
s.add_development_dependency('yard', '
|
22
|
+
s.add_development_dependency('bundler', '~> 1.3')
|
23
|
+
s.add_development_dependency('mocha', '~> 0.13')
|
24
|
+
s.add_development_dependency('mini_magick', '~> 3.5')
|
25
|
+
s.add_development_dependency('narray', '~> 0.6.0.8')
|
26
|
+
s.add_development_dependency('rake', '~> 0.9.6')
|
27
|
+
s.add_development_dependency('rmagick', '~> 2.13.2')
|
28
|
+
s.add_development_dependency('rspec', '~> 2.13')
|
29
|
+
s.add_development_dependency('yard', '~> 0.8.5')
|
31
30
|
end
|
data/lib/dicom/anonymizer.rb
CHANGED
@@ -1,670 +1,649 @@
|
|
1
|
-
module DICOM
|
2
|
-
|
3
|
-
# This is a convenience class for handling the anonymization
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
#
|
18
|
-
|
19
|
-
#
|
20
|
-
|
21
|
-
#
|
22
|
-
|
23
|
-
#
|
24
|
-
|
25
|
-
#
|
26
|
-
attr_accessor :
|
27
|
-
#
|
28
|
-
|
29
|
-
#
|
30
|
-
attr_accessor :
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
#
|
36
|
-
|
37
|
-
#
|
38
|
-
|
39
|
-
#
|
40
|
-
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
@
|
71
|
-
|
72
|
-
@
|
73
|
-
|
74
|
-
@
|
75
|
-
|
76
|
-
@
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
#
|
136
|
-
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
#
|
275
|
-
#
|
276
|
-
#
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
#
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
end
|
328
|
-
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
# @
|
333
|
-
#
|
334
|
-
#
|
335
|
-
def
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
#
|
349
|
-
#
|
350
|
-
#
|
351
|
-
#
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
#
|
366
|
-
#
|
367
|
-
#
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
#
|
413
|
-
#
|
414
|
-
#
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
#
|
434
|
-
#
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
@
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
# Open file and prepare to write text:
|
651
|
-
File.open(@identity_file, 'w') do |output|
|
652
|
-
# Cycle through each
|
653
|
-
@tags.each_index do |i|
|
654
|
-
if @enumerations[i]
|
655
|
-
# This tag has had enumeration. Gather original and anonymized values:
|
656
|
-
old_values = @enum_old_hash[@tags[i]]
|
657
|
-
new_values = @enum_new_hash[@tags[i]]
|
658
|
-
# Print the tag label, then new_value;old_value in the following rows.
|
659
|
-
output.print @tags[i] + "\n"
|
660
|
-
old_values.each_index do |j|
|
661
|
-
output.print new_values[j].to_s.rstrip + ";" + old_values[j].to_s.rstrip + "\n"
|
662
|
-
end
|
663
|
-
# Print empty line for separation between different tags:
|
664
|
-
output.print "\n"
|
665
|
-
end
|
666
|
-
end
|
667
|
-
end
|
668
|
-
end
|
669
|
-
end
|
670
|
-
end
|
1
|
+
module DICOM
|
2
|
+
|
3
|
+
# This is a convenience class for handling the anonymization
|
4
|
+
# (de-identification) of DICOM files.
|
5
|
+
#
|
6
|
+
# @note
|
7
|
+
# For a thorough introduction to the concept of DICOM anonymization,
|
8
|
+
# please refer to The DICOM Standard, Part 15: Security and System
|
9
|
+
# Management Profiles, Annex E: Attribute Confidentiality Profiles.
|
10
|
+
# For guidance on settings for individual data elements, please
|
11
|
+
# refer to DICOM PS 3.15, Annex E, Table E.1-1: Application Level
|
12
|
+
# Confidentiality Profile Attributes.
|
13
|
+
#
|
14
|
+
class Anonymizer
|
15
|
+
include Logging
|
16
|
+
|
17
|
+
# An AuditTrail instance used for this anonymization (if specified).
|
18
|
+
attr_reader :audit_trail
|
19
|
+
# The file name used for the AuditTrail serialization (if specified).
|
20
|
+
attr_reader :audit_trail_file
|
21
|
+
# A boolean that if set as true will cause all anonymized tags to be blank instead of get some generic value.
|
22
|
+
attr_accessor :blank
|
23
|
+
# An hash of elements (represented by tag keys) that will be deleted from the DICOM objects on anonymization.
|
24
|
+
attr_reader :delete
|
25
|
+
# A boolean that if set as true, will make the anonymization delete all private tags.
|
26
|
+
attr_accessor :delete_private
|
27
|
+
# The cryptographic hash function to be used for encrypting DICOM values recorded in an audit trail file.
|
28
|
+
attr_reader :encryption
|
29
|
+
# A boolean that if set as true will cause all anonymized tags to be get enumerated values, to enable post-anonymization re-identification by the user.
|
30
|
+
attr_accessor :enumeration
|
31
|
+
# The logger level which is applied to DObject operations during anonymization (defaults to Logger::FATAL).
|
32
|
+
attr_reader :logger_level
|
33
|
+
# A boolean that if set as true will cause all anonymized files to be written with random file names (if write_path has been specified).
|
34
|
+
attr_accessor :random_file_name
|
35
|
+
# A boolean that if set as true, will cause the anonymization to run on all levels of the DICOM file tag hierarchy.
|
36
|
+
attr_accessor :recursive
|
37
|
+
# A boolean indicating whether or not UIDs shall be replaced when executing the anonymization.
|
38
|
+
attr_accessor :uid
|
39
|
+
# The DICOM UID root to use when generating new UIDs.
|
40
|
+
attr_accessor :uid_root
|
41
|
+
# The path where the anonymized files will be saved. If this value is not set, the original DICOM files will be overwritten.
|
42
|
+
attr_accessor :write_path
|
43
|
+
|
44
|
+
# Creates an Anonymizer instance.
|
45
|
+
#
|
46
|
+
# @note To customize logging behaviour, refer to the Logging module documentation.
|
47
|
+
# @param [Hash] options the options to create an anonymizer instance with
|
48
|
+
# @option options [String] :audit_trail a file name path (if the file contains old audit data, these are loaded and used in the current anonymization)
|
49
|
+
# @option options [Boolean] :blank toggles whether to set the values of anonymized elements as empty instead of some generic value
|
50
|
+
# @option options [Boolean] :delete_private toggles whether private elements are to be deleted
|
51
|
+
# @option options [TrueClass, Digest::Class] :encryption if set as true, the default hash function (MD5) will be used for representing DICOM values in an audit file. Otherwise a Digest class can be given, e.g. Digest::SHA256
|
52
|
+
# @option options [Boolean] :enumeration toggles whether (some) elements get enumerated values (to enable post-anonymization re-identification)
|
53
|
+
# @option options [Fixnum] :logger_level the logger level which is applied to DObject operations during anonymization (defaults to Logger::FATAL)
|
54
|
+
# @option options [Boolean] :random_file_name toggles whether anonymized files will be given random file names when rewritten (in combination with the :write_path option)
|
55
|
+
# @option options [Boolean] :recursive toggles whether to anonymize on all sub-levels of the DICOM object tag hierarchies
|
56
|
+
# @option options [Boolean] :uid toggles whether UIDs will be replaced with custom generated UIDs (beware that to preserve UID relations in studies/series, the audit_trail feature must be used)
|
57
|
+
# @option options [String] :uid_root an organization (or custom) UID root to use when replacing UIDs
|
58
|
+
# @option options [String] :write_path a directory where the anonymized files are re-written (if not specified, files are overwritten)
|
59
|
+
# @example Create an Anonymizer instance and increase the log output
|
60
|
+
# a = Anonymizer.new
|
61
|
+
# a.logger.level = Logger::INFO
|
62
|
+
# @example Perform anonymization using the audit trail feature
|
63
|
+
# a = Anonymizer.new(:audit_trail => 'trail.json')
|
64
|
+
# a.enumeration = true
|
65
|
+
# a.write_path = '//anonymized/'
|
66
|
+
# a.anonymize('//dicom/today/')
|
67
|
+
#
|
68
|
+
def initialize(options={})
|
69
|
+
# Transfer options to attributes:
|
70
|
+
@blank = options[:blank]
|
71
|
+
@delete_private = options[:delete_private]
|
72
|
+
@enumeration = options[:enumeration]
|
73
|
+
@logger_level = options[:logger_level] || Logger::FATAL
|
74
|
+
@random_file_name = options[:random_file_name]
|
75
|
+
@recursive = options[:recursive]
|
76
|
+
@uid = options[:uid]
|
77
|
+
@uid_root = options[:uid_root] ? options[:uid_root] : UID_ROOT
|
78
|
+
@write_path = options[:write_path]
|
79
|
+
# Array of folders to be processed for anonymization:
|
80
|
+
@folders = Array.new
|
81
|
+
# Folders that will be skipped:
|
82
|
+
@exceptions = Array.new
|
83
|
+
# Data elements which will be anonymized (the array will hold a list of tag strings):
|
84
|
+
@tags = Array.new
|
85
|
+
# Default values to use on anonymized data elements:
|
86
|
+
@values = Array.new
|
87
|
+
# Which data elements will have enumeration applied, if requested by the user:
|
88
|
+
@enumerations = Array.new
|
89
|
+
# We use a Hash to store information from DICOM files if enumeration is desired:
|
90
|
+
@enum_old_hash = Hash.new
|
91
|
+
@enum_new_hash = Hash.new
|
92
|
+
# All the files to be anonymized will be put in this array:
|
93
|
+
@files = Array.new
|
94
|
+
@prefixes = Hash.new
|
95
|
+
# Setup audit trail if requested:
|
96
|
+
if options[:audit_trail]
|
97
|
+
@audit_trail_file = options[:audit_trail]
|
98
|
+
if File.exists?(@audit_trail_file) && File.size(@audit_trail_file) > 2
|
99
|
+
# Load the pre-existing audit trail from file:
|
100
|
+
@audit_trail = AuditTrail.read(@audit_trail_file)
|
101
|
+
else
|
102
|
+
# Start from scratch with an empty audit trail:
|
103
|
+
@audit_trail = AuditTrail.new
|
104
|
+
end
|
105
|
+
# Set up encryption if indicated:
|
106
|
+
if options[:encryption]
|
107
|
+
require 'digest'
|
108
|
+
if options[:encryption].respond_to?(:hexdigest)
|
109
|
+
@encryption = options[:encryption]
|
110
|
+
else
|
111
|
+
@encryption = Digest::MD5
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
# Set the default data elements to be anonymized:
|
116
|
+
set_defaults
|
117
|
+
end
|
118
|
+
|
119
|
+
# Checks for equality.
|
120
|
+
#
|
121
|
+
# Other and self are considered equivalent if they are
|
122
|
+
# of compatible types and their attributes are equivalent.
|
123
|
+
#
|
124
|
+
# @param other an object to be compared with self.
|
125
|
+
# @return [Boolean] true if self and other are considered equivalent
|
126
|
+
#
|
127
|
+
def ==(other)
|
128
|
+
if other.respond_to?(:to_anonymizer)
|
129
|
+
other.send(:state) == state
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
alias_method :eql?, :==
|
134
|
+
|
135
|
+
# Anonymizes the given DICOM data with the settings of this Anonymizer instance.
|
136
|
+
#
|
137
|
+
# @param [String, DObject, Array<String, DObject>] data single or multiple DICOM data (directories, file paths, binary strings, DICOM objects)
|
138
|
+
# @return [Array<DObject>] an array of the anonymized DICOM objects
|
139
|
+
#
|
140
|
+
def anonymize(data)
|
141
|
+
dicom = prepare(data)
|
142
|
+
if @tags.length > 0
|
143
|
+
dicom.each do |dcm|
|
144
|
+
anonymize_dcm(dcm)
|
145
|
+
# Write DICOM object to file unless it was passed to the anonymizer as an object:
|
146
|
+
write(dcm) unless dcm.was_dcm_on_input
|
147
|
+
end
|
148
|
+
else
|
149
|
+
logger.warn("No tags have been selected for anonymization. Aborting anonymization.")
|
150
|
+
end
|
151
|
+
# Reset the ruby-dicom log threshold to its original level:
|
152
|
+
logger.level = @original_level
|
153
|
+
# Save the audit trail (if used):
|
154
|
+
@audit_trail.write(@audit_trail_file) if @audit_trail
|
155
|
+
logger.info("Anonymization complete.")
|
156
|
+
dicom
|
157
|
+
end
|
158
|
+
|
159
|
+
# Specifies that the given tag is to be completely deleted
|
160
|
+
# from the anonymized DICOM objects.
|
161
|
+
#
|
162
|
+
# @param [String] tag a data element tag
|
163
|
+
# @example Completely delete the Patient's Name tag from the DICOM files
|
164
|
+
# a.delete_tag('0010,0010')
|
165
|
+
#
|
166
|
+
def delete_tag(tag)
|
167
|
+
raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
|
168
|
+
raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
|
169
|
+
@delete[tag] = true
|
170
|
+
end
|
171
|
+
|
172
|
+
# Checks the enumeration status of this tag.
|
173
|
+
#
|
174
|
+
# @param [String] tag a data element tag
|
175
|
+
# @return [Boolean, NilClass] the enumeration status of the tag, or nil if the tag has no match
|
176
|
+
#
|
177
|
+
def enum(tag)
|
178
|
+
raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
|
179
|
+
raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
|
180
|
+
pos = @tags.index(tag)
|
181
|
+
if pos
|
182
|
+
return @enumerations[pos]
|
183
|
+
else
|
184
|
+
logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
|
185
|
+
return nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Computes a hash code for this object.
|
190
|
+
#
|
191
|
+
# @note Two objects with the same attributes will have the same hash code.
|
192
|
+
#
|
193
|
+
# @return [Fixnum] the object's hash code
|
194
|
+
#
|
195
|
+
def hash
|
196
|
+
state.hash
|
197
|
+
end
|
198
|
+
|
199
|
+
# Removes a tag from the list of tags that will be anonymized.
|
200
|
+
#
|
201
|
+
# @param [String] tag a data element tag
|
202
|
+
# @example Do not anonymize the Patient's Name tag
|
203
|
+
# a.remove_tag('0010,0010')
|
204
|
+
#
|
205
|
+
def remove_tag(tag)
|
206
|
+
raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
|
207
|
+
raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
|
208
|
+
pos = @tags.index(tag)
|
209
|
+
if pos
|
210
|
+
@tags.delete_at(pos)
|
211
|
+
@values.delete_at(pos)
|
212
|
+
@enumerations.delete_at(pos)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Sets the anonymization settings for the specified tag. If the tag is already present in the list
|
217
|
+
# of tags to be anonymized, its settings are updated, and if not, a new tag entry is created.
|
218
|
+
#
|
219
|
+
# @param [String] tag a data element tag
|
220
|
+
# @param [Hash] options the anonymization settings for the specified tag
|
221
|
+
# @option options [String, Integer, Float] :value the replacement value to be used when anonymizing this data element. Defaults to the pre-existing value and '' for new tags.
|
222
|
+
# @option options [String, Integer, Float] :enum specifies if enumeration is to be used for this tag. Defaults to the pre-existing value and false for new tags.
|
223
|
+
# @example Set the anonymization settings of the Patient's Name tag
|
224
|
+
# a.set_tag('0010,0010', :value => 'MrAnonymous', :enum => true)
|
225
|
+
#
|
226
|
+
def set_tag(tag, options={})
|
227
|
+
raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
|
228
|
+
raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
|
229
|
+
pos = @tags.index(tag)
|
230
|
+
if pos
|
231
|
+
# Update existing values:
|
232
|
+
@values[pos] = options[:value] if options[:value]
|
233
|
+
@enumerations[pos] = options[:enum] if options[:enum] != nil
|
234
|
+
else
|
235
|
+
# Add new elements:
|
236
|
+
@tags << tag
|
237
|
+
@values << (options[:value] ? options[:value] : default_value(tag))
|
238
|
+
@enumerations << (options[:enum] ? options[:enum] : false)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns self.
|
243
|
+
#
|
244
|
+
# @return [Anonymizer] self
|
245
|
+
#
|
246
|
+
def to_anonymizer
|
247
|
+
self
|
248
|
+
end
|
249
|
+
|
250
|
+
# Gives the value which will be used when anonymizing this tag.
|
251
|
+
#
|
252
|
+
# @note If enumeration is selected for a string type tag, a number will be
|
253
|
+
# appended in addition to the string that is returned here.
|
254
|
+
#
|
255
|
+
# @param [String] tag a data element tag
|
256
|
+
# @return [String, Integer, Float, NilClass] the replacement value for the specified tag, or nil if the tag is not matched
|
257
|
+
#
|
258
|
+
def value(tag)
|
259
|
+
raise ArgumentError, "Expected String, got #{tag.class}." unless tag.is_a?(String)
|
260
|
+
raise ArgumentError, "Expected a valid tag of format 'GGGG,EEEE', got #{tag}." unless tag.tag?
|
261
|
+
pos = @tags.index(tag)
|
262
|
+
if pos
|
263
|
+
return @values[pos]
|
264
|
+
else
|
265
|
+
logger.warn("The specified tag (#{tag}) was not found in the list of tags to be anonymized.")
|
266
|
+
return nil
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
|
271
|
+
private
|
272
|
+
|
273
|
+
|
274
|
+
# Performs anonymization on a DICOM object.
|
275
|
+
#
|
276
|
+
# @param [DObject] dcm a DICOM object
|
277
|
+
#
|
278
|
+
def anonymize_dcm(dcm)
|
279
|
+
# Extract the data element parents to investigate:
|
280
|
+
parents = element_parents(dcm)
|
281
|
+
parents.each do |parent|
|
282
|
+
# Anonymize the desired tags:
|
283
|
+
@tags.each_index do |j|
|
284
|
+
if parent.exists?(@tags[j])
|
285
|
+
element = parent[@tags[j]]
|
286
|
+
if element.is_a?(Element)
|
287
|
+
if @blank
|
288
|
+
value = ''
|
289
|
+
elsif @enumeration
|
290
|
+
old_value = element.value
|
291
|
+
# Only launch enumeration logic if there is an actual value to the data element:
|
292
|
+
if old_value
|
293
|
+
value = enumerated_value(old_value, j)
|
294
|
+
else
|
295
|
+
value = ''
|
296
|
+
end
|
297
|
+
else
|
298
|
+
# Use the value that has been set for this tag:
|
299
|
+
value = @values[j]
|
300
|
+
end
|
301
|
+
element.value = value
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
# Delete elements marked for deletion:
|
306
|
+
@delete.each_key do |tag|
|
307
|
+
parent.delete(tag) if parent.exists?(tag)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
# General DICOM object manipulation:
|
311
|
+
# Add a Patient Identity Removed attribute (as per
|
312
|
+
# DICOM PS 3.15, Annex E, E.1.1 De-Identifier, point 6):
|
313
|
+
dcm.add(Element.new('0012,0062', 'YES'))
|
314
|
+
# Add a De-Identification Method Code Sequence Item:
|
315
|
+
dcm.add(Sequence.new('0012,0064')) unless dcm.exists?('0012,0064')
|
316
|
+
i = dcm['0012,0064'].add_item
|
317
|
+
i.add(Element.new('0012,0063', 'De-identified by the ruby-dicom Anonymizer'))
|
318
|
+
# FIXME: At some point we should add a set of de-indentification method codes, as per
|
319
|
+
# DICOM PS 3.16 CID 7050 which corresponds to the settings chosen for the anonymizer.
|
320
|
+
# Delete the old File Meta Information group (as per
|
321
|
+
# DICOM PS 3.15, Annex E, E.1.1 De-Identifier, point 7):
|
322
|
+
dcm.delete_group('0002')
|
323
|
+
# Handle UIDs if requested:
|
324
|
+
replace_uids(parents) if @uid
|
325
|
+
# Delete private tags if indicated:
|
326
|
+
dcm.delete_private if @delete_private
|
327
|
+
end
|
328
|
+
|
329
|
+
# Gives the value to be used for the audit trail, which is either
|
330
|
+
# the original value itself, or an encrypted string based on it.
|
331
|
+
#
|
332
|
+
# @param [String, Integer, Float] original the original value of the tag to be anonymized
|
333
|
+
# @return [String, Integer, Float] with encryption, a hash string is returned, otherwise the original value
|
334
|
+
#
|
335
|
+
def at_value(original)
|
336
|
+
@encryption ? @encryption.hexdigest(original) : original
|
337
|
+
end
|
338
|
+
|
339
|
+
# Creates a hash that is used for storing information that is used when enumeration is selected.
|
340
|
+
#
|
341
|
+
def create_enum_hash
|
342
|
+
@enumerations.each_index do |i|
|
343
|
+
@enum_old_hash[@tags[i]] = Array.new
|
344
|
+
@enum_new_hash[@tags[i]] = Array.new
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Determines a default value to use for anonymizing the given tag.
|
349
|
+
#
|
350
|
+
# @param [String] tag a data element tag
|
351
|
+
# @return [String, Integer, Float] the default replacement value for a given tag
|
352
|
+
#
|
353
|
+
def default_value(tag)
|
354
|
+
name, vr = LIBRARY.name_and_vr(tag)
|
355
|
+
conversion = VALUE_CONVERSION[vr] || :to_s
|
356
|
+
case conversion
|
357
|
+
when :to_i then return 0
|
358
|
+
when :to_f then return 0.0
|
359
|
+
else
|
360
|
+
# Assume type is string and return an empty string:
|
361
|
+
return ''
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Creates a write path for the given DICOM object, based on the object's
|
366
|
+
# original file path and the write_path attribute.
|
367
|
+
#
|
368
|
+
# @param [DObject] dcm a DICOM object
|
369
|
+
# @return [String] the destination directory path
|
370
|
+
#
|
371
|
+
def destination(dcm)
|
372
|
+
# Split the source path into dir and file:
|
373
|
+
source_dir = File.dirname(dcm.source)
|
374
|
+
source_folders = source_dir.split(File::SEPARATOR)
|
375
|
+
target_folders = @write_path.split(File::SEPARATOR)
|
376
|
+
# If the first element is the current dir symbol, get rid of it:
|
377
|
+
source_folders.delete('.')
|
378
|
+
# Check for equalness of folder names in a range limited by the shortest array:
|
379
|
+
common_length = [source_folders.length, target_folders.length].min
|
380
|
+
uncommon_index = nil
|
381
|
+
common_length.times do |i|
|
382
|
+
if target_folders[i] != source_folders[i]
|
383
|
+
uncommon_index = i
|
384
|
+
break
|
385
|
+
end
|
386
|
+
end
|
387
|
+
# Create the output path by joining the two paths together using the determined index:
|
388
|
+
append_path = uncommon_index ? source_folders[uncommon_index..-1] : nil
|
389
|
+
[target_folders, append_path].compact.join(File::SEPARATOR)
|
390
|
+
end
|
391
|
+
|
392
|
+
# Extracts all parents from a DObject instance which potentially
|
393
|
+
# have child (data) elements. This typically means the DObject
|
394
|
+
# instance itself as well as items (i.e. not sequences).
|
395
|
+
# Note that unless the @recursive attribute has been set,
|
396
|
+
# this method will only return the DObject (placed inside an array).
|
397
|
+
#
|
398
|
+
# @param [DObject] dcm a DICOM object
|
399
|
+
# @return [Array<DObject, Item>] an array containing either just a DObject or also all parental child items within the tag hierarchy
|
400
|
+
#
|
401
|
+
def element_parents(dcm)
|
402
|
+
parents = Array.new
|
403
|
+
parents << dcm
|
404
|
+
if @recursive
|
405
|
+
dcm.sequences.each do |s|
|
406
|
+
parents += element_parents_recursive(s)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
parents
|
410
|
+
end
|
411
|
+
|
412
|
+
# Recursively extracts all item parents from a sequence instance (including
|
413
|
+
# any sub-sequences) which actually contain child (data) elements.
|
414
|
+
#
|
415
|
+
# @param [Sequence] sequence a Sequence instance
|
416
|
+
# @return [Array<Item>] an array containing items within the tag hierarchy that contains child elements
|
417
|
+
#
|
418
|
+
def element_parents_recursive(sequence)
|
419
|
+
parents = Array.new
|
420
|
+
sequence.items.each do |i|
|
421
|
+
parents << i if i.elements?
|
422
|
+
i.sequences.each do |s|
|
423
|
+
parents += element_parents_recursive(s)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
parents
|
427
|
+
end
|
428
|
+
|
429
|
+
# Handles the enumeration for the given data element tag.
|
430
|
+
# If its value has been encountered before, its corresponding enumerated
|
431
|
+
# replacement value is retrieved, and if a new original value is encountered,
|
432
|
+
# a new enumerated replacement value is found by increasing an index by 1.
|
433
|
+
#
|
434
|
+
# @param [String, Integer, Float] original the original value of the tag to be anonymized
|
435
|
+
# @param [Fixnum] j the index of this tag in the tag-related instance arrays
|
436
|
+
# @return [String, Integer, Float] the replacement value which is used for the anonymization of the tag
|
437
|
+
#
|
438
|
+
def enumerated_value(original, j)
|
439
|
+
# Is enumeration requested for this tag?
|
440
|
+
if @enumerations[j]
|
441
|
+
if @audit_trail
|
442
|
+
# Check if the UID has been encountered already:
|
443
|
+
replacement = @audit_trail.replacement(@tags[j], at_value(original))
|
444
|
+
unless replacement
|
445
|
+
# This original value has not been encountered yet. Determine the index to use.
|
446
|
+
index = @audit_trail.records(@tags[j]).length + 1
|
447
|
+
# Create the replacement value:
|
448
|
+
if @values[j].is_a?(String)
|
449
|
+
replacement = @values[j] + index.to_s
|
450
|
+
else
|
451
|
+
replacement = @values[j] + index
|
452
|
+
end
|
453
|
+
# Add this tag record to the audit trail:
|
454
|
+
@audit_trail.add_record(@tags[j], at_value(original), replacement)
|
455
|
+
end
|
456
|
+
else
|
457
|
+
# Retrieve earlier used anonymization values:
|
458
|
+
previous_old = @enum_old_hash[@tags[j]]
|
459
|
+
previous_new = @enum_new_hash[@tags[j]]
|
460
|
+
p_index = previous_old.length
|
461
|
+
if previous_old.index(original) == nil
|
462
|
+
# Current value has not been encountered before:
|
463
|
+
replacement = @values[j]+(p_index + 1).to_s
|
464
|
+
# Store value in array (and hash):
|
465
|
+
previous_old << original
|
466
|
+
previous_new << replacement
|
467
|
+
@enum_old_hash[@tags[j]] = previous_old
|
468
|
+
@enum_new_hash[@tags[j]] = previous_new
|
469
|
+
else
|
470
|
+
# Current value has been observed before:
|
471
|
+
replacement = previous_new[previous_old.index(original)]
|
472
|
+
end
|
473
|
+
end
|
474
|
+
else
|
475
|
+
replacement = @values[j]
|
476
|
+
end
|
477
|
+
return replacement
|
478
|
+
end
|
479
|
+
|
480
|
+
# Establishes a prefix for a given UID tag.
|
481
|
+
# This makes it somewhat easier to distinguish
|
482
|
+
# between different types of random generated UIDs.
|
483
|
+
#
|
484
|
+
# @param [String] tag a data element string tag
|
485
|
+
#
|
486
|
+
def prefix(tag)
|
487
|
+
if @prefixes[tag]
|
488
|
+
@prefixes[tag]
|
489
|
+
else
|
490
|
+
@prefixes[tag] = @prefixes.length + 1
|
491
|
+
@prefixes[tag]
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
# Prepares the data for anonymization.
|
496
|
+
#
|
497
|
+
# @param [String, DObject, Array<String, DObject>] data single or multiple DICOM data (directories, file paths, binary strings, DICOM objects)
|
498
|
+
# @return [Array] the original data (wrapped in an array) as well as an array of loaded DObject instances
|
499
|
+
#
|
500
|
+
def prepare(data)
|
501
|
+
logger.info("Loading DICOM data.")
|
502
|
+
# Temporarily adjust the ruby-dicom log threshold (usually to suppress messages from the DObject class):
|
503
|
+
@original_level = logger.level
|
504
|
+
logger.level = @logger_level
|
505
|
+
dicom = DICOM.load(data)
|
506
|
+
logger.level = @original_level
|
507
|
+
logger.info("#{dicom.length} DICOM objects have been prepared for anonymization.")
|
508
|
+
logger.level = @logger_level
|
509
|
+
# Set up enumeration if requested:
|
510
|
+
create_enum_hash if @enumeration
|
511
|
+
require 'securerandom' if @random_file_name
|
512
|
+
dicom
|
513
|
+
end
|
514
|
+
|
515
|
+
# Replaces the UIDs of the given DICOM object.
|
516
|
+
#
|
517
|
+
# @note Empty UIDs are ignored (we don't generate new UIDs for these).
|
518
|
+
# @note If AuditTrail is set, the relationship between old and new UIDs are preserved,
|
519
|
+
# and the relations between files in a study/series should remain valid.
|
520
|
+
# @param [Array<DObject, Item>] parents dicom parent objects who's child elements will be investigated
|
521
|
+
#
|
522
|
+
def replace_uids(parents)
|
523
|
+
parents.each do |parent|
|
524
|
+
parent.each_element do |element|
|
525
|
+
if element.vr == ('UI') and !@static_uids[element.tag]
|
526
|
+
original = element.value
|
527
|
+
if original && original.length > 0
|
528
|
+
# We have a UID value, go ahead and replace it:
|
529
|
+
if @audit_trail
|
530
|
+
# Check if the UID has been encountered already:
|
531
|
+
replacement = @audit_trail.replacement('uids', original)
|
532
|
+
unless replacement
|
533
|
+
# The UID has not been stored previously. Generate a new one:
|
534
|
+
replacement = DICOM.generate_uid(@uid_root, prefix(element.tag))
|
535
|
+
# Add this tag record to the audit trail:
|
536
|
+
@audit_trail.add_record('uids', original, replacement)
|
537
|
+
end
|
538
|
+
# Replace the UID in the DICOM object:
|
539
|
+
element.value = replacement
|
540
|
+
else
|
541
|
+
# We don't care about preserving UID relations. Just insert a custom UID:
|
542
|
+
element.value = DICOM.generate_uid(@uid_root, prefix(element.tag))
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
# Sets up some default information variables that are used by the Anonymizer.
|
551
|
+
#
|
552
|
+
def set_defaults
|
553
|
+
# Some UIDs should not be remapped even if uid anonymization has been requested:
|
554
|
+
@static_uids = {
|
555
|
+
# Private related:
|
556
|
+
'0002,0100' => true,
|
557
|
+
'0004,1432' => true,
|
558
|
+
# Coding scheme related:
|
559
|
+
'0008,010C' => true,
|
560
|
+
'0008,010D' => true,
|
561
|
+
# Transfer syntax related:
|
562
|
+
'0002,0010' => true,
|
563
|
+
'0400,0010' => true,
|
564
|
+
'0400,0510' => true,
|
565
|
+
'0004,1512' => true,
|
566
|
+
# SOP class related:
|
567
|
+
'0000,0002' => true,
|
568
|
+
'0000,0003' => true,
|
569
|
+
'0002,0002' => true,
|
570
|
+
'0004,1510' => true,
|
571
|
+
'0004,151A' => true,
|
572
|
+
'0008,0016' => true,
|
573
|
+
'0008,001A' => true,
|
574
|
+
'0008,001B' => true,
|
575
|
+
'0008,0062' => true,
|
576
|
+
'0008,1150' => true,
|
577
|
+
'0008,115A' => true
|
578
|
+
}
|
579
|
+
# Sets up default tags that will be anonymized, along with default replacement values and enumeration settings.
|
580
|
+
# This data is stored in 3 separate instance arrays for tags, values and enumeration.
|
581
|
+
data = [
|
582
|
+
['0008,0012', '20000101', false], # Instance Creation Date
|
583
|
+
['0008,0013', '000000.00', false], # Instance Creation Time
|
584
|
+
['0008,0020', '20000101', false], # Study Date
|
585
|
+
['0008,0021', '20000101', false], # Series Date
|
586
|
+
['0008,0022', '20000101', false], # Acquisition Date
|
587
|
+
['0008,0023', '20000101', false], # Image Date
|
588
|
+
['0008,0030', '000000.00', false], # Study Time
|
589
|
+
['0008,0031', '000000.00', false], # Series Time
|
590
|
+
['0008,0032', '000000.00', false], # Acquisition Time
|
591
|
+
['0008,0033', '000000.00', false], # Image Time
|
592
|
+
['0008,0050', '', true], # Accession Number
|
593
|
+
['0008,0080', 'Institution', true], # Institution name
|
594
|
+
['0008,0081', 'Address', true], # Institution Address
|
595
|
+
['0008,0090', 'Physician', true], # Referring Physician's name
|
596
|
+
['0008,1010', 'Station', true], # Station name
|
597
|
+
['0008,1040', 'Department', true], # Institutional Department name
|
598
|
+
['0008,1070', 'Operator', true], # Operator's Name
|
599
|
+
['0010,0010', 'Patient', true], # Patient's name
|
600
|
+
['0010,0020', 'ID', true], # Patient's ID
|
601
|
+
['0010,0030', '20000101', false], # Patient's Birth Date
|
602
|
+
['0010,0040', 'O', false], # Patient's Sex
|
603
|
+
['0010,1010', '', false], # Patient's Age
|
604
|
+
['0020,4000', '', false], # Image Comments
|
605
|
+
].transpose
|
606
|
+
@tags = data[0]
|
607
|
+
@values = data[1]
|
608
|
+
@enumerations = data[2]
|
609
|
+
# Tags to be deleted completely during anonymization:
|
610
|
+
@delete = Hash.new
|
611
|
+
end
|
612
|
+
|
613
|
+
# Collects the attributes of this instance.
|
614
|
+
#
|
615
|
+
# @return [Array] an array of attributes
|
616
|
+
#
|
617
|
+
def state
|
618
|
+
[
|
619
|
+
@tags, @values, @enumerations, @delete, @blank,
|
620
|
+
@delete_private, @enumeration, @logger_level,
|
621
|
+
@random_file_name, @recursive, @uid, @uid_root, @write_path
|
622
|
+
]
|
623
|
+
end
|
624
|
+
|
625
|
+
# Writes a DICOM object to file.
|
626
|
+
#
|
627
|
+
# @param [DObject] dcm a DICOM object
|
628
|
+
#
|
629
|
+
def write(dcm)
|
630
|
+
if @write_path
|
631
|
+
# The DICOM object is to be written to a separate directory. If the
|
632
|
+
# original and the new directories have a common root, this is taken into
|
633
|
+
# consideration when determining the object's write path:
|
634
|
+
path = destination(dcm)
|
635
|
+
if @random_file_name
|
636
|
+
file_name = "#{SecureRandom.hex(16)}.dcm"
|
637
|
+
else
|
638
|
+
file_name = File.basename(dcm.source)
|
639
|
+
end
|
640
|
+
dcm.write(File.join(path, file_name))
|
641
|
+
else
|
642
|
+
# The original DICOM file is overwritten with the anonymized DICOM object:
|
643
|
+
dcm.write(dcm.source)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
end
|
648
|
+
|
649
|
+
end
|