dicom 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ = 0.3
2
+
3
+ === 12th October, 2008
4
+
5
+ * The DRead class is now able to keep track of the position of the tags inside the hierarchy of sequences and items.
6
+ * DObject class has seen a number of improvements to allow taking advantage of this hierarchy awareness:
7
+ * Method get_pos() now can take an array of positions as an argument when searching for tags,
8
+ meaning it will only search for hits amongst the positions provided.
9
+ * Method print() has been improved with new options to visualize the tree structure of the DICOM file.
10
+ * Method print() is able to print to file as well. The text file will be put in the same folder as the DICOM file.
11
+ * New method below(), which lets you specify a sequence or item, and this method will return the position
12
+ of all tags contained in this sequence/item.
13
+
1
14
  = 0.2
2
15
 
3
16
  === 10th August, 2008
@@ -6,8 +19,8 @@
6
19
  * New DLibrary class which handles all interaction with the dictionary.
7
20
  * Dictionary can be loaded before reading files, which will considerably speed up the process if reading multiple files.
8
21
  * 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.
22
+ * DRead class is more resistant to breaking if it is handed a faulty file to read, and will return an error message instead of halting execution.
23
+ * Added option to load DICOM object in verbose or silent mode.
11
24
 
12
25
  = 0.1
13
26
 
data/DOCUMENTATION CHANGED
@@ -15,7 +15,9 @@ PUBLIC CLASS METHODS
15
15
  new()
16
16
 
17
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.
18
+ Useful if you want to make a script that reads hundreds or thousands of DICOM files,
19
+ because you can save time by loading the library one time at startup instead of
20
+ having the library being loaded for each DICOM file being read.
19
21
  Example:
20
22
  lib = DLibrary.new()
21
23
 
@@ -35,11 +37,21 @@ PUBLIC CLASS METHODS
35
37
 
36
38
  PUBLIC INSTANCE METHODS
37
39
 
40
+ below(id, *options)
41
+ Returns the positions of all tags inside the hierarchy of a sequence or an item.
42
+ This is useful if you later want to get the position(s) of a certain tag,
43
+ restricted to the positions inside the given sequence or item.
44
+ Example 1: (Return all tag positions that is contained in the following sequence)
45
+ pos = obj.below("3006,0082")
46
+ Example 2: (Return all tag positions that is contained only directly beneath the following sequence)
47
+ pos = obj.below("3006,0082", next_only=true)
48
+
38
49
  get_frames()
39
50
  Returns the number of frames present in the image data in the DICOM file.
40
51
 
41
52
  get_image_magick()
42
- Returns an array of RMagick image objects, where the size of the array corresponds with the number of frames in the image data.
53
+ Returns an array of RMagick image objects, where the size of the array corresponds
54
+ with the number of frames in the image data.
43
55
  To call this method the user needs to have performed " require 'RMagick' " in advance.
44
56
  Example (retrieve object and display first frame):
45
57
  require 'RMagick'
@@ -58,8 +70,12 @@ PUBLIC INSTANCE METHODS
58
70
  get_image_pos()
59
71
  Returns the index(es) of the tag(s) that contain image data.
60
72
 
61
- get_pos(label)
62
- Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag label.
73
+ get_pos(id, *options)
74
+ Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag ID.
75
+ Example 1: (Find all occurences of the specified tag in the object)
76
+ pos = obj.get_pos("3006,0080")
77
+ Example 2: (Find all occurences of the specified tag inside the specified sequence)
78
+ pos = obj.get_pos("3006,0080", obj.below("3006,0082"))
63
79
 
64
80
  get_raw(id)
65
81
  Returns the raw data of the DICOM tag that matches the supplied tag ID.
@@ -69,12 +85,19 @@ PUBLIC INSTANCE METHODS
69
85
  Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
70
86
  The ID may be a tag index, tag name or tag label.
71
87
 
72
- print(id)
73
- Prints the information of a specific tag (index, label, name, type, length, value).
74
- The ID may be a tag name or tag label.
88
+ print(id, *options)
89
+ Prints the information of one or many tag(s):
90
+ (index, [hierarchy level,] label, name, type, length, value)
91
+ The method can print to both screen or to a text file. If print to file is chosen,
92
+ the text file will be put in the folder of the original DICOM file with a '.txt' extension.
93
+ The ID may be a tag name, label or position, or it might be an array of positions.
94
+ Example 1: (Print all tags to file, with both tree visualization and level numbers)
95
+ obj.print(true, levels=true, tree=true, file=true)
96
+ Example 2: (Print an array of tags to screen, no level or tree visualization)
97
+ obj.print([4,5,6])
75
98
 
76
99
  print_all()
77
- Prints information of all tags stored in the DICOM object.
100
+ Prints information of all tags stored in the DICOM object to the screen.
78
101
 
79
102
  print_properties()
80
- Prints the key structural properties of the DICOM file.
103
+ Prints the key structural properties of the DICOM file to the screen.
data/README CHANGED
@@ -11,8 +11,10 @@ BASIC USAGE
11
11
 
12
12
  require 'dicom'
13
13
  # Read file:
14
- dcm = DICOM::DObject.new("myFile.dcm")
15
- # Check out the contents:
14
+ dcm = DICOM::DObject.new("myFile.dcm")
15
+ # Display some key information about the file:
16
+ dcm.print_properties()
17
+ # Print all tags to screen:
16
18
  dcm.print_all()
17
19
  # Retrieve a tag value:
