dicom 0.1

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.
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.