dicom 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # Copyright 2008-2009 Christoffer Lerv�g
2
+
1
3
  module DICOM
2
4
  # Class which holds the methods that interact with the DICOM dictionary.
3
5
  class DLibrary
@@ -6,22 +8,9 @@ module DICOM
6
8
 
7
9
  # Initialize the DRead instance.
8
10
  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
-
11
+ # Dictionary content will be stored in instance arrays.
22
12
  # Load the dictionary:
23
13
  dict = Dictionary.new()
24
-
25
14
  # Data elements:
26
15
  de = dict.load_tags()
27
16
  @de_label = de[0]
@@ -80,22 +69,23 @@ module DICOM
80
69
 
81
70
 
82
71
  # Checks whether a given string is a valid transfer syntax or not.
83
- def check_transfer_syntax(label)
72
+ def check_ts_validity(label)
73
+ res = false
84
74
  pos = @uid_value.index(label)
85
- if pos >= 1 and pos <= 34
86
- # Valid:
87
- return true
88
- else
89
- # Invalid:
90
- return false
75
+ if pos != nil
76
+ if pos >= 1 and pos <= 34
77
+ # Proved valid:
78
+ res = true
79
+ end
91
80
  end
81
+ return res
92
82
  end
93
83
 
94
84
 
95
85
  # 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)
86
+ def get_uid(value)
87
+ # Find the position of the specified value in the array:
88
+ pos = @uid_value.index(value)
99
89
  # Fetch the name of this UID:
100
90
  if pos != nil
101
91
  name = @uid_name[pos]
@@ -104,6 +94,21 @@ module DICOM
104
94
  end
105
95
  return name
106
96
  end
97
+
98
+
99
+ # Checks if the supplied transfer syntax indicates the presence of pixel compression or not.
100
+ def get_compression(value)
101
+ res = false
102
+ # Index less or equal to 4 means no compression.
103
+ pos = @uid_value.index(value)
104
+ if pos != nil
105
+ if pos > 4
106
+ # It seems we have compression:
107
+ res = true
108
+ end
109
+ end
110
+ return res
111
+ end
107
112
 
108
113
 
109
114
  # Following methods are private.
@@ -1,40 +1,45 @@
1
- # Copyright 2008 Christoffer Lerv�g
2
-
1
+ # Copyright 2008-2009 Christoffer Lerv�g
2
+ #
3
3
  # This program is free software: you can redistribute it and/or modify
4
4
  # it under the terms of the GNU General Public License as published by
5
5
  # the Free Software Foundation, either version 3 of the License, or
6
6
  # (at your option) any later version.
7
-
7
+ #
8
8
  # This program is distributed in the hope that it will be useful,
9
9
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
  # GNU General Public License for more details.
12
-
12
+ #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
-
15
+ #
16
16
  #--------------------------------------------------------------------------------------------------
17
17
 
18
18
  # TODO:
19
- # -Support for writing DICOM files.
19
+ # -Support for writing complex (hierarchical) DICOM files (basic write support is featured).
20
20
  # -Full support for compressed image data.
21
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 all tags contained within a specific sequence.
22
+ # -Support for color image data to get_image_narray() and get_image_magick().
23
+ # -Complete support for Big endian (basic support is already featured).
24
+ # -Complete support for multiple frame image data to NArray and RMagick objects (partial support already featured).
25
+ # -Reading of image data in files that contain two different and unrelated images (this problem has been observed with some MR images).
27
26
 
28
27
  module DICOM
29
28
 
30
29
  # Class for handling the DICOM contents:
31
30
  class DObject
32
31
 
33
- attr_reader :read_success, :modality
32
+ attr_reader :read_success, :write_success, :modality
34
33
 
35
34
  # Initialize the DObject instance.
36
- def initialize(file_name=nil, verbose=false, lib=nil)
37
- # Initialize the key variables for the DICOM object:
35
+ def initialize(file_name=nil, opts={})
36
+ # Process option values, setting defaults for the ones that are not specified:
37
+ @verbose = opts[:verbose]
38
+ @lib = opts[:lib] || DLibrary.new
39
+ # Default verbosity is true:
40
+ @verbose = true if @verbose == nil
41
+
42
+ # Initialize variables that will be used for the DICOM object:
38
43
  @names = Array.new()
39
44
  @labels = Array.new()
40
45
  @types = Array.new()
@@ -55,75 +60,99 @@ module DICOM
55
60
  @file_endian = false
56
61
  # Information about the DICOM object:
57
62
  @modality = nil
58
- # Handling variables:
59
- @verbose = verbose
60
63
  # Control variables:
61
64
  @read_success = false
62
- # Load the library class (DICOM dictionary):
63
- if lib != nil
64
- # Library already specified by user:
65
- @lib = lib
66
- else
67
- @lib = DLibrary.new()
68
- end
69
- # If a (valid) file name string is supplied, launch the method to read DICOM file:
65
+ # Check endianness of the system (false if little endian):
66
+ @sys_endian = check_sys_endian()
67
+ # Set format strings for packing/unpacking:
68
+ set_format_strings()
69
+
70
+ # If a (valid) file name string is supplied, call the method to read the DICOM file:
70
71
  if file_name != nil and file_name != ""
71
72
  @file = file_name
72
73
  read_file(file_name)
73
74
  end
74
- end
75
+ end # of method initialize
75
76
 
76
77
 
77
78
  # Returns a DICOM object by reading the file specified.
78
- # This is accomplished by initliazing the DRead class, which returns the DICOM object, if successful.
79
+ # This is accomplished by initliazing the DRead class, which loads DICOM information to arrays.
79
80
  # For the time being, this method is called automatically when initializing the DObject class,
80
81
  # but in the future, when write support is added, this method may have to be called manually.
81
82
  def read_file(file_name)