18
20
  name = dcm.get_value("0010.0010")
@@ -56,3 +58,4 @@ Oslo, Norway
56
58
  Email:
57
59
  chris.lervag @nospam @gmail.com
58
60
  Please don't hesitate to email me if have any thoughts on this project!
61
+
data/lib/DLibrary.rb CHANGED
@@ -21,6 +21,7 @@ module DICOM
21
21
 
22
22
  # Load the dictionary:
23
23
  dict = Dictionary.new()
24
+
24
25
  # Data elements:
25
26
  de = dict.load_tags()
26
27
  @de_label = de[0]
@@ -91,6 +92,20 @@ module DICOM
91
92
  end
92
93
 
93
94
 
95
+ # Returns the name corresponding to a given UID.
96
+ def get_uid(label)
97
+ # Find the position of the specified label in the array:
98
+ pos = @uid_value.index(label)
99
+ # Fetch the name of this UID:
100
+ if pos != nil
101
+ name = @uid_name[pos]
102
+ else
103
+ name = "Unknown UID!"
104
+ end
105
+ return name
106
+ end
107
+
108
+
94
109
  # Following methods are private.
95
110
  private
96
111
 
data/lib/DObject.rb CHANGED
@@ -30,7 +30,7 @@ module DICOM
30
30
  # Class for handling the DICOM contents:
31
31
  class DObject
32
32
 
33
- attr_reader :read_success
33
+ attr_reader :read_success, :modality
34
34
 
35
35
  # Initialize the DObject instance.
36
36
  def initialize(file_name=nil, verbose=false, lib=nil)
@@ -41,6 +41,7 @@ module DICOM
41
41
  @lengths = Array.new()
42
42
  @values = Array.new()
43
43
  @raw = Array.new()
44
+ @levels = Array.new()
44
45
  # Array that will holde any messages generated while reading the DICOM file:
45
46
  @msg = Array.new()
46
47
  # Array to keep track of sequences/structure of the dicom tags:
@@ -52,6 +53,8 @@ module DICOM
52
53
  @color = false
53
54
  @explicit = true
54
55
  @file_endian = false
56
+ # Information about the DICOM object:
57
+ @modality = nil
55
58
  # Handling variables:
56
59
  @verbose = verbose
57
60
  # Control variables:
@@ -65,6 +68,7 @@ module DICOM
65
68
  end
66
69
  # If a (valid) file name string is supplied, launch the method to read DICOM file:
67
70
  if file_name != nil and file_name != ""
71
+ @file = file_name
68
72
  read_file(file_name)
69
73
  end
70
74
  end
@@ -90,16 +94,19 @@ module DICOM
90
94
  @lengths = data[3]
91
95
  @values = data[4]
92
96
  @raw = data[5]
97
+ @levels = data[6]
93
98
  # Other information:
94
- @compression = data[6]
95
- @color = data[7]
96
- @explicit = data[8]
97
- @file_endian = data[9]
99
+ @compression = data[7]
100
+ @color = data[8]
101
+ @explicit = data[9]
102
+ @file_endian = data[10]
98
103
  # Index of last element in tag arrays:
99
104
  @last_index=@names.length-1
105
+ # Set the modality of the DICOM object:
106
+ set_modality()
100
107
  end
101
108
  # The messages must be stored regardless of success or failure:
102
- messages = data[10]
109
+ messages = data[11]
103
110
  # If any messages has been recorded, send these to the message handling method:
104
111
  if messages.size != 0
105
112
  add_msg(messages)
@@ -306,15 +313,37 @@ module DICOM
306
313
  end
307
314
 
308
315
 
309
- # Returns the index(es) of the tag(s) in the DICOM file that match the supplied tag label.
310
- def get_pos(label)
311
- # There is probably a more elegant method to do this, but I havent found it at this time.
316
+ # Returns an array of the index(es) of the tag(s) in the DICOM file that match the supplied tag label, name or position.
317
+ # If no match is found, the method will return false.
318
+ # Additional options:
319
+ # If an array of positions is specified as options[0], the method will search for hits in this
320
+ # array of positions instead of searching for hits in the entire object.
321
+ # If options[0]=false, then this method should also return false.
322
+ def get_pos(label, *options)
323
+ if options[0] == false
324
+ return false
325
+ end
326
+ # There may be a more elegant/efficient method to do this, but I leave that for later optimization.
327
+ # Array that will contain the positions where the supplied label gives a match:
312
328
  indexes = Array.new()
313
- (0..@last_index).each do |i|
329
+ # Either use the supplied array, or we will create an array that contain the indices of the entire DICOM object:
330
+ if options[0].is_a?(Array)
331
+ search_array=options[0]
332
+ else
333
+ search_array = Array.new(@names.size) {|i| i}
334
+ end
335
+ # Do the search:
336
+ #(0..@last_index).each do |i|
337
+ search_array.each do |i|
314
338
  if @labels[i] == label
315
339
  indexes += [i]
340
+ elsif @names[i] == label
341
+ indexes += [i]
342
+ elsif label == i
343
+ indexes += [i]
316
344
  end
317
345
  end
346
+ # Return false if no hits are found, else return the array of indices:
318
347
  if indexes.size == 0
319
348
  #puts "Notice: The requested position of label "+label+" was not identified."
320
349
  return false
@@ -324,43 +353,171 @@ module DICOM
324
353
  end
325
354
 
326
355
 
