dicom 0.6.1 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- # Copyright 2008-2009 Christoffer Lervag
1
+ # Copyright 2008-2010 Christoffer Lervag
2
2
 
3
3
  module DICOM
4
4
  # Class which holds the methods that interact with the DICOM dictionary.
@@ -30,7 +30,7 @@ module DICOM
30
30
  def check_ts_validity(uid)
31
31
  result = false
32
32
  value = @uid[uid.rstrip]
33
- if value != nil
33
+ if value
34
34
  if value[1] == "Transfer Syntax"
35
35
  # Proved valid:
36
36
  result = true
@@ -45,7 +45,7 @@ module DICOM
45
45
  result = false
46
46
  if uid
47
47
  value = @uid[uid.rstrip]
48
- if value != nil
48
+ if value
49
49
  if value[1] == "Transfer Syntax" and not value[0].include?("Endian")
50
50
  # It seems we have compression:
51
51
  result = true
@@ -56,40 +56,62 @@ module DICOM
56
56
  end
57
57
 
58
58
 
59
- # Returns data element name and value representation from the dictionary if the data element
60
- # is recognized, else it returns "Unknown Name" and "UN".
59
+ # Returns data element name and value representation from the dictionary unless the data element
60
+ # is private. If a non-private tag is not recognized, "Unknown Name" and "UN" is returned.
61
61
  def get_name_vr(tag)
62
- values = @tags[tag]
63
- if values != nil
64
- name = values[1]
65
- vr = values[0][0]
62
+ if tag.private? and tag[5..8] != "0000"
63
+ name = "Private"
64
+ vr = "UN"
66
65
  else
67
- # For the tags that are not recognised, we need to do some additional testing to see if it is one of the special cases:
68
- # Split tag in group and element:
69
- group = tag[0..3]
70
- element = tag[5..8]
71
- if element == "0000"
72
- # Group length:
73
- name = "Group Length"
74
- vr = "UL"
75
- elsif tag[0..6] == "0020,31"
76
- # Source Image ID's: (Retired)
77
- values = @tags["0020,31xx"]
66
+ # Check the dictionary:
67
+ values = @tags[tag]
68
+ if values
78
69
  name = values[1]
79
70
  vr = values[0][0]
80
- elsif tag[0..1] == "50" or tag[0..1] == "60"
81
- # Group 50xx (retired) and 60xx:
82
- new_tag = tag[0..1]+"xx"+tag[4..8]
83
- values = @tags[new_tag]
84
- if values != nil
71
+ else
72
+ # For the tags that are not recognised, we need to do some additional testing to see if it is one of the special cases:
73
+ # Split tag in group and element:
74
+ group = tag[0..3]
75
+ element = tag[5..8]
76
+ if element == "0000"
77
+ # Group length:
78
+ name = "Group Length"
79
+ vr = "UL"
80
+ elsif tag[0..6] == "0020,31"
81
+ # Source Image ID's (Retired):
82
+ values = @tags["0020,31xx"]
85
83
  name = values[1]
86
84
  vr = values[0][0]
85
+ elsif group == "1000" and element =~ /\A\h{3}[0-5]\z/
86
+ # Group 1000,xxx[0-5] (Retired):
87
+ new_tag = group + "xx" + element[3..3]
88
+ values = @tags[new_tag]
89
+ elsif group == "1010"
90
+ # Group 1010,xxxx (Retired):
91
+ new_tag = group + "xxxx"
92
+ values = @tags[new_tag]
93
+ elsif tag[0..1] == "50" or tag[0..1] == "60"
94
+ # Group 50xx (Retired) and 60xx:
95
+ new_tag = tag[0..1]+"xx"+tag[4..8]
96
+ values = @tags[new_tag]
97
+ if values
98
+ name = values[1]
99
+ vr = values[0][0]
100
+ end
101
+ elsif tag[0..1] == "7F" and tag[5..6] == "00"
102
+ # Group 7Fxx,00[10,11,20,30,40] (Retired):
103
+ new_tag = tag[0..1]+"xx"+tag[4..8]
104
+ values = @tags[new_tag]
105
+ if values
106
+ name = values[1]
107
+ vr = values[0][0]
108
+ end
109
+ end
110
+ # If none of the above checks yielded a result, the tag is unknown:
111
+ unless name
112
+ name = "Unknown Name"
113
+ vr = "UN"
87
114
  end
88
- end
89
- # If none of the above checks yielded a result, the tag is unknown:
90
- if name == nil
91
- name = "Unknown Name"
92
- vr = "UN"
93
115
  end
94
116
  end
95
117
  return [name,vr]
@@ -125,7 +147,7 @@ module DICOM
125
147
  def get_uid(uid)
126
148
  value = @uid[uid.rstrip]
127
149
  # Fetch the name of this UID:
128
- if value != nil
150
+ if value
129
151
  name = value[0]
130
152
  else
131
153
  name = "Unknown UID!"
@@ -0,0 +1,1579 @@
1
+ # Copyright 2008-2010 Christoffer Lervag
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ #
16
+ #--------------------------------------------------------------------------------------------------
17
+
18
+ # TODO:
19
+ # -The retrieve file network functionality (get_image in DClient class) has not been tested.
20
+ # -Make the networking code more intelligent in its handling of unexpected network communication.
21
+ # -Full support for compressed image data.
22
+ # -Read/Write 12 bit image data.
23
+ # -Support for color image data.
24
+ # -Complete support for Big endian (Everything but signed short and signed long has been implemented).
25
+ # -Complete support for multiple frame image data to NArray and RMagick objects (partial support already featured).
26
+ # -Image handling does not take into consideration DICOM tags which specify orientation, samples per pixel and photometric interpretation.
27
+ # -More robust and flexible options for reorienting extracted pixel arrays?
28
+ # -Could the usage of arrays in DObject be replaced with something better, or at least improved upon, to give cleaner code and more efficient execution?
29
+ # -A curious observation: Loading the DLibrary is exceptionally slow on my Ruby 1.9.1 install: 0.4 seconds versus ~0.01 seconds on my Ruby 1.8.7 install!
30
+
31
+ module DICOM
32
+
33
+ # Class for interacting with the DICOM object.
34
+ class DObject
35
+
36
+ attr_reader :read_success, :write_success, :modality, :errors, :segments,
37
+ :names, :tags, :vr, :lengths, :values, :bin, :levels
38
+
39
+ # Initialize the DObject instance.
40
+ def initialize(string=nil, options={})
41
+ # Process option values, setting defaults for the ones that are not specified:
42
+ @verbose = options[:verbose]
43
+ segment_size = options[:segment_size]
44
+ bin = options[:bin]
45
+ syntax = options[:syntax]
46
+ # Default verbosity is true:
47
+ @verbose = true if @verbose == nil
48
+
49
+ # Initialize variables that will be used for the DICOM object:
50
+ @names = Array.new
51
+ @tags = Array.new
52
+ @vr = Array.new
53
+ @lengths = Array.new
54
+ @values = Array.new
55
+ @bin = Array.new
56
+ @levels = Array.new
57
+ # Array that will holde any messages generated while reading the DICOM file:
58
+ @errors = Array.new
59
+ # Array to keep track of sequences/structure of the dicom elements:
60
+ @sequence = Array.new
61
+ # Structural information (default values):
62
+ @compression = false
63
+ @color = false
64
+ @explicit = true
65
+ @file_endian = false
66
+ # Information about the DICOM object:
67
+ @modality = nil
68
+ # Control variables:
69
+ @read_success = false
70
+ # Initialize a Stream instance which is used for encoding/decoding:
71
+ @stream = Stream.new(nil, @file_endian, @explicit)
72
+
73
+ # If a (valid) file name string is supplied, call the method to read the DICOM file:
74
+ if string.is_a?(String) and string != ""
75
+ @file = string
76
+ read(string, :bin => bin, :segment_size => segment_size, :syntax => syntax)
77
+ end
78
+ end # of initialize
79
+
80
+
81
+ # Returns a DICOM object by reading the file specified.
82
+ # This is accomplished by initliazing the DRead class, which loads DICOM information to arrays.
83
+ # For the time being, this method is called automatically when initializing the DObject class,
84
+ # but in the future, when write support is added, this method may have to be called manually.
85
+ def read(string, options = {})
86
+ r = DRead.new(string, :sys_endian => @sys_endian, :bin => options[:bin], :syntax => options[:syntax])
87
+ # Store the data to the instance variables if the readout was a success:
88
+ if r.success
89
+ @read_success = true
90
+ @names = r.names
91
+ @tags = r.tags
92
+ @vr = r.vr
93
+ @lengths = r.lengths
94
+ @values = r.values
95
+ @bin = r.bin
96
+ @levels = r.levels
97
+ @explicit = r.explicit
98
+ @file_endian = r.file_endian
99
+ # Update Stream instance with settings from this DICOM file:
100
+ @stream.set_endian(@file_endian)
101
+ @stream.explicit = @explicit
102
+ # Update status variables for this object:
103
+ check_properties
104
+ # Set the modality of the DICOM object:
105
+ set_modality
106
+ else
107
+ @read_success = false
108
+ end
109
+ # Check if a partial extraction has been requested (used for network communication purposes)
110
+ if options[:segment_size]
111
+ @segments = r.extract_segments(options[:segment_size])
112
+ end
113
+ # If any messages has been recorded, send these to the message handling method:
114
+ add_msg(r.msg) if r.msg.length > 0
115
+ end
116
+
117
+
118
+ # Transfers necessary information from the DObject to the DWrite class, which
119
+ # will attempt to write this information to a valid DICOM file.
120
+ def write(file_name, transfer_syntax = nil)
121
+ w = set_write_object(file_name, transfer_syntax)
122
+ w.write
123
+ # Write process succesful?
124
+ @write_success = w.success
125
+ # If any messages has been recorded, send these to the message handling method:
126
+ add_msg(w.msg) if w.msg.length > 0
127
+ end
128
+
129
+
130
+ # Encodes the DICOM object into a series of binary string segments with a specified maximum length.
131
+ def encode_segments(size)
132
+ w = set_write_object
133
+ @segments = w.encode_segments(size)
134
+ # Write process succesful?
135
+ @write_success = w.success
136
+ # If any messages has been recorded, send these to the message handling method:
137
+ add_msg(w.msg) if w.msg.length > 0
138
+ end
139
+
140
+
141
+ #################################################
142
+ # START OF METHODS FOR READING INFORMATION FROM DICOM OBJECT:
143
+ #################################################
144
+
145
+
146
+ # Returns the image pixel data in a standard Ruby array.
147
+ # Returns false if it fails to retrieve image data.
148
+ # The array does not carry the dimensions of the pixel data, it will be a one dimensional array (vector).
149
+ # :rescale => true - Return processed, rescaled presentation values instead of the original, full pixel range.
150
+ def get_image(options={})
151
+ pixel_data = false
152
+ pixel_element_pos = get_image_pos
153
+ # A hack for the special case (some MR files), where two images are stored (one is a smaller thumbnail image):
154
+ pixel_element_pos = [pixel_element_pos.last] if pixel_element_pos.length > 1 and get_value("0028,0011", :array => true).length > 1
155
+ # For now we only support returning pixel data if the image is located in a single pixel data element:
156
+ if pixel_element_pos.length == 1
157
+ # All of the pixel data is located in one element:
158
+ pixel_data = get_pixels(pixel_element_pos[0])
159
+ else
160
+ add_msg("Warning: Method get_image() does not currently support returning pixel data from encapsulated images!")
161
+ end
162
+ # Remap the image from pixel values to presentation values if the user has requested this:
163
+ if options[:rescale] == true and pixel_data
164
+ # Process pixel data for presentation according to the image information in the DICOM object:
165
+ center, width, intercept, slope = window_level_values
166
+ if options[:narray] == true
167
+ # Use numerical array (faster):
168
+ pixel_data = process_presentation_values_narray(pixel_data, center, width, slope, intercept, -65535, 65535).to_a
169
+ else
170
+ # Use standard Ruby array (slower):
171
+ pixel_data = process_presentation_values(pixel_data, center, width, slope, intercept, -65535, 65535)
172
+ end
173
+ end
174
+ return pixel_data
175
+ end
176
+
177
+
178
+ # Returns a 3d NArray object where the array dimensions corresponds to [frames, columns, rows].
179
+ # Returns false if it fails to retrieve image data.
180
+ # To call this method the user needs to loaded the NArray library in advance (require 'narray').
181
+ # Options:
182
+ # :rescale => true - Return processed, rescaled presentation values instead of the original, full pixel range.
183
+ def get_image_narray(options={})
184
+ # Are we able to make a pixel array?
185
+ if @compression == nil
186
+ add_msg("It seems pixel data is not present in this DICOM object.")
187
+ return false
188
+ elsif @compression == true
189
+ add_msg("Reading compressed data to a NArray object not supported yet.")
190
+ return false
191
+ elsif @color
192
+ add_msg("Warning: Unpacking color pixel data is not supported yet for this method.")
193
+ return false
194
+ end
195
+ # Gather information about the dimensions of the pixel data:
196
+ rows = get_value("0028,0010", :array => true)[0]
197
+ columns = get_value("0028,0011", :array => true)[0]
198
+ frames = get_frames
199
+ pixel_element_pos = get_image_pos
200
+ # A hack for the special case (some MR files), where two images are stored (one is a smaller thumbnail image):
201
+ pixel_element_pos = [pixel_element_pos.last] if pixel_element_pos.length > 1 and get_value("0028,0011", :array => true).length > 1
202
+ # Creating a NArray object using int to make sure we have the necessary range for our numbers:
203
+ pixel_data = NArray.int(frames,columns,rows)
204
+ pixel_frame = NArray.int(columns,rows)
205
+ # Handling of pixel data will depend on whether we have one or more frames,
206
+ # and if it is located in one or more data elements:
207
+ if pixel_element_pos.length == 1
208
+ # All of the pixel data is located in one element:
209
+ pixel_array = get_pixels(pixel_element_pos[0])
210
+ frames.times do |i|
211
+ (columns*rows).times do |j|
212
+ pixel_frame[j] = pixel_array[j+i*columns*rows]
213
+ end
214
+ pixel_data[i,true,true] = pixel_frame
215
+ end
216
+ else
217
+ # Pixel data is encapsulated in items:
218
+ frames.times do |i|
219
+ pixel_array = get_pixels(pixel_element_pos[i])
220
+ (columns*rows).times do |j|
221
+ pixel_frame[j] = pixel_array[j+i*columns*rows]
222
+ end
223
+ pixel_data[i,true,true] = pixel_frame
224
+ end
225
+ end
226
+ # Remap the image from pixel values to presentation values if the user has requested this:
227
+ if options[:rescale] == true
228
+ # Process pixel data for presentation according to the image information in the DICOM object:
229
+ center, width, intercept, slope = window_level_values
230
+ pixel_data = process_presentation_values_narray(pixel_data, center, width, slope, intercept, -65535, 65535)
231
+ end
232
+ return pixel_data
233
+ end # of get_image_narray
234
+
235
+
236
+ # Returns an array of RMagick image objects, where the size of the array corresponds to the number of frames in the image data.
237
+ # Returns false if it fails to retrieve image data.
238
+ # To call this method the user needs to have loaded the ImageMagick library in advance (require 'RMagick').
239
+ # Options:
240
+ # :rescale => true - Return processed, rescaled presentation values instead of the original, full pixel range.
241
+ # :narray => true - Use NArray when rescaling pixel values (faster than using RMagick/Ruby array).
242
+ def get_image_magick(options={})
243
+ # Are we able to make an image?
244
+ if @compression == nil
245
+ add_msg("Notice: It seems pixel data is not present in this DICOM object.")
246
+ return false
247
+ elsif @color
248
+ add_msg("Warning: Unpacking color pixel data is not supported yet for this method.")
249
+ return false
250
+ end
251
+ # Gather information about the dimensions of the image data:
252
+ rows = get_value("0028,0010", :array => true)[0]
253
+ columns = get_value("0028,0011", :array => true)[0]
254
+ frames = get_frames
255
+ pixel_element_pos = get_image_pos
256
+ # Array that will hold the RMagick image objects, one object for each frame:
257
+ images = Array.new(frames)
258
+ # A hack for the special case (some MR files), where two images are stored (one is a smaller thumbnail image):
259
+ pixel_element_pos = [pixel_element_pos.last] if pixel_element_pos.length > 1 and get_value("0028,0011", :array => true).length > 1
260
+ # Handling of pixel data will depend on whether we have one or more frames,
261
+ # and if it is located in one or more data elements:
262
+ if pixel_element_pos.length == 1
263
+ # All of the pixel data is located in one data element:
264
+ if frames > 1
265
+ add_msg("Unfortunately, this method only supports reading the first image frame for 3D pixel data as of now.")
266
+ end
267
+ images = read_image_magick(pixel_element_pos[0], columns, rows, frames, options)
268
+ images = [images] unless images.is_a?(Array)
269
+ else
270
+ # Image data is encapsulated in items:
271
+ frames.times do |i|
272
+ image = read_image_magick(pixel_element_pos[i], columns, rows, 1, options)
273
+ images[i] = image
274
+ end
275
+ end
276
+ return images
277
+ end # of get_image_magick
278
+
279
+
280
+ # Returns the number of frames present in the image data in the DICOM file.
281
+ def get_frames
282
+ frames = get_value("0028,0008", :silent => true)
283
+ # If the DICOM object does not specify the number of frames explicitly, assume 1 image frame:
284
+ frames = 1 unless frames
285
+ return frames.to_i
286
+ end
287
+
288
+
289
+ # Returns the index(es) of the element(s) that contain image data.
290
+ def get_image_pos
291
+ image_element_pos = get_pos("7FE0,0010")
292
+ item_pos = get_pos("FFFE,E000")
293
+ # Proceed only if an image element actually exists:
294
+ if image_element_pos.length == 0
295
+ return false
296
+ else
297
+ # Check if we have item elements:
298
+ if item_pos.length == 0
299
+ return image_element_pos
300
+ else
301
+ # Extract item positions that occur after the image element position:
302
+ late_item_pos = item_pos.select {|item| image_element_pos[0] < item}
303
+ # Check if there are items appearing after the image element.
304
+ if late_item_pos.length == 0
305
+ # None occured after the image element position:
306
+ return image_element_pos
307
+ else
308
+ # Determine which of these late item elements contain image data.
309
+ # Usually, there are frames+1 late items, and all except
310
+ # the first item contain an image frame:
311
+ frames = get_frames
312
+ if frames != false # note: function get_frames will never return false
313
+ if late_item_pos.length == frames.to_i+1
314
+ return late_item_pos[1..late_item_pos.length-1]
315
+ else
316
+ add_msg("Warning: Unexpected behaviour in DICOM file for method get_image_pos. Expected number of image data items not equal to number of frames+1.")
317
+ return Array.new
318
+ end
319
+ else
320
+ add_msg("Warning: 'Number of Frames' data element not found.")
321
+ return Array.new
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+
329
+ # Returns an array of the index(es) of the element(s) in the DICOM file that match the supplied element position, tag or name.
330
+ # If no match is found, the method will return false.
331
+ # Additional options:
332
+ # :selection => mySelection - tells the method to search for matches in this specific array of positions instead of searching
333
+ # through the entire DICOM object. If mySelection is empty, the returned array will also be empty.
334
+ # :partial => true - get_pos will not only search for exact matches, but will search the names and tags arrays for strings that contain the given search string.
335
+ # :parent => element - This method will return only matches that are children of the specified (parent) data element.
336
+ def get_pos(query, options={})
337
+ search_array = Array.new
338
+ indexes = Array.new
339
+ # For convenience, allow query to be a one-element array (its value will be extracted):
340
+ if query.is_a?(Array)
341
+ if query.length > 1 or query.length == 0
342
+ add_msg("Warning: Invalid array length supplied to method get_pos().")
343
+ return Array.new
344
+ else
345
+ query = query[0]
346
+ end
347
+ end
348
+ # Check if query is a number (some methods want to have the ability to call get_pos with a number):
349
+ if query.is_a?(Integer)
350
+ # Return the position if it is valid:
351
+ if query >= 0 and query < @names.length
352
+ indexes = [query]
353
+ else
354
+ add_msg("Error: The specified array position (#{query}) is out of range (valid: 0-#{@tags.length}).")
355
+ end
356
+ elsif query.is_a?(String)
357
+ # Has the user specified an array to search within?
358
+ search_array = options[:selection] if options[:selection].is_a?(Array)
359
+ # Has the user specified a specific parent which will restrict our search to only it's children?
360
+ if options[:parent]
361
+ parent_pos = get_pos(options[:parent], :next_only => options[:next_only])
362
+ if parent_pos.length == 0
363
+ add_msg("Error: Invalid parent supplied to method get_pos().")
364
+ return Array.new
365
+ elsif parent_pos.length > 1
366
+ add_msg("Error: The parent you supplied to method get_pos() gives multiple hits. A more precise parent specification is needed.")
367
+ return Array.new
368
+ end
369
+ # Find the children of this particular tag:
370
+ children_pos = children(parent_pos)
371
+ # If selection has also been specified along with parent, we need to extract the array positions that are common to the two arrays:
372
+ if search_array.length > 0
373
+ search_array = search_array & children_pos
374
+ else
375
+ search_array = children_pos
376
+ end
377
+ end
378
+ # Search the entire DICOM object if no restrictions have been set:
379
+ search_array = Array.new(@names.length) {|i| i} unless options[:selection] or options[:parent]
380
+ # Perform search:
381
+ if options[:partial] == true
382
+ # Search for partial string matches:
383
+ partial_indexes = search_array.all_indices_partial_match(@tags, query.upcase)
384
+ if partial_indexes.length > 0
385
+ indexes = partial_indexes
386
+ else
387
+ indexes = search_array.all_indices_partial_match(@names, query)
388
+ end
389
+ else
390
+ # Search for identical matches:
391
+ if query[4..4] == ","
392
+ indexes = search_array.all_indices(@tags, query.upcase)
393
+ else
394
+ indexes = search_array.all_indices(@names, query)
395
+ end
396
+ end
397
+ end
398
+ return indexes
399
+ end # of get_pos
400
+
401
+
402
+ # Dumps the binary content of the Pixel Data element to file.
403
+ def image_to_file(file)
404
+ pos = get_image_pos
405
+ # Pixel data may be located in several elements:
406
+ pos.each_index do |i|
407
+ pixel_data = get_bin(pos[i])
408
+ if pos.length == 1
409
+ f = File.new(file, "wb")
410
+ else
411
+ f = File.new(file + i.to_s, "wb")
412
+ end
413
+ f.write(pixel_data)
414
+ f.close
415
+ end
416
+ end
417
+
418
+
419
+ # Returns the positions of all data elements inside the hierarchy of a sequence or an item.
420
+ # Options:
421
+ # :next_only => true - The method will only search immediately below the specified item or sequence (that is, in the level of parent + 1).
422
+ def children(element, options={})
423
+ # Process option values, setting defaults for the ones that are not specified:
424
+ opt_next_only = options[:next_only] || false
425
+ children_pos = Array.new
426
+ # Retrieve array position:
427
+ pos = get_pos(element)
428
+ if pos.length == 0
429
+ add_msg("Warning: Invalid data element provided to method children().")
430
+ elsif pos.length > 1
431
+ add_msg("Warning: Method children() does not allow a query which yields multiple array hits. Please use array position instead of tag/name.")
432
+ else
433
+ # Proceed to find the value:
434
+ # First we need to establish in which positions to perform the search:
435
+ pos.each do |p|
436
+ parent_level = @levels[p]
437
+ remain_array = @levels[p+1..@levels.length-1]
438
+ extract = true
439
+ remain_array.each_index do |i|
440
+ if (remain_array[i] > parent_level) and (extract == true)
441
+ # If search is targetted at any specific level, we can just add this position:
442
+ if not opt_next_only == true
443
+ children_pos << (p+1+i)
444
+ else
445
+ # As search is restricted to parent level + 1, do a test for this:
446
+ if remain_array[i] == parent_level + 1
447
+ children_pos << (p+1+i)
448
+ end
449
+ end
450
+ else
451
+ # If we encounter a position who's level is not deeper than the original level, we can not extract any more values:
452
+ extract = false
453
+ end
454
+ end
455
+ end
456
+ end
457
+ return children_pos
458
+ end
459
+
460
+
461
+ # Returns the value (processed binary data) of the requested DICOM data element.
462
+ # Data element may be specified by array position, tag or name.
463
+ # Options:
464
+ # :array => true - Allows the query of the value of a tag that occurs more than one time in the
465
+ # DICOM object. Values will be returned in an array with length equal to the number
466
+ # of occurances of the tag. If keyword is not specified, the method returns false in this case.
467
+ # :silent => true - As this method is also used internally, we want the possibility of warnings not being
468
+ # raised even if verbose is set to true by the user, in order to avoid unnecessary confusion.
469
+ def get_value(element, options={})
470
+ value = false
471
+ # Retrieve array position:
472
+ pos = get_pos(element)
473
+ if pos.length == 0
474
+ add_msg("Warning: Invalid data element provided to method get_value() (#{element}).") unless options[:silent]
475
+ elsif pos.length > 1
476
+ # Multiple 'hits':
477
+ if options[:array] == true
478
+ # Retrieve all values into an array:
479
+ value = Array.new
480
+ pos.each do |i|
481
+ value << @values[i]
482
+ end
483
+ else
484
+ add_msg("Warning: Method get_value() does not allow a query which yields multiple array hits (#{element}). Please use array position instead of tag/name, or use option (:array => true) to return all values.") unless options[:silent]
485
+ end
486
+ else
487
+ # One single match:
488
+ value = @values[pos[0]]
489
+ # Return the single value in an array if keyword :array used:
490
+ value = [value] if options[:array]
491
+ end
492
+ return value
493
+ end
494
+
495
+
496
+ # Returns the unprocessed, binary string of the requested DICOM data element.
497
+ # Data element may be specified by array position, tag or name.
498
+ # Options:
499
+ # :array => true - Allows the query of the (binary) value of a tag that occurs more than one time in the
500
+ # DICOM object. Values will be returned in an array with length equal to the number
501
+ # of occurances of the tag. If keyword is not specified, the method returns false in this case.
502
+ def get_bin(element, options={})
503
+ value = false
504
+ # Retrieve array position:
505
+ pos = get_pos(element)
506
+ if pos.length == 0
507
+ add_msg("Warning: Invalid data element provided to method get_bin().")
508
+ elsif pos.length > 1
509
+ # Multiple 'hits':
510
+ if options[:array] == true
511
+ # Retrieve all values into an array:
512
+ value = Array.new
513
+ pos.each do |i|
514
+ value << @bin[i]
515
+ end
516
+ else
517
+ add_msg("Warning: Method get_bin() does not allow a query which yields multiple array hits. Please use array position instead of tag/name, or use keyword (:array => true).")
518
+ end
519
+ else
520
+ # One single match:
521
+ value = @bin[pos[0]]
522
+ # Return the single value in an array if keyword :array used:
523
+ value = [value] if options[:array]
524
+ end
525
+ return value
526
+ end
527
+
528
+
529
+ # Returns the position of (possible) parents of the specified data element in the hierarchy structure of the DICOM object.
530
+ def parents(element)
531
+ parent_pos = Array.new
532
+ # Retrieve array position:
533
+ pos = get_pos(element)
534
+ if pos.length == 0
535
+ add_msg("Warning: Invalid data element provided to method parents().")
536
+ elsif pos.length > 1
537
+ add_msg("Warning: Method parents() does not allow a query which yields multiple array hits. Please use array position instead of tag/name.")
538
+ else
539
+ # Proceed to find the value:
540
+ # Get the level of our element:
541
+ level = @levels[pos[0]]
542
+ # Element can obviously only have parents if it is not a top level element:
543
+ unless level == 0
544
+ # Search backwards, and record the position every time we encounter an upwards change in the level number.
545
+ prev_level = level
546
+ search_arr = @levels[0..pos[0]-1].reverse
547
+ search_arr.each_index do |i|
548
+ if search_arr[i] < prev_level
549
+ parent_pos << search_arr.length-i-1
550
+ prev_level = search_arr[i]
551
+ end
552
+ end
553
+ # When the element has several generations of parents, we want its top parent to be first in the returned array:
554
+ parent_pos = parent_pos.reverse
555
+ end
556
+ end
557
+ return parent_pos
558
+ end
559
+
560
+
561
+ ##############################################
562
+ ####### START OF METHODS FOR PRINTING INFORMATION:######
563
+ ##############################################
564
+
565
+
566
+ # Prints the information of all elements stored in the DICOM object.
567
+ # This method is kept for backwards compatibility.
568
+ # Instead of calling print_all you may use print(true) for the same functionality.
569
+ def print_all
570
+ print(true)
571
+ end
572
+
573
+
574
+ # Prints the information of the specified elements: Index, [hierarchy level, tree visualisation,] tag, name, vr, length, value
575
+ # The supplied variable may be a single position, an array of positions, or true - which will make the method print all elements.
576
+ # Optional arguments:
577
+ # :levels => true - method will print the level numbers for each element.
578
+ # :tree => true - method will print a tree structure for the elements.
579
+ # :file => true - method will print to file instead of printing to screen.
580
+ def print(pos, options={})
581
+ # Process option values, setting defaults for the ones that are not specified:
582
+ opt_levels = options[:levels] || false
583
+ opt_tree = options[:tree] || false
584
+ opt_file = options[:file] || false
585
+ if pos == true
586
+ # Create a complete array of indices:
587
+ pos_valid = Array.new(@names.length) {|i| i}
588
+ elsif not pos.is_a?(Array)
589
+ # Convert number to array:
590
+ pos_valid = [pos]
591
+ else
592
+ # Use the supplied array of numbers:
593
+ pos_valid = pos
594
+ end
595
+ # Extract the information to be printed from the object arrays:
596
+ indices = Array.new
597
+ levels = Array.new
598
+ tags = Array.new
599
+ names = Array.new
600
+ types = Array.new
601
+ lengths = Array.new
602
+ values = Array.new
603
+ # There may be a more elegant way to do this.
604
+ pos_valid.each do |pos|
605
+ tags << @tags[pos]
606
+ levels << @levels[pos].to_s
607
+ names << @names[pos]
608
+ types << @vr[pos]
609
+ lengths << @lengths[pos].to_s
610
+ values << @values[pos].to_s
611
+ end
612
+ # We have collected the data that is to be printed, now we need to do some string manipulation if hierarchy is to be displayed:
613
+ if opt_tree
614
+ # Tree structure requested.
615
+ front_symbol = "| "
616
+ tree_symbol = "|_"
617
+ tags.each_index do |i|
618
+ if levels[i] != "0"
619
+ tags[i] = front_symbol*(levels[i].to_i-1) + tree_symbol + tags[i]
620
+ end
621
+ end
622
+ end
623
+ # Extract the string lengths which are needed to make the formatting nice:
624
+ tag_lengths = Array.new
625
+ lev_lengths = Array.new
626
+ name_lengths = Array.new
627
+ type_lengths = Array.new
628
+ length_lengths = Array.new
629
+ names.each_index do |i|
630
+ tag_lengths[i] = tags[i].length
631
+ lev_lengths[i] = levels[i].length
632
+ name_lengths[i] = names[i].length
633
+ type_lengths[i] = types[i].length
634
+ length_lengths[i] = lengths[i].to_s.length
635
+ end
636
+ # To give the printed output a nice format we need to check the string lengths of some of these arrays:
637
+ index_maxL = pos_valid.max.to_s.length
638
+ lev_maxL = lev_lengths.max
639
+ tag_maxL = tag_lengths.max
640
+ name_maxL = name_lengths.max
641
+ type_maxL = type_lengths.max
642
+ length_maxL = length_lengths.max
643
+ # Construct the strings, one for each line of output, where each line contain the information of one data element:
644
+ elements = Array.new
645
+ # Start of loop which formats the element data:
646
+ # (This loop is what consumes most of the computing time of this method)
647
+ tags.each_index do |i|
648
+ # Configure empty spaces:
649
+ s = " "
650
+ f0 = " "*(index_maxL-pos_valid[i].to_s.length)
651
+ f1 = " "*(lev_maxL-levels[i].length)
652
+ f2 = " "*(tag_maxL-tags[i].length+1)
653
+ f3 = " "*(name_maxL-names[i].length+1)
654
+ f4 = " "*(type_maxL-types[i].length+1)
655
+ f5 = " "*(length_maxL-lengths[i].length)
656
+ # Display levels?
657
+ if opt_levels
658
+ lev = levels[i] + f1
659
+ else
660
+ lev = ""
661
+ end
662
+ # Restrict length of value string:
663
+ if values[i].length > 28
664
+ value = (values[i])[0..27]+" ..."
665
+ else
666
+ value = (values[i])
667
+ end
668
+ # Insert descriptive text for elements that hold binary data:
669
+ case types[i]
670
+ when "OW","OB","UN"
671
+ value = "(Binary Data)"
672
+ when "SQ"
673
+ value = "(Encapsulated Elements)"
674
+ when "()"
675
+ if tags[i].include?("FFFE,E000") # (Item)
676
+ value = "(Encapsulated Elements)"
677
+ else
678
+ value = ""
679
+ end
680
+ end
681
+ elements << (f0 + pos_valid[i].to_s + s + lev + s + tags[i] + f2 + names[i] + f3 + types[i] + f4 + f5 + lengths[i].to_s + s + s + value.rstrip)
682
+ end
683
+ # Print to either screen or file, depending on what the user requested:
684
+ if opt_file
685
+ print_file(elements)
686
+ else
687
+ print_screen(elements)
688
+ end
689
+ end # of print
690
+
691
+
692
+ # Prints the key structural properties of the DICOM file.
693
+ def print_properties
694
+ # Explicitness:
695
+ if @explicit
696
+ explicit = "Explicit"
697
+ else
698
+ explicit = "Implicit"
699
+ end
700
+ # Endianness:
701
+ if @file_endian
702
+ endian = "Big Endian"
703
+ else
704
+ endian = "Little Endian"
705
+ end
706
+ # Pixel data:
707
+ if @compression == nil
708
+ pixels = "No"
709
+ else
710
+ pixels = "Yes"
711
+ end
712
+ # Colors:
713
+ if @color
714
+ image = "Colors"
715
+ else
716
+ image = "Greyscale"
717
+ end
718
+ # Compression:
719
+ if @compression == true
720
+ compression = LIBRARY.get_uid(get_value("0002,0010").rstrip)
721
+ else
722
+ compression = "No"
723
+ end
724
+ # Bits per pixel (allocated):
725
+ bits = get_value("0028,0100", :array => true, :silent => true)
726
+ bits = bits[0].to_s if bits
727
+ # Print the file properties:
728
+ puts "Key properties of DICOM object:"
729
+ puts "-------------------------------"
730
+ puts "File: " + @file
731
+ puts "Modality: " + @modality.to_s
732
+ puts "Value repr.: " + explicit
733
+ puts "Byte order: " + endian
734
+ puts "Pixel data: " + pixels
735
+ if pixels == "Yes"
736
+ puts "Image: " + image if image
737
+ puts "Compression: " + compression if compression
738
+ puts "Bits per pixel: " + bits if bits
739
+ end
740
+ puts "-------------------------------"
741
+ end # of print_properties
742
+
743
+
744
+ ####################################################
745
+ ### START OF METHODS FOR WRITING INFORMATION TO THE DICOM OBJECT:
746
+ ####################################################
747
+
748
+
749
+ # Writes pixel data from a Ruby Array object to the pixel data element.
750
+ def set_image(pixel_array)
751
+ # Encode this array using the standard class method:
752
+ set_value(pixel_array, "7FE0,0010", :create => true)
753
+ end
754
+
755
+
756
+ # Reads binary information from file and inserts it in the pixel data element.
757
+ def set_image_file(file)
758
+ # Try to read file:
759
+ begin
760
+ f = File.new(file, "rb")
761
+ bin = f.read(f.stat.size)
762
+ rescue
763
+ # Reading file was not successful. Register an error message.
764
+ add_msg("Reading specified file was not successful for some reason. No data has been added.")
765
+ return
766
+ end
767
+ if bin.length > 0
768
+ pos = @tags.index("7FE0,0010")
769
+ # Modify element:
770
+ set_value(bin, "7FE0,0010", :create => true, :bin => true)
771
+ else
772
+ add_msg("Content of file is of zero length. Nothing to store.")
773
+ end
774
+ end
775
+
776
+
777
+ # Transfers pixel data from a RMagick object to the pixel data element.
778
+ # NB! Because of rescaling when importing pixel values to a RMagick object, and the possible
779
+ # difference between presentation values and pixel values, the use of set_image_magick() may
780
+ # result in pixel data that is completely different from what is expected.
781
+ # This method should be used only with great care!
782
+ # If value rescaling is wanted, both :min and :max must be set!
783
+ # Options:
784
+ # :max => value - Pixel values will be rescaled using this as the new maximum value.
785
+ # :min => value - Pixel values will be rescaled, using this as the new minimum value.
786
+ def set_image_magick(magick_obj, options={})
787
+ # Export the RMagick object to a standard Ruby array of numbers:
788
+ pixel_array = magick_obj.export_pixels(x=0, y=0, columns=magick_obj.columns, rows=magick_obj.rows, map="I")
789
+ # Rescale pixel values?
790
+ if options[:min] and options[:max]
791
+ p_min = pixel_array.min
792
+ p_max = pixel_array.max
793
+ if p_min != options[:min] or p_max != options[:max]
794
+ wanted_range = options[:max] - options[:min]
795
+ factor = wanted_range.to_f/(pixel_array.max - pixel_array.min).to_f
796
+ offset = pixel_array.min - options[:min]
797
+ pixel_array.collect!{|x| ((x*factor)-offset).round}
798
+ end
799
+ end
800
+ # Encode this array using the standard class method:
801
+ set_value(pixel_array, "7FE0,0010", :create => true)
802
+ end
803
+
804
+
805
+ # Transfers pixel data from a NArray object to the pixel data element.
806
+ # If value rescaling is wanted, both :min and :max must be set!
807
+ # Options:
808
+ # :max => value - Pixel values will be rescaled using this as the new maximum value.
809
+ # :min => value - Pixel values will be rescaled, using this as the new minimum value.
810
+ def set_image_narray(narray, options={})
811
+ # Rescale pixel values?
812
+ if options[:min] and options[:max]
813
+ n_min = narray.min
814
+ n_max = narray.max
815
+ if n_min != options[:min] or n_max != options[:max]
816
+ wanted_range = options[:max] - options[:min]
817
+ factor = wanted_range.to_f/(n_max - n_min).to_f
818
+ offset = n_min - options[:min]
819
+ narray = narray*factor-offset
820
+ end
821
+ end
822
+ # Export the NArray object to a standard Ruby array of numbers:
823
+ pixel_array = narray.to_a.flatten!
824
+ # Encode this array using the standard class method:
825
+ set_value(pixel_array, "7FE0,0010", :create => true)
826
+ end
827
+
828
+
829
+ # Removes an element from the DICOM object.
830
+ # Options:
831
+ # :ignore_children => true - Force the method to ignore children when removing an element.
832
+ # (default behaviour is to remove any children if a sequence or item is removed)
833
+ def remove(element, options={})
834
+ positions = get_pos(element)
835
+ if positions.length == 0
836
+ add_msg("Warning: The given data element (#{element}) could not be found in the DICOM object. Method remove() has no data element to remove.")
837
+ elsif positions.length > 1
838
+ add_msg("Warning: Method remove() does not allow an element query which yields multiple array hits (#{element}). Please use array position instead of tag/name. Value(s) NOT removed.")
839
+ else
840
+ # Check if the tag selected for removal has children (relevant for sequence/item tags):
841
+ unless options[:ignore_children]
842
+ child_pos = children(positions)
843
+ # Add the positions of the children (if they exist) to our original tag's position array:
844
+ positions << child_pos if child_pos.length > 0
845
+ end
846
+ positions.flatten!
847
+ # Loop through all positions (important to do this in reverse to retain predictable array positions):
848
+ positions.reverse.each do |pos|
849
+ # Update group length
850
+ # (Possible weakness: Group length tag contained inside a sequence/item. Code needs a slight rewrite to make it more robust)
851
+ if @tags[pos][5..8] != "0000"
852
+ # Note: When removing an item/sequence, its length value must not be used for 'change' (it's value is in reality nil):
853
+ if @vr[pos] == "()" or @vr[pos] == "SQ"
854
+ change = 0
855
+ else
856
+ change = @lengths[pos]
857
+ end
858
+ vr = @vr[pos]
859
+ update_group_and_parents_length(pos, vr, change, -1)
860
+ end
861
+ # Remove entry from arrays:
862
+ @tags.delete_at(pos)
863
+ @levels.delete_at(pos)
864
+ @names.delete_at(pos)
865
+ @vr.delete_at(pos)
866
+ @lengths.delete_at(pos)
867
+ @values.delete_at(pos)
868
+ @bin.delete_at(pos)
869
+ end
870
+ end
871
+ end
872
+
873
+
874
+ # Removes all private data elements from the DICOM object.
875
+ def remove_private
876
+ # Private data elemements have a group tag that is odd.
877
+ odd_group = ["1,","3,","5,","7,","9,","B,","D,","F,"]
878
+ odd_group.each do |odd|
879
+ positions = get_pos(odd, :partial => true)
880
+ # Delete all entries (important to do this in reverse order).
881
+ positions.reverse.each do |pos|
882
+ remove(pos)
883
+ end
884
+ end
885
+ end
886
+
887
+
888
+ # Sets the value of a data element by modifying an existing element or creating a new one.
889
+ # If the supplied value is not binary, it will attempt to encode the value to binary itself.
890
+ # Options:
891
+ # :create => false - Only update the specified element (do not create if missing).
892
+ # :bin => bin_data - Value is already encoded as a binary string.
893
+ # :vr => string - If creating a private element, the value representation must be provided to ensure proper encoding.
894
+ # :parent => element - If an element is to be created inside a sequence/item, it's parent must be specified to ensure proper placement.
895
+ def set_value(value, element, options={})
896
+ # Options:
897
+ bin = options[:bin] # =true means value already encoded
898
+ vr = options[:vr] # a string which tells us what kind of type an unknown data element is
899
+ # Retrieve array position:
900
+ pos = get_pos(element, options)
901
+ # We do not support changing multiple data elements:
902
+ if pos.length > 1
903
+ add_msg("Warning: Method set_value() does not allow an element query (#{element}) which yields multiple array hits. Please use array position instead of tag/name. Value(s) NOT saved.")
904
+ return
905
+ end
906
+ if pos.length == 0 and options[:create] == false
907
+ # Since user has requested an element shall only be updated, we can not do so as the element position is not valid:
908
+ add_msg("Warning: Invalid data element (#{element}) provided to method set_value(). Value NOT updated.")
909
+ elsif options[:create] == false
910
+ # Modify element:
911
+ modify_element(value, pos[0], :bin => bin)
912
+ else
913
+ # User wants to create an element (or modify it if it is already present).
914
+ unless pos.length == 0
915
+ # The data element already exist, so we modify instead of creating:
916
+ modify_element(value, pos[0], :bin => bin)
917
+ else
918
+ # We need to create element:
919
+ # In the case that name has been provided instead of a tag, check with the library first:
920
+ tag = LIBRARY.get_tag(element)
921
+ # If this doesnt give a match, we may be dealing with a private tag:
922
+ tag = element unless tag
923
+ unless element.is_a?(String)
924
+ add_msg("Warning: Invalid data element (#{element}) provided to method set_value(). Value NOT updated.")
925
+ else
926
+ unless element.is_a_tag?
927
+ add_msg("Warning: Method set_value could not create data element, because the data element tag (#{element}) is invalid (Expected format of tags is 'GGGG,EEEE').")
928
+ else
929
+ # As we wish to create a new data element, we need to find out where to insert it in the element arrays:
930
+ # We will do this by finding the array position of the last element that will (alphabetically/numerically) stay in front of this element.
931
+ if @tags.length > 0
932
+ if options[:parent]
933
+ # Parent specified:
934
+ parent_pos = get_pos(options[:parent])
935
+ if parent_pos.length > 1
936
+ add_msg("Error: Method set_value() could not create data element, because the specified parent element (#{options[:parent]}) returns multiple hits.")
937
+ return
938
+ end
939
+ indexes = children(parent_pos, :next_only => true)
940
+ level = @levels[parent_pos.first]+1
941
+ else
942
+ # No parent (fetch top level elements):
943
+ full_array = Array.new(@levels.length) {|i| i}
944
+ indexes = full_array.all_indices(@levels, 0)
945
+ level = 0
946
+ end
947
+ # Loop through the selection:
948
+ index = -1
949
+ quit = false
950
+ while quit != true do
951
+ if index+1 >= indexes.length # We have reached end of array.
952
+ quit = true
953
+ elsif tag < @tags[indexes[index+1]]
954
+ quit = true
955
+ else # Increase index in anticipation of a 'hit'.
956
+ index += 1
957
+ end
958
+ end
959
+ # Determine the index to pass on:
960
+ if index == -1
961
+ # Empty parent tag or new tag belongs in front of our indexes:
962
+ if indexes.length == 0
963
+ full_index = parent_pos.first
964
+ else
965
+ full_index = indexes.first-1
966
+ end
967
+ else
968
+ full_index = indexes[index]
969
+ end
970
+ else
971
+ # We are dealing with an empty DICOM object:
972
+ full_index = nil
973
+ level = 0
974
+ end
975
+ # The necessary information is gathered; create new data element:
976
+ create_element(value, tag, full_index, level, :bin => bin, :vr => vr)
977
+ end
978
+ end
979
+ end
980
+ end
981
+ end # of set_value
982
+
983
+
984
+ ##################################################
985
+ ############## START OF PRIVATE METHODS: ########
986
+ ##################################################
987
+ private
988
+
989
+
990
+ # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
991
+ def add_msg(msg)
992
+ puts msg if @verbose
993
+ @errors << msg
994
+ @errors.flatten
995
+ end
996
+
997
+
998
+ # Checks the status of the pixel data that has been read from the DICOM file: whether it exists at all and if its greyscale or color.
999
+ # Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
1000
+ def check_properties
1001
+ # Check if pixel data is present:
1002
+ if @tags.index("7FE0,0010") == nil
1003
+ # No pixel data in DICOM file:
1004
+ @compression = nil
1005
+ else
1006
+ @compression = LIBRARY.get_compression(get_value("0002,0010", :silent => true))
1007
+ end
1008
+ # Set color variable as true if our object contain a color image:
1009
+ col_string = get_value("0028,0004", :silent => true)
1010
+ if col_string != false
1011
+ if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
1012
+ @color = true
1013
+ end
1014
+ end
1015
+ end
1016
+
1017
+
1018
+ # Creates a new data element:
1019
+ def create_element(value, tag, last_pos, level, options={})
1020
+ bin_only = options[:bin]
1021
+ vr = options[:vr].upcase if options[:vr].is_a?(String)
1022
+ # Fetch the VR:
1023
+ info = LIBRARY.get_name_vr(tag)
1024
+ vr = info[1] unless vr
1025
+ name = info[0]
1026
+ # Encode binary (if a binary is not provided):
1027
+ if bin_only == true
1028
+ # Data already encoded.
1029
+ bin = value
1030
+ value = nil
1031
+ else
1032
+ if vr != "UN"
1033
+ # Encode:
1034
+ bin = encode(value, vr)
1035
+ else
1036
+ add_msg("Error. Unable to encode data element value with unknown Value Representation!")
1037
+ end
1038
+ end
1039
+ # Put the information of this data element into the arrays:
1040
+ if bin
1041
+ # 4 different scenarios: Array is empty, or: element is put in front, inside array, or at end of array:
1042
+ # NB! No support for hierarchy at this time! Defaulting to level = 0.
1043
+ if last_pos == nil
1044
+ # We have empty DICOM object:
1045
+ @tags = [tag]
1046
+ @levels = [level]
1047
+ @names = [name]
1048
+ @vr = [vr]
1049
+ @lengths = [bin.length]
1050
+ @values = [value]
1051
+ @bin = [bin]
1052
+ pos = 0
1053
+ elsif last_pos == -1
1054
+ # Insert in front of arrays:
1055
+ @tags = [tag] + @tags
1056
+ @levels = [level] + @levels
1057
+ @names = [name] + @names
1058
+ @vr = [vr] + @vr
1059
+ @lengths = [bin.length] + @lengths
1060
+ @values = [value] + @values
1061
+ @bin = [bin] + @bin
1062
+ pos = 0
1063
+ elsif last_pos == @tags.length-1
1064
+ # Insert at end arrays:
1065
+ @tags = @tags + [tag]
1066
+ @levels = @levels + [level]
1067
+ @names = @names + [name]
1068
+ @vr = @vr + [vr]
1069
+ @lengths = @lengths + [bin.length]
1070
+ @values = @values + [value]
1071
+ @bin = @bin + [bin]
1072
+ pos = @tags.length-1
1073
+ else
1074
+ # Insert somewhere inside the array:
1075
+ @tags = @tags[0..last_pos] + [tag] + @tags[(last_pos+1)..(@tags.length-1)]
1076
+ @levels = @levels[0..last_pos] + [level] + @levels[(last_pos+1)..(@levels.length-1)]
1077
+ @names = @names[0..last_pos] + [name] + @names[(last_pos+1)..(@names.length-1)]
1078
+ @vr = @vr[0..last_pos] + [vr] + @vr[(last_pos+1)..(@vr.length-1)]
1079
+ @lengths = @lengths[0..last_pos] + [bin.length] + @lengths[(last_pos+1)..(@lengths.length-1)]
1080
+ @values = @values[0..last_pos] + [value] + @values[(last_pos+1)..(@values.length-1)]
1081
+ @bin = @bin[0..last_pos] + [bin] + @bin[(last_pos+1)..(@bin.length-1)]
1082
+ pos = last_pos + 1
1083
+ end
1084
+ # Update group length (as long as it was not a top-level group length element that was created):
1085
+ if @tags[pos][5..8] != "0000" or level != 0
1086
+ change = bin.length
1087
+ update_group_and_parents_length(pos, vr, change, 1)
1088
+ end
1089
+ else
1090
+ add_msg("Binary is nil. Nothing to save.")
1091
+ end
1092
+ end # of create_element
1093
+
1094
+
1095
+ # Encodes a value to binary (used for inserting values into a DICOM object).
1096
+ # Future development: Encoding of tags should be moved to the Stream class,
1097
+ # and encoding of image data should be 'outsourced' to a method of its own (encode_image).
1098
+ def encode(value, vr)
1099
+ # VR will decide how to encode this value:
1100
+ case vr
1101
+ when "AT" # (Data element tag: Assume it has the format "GGGG,EEEE"
1102
+ if value.is_a_tag?
1103
+ bin = @stream.encode_tag(value)
1104
+ else
1105
+ add_msg("Invalid tag format (#{value}). Expected format: 'GGGG,EEEE'")
1106
+ end
1107
+ # We have a number of VRs that are encoded as string:
1108
+ when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT'
1109
+ # In case we are dealing with a number string element, the supplied value might be a number
1110
+ # instead of a string, and as such, we convert to string just to make sure this will work nicely:
1111
+ value = value.to_s
1112
+ bin = @stream.encode_value(value, "STR")
1113
+ # Image related value representations:
1114
+ when "OW"
1115
+ # What bit depth to use when encoding the pixel data?
1116
+ bit_depth = get_value("0028,0100", :array => true)[0]
1117
+ if bit_depth == false
1118
+ # Data element not specified:
1119
+ add_msg("Attempted to encode pixel data, but the 'Bit Depth' Data Element (0028,0100) is missing.")
1120
+ else
1121
+ # 8, 12 or 16 bits per pixel?
1122
+ case bit_depth
1123
+ when 8
1124
+ bin = @stream.encode(value, "BY")
1125
+ when 12
1126
+ # 12 bit not supported yet!
1127
+ add_msg("Encoding 12 bit pixel values not supported yet. Please change the bit depth to 8 or 16 bits.")
1128
+ when 16
1129
+ # Signed or unsigned integer?
1130
+ pixel_representation = get_value("0028,0103", :array => true)[0]
1131
+ if pixel_representation
1132
+ if pixel_representation.to_i == 1
1133
+ # Signed integers:
1134
+ bin = @stream.encode(value, "SS")
1135
+ else
1136
+ # Unsigned integers:
1137
+ bin = @stream.encode(value, "US")
1138
+ end
1139
+ else
1140
+ add_msg("Attempted to encode pixel data, but the 'Pixel Representation' Data Element (0028,0103) is missing.")
1141
+ end
1142
+ else
1143
+ # Unknown bit depth:
1144
+ add_msg("Unknown bit depth #{bit_depth}. No data encoded.")
1145
+ end
1146
+ end
1147
+ # All other VR's:
1148
+ else
1149
+ # Just encode:
1150
+ bin = @stream.encode(value, vr)
1151
+ end
1152
+ return bin
1153
+ end # of encode
1154
+
1155
+
1156
+ # Find the position(s) of the group length tag(s) that the given tag is associated with.
1157
+ # If a group length tag does not exist, return an empty array.
1158
+ def find_group_length(pos)
1159
+ positions = Array.new
1160
+ group = @tags[pos][0..4]
1161
+ # Check if our tag is part of a sequence/item:
1162
+ if @levels[pos] > 0
1163
+ # Add (possible) group length of top parent:
1164
+ parent_positions = parents(pos)
1165
+ first_parent_gl_pos = find_group_length(parent_positions.first)
1166
+ positions << first_parent_gl_pos.first if first_parent_gl_pos.length > 0
1167
+ # Add (possible) group length at current tag's level:
1168
+ valid_positions = children(parent_positions.last)
1169
+ level_gl_pos = get_pos(group+"0000", :array => valid_positions)
1170
+ positions << level_gl_pos.first if level_gl_pos.length > 0
1171
+ else
1172
+ # We are dealing with a top level tag:
1173
+ gl_pos = get_pos(group+"0000")
1174
+ # Note: Group level tags of this type may be found elsewhere in the DICOM object inside other
1175
+ # sequences/items. We must make sure that such tags are not added to our list:
1176
+ gl_pos.each do |gl|
1177
+ positions << gl if @levels[gl] == 0
1178
+ end
1179
+ end
1180
+ return positions
1181
+ end
1182
+
1183
+
1184
+ # Unpacks and returns pixel data from a specified data element array position:
1185
+ def get_pixels(pos)
1186
+ pixels = false
1187
+ # We need to know what kind of bith depth and integer type the pixel data is saved with:
1188
+ bit_depth = get_value("0028,0100", :array => true)[0]
1189
+ pixel_representation = get_value("0028,0103", :array => true)[0]
1190
+ unless bit_depth == false
1191
+ # Load the binary pixel data to the Stream instance:
1192
+ @stream.set_string(get_bin(pos))
1193
+ # Number of bytes used per pixel will determine how to unpack this:
1194
+ case bit_depth
1195
+ when 8
1196
+ pixels = @stream.decode_all("BY") # Byte/Character/Fixnum (1 byte)
1197
+ when 16
1198
+ if pixel_representation
1199
+ if pixel_representation.to_i == 1
1200
+ pixels = @stream.decode_all("SS") # Signed short (2 bytes)
1201
+ else
1202
+ pixels = @stream.decode_all("US") # Unsigned short (2 bytes)
1203
+ end
1204
+ else
1205
+ add_msg("Error: Attempted to decode pixel data, but the 'Pixel Representation' Data Element (0028,0103) is missing.")
1206
+ end
1207
+ when 12
1208
+ # 12 BIT SIMPLY NOT WORKING YET!
1209
+ # This one is a bit more tricky to extract.
1210
+ # I havent really given this priority so far as 12 bit image data is rather rare.
1211
+ add_msg("Warning: Decoding bit depth 12 is not implemented yet! Please contact the author.")
1212
+ else
1213
+ raise "Bit depth ["+bit_depth.to_s+"] has not received implementation in this procedure yet. Please contact the author."
1214
+ end
1215
+ else
1216
+ add_msg("Error: Attempted to decode pixel data, but the 'Bit Depth' Data Element (0028,0010) is missing.")
1217
+ end
1218
+ return pixels
1219
+ end
1220
+
1221
+
1222
+ # Modifies existing data element:
1223
+ def modify_element(value, pos, options={})
1224
+ bin_only = options[:bin]
1225
+ # Fetch the VR and old length:
1226
+ vr = @vr[pos]
1227
+ old_length = @lengths[pos]
1228
+ # Encode binary (if a binary is not provided):
1229
+ if bin_only == true
1230
+ # Data already encoded.
1231
+ bin = value
1232
+ value = nil
1233
+ else
1234
+ if vr != "UN"
1235
+ # Encode:
1236
+ bin = encode(value, vr)
1237
+ else
1238
+ add_msg("Error. Unable to encode data element with unknown Value Representation!")
1239
+ end
1240
+ end
1241
+ # Update the arrays with this new information:
1242
+ if bin
1243
+ # Replace array entries for this element:
1244
+ #@vr[pos] = vr # for the time being there is no logic for updating/changing vr.
1245
+ @lengths[pos] = bin.length
1246
+ @values[pos] = value
1247
+ @bin[pos] = bin
1248
+ # Update group length (as long as it was not the group length that was modified):
1249
+ if @tags[pos][5..8] != "0000"
1250
+ change = bin.length - old_length
1251
+ update_group_and_parents_length(pos, vr, change, 0)
1252
+ end
1253
+ else
1254
+ add_msg("Binary is nil. Nothing to save.")
1255
+ end
1256
+ end
1257
+
1258
+
1259
+ # Prints the selected elements to an ascii text file.
1260
+ # The text file will be saved in the folder of the original DICOM file,
1261
+ # with the original file name plus a .txt extension.
1262
+ def print_file(elements)
1263
+ File.open( @file + '.txt', 'w' ) do |output|
1264
+ elements.each do | line |
1265
+ output.print line + "\n"
1266
+ end
1267
+ end
1268
+ end
1269
+
1270
+
1271
+ # Prints the selected elements to screen.
1272
+ def print_screen(elements)
1273
+ elements.each do |element|
1274
+ puts element
1275
+ end
1276
+ end
1277
+
1278
+
1279
+ # Converts original pixel data values to presentation values.
1280
+ def process_presentation_values(pixel_data, center, width, slope, intercept, min_allowed, max_allowed)
1281
+ # Rescale:
1282
+ # PixelOutput = slope * pixel_values + intercept
1283
+ if intercept != 0 or slope != 1
1284
+ pixel_data.collect!{|x| (slope * x) + intercept}
1285
+ end
1286
+ # Contrast enhancement by black and white thresholding:
1287
+ if center and width
1288
+ low = center - width/2
1289
+ high = center + width/2
1290
+ pixel_data.each_index do |i|
1291
+ if pixel_data[i] < low
1292
+ pixel_data[i] = low
1293
+ elsif pixel_data[i] > high
1294
+ pixel_data[i] = high
1295
+ end
1296
+ end
1297
+ end
1298
+ # Need to introduce an offset?
1299
+ min_pixel_value = pixel_data.min
1300
+ if min_allowed
1301
+ if min_pixel_value < min_allowed
1302
+ offset = min_pixel_value.abs
1303
+ pixel_data.collect!{|x| x + offset}
1304
+ end
1305
+ end
1306
+ # Downscale pixel range?
1307
+ max_pixel_value = pixel_data.max
1308
+ if max_allowed
1309
+ if max_pixel_value > max_allowed
1310
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
1311
+ pixel_data.collect!{|x| x / factor}
1312
+ end
1313
+ end
1314
+ return pixel_data
1315
+ end
1316
+
1317
+
1318
+ # Converts original pixel data values to a RMagick image object containing presentation values.
1319
+ def process_presentation_values_magick(pixel_data, center, width, slope, intercept, max_allowed, columns, rows)
1320
+ # Rescale:
1321
+ # PixelOutput = slope * pixel_values + intercept
1322
+ if intercept != 0 or slope != 1
1323
+ pixel_data.collect!{|x| (slope * x) + intercept}
1324
+ end
1325
+ # Need to introduce an offset?
1326
+ offset = 0
1327
+ min_pixel_value = pixel_data.min
1328
+ if min_pixel_value < 0
1329
+ offset = min_pixel_value.abs
1330
+ pixel_data.collect!{|x| x + offset}
1331
+ end
1332
+ # Downscale pixel range?
1333
+ factor = 1
1334
+ max_pixel_value = pixel_data.max
1335
+ if max_allowed
1336
+ if max_pixel_value > max_allowed
1337
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
1338
+ pixel_data.collect!{|x| x / factor}
1339
+ end
1340
+ end
1341
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
1342
+ # Contrast enhancement by black and white thresholding:
1343
+ if center and width
1344
+ low = (center - width/2 + offset) / factor
1345
+ high = (center + width/2 + offset) / factor
1346
+ image = image.level(low, high)
1347
+ end
1348
+ return image
1349
+ end
1350
+
1351
+
1352
+ # Converts original pixel data values to presentation values, using the faster numerical array.
1353
+ # If a Ruby array is supplied, this returns a one-dimensional NArray object (i.e. no columns & rows).
1354
+ # If a NArray is supplied, the NArray is returned with its original dimensions.
1355
+ def process_presentation_values_narray(pixel_data, center, width, slope, intercept, min_allowed, max_allowed)
1356
+ if pixel_data.is_a?(Array)
1357
+ n_arr = NArray.to_na(pixel_data)
1358
+ else
1359
+ n_arr = pixel_data
1360
+ end
1361
+ # Rescale:
1362
+ # PixelOutput = slope * pixel_values + intercept
1363
+ if intercept != 0 or slope != 1
1364
+ n_arr = slope * n_arr + intercept
1365
+ end
1366
+ # Contrast enhancement by black and white thresholding:
1367
+ if center and width
1368
+ low = center - width/2
1369
+ high = center + width/2
1370
+ n_arr[n_arr < low] = low
1371
+ n_arr[n_arr > high] = high
1372
+ end
1373
+ # Need to introduce an offset?
1374
+ min_pixel_value = n_arr.min
1375
+ if min_allowed
1376
+ if min_pixel_value < min_allowed
1377
+ offset = min_pixel_value.abs
1378
+ n_arr = n_arr + offset
1379
+ end
1380
+ end
1381
+ # Downscale pixel range?
1382
+ max_pixel_value = n_arr.max
1383
+ if max_allowed
1384
+ if max_pixel_value > max_allowed
1385
+ factor = (max_pixel_value.to_f/max_allowed.to_f).ceil
1386
+ n_arr = n_arr / factor
1387
+ end
1388
+ end
1389
+ return n_arr
1390
+ end
1391
+
1392
+
1393
+ # Returns one or more RMagick image objects from the binary string pixel data,
1394
+ # performing decompression of data if necessary.
1395
+ def read_image_magick(pos, columns, rows, frames, options={})
1396
+ if columns == false or rows == false
1397
+ add_msg("Error: Method read_image_magick() does not have enough data available to build an image object.")
1398
+ return false
1399
+ end
1400
+ unless @compression
1401
+ # Non-compressed, just return the array contained on the particular element:
1402
+ pixel_data = get_pixels(pos)
1403
+ # Remap the image from pixel values to presentation values if the user has requested this:
1404
+ if options[:rescale] == true
1405
+ # Process pixel data for presentation according to the image information in the DICOM object:
1406
+ center, width, intercept, slope = window_level_values
1407
+ # What tools will be used to process the pixel presentation values?
1408
+ if options[:narray] == true
1409
+ # Use numerical array (fast):
1410
+ pixel_data = process_presentation_values_narray(pixel_data, center, width, slope, intercept, 0, Magick::QuantumRange).to_a
1411
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
1412
+ else
1413
+ # Use a combination of ruby array and RMagick processing:
1414
+ image = process_presentation_values_magick(pixel_data, center, width, slope, intercept, Magick::QuantumRange, columns, rows)
1415
+ end
1416
+ else
1417
+ # Load original pixel values to a RMagick object:
1418
+ image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, "I", pixel_data)
1419
+ end
1420
+ return image
1421
+ else
1422
+ # Image data is compressed, we will attempt to deflate it using RMagick (ImageMagick):
1423
+ begin
1424
+ image = Magick::Image.from_blob(@bin[pos])
1425
+ return image
1426
+ rescue
1427
+ add_msg("RMagick did not succeed in decoding the compressed image data.")
1428
+ return false
1429
+ end
1430
+ end
1431
+ end
1432
+
1433
+
1434
+ # Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
1435
+ def set_modality
1436
+ value = get_value("0008,0016", :silent => true)
1437
+ if value == false
1438
+ @modality = "Not specified"
1439
+ else
1440
+ modality = LIBRARY.get_uid(value.rstrip)
1441
+ @modality = modality
1442
+ end
1443
+ end
1444
+
1445
+
1446
+ # Handles the creation of a DWrite object, and returns this object to the calling method.
1447
+ def set_write_object(file_name = nil, transfer_syntax = nil)
1448
+ unless transfer_syntax
1449
+ transfer_syntax = get_value("0002,0010", :silent => true)
1450
+ transfer_syntax = "1.2.840.10008.1.2" if not transfer_syntax # Default is implicit, little endian
1451
+ end
1452
+ w = DWrite.new(file_name, :sys_endian => @sys_endian, :transfer_syntax => transfer_syntax)
1453
+ w.tags = @tags
1454
+ w.vr = @vr
1455
+ w.lengths = @lengths
1456
+ w.bin = @bin
1457
+ w.rest_endian = @file_endian
1458
+ w.rest_explicit = @explicit
1459
+ return w
1460
+ end
1461
+
1462
+
1463
+ # Updates the group length value when a data element has been updated, created or removed.
1464
+ # If the tag is part of a sequence/item, and its parent have length values, these parents' lengths are also updated.
1465
+ # The variable value_change_length holds the change in value length for the updated data element.
1466
+ # (value_change_length should be positive when a data element is removed - it will only be negative when editing an element to a shorter value)
1467
+ # The variable existance is -1 if data element has been removed, +1 if element has been added and 0 if it has been updated.
1468
+ # There is some repetition of code in this method, so there is possible a potential to clean it up somewhat.
1469
+ def update_group_and_parents_length(pos, vr, value_change_length, existance)
1470
+ update_positions = Array.new
1471
+ # Is this a tag with parents?
1472
+ if @levels[pos] > 0
1473
+ parent_positions = parents(pos)
1474
+ parent_positions.each do |parent|
1475
+ # If the parent has a length value, then it must be added to our list of tags that will have its length updated:
1476
+ # Items/sequences that use delimitation items, have their lengths set to "UNDEFINED" by Ruby DICOM.
1477
+ # Obviously, these items/sequences will not have their lengths changed.
1478
+ unless @lengths[parent].is_a?(String)
1479
+ if @lengths[parent] > 0
1480
+ update_positions << parent
1481
+ else
1482
+ # However, a (previously) empty sequence/item that does not use delimiation items, should also have its length updated:
1483
+ # The search for a delimitation item is somewhat slow, so only do this if the length was 0.
1484
+ children_positions = children(parent, :next_only => true)
1485
+ update_positions << parent if children_positions.length == 1 and @tags[children_positions[0]][0..7] != "FFFE,E0"
1486
+ end
1487
+ end
1488
+ end
1489
+ end
1490
+ # Check for a corresponding group length tag:
1491
+ gl_pos = find_group_length(pos)
1492
+ # Join the arrays if group length tag(s) were actually discovered (Operator | can be used here for simplicity, but seems to be not working in Ruby 1.8)
1493
+ gl_pos.each do |gl|
1494
+ update_positions << gl
1495
+ end
1496
+ existance = 0 unless existance
1497
+ # If group length(s)/parent(s) to be updated exists, calculate change:
1498
+ if update_positions
1499
+ values = Array.new
1500
+ if existance == 0
1501
+ # Element has only been updated, so we only need to think about the change in length of its value:
1502
+ update_positions.each do |up|
1503
+ # If we have a group length, value will be changed, if it is a sequence/item, length will be changed:
1504
+ if @tags[up][5..8] == "0000"
1505
+ values << @values[up] + value_change_length
1506
+ else
1507
+ values << @lengths[up] + value_change_length
1508
+ end
1509
+ end
1510
+ else
1511
+ # Element has either been created or removed. This means we need to calculate the length of its other parts.
1512
+ if @explicit
1513
+ # In the explicit scenario it is slightly complex to determine this value:
1514
+ element_length = 0
1515
+ # VR?:
1516
+ unless @tags[pos] == "FFFE,E000" or @tags[pos] == "FFFE,E00D" or @tags[pos] == "FFFE,E0DD"
1517
+ element_length += 2
1518
+ end
1519
+ # Length value:
1520
+ case @vr[pos]
1521
+ when "OB","OW","SQ","UN"
1522
+ if pos > @tags.index("7FE0,0010").to_i and @tags.index("7FE0,0010").to_i != 0
1523
+ element_length += 4
1524
+ else
1525
+ element_length += 6
1526
+ end
1527
+ when "()"
1528
+ element_length += 4
1529
+ else
1530
+ element_length += 2
1531
+ end
1532
+ else
1533
+ # In the implicit scenario it is easier:
1534
+ element_length = 4
1535
+ end
1536
+ # Update group length for creation/deletion scenario:
1537
+ change = (4 + element_length + value_change_length) * existance
1538
+ update_positions.each do |up|
1539
+ # If we have a group length, value will be changed, if it is a sequence/item, length will be changed:
1540
+ if @tags[up][5..8] == "0000"
1541
+ values << @values[up] + change
1542
+ else
1543
+ values << @lengths[up] + change
1544
+ end
1545
+ end
1546
+ end
1547
+ # Write the new Group Length(s)/parent(s) value(s):
1548
+ update_positions.each_index do |i|
1549
+ # If we have a group length, value will be changed, if it is a sequence/item, length will be changed:
1550
+ if @tags[update_positions[i]][5..8] == "0000"
1551
+ # Encode the new value to binary:
1552
+ bin = encode(values[i], "UL")
1553
+ # Update arrays:
1554
+ @values[update_positions[i]] = values[i]
1555
+ @bin[update_positions[i]] = bin
1556
+ else
1557
+ @lengths[update_positions[i]] = values[i]
1558
+ end
1559
+ end
1560
+ end
1561
+ end # of update_group_and_parents_length
1562
+
1563
+
1564
+ # Gathers and returns the window level values needed to convert the original pixel values to presentation values.
1565
+ def window_level_values
1566
+ center = get_value("0028,1050", :silent => true)
1567
+ width = get_value("0028,1051", :silent => true)
1568
+ intercept = get_value("0028,1052", :silent => true) || 0
1569
+ slope = get_value("0028,1053", :silent => true) || 1
1570
+ center = center.to_i if center
1571
+ width = width.to_i if width
1572
+ intercept = intercept.to_i
1573
+ slope = slope.to_i
1574
+ return center, width, intercept, slope
1575
+ end
1576
+
1577
+
1578
+ end # of class
1579
+ end # of module