dicom 0.9.6 → 0.9.7

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