dicom 0.4 → 0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +47 -25
- data/DOCUMENTATION +72 -43
- data/README +20 -20
- data/lib/Anonymizer.rb +61 -56
- data/lib/DLibrary.rb +47 -24
- data/lib/DObject.rb +457 -461
- data/lib/DRead.rb +131 -130
- data/lib/DWrite.rb +87 -89
- data/lib/Dictionary.rb +14 -12
- data/lib/dicom.rb +1 -0
- data/lib/ruby_extensions.rb +25 -0
- metadata +12 -9
data/lib/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
|