dicom 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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