82
- if file_name == nil or file_name == ""
83
- add_msg("Warning: A valid file name string was not supplied to method read_file(). Returning false.")
84
- return false
83
+ dcm = DRead.new(file_name, :lib => @lib, :sys_endian => @sys_endian)
84
+ # Store the data to the instance variables if the readout was a success:
85
+ if dcm.success
86
+ @read_success = true
87
+ @names = dcm.names
88
+ @labels = dcm.labels
89
+ @types = dcm.types
90
+ @lengths = dcm.lengths
91
+ @values = dcm.values
92
+ @raw = dcm.raw
93
+ @levels = dcm.levels
94
+ @explicit = dcm.explicit
95
+ @file_endian = dcm.file_endian
96
+ # Set format strings for packing/unpacking:
97
+ set_format_strings(@file_endian)
98
+ # Index of last element in tag arrays:
99
+ @last_index=@names.length-1
100
+ # Update status variables for this object:
101
+ check_properties()
102
+ # Set the modality of the DICOM object:
103
+ set_modality()
85
104
  else
86
- dcm = DRead.new(file_name, @lib)
87
- data = dcm.return_data()
88
- # Store the data to the instance variables if the readout was a success:
89
- if dcm.success
90
- @read_success = true
91
- @names = data[0]
92
- @labels = data[1]
93
- @types = data[2]
94
- @lengths = data[3]
95
- @values = data[4]
96
- @raw = data[5]
97
- @levels = data[6]
98
- # Other information:
99
- @compression = data[7]
100
- @color = data[8]
101
- @explicit = data[9]
102
- @file_endian = data[10]
103
- # Index of last element in tag arrays:
104
- @last_index=@names.length-1
105
- # Set the modality of the DICOM object:
106
- set_modality()
107
- end
108
- # The messages must be stored regardless of success or failure:
109
- messages = data[11]
110
- # If any messages has been recorded, send these to the message handling method:
111
- if messages.size != 0
112
- add_msg(messages)
113
- end
105
+ @read_success = false
106
+ end
107
+ # If any messages has been recorded, send these to the message handling method:
108
+ if dcm.msg.size != 0
109
+ add_msg(dcm.msg)
110
+ end
111
+ end
112
+
113
+
114
+ # Transfers necessary information from the DObject to the DWrite class, which
115
+ # will attempt to write this information to a valid DICOM file.
116
+ def write_file(file_name)
117
+ w = DWrite.new(file_name, :lib => @lib, :sys_endian => @sys_endian)
118
+ w.labels = @labels
119
+ w.types = @types
120
+ w.lengths = @lengths
121
+ w.raw = @raw
122
+ w.rest_endian = @file_endian
123
+ w.rest_explicit = @explicit
124
+ w.write
125
+ # Write process succesful?
126
+ @write_success = w.success
127
+ # If any messages has been recorded, send these to the message handling method:
128
+ if w.msg.size != 0
129
+ add_msg(w.msg)
114
130
  end
115
131
  end
132
+
133
+
134
+ #################################################
135
+ # START OF METHODS FOR READING INFORMATION FROM DICOM OBJECT:#
136
+ #################################################
116
137
 
117
138
 
118
- # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
119
- def add_msg(msg)
120
- if @verbose
121
- puts msg
139
+ # Checks the status of the pixel data that has been read from the DICOM file: whether it exists at all and if its greyscale or color.
140
+ # Modifies instance variable @color if color image is detected and instance variable @compression if no pixel data is detected.
141
+ def check_properties()
142
+ # Check if pixel data is present:
143
+ if @labels.index("7FE0,0010") == nil
144
+ # No pixel data in DICOM file:
145
+ @compression = nil
146
+ else
147
+ @compression = @lib.get_compression(get_value("0002,0010"))
122
148
  end
123
- if (msg.is_a? String)
124
- msg=[msg]
149
+ # Set color variable as true if our object contain a color image:
150
+ col_string = get_value("0028,0004")
151
+ if col_string != false
152
+ if (col_string.include? "RGB") or (col_string.include? "COLOR") or (col_string.include? "COLOUR")
153
+ @color = true
154
+ end
125
155
  end
126
- @msg += msg
127
156
  end
128
157
 
129
158
 
@@ -131,7 +160,7 @@ module DICOM
131
160
  def read_image_magick(pos, columns, rows)
132
161
  if @compression != true
133
162
  # Non-compressed, just return the array contained in the particular tag:
134
- image_data=get_value(pos)
163
+ image_data=get_pixels(pos)
135
164
  image = Magick::Image.new(columns,rows)
136
165
  image.import_pixels(0, 0, columns, rows, "I", image_data)
137
166
  return image
@@ -178,7 +207,7 @@ module DICOM
178
207
  # and if it is located in one or more tags:
179
208
  if image_pos.size == 1
180
209
  # All of the image data is located in one tag:
181
- image_data = get_value(image_pos[0])
210
+ image_data = get_pixels(image_pos[0])
182
211
  #image_data = get_image_data(image_pos[0])
183
212
  (0..frames-1).each do |i|
184
213
  (0..columns*rows-1).each do |j|
@@ -210,7 +239,7 @@ module DICOM
210
239
  image[i,true,true]=temp_image
211
240
  end
212
241
  return image
213
- end
242
+ end # of method get_image_narray
214
243
 
215
244
 
216
245
  # Returns an array of RMagick image objects, where the size of the array corresponds with the number of frames in the image data.
@@ -259,7 +288,7 @@ module DICOM
259
288
  end
260
289
  end
261
290
  return image_arr
262
- end
291
+ end # of method get_image_magick
263
292
 
264
293
 
265
294
  # Returns the number of frames present in the image data in the DICOM file.
@@ -271,6 +300,42 @@ module DICOM
271
300
  end
272
301
  return frames.to_i
273
302
  end
