dicom 0.6.1 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
- # Copyright 2008-2009 Christoffer Lervag
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 # of method initialize
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 += [path] if path
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
- @exceptions += [path] if path
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 += [tag]
65
- @values += [value]
66
- @enum += [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 # of method add_tag
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 # of method change_enum
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 # of method change_value
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() if @enumeration
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, :lib => @lib)
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
- if @blank
146
- value = ""
147
- elsif @enumeration
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
- else
157
- # Value is simply value in array:
158
- value = @values[j]
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
- all_write = false unless obj.write_success
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 # of @files.each...
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, then this is probably the reason."
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 = @lib.get_name_vr(@tags[i])
212
- names += [arr[0]]
213
- types += [arr[1]]
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 += [tag + f1 + names[i] + f2 + types[i] + f3 + value.to_s + f4 + enum.to_s ]
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 # of method print_tags
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 # of method remove_tag
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 # of method common_path
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 += [current]
315
- previous_new += [value]
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 # of if previous.index...else..
342
+ end
322
343
  else
323
344
  value = @values[j]
324
- end # of if @enum[j]..else..
345
+ end
325
346
  return value
326
- end # of method handle_enumeration
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 += [path] # Store the file in our array
368
+ @files << path # Store the file in our array
348
369
  end
349
370
  end
350
- end # of for dir...
351
- end # of method load_files
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[(@write_path.length-1)..(@write_path.length-1)]
359
- @write_path = @write_path + "/" unless last_character == "/"
360
- # Separate behaviour if we have one, or several files in our array:
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 += [@write_path + str_arr.last]
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 += [@write_path + part_to_write]
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 += [@write_path + file]
403
+ @write_paths << @write_path + file
383
404
  end
384
405
  end
385
- end # of if @files.length..
386
- end # of method process_write_paths
406
+ end
407
+ end
387
408
 
388
409
 
389
- # Default tags that will be anonymized, along with some settings for each:
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 # of method set_defaults
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 # of if @enum[i]
432
- end # of @tags.each...
433
- end # of File.open...
434
- end # of method write...
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, :lib => @lib)
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.receive_single_transmission(@connection).first
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
- # Send confirmation response:
314
- @link.handle_response(@connection)
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", destination], # Move destination
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, vr/type, value]
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", "1.2.826.0.1.3680043.8.641"], # Implementation UID
578
- ["55", "STR", "RUBY_DICOM"] # Implementation Version
578
+ ["52", "STR", UID], # Implementation UID
579
+ ["55", "STR", NAME] # Implementation Version (Name & version)
579
580
  ]
580
581
  end
581
582