dicom 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,27 @@ module DICOM
14
14
 
15
15
  include ImageProcessor
16
16
 
17
+ # Creates an Element with the given arguments and connects it to self.
18
+ #
19
+ # @param [String] tag an element tag
20
+ # @param [String, Integer, Float, Array, NilClass] value an element value
21
+ # @param [Hash] options any options used for creating the element (see Element.new documentation)
22
+ #
23
+ def add_element(tag, value, options={})
24
+ add(e = Element.new(tag, value, options))
25
+ e
26
+ end
27
+
28
+ # Creates a Sequence with the given arguments and connects it to self.
29
+ #
30
+ # @param [String] tag a sequence tag
31
+ # @param [Hash] options any options used for creating the sequence (see Sequence.new documentation)
32
+ #
33
+ def add_sequence(tag, options={})
34
+ add(s = Sequence.new(tag, options))
35
+ s
36
+ end
37
+
17
38
  # Checks if colored pixel data is present.
18
39
  #
19
40
  # @return [Boolean] true if the object contains colored pixels, and false if not
@@ -22,7 +43,7 @@ module DICOM
22
43
  # "Photometric Interpretation" is contained in the data element "0028,0004":
23
44
  begin
24
45
  photometric = photometry
25
- if photometric.include?("COLOR") or photometric.include?("RGB") or photometric.include?("YBR")
46
+ if photometric.include?('COLOR') or photometric.include?('RGB') or photometric.include?('YBR')
26
47
  return true
27
48
  else
28
49
  return false
@@ -56,8 +77,8 @@ module DICOM
56
77
  raise ArgumentError, "Expected String, got #{bin.class}." unless bin.is_a?(String)
57
78
  pixels = false
58
79
  # We need to know what kind of bith depth and integer type the pixel data is saved with:
59
- bit_depth_element = self["0028,0100"]
60
- pixel_representation_element = self["0028,0103"]
80
+ bit_depth_element = self['0028,0100']
81
+ pixel_representation_element = self['0028,0103']
61
82
  if bit_depth_element and pixel_representation_element
62
83
  # Load the binary pixel data to the Stream instance:
63
84
  stream.set_string(bin)
@@ -81,8 +102,8 @@ module DICOM
81
102
  raise ArgumentError, "Expected Array, got #{pixels.class}." unless pixels.is_a?(Array)
82
103
  bin = false
83
104
  # We need to know what kind of bith depth and integer type the pixel data is saved with:
84
- bit_depth_element = self["0028,0100"]
85
- pixel_representation_element = self["0028,0103"]
105
+ bit_depth_element = self['0028,0100']
106
+ pixel_representation_element = self['0028,0103']
86
107
  if bit_depth_element and pixel_representation_element
87
108
  template = template_string(bit_depth_element.value.to_i)
88
109
  bin = stream.encode(pixels, template) if template
@@ -187,7 +208,7 @@ module DICOM
187
208
  #
188
209
  def image_from_file(file)
189
210
  raise ArgumentError, "Expected #{String}, got #{file.class}." unless file.is_a?(String)
190
- f = File.new(file, "rb")
211
+ f = File.new(file, 'rb')
191
212
  bin = f.read(f.stat.size)
192
213
  if bin.length > 0
193
214
  # Write the binary data to the Pixel Data Element:
@@ -235,18 +256,18 @@ module DICOM
235
256
  parts = file.split('.')
236
257
  if parts.length > 1
237
258
  base = parts[0..-2].join
238
- extension = "." + parts.last
259
+ extension = '.' + parts.last
239
260
  else
240
261
  base = file
241
- extension = ""
262
+ extension = ''
242
263
  end
243
264
  # Get the binary image strings and dump them to the file(s):
244
265
  images = image_strings
245
266
  images.each_index do |i|
246
267
  if images.length == 1
247
- f = File.new(file, "wb")
268
+ f = File.new(file, 'wb')
248
269
  else
249
- f = File.new("#{base}-#{i}#{extension}", "wb")
270
+ f = File.new("#{base}-#{i}#{extension}", 'wb')
250
271
  end
251
272
  f.write(images[i])
252
273
  f.close
@@ -265,7 +286,7 @@ module DICOM
265
286
  # @param [MagickImage] image the image to be assigned to the pixel data element
266
287
  #
