dicom 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/CHANGELOG +24 -0
  2. data/COPYING +674 -0
  3. data/DOCUMENTATION +66 -0
  4. data/README +57 -0
  5. data/lib/DObject.rb +439 -0
  6. data/lib/DRead.rb +1204 -0
  7. data/lib/dicom.rb +2 -0
  8. metadata +52 -0
@@ -0,0 +1,66 @@
1
+ DICOM is a small library for reading DICOM files. It is written completely in Ruby and has no external dependencies.
2
+
3
+ Copyright 2008 Christoffer Lervåg (chris.lervag@gmail.com)
4
+
5
+ INSTALLATION
6
+
7
+ gem install dicom
8
+
9
+ DOCUMENTATION
10
+
11
+ CLASS DObject
12
+
13
+ PUBLIC CLASS METHODS
14
+
15
+ new(filename)
16
+
17
+ Initialize a new DICOM object.
18
+ Example:
19
+ require 'dicom'
20
+ obj = DICOM::DObject.new("myFile.dcm")
21
+
22
+ PUBLIC INSTANCE METHODS
23
+
24
+ get_frames()
25
+ Returns the number of frames present in the image data in the DICOM file.
26
+
27
+ get_image_magick()
28
+ Returns an array of RMagick image objects, where the size of the array corresponds with the number of frames in the image data.
29
+ To call this method the user needs to have performed " require 'RMagick' " in advance.
30
+ Example (retrieve object and display first frame):
31
+ require 'RMagick'
32
+ data = dicom.get_image_magick()
33
+ data[0].display
34
+
35
+ get_image_narray()
36
+ Returns a 3d NArray object where the array dimensions are related to [frames, columns, rows].
37
+ To call this method the user needs to have performed " require 'narray' " in advance.
38
+ Example (retrieve object and display first frame):
39
+ require 'narray'
40
+ require 'nimage'
41
+ data = obj.get_image_narray()
42
+ NImage.show data[0,true,true]
43
+
44
+ get_image_pos()
45
+ Returns the index(es) of the tag(s) that contain image data.
46
+
47
+ get_pos(label)
48
+ Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag label.
49
+
50
+ get_raw(id)
51
+ Returns the raw data of the DICOM tag that matches the supplied tag ID.
52
+ The ID may be a tag index, tag name or tag label.
53
+
54
+ get_value(id)
55
+ Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
56
+ The ID may be a tag index, tag name or tag label.
57
+
58
+ print(id)
59
+ Prints the information of a specific tag (index, label, name, type, length, value).
60
+ The ID may be a tag name or tag label.
61
+
62
+ print_all()
63
+ Prints information of all tags stored in the DICOM object.
64
+
65
+ print_properties()
66
+ Prints the key structural properties of the DICOM file.
data/README ADDED
@@ -0,0 +1,57 @@
1
+ DICOM Project for Ruby
2
+ ======================
3
+
4
+ SUMMARY
5
+ --------
6
+
7
+ This is a fairly basic library for reading DICOM files in Ruby. Digital Imaging and Communications in Medicine (DICOM) is a standard for handling, storing, printing, and transmitting information in medical imaging. It includes a file format definition and a network communications protocol. The library currently only supports reading of the file format.
8
+
9
+ BASIC USAGE
10
+ -----------
11
+
12
+ require 'dicom'
13
+ # Read file:
14
+ dcm = DICOM::DObject.new("myFile.dcm")
15
+ # Check out the contents:
16
+ dcm.print_all()
17
+ # Retrieve a tag value:
18
+ name = dcm.get_value("0010.0010")
19
+ # Retrieve pixel data:
20
+ pixels = dcm.get_value("7FE0.0010")
21
+ # Load pixel data in a RMagick object and display on screen:
22
+ image = dcm.get_image_magick()
23
+ image[0].display
24
+ # Load pixel data in a NArray object and display on screen:
25
+ image = dcm.get_image_narray()
26
+ NImage.show image[0,true,true]
27
+
28
+ COPYRIGHT
29
+ ---------
30
+
31
+ Copyright 2008 Christoffer Lervåg
32
+
33
+ This program is free software: you can redistribute it and/or modify
34
+ it under the terms of the GNU General Public License as published by
35
+ the Free Software Foundation, either version 3 of the License, or
36
+ (at your option) any later version.
37
+
38
+ This program is distributed in the hope that it will be useful,
39
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
40
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41
+ GNU General Public License for more details.
42
+
43
+ You should have received a copy of the GNU General Public License
44
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
45
+
46
+
47
+ ABOUT THE AUTHOR
48
+ ----------------
49
+
50
+ Name:
51
+ Christoffer Lervåg
52
+
53
+ Location:
54
+ Oslo, Norway
55
+
56
+ Email:
57
+ chris.lervag@gmail.com
@@ -0,0 +1,439 @@
1
+ # Copyright 2008 Christoffer Lerv�g
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
+ # -Support for writing DICOM files.
20
+ # -Support for compressed image data.
21
+ # -Read 12 bit image data correctly.
22
+ # -Support for color image data in get_image_narray() and get_image_magick().
23
+ # -Proper support for Big endian.
24
+ # -Proper support for multiple frame image data.
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.
27
+
28
+ module DICOM
29
+
30
+ # Class for handling the DICOM contents:
31
+ class DObject
32
+
33
+ # Initialize the DObject instance.
34
+ def initialize(file_name)
35
+ # Initialize the key variables for the DICOM object:
36
+ @names = Array.new()
37
+ @labels = Array.new()
38
+ @types = Array.new()
39
+ @lengths = Array.new()
40
+ @values = Array.new()
41
+ @raw = Array.new()
42
+ # Index of last element in tag arrays:
43
+ @last_index=0
44
+ # Structural information (default values):
45
+ @compression = false
46
+ @color = false
47
+ @explicit = true
48
+ @file_endian = false
49
+ # If a file name string is supplied, launch the method to read DICOM file:
50
+ if file_name != nil and file_name != ""
51
+ read_file(file_name)
52
+ end
53
+ end
54
+
55
+
56
+ # Returns a DICOM object by reading the file specified.
57
+ # This is accomplished by initliazing the DRead class, which returns the DICOM object, if successful.
58
+ # 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.
60
+ def read_file(file_name)
61
+ if file_name == nil or file_name == ""
62
+ puts "Warning: A valid file name string was not supplied to method read_file(). Returning false."
63
+ return false
64
+ else
65
+ dcm = DRead.new(file_name)
66
+ 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
80
+ end
81
+ end
82
+
83
+
84
+ # Returns a 3d NArray object where the array dimensions are related to [frames, columns, rows].
85
+ # To call this method the user needs to have performed " require 'narray' " in advance.
86
+ def get_image_narray()
87
+ # Does pixel data exist at all in the DICOM object?
88
+ if @compression == nil
89
+ puts "It seems pixel data is not present in this DICOM object: returning false."
90
+ return false
91
+ end
92
+ # 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."
95
+ return false
96
+ end
97
+ # No support yet for retrieving color pixel data:
98
+ if @color
99
+ puts "Warning: Unpacking color pixel data is not supported yet for this method: returning false."
100
+ return false
101
+ end
102
+ # Gather information about the dimensions of the image data:
103
+ rows = get_value("0028.0010")
104
+ columns = get_value("0028.0011")
105
+ frames = get_frames()
106
+ image_pos = get_image_pos()
107
+ # Creating a NArray object using int to make sure we have a big enough range for our numbers:
108
+ image = NArray.int(frames,columns,rows)
109
+ image_temp = NArray.int(columns,rows)
110
+ # Handling of image data will depend on whether we have one or more frames,
111
+ # and if it is located in one or more tags:
112
+ if image_pos.size == 1
113
+ # All of the image data is located in one tag:
114
+ image_data = get_value(image_pos[0])
115
+ (0..frames-1).each do |i|
116
+ (0..columns*rows-1).each do |j|
117
+ image_temp[j] = image_data[j+i*columns*rows]
118
+ end
119
+ image[i,true,true] = image_temp
120
+ end
121
+ else
122
+ # Image data is encapsulated in items:
123
+ (0..frames-1).each do |i|
124
+ image_data=get_value(image_pos[i])
125
+ (0..columns*rows-1).each do |j|
126
+ image_temp[j] = image_data[j+i*columns*rows]
127
+ end
128
+ image[i,true,true] = image_temp
129
+ end
130
+ end
131
+ # Turn around the images to get the expected orientation when displaying on the screen:
132
+ (0..frames-1).each do |i|
133
+ temp_image=image[i,true,true]
134
+ #Transpose the images:
135
+ temp_image.transpose(1,0)
136
+ #Need to mirror the y-axis:
137
+ (0..temp_image.shape[0]-1).each do |j|
138
+ temp_image[j,0..temp_image.shape[1]-1] = temp_image[j,temp_image.shape[1]-1..0]
139
+ end
140
+ # Put the reoriented image back in the image matrixx:
141
+ image[i,true,true]=temp_image
142
+ end
143
+ return image
144
+ end
145
+
146
+
147
+ # Returns an array of RMagick image objects, where the size of the array corresponds with the number of frames in the image data.
148
+ # To call this method the user needs to have performed " require 'RMagick' " in advance.
149
+ def get_image_magick()
150
+ # Does pixel data exist at all in the DICOM object?
151
+ 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."
158
+ return false
159
+ end
160
+ # No support yet for color pixel data:
161
+ if @color
162
+ puts "Warning: Unpacking color pixel data is not supported yet for this method: aborting."
163
+ return false
164
+ end
165
+ # Gather information about the dimensions of the image data:
166
+ rows = get_value("0028.0010")
167
+ columns = get_value("0028.0011")
168
+ frames = get_frames()
169
+ image_pos = get_image_pos()
170
+ # Array that will hold the RMagick image objects, one image object for each frame:
171
+ image_arr = Array.new(frames)
172
+ # Handling of image data will depend on whether we have one or more frames,
173
+ if image_pos.size == 1
174
+ # 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
180
+ end
181
+ else
182
+ # Image data is encapsulated in items:
183
+ (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)
187
+ image_arr[i] = image
188
+ end
189
+ end
190
+ return image_arr
191
+ end
192
+
193
+
194
+ # Returns the number of frames present in the image data in the DICOM file.
195
+ def get_frames()
196
+ frames = get_value("0028.0008")
197
+ if frames == false
198
+ # If file does not specify number of tags, assume 1 image frame.
199
+ frames = 1
200
+ end
201
+ return frames.to_i
202
+ end
203
+
204
+
205
+ # Returns the index(es) of the tag(s) that contain image data.
206
+ def get_image_pos()
207
+ image_tag_pos = get_pos("7FE0.0010")
208
+ item_pos = get_pos("FFFE.E000")
209
+ # Proceed only if image tag actually exists:
210
+ if image_tag_pos == false
211
+ return false
212
+ else
213
+ # Check if we have item tags:
214
+ if item_pos == false
215
+ return image_tag_pos
216
+ else
217
+ # Extract item positions that occur after the image tag position:
218
+ late_item_pos = item_pos.select {|item| image_tag_pos[0] < item}
219
+ # Check if there are items appearing after the image tag.
220
+ if late_item_pos.size == 0
221
+ # None occured after the image tag position:
222
+ return image_tag_pos
223
+ else
224
+ # Determine which of these late item tags contain image data.
225
+ # Usually, there are frames+1 late items, and all except
226
+ # the first item contain an image frame:
227
+ frames = get_value("0028.0008")
228
+ if frames != false
229
+ if late_item_pos.size == frames.to_i+1
230
+ return late_item_pos[1..late_item_pos.size-1]
231
+ 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."
234
+ return false
235
+ end
236
+ else
237
+ puts "Warning: Number of frames tag not found. Method get_image_pos() will return false."
238
+ return false
239
+ end
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+
246
+ # Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag label.
247
+ def get_pos(label)
248
+ # There is probably a more elegant method to do this, but I havent found it at this time.
249
+ indexes = Array.new()
250
+ (0..@last_index).each do |i|
251
+ if @labels[i] == label
252
+ indexes += [i]
253
+ end
254
+ end
255
+ if indexes.size == 0
256
+ #puts "Notice: The requested position of label "+label+" was not identified."
257
+ return false
258
+ else
259
+ return indexes
260
+ end
261
+ end
262
+
263
+
264
+ # Prints information of all tags stored in the DICOM object.
265
+ # Calls private method print_index() to do the actual printing.
266
+ 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)
277
+ end
278
+ end
279
+
280
+
281
+ # Prints the information of a specific tag (index, label, name, type, length, value).
282
+ # The ID may be a tag name or tag label.
283
+ # Calls private method print_index() to do the actual printing.
284
+ def print(id)
285
+ if id == nil
286
+ puts "Please specify either a tag category name or a tag adress when using the print() function."
287
+ else
288
+ # First search the labels (Adresses):
289
+ match=@labels.index(id)
290
+ if match == nil
291
+ match=@names.index(id)
292
+ end
293
+ if match == nil
294
+ puts "Tag " + id + " not recognised in this DICOM file."
295
+ else
296
+ print_index(match)
297
+ end
298
+ end
299
+ end
300
+
301
+
302
+ # Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
303
+ # The ID may be a tag index, tag name or tag label.
304
+ def get_value(id)
305
+ if id == nil
306
+ puts "A tag label, category name or index number must be specified when calling the get_value() method!"
307
+ return false
308
+ else
309
+ # Assume we have been fed a tag label:
310
+ pos=@labels.index(id)
311
+ # If this does not give a hit, assume we have been fed a tag name:
312
+ if pos==nil
313
+ pos=@names.index(id)
314
+ end
315
+ # If we still dont have a hit, check if it is a valid number within the array range:
316
+ if pos == nil
317
+ if (id.is_a? Integer)
318
+ if id >= 0 and id <= @last_index
319
+ # The id supplied is a valid position, return its corresponding value:
320
+ return @values[id]
321
+ else
322
+ return false
323
+ end
324
+ else
325
+ return false
326
+ end
327
+ else
328
+ # We have a valid position, return the value:
329
+ return @values[pos]
330
+ end
331
+ end
332
+ end
333
+
334
+
335
+ # Returns the raw data of the DICOM tag that matches the supplied tag ID.
336
+ # The ID may be a tag index, tag name or tag label.
337
+ def get_raw(id)
338
+ if id == nil
339
+ puts "A tag label, category name or index number must be specified when calling the get_raw() method!"
340
+ return false
341
+ else
342
+ # Assume we have been fed a tag label:
343
+ pos=@labels.index(id)
344
+ # If this does not give a hit, assume we have been fed a tag name:
345
+ if pos==nil
346
+ pos=@names.index(id)
347
+ end
348
+ # If we still dont have a hit, check if it is a valid number within the array range:
349
+ if pos == nil
350
+ if (id.is_a? Integer)
351
+ if id >= 0 and id <= @last_index
352
+ # The id supplied is a valid position, return its corresponding value:
353
+ return @raw[id]
354
+ else
355
+ return false
356
+ end
357
+ else
358
+ return false
359
+ end
360
+ else
361
+ # We have a valid position, return the value:
362
+ return @raw[pos]
363
+ end
364
+ end
365
+ end
366
+
367
+
368
+ # Prints the key structural properties of the DICOM file.
369
+ def print_properties()
370
+ if @explicit
371
+ part1 = "Explicit VR, "
372
+ else
373
+ part1 = "Implicit VR, "
374
+ end
375
+ if @file_endian
376
+ part2 = "Big Endian, with "
377
+ else
378
+ part2 = "Little Endian, with "
379
+ end
380
+ if @compression == false
381
+ part3 = "Uncompressed, "
382
+ else
383
+ part3 = "Compressed, "
384
+ end
385
+ if @color
386
+ part4 = "Color pixel data."
387
+ else
388
+ part4 = "Greyscale pixel data."
389
+ end
390
+ if @compression == nil
391
+ part3 = "No pixel data."
392
+ part4 = ""
393
+ end
394
+ puts part1 + part2 + part3 + part4
395
+ end
396
+
397
+
398
+ # Following methods are private.
399
+ private
400
+
401
+
402
+ # Private method.
403
+ # Prints the information of a specific tag to the screen, identified by its index.
404
+ # Optional argument [maxL] is used to format the printing, making it look nicer on screen.
405
+ def print_index(pos,*maxL)
406
+ if pos < 0 or pos > @last_index
407
+ puts "The specified index "+pos.to_s+" is outside the bounds of the tags array."
408
+ else
409
+ if maxL[0] == nil
410
+ maxL=@names[pos].length
411
+ else
412
+ maxL=maxL[0]
413
+ end
414
+ s = " "
415
+ t = "\t"
416
+ if pos < 10 then p1 = " "+pos.to_s else p1 = pos.to_s end
417
+ p2 = @labels[pos]
418
+ p3 = @names[pos]
419
+ s3 = " "*(maxL-@names[pos].length+1)
420
+ p4 = @types[pos]
421
+ p5 = @lengths[pos].to_s
422
+ # We dont want to print tag contents if it is binary data:
423
+ if @types[pos] == "OB" or @types[pos] == "OW" or @types[pos] == "UN"
424
+ p6 = "(binary data)"
425
+ else
426
+ if @values[pos].to_s.length > 28
427
+ p6 = (@values[pos].to_s)[0..27]+" ..."
428
+ else
429
+ p6 = (@values[pos].to_s)
430
+ end
431
+ end
432
+ # Put the pieces together and print it:
433
+ puts p1 + s*2 + p2 + s + p3 + s3 + p4 + t + p5 + t + p6
434
+ end
435
+ end
436
+
437
+
438
+ end # End of class.
439
+ end # End of module.