dicom 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ = 0.2
2
+
3
+ === 10th August, 2008
4
+
5
+ * Data dictionary has been upgraded and is now compliant to the official standard.
6
+ * New DLibrary class which handles all interaction with the dictionary.
7
+ * Dictionary can be loaded before reading files, which will considerably speed up the process if reading multiple files.
8
+ * Reading compressed pixel data into an RMagick object is supported in principle, although it lacks proper testing at this point.
9
+ * DRead class is more resistant to breaking if it is handed a faulty file to read.
10
+ * Added option to load DICOM object verbose or silent.
11
+
1
12
  = 0.1
2
13
 
3
14
  === 20th July, 2008
@@ -13,11 +24,12 @@ Features:
13
24
  * Retrieve image data as RMagick object
14
25
  * Print file properties
15
26
  * Print tag information
27
+
16
28
 
17
29
  Known issues:
18
30
  * 12 bit image data not supported
19
31
  * Color images not supported in NArray and RMagick retrieve methods
20
- * Unpacking compressed image data not supported
32
+ * Unpacking compressed image data has basic support but is not properly tested yet.
21
33
  * Reading Big Endian files probably has some issues
22
34
  * Reading Little Endian files on a Big Endian system probably has issues as well
23
35
  * Reading of multiple frame image data is not particularly robust at this time
@@ -8,16 +8,30 @@ gem install dicom
8
8
 
9
9
  DOCUMENTATION
10
10
 
11
+ CLASS DLibrary
12
+
13
+ PUBLIC CLASS METHODS
14
+
15
+ new()
16
+
17
+ Initialize a new Library (dictionary) object.
18
+ Useful if you want to make a script that reads hundreds or thousands of DICOM files, because you can save time by loading the library one time at startup instead of having the library being loaded for each DICOM file being read.
19
+ Example:
20
+ lib = DLibrary.new()
21
+
22
+
11
23
  CLASS DObject
12
24
 
13
25
  PUBLIC CLASS METHODS
14
26
 
15
- new(filename)
27
+ new(filename, *options)
16
28
 
17
29
  Initialize a new DICOM object.
18
- Example:
30
+ Example 1 (The simple way):
19
31
  require 'dicom'
20
32
  obj = DICOM::DObject.new("myFile.dcm")
33
+ Example 2 (Using a pre-loaded library to speed up reading when reading multiple files):
34
+ obj = DICOM::DObject.new("myFile.dcm", verbose=false, library=lib)
21
35
 
22
36
  PUBLIC INSTANCE METHODS
23
37
 
data/README CHANGED
@@ -1,4 +1,4 @@
1
- DICOM Project for Ruby
1
+ RUBY DICOM
2
2
  ======================
3
3
 
4
4
  SUMMARY
@@ -44,14 +44,15 @@ You should have received a copy of the GNU General Public License
44
44
  along with this program. If not, see <http://www.gnu.org/licenses/>.
45
45
 
46
46
 
47
- ABOUT THE AUTHOR
48
- ----------------
47
+ ABOUT ME
48
+ --------
49
49
 
50
50
  Name:
51
- Christoffer Lervåg
51
+ Christoffer Lervåg
52
52
 
53
53
  Location:
54
54
  Oslo, Norway
55
55
 
56
56
  Email:
57
- chris.lervag@gmail.com
57
+ chris.lervag @nospam @gmail.com
58
+ Please don't hesitate to email me if have any thoughts on this project!
@@ -0,0 +1,105 @@
1
+ module DICOM
2
+ # Class which holds the methods that interact with the DICOM dictionary.
3
+ class DLibrary
4
+
5
+ attr_reader :de_label, :de_vr, :de_name, :uid_value, :uid_name, :uid_type, :pi_type, :pi_description
6
+
7
+ # Initialize the DRead instance.
8
+ def initialize()
9
+ # Instance arrays which hold library content:
10
+ # Data elements:
11
+ @de_label = Array.new()
12
+ @de_vr = Array.new()
13
+ @de_name = Array.new()
14
+ # UID:
15
+ @uid_value = Array.new()
16
+ @uid_name = Array.new()
17
+ @uid_type = Array.new()
18
+ # Photometric Interpretation:
19
+ @pi_type = Array.new()
20
+ @pi_description = Array.new()
21
+
22
+ # Load the dictionary:
23
+ dict = Dictionary.new()
24
+ # Data elements:
25
+ de = dict.load_tags()
26
+ @de_label = de[0]
27
+ @de_vr = de[1]
28
+ @de_name = de[2]
29
+ # Photometric Interpretation:
30
+ pi = dict.load_image_types()
31
+ @pi_type = pi[0]
32
+ @pi_description = pi[1]
33
+ # UID:
34
+ uid = dict.load_uid()
35
+ @uid_value = uid[0]
36
+ @uid_name = uid[1]
37
+ @uid_type = uid[2]
38
+ end
39
+
40
+
41
+ # Returns data element name and value representation from library if tag is recognised, else it returns "Unknown Name" and "UN".
42
+ def get_name_vr(label)
43
+ pos = get_pos(label)
44
+ if pos != nil
45
+ name = @de_name[pos]
46
+ vr = @de_vr[pos][0]
47
+ else
48
+ # For the labels that are not recognised, we need to do some additional testing to see if it is one of the special cases:
49
+ # Split label in group and element:
50
+ group = label[0..3]
51
+ element = label[5..8]
52
+ # Check for group length:
53
+ if element == "0000"
54
+ name = "Group Length"
55
+ vr = "UL"
56
+ end
57
+ # Source Image ID's: (Retired)
58
+ if label[0..6] == "0020,31"
59
+ pos = get_pos("0020,31xx")
60
+ name = @de_name[pos]
61
+ vr = @de_vr[pos][0]
62
+ end
63
+ # Group 50xx (retired) and 60xx:
64
+ if label[0..1] == "50" or label[0..1] == "60"
65
+ pos = get_pos(label[0..1]+"xx"+label[4..8])
66
+ if pos != nil
67
+ name = @de_name[pos]
68
+ vr = @de_vr[pos][0]
69
+ end
70
+ end
71
+ # If none of the above checks yielded a result, the label is unknown:
72
+ if name == nil
73
+ name = "Unknown Name"
74
+ vr = "UN"
75
+ end
76
+ end
77
+ return [name,vr]
78
+ end
79
+
80
+
81
+ # Checks whether a given string is a valid transfer syntax or not.
82
+ def check_transfer_syntax(label)
83
+ pos = @uid_value.index(label)
84
+ if pos >= 1 and pos <= 34
85
+ # Valid:
86
+ return true
87
+ else
88
+ # Invalid:
89
+ return false
90
+ end
91
+ end
92
+
93
+
94
+ # Following methods are private.
95
+ private
96
+
97
+ # Returns the position of the supplied data element name in the Dictionary array.
98
+ def get_pos(label)
99
+ pos = @de_label.index(label)
100
+ return pos
101
+ end
102
+
103
+
104
+ end # end of class
105
+ end # end of module
@@ -17,21 +17,23 @@
17
17
 
18
18
  # TODO:
19
19
  # -Support for writing DICOM files.
20
- # -Support for compressed image data.
20
+ # -Full support for compressed image data.
21
21
  # -Read 12 bit image data correctly.
22
22
  # -Support for color image data in get_image_narray() and get_image_magick().
23
23
  # -Proper support for Big endian.
24
24
  # -Proper support for multiple frame image data.
25
25
  # -Reading of image data in files that contain two different and unrelated images (observed in some MR images)
26
- # -A method to retrieve the index of tags contained within a sequence.
26
+ # -A method to retrieve the index of all tags contained within a specific sequence.
27
27
 
28
28
  module DICOM
29
29
 
30
30
  # Class for handling the DICOM contents:
31
31
  class DObject
32
32
 
33
+ attr_reader :read_success
34
+
33
35
  # Initialize the DObject instance.
34
- def initialize(file_name)
36
+ def initialize(file_name=nil, verbose=false, lib=nil)
35
37
  # Initialize the key variables for the DICOM object:
36
38
  @names = Array.new()
37
39
  @labels = Array.new()
@@ -39,6 +41,10 @@ module DICOM
39
41
  @lengths = Array.new()
40
42
  @values = Array.new()
41
43
  @raw = Array.new()
44
+ # Array that will holde any messages generated while reading the DICOM file:
45
+ @msg = Array.new()
46
+ # Array to keep track of sequences/structure of the dicom tags:
47
+ @sequence = Array.new()
42
48
  # Index of last element in tag arrays:
43
49
  @last_index=0
44
50
  # Structural information (default values):
@@ -46,7 +52,18 @@ module DICOM
46
52
  @color = false
47
53
  @explicit = true
48
54
  @file_endian = false
49
- # If a file name string is supplied, launch the method to read DICOM file:
55
+ # Handling variables:
56
+ @verbose = verbose
57
+ # Control variables:
58
+ @read_success = false
59
+ # Load the library class (DICOM dictionary):
60
+ if lib != nil
61
+ # Library already specified by user:
62
+ @lib = lib
63
+ else
64
+ @lib = DLibrary.new()
65
+ end
66
+ # If a (valid) file name string is supplied, launch the method to read DICOM file:
50
67
  if file_name != nil and file_name != ""