327
- # Prints information of all tags stored in the DICOM object.
328
- # Calls private method print_index() to do the actual printing.
329
- def print_all()
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)
356
+ # Returns the positions of all tags inside the hierarchy of a sequence or an item.
357
+ # Options:
358
+ # If options[0] = true, then this method will only search immediately below the specified
359
+ # item or sequence (that is, in the level of parent 1).
360
+ def below(tag, *options)
361
+ restriction = options[0]
362
+ # Retrieve position of parent tag which from which we will search:
363
+ pos = get_pos(tag)
364
+ if pos == false
365
+ return false
366
+ end
367
+ if pos.size > 1
368
+ add_msg("Warning: The supplied parent tag gives multiple hits. Search will be applied to all hits. To avoid this behaviour, specify position instead of label.")
369
+ end
370
+ # First we need to establish in which positions to perform the search:
371
+ below_pos = Array.new()
372
+ pos.each do |p|
373
+ parent_level = @levels[p]
374
+ remain_array = @levels[p+1..@levels.size-1]
375
+ extract = true
376
+ remain_array.each_index do |i|
377
+ if (remain_array[i] > parent_level) and (extract == true)
378
+ # If search is targetted at any specific level, we can just add this position:
379
+ if not restriction == true
380
+ below_pos += [p+1+i]
381
+ else
382
+ # As search is restricted to parent level + 1, do a test for this:
383
+ if remain_array[i] == parent_level + 1
384
+ below_pos += [p+1+i]
385
+ end
386
+ end
387
+ else
388
+ # If we encounter a position who's level is not deeper than the original level, we can not extract any more values:
389
+ extract = false
390
+ end
342
391
  end
343
392
  end
393
+ # Positions to search in have been established, now we can perform the actual search:
394
+ if below_pos.size == 0
395
+ return false
396
+ else
397
+ return below_pos
398
+ end
344
399
  end
345
400
 
346
401
 
347
- # Prints the information of a specific tag (index, label, name, type, length, value).
348
- # The ID may be a tag name or tag label.
349
- # Calls private method print_index() to do the actual printing.
350
- def print(id)
351
- if id == nil
352
- add_msg("Please specify either a tag category name or a tag adress when using the print() function.")
402
+ # Prints information of all tags stored in the DICOM object.
403
+ # This method is kept for backwards compatibility.
404
+ # Instead of calling print_all() you may use print(true) for the same functionality.
405
+ def print_all()
406
+ print(true)
407
+ end
408
+
409
+
410
+ # Prints the tag information of the specified tags (index, [hierarchy level, tree visualisation,] label, name, type, length, value)
411
+ # The supplied variable may be a single position, an array of positions, or true - which will make the method print all tags in object.
412
+ # Tag(s) may be specified by position, label or name.
413
+ # Options:
414
+ # options[0] = true will make the method print the level numbers for each tag.
415
+ # options[1] = true will make the method print a tree structure for the tags.
416
+ # options[2] = true will make the method print to file instead of printing to screen.
417
+ def print(pos, *options)
418
+ # Convert to array if number:
419
+ if not pos.is_a?(Array) and pos != true
420
+ pos_valid = get_pos(pos)
421
+ elsif pos == true
422
+ # Create an array of positions which a
423
+ pos_valid = Array.new(@names.length)
424
+ # Fill in indices:
425
+ pos_valid.each_index do |i|
426
+ pos_valid[i]=i
427
+ end
353
428
  else
354
- # First search the labels (Adresses):
355
- match=@labels.index(id)
356
- if match == nil
357
- match=@names.index(id)
429
+ # Check that the supplied array contains valid positions:
430
+ pos_valid = Array.new()
431
+ pos.each_index do |i|
432
+ if pos[i] >= 0 and pos[i] <= @names.length
433
+ pos_valid += [pos[i]]
434
+ end
358
435
  end
359
- if match == nil
360
- add_msg("Tag " + id + " not recognised in this DICOM file.")
436
+ end
437
+ # Continue only if we have valid positions:
438
+ if pos_valid == false
439
+ return
440
+ elsif pos_valid.size == 0
441
+ return
442
+ end
443
+ # We have valid positions and are ready to start process the tags:
444
+ # Extract the information to be printed from the object arrays:
445
+ indices = Array.new()
446
+ levels = Array.new()
447
+ labels = Array.new()
448
+ names = Array.new()
449
+ types = Array.new()
450
+ lengths = Array.new()
451
+ values = Array.new()
452
+ # There may be a more elegant way to do this.
453
+ pos_valid.each_index do |i|
454
+ labels += [@labels[pos_valid[i]]]
455
+ levels += [@levels[pos_valid[i]]]
456
+ names += [@names[pos_valid[i]]]
457
+ types += [@types[pos_valid[i]]]
458
+ lengths += [@lengths[pos_valid[i]]]
459
+ values += [@values[pos_valid[i]]]
460
+ end
461
+ # We have collected the data that is to be printed, now we need to do some string manipulation if hierarchy is to be displayed:
462
+ if options[1] == true
463
+ # Tree structure requested.
464
+ front_symbol = "| "
465
+ tree_symbol = "|_"
466
+ labels.each_index do |i|
467
+ if levels[i] != 0
468
+ labels[i] = front_symbol*(levels[i]-1) + tree_symbol + labels[i]
469
+ end
470
+ end
471
+ end
472
+ # Extract the string lengths which are needed to make the formatting nice:
473
+ label_lengths = Array.new()
474
+ name_lengths = Array.new()
475
+ type_lengths = Array.new()
476
+ length_lengths = Array.new()
477
+ names.each_index do |i|
478
+ label_lengths[i] = labels[i].length
479
+ name_lengths[i] = names[i].length
480
+ type_lengths[i] = types[i].length
481
+ length_lengths[i] = lengths[i].to_s.length
482
+ end
483
+ # To give the printed output a nice format we need to check the string lengths of some of these arrays:
484
+ index_maxL = pos_valid.max.to_s.length
485
+ label_maxL = label_lengths.max
486
+ name_maxL = name_lengths.max
487
+ type_maxL = type_lengths.max
488
+ length_maxL = length_lengths.max
489
+ # Construct the strings, one for each line of output, where each line contain the information of one tag:
490
+ tags = Array.new()
491
+ labels.each_index do |i|
492
+ # Configure empty spaces:
493
+ s = " "
494
+ f0 = " "*(index_maxL-pos_valid[i].to_s.length)
495
+ f2 = " "*(label_maxL-labels[i].length+1)
496
+ f3 = " "*(name_maxL-names[i].length+1)
497
+ f4 = " "*(type_maxL-types[i].length+1)
498
+ f5 = " "*(length_maxL-lengths[i].to_s.length)
499
+ # Display levels?
500
+ if options[0] == true
501
+ lev = levels[i].to_s + s
502
+ else
503
+ lev = ""
504
+ end
505
+ # Restrict length of value string:
506
+ if values[i].to_s.length > 28
507
+ value = (values[i].to_s)[0..27]+" ..."
361
508
  else
