dicom 0.7 → 0.8

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