51
68
  read_file(file_name)
52
69
  end
@@ -56,27 +73,70 @@ module DICOM
56
73
  # Returns a DICOM object by reading the file specified.
57
74
  # This is accomplished by initliazing the DRead class, which returns the DICOM object, if successful.
58
75
  # For the time being, this method is called automatically when initializing the DObject class,
59
- # but in the future, when write support is added, this method will have to be activated or called manually.
76
+ # but in the future, when write support is added, this method may have to be called manually.
60
77
  def read_file(file_name)
61
78
  if file_name == nil or file_name == ""
62
- puts "Warning: A valid file name string was not supplied to method read_file(). Returning false."
79
+ add_msg("Warning: A valid file name string was not supplied to method read_file(). Returning false.")
63
80
  return false
64
81
  else
65
- dcm = DRead.new(file_name)
82
+ dcm = DRead.new(file_name, @lib)
66
83
  data = dcm.return_data()
67
- @names = data[0]
68
- @labels = data[1]
69
- @types = data[2]
70
- @lengths = data[3]
71
- @values = data[4]
72
- @raw = data[5]
73
- # Other information:
74
- @compression = data[6]
75
- @color = data[7]
76
- @explicit = data[8]
77
- @file_endian = data[9]
78
- # Index of last element in tag arrays:
79
- @last_index=@names.length-1
84
+ # Store the data to the instance variables if the readout was a success:
85
+ if dcm.success
86
+ @read_success = true
87
+ @names = data[0]
88
+ @labels = data[1]
89
+ @types = data[2]
90
+ @lengths = data[3]
91
+ @values = data[4]
92
+ @raw = data[5]
93
+ # Other information:
94
+ @compression = data[6]
95
+ @color = data[7]
96
+ @explicit = data[8]
97
+ @file_endian = data[9]
98
+ # Index of last element in tag arrays:
99
+ @last_index=@names.length-1
100
+ end
101
+ # The messages must be stored regardless of success or failure:
102
+ messages = data[10]
103
+ # If any messages has been recorded, send these to the message handling method:
104
+ if messages.size != 0
105
+ add_msg(messages)
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
112
+ def add_msg(msg)
113
+ if @verbose
114
+ puts msg
115
+ end
116
+ if (msg.is_a? String)
117
+ msg=[msg]
118
+ end
119
+ @msg += msg
120
+ end
121
+
122
+
123
+ # Returns image data from the provided tag index, performing decompression of data if necessary.
124
+ def read_image_magick(pos, columns, rows)
125
+ if @compression != true
126
+ # Non-compressed, just return the array contained in the particular tag:
127
+ image_data=get_value(pos)
128
+ image = Magick::Image.new(columns,rows)
129
+ image.import_pixels(0, 0, columns, rows, "I", image_data)
130
+ return image
131
+ else
132
+ # Image data is compressed, we will attempt to unpack it using RMagick (ImageMagick):
133
+ begin
134
+ image = Magick::Image.from_blob(@raw[pos])
135
+ return image
136
+ rescue
137
+ add_msg("RMagick did not succeed in decoding the compressed image data. Returning false.")
138
+ return false
139
+ end
80
140
  end
81
141
  end
82
142
 
@@ -86,22 +146,22 @@ module DICOM
86
146
  def get_image_narray()
87
147
  # Does pixel data exist at all in the DICOM object?
88
148
  if @compression == nil
89
- puts "It seems pixel data is not present in this DICOM object: returning false."
149
+ add_msg("It seems pixel data is not present in this DICOM object: returning false.")
90
150
  return false
91
151
  end
92
152
  # No support yet for retrieving compressed data:
93
- if @compression != false and @compression != nil
94
- puts "Warning: Unpacking compressed pixel data is not supported yet for this method: returning false."
153
+ if @compression == true
154
+ add_msg("Reading compressed data to a NArray object not supported yet: returning false.")
95
155
  return false
96
156
  end
97
157
  # No support yet for retrieving color pixel data:
98
158
  if @color
99
- puts "Warning: Unpacking color pixel data is not supported yet for this method: returning false."
159
+ add_msg("Warning: Unpacking color pixel data is not supported yet for this method: returning false.")
100
160
  return false
101
161
  end
102
162
  # Gather information about the dimensions of the image data:
103
- rows = get_value("0028.0010")
104
- columns = get_value("0028.0011")
163
+ rows = get_value("0028,0010")
164
+ columns = get_value("0028,0011")
105
165
  frames = get_frames()