362
- print_index(match)
509
+ value = (values[i].to_s)
363
510
  end
511
+ if types[i] == "OB" or types[i] == "OW" or types[i] == "UN"
512
+ value = "(Binary data)"
513
+ end
514
+ tags += [f0 + pos_valid[i].to_s + s + lev + s + labels[i] + f2 + names[i] + f3 + types[i] + f4 + f5 + lengths[i].to_s + s + s + value]
515
+ end
516
+ # Print to either screen or file, depending on what the user requested:
517
+ if options[2] == true
518
+ print_file(tags)
519
+ else
520
+ print_screen(tags)
364
521
  end
365
522
  end
366
523
 
@@ -433,70 +590,87 @@ module DICOM
433
590
 
434
591
  # Prints the key structural properties of the DICOM file.
435
592
  def print_properties()
593
+ # Explicitness:
436
594
  if @explicit
437
- part1 = "Explicit VR, "
595
+ explicit = "Explicit"
438
596
  else
439
- part1 = "Implicit VR, "
597
+ explicit = "Implicit"
440
598
  end
599
+ # Endianness:
441
600
  if @file_endian
442
- part2 = "Big Endian, with "
601
+ endian = "Big Endian"
443
602
  else
444
- part2 = "Little Endian, with "
603
+ endian = "Little Endian"
445
604
  end
446
- if @compression == false
447
- part3 = "Uncompressed, "
605
+ # Pixel data:
606
+ if @compression == nil
607
+ pixels = "No"
448
608
  else
449
- part3 = "Compressed, "
609
+ pixels = "Yes"
450
610
  end
611
+ # Colors:
451
612
  if @color
452
- part4 = "Color pixel data."
613
+ image = "Colors"
453
614
  else
454
- part4 = "Greyscale pixel data."
615
+ image = "Greyscale"
455
616
  end
456
- if @compression == nil
457
- part3 = "No pixel data."
458
- part4 = ""
617
+ # Compression:
618
+ if @compression == true
619
+ compression = @lib.get_uid(get_value("0002,0010").rstrip)
620
+ else
621
+ compression = "No"
459
622
  end
460
- puts part1 + part2 + part3 + part4
623
+ # Bits per pixel (allocated):
624
+ bits = get_value("0028,0100").to_s
625
+ # Print the file properties:
626
+ puts "Key properties of DICOM object:"
627
+ puts "-------------------------------"
628
+ puts "File: " + @file
629
+ puts "Modality: " + @modality
630
+ puts "Value repr.: " + explicit
631
+ puts "Byte order: " + endian
632
+ puts "Pixel data: " + pixels
633
+ if pixels == "Yes"
634
+ puts "Image: " + image
635
+ puts "Compression: " + compression
636
+ puts "Bits per pixel: " + bits
637
+ end
638
+ puts "-------------------------------"
461
639
  end
462
640
 
463
641
 
464
- # Following methods are private.
642
+ # ***** PS: Following methods are private! *****
465
643
  private
466
644
 
467
645
 
468
- # Private method.
469
- # Prints the information of a specific tag to the screen, identified by its index.
470
- # Optional argument [maxL] is used to format the printing, making it look nicer on screen.
471
- def print_index(pos,*maxL)
472
- if pos < 0 or pos > @last_index
473
- add_msg("The specified index "+pos.to_s+" is outside the bounds of the tags array.")
646
+ # Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
647
+ def set_modality()
648
+ value = get_value("0008,0016")
649
+ if value == false
650
+ @modality = "Not specified"
474
651
  else
