dicom 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ module DICOM
2
+
3
+ # The AuditTrail class handles key/value storage for the Anonymizer.
4
+ # When using the advanced Anonymization options such as enumeration
5
+ # and UID replacement, the AuditTrail class keeps track of key/value
6
+ # pairs and dumps this information to a text file using the json format.
7
+ # This enables us to ensure a unique relationship between the anonymized
8
+ # values and the original values, as well as preserving this relationship
9
+ # for later restoration of original values.
10
+ #
11
+ class AuditTrail
12
+
13
+ # The hash used for storing the key/value pairs of this instace.
14
+ attr_reader :dictionary
15
+
16
+ # Creates a new AuditTrail instance by loading the information stored
17
+ # in the specified file.
18
+ #
19
+ # === Parameters
20
+ #
21
+ # * <tt>file_name</tt> -- The path to a file containing a previously stored audit trail.
22
+ #
23
+ def self.read(file_name)
24
+ audit_trail = AuditTrail.new
25
+ audit_trail.load(file_name)
26
+ return audit_trail
27
+ end
28
+
29
+ # Creates a new AuditTrail instance.
30
+ #
31
+ def initialize
32
+ # The AuditTrail requires JSON for serialization:
33
+ require 'json'
34
+ # Define the key/value hash used for tag records:
35
+ @dictionary = Hash.new
36
+ end
37
+
38
+ # Adds a tag record to the log.
39
+ #
40
+ # === Parameters
41
+ #
42
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
43
+ # * <tt>original</tt> -- The original value (e.q. "John Doe").
44
+ # * <tt>replacement</tt> -- The replacement value (e.q. "Patient1").
45
+ #
46
+ def add_record(tag, original, replacement)
47
+ @dictionary[tag] = Hash.new unless @dictionary.key?(tag)
48
+ @dictionary[tag][original] = replacement
49
+ end
50
+
51
+ # Loads the key/value dictionary hash from a specified file.
52
+ #
53
+ # === Parameters
54
+ #
55
+ # * <tt>file_name</tt> -- The path to a file containing a previously stored audit trail.
56
+ #
57
+ def load(file_name)
58
+ @dictionary = JSON.load(File.new(file_name, "r"))
59
+ end
60
+
61
+ # Retrieves the replacement value used for the given tag and its original value.
62
+ #
63
+ # === Parameters
64
+ #
65
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
66
+ # * <tt>replacement</tt> -- The replacement value (e.q. "Patient1").
67
+ #
68
+ def original(tag, replacement)
69
+ original = nil
70
+ if @dictionary.key?(tag)
71
+ original = @dictionary[tag].key(replacement)
72
+ end
73
+ return original
74
+ end
75
+
76
+ # Returns the key/value pairs for a specific tag.
77
+ #
78
+ # === Parameters
79
+ #
80
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
81
+ #
82
+ def records(tag)
83
+ if @dictionary.key?(tag)
84
+ return @dictionary[tag]
85
+ else
86
+ return Hash.new
87
+ end
88
+ end
89
+
90
+ # Retrieves the replacement value used for the given tag and its original value.
91
+ #
92
+ # === Parameters
93
+ #
94
+ # * <tt>tag</tt> -- The tag string (e.q. "0010,0010").
95
+ # * <tt>original</tt> -- The original value (e.q. "John Doe").
96
+ #
97
+ def replacement(tag, original)
98
+ replacement = nil
99
+ replacement = @dictionary[tag][original] if @dictionary.key?(tag)
100
+ return replacement
101
+ end
102
+
103
+ # Dumps the key/value pairs to a json string which is written to
104
+ # file as specified by the @file_name attribute of this instance.
105
+ #
106
+ #
107
+ # === Parameters
108
+ #
109
+ # * <tt>file_name</tt> -- The file name string to be used for storing & retrieving key/value pairs on disk.
110
+ #
111
+ def write(file_name)
112
+ str = JSON.pretty_generate(@dictionary)
113
+ File.open(file_name, 'w') {|f| f.write(str) }
114
+ end
115
+
116
+ end
117
+ end
@@ -1,7 +1,6 @@
1
-
2
1
  module DICOM
