dicom 0.8 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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