267
288
  def image=(image)
268
- raise ArgumentError, "Expected one of the supported image objects (#{valid_image_objects}), got #{image.class}." unless valid_image_objects.include?(image.class)
289
+ raise ArgumentError, "Expected one of the supported image classes: #{valid_image_objects} (got #{image.class})" unless valid_image_objects.include?(image.class.to_s)
269
290
  # Export to pixels using the proper image processor:
270
291
  pixels = export_pixels(image, photometry)
271
292
  # Encode and write to the Pixel Data Element:
@@ -277,7 +298,7 @@ module DICOM
277
298
  # @return [Integer, NilClass] the number of columns, or nil (if the columns value is undefined)
278
299
  #
279
300
  def num_cols
280
- self["0028,0011"].value rescue nil
301
+ self['0028,0011'].value rescue nil
281
302
  end
282
303
 
283
304
  # Gives the number of frames in the pixel data.
@@ -286,7 +307,7 @@ module DICOM
286
307
  # @return [Integer] the number of rows
287
308
  #
288
309
  def num_frames
289
- (self["0028,0008"].is_a?(Element) == true ? self["0028,0008"].value.to_i : 1)
310
+ (self['0028,0008'].is_a?(Element) == true ? self['0028,0008'].value.to_i : 1)
290
311
  end
291
312
 
292
313
  # Gives the number of rows in the pixel data.
@@ -294,7 +315,7 @@ module DICOM
294
315
  # @return [Integer, NilClass] the number of rows, or nil (if the rows value is undefined)
295
316
  #
296
317
  def num_rows
297
- self["0028,0010"].value rescue nil
318
+ self['0028,0010'].value rescue nil
298
319
  end
299
320
 
300
321
  # Creates an NArray containing the pixel data. If the pixel data is an image
@@ -402,22 +423,21 @@ module DICOM
402
423
  # @param [Array<Integer>, NArray] values an Array (or NArray) containing integer pixel values
403
424
  #
404
425
  def pixels=(values)
405
- raise ArgumentError, "Expected Array or NArray, got #{values.class}." unless [Array, NArray].include?(values.class)
406
- if values.is_a?(NArray)
407
- # When NArray, convert to a Ruby Array:
426
+ raise ArgumentError, "The given argument does not respond to #to_a (got an argument of class #{values.class})" unless values.respond_to?(:to_a)
427
+ if values.class.ancestors.to_s.include?('NArray')
428
+ # With an NArray argument, make sure that it gets properly converted to an Array:
408
429
  if values.shape.length > 2
409
- # We need to take care when reshaping this 3D array so that the pixel values falls properly into place:
430
+ # For a 3D NArray we need to rearrange to ensure that the pixels get their
431
+ # proper order when converting to an ordinary Array instance:
410
432
  narr = NArray.int(values.shape[1] * values.shape[2], values.shape[0])
411
433
  values.shape[0].times do |i|
412
434
  narr[true, i] = values[i, true, true].reshape(values.shape[1] * values.shape[2])
413
435
  end
414
- values = narr.to_a
415
- else
416
- values = values.to_a
436
+ values = narr
417
437
  end
418
438
  end
419
439
  # Encode the pixel data:
420
- bin = encode_pixels(values.flatten)
440
+ bin = encode_pixels(values.to_a.flatten)
421
441
  # Write the binary data to the Pixel Data Element:
422
442
  write_pixels(bin)
423
443
  end
@@ -444,7 +464,7 @@ module DICOM
444
464
  raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
445
465
  if photometry == PI_PALETTE_COLOR
446
466
  # Only one channel is checked and it is assumed that all channels have the same number of bits.
447
- return self["0028,1101"].value.split("\\").last.to_i
467
+ return self['0028,1101'].value.split("\\").last.to_i
448
468
  else
449
469
  return bit_depth
450
470
  end
@@ -458,7 +478,7 @@ module DICOM
458
478
  #
459
479
  def bit_depth
460
480
  raise "The 'Bits Allocated' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0100")
461
- return value("0028,0100")
481
+ return value('0028,0100')
462
482
  end
463
483
 
464
484
  # Performs a run length decoding on the input stream.
@@ -524,7 +544,7 @@ module DICOM
524
544
  #
525
545
  def photometry
