dicom 0.7 → 0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,696 @@
1
+ # Copyright 2008-2010 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # Super class which contains common code for both the DObject and Item classes.
6
+ # This class includes the image related methods, since images may be stored either directly in the DObject,
7
+ # or in items (encapsulated items in the "Pixel Data" element or in "Icon Image Sequence" items).
8
+ #
9
+ # === Inheritance
10
+ #
11
+ # As the SuperItem class inherits from the SuperParent class, all SuperParent methods are also available to objects which has inherited SuperItem.
12
+ #
13
+ class SuperItem < SuperParent
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
+ photometric = (self["0028,0004"].is_a?(DataElement) == true ? self["0028,0004"].value.upcase : "")
21
+ if photometric.include?("COLOR") or photometric.include?("RGB") or photometric.include?("YBR")
22
+ return true
23
+ else
24
+ return false
25
+ end
26
+ end
27
+
28
+ # Checks if compressed pixel data is present.
29
+ # Returns true if it is, false if not.
30
+ #
31
+ def compression?
32
+ # If compression is used, the pixel data element is a Sequence (with encapsulated elements), instead of a DataElement:
33
+ if self[PIXEL_TAG].is_a?(Sequence)
34
+ return true
35
+ else
36
+ return false
37
+ end
38
+ end
39
+
40
+ # Unpacks a binary pixel string and returns decoded pixel values in an array. Returns false if the decoding is unsuccesful.
41
+ # The decode is performed using values defined in the image related data elements of the DObject instance.
42
+ #
43
+ # === Parameters
44
+ #
45
+ # * <tt>bin</tt> -- A binary String containing the pixels that will be decoded.
46
+ # * <tt>stream</tt> -- A Stream instance to be used for decoding the pixels (optional).
47
+ #
48
+ def decode_pixels(bin, stream=@stream)
49
+ pixels = false
50
+ # We need to know what kind of bith depth and integer type the pixel data is saved with:
51
+ bit_depth_element = self["0028,0100"]
52
+ pixel_representation_element = self["0028,0103"]
53
+ if bit_depth_element and pixel_representation_element
54
+ # Load the binary pixel data to the Stream instance:
55
+ stream.set_string(bin)
56
+ # Number of bytes used per pixel will determine how to unpack this:
57
+ case bit_depth_element.value.to_i
58
+ when 8
59
+ pixels = stream.decode_all("BY") # Byte/Character/Fixnum (1 byte)
60
+ when 16
61
+ if pixel_representation_element.value.to_i == 1
62
+ pixels = stream.decode_all("SS") # Signed short (2 bytes)
63
+ else
64
+ pixels = stream.decode_all("US") # Unsigned short (2 bytes)
65
+ end
66
+ when 12
67
+ # 12 BIT SIMPLY NOT WORKING YET!
68
+ # This one is a bit tricky to extract. I havent really given this priority so far as 12 bit image data is rather rare.
69
+ raise "Decoding bit depth 12 is not implemented yet! Please contact the author (or edit the source code)."
70
+ else
71
+ raise "The Bit Depth #{bit_depth_element.value} has not received implementation in this procedure yet. Please contact the author (or edit the source code)."
72
+ end
73
+ else
74
+ raise "The Data Element which specifies Bit Depth is missing. Unable to decode pixel data." unless bit_depth_element
75
+ raise "The Data Element which specifies Pixel Representation is missing. Unable to decode pixel data." unless pixel_representation_element
76
+ end
77
+ return pixels
78
+ end
79
+
80
+ # Packs a pixel value array and returns an encoded binary string. Returns false if the encoding is unsuccesful.
81
+ # The encoding is performed using values defined in the image related data elements of the DObject instance.
82
+ #
83
+ # === Parameters
84
+ #
85
+ # * <tt>pixels</tt> -- An array containing the pixel values that will be encoded.
86
+ # * <tt>stream</tt> -- A Stream instance to be used for encoding the pixels (optional).
87
+ #
88
+ def encode_pixels(pixels, stream=@stream)
89
+ bin = false
90
+ # We need to know what kind of bith depth and integer type the pixel data is saved with:
91
+ bit_depth_element = self["0028,0100"]
92
+ pixel_representation_element = self["0028,0103"]
93
+ if bit_depth_element and pixel_representation_element
94
+ # Number of bytes used per pixel will determine how to pack this:
95
+ case bit_depth_element.value.to_i
96
+ when 8
97
+ bin = stream.encode(pixels, "BY") # Byte/Character/Fixnum (1 byte)
98
+ when 16
99
+ if pixel_representation_element.value.to_i == 1
100
+ bin = stream.encode(pixels, "SS") # Signed short (2 bytes)
101
+ else
102
+ bin = stream.encode(pixels, "US") # Unsigned short (2 bytes)
103
+ end
104
+ when 12
105
+ # 12 BIT SIMPLY NOT WORKING YET!
106
+ # This one is a bit tricky to encode. I havent really given this priority so far as 12 bit image data is rather rare.
107
+ raise "Encoding bit depth 12 is not implemented yet! Please contact the author (or edit the source code)."
108
+ else
109
+ raise "The Bit Depth #{bit_depth} has not received implementation in this procedure yet. Please contact the author (or edit the source code)."
110
+ end
111
+ else
112
+ raise "The Data Element which specifies Bit Depth is missing. Unable to encode pixel data." unless bit_depth_element
113
+ raise "The Data Element which specifies Pixel Representation is missing. Unable to encode pixel data." unless pixel_representation_element
114
+ end
115
+ return bin
116
+ end
117
+
118
+ # Returns the image pixel values in a standard Ruby Array.
119
+ # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
120
+ #
121
+ # === Notes
122
+ #
123
+ # * The returned array does not carry the dimensions of the pixel data: It is put in a one dimensional Array (vector).
124
+ #
125
+ # === Parameters
126
+ #
127
+ # * <tt>options</tt> -- A hash of parameters.
128
+ #
129
+ # === Options
130
+ #
131
+ # * <tt>:rescale</tt> -- Boolean. If set as true, makes the method return processed, rescaled presentation values instead of the original, full pixel range.
132
+ # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray instead of Ruby Array in the rescale process, for faster execution.
133
+ #
134
+ # === Examples
135
+ #
136
+ # # Simply retrieve the pixel data:
137
+ # pixels = obj.get_image
138
+ # # Retrieve the pixel data rescaled to presentation values according to window center/width settings:
139
+ # pixels = obj.get_image(:rescale => true)
140
+ # # Retrieve the rescaled pixel data while using a numerical array in the rescaling process (~2 times faster):
141
+ # pixels = obj.get_image(:rescale => true, :narray => true)
142
+ #
143
+ def get_image(options={})
144
+ if exists?(PIXEL_TAG)
145
+ # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
146
+ if compression?
147
+ pixels = decompress(image_strings.first)
148
+ else
149
+ pixels = decode_pixels(image_strings.first)
150
+ end
151
+ if pixels
152
+ # Remap the image from pixel values to presentation values if the user has requested this:
153
+ if options[:rescale]
154
+ if options[:narray]
155
+ # Use numerical array (faster):
156
+ pixels = process_presentation_values_narray(pixels, -65535, 65535).to_a
157
+ else
158
+ # Use standard Ruby array (slower):
159
+ pixels = process_presentation_values(pixels, -65535, 65535)
160
+ end
161
+ end
162
+ else
163
+ add_msg("Warning: Decompressing pixel values has failed. Array can not be filled.")
164
+ pixels = false
165
+ end
166
+ else
167
+ pixels = nil
168
+ end
169
+ return pixels
170
+ end
171
+
172
+ # Returns a RMagick image, created from the encoded pixel data using the image related data elements in the DObject instance.
173
+ # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
174
+ #
175
+ # === Notes
176
+ #
177
+ # * To call this method the user needs to have loaded the ImageMagick bindings in advance (require 'RMagick').
178
+ #
179
+ # === Parameters
180
+ #
181
+ # * <tt>options</tt> -- A hash of parameters.
182
+ #
183
+ # === Options
184
+ #
185
+ # * <tt>:rescale</tt> -- Boolean. If set as true, makes the method return processed, rescaled presentation values instead of the original, full pixel range.
186
+ # * <tt>:narray</tt> -- Boolean. If set as true, forces the use of NArray instead of RMagick/Ruby Array in the rescale process, for faster execution.
187
+ #
188
+ # === Examples
189
+ #
190
+ # # Retrieve pixel data as RMagick object and display it:
191
+ # image = obj.get_image_magick
192
+ # image.display
193
+ # # Retrieve image object rescaled to presentation values according to window center/width settings:
194
+ # image = obj.get_image_magick(:rescale => true)
195
+ # # Retrieve rescaled image object while using a numerical array in the rescaling process (~2 times faster):
196
+ # images = obj.get_image_magick(:rescale => true, :narray => true)
197
+ #
198
+ def get_image_magick(options={})
199
+ if exists?(PIXEL_TAG)
200
+ unless color?
201
+ # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
202
+ if compression?
203
+ pixels = decompress(image_strings.first)
204
+ else
205
+ pixels = decode_pixels(image_strings.first)
206
+ end
207
+ if pixels
208
+ rows, columns, frames = image_properties
209
+ image = read_image_magick(pixels, columns, rows, frames, options)
210
+ add_msg("Warning: Unfortunately, this method only supports reading the first image frame for 3D pixel data as of now.") if frames > 1
211
+ else
212
+ add_msg("Warning: Decompressing pixel values has failed. RMagick image can not be filled.")
213
+ image = false
214
+ end
215
+ else
216
+ add_msg("The DICOM object contains colored pixel data, which is not supported in this method yet.")
217
+ image = false
218
+ end
219
+ else
220
+ image = nil
221
+ end
222
+ return image
223
+ end
224
+
225
+ # Returns a 3-dimensional NArray object where the array dimensions corresponds to [frames, columns, rows].
226
+ # Returns nil if no pixel data is present, and false if it fails to retrieve pixel data which is present.
227
+ #
228
+ # === Notes
229
+ #
230
+ # * To call this method the user needs to loaded the NArray library in advance (require 'narray').
231
+ #
232
+ # === Parameters
233
+ #
234
+ # * <tt>options</tt> -- A hash of parameters.
235
+ #
236
+ # === Options
237
+ #
238
+ # * <tt>:rescale</tt> -- Boolean. If set as true, makes the method return processed, rescaled presentation values instead of the original, full pixel range.
239
+ #
240
+ # === Examples
241
+ #
242
+ # # Retrieve numerical pixel array:
243
+ # data = obj.get_image_narray
244
+ # # Retrieve numerical pixel array rescaled from the original pixel values to presentation values:
245
+ # data = obj.get_image_narray(:rescale => true)
246
+ #
247
+ def get_image_narray(options={})
248
+ if exists?(PIXEL_TAG)
249
+ unless color?
250
+ # For now we only support returning pixel data of the first frame, if the image is located in multiple pixel data items:
251
+ if compression?
252
+ pixels = decompress(image_strings.first)
253
+ else
254
+ pixels = decode_pixels(image_strings.first)
255
+ end
256
+ if pixels
257
+ # Decode the pixel values, then import to NArray and give it the proper shape:
258
+ rows, columns, frames = image_properties
259
+ pixel_data = NArray.to_na(pixels).reshape!(frames, columns, rows)
260
+ # Remap the image from pixel values to presentation values if the user has requested this:
261
+ pixel_data = process_presentation_values_narray(pixel_data, -65535, 65535) if options[:rescale]
262
+ else
263
+ add_msg("Warning: Decompressing pixel values has failed. Numerical array can not be filled.")
264
+ pixel_data = false
265
+ end
266
+ else
267
+ add_msg("The DICOM object contains colored pixel data, which is not supported in this method yet.")
268
+ pixel_data = false
269
+ end
270
+ else
271
+ pixel_data = nil
272
+ end
273
+ return pixel_data
274
+ end
275
+
276
+ # Reads a binary string from a specified file and writes it to the value field of the pixel data element (7FE0,0010).
277
+ #
278
+ # === Parameters
279
+ #
280
+ # * <tt>file</tt> -- A string which specifies the path of the file containing pixel data.
281
+ #
282
+ # === Examples
283
+ #
284
+ # obj.image_from_file("custom_image.bin")
285
+ #
286
+ def image_from_file(file)
287
+ # Read and extract:
288
+ f = File.new(file, "rb")
289
+ bin = f.read(f.stat.size)
290
+ if bin.length > 0
291
+ # Write the binary data to the Pixel Data Element:
292
+ set_pixels(bin)
293
+ else
294
+ add_msg("Notice: The specified file (#{file}) is empty. Nothing to store.")
295
+ end
296
+ end
297
+
298
+ # Returns data related to the shape of the pixel data. The data is returned as three integers: rows, columns & number of frames.
299
+ #
300
+ # === Examples
301
+ #
302
+ # rows, cols, frames = obj.image_properties
303
+ #
304
+ def image_properties
305
+ row_element = self["0028,0010"]
306
+ column_element = self["0028,0011"]
307
+ frames = (self["0028,0008"].is_a?(DataElement) == true ? self["0028,0008"].value.to_i : 1)
308
+ unless row_element and column_element
309
+ raise "The Data Element which specifies Rows is missing. Unable to gather enough information to constuct an image." unless row_element
310
+ raise "The Data Element which specifies Columns is missing. Unable to gather enough information to constuct an image." unless column_element
311
+ else
312
+ return row_element.value, column_element.value, frames
313
+ end
314
+ end
315
+
316
+ # Dumps the binary content of the Pixel Data element to a file.
317
+ #
318
+ # === Parameters
319
+ #
320
+ # * <tt>file</tt> -- A string which specifies the file path to use when dumping the pixel data.
321
+ #
322
+ # === Examples
323
+ #
324
+ # obj.image_to_file("exported_image.bin")
325
+ #
326
+ def image_to_file(file)
327
+ # Get the binary image strings and dump them to file:
328
+ images = image_strings
329
+ images.each_index do |i|
330
+ if images.length == 1
331
+ f = File.new(file, "wb")
332
+ else
333
+ f = File.new("#{file}_#{i}", "wb")
334
+ end
335
+ f.write(images[i])
336
+ f.close
337
+ end
338
+ end
339
+
340
+ # Returns the pixel data binary string(s) of this parent in an array.
341
+ # If no pixel data is present, returns an empty array.
342
+ #
343
+ def image_strings
344
+ # Pixel data may be a single binary string in the pixel data element,
345
+ # or located in several encapsulated item elements:
346
+ pixel_element = self[PIXEL_TAG]
347
+ strings = Array.new
348
+ if pixel_element.is_a?(DataElement)
349
+ strings << pixel_element.bin
350
+ elsif pixel_element.is_a?(Sequence)
351
+ pixel_items = pixel_element.children.first.children
352
+ pixel_items.each do |item|
353
+ strings << item.bin
354
+ end
355
+ end
356
+ return strings
357
+ end
358
+
359
+ # Removes all Sequence elements from the DObject or Item instance.
360
+ #
361
+ def remove_sequences
362
+ @tags.each_value do |element|
363
+ remove(element.tag) if element.is_a?(Sequence)
364
+ end
365
+ end
366
+
367
+ # Encodes pixel data from a Ruby Array and writes it to the pixel data element (7FE0,0010).
368
+ #
369
+ # === Parameters
370
+ #
371
+ # * <tt>pixels</tt> -- An array of pixel values (integers).
372
+ #
373
+ def set_image(pixels)
374
+ if pixels.is_a?(Array)
375
+ # Encode the pixel data:
376
+ bin = encode_pixels(pixels)
377
+ # Write the binary data to the Pixel Data Element:
378
+ set_pixels(bin)
379
+ else
380
+ raise "Unexpected object type (#{pixels.class}) for the pixels parameter. Array was expected."
381
+ end
382
+ end
383
+
384
+ # Encodes pixel data from a RMagick image object and writes it to the pixel data element (7FE0,0010).
385
+ #
386
+ # === Restrictions
387
+ #
388
+ # If pixel value rescaling is wanted, BOTH <b>:min</b> and <b>:max</b> must be set!
389
+ #
390
+ # Because of rescaling when importing pixel values to a RMagick object, and the possible
391
+ # difference between presentation values and pixel values, the use of set_image_magick() may
392
+ # result in pixel data that differs from what is expected. This method must be used with care!
393
+ #
394
+ # === Options
395
+ #
396
+ # * <tt>:max</tt> -- Fixnum. Pixel values will be rescaled using this as the new maximum value.
397
+ # * <tt>:min</tt> -- Fixnum. Pixel values will be rescaled using this as the new minimum value.
398
+ #
399
+ # === Examples
400
+ #
401
+ # # Encode an image object while requesting that only a specific pixel value range is used:
402
+ # obj.set_image_magick(my_image, :min => -2000, :max => 3000)
403
+ #
404
+ def set_image_magick(magick_image, options={})
405
+ # Export the RMagick object to a standard Ruby Array:
406
+ pixels = magick_image.export_pixels(x=0, y=0, columns=magick_image.columns, rows=magick_image.rows, map="I")
407
+ # Rescale pixel values?
408
+ if options[:min] and options[:max]
409
+ p_min = pixels.min
410
+ p_max = pixels.max
411
+ if p_min != options[:min] or p_max != options[:max]
412
+ wanted_range = options[:max] - options[:min]
413
+ factor = wanted_range.to_f/(pixels.max - pixels.min).to_f
414
+ offset = pixels.min - options[:min]
415
+ pixels.collect!{|x| ((x*factor)-offset).round}
416
+ end
417
+ end
418
+ # Encode and write to the Pixel Data Element:
419
+ set_image(pixels)
420
+ end
421
+
422
+ # Encodes pixel data from a NArray and writes it to the pixel data element (7FE0,0010).
423
+ #
424
+ # === Restrictions
425
+ #
426
+ # * If pixel value rescaling is wanted, BOTH <b>:min</b> and <b>:max</b> must be set!
427
+ #
428
+ # === Options
429
+ #
430
+ # * <tt>:max</tt> -- Fixnum. Pixel values will be rescaled using this as the new maximum value.
431
+ # * <tt>:min</tt> -- Fixnum. Pixel values will be rescaled using this as the new minimum value.
432
+ #
433
+ # === Examples
434
+ #
435
+ # # Encode a numerical pixel array while requesting that only a specific pixel value range is used:
436
+ # obj.set_image_narray(pixels, :min => -2000, :max => 3000)
437
+ #
438
+ def set_image_narray(narray, options={})
439
+ # Rescale pixel values?
440
+ if options[:min] and options[:max]
441
+ n_min = narray.min
442
+ n_max = narray.max
443
+ if n_min != options[:min] or n_max != options[:max]
444
+ wanted_range = options[:max] - options[:min]
445
+ factor = wanted_range.to_f/(n_max - n_min).to_f
446
+ offset = n_min - options[:min]
447
+ narray = narray*factor-offset
448
+ end
449
+ end
450
+ # Export the NArray object to a standard Ruby Array:
451
+ pixels = narray.to_a.flatten!
452
+ # Encode and write to the Pixel Data Element:
453
+ set_image(pixels)
454
+ end
455
+
456
+
457
+ # Following methods are private:
458
+ private
459
+
460
+
461
+ # Attempts to decompress compressed pixel data.
462
+ # If successful, returns the pixel data in a Ruby Array. If not, returns false.
463
+ #
464
+ # === Notes
465
+ #
466
+ # The method tries to use RMagick of unpacking, but it seems that ImageMagick is not able to handle most of the
467
+ # compressed image variants used in the DICOM standard. To get a more robust implementation which is able to handle
468
+ # most types of compressed DICOM files, something else is needed.
469
+ #
470
+ # Probably a good candidate to use is the PVRG-JPEG library, which seems to be able to handle everything that is jpeg.
471
+ # It exists in the Ubuntu repositories, where it can be installed and run through terminal. For source code, and some
472
+ # additional information, check this link: http://www.panix.com/~eli/jpeg/
473
+ #
474
+ # Another idea would be to study how other open source libraries, like GDCM handle these files.
475
+ #
476
+ # === Parameters
477
+ #
478
+ # * <tt>string</tt> -- A binary string which has been extracted from the pixel data element of the DICOM object.
479
+ #
480
+ def decompress(string)
481
+ pixels = false
482
+ # We attempt to decompress the pixels using RMagick (ImageMagick):
483
+ begin
484
+ image = Magick::Image.from_blob(string)
485
+ if color?
486
+ pixels = image.export_pixels(0, 0, image.columns, image.rows, "RGB")
487
+ else
488
+ pixels = image.export_pixels(0, 0, image.columns, image.rows, "I")
489
+ end
490
+ rescue
491
+ add_msg("Warning: Decoding the compressed image data from this DICOM object was NOT successful!")
492
+ end
493
+ return image
494
+ end
495
+
496
+
497
+ # Converts original pixel data values to presentation values, which are returned.
498
+ #
499
+ # === Parameters
500
+ #
501
+ # * <tt>pixel_data</tt> -- An array of pixel values (integers).
502
+ # * <tt>min_allowed</tt> -- Fixnum. The minimum value allowed for the returned pixels.
503
+ # * <tt>max_allowed</tt> -- Fixnum. The maximum value allowed for the returned pixels.
504
+ #
505
+ def process_presentation_values(pixel_data, min_allowed, max_allowed)
506
+ # Process pixel data for presentation according to the image information in the DICOM object:
507
+ center, width, intercept, slope = window_level_values
508
+ # PixelOutput = slope * pixel_values + intercept
509
+ if intercept != 0 or slope != 1
510
+ pixel_data.collect!{|x| (slope * x) + intercept}
511
+ end
512
+ # Contrast enhancement by black and white thresholding:
513
+ if center and width
514
+ low = center - width/2
515
+ high = center + width/2
516
+ pixel_data.each_index do |i|
517
+ if pixel_data[i] < low
518
+ pixel_data[i] = low
519
+ elsif pixel_data[i] > high
520
+ pixel_data[i] = high
521
+ end
522
+ end
523
+ end
524
+ # Need to introduce an offset?
525
+ min_pixel_value = pixel_data.min
526
+ if min_allowed
527
+ if min_pixel_value < min_allowed
528
+ offset = min_pixel_value.abs
529
+ pixel_data.collect!{|x| x + offset}
530
+ end
531
+ end
532
+ # Downscale pixel range?
533
+ max_pixel_value = pixel_data.max
534
+ if max_allowed
535
+ if max_pixel_value > max_allowed
536
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
537
+ pixel_data.collect!{|x| x / factor}
538
+ end
539
+ end
540
+ return pixel_data
541
+ end
542
+
543
+ # Converts original pixel data values to a RMagick image object containing presentation values.
544
+ # Returns the RMagick image object.
545
+ #
546
+ # === Parameters
547
+ #
548
+ # * <tt>pixel_data</tt> -- An array of pixel values (integers).
549
+ # * <tt>max_allowed</tt> -- Fixnum. The maximum value allowed for the returned pixels.
550
+ # * <tt>columns</tt> -- Fixnum. Number of columns in the image to be created.
551
+ # * <tt>rows</tt> -- Fixnum. Number of rows in the image to be created.
552
+ #
553
+ def process_presentation_values_magick(pixel_data, max_allowed, columns, rows)
554
+ # Process pixel data for presentation according to the image information in the DICOM object:
555
+ center, width, intercept, slope = window_level_values
556
+ # PixelOutput = slope * pixel_values + intercept
557
+ if intercept != 0 or slope != 1
558
+ pixel_data.collect!{|x| (slope * x) + intercept}
559
+ end
560
+ # Need to introduce an offset?
561
+ offset = 0
562
+ min_pixel_value = pixel_data.min
563
+ if min_pixel_value < 0
564
+ offset = min_pixel_value.abs
565
+ pixel_data.collect!{|x| x + offset}
566
+ end
567
+ # Downscale pixel range?
568
+ factor = 1
569
+ max_pixel_value = pixel_data.max
570
+ if max_allowed
571
+ if max_pixel_value > max_allowed
572
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
573
+ pixel_data.collect!{|x| x / factor}
574
+ end
575
+ end
576
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
577
+ # Contrast enhancement by black and white thresholding:
578
+ if center and width
579
+ low = (center - width/2 + offset) / factor
580
+ high = (center + width/2 + offset) / factor
581
+ image = image.level(low, high)
582
+ end
583
+ return image
584
+ end
585
+
586
+ # Converts original pixel data values to presentation values, using the efficient NArray library.
587
+ #
588
+ # === Notes
589
+ #
590
+ # * If a Ruby Array is supplied, the method returns a one-dimensional NArray object (i.e. no columns & rows).
591
+ # * If a NArray is supplied, the NArray is returned with its original dimensions.
592
+ #
593
+ # === Parameters
594
+ #
595
+ # * <tt>pixel_data</tt> -- An Array/NArray of pixel values (integers).
596
+ # * <tt>min_allowed</tt> -- Fixnum. The minimum value allowed for the returned pixels.
597
+ # * <tt>max_allowed</tt> -- Fixnum. The maximum value allowed for the returned pixels.
598
+ #
599
+ def process_presentation_values_narray(pixel_data, min_allowed, max_allowed)
600
+ # Process pixel data for presentation according to the image information in the DICOM object:
601
+ center, width, intercept, slope = window_level_values
602
+ # Need to convert to NArray?
603
+ if pixel_data.is_a?(Array)
604
+ n_arr = NArray.to_na(pixel_data)
605
+ else
606
+ n_arr = pixel_data
607
+ end
608
+ # Rescale:
609
+ # PixelOutput = slope * pixel_values + intercept
610
+ if intercept != 0 or slope != 1
611
+ n_arr = slope * n_arr + intercept
612
+ end
613
+ # Contrast enhancement by black and white thresholding:
614
+ if center and width
615
+ low = center - width/2
616
+ high = center + width/2
617
+ n_arr[n_arr < low] = low
618
+ n_arr[n_arr > high] = high
619
+ end
620
+ # Need to introduce an offset?
621
+ min_pixel_value = n_arr.min
622
+ if min_allowed
623
+ if min_pixel_value < min_allowed
624
+ offset = min_pixel_value.abs
625
+ n_arr = n_arr + offset
626
+ end
627
+ end
628
+ # Downscale pixel range?
629
+ max_pixel_value = n_arr.max
630
+ if max_allowed
631
+ if max_pixel_value > max_allowed
632
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
633
+ n_arr = n_arr / factor
634
+ end
635
+ end
636
+ return n_arr
637
+ end
638
+
639
+ # Creates a RMagick image object from the specified pixel value array, and returns this image.
640
+ #
641
+ # === Restrictions
642
+ #
643
+ # Reading compressed data has been removed for now as it never seemed to work on any of the samples.
644
+ # Hopefully, a more robust solution will be found and included in a future version.
645
+ # Tests with RMagick can be tried with something like:
646
+ # image = Magick::Image.from_blob(element.bin)
647
+ #
648
+ def read_image_magick(pixel_data, columns, rows, frames, options={})
649
+ # Remap the image from pixel values to presentation values if the user has requested this:
650
+ if options[:rescale] == true
651
+ # What tools will be used to process the pixel presentation values?
652
+ if options[:narray] == true
653
+ # Use numerical array (fast):
654
+ pixel_data = process_presentation_values_narray(pixel_data, 0, Magick::QuantumRange).to_a
655
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
656
+ else
657
+ # Use a combination of ruby array and RMagick processing:
658
+ image = process_presentation_values_magick(pixel_data, Magick::QuantumRange, columns, rows)
659
+ end
660
+ else
661
+ # Load original pixel values to a RMagick object:
662
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
663
+ end
664
+ return image
665
+ end
666
+
667
+ # Transfers a pre-encoded binary string to the pixel data element, either by overwriting the existing
668
+ # element value, or creating a new one DataElement.
669
+ #
670
+ def set_pixels(bin)
671
+ if self.exists?(PIXEL_TAG)
672
+ # Update existing Data Element:
673
+ self[PIXEL_TAG].bin = bin
674
+ else
675
+ # Create new Data Element:
676
+ pixel_element = DataElement.new(PIXEL_TAG, bin, :encoded => true, :parent => self)
677
+ end
678
+ end
679
+
680
+ # Gathers and returns the window level values needed to convert the original pixel values to presentation values.
681
+ #
682
+ # === Notes
683
+ #
684
+ # If some of these values are missing in the DObject instance, default values are used instead
685
+ # for intercept and slope, while center and width are set to nil. No errors are raised.
686
+ #
687
+ def window_level_values
688
+ center = (self["0028,1050"].is_a?(DataElement) == true ? self["0028,1050"].value.to_i : nil)
689
+ width = (self["0028,1051"].is_a?(DataElement) == true ? self["0028,1051"].value.to_i : nil)
690
+ intercept = (self["0028,1052"].is_a?(DataElement) == true ? self["0028,1052"].value.to_i : 0)
691
+ slope = (self["0028,1053"].is_a?(DataElement) == true ? self["0028,1053"].value.to_i : 1)
692
+ return center, width, intercept, slope
693
+ end
694
+
695
+ end
696
+ end