dicom 0.9.3 → 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  module DICOM
2
2
 
3
- # The Elemental mix-in module contains methods that are common among the different element classes:
3
+ # The Elemental mix-in module contains methods that
4
+ # are common among the various element type classes:
4
5
  # * Element
5
6
  # * Item
6
7
  # * Sequence
@@ -20,16 +21,18 @@ module DICOM
20
21
  # The elemental's value representation (String).
21
22
  attr_reader :vr
22
23
 
23
- # Returns the method (symbol) corresponding to the name string of this element.
24
+ # Gives the method (symbol) corresponding to the name string of this element.
25
+ #
26
+ # @return [Symbol, NilClass] the matched element method (or nil, if no match is made)
24
27
  #
25
28
  def name_as_method
26
29
  LIBRARY.as_method(@name)
27
30
  end
28
31
 
29
- # Retrieves the entire chain of parents connected to this elemental.
30
- # The parents are returned in an array, where the first entry is the
31
- # immediate parent and the last entry is the top parent.
32
- # Returns an empty array if no parent is defined.
32
+ # Retrieves the entire chain of parents connected to this elemental
33
+ # (or an empty array, if the element is parent-less).
34
+ #
35
+ # @return [Array] array of parents (immediate parent first, top parent last)
33
36
  #
34
37
  def parents
35
38
  all_parents = Array.new
@@ -41,16 +44,12 @@ module DICOM
41
44
  return all_parents
42
45
  end
43
46
 
44
- # Sets a specified parent instance as this elemental's parent, while taking care to delete this elemental from any previous parent
45
- # as well as adding itself to the new parent (unless new parent is nil).
46
- #
47
- # === Parameters
48
- #
49
- # * <tt>new_parent</tt> -- A parent object (which can be either a DObject, Item or Sequence instance), or nil.
47
+ # Sets a specified parent instance as this elemental's parent, while taking
48
+ # care to delete this elemental from any previous parent, as well as adding
49
+ # itself to the new parent (unless new parent is nil).
50
50
  #
51
- # === Examples
52
- #
53
- # # Create a new Sequence and connect it to a DObject instance:
51
+ # @param [DObject, Item, Sequence, NilClass] new_parent the new parent object for this elemental
52
+ # @example Create a new Sequence and connect it to a DObject instance
54
53
  # structure_set_roi = Sequence.new("3006,0020")
55
54
  # structure_set_roi.parent = dcm
56
55
  #
@@ -76,12 +75,10 @@ module DICOM
76
75
  @parent = new_parent
77
76
  end
78
77
 
79
- # Sets a specified parent instance as this elemental's parent, without doing any other updates, like removing the elemental
80
- # from any previous parent or adding itself to the new parent.
81
- #
82
- # === Parameters
78
+ # Sets a specified parent instance as this elemental's parent, without doing any other updates,
79
+ # like removing the elemental from any previous parent or adding itself to the new parent.
83
80
  #
84
- # * <tt>new_parent</tt> -- A parent object (which can be either a DObject, Item or Sequence instance), or nil.
81
+ # @param [DObject, Item, Sequence, NilClass] new_parent the new parent object for this elemental
85
82
  #
86
83
  def set_parent(new_parent)
87
84
  # Set the new parent (should we bother to test for parent validity here?):
@@ -90,10 +87,8 @@ module DICOM
90
87
 
91
88
  # Returns a Stream instance which can be used for encoding a value to binary.
92
89
  #
93
- # === Notes
94
- #
95
- # * Retrieves the Stream instance of the top parent DObject instance.
96
- # If this fails, a new Stream instance is created (with Little Endian encoding assumed).
90
+ # @note Retrieves the Stream instance of the top parent DObject instance.
91
+ # If this fails, a new Stream instance is created (with little endian encoding assumed).
97
92
  #
98
93
  def stream
99
94
  if top_parent.is_a?(DObject)
@@ -105,9 +100,8 @@ module DICOM
105
100
 
106
101
  # Returns the top parent of a particular elemental.
107
102
  #
108
- # === Notes
109
- #
110
- # Unless the elemental, or one of its parent instances, are independent, the top parent will be a DObject instance.
103
+ # @note Unless the elemental, or one of its parent instances, are independent,
104
+ # the top parent will be a DObject instance.
111
105
  #
112
106
  def top_parent
113
107
  # The top parent is determined recursively:
@@ -1,122 +1,122 @@
1
- module DICOM
2
-
3
- # This class handles DICOM files that have been received through network communication.
4
- #
5
- # === Notes
6
- #
7
- # The purpose of this class is to make it as easy as possible for users to customize the way
8
- # DICOM files are handled when they are received through the network.
9
- #
10
- # The default behaviour is to save the files to disk using a folder structure determined by a few select tags of the DICOM file.
11
- #
12
- # Some suggested alternatives for user customization:
13
- # * Analyzing tags and/or image data to determine further actions.
14
- # * Modify the DICOM object before it is saved to disk.
15
- # * Modify the folder structure in which DICOM files are saved to disk.
16
- # * Store DICOM contents in a database (highly relevant if you are building a Ruby on Rails DICOM application).
17
- # * Retransmit the DICOM object to another network destination using the DClient class.
18
- # * Write information to a log file.
19
- #
20
- class FileHandler
21
-
22
- # Saves a single DICOM object to file.
23
- # Returns a status message stating where the file has been saved.
24
- #
25
- # Modify this method if you want to change the way your server saves incoming files.
26
- #
27
- # === Notes
28
- #
29
- # As default, files will be saved with the following path:
30
- # <tt> path_prefix/<PatientID>/<StudyDate>/<Modality>/ </tt>
31
- #
32
- # === Parameters
33
- #
34
- # * <tt>path_prefix</tt> -- String. Specifies the root path of the DICOM storage.
35
- # * <tt>dcm</tt> -- A DObject instance which will be written to file.
36
- # * <tt>transfer_syntax</tt> -- String. Specifies the transfer syntax that will be used to write the DICOM file.
37
- #
38
- def self.save_file(path_prefix, dcm, transfer_syntax)
39
- # File name is set using the SOP Instance UID:
40
- file_name = dcm.value("0008,0018") || "missing_SOP_UID"
41
- extension = ".dcm"
42
- folders = Array.new(3)
43
- folders[0] = dcm.value("0010,0020") || "PatientID"
44
- folders[1] = dcm.value("0008,0020") || "StudyDate"
45
- folders[2] = dcm.value("0008,0060") || "Modality"
46
- local_path = folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
47
- full_path = path_prefix + local_path + extension
48
- # Save the DICOM object to disk:
49
- dcm.write(full_path, :transfer_syntax => transfer_syntax)
50
- message = [:info, "DICOM file saved to: #{full_path}"]
51
- return message
52
- end
53
-
54
- # Handles the reception of a series of DICOM objects which are received in a single association.
55
- #
56
- # Modify this method if you want to change the way your server handles incoming file series.
57
- #
58
- # === Notes
59
- #
60
- # Default action: Pass each file to the class method which saves files to disk.
61
- #
62
- # === Parameters
63
- #
64
- # * <tt>path</tt> -- String. Specifies the root path of the DICOM storage.
65
- # * <tt>objects</tt> -- An array containing the DObject instances which were received.
66
- # * <tt>transfer_syntaxes</tt> -- An array containing the transfer syntaxes belonging to the received objects.
67
- #
68
- def self.receive_files(path, objects, transfer_syntaxes)
69
- all_success = true
70
- successful, too_short, parse_fail, handle_fail = 0, 0, 0, 0
71
- total = objects.length
72
- message = nil
73
- messages = Array.new
74
- # Process each DICOM object:
75
- objects.each_index do |i|
76
- if objects[i].length > 8
77
- # Temporarily increase the log threshold to suppress messages from the DObject class:
78
- server_level = DICOM.logger.level
79
- DICOM.logger.level = Logger::FATAL
80
- # Parse the received data string and load it to a DICOM object:
81
- dcm = DObject.parse(objects[i], :no_meta => true, :syntax => transfer_syntaxes[i])
82
- # Reset the logg threshold:
83
- DICOM.logger.level = server_level
84
- if dcm.read?
85
- begin
86
- message = self.save_file(path, dcm, transfer_syntaxes[i])
87
- successful += 1
88
- rescue
89
- handle_fail += 1
90
- all_success = false
91
- messages << [:error, "Saving file failed!"]
92
- end
93
- else
94
- parse_fail += 1
95
- all_success = false
96
- messages << [:error, "Invalid DICOM data encountered: The received string was not parsed successfully."]
97
- end
98
- else
99
- too_short += 1
100
- all_success = false
101
- messages << [:error, "Invalid data encountered: The received string was too small to contain any DICOM data."]
102
- end
103
- end
104
- # Create a summary status message, when multiple files have been received:
105
- if total > 1
106
- if successful == total
107
- messages << [:info, "All #{total} DICOM files received successfully."]
108
- else
109
- if successful == 0
110
- messages << [:warn, "All #{total} received DICOM files failed!"]
111
- else
112
- messages << [:warn, "Only #{successful} of #{total} DICOM files received successfully!"]
113
- end
114
- end
115
- else
116
- messages = [message] if all_success
117
- end
118
- return all_success, messages
119
- end
120
-
121
- end
1
+ module DICOM
2
+
3
+ # This class handles DICOM files that have been received through network communication.
4
+ #
5
+ # === Notes
6
+ #
7
+ # The purpose of this class is to make it as easy as possible for users to customize the way
8
+ # DICOM files are handled when they are received through the network.
9
+ #
10
+ # The default behaviour is to save the files to disk using a folder structure determined by a few select tags of the DICOM file.
11
+ #
12
+ # Some suggested alternatives for user customization:
13
+ # * Analyzing tags and/or image data to determine further actions.
14
+ # * Modify the DICOM object before it is saved to disk.
15
+ # * Modify the folder structure in which DICOM files are saved to disk.
16
+ # * Store DICOM contents in a database (highly relevant if you are building a Ruby on Rails DICOM application).
17
+ # * Retransmit the DICOM object to another network destination using the DClient class.
18
+ # * Write information to a log file.
19
+ #
20
+ class FileHandler
21
+
22
+ # Saves a single DICOM object to file.
23
+ # Returns a status message stating where the file has been saved.
24
+ #
25
+ # Modify this method if you want to change the way your server saves incoming files.
26
+ #
27
+ # === Notes
28
+ #
29
+ # As default, files will be saved with the following path:
30
+ # <tt> path_prefix/<PatientID>/<StudyDate>/<Modality>/ </tt>
31
+ #
32
+ # === Parameters
33
+ #
34
+ # * <tt>path_prefix</tt> -- String. Specifies the root path of the DICOM storage.
35
+ # * <tt>dcm</tt> -- A DObject instance which will be written to file.
36
+ # * <tt>transfer_syntax</tt> -- String. Specifies the transfer syntax that will be used to write the DICOM file.
37
+ #
38
+ def self.save_file(path_prefix, dcm, transfer_syntax)
39
+ # File name is set using the SOP Instance UID:
40
+ file_name = dcm.value("0008,0018") || "missing_SOP_UID"
41
+ extension = ".dcm"
42
+ folders = Array.new(3)
43
+ folders[0] = dcm.value("0010,0020") || "PatientID"
44
+ folders[1] = dcm.value("0008,0020") || "StudyDate"
45
+ folders[2] = dcm.value("0008,0060") || "Modality"
46
+ local_path = folders.join(File::SEPARATOR) + File::SEPARATOR + file_name
47
+ full_path = path_prefix + local_path + extension
48
+ # Save the DICOM object to disk:
49
+ dcm.write(full_path, :transfer_syntax => transfer_syntax)
50
+ message = [:info, "DICOM file saved to: #{full_path}"]
51
+ return message
52
+ end
53
+
54
+ # Handles the reception of a series of DICOM objects which are received in a single association.
55
+ #
56
+ # Modify this method if you want to change the way your server handles incoming file series.
57
+ #
58
+ # === Notes
59
+ #
60
+ # Default action: Pass each file to the class method which saves files to disk.
61
+ #
62
+ # === Parameters
63
+ #
64
+ # * <tt>path</tt> -- String. Specifies the root path of the DICOM storage.
65
+ # * <tt>objects</tt> -- An array containing the DObject instances which were received.
66
+ # * <tt>transfer_syntaxes</tt> -- An array containing the transfer syntaxes belonging to the received objects.
67
+ #
68
+ def self.receive_files(path, objects, transfer_syntaxes)
69
+ all_success = true
70
+ successful, too_short, parse_fail, handle_fail = 0, 0, 0, 0
71
+ total = objects.length
72
+ message = nil
73
+ messages = Array.new
74
+ # Process each DICOM object:
75
+ objects.each_index do |i|
76
+ if objects[i].length > 8
77
+ # Temporarily increase the log threshold to suppress messages from the DObject class:
78
+ server_level = DICOM.logger.level
79
+ DICOM.logger.level = Logger::FATAL
80
+ # Parse the received data string and load it to a DICOM object:
81
+ dcm = DObject.parse(objects[i], :no_meta => true, :syntax => transfer_syntaxes[i])
82
+ # Reset the logg threshold:
83
+ DICOM.logger.level = server_level
84
+ if dcm.read?
85
+ begin
86
+ message = self.save_file(path, dcm, transfer_syntaxes[i])
87
+ successful += 1
88
+ rescue
89
+ handle_fail += 1
90
+ all_success = false
91
+ messages << [:error, "Saving file failed!"]
92
+ end
93
+ else
94
+ parse_fail += 1
95
+ all_success = false
96
+ messages << [:error, "Invalid DICOM data encountered: The received string was not parsed successfully."]
97
+ end
98
+ else
99
+ too_short += 1
100
+ all_success = false
101
+ messages << [:error, "Invalid data encountered: The received string was too small to contain any DICOM data."]
102
+ end
103
+ end
104
+ # Create a summary status message, when multiple files have been received:
105
+ if total > 1
106
+ if successful == total
107
+ messages << [:info, "All #{total} DICOM files received successfully."]
108
+ else
109
+ if successful == 0
110
+ messages << [:warn, "All #{total} received DICOM files failed!"]
111
+ else
112
+ messages << [:warn, "Only #{successful} of #{total} DICOM files received successfully!"]
113
+ end
114
+ end
115
+ else
116
+ messages = [message] if all_success
117
+ end
118
+ return all_success, messages
119
+ end
120
+
121
+ end
122
122
  end
