dicom 0.9.1 → 0.9.2
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.rdoc +17 -0
- data/README.rdoc +16 -3
- data/lib/dicom.rb +3 -1
- data/lib/dicom/anonymizer.rb +35 -54
- data/lib/dicom/d_client.rb +49 -64
- data/lib/dicom/d_object.rb +511 -416
- data/lib/dicom/d_read.rb +21 -34
- data/lib/dicom/d_server.rb +21 -61
- data/lib/dicom/d_write.rb +3 -6
- data/lib/dicom/element.rb +1 -1
- data/lib/dicom/file_handler.rb +14 -9
- data/lib/dicom/image_item.rb +7 -6
- data/lib/dicom/link.rb +42 -77
- data/lib/dicom/logging.rb +158 -0
- data/lib/dicom/parent.rb +9 -8
- data/lib/dicom/sequence.rb +1 -1
- data/lib/dicom/version.rb +1 -1
- metadata +5 -4
data/lib/dicom/d_object.rb
CHANGED
@@ -1,416 +1,511 @@
|
|
1
|
-
# === TODO:
|
2
|
-
#
|
3
|
-
# * The retrieve file network functionality (get_image() in DClient class) has not been tested.
|
4
|
-
# * Make the networking code more intelligent in its handling of unexpected network communication.
|
5
|
-
# * Full support for compressed image data.
|
6
|
-
# * Read/Write 12 bit image data.
|
7
|
-
# * Full color support (RGB and PALETTE COLOR with get_object_magick() already implemented).
|
8
|
-
# * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_narray().
|
9
|
-
# * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
|
10
|
-
# * More robust and flexible options for reorienting extracted pixel arrays?
|
11
|
-
# * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
|
12
|
-
# * Add these as github issues and remove this list!
|
13
|
-
|
14
|
-
|
15
|
-
# Copyright 2008-2011 Christoffer Lervag
|
16
|
-
#
|
17
|
-
# This program is free software: you can redistribute it and/or modify
|
18
|
-
# it under the terms of the GNU General Public License as published by
|
19
|
-
# the Free Software Foundation, either version 3 of the License, or
|
20
|
-
# (at your option) any later version.
|
21
|
-
#
|
22
|
-
# This program is distributed in the hope that it will be useful,
|
23
|
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
24
|
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
25
|
-
# GNU General Public License for more details.
|
26
|
-
#
|
27
|
-
# You should have received a copy of the GNU General Public License
|
28
|
-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
29
|
-
#
|
30
|
-
module DICOM
|
31
|
-
|
32
|
-
# The DObject class is the main class for interacting with the DICOM object.
|
33
|
-
# Reading from and writing to files is executed from instances of this class.
|
34
|
-
#
|
35
|
-
# === Inheritance
|
36
|
-
#
|
37
|
-
# As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
|
38
|
-
# all ImageItem and Parent methods are also available to instances of DObject.
|
39
|
-
#
|
40
|
-
class DObject < ImageItem
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
#
|
45
|
-
|
46
|
-
#
|
47
|
-
|
48
|
-
#
|
49
|
-
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
#
|
148
|
-
|
149
|
-
#
|
150
|
-
|
151
|
-
#
|
152
|
-
|
153
|
-
#
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
#
|
203
|
-
|
204
|
-
#
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
#
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
#
|
302
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
|
306
|
-
#
|
307
|
-
#
|
308
|
-
#
|
309
|
-
def
|
310
|
-
|
311
|
-
|
312
|
-
#
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
#
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
1
|
+
# === TODO:
|
2
|
+
#
|
3
|
+
# * The retrieve file network functionality (get_image() in DClient class) has not been tested.
|
4
|
+
# * Make the networking code more intelligent in its handling of unexpected network communication.
|
5
|
+
# * Full support for compressed image data.
|
6
|
+
# * Read/Write 12 bit image data.
|
7
|
+
# * Full color support (RGB and PALETTE COLOR with get_object_magick() already implemented).
|
8
|
+
# * Support for extraction of multiple encapsulated pixel data frames in get_image() and get_image_narray().
|
9
|
+
# * Image handling currently ignores DICOM tags like Pixel Aspect Ratio, Image Orientation and (to some degree) Photometric Interpretation.
|
10
|
+
# * More robust and flexible options for reorienting extracted pixel arrays?
|
11
|
+
# * A curious observation: Creating a DLibrary instance is exceptionally slow on Ruby 1.9.1: 0.4 seconds versus ~0.01 seconds on Ruby 1.8.7!
|
12
|
+
# * Add these as github issues and remove this list!
|
13
|
+
|
14
|
+
|
15
|
+
# Copyright 2008-2011 Christoffer Lervag
|
16
|
+
#
|
17
|
+
# This program is free software: you can redistribute it and/or modify
|
18
|
+
# it under the terms of the GNU General Public License as published by
|
19
|
+
# the Free Software Foundation, either version 3 of the License, or
|
20
|
+
# (at your option) any later version.
|
21
|
+
#
|
22
|
+
# This program is distributed in the hope that it will be useful,
|
23
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
24
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
25
|
+
# GNU General Public License for more details.
|
26
|
+
#
|
27
|
+
# You should have received a copy of the GNU General Public License
|
28
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
29
|
+
#
|
30
|
+
module DICOM
|
31
|
+
|
32
|
+
# The DObject class is the main class for interacting with the DICOM object.
|
33
|
+
# Reading from and writing to files is executed from instances of this class.
|
34
|
+
#
|
35
|
+
# === Inheritance
|
36
|
+
#
|
37
|
+
# As the DObject class inherits from the ImageItem class, which itself inherits from the Parent class,
|
38
|
+
# all ImageItem and Parent methods are also available to instances of DObject.
|
39
|
+
#
|
40
|
+
class DObject < ImageItem
|
41
|
+
include Logging
|
42
|
+
|
43
|
+
# Creates a DObject instance by downloading a DICOM file
|
44
|
+
# specified by a hyperlink, and parsing the retrieved file.
|
45
|
+
#
|
46
|
+
# === Restrictions
|
47
|
+
#
|
48
|
+
# * Highly experimental and un-tested!
|
49
|
+
# * Designed for HTTP protocol only.
|
50
|
+
# * Whether this method should be included or removed from ruby-dicom is up for debate.
|
51
|
+
#
|
52
|
+
# === Parameters
|
53
|
+
#
|
54
|
+
# * <tt>link</tt> -- A hyperlink string which specifies remote location of the DICOM file to be loaded.
|
55
|
+
#
|
56
|
+
def self.get(link)
|
57
|
+
raise ArgumentError, "Invalid argument 'link'. Expected String, got #{link.class}." unless link.is_a?(String)
|
58
|
+
raise ArgumentError, "Invalid argument 'link'. Expected a string starting with 'http', got #{link}." unless link.index('http') == 0
|
59
|
+
require 'open-uri'
|
60
|
+
bin = nil
|
61
|
+
file = nil
|
62
|
+
# Try to open the remote file using open-uri:
|
63
|
+
retrials = 0
|
64
|
+
begin
|
65
|
+
file = open(link, 'rb') # binary encoding (ASCII-8BIT)
|
66
|
+
rescue Exception => e
|
67
|
+
if retrials > 3
|
68
|
+
retrials = 0
|
69
|
+
raise "Unable to retrieve the file. File does not exist?"
|
70
|
+
else
|
71
|
+
logger.warn("Exception in ruby-dicom when loading a dicom file from: #{file}")
|
72
|
+
logger.debug("Retrying... #{retrials}")
|
73
|
+
retrials += 1
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
bin = File.open(file, "rb") { |f| f.read }
|
78
|
+
# Parse the file contents and create the DICOM object:
|
79
|
+
if bin
|
80
|
+
obj = self.parse(bin)
|
81
|
+
else
|
82
|
+
obj = self.new
|
83
|
+
obj.read_success = false
|
84
|
+
end
|
85
|
+
return obj
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates a DObject instance by parsing an encoded binary DICOM string.
|
89
|
+
#
|
90
|
+
# === Parameters
|
91
|
+
#
|
92
|
+
# * <tt>string</tt> -- An encoded binary string containing DICOM information.
|
93
|
+
# * <tt>options</tt> -- A hash of parameters.
|
94
|
+
#
|
95
|
+
# === Options
|
96
|
+
#
|
97
|
+
# * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
|
98
|
+
# * <tt>:syntax</tt> -- String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the binary string.
|
99
|
+
#
|
100
|
+
def self.parse(string, options={})
|
101
|
+
syntax = options[:syntax]
|
102
|
+
no_header = options[:no_meta]
|
103
|
+
raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
|
104
|
+
raise ArgumentError, "Invalid option :syntax. Expected String, got #{syntax.class}." if syntax && !syntax.is_a?(String)
|
105
|
+
obj = self.new
|
106
|
+
obj.read(string, :bin => true, :no_meta => no_header, :syntax => syntax)
|
107
|
+
return obj
|
108
|
+
end
|
109
|
+
|
110
|
+
# Creates a DObject instance by reading and parsing a DICOM file.
|
111
|
+
#
|
112
|
+
# === Parameters
|
113
|
+
#
|
114
|
+
# * <tt>file</tt> -- A string which specifies the path of the DICOM file to be loaded.
|
115
|
+
#
|
116
|
+
def self.read(file)
|
117
|
+
raise ArgumentError, "Invalid argument 'file'. Expected String, got #{file.class}." unless file.is_a?(String)
|
118
|
+
# Read the file content:
|
119
|
+
bin = nil
|
120
|
+
unless File.exist?(file)
|
121
|
+
logger.error("Invalid (non-existing) file: #{file}")
|
122
|
+
else
|
123
|
+
unless File.readable?(file)
|
124
|
+
logger.error("File exists but I don't have permission to read it: #{file}")
|
125
|
+
else
|
126
|
+
if File.directory?(file)
|
127
|
+
logger.error("Expected a file, got a directory: #{file}")
|
128
|
+
else
|
129
|
+
if File.size(file) < 8
|
130
|
+
logger.error("This file is too small to contain any DICOM information: #{file}.")
|
131
|
+
else
|
132
|
+
bin = File.open(file, "rb") { |f| f.read }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
# Parse the file contents and create the DICOM object:
|
138
|
+
if bin
|
139
|
+
obj = self.parse(bin)
|
140
|
+
else
|
141
|
+
obj = self.new
|
142
|
+
obj.read_success = false
|
143
|
+
end
|
144
|
+
return obj
|
145
|
+
end
|
146
|
+
|
147
|
+
# A boolean set as false. This attribute is included to provide consistency with other object types for the internal methods which use it.
|
148
|
+
attr_reader :parent
|
149
|
+
# A boolean which is set as true if a DICOM file has been successfully read & parsed from a file (or binary string).
|
150
|
+
attr_accessor :read_success
|
151
|
+
# The Stream instance associated with this DObject instance (this attribute is mostly used internally).
|
152
|
+
attr_reader :stream
|
153
|
+
# A boolean which is set as true if a DObject instance has been successfully written to file (or successfully encoded).
|
154
|
+
attr_reader :write_success
|
155
|
+
|
156
|
+
alias_method :read?, :read_success
|
157
|
+
alias_method :written?, :write_success
|
158
|
+
|
159
|
+
# Creates a DObject instance (DObject is an abbreviation for "DICOM object").
|
160
|
+
#
|
161
|
+
# === Notes
|
162
|
+
#
|
163
|
+
# The DObject instance holds references to the different types of objects (Element, Item, Sequence)
|
164
|
+
# that makes up a DICOM object. A DObject is typically buildt by reading and parsing a file or a
|
165
|
+
# binary string, but can also be buildt from an empty state by the user.
|
166
|
+
#
|
167
|
+
# To customize logging behaviour, refer to the Logging module documentation.
|
168
|
+
#
|
169
|
+
# === Parameters
|
170
|
+
#
|
171
|
+
# * <tt>string</tt> -- (Deprecated) A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed. The parameter defaults to nil, in which case an empty DObject instance is created.
|
172
|
+
# * <tt>options</tt> -- A hash of parameters.
|
173
|
+
#
|
174
|
+
# === Options
|
175
|
+
#
|
176
|
+
# * <tt>:bin</tt> -- (Deprecated) Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
|
177
|
+
# * <tt>:syntax</tt> -- (Deprecated) String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the file/binary string.
|
178
|
+
#
|
179
|
+
# === Examples
|
180
|
+
#
|
181
|
+
# # Load a DICOM file (Deprecated: please use DObject.read() instead):
|
182
|
+
# require 'dicom'
|
183
|
+
# obj = DICOM::DObject.new("test.dcm")
|
184
|
+
# # Read a DICOM file that has already been loaded into memory in a binary string (with a known transfer syntax):
|
185
|
+
# # (Deprecated: please use DObject.parse() instead)
|
186
|
+
# obj = DICOM::DObject.new(binary_string, :bin => true, :syntax => string_transfer_syntax)
|
187
|
+
# # Create an empty DICOM object
|
188
|
+
# obj = DICOM::DObject.new
|
189
|
+
# # Increasing the log message threshold (default level is INFO):
|
190
|
+
# DICOM.logger.level = Logger::WARN
|
191
|
+
#
|
192
|
+
def initialize(string=nil, options={})
|
193
|
+
# Deprecation warning:
|
194
|
+
logger.warn("Calling DOBject#new with a string argument is deprecated. Please use DObject#read (for reading files) or DObject#parse (for parsing strings) instead. Support for DObject#new with a string argument will be removed in a future version.") if string
|
195
|
+
# Removal warning:
|
196
|
+
logger.warn("The option :verbose no longer has any meaning. Please specify logger levels instead, e.g. DICOM.logger.level = Logger::WARN (refer to the documentation for more details).") if options[:verbose] == false
|
197
|
+
# Initialization of variables that DObject share with other parent elements:
|
198
|
+
initialize_parent
|
199
|
+
# Structural information (default values):
|
200
|
+
@explicit = true
|
201
|
+
@file_endian = false
|
202
|
+
# Control variables:
|
203
|
+
@read_success = nil
|
204
|
+
# Initialize a Stream instance which is used for encoding/decoding:
|
205
|
+
@stream = Stream.new(nil, @file_endian)
|
206
|
+
# The DObject instance is the top of the hierarchy and unlike other elements it has no parent:
|
207
|
+
@parent = nil
|
208
|
+
# For convenience, call the read method if a string has been supplied:
|
209
|
+
if string.is_a?(String)
|
210
|
+
@file = string unless options[:bin]
|
211
|
+
read(string, options)
|
212
|
+
elsif string
|
213
|
+
raise ArgumentError, "Invalid argument. Expected String (or nil), got #{string.class}."
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Encodes the DICOM object into a series of binary string segments with a specified maximum length.
|
218
|
+
#
|
219
|
+
# Returns the encoded binary strings in an array.
|
220
|
+
#
|
221
|
+
# === Parameters
|
222
|
+
#
|
223
|
+
# * <tt>max_size</tt> -- An integer (Fixnum) which specifies the maximum allowed size of the binary data strings which will be encoded.
|
224
|
+
# * <tt>transfer_syntax</tt> -- The transfer syntax string to be used when encoding the DICOM object to string segments. When this method is used for making network packets, the transfer_syntax is not part of the object, and thus needs to be specified. Defaults to the DObject's transfer syntax/Implicit little endian.
|
225
|
+
#
|
226
|
+
# === Examples
|
227
|
+
#
|
228
|
+
# encoded_strings = obj.encode_segments(16384)
|
229
|
+
#
|
230
|
+
def encode_segments(max_size, transfer_syntax=transfer_syntax)
|
231
|
+
raise ArgumentError, "Invalid argument. Expected an Integer, got #{max_size.class}." unless max_size.is_a?(Integer)
|
232
|
+
raise ArgumentError, "Argument too low (#{max_size}), please specify a bigger Integer." unless max_size > 16
|
233
|
+
raise "Can not encode binary segments for an empty DICOM object." if children.length == 0
|
234
|
+
w = DWrite.new(self, transfer_syntax, file_name=nil)
|
235
|
+
w.encode_segments(max_size)
|
236
|
+
# Write process succesful?
|
237
|
+
@write_success = w.success
|
238
|
+
return w.segments
|
239
|
+
end
|
240
|
+
|
241
|
+
# Prints information of interest related to the DICOM object.
|
242
|
+
# Calls the print() method of Parent as well as the information() method of DObject.
|
243
|
+
#
|
244
|
+
def print_all
|
245
|
+
puts ""
|
246
|
+
print(:value_max => 30)
|
247
|
+
summary
|
248
|
+
end
|
249
|
+
|
250
|
+
# Fills a DICOM object by reading and parsing the specified DICOM file,
|
251
|
+
# and transfers the DICOM data to the DICOM object (self).
|
252
|
+
#
|
253
|
+
# === Notes
|
254
|
+
#
|
255
|
+
# * This method is called automatically when initializing the DObject class with a file parameter.
|
256
|
+
# * In practice this method is rarely called by the user, and in fact it may be removed entirely at some stage.
|
257
|
+
#
|
258
|
+
# === Parameters
|
259
|
+
#
|
260
|
+
# * <tt>string</tt> -- A string which specifies either the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
|
261
|
+
# * <tt>options</tt> -- A hash of parameters.
|
262
|
+
#
|
263
|
+
# === Options
|
264
|
+
#
|
265
|
+
# * <tt>:bin</tt> -- Boolean. If true, the string parameter will be interpreted as a binary DICOM string instead of a path string.
|
266
|
+
# * <tt>:no_meta</tt> -- Boolean. If true, the parsing algorithm is instructed that the binary DICOM string contains no meta header.
|
267
|
+
# * <tt>:syntax</tt> -- String. If a syntax string is specified, the parsing algorithm will be forced to use this transfer syntax when decoding the file/binary string.
|
268
|
+
#
|
269
|
+
def read(string, options={})
|
270
|
+
raise ArgumentError, "Invalid argument 'string'. Expected String, got #{string.class}." unless string.is_a?(String)
|
271
|
+
# Clear any existing DObject tags, then read:
|
272
|
+
@tags = Hash.new
|
273
|
+
r = DRead.new(self, string, options)
|
274
|
+
# If reading failed, and no transfer syntax was detected, we will make another attempt at reading the file while forcing explicit (little endian) decoding.
|
275
|
+
# This will help for some rare cases where the DICOM file is saved (erroneously, Im sure) with explicit encoding without specifying the transfer syntax tag.
|
276
|
+
if !r.success and !exists?("0002,0010")
|
277
|
+
logger.debug("First attempt at parsing the file failed.\nAttempting a second pass (assuming Explicit Little Endian transfer syntax).")
|
278
|
+
# Clear the existing DObject tags:
|
279
|
+
@tags = Hash.new
|
280
|
+
r_explicit = DRead.new(self, string, :bin => options[:bin], :no_meta => options[:no_meta], :syntax => EXPLICIT_LITTLE_ENDIAN)
|
281
|
+
# Only extract information from this new attempt if it was successful:
|
282
|
+
r = r_explicit if r_explicit.success
|
283
|
+
end
|
284
|
+
# Pass along any messages that has been recorded:
|
285
|
+
r.msg.each { |m| logger.public_send(m.first, m.last) }
|
286
|
+
# Store the data to the instance variables if the readout was a success:
|
287
|
+
if r.success
|
288
|
+
logger.info("The DICOM file has been successfully parsed.")
|
289
|
+
@read_success = true
|
290
|
+
# Update instance variables based on the properties of the DICOM object:
|
291
|
+
@explicit = r.explicit
|
292
|
+
@file_endian = r.file_endian
|
293
|
+
@signature = r.signature
|
294
|
+
@stream.endian = @file_endian
|
295
|
+
else
|
296
|
+
logger.warn("Parsing the DICOM file has failed.")
|
297
|
+
@read_success = false
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Gathers key information about the DObject as well as some system data, and prints this information to the screen.
|
302
|
+
#
|
303
|
+
# This information includes properties like encoding, byte order, modality and various image properties.
|
304
|
+
#
|
305
|
+
#--
|
306
|
+
# FIXME: Perhaps this method should be split up in one or two separate methods
|
307
|
+
# which just builds the information arrays, and a third method for printing this to the screen.
|
308
|
+
#
|
309
|
+
def summary
|
310
|
+
sys_info = Array.new
|
311
|
+
info = Array.new
|
312
|
+
# Version of Ruby DICOM used:
|
313
|
+
sys_info << "Ruby DICOM version: #{VERSION}"
|
314
|
+
# System endian:
|
315
|
+
cpu = (CPU_ENDIAN ? "Big Endian" : "Little Endian")
|
316
|
+
sys_info << "Byte Order (CPU): #{cpu}"
|
317
|
+
# File path/name:
|
318
|
+
info << "File: #{@file}"
|
319
|
+
# Modality:
|
320
|
+
modality = (exists?("0008,0016") ? LIBRARY.get_syntax_description(self["0008,0016"].value) : "SOP Class unknown or not specified!")
|
321
|
+
info << "Modality: #{modality}"
|
322
|
+
# Meta header presence (Simply check for the presence of the transfer syntax data element), VR and byte order:
|
323
|
+
transfer_syntax = self["0002,0010"]
|
324
|
+
if transfer_syntax
|
325
|
+
syntax_validity, explicit, endian = LIBRARY.process_transfer_syntax(transfer_syntax.value)
|
326
|
+
if syntax_validity
|
327
|
+
meta_comment, explicit_comment, encoding_comment = "", "", ""
|
328
|
+
else
|
329
|
+
meta_comment = " (But unknown/invalid transfer syntax: #{transfer_syntax})"
|
330
|
+
explicit_comment = " (Assumed)"
|
331
|
+
encoding_comment = " (Assumed)"
|
332
|
+
end
|
333
|
+
explicitness = (explicit ? "Explicit" : "Implicit")
|
334
|
+
encoding = (endian ? "Big Endian" : "Little Endian")
|
335
|
+
meta = "Yes#{meta_comment}"
|
336
|
+
else
|
337
|
+
meta = "No"
|
338
|
+
explicitness = (@explicit == true ? "Explicit" : "Implicit")
|
339
|
+
encoding = (@file_endian == true ? "Big Endian" : "Little Endian")
|
340
|
+
explicit_comment = " (Assumed)"
|
341
|
+
encoding_comment = " (Assumed)"
|
342
|
+
end
|
343
|
+
info << "Meta Header: #{meta}"
|
344
|
+
info << "Value Representation: #{explicitness}#{explicit_comment}"
|
345
|
+
info << "Byte Order (File): #{encoding}#{encoding_comment}"
|
346
|
+
# Pixel data:
|
347
|
+
pixels = self[PIXEL_TAG]
|
348
|
+
unless pixels
|
349
|
+
info << "Pixel Data: No"
|
350
|
+
else
|
351
|
+
info << "Pixel Data: Yes"
|
352
|
+
# Image size:
|
353
|
+
cols = (exists?("0028,0011") ? self["0028,0011"].value : "Columns missing")
|
354
|
+
rows = (exists?("0028,0010") ? self["0028,0010"].value : "Rows missing")
|
355
|
+
info << "Image Size: #{cols}*#{rows}"
|
356
|
+
# Frames:
|
357
|
+
frames = value("0028,0008") || "1"
|
358
|
+
unless frames == "1" or frames == 1
|
359
|
+
# Encapsulated or 3D pixel data:
|
360
|
+
if pixels.is_a?(Element)
|
361
|
+
frames = frames.to_s + " (3D Pixel Data)"
|
362
|
+
else
|
363
|
+
frames = frames.to_s + " (Encapsulated Multiframe Image)"
|
364
|
+
end
|
365
|
+
end
|
366
|
+
info << "Number of frames: #{frames}"
|
367
|
+
# Color:
|
368
|
+
colors = (exists?("0028,0004") ? self["0028,0004"].value : "Not specified")
|
369
|
+
info << "Photometry: #{colors}"
|
370
|
+
# Compression:
|
371
|
+
if transfer_syntax
|
372
|
+
compression = LIBRARY.get_compression(transfer_syntax.value)
|
373
|
+
if compression
|
374
|
+
compression = LIBRARY.get_syntax_description(transfer_syntax.value) || "Unknown UID!"
|
375
|
+
else
|
376
|
+
compression = "No"
|
377
|
+
end
|
378
|
+
else
|
379
|
+
compression = "No (Assumed)"
|
380
|
+
end
|
381
|
+
info << "Compression: #{compression}"
|
382
|
+
# Pixel bits (allocated):
|
383
|
+
bits = (exists?("0028,0100") ? self["0028,0100"].value : "Not specified")
|
384
|
+
info << "Bits per Pixel: #{bits}"
|
385
|
+
end
|
386
|
+
# Print the DICOM object's key properties:
|
387
|
+
separator = "-------------------------------------------"
|
388
|
+
puts "System Properties:"
|
389
|
+
puts separator + "\n"
|
390
|
+
puts sys_info
|
391
|
+
puts "\n"
|
392
|
+
puts "DICOM Object Properties:"
|
393
|
+
puts separator
|
394
|
+
puts info
|
395
|
+
puts separator
|
396
|
+
return info
|
397
|
+
end
|
398
|
+
|
399
|
+
# Returns the transfer syntax string of the DObject.
|
400
|
+
#
|
401
|
+
# If a transfer syntax has not been defined in the DObject, a default tansfer syntax is assumed and returned.
|
402
|
+
#
|
403
|
+
def transfer_syntax
|
404
|
+
return value("0002,0010") || IMPLICIT_LITTLE_ENDIAN
|
405
|
+
end
|
406
|
+
|
407
|
+
# Changes the transfer syntax Element of the DObject instance, and performs re-encoding of all
|
408
|
+
# numerical values if a switch of endianness is implied.
|
409
|
+
#
|
410
|
+
# === Restrictions
|
411
|
+
#
|
412
|
+
# This method does not change the compressed state of the pixel data element. Changing the transfer syntax between
|
413
|
+
# an uncompressed and compressed state will NOT change the pixel data accordingly (this must be taken care of manually).
|
414
|
+
#
|
415
|
+
# === Parameters
|
416
|
+
#
|
417
|
+
# * <tt>new_syntax</tt> -- The new transfer syntax string which will be applied to the DObject.
|
418
|
+
#
|
419
|
+
def transfer_syntax=(new_syntax)
|
420
|
+
valid_ts, new_explicit, new_endian = LIBRARY.process_transfer_syntax(new_syntax)
|
421
|
+
raise ArgumentError, "Invalid transfer syntax specified: #{new_syntax}" unless valid_ts
|
422
|
+
# Get the old transfer syntax and write the new one to the DICOM object:
|
423
|
+
old_syntax = transfer_syntax
|
424
|
+
valid_ts, old_explicit, old_endian = LIBRARY.process_transfer_syntax(old_syntax)
|
425
|
+
if exists?("0002,0010")
|
426
|
+
self["0002,0010"].value = new_syntax
|
427
|
+
else
|
428
|
+
add(Element.new("0002,0010", new_syntax))
|
429
|
+
end
|
430
|
+
# Update our Stream instance with the new encoding:
|
431
|
+
@stream.endian = new_endian
|
432
|
+
# If endianness is changed, re-encode elements (only elements depending on endianness will actually be re-encoded):
|
433
|
+
encode_children(old_endian) if old_endian != new_endian
|
434
|
+
end
|
435
|
+
|
436
|
+
# Passes the DObject to the DWrite class, which traverses the data element
|
437
|
+
# structure and encodes a proper DICOM binary string, which is finally written to the specified file.
|
438
|
+
#
|
439
|
+
# === Parameters
|
440
|
+
#
|
441
|
+
# * <tt>file_name</tt> -- A string which identifies the path & name of the DICOM file which is to be written to disk.
|
442
|
+
# * <tt>options</tt> -- A hash of parameters.
|
443
|
+
#
|
444
|
+
# === Options
|
445
|
+
#
|
446
|
+
# * <tt>:add_meta</tt> -- Boolean. If set to false, no manipulation of the DICOM object's meta group will be performed before the DObject is written to file.
|
447
|
+
#
|
448
|
+
# === Examples
|
449
|
+
#
|
450
|
+
# obj.write(path + "test.dcm")
|
451
|
+
#
|
452
|
+
def write(file_name, options={})
|
453
|
+
raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}." unless file_name.is_a?(String)
|
454
|
+
insert_missing_meta unless options[:add_meta] == false
|
455
|
+
w = DWrite.new(self, transfer_syntax, file_name, options)
|
456
|
+
w.write
|
457
|
+
# Write process succesful?
|
458
|
+
@write_success = w.success
|
459
|
+
end
|
460
|
+
|
461
|
+
|
462
|
+
# Following methods are private:
|
463
|
+
private
|
464
|
+
|
465
|
+
|
466
|
+
# Adds any missing meta group (0002,xxxx) data elements to the DICOM object,
|
467
|
+
# to ensure that a valid DICOM object will be written to file.
|
468
|
+
#
|
469
|
+
def insert_missing_meta
|
470
|
+
# File Meta Information Version:
|
471
|
+
Element.new("0002,0001", [0,1], :parent => self) unless exists?("0002,0001")
|
472
|
+
# Media Storage SOP Class UID:
|
473
|
+
Element.new("0002,0002", value("0008,0016"), :parent => self) unless exists?("0002,0002")
|
474
|
+
# Media Storage SOP Instance UID:
|
475
|
+
Element.new("0002,0003", value("0008,0018"), :parent => self) unless exists?("0002,0003")
|
476
|
+
# Transfer Syntax UID:
|
477
|
+
Element.new("0002,0010", transfer_syntax, :parent => self) unless exists?("0002,0010")
|
478
|
+
if !exists?("0002,0012") and !exists?("0002,0013")
|
479
|
+
# Implementation Class UID:
|
480
|
+
Element.new("0002,0012", UID, :parent => self)
|
481
|
+
# Implementation Version Name:
|
482
|
+
Element.new("0002,0013", NAME, :parent => self)
|
483
|
+
end
|
484
|
+
# Source Application Entity Title:
|
485
|
+
Element.new("0002,0016", DICOM.source_app_title, :parent => self) unless exists?("0002,0016")
|
486
|
+
# Group Length: Remove the old one (if it exists) before creating a new one.
|
487
|
+
remove("0002,0000")
|
488
|
+
Element.new("0002,0000", meta_group_length, :parent => self)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Determines and returns the length of the meta group in the DObject instance.
|
492
|
+
#
|
493
|
+
def meta_group_length
|
494
|
+
group_length = 0
|
495
|
+
meta_elements = group(META_GROUP)
|
496
|
+
tag = 4
|
497
|
+
vr = 2
|
498
|
+
meta_elements.each do |element|
|
499
|
+
case element.vr
|
500
|
+
when "OB","OW","OF","SQ","UN","UT"
|
501
|
+
length = 6
|
502
|
+
else
|
503
|
+
length = 2
|
504
|
+
end
|
505
|
+
group_length += tag + vr + length + element.bin.length
|
506
|
+
end
|
507
|
+
return group_length
|
508
|
+
end
|
509
|
+
|
510
|
+
end
|
511
|
+
end
|