526
546
  raise "The 'Photometric Interpretation' Element is missing from this DICOM instance. Unable to encode/decode pixel data." unless exists?("0028,0004")
527
- return value("0028,0004").upcase
547
+ return value('0028,0004').upcase
528
548
  end
529
549
 
530
550
  # Processes the pixel array based on attributes defined in the DICOM object,
@@ -539,16 +559,16 @@ module DICOM
539
559
  proper_rgb = false
540
560
  photometric = photometry()
541
561
  # (With RLE COLOR PALETTE the Planar Configuration is not set)
542
- planar = self["0028,0006"].is_a?(Element) ? self["0028,0006"].value : 0
562
+ planar = self['0028,0006'].is_a?(Element) ? self['0028,0006'].value : 0
543
563
  # Step 1: Produce an array with RGB values. At this time, YBR is not supported in ruby-dicom,
544
564
  # so this leaves us with a possible conversion from PALETTE COLOR:
545
- if photometric.include?("COLOR")
565
+ if photometric.include?('COLOR')
546
566
  # Pseudo colors (rgb values grabbed from a lookup table):
547
567
  rgb = Array.new(pixels.length*3)
548
568
  # Prepare the lookup data arrays:
549
- lookup_binaries = [self["0028,1201"].bin, self["0028,1202"].bin, self["0028,1203"].bin]
569
+ lookup_binaries = [self['0028,1201'].bin, self['0028,1202'].bin, self['0028,1203'].bin]
550
570
  lookup_values = Array.new
551
- nr_bits = self["0028,1101"].value.split("\\").last.to_i
571
+ nr_bits = self['0028,1101'].value.split("\\").last.to_i
552
572
  template = template_string(nr_bits)
553
573
  lookup_binaries.each do |bin|
554
574
  stream.set_string(bin)
@@ -561,7 +581,7 @@ module DICOM
561
581
  end
562
582
  # As we have now ordered the pixels in RGB order, modify planar configuration to reflect this:
563
583
  planar = 0
564
- elsif photometric.include?("YBR")
584
+ elsif photometric.include?('YBR')
565
585
  rgb = false
566
586
  else
567
587
  rgb = pixels
@@ -739,13 +759,13 @@ module DICOM
739
759
  #
740
760
  def signed_pixels?
741
761
  raise "The 'Pixel Representation' data element is missing from this DICOM instance. Unable to process pixel data." unless exists?("0028,0103")
742
- case value("0028,0103")
762
+ case value('0028,0103')
743
763
  when 1
744
764
  return true
745
765
  when 0
746
766
  return false
747
767
  else
748
- raise "Invalid value encountered (#{value("0028,0103")}) in the 'Pixel Representation' data element. Expected 0 or 1."
768
+ raise "Invalid value encountered (#{value('0028,0103')}) in the 'Pixel Representation' data element. Expected 0 or 1."
749
769
  end
750
770
  end
751
771
 
@@ -757,22 +777,22 @@ module DICOM
757
777
  #
758
778
  def template_string(depth)
759
779
  template = false
760
- pixel_representation = self["0028,0103"].value.to_i
780
+ pixel_representation = self['0028,0103'].value.to_i
761
781
  # Number of bytes used per pixel will determine how to unpack this:
762
782
  case depth
763
783
  when 8 # (1 byte)
764
- template = "BY" # Byte/Character/Fixnum
784
+ template = 'BY' # Byte/Character/Fixnum
765
785
  when 16 # (2 bytes)
766
786
  if pixel_representation == 1
767
- template = "SS" # Signed short
787
+ template = 'SS' # Signed short
768
788
  else
769
- template = "US" # Unsigned short
789
+ template = 'US' # Unsigned short
770
790
  end
771
791
  when 32 # (4 bytes)
772
792
  if pixel_representation == 1
773
- template = "SL" # Signed long
793
+ template = 'SL' # Signed long
774
794
  else
775
- template = "UL" # Unsigned long
795
+ template = 'UL' # Unsigned long
776
796
  end
777
797
  when 12
778
798
  # 12 BIT SIMPLY NOT IMPLEMENTED YET!
@@ -793,10 +813,10 @@ module DICOM
793
813
  # @return [Array<Integer, NilClass>] center, width, intercept and slope
794
814
  #
795
815
  def window_level_values
