dicom 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.rdoc +44 -0
- data/CONTRIBUTING.rdoc +83 -0
- data/README.rdoc +3 -2
- data/dicom.gemspec +12 -13
- data/lib/dicom/anonymizer.rb +649 -670
- data/lib/dicom/audit_trail.rb +7 -0
- data/lib/dicom/constants.rb +1 -1
- data/lib/dicom/d_library.rb +46 -31
- data/lib/dicom/d_object.rb +38 -24
- data/lib/dicom/d_read.rb +20 -6
- data/lib/dicom/d_server.rb +3 -3
- data/lib/dicom/d_write.rb +7 -2
- data/lib/dicom/deprecated.rb +318 -0
- data/lib/dicom/image_item.rb +62 -42
- data/lib/dicom/image_processor.rb +2 -2
- data/lib/dicom/image_processor_mini_magick.rb +6 -6
- data/lib/dicom/image_processor_r_magick.rb +6 -6
- data/lib/dicom/link.rb +7 -11
- data/lib/dicom/logging.rb +2 -1
- data/lib/dicom/variables.rb +36 -1
- data/lib/dicom/version.rb +1 -1
- data/rakefile.rb +3 -1
- metadata +46 -63
data/lib/dicom/image_item.rb
CHANGED
@@ -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?(
|
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[
|
60
|
-
pixel_representation_element = self[
|
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[
|
85
|
-
pixel_representation_element = self[
|
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,
|
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 =
|
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,
|
268
|
+
f = File.new(file, 'wb')
|
248
269
|
else
|
249
|
-
f = File.new("#{base}-#{i}#{extension}",
|
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
|
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[
|
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[
|
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[
|
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, "
|
406
|
-
if values.
|
407
|
-
#
|
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
|
-
#
|
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
|
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[
|
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(
|
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(
|
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[
|
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?(
|
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[
|
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[
|
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?(
|
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(
|
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(
|
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[
|
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 =
|
784
|
+
template = 'BY' # Byte/Character/Fixnum
|
765
785
|
when 16 # (2 bytes)
|
766
786
|
if pixel_representation == 1
|
767
|
-
template =
|
787
|
+
template = 'SS' # Signed short
|
768
788
|
else
|
769
|
-
template =
|
789
|
+
template = 'US' # Unsigned short
|
770
790
|
end
|
771
791
|
when 32 # (4 bytes)
|
772
792
|
if pixel_representation == 1
|
773
|
-
template =
|
793
|
+
template = 'SL' # Signed long
|
774
794
|
else
|
775
|
-
template =
|
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[
|
797
|
-
width = (self[
|
798
|
-
intercept = (self[
|
799
|
-
slope = (self[
|
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=
|
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?(
|
60
|
-
return
|
61
|
-
elsif photometry.include?(
|
62
|
-
return
|
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
|
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=
|
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?(
|
95
|
-
return
|
96
|
-
elsif photometry.include?(
|
97
|
-
return
|
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
|
99
|
+
return 'I' # (Assuming monochromeX - greyscale)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
data/lib/dicom/link.rb
CHANGED
@@ -1415,7 +1415,7 @@ module DICOM
|
|
1415
1415
|
# Convert the variable to an empty string.
|
1416
1416
|
data = ""
|
1417
1417
|
end
|
1418
|
-
|
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
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
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
|
-
|
1432
|
+
data
|
1437
1433
|
end
|
1438
1434
|
|
1439
1435
|
# Sets some default values related to encoding.
|
data/lib/dicom/logging.rb
CHANGED
@@ -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
|
data/lib/dicom/variables.rb
CHANGED
@@ -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 = '
|
103
|
+
self.source_app_title = 'RUBY-DICOM'
|
69
104
|
|
70
105
|
end
|
data/lib/dicom/version.rb
CHANGED
data/rakefile.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# Available commands:
|
2
2
|
# Testing the specification:
|
3
3
|
# bundle exec rake spec
|
4
|
-
# Building a gem
|
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
|
|