3
2
 
4
- # Ruby DICOM's Implementation UID.
3
+ # Ruby DICOM's registered DICOM UID root (Implementation Class UID).
5
4
  UID = "1.2.826.0.1.3680043.8.641"
6
5
  # Ruby DICOM name & version (max 16 characters).
7
6
  NAME = "RUBY_DCM_" + DICOM::VERSION
@@ -517,12 +517,12 @@ module DICOM
517
517
  # Temporarily increase the log threshold to suppress messages from the DObject class:
518
518
  client_level = logger.level
519
519
  logger.level = Logger::FATAL
520
- obj = DObject.read(file_or_object)
520
+ dcm = DObject.read(file_or_object)
521
521
  # Reset the logg threshold:
522
522
  logger.level = client_level
523
- if obj.read_success
523
+ if dcm.read_success
524
524
  # Load the DICOM object:
525
- objects << obj
525
+ objects << dcm
526
526
  else
527
527
  status = false
528
528
  message = "Failed to read a DObject from this file: #{file_or_object}"
@@ -538,10 +538,10 @@ module DICOM
538
538
  end
539
539
  # Extract available transfer syntaxes for the various sop classes found amongst these objects
540
540
  syntaxes = Hash.new
541
- objects.each do |obj|
542
- sop_class = obj.value("0008,0016")
541
+ objects.each do |dcm|
542
+ sop_class = dcm.value("0008,0016")
543
543
  if sop_class
544
- transfer_syntaxes = available_transfer_syntaxes(obj.transfer_syntax)
544
+ transfer_syntaxes = available_transfer_syntaxes(dcm.transfer_syntax)
545
545
  if syntaxes[sop_class]
546
546
  syntaxes[sop_class] << transfer_syntaxes
547
547
  else
@@ -670,10 +670,10 @@ module DICOM
670
670
  # conveys the information from the selected DICOM file.
671
671
  #
672
672
  def perform_send(objects)
673
- objects.each_with_index do |obj, index|
673
+ objects.each_with_index do |dcm, index|
674
674
  # Gather necessary information from the object (SOP Class & Instance UID):
675
- sop_class = obj.value("0008,0016")
676
- sop_instance = obj.value("0008,0018")
675
+ sop_class = dcm.value("0008,0016")
676
+ sop_instance = dcm.value("0008,0018")
677
677
  if sop_class and sop_instance
678
678
  # Only send the image if its sop_class has been accepted by the receiver:
679
679
  if @approved_syntaxes[sop_class]
@@ -685,11 +685,11 @@ module DICOM
685
685
  selected_transfer_syntax = @approved_syntaxes[sop_class][1]
686
686
  # Encode our DICOM object to a binary string which is split up in pieces, sufficiently small to fit within the specified maximum pdu length:
687
687
  # Set the transfer syntax of the DICOM object equal to the one accepted by the SCP:
688
- obj.transfer_syntax = selected_transfer_syntax
688
+ dcm.transfer_syntax = selected_transfer_syntax
689
689
  # Remove the Meta group, since it doesn't belong in a DICOM file transfer:
690
- obj.remove_group(META_GROUP)
690
+ dcm.delete_group(META_GROUP)
691
691
  max_header_length = 14
692
- data_packages = obj.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
692
+ data_packages = dcm.encode_segments(@max_pdu_length - max_header_length, selected_transfer_syntax)
693
693
  @link.build_command_fragment(PDU_DATA, presentation_context_id, COMMAND_LAST_FRAGMENT, @command_elements)
694
694
  @link.transmit
695
695
  # Transmit all but the last data strings:
@@ -728,7 +728,7 @@ module DICOM
728
728
  presentation_contexts.each do |pc|
729
729
  # Determine what abstract syntax this particular presentation context's id corresponds to:
730
730
  id = pc[:presentation_context_id]
731
- raise "Error! Even presentation context ID received in the association response. This is not allowed according to the DICOM standard!" if id[0] == 0 # If even number.
731
+ raise "Error! Even presentation context ID received in the association response. This is not allowed according to the DICOM standard!" if id.even?
732
732
  abstract_syntax = find_abstract_syntax(id)
733
733
  if pc[:result] == 0
734
734
  accepted_pc += 1
@@ -1,18 +1,18 @@
1
1
  # === TODO:
2
2
  #
3
- # * The retrieve file network functionality (get_image() in DClient class) has not been tested.
3
+ # * The retrieve file network functionality (#get_image in DClient class) has not been tested.
4
4
  # * Make the networking code more intelligent in its handling of unexpected network communication.
5
5
  # * Full support for compressed image data.
6
6
  # * Read/Write 12 bit image data.
7
- # * Full color support (RGB and PALETTE COLOR with get_object_magick() already implemented).
8
- # * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_narray().
7
+ # * Full color support (RGB and PALETTE COLOR with #image already implemented).
8
+ # * Support for extraction of multiple encapsulated pixel data frames in #pixels and #narray.
9
9
  # * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
10
10
  # * More robust and flexible options for reorienting extracted pixel arrays?
11
11
  # * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
12
12
  # * Add these as github issues and remove this list!
13
13
 
14
14
 
15
- # Copyright 2008-2011 Christoffer Lervag
15
+ # Copyright 2008-2012 Christoffer Lervag
16
16
  #
17
17
  # This program is free software: you can redistribute it and/or modify
18
18
  # it under the terms of the GNU General Public License as published by
@@ -77,12 +77,12 @@ module DICOM
77
77
  bin = File.open(file, "rb") { |f| f.read }
78
78
  # Parse the file contents and create the DICOM object:
79
79
  if bin
80
- obj = self.parse(bin)
80
+ dcm = self.parse(bin)
81
81
  else
82
- obj = self.new
83
- obj.read_success = false
82
+ dcm = self.new
83
+ dcm.read_success = false
84
84
  end
85
- return obj
85
+ return dcm
86
86
  end
87
87
 
88
88
  # Creates a DObject instance by parsing an encoded binary DICOM string.
@@ -102,9 +102,9 @@ module DICOM
102
102
  no_header = options[:no_meta]
103
103
  raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
104
104
  raise ArgumentError, "Invalid option :syntax. Expected String, got #{syntax.class}." if syntax && !syntax.is_a?(String)
105
- obj = self.new
106
- obj.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
- return obj
105
+ dcm = self.new
106
+ dcm.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
107
+ return dcm
108
108
  end
109
109
 
110
110
  # Creates a DObject instance by reading and parsing a DICOM file.
@@ -136,12 +136,12 @@ module DICOM
136
136
  end
137
137
  # Parse the file contents and create the DICOM object:
138
138
  if bin
139
- obj = self.parse(bin)
139
+ dcm = self.parse(bin)
140
140
  else
141
- obj = self.new
142
- obj.read_success = false
141
+ dcm = self.new
142
+ dcm.read_success = false
143
143
  end
144
- return obj
144
+ return dcm
145
145
  end
146
146
 
147
147
  # A boolean set as false. This attribute is included to provide consistency with other object types for the internal methods which use it.
@@ -180,12 +180,12 @@ module DICOM
180
180
  #
181
181
  # # Load a DICOM file (Deprecated: please use DObject.read() instead):
182
182
  # require 'dicom'
183
- # obj = DICOM::DObject.new("test.dcm")
183
+ # dcm = DICOM::DObject.new("test.dcm")
184
184
  # # Read a DICOM file that has already been loaded into memory in a binary string (with a known transfer syntax):