475
- if maxL[0] == nil
476
- maxL=@names[pos].length
477
- else
478
- maxL=maxL[0]
479
- end
480
- s = " "
481
- t = "\t"
482
- if pos < 10 then p1 = " "+pos.to_s else p1 = pos.to_s end
483
- p2 = @labels[pos]
484
- p3 = @names[pos]
485
- s3 = " "*(maxL-@names[pos].length+1)
486
- p4 = @types[pos]
487
- p5 = @lengths[pos].to_s
488
- # We dont want to print tag contents if it is binary data:
489
- if @types[pos] == "OB" or @types[pos] == "OW" or @types[pos] == "UN"
490
- p6 = "(binary data)"
491
- else
492
- if @values[pos].to_s.length > 28
493
- p6 = (@values[pos].to_s)[0..27]+" ..."
494
- else
495
- p6 = (@values[pos].to_s)
496
- end
652
+ modality = @lib.get_uid(value.rstrip)
653
+ @modality = modality
654
+ end
655
+ end
656
+
657
+
658
+ # Prints the selected tags to an ascii text file.
659
+ # The text file will be saved in the folder of the original DICOM file,
660
+ # with the original file name plus a .txt extension.
661
+ def print_file(tags)
662
+ File.open( @file + '.txt', 'w' ) do |output|
663
+ tags.each do | line |
664
+ output.print line + "\n"
497
665
  end
498
- # Put the pieces together and print it:
499
- puts p1 + s*2 + p2 + s + p3 + s3 + p4 + t + p5 + t + p6
666
+ end
667
+ end
668
+
669
+
670
+ # Prints the selected tags to screen.
671
+ def print_screen(tags)
672
+ tags.each do | tag |
673
+ puts tag
500
674
  end
501
675
  end
502
676
 
data/lib/DRead.rb CHANGED
@@ -6,6 +6,8 @@ module DICOM
6
6
 
7
7
  # Initialize the DRead instance.
8
8
  def initialize(file_name=nil, lib=nil)
9
+ @a=0
10
+ @b=0
9
11
  # Variables that hold data that will be returned to the person/procedure using this class:
10
12
  # Arrays that will hold information from the DICOM file:
11
13
  @names = Array.new()
@@ -14,6 +16,14 @@ module DICOM
14
16
  @lengths = Array.new()
15
17
  @values = Array.new()
16
18
  @raw = Array.new()
19
+ @levels = Array.new()
20
+ # Keeping track of how many bytes have been read from the file up to and including each tag:
21
+ # This is necessary for tracking the hiearchy in some DICOM files.
22
+ @integrated_lengths = Array.new()
23
+ @header_length = 0
24
+ # Keep track of the hierarchy of tags (this will be used to determine when a sequence or item is finished):
25
+ @hierarchy = Array.new()
26
+ @hierarchy_error = false
17
27
  # Array that will holde any messages generated while reading the DICOM file:
18
28
  @msg = Array.new()
19
29
  # Explicitness (explicit (true) by default):
@@ -32,8 +42,6 @@ module DICOM
32
42
  # Variables used internally when reading the dicom file:
33
43
  # If tag does not exist in the library it is unknown:
34
44
  @unknown = false
35
- # Does the particular tag contain any information?
36
- @content = true
37
45
  # Check endianness of the system (false if little endian):
38
46
  @sys_endian=check_sys_endian()
39
47
  # Endianness of the remaining groups after the first group:
@@ -48,6 +56,8 @@ module DICOM
48
56
  @data_length = 0
49
57
  # Variable used to tell whether file was read succesfully or not:
50
58
  @success = false
59
+ # Keeping track of the tag level while reading through the file:
60
+ @current_level = 0
51
61
 
52
62
  # Open file for binary reading:
53
63
  begin
@@ -70,6 +80,11 @@ module DICOM
70
80
  if header == false
71
81
  @file.close()
72
82
  @file = File.new(file_name, "rb")
83
+ @header_length = 0
84
+ elsif header == nil
85
+ # Reading the file did not succeed, and we need to abort.
86
+ @msg += ["Error! Could not read: "+ file_name + " It might be a directory. Returning."]
87
+ return
73
88
  end
74
89
 
75
90
  # Initiate the process to read tags:
@@ -78,7 +93,7 @@ module DICOM
78
93
  while tag != false and temp_check== true do
79
94
  tag=process_tag()
80
95
  # Store the tag information in arrays:
81
- if tag != false and @content == true
96
+ if tag != false
82
97
  @names+=[tag[0]]
83
98
  @labels+=[tag[1]]
84
99
  @types+=[tag[2]]
@@ -105,7 +120,7 @@ module DICOM
105
120
 
106
121
  # Returns the relevant information gathered from the read dicom procedure.
107
122
  def return_data()
108
- return [@names,@labels,@types,@lengths,@values,@raw,@compression,@color,@explicit, @file_endian, @msg]
123
+ return [@names,@labels,@types,@lengths,@values,@raw,@levels,@compression,@color,@explicit, @file_endian, @msg]
109
124
  end
110
125
 
111
126
 
@@ -115,10 +130,17 @@ module DICOM
115
130
  # consequtive zero bytes followed by 4 bytes that spell the string 'DICM'.
116
131
  # Apparently, some providers seems to skip this in their DICOM files.
117
132
  # First 128 bytes should be zeroes:
118
- bin1=@file.read(128)
133
+ begin
134
+ bin1=@file.read(128)
135
+ @header_length += 128
136
+ rescue
137
+ # The file could not be read. Most likely because the file name variable supplied to this instance was in fact a directory.
138
+ return nil
139
+ end
119
140
  str_header1=bin1.unpack('a' * 128).to_s
120
141
  # Next 4 bytes should spell 'DICM':
121
142
  bin2=@file.read(4)
143
+ @header_length += 4
122
144
  str_header2=bin2.unpack('a' * 4).to_s
123
145
  # If we dont have this expected header, we will still try to read it is a DICOM file.
