dicom 0.4 → 0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +47 -25
- data/DOCUMENTATION +72 -43
- data/README +20 -20
- data/lib/Anonymizer.rb +61 -56
- data/lib/DLibrary.rb +47 -24
- data/lib/DObject.rb +457 -461
- data/lib/DRead.rb +131 -130
- data/lib/DWrite.rb +87 -89
- data/lib/Dictionary.rb +14 -12
- data/lib/dicom.rb +1 -0
- data/lib/ruby_extensions.rb +25 -0
- metadata +12 -9
data/lib/DLibrary.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
# Copyright 2008-2009 Christoffer
|
1
|
+
# Copyright 2008-2009 Christoffer Lervag
|
2
2
|
|
3
3
|
module DICOM
|
4
4
|
# Class which holds the methods that interact with the DICOM dictionary.
|
5
5
|
class DLibrary
|
6
6
|
|
7
|
-
attr_reader :
|
7
|
+
attr_reader :de_tag, :de_vr, :de_name, :uid_value, :uid_name, :uid_type, :pi_type, :pi_description
|
8
8
|
|
9
9
|
# Initialize the DRead instance.
|
10
10
|
def initialize()
|
@@ -12,8 +12,8 @@ module DICOM
|
|
12
12
|
# Load the dictionary:
|
13
13
|
dict = Dictionary.new()
|
14
14
|
# Data elements:
|
15
|
-
de = dict.
|
16
|
-
@
|
15
|
+
de = dict.load_data_elements()
|
16
|
+
@de_tag = de[0]
|
17
17
|
@de_vr = de[1]
|
18
18
|
@de_name = de[2]
|
19
19
|
# Photometric Interpretation:
|
@@ -28,37 +28,37 @@ module DICOM
|
|
28
28
|
end
|
29
29
|
|
30
30
|
|
31
|
-
# Returns data element name and value representation from library if
|
32
|
-
def get_name_vr(
|
33
|
-
pos = get_pos(
|
31
|
+
# Returns data element name and value representation from library if data element is recognized, else it returns "Unknown Name" and "UN".
|
32
|
+
def get_name_vr(tag)
|
33
|
+
pos = get_pos(tag)
|
34
34
|
if pos != nil
|
35
35
|
name = @de_name[pos]
|
36
36
|
vr = @de_vr[pos][0]
|
37
37
|
else
|
38
|
-
# For the
|
39
|
-
# Split
|
40
|
-
group =
|
41
|
-
element =
|
38
|
+
# For the tags that are not recognised, we need to do some additional testing to see if it is one of the special cases:
|
39
|
+
# Split tag in group and element:
|
40
|
+
group = tag[0..3]
|
41
|
+
element = tag[5..8]
|
42
42
|
# Check for group length:
|
43
43
|
if element == "0000"
|
44
44
|
name = "Group Length"
|
45
45
|
vr = "UL"
|
46
46
|
end
|
47
47
|
# Source Image ID's: (Retired)
|
48
|
-
if
|
48
|
+
if tag[0..6] == "0020,31"
|
49
49
|
pos = get_pos("0020,31xx")
|
50
50
|
name = @de_name[pos]
|
51
51
|
vr = @de_vr[pos][0]
|
52
52
|
end
|
53
53
|
# Group 50xx (retired) and 60xx:
|
54
|
-
if
|
55
|
-
pos = get_pos(
|
54
|
+
if tag[0..1] == "50" or tag[0..1] == "60"
|
55
|
+
pos = get_pos(tag[0..1]+"xx"+tag[4..8])
|
56
56
|
if pos != nil
|
57
57
|
name = @de_name[pos]
|
58
58
|
vr = @de_vr[pos][0]
|
59
59
|
end
|
60
60
|
end
|
61
|
-
# If none of the above checks yielded a result, the
|
61
|
+
# If none of the above checks yielded a result, the tag is unknown:
|
62
62
|
if name == nil
|
63
63
|
name = "Unknown Name"
|
64
64
|
vr = "UN"
|
@@ -68,17 +68,40 @@ module DICOM
|
|
68
68
|
end
|
69
69
|
|
70
70
|
|
71
|
+
# Returns the tag that matches the supplied data element name,
|
72
|
+
# or if a tag is supplied, return that tag.
|
73
|
+
def get_tag(value)
|
74
|
+
tag = false
|
75
|
+
# The supplied value should be a string:
|
76
|
+
if value.is_a?(String)
|
77
|
+
# Is it a tag?
|
78
|
+
# A tag is a string with 9 characters, where the 5th character should be a comma.
|
79
|
+
if value[4..4] == ',' and value.length == 9
|
80
|
+
# This is a tag.
|
81
|
+
# (Here it is possible to have some further logic to check the validity of the string as a tag.)
|
82
|
+
tag = value
|
83
|
+
else
|
84
|
+
# We have presumably been dealt a name. Search the dictionary to see if we can identify
|
85
|
+
# it along with its corresponding tag:
|
86
|
+
pos = @de_name.index(value)
|
87
|
+
tag = @de_tag[pos] unless pos == nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
return tag
|
91
|
+
end
|
92
|
+
|
93
|
+
|
71
94
|
# Checks whether a given string is a valid transfer syntax or not.
|
72
|
-
def check_ts_validity(
|
73
|
-
|
74
|
-
pos = @uid_value.index(
|
95
|
+
def check_ts_validity(value)
|
96
|
+
result = false
|
97
|
+
pos = @uid_value.index(value)
|
75
98
|
if pos != nil
|
76
99
|
if pos >= 1 and pos <= 34
|
77
100
|
# Proved valid:
|
78
|
-
|
101
|
+
result = true
|
79
102
|
end
|
80
103
|
end
|
81
|
-
return
|
104
|
+
return result
|
82
105
|
end
|
83
106
|
|
84
107
|
|
@@ -94,8 +117,8 @@ module DICOM
|
|
94
117
|
end
|
95
118
|
return name
|
96
119
|
end
|
97
|
-
|
98
|
-
|
120
|
+
|
121
|
+
|
99
122
|
# Checks if the supplied transfer syntax indicates the presence of pixel compression or not.
|
100
123
|
def get_compression(value)
|
101
124
|
res = false
|
@@ -115,8 +138,8 @@ module DICOM
|
|
115
138
|
private
|
116
139
|
|
117
140
|
# Returns the position of the supplied data element name in the Dictionary array.
|
118
|
-
def get_pos(
|
119
|
-
pos = @
|
141
|
+
def get_pos(tag)
|
142
|
+
pos = @de_tag.index(tag)
|
120
143
|
return pos
|
121
144
|
end
|
122
145
|
|
data/lib/DObject.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2008-2009 Christoffer
|
1
|
+
# Copyright 2008-2009 Christoffer Lervag
|
2
2
|
#
|
3
3
|
# This program is free software: you can redistribute it and/or modify
|
4
4
|
# it under the terms of the GNU General Public License as published by
|
@@ -29,7 +29,8 @@ module DICOM
|
|
29
29
|
# Class for handling the DICOM contents:
|
30
30
|
class DObject
|
31
31
|
|
32
|
-
attr_reader :read_success, :write_success, :modality
|
32
|
+
attr_reader :read_success, :write_success, :modality, :errors,
|
33
|
+
:names, :tags, :types, :lengths, :values, :raw, :levels
|
33
34
|
|
34
35
|
# Initialize the DObject instance.
|
35
36
|
def initialize(file_name=nil, opts={})
|
@@ -38,20 +39,20 @@ module DICOM
|
|
38
39
|
@lib = opts[:lib] || DLibrary.new
|
39
40
|
# Default verbosity is true:
|
40
41
|
@verbose = true if @verbose == nil
|
41
|
-
|
42
|
+
|
42
43
|
# Initialize variables that will be used for the DICOM object:
|
43
44
|
@names = Array.new()
|
44
|
-
@
|
45
|
+
@tags = Array.new()
|
45
46
|
@types = Array.new()
|
46
47
|
@lengths = Array.new()
|
47
48
|
@values = Array.new()
|
48
49
|
@raw = Array.new()
|
49
50
|
@levels = Array.new()
|
50
51
|
# Array that will holde any messages generated while reading the DICOM file:
|
51
|
-
@
|
52
|
-
# Array to keep track of sequences/structure of the dicom
|
52
|
+
@errors = Array.new()
|
53
|
+
# Array to keep track of sequences/structure of the dicom elements:
|
53
54
|
@sequence = Array.new()
|
54
|
-
# Index of last element in
|
55
|
+
# Index of last element in data element arrays:
|
55
56
|
@last_index=0
|
56
57
|
# Structural information (default values):
|
57
58
|
@compression = false
|
@@ -66,11 +67,11 @@ module DICOM
|
|
66
67
|
@sys_endian = check_sys_endian()
|
67
68
|
# Set format strings for packing/unpacking:
|
68
69
|
set_format_strings()
|
69
|
-
|
70
|
+
|
70
71
|
# If a (valid) file name string is supplied, call the method to read the DICOM file:
|
71
|
-
if file_name
|
72
|
+
if file_name.is_a?(String) and file_name != ""
|
72
73
|
@file = file_name
|
73
|
-
|
74
|
+
read(file_name)
|
74
75
|
end
|
75
76
|
end # of method initialize
|
76
77
|
|
@@ -79,13 +80,13 @@ module DICOM
|
|
79
80
|
# This is accomplished by initliazing the DRead class, which loads DICOM information to arrays.
|
80
81
|
# For the time being, this method is called automatically when initializing the DObject class,
|
81
82
|
# but in the future, when write support is added, this method may have to be called manually.
|
82
|
-
def
|
83
|
+
def read(file_name)
|
83
84
|
dcm = DRead.new(file_name, :lib => @lib, :sys_endian => @sys_endian)
|
84
85
|
# Store the data to the instance variables if the readout was a success:
|
85
86
|
if dcm.success
|
86
87
|
@read_success = true
|
87
88
|
@names = dcm.names
|
88
|
-
@
|
89
|
+
@tags = dcm.tags
|
89
90
|
@types = dcm.types
|
90
91
|
@lengths = dcm.lengths
|
91
92
|
@values = dcm.values
|
@@ -95,7 +96,7 @@ module DICOM
|
|
95
96
|
@file_endian = dcm.file_endian
|
96
97
|
# Set format strings for packing/unpacking:
|
97
98
|
set_format_strings(@file_endian)
|
98
|
-
# Index of last element in
|
99
|
+
# Index of last data element in element arrays:
|
99
100
|
@last_index=@names.length-1
|
100
101
|
# Update status variables for this object:
|
101
102
|
check_properties()
|
@@ -109,13 +110,13 @@ module DICOM
|
|
109
110
|
add_msg(dcm.msg)
|
110
111
|
end
|
111
112
|
end
|
112
|
-
|
113
|
-
|
113
|
+
|
114
|
+
|
114
115
|
# Transfers necessary information from the DObject to the DWrite class, which
|
115
116
|
# will attempt to write this information to a valid DICOM file.
|
116
|
-
def
|
117
|
+
def write(file_name)
|
117
118
|
w = DWrite.new(file_name, :lib => @lib, :sys_endian => @sys_endian)
|
118
|
-
w.
|
119
|
+
w.tags = @tags
|
119
120
|
w.types = @types
|
120
121
|
w.lengths = @lengths
|
121
122
|
w.raw = @raw
|
@@ -129,8 +130,8 @@ module DICOM
|
|
129
130
|
add_msg(w.msg)
|
130
131
|
end
|
131
132
|
end
|
132
|
-
|
133
|
-
|
133
|
+
|
134
|
+
|
134
135
|
#################################################
|
135
136
|
# START OF METHODS FOR READING INFORMATION FROM DICOM OBJECT:#
|
136
137
|
#################################################
|
@@ -140,14 +141,14 @@ module DICOM
|
|
140
141
|
# Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
|
141
142
|
def check_properties()
|
142
143
|
# Check if pixel data is present:
|
143
|
-
if @
|
144
|
+
if @tags.index("7FE0,0010") == nil
|
144
145
|
# No pixel data in DICOM file:
|
145
146
|
@compression = nil
|
146
147
|
else
|
147
|
-
@compression = @lib.get_compression(get_value("0002,0010"))
|
148
|
+
@compression = @lib.get_compression(get_value("0002,0010", :silent => true))
|
148
149
|
end
|
149
150
|
# Set color variable as true if our object contain a color image:
|
150
|
-
col_string = get_value("0028,0004")
|
151
|
+
col_string = get_value("0028,0004", :silent => true)
|
151
152
|
if col_string != false
|
152
153
|
if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
|
153
154
|
@color = true
|
@@ -156,10 +157,14 @@ module DICOM
|
|
156
157
|
end
|
157
158
|
|
158
159
|
|
159
|
-
# Returns image data from the provided
|
160
|
+
# Returns image data from the provided element index, performing decompression of data if necessary.
|
160
161
|
def read_image_magick(pos, columns, rows)
|
162
|
+
if pos == false or columns == false or rows == false
|
163
|
+
add_msg("Error: Method read_image_magick() does not have enough data available to build an image object.")
|
164
|
+
return false
|
165
|
+
end
|
161
166
|
if @compression != true
|
162
|
-
# Non-compressed, just return the array contained
|
167
|
+
# Non-compressed, just return the array contained on the particular element:
|
163
168
|
image_data=get_pixels(pos)
|
164
169
|
image = Magick::Image.new(columns,rows)
|
165
170
|
image.import_pixels(0, 0, columns, rows, "I", image_data)
|
@@ -204,9 +209,9 @@ module DICOM
|
|
204
209
|
image = NArray.int(frames,columns,rows)
|
205
210
|
image_temp = NArray.int(columns,rows)
|
206
211
|
# Handling of image data will depend on whether we have one or more frames,
|
207
|
-
# and if it is located in one or more
|
212
|
+
# and if it is located in one or more elements:
|
208
213
|
if image_pos.size == 1
|
209
|
-
# All of the image data is located in one
|
214
|
+
# All of the image data is located in one element:
|
210
215
|
image_data = get_pixels(image_pos[0])
|
211
216
|
#image_data = get_image_data(image_pos[0])
|
212
217
|
(0..frames-1).each do |i|
|
@@ -264,7 +269,7 @@ module DICOM
|
|
264
269
|
image_arr = Array.new(frames)
|
265
270
|
# Handling of image data will depend on whether we have one or more frames,
|
266
271
|
if image_pos.size == 1
|
267
|
-
# All of the image data is located in one
|
272
|
+
# All of the image data is located in one element:
|
268
273
|
#image_data = get_image_data(image_pos[0])
|
269
274
|
#(0..frames-1).each do |i|
|
270
275
|
# image = Magick::Image.new(columns,rows)
|
@@ -295,14 +300,14 @@ module DICOM
|
|
295
300
|
def get_frames()
|
296
301
|
frames = get_value("0028,0008")
|
297
302
|
if frames == false
|
298
|
-
# If
|
303
|
+
# If the DICOM object does not specify the number of frames explicitly, assume 1 image frame.
|
299
304
|
frames = 1
|
300
305
|
end
|
301
306
|
return frames.to_i
|
302
307
|
end
|
303
|
-
|
304
|
-
|
305
|
-
# Unpacks and returns pixel data from a specified
|
308
|
+
|
309
|
+
|
310
|
+
# Unpacks and returns pixel data from a specified data element array position:
|
306
311
|
def get_pixels(pos)
|
307
312
|
pixels = false
|
308
313
|
# We need to know what kind of bith depth the pixel data is saved with:
|
@@ -332,32 +337,32 @@ module DICOM
|
|
332
337
|
raise "Bit depth ["+bit_depth.to_s+"] has not received implementation in this procedure yet. Please contact the author."
|
333
338
|
end # of case bit_depth
|
334
339
|
else
|
335
|
-
add_msg("Error: DICOM object does not contain
|
340
|
+
add_msg("Error: DICOM object does not contain the 'Bit Depth' data element (0028,0010).")
|
336
341
|
end # of if bit_depth ..
|
337
342
|
return pixels
|
338
343
|
end # of method get_pixels
|
339
344
|
|
340
345
|
|
341
|
-
# Returns the index(es) of the
|
346
|
+
# Returns the index(es) of the element(s) that contain image data.
|
342
347
|
def get_image_pos()
|
343
|
-
|
348
|
+
image_element_pos = get_pos("7FE0,0010")
|
344
349
|
item_pos = get_pos("FFFE,E000")
|
345
|
-
# Proceed only if image
|
346
|
-
if
|
350
|
+
# Proceed only if an image element actually exists:
|
351
|
+
if image_element_pos == false
|
347
352
|
return false
|
348
353
|
else
|
349
|
-
# Check if we have item
|
354
|
+
# Check if we have item elements:
|
350
355
|
if item_pos == false
|
351
|
-
return
|
356
|
+
return image_element_pos
|
352
357
|
else
|
353
|
-
# Extract item positions that occur after the image
|
354
|
-
late_item_pos = item_pos.select {|item|
|
355
|
-
# Check if there are items appearing after the image
|
358
|
+
# Extract item positions that occur after the image element position:
|
359
|
+
late_item_pos = item_pos.select {|item| image_element_pos[0] < item}
|
360
|
+
# Check if there are items appearing after the image element.
|
356
361
|
if late_item_pos.size == 0
|
357
|
-
# None occured after the image
|
358
|
-
return
|
362
|
+
# None occured after the image element position:
|
363
|
+
return image_element_pos
|
359
364
|
else
|
360
|
-
# Determine which of these late item
|
365
|
+
# Determine which of these late item elements contain image data.
|
361
366
|
# Usually, there are frames+1 late items, and all except
|
362
367
|
# the first item contain an image frame:
|
363
368
|
frames = get_frames()
|
@@ -369,7 +374,7 @@ module DICOM
|
|
369
374
|
return false
|
370
375
|
end
|
371
376
|
else
|
372
|
-
add_msg("Warning: Number of
|
377
|
+
add_msg("Warning: 'Number of Frames' data element not found. Method get_image_pos() will return false.")
|
373
378
|
return false
|
374
379
|
end
|
375
380
|
end
|
@@ -378,58 +383,81 @@ module DICOM
|
|
378
383
|
end # of method get_image_pos
|
379
384
|
|
380
385
|
|
381
|
-
# Returns an array of the index(es) of the
|
386
|
+
# Returns an array of the index(es) of the element(s) in the DICOM file that match the supplied element position, tag or name.
|
382
387
|
# If no match is found, the method will return false.
|
383
388
|
# Additional options:
|
384
|
-
# :array => myArray - tells the method to search for
|
385
|
-
# through the entire DICOM
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
# Either use the supplied array, or we will create an array that contain the indices of the entire DICOM object:
|
399
|
-
if opt_array.is_a?(Array)
|
400
|
-
search_array=opt_array
|
389
|
+
# :array => myArray - tells the method to search for matches in this specific array of positions instead of searching
|
390
|
+
# through the entire DICOM object. If myArray equals false, the method will return false.
|
391
|
+
# :partial => true - get_pos will not only search for exact matches, but will search the names and tags arrays for
|
392
|
+
# strings that contain the given search string.
|
393
|
+
def get_pos(query, opts={})
|
394
|
+
# Optional keywords:
|
395
|
+
keyword_array = opts[:array]
|
396
|
+
keyword_partial = opts[:partial]
|
397
|
+
indexes = Array.new()
|
398
|
+
# For convenience, allow query to be a one-element array (its value will be extracted):
|
399
|
+
if query.is_a?(Array)
|
400
|
+
if query.length > 1 or query.length == 0
|
401
|
+
add_msg("Invalid array length supplied to method get_pos.")
|
402
|
+
return false
|
401
403
|
else
|
402
|
-
|
404
|
+
query = query[0]
|
403
405
|
end
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
406
|
+
end
|
407
|
+
if keyword_array == false
|
408
|
+
# If the supplied array option equals false, it signals that the user tries to search for an element
|
409
|
+
# in an invalid position, and as such, this method will also return false:
|
410
|
+
add_msg("Warning: Attempted to call get_pos() with query #{query}, but since keyword :array is false I will return false.")
|
411
|
+
indexes = false
|
412
|
+
else
|
413
|
+
# Check if query is a number (some methods want to have the ability to call get_pos() with a number):
|
414
|
+
if query.is_a?(Integer)
|
415
|
+
# Return the position if it is valid:
|
416
|
+
indexes = [query] if query >= 0 and query < @names.length
|
417
|
+
elsif query.is_a?(String)
|
418
|
+
# Either use the supplied array, or search the entire DICOM object:
|
419
|
+
if keyword_array.is_a?(Array)
|
420
|
+
search_array = keyword_array
|
421
|
+
else
|
422
|
+
search_array = Array.new(@names.length) {|i| i}
|
423
|
+
end
|
424
|
+
# Perform search:
|
425
|
+
if keyword_partial == true
|
426
|
+
# Search for partial string matches:
|
427
|
+
partial_indexes = search_array.all_indices_partial_match(@tags, query.upcase)
|
428
|
+
if partial_indexes.length > 0
|
429
|
+
indexes = partial_indexes
|
430
|
+
else
|
431
|
+
indexes = search_array.all_indices_partial_match(@names, query)
|
432
|
+
end
|
433
|
+
else
|
434
|
+
# Search for identical matches:
|
435
|
+
if query[4..4] == ","
|
436
|
+
indexes = search_array.all_indices(@tags, query.upcase)
|
437
|
+
else
|
438
|
+
indexes = search_array.all_indices(@names, query)
|
439
|
+
end
|
412
440
|
end
|
413
441
|
end
|
414
|
-
# If no
|
415
|
-
indexes = false if indexes.
|
442
|
+
# Policy: If no matches found, return false instead of an empty array:
|
443
|
+
indexes = false if indexes.length == 0
|
416
444
|
end
|
417
445
|
return indexes
|
418
446
|
end # of method get_pos
|
419
|
-
|
420
|
-
|
421
|
-
# Dumps the binary content of the Pixel Data
|
447
|
+
|
448
|
+
|
449
|
+
# Dumps the binary content of the Pixel Data element to file.
|
422
450
|
def image_to_file(file)
|
423
451
|
pos = get_image_pos()
|
424
452
|
if pos
|
425
453
|
if pos.length == 1
|
426
|
-
# Pixel data located in one
|
454
|
+
# Pixel data located in one element:
|
427
455
|
pixel_data = get_raw(pos[0])
|
428
456
|
f = File.new(file, "wb")
|
429
457
|
f.write(pixel_data)
|
430
458
|
f.close()
|
431
459
|
else
|
432
|
-
# Pixel data located in several
|
460
|
+
# Pixel data located in several elements:
|
433
461
|
pos.each_index do |i|
|
434
462
|
pixel_data = get_raw(pos[i])
|
435
463
|
f = File.new(file + i.to_s, "wb")
|
@@ -441,159 +469,161 @@ module DICOM
|
|
441
469
|
end # of method image_to_file
|
442
470
|
|
443
471
|
|
444
|
-
# Returns the positions of all
|
472
|
+
# Returns the positions of all data elements inside the hierarchy of a sequence or an item.
|
445
473
|
# Options:
|
446
474
|
# :next_only => true - The method will only search immediately below the specified
|
447
475
|
# item or sequence (that is, in the level of parent + 1).
|
448
|
-
def children(
|
476
|
+
def children(element, opts={})
|
449
477
|
# Process option values, setting defaults for the ones that are not specified:
|
450
478
|
opt_next_only = opts[:next_only] || false
|
451
|
-
|
452
|
-
|
479
|
+
value = false
|
480
|
+
# Retrieve array position:
|
481
|
+
pos = get_pos(element)
|
453
482
|
if pos == false
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
483
|
+
add_msg("Warning: Invalid data element provided to method children(). Returning false.")
|
484
|
+
else
|
485
|
+
if pos.size > 1
|
486
|
+
add_msg("Warning: Method children() does not allow a query which yields multiple array hits. Please use array position instead of tag/name. Returning false.")
|
487
|
+
else
|
488
|
+
# Proceed to find the value:
|
489
|
+
# First we need to establish in which positions to perform the search:
|
490
|
+
below_pos = Array.new()
|
491
|
+
pos.each do |p|
|
492
|
+
parent_level = @levels[p]
|
493
|
+
remain_array = @levels[p+1..@levels.size-1]
|
494
|
+
extract = true
|
495
|
+
remain_array.each_index do |i|
|
496
|
+
if (remain_array[i] > parent_level) and (extract == true)
|
497
|
+
# If search is targetted at any specific level, we can just add this position:
|
498
|
+
if not opt_next_only == true
|
499
|
+
below_pos += [p+1+i]
|
500
|
+
else
|
501
|
+
# As search is restricted to parent level + 1, do a test for this:
|
502
|
+
if remain_array[i] == parent_level + 1
|
503
|
+
below_pos += [p+1+i]
|
504
|
+
end
|
505
|
+
end
|
506
|
+
else
|
507
|
+
# If we encounter a position who's level is not deeper than the original level, we can not extract any more values:
|
508
|
+
extract = false
|
474
509
|
end
|
475
510
|
end
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
end
|
480
|
-
end
|
481
|
-
end
|
482
|
-
# Positions to search in have been established, now we can perform the actual search:
|
483
|
-
if below_pos.size == 0
|
484
|
-
return false
|
485
|
-
else
|
486
|
-
return below_pos
|
511
|
+
end # of pos.each do..
|
512
|
+
value = below_pos if below_pos.size != 0
|
513
|
+
end # of if pos.size..else..
|
487
514
|
end
|
515
|
+
return value
|
488
516
|
end # of method below
|
489
517
|
|
490
518
|
|
491
|
-
# Returns the value (processed raw data) of the DICOM
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
519
|
+
# Returns the value (processed raw data) of the requested DICOM data element.
|
520
|
+
# Data element may be specified by array position, tag or name.
|
521
|
+
# Options:
|
522
|
+
# :array => true - Allows the query of the value of a tag that occurs more than one time in the
|
523
|
+
# DICOM object. Values will be returned in an array with length equal to the number
|
524
|
+
# of occurances of the tag. If keyword is not specified, the method returns false in this case.
|
525
|
+
def get_value(element, opts={})
|
526
|
+
opts_array = opts[:array]
|
527
|
+
# As this method is also used internally, we want the possibility of warnings not being raised even if verbose is set to true by the user, to avoid confusion.
|
528
|
+
silent = opts[:silent]
|
529
|
+
value = false
|
530
|
+
# Retrieve array position:
|
531
|
+
pos = get_pos(element)
|
532
|
+
if pos == false
|
533
|
+
add_msg("Warning: Invalid data element provided to method get_value(). Returning false.") unless silent
|
497
534
|
else
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
# If we still dont have a hit, check if it is a valid number within the array range:
|
505
|
-
if pos == nil
|
506
|
-
if (id.is_a? Integer)
|
507
|
-
if id >= 0 and id <= @last_index
|
508
|
-
# The id supplied is a valid position, return its corresponding value:
|
509
|
-
return @values[id]
|
510
|
-
else
|
511
|
-
return false
|
535
|
+
if pos.size > 1
|
536
|
+
if opts_array == true
|
537
|
+
# Retrieve all values into an array:
|
538
|
+
value = []
|
539
|
+
pos.each do |i|
|
540
|
+
value << @values[i]
|
512
541
|
end
|
513
542
|
else
|
514
|
-
|
543
|
+
add_msg("Warning: Method get_value() does not allow a query which yields multiple array hits. Please use array position instead of tag/name, or use keyword (:array => true). Returning false.") unless silent
|
515
544
|
end
|
516
545
|
else
|
517
|
-
|
518
|
-
return @values[pos]
|
546
|
+
value = @values[pos[0]]
|
519
547
|
end
|
520
548
|
end
|
549
|
+
return value
|
521
550
|
end # of method get_value
|
522
551
|
|
523
552
|
|
524
|
-
# Returns the raw data of the DICOM
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
553
|
+
# Returns the raw data of the requested DICOM data element.
|
554
|
+
# Data element may be specified by array position, tag or name.
|
555
|
+
# Options:
|
556
|
+
# :array => true - Allows the query of the value of a tag that occurs more than one time in the
|
557
|
+
# DICOM object. Values will be returned in an array with length equal to the number
|
558
|
+
# of occurances of the tag. If keyword is not specified, the method returns false in this case.
|
559
|
+
def get_raw(element, opts={})
|
560
|
+
opts_array = opts[:array]
|
561
|
+
value = false
|
562
|
+
# Retrieve array position:
|
563
|
+
pos = get_pos(element)
|
564
|
+
if pos == false
|
565
|
+
add_msg("Warning: Invalid data element provided to method get_raw(). Returning false.")
|
530
566
|
else
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
# If we still dont have a hit, check if it is a valid number within the array range:
|
538
|
-
if pos == nil
|
539
|
-
if (id.is_a? Integer)
|
540
|
-
if id >= 0 and id <= @last_index
|
541
|
-
# The id supplied is a valid position, return its corresponding value:
|
542
|
-
return @raw[id]
|
543
|
-
else
|
544
|
-
return false
|
567
|
+
if pos.size > 1
|
568
|
+
if opts_array == true
|
569
|
+
# Retrieve all values into an array:
|
570
|
+
value = []
|
571
|
+
pos.each do |i|
|
572
|
+
value << @raw[i]
|
545
573
|
end
|
546
574
|
else
|
547
|
-
|
575
|
+
add_msg("Warning: Method get_raw() does not allow a query which yields multiple array hits. Please use array position instead of tag/name, or use keyword (:array => true). Returning false.")
|
548
576
|
end
|
549
577
|
else
|
550
|
-
|
551
|
-
return @raw[pos]
|
578
|
+
value = @raw[pos[0]]
|
552
579
|
end
|
553
580
|
end
|
581
|
+
return value
|
554
582
|
end # of method get_raw
|
555
|
-
|
556
|
-
|
557
|
-
# Returns the position of (possible) parents of the specified
|
558
|
-
def parents(
|
559
|
-
|
560
|
-
|
583
|
+
|
584
|
+
|
585
|
+
# Returns the position of (possible) parents of the specified data element in the hierarchy structure of the DICOM object.
|
586
|
+
def parents(element)
|
587
|
+
value = false
|
588
|
+
# Retrieve array position:
|
589
|
+
pos = get_pos(element)
|
561
590
|
if pos == false
|
562
|
-
parents
|
591
|
+
add_msg("Warning: Invalid data element provided to method parents(). Returning false.")
|
563
592
|
else
|
564
|
-
|
565
|
-
|
566
|
-
# Get level of our tag:
|
567
|
-
level = @levels[pos]
|
568
|
-
# If tag is top level it can obviously have no parents:
|
569
|
-
if level == 0
|
570
|
-
parents = false
|
593
|
+
if pos.length > 1
|
594
|
+
add_msg("Warning: Method parents() does not allow a query which yields multiple array hits. Please use array position instead of tag/name. Returning false.")
|
571
595
|
else
|
572
|
-
#
|
573
|
-
#
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
596
|
+
# Proceed to find the value:
|
597
|
+
# Get the level of our element:
|
598
|
+
level = @levels[pos[0]]
|
599
|
+
# Element can obviously only have parents if it is not a top level element:
|
600
|
+
unless level == 0
|
601
|
+
# Search backwards, and record the position every time we encounter an upwards change in the level number.
|
602
|
+
parents = Array.new()
|
603
|
+
prev_level = level
|
604
|
+
search_arr = @levels[0..pos[0]-1].reverse
|
605
|
+
search_arr.each_index do |i|
|
606
|
+
if search_arr[i] < prev_level
|
607
|
+
parents += [search_arr.length-i-1]
|
608
|
+
prev_level = search_arr[i]
|
609
|
+
end
|
581
610
|
end
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
611
|
+
# When the element has several generations of parents, we want its top parent to be first in the returned array:
|
612
|
+
parents = parents.reverse
|
613
|
+
value = parents if parents.length > 0
|
614
|
+
end # of if level == 0
|
615
|
+
end # of if pos.length..else..
|
616
|
+
end
|
617
|
+
return value
|
588
618
|
end # of method parents
|
589
|
-
|
590
|
-
|
619
|
+
|
620
|
+
|
591
621
|
##############################################
|
592
622
|
####### START OF METHODS FOR PRINTING INFORMATION:######
|
593
623
|
##############################################
|
594
624
|
|
595
625
|
|
596
|
-
# Prints information of all
|
626
|
+
# Prints the information of all elements stored in the DICOM object.
|
597
627
|
# This method is kept for backwards compatibility.
|
598
628
|
# Instead of calling print_all() you may use print(true) for the same functionality.
|
599
629
|
def print_all()
|
@@ -601,98 +631,86 @@ module DICOM
|
|
601
631
|
end
|
602
632
|
|
603
633
|
|
604
|
-
# Prints the
|
605
|
-
# The supplied variable may be a single position, an array of positions, or true - which will make the method print all
|
606
|
-
#
|
607
|
-
#
|
608
|
-
# :
|
609
|
-
#
|
610
|
-
# :file => true - will make the method print to file instead of printing to screen.
|
634
|
+
# Prints the information of the specified elements: Index, [hierarchy level, tree visualisation,] tag, name, type, length, value
|
635
|
+
# The supplied variable may be a single position, an array of positions, or true - which will make the method print all elements.
|
636
|
+
# Optional arguments:
|
637
|
+
# :levels => true - method will print the level numbers for each element.
|
638
|
+
# :tree => true - method will print a tree structure for the elements.
|
639
|
+
# :file => true - method will print to file instead of printing to screen.
|
611
640
|
def print(pos, opts={})
|
612
641
|
# Process option values, setting defaults for the ones that are not specified:
|
613
642
|
opt_levels = opts[:levels] || false
|
614
643
|
opt_tree = opts[:tree] || false
|
615
|
-
opt_file = opts[:file] || false
|
616
|
-
#
|
644
|
+
opt_file = opts[:file] || false
|
645
|
+
# If pos is false, abort, and inform the user:
|
646
|
+
if pos == false
|
647
|
+
add_msg("Warning: Method print() was supplied false instead of a valid position. Aborting print.")
|
648
|
+
return
|
649
|
+
end
|
617
650
|
if not pos.is_a?(Array) and pos != true
|
618
|
-
|
651
|
+
# Convert to array if number:
|
652
|
+
pos_valid = [pos]
|
619
653
|
elsif pos == true
|
620
|
-
# Create
|
621
|
-
pos_valid = Array.new(@names.length)
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
end
|
626
|
-
else
|
627
|
-
# Check that the supplied array contains valid positions:
|
628
|
-
pos_valid = Array.new()
|
629
|
-
pos.each_index do |i|
|
630
|
-
if pos[i] >= 0 and pos[i] <= @names.length
|
631
|
-
pos_valid += [pos[i]]
|
632
|
-
end
|
633
|
-
end
|
634
|
-
end
|
635
|
-
# Continue only if we have valid positions:
|
636
|
-
if pos_valid == false
|
637
|
-
return
|
638
|
-
elsif pos_valid.size == 0
|
639
|
-
return
|
654
|
+
# Create a complete array of indices:
|
655
|
+
pos_valid = Array.new(@names.length) {|i| i}
|
656
|
+
else
|
657
|
+
# Use the supplied array of numbers:
|
658
|
+
pos_valid = pos
|
640
659
|
end
|
641
|
-
# We have valid positions and are ready to start process the tags:
|
642
660
|
# Extract the information to be printed from the object arrays:
|
643
661
|
indices = Array.new()
|
644
662
|
levels = Array.new()
|
645
|
-
|
663
|
+
tags = Array.new()
|
646
664
|
names = Array.new()
|
647
665
|
types = Array.new()
|
648
666
|
lengths = Array.new()
|
649
667
|
values = Array.new()
|
650
668
|
# There may be a more elegant way to do this.
|
651
669
|
pos_valid.each do |pos|
|
652
|
-
|
670
|
+
tags += [@tags[pos]]
|
653
671
|
levels += [@levels[pos]]
|
654
672
|
names += [@names[pos]]
|
655
673
|
types += [@types[pos]]
|
656
674
|
lengths += [@lengths[pos].to_s]
|
657
675
|
values += [@values[pos].to_s]
|
658
|
-
end
|
676
|
+
end
|
659
677
|
# We have collected the data that is to be printed, now we need to do some string manipulation if hierarchy is to be displayed:
|
660
678
|
if opt_tree
|
661
679
|
# Tree structure requested.
|
662
680
|
front_symbol = "| "
|
663
681
|
tree_symbol = "|_"
|
664
|
-
|
682
|
+
tags.each_index do |i|
|
665
683
|
if levels[i] != 0
|
666
|
-
|
684
|
+
tags[i] = front_symbol*(levels[i]-1) + tree_symbol + tags[i]
|
667
685
|
end
|
668
686
|
end
|
669
687
|
end
|
670
688
|
# Extract the string lengths which are needed to make the formatting nice:
|
671
|
-
|
689
|
+
tag_lengths = Array.new()
|
672
690
|
name_lengths = Array.new()
|
673
691
|
type_lengths = Array.new()
|
674
692
|
length_lengths = Array.new()
|
675
693
|
names.each_index do |i|
|
676
|
-
|
694
|
+
tag_lengths[i] = tags[i].length
|
677
695
|
name_lengths[i] = names[i].length
|
678
696
|
type_lengths[i] = types[i].length
|
679
697
|
length_lengths[i] = lengths[i].to_s.length
|
680
698
|
end
|
681
699
|
# To give the printed output a nice format we need to check the string lengths of some of these arrays:
|
682
700
|
index_maxL = pos_valid.max.to_s.length
|
683
|
-
|
701
|
+
tag_maxL = tag_lengths.max
|
684
702
|
name_maxL = name_lengths.max
|
685
703
|
type_maxL = type_lengths.max
|
686
704
|
length_maxL = length_lengths.max
|
687
|
-
# Construct the strings, one for each line of output, where each line contain the information of one
|
688
|
-
|
689
|
-
# Start of loop which formats the
|
705
|
+
# Construct the strings, one for each line of output, where each line contain the information of one data element:
|
706
|
+
elements = Array.new()
|
707
|
+
# Start of loop which formats the element data:
|
690
708
|
# (This loop is what consumes most of the computing time of this method)
|
691
|
-
|
709
|
+
tags.each_index do |i|
|
692
710
|
# Configure empty spaces:
|
693
711
|
s = " "
|
694
712
|
f0 = " "*(index_maxL-pos_valid[i].to_s.length)
|
695
|
-
f2 = " "*(
|
713
|
+
f2 = " "*(tag_maxL-tags[i].length+1)
|
696
714
|
f3 = " "*(name_maxL-names[i].length+1)
|
697
715
|
f4 = " "*(type_maxL-types[i].length+1)
|
698
716
|
f5 = " "*(length_maxL-lengths[i].to_s.length)
|
@@ -708,23 +726,23 @@ module DICOM
|
|
708
726
|
else
|
709
727
|
value = (values[i])
|
710
728
|
end
|
711
|
-
# Insert descriptive text for
|
729
|
+
# Insert descriptive text for elements that hold binary data:
|
712
730
|
case types[i]
|
713
731
|
when "OW","OB","UN"
|
714
732
|
value = "(Binary Data)"
|
715
733
|
when "SQ","()"
|
716
|
-
value = "(Encapsulated Elements)"
|
734
|
+
value = "(Encapsulated Elements)"
|
717
735
|
end
|
718
|
-
|
736
|
+
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]
|
719
737
|
end
|
720
738
|
# Print to either screen or file, depending on what the user requested:
|
721
739
|
if opt_file
|
722
|
-
print_file(
|
740
|
+
print_file(elements)
|
723
741
|
else
|
724
|
-
print_screen(
|
725
|
-
end # of
|
742
|
+
print_screen(elements)
|
743
|
+
end # of tags.each do |i|
|
726
744
|
end # of method print
|
727
|
-
|
745
|
+
|
728
746
|
|
729
747
|
# Prints the key structural properties of the DICOM file.
|
730
748
|
def print_properties()
|
@@ -775,14 +793,14 @@ module DICOM
|
|
775
793
|
end
|
776
794
|
puts "-------------------------------"
|
777
795
|
end # of method print_properties
|
778
|
-
|
779
|
-
|
796
|
+
|
797
|
+
|
780
798
|
####################################################
|
781
799
|
### START OF METHODS FOR WRITING INFORMATION TO THE DICOM OBJECT:#
|
782
800
|
####################################################
|
783
|
-
|
784
|
-
|
785
|
-
# Reads binary information from file and inserts it in the pixel data
|
801
|
+
|
802
|
+
|
803
|
+
# Reads binary information from file and inserts it in the pixel data element:
|
786
804
|
def set_image_file(file)
|
787
805
|
# Try to read file:
|
788
806
|
begin
|
@@ -791,104 +809,99 @@ module DICOM
|
|
791
809
|
rescue
|
792
810
|
# Reading file was not successful. Register an error message.
|
793
811
|
add_msg("Reading specified file was not successful for some reason. No data has been added.")
|
812
|
+
return
|
794
813
|
end
|
795
814
|
if bin.length > 0
|
796
|
-
pos = @
|
797
|
-
# Modify
|
798
|
-
set_value(bin,
|
815
|
+
pos = @tags.index("7FE0,0010")
|
816
|
+
# Modify element:
|
817
|
+
set_value(bin, "7FE0,0010", :create => true, :bin => true)
|
799
818
|
else
|
800
819
|
add_msg("Content of file is of zero length. Nothing to store.")
|
801
820
|
end # of if bin.length > 0
|
802
821
|
end # of method set_image_file
|
803
|
-
|
804
|
-
|
805
|
-
# Transfers pixel data from a RMagick object to the pixel data
|
822
|
+
|
823
|
+
|
824
|
+
# Transfers pixel data from a RMagick object to the pixel data element:
|
806
825
|
def set_image_magick(magick_obj)
|
807
826
|
# Export the RMagick object to a standard Ruby array of numbers:
|
808
827
|
pixel_array = magick_obj.export_pixels(x=0, y=0, columns=magick_obj.columns, rows=magick_obj.rows, map="I")
|
809
828
|
# Encode this array using the standard class method:
|
810
|
-
set_value(pixel_array,
|
829
|
+
set_value(pixel_array, "7FE0,0010", :create => true)
|
811
830
|
end
|
812
|
-
|
813
|
-
|
814
|
-
# Removes
|
815
|
-
def
|
816
|
-
pos = get_pos(
|
817
|
-
if pos !=
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
831
|
+
|
832
|
+
|
833
|
+
# Removes an element from the DICOM object:
|
834
|
+
def remove(element)
|
835
|
+
pos = get_pos(element)
|
836
|
+
if pos != false
|
837
|
+
if pos.length > 1
|
838
|
+
add_msg("Warning: Method remove() does not allow an element query which yields multiple array hits. Please use array position instead of tag/name. Value NOT removed.")
|
839
|
+
else
|
840
|
+
# Extract first array number:
|
841
|
+
pos = pos[0]
|
842
|
+
# Update group length:
|
843
|
+
if @tags[pos][5..8] != "0000"
|
844
|
+
change = @lengths[pos]
|
845
|
+
vr = @types[pos]
|
846
|
+
update_group_length(pos, vr, change, -1)
|
847
|
+
end
|
848
|
+
# Remove entry from arrays:
|
849
|
+
@tags.delete_at(pos)
|
850
|
+
@levels.delete_at(pos)
|
851
|
+
@names.delete_at(pos)
|
852
|
+
@types.delete_at(pos)
|
853
|
+
@lengths.delete_at(pos)
|
854
|
+
@values.delete_at(pos)
|
855
|
+
@raw.delete_at(pos)
|
825
856
|
end
|
826
|
-
# Remove entry from arrays:
|
827
|
-
@labels.delete_at(pos)
|
828
|
-
@levels.delete_at(pos)
|
829
|
-
@names.delete_at(pos)
|
830
|
-
@types.delete_at(pos)
|
831
|
-
@lengths.delete_at(pos)
|
832
|
-
@values.delete_at(pos)
|
833
|
-
@raw.delete_at(pos)
|
834
857
|
else
|
835
|
-
add_msg("
|
858
|
+
add_msg("Warning: The data element #{element} could not be found in the DICOM object. Method remove() has no data element to remove.")
|
836
859
|
end
|
837
860
|
end
|
838
|
-
|
839
|
-
|
840
|
-
# Sets the value of a
|
841
|
-
# If the supplied value is not binary, it will attempt to encode
|
842
|
-
def set_value(value, opts={})
|
861
|
+
|
862
|
+
|
863
|
+
# Sets the value of a data element by modifying an existing element or creating a new one.
|
864
|
+
# If the supplied value is not binary, it will attempt to encode the value to binary itself.
|
865
|
+
def set_value(value, element, opts={})
|
843
866
|
# Options:
|
844
|
-
|
845
|
-
pos = opts[:pos]
|
846
|
-
create = opts[:create] # =false means no tag creation
|
867
|
+
create = opts[:create] # =false means no element creation
|
847
868
|
bin = opts[:bin] # =true means value already encoded
|
848
|
-
#
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
if pos != nil
|
856
|
-
unless pos >= 0 and pos <= @labels.length
|
857
|
-
# This is not a valid position:
|
858
|
-
pos = nil
|
859
|
-
end
|
860
|
-
else
|
861
|
-
pos = get_pos(label)
|
862
|
-
if pos == false
|
863
|
-
pos = nil
|
864
|
-
elsif pos.size > 1
|
865
|
-
pos = 'abort'
|
866
|
-
add_msg("The supplied label is found at multiple locations in the DICOM object. Will not update.")
|
867
|
-
end
|
868
|
-
end # of if pos != nil
|
869
|
-
# Create or modify?
|
870
|
-
if create == false
|
871
|
-
# User wants modification only. Proceed only if we have a valid position:
|
872
|
-
unless pos == nil
|
873
|
-
# Modify tag:
|
874
|
-
modify_tag(value, :bin => bin, :pos => pos)
|
869
|
+
# Retrieve array position:
|
870
|
+
pos = get_pos(element)
|
871
|
+
# We do not support changing multiple data elements:
|
872
|
+
if pos.is_a?(Array)
|
873
|
+
if pos.length > 1
|
874
|
+
add_msg("Warning: Method set_value() does not allow an element query which yields multiple array hits. Please use array position instead of tag/name. Value NOT saved.")
|
875
|
+
return
|
875
876
|
end
|
877
|
+
end
|
878
|
+
if pos == false and create == false
|
879
|
+
# Since user has requested an element shall only be updated, we can not do so as the element position is not valid:
|
880
|
+
add_msg("Warning: Invalid data element provided to method set_value(). Value NOT updated.")
|
881
|
+
elsif create == false
|
882
|
+
# Modify element:
|
883
|
+
modify_element(value, pos[0], :bin => bin)
|
876
884
|
else
|
877
|
-
# User wants to create (or modify
|
878
|
-
unless pos ==
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
885
|
+
# User wants to create an element (or modify it if it is already present).
|
886
|
+
unless pos == false
|
887
|
+
# The data element already exist, so we modify instead of creating:
|
888
|
+
modify_element(value, pos[0], :bin => bin)
|
889
|
+
else
|
890
|
+
# We need to create element:
|
891
|
+
tag = @lib.get_tag(element)
|
892
|
+
if tag == false
|
893
|
+
add_msg("Warning: Method set_value() could not create data element, either because data element name was not recognized in the library, or data element tag is invalid (Expected format of tags is 'GGGG,EEEE').")
|
894
|
+
else
|
895
|
+
# As we wish to create a new data element, we need to find out where to insert it in the element arrays:
|
896
|
+
# We will do this by finding the last array position of the last element that will (alphabetically/numerically) stay in front of this element.
|
897
|
+
if @tags.size > 0
|
883
898
|
# Search the array:
|
884
899
|
index = -1
|
885
900
|
quit = false
|
886
901
|
while quit != true do
|
887
|
-
if index+1 >= @
|
902
|
+
if index+1 >= @tags.length # We have reached end of array.
|
888
903
|
quit = true
|
889
|
-
|
890
|
-
#quit = true
|
891
|
-
elsif label < @labels[index+1] # We are past the correct position.
|
904
|
+
elsif tag < @tags[index+1] # We are past the correct position.
|
892
905
|
quit = true
|
893
906
|
else # Increase index in anticipation of a 'hit'.
|
894
907
|
index += 1
|
@@ -898,20 +911,11 @@ module DICOM
|
|
898
911
|
# We are dealing with an empty DICOM object:
|
899
912
|
index = nil
|
900
913
|
end
|
901
|
-
#
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
# Label did not pass our check:
|
907
|
-
add_msg("The label you specified (#{label}) does not seem valid. Please use the format 'GGGG,EEEE'.")
|
908
|
-
end
|
909
|
-
else
|
910
|
-
# Modify existing:
|
911
|
-
modify_tag(value, :bin => bin, :pos => pos)
|
912
|
-
end
|
913
|
-
end
|
914
|
-
end # of if create == false
|
914
|
+
# The necessary information is gathered; create new data element:
|
915
|
+
create_element(value, tag, index, :bin => bin)
|
916
|
+
end # of if tag ==..else..
|
917
|
+
end # of unless pos ==..else..
|
918
|
+
end # of if pos ==..and create ==..else..
|
915
919
|
end # of method set_value
|
916
920
|
|
917
921
|
|
@@ -929,28 +933,26 @@ module DICOM
|
|
929
933
|
if (msg.is_a? String)
|
930
934
|
msg=[msg]
|
931
935
|
end
|
932
|
-
@
|
936
|
+
@errors += msg
|
933
937
|
end
|
934
|
-
|
935
|
-
|
938
|
+
|
939
|
+
|
936
940
|
# Checks the endianness of the system. Returns false if little endian, true if big endian.
|
937
941
|
def check_sys_endian()
|
938
942
|
x = 0xdeadbeef
|
939
943
|
endian_type = {
|
940
944
|
Array(x).pack("V*") => false, #:little
|
941
|
-
Array(x).pack("N*") => true #:big
|
945
|
+
Array(x).pack("N*") => true #:big
|
942
946
|
}
|
943
|
-
return endian_type[Array(x).pack("L*")]
|
947
|
+
return endian_type[Array(x).pack("L*")]
|
944
948
|
end
|
945
|
-
|
946
|
-
|
947
|
-
# Creates a new
|
948
|
-
def
|
949
|
+
|
950
|
+
|
951
|
+
# Creates a new data element:
|
952
|
+
def create_element(value, tag, last_pos, opts={})
|
949
953
|
bin_only = opts[:bin]
|
950
|
-
label = opts[:label]
|
951
|
-
lastpos = opts[:lastpos]
|
952
954
|
# Fetch the VR:
|
953
|
-
info = @lib.get_name_vr(
|
955
|
+
info = @lib.get_name_vr(tag)
|
954
956
|
vr = info[1]
|
955
957
|
name = info[0]
|
956
958
|
# Encode binary (if a binary is not provided):
|
@@ -963,67 +965,64 @@ module DICOM
|
|
963
965
|
# Encode:
|
964
966
|
bin = encode(value, vr)
|
965
967
|
else
|
966
|
-
add_msg("Error. Unable to encode
|
968
|
+
add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
|
967
969
|
end
|
968
970
|
end
|
969
|
-
# Put this
|
971
|
+
# Put the information of this data element into the arrays:
|
970
972
|
if bin
|
971
|
-
#
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
#else
|
1019
|
-
#add_msg("Binary does not have a positive length, nothing to save.")
|
1020
|
-
#end # of if bin.length > 0
|
973
|
+
# 4 different scenarios: Array is empty, or: element is put in front, inside array, or at end of array:
|
974
|
+
# NB! No support for hierarchy at this time! Defaulting to level = 0.
|
975
|
+
if last_pos == nil
|
976
|
+
# We have empty DICOM object:
|
977
|
+
@tags = [tag]
|
978
|
+
@levels = [0]
|
979
|
+
@names = [name]
|
980
|
+
@types = [vr]
|
981
|
+
@lengths = [bin.length]
|
982
|
+
@values = [value]
|
983
|
+
@raw = [bin]
|
984
|
+
elsif last_pos == -1
|
985
|
+
# Insert in front of arrays:
|
986
|
+
@tags = [tag] + @tags
|
987
|
+
@levels = [0] + @levels
|
988
|
+
@names = [name] + @names
|
989
|
+
@types = [vr] + @types
|
990
|
+
@lengths = [bin.length] + @lengths
|
991
|
+
@values = [value] + @values
|
992
|
+
@raw = [bin] + @raw
|
993
|
+
elsif last_pos == @tags.length-1
|
994
|
+
# Insert at end arrays:
|
995
|
+
@tags = @tags + [tag]
|
996
|
+
@levels = @levels + [0]
|
997
|
+
@names = @names + [name]
|
998
|
+
@types = @types + [vr]
|
999
|
+
@lengths = @lengths + [bin.length]
|
1000
|
+
@values = @values + [value]
|
1001
|
+
@raw = @raw + [bin]
|
1002
|
+
else
|
1003
|
+
# Insert somewhere inside the array:
|
1004
|
+
@tags = @tags[0..last_pos] + [tag] + @tags[(last_pos+1)..(@tags.length-1)]
|
1005
|
+
@levels = @levels[0..last_pos] + [0] + @levels[(last_pos+1)..(@levels.length-1)]
|
1006
|
+
@names = @names[0..last_pos] + [name] + @names[(last_pos+1)..(@names.length-1)]
|
1007
|
+
@types = @types[0..last_pos] + [vr] + @types[(last_pos+1)..(@types.length-1)]
|
1008
|
+
@lengths = @lengths[0..last_pos] + [bin.length] + @lengths[(last_pos+1)..(@lengths.length-1)]
|
1009
|
+
@values = @values[0..last_pos] + [value] + @values[(last_pos+1)..(@values.length-1)]
|
1010
|
+
@raw = @raw[0..last_pos] + [bin] + @raw[(last_pos+1)..(@raw.length-1)]
|
1011
|
+
end
|
1012
|
+
# Update last index variable as we have added to our arrays:
|
1013
|
+
@last_index += 1
|
1014
|
+
# Update group length (as long as it was not a group length element that was created):
|
1015
|
+
pos = @tags.index(tag)
|
1016
|
+
if @tags[pos][5..8] != "0000"
|
1017
|
+
change = bin.length
|
1018
|
+
update_group_length(pos, vr, change, 1)
|
1019
|
+
end
|
1021
1020
|
else
|
1022
1021
|
add_msg("Binary is nil. Nothing to save.")
|
1023
1022
|
end
|
1024
|
-
end # of method
|
1025
|
-
|
1026
|
-
|
1023
|
+
end # of method create_element
|
1024
|
+
|
1025
|
+
|
1027
1026
|
# Encodes a value to binary (used for inserting values to a DICOM object).
|
1028
1027
|
def encode(value, vr)
|
1029
1028
|
# Our value needs to be inside an array to be encoded:
|
@@ -1042,7 +1041,7 @@ module DICOM
|
|
1042
1041
|
bin = value.pack(@fs)
|
1043
1042
|
when "FD"
|
1044
1043
|
bin = value.pack(@fd)
|
1045
|
-
when "AT" # (
|
1044
|
+
when "AT" # (Data element tag: Assume it has the format GGGGEEEE (no comma separation))
|
1046
1045
|
# Encode letter pairs indexes in following order 10 3 2:
|
1047
1046
|
# NB! This may not be encoded correctly on Big Endian files or computers.
|
1048
1047
|
old_format=value[0]
|
@@ -1051,6 +1050,9 @@ module DICOM
|
|
1051
1050
|
|
1052
1051
|
# We have a number of VRs that are encoded as string:
|
1053
1052
|
when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT'
|
1053
|
+
# In case we are dealing with a number string element, the supplied value might be a number
|
1054
|
+
# instead of a string, and as such, we convert to string just to make sure this will work nicely:
|
1055
|
+
value[0] = value[0].to_s
|
1054
1056
|
# Odd/even test (num[0]=1 if num is odd):
|
1055
1057
|
if value[0].length[0] == 1
|
1056
1058
|
# Odd (add a zero byte):
|
@@ -1064,8 +1066,8 @@ module DICOM
|
|
1064
1066
|
# What bit depth to use when encoding the pixel data?
|
1065
1067
|
bit_depth = get_value("0028,0100")
|
1066
1068
|
if bit_depth == false
|
1067
|
-
#
|
1068
|
-
add_msg("Attempted to encode pixel data, but
|
1069
|
+
# Data element not specified:
|
1070
|
+
add_msg("Attempted to encode pixel data, but 'Bit Depth' data element is missing (0028,0100).")
|
1069
1071
|
else
|
1070
1072
|
# 8,12 or 16 bits?
|
1071
1073
|
case bit_depth
|
@@ -1082,16 +1084,14 @@ module DICOM
|
|
1082
1084
|
end # of case bit_depth
|
1083
1085
|
end # of if bit_depth..else..
|
1084
1086
|
else # Unsupported VR:
|
1085
|
-
add_msg("
|
1087
|
+
add_msg("Element type #{vr} does not have a dedicated encoding option assigned. Please contact author.")
|
1086
1088
|
end # of case vr
|
1087
1089
|
return bin
|
1088
1090
|
end # of method encode
|
1089
|
-
|
1090
|
-
# Modifies existing
|
1091
|
-
def
|
1091
|
+
|
1092
|
+
# Modifies existing data element:
|
1093
|
+
def modify_element(value, pos, opts={})
|
1092
1094
|
bin_only = opts[:bin]
|
1093
|
-
pos = opts[:pos]
|
1094
|
-
pos = pos[0] if pos.is_a?(Array)
|
1095
1095
|
# Fetch the VR and old length:
|
1096
1096
|
vr = @types[pos]
|
1097
1097
|
old_length = @lengths[pos]
|
@@ -1105,54 +1105,50 @@ module DICOM
|
|
1105
1105
|
# Encode:
|
1106
1106
|
bin = encode(value, vr)
|
1107
1107
|
else
|
1108
|
-
add_msg("Error. Unable to encode
|
1108
|
+
add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
|
1109
1109
|
end
|
1110
1110
|
end
|
1111
1111
|
# Update the arrays with this new information:
|
1112
1112
|
if bin
|
1113
|
-
#
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
end
|
1124
|
-
#else
|
1125
|
-
#add_msg("Binary does not have a positive length, nothing to save.")
|
1126
|
-
#end
|
1113
|
+
# Replace array entries for this element:
|
1114
|
+
#@types[pos] = vr # for the time being there is no logic for updating type.
|
1115
|
+
@lengths[pos] = bin.length
|
1116
|
+
@values[pos] = value
|
1117
|
+
@raw[pos] = bin
|
1118
|
+
# Update group length (as long as it was not the group length that was modified):
|
1119
|
+
if @tags[pos][5..8] != "0000"
|
1120
|
+
change = bin.length - old_length
|
1121
|
+
update_group_length(pos, vr, change, 0)
|
1122
|
+
end
|
1127
1123
|
else
|
1128
1124
|
add_msg("Binary is nil. Nothing to save.")
|
1129
1125
|
end
|
1130
|
-
end # of method
|
1126
|
+
end # of method modify_element
|
1131
1127
|
|
1132
1128
|
|
1133
|
-
# Prints the selected
|
1129
|
+
# Prints the selected elements to an ascii text file.
|
1134
1130
|
# The text file will be saved in the folder of the original DICOM file,
|
1135
1131
|
# with the original file name plus a .txt extension.
|
1136
|
-
def print_file(
|
1132
|
+
def print_file(elements)
|
1137
1133
|
File.open( @file + '.txt', 'w' ) do |output|
|
1138
|
-
|
1134
|
+
elements.each do | line |
|
1139
1135
|
output.print line + "\n"
|
1140
1136
|
end
|
1141
1137
|
end
|
1142
1138
|
end
|
1143
1139
|
|
1144
|
-
|
1145
|
-
# Prints the selected
|
1146
|
-
def print_screen(
|
1147
|
-
|
1148
|
-
puts
|
1140
|
+
|
1141
|
+
# Prints the selected elements to screen.
|
1142
|
+
def print_screen(elements)
|
1143
|
+
elements.each do |element|
|
1144
|
+
puts element
|
1149
1145
|
end
|
1150
1146
|
end
|
1151
|
-
|
1152
|
-
|
1147
|
+
|
1148
|
+
|
1153
1149
|
# Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
|
1154
1150
|
def set_modality()
|
1155
|
-
value = get_value("0008,0016")
|
1151
|
+
value = get_value("0008,0016", :silent => true)
|
1156
1152
|
if value == false
|
1157
1153
|
@modality = "Not specified"
|
1158
1154
|
else
|
@@ -1160,8 +1156,8 @@ module DICOM
|
|
1160
1156
|
@modality = modality
|
1161
1157
|
end
|
1162
1158
|
end
|
1163
|
-
|
1164
|
-
|
1159
|
+
|
1160
|
+
|
1165
1161
|
# Sets the format strings that will be used for packing/unpacking numbers depending on endianness of file/system.
|
1166
1162
|
def set_format_strings(file_endian=@file_endian)
|
1167
1163
|
if @file_endian == @sys_endian
|
@@ -1186,50 +1182,50 @@ module DICOM
|
|
1186
1182
|
@fd = "G*"
|
1187
1183
|
end
|
1188
1184
|
end
|
1189
|
-
|
1190
|
-
|
1191
|
-
# Updates the group length value when a
|
1192
|
-
#
|
1193
|
-
# (Change should be positive when a
|
1194
|
-
#
|
1185
|
+
|
1186
|
+
|
1187
|
+
# Updates the group length value when a data element has been updated, created or removed:
|
1188
|
+
# The variable change holds the change in value length for the updated data element.
|
1189
|
+
# (Change should be positive when a data element is removed - it will only be negative when editing an element to a shorter value)
|
1190
|
+
# The variable existance is -1 if data element has been removed, +1 if element has been added and 0 if it has been updated.
|
1195
1191
|
# (Perhaps in the future this functionality might be moved to the DWrite class, it might give an easier implementation)
|
1196
1192
|
def update_group_length(pos, type, change, existance)
|
1197
1193
|
# Find position of relevant group length (if it exists):
|
1198
|
-
gl_pos = @
|
1194
|
+
gl_pos = @tags.index(@tags[pos][0..4] + "0000")
|
1199
1195
|
existance = 0 if existance == nil
|
1200
1196
|
# If it exists, calculate change:
|
1201
1197
|
if gl_pos
|
1202
1198
|
if existance == 0
|
1203
|
-
#
|
1199
|
+
# Element has only been updated, so we only need to think about value change:
|
1204
1200
|
value = @values[gl_pos] + change
|
1205
1201
|
else
|
1206
|
-
#
|
1202
|
+
# Element has either been created or removed. This means we need to calculate the length of its other parts.
|
1207
1203
|
if @explicit
|
1208
1204
|
# In the explicit scenario it is slightly complex to determine this value:
|
1209
|
-
|
1205
|
+
element_length = 0
|
1210
1206
|
# VR?:
|
1211
|
-
unless @
|
1212
|
-
|
1207
|
+
unless @tags[pos] == "FFFE,E000" or @tags[pos] == "FFFE,E00D" or @tags[pos] == "FFFE,E0DD"
|
1208
|
+
element_length += 2
|
1213
1209
|
end
|
1214
1210
|
# Length value:
|
1215
1211
|
case @types[pos]
|
1216
1212
|
when "OB","OW","SQ","UN"
|
1217
|
-
if pos > @
|
1218
|
-
|
1213
|
+
if pos > @tags.index("7FE0,0010").to_i and @tags.index("7FE0,0010").to_i != 0
|
1214
|
+
element_length += 4
|
1219
1215
|
else
|
1220
|
-
|
1216
|
+
element_length += 6
|
1221
1217
|
end
|
1222
1218
|
when "()"
|
1223
|
-
|
1219
|
+
element_length += 4
|
1224
1220
|
else
|
1225
|
-
|
1221
|
+
element_length += 2
|
1226
1222
|
end # of case
|
1227
1223
|
else
|
1228
1224
|
# In the implicit scenario it is easier:
|
1229
|
-
|
1225
|
+
element_length = 4
|
1230
1226
|
end
|
1231
1227
|
# Update group length for creation/deletion scenario:
|
1232
|
-
change = (4 +
|
1228
|
+
change = (4 + element_length + change) * existance
|
1233
1229
|
value = @values[gl_pos] + change
|
1234
1230
|
end
|
1235
1231
|
# Write the new Group Length value:
|
@@ -1243,4 +1239,4 @@ module DICOM
|
|
1243
1239
|
|
1244
1240
|
|
1245
1241
|
end # End of class
|
1246
|
-
end # End of module
|
1242
|
+
end # End of module
|