303
+
304
+
305
+ # Unpacks and returns pixel data from a specified tag position:
306
+ def get_pixels(pos)
307
+ pixels = false
308
+ # We need to know what kind of bith depth the pixel data is saved with:
309
+ bit_depth = get_value("0028,0100")
310
+ if bit_depth != false
311
+ # Load the binary pixel data:
312
+ bin = get_raw(pos)
313
+ # Number of bytes used per pixel will determine how to unpack this:
314
+ case bit_depth
315
+ when 8
316
+ pixels = bin.unpack(@by) # Byte/Character/Fixnum (1 byte)
317
+ when 16
318
+ pixels = bin.unpack(@us) # Unsigned short (2 bytes)
319
+ when 12
320
+ # 12 BIT SIMPLY NOT WORKING YET!
321
+ # This one is a bit more tricky to extract.
322
+ # I havent really given this priority so far as 12 bit image data is rather rare.
323
+ add_msg("Warning: Bit depth 12 is not working correctly at this time! Please contact the author.")
324
+ #pixels = Array.new(length)
325
+ #(length).times do |i|
326
+ #hex = bin.unpack('H3')
327
+ #hex4 = "0"+hex[0]
328
+ #num = hex[0].unpack('v')
329
+ #data[i] = num
330
+ #end
331
+ else
332
+ raise "Bit depth ["+bit_depth.to_s+"] has not received implementation in this procedure yet. Please contact the author."
333
+ end # of case bit_depth
334
+ else
335
+ add_msg("Error: DICOM object does not contain bit depth tag (0028,0010).")
336
+ end # of if bit_depth ..
337
+ return pixels
338
+ end # of method get_pixels
274
339
 
275
340
 
276
341
  # Returns the index(es) of the tag(s) that contain image data.
@@ -310,55 +375,79 @@ module DICOM
310
375
  end
311
376
  end
312
377
  end
313
- end
378
+ end # of method get_image_pos
314
379
 
315
380
 
316
381
  # 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
382
  # If no match is found, the method will return false.
318
383
  # 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:
328
- indexes = Array.new()
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]
384
+ # :array => myArray - tells the method to search for hits in this specific array of positions instead of searching
385
+ # through the entire DICOM obhject. If myArray is false, so will the return of this method.
386
+ # If array is nil (not specified), then the entire DICOM object will be searched.
387
+ def get_pos(label, opts={})
388
+ # Process option value:
389
+ opt_array = opts[:array]
390
+ if opt_array == false
391
+ # If the supplied array option equals false, it signals that the user tries to search for a tag
392
+ # in an invalid position, and as such, this method should also return false:
393
+ indexes = false
332
394
  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|
338
- if @labels[i] == label
339
- indexes += [i]
340
- elsif @names[i] == label
341
- indexes += [i]
342
- elsif label == i
343
- indexes += [i]
395
+ # Perform search to find indexes:
396
+ # Array that will contain the positions where the supplied label gives a match:
397
+ indexes = Array.new()
398
+ # Either use the supplied array, or we will create an array that contain the indices of the entire DICOM object:
399
+ if opt_array.is_a?(Array)
400
+ search_array=opt_array
401
+ else
402
+ search_array = Array.new(@names.size) {|i| i}
344
403
  end
404
+ # Do the search:
405
+ search_array.each do |i|
406
+ if @labels[i] == label
407
+ indexes += [i]
408
+ elsif @names[i] == label
409
+ indexes += [i]
410
+ elsif label == i
411
+ indexes += [i]
412
+ end
413
+ end
414
+ # If no hits occured, we will return false instead of an empty array:
415
+ indexes = false if indexes.size == 0
345
416
  end
346
- # Return false if no hits are found, else return the array of indices:
347
- if indexes.size == 0
348
- #puts "Notice: The requested position of label "+label+" was not identified."
349
- return false
350
- else
351
- return indexes
352
- end
353
- end
417
+ return indexes
418
+ end # of method get_pos
419
+
420
+
421
+ # Dumps the binary content of the Pixel Data tag to file.
422
+ def image_to_file(file)
423
+ pos = get_image_pos()
424
+ if pos
425
+ if pos.length == 1
426
+ # Pixel data located in one tag:
427
+ pixel_data = get_raw(pos[0])
428
+ f = File.new(file, "wb")
429
+ f.write(pixel_data)
430
+ f.close()
431
+ else
432
+ # Pixel data located in several tags:
433
+ pos.each_index do |i|
434
+ pixel_data = get_raw(pos[i])
435
+ f = File.new(file + i.to_s, "wb")
436
+ f.write(pixel_data)
437
+ f.close()
438
+ end
439
+ end
440
+ end # of if pos =...
441
+ end # of method image_to_file
354
442
 
355
443
 
356
444
  # Returns the positions of all tags inside the hierarchy of a sequence or an item.
357
445
  # 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]
446
+ # :next_only => true - The method will only search immediately below the specified
447
+ # item or sequence (that is, in the level of parent + 1).
448
+ def children(tag, opts={})
449
+ # Process option values, setting defaults for the ones that are not specified:
450
+ opt_next_only = opts[:next_only] || false
362
451
  # Retrieve position of parent tag which from which we will search:
363
452
  pos = get_pos(tag)
364
453
  if pos == false
@@ -376,7 +465,7 @@ module DICOM
376
465
  remain_array.each_index do |i|
377
466
  if (remain_array[i] > parent_level) and (extract == true)
378
467
  # If search is targetted at any specific level, we can just add this position:
379
- if not restriction == true
468
+ if not opt_next_only == true
380
469
  below_pos += [p+1+i]
381
470
  else
382
471
  # As search is restricted to parent level + 1, do a test for this:
@@ -396,7 +485,112 @@ module DICOM
396
485
  else
397
486
  return below_pos
398
487
  end