124
146
  if str_header2 != 'DICM' then
@@ -173,17 +195,15 @@ module DICOM
173
195
 
174
196
 
175
197
  # Governs the process of reading tags in the DICOM file.
198
+ # (This method needs to be cleaned up a bit, it just isnt that easy to see whats
199
+ #going on here in all cases. Perhaps some day I will get the courage to have a go at it again.)
176
200
  def process_tag()
177
201
  #STEP 1: ------------------------------------------------------
178
- # Read the tag label, but exit if the method signals that we have reached end of file:
179
- @content = true
202
+ # Read the tag label, but do not continue if the method signals that we have reached end of file:
180
203
  label=read_label()
181
204
  if label == false
182
205
  return false
183
206
  end
184
- if @content == false # PS: @content switch is not active atm it seems!
185
- return
186
- end
187
207
  # Retrieve the tag name and type based on the label we have read from file:
188
208
  lib_data = @lib.get_name_vr(label)
189
209
  name = lib_data[0]
@@ -210,14 +230,27 @@ module DICOM
210
230
  if length == "UNDEFINED"
211
231
  if label == "7FE0,0010"
212
232
  data = "(Encapsulated pixel data)"
213
- name = "Encapsulated image"
233
+ name = "Encapsulated image(s)"
234
+ type = "SQ"
235
+ elsif type == "SQ" or type == "()"
236
+ # Do not change name of tag.
237
+ data = "(Encapsulated tags)"
214
238
  else
215
239
  data = "(Encapsulated data)"
216
240
  name = "Encapsulated information"
217
241
  end
242
+ # Set hiearchy level:
243
+ set_level(type, length, label)
218
244
  return [name,label,type,length,data]
219
245
  end
220
- # Some special handling for item related tags:
246
+ # Add the length of the content of the tag to the last element in the integrated_lengths array:
247
+ # (but not if it is a sequence or item, as in this case the length of the tag is its sub-tags)
248
+ if length.to_i != 0 and type != "SQ" and type != "()"
249
+ @integrated_lengths[@integrated_lengths.size-1] += length
250
+ end
251
+ # Set hiearchy level:
252
+ set_level(type, length, label)
253
+ # Some special handling for item related tags, which may result in returning without reading data:
221
254
  if type == "()"
222
255
  # If length is zero, just return:
223
256
  if length == 0
@@ -233,26 +266,29 @@ module DICOM
233
266
  if @sq_length != true
234
267
  # Treat the item as containing image data:
235
268
  type = "OW" # A more general approach should be implemented here.
269
+ # For this special case, where item contains the data itself, instead of in sub-tags,
270
+ # we declare that there is to be no sub-level after all.
271
+ # This handling is not particularly obvious or elegant, and perhaps in the future I will
272
+ # be able to rewrite this whole process_tag method to something more sane.
273
+ @current_level = @current_level - 1
236
274
  end
237
275
  end
238
276
  end
239
277
  # STEP 3: ----------------------------------------
240
278
  # Finally read the tag data.
241
- if @content == true
242
- tag_data = read_data(type,length)
243
- value = tag_data[0]
244
- raw = tag_data[1]
245
- # Check for the Transfer Syntax UID tag, and process it:
246
- if label == "0002,0010"
247
- process_syntax(value)
248
- end
249
- if type == "SQ" or type == "()"
250
- @data_length = length # To avoid false errors. In time perhaps a better way of handling this will be found.
251
- else
252
- @data_length = raw.length
253
- end
254
- return [name,label,type,length,value,raw]
279
+ tag_data = read_data(type,length)
280
+ value = tag_data[0]
281
+ raw = tag_data[1]
282
+ # Check for the Transfer Syntax UID tag, and process it:
283
+ if label == "0002,0010"
284
+ process_syntax(value)
285
+ end
286
+ if type == "SQ" or type == "()"
287
+ @data_length = length # To avoid false errors. In time perhaps a better way of handling this will be found.
288
+ else
289
+ @data_length = raw.length
255
290
  end
291
+ return [name,label,type,length,value,raw]
256
292
  end
257
293
  # END READ TAG
258
294
 
@@ -262,9 +298,18 @@ module DICOM
262
298
  bin1=@file.read(2)
263
299
  bin2=@file.read(2)
264
300
  # Check if we have reached end of file before proceeding:
265
- if bin1 == nil
301
+ if bin1 == nil or bin2 == nil
266
302
  return false
267
303
  end
304
+ # Add the length of the tag label. If this was the first label read from file, we need to add the header length too:
305
+ if @integrated_lengths.length == 0
306
+ # Increase the array with the length of the header + the 4 bytes:
307
+ @integrated_lengths += [@header_length + 4]
308
+ else
309
+ # For the remaining tags, increase the array with the integrated length of the previous tags + the 4 bytes:
310
+ @integrated_lengths += [@integrated_lengths[@integrated_lengths.length-1] + 4]
311
+ end
312
+ # Unpack the blobs:
268
313
  label1=bin1.unpack('h*').to_s.reverse.upcase
269
314
  label2=bin2.unpack('h*').to_s.reverse.upcase
270
315
  # Special treatment of tags that are of the first "0002" group:
@@ -310,10 +355,12 @@ module DICOM
310
355
  # It seems we need to have a special case for item labels in the explicit scenario:
311
356
  if label == "FFFE,E000" or label == "FFFE,E00D" or label == "FFFE,E0DD"
312
357
  bin=@file.read(4)