106
166
  image_pos = get_image_pos()
107
167
  # Creating a NArray object using int to make sure we have a big enough range for our numbers:
@@ -112,6 +172,7 @@ module DICOM
112
172
  if image_pos.size == 1
113
173
  # All of the image data is located in one tag:
114
174
  image_data = get_value(image_pos[0])
175
+ #image_data = get_image_data(image_pos[0])
115
176
  (0..frames-1).each do |i|
116
177
  (0..columns*rows-1).each do |j|
117
178
  image_temp[j] = image_data[j+i*columns*rows]
@@ -122,6 +183,7 @@ module DICOM
122
183
  # Image data is encapsulated in items:
123
184
  (0..frames-1).each do |i|
124
185
  image_data=get_value(image_pos[i])
186
+ #image_data = get_image_data(image_pos[i])
125
187
  (0..columns*rows-1).each do |j|
126
188
  image_temp[j] = image_data[j+i*columns*rows]
127
189
  end
@@ -149,22 +211,17 @@ module DICOM
149
211
  def get_image_magick()
150
212
  # Does pixel data exist at all in the DICOM object?
151
213
  if @compression == nil
152
- puts "It seems pixel data is not present in this DICOM object: returning false."
153
- return false
154
- end
155
- # No support yet for retrieving compressed data:
156
- if @compression != false and @compression != nil
157
- puts "Warning: Unpacking compressed pixel data is not supported yet for this method: aborting."
214
+ add_msg("It seems pixel data is not present in this DICOM object: returning false.")
158
215
  return false
159
216
  end
160
217
  # No support yet for color pixel data:
161
218
  if @color
162
- puts "Warning: Unpacking color pixel data is not supported yet for this method: aborting."
219
+ add_msg("Warning: Unpacking color pixel data is not supported yet for this method: aborting.")
163
220
  return false
164
221
  end
165
222
  # Gather information about the dimensions of the image data:
166
- rows = get_value("0028.0010")
167
- columns = get_value("0028.0011")
223
+ rows = get_value("0028,0010")
224
+ columns = get_value("0028,0011")
168
225
  frames = get_frames()
169
226
  image_pos = get_image_pos()
170
227
  # Array that will hold the RMagick image objects, one image object for each frame:
@@ -172,18 +229,25 @@ module DICOM
172
229
  # Handling of image data will depend on whether we have one or more frames,
173
230
  if image_pos.size == 1
174
231
  # All of the image data is located in one tag:
175
- image_data = get_value(image_pos[0])
176
- (0..frames-1).each do |i|
177
- image = Magick::Image.new(columns,rows)
178
- image.import_pixels(0, 0, columns, rows, "I", image_data)
179
- image_arr[i] = image
232
+ #image_data = get_image_data(image_pos[0])
233
+ #(0..frames-1).each do |i|
234
+ # image = Magick::Image.new(columns,rows)
235
+ # image.import_pixels(0, 0, columns, rows, "I", image_data)
236
+ # image_arr[i] = image
237
+ #end
238
+ if frames > 1
239
+ add_msg("Unfortunately, this method only supports reading the first image frame as of now.")
180
240
  end
241
+ image = read_image_magick(image_pos[0], columns, rows)
242
+ image_arr[0] = image
243
+ #image_arr[i] = image
181
244
  else
182
245
  # Image data is encapsulated in items:
183
246
  (0..frames-1).each do |i|
184
- image_data=get_value(image_pos[i])
185
- image = Magick::Image.new(columns,rows)
186
- image.import_pixels(0, 0, columns, rows, "I", image_data)
247
+ #image_data=get_image_data(image_pos[i])
248
+ #image = Magick::Image.new(columns,rows)
249
+ #image.import_pixels(0, 0, columns, rows, "I", image_data)
250
+ image = read_image_magick(image_pos[i], columns, rows)
187
251
  image_arr[i] = image
188
252
  end
189
253
  end
@@ -193,7 +257,7 @@ module DICOM
193
257
 
194
258
  # Returns the number of frames present in the image data in the DICOM file.
195
259
  def get_frames()
196
- frames = get_value("0028.0008")
260
+ frames = get_value("0028,0008")
197
261
  if frames == false
198
262
  # If file does not specify number of tags, assume 1 image frame.
199
263
  frames = 1
@@ -204,8 +268,8 @@ module DICOM
204
268
 
205
269
  # Returns the index(es) of the tag(s) that contain image data.
206
270
  def get_image_pos()