185
185
  # # (Deprecated: please use DObject.parse() instead)
186
- # obj = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
186
+ # dcm = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
187
187
  # # Create an empty DICOM object
188
- # obj = DICOM::DObject.new
188
+ # dcm = DICOM::DObject.new
189
189
  # # Increasing the log message threshold (default level is INFO):
190
190
  # DICOM.logger.level = Logger::WARN
191
191
  #
@@ -214,6 +214,16 @@ module DICOM
214
214
  end
215
215
  end
216
216
 
217
+ # Returns true if the argument is an instance with attributes equal to self.
218
+ #
219
+ def ==(other)
220
+ if other.respond_to?(:to_dcm)
221
+ other.send(:state) == state
222
+ end
223
+ end
224
+
225
+ alias_method :eql?, :==
226
+
217
227
  # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
218
228
  #
219
229
  # Returns the encoded binary strings in an array.
@@ -225,7 +235,7 @@ module DICOM
225
235
  #
226
236
  # === Examples
227
237
  #
228
- # encoded_strings = obj.encode_segments(16384)
238
+ # encoded_strings = dcm.encode_segments(16384)
229
239
  #
230
240
  def encode_segments(max_size, transfer_syntax=transfer_syntax)
231
241
  raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
@@ -238,6 +248,12 @@ module DICOM
238
248
  return w.segments
239
249
  end
240
250
 
251
+ # Generates a Fixnum hash value for this instance.
252
+ #
253
+ def hash
254
+ state.hash
255
+ end
256
+
241
257
  # Prints information of interest related to the DICOM object.
242
258
  # Calls the print() method of Parent as well as the information() method of DObject.
243
259
  #
@@ -396,6 +412,12 @@ module DICOM
396
412
  return info
397
413
  end
398
414
 
415
+ # Returns self.
416
+ #
417
+ def to_dcm
418
+ self
419
+ end
420
+
399
421
  # Returns the transfer syntax string of the DObject.
400
422
  #
401
423
  # If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
@@ -447,7 +469,7 @@ module DICOM
447
469
  #
448
470
  # === Examples
449
471
  #
450
- # obj.write(path + "test.dcm")
472
+ # dcm.write(path + "test.dcm")
451
473
  #
452
474
  def write(file_name, options={})
453
475
  raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
@@ -483,8 +505,8 @@ module DICOM
483
505
  end
484
506
  # Source Application Entity Title:
485
507
  Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
486
- # Group Length: Remove the old one (if it exists) before creating a new one.
487
- remove("0002,0000")
508
+ # Group Length: Delete the old one (if it exists) before creating a new one.
509
+ delete("0002,0000")
488
510
  Element.new("0002,0000", meta_group_length, :parent => self)
489
511
  end
490
512
 
@@ -507,5 +529,11 @@ module DICOM
507
529
  return group_length
508
530
  end
509
531
 
532
+ # Returns the attributes (children) of this instance (for comparison purposes).
533
+ #
534
+ def state
535
+ @tags
536
+ end
537
+
510
538
  end
511
539
  end
@@ -19,7 +19,7 @@ module DICOM
19
19
  # An array which records any status messages that are generated while parsing the DICOM string.
20
20
  attr_reader :msg
21
21
  # A DObject instance which the parsed data elements will be connected to.
22
- attr_reader :obj
22
+ attr_reader :dcm
23
23
  # A boolean which records whether the DICOM string contained the proper DICOM header signature of 128 bytes + 'DICM'.
24
24
  attr_reader :signature
25
25
  # A boolean which reports whether the DICOM string was parsed successfully (true) or not (false).
@@ -30,7 +30,7 @@ module DICOM
30
30
  #
31
31
  # === Parameters
32
32
  #