399
- end
488
+ end # of method below
489
+
490
+
491
+ # Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
492
+ # The ID may be a tag index, tag name or tag label.
493
+ def get_value(id)
494
+ if id == nil
495
+ add_msg("A tag label, category name or index number must be specified when calling the get_value() method!")
496
+ return false
497
+ else
498
+ # Assume we have been fed a tag label:
499
+ pos=@labels.index(id)
500
+ # If this does not give a hit, assume we have been fed a tag name:
501
+ if pos==nil
502
+ pos=@names.index(id)
503
+ end
504
+ # If we still dont have a hit, check if it is a valid number within the array range:
505
+ if pos == nil
506
+ if (id.is_a? Integer)
507
+ if id >= 0 and id <= @last_index
508
+ # The id supplied is a valid position, return its corresponding value:
509
+ return @values[id]
510
+ else
511
+ return false
512
+ end
513
+ else
514
+ return false
515
+ end
516
+ else
517
+ # We have a valid position, return the value:
518
+ return @values[pos]
519
+ end
520
+ end
521
+ end # of method get_value
522
+
523
+
524
+ # Returns the raw data of the DICOM tag that matches the supplied tag ID.
525
+ # The ID may be a tag index, tag name or tag label.
526
+ def get_raw(id)
527
+ if id == nil
528
+ add_msg("A tag label, category name or index number must be specified when calling the get_raw() method!")
529
+ return false
530
+ else
531
+ # Assume we have been fed a tag label:
532
+ pos=@labels.index(id)
533
+ # If this does not give a hit, assume we have been fed a tag name:
534
+ if pos==nil
535
+ pos=@names.index(id)
536
+ end
537
+ # If we still dont have a hit, check if it is a valid number within the array range:
538
+ if pos == nil
539
+ if (id.is_a? Integer)
540
+ if id >= 0 and id <= @last_index
541
+ # The id supplied is a valid position, return its corresponding value:
542
+ return @raw[id]
543
+ else
544
+ return false
545
+ end
546
+ else
547
+ return false
548
+ end
549
+ else
550
+ # We have a valid position, return the value:
551
+ return @raw[pos]
552
+ end
553
+ end
554
+ end # of method get_raw
555
+
556
+
557
+ # Returns the position of (possible) parents of the specified tag in the hierarchy structure of the DICOM object.
558
+ def parents(tag)
559
+ # Get array position:
560
+ pos = get_pos(tag)
561
+ if pos == false
562
+ parents = false
563
+ else
564
+ # Extracting first value in array pos:
565
+ pos = pos[0]
566
+ # Get level of our tag:
567
+ level = @levels[pos]
568
+ # If tag is top level it can obviously have no parents:
569
+ if level == 0
570
+ parents = false
571
+ else
572
+ # Search backwards, and record the position every time we encounter an
573
+ # upwards change in the level number.
574
+ parents = Array.new()
575
+ prev_level = level
576
+ search_arr = @levels[0..pos-1].reverse
577
+ search_arr.each_index do |i|
578
+ if search_arr[i] < prev_level
579
+ parents += [search_arr.length-i-1]
580
+ prev_level = search_arr[i]
581
+ end
582
+ end
583
+ # When tag has several generations of parents, we want its top parent to be first in the returned array:
584
+ parents = parents.reverse
585
+ end # of if level == 0
586
+ end # of if pos == false..else
587
+ return parents
588
+ end # of method parents
589
+
590
+
591
+ ##############################################
592
+ ####### START OF METHODS FOR PRINTING INFORMATION:######
593
+ ##############################################
400
594
 
401
595
 
402
596
  # Prints information of all tags stored in the DICOM object.
@@ -411,10 +605,14 @@ module DICOM
411
605
  # 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
606
  # Tag(s) may be specified by position, label or name.
413
607
  # 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)
608
+ # :levels => true - will make the method print the level numbers for each tag.
609
+ # tree => true - will make the method print a tree structure for the tags.
610
+ # :file => true - will make the method print to file instead of printing to screen.
611
+ def print(pos, opts={})
612
+ # Process option values, setting defaults for the ones that are not specified:
613
+ opt_levels = opts[:levels] || false
614
+ opt_tree = opts[:tree] || false
615
+ opt_file = opts[:file] || false
418
616
  # Convert to array if number:
419
617
  if not pos.is_a?(Array) and pos != true
420
618
  pos_valid = get_pos(pos)
@@ -425,7 +623,7 @@ module DICOM
425
623
  pos_valid.each_index do |i|
426
624
  pos_valid[i]=i
427
625
  end
428
- else
626
+ else
429
627
  # Check that the supplied array contains valid positions:
430
628
  pos_valid = Array.new()
431
629
  pos.each_index do |i|
@@ -433,7 +631,7 @@ module DICOM
433
631
  pos_valid += [pos[i]]
434
632
  end
435
633
  end
436
- end
634
+ end
437
635
  # Continue only if we have valid positions:
438
636
  if pos_valid == false
439
637
  return
@@ -450,16 +648,16 @@ module DICOM
450
648
  lengths = Array.new()
451
649
  values = Array.new()
452
650
  # 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
651
+ pos_valid.each do |pos|
652
+ labels += [@labels[pos]]
653
+ levels += [@levels[pos]]
654
+ names += [@names[pos]]
655
+ types += [@types[pos]]
656
+ lengths += [@lengths[pos].to_s]
657
+ values += [@values[pos].to_s]
658
+ end
461
659
  # 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
660
+ if opt_tree
463
661
  # Tree structure requested.
464
662
  front_symbol = "| "
465
663
  tree_symbol = "|_"
@@ -487,7 +685,9 @@ module DICOM
487
685
  type_maxL = type_lengths.max
488
686
  length_maxL = length_lengths.max
489
687
  # Construct the strings, one for each line of output, where each line contain the information of one tag:
490
- tags = Array.new()
688
+ tags = Array.new()
689
+ # Start of loop which formats the tags:
690
+ # (This loop is what consumes most of the computing time of this method)
491
691
  labels.each_index do |i|
492
692
  # Configure empty spaces:
493
693
  s = " "
@@ -497,96 +697,34 @@ module DICOM
497
697
  f4 = " "*(type_maxL-types[i].length+1)
498
698
  f5 = " "*(length_maxL-lengths[i].to_s.length)
499
699
  # Display levels?
500
- if options[0] == true
700
+ if opt_levels
501
701
  lev = levels[i].to_s + s
502
702
  else
503
703
  lev = ""
504
704
  end
505
705
  # Restrict length of value string:
506
- if values[i].to_s.length > 28
507
- value = (values[i].to_s)[0..27]+" ..."
706
+ if values[i].length > 28
707
+ value = (values[i])[0..27]+" ..."
508
708
  else
509
- value = (values[i].to_s)
709
+ value = (values[i])
510
710
  end
511
- if types[i] == "OB" or types[i] == "OW" or types[i] == "UN"
512
- value = "(Binary data)"
711
+ # Insert descriptive text for tags that hold binary data:
712
+ case types[i]
713
+ when "OW","OB","UN"
714
+ value = "(Binary Data)"
715
+ when "SQ","()"
716
+ value = "(Encapsulated Elements)"
513
717
  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]
718
+ 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.rstrip]
515
719
  end
516
720
  # Print to either screen or file, depending on what the user requested:
517
- if options[2] == true
721
+ if opt_file
518
722
  print_file(tags)
519
723
  else
520
724
  print_screen(tags)
521
- end
522
- end
523
-
524
-
525
- # Returns the value (processed raw data) of the DICOM tag that matches the supplied tag ID.
526
- # The ID may be a tag index, tag name or tag label.
527
- def get_value(id)
528
- if id == nil
529
- add_msg("A tag label, category name or index number must be specified when calling the get_value() method!")
530
- return false
531
- else
532
- # Assume we have been fed a tag label:
533
- pos=@labels.index(id)
534
- # If this does not give a hit, assume we have been fed a tag name:
535
- if pos==nil
536
- pos=@names.index(id)
537
- end
538
- # If we still dont have a hit, check if it is a valid number within the array range:
539
- if pos == nil
540
- if (id.is_a? Integer)
541
- if id >= 0 and id <= @last_index
542
- # The id supplied is a valid position, return its corresponding value:
543
- return @values[id]
544
- else
545
- return false
546
- end
547
- else
548
- return false
549
- end
550
- else
551
- # We have a valid position, return the value:
552
- return @values[pos]
553
- end
554
- end
555
- end
556
-
557
-
558
- # Returns the raw data of the DICOM tag that matches the supplied tag ID.
559
- # The ID may be a tag index, tag name or tag label.
560
- def get_raw(id)
561
- if id == nil
562
- add_msg("A tag label, category name or index number must be specified when calling the get_raw() method!")
563
- return false
564
- else
565
- # Assume we have been fed a tag label:
566
- pos=@labels.index(id)
567
- # If this does not give a hit, assume we have been fed a tag name:
568
- if pos==nil
569
- pos=@names.index(id)
570
- end
571
- # If we still dont have a hit, check if it is a valid number within the array range:
572
- if pos == nil
573
- if (id.is_a? Integer)
574
- if id >= 0 and id <= @last_index
575
- # The id supplied is a valid position, return its corresponding value:
576
- return @raw[id]
577
- else
578
- return false
579
- end
580
- else
581
- return false
582
- end
583
- else
584
- # We have a valid position, return the value:
585
- return @raw[pos]
586
- end
587
- end
588
- end
589
-
725
+ end # of labels.each do |i|
726
+ end # of method print
727
+
590
728
 
591
729
  # Prints the key structural properties of the DICOM file.
592
730
  def print_properties()
@@ -626,7 +764,7 @@ module DICOM
626
764
  puts "Key properties of DICOM object:"
627
765
  puts "-------------------------------"
628
766
  puts "File: " + @file
629
- puts "Modality: " + @modality
767
+ puts "Modality: " + @modality.to_s
630
768
  puts "Value repr.: " + explicit
631
769
  puts "Byte order: " + endian
632
770
  puts "Pixel data: " + pixels
@@ -636,23 +774,360 @@ module DICOM
636
774
  puts "Bits per pixel: " + bits
637
775
  end
638
776
  puts "-------------------------------"
777
+ end # of method print_properties
778
+
779
+
780
+ ####################################################
781
+ ### START OF METHODS FOR WRITING INFORMATION TO THE DICOM OBJECT:#
782
+ ####################################################
783
+
784
+
785
+ # Reads binary information from file and inserts it in the pixel data tag:
786
+ def set_image_file(file)
787
+ # Try to read file:
788
+ begin
789
+ f = File.new(file, "rb")
790
+ bin = f.read(f.stat.size)
791
+ rescue
792
+ # Reading file was not successful. Register an error message.
793
+ add_msg("Reading specified file was not successful for some reason. No data has been added.")
794
+ end
795
+ if bin.length > 0
796
+ pos = @labels.index("7FE0,0010")
797
+ # Modify tag:
798
+ set_value(bin, :label => "7FE0,0010", :create => true, :bin => true)
799
+ else
800
+ add_msg("Content of file is of zero length. Nothing to store.")
801
+ end # of if bin.length > 0
802
+ end # of method set_image_file
803
+
804
+
805
+ # Transfers pixel data from a RMagick object to the pixel data tag:
806
+ def set_image_magick(magick_obj)
807
+ # Export the RMagick object to a standard Ruby array of numbers:
808
+ pixel_array = magick_obj.export_pixels(x=0, y=0, columns=magick_obj.columns, rows=magick_obj.rows, map="I")
809
+ # Encode this array using the standard class method:
810
+ set_value(pixel_array, :label => "7FE0,0010", :create => true)
811
+ end
812
+
813
+
814
+ # Removes a tag from the DICOM object:
815
+ def remove_tag(tag)
816
+ pos = get_pos(tag)
817
+ if pos != nil
818
+ # Extract first array number:
819
+ pos = pos[0]
820
+ # Update group length:
821
+ if @labels[pos][5..8] != "0000"
822
+ change = @lengths[pos]
823
+ vr = @types[pos]
824
+ update_group_length(pos, vr, change, -1)
825
+ end
826
+ # Remove entry from arrays:
827
+ @labels.delete_at(pos)
828
+ @levels.delete_at(pos)
829
+ @names.delete_at(pos)
830
+ @types.delete_at(pos)
831
+ @lengths.delete_at(pos)
832
+ @values.delete_at(pos)
833
+ @raw.delete_at(pos)
834
+ else
835
+ add_msg("Tag #{tag} not found in DICOM object.")
836
+ end
639
837
  end