207
- image_tag_pos = get_pos("7FE0.0010")
208
- item_pos = get_pos("FFFE.E000")
271
+ image_tag_pos = get_pos("7FE0,0010")
272
+ item_pos = get_pos("FFFE,E000")
209
273
  # Proceed only if image tag actually exists:
210
274
  if image_tag_pos == false
211
275
  return false
@@ -224,17 +288,16 @@ module DICOM
224
288
  # Determine which of these late item tags contain image data.
225
289
  # Usually, there are frames+1 late items, and all except
226
290
  # the first item contain an image frame:
227
- frames = get_value("0028.0008")
228
- if frames != false
291
+ frames = get_frames()
292
+ if frames != false # note: function get_frames will never return false
229
293
  if late_item_pos.size == frames.to_i+1
230
294
  return late_item_pos[1..late_item_pos.size-1]
231
295
  else
232
- puts "Warning: Unexpected behaviour in DICOM file for method get_image_pos()."
233
- puts "Expected number of image data items not equal to number of frames+1, returning false."
296
+ 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.")
234
297
  return false
235
298
  end
236
299
  else
237
- puts "Warning: Number of frames tag not found. Method get_image_pos() will return false."
300
+ add_msg("Warning: Number of frames tag not found. Method get_image_pos() will return false.")
238
301
  return false
239
302
  end
240
303
  end
@@ -264,16 +327,19 @@ module DICOM
264
327
  # Prints information of all tags stored in the DICOM object.
265
328
  # Calls private method print_index() to do the actual printing.
266
329
  def print_all()
267
- # Extract information on the largest string in the array:
268
- str_lengths=Array.new(@last_index+1,0)
269
- (0..@last_index).each do |i|
270
- str_lengths[i]=@names[i].length
271
- end
272
- maxL=str_lengths.max
273
- # Print to screen in a neat way:
274
- puts " "
275
- (0..@last_index).each do |i|
276
- print_index(i,maxL)
330
+ # Only proceed if some tags have been loaded:
331
+ if @names.length > 0
332
+ # Extract information on the largest string in the array:
333
+ str_lengths=Array.new(@last_index+1,0)
334
+ (0..@last_index).each do |i|
335
+ str_lengths[i]=@names[i].length
336
+ end
337
+ maxL=str_lengths.max
338
+ # Print to screen in a neat way:
339
+ puts " "
340
+ (0..@last_index).each do |i|
341
+ print_index(i,maxL)
342
+ end
277
343
  end
278
344
  end
279
345
 
@@ -283,7 +349,7 @@ module DICOM
283
349
  # Calls private method print_index() to do the actual printing.
284
350
  def print(id)
285
351
  if id == nil
286
- puts "Please specify either a tag category name or a tag adress when using the print() function."
352
+ add_msg("Please specify either a tag category name or a tag adress when using the print() function.")
287
353
  else
288
354
  # First search the labels (Adresses):
289
355
  match=@labels.index(id)
@@ -291,7 +357,7 @@ module DICOM
291
357
  match=@names.index(id)
292
358
  end
293
359
  if match == nil
294
- puts "Tag " + id + " not recognised in this DICOM file."
360
+ add_msg("Tag " + id + " not recognised in this DICOM file.")
295
361
  else
296
362
  print_index(match)
297
363
  end
@@ -303,7 +369,7 @@ module DICOM
303
369
  # The ID may be a tag index, tag name or tag label.
304
370
  def get_value(id)
305
371
  if id == nil
306
- puts "A tag label, category name or index number must be specified when calling the get_value() method!"
372
+ add_msg("A tag label, category name or index number must be specified when calling the get_value() method!")
307
373
  return false
308
374
  else
309
375
  # Assume we have been fed a tag label:
@@ -336,7 +402,7 @@ module DICOM
336
402
  # The ID may be a tag index, tag name or tag label.
337
403
  def get_raw(id)
338
404
  if id == nil
339
- puts "A tag label, category name or index number must be specified when calling the get_raw() method!"
405
+ add_msg("A tag label, category name or index number must be specified when calling the get_raw() method!")
340
406
  return false
341
407
  else
342
408
  # Assume we have been fed a tag label:
@@ -404,7 +470,7 @@ module DICOM
404
470
  # Optional argument [maxL] is used to format the printing, making it look nicer on screen.
405
471
  def print_index(pos,*maxL)
406
472
  if pos < 0 or pos > @last_index
407
- puts "The specified index "+pos.to_s+" is outside the bounds of the tags array."
473
+ add_msg("The specified index "+pos.to_s+" is outside the bounds of the tags array.")
408
474
  else
409
475
  if maxL[0] == nil
410
476
  maxL=@names[pos].length