33
- # * <tt>obj</tt> -- A DObject instance which the parsed data elements will be connected to.
33
+ # * <tt>dcm</tt> -- A DObject instance which the parsed data elements will be connected to.
34
34
  # * <tt>string</tt> -- A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
35
35
  # * <tt>options</tt> -- A hash of parameters.
36
36
  #
@@ -40,13 +40,13 @@ module DICOM
40
40
  # * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
41
41
  # * <tt>:syntax</tt> -- String. If specified, the decoding of the DICOM string will be forced to use this transfer syntax.
42
42
  #
43
- def initialize(obj, string=nil, options={})
43
+ def initialize(dcm, string=nil, options={})
44
44
  # Set the DICOM object as an instance variable:
45
- @obj = obj
45
+ @dcm = dcm
46
46
  # If a transfer syntax has been specified as an option for a DICOM object, make sure that it makes it into the object:
47
47
  if options[:syntax]
48
48
  @transfer_syntax = options[:syntax]
49
- obj.add(Element.new("0002,0010", options[:syntax])) if obj.is_a?(DObject)
49
+ dcm.add(Element.new("0002,0010", options[:syntax])) if dcm.is_a?(DObject)
50
50
  end
51
51
  # Initiate the variables that are used during file reading:
52
52
  init_variables
@@ -292,7 +292,7 @@ module DICOM
292
292
  length = @stream.decode(bytes, "SL") # (4)
293
293
  end
294
294
  # Check that length is valid (according to the DICOM standard, it must be even):
295
- raise "Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length%2 == 1 and length > 0
295
+ raise "Encountered a Data Element (#{tag}) with an invalid (odd) value length." if length.odd? and length > 0
296
296
  return vr, length
297
297
  end
298
298
 
@@ -366,7 +366,7 @@ module DICOM
366
366
  def switch_syntax
367
367
  # Get the transfer syntax string, unless it has already been provided by keyword:
368
368
  unless @transfer_syntax
369
- ts_element = @obj["0002,0010"]
369
+ ts_element = @dcm["0002,0010"]
370
370
  if ts_element
371
371
  @transfer_syntax = ts_element.value
372
372
  else
@@ -411,9 +411,9 @@ module DICOM
411
411
  # When the file switch from group 0002 to a later group we will update encoding values, and this switch will keep track of that:
412
412
  @switched = false
413
413
  # Keeping track of the data element parent status while parsing the DICOM string:
414
- @current_parent = @obj
414
+ @current_parent = @dcm
415
415
  # Keeping track of what is the current data element:
416
- @current_element = @obj
416
+ @current_element = @dcm
417
417
  # Items contained under the pixel data element may contain data directly, so we need a variable to keep track of this:
418
418
  @enc_image = false
419
419
  # Assume header size is zero bytes until otherwise is determined:
@@ -33,6 +33,8 @@ module DICOM
33
33
 
34
34
  # A customized FileHandler class to use instead of the default FileHandler included with Ruby DICOM.
35
35
  attr_accessor :file_handler
36
+ # The hostname that the TCPServer binds to.
37
+ attr_accessor :host
36
38
  # The name of the server (application entity).
37
39
  attr_accessor :host_ae
38
40
  # The maximum allowed size of network packages (in bytes).
@@ -61,6 +63,7 @@ module DICOM
61
63
  # === Options
62
64
  #
63
65
  # * <tt>:file_handler</tt> -- A customized FileHandler class to use instead of the default FileHandler.
66
+ # * <tt>:host</tt> -- String. The hostname that the TCPServer binds to. Defaults to '127.0.0.1'.
64
67
  # * <tt>:host_ae</tt> -- String. The name of the server (application entity).
65
68
  # * <tt>:max_package_size</tt> -- Fixnum. The maximum allowed size of network packages (in bytes).
66
69
  # * <tt>:timeout</tt> -- Fixnum. The maximum period the server will wait on an answer from a client before aborting the communication.
@@ -79,6 +82,7 @@ module DICOM
79
82
  @port = port
