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.
- 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
|