796
- center = (self["0028,1050"].is_a?(Element) == true ? self["0028,1050"].value.to_i : nil)
797
- width = (self["0028,1051"].is_a?(Element) == true ? self["0028,1051"].value.to_i : nil)
798
- intercept = (self["0028,1052"].is_a?(Element) == true ? self["0028,1052"].value.to_i : 0)
799
- slope = (self["0028,1053"].is_a?(Element) == true ? self["0028,1053"].value.to_i : 1)
816
+ center = (self['0028,1050'].is_a?(Element) == true ? self['0028,1050'].value.to_i : nil)
817
+ width = (self['0028,1051'].is_a?(Element) == true ? self['0028,1051'].value.to_i : nil)
818
+ intercept = (self['0028,1052'].is_a?(Element) == true ? self['0028,1052'].value.to_i : 0)
819
+ slope = (self['0028,1053'].is_a?(Element) == true ? self['0028,1053'].value.to_i : 1)
800
820
  return center, width, intercept, slope
801
821
  end
802
822
 
@@ -47,10 +47,10 @@ module DICOM
47
47
 
48
48
  # Gives an array containing the image objects that are supported by the image processor.
49
49
  #
50
- # @return [Array] the valid image classes
50
+ # @return [Array<String>] the valid image classes
51
51
  #
52
52
  def valid_image_objects
53
- return [Magick::Image, MiniMagick::Image]
53
+ return ['Magick::Image', 'MiniMagick::Image']
54
54
  end
55
55
 
56
56
 
@@ -45,7 +45,7 @@ module DICOM
45
45
  # @param [String] format the image format to use
46
46
  # @return [Magick::Image] a mini_magick image object
47
47
  #
48
- def import_pixels(blob, columns, rows, depth, photometry, format="png")
48
+ def import_pixels(blob, columns, rows, depth, photometry, format='png')
49
49
  image = MiniMagick::Image.import_pixels(blob, columns, rows, depth, im_map(photometry), format)
50
50
  end
51
51
 
@@ -56,12 +56,12 @@ module DICOM
56
56
  #
57
57
  def im_map(photometry)
58
58
  raise ArgumentError, "Expected String, got #{photometry.class}." unless photometry.is_a?(String)
59
- if photometry.include?("COLOR") or photometry.include?("RGB")
60
- return "rgb"
61
- elsif photometry.include?("YBR")
62
- return "ybr"
59
+ if photometry.include?('COLOR') or photometry.include?('RGB')
60
+ return 'rgb'
61
+ elsif photometry.include?('YBR')
62
+ return 'ybr'
63
63
  else
64
- return "gray" # (Assuming monochromeX - greyscale)
64
+ return 'gray' # (Assuming monochromeX - greyscale)
65
65
  end
66
66
  end
67
67
 
@@ -63,7 +63,7 @@ module DICOM
63
63
  # @param [String] format the image format to use
64
64
  # @return [Magick::Image] an RMagick image object
65
65
  #
66
- def import_pixels(blob, columns, rows, depth, photometry, format="png")
66
+ def import_pixels(blob, columns, rows, depth, photometry, format='png')
67
67
  image = Magick::Image.new(columns,rows).import_pixels(0, 0, columns, rows, rm_map(photometry), blob, rm_data_type(depth))
68
68
  end
69
69
 
@@ -91,12 +91,12 @@ module DICOM
91
91
  #
92
92
  def rm_map(photometry)
93
93
  raise ArgumentError, "Expected String, got #{photometry.class}." unless photometry.is_a?(String)
94
- if photometry.include?("COLOR") or photometry.include?("RGB")
95
- return "RGB"
96
- elsif photometry.include?("YBR")
97
- return "YBR"
94
+ if photometry.include?('COLOR') or photometry.include?('RGB')
95
+ return 'RGB'
96
+ elsif photometry.include?('YBR')
97
+ return 'YBR'
98
98
  else
99
- return "I" # (Assuming monochromeX - greyscale)
99
+ return 'I' # (Assuming monochromeX - greyscale)
100
100
  end
101
101
  end
102
102
 
@@ -1415,7 +1415,7 @@ module DICOM
1415
1415
  # Convert the variable to an empty string.
1416
1416
  data = ""
1417
1417
  end
1418
- return data
1418
+ data
1419
1419
  end
