dicom 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +55 -0
- data/README +51 -29
- data/init.rb +1 -0
- data/lib/dicom.rb +35 -21
- data/lib/dicom/{Anonymizer.rb → anonymizer.rb} +178 -80
- data/lib/dicom/constants.rb +121 -0
- data/lib/dicom/d_client.rb +888 -0
- data/lib/dicom/d_library.rb +208 -0
- data/lib/dicom/d_object.rb +424 -0
- data/lib/dicom/d_read.rb +433 -0
- data/lib/dicom/d_server.rb +397 -0
- data/lib/dicom/d_write.rb +420 -0
- data/lib/dicom/data_element.rb +175 -0
- data/lib/dicom/{Dictionary.rb → dictionary.rb} +390 -398
- data/lib/dicom/elements.rb +82 -0
- data/lib/dicom/file_handler.rb +116 -0
- data/lib/dicom/item.rb +87 -0
- data/lib/dicom/{Link.rb → link.rb} +749 -388
- data/lib/dicom/ruby_extensions.rb +44 -35
- data/lib/dicom/sequence.rb +62 -0
- data/lib/dicom/stream.rb +493 -0
- data/lib/dicom/super_item.rb +696 -0
- data/lib/dicom/super_parent.rb +615 -0
- metadata +25 -18
- data/DOCUMENTATION +0 -469
- data/lib/dicom/DClient.rb +0 -584
- data/lib/dicom/DLibrary.rb +0 -194
- data/lib/dicom/DObject.rb +0 -1579
- data/lib/dicom/DRead.rb +0 -532
- data/lib/dicom/DServer.rb +0 -304
- data/lib/dicom/DWrite.rb +0 -410
- data/lib/dicom/FileHandler.rb +0 -50
- data/lib/dicom/Stream.rb +0 -354
@@ -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
|