80
83
  # Optional parameters (and default values):
81
84
  @file_handler = options[:file_handler] || FileHandler
85
+ @host = options[:host] || '127.0.0.1'
82
86
  @host_ae = options[:host_ae] || "RUBY_DICOM"
83
87
  @max_package_size = options[:max_package_size] || 32768 # 16384
84
88
  @timeout = options[:timeout] || 10 # seconds
@@ -144,14 +148,14 @@ module DICOM
144
148
  end
145
149
  end
146
150
 
147
- # Removes a specific abstract syntax from the list of abstract syntaxes that the server will accept.
151
+ # Deletes a specific abstract syntax from the list of abstract syntaxes that the server will accept.
148
152
  #
149
153
  #
150
154
  # === Parameters
151
155
  #
152
156
  # * <tt>uid</tt> -- An abstract syntax UID string.
153
157
  #
154
- def remove_abstract_syntax(uid)
158
+ def delete_abstract_syntax(uid)
155
159
  if uid.is_a?(String)
156
160
  @accepted_abstract_syntaxes.delete(uid)
157
161
  else
@@ -159,13 +163,13 @@ module DICOM
159
163
  end
160
164
  end
161
165
 
162
- # Removes a specific transfer syntax from the list of transfer syntaxes that the server will accept.
166
+ # Deletes a specific transfer syntax from the list of transfer syntaxes that the server will accept.
163
167
  #
164
168
  # === Parameters
165
169
  #
166
170
  # * <tt>uid</tt> -- A transfer syntax UID string.
167
171
  #
168
- def remove_transfer_syntax(uid)
172
+ def delete_transfer_syntax(uid)
169
173
  if uid.is_a?(String)
170
174
  @accepted_transfer_syntaxes.delete(uid)
171
175
  else
@@ -177,9 +181,9 @@ module DICOM
177
181
  #
178
182
  # === Notes
179
183
  #
180
- # * Following such a removal, the user must ensure to add the specific abstract syntaxes that are to be accepted by the server.
184
+ # * Following such a clearance, the user must ensure to add the specific abstract syntaxes that are to be accepted by the server.
181
185
  #
182
- def remove_all_abstract_syntaxes
186
+ def clear_abstract_syntaxes
183
187
  @accepted_abstract_syntaxes = Hash.new
184
188
  end
185
189
 
@@ -187,9 +191,9 @@ module DICOM
187
191
  #
188
192
  # === Notes
189
193
  #
190
- # * Following such a removal, the user must ensure to add the specific transfer syntaxes that are to be accepted by the server.
194
+ # * Following such a clearance, the user must ensure to add the specific transfer syntaxes that are to be accepted by the server.
191
195
  #
192
- def remove_all_transfer_syntaxes
196
+ def clear_transfer_syntaxes
193
197
  @accepted_transfer_syntaxes = Hash.new
194
198
  end
195
199
 
@@ -209,7 +213,7 @@ module DICOM
209
213
  logger.info("Started DICOM SCP server on port #{@port}.")
210
214
  logger.info("Waiting for incoming transmissions...\n\n")
211
215
  # Initiate server:
212
- @scp = TCPServer.new(@port)
216
+ @scp = TCPServer.new(@host, @port)
213
217
  # Use a loop to listen for incoming messages:
214
218
  loop do
215
219
  Thread.start(@scp.accept) do |session|
@@ -279,7 +283,7 @@ module DICOM
279
283
  #
280
284
  # === Notes
281
285
  #
282
- # Other things can potentionally be checked here too, if we want to make the server more strict with regards to what information is received:
286
+ # Other things can potentially be checked here too, if we want to make the server more strict with regards to what information is received:
283
287
  # * Application context name, calling AE title, called AE title
284
288
  # * Description of error codes are given in the DICOM Standard, PS 3.8, Chapter 9.3.4 (Table 9-21).
285
289
  #