358
+ @integrated_lengths[@integrated_lengths.length-1] += 4
313
359
  length = get_SL(bin)
314
360
  else
315
361
  # Read tag type field (2 bytes - since we are not dealing with an item related tag):
316
362
  bin=@file.read(2)
363
+ @integrated_lengths[@integrated_lengths.length-1] += 2
317
364
  type=bin.unpack('a*').to_s
318
365
  end
319
366
  # Two (three) possible structures for value length here, dependent on tag type:
@@ -321,20 +368,24 @@ module DICOM
321
368
  when "OB","OW","SQ","UN"
322
369
  # Two empty bytes should occur here, according to the standard:
323
370
  bin=@file.read(2)
371
+ @integrated_lengths[@integrated_lengths.length-1] += 2
324
372
  # Read value length (4 bytes):
325
373
  bin=@file.read(4)
374
+ @integrated_lengths[@integrated_lengths.length-1] += 4
326
375
  length=get_SL(bin)
327
376
  when "()"
328
377
  #An empty entry for the item related tags (As it has already been processed).
329
378
  else
330
379
  # For all the other tag types: Read value length (2 bytes):
331
380
  bin=@file.read(2)
381
+ @integrated_lengths[@integrated_lengths.length-1] += 2
332
382
  length=get_US(bin)
333
383
  end
334
384
  else
335
385
  #IMPLICIT:
336
386
  # Read value length (4 bytes):
337
387
  bin=@file.read(4)
388
+ @integrated_lengths[@integrated_lengths.length-1] += 4
338
389
  length = get_SL(bin)
339
390
  end
340
391
  # For encapsulated data, the tag length will not be defined. To convey this,
@@ -471,7 +522,7 @@ module DICOM
471
522
  end
472
523
 
473
524
  # For everything else, assume string type information:
474
- when 'AE','AS','CS','DA','DS','IS','LO','LT','PN','SH','ST','TM','UI','VR'
525
+ when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT' #,'VR'
475
526
  bin=@file.read(length)
476
527
  data=bin.unpack('a*').to_s
477
528
  else
@@ -486,6 +537,79 @@ module DICOM
486
537
  # END TAG DATA
487
538
 
488
539
 
540
+ # Sets the level of the current tag in the hiearchy.
541
+ # The default (top) level is zero.
542
+ def set_level(type, length, label)
543
+ # Set the level of this tag:
544
+ @levels += [@current_level]
545
+ # Determine if there is a level change for the following tag:
546
+ # If tag is a sequence, the level of the following tags will be increased by one.
547
+ # If tag is an item, the level of the following tags will be increased by one.
548
+ # Note the following exception:
549
+ # If label is "Item", and it contains data (image fragment) directly, which is to say,
550
+ # not in its sub-tags, we should not increase the level. (This is fixed in the process_tag method.)
551
+ if type == "SQ"
552
+ increase = true
553
+ elsif label =="FFFE,E000"
554
+ increase = true
555
+ else
556
+ increase = false
557
+ end
558
+ if increase == true
559
+ @current_level = @current_level + 1
560
+ # If length of sequence/item is specified, we must note this length + the current tag position in the arrays:
561
+ if length.to_i != 0
562
+ @hierarchy += [[length,@integrated_lengths.last]]
563
+ else
564
+ @hierarchy += [type]
565
+ end
566
+ end
567
+ # Need to check whether a previous sequence or item has ended, if so the level must be decreased by one:
568
+ # In the case of tag specification:
569
+ if (label == "FFFE,E00D") or (label == "FFFE,E0DD")
570
+ @current_level = @current_level - 1
571
+ end
572
+ # In the case of sequence and item length specification:
573
+ # Check the last position in the hieararchy array.
574
+ # If it is an array (of length and position), then we need to check the integrated_lengths array
575
+ # to see if the current sub-level has expired.
576
+ if @hierarchy.size > 0
577
+ check_level_end()
578
+ end
579
+ end
580
+
581
+
582
+ # Checks how far we've read in the DICOM file to determine if we have reached a point
583
+ # where sub-levels are ending. This method is recursive, as multiple sequences/items might end at the same point.
584
+ def check_level_end()
585
+ # The test is only meaningful to perform if we are not expecting an 'end of sequence/item' tag to signal the level-change.
586
+ if (@hierarchy.last).is_a?(Array)
587
+ described_length = (@hierarchy.last)[0]
588
+ previous_length = (@hierarchy.last)[1]
589
+ current_length = @integrated_lengths.last
590
+ current_diff = current_length - previous_length
591
+ if current_diff == described_length
592
+ # Decrease level by one:
593
+ @current_level = @current_level - 1
594
+ # Also we need to delete the last entry of the @hierarchy array:
595
+ if (@hierarchy.size > 1)
596
+ @hierarchy = @hierarchy[0..(@hierarchy.size-2)]
597
+ # There might be numerous levels that ends at this particular point, so we need to do a recursive repeat to check.
598
+ check_level_end()
599
+ else
600
+ @hierarchy = Array.new()
601
+ end
602
+ elsif current_diff > described_length
603
+ # Only register this type of error one time per file to avoid a spamming effect:
604
+ if not @hierarchy_error
605
+ @msg += ["Unexpected hierarchy incident: Current length difference is greater than the expected value, which should not occur. This will not pose any problems unless you intend to query the object for tags in the hierarchy."]
606
+ @hierarchy_error = true
607
+ end
608
+ end
609
+ end
610
+ end
611
+
612
+
489
613
  # Returns the (processed) value of a DICOM tag based on an input tag label, category name or array index.
