dicom 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +47 -25
- data/DOCUMENTATION +72 -43
- data/README +20 -20
- data/lib/Anonymizer.rb +61 -56
- data/lib/DLibrary.rb +47 -24
- data/lib/DObject.rb +457 -461
- data/lib/DRead.rb +131 -130
- data/lib/DWrite.rb +87 -89
- data/lib/Dictionary.rb +14 -12
- data/lib/dicom.rb +1 -0
- data/lib/ruby_extensions.rb +25 -0
- metadata +12 -9
data/lib/DWrite.rb
CHANGED
@@ -1,31 +1,29 @@
|
|
1
|
-
# Copyright 2008-2009 Christoffer
|
1
|
+
# Copyright 2008-2009 Christoffer Lervag
|
2
2
|
|
3
3
|
# Some notes about this DICOM file writing class:
|
4
|
-
# In its current state, this class will always try to write the file such that it is
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# to modify the source code here to avoid this.
|
4
|
+
# In its current state, this class will always try to write the file such that it is compliant to the
|
5
|
+
# official standard (DICOM 3 Part 10), containing header and meta information (group 0002).
|
6
|
+
# If this is unwanted behaviour, it is easy to modify the source code here to avoid this.
|
8
7
|
#
|
9
|
-
# It is important to note, that while the goal is to be fully DICOM compliant,
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#Please contact the author if you discover any issues with file creation.
|
8
|
+
# It is important to note, that while the goal is to be fully DICOM compliant, no guarantees are given
|
9
|
+
# that this is actually achieved. You are encouraged to thouroughly test your files for compatibility after creation.
|
10
|
+
# Please contact the author if you discover any issues with file creation.
|
13
11
|
|
14
12
|
module DICOM
|
15
13
|
# Class for writing the data from DObject to a valid DICOM file:
|
16
14
|
class DWrite
|
17
|
-
attr_writer :
|
15
|
+
attr_writer :tags, :types, :lengths, :raw, :rest_endian, :rest_explicit
|
18
16
|
attr_reader :success, :msg
|
19
|
-
|
17
|
+
|
20
18
|
# Initialize the DWrite instance.
|
21
19
|
def initialize(file_name=nil, opts={})
|
22
20
|
# Process option values, setting defaults for the ones that are not specified:
|
23
21
|
@lib = opts[:lib] || DLibrary.new
|
24
22
|
@sys_endian = opts[:sys_endian] || false
|
25
23
|
@file_name = file_name
|
26
|
-
|
27
|
-
# Create arrays used for storing
|
28
|
-
@
|
24
|
+
|
25
|
+
# Create arrays used for storing data element information:
|
26
|
+
@tags = Array.new
|
29
27
|
@types = Array.new
|
30
28
|
@lengths = Array.new
|
31
29
|
@raw = Array.new
|
@@ -37,11 +35,11 @@ module DICOM
|
|
37
35
|
# Endianness of the remaining groups after the first group:
|
38
36
|
@rest_endian = false
|
39
37
|
end # of method initialize
|
40
|
-
|
41
|
-
|
38
|
+
|
39
|
+
|
42
40
|
# Writes the DICOM information to file.
|
43
41
|
def write()
|
44
|
-
if @
|
42
|
+
if @tags.size > 0
|
45
43
|
# Check if we are able to create given file:
|
46
44
|
open_file(@file_name)
|
47
45
|
# Read the initial header of the file:
|
@@ -52,11 +50,11 @@ module DICOM
|
|
52
50
|
write_header()
|
53
51
|
# Write meta information (if it is not present in the DICOM object):
|
54
52
|
write_meta()
|
55
|
-
# Write
|
56
|
-
@
|
57
|
-
|
53
|
+
# Write data elements:
|
54
|
+
@tags.each_index do |i|
|
55
|
+
write_data_element(i)
|
58
56
|
end
|
59
|
-
# We are finished writing the
|
57
|
+
# We are finished writing the data elements, and as such, can close the file:
|
60
58
|
@file.close()
|
61
59
|
# Mark this write session as successful:
|
62
60
|
@success = true
|
@@ -66,15 +64,15 @@ module DICOM
|
|
66
64
|
return
|
67
65
|
end # of if @file != nil
|
68
66
|
else
|
69
|
-
@msg += ["Error. No
|
70
|
-
end # of if @
|
67
|
+
@msg += ["Error. No data elements to write."]
|
68
|
+
end # of if @tags.size > 0
|
71
69
|
end # of method write
|
72
|
-
|
73
|
-
|
70
|
+
|
71
|
+
|
74
72
|
# Following methods are private:
|
75
73
|
private
|
76
|
-
|
77
|
-
|
74
|
+
|
75
|
+
|
78
76
|
# Writes the official DICOM header:
|
79
77
|
def write_header()
|
80
78
|
# Fill in 128 empty bytes:
|
@@ -82,24 +80,24 @@ module DICOM
|
|
82
80
|
# Write the string "DICM" which is central to DICOM standards compliance:
|
83
81
|
@file.write("DICM")
|
84
82
|
end # of write_header
|
85
|
-
|
86
|
-
|
83
|
+
|
84
|
+
|
87
85
|
# Inserts group 0002 if it is missing, to ensure DICOM compliance.
|
88
86
|
def write_meta()
|
89
87
|
# We will check for the existance of 5 group 0002 elements, and if they are not present, we will insert them:
|
90
88
|
pos = Array.new()
|
91
89
|
meta = Array.new()
|
92
90
|
# File Meta Information Version:
|
93
|
-
pos += [@
|
91
|
+
pos += [@tags.index("0002,0001")]
|
94
92
|
meta += [["0002,0001", "OB", 2, ["0100"].pack("H*")]]
|
95
93
|
# Transfer Syntax UID:
|
96
|
-
pos += [@
|
94
|
+
pos += [@tags.index("0002,0010")]
|
97
95
|
meta += [["0002,0010", "UI", 18, ["1.2.840.10008.1.2"].pack("a*")+["00"].pack("H*")]] # Implicit, little endian
|
98
96
|
# Implementation Class UID:
|
99
|
-
pos += [@
|
97
|
+
pos += [@tags.index("0002,0012")]
|
100
98
|
meta += [["0002,0012", "UI", 26, ["1.2.826.0.1.3680043.8.641"].pack("a*")+["00"].pack("H*")]] # Ruby DICOM UID
|
101
99
|
# Implementation Version Name:
|
102
|
-
pos += [@
|
100
|
+
pos += [@tags.index("0002,0013")]
|
103
101
|
meta += [["0002,0013", "SH", 10, ["RUBY_DICOM"].pack("a*")]]
|
104
102
|
# Insert meta information:
|
105
103
|
meta_added = false
|
@@ -107,14 +105,14 @@ module DICOM
|
|
107
105
|
# Only insert element if it does not already exist (corresponding pos element shows no match):
|
108
106
|
if pos[i] == nil
|
109
107
|
meta_added = true
|
110
|
-
# Find where to insert this data element
|
108
|
+
# Find where to insert this data element.
|
111
109
|
index = -1
|
112
|
-
|
110
|
+
tag = meta[i][0]
|
113
111
|
quit = false
|
114
112
|
while quit != true do
|
115
|
-
if
|
113
|
+
if tag < @tags[index+1]
|
116
114
|
quit = true
|
117
|
-
elsif @
|
115
|
+
elsif @tags[index+1][0..3] != "0002"
|
118
116
|
# Abort to avoid needlessly going through the whole array.
|
119
117
|
quit = true
|
120
118
|
else
|
@@ -125,13 +123,13 @@ module DICOM
|
|
125
123
|
# Insert data element in the correct array position:
|
126
124
|
if index == -1
|
127
125
|
# Insert at the beginning of array:
|
128
|
-
@
|
126
|
+
@tags = [meta[i][0]] + @tags
|
129
127
|
@types = [meta[i][1]] + @types
|
130
128
|
@lengths = [meta[i][2]] + @lengths
|
131
129
|
@raw = [meta[i][3]] + @raw
|
132
130
|
else
|
133
131
|
# One or more elements comes before this element:
|
134
|
-
@
|
132
|
+
@tags = @tags[0..index] + [meta[i][0]] + @tags[(index+1)..(@tags.length-1)]
|
135
133
|
@types = @types[0..index] + [meta[i][1]] + @types[(index+1)..(@types.length-1)]
|
136
134
|
@lengths = @lengths[0..index] + [meta[i][2]] + @lengths[(index+1)..(@lengths.length-1)]
|
137
135
|
@raw = @raw[0..index] + [meta[i][3]] + @raw[(index+1)..(@raw.length-1)]
|
@@ -143,86 +141,86 @@ module DICOM
|
|
143
141
|
quit = false
|
144
142
|
j = 0
|
145
143
|
while quit == false do
|
146
|
-
if @
|
144
|
+
if @tags[j][0..3] != "0002"
|
147
145
|
quit = true
|
148
146
|
else
|
149
147
|
# Add to length if group 0002:
|
150
|
-
if @
|
148
|
+
if @tags[j] != "0002,0000"
|
151
149
|
if @types[j] == "OB"
|
152
150
|
length += 12 + @lengths[j]
|
153
151
|
else
|
154
152
|
length += 8 + @lengths[j]
|
155
153
|
end
|
156
154
|
end
|
157
|
-
j += 1
|
158
|
-
end # of if @
|
155
|
+
j += 1
|
156
|
+
end # of if @tags[j][0..3]..
|
159
157
|
end # of while
|
160
158
|
# Set group length:
|
161
|
-
gl_pos = @
|
159
|
+
gl_pos = @tags.index("0002,0000")
|
162
160
|
gl_info = ["0002,0000", "UL", 4, [length].pack("I*")]
|
163
161
|
# Update group length, but only if there have been some modifications or GL is nonexistant:
|
164
162
|
if meta_added == true or gl_pos != nil
|
165
163
|
if gl_pos == nil
|
166
164
|
# Add group length (to beginning of arrays):
|
167
|
-
@
|
165
|
+
@tags = [gl_info[0]] + @tags
|
168
166
|
@types = [gl_info[1]] + @types
|
169
167
|
@lengths = [gl_info[2]] + @lengths
|
170
168
|
@raw = [gl_info[3]] + @raw
|
171
169
|
else
|
172
170
|
# Edit existing group length:
|
173
|
-
@
|
171
|
+
@tags[gl_pos] = gl_info[0]
|
174
172
|
@types[gl_pos] = gl_info[1]
|
175
173
|
@lengths[gl_pos] = gl_info[2]
|
176
174
|
@raw[gl_pos] = gl_info[3]
|
177
175
|
end
|
178
176
|
end
|
179
177
|
end # of method write_meta
|
180
|
-
|
181
|
-
|
182
|
-
# Writes
|
183
|
-
def
|
184
|
-
# Step 1: Write
|
185
|
-
|
178
|
+
|
179
|
+
|
180
|
+
# Writes each data element to file:
|
181
|
+
def write_data_element(i)
|
182
|
+
# Step 1: Write tag:
|
183
|
+
write_tag(i)
|
186
184
|
# Step 2: Write [type] and value length:
|
187
185
|
write_type_length(i)
|
188
186
|
# Step 3: Write value:
|
189
187
|
write_value(i)
|
190
188
|
# If DICOM object contains encapsulated pixel data, we need some special handling for its items:
|
191
|
-
if @
|
189
|
+
if @tags[i] == "7FE0,0010"
|
192
190
|
@enc_image = true if @lengths[i].to_i == 0
|
193
191
|
end
|
194
192
|
# Should have some kind of test that the last data was written succesfully?
|
195
|
-
end # of method
|
196
|
-
|
197
|
-
|
198
|
-
# Writes the
|
199
|
-
def
|
200
|
-
#
|
193
|
+
end # of method write_data_element
|
194
|
+
|
195
|
+
|
196
|
+
# Writes the tag (first part of the data element):
|
197
|
+
def write_tag(i)
|
198
|
+
# Tag is originally of the form "0002,0010".
|
201
199
|
# We need to reformat to get rid of the comma:
|
202
|
-
|
200
|
+
tag = @tags[i][0..3] + @tags[i][5..8]
|
203
201
|
# Whether DICOM file is big or little endian, the first 0002 group is always little endian encoded.
|
204
202
|
# On a big endian system, I believe the order of the numbers need not be changed,
|
205
203
|
# but this has not been tested yet.
|
206
204
|
if @sys_endian == false
|
207
205
|
# System is little endian:
|
208
206
|
# Change the order of the numbers so that it becomes correct when packed as hex:
|
209
|
-
|
207
|
+
tag_corr = tag[2..3] + tag[0..1] + tag[6..7] + tag[4..5]
|
210
208
|
end
|
211
209
|
# When we shift from group 0002 to another group we need to update our endian/explicitness variables:
|
212
|
-
if
|
210
|
+
if tag[0..3] != "0002" and @switched == false
|
213
211
|
switch_syntax()
|
214
212
|
end
|
215
|
-
# Perhaps we need to rearrange the
|
213
|
+
# Perhaps we need to rearrange the tag if the file encoding is now big endian:
|
216
214
|
if not @endian
|
217
215
|
# Need to rearrange the first and second part of each string:
|
218
|
-
|
216
|
+
tag_corr = tag
|
219
217
|
end
|
220
218
|
# Write to file:
|
221
|
-
@file.write([
|
222
|
-
end # of method
|
223
|
-
|
224
|
-
|
225
|
-
# Writes the type (if to be written) and length value (these two are the middle part of the data element):
|
219
|
+
@file.write([tag_corr].pack('H*'))
|
220
|
+
end # of method write_tag
|
221
|
+
|
222
|
+
|
223
|
+
# Writes the type (VR) (if it is to be written) and length value (these two are the middle part of the data element):
|
226
224
|
def write_type_length(i)
|
227
225
|
# First some preprocessing:
|
228
226
|
# Set length value:
|
@@ -243,12 +241,12 @@ module DICOM
|
|
243
241
|
# *****EXPLICIT*****:
|
244
242
|
if @explicit == true
|
245
243
|
# Step 1: Write VR (if it is to be written)
|
246
|
-
unless @
|
247
|
-
# Write
|
244
|
+
unless @tags[i] == "FFFE,E000" or @tags[i] == "FFFE,E00D" or @tags[i] == "FFFE,E0DD"
|
245
|
+
# Write data element type (VR) (2 bytes - since we are not dealing with an item related element):
|
248
246
|
@file.write([@types[i]].pack('a*'))
|
249
247
|
end
|
250
248
|
# Step 2: Write length
|
251
|
-
# Three possible structures for value length here, dependent on
|
249
|
+
# Three possible structures for value length here, dependent on data element type:
|
252
250
|
case @types[i]
|
253
251
|
when "OB","OW","SQ","UN"
|
254
252
|
if @enc_image
|
@@ -264,11 +262,11 @@ module DICOM
|
|
264
262
|
end
|
265
263
|
when "()"
|
266
264
|
# 4 bytes:
|
267
|
-
# For
|
265
|
+
# For tags "FFFE,E000", "FFFE,E00D" and "FFFE,E0DD"
|
268
266
|
@file.write(length4)
|
269
267
|
else
|
270
268
|
# 2 bytes:
|
271
|
-
# For all the other
|
269
|
+
# For all the other data element types, value length is 2 bytes:
|
272
270
|
@file.write(length2)
|
273
271
|
end # of case type
|
274
272
|
else
|
@@ -278,15 +276,15 @@ module DICOM
|
|
278
276
|
@file.write(length4)
|
279
277
|
end # of if @explicit == true
|
280
278
|
end # of method write_type_length
|
281
|
-
|
282
|
-
|
279
|
+
|
280
|
+
|
283
281
|
# Writes the value (last part of the data element):
|
284
282
|
def write_value(i)
|
285
283
|
# This is pretty straightforward, just dump the binary data to the file:
|
286
284
|
@file.write(@raw[i])
|
287
285
|
end # of method write_value
|
288
|
-
|
289
|
-
|
286
|
+
|
287
|
+
|
290
288
|
# Tests if the file is writable and opens it.
|
291
289
|
def open_file(file)
|
292
290
|
# Two cases: File already exists or it does not.
|
@@ -324,7 +322,7 @@ module DICOM
|
|
324
322
|
|
325
323
|
# Changes encoding variables as the file writing proceeds past the initial 0002 group of the DICOM file.
|
326
324
|
def switch_syntax()
|
327
|
-
# The information from the Transfer syntax
|
325
|
+
# The information from the Transfer syntax element (if present), needs to be processed:
|
328
326
|
process_transfer_syntax()
|
329
327
|
# We only plan to run this method once:
|
330
328
|
@switched = true
|
@@ -340,11 +338,11 @@ module DICOM
|
|
340
338
|
end
|
341
339
|
|
342
340
|
|
343
|
-
# Checks the Transfer Syntax UID
|
341
|
+
# Checks the Transfer Syntax UID element and updates class variables to prepare for correct writing of DICOM file.
|
344
342
|
def process_transfer_syntax()
|
345
|
-
ts_pos = @
|
343
|
+
ts_pos = @tags.index("0002,0010")
|
346
344
|
if ts_pos != nil
|
347
|
-
ts_value = @raw[ts_pos].unpack('a*').
|
345
|
+
ts_value = @raw[ts_pos].unpack('a*').join.rstrip
|
348
346
|
valid = @lib.check_ts_validity(ts_value)
|
349
347
|
if not valid
|
350
348
|
@msg+=["Warning: Invalid/unknown transfer syntax! Will still write the file, but you should give this a closer look."]
|
@@ -375,8 +373,8 @@ module DICOM
|
|
375
373
|
end # of case ts_value
|
376
374
|
end # of if ts_pos != nil
|
377
375
|
end # of method process_syntax
|
378
|
-
|
379
|
-
|
376
|
+
|
377
|
+
|
380
378
|
# Sets the pack format strings that will be used for numbers depending on endianness of file/system.
|
381
379
|
def set_pack_strings
|
382
380
|
if @endian
|
@@ -401,14 +399,14 @@ module DICOM
|
|
401
399
|
@fd = "G*"
|
402
400
|
end
|
403
401
|
end
|
404
|
-
|
405
|
-
|
402
|
+
|
403
|
+
|
406
404
|
# Initializes the variables used when executing this program.
|
407
405
|
def init_variables()
|
408
406
|
# Variables that are accesible from outside:
|
409
407
|
# Until a DICOM write has completed successfully the status is 'unsuccessful':
|
410
408
|
@success = false
|
411
|
-
|
409
|
+
|
412
410
|
# Variables used internally:
|
413
411
|
# Default explicitness of start of DICOM file:
|
414
412
|
@explicit = true
|
@@ -424,9 +422,9 @@ module DICOM
|
|
424
422
|
end
|
425
423
|
# Set which format strings to use when unpacking numbers:
|
426
424
|
set_pack_strings
|
427
|
-
# Items contained under the
|
425
|
+
# Items contained under the Pixel Data element needs some special attention to write correctly:
|
428
426
|
@enc_image = false
|
429
427
|
end # of method init_variables
|
430
|
-
|
428
|
+
|
431
429
|
end # of class
|
432
430
|
end # of module
|
data/lib/Dictionary.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# coding: ISO-8859-1
|
2
|
+
|
3
|
+
# Copyright 2008-2009 Christoffer Lervag
|
2
4
|
|
3
5
|
module DICOM
|
4
6
|
# This class holds all the dictionary data.
|
@@ -6,7 +8,7 @@ module DICOM
|
|
6
8
|
class Dictionary
|
7
9
|
|
8
10
|
# Loads the image type library (Photometric Interpretation).
|
9
|
-
# (
|
11
|
+
# (Data element tag: 0028,0004)
|
10
12
|
def load_image_types()
|
11
13
|
return [
|
12
14
|
["MONOCHROME1", "grey level image description (high values=dark, low values=bright)"],
|
@@ -58,7 +60,7 @@ module DICOM
|
|
58
60
|
def load_uid()
|
59
61
|
return [
|
60
62
|
["1.2.840.10008.1.1", "Verification SOP Class", "SOP Class"],
|
61
|
-
# Start: Transfer syntax (
|
63
|
+
# Start: Transfer syntax (Data element tag: 0002,0010)
|
62
64
|
["1.2.840.10008.1.2", "Implicit VR Little Endian: Default Transfer Syntax for DICOM", "Transfer Syntax"],
|
63
65
|
["1.2.840.10008.1.2.1", "Explicit VR Little Endian", "Transfer Syntax"],
|
64
66
|
["1.2.840.10008.1.2.1.99", "Deflated Explicit VR Little Endian", "Transfer Syntax"],
|
@@ -360,8 +362,8 @@ module DICOM
|
|
360
362
|
end
|
361
363
|
|
362
364
|
|
363
|
-
#
|
364
|
-
def
|
365
|
+
# Data element dictionary:
|
366
|
+
def load_data_elements()
|
365
367
|
return [
|
366
368
|
# E.1 Registry of DICOM command elements
|
367
369
|
# Group 0000
|
@@ -2872,16 +2874,16 @@ module DICOM
|
|
2872
2874
|
["60xx,1500", ["LO"], "Overlay Label"],
|
2873
2875
|
["60xx,3000", ["OB","OW"], "Overlay Data"],
|
2874
2876
|
["60xx,4000", ["LT"], "Overlay Comments"], # RET
|
2875
|
-
# Pixel data and structural
|
2877
|
+
# Pixel data and structural data elements:
|
2876
2878
|
["7FE0,0010", ["OW","OB"], "Pixel Data"],
|
2877
2879
|
["FFFA,FFFA", ["SQ"], "Digital Signatures Sequence"],
|
2878
2880
|
["FFFC,FFFC", ["OB"], "Data Set Trailing Padding"],
|
2879
|
-
["FFFE,E000", ["()"], "Item"], # VR does not exist for this
|
2880
|
-
["FFFE,E00D", ["()"], "Item Delimitation Item"], # VR does not exist for this
|
2881
|
-
["FFFE,E0DD", ["()"], "Sequence Delimitation Item"] # VR does not exist for this
|
2881
|
+
["FFFE,E000", ["()"], "Item"], # VR does not exist for this element
|
2882
|
+
["FFFE,E00D", ["()"], "Item Delimitation Item"], # VR does not exist for this element
|
2883
|
+
["FFFE,E0DD", ["()"], "Sequence Delimitation Item"] # VR does not exist for this element
|
2882
2884
|
].transpose
|
2883
|
-
end # End of method
|
2884
|
-
|
2885
|
-
|
2885
|
+
end # End of method load_data_elements()
|
2886
|
+
|
2887
|
+
|
2886
2888
|
end # End of class
|
2887
2889
|
end # End of module
|