838
+
839
+
840
+ # Sets the value of a tag, by modifying an existing tag or creating a new tag.
841
+ # If the supplied value is not binary, it will attempt to encode it to binary itself.
842
+ def set_value(value, opts={})
843
+ # Options:
844
+ label = opts[:label]
845
+ pos = opts[:pos]
846
+ create = opts[:create] # =false means no tag creation
847
+ bin = opts[:bin] # =true means value already encoded
848
+ # Abort if neither label nor position has been specified:
849
+ if label == nil and pos == nil
850
+ add_msg("Valid position not provided; can not modify or create tag. Please use keyword :pos or :label to specify.")
851
+ return
852
+ end
853
+ # If position is specified, check that it is valid.
854
+ # If label is specified, check that it doesnt correspond to multiple labels:
855
+ if pos != nil
856
+ unless pos >= 0 and pos <= @labels.length
857
+ # This is not a valid position:
858
+ pos = nil
859
+ end
860
+ else
861
+ pos = get_pos(label)
862
+ if pos == false
863
+ pos = nil
864
+ elsif pos.size > 1
865
+ pos = 'abort'
866
+ add_msg("The supplied label is found at multiple locations in the DICOM object. Will not update.")
867
+ end
868
+ end # of if pos != nil
869
+ # Create or modify?
870
+ if create == false
871
+ # User wants modification only. Proceed only if we have a valid position:
872
+ unless pos == nil
873
+ # Modify tag:
874
+ modify_tag(value, :bin => bin, :pos => pos)
875
+ end
876
+ else
877
+ # User wants to create (or modify if present). Only abort if multiple hits have been found.
878
+ unless pos == 'abort'
879
+ if pos == nil
880
+ # As we wish to create new tag, we need to find out where to insert it in the tag array:
881
+ # We will do this by finding the last array position of the last tag that will stay in front of this tag.
882
+ if @labels.size > 0
883
+ # Search the array:
884
+ index = -1
885
+ quit = false
886
+ while quit != true do
887
+ if index+1 >= @labels.length # We have reached end of array.
888
+ quit = true
889
+ #elsif index+1 == @labels.length
890
+ #quit = true
891
+ elsif label < @labels[index+1] # We are past the correct position.
892
+ quit = true
893
+ else # Increase index in anticipation of a 'hit'.
894
+ index += 1
895
+ end
896
+ end # of while
897
+ else
898
+ # We are dealing with an empty DICOM object:
899
+ index = nil
900
+ end
901
+ # Before we allow tag creation, do a simple check that the label seems valid:
902
+ if label.length == 9
903
+ # Create new tag:
904
+ create_tag(value, :bin => bin, :label => label, :lastpos => index)
905
+ else
906
+ # Label did not pass our check:
907
+ add_msg("The label you specified (#{label}) does not seem valid. Please use the format 'GGGG,EEEE'.")
908
+ end
909
+ else
910
+ # Modify existing:
911
+ modify_tag(value, :bin => bin, :pos => pos)
912
+ end
913
+ end
914
+ end # of if create == false
915
+ end # of method set_value
640
916
 
641
917
 
642
- # ***** PS: Following methods are private! *****
918
+ ##################################################
919
+ ############## START OF PRIVATE METHODS:################
920
+ ##################################################
643
921
  private
644
922
 
645
923
 
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"
651
- else
652
- modality = @lib.get_uid(value.rstrip)
653
- @modality = modality
924
+ # Adds a warning or error message to the instance array holding messages, and if verbose variable is true, prints the message as well.
925
+ def add_msg(msg)
926
+ if @verbose
927
+ puts msg
928
+ end
929
+ if (msg.is_a? String)
930
+ msg=[msg]
654
931
  end
932
+ @msg += msg
655
933
  end