@@ -1,861 +1,819 @@
1
- module DICOM
2
-
3
- # Super class which contains common code for both the DObject and Item classes.
4
- # This class includes the image related methods, since images may be stored either directly in the DObject,
5
- # or in items (encapsulated items in the "Pixel Data" element or in "Icon Image Sequence" items).
6
- #
7
- # === Inheritance
8
- #
9
- # As the ImageItem class inherits from the Parent class, all Parent methods are also available to objects which has inherited ImageItem.
10
- #
11
- class ImageItem < Parent
12
-
13
- include ImageProcessor
14
- include Logging
15
-
16
- # Checks if colored pixel data is present.
17
- # Returns true if it is, false if not.
18
- #
19
- def color?
20
- # "Photometric Interpretation" is contained in the data element "0028,0004":
21
- begin
22
- photometric = photometry
23
- if photometric.include?("COLOR") or photometric.include?("RGB") or photometric.include?("YBR")
24
- return true
25
- else
26
- return false
27
- end
28
- rescue
29
- return false
30
- end
31
- end
32
-
33
- # Checks if compressed pixel data is present.
34
- # Returns true if it is, false if not.
35
- #
36
- def compression?
37
- # If compression is used, the pixel data element is a Sequence (with encapsulated elements), instead of a Element:
38
- if self[PIXEL_TAG].is_a?(Sequence)
39
- return true
40
- else
41
- return false
42
- end
43
- end
44
-
45
- # Unpacks a binary pixel string and returns decoded pixel values in an array.
46
- # The decode is performed using values defined in the image related data elements of the DObject instance.
47
- #
48
- # === Parameters
49
- #
50
- # * <tt>bin</tt> -- A binary String containing the pixels that will be decoded.
51
- # * <tt>stream</tt> -- A Stream instance to be used for decoding the pixels (optional).
52
- #
53
- def decode_pixels(bin, stream=@stream)
54
- raise ArgumentError, "Expected String, got #{bin.class}." unless bin.is_a?(String)
55
- pixels = false
56
- # We need to know what kind of bith depth and integer type the pixel data is saved with:
57
- bit_depth_element = self["0028,0100"]
58
- pixel_representation_element = self["0028,0103"]
59
- if bit_depth_element and pixel_representation_element
60
- # Load the binary pixel data to the Stream instance:
61
- stream.set_string(bin)
62
- template = template_string(bit_depth_element.value.to_i)
63
- pixels = stream.decode_all(template) if template
64
- else
65
- raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to decode pixel data." unless bit_depth_element
66
- raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to decode pixel data." unless pixel_representation_element
67
- end
68
- return pixels
69
- end
70
-
71
- # Packs a pixel value array and returns an encoded binary string.
72
- # The encoding is performed using values defined in the image related data elements of the DObject instance.
73
- #
74
- # === Parameters
75
- #
76
- # * <tt>pixels</tt> -- An array containing the pixel values that will be encoded.
77
- # * <tt>stream</tt> -- A Stream instance to be used for encoding the pixels (optional).
78
- #
79
- def encode_pixels(pixels, stream=@stream)
80
- raise ArgumentError, "Expected Array, got #{pixels.class}." unless pixels.is_a?(Array)
81
- bin = false
82
- # We need to know what kind of bith depth and integer type the pixel data is saved with:
83
- bit_depth_element = self["0028,0100"]
84
- pixel_representation_element = self["0028,0103"]
85
- if bit_depth_element and pixel_representation_element
86
- template = template_string(bit_depth_element.value.to_i)
87
- bin = stream.encode(pixels, template) if template
88
- else
89
- raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to encode the pixel data." unless bit_depth_element
90
- raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to encode the pixel data." unless pixel_representation_element
91
- end
92
- return bin
93
- end
94
-
95
- # Returns a single image object, created from the encoded pixel data using the image related data elements in the DICOM object.
96
- # If the object contains multiple image frames, the first image frame is returned, unless the :frame option is used.
97
- # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
98
- #
99
- # === Notes
100
- #
101
- # * Returns an image object in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.
102
- # * When calling this method the corresponding image processor gem must have been loaded in advance (example: require 'RMagick').
103
- #
104
- # === Parameters
105
- #
106
- # * <tt>options</tt> -- A hash of parameters.
107
- #
108
- # === Options
109
- #
110
- # * <tt>:frame</tt> -- Fixnum. For DICOM objects containing multiple frames, this option can be used to extract a specific image frame. Defaults to 0.
111
- # * <tt>:level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
112
- # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray for the pixel remap process (for faster execution).
113
- # * <tt>:remap</tt> -- Boolean. If set as true, the returned pixel values are remapped to presentation values.
114
- #
115
- # === Examples
116
- #
117
- # # Retrieve pixel data as an RMagick image object and display it:
118
- # image = obj.image
119
- # image.display
120
- # # Retrieve frame number 5 in the pixel data:
121
- # image = obj.image(:frame => 5)
122
- #
123
- def image(options={})
124
- options[:frame] = options[:frame] || 0
125
- image = images(options).first
126
- image = false if image.nil? && exists?(PIXEL_TAG)
127
- return image
128
- end
129
-
130
- # Returns an array of image objects, created from the encoded pixel data using the image related data elements in the DICOM object.
131
- # Returns an empty array if no data is present, or if it fails to retrieve pixel data which is present.
132
- #
133
- # === Notes
134
- #
135
- # * Returns an array of image objects in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.
136
- # * When calling this method the corresponding image processor gem must have been loaded in advance (example: require 'RMagick').
137
- #
138
- # === Parameters
139
- #
140
- # * <tt>options</tt> -- A hash of parameters.
141
- #
142
- # === Options
143
- #
144
- # * <tt>:frame</tt> -- Fixnum. Makes the method return an array containing only the image object corresponding to the specified frame number.
145
- # * <tt>:level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
146
- # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray for the pixel remap process (for faster execution).
147
- # * <tt>:remap</tt> -- Boolean. If set as true, the returned pixel values are remapped to presentation values.
148
- #
149
- # === Examples
150
- #
151
- # # Retrieve the pixel data as RMagick image objects:
152
- # images = obj.images
153
- # # Retrieve the pixel data as RMagick image objects, remapped to presentation values (but without any leveling):
154
- # images = obj.images(:remap => true)
155
- # # Retrieve the pixel data as RMagick image objects, remapped to presentation values and leveled using the default center/width values in the DICOM object:
156
- # images = obj.images(:level => true)
157
- # # Retrieve the pixel data as RMagick image objects, remapped to presentation values, leveled with the specified center/width values and using numerical array for the rescaling (~twice as fast).
158
- # images = obj.images(:level => [-200,1000], :narray => true)
159
- #
160
- def images(options={})
161
- images = Array.new
162
- if exists?(PIXEL_TAG)
163
- # Gather the pixel data strings, and pick a single frame if indicated by options:
164
- strings = image_strings(split_to_frames=true)
165
- strings = [strings[options[:frame]]] if options[:frame]
166
- if compression?
167
- # Decompress, either to numbers (RLE) or to an image object (image based compressions):
168
- if [TXS_RLE].include?(transfer_syntax)
169
- pixel_frames = Array.new
170
- strings.each {|string| pixel_frames << decode_rle(num_cols, num_rows, string)}
171
- else
172
- images = decompress(strings) || Array.new
173
- logger.warn("Decompressing pixel values has failed (unsupported transfer syntax: '#{transfer_syntax}' - #{LIBRARY.get_syntax_description(transfer_syntax)})") unless images.length > 0
174
- end
175
- else
176
- # Uncompressed: Decode to numbers.
177
- pixel_frames = Array.new
178
- strings.each {|string| pixel_frames << decode_pixels(string)}
179
- end
180
- if pixel_frames
181
- images = Array.new
182
- pixel_frames.each do |pixels|
183
- # Pixel values and pixel order may need to be rearranged if we have color data:
184
- pixels = process_colors(pixels) if color?
185
- if pixels
186
- images << read_image(pixels, num_cols, num_rows, options)
187
- else
188
- logger.warn("Processing pixel values for this particular color mode failed, unable to construct image(s).")
189
- end
190
- end
191
- end
192
- end
193
- return images
194
- end
195
-
196
- # Reads a binary string from a specified file and writes it to the value field of the pixel data element (7FE0,0010).
197
- #
198
- # === Parameters
199
- #
200
- # * <tt>file</tt> -- A string which specifies the path of the file containing pixel data.
201
- #
202
- # === Examples
203
- #
204
- # obj.image_from_file("custom_image.dat")
205
- #
206
- def image_from_file(file)
207
- raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
208
- f = File.new(file, "rb")
209
- bin = f.read(f.stat.size)
210
- if bin.length > 0
211
- # Write the binary data to the Pixel Data Element:
212
- write_pixels(bin)
213
- else
214
- logger.info("The specified file (#{file}) is empty. Nothing to transfer.")
215
- end
216
- end
217
-
218
- # Returns the pixel data binary string(s) in an array.
219
- # If no pixel data is present, returns an empty array.
220
- #
221
- # === Parameters
222
- #
223
- # * <tt>split</tt> -- Boolean. If true, a pixel data string containing 3d volumetric data will be split into N substrings, where N equals the number of frames.
224
- #
225
- def image_strings(split=false)
226
- # Pixel data may be a single binary string in the pixel data element,
227
- # or located in several encapsulated item elements:
228
- pixel_element = self[PIXEL_TAG]
229
- strings = Array.new
230
- if pixel_element.is_a?(Element)
231
- if split
232
- strings = pixel_element.bin.dup.divide(num_frames)
233
- else
234
- strings << pixel_element.bin
235
- end
236
- elsif pixel_element.is_a?(Sequence)
237
- pixel_items = pixel_element.children.first.children
238
- pixel_items.each {|item| strings << item.bin}
239
- end
240
- return strings
241
- end
242
-
243
- # Dumps the binary content of the Pixel Data element to the specified file.
244
- #
245
- # === Notes
246
- #
247
- # * If the DICOM object contains multi-fragment pixel data, each fragment will be dumped to separate files (e.q. 'fragment-0.dat', 'fragment-1.dat').
248
- #
249
- # === Parameters
250
- #
251
- # * <tt>file</tt> -- A string which specifies the file path to use when dumping the pixel data.
252
- #
253
- # === Examples
254
- #
255
- # obj.image_to_file("exported_image.dat")
256
- #
257
- def image_to_file(file)
258
- raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
259
- # Split the file name in case of multiple fragments:
260
- parts = file.split('.')
261
- if parts.length > 1
262
- base = parts[0..-2].join
263
- extension = "." + parts.last
264
- else
265
- base = file
266
- extension = ""
267
- end
268
- # Get the binary image strings and dump them to the file(s):
269
- images = image_strings
270
- images.each_index do |i|
271
- if images.length == 1
272
- f = File.new(file, "wb")
273
- else
274
- f = File.new("#{base}-#{i}#{extension}", "wb")
275
- end
276
- f.write(images[i])
277
- f.close
278
- end
279
- end
280
-
281
- # Encodes pixel data from a (Magick) image object and writes it to the pixel data element (7FE0,0010).
282
- #
283
- # === Restrictions
284
- #
285
- # Because of pixel value issues related to image objects (images dont like signed integers), and the possible
286
- # difference between presentation values and raw pixel values, the use of image=() may
287
- # result in pixel data where the integer values differs somewhat from what is expected. Use with care!
288
- # For precise pixel value processing, use the Array and NArray based pixel data methods instead.
289
- #
290
- def image=(image)
291
- raise ArgumentError, "Expected one of the supported image objects (#{valid_image_objects}), got #{image.class}." unless valid_image_objects.include?(image.class)
292
- # Export to pixels using the proper image processor:
293
- pixels = export_pixels(image, photometry)
294
- # Encode and write to the Pixel Data Element:
295
- self.pixels = pixels
296
- end
297
-
298
- # Returns the number of columns in the pixel data (as an Integer).
299
- # Returns nil if the value is not defined.
300
- #
301
- def num_cols
302
- self["0028,0011"].value rescue nil
303
- end
304
-
305
- # Returns the number of frames in the pixel data (as an Integer).
306
- # Assumes and returns 1 if the value is not defined.
307
- #
308
- def num_frames
309
- (self["0028,0008"].is_a?(Element) == true ? self["0028,0008"].value.to_i : 1)
310
- end
311
-
312
- # Returns the number of rows in the pixel data (as an Integer).
313
- # Returns nil if the value is not defined.
314
- #
315
- def num_rows
316
- self["0028,0010"].value rescue nil
317
- end
318
-
319
- # Returns an NArray containing the pixel data. If the pixel data is an image (single frame), a 2-dimensional
320
- # NArray is returned [columns, rows]. If a the pixel data is 3-dimensional (more than one frame),
321
- # a 3-dimensional NArray is returned [frames, columns, rows].
322
- # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
323
- #
324
- # === Notes
325
- #
326
- # * To call this method you need to have loaded the NArray library in advance (require 'narray').
327
- #
328
- # === Parameters
329
- #
330
- # * <tt>options</tt> -- A hash of parameters.
331
- #
332
- # === Options
333
- #
334
- # * <tt>:level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
335
- # * <tt>:remap</tt> -- Boolean. If set as true, the returned pixel values are remapped to presentation values.
336
- # * <tt>:volume</tt> -- Boolean. If set as true, the returned array will always be 3-dimensional, even if the pixel data only has one frame.
337
- #
338
- # === Examples
339
- #
340
- # # Retrieve numerical pixel array:
341
- # data = obj.narray
342
- # # Retrieve numerical pixel array remapped from the original pixel values to presentation values:
343
- # data = obj.narray(:remap => true)
344
- #
345
- def narray(options={})
346
- pixels = nil
347
- if exists?(PIXEL_TAG)
348
- unless color?
349
- # Decode the pixel values: For now we only support returning pixel data of the first frame (if the image is located in multiple pixel data items).
350
- if compression?
351
- pixels = decompress(image_strings.first)
352
- else
353
- pixels = decode_pixels(image_strings.first)
354
- end
355
- if pixels
356
- # Import the pixels to NArray and give it a proper shape:
357
- raise "Missing Rows and/or Columns Element. Unable to construct pixel data array." unless num_rows and num_cols
358
- if num_frames > 1 or options[:volume]
359
- # Create an empty 3D NArray. fill it with pixels frame by frame, then reassign the pixels variable to it:
360
- narr = NArray.int(num_frames, num_cols, num_rows)
361
- num_frames.times do |i|
362
- narr[i, true, true] = NArray.to_na(pixels[(i * num_cols * num_rows)..((i + 1) * num_cols * num_rows - 1)]).reshape!(num_cols, num_rows)
363
- end
364
- pixels = narr
365
- else
366
- pixels = NArray.to_na(pixels).reshape!(num_cols, num_rows)
367
- end
368
- # Remap the image from pixel values to presentation values if the user has requested this:
369
- pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]) if options[:remap] or options[:level]
370
- else
371
- logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
372
- end
373
- else
374
- logger.warn("The DICOM object contains colored pixel data. Retrieval of colored pixels is not supported by this method yet.")
375
- pixels = false
376
- end
377
- end
378
- return pixels
379
- end
380
-
381
- # Returns the Pixel Data values in a standard Ruby Array.
382
- # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
383
- #
384
- # === Notes
385
- #
386
- # * The returned array does not carry the dimensions of the pixel data: It is put in a one dimensional Array (vector).
387
- #
388
- # === Parameters
389
- #
390
- # * <tt>options</tt> -- A hash of parameters.
391
- #
392
- # === Options
393
- #
394
- # * <tt>:level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
395
- # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray for the pixel remap process (for faster execution).
396
- # * <tt>:remap</tt> -- Boolean. If set as true, the returned pixel values are remapped to presentation values.
397
- #
398
- # === Examples
399
- #
400
- # # Simply retrieve the pixel data:
401
- # pixels = obj.pixels
402
- # # Retrieve the pixel data remapped to presentation values according to window center/width settings:
403
- # pixels = obj.pixels(:remap => true)
404
- # # Retrieve the remapped pixel data while using numerical array (~twice as fast):
405
- # pixels = obj.pixels(:remap => true, :narray => true)
406
- #
407
- def pixels(options={})
408
- pixels = nil
409
- if exists?(PIXEL_TAG)
410
- # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
411
- if compression?
412
- pixels = decompress(image_strings.first)
413
- else
414
- pixels = decode_pixels(image_strings.first)
415
- end
416
- if pixels
417
- # Remap the image from pixel values to presentation values if the user has requested this:
418
- if options[:remap] or options[:level]
419
- if options[:narray]
420
- # Use numerical array (faster):
421
- pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]).to_a
422
- else
423
- # Use standard Ruby array (slower):
424
- pixels = process_presentation_values(pixels, -65535, 65535, options[:level])
425
- end
426
- end
427
- else
428
- logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
429
- end
430
- end
431
- return pixels
432
- end
433
-
434
- # Encodes pixel data from a Ruby Array or NArray, and writes it to the pixel data element (7FE0,0010).
435
- #
436
- # === Parameters
437
- #
438
- # * <tt>values</tt> -- An Array (or NArray) containing integer pixel values.
439
- #
440
- def pixels=(values)
441
- raise ArgumentError, "Expected Array or NArray, got #{values.class}." unless [Array, NArray].include?(values.class)
442
- if values.is_a?(NArray)
443
- # When NArray, convert to a Ruby Array:
444
- if values.shape.length > 2
445
- # We need to take care when reshaping this 3D array so that the pixel values falls properly into place:
446
- narr = NArray.int(values.shape[1] * values.shape[2], values.shape[0])
447
- values.shape[0].times do |i|
448
- narr[true, i] = values[i, true, true].reshape(values.shape[1] * values.shape[2])
449
- end
450
- values = narr.to_a
451
- else
452
- values = values.to_a
453
- end
454
- end
455
- # Encode the pixel data:
456
- bin = encode_pixels(values.flatten)
457
- # Write the binary data to the Pixel Data Element:
458
- write_pixels(bin)
459
- end
460
-
461
- # Delete all Sequence instances from the DObject or Item instance.
462
- #
463
- def delete_sequences
464
- @tags.each_value do |element|
465
- delete(element.tag) if element.is_a?(Sequence)
466
- end
467
- end
468
-
469
-
470
- # Following methods are private:
471
- private
472
-
473
- # Returns the effective bit depth of the pixel data (considers a special case for Palette colored images).
474
- #
475
- def actual_bit_depth
476
- raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
477
- if photometry == PI_PALETTE_COLOR
478
- # Only one channel is checked and it is assumed that all channels have the same number of bits.
479
- return self["0028,1101"].value.split("\\").last.to_i
480
- else
481
- return bit_depth
482
- end
483
- end
484
-
485
-
486
- # Returns the value from the "Bits Allocated" Element.
487
- #
488
- def bit_depth
489
- raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
490
- return value("0028,0100")
491
- end
492
-
493
- # Performes a run length decoding on the input stream.
494
- #
495
- # === Notes
496
- #
497
- # * For details on RLE encoding, refer to the DICOM standard, PS3.5, Section 8.2.2 as well as Annex G.
498
- #
499
- # === Parameters
500
- #
501
- # * <tt>cols</tt> - number of colums of the encoded image
502
- # * <tt>rows</tt> - number of rows of the encoded image
503
- # * <tt>string</tt> - packed data
504
- #
505
- #--
506
- # TODO: Remove cols and rows, were only added for debugging.
507
- #
508
- def decode_rle(cols, rows, string)
509
- pixels = Array.new
510
- # RLE header specifying the number of segments:
511
- header = string[0...64].unpack('L*')
512
- image_segments = Array.new
513
- # Extracting all start and endpoints of the different segments:
514
- header.each_index do |n|
515
- if n == 0
516
- # This one need no processing.
517
- elsif n == header[0]
518
- # It's the last one
519
- image_segments << [header[n], -1]
520
- break
521
- else
522
- image_segments << [header[n], header[n + 1] - 1]
523
- end
524
- end
525
- # Iterate over each segment and extract pixel data:
526
- image_segments.each do |range|
527
- segment_data = Array.new
528
- next_bytes = -1
529
- next_multiplier = 0
530
- # Iterate this segment's pixel string:
531
- string[range[0]..range[1]].each_byte do |b|
532
- if next_multiplier > 0
533
- next_multiplier.times { segment_data << b }
534
- next_multiplier = 0
535
- elsif next_bytes > 0
536
- segment_data << b
537
- next_bytes -= 1
538
- elsif b <= 127
539
- next_bytes = b + 1
540
- else
541
- # Explaining the 257 at this point is a little bit complicate. Basically it has something
542
- # to do with the algorithm described in the DICOM standard and that the value -1 as uint8 is 255.
543
- # TODO: Is this architectur safe or does it only work on Intel systems???
544
- next_multiplier = 257 - b
545
- end
546
- end
547
- # Verify that the RLE decoding has executed properly:
548
- throw "Size mismatch #{segment_data.size} != #{rows * cols}" if segment_data.size != rows * cols
549
- pixels += segment_data
550
- end
551
- return pixels
552
- end
553
-
554
- # Returns the value from the "Photometric Interpretation" Element.
555
- # Raises an error if it is missing.
556
- #
557
- def photometry
558
- raise "The 'Photometric Interpretation' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0004")
559
- return value("0028,0004").upcase
560
- end
561
-
562
- # Processes the pixel array based on attributes defined in the DICOM object to produce a pixel array
563
- # with correct pixel colors (RGB) as well as pixel order (RGB-pixel1, RGB-pixel2, etc).
564
- # The relevant DICOM tags are Photometric Interpretation (0028,0004) and Planar Configuration (0028,0006).
565
- #
566
- # === Parameters
567
- #
568
- # * <tt>pixels</tt> -- An array of pixel values (integers).
569
- #
570
- def process_colors(pixels)
571
- proper_rgb = false
572
- photometric = photometry()
573
- # (With RLE COLOR PALETTE the Planar Configuration is not set)
574
- planar = self["0028,0006"].is_a?(Element) ? self["0028,0006"].value : 0
575
- # Step 1: Produce an array with RGB values. At this time, YBR is not supported in ruby-dicom,
576
- # so this leaves us with a possible conversion from PALETTE COLOR:
577
- if photometric.include?("COLOR")
578
- # Pseudo colors (rgb values grabbed from a lookup table):
579
- rgb = Array.new(pixels.length*3)
580
- # Prepare the lookup data arrays:
581
- lookup_binaries = [self["0028,1201"].bin, self["0028,1202"].bin, self["0028,1203"].bin]
582
- lookup_values = Array.new
583
- nr_bits = self["0028,1101"].value.split("\\").last.to_i
584
- template = template_string(nr_bits)
585
- lookup_binaries.each do |bin|
586
- stream.set_string(bin)
587
- lookup_values << stream.decode_all(template)
588
- end
589
- lookup_values = lookup_values.transpose
590
- # Fill the RGB array, one RGB pixel group (3 pixels) at a time:
591
- pixels.each_index do |i|
592
- rgb[i*3, 3] = lookup_values[pixels[i]]
593
- end
594
- # As we have now ordered the pixels in RGB order, modify planar configuration to reflect this:
595
- planar = 0
596
- elsif photometric.include?("YBR")
597
- rgb = false
598
- else
599
- rgb = pixels
600
- end
601
- # Step 2: If indicated by the planar configuration, the order of the pixels need to be rearranged:
602
- if rgb
603
- if planar == 1
604
- # Rearrange from [RRR...GGG....BBB...] to [(RGB)(RGB)(RGB)...]:
605
- r_ind = [rgb.length/3-1, rgb.length*2/3-1, rgb.length-1]
606
- l_ind = [0, rgb.length/3, rgb.length*2/3]
607
- proper_rgb = [rgb[l_ind[0]..r_ind[0]], rgb[l_ind[1]..r_ind[1]], rgb[l_ind[2]..r_ind[2]]].transpose.flatten
608
- else
609
- proper_rgb = rgb
610
- end
611
- end
612
- return proper_rgb
613
- end
614
-
615
- # Converts original pixel data values to presentation values, which are returned.
616
- #
617
- # === Parameters
618
- #
619
- # * <tt>pixel_data</tt> -- An array of pixel values (integers).
620
- # * <tt>min_allowed</tt> -- Fixnum. The minimum value allowed in the returned pixels.
621
- # * <tt>max_allowed</tt> -- Fixnum. The maximum value allowed in the returned pixels.
622
- # * <tt>level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
623
- #
624
- def process_presentation_values(pixel_data, min_allowed, max_allowed, level=nil)
625
- # Process pixel data for presentation according to the image information in the DICOM object:
626
- center, width, intercept, slope = window_level_values
627
- # Have image leveling been requested?
628
- if level
629
- # If custom values are specified in an array, use those. If not, the default values from the DICOM object are used:
630
- if level.is_a?(Array)
631
- center = level[0]
632
- width = level[1]
633
- end
634
- else
635
- center, width = false, false
636
- end
637
- # PixelOutput = slope * pixel_values + intercept
638
- if intercept != 0 or slope != 1
639
- pixel_data.collect!{|x| (slope * x) + intercept}
640
- end
641
- # Contrast enhancement by black and white thresholding:
642
- if center and width
643
- low = center - width/2
644
- high = center + width/2
645
- pixel_data.each_index do |i|
646
- if pixel_data[i] < low
647
- pixel_data[i] = low
648
- elsif pixel_data[i] > high
649
- pixel_data[i] = high
650
- end
651
- end
652
- end
653
- # Need to introduce an offset?
654
- min_pixel_value = pixel_data.min
655
- if min_allowed
656
- if min_pixel_value < min_allowed
657
- offset = min_pixel_value.abs
658
- pixel_data.collect!{|x| x + offset}
659
- end
660
- end
661
- # Downscale pixel range?
662
- max_pixel_value = pixel_data.max
663
- if max_allowed
664
- if max_pixel_value > max_allowed
665
- factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
666
- pixel_data.collect!{|x| x / factor}
667
- end
668
- end
669
- return pixel_data
670
- end
671
-
672
- # Converts original pixel data values to presentation values, using the efficient NArray library.
673
- #
674
- # === Notes
675
- #
676
- # * If a Ruby Array is supplied, the method returns a one-dimensional NArray object (i.e. no columns & rows).
677
- # * If a NArray is supplied, the NArray is returned with its original dimensions.
678
- #
679
- # === Parameters
680
- #
681
- # * <tt>pixel_data</tt> -- An Array/NArray of pixel values (integers).
682
- # * <tt>min_allowed</tt> -- Fixnum. The minimum value allowed in the returned pixels.
683
- # * <tt>max_allowed</tt> -- Fixnum. The maximum value allowed in the returned pixels.
684
- # * <tt>level</tt> -- Boolean or array. If set as true window leveling is performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
685
- #
686
- def process_presentation_values_narray(pixel_data, min_allowed, max_allowed, level=nil)
687
- # Process pixel data for presentation according to the image information in the DICOM object:
688
- center, width, intercept, slope = window_level_values
689
- # Have image leveling been requested?
690
- if level
691
- # If custom values are specified in an array, use those. If not, the default values from the DICOM object are used:
692
- if level.is_a?(Array)
693
- center = level[0]
694
- width = level[1]
695
- end
696
- else
697
- center, width = false, false
698
- end
699
- # Need to convert to NArray?
700
- if pixel_data.is_a?(Array)
701
- n_arr = NArray.to_na(pixel_data)
702
- else
703
- n_arr = pixel_data
704
- end
705
- # Remap:
706
- # PixelOutput = slope * pixel_values + intercept
707
- if intercept != 0 or slope != 1
708
- n_arr = slope * n_arr + intercept
709
- end
710
- # Contrast enhancement by black and white thresholding:
711
- if center and width
712
- low = center - width/2
713
- high = center + width/2
714
- n_arr[n_arr < low] = low
715
- n_arr[n_arr > high] = high
716
- end
717
- # Need to introduce an offset?
718
- min_pixel_value = n_arr.min
719
- if min_allowed
720
- if min_pixel_value < min_allowed
721
- offset = min_pixel_value.abs
722
- n_arr = n_arr + offset
723
- end
724
- end
725
- # Downscale pixel range?
726
- max_pixel_value = n_arr.max
727
- if max_allowed
728
- if max_pixel_value > max_allowed
729
- factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
730
- n_arr = n_arr / factor
731
- end
732
- end
733
- return n_arr
734
- end
735
-
736
- # Creates an image object from the specified pixel value array, performing presentation value processing if requested.
737
- # Returns the image object.
738
- #
739
- # === Parameters
740
- #
741
- # * <tt>pixel_data</tt> -- An array of pixel values (integers).
742
- # * <tt>columns</tt> -- Fixnum. Number of columns in the pixel data.
743
- # * <tt>rows</tt> -- Fixnum. Number of rows in the pixel data.
744
- # * <tt>options</tt> -- A hash of parameters.
745
- #
746
- # === Options
747
- #
748
- # * <tt>:remap</tt> -- Boolean. If set, pixel values will be remapped to presentation values (using intercept and slope values from the DICOM object).
749
- # * <tt>:level</tt> -- Boolean or array. If set (as true) window leveling are performed using default values from the DICOM object. If an array ([center, width]) is specified, these custom values are used instead.
750
- # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray for the pixel remap process (for faster execution).
751
- #
752
- # === Notes
753
- #
754
- # * Definitions for Window Center and Width can be found in the DICOM standard, PS 3.3 C.11.2.1.2
755
- #
756
- def read_image(pixel_data, columns, rows, options={})
757
- raise ArgumentError, "Expected Array for pixel_data, got #{pixel_data.class}" unless pixel_data.is_a?(Array)
758
- raise ArgumentError, "Expected Integer for columns, got #{columns.class}" unless columns.is_a?(Integer)
759
- raise ArgumentError, "Expected Rows for columns, got #{rows.class}" unless rows.is_a?(Integer)
760
- raise ArgumentError, "Size of pixel_data must be at least equal to columns*rows. Got #{columns}*#{rows}=#{columns*rows}, which is less than the array size #{pixel_data.length}" if columns * rows > pixel_data.length
761
- # Remap the image from pixel values to presentation values if the user has requested this:
762
- if options[:remap] or options[:level]
763
- # How to perform the remapping? NArray (fast) or Ruby Array (slow)?
764
- if options[:narray] == true
765
- pixel_data = process_presentation_values_narray(pixel_data, 0, 65535, options[:level]).to_a
766
- else
767
- pixel_data = process_presentation_values(pixel_data, 0, 65535, options[:level])
768
- end
769
- else
770
- # No remapping, but make sure that we pass on unsigned pixel values to the image processor:
771
- pixel_data = pixel_data.to_unsigned(bit_depth) if signed_pixels?
772
- end
773
- image = import_pixels(pixel_data.to_blob(actual_bit_depth), columns, rows, actual_bit_depth, photometry)
774
- return image
775
- end
776
-
777
- # Returns true if the Pixel Representation indicates signed pixel values, and false if it indicates unsigned pixel values.
778
- # Raises an error if the Pixel Representation element is not present.
779
- #
780
- def signed_pixels?
781
- raise "The 'Pixel Representation' data element is missing from this DICOM instance. Unable to process pixel data." unless exists?("0028,0103")
782
- case value("0028,0103")
783
- when 1
784
- return true
785
- when 0
786
- return false
787
- else
788
- raise "Invalid value encountered (#{value("0028,0103")}) in the 'Pixel Representation' data element. Expected 0 or 1."
789
- end
790
- end
791
-
792
- # Determines and returns a template string for pack/unpacking pixel data, based on
793
- # the number of bits per pixel as well as the pixel representation (signed or unsigned).
794
- #
795
- # === Parameters
796
- #
797
- # * <tt>depth</tt> -- Integer. The number of allocated bits in the integers to be decoded/encoded.
798
- #
799
- def template_string(depth)
800
- template = false
801
- pixel_representation = self["0028,0103"].value.to_i
802
- # Number of bytes used per pixel will determine how to unpack this:
803
- case depth
804
- when 8 # (1 byte)
805
- template = "BY" # Byte/Character/Fixnum
806
- when 16 # (2 bytes)
807
- if pixel_representation == 1
808
- template = "SS" # Signed short
809
- else
810
- template = "US" # Unsigned short
811
- end
812
- when 32 # (4 bytes)
813
- if pixel_representation == 1
814
- template = "SL" # Signed long
815
- else
816
- template = "UL" # Unsigned long
817
- end
818
- when 12
819
- # 12 BIT SIMPLY NOT IMPLEMENTED YET!
820
- # This one is a bit tricky. I havent really given this priority so far as 12 bit image data is rather rare.
821
- raise "Packing/unpacking pixel data of bit depth 12 is not implemented yet! Please contact the author (or edit the source code)."
822
- else
823
- raise ArgumentError, "Encoding/Decoding pixel data with this Bit Depth (#{depth}) is not implemented."
824
- end
825
- return template
826
- end
827
-
828
- # Gathers and returns the window level values needed to convert the original pixel values to presentation values.
829
- #
830
- # === Notes
831
- #
832
- # If some of these values are missing in the DObject instance, default values are used instead
833
- # for intercept and slope, while center and width are set to nil. No errors are raised.
834
- #
835
- def window_level_values
836
- center = (self["0028,1050"].is_a?(Element) == true ? self["0028,1050"].value.to_i : nil)
837
- width = (self["0028,1051"].is_a?(Element) == true ? self["0028,1051"].value.to_i : nil)
838
- intercept = (self["0028,1052"].is_a?(Element) == true ? self["0028,1052"].value.to_i : 0)
839
- slope = (self["0028,1053"].is_a?(Element) == true ? self["0028,1053"].value.to_i : 1)
840
- return center, width, intercept, slope
841
- end
842
-
843
- # Transfers a pre-encoded binary string to the pixel data element, either by
844
- # overwriting the existing element value, or creating a new "Pixel Data" element.
845
- #
846
- # === Parameters
847
- #
848
- # * <tt>bin</tt> -- A binary string containing encoded pixel data.
849
- #
850
- def write_pixels(bin)
851
- if self.exists?(PIXEL_TAG)
852
- # Update existing Data Element:
853
- self[PIXEL_TAG].bin = bin
854
- else
855
- # Create new Data Element:
856
- pixel_element = Element.new(PIXEL_TAG, bin, :encoded => true, :parent => self)
857
- end
858
- end
859
-
860
- end
861
- end
1
+ module DICOM
2
+
3
+ # Super class which contains common code for both the DObject and Item classes.
4
+ # This class includes the image related methods, since images may be stored either
5
+ # directly in the DObject, or in items (encapsulated items in the "Pixel Data"
6
+ # element or in "Icon Image Sequence" items).
7
+ #
8
+ # === Inheritance
9
+ #
10
+ # As the ImageItem class inherits from the Parent class, all Parent methods are
11
+ # also available to objects which has inherited ImageItem.
12
+ #
13
+ class ImageItem < Parent
14
+
15
+ include ImageProcessor
16
+
17
+ # Checks if colored pixel data is present.
18
+ #
19
+ # @return [Boolean] true if the object contains colored pixels, and false if not
20
+ #
21
+ def color?
22
+ # "Photometric Interpretation" is contained in the data element "0028,0004":
23
+ begin
24
+ photometric = photometry
25
+ if photometric.include?("COLOR") or photometric.include?("RGB") or photometric.include?("YBR")
26
+ return true
27
+ else
28
+ return false
29
+ end
30
+ rescue
31
+ return false
32
+ end
33
+ end
34
+
35
+ # Checks if compressed pixel data is present.
36
+ #
37
+ # @return [Boolean] true if the object contains compressed pixels, and false if not
38
+ #
39
+ def compression?
40
+ # If compression is used, the pixel data element is a Sequence (with encapsulated elements), instead of a Element:
41
+ if self[PIXEL_TAG].is_a?(Sequence)
42
+ return true
43
+ else
44
+ return false
45
+ end
46
+ end
47
+
48
+ # Unpacks pixel values from a binary pixel string. The decode is performed
49
+ # using values defined in the image related elements of the DObject instance.
50
+ #
51
+ # @param [String] bin a binary string containing the pixels to be decoded
52
+ # @param [Stream] stream a Stream instance to be used for decoding the pixels (optional)
53
+ # @return [Array<Integer>] decoded pixel values
54
+ #
55
+ def decode_pixels(bin, stream=@stream)
56
+ raise ArgumentError, "Expected String, got #{bin.class}." unless bin.is_a?(String)
57
+ pixels = false
58
+ # We need to know what kind of bith depth and integer type the pixel data is saved with:
59
+ bit_depth_element = self["0028,0100"]
60
+ pixel_representation_element = self["0028,0103"]
61
+ if bit_depth_element and pixel_representation_element
62
+ # Load the binary pixel data to the Stream instance:
63
+ stream.set_string(bin)
64
+ template = template_string(bit_depth_element.value.to_i)
65
+ pixels = stream.decode_all(template) if template
66
+ else
67
+ raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to decode pixel data." unless bit_depth_element
68
+ raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to decode pixel data." unless pixel_representation_element
69
+ end
70
+ return pixels
71
+ end
72
+
73
+ # Packs a pixel value array to a binary pixel string. The encoding is performed
74
+ # using values defined in the image related elements of the DObject instance.
75
+ #
76
+ # @param [Array<Integer>] pixels an array containing the pixel values to be encoded
77
+ # @param [Stream] stream a Stream instance to be used for encoding the pixels (optional)
78
+ # @return [String] encoded pixel string
79
+ #
80
+ def encode_pixels(pixels, stream=@stream)
81
+ raise ArgumentError, "Expected Array, got #{pixels.class}." unless pixels.is_a?(Array)
82
+ bin = false
83
+ # We need to know what kind of bith depth and integer type the pixel data is saved with:
84
+ bit_depth_element = self["0028,0100"]
85
+ pixel_representation_element = self["0028,0103"]
86
+ if bit_depth_element and pixel_representation_element
87
+ template = template_string(bit_depth_element.value.to_i)
88
+ bin = stream.encode(pixels, template) if template
89
+ else
90
+ raise "The Element specifying Bit Depth (0028,0100) is missing. Unable to encode the pixel data." unless bit_depth_element
91
+ raise "The Element specifying Pixel Representation (0028,0103) is missing. Unable to encode the pixel data." unless pixel_representation_element
92
+ end
93
+ return bin
94
+ end
95
+
96
+ # Extracts a single image object, created from the encoded pixel data using
97
+ # the image related elements in the DICOM object. If the object contains multiple
98
+ # image frames, the first image frame is returned, unless the :frame option is used.
99
+ #
100
+ # @note Creates an image object in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.
101
+ # @note When calling this method the corresponding image processor gem must have been loaded in advance (example: require 'RMagick').
102
+ #
103
+ # @param [Hash] options the options to use for extracting the image
104
+ # @option options [Integer] :frame for DICOM objects containing multiple frames, this option can be used to extract a specific image frame (defaults to 0)
105
+ # @option options [TrueClass, Array<Integer>] :level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
106
+ # @option options [Boolean] :narray if true, forces the use of NArray for the pixel remap process (for faster execution)
107
+ # @option options [Boolean] :remap if true, the returned pixel values are remapped to presentation values
108
+ # @return [MagickImage, NilClass, FalseClass] an image object, alternatively nil (if no image present) or false (if image decode failed)
109
+ #
110
+ # @example Retrieve pixel data as an RMagick image object and display it
111
+ # image = dcm.image
112
+ # image.display
113
+ # @example Retrieve frame index 5 in the pixel data
114
+ # image = dcm.image(:frame => 5)
115
+ #
116
+ def image(options={})
117
+ options[:frame] = options[:frame] || 0
118
+ image = images(options).first
119
+ image = false if image.nil? && exists?(PIXEL_TAG)
120
+ return image
121
+ end
122
+
123
+ # Extracts an array of image objects, created from the encoded pixel data using
124
+ # the image related elements in the DICOM object.
125
+ #
126
+ # @note Creates an array of image objects in accordance with the selected image processor. Available processors are :rmagick and :mini_magick.
127
+ # @note When calling this method the corresponding image processor gem must have been loaded in advance (example: require 'RMagick').
128
+ #
129
+ # @param [Hash] options the options to use for extracting the images
130
+ # @option options [Integer] :frame makes the method return an array containing only the image object corresponding to the specified frame number
131
+ # @option options [TrueClass, Array<Integer>] :level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
132
+ # @option options [Boolean] :narray if true, forces the use of NArray for the pixel remap process (for faster execution)
133
+ # @option options [Boolean] :remap if true, the returned pixel values are remapped to presentation values
134
+ # @return [Array<MagickImage, NilClass>] an array of image objects, alternatively an empty array (if no image present or image decode failed)
135
+ #
136
+ # @example Retrieve the pixel data as RMagick image objects
137
+ # images = dcm.images
138
+ # @example Retrieve the pixel data as RMagick image objects, remapped to presentation values (but without any leveling)
139
+ # images = dcm.images(:remap => true)
140
+ # @example Retrieve the pixel data as RMagick image objects, remapped to presentation values and leveled using the default center/width values in the DICOM object
141
+ # images = dcm.images(:level => true)
142
+ # @example Retrieve the pixel data as RMagick image objects, remapped to presentation values, leveled with the specified center/width values and using numerical array for the rescaling (~twice as fast)
143
+ # images = dcm.images(:level => [-200,1000], :narray => true)
144
+ #
145
+ def images(options={})
146
+ images = Array.new
147
+ if exists?(PIXEL_TAG)
148
+ # Gather the pixel data strings, and pick a single frame if indicated by options:
149
+ strings = image_strings(split_to_frames=true)
150
+ strings = [strings[options[:frame]]] if options[:frame]
151
+ if compression?
152
+ # Decompress, either to numbers (RLE) or to an image object (image based compressions):
153
+ if [TXS_RLE].include?(transfer_syntax)
154
+ pixel_frames = Array.new
155
+ strings.each {|string| pixel_frames << decode_rle(num_cols, num_rows, string)}
156
+ else
157
+ images = decompress(strings) || Array.new
158
+ logger.warn("Decompressing pixel values has failed (unsupported transfer syntax: '#{transfer_syntax}' - #{LIBRARY.uid(transfer_syntax) ? LIBRARY.uid(transfer_syntax).name : 'Unknown transfer syntax!'})") unless images.length > 0
159
+ end
160
+ else
161
+ # Uncompressed: Decode to numbers.
162
+ pixel_frames = Array.new
163
+ strings.each {|string| pixel_frames << decode_pixels(string)}
164
+ end
165
+ if pixel_frames
166
+ images = Array.new
167
+ pixel_frames.each do |pixels|
168
+ # Pixel values and pixel order may need to be rearranged if we have color data:
169
+ pixels = process_colors(pixels) if color?
170
+ if pixels
171
+ images << read_image(pixels, num_cols, num_rows, options)
172
+ else
173
+ logger.warn("Processing pixel values for this particular color mode failed, unable to construct image(s).")
174
+ end
175
+ end
176
+ end
177
+ end
178
+ return images
179
+ end
180
+
181
+ # Reads a binary string from a specified file and writes it to the value field of the pixel data element (7FE0,0010).
182
+ #
183
+ # @param [String] file a string which specifies the path of the file containing pixel data
184
+ #
185
+ # @example Load pixel data from a file
186
+ # dcm.image_from_file("custom_image.dat")
187
+ #
188
+ def image_from_file(file)
189
+ raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
190
+ f = File.new(file, "rb")
191
+ bin = f.read(f.stat.size)
192
+ if bin.length > 0
193
+ # Write the binary data to the Pixel Data Element:
194
+ write_pixels(bin)
195
+ else
196
+ logger.info("The specified file (#{file}) is empty. Nothing to transfer.")
197
+ end
198
+ end
199
+
200
+ # Extracts the pixel data binary string(s) in an array.
201
+ #
202
+ # @param [Boolean] split if true, a pixel data string containing 3D volumetric data will be split into N substrings (where N equals the number of frames)
203
+ # @return [Array<String, NilClass>] an array of pixel data strings, or an empty array (if no pixel data present)
204
+ #
205
+ def image_strings(split=false)
206
+ # Pixel data may be a single binary string in the pixel data element,
207
+ # or located in several encapsulated item elements:
208
+ pixel_element = self[PIXEL_TAG]
209
+ strings = Array.new
210
+ if pixel_element.is_a?(Element)
211
+ if split
212
+ strings = pixel_element.bin.dup.divide(num_frames)
213
+ else
214
+ strings << pixel_element.bin
215
+ end
216
+ elsif pixel_element.is_a?(Sequence)
217
+ pixel_items = pixel_element.children.first.children
218
+ pixel_items.each {|item| strings << item.bin}
219
+ end
220
+ return strings
221
+ end
222
+
223
+ # Dumps the binary content of the Pixel Data element to the specified file.
224
+ #
225
+ # If the DICOM object contains multi-fragment pixel data, each fragment
226
+ # will be dumped to separate files (e.q. 'fragment-0.dat', 'fragment-1.dat').
227
+ #
228
+ # @param [String] file a string which specifies the file path to use when dumping the pixel data
229
+ # @example Dumping the pixel data to a file
230
+ # dcm.image_to_file("exported_image.dat")
231
+ #
232
+ def image_to_file(file)
233
+ raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
234
+ # Split the file name in case of multiple fragments:
235
+ parts = file.split('.')
236
+ if parts.length > 1
237
+ base = parts[0..-2].join
238
+ extension = "." + parts.last
239
+ else
240
+ base = file
241
+ extension = ""
242
+ end
243
+ # Get the binary image strings and dump them to the file(s):
244
+ images = image_strings
245
+ images.each_index do |i|
246
+ if images.length == 1
247
+ f = File.new(file, "wb")
248
+ else
249
+ f = File.new("#{base}-#{i}#{extension}", "wb")
250
+ end
251
+ f.write(images[i])
252
+ f.close
253
+ end
254
+ end
255
+
256
+ # Encodes pixel data from a (Magick) image object and writes it to the
257
+ # pixel data element (7FE0,0010).
258
+ #
259
+ # Because of pixel value issues related to image objects (images don't like
260
+ # signed integers), and the possible difference between presentation values
261
+ # and raw pixel values, the use of image=() may result in pixel data where the
262
+ # integer values differs somewhat from what is expected. Use with care! For
263
+ # precise pixel value processing, use the Array and NArray based pixel data methods instead.
264
+ #
265
+ # @param [MagickImage] image the image to be assigned to the pixel data element
266
+ #
267
+ def image=(image)
268
+ raise ArgumentError, "Expected one of the supported image objects (#{valid_image_objects}), got #{image.class}." unless valid_image_objects.include?(image.class)
269
+ # Export to pixels using the proper image processor:
270
+ pixels = export_pixels(image, photometry)
271
+ # Encode and write to the Pixel Data Element:
272
+ self.pixels = pixels
273
+ end
274
+
275
+ # Gives the number of columns in the pixel data.
276
+ #
277
+ # @return [Integer, NilClass] the number of columns, or nil (if the columns value is undefined)
278
+ #
279
+ def num_cols
280
+ self["0028,0011"].value rescue nil
281
+ end
282
+
283
+ # Gives the number of frames in the pixel data.
284
+ #
285
+ # @note Assumes and gives 1 if the number of frames value is not defined.
286
+ # @return [Integer] the number of rows
287
+ #
288
+ def num_frames
289
+ (self["0028,0008"].is_a?(Element) == true ? self["0028,0008"].value.to_i : 1)
290
+ end
291
+
292
+ # Gives the number of rows in the pixel data.
293
+ #
294
+ # @return [Integer, NilClass] the number of rows, or nil (if the rows value is undefined)
295
+ #
296
+ def num_rows
297
+ self["0028,0010"].value rescue nil
298
+ end
299
+
300
+ # Creates an NArray containing the pixel data. If the pixel data is an image
301
+ # (single frame), a 2-dimensional NArray is returned [columns, rows]. If the
302
+ # pixel data is 3-dimensional (more than one frame), a 3-dimensional NArray
303
+ # is returned [frames, columns, rows].
304
+ #
305
+ # @note To call this method you need to have loaded the NArray library in advance (require 'narray').
306
+ #
307
+ # @param [Hash] options the options to use for extracting the pixel data
308
+ # @option options [TrueClass, Array<Integer>] :level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
309
+ # @option options [Boolean] :remap if true, the returned pixel values are remapped to presentation values
310
+ # @option options [Boolean] :volume if true, the returned array will always be 3-dimensional, even if the pixel data only has one frame
311
+ # @return [NArray, NilClass, FalseClass] an NArray of pixel values, alternatively nil (if no image present) or false (if image decode failed)
312
+ #
313
+ # @example Retrieve numerical pixel array
314
+ # data = dcm.narray
315
+ # @example Retrieve numerical pixel array remapped from the original pixel values to presentation values
316
+ # data = dcm.narray(:remap => true)
317
+ #
318
+ def narray(options={})
319
+ pixels = nil
320
+ if exists?(PIXEL_TAG)
321
+ unless color?
322
+ # Decode the pixel values: For now we only support returning pixel data of the first frame (if the image is located in multiple pixel data items).
323
+ if compression?
324
+ pixels = decompress(image_strings.first)
325
+ else
326
+ pixels = decode_pixels(image_strings.first)
327
+ end
328
+ if pixels
329
+ # Import the pixels to NArray and give it a proper shape:
330
+ raise "Missing Rows and/or Columns Element. Unable to construct pixel data array." unless num_rows and num_cols
331
+ if num_frames > 1 or options[:volume]
332
+ # Create an empty 3D NArray. fill it with pixels frame by frame, then reassign the pixels variable to it:
333
+ narr = NArray.int(num_frames, num_cols, num_rows)
334
+ num_frames.times do |i|
335
+ narr[i, true, true] = NArray.to_na(pixels[(i * num_cols * num_rows)..((i + 1) * num_cols * num_rows - 1)]).reshape!(num_cols, num_rows)
336
+ end
337
+ pixels = narr
338
+ else
339
+ pixels = NArray.to_na(pixels).reshape!(num_cols, num_rows)
340
+ end
341
+ # Remap the image from pixel values to presentation values if the user has requested this:
342
+ pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]) if options[:remap] or options[:level]
343
+ else
344
+ logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
345
+ end
346
+ else
347
+ logger.warn("The DICOM object contains colored pixel data. Retrieval of colored pixels is not supported by this method yet.")
348
+ pixels = false
349
+ end
350
+ end
351
+ return pixels
352
+ end
353
+
354
+ # Extracts the Pixel Data values in an ordinary Ruby Array.
355
+ # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
356
+ #
357
+ # The returned array does not carry the dimensions of the pixel data:
358
+ # It is put in a one dimensional Array (vector).
359
+ #
360
+ # @param [Hash] options the options to use for extracting the pixel data
361
+ # @option options [TrueClass, Array<Integer>] :level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
362
+ # @option options [Boolean] :narray if true, forces the use of NArray for the pixel remap process (for faster execution)
363
+ # @option options [Boolean] :remap if true, the returned pixel values are remapped to presentation values
364
+ # @return [Array, NilClass, FalseClass] an Array of pixel values, alternatively nil (if no image present) or false (if image decode failed)
365
+ #
366
+ # @example Simply retrieve the pixel data
367
+ # pixels = dcm.pixels
368
+ # @example Retrieve the pixel data remapped to presentation values according to window center/width settings
369
+ # pixels = dcm.pixels(:remap => true)
370
+ # @example Retrieve the remapped pixel data while using numerical array (~twice as fast)
371
+ # pixels = dcm.pixels(:remap => true, :narray => true)
372
+ #
373
+ def pixels(options={})
374
+ pixels = nil
375
+ if exists?(PIXEL_TAG)
376
+ # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
377
+ if compression?
378
+ pixels = decompress(image_strings.first)
379
+ else
380
+ pixels = decode_pixels(image_strings.first)
381
+ end
382
+ if pixels
383
+ # Remap the image from pixel values to presentation values if the user has requested this:
384
+ if options[:remap] or options[:level]
385
+ if options[:narray]
386
+ # Use numerical array (faster):
387
+ pixels = process_presentation_values_narray(pixels, -65535, 65535, options[:level]).to_a
388
+ else
389
+ # Use standard Ruby array (slower):
390
+ pixels = process_presentation_values(pixels, -65535, 65535, options[:level])
391
+ end
392
+ end
393
+ else
394
+ logger.warn("Decompressing the Pixel Data failed. Pixel values can not be extracted.")
395
+ end
396
+ end
397
+ return pixels
398
+ end
399
+
400
+ # Encodes pixel data from a Ruby Array or NArray, and writes it to the pixel data element (7FE0,0010).
401
+ #
402
+ # @param [Array<Integer>, NArray] values an Array (or NArray) containing integer pixel values
403
+ #
404
+ def pixels=(values)
405
+ raise ArgumentError, "Expected Array or NArray, got #{values.class}." unless [Array, NArray].include?(values.class)
406
+ if values.is_a?(NArray)
407
+ # When NArray, convert to a Ruby Array:
408
+ if values.shape.length > 2
409
+ # We need to take care when reshaping this 3D array so that the pixel values falls properly into place:
410
+ narr = NArray.int(values.shape[1] * values.shape[2], values.shape[0])
411
+ values.shape[0].times do |i|
412
+ narr[true, i] = values[i, true, true].reshape(values.shape[1] * values.shape[2])
413
+ end
414
+ values = narr.to_a
415
+ else
416
+ values = values.to_a
417
+ end
418
+ end
419
+ # Encode the pixel data:
420
+ bin = encode_pixels(values.flatten)
421
+ # Write the binary data to the Pixel Data Element:
422
+ write_pixels(bin)
423
+ end
424
+
425
+ # Delete all Sequence instances from the DObject or Item instance.
426
+ #
427
+ def delete_sequences
428
+ @tags.each_value do |element|
429
+ delete(element.tag) if element.is_a?(Sequence)
430
+ end
431
+ end
432
+
433
+
434
+ private
435
+
436
+
437
+ # Gives the effective bit depth of the pixel data (considers a special case
438
+ # for Palette colored images).
439
+ #
440
+ # @raise [RuntimeError] if the 'Bits Allocated' element is missing
441
+ # @return [Integer] the effective bit depth of the pixel data
442
+ #
443
+ def actual_bit_depth
444
+ raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
445
+ if photometry == PI_PALETTE_COLOR
446
+ # Only one channel is checked and it is assumed that all channels have the same number of bits.
447
+ return self["0028,1101"].value.split("\\").last.to_i
448
+ else
449
+ return bit_depth
450
+ end
451
+ end
452
+
453
+
454
+ # Gives the value from the "Bits Allocated" Element.
455
+ #
456
+ # @raise [RuntimeError] if the 'Bits Allocated' element is missing
457
+ # @return [Integer] the number of bits allocated
458
+ #
459
+ def bit_depth
460
+ raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
461
+ return value("0028,0100")
462
+ end
463
+
464
+ # Performs a run length decoding on the input stream.
465
+ #
466
+ # @note For details on RLE encoding, refer to the DICOM standard, PS3.5, Section 8.2.2 as well as Annex G.
467
+ #
468
+ # @param [Integer] cols number of colums of the encoded image
469
+ # @param [Integer] rows number of rows of the encoded image
470
+ # @param [Integer] string the encoded pixel string
471
+ # @return [Array<Integer>] the decoded pixel values
472
+ #
473
+ def decode_rle(cols, rows, string)
474
+ # FIXME: Remove cols and rows (were only added for debugging).
475
+ pixels = Array.new
476
+ # RLE header specifying the number of segments:
477
+ header = string[0...64].unpack('L*')
478
+ image_segments = Array.new
479
+ # Extracting all start and endpoints of the different segments:
480
+ header.each_index do |n|
481
+ if n == 0
482
+ # This one need no processing.
483
+ elsif n == header[0]
484
+ # It's the last one
485
+ image_segments << [header[n], -1]
486
+ break
487
+ else
488
+ image_segments << [header[n], header[n + 1] - 1]
489
+ end
490
+ end
491
+ # Iterate over each segment and extract pixel data:
492
+ image_segments.each do |range|
493
+ segment_data = Array.new
494
+ next_bytes = -1
495
+ next_multiplier = 0
496
+ # Iterate this segment's pixel string:
497
+ string[range[0]..range[1]].each_byte do |b|
498
+ if next_multiplier > 0
499
+ next_multiplier.times { segment_data << b }
500
+ next_multiplier = 0
501
+ elsif next_bytes > 0
502
+ segment_data << b
503
+ next_bytes -= 1
504
+ elsif b <= 127
505
+ next_bytes = b + 1
506
+ else
507
+ # Explaining the 257 at this point is a little bit complicate. Basically it has something
508
+ # to do with the algorithm described in the DICOM standard and that the value -1 as uint8 is 255.
509
+ # TODO: Is this architectur safe or does it only work on Intel systems???
510
+ next_multiplier = 257 - b
511
+ end
512
+ end
513
+ # Verify that the RLE decoding has executed properly:
514
+ throw "Size mismatch #{segment_data.size} != #{rows * cols}" if segment_data.size != rows * cols
515
+ pixels += segment_data
516
+ end
517
+ return pixels
518
+ end
519
+
520
+ # Gives the value from the "Photometric Interpretation" Element.
521
+ #
522
+ # @raise [RuntimeError] if the 'Photometric Interpretation' element is missing
523
+ # @return [String] the photometric interpretation
524
+ #
525
+ def photometry
526
+ raise "The 'Photometric Interpretation' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0004")
527
+ return value("0028,0004").upcase
528
+ end
529
+
530
+ # Processes the pixel array based on attributes defined in the DICOM object,
531
+ #to produce a pixel array with correct pixel colors (RGB) as well as pixel
532
+ # order (RGB-pixel1, RGB-pixel2, etc). The relevant DICOM tags are
533
+ # Photometric Interpretation (0028,0004) and Planar Configuration (0028,0006).
534
+ #
535
+ # @param [Array<Integer>] pixels an array of (unsorted) color pixel values
536
+ # @return [Array<Integer>] an array of properly (sorted) color pixel values
537
+ #
538
+ def process_colors(pixels)
539
+ proper_rgb = false
540
+ photometric = photometry()
541
+ # (With RLE COLOR PALETTE the Planar Configuration is not set)
542
+ planar = self["0028,0006"].is_a?(Element) ? self["0028,0006"].value : 0
543
+ # Step 1: Produce an array with RGB values. At this time, YBR is not supported in ruby-dicom,
544
+ # so this leaves us with a possible conversion from PALETTE COLOR:
545
+ if photometric.include?("COLOR")
546
+ # Pseudo colors (rgb values grabbed from a lookup table):
547
+ rgb = Array.new(pixels.length*3)
548
+ # Prepare the lookup data arrays:
549
+ lookup_binaries = [self["0028,1201"].bin, self["0028,1202"].bin, self["0028,1203"].bin]
550
+ lookup_values = Array.new
551
+ nr_bits = self["0028,1101"].value.split("\\").last.to_i
552
+ template = template_string(nr_bits)
553
+ lookup_binaries.each do |bin|
554
+ stream.set_string(bin)
555
+ lookup_values << stream.decode_all(template)
556
+ end
557
+ lookup_values = lookup_values.transpose
558
+ # Fill the RGB array, one RGB pixel group (3 pixels) at a time:
559
+ pixels.each_index do |i|
560
+ rgb[i*3, 3] = lookup_values[pixels[i]]
561
+ end
562
+ # As we have now ordered the pixels in RGB order, modify planar configuration to reflect this:
563
+ planar = 0
564
+ elsif photometric.include?("YBR")
565
+ rgb = false
566
+ else
567
+ rgb = pixels
568
+ end
569
+ # Step 2: If indicated by the planar configuration, the order of the pixels need to be rearranged:
570
+ if rgb
571
+ if planar == 1
572
+ # Rearrange from [RRR...GGG....BBB...] to [(RGB)(RGB)(RGB)...]:
573
+ r_ind = [rgb.length/3-1, rgb.length*2/3-1, rgb.length-1]
574
+ l_ind = [0, rgb.length/3, rgb.length*2/3]
575
+ proper_rgb = [rgb[l_ind[0]..r_ind[0]], rgb[l_ind[1]..r_ind[1]], rgb[l_ind[2]..r_ind[2]]].transpose.flatten
576
+ else
577
+ proper_rgb = rgb
578
+ end
579
+ end
580
+ return proper_rgb
581
+ end
582
+
583
+ # Converts original pixel data values to presentation values.
584
+ #
585
+ # @param [Array<Integer>] pixel_data an array of pixel values (integers)
586
+ # @param [Integer] min_allowed the minimum value allowed in the pixel data
587
+ # @param [Integer] max_allowed the maximum value allowed in the pixel data
588
+ # @param [Boolean, Array<Integer>] level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
589
+ # @return [Array<Integer>] presentation values
590
+ #
591
+ def process_presentation_values(pixel_data, min_allowed, max_allowed, level=nil)
592
+ # Process pixel data for presentation according to the image information in the DICOM object:
593
+ center, width, intercept, slope = window_level_values
594
+ # Have image leveling been requested?
595
+ if level
596
+ # If custom values are specified in an array, use those. If not, the default values from the DICOM object are used:
597
+ if level.is_a?(Array)
598
+ center = level[0]
599
+ width = level[1]
600
+ end
601
+ else
602
+ center, width = false, false
603
+ end
604
+ # PixelOutput = slope * pixel_values + intercept
605
+ if intercept != 0 or slope != 1
606
+ pixel_data.collect!{|x| (slope * x) + intercept}
607
+ end
608
+ # Contrast enhancement by black and white thresholding:
609
+ if center and width
610
+ low = center - width/2
611
+ high = center + width/2
612
+ pixel_data.each_index do |i|
613
+ if pixel_data[i] < low
614
+ pixel_data[i] = low
615
+ elsif pixel_data[i] > high
616
+ pixel_data[i] = high
617
+ end
618
+ end
619
+ end
620
+ # Need to introduce an offset?
621
+ min_pixel_value = pixel_data.min
622
+ if min_allowed
623
+ if min_pixel_value < min_allowed
624
+ offset = min_pixel_value.abs
625
+ pixel_data.collect!{|x| x + offset}
626
+ end
627
+ end
628
+ # Downscale pixel range?
629
+ max_pixel_value = pixel_data.max
630
+ if max_allowed
631
+ if max_pixel_value > max_allowed
632
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
633
+ pixel_data.collect!{|x| x / factor}
634
+ end
635
+ end
636
+ return pixel_data
637
+ end
638
+
639
+ # Converts original pixel data values to presentation values, using the efficient NArray library.
640
+ #
641
+ # @note If a Ruby Array is supplied, the method returns a one-dimensional NArray object (i.e. no columns & rows).
642
+ # @note If a NArray is supplied, the NArray is returned with its original dimensions.
643
+ #
644
+ # @param [Array<Integer>, NArray] pixel_data pixel values
645
+ # @param [Integer] min_allowed the minimum value allowed in the pixel data
646
+ # @param [Integer] max_allowed the maximum value allowed in the pixel data
647
+ # @param [Boolean, Array<Integer>] level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
648
+ # @return [Array<Integer>, NArray] presentation values
649
+ #
650
+ def process_presentation_values_narray(pixel_data, min_allowed, max_allowed, level=nil)
651
+ # Process pixel data for presentation according to the image information in the DICOM object:
652
+ center, width, intercept, slope = window_level_values
653
+ # Have image leveling been requested?
654
+ if level
655
+ # If custom values are specified in an array, use those. If not, the default values from the DICOM object are used:
656
+ if level.is_a?(Array)
657
+ center = level[0]
658
+ width = level[1]
659
+ end
660
+ else
661
+ center, width = false, false
662
+ end
663
+ # Need to convert to NArray?
664
+ if pixel_data.is_a?(Array)
665
+ n_arr = NArray.to_na(pixel_data)
666
+ else
667
+ n_arr = pixel_data
668
+ end
669
+ # Remap:
670
+ # PixelOutput = slope * pixel_values + intercept
671
+ if intercept != 0 or slope != 1
672
+ n_arr = slope * n_arr + intercept
673
+ end
674
+ # Contrast enhancement by black and white thresholding:
675
+ if center and width
676
+ low = center - width/2
677
+ high = center + width/2
678
+ n_arr[n_arr < low] = low
679
+ n_arr[n_arr > high] = high
680
+ end
681
+ # Need to introduce an offset?
682
+ min_pixel_value = n_arr.min
683
+ if min_allowed
684
+ if min_pixel_value < min_allowed
685
+ offset = min_pixel_value.abs
686
+ n_arr = n_arr + offset
687
+ end
688
+ end
689
+ # Downscale pixel range?
690
+ max_pixel_value = n_arr.max
691
+ if max_allowed
692
+ if max_pixel_value > max_allowed
693
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
694
+ n_arr = n_arr / factor
695
+ end
696
+ end
697
+ return n_arr
698
+ end
699
+
700
+ # Creates an image object from the specified pixel value array, performing
701
+ # presentation value processing if requested.
702
+ #
703
+ # @note Definitions for Window Center and Width can be found in the DICOM standard, PS 3.3 C.11.2.1.2
704
+ #
705
+ # @param [Array<Integer>] pixel_data an array of pixel values
706
+ # @param [Integer] columns the number of columns in the pixel data
707
+ # @param [Integer] rows the number of rows in the pixel data
708
+ # @param [Hash] options the options to use for reading the image
709
+ # @option options [Boolean] :remap if true, pixel values are remapped to presentation values (using intercept and slope values from the DICOM object)
710
+ # @option options [Boolean, Array<Integer>] :level if true, window leveling is performed using default values from the DICOM object, or if an array ([center, width]) is specified, these custom values are used instead
711
+ # @option options [Boolean] :narray if true, forces the use of NArray for the pixel remap process (for faster execution)
712
+ # @return [MagickImage] the extracted image object
713
+ #
714
+ def read_image(pixel_data, columns, rows, options={})
715
+ raise ArgumentError, "Expected Array for pixel_data, got #{pixel_data.class}" unless pixel_data.is_a?(Array)
716
+ raise ArgumentError, "Expected Integer for columns, got #{columns.class}" unless columns.is_a?(Integer)
717
+ raise ArgumentError, "Expected Rows for columns, got #{rows.class}" unless rows.is_a?(Integer)
718
+ raise ArgumentError, "Size of pixel_data must be at least equal to columns*rows. Got #{columns}*#{rows}=#{columns*rows}, which is less than the array size #{pixel_data.length}" if columns * rows > pixel_data.length
719
+ # Remap the image from pixel values to presentation values if the user has requested this:
720
+ if options[:remap] or options[:level]
721
+ # How to perform the remapping? NArray (fast) or Ruby Array (slow)?
722
+ if options[:narray] == true
723
+ pixel_data = process_presentation_values_narray(pixel_data, 0, 65535, options[:level]).to_a
724
+ else
725
+ pixel_data = process_presentation_values(pixel_data, 0, 65535, options[:level])
726
+ end
727
+ else
728
+ # No remapping, but make sure that we pass on unsigned pixel values to the image processor:
729
+ pixel_data = pixel_data.to_unsigned(bit_depth) if signed_pixels?
730
+ end
731
+ image = import_pixels(pixel_data.to_blob(actual_bit_depth), columns, rows, actual_bit_depth, photometry)
732
+ return image
733
+ end
734
+
735
+ # Checks if the Pixel Representation indicates signed pixel values or not.
736
+ #
737
+ # @raise [RuntimeError] if the 'Pixel Representation' element is missing
738
+ # @return [Boolean] true if pixel values are signed, false if not
739
+ #
740
+ def signed_pixels?
741
+ raise "The 'Pixel Representation' data element is missing from this DICOM instance. Unable to process pixel data." unless exists?("0028,0103")
742
+ case value("0028,0103")
743
+ when 1
744
+ return true
745
+ when 0
746
+ return false
747
+ else
748
+ raise "Invalid value encountered (#{value("0028,0103")}) in the 'Pixel Representation' data element. Expected 0 or 1."
749
+ end
750
+ end
751
+
752
+ # Determines the template/format string for pack/unpacking pixel data, based on
753
+ # the number of bits per pixel as well as the pixel representation (signed or unsigned).
754
+ #
755
+ # @param [Integer] depth the number of allocated bits in the integers to be decoded/encoded
756
+ # @return [String] a format string
757
+ #
758
+ def template_string(depth)
759
+ template = false
760
+ pixel_representation = self["0028,0103"].value.to_i
761
+ # Number of bytes used per pixel will determine how to unpack this:
762
+ case depth
763
+ when 8 # (1 byte)
764
+ template = "BY" # Byte/Character/Fixnum
765
+ when 16 # (2 bytes)
766
+ if pixel_representation == 1
767
+ template = "SS" # Signed short
768
+ else
769
+ template = "US" # Unsigned short
770
+ end
771
+ when 32 # (4 bytes)
772
+ if pixel_representation == 1
773
+ template = "SL" # Signed long
774
+ else
775
+ template = "UL" # Unsigned long
776
+ end
777
+ when 12
778
+ # 12 BIT SIMPLY NOT IMPLEMENTED YET!
779
+ # This one is a bit tricky. I havent really given this priority so far as 12 bit image data is rather rare.
780
+ raise "Packing/unpacking pixel data of bit depth 12 is not implemented yet! Please contact the author (or edit the source code)."
781
+ else
782
+ raise ArgumentError, "Encoding/Decoding pixel data with this Bit Depth (#{depth}) is not implemented."
783
+ end
784
+ return template
785
+ end
786
+
787
+ # Collects the window level values needed to convert the original pixel
788
+ # values to presentation values.
789
+ #
790
+ # @note If some of these values are missing in the DObject instance,
791
+ # default values are used instead for intercept and slope, while center
792
+ # and width are set to nil. No errors are raised.
793
+ # @return [Array<Integer, NilClass>] center, width, intercept and slope
794
+ #
795
+ def window_level_values
796
+ center = (self["0028,1050"].is_a?(Element) == true ? self["0028,1050"].value.to_i : nil)
797
+ width = (self["0028,1051"].is_a?(Element) == true ? self["0028,1051"].value.to_i : nil)
798
+ intercept = (self["0028,1052"].is_a?(Element) == true ? self["0028,1052"].value.to_i : 0)
799
+ slope = (self["0028,1053"].is_a?(Element) == true ? self["0028,1053"].value.to_i : 1)
800
+ return center, width, intercept, slope
801
+ end
802
+
803
+ # Transfers a pre-encoded binary string to the pixel data element, either by
804
+ # overwriting the existing element value, or creating a new "Pixel Data" element.
805
+ #
806
+ # @param [String] bin a binary string containing encoded pixel data
807
+ #
808
+ def write_pixels(bin)
809
+ if self.exists?(PIXEL_TAG)
810
+ # Update existing Data Element:
811
+ self[PIXEL_TAG].bin = bin
812
+ else
813
+ # Create new Data Element:
814
+ pixel_element = Element.new(PIXEL_TAG, bin, :encoded => true, :parent => self)
815
+ end
816
+ end
817
+
818
+ end
819
+ end