dicom 0.7 → 0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +55 -0
- data/README +51 -29
- data/init.rb +1 -0
- data/lib/dicom.rb +35 -21
- data/lib/dicom/{Anonymizer.rb → anonymizer.rb} +178 -80
- data/lib/dicom/constants.rb +121 -0
- data/lib/dicom/d_client.rb +888 -0
- data/lib/dicom/d_library.rb +208 -0
- data/lib/dicom/d_object.rb +424 -0
- data/lib/dicom/d_read.rb +433 -0
- data/lib/dicom/d_server.rb +397 -0
- data/lib/dicom/d_write.rb +420 -0
- data/lib/dicom/data_element.rb +175 -0
- data/lib/dicom/{Dictionary.rb → dictionary.rb} +390 -398
- data/lib/dicom/elements.rb +82 -0
- data/lib/dicom/file_handler.rb +116 -0
- data/lib/dicom/item.rb +87 -0
- data/lib/dicom/{Link.rb → link.rb} +749 -388
- data/lib/dicom/ruby_extensions.rb +44 -35
- data/lib/dicom/sequence.rb +62 -0
- data/lib/dicom/stream.rb +493 -0
- data/lib/dicom/super_item.rb +696 -0
- data/lib/dicom/super_parent.rb +615 -0
- metadata +25 -18
- data/DOCUMENTATION +0 -469
- data/lib/dicom/DClient.rb +0 -584
- data/lib/dicom/DLibrary.rb +0 -194
- data/lib/dicom/DObject.rb +0 -1579
- data/lib/dicom/DRead.rb +0 -532
- data/lib/dicom/DServer.rb +0 -304
- data/lib/dicom/DWrite.rb +0 -410
- data/lib/dicom/FileHandler.rb +0 -50
- data/lib/dicom/Stream.rb +0 -354
data/lib/dicom/DLibrary.rb
DELETED
@@ -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
|