dicom 0.6.1 → 0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +42 -20
- data/DOCUMENTATION +117 -71
- data/README +3 -3
- data/lib/dicom.rb +23 -12
- data/lib/{Anonymizer.rb → dicom/Anonymizer.rb} +101 -79
- data/lib/{DClient.rb → dicom/DClient.rb} +12 -11
- data/lib/{DLibrary.rb → dicom/DLibrary.rb} +53 -31
- data/lib/dicom/DObject.rb +1579 -0
- data/lib/{DRead.rb → dicom/DRead.rb} +42 -43
- data/lib/{DServer.rb → dicom/DServer.rb} +34 -20
- data/lib/{DWrite.rb → dicom/DWrite.rb} +27 -31
- data/lib/{Dictionary.rb → dicom/Dictionary.rb} +434 -32
- data/lib/dicom/FileHandler.rb +50 -0
- data/lib/{Link.rb → dicom/Link.rb} +312 -167
- data/lib/{Stream.rb → dicom/Stream.rb} +1 -1
- data/lib/dicom/ruby_extensions.rb +47 -0
- metadata +16 -15
- data/lib/DObject.rb +0 -1194
- data/lib/ruby_extensions.rb +0 -36
@@ -1,4 +1,5 @@
|
|
1
|
-
# Copyright 2008-
|
1
|
+
# Copyright 2008-2010 Christoffer Lervag
|
2
|
+
|
2
3
|
module DICOM
|
3
4
|
|
4
5
|
# Class for anonymizing DICOM files:
|
@@ -6,21 +7,25 @@ module DICOM
|
|
6
7
|
# ftp://medical.nema.org/medical/dicom/Supps/sup142_03.pdf
|
7
8
|
class Anonymizer
|
8
9
|
|
9
|
-
attr_accessor :blank, :enumeration, :identity_file, :verbose, :write_path
|
10
|
+
attr_accessor :blank, :enumeration, :identity_file, :remove_private, :verbose, :write_path
|
10
11
|
|
11
12
|
# Initialize the Anonymizer instance:
|
12
13
|
def initialize(opts={})
|
13
14
|
# Default verbosity is true: # NB: verbosity is not used currently
|
14
15
|
@verbose = opts[:verbose]
|
15
16
|
@verbose = true if @verbose == nil
|
16
|
-
# Load library:
|
17
|
-
@lib = DLibrary.new
|
18
17
|
# Default value of accessors:
|
18
|
+
# Replace all values with a blank string?
|
19
19
|
@blank = false
|
20
|
+
# Enumerate selected replacement values?
|
20
21
|
@enumeration = false
|
22
|
+
# All private tags may be removed if desired:
|
23
|
+
@remove_private = false
|
24
|
+
# A separate path may be selected for writing the anonymized files:
|
21
25
|
@write_path = nil
|
22
26
|
# Array of folders to be processed for anonymization:
|
23
27
|
@folders = Array.new
|
28
|
+
# Folders that will be skipped:
|
24
29
|
@exceptions = Array.new
|
25
30
|
# Data elements which will be anonymized (the array will hold a list of tag strings):
|
26
31
|
@tags = Array.new
|
@@ -36,19 +41,23 @@ module DICOM
|
|
36
41
|
# Write paths will be determined later and put in this array:
|
37
42
|
@write_paths = Array.new
|
38
43
|
# Set the default data elements to be anonymized:
|
39
|
-
set_defaults
|
40
|
-
end
|
44
|
+
set_defaults
|
45
|
+
end
|
41
46
|
|
42
47
|
|
43
48
|
# Adds a folder who's files will be anonymized:
|
44
49
|
def add_folder(path)
|
45
|
-
@folders
|
50
|
+
@folders << path if path
|
46
51
|
end
|
47
52
|
|
48
53
|
|
49
54
|
# Adds an exception folder that is to be avoided when anonymizing:
|
50
55
|
def add_exception(path)
|
51
|
-
|
56
|
+
if path
|
57
|
+
# Remove last character if the path ends with a file separator:
|
58
|
+
path.chop! if path[-1..-1] == File::SEPARATOR
|
59
|
+
@exceptions << path if path
|
60
|
+
end
|
52
61
|
end
|
53
62
|
|
54
63
|
|
@@ -61,9 +70,9 @@ module DICOM
|
|
61
70
|
if tag.is_a?(String)
|
62
71
|
if tag.length == 9
|
63
72
|
# Add anonymization information for this tag:
|
64
|
-
@tags
|
65
|
-
@values
|
66
|
-
@enum
|
73
|
+
@tags << tag
|
74
|
+
@values << value
|
75
|
+
@enum << enum
|
67
76
|
else
|
68
77
|
puts "Warning: Invalid tag length. Please use the form 'GGGG,EEEE'."
|
69
78
|
end
|
@@ -73,7 +82,7 @@ module DICOM
|
|
73
82
|
else
|
74
83
|
puts "Warning: No tag supplied. Nothing to add."
|
75
84
|
end
|
76
|
-
end
|
85
|
+
end
|
77
86
|
|
78
87
|
|
79
88
|
# Set enumeration status for a specific tag (toggle true/false)
|
@@ -88,7 +97,7 @@ module DICOM
|
|
88
97
|
else
|
89
98
|
puts "Specified tag not found in anonymization array. No changes made."
|
90
99
|
end
|
91
|
-
end
|
100
|
+
end
|
92
101
|
|
93
102
|
|
94
103
|
# Changes the value used in anonymization for a specific tag:
|
@@ -103,7 +112,7 @@ module DICOM
|
|
103
112
|
else
|
104
113
|
puts "Specified tag not found in anonymization array. No changes made."
|
105
114
|
end
|
106
|
-
end
|
115
|
+
end
|
107
116
|
|
108
117
|
|
109
118
|
# Executes the anonymization process:
|
@@ -113,7 +122,7 @@ module DICOM
|
|
113
122
|
puts "Initiating anonymization process."
|
114
123
|
start_time = Time.now.to_f
|
115
124
|
puts "Searching for files..."
|
116
|
-
load_files
|
125
|
+
load_files
|
117
126
|
puts "Done."
|
118
127
|
if @files.length > 0
|
119
128
|
if @tags.length > 0
|
@@ -121,7 +130,7 @@ module DICOM
|
|
121
130
|
if @write_path
|
122
131
|
# Determine the write paths, as anonymized files will be written to a separate location:
|
123
132
|
puts "Processing write paths..."
|
124
|
-
process_write_paths
|
133
|
+
process_write_paths
|
125
134
|
puts "Done"
|
126
135
|
else
|
127
136
|
# Overwriting old files:
|
@@ -130,59 +139,71 @@ module DICOM
|
|
130
139
|
end
|
131
140
|
# If the user wants enumeration, we need to prepare variables for storing
|
132
141
|
# existing information associated with each tag:
|
133
|
-
create_enum_hash
|
142
|
+
create_enum_hash if @enumeration
|
134
143
|
# Start the read/update/write process:
|
135
144
|
puts "Initiating read/update/write process (This may take some time)..."
|
136
145
|
# Monitor whether every file read/write was successful:
|
137
146
|
all_read = true
|
138
147
|
all_write = true
|
148
|
+
files_written = 0
|
149
|
+
files_failed_read = 0
|
139
150
|
@files.each_index do |i|
|
140
151
|
# Read existing file to DICOM object:
|
141
|
-
obj = DICOM::DObject.new(@files[i], :verbose => verbose
|
152
|
+
obj = DICOM::DObject.new(@files[i], :verbose => verbose)
|
142
153
|
if obj.read_success
|
143
154
|
# Anonymize the desired tags:
|
144
155
|
@tags.each_index do |j|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
# Get old value:
|
149
|
-
current = obj.get_value(@tags[j])
|
150
|
-
# Only launch enumeration logic if tag exists:
|
151
|
-
if current != false
|
152
|
-
value = get_enumeration_value(current, j)
|
153
|
-
else
|
156
|
+
positions = obj.get_pos(@tags[j])
|
157
|
+
positions.each do |pos|
|
158
|
+
if @blank
|
154
159
|
value = ""
|
160
|
+
elsif @enumeration
|
161
|
+
old_value = obj.get_value(pos, :silent => true)
|
162
|
+
# Only launch enumeration logic if tag exists:
|
163
|
+
if old_value
|
164
|
+
value = get_enumeration_value(old_value, j)
|
165
|
+
else
|
166
|
+
value = ""
|
167
|
+
end
|
168
|
+
else
|
169
|
+
# Value is simply value in array:
|
170
|
+
value = @values[j]
|
155
171
|
end
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
end # of if @blank..else..
|
160
|
-
# Update DICOM object with new value:
|
161
|
-
obj.set_value(value, @tags[j], :create => false)
|
172
|
+
# Update DICOM object with new value:
|
173
|
+
obj.set_value(value, pos, :create => false, :silent => true)
|
174
|
+
end
|
162
175
|
end
|
176
|
+
# Remove private tags?
|
177
|
+
obj.remove_private if @remove_private
|
163
178
|
# Write DICOM file:
|
164
179
|
obj.write(@write_paths[i])
|
165
|
-
|
180
|
+
if obj.write_success
|
181
|
+
files_written += 1
|
182
|
+
else
|
183
|
+
all_write = false
|
184
|
+
end
|
166
185
|
else
|
167
186
|
all_read = false
|
187
|
+
files_failed_read += 1
|
168
188
|
end
|
169
|
-
end
|
189
|
+
end
|
190
|
+
# Finished anonymizing files. Print elapsed time and status of anonymization:
|
170
191
|
end_time = Time.now.to_f
|
171
192
|
puts "Anonymization process completed!"
|
172
193
|
if all_read
|
173
194
|
puts "All files in specified folder(s) were SUCCESSFULLY read to DICOM objects."
|
174
195
|
else
|
175
|
-
puts "Some files were NOT successfully read. If folder(s) contain non-DICOM files,
|
196
|
+
puts "Some files were NOT successfully read (#{files_failed_read} files). If folder(s) contain non-DICOM files, this is probably the reason."
|
176
197
|
end
|
177
198
|
if all_write
|
178
|
-
puts "All DICOM objects were SUCCESSFULLY written as DICOM files."
|
199
|
+
puts "All DICOM objects were SUCCESSFULLY written as DICOM files (#{files_written} files)."
|
179
200
|
else
|
180
|
-
puts "Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look."
|
201
|
+
puts "Some DICOM objects were NOT succesfully written to file. You are advised to have a closer look (#{files_written} files succesfully written)."
|
181
202
|
end
|
182
203
|
# Has user requested enumeration and specified an identity file in which to store the anonymized values?
|
183
204
|
if @enumeration and @identity_file
|
184
205
|
puts "Writing identity file."
|
185
|
-
write_identity_file
|
206
|
+
write_identity_file
|
186
207
|
puts "Done"
|
187
208
|
end
|
188
209
|
elapsed = (end_time-start_time).to_s
|
@@ -199,7 +220,7 @@ module DICOM
|
|
199
220
|
|
200
221
|
# Prints a list of which tags are currently selected for anonymization along with
|
201
222
|
# replacement values that will be used and enumeration status.
|
202
|
-
def print
|
223
|
+
def print
|
203
224
|
# Extract the string lengths which are needed to make the formatting nice:
|
204
225
|
names = Array.new
|
205
226
|
types = Array.new
|
@@ -208,9 +229,9 @@ module DICOM
|
|
208
229
|
type_lengths = Array.new
|
209
230
|
value_lengths = Array.new
|
210
231
|
@tags.each_index do |i|
|
211
|
-
arr =
|
212
|
-
names
|
213
|
-
types
|
232
|
+
arr = LIBRARY.get_name_vr(@tags[i])
|
233
|
+
names << arr[0]
|
234
|
+
types << arr[1]
|
214
235
|
tag_lengths[i] = @tags[i].length
|
215
236
|
name_lengths[i] = names[i].length
|
216
237
|
type_lengths[i] = types[i].length
|
@@ -243,13 +264,13 @@ module DICOM
|
|
243
264
|
value = @values[i]
|
244
265
|
end
|
245
266
|
tag = @tags[i]
|
246
|
-
lines
|
267
|
+
lines << tag + f1 + names[i] + f2 + types[i] + f3 + value.to_s + f4 + enum.to_s
|
247
268
|
end
|
248
269
|
# Print to screen:
|
249
270
|
lines.each do |line|
|
250
271
|
puts line
|
251
272
|
end
|
252
|
-
end
|
273
|
+
end
|
253
274
|
|
254
275
|
|
255
276
|
# Removes a tag from the list of tags that will be anonymized:
|
@@ -262,7 +283,7 @@ module DICOM
|
|
262
283
|
else
|
263
284
|
puts "Specified tag not found in anonymization array. No changes made."
|
264
285
|
end
|
265
|
-
end
|
286
|
+
end
|
266
287
|
|
267
288
|
|
268
289
|
# The following methods are private:
|
@@ -287,11 +308,11 @@ module DICOM
|
|
287
308
|
result = index - 1
|
288
309
|
end
|
289
310
|
return result
|
290
|
-
end
|
311
|
+
end
|
291
312
|
|
292
313
|
|
293
314
|
# Creates a hash that is used for storing information used when enumeration is desired.
|
294
|
-
def create_enum_hash
|
315
|
+
def create_enum_hash
|
295
316
|
@enum.each_index do |i|
|
296
317
|
@enum_old_hash[@tags[i]] = Array.new
|
297
318
|
@enum_new_hash[@tags[i]] = Array.new
|
@@ -311,23 +332,23 @@ module DICOM
|
|
311
332
|
# Current value has not been encountered before:
|
312
333
|
value = @values[j]+(p_index + 1).to_s
|
313
334
|
# Store value in array (and hash):
|
314
|
-
previous_old
|
315
|
-
previous_new
|
335
|
+
previous_old << current
|
336
|
+
previous_new << value
|
316
337
|
@enum_old_hash[@tags[j]] = previous_old
|
317
338
|
@enum_new_hash[@tags[j]] = previous_new
|
318
339
|
else
|
319
340
|
# Current value has been observed before:
|
320
341
|
value = previous_new[previous_old.index(current)]
|
321
|
-
end
|
342
|
+
end
|
322
343
|
else
|
323
344
|
value = @values[j]
|
324
|
-
end
|
345
|
+
end
|
325
346
|
return value
|
326
|
-
end
|
347
|
+
end
|
327
348
|
|
328
349
|
|
329
350
|
# Discover all the files contained in the specified directory and all its sub-directories:
|
330
|
-
def load_files
|
351
|
+
def load_files
|
331
352
|
# Load find library:
|
332
353
|
require 'find'
|
333
354
|
# Iterate through the folders (and its subfolders) to extract all files:
|
@@ -344,50 +365,50 @@ module DICOM
|
|
344
365
|
Find.prune # Don't look any further into this directory.
|
345
366
|
end
|
346
367
|
else
|
347
|
-
@files
|
368
|
+
@files << path # Store the file in our array
|
348
369
|
end
|
349
370
|
end
|
350
|
-
end
|
351
|
-
end
|
371
|
+
end
|
372
|
+
end
|
352
373
|
|
353
374
|
|
354
375
|
# Analyses the write_path and the 'read' file path to determine if the have some common root.
|
355
376
|
# If there are parts of file that exist also in write path, it will not add those parts to write_path.
|
356
|
-
def process_write_paths
|
357
|
-
# First make sure @write_path ends with a
|
358
|
-
last_character = @write_path[
|
359
|
-
@write_path = @write_path +
|
360
|
-
#
|
377
|
+
def process_write_paths
|
378
|
+
# First make sure @write_path ends with a file separator character:
|
379
|
+
last_character = @write_path[-1..-1]
|
380
|
+
@write_path = @write_path + File::SEPARATOR unless last_character == File::SEPARATOR
|
381
|
+
# Differing behaviour if we have one, or several files in our array:
|
361
382
|
if @files.length == 1
|
362
383
|
# One file.
|
363
384
|
# Write path is requested write path + old file name:
|
364
|
-
str_arr = @files[0].split(
|
365
|
-
@write_paths
|
385
|
+
str_arr = @files[0].split(File::SEPARATOR)
|
386
|
+
@write_paths << @write_path + str_arr.last
|
366
387
|
else
|
367
388
|
# Several files.
|
368
389
|
# Find out how much of the path they have in common, remove that and
|
369
390
|
# add the remaining to the @write_path:
|
370
|
-
str_arr = @files[0].split(
|
391
|
+
str_arr = @files[0].split(File::SEPARATOR)
|
371
392
|
last_match_index = common_path(str_arr, 0)
|
372
393
|
if last_match_index >= 0
|
373
394
|
# Remove the matching folders from the path that will be added to @write_path:
|
374
395
|
@files.each do |file|
|
375
|
-
arr = file.split(
|
376
|
-
part_to_write = arr[(last_match_index+1)..(arr.length-1)].join(
|
377
|
-
@write_paths
|
396
|
+
arr = file.split(File::SEPARATOR)
|
397
|
+
part_to_write = arr[(last_match_index+1)..(arr.length-1)].join(File::SEPARATOR)
|
398
|
+
@write_paths << @write_path + part_to_write
|
378
399
|
end
|
379
400
|
else
|
380
401
|
# No common folders. Add all of original path to write path:
|
381
402
|
@files.each do |file|
|
382
|
-
@write_paths
|
403
|
+
@write_paths << @write_path + file
|
383
404
|
end
|
384
405
|
end
|
385
|
-
end
|
386
|
-
end
|
406
|
+
end
|
407
|
+
end
|
387
408
|
|
388
409
|
|
389
|
-
# Default tags that will be anonymized, along with
|
390
|
-
def set_defaults
|
410
|
+
# Default tags that will be anonymized, along with default replacement value and enumeration setting.
|
411
|
+
def set_defaults
|
391
412
|
data = [
|
392
413
|
["0008,0012", "20000101", false], # Instance Creation Date
|
393
414
|
["0008,0013", "000000.00", false], # Instance Creation Time
|
@@ -398,6 +419,7 @@ module DICOM
|
|
398
419
|
["0008,0080", "Institution", true], # Institution name
|
399
420
|
["0008,0090", "Physician", true], # Referring Physician's name
|
400
421
|
["0008,1010", "Station", true], # Station name
|
422
|
+
["0008,1070", "Operator", true], # Operator's Name
|
401
423
|
["0010,0010", "Patient", true], # Patient's name
|
402
424
|
["0010,0020", "ID", true], # Patient's ID
|
403
425
|
["0010,0030", "20000101", false], # Patient's Birth Date
|
@@ -407,12 +429,12 @@ module DICOM
|
|
407
429
|
@tags = data[0]
|
408
430
|
@values = data[1]
|
409
431
|
@enum = data[2]
|
410
|
-
end
|
432
|
+
end
|
411
433
|
|
412
434
|
|
413
435
|
# Writes an identity file, which allows reidentification of DICOM files that have been anonymized
|
414
436
|
# using the enumeration feature. Values will be saved in a text file, using semi colon delineation.
|
415
|
-
def write_identity_file
|
437
|
+
def write_identity_file
|
416
438
|
# Open file and prepare to write text:
|
417
439
|
File.open( @identity_file, 'w' ) do |output|
|
418
440
|
# Cycle through each
|
@@ -428,10 +450,10 @@ module DICOM
|
|
428
450
|
end
|
429
451
|
# Print empty line for separation between different tags:
|
430
452
|
output.print "\n"
|
431
|
-
end
|
432
|
-
end
|
433
|
-
end
|
434
|
-
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|
435
457
|
|
436
458
|
|
437
459
|
end # of class
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2009 Christoffer Lervag
|
1
|
+
# Copyright 2009-2010 Christoffer Lervag
|
2
2
|
|
3
3
|
module DICOM
|
4
4
|
|
@@ -16,7 +16,6 @@ module DICOM
|
|
16
16
|
@port = port
|
17
17
|
# Optional parameters (and default values):
|
18
18
|
@ae = options[:ae] || "RUBY_DICOM"
|
19
|
-
@lib = options[:lib] || DLibrary.new
|
20
19
|
@host_ae = options[:host_ae] || "DEFAULT"
|
21
20
|
@max_package_size = options[:max_package_size] || 32768 # 16384
|
22
21
|
@timeout = options[:timeout] || 10 # seconds
|
@@ -139,7 +138,7 @@ module DICOM
|
|
139
138
|
# Send a DICOM file to a service class provider (SCP/PACS).
|
140
139
|
def send(file_path)
|
141
140
|
# Load the DICOM file from the specified path:
|
142
|
-
obj = DObject.new(file_path, :verbose => false
|
141
|
+
obj = DObject.new(file_path, :verbose => false)
|
143
142
|
if obj.read_success
|
144
143
|
# Get the SOP Class UID (abstract syntax) from the DICOM obj:
|
145
144
|
@abstract_syntax = obj.get_value("0008,0016")
|
@@ -219,7 +218,7 @@ module DICOM
|
|
219
218
|
@link.build_association_request(@application_context_uid, @abstract_syntax, @transfer_syntax, @user_information)
|
220
219
|
@connection = TCPSocket.new(@host_ip, @port)
|
221
220
|
@link.transmit(@connection)
|
222
|
-
info = @link.
|
221
|
+
info = @link.receive_multiple_transmissions(@connection).first
|
223
222
|
# Interpret the results:
|
224
223
|
if info[:valid]
|
225
224
|
if info[:pdu] == "02"
|
@@ -309,9 +308,11 @@ module DICOM
|
|
309
308
|
@link.build_data_fragment(@data_elements) # (uses flag = 02)
|
310
309
|
@link.transmit(@connection)
|
311
310
|
# Listen for incoming file data:
|
312
|
-
@link.handle_incoming_data(@connection, path)
|
313
|
-
|
314
|
-
|
311
|
+
success = @link.handle_incoming_data(@connection, path)
|
312
|
+
if success
|
313
|
+
# Send confirmation response:
|
314
|
+
@link.handle_response(@connection)
|
315
|
+
end
|
315
316
|
end
|
316
317
|
# Close the DICOM link:
|
317
318
|
establish_release
|
@@ -409,7 +410,7 @@ module DICOM
|
|
409
410
|
@command_elements = [
|
410
411
|
["0000,0002", "UI", @abstract_syntax], # Affected SOP Class UID
|
411
412
|
["0000,0100", "US", 16], # Command Field: 16 (C-GET-RQ)
|
412
|
-
["0000,0600", "AE",
|
413
|
+
["0000,0600", "AE", @ae], # Destination is ourselves
|
413
414
|
["0000,0700", "US", 0], # Priority: 0: medium
|
414
415
|
["0000,0800", "US", 1] # Data Set Type: 1
|
415
416
|
]
|
@@ -570,12 +571,12 @@ module DICOM
|
|
570
571
|
end
|
571
572
|
|
572
573
|
|
573
|
-
# Set user information [item type code,
|
574
|
+
# Set user information [item type code, VR, value]
|
574
575
|
def set_user_information_array
|
575
576
|
@user_information = [
|
576
577
|
["51", "UL", @max_package_size], # Max PDU Length
|
577
|
-
["52", "STR",
|
578
|
-
["55", "STR",
|
578
|
+
["52", "STR", UID], # Implementation UID
|
579
|
+
["55", "STR", NAME] # Implementation Version (Name & version)
|
579
580
|
]
|
580
581
|
end
|
581
582
|
|