490
614
  def get_value(id)
491
615
  # Assume we have been fed a tag label:
data/lib/Dictionary.rb CHANGED
@@ -17,6 +17,44 @@ module DICOM
17
17
  return [a,b]
18
18
  end
19
19
 
20
+
21
+ # Loads the value representation library.
22
+ # Consists of VR name, meaning and data format.
23
+ def load_vr()
24
+ d = Array.new()
25
+ e = Array.new()
26
+ f = Array.new()
27
+ d+=["AE"] and e+=["Application entity"] and f+=["String"]
28
+ d+=["AS"] and e+=["Age string"] and f+=["String"]
29
+ d+=["AT"] and e+=["Attribute tag"] and f+=["Two 2-byte integers"]
30
+ d+=["CS"] and e+=["Code string"] and f+=["String"]
31
+ d+=["DA"] and e+=["Date"] and f+=["String"]
32
+ d+=["DS"] and e+=["Decimal string"] and f+=["String"]
33
+ d+=["DT"] and e+=["Date time"] and f+=["String"]
34
+ d+=["FL"] and e+=["Floating point single"] and f+=["4-byte floating point"]
35
+ d+=["FD"] and e+=["Floating point double"] and f+=["8-byte floating point"]
36
+ d+=["IS"] and e+=["Integer string"] and f+=["String"]
37
+ d+=["LO"] and e+=["Long string"] and f+=["String"]
38
+ d+=["LT"] and e+=["Long text"] and f+=["String"]
39
+ d+=["OB"] and e+=["Other byte string"] and f+=["1-byte integers"]
40
+ d+=["OF"] and e+=["Other float string"] and f+=["4-byte floating point numbers"]
41
+ d+=["OW"] and e+=["Other word string"] and f+=["2-byte integers"]
42
+ d+=["PN"] and e+=["Person name"] and f+=["String"]
43
+ d+=["SH"] and e+=["Short string"] and f+=["String"]
44
+ d+=["SL"] and e+=["Signed long"] and f+=["4-byte integer"]
45
+ d+=["SQ"] and e+=["Sequence of items"] and f+=["Unknown"]
46
+ d+=["SS"] and e+=["Signed short"] and f+=["2-byte integer"]
47
+ d+=["ST"] and e+=["Short text"] and f+=["String"]
48
+ d+=["TM"] and e+=["Time"] and f+=["String"]
49
+ d+=["UI"] and e+=["Unique identifier"] and f+=["String"]
50
+ d+=["UL"] and e+=["Unsigned long"] and f+=["4-byte integer"]
51
+ d+=["UN"] and e+=["Unknown"] and f+=["Unknown"]
52
+ d+=["US"] and e+=["Unsigned short"] and f+=["2-byte integer"]
53
+ d+=["UT"] and e+=["Unlimited text"] and f+=["String"]
54
+ return [d,e,f]
55
+ end
56
+
57
+
20
58
  # Table A.1 UID Values (DICOM Part 6, Annex A: Registry of DICOM unique identifiers)
21
59
  def load_uid()
22
60
  r = Array.new()
metadata CHANGED
@@ -1,33 +1,26 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.4
3
- specification_version: 1
4
2
  name: dicom
5
3
  version: !ruby/object:Gem::Version
6
- version: "0.2"
7
- date: 2008-08-10 00:00:00 +02:00
8
- summary: Library for reading DICOM files.
9
- require_paths:
10
- - lib
11
- email: chris.lervag@gmail.com
12
- homepage: http://rubyforge.org/projects/dicom/
13
- rubyforge_project: dicom
14
- description: DICOM is a standard widely used throughout the world to store and transfer medical image data. This project aims to make a library that is able to handle DICOM in the Ruby language, to the benefit of any student or professional who would like to use Ruby to process their DICOM files.
15
- autorequire:
16
- default_executable:
17
- bindir: bin
18
- has_rdoc: false
19
- required_ruby_version: !ruby/object:Gem::Version::Requirement
20
- requirements:
21
- - - ">"
22
- - !ruby/object:Gem::Version
23
- version: 0.0.0
24
- version:
4
+ version: "0.3"
25
5
  platform: ruby
26
- signing_key:
27
- cert_chain:
28
- post_install_message:
29
6
  authors:
30
- - "Christoffer Lerv\xE5g"
7
+ - Christoffer Lervag
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-10-12 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: DICOM is a standard widely used throughout the world to store and transfer medical image data. This project aims to make a library that is able to handle DICOM in the Ruby language, to the benefit of any student or professional who would like to use Ruby to process their DICOM files.
17
+ email: chris.lervag@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
31
24
  files:
32
25
  - lib/DObject.rb
33
26
  - lib/DLibrary.rb
@@ -38,17 +31,31 @@ files:
38
31
  - COPYING
39
32
  - README
40
33
  - DOCUMENTATION
41
- test_files: []
42
-
34
+ has_rdoc: false
35
+ homepage: http://dicom.rubyforge.org/
36
+ post_install_message:
43
37
  rdoc_options: []
44
38
 
45
- extra_rdoc_files: []
46
-
47
- executables: []
48
-
49
- extensions: []
50
-
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
51
53
  requirements: []
52
54
 
53
- dependencies: []
55
+ rubyforge_project: dicom
56
+ rubygems_version: 1.3.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: Library for reading DICOM files.
60
+ test_files: []
54
61