1420
1420
 
1421
1421
  # Receives the data from an incoming network transmission.
@@ -1423,17 +1423,13 @@ module DICOM
1423
1423
  #
1424
1424
  def receive_transmission_data
1425
1425
  data = false
1426
- t1 = Time.now.to_f
1427
- @receive = true
1428
- thr = Thread.new{ data=@session.recv(@max_receive_size); @receive=false }
1429
- while @receive
1430
- if (Time.now.to_f - t1) > @timeout
1431
- Thread.kill(thr)
1432
- logger.error("No answer was received within the specified timeout period. Aborting.")
1433
- stop_receiving
1434
- end
1426
+ response = IO.select([@session], nil, nil, @timeout)
1427
+ if response.nil?
1428
+ logger.error("No answer was received within the specified timeout period. Aborting.")
1429
+ else
1430
+ data = @session.recv(@max_receive_size)
1435
1431
  end
1436
- return data
1432
+ data
1437
1433
  end
1438
1434
 
1439
1435
  # Sets some default values related to encoding.
@@ -122,6 +122,7 @@ module DICOM
122
122
  end
123
123
 
124
124
  # The logger object setter.
125
+ #
125
126
  # This method is used to replace the default logger instance with
126
127
  # a custom logger of your own.
127
128
  #
@@ -152,4 +153,4 @@ module DICOM
152
153
  # Include the Logging module so we can use DICOM.logger.
153
154
  include Logging
154
155
 
155
- end
156
+ end
@@ -36,6 +36,41 @@ module DICOM
36
36
  return uid
37
37
  end
38
38
 
39
+ # Loads DICOM data to DObject instances and returns them in an array.
40
+ # Invalid DICOM sources (files) are ignored.
41
+ # If no valid DICOM source is given, an empty array is returned.
42
+ #
43
+ # @param [String, DObject, Array<String, DObject>] data single or multiple DICOM data (directories, file paths, binary strings, DICOM objects)
44
+ # @return [Array<DObject>] an array of successfully loaded DICOM objects
45
+ #
46
+ def load(data)
47
+ data = Array[data] unless data.respond_to?(:to_ary)
48
+ ary = Array.new
49
+ data.each do |element|
50
+ if element.is_a?(String)
51
+ begin
52
+ if File.directory?(element)
53
+ files = Dir[File.join(element, '**/*')].reject {|f| File.directory?(f) }
54
+ dcms = files.collect {|f| DObject.read(f)}
55
+ elsif File.file?(element)
56
+ dcms = [DObject.read(element)]
57
+ else
58
+ dcms = [DObject.parse(element)]
59
+ end
60
+ rescue
61
+ dcms = [DObject.parse(element)]
62
+ end
63
+ ary += dcms.keep_if {|dcm| dcm.read?}
64
+ else
65
+ # The element was not a string, and the only remaining valid element type is a DICOM object:
66
+ raise ArgumentError, "Invalid element (#{element.class}) given. Expected string or DObject." unless element.respond_to?(:to_dcm)
67
+ element.was_dcm_on_input = true
68
+ ary << element.to_dcm
69
+ end
70
+ end
71
+ ary
72
+ end
73
+
39
74
  # Use tags as key. Example: '0010,0010'
40
75
  #
41
76
  def key_use_tags
@@ -65,6 +100,6 @@ module DICOM
65
100
  # The default key representation.
66
101
  self.key_representation = :name
67
102
  # The default source application entity title.
68
- self.source_app_title = 'RUBY_DICOM'
103
+ self.source_app_title = 'RUBY-DICOM'
69
104
 
70
105
  end
@@ -1,6 +1,6 @@
1
1
  module DICOM
2
2
 
3
3
  # The ruby-dicom version string.
4
- VERSION = '0.9.4'
4
+ VERSION = '0.9.5'
5
5
 
6
6
  end
@@ -1,8 +1,10 @@
1
1
  # Available commands:
2
2
  # Testing the specification:
3
3
  # bundle exec rake spec
4
- # Building a gem package from source:
4
+ # Building a gem from source with rake:
5
5
  # bundle exec rake package
6
+ # Building a gem from source with rubygems:
7
+ # bundle exec gem build dicom.gemspec
6
8
  # Create html documentation files:
7
9
  # bundle exec rake yard
8
10