dicom 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,615 @@
1
+ # Copyright 2008-2010 Christoffer Lervag
2
+
3
+ module DICOM
4
+
5
+ # Super class which contains common code for all parent elements.
6
+ #
7
+ # === Inheritance
8
+ #
9
+ # Since all parent elements inherit from this class, these methods are available to instances of the following classes:
10
+ # * DObject
11
+ # * Item
12
+ # * Sequence
13
+ #
14
+ class SuperParent
15
+
16
+ # Returns the specified child element.
17
+ # If the requested data element isn't found, nil is returned.
18
+ #
19
+ # === Notes
20
+ #
21
+ # * Only immediate children are searched. Grandchildren etc. are not included.
22
+ #
23
+ # === Parameters
24
+ #
25
+ # * <tt>tag</tt> -- A tag string which identifies the data element to be returned (Exception: In the case where an Item is wanted, an index (Fixnum) is used instead).
26
+ #
27
+ # === Examples
28
+ #
29
+ # # Extract the "Pixel Data" data element from the DObject instance:
30
+ # pixel_data_element = obj["7FE0,0010"]
31
+ # # Extract the first Item from a Sequence:
32
+ # first_item = obj["3006,0020"][1]
33
+ #
34
+ def [](tag)
35
+ return @tags[tag]
36
+ end
37
+
38
+ # Adds a DataElement or Sequence instance to self (where self can be either a DObject or Item instance).
39
+ #
40
+ # === Restrictions
41
+ #
42
+ # * Items can not be added with this method.
43
+ #
44
+ # === Parameters
45
+ #
46
+ # * <tt>element</tt> -- An element (DataElement or Sequence).
47
+ #
48
+ # === Examples
49
+ #
50
+ # # Set a new patient's name to the DICOM object:
51
+ # obj.add(DataElement.new("0010,0010", "John_Doe"))
52
+ # # Add a previously defined element roi_name to the first item in the following sequence:
53
+ # obj["3006,0020"][1].add(roi_name)
54
+ #
55
+ def add(element)
56
+ unless element.is_a?(Item)
57
+ unless self.is_a?(Sequence)
58
+ # If we are replacing an existing Element, we need to make sure that this Element's parent value is erased before proceeding.
59
+ self[element.tag].parent = nil if exists?(element.tag)
60
+ # Add the element, and set its parent attribute:
61
+ @tags[element.tag] = element
62
+ element.parent = self
63
+ else
64
+ raise "A Sequence is not allowed to have elements added to it. Use the method add_item() instead if the intention is to add an Item."
65
+ end
66
+ else
67
+ raise "An Item is not allowed as a parameter to the add() method. Use add_item() instead."
68
+ end
69
+ end
70
+
71
+ # Adds a child item to a Sequence (or Item in some cases where pixel data is encapsulated).
72
+ # If no existing Item is specified, an empty item will be added.
73
+ #
74
+ # === Notes
75
+ # * Items are specified by index (starting at 1) instead of a tag string!
76
+ #
77
+ # === Parameters
78
+ #
79
+ # * <tt>item</tt> -- The Item instance that is to be added (defaults to nil, in which case an empty Item will be added).
80
+ # * <tt>options</tt> -- A hash of parameters.
81
+ #
82
+ # === Options
83
+ #
84
+ # * <tt>:index</tt> -- Fixnum. If the Item is to be inserted at a specific index (Item number), this option parameter needs to set.
85
+ #
86
+ # === Examples
87
+ #
88
+ # # Add an empty Item to a specific Sequence:
89
+ # obj["3006,0020"].add_item
90
+ # # Add an existing Item at the 2nd item position/index in the specific Sequence:
91
+ # obj["3006,0020"].add_item(my_item, :index => 2)
92
+ #
93
+ def add_item(item=nil, options={})
94
+ unless self.is_a?(DObject)
95
+ if item
96
+ if item.is_a?(Item)
97
+ if options[:index]
98
+ # This Item will take a specific index, and all existing Items with index higher or equal to this number will have their index increased by one.
99
+ # Check if index is valid (must be an existing index):
100
+ if options[:index] >= 1
101
+ # If the index value is larger than the max index present, we dont need to modify the existing items.
102
+ unless options[:index] > @tags.length
103
+ # Extract existing Hash entries to an array:
104
+ pairs = @tags.sort
105
+ @tags = Hash.new
106
+ # Change the key of those equal or larger than index and put these key,value pairs back in a new Hash:
107
+ pairs.each do |pair|
108
+ if pair[0] < options[:index]
109
+ @tags[pair[0]] = pair[1] # (Item keeps its old index)
110
+ else
111
+ @tags[pair[0]+1] = pair[1]
112
+ pair[1].index = pair[0]+1 # (Item gets updated with its new index)
113
+ end
114
+ end
115
+ else
116
+ # Set the index value one higher than the already existing max value:
117
+ options[:index] = @tags.length + 1
118
+ end
119
+ #,Add the new Item and set its index:
120
+ @tags[options[:index]] = item
121
+ item.index = options[:index]
122
+ else
123
+ raise "The specified index (#{options[:index]}) is out of range (Minimum allowed index value is 1)."
124
+ end
125
+ else
126
+ # Add the existing Item to this Sequence:
127
+ index = @tags.length + 1
128
+ @tags[index] = item
129
+ # Let the Item know what index key it's got in it's parent's Hash:
130
+ item.index = index
131
+ end
132
+ else
133
+ raise "The specified parameter is not an Item. Only Items are allowed to be added to a Sequence."
134
+ end
135
+ else
136
+ # Create an empty Item with self as parent.
137
+ index = @tags.length + 1
138
+ item = Item.new(:parent => self)
139
+ end
140
+ else
141
+ raise "An Item #{item} was attempted added to a DObject instance #{self}, which is not allowed."
142
+ end
143
+ end
144
+
145
+ # Returns all (immediate) child elements in an array (sorted by element tag).
146
+ # If this particular parent doesn't have any children, an empty array is returned
147
+ #
148
+ # === Examples
149
+ #
150
+ # # Retrieve all top level data elements in a DICOM object:
151
+ # top_level_elements = obj.children
152
+ #
153
+ def children
154
+ return @tags.sort.transpose[1] || Array.new
155
+ end
156
+
157
+ # Checks if an element actually has any child elements.
158
+ # Returns true if it has and false if it doesn't.
159
+ #
160
+ # === Notes
161
+ #
162
+ # Notice the subtle difference between the children? and is_parent? methods. While they
163
+ # will give the same result in most real use cases, they differ when used on parent elements
164
+ # that do not have any children added yet.
165
+ #
166
+ # For example, when called on an empty Sequence, the children? method will return false,
167
+ # while the is_parent? method still returns true.
168
+ #
169
+ def children?
170
+ if @tags.length > 0
171
+ return true
172
+ else
173
+ return false
174
+ end
175
+ end
176
+
177
+ # Counts and returns the number of elements contained directly in this parent.
178
+ # This count does NOT include the number of elements contained in any possible child elements.
179
+ #
180
+ def count
181
+ return @tags.length
182
+ end
183
+
184
+ # Counts and returns the total number of elements contained in this parent.
185
+ # This count includes all the elements contained in any possible child elements.
186
+ #
187
+ def count_all
188
+ # Iterate over all elements, and repeat recursively for all elements which themselves contain children.
189
+ total_count = count
190
+ @tags.each_value do |value|
191
+ total_count += value.count_all if value.children?
192
+ end
193
+ return total_count
194
+ end
195
+
196
+ # Re-encodes the binary data strings of all child DataElement instances.
197
+ # This also includes all the elements contained in any possible child elements.
198
+ #
199
+ # === Notes
200
+ #
201
+ # This method is not intended for external use, but for technical reasons (the fact that is called between
202
+ # instances of different classes), cannot be made private.
203
+ #
204
+ # === Parameters
205
+ #
206
+ # * <tt>old_endian</tt> -- The previous endianness of the elements/DObject instance (used for decoding values from binary).
207
+ #
208
+ def encode_children(old_endian)
209
+ # Cycle through all levels of children recursively:
210
+ children.each do |element|
211
+ if element.children?
212
+ element.encode_children(old_endian)
213
+ elsif element.is_a?(DataElement)
214
+ encode_child(element, old_endian)
215
+ end
216
+ end
217
+ end
218
+
219
+ # Checks whether a specific data element tag is defined for this parent.
220
+ # Returns true if the tag is found and false if not.
221
+ #
222
+ # === Parameters
223
+ #
224
+ # * <tt>tag</tt> -- A tag string which identifies the data element that is queried (Exception: In the case of an Item query, an index (Fixnum) is used instead).
225
+ #
226
+ # === Examples
227
+ #
228
+ # process_name(obj["0010,0010"]) if obj.exists?("0010,0010")
229
+ #
230
+ def exists?(tag)
231
+ if @tags[tag]
232
+ return true
233
+ else
234
+ return false
235
+ end
236
+ end
237
+
238
+ # Returns an array of all child elements that belongs to the specified group.
239
+ # If no matches are found, returns an empty array.
240
+ #
241
+ # === Parameters
242
+ #
243
+ # * <tt>group_string</tt> -- A group string (the first 4 characters of a tag string).
244
+ #
245
+ def group(group_string)
246
+ found = Array.new
247
+ children.each do |child|
248
+ found << child if child.tag.group == group_string
249
+ end
250
+ return found
251
+ end
252
+
253
+ # Gathers the desired information from the selected data elements and processes this information to make
254
+ # a text output which is nicely formatted. Returns a text array and an index of the last data element.
255
+ #
256
+ # === Notes
257
+ #
258
+ # This method is not intended for external use, but for technical reasons (the fact that is called between
259
+ # instances of different classes), cannot be made private.
260
+ #
261
+ # The method is used by the print() method to construct the text output.
262
+ #
263
+ # === Parameters
264
+ #
265
+ # * <tt>index</tt> -- Fixnum. The index which is given to the first child of this parent.
266
+ # * <tt>max_digits</tt> -- Fixnum. The maximum number of digits in the index of an element (which is the index of the last element).
267
+ # * <tt>max_name</tt> -- Fixnum. The maximum number of characters in the name of any element to be printed.
268
+ # * <tt>max_length</tt> -- Fixnum. The maximum number of digits in the length of an element.
269
+ # * <tt>max_generations</tt> -- Fixnum. The maximum number of generations of children for this parent.
270
+ # * <tt>visualization</tt> -- An array of string symbols which visualizes the tree structure that the children of this particular parent belongs to. For no visualization, an empty array is passed.
271
+ # * <tt>options</tt> -- A hash of parameters.
272
+ #
273
+ # === Options
274
+ #
275
+ # * <tt>:value_max</tt> -- Fixnum. If a value max length is specified, the data elements who's value exceeds this length will be trimmed to this length.
276
+ #
277
+ #--
278
+ # FIXME: This method is somewhat complex, and some simplification, if possible, wouldn't hurt.
279
+ #
280
+ def handle_print(index, max_digits, max_name, max_length, max_generations, visualization, options={})
281
+ elements = Array.new
282
+ s = " "
283
+ hook_symbol = "|_"
284
+ last_item_symbol = " "
285
+ nonlast_item_symbol = "| "
286
+ children.each_with_index do |element, i|
287
+ n_parents = element.parents.length
288
+ # Formatting: Index
289
+ i_s = s*(max_digits-(index).to_s.length)
290
+ # Formatting: Name (and Tag)
291
+ if element.tag == ITEM_TAG
292
+ # Add index numbers to the Item names:
293
+ name = "#{element.name} (\##{i+1})"
294
+ else
295
+ name = element.name
296
+ end
297
+ n_s = s*(max_name-name.length)
298
+ # Formatting: Tag
299
+ tag = "#{visualization.join}#{element.tag}"
300
+ t_s = s*((max_generations-1)*2+9-tag.length)
301
+ # Formatting: Length
302
+ l_s = s*(max_length-element.length.to_s.length)
303
+ # Formatting Value:
304
+ if element.is_a?(DataElement)
305
+ value = element.value.to_s
306
+ else
307
+ value = ""
308
+ end
309
+ if options[:value_max]
310
+ value = "#{value[0..(options[:value_max]-3)]}.." if value.length > options[:value_max]
311
+ end
312
+ elements << "#{i_s}#{index} #{tag}#{t_s} #{name}#{n_s} #{element.vr} #{l_s}#{element.length} #{value}"
313
+ index += 1
314
+ # If we have child elements, print those elements recursively:
315
+ if element.children?
316
+ if n_parents > 1
317
+ child_visualization = Array.new
318
+ child_visualization.replace(visualization)
319
+ if element == children.first
320
+ if children.length == 1
321
+ # Last item:
322
+ child_visualization.insert(n_parents-2, last_item_symbol)
323
+ else
324
+ # More items follows:
325
+ child_visualization.insert(n_parents-2, nonlast_item_symbol)
326
+ end
327
+ elsif element == children.last
328
+ # Last item:
329
+ child_visualization[n_parents-2] = last_item_symbol
330
+ child_visualization.insert(-1, hook_symbol)
331
+ else
332
+ # Neither first nor last (more items follows):
333
+ child_visualization.insert(n_parents-2, nonlast_item_symbol)
334
+ end
335
+ elsif n_parents == 1
336
+ child_visualization = Array.new(1, hook_symbol)
337
+ else
338
+ child_visualization = Array.new
339
+ end
340
+ new_elements, index = element.handle_print(index, max_digits, max_name, max_length, max_generations, child_visualization, options)
341
+ elements << new_elements
342
+ end
343
+ end
344
+ return elements.flatten, index
345
+ end
346
+
347
+ # Checks if an element is a parent.
348
+ # Returns true for all parent elements.
349
+ #
350
+ def is_parent?
351
+ return true
352
+ end
353
+
354
+ # Sets the length of a Sequence or Item.
355
+ #
356
+ # === Parameters
357
+ #
358
+ # * <tt>new_length</tt> -- Fixnum. The new length to assign to the Sequence/Item.
359
+ #
360
+ def length=(new_length)
361
+ unless self.is_a?(DObject)
362
+ @length = new_length
363
+ else
364
+ raise "Length can not be set for a DObject instance."
365
+ end
366
+ end
367
+
368
+ # Prints all child elements of this particular parent.
369
+ # Information such as tag, parent-child relationship, name, vr, length and value is gathered for each data element
370
+ # and processed to produce a nicely formatted output.
371
+ #
372
+ # === Parameters
373
+ #
374
+ # * <tt>options</tt> -- A hash of parameters.
375
+ #
376
+ # === Options
377
+ #
378
+ # * <tt>:value_max</tt> -- Fixnum. If a value max length is specified, the data elements who's value exceeds this length will be trimmed to this length.
379
+ # * <tt>:file</tt> -- String. If a file path is specified, the output will be printed to this file instead of being printed to the screen.
380
+ #
381
+ # === Examples
382
+ #
383
+ # # Print a DObject instance to screen
384
+ # obj.print
385
+ # # Print the obj to the screen, but specify a 25 character value cutoff to produce better-looking results:
386
+ # obj.print(:value_max => 25)
387
+ # # Print to a text file the elements that belong to a specific Sequence:
388
+ # obj["3006,0020"].print(:file => "dicom.txt")
389
+ #
390
+ #--
391
+ # FIXME: Perhaps a :children => false option would be a good idea (to avoid lengthy printouts in cases where this would be desirable)?
392
+ # FIXME: Speed. The new print algorithm may seem to be slower than the old one (observed on complex, hiearchical DICOM files). Perhaps it can be optimized?
393
+ #
394
+ def print(options={})
395
+ # We first gather some properties that is necessary to produce a nicely formatted printout (max_lengths, count_all),
396
+ # then the actual information is gathered (handle_print),
397
+ # and lastly, we pass this information on to the methods which print the output (print_file or print_screen).
398
+ if count > 0
399
+ max_name, max_length, max_generations = max_lengths
400
+ max_digits = count_all.to_s.length
401
+ visualization = Array.new
402
+ elements, index = handle_print(start_index=1, max_digits, max_name, max_length, max_generations, visualization, options)
403
+ if options[:file]
404
+ print_file(elements, options[:file])
405
+ else
406
+ print_screen(elements)
407
+ end
408
+ else
409
+ puts "Notice: Object #{self} is empty (contains no data elements)!"
410
+ end
411
+ end
412
+
413
+ # Finds and returns the maximum character lengths of name and length which occurs for any child element,
414
+ # as well as the maximum number of generations of elements.
415
+ #
416
+ # === Notes
417
+ #
418
+ # This method is not intended for external use, but for technical reasons (the fact that is called between
419
+ # instances of different classes), cannot be made private.
420
+ #
421
+ # The method is used by the print() method to achieve a proper format in its output.
422
+ #
423
+ def max_lengths
424
+ max_name = 0
425
+ max_length = 0
426
+ max_generations = 0
427
+ children.each do |element|
428
+ if element.children?
429
+ max_nc, max_lc, max_gc = element.max_lengths
430
+ max_name = max_nc if max_nc > max_name
431
+ max_length = max_lc if max_lc > max_length
432
+ max_generations = max_gc if max_gc > max_generations
433
+ end
434
+ n_length = element.name.length
435
+ l_length = element.length.to_s.length
436
+ generations = element.parents.length
437
+ max_name = n_length if n_length > max_name
438
+ max_length = l_length if l_length > max_length
439
+ max_generations = generations if generations > max_generations
440
+ end
441
+ return max_name, max_length, max_generations
442
+ end
443
+
444
+ # Removes the specified element from this parent.
445
+ #
446
+ # === Parameters
447
+ #
448
+ # * <tt>tag</tt> -- A tag string which specifies the element to be removed (Exception: In the case of an Item removal, an index (Fixnum) is used instead).
449
+ #
450
+ # === Examples
451
+ #
452
+ # # Remove a DataElement from a DObject instance:
453
+ # obj.remove("0008,0090")
454
+ # # Remove Item 1 from a specific Sequence:
455
+ # obj["3006,0020"].remove(1)
456
+ #
457
+ def remove(tag)
458
+ # We need to delete the specified child element's parent reference in addition to removing it from the tag Hash.
459
+ element = @tags[tag]
460
+ if element
461
+ element.parent = nil
462
+ @tags.delete(tag)
463
+ end
464
+ end
465
+
466
+ # Removes all data elements of the specified group from this parent.
467
+ #
468
+ # === Parameters
469
+ #
470
+ # * <tt>group_string</tt> -- A group string (the first 4 characters of a tag string).
471
+ #
472
+ # === Examples
473
+ #
474
+ # # Remove the File Meta Group of a DICOM object:
475
+ # obj.remove_group("0002")
476
+ #
477
+ def remove_group(group_string)
478
+ group_elements = group(group_string)
479
+ group_elements.each do |element|
480
+ remove(element.tag)
481
+ end
482
+ end
483
+
484
+ # Removes all private data elements from the child elements of this parent.
485
+ #
486
+ # === Examples
487
+ #
488
+ # # Remove all private elements from a DObject instance:
489
+ # obj.remove_private
490
+ # # Remove only private elements belonging to a specific Sequence:
491
+ # obj["3006,0020"].remove_private
492
+ #
493
+ def remove_private
494
+ # Iterate all children, and repeat recursively if a child itself has children, to remove all private data elements:
495
+ children.each do |element|
496
+ remove(element.tag) if element.tag.private?
497
+ element.remove_private if element.children?
498
+ end
499
+ end
500
+
501
+ # Resets the length of a Sequence or Item to -1, which is the number used for 'undefined' length.
502
+ #
503
+ def reset_length
504
+ unless self.is_a?(DObject)
505
+ @length = -1
506
+ @bin = ""
507
+ else
508
+ raise "Length can not be set for a DObject instance."
509
+ end
510
+ end
511
+
512
+ # Returns the value of a specific DataElement child of this parent.
513
+ # Returns nil if the child element does not exist.
514
+ #
515
+ # === Notes
516
+ #
517
+ # * Only DataElement instances have values. Parent elements like Sequence and Item have no value themselves.
518
+ # If the specified <tt>tag</tt> is that of a parent element, <tt>value()</tt> will raise an exception.
519
+ #
520
+ # === Parameters
521
+ #
522
+ # * <tt>tag</tt> -- A tag string which identifies the child DataElement.
523
+ #
524
+ # === Examples
525
+ #
526
+ # # Get the patient's name value:
527
+ # name = obj.value("0010,0010")
528
+ # # Get the Frame of Reference UID from the first item in the Referenced Frame of Reference Sequence:
529
+ # uid = obj["3006,0010"][1].value("0020,0052")
530
+ #
531
+ def value(tag)
532
+ if exists?(tag)
533
+ if @tags[tag].is_parent?
534
+ raise "Illegal parameter '#{tag}'. Parent elements, like the referenced '#{@tags[tag].class}', have no value. Only DataElement tags are valid."
535
+ else
536
+ return @tags[tag].value
537
+ end
538
+ else
539
+ return nil
540
+ end
541
+ end
542
+
543
+
544
+ # Following methods are private:
545
+ private
546
+
547
+
548
+ # Re-encodes the value of a child DataElement (but only if the DataElement encoding is
549
+ # influenced by a shift in endianness).
550
+ #
551
+ # === Parameters
552
+ #
553
+ # * <tt>element</tt> -- The DataElement who's value will be re-encoded.
554
+ # * <tt>old_endian</tt> -- The previous endianness of the element binary (used for decoding the value).
555
+ #
556
+ #--
557
+ # FIXME: Tag with VR AT has no re-encoding yet..
558
+ #
559
+ def encode_child(element, old_endian)
560
+ if element.tag == "7FE0,0010"
561
+ # As encoding settings of the DObject has already been changed, we need to decode the old pixel values with the old encoding:
562
+ stream_old_endian = Stream.new(nil, old_endian)
563
+ pixels = decode_pixels(element.bin, stream_old_endian)
564
+ encode_pixels(pixels, stream)
565
+ else
566
+ # Not all types of tags needs to be reencoded when switching endianness:
567
+ case element.vr
568
+ when "US", "SS", "UL", "SL", "FL", "FD", "OF", "OW" # Numbers
569
+ # Re-encode, as long as it is not a group 0002 element (which must always be little endian):
570
+ unless element.tag.group == "0002"
571
+ stream_old_endian = Stream.new(element.bin, old_endian)
572
+ numbers = stream_old_endian.decode(element.length, element.vr)
573
+ element.value = numbers
574
+ end
575
+ #when "AT" # Tag reference
576
+ end
577
+ end
578
+ end
579
+
580
+ # Initializes common variables among the parent elements.
581
+ #
582
+ def initialize_parent
583
+ # All child data elements and sequences are stored in a hash where the tag string is used as key:
584
+ @tags = Hash.new
585
+ end
586
+
587
+ # Prints an array of data element ascii text lines gathered by the print() method to file.
588
+ #
589
+ # === Parameters
590
+ #
591
+ # * <tt>elements</tt> -- An array of formatted data element lines.
592
+ # * <tt>file</tt> -- A path & file string.
593
+ #
594
+ def print_file(elements, file)
595
+ File.open(file, 'w') do |output|
596
+ elements.each do |line|
597
+ output.print line + "\n"
598
+ end
599
+ end
600
+ end
601
+
602
+ # Prints an array of data element ascii text lines gathered by the print() method to the screen.
603
+ #
604
+ # === Parameters
605
+ #
606
+ # * <tt>elements</tt> -- An array of formatted data element lines.
607
+ #
608
+ def print_screen(elements)
609
+ elements.each do |line|
610
+ puts line
611
+ end
612
+ end
613
+
614
+ end
615
+ end