934
+
935
+
936
+ # Checks the endianness of the system. Returns false if little endian, true if big endian.
937
+ def check_sys_endian()
938
+ x = 0xdeadbeef
939
+ endian_type = {
940
+ Array(x).pack("V*") => false, #:little
941
+ Array(x).pack("N*") => true #:big
942
+ }
943
+ return endian_type[Array(x).pack("L*")]
944
+ end
945
+
946
+
947
+ # Creates a new tag:
948
+ def create_tag(value, opts={})
949
+ bin_only = opts[:bin]
950
+ label = opts[:label]
951
+ lastpos = opts[:lastpos]
952
+ # Fetch the VR:
953
+ info = @lib.get_name_vr(label)
954
+ vr = info[1]
955
+ name = info[0]
956
+ # Encode binary (if a binary is not provided):
957
+ if bin_only == true
958
+ # Data already encoded.
959
+ bin = value
960
+ value = nil
961
+ else
962
+ if vr != "UN"
963
+ # Encode:
964
+ bin = encode(value, vr)
965
+ else
966
+ add_msg("Error. Unable to encode tag value of unknown type!")
967
+ end
968
+ end
969
+ # Put this tag information into the arrays:
970
+ if bin
971
+ #if bin.length > 0
972
+ # 4 different scenarios: Array is empty, or: tag is put in front, inside array, or at end of array:
973
+ if lastpos == nil
974
+ # We have empty DICOM object:
975
+ @labels = [label]
976
+ @levels = [0]
977
+ @names = [name]
978
+ @types = [vr]
979
+ @lengths = [bin.length]
980
+ @values = [value]
981
+ @raw = [bin]
982
+ elsif lastpos == -1
983
+ # Insert in front of arrays:
984
+ @labels = [label] + @labels
985
+ @levels = [0] + @levels # NB! No support for hierarchy at this time!
986
+ @names = [name] + @names
987
+ @types = [vr] + @types
988
+ @lengths = [bin.length] + @lengths
989
+ @values = [value] + @values
990
+ @raw = [bin] + @raw
991
+ elsif lastpos == @labels.length-1
992
+ # Insert at end arrays:
993
+ @labels = @labels + [label]
994
+ @levels = @levels + [0] # Defaulting to level = 0
995
+ @names = @names + [name]
996
+ @types = @types + [vr]
997
+ @lengths = @lengths + [bin.length]
998
+ @values = @values + [value]
999
+ @raw = @raw + [bin]
1000
+ else
1001
+ # Insert somewhere inside the array:
1002
+ @labels = @labels[0..lastpos] + [label] + @labels[(lastpos+1)..(@labels.length-1)]
1003
+ @levels = @levels[0..lastpos] + [0] + @levels[(lastpos+1)..(@levels.length-1)] # Defaulting to level = 0
1004
+ @names = @names[0..lastpos] + [name] + @names[(lastpos+1)..(@names.length-1)]
1005
+ @types = @types[0..lastpos] + [vr] + @types[(lastpos+1)..(@types.length-1)]
1006
+ @lengths = @lengths[0..lastpos] + [bin.length] + @lengths[(lastpos+1)..(@lengths.length-1)]
1007
+ @values = @values[0..lastpos] + [value] + @values[(lastpos+1)..(@values.length-1)]
1008
+ @raw = @raw[0..lastpos] + [bin] + @raw[(lastpos+1)..(@raw.length-1)]
1009
+ end
1010
+ # Update last index variable as we have added to our arrays:
1011
+ @last_index += 1
1012
+ # Update group length (as long as it was not a group length tag that was created):
1013
+ pos = @labels.index(label)
1014
+ if @labels[pos][5..8] != "0000"
1015
+ change = bin.length
1016
+ update_group_length(pos, vr, change, 1)
1017
+ end
1018
+ #else
1019
+ #add_msg("Binary does not have a positive length, nothing to save.")
1020
+ #end # of if bin.length > 0
1021
+ else
1022
+ add_msg("Binary is nil. Nothing to save.")
1023
+ end
1024
+ end # of method create_tag
1025
+
1026
+
1027
+ # Encodes a value to binary (used for inserting values to a DICOM object).
1028
+ def encode(value, vr)
1029
+ # Our value needs to be inside an array to be encoded:
1030
+ value = [value] if not value.is_a?(Array)
1031
+ # VR will decide how to encode this value:
1032
+ case vr
1033
+ when "UL"
1034
+ bin = value.pack(@ul)
1035
+ when "SL"
1036
+ bin = value.pack(@sl)
1037
+ when "US"
1038
+ bin = value.pack(@us)
1039
+ when "SS"
1040
+ bin = value.pack(@ss)
1041
+ when "FL"
1042
+ bin = value.pack(@fs)
1043
+ when "FD"
1044
+ bin = value.pack(@fd)
1045
+ when "AT" # (tag label - assumes it has the format GGGGEEEE (no comma separation))
1046
+ # Encode letter pairs indexes in following order 10 3 2:
1047
+ # NB! This may not be encoded correctly on Big Endian files or computers.
1048
+ old_format=value[0]
1049
+ new_format = old_format[2..3]+old_format[0..1]+old_format[6..7]+old_format[4..5]
1050
+ bin = [new_format].pack("H*")
1051
+
1052
+ # We have a number of VRs that are encoded as string:
1053
+ when 'AE','AS','CS','DA','DS','DT','IS','LO','LT','PN','SH','ST','TM','UI','UT'
1054
+ # Odd/even test (num[0]=1 if num is odd):
1055
+ if value[0].length[0] == 1
1056
+ # Odd (add a zero byte):
1057
+ bin = value.pack('a*') + ["00"].pack("H*")
1058
+ else
1059
+ # Even:
1060
+ bin = value.pack('a*')
1061
+ end
1062
+ # Image related VR's:
1063
+ when "OW"
1064
+ # What bit depth to use when encoding the pixel data?
1065
+ bit_depth = get_value("0028,0100")
1066
+ if bit_depth == false
1067
+ # Tag not specified:
1068
+ add_msg("Attempted to encode pixel data, but bit depth tag is missing (0028,0100).")
1069
+ else
1070
+ # 8,12 or 16 bits?
1071
+ case bit_depth
1072
+ when 8
1073
+ bin = value.pack(@by)
1074
+ when 12
1075
+ # 12 bit not supported yet!
1076
+ add_msg("Encoding 12 bit pixel values not supported yet. Please change the bit depth to 8 or 16 bits.")
1077
+ when 16
1078
+ bin = value.pack(@us)
1079
+ else
1080
+ # Unknown bit depth:
1081
+ add_msg("Unknown bit depth #{bit_depth}. No data encoded.")
1082
+ end # of case bit_depth
1083
+ end # of if bit_depth..else..
1084
+ else # Unsupported VR:
1085
+ add_msg("Tag type #{vr} does not have a dedicated encoding option assigned. Please contact author.")
1086
+ end # of case vr
1087
+ return bin
1088
+ end # of method encode
1089
+
1090
+ # Modifies existing tag:
1091
+ def modify_tag(value, opts={})
1092
+ bin_only = opts[:bin]
1093
+ pos = opts[:pos]
1094
+ pos = pos[0] if pos.is_a?(Array)
1095
+ # Fetch the VR and old length:
1096
+ vr = @types[pos]
1097
+ old_length = @lengths[pos]
1098
+ # Encode binary (if a binary is not provided):
1099
+ if bin_only == true
1100
+ # Data already encoded.
1101
+ bin = value
1102
+ value = nil
1103
+ else
1104
+ if vr != "UN"
1105
+ # Encode:
1106
+ bin = encode(value, vr)
1107
+ else
1108
+ add_msg("Error. Unable to encode tag value of unknown type!")
1109
+ end
1110
+ end
1111
+ # Update the arrays with this new information:
1112
+ if bin
1113
+ #if bin.length > 0
1114
+ # Replace array entries for this tag:
1115
+ #@types[pos] = vr # for the time being there is no logic for updating type.
1116
+ @lengths[pos] = bin.length
1117
+ @values[pos] = value
1118
+ @raw[pos] = bin
1119
+ # Update group length (as long as it was not the group length that was modified):
1120
+ if @labels[pos][5..8] != "0000"
1121
+ change = bin.length - old_length
1122
+ update_group_length(pos, vr, change, 0)
1123
+ end
1124
+ #else
1125
+ #add_msg("Binary does not have a positive length, nothing to save.")
1126
+ #end
1127
+ else
1128
+ add_msg("Binary is nil. Nothing to save.")
1129
+ end
1130
+ end # of method modify_tag
656
1131
 
657
1132
 
658
1133
  # Prints the selected tags to an ascii text file.
@@ -673,7 +1148,99 @@ module DICOM
673
1148
  puts tag
674
1149
  end
675
1150
  end
1151
+
1152
+
1153
+ # Sets the modality variable of the current DICOM object, by querying the library with the object's SOP Class UID.
1154
+ def set_modality()
1155
+ value = get_value("0008,0016")
1156
+ if value == false
1157
+ @modality = "Not specified"
1158
+ else
1159
+ modality = @lib.get_uid(value.rstrip)
1160
+ @modality = modality
1161
+ end
1162
+ end
1163
+
1164
+
1165
+ # Sets the format strings that will be used for packing/unpacking numbers depending on endianness of file/system.
1166
+ def set_format_strings(file_endian=@file_endian)
1167
+ if @file_endian == @sys_endian
1168
+ # System endian equals file endian:
1169
+ # Native byte order.
1170
+ @by = "C*" # Byte (1 byte)
1171
+ @us = "S*" # Unsigned short (2 bytes)
1172
+ @ss = "s*" # Signed short (2 bytes)
1173
+ @ul = "I*" # Unsigned long (4 bytes)
1174
+ @sl = "l*" # Signed long (4 bytes)
1175
+ @fs = "e*" # Floating point single (4 bytes)
1176
+ @fd = "E*" # Floating point double ( 8 bytes)
1177
+ else
1178
+ # System endian not equal to file endian:
1179
+ # Network byte order.
1180
+ @by = "C*"
1181
+ @us = "n*"
1182
+ @ss = "n*" # Not correct (gives US)
1183
+ @ul = "N*"
1184
+ @sl = "N*" # Not correct (gives UL)
1185
+ @fs = "g*"
1186
+ @fd = "G*"
1187
+ end
1188
+ end
1189
+
1190
+
1191
+ # Updates the group length value when a tag has been updated, created or removed:
1192
+ # Variable change holds the change in value length for the updated tag.
1193
+ # (Change should be positive when a tag is removed - it will only be negative when editing a tag to a shorter value)
1194
+ # Variable existance is -1 if tag has been removed, +1 if tag has been added and 0 if it has been updated.
1195
+ # (Perhaps in the future this functionality might be moved to the DWrite class, it might give an easier implementation)
1196
+ def update_group_length(pos, type, change, existance)
1197
+ # Find position of relevant group length (if it exists):
1198
+ gl_pos = @labels.index(@labels[pos][0..4] + "0000")
1199
+ existance = 0 if existance == nil
1200
+ # If it exists, calculate change:
1201
+ if gl_pos
1202
+ if existance == 0
1203
+ # Tag has only been updated, so we only need to think about value change:
1204
+ value = @values[gl_pos] + change
1205
+ else
1206
+ # Tag has either been created or removed. This means we need to calculate the length of its other parts.
1207
+ if @explicit
1208
+ # In the explicit scenario it is slightly complex to determine this value:
1209
+ tag_length = 0
1210
+ # VR?:
1211
+ unless @labels[pos] == "FFFE,E000" or @labels[pos] == "FFFE,E00D" or @labels[pos] == "FFFE,E0DD"
1212
+ tag_length += 2
1213
+ end
1214
+ # Length value:
1215
+ case @types[pos]
1216
+ when "OB","OW","SQ","UN"
1217
+ if pos > @labels.index("7FE0,0010").to_i and @labels.index("7FE0,0010").to_i != 0
1218
+ tag_length += 4
1219
+ else
1220
+ tag_length += 6
1221
+ end
1222
+ when "()"
1223
+ tag_length += 4
1224
+ else
1225
+ tag_length += 2
1226
+ end # of case
1227
+ else
1228
+ # In the implicit scenario it is easier:
1229
+ tag_length = 4
1230
+ end
1231
+ # Update group length for creation/deletion scenario:
1232
+ change = (4 + tag_length + change) * existance
1233
+ value = @values[gl_pos] + change
1234
+ end
1235
+ # Write the new Group Length value:
1236
+ # Encode the new value to binary:
1237
+ bin = encode(value, "UL")
1238
+ # Update arrays:
1239
+ @values[gl_pos] = value
1240
+ @raw[gl_pos] = bin
1241
+ end # of if gl_pos
1242
+ end # of method update_group_length
676
1243
 
677
1244
 
678
- end # End of class.
679
- end # End of module.
1245
+ end # End of class
1246
+ end # End of module