dicom 0.7 → 0.8

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