dicom 0.8 → 0.9

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