dicom 0.6.1 → 0.7
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 +42 -20
- data/DOCUMENTATION +117 -71
- data/README +3 -3
- data/lib/dicom.rb +23 -12
- data/lib/{Anonymizer.rb → dicom/Anonymizer.rb} +101 -79
- data/lib/{DClient.rb → dicom/DClient.rb} +12 -11
- data/lib/{DLibrary.rb → dicom/DLibrary.rb} +53 -31
- data/lib/dicom/DObject.rb +1579 -0
- data/lib/{DRead.rb → dicom/DRead.rb} +42 -43
- data/lib/{DServer.rb → dicom/DServer.rb} +34 -20
- data/lib/{DWrite.rb → dicom/DWrite.rb} +27 -31
- data/lib/{Dictionary.rb → dicom/Dictionary.rb} +434 -32
- data/lib/dicom/FileHandler.rb +50 -0
- data/lib/{Link.rb → dicom/Link.rb} +312 -167
- data/lib/{Stream.rb → dicom/Stream.rb} +1 -1
- data/lib/dicom/ruby_extensions.rb +47 -0
- metadata +16 -15
- data/lib/DObject.rb +0 -1194
- data/lib/ruby_extensions.rb +0 -36
@@ -0,0 +1,47 @@
|
|
1
|
+
# This file contains extensions to the Ruby library which are used by Ruby DICOM.
|
2
|
+
|
3
|
+
class Array
|
4
|
+
|
5
|
+
# Searching all indices, or a subset of indices, in an array, and returning all indices
|
6
|
+
# where the array's value equals the queried value.
|
7
|
+
def all_indices(array, value)
|
8
|
+
result = []
|
9
|
+
self.each do |pos|
|
10
|
+
result << pos if array[pos] == value
|
11
|
+
end
|
12
|
+
return result
|
13
|
+
end
|
14
|
+
|
15
|
+
# Similar to method above, but this one returns the position of all strings that
|
16
|
+
# contain the query string (exact match not required).
|
17
|
+
def all_indices_partial_match(array, value)
|
18
|
+
result = []
|
19
|
+
self.each do |pos|
|
20
|
+
result << pos if array[pos].include?(value)
|
21
|
+
end
|
22
|
+
return result
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class String
|
28
|
+
|
29
|
+
# Check if a given string appears to be a valid tag (GGGG,EEEE) by regexp matching.
|
30
|
+
# The method tests that the string is exactly composed of 4 HEX characters, followed by
|
31
|
+
# a comma, then 4 new HEX characters, which constitutes the tag format used by Ruby DICOM.
|
32
|
+
def is_a_tag?
|
33
|
+
result = false
|
34
|
+
#result = true if self =~ /\A\h{4},\h{4}\z/ # (turns out the hex reference '\h' isnt compatible with ruby 1.8)
|
35
|
+
result = true if self =~ /\A[a-fA-F\d]{4},[a-fA-F\d]{4}\z/
|
36
|
+
return result
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if a given tag string indicates a private tag (Odd group number) by doing a regexp matching.
|
40
|
+
def private?
|
41
|
+
result = false
|
42
|
+
#result = true if self.upcase =~ /\A\h{3}[1,3,5,7,9,B,D,F],\h{4}\z/ # (incompatible with ruby 1.8)
|
43
|
+
result = true if self.upcase =~ /\A[a-fA-F\d]{3}[1,3,5,7,9,B,D,F],[a-fA-F\d]{4}\z/
|
44
|
+
return result
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dicom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: "0.7"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christoffer Lervag
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-02-28 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -22,20 +22,21 @@ extensions: []
|
|
22
22
|
extra_rdoc_files: []
|
23
23
|
|
24
24
|
files:
|
25
|
-
- lib/DObject.rb
|
26
|
-
- lib/DRead.rb
|
27
|
-
- lib/DWrite.rb
|
28
25
|
- lib/dicom.rb
|
29
|
-
- lib/
|
30
|
-
- lib/
|
31
|
-
- lib/
|
32
|
-
- lib/
|
33
|
-
- lib/
|
34
|
-
- lib/
|
35
|
-
- lib/
|
36
|
-
- lib/
|
37
|
-
-
|
26
|
+
- lib/dicom/DObject.rb
|
27
|
+
- lib/dicom/Link.rb
|
28
|
+
- lib/dicom/DLibrary.rb
|
29
|
+
- lib/dicom/DWrite.rb
|
30
|
+
- lib/dicom/Dictionary.rb
|
31
|
+
- lib/dicom/FileHandler.rb
|
32
|
+
- lib/dicom/DServer.rb
|
33
|
+
- lib/dicom/DClient.rb
|
34
|
+
- lib/dicom/Anonymizer.rb
|
35
|
+
- lib/dicom/Stream.rb
|
36
|
+
- lib/dicom/ruby_extensions.rb
|
37
|
+
- lib/dicom/DRead.rb
|
38
38
|
- README
|
39
|
+
- DOCUMENTATION
|
39
40
|
- COPYING
|
40
41
|
- CHANGELOG
|
41
42
|
has_rdoc: true
|
@@ -62,7 +63,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
63
|
requirements: []
|
63
64
|
|
64
65
|
rubyforge_project: dicom
|
65
|
-
rubygems_version: 1.3.
|
66
|
+
rubygems_version: 1.3.5
|
66
67
|
signing_key:
|
67
68
|
specification_version: 3
|
68
69
|
summary: Library for handling DICOM files and DICOM network communication.
|
data/lib/DObject.rb
DELETED
@@ -1,1194 +0,0 @@
|
|
1
|
-
# Copyright 2008-2009 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
|
-
# -Improve the retrieve file network functionality
|
20
|
-
# -Make the networking more intelligent in its handling of (unexpected) messages
|
21
|
-
# -Support for writing complex (hierarchical) DICOM files (basic write support is featured).
|
22
|
-
# -Full support for compressed image data.
|
23
|
-
# -Read 12 bit image data correctly.
|
24
|
-
# -Support for color image data to get_image_narray and get_image_magick.
|
25
|
-
# -Complete support for Big endian (basic support is already featured).
|
26
|
-
# -Complete support for multiple frame image data to NArray and RMagick objects (partial support already featured).
|
27
|
-
# -Make the image handling more intelligent with respect to interpreting data elements that hold information on the image and its properties.
|
28
|
-
|
29
|
-
module DICOM
|
30
|
-
|
31
|
-
# Class for interacting with the DICOM object.
|
32
|
-
class DObject
|
33
|
-
|
34
|
-
attr_reader :read_success, :write_success, :modality, :errors, :segments,
|
35
|
-
:names, :tags, :types, :lengths, :values, :raw, :levels
|
36
|
-
|
37
|
-
# Initialize the DObject instance.
|
38
|
-
def initialize(string=nil, options={})
|
39
|
-
# Process option values, setting defaults for the ones that are not specified:
|
40
|
-
@verbose = options[:verbose]
|
41
|
-
@lib = options[:lib] || DLibrary.new
|
42
|
-
segment_size = options[:segment_size]
|
43
|
-
bin = options[:bin]
|
44
|
-
syntax = options[:syntax]
|
45
|
-
# Default verbosity is true:
|
46
|
-
@verbose = true if @verbose == nil
|
47
|
-
|
48
|
-
# Initialize variables that will be used for the DICOM object:
|
49
|
-
@names = Array.new
|
50
|
-
@tags = Array.new
|
51
|
-
@types = Array.new
|
52
|
-
@lengths = Array.new
|
53
|
-
@values = Array.new
|
54
|
-
@raw = Array.new
|
55
|
-
@levels = Array.new
|
56
|
-
# Array that will holde any messages generated while reading the DICOM file:
|
57
|
-
@errors = Array.new
|
58
|
-
# Array to keep track of sequences/structure of the dicom elements:
|
59
|
-
@sequence = Array.new
|
60
|
-
# Index of last element in data element arrays:
|
61
|
-
@last_index=0
|
62
|
-
# Structural information (default values):
|
63
|
-
@compression = false
|
64
|
-
@color = false
|
65
|
-
@explicit = true
|
66
|
-
@file_endian = false
|
67
|
-
# Information about the DICOM object:
|
68
|
-
@modality = nil
|
69
|
-
# Control variables:
|
70
|
-
@read_success = false
|
71
|
-
# Initialize a Stream instance which is used for encoding/decoding:
|
72
|
-
@stream = Stream.new(nil, @file_endian, @explicit)
|
73
|
-
|
74
|
-
# If a (valid) file name string is supplied, call the method to read the DICOM file:
|
75
|
-
if string.is_a?(String) and string != ""
|
76
|
-
@file = string
|
77
|
-
read(string, :bin => bin, :segment_size => segment_size, :syntax => syntax)
|
78
|
-
end
|
79
|
-
end # of initialize
|
80
|
-
|
81
|
-
|
82
|
-
# Returns a DICOM object by reading the file specified.
|
83
|
-
# This is accomplished by initliazing the DRead class, which loads DICOM information to arrays.
|
84
|
-
# For the time being, this method is called automatically when initializing the DObject class,
|
85
|
-
# but in the future, when write support is added, this method may have to be called manually.
|
86
|
-
def read(string, options = {})
|
87
|
-
r = DRead.new(string, :lib => @lib, :sys_endian => @sys_endian, :bin => options[:bin], :syntax => options[:syntax])
|
88
|
-
# Store the data to the instance variables if the readout was a success:
|
89
|
-
if r.success
|
90
|
-
@read_success = true
|
91
|
-
@names = r.names
|
92
|
-
@tags = r.tags
|
93
|
-
@types = r.types
|
94
|
-
@lengths = r.lengths
|
95
|
-
@values = r.values
|
96
|
-
@raw = r.raw
|
97
|
-
@levels = r.levels
|
98
|
-
@explicit = r.explicit
|
99
|
-
@file_endian = r.file_endian
|
100
|
-
# Update Stream instance with settings from this DICOM file:
|
101
|
-
@stream.set_endian(@file_endian)
|
102
|
-
@stream.explicit = @explicit
|
103
|
-
# Index of last data element in element arrays:
|
104
|
-
@last_index=@names.length-1
|
105
|
-
# Update status variables for this object:
|
106
|
-
check_properties
|
107
|
-
# Set the modality of the DICOM object:
|
108
|
-
set_modality
|
109
|
-
else
|
110
|
-
@read_success = false
|
111
|
-
end
|
112
|
-
# Check if a partial extraction has been requested (used for network communication purposes)
|
113
|
-
if options[:segment_size]
|
114
|
-
@segments = r.extract_segments(options[:segment_size])
|
115
|
-
end
|
116
|
-
# If any messages has been recorded, send these to the message handling method:
|
117
|
-
add_msg(r.msg) if r.msg.size != 0
|
118
|
-
end
|
119
|
-
|
120
|
-
|
121
|
-
# Transfers necessary information from the DObject to the DWrite class, which
|
122
|
-
# will attempt to write this information to a valid DICOM file.
|
123
|
-
def write(file_name, transfer_syntax = nil)
|
124
|
-
w = set_write_object(file_name, transfer_syntax)
|
125
|
-
w.write
|
126
|
-
# Write process succesful?
|
127
|
-
@write_success = w.success
|
128
|
-
# If any messages has been recorded, send these to the message handling method:
|
129
|
-
add_msg(w.msg) if w.msg.size != 0
|
130
|
-
end
|
131
|
-
|
132
|
-
|
133
|
-
# Encodes the DICOM object into a series of binary string segments with a specified maximum length.
|
134
|
-
def encode_segments(size)
|
135
|
-
w = set_write_object
|
136
|
-
@segments = w.encode_segments(size)
|
137
|
-
# Write process succesful?
|
138
|
-
@write_success = w.success
|
139
|
-
# If any messages has been recorded, send these to the message handling method:
|
140
|
-
add_msg(w.msg) if w.msg.size != 0
|
141
|
-
end
|
142
|
-
|
143
|
-
|
144
|
-
#################################################
|
145
|
-
# START OF METHODS FOR READING INFORMATION FROM DICOM OBJECT:
|
146
|
-
#################################################
|
147
|
-
|
148
|
-
|
149
|
-
# 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.
|
150
|
-
# Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
|
151
|
-
def check_properties
|
152
|
-
# Check if pixel data is present:
|
153
|
-
if @tags.index("7FE0,0010") == nil
|
154
|
-
# No pixel data in DICOM file:
|
155
|
-
@compression = nil
|
156
|
-
else
|
157
|
-
@compression = @lib.get_compression(get_value("0002,0010", :silent => true))
|
158
|
-
end
|
159
|
-
# Set color variable as true if our object contain a color image:
|
160
|
-
col_string = get_value("0028,0004", :silent => true)
|
161
|
-
if col_string != false
|
162
|
-
if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
|
163
|
-
@color = true
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
|
169
|
-
# Returns image data from the provided element index, performing decompression of data if necessary.
|
170
|
-
def read_image_magick(pos, columns, rows)
|
171
|
-
if pos == false or columns == false or rows == false
|
172
|
-
add_msg("Error: Method read_image_magick does not have enough data available to build an image object.")
|
173
|
-
return false
|
174
|
-
end
|
175
|
-
unless @compression
|
176
|
-
# Non-compressed, just return the array contained on the particular element:
|
177
|
-
image_data = get_pixels(pos)
|
178
|
-
image = Magick::Image.new(columns,rows)
|
179
|
-
image.import_pixels(0, 0, columns, rows, "I", image_data)
|
180
|
-
return image
|
181
|
-
else
|
182
|
-
# Image data is compressed, we will attempt to deflate it using RMagick (ImageMagick):
|
183
|
-
begin
|
184
|
-
image = Magick::Image.from_blob(@raw[pos])
|
185
|
-
return image
|
186
|
-
rescue
|
187
|
-
add_msg("RMagick did not succeed in decoding the compressed image data. Returning false.")
|
188
|
-
return false
|
189
|
-
end
|
190
|
-
end
|
191
|
-
end
|
192
|
-
|
193
|
-
|
194
|
-
# Returns a 3d NArray object where the array dimensions are related to [frames, columns, rows].
|
195
|
-
# To call this method the user needs to have performed " require 'narray' " in advance.
|
196
|
-
def get_image_narray
|
197
|
-
# Does pixel data exist at all in the DICOM object?
|
198
|
-
if @compression == nil
|
199
|
-
add_msg("It seems pixel data is not present in this DICOM object: returning false.")
|
200
|
-
return false
|
201
|
-
end
|
202
|
-
# No support yet for retrieving compressed data:
|
203
|
-
if @compression == true
|
204
|
-
add_msg("Reading compressed data to a NArray object not supported yet: returning false.")
|
205
|
-
return false
|
206
|
-
end
|
207
|
-
# No support yet for retrieving color pixel data:
|
208
|
-
if @color
|
209
|
-
add_msg("Warning: Unpacking color pixel data is not supported yet for this method: returning false.")
|
210
|
-
return false
|
211
|
-
end
|
212
|
-
# Gather information about the dimensions of the image data:
|
213
|
-
rows = get_value("0028,0010")
|
214
|
-
columns = get_value("0028,0011")
|
215
|
-
frames = get_frames
|
216
|
-
image_pos = get_image_pos
|
217
|
-
# Creating a NArray object using int to make sure we have a big enough range for our numbers:
|
218
|
-
image = NArray.int(frames,columns,rows)
|
219
|
-
image_temp = NArray.int(columns,rows)
|
220
|
-
# Handling of image data will depend on whether we have one or more frames,
|
221
|
-
# and if it is located in one or more elements:
|
222
|
-
if image_pos.size == 1
|
223
|
-
# All of the image data is located in one element:
|
224
|
-
image_data = get_pixels(image_pos[0])
|
225
|
-
#image_data = get_image_data(image_pos[0])
|
226
|
-
(0..frames-1).each do |i|
|
227
|
-
(0..columns*rows-1).each do |j|
|
228
|
-
image_temp[j] = image_data[j+i*columns*rows]
|
229
|
-
end
|
230
|
-
image[i,true,true] = image_temp
|
231
|
-
end
|
232
|
-
else
|
233
|
-
# Image data is encapsulated in items:
|
234
|
-
(0..frames-1).each do |i|
|
235
|
-
image_data=get_value(image_pos[i])
|
236
|
-
#image_data = get_image_data(image_pos[i])
|
237
|
-
(0..columns*rows-1).each do |j|
|
238
|
-
image_temp[j] = image_data[j+i*columns*rows]
|
239
|
-
end
|
240
|
-
image[i,true,true] = image_temp
|
241
|
-
end
|
242
|
-
end
|
243
|
-
# Turn around the images to get the expected orientation when displaying on the screen:
|
244
|
-
(0..frames-1).each do |i|
|
245
|
-
temp_image=image[i,true,true]
|
246
|
-
#Transpose the images:
|
247
|
-
temp_image.transpose(1,0)
|
248
|
-
#Need to mirror the y-axis:
|
249
|
-
(0..temp_image.shape[0]-1).each do |j|
|
250
|
-
temp_image[j,0..temp_image.shape[1]-1] = temp_image[j,temp_image.shape[1]-1..0]
|
251
|
-
end
|
252
|
-
# Put the reoriented image back in the image matrixx:
|
253
|
-
image[i,true,true]=temp_image
|
254
|
-
end
|
255
|
-
return image
|
256
|
-
end # of get_image_narray
|
257
|
-
|
258
|
-
|
259
|
-
# Returns an array of RMagick image objects, where the size of the array corresponds to the number of frames in the image data.
|
260
|
-
# To call this method the user needs to have loaded the ImageMagick library in advance (require 'RMagick').
|
261
|
-
def get_image_magick
|
262
|
-
# Does pixel data exist at all in the DICOM object?
|
263
|
-
if @compression == nil
|
264
|
-
add_msg("It seems pixel data is not present in this DICOM object: Returning false.")
|
265
|
-
return false
|
266
|
-
end
|
267
|
-
# No support yet for color pixel data:
|
268
|
-
if @color
|
269
|
-
add_msg("Warning: Unpacking color pixel data is not supported yet for this method: Aborting.")
|
270
|
-
return false
|
271
|
-
end
|
272
|
-
# Gather information about the dimensions of the image data:
|
273
|
-
rows = get_value("0028,0010", :array => true)
|
274
|
-
columns = get_value("0028,0011", :array => true)
|
275
|
-
rows = [rows] unless rows.is_a?(Array)
|
276
|
-
columns = [columns] unless columns.is_a?(Array)
|
277
|
-
frames = get_frames
|
278
|
-
image_pos = get_image_pos
|
279
|
-
# Array that will hold the RMagick image objects, one image object for each frame:
|
280
|
-
image_arr = Array.new(frames)
|
281
|
-
# A hack for the special case (some MR files), where two images are stored (one is a smaller thumbnail image):
|
282
|
-
if image_pos.length > 1 and columns.length > 1
|
283
|
-
image_pos = [image_pos.last]
|
284
|
-
columns = [columns[0]]
|
285
|
-
rows = [rows[0]]
|
286
|
-
end
|
287
|
-
# Handling of image data will depend on whether we have one or more frames,
|
288
|
-
if image_pos.size == 1
|
289
|
-
# All of the image data is located in one data element:
|
290
|
-
if frames > 1
|
291
|
-
add_msg("Unfortunately, this method only supports reading the first image frame as of now.")
|
292
|
-
end
|
293
|
-
image = read_image_magick(image_pos[0], columns[0], rows[0])
|
294
|
-
image_arr[0] = image
|
295
|
-
else
|
296
|
-
# Image data is encapsulated in items:
|
297
|
-
(0..frames-1).each do |i|
|
298
|
-
image = read_image_magick(image_pos[i], columns[0], rows[0])
|
299
|
-
image_arr[i] = image
|
300
|
-
end
|
301
|
-
end
|
302
|
-
return image_arr
|
303
|
-
end # of get_image_magick
|
304
|
-
|
305
|
-
|
306
|
-
# Returns the number of frames present in the image data in the DICOM file.
|
307
|
-
def get_frames
|
308
|
-
frames = get_value("0028,0008", :silent => true)
|
309
|
-
# If the DICOM object does not specify the number of frames explicitly, assume 1 image frame:
|
310
|
-
frames = 1 unless frames
|
311
|
-
return frames.to_i
|
312
|
-
end
|
313
|
-
|
314
|
-
|
315
|
-
# Unpacks and returns pixel data from a specified data element array position:
|
316
|
-
def get_pixels(pos)
|
317
|
-
pixels = false
|
318
|
-
# We need to know what kind of bith depth the pixel data is saved with:
|
319
|
-
bit_depth = get_value("0028,0100", :array => true)
|
320
|
-
unless bit_depth == false
|
321
|
-
# Load the binary pixel data to the Stream instance:
|
322
|
-
@stream.set_string(get_raw(pos))
|
323
|
-
bit_depth = bit_depth.first if bit_depth.is_a?(Array)
|
324
|
-
# Number of bytes used per pixel will determine how to unpack this:
|
325
|
-
case bit_depth
|
326
|
-
when 8
|
327
|
-
pixels = @stream.decode_all("BY") # Byte/Character/Fixnum (1 byte)
|
328
|
-
when 16
|
329
|
-
pixels = @stream.decode_all("US") # Unsigned short (2 bytes)
|
330
|
-
when 12
|
331
|
-
# 12 BIT SIMPLY NOT WORKING YET!
|
332
|
-
# This one is a bit more tricky to extract.
|
333
|
-
# I havent really given this priority so far as 12 bit image data is rather rare.
|
334
|
-
add_msg("Warning: Bit depth 12 is not working correctly at this time! Please contact the author.")
|
335
|
-
else
|
336
|
-
raise "Bit depth ["+bit_depth.to_s+"] has not received implementation in this procedure yet. Please contact the author."
|
337
|
-
end # of case bit_depth
|
338
|
-
else
|
339
|
-
add_msg("Error: DICOM object does not contain the 'Bit Depth' data element (0028,0010).")
|
340
|
-
end # of if bit_depth ..
|
341
|
-
return pixels
|
342
|
-
end
|
343
|
-
|
344
|
-
|
345
|
-
# Returns the index(es) of the element(s) that contain image data.
|
346
|
-
def get_image_pos
|
347
|
-
image_element_pos = get_pos("7FE0,0010")
|
348
|
-
item_pos = get_pos("FFFE,E000")
|
349
|
-
# Proceed only if an image element actually exists:
|
350
|
-
if image_element_pos == false
|
351
|
-
return false
|
352
|
-
else
|
353
|
-
# Check if we have item elements:
|
354
|
-
if item_pos == false
|
355
|
-
return image_element_pos
|
356
|
-
else
|
357
|
-
# Extract item positions that occur after the image element position:
|
358
|
-
late_item_pos = item_pos.select {|item| image_element_pos[0] < item}
|
359
|
-
# Check if there are items appearing after the image element.
|
360
|
-
if late_item_pos.size == 0
|
361
|
-
# None occured after the image element position:
|
362
|
-
return image_element_pos
|
363
|
-
else
|
364
|
-
# Determine which of these late item elements contain image data.
|
365
|
-
# Usually, there are frames+1 late items, and all except
|
366
|
-
# the first item contain an image frame:
|
367
|
-
frames = get_frames
|
368
|
-
if frames != false # note: function get_frames will never return false
|
369
|
-
if late_item_pos.size == frames.to_i+1
|
370
|
-
return late_item_pos[1..late_item_pos.size-1]
|
371
|
-
else
|
372
|
-
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, returning false.")
|
373
|
-
return false
|
374
|
-
end
|
375
|
-
else
|
376
|
-
add_msg("Warning: 'Number of Frames' data element not found. Method get_image_pos will return false.")
|
377
|
-
return false
|
378
|
-
end
|
379
|
-
end
|
380
|
-
end
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
|
385
|
-
# Returns an array of the index(es) of the element(s) in the DICOM file that match the supplied element position, tag or name.
|
386
|
-
# If no match is found, the method will return false.
|
387
|
-
# Additional options:
|
388
|
-
# :array => myArray - tells the method to search for matches in this specific array of positions instead of searching
|
389
|
-
# through the entire DICOM object. If myArray equals false, the method will return false.
|
390
|
-
# :partial => true - get_pos will not only search for exact matches, but will search the names and tags arrays for
|
391
|
-
# strings that contain the given search string.
|
392
|
-
def get_pos(query, options={})
|
393
|
-
indexes = Array.new
|
394
|
-
# For convenience, allow query to be a one-element array (its value will be extracted):
|
395
|
-
if query.is_a?(Array)
|
396
|
-
if query.length > 1 or query.length == 0
|
397
|
-
add_msg("Invalid array length supplied to method get_pos.")
|
398
|
-
return false
|
399
|
-
else
|
400
|
-
query = query[0]
|
401
|
-
end
|
402
|
-
end
|
403
|
-
if options[:array] == false
|
404
|
-
# If the supplied array option equals false, it signals that the user tries to search for an element
|
405
|
-
# in an invalid position, and as such, this method will also return false:
|
406
|
-
add_msg("Warning: Attempted to call get_pos with query #{query}, but since keyword :array is false I will return false.")
|
407
|
-
indexes = false
|
408
|
-
else
|
409
|
-
# Check if query is a number (some methods want to have the ability to call get_pos with a number):
|
410
|
-
if query.is_a?(Integer)
|
411
|
-
# Return the position if it is valid:
|
412
|
-
indexes = [query] if query >= 0 and query < @names.length
|
413
|
-
elsif query.is_a?(String)
|
414
|
-
# Either use the supplied array, or search the entire DICOM object:
|
415
|
-
if options[:array].is_a?(Array)
|
416
|
-
search_array = options[:array]
|
417
|
-
else
|
418
|
-
search_array = Array.new(@names.length) {|i| i}
|
419
|
-
end
|
420
|
-
# Perform search:
|
421
|
-
if options[:partial] == true
|
422
|
-
# Search for partial string matches:
|
423
|
-
partial_indexes = search_array.all_indices_partial_match(@tags, query.upcase)
|
424
|
-
if partial_indexes.length > 0
|
425
|
-
indexes = partial_indexes
|
426
|
-
else
|
427
|
-
indexes = search_array.all_indices_partial_match(@names, query)
|
428
|
-
end
|
429
|
-
else
|
430
|
-
# Search for identical matches:
|
431
|
-
if query[4..4] == ","
|
432
|
-
indexes = search_array.all_indices(@tags, query.upcase)
|
433
|
-
else
|
434
|
-
indexes = search_array.all_indices(@names, query)
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|
438
|
-
# Policy: If no matches found, return false instead of an empty array:
|
439
|
-
indexes = false if indexes.length == 0
|
440
|
-
end
|
441
|
-
return indexes
|
442
|
-
end # of get_pos
|
443
|
-
|
444
|
-
|
445
|
-
# Dumps the binary content of the Pixel Data element to file.
|
446
|
-
def image_to_file(file)
|
447
|
-
pos = get_image_pos
|
448
|
-
if pos
|
449
|
-
if pos.length == 1
|
450
|
-
# Pixel data located in one element:
|
451
|
-
pixel_data = get_raw(pos[0])
|
452
|
-
f = File.new(file, "wb")
|
453
|
-
f.write(pixel_data)
|
454
|
-
f.close
|
455
|
-
else
|
456
|
-
# Pixel data located in several elements:
|
457
|
-
pos.each_index do |i|
|
458
|
-
pixel_data = get_raw(pos[i])
|
459
|
-
f = File.new(file + i.to_s, "wb")
|
460
|
-
f.write(pixel_data)
|
461
|
-
f.close
|
462
|
-
end
|
463
|
-
end
|
464
|
-
end
|
465
|
-
end
|
466
|
-
|
467
|
-
|
468
|
-
# Returns the positions of all data elements inside the hierarchy of a sequence or an item.
|
469
|
-
# Options:
|
470
|
-
# :next_only => true - The method will only search immediately below the specified
|
471
|
-
# item or sequence (that is, in the level of parent + 1).
|
472
|
-
def children(element, options={})
|
473
|
-
# Process option values, setting defaults for the ones that are not specified:
|
474
|
-
opt_next_only = options[:next_only] || false
|
475
|
-
value = false
|
476
|
-
# Retrieve array position:
|
477
|
-
pos = get_pos(element)
|
478
|
-
if pos == false
|
479
|
-
add_msg("Warning: Invalid data element provided to method children. Returning false.")
|
480
|
-
else
|
481
|
-
if pos.size > 1
|
482
|
-
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.")
|
483
|
-
else
|
484
|
-
# Proceed to find the value:
|
485
|
-
# First we need to establish in which positions to perform the search:
|
486
|
-
below_pos = Array.new
|
487
|
-
pos.each do |p|
|
488
|
-
parent_level = @levels[p]
|
489
|
-
remain_array = @levels[p+1..@levels.size-1]
|
490
|
-
extract = true
|
491
|
-
remain_array.each_index do |i|
|
492
|
-
if (remain_array[i] > parent_level) and (extract == true)
|
493
|
-
# If search is targetted at any specific level, we can just add this position:
|
494
|
-
if not opt_next_only == true
|
495
|
-
below_pos << (p+1+i)
|
496
|
-
else
|
497
|
-
# As search is restricted to parent level + 1, do a test for this:
|
498
|
-
if remain_array[i] == parent_level + 1
|
499
|
-
below_pos << (p+1+i)
|
500
|
-
end
|
501
|
-
end
|
502
|
-
else
|
503
|
-
# If we encounter a position who's level is not deeper than the original level, we can not extract any more values:
|
504
|
-
extract = false
|
505
|
-
end
|
506
|
-
end
|
507
|
-
end # of pos.each do..
|
508
|
-
value = below_pos if below_pos.size != 0
|
509
|
-
end # of if pos.size..else..
|
510
|
-
end
|
511
|
-
return value
|
512
|
-
end
|
513
|
-
|
514
|
-
|
515
|
-
# Returns the value (processed raw data) of the requested DICOM data element.
|
516
|
-
# Data element may be specified by array position, tag or name.
|
517
|
-
# Options:
|
518
|
-
# :array => true - Allows the query of the value of a tag that occurs more than one time in the
|
519
|
-
# DICOM object. Values will be returned in an array with length equal to the number
|
520
|
-
# of occurances of the tag. If keyword is not specified, the method returns false in this case.
|
521
|
-
# :silent => true - As this method is also used internally, we want the possibility of warnings not being
|
522
|
-
# raised even if verbose is set to true by the user, in order to avoid confusion.
|
523
|
-
def get_value(element, options={})
|
524
|
-
value = false
|
525
|
-
# Retrieve array position:
|
526
|
-
pos = get_pos(element)
|
527
|
-
if pos == false
|
528
|
-
add_msg("Warning: Invalid data element provided to method get_value. Returning false.") unless options[:silent]
|
529
|
-
else
|
530
|
-
if pos.size > 1
|
531
|
-
if options[:array] == true
|
532
|
-
# Retrieve all values into an array:
|
533
|
-
value = Array.new
|
534
|
-
pos.each do |i|
|
535
|
-
value << @values[i]
|
536
|
-
end
|
537
|
-
else
|
538
|
-
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 options[:silent]
|
539
|
-
end
|
540
|
-
else
|
541
|
-
value = @values[pos[0]]
|
542
|
-
end
|
543
|
-
end
|
544
|
-
return value
|
545
|
-
end
|
546
|
-
|
547
|
-
|
548
|
-
# Returns the raw data of the requested DICOM data element.
|
549
|
-
# Data element may be specified by array position, tag or name.
|
550
|
-
# Options:
|
551
|
-
# :array => true - Allows the query of the value of a tag that occurs more than one time in the
|
552
|
-
# DICOM object. Values will be returned in an array with length equal to the number
|
553
|
-
# of occurances of the tag. If keyword is not specified, the method returns false in this case.
|
554
|
-
def get_raw(element, options={})
|
555
|
-
value = false
|
556
|
-
# Retrieve array position:
|
557
|
-
pos = get_pos(element)
|
558
|
-
if pos == false
|
559
|
-
add_msg("Warning: Invalid data element provided to method get_raw. Returning false.")
|
560
|
-
else
|
561
|
-
if pos.size > 1
|
562
|
-
if options[:array] == true
|
563
|
-
# Retrieve all values into an array:
|
564
|
-
value = Array.new
|
565
|
-
pos.each do |i|
|
566
|
-
value << @raw[i]
|
567
|
-
end
|
568
|
-
else
|
569
|
-
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.")
|
570
|
-
end
|
571
|
-
else
|
572
|
-
value = @raw[pos[0]]
|
573
|
-
end
|
574
|
-
end
|
575
|
-
return value
|
576
|
-
end
|
577
|
-
|
578
|
-
|
579
|
-
# Returns the position of (possible) parents of the specified data element in the hierarchy structure of the DICOM object.
|
580
|
-
def parents(element)
|
581
|
-
value = false
|
582
|
-
# Retrieve array position:
|
583
|
-
pos = get_pos(element)
|
584
|
-
if pos == false
|
585
|
-
add_msg("Warning: Invalid data element provided to method parents. Returning false.")
|
586
|
-
else
|
587
|
-
if pos.length > 1
|
588
|
-
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.")
|
589
|
-
else
|
590
|
-
# Proceed to find the value:
|
591
|
-
# Get the level of our element:
|
592
|
-
level = @levels[pos[0]]
|
593
|
-
# Element can obviously only have parents if it is not a top level element:
|
594
|
-
unless level == 0
|
595
|
-
# Search backwards, and record the position every time we encounter an upwards change in the level number.
|
596
|
-
parents = Array.new
|
597
|
-
prev_level = level
|
598
|
-
search_arr = @levels[0..pos[0]-1].reverse
|
599
|
-
search_arr.each_index do |i|
|
600
|
-
if search_arr[i] < prev_level
|
601
|
-
parents << search_arr.length-i-1
|
602
|
-
prev_level = search_arr[i]
|
603
|
-
end
|
604
|
-
end
|
605
|
-
# When the element has several generations of parents, we want its top parent to be first in the returned array:
|
606
|
-
parents = parents.reverse
|
607
|
-
value = parents if parents.length > 0
|
608
|
-
end
|
609
|
-
end
|
610
|
-
end
|
611
|
-
return value
|
612
|
-
end
|
613
|
-
|
614
|
-
|
615
|
-
##############################################
|
616
|
-
####### START OF METHODS FOR PRINTING INFORMATION:######
|
617
|
-
##############################################
|
618
|
-
|
619
|
-
|
620
|
-
# Prints the information of all elements stored in the DICOM object.
|
621
|
-
# This method is kept for backwards compatibility.
|
622
|
-
# Instead of calling print_all you may use print(true) for the same functionality.
|
623
|
-
def print_all
|
624
|
-
print(true)
|
625
|
-
end
|
626
|
-
|
627
|
-
|
628
|
-
# Prints the information of the specified elements: Index, [hierarchy level, tree visualisation,] tag, name, type, length, value
|
629
|
-
# The supplied variable may be a single position, an array of positions, or true - which will make the method print all elements.
|
630
|
-
# Optional arguments:
|
631
|
-
# :levels => true - method will print the level numbers for each element.
|
632
|
-
# :tree => true - method will print a tree structure for the elements.
|
633
|
-
# :file => true - method will print to file instead of printing to screen.
|
634
|
-
def print(pos, options={})
|
635
|
-
# Process option values, setting defaults for the ones that are not specified:
|
636
|
-
opt_levels = options[:levels] || false
|
637
|
-
opt_tree = options[:tree] || false
|
638
|
-
opt_file = options[:file] || false
|
639
|
-
# If pos is false, abort, and inform the user:
|
640
|
-
if pos == false
|
641
|
-
add_msg("Warning: Method print was supplied false instead of a valid position. Aborting print.")
|
642
|
-
return
|
643
|
-
end
|
644
|
-
if not pos.is_a?(Array) and pos != true
|
645
|
-
# Convert to array if number:
|
646
|
-
pos_valid = [pos]
|
647
|
-
elsif pos == true
|
648
|
-
# Create a complete array of indices:
|
649
|
-
pos_valid = Array.new(@names.length) {|i| i}
|
650
|
-
else
|
651
|
-
# Use the supplied array of numbers:
|
652
|
-
pos_valid = pos
|
653
|
-
end
|
654
|
-
# Extract the information to be printed from the object arrays:
|
655
|
-
indices = Array.new
|
656
|
-
levels = Array.new
|
657
|
-
tags = Array.new
|
658
|
-
names = Array.new
|
659
|
-
types = Array.new
|
660
|
-
lengths = Array.new
|
661
|
-
values = Array.new
|
662
|
-
# There may be a more elegant way to do this.
|
663
|
-
pos_valid.each do |pos|
|
664
|
-
tags << @tags[pos]
|
665
|
-
levels << @levels[pos]
|
666
|
-
names << @names[pos]
|
667
|
-
types << @types[pos]
|
668
|
-
lengths << @lengths[pos].to_s
|
669
|
-
values << @values[pos].to_s
|
670
|
-
end
|
671
|
-
# We have collected the data that is to be printed, now we need to do some string manipulation if hierarchy is to be displayed:
|
672
|
-
if opt_tree
|
673
|
-
# Tree structure requested.
|
674
|
-
front_symbol = "| "
|
675
|
-
tree_symbol = "|_"
|
676
|
-
tags.each_index do |i|
|
677
|
-
if levels[i] != 0
|
678
|
-
tags[i] = front_symbol*(levels[i]-1) + tree_symbol + tags[i]
|
679
|
-
end
|
680
|
-
end
|
681
|
-
end
|
682
|
-
# Extract the string lengths which are needed to make the formatting nice:
|
683
|
-
tag_lengths = Array.new
|
684
|
-
name_lengths = Array.new
|
685
|
-
type_lengths = Array.new
|
686
|
-
length_lengths = Array.new
|
687
|
-
names.each_index do |i|
|
688
|
-
tag_lengths[i] = tags[i].length
|
689
|
-
name_lengths[i] = names[i].length
|
690
|
-
type_lengths[i] = types[i].length
|
691
|
-
length_lengths[i] = lengths[i].to_s.length
|
692
|
-
end
|
693
|
-
# To give the printed output a nice format we need to check the string lengths of some of these arrays:
|
694
|
-
index_maxL = pos_valid.max.to_s.length
|
695
|
-
tag_maxL = tag_lengths.max
|
696
|
-
name_maxL = name_lengths.max
|
697
|
-
type_maxL = type_lengths.max
|
698
|
-
length_maxL = length_lengths.max
|
699
|
-
# Construct the strings, one for each line of output, where each line contain the information of one data element:
|
700
|
-
elements = Array.new
|
701
|
-
# Start of loop which formats the element data:
|
702
|
-
# (This loop is what consumes most of the computing time of this method)
|
703
|
-
tags.each_index do |i|
|
704
|
-
# Configure empty spaces:
|
705
|
-
s = " "
|
706
|
-
f0 = " "*(index_maxL-pos_valid[i].to_s.length)
|
707
|
-
f2 = " "*(tag_maxL-tags[i].length+1)
|
708
|
-
f3 = " "*(name_maxL-names[i].length+1)
|
709
|
-
f4 = " "*(type_maxL-types[i].length+1)
|
710
|
-
f5 = " "*(length_maxL-lengths[i].to_s.length)
|
711
|
-
# Display levels?
|
712
|
-
if opt_levels
|
713
|
-
lev = levels[i].to_s + s
|
714
|
-
else
|
715
|
-
lev = ""
|
716
|
-
end
|
717
|
-
# Restrict length of value string:
|
718
|
-
if values[i].length > 28
|
719
|
-
value = (values[i])[0..27]+" ..."
|
720
|
-
else
|
721
|
-
value = (values[i])
|
722
|
-
end
|
723
|
-
# Insert descriptive text for elements that hold binary data:
|
724
|
-
case types[i]
|
725
|
-
when "OW","OB","UN"
|
726
|
-
value = "(Binary Data)"
|
727
|
-
when "SQ","()"
|
728
|
-
value = "(Encapsulated Elements)"
|
729
|
-
end
|
730
|
-
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)
|
731
|
-
end
|
732
|
-
# Print to either screen or file, depending on what the user requested:
|
733
|
-
if opt_file
|
734
|
-
print_file(elements)
|
735
|
-
else
|
736
|
-
print_screen(elements)
|
737
|
-
end
|
738
|
-
end # of print
|
739
|
-
|
740
|
-
|
741
|
-
# Prints the key structural properties of the DICOM file.
|
742
|
-
def print_properties
|
743
|
-
# Explicitness:
|
744
|
-
if @explicit
|
745
|
-
explicit = "Explicit"
|
746
|
-
else
|
747
|
-
explicit = "Implicit"
|
748
|
-
end
|
749
|
-
# Endianness:
|
750
|
-
if @file_endian
|
751
|
-
endian = "Big Endian"
|
752
|
-
else
|
753
|
-
endian = "Little Endian"
|
754
|
-
end
|
755
|
-
# Pixel data:
|
756
|
-
if @compression == nil
|
757
|
-
pixels = "No"
|
758
|
-
else
|
759
|
-
pixels = "Yes"
|
760
|
-
end
|
761
|
-
# Colors:
|
762
|
-
if @color
|
763
|
-
image = "Colors"
|
764
|
-
else
|
765
|
-
image = "Greyscale"
|
766
|
-
end
|
767
|
-
# Compression:
|
768
|
-
if @compression == true
|
769
|
-
compression = @lib.get_uid(get_value("0002,0010").rstrip)
|
770
|
-
else
|
771
|
-
compression = "No"
|
772
|
-
end
|
773
|
-
# Bits per pixel (allocated):
|
774
|
-
bits = get_value("0028,0100", :array => true)
|
775
|
-
bits = bits[0].to_s if bits
|
776
|
-
# Print the file properties:
|
777
|
-
puts "Key properties of DICOM object:"
|
778
|
-
puts "-------------------------------"
|
779
|
-
puts "File: " + @file
|
780
|
-
puts "Modality: " + @modality.to_s
|
781
|
-
puts "Value repr.: " + explicit
|
782
|
-
puts "Byte order: " + endian
|
783
|
-
puts "Pixel data: " + pixels
|
784
|
-
if pixels == "Yes"
|
785
|
-
puts "Image: " + image
|
786
|
-
puts "Compression: " + compression
|
787
|
-
puts "Bits per pixel: " + bits
|
788
|
-
end
|
789
|
-
puts "-------------------------------"
|
790
|
-
end # of print_properties
|
791
|
-
|
792
|
-
|
793
|
-
####################################################
|
794
|
-
### START OF METHODS FOR WRITING INFORMATION TO THE DICOM OBJECT:
|
795
|
-
####################################################
|
796
|
-
|
797
|
-
|
798
|
-
# Reads binary information from file and inserts it in the pixel data element:
|
799
|
-
def set_image_file(file)
|
800
|
-
# Try to read file:
|
801
|
-
begin
|
802
|
-
f = File.new(file, "rb")
|
803
|
-
bin = f.read(f.stat.size)
|
804
|
-
rescue
|
805
|
-
# Reading file was not successful. Register an error message.
|
806
|
-
add_msg("Reading specified file was not successful for some reason. No data has been added.")
|
807
|
-
return
|
808
|
-
end
|
809
|
-
if bin.length > 0
|
810
|
-
pos = @tags.index("7FE0,0010")
|
811
|
-
# Modify element:
|
812
|
-
set_value(bin, "7FE0,0010", :create => true, :bin => true)
|
813
|
-
else
|
814
|
-
add_msg("Content of file is of zero length. Nothing to store.")
|
815
|
-
end
|
816
|
-
end
|
817
|
-
|
818
|
-
|
819
|
-
# Transfers pixel data from a RMagick object to the pixel data element:
|
820
|
-
def set_image_magick(magick_obj)
|
821
|
-
# Export the RMagick object to a standard Ruby array of numbers:
|
822
|
-
pixel_array = magick_obj.export_pixels(x=0, y=0, columns=magick_obj.columns, rows=magick_obj.rows, map="I")
|
823
|
-
# Encode this array using the standard class method:
|
824
|
-
set_value(pixel_array, "7FE0,0010", :create => true)
|
825
|
-
end
|
826
|
-
|
827
|
-
|
828
|
-
# Removes an element from the DICOM object:
|
829
|
-
def remove(element)
|
830
|
-
pos = get_pos(element)
|
831
|
-
if pos != false
|
832
|
-
if pos.length > 1
|
833
|
-
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.")
|
834
|
-
else
|
835
|
-
# Extract first array number:
|
836
|
-
pos = pos[0]
|
837
|
-
# Update group length:
|
838
|
-
if @tags[pos][5..8] != "0000"
|
839
|
-
change = @lengths[pos]
|
840
|
-
vr = @types[pos]
|
841
|
-
update_group_length(pos, vr, change, -1)
|
842
|
-
end
|
843
|
-
# Remove entry from arrays:
|
844
|
-
@tags.delete_at(pos)
|
845
|
-
@levels.delete_at(pos)
|
846
|
-
@names.delete_at(pos)
|
847
|
-
@types.delete_at(pos)
|
848
|
-
@lengths.delete_at(pos)
|
849
|
-
@values.delete_at(pos)
|
850
|
-
@raw.delete_at(pos)
|
851
|
-
end
|
852
|
-
else
|
853
|
-
add_msg("Warning: The data element #{element} could not be found in the DICOM object. Method remove has no data element to remove.")
|
854
|
-
end
|
855
|
-
end
|
856
|
-
|
857
|
-
|
858
|
-
# Sets the value of a data element by modifying an existing element or creating a new one.
|
859
|
-
# If the supplied value is not binary, it will attempt to encode the value to binary itself.
|
860
|
-
def set_value(value, element, options={})
|
861
|
-
# Options:
|
862
|
-
create = options[:create] # =false means no element creation
|
863
|
-
bin = options[:bin] # =true means value already encoded
|
864
|
-
# Retrieve array position:
|
865
|
-
pos = get_pos(element)
|
866
|
-
# We do not support changing multiple data elements:
|
867
|
-
if pos.is_a?(Array)
|
868
|
-
if pos.length > 1
|
869
|
-
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.")
|
870
|
-
return
|
871
|
-
end
|
872
|
-
end
|
873
|
-
if pos == false and create == false
|
874
|
-
# Since user has requested an element shall only be updated, we can not do so as the element position is not valid:
|
875
|
-
add_msg("Warning: Invalid data element provided to method set_value. Value NOT updated.")
|
876
|
-
elsif create == false
|
877
|
-
# Modify element:
|
878
|
-
modify_element(value, pos[0], :bin => bin)
|
879
|
-
else
|
880
|
-
# User wants to create an element (or modify it if it is already present).
|
881
|
-
unless pos == false
|
882
|
-
# The data element already exist, so we modify instead of creating:
|
883
|
-
modify_element(value, pos[0], :bin => bin)
|
884
|
-
else
|
885
|
-
# We need to create element:
|
886
|
-
tag = @lib.get_tag(element)
|
887
|
-
if tag == false
|
888
|
-
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').")
|
889
|
-
else
|
890
|
-
# As we wish to create a new data element, we need to find out where to insert it in the element arrays:
|
891
|
-
# We will do this by finding the array position of the last element that will (alphabetically/numerically) stay in front of this element.
|
892
|
-
if @tags.size > 0
|
893
|
-
# Search the array:
|
894
|
-
index = -1
|
895
|
-
quit = false
|
896
|
-
while quit != true do
|
897
|
-
if index+1 >= @tags.length # We have reached end of array.
|
898
|
-
quit = true
|
899
|
-
elsif tag < @tags[index+1] and @levels[index+1] == 0 # We are past the correct position (only match against top level tags).
|
900
|
-
quit = true
|
901
|
-
else # Increase index in anticipation of a 'hit'.
|
902
|
-
index += 1
|
903
|
-
end
|
904
|
-
end # of while
|
905
|
-
else
|
906
|
-
# We are dealing with an empty DICOM object:
|
907
|
-
index = nil
|
908
|
-
end
|
909
|
-
# The necessary information is gathered; create new data element:
|
910
|
-
create_element(value, tag, index, :bin => bin)
|
911
|
-
end
|
912
|
-
end
|
913
|
-
end
|
914
|
-
end # of set_value
|
915
|
-
|
916
|
-
|
917
|
-
##################################################
|
918
|
-
############## START OF PRIVATE METHODS: ########
|
919
|
-
##################################################
|
920
|
-
private
|
921
|
-
|
922
|
-
|
923
|
-
# Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
|
924
|
-
def add_msg(msg)
|
925
|
-
puts msg if @verbose
|
926
|
-
@errors << msg
|
927
|
-
@errors.flatten
|
928
|
-
end
|
929
|
-
|
930
|
-
|
931
|
-
# Creates a new data element:
|
932
|
-
def create_element(value, tag, last_pos, options={})
|
933
|
-
bin_only = options[:bin]
|
934
|
-
# Fetch the VR:
|
935
|
-
info = @lib.get_name_vr(tag)
|
936
|
-
vr = info[1]
|
937
|
-
name = info[0]
|
938
|
-
# Encode binary (if a binary is not provided):
|
939
|
-
if bin_only == true
|
940
|
-
# Data already encoded.
|
941
|
-
bin = value
|
942
|
-
value = nil
|
943
|
-
else
|
944
|
-
if vr != "UN"
|
945
|
-
# Encode:
|
946
|
-
bin = encode(value, vr)
|
947
|
-
else
|
948
|
-
add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
|
949
|
-
end
|
950
|
-
end
|
951
|
-
# Put the information of this data element into the arrays:
|
952
|
-
if bin
|
953
|
-
# 4 different scenarios: Array is empty, or: element is put in front, inside array, or at end of array:
|
954
|
-
# NB! No support for hierarchy at this time! Defaulting to level = 0.
|
955
|
-
if last_pos == nil
|
956
|
-
# We have empty DICOM object:
|
957
|
-
@tags = [tag]
|
958
|
-
@levels = [0]
|
959
|
-
@names = [name]
|
960
|
-
@types = [vr]
|
961
|
-
@lengths = [bin.length]
|
962
|
-
@values = [value]
|
963
|
-
@raw = [bin]
|
964
|
-
elsif last_pos == -1
|
965
|
-
# Insert in front of arrays:
|
966
|
-
@tags = [tag] + @tags
|
967
|
-
@levels = [0] + @levels
|
968
|
-
@names = [name] + @names
|
969
|
-
@types = [vr] + @types
|
970
|
-
@lengths = [bin.length] + @lengths
|
971
|
-
@values = [value] + @values
|
972
|
-
@raw = [bin] + @raw
|
973
|
-
elsif last_pos == @tags.length-1
|
974
|
-
# Insert at end arrays:
|
975
|
-
@tags = @tags + [tag]
|
976
|
-
@levels = @levels + [0]
|
977
|
-
@names = @names + [name]
|
978
|
-
@types = @types + [vr]
|
979
|
-
@lengths = @lengths + [bin.length]
|
980
|
-
@values = @values + [value]
|
981
|
-
@raw = @raw + [bin]
|
982
|
-
else
|
983
|
-
# Insert somewhere inside the array:
|
984
|
-
@tags = @tags[0..last_pos] + [tag] + @tags[(last_pos+1)..(@tags.length-1)]
|
985
|
-
@levels = @levels[0..last_pos] + [0] + @levels[(last_pos+1)..(@levels.length-1)]
|
986
|
-
@names = @names[0..last_pos] + [name] + @names[(last_pos+1)..(@names.length-1)]
|
987
|
-
@types = @types[0..last_pos] + [vr] + @types[(last_pos+1)..(@types.length-1)]
|
988
|
-
@lengths = @lengths[0..last_pos] + [bin.length] + @lengths[(last_pos+1)..(@lengths.length-1)]
|
989
|
-
@values = @values[0..last_pos] + [value] + @values[(last_pos+1)..(@values.length-1)]
|
990
|
-
@raw = @raw[0..last_pos] + [bin] + @raw[(last_pos+1)..(@raw.length-1)]
|
991
|
-
end
|
992
|
-
# Update last index variable as we have added to our arrays:
|
993
|
-
@last_index += 1
|
994
|
-
# Update group length (as long as it was not a group length element that was created):
|
995
|
-
pos = @tags.index(tag)
|
996
|
-
if @tags[pos][5..8] != "0000"
|
997
|
-
change = bin.length
|
998
|
-
update_group_length(pos, vr, change, 1)
|
999
|
-
end
|
1000
|
-
else
|
1001
|
-
add_msg("Binary is nil. Nothing to save.")
|
1002
|
-
end
|
1003
|
-
end # of create_element
|
1004
|
-
|
1005
|
-
|
1006
|
-
# Encodes a value to binary (used for inserting values into a DICOM object).
|
1007
|
-
def encode(value, vr)
|
1008
|
-
# VR will decide how to encode this value:
|
1009
|
-
case vr
|
1010
|
-
when "AT" # (Data element tag: Assume it has the format "GGGG,EEEE"
|
1011
|
-
if value.is_a_tag?
|
1012
|
-
bin = @stream.encode_tag(value)
|
1013
|
-
else
|
1014
|
-
add_msg("Invalid tag format (#{value}). Expected format: 'GGGG,EEEE'")
|
1015
|
-
end
|
1016
|
-
# We have a number of VRs that are encoded as string:
|
1017
|
-
when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT'
|
1018
|
-
# In case we are dealing with a number string element, the supplied value might be a number
|
1019
|
-
# instead of a string, and as such, we convert to string just to make sure this will work nicely:
|
1020
|
-
value = value.to_s
|
1021
|
-
bin = @stream.encode_value(value, "STR")
|
1022
|
-
# Image related value representations:
|
1023
|
-
when "OW"
|
1024
|
-
# What bit depth to use when encoding the pixel data?
|
1025
|
-
bit_depth = get_value("0028,0100")
|
1026
|
-
if bit_depth == false
|
1027
|
-
# Data element not specified:
|
1028
|
-
add_msg("Attempted to encode pixel data, but 'Bit Depth' data element is missing (0028,0100).")
|
1029
|
-
else
|
1030
|
-
# 8,12 or 16 bits?
|
1031
|
-
case bit_depth
|
1032
|
-
when 8
|
1033
|
-
bin = @stream.encode(value, "BY")
|
1034
|
-
when 12
|
1035
|
-
# 12 bit not supported yet!
|
1036
|
-
add_msg("Encoding 12 bit pixel values not supported yet. Please change the bit depth to 8 or 16 bits.")
|
1037
|
-
when 16
|
1038
|
-
bin = @stream.encode(value, "US")
|
1039
|
-
else
|
1040
|
-
# Unknown bit depth:
|
1041
|
-
add_msg("Unknown bit depth #{bit_depth}. No data encoded.")
|
1042
|
-
end
|
1043
|
-
end
|
1044
|
-
# All other VR's:
|
1045
|
-
else
|
1046
|
-
# Just encode:
|
1047
|
-
bin = @stream.encode(value, vr)
|
1048
|
-
end # of case vr
|
1049
|
-
return bin
|
1050
|
-
end # of encode
|
1051
|
-
|
1052
|
-
|
1053
|
-
# Modifies existing data element:
|
1054
|
-
def modify_element(value, pos, options={})
|
1055
|
-
bin_only = options[:bin]
|
1056
|
-
# Fetch the VR and old length:
|
1057
|
-
vr = @types[pos]
|
1058
|
-
old_length = @lengths[pos]
|
1059
|
-
# Encode binary (if a binary is not provided):
|
1060
|
-
if bin_only == true
|
1061
|
-
# Data already encoded.
|
1062
|
-
bin = value
|
1063
|
-
value = nil
|
1064
|
-
else
|
1065
|
-
if vr != "UN"
|
1066
|
-
# Encode:
|
1067
|
-
bin = encode(value, vr)
|
1068
|
-
else
|
1069
|
-
add_msg("Error. Unable to encode data element value of unknown type (Value Representation)!")
|
1070
|
-
end
|
1071
|
-
end
|
1072
|
-
# Update the arrays with this new information:
|
1073
|
-
if bin
|
1074
|
-
# Replace array entries for this element:
|
1075
|
-
#@types[pos] = vr # for the time being there is no logic for updating type.
|
1076
|
-
@lengths[pos] = bin.length
|
1077
|
-
@values[pos] = value
|
1078
|
-
@raw[pos] = bin
|
1079
|
-
# Update group length (as long as it was not the group length that was modified):
|
1080
|
-
if @tags[pos][5..8] != "0000"
|
1081
|
-
change = bin.length - old_length
|
1082
|
-
update_group_length(pos, vr, change, 0)
|
1083
|
-
end
|
1084
|
-
else
|
1085
|
-
add_msg("Binary is nil. Nothing to save.")
|
1086
|
-
end
|
1087
|
-
end
|
1088
|
-
|
1089
|
-
|
1090
|
-
# Prints the selected elements to an ascii text file.
|
1091
|
-
# The text file will be saved in the folder of the original DICOM file,
|
1092
|
-
# with the original file name plus a .txt extension.
|
1093
|
-
def print_file(elements)
|
1094
|
-
File.open( @file + '.txt', 'w' ) do |output|
|
1095
|
-
elements.each do | line |
|
1096
|
-
output.print line + "\n"
|
1097
|
-
end
|
1098
|
-
end
|
1099
|
-
end
|
1100
|
-
|
1101
|
-
|
1102
|
-
# Prints the selected elements to screen.
|
1103
|
-
def print_screen(elements)
|
1104
|
-
elements.each do |element|
|
1105
|
-
puts element
|
1106
|
-
end
|
1107
|
-
end
|
1108
|
-
|
1109
|
-
|
1110
|
-
# Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
|
1111
|
-
def set_modality
|
1112
|
-
value = get_value("0008,0016", :silent => true)
|
1113
|
-
if value == false
|
1114
|
-
@modality = "Not specified"
|
1115
|
-
else
|
1116
|
-
modality = @lib.get_uid(value.rstrip)
|
1117
|
-
@modality = modality
|
1118
|
-
end
|
1119
|
-
end
|
1120
|
-
|
1121
|
-
|
1122
|
-
# Handles the creation of a DWrite object, and returns this object to the calling method.
|
1123
|
-
def set_write_object(file_name = nil, transfer_syntax = nil)
|
1124
|
-
unless transfer_syntax
|
1125
|
-
transfer_syntax = get_value("0002,0010", :silent => true)
|
1126
|
-
transfer_syntax = "1.2.840.10008.1.2" if not transfer_syntax # Default is implicit, little endian
|
1127
|
-
end
|
1128
|
-
w = DWrite.new(file_name, :lib => @lib, :sys_endian => @sys_endian, :transfer_syntax => transfer_syntax)
|
1129
|
-
w.tags = @tags
|
1130
|
-
w.types = @types
|
1131
|
-
w.lengths = @lengths
|
1132
|
-
w.raw = @raw
|
1133
|
-
w.rest_endian = @file_endian
|
1134
|
-
w.rest_explicit = @explicit
|
1135
|
-
return w
|
1136
|
-
end
|
1137
|
-
|
1138
|
-
|
1139
|
-
# Updates the group length value when a data element has been updated, created or removed:
|
1140
|
-
# The variable change holds the change in value length for the updated data element.
|
1141
|
-
# (Change should be positive when a data element is removed - it will only be negative when editing an element to a shorter value)
|
1142
|
-
# The variable existance is -1 if data element has been removed, +1 if element has been added and 0 if it has been updated.
|
1143
|
-
# (Perhaps in the future this functionality might be moved to the DWrite class, it might give an easier implementation)
|
1144
|
-
def update_group_length(pos, type, change, existance)
|
1145
|
-
# Find position of relevant group length (if it exists):
|
1146
|
-
gl_pos = @tags.index(@tags[pos][0..4] + "0000")
|
1147
|
-
existance = 0 if existance == nil
|
1148
|
-
# If it exists, calculate change:
|
1149
|
-
if gl_pos
|
1150
|
-
if existance == 0
|
1151
|
-
# Element has only been updated, so we only need to think about value change:
|
1152
|
-
value = @values[gl_pos] + change
|
1153
|
-
else
|
1154
|
-
# Element has either been created or removed. This means we need to calculate the length of its other parts.
|
1155
|
-
if @explicit
|
1156
|
-
# In the explicit scenario it is slightly complex to determine this value:
|
1157
|
-
element_length = 0
|
1158
|
-
# VR?:
|
1159
|
-
unless @tags[pos] == "FFFE,E000" or @tags[pos] == "FFFE,E00D" or @tags[pos] == "FFFE,E0DD"
|
1160
|
-
element_length += 2
|
1161
|
-
end
|
1162
|
-
# Length value:
|
1163
|
-
case @types[pos]
|
1164
|
-
when "OB","OW","SQ","UN"
|
1165
|
-
if pos > @tags.index("7FE0,0010").to_i and @tags.index("7FE0,0010").to_i != 0
|
1166
|
-
element_length += 4
|
1167
|
-
else
|
1168
|
-
element_length += 6
|
1169
|
-
end
|
1170
|
-
when "()"
|
1171
|
-
element_length += 4
|
1172
|
-
else
|
1173
|
-
element_length += 2
|
1174
|
-
end # of case
|
1175
|
-
else
|
1176
|
-
# In the implicit scenario it is easier:
|
1177
|
-
element_length = 4
|
1178
|
-
end
|
1179
|
-
# Update group length for creation/deletion scenario:
|
1180
|
-
change = (4 + element_length + change) * existance
|
1181
|
-
value = @values[gl_pos] + change
|
1182
|
-
end
|
1183
|
-
# Write the new Group Length value:
|
1184
|
-
# Encode the new value to binary:
|
1185
|
-
bin = encode(value, "UL")
|
1186
|
-
# Update arrays:
|
1187
|
-
@values[gl_pos] = value
|
1188
|
-
@raw[gl_pos] = bin
|
1189
|
-
end
|
1190
|
-
end # of update_group_length
|
1191
|
-
|
1192
|
-
|
1193
|
-
end # of class
|
1194
|
-
end # of module
|