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.
@@ -1,194 +0,0 @@
1
- # Copyright 2008-2010 Christoffer Lervag
2
-
3
- module DICOM
4
- # Class which holds the methods that interact with the DICOM dictionary.
5
- class DLibrary
6
-
7
- attr_reader :tags, :uid
8
-
9
- # Initialize the DRead instance.
10
- def initialize
11
- # Dictionary content will be stored in a number of hash objects.
12
- # Load the dictionary:
13
- dic = Dictionary.new
14
- # Data elements:
15
- # Value of this hash is a two-element array [vr, name] (where vr itself is an array of 1-3 elements)
16
- @tags = dic.load_data_elements
17
- # UID (DICOM unique identifiers):
18
- # Value of this hash is a two-element array [description, type]
19
- @uid = dic.load_uid
20
- # Photometric Interpretation: (not in use yet)
21
- #@image_types = dic.load_image_types
22
- # Value representation library: (not in use yet)
23
- #@vr = dic.load_vr
24
- # Frame of reference library: (not in use yet)
25
- #@frame_of_ref = dic.load_frame_of_ref
26
- end
27
-
28
-
29
- # Checks whether a given string is a valid transfer syntax or not.
30
- def check_ts_validity(uid)
31
- result = false
32
- value = @uid[uid.rstrip]
33
- if value
34
- if value[1] == "Transfer Syntax"
35
- # Proved valid:
36
- result = true
37
- end
38
- end
39
- return result
40
- end
41
-
42
-
43
- # Checks if the supplied transfer syntax indicates the presence of pixel compression or not.
44
- def get_compression(uid)
45
- result = false
46
- if uid
47
- value = @uid[uid.rstrip]
48
- if value
49
- if value[1] == "Transfer Syntax" and not value[0].include?("Endian")
50
- # It seems we have compression:
51
- result = true
52
- end
53
- end
54
- end
55
- return result
56
- end
57
-
58
-
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
- def get_name_vr(tag)
62
- if tag.private? and tag[5..8] != "0000"
63
- name = "Private"
64
- vr = "UN"
65
- else
66
- # Check the dictionary:
67
- values = @tags[tag]
68
- if values
69
- name = values[1]
70
- vr = values[0][0]
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"]
83
- name = values[1]
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"
114
- end
115
- end
116
- end
117
- return [name,vr]
118
- end
119
-
120
-
121
- # Returns the tag that matches the supplied data element name,
122
- # or if a tag is supplied, return that tag.
123
- # (This method may be considered for removal: Does the usefulnes of being able to create a tag by Name,
124
- # outweigh the performance impact of having this method?)
125
- def get_tag(name)
126
- tag = false
127
- # The supplied value should be a string:
128
- if name.is_a?(String)
129
- if name.is_a_tag?
130
- # This is a tag:
131
- tag = name
132
- else
133
- # We have presumably been dealt a name. Search the dictionary to see if we can identify
134
- # this name and return its corresponding tag:
135
- @tags.each_pair do |key, value|
136
- if value[1] == name
137
- tag = key
138
- end
139
- end
140
- end
141
- end
142
- return tag
143
- end
144
-
145
-
146
- # Returns the name/description corresponding to a given UID.
147
- def get_uid(uid)
148
- value = @uid[uid.rstrip]
149
- # Fetch the name of this UID:
150
- if value
151
- name = value[0]
152
- else
153
- name = "Unknown UID!"
154
- end
155
- return name
156
- end
157
-
158
-
159
- # Checks the Transfer Syntax UID and return the encoding settings associated with this value.
160
- def process_transfer_syntax(value)
161
- valid = check_ts_validity(value)
162
- case value
163
- # Some variations with uncompressed pixel data:
164
- when "1.2.840.10008.1.2"
165
- # Implicit VR, Little Endian
166
- explicit = false
167
- endian = false
168
- when "1.2.840.10008.1.2.1"
169
- # Explicit VR, Little Endian
170
- explicit = true
171
- endian = false
172
- when "1.2.840.10008.1.2.1.99"
173
- # Deflated Explicit VR, Little Endian
174
- #@msg += ["Warning: Transfer syntax 'Deflated Explicit VR, Little Endian' is untested. Unknown if this is handled correctly!"]
175
- explicit = true
176
- endian = false
177
- when "1.2.840.10008.1.2.2"
178
- # Explicit VR, Big Endian
179
- explicit = true
180
- endian = true
181
- else
182
- # For everything else, assume compressed pixel data, with Explicit VR, Little Endian:
183
- explicit = true
184
- endian = false
185
- end
186
- return [valid, explicit, endian]
187
- end
188
-
189
-
190
- # Following methods are private.
191
- #private
192
-
193
- end # of class
194
- end # of module
data/lib/dicom/DObject.rb DELETED
@@ -1,1579 +0,0 @@
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