dicom 0.6.1 → 0.7
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.
- 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
|
|