dicoms 1.0.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE.md +0 -2
- data/dicoms.gemspec +2 -1
- data/lib/dicoms.rb +4 -0
- data/lib/dicoms/cli.rb +176 -51
- data/lib/dicoms/explode.rb +421 -0
- data/lib/dicoms/histogram.rb +50 -0
- data/lib/dicoms/info.rb +43 -0
- data/lib/dicoms/progress.rb +22 -7
- data/lib/dicoms/projection.rb +40 -30
- data/lib/dicoms/sequence.rb +16 -11
- data/lib/dicoms/shared_settings.rb +7 -0
- data/lib/dicoms/sigmoid.rb +121 -0
- data/lib/dicoms/stats.rb +2 -2
- data/lib/dicoms/support.rb +48 -2
- data/lib/dicoms/transfer.rb +31 -23
- data/lib/dicoms/version.rb +1 -1
- metadata +34 -2
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'histogram/narray'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
def histogram(dicom_directory, options = {})
|
5
|
+
bins, freqs = compute_histogram(dicom_directory, options)
|
6
|
+
print_histogram bins, freqs, options
|
7
|
+
end
|
8
|
+
|
9
|
+
def compute_histogram(dicom_directory, options = {})
|
10
|
+
sequence = Sequence.new(dicom_directory)
|
11
|
+
bin_width = options[:bin_width]
|
12
|
+
|
13
|
+
if sequence.size == 1
|
14
|
+
data = sequence.first.narray
|
15
|
+
else
|
16
|
+
maxx = sequence.metadata.nx
|
17
|
+
maxy = sequence.metadata.ny
|
18
|
+
maxz = sequence.metadata.nz
|
19
|
+
data = NArray.sfloat(maxx, maxy, maxz)
|
20
|
+
sequence.each do |dicom, z, file|
|
21
|
+
data[true, true, z] = sequence.dicom_pixels(dicom)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
bins, freqs = data.histogram(:scott, :bin_boundary => :min, bin_width: bin_width)
|
25
|
+
[bins, freqs]
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_histogram(bins, freqs, options = {})
|
29
|
+
compact = options[:compact]
|
30
|
+
bin_labels = bins.to_a.map { |v| v.round.to_s }
|
31
|
+
label_width = bin_labels.map(&:size).max
|
32
|
+
sep = ": "
|
33
|
+
bar_width = terminal_size.last.to_i - label_width - sep.size
|
34
|
+
div = [1, freqs.max / bar_width.to_f].max
|
35
|
+
compact = true
|
36
|
+
empty = false
|
37
|
+
bin_labels.zip(freqs.to_a).each do |bin, freq|
|
38
|
+
rep = ((freq/div).round)
|
39
|
+
if compact && rep == 0
|
40
|
+
unless empty
|
41
|
+
puts "%#{label_width}s" % ['...']
|
42
|
+
empty = true
|
43
|
+
end
|
44
|
+
next
|
45
|
+
end
|
46
|
+
puts "%#{label_width}s#{sep}%-#{bar_width}s" % [bin, '#'*rep]
|
47
|
+
empty = false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/dicoms/info.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
def info(dicom_directory, options = {})
|
5
|
+
dicom_files = find_dicom_files(dicom_directory)
|
6
|
+
if dicom_files.empty?
|
7
|
+
raise "ERROR: no se han encontrado archivos DICOM en: \n #{dicom_directory}"
|
8
|
+
end
|
9
|
+
if options[:output]
|
10
|
+
if File.directory?(dicom_directory)
|
11
|
+
output_dir = options[:output]
|
12
|
+
else
|
13
|
+
output_file = options[:output]
|
14
|
+
if File.exists?(output_file)
|
15
|
+
raise "File #{output_file} already exits"
|
16
|
+
end
|
17
|
+
output = File.open(output_file, 'w')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
if output_dir
|
21
|
+
FileUtils.mkdir_p output_dir
|
22
|
+
dicom_files.each do |file|
|
23
|
+
output_file = File.join(output_dir, File.basename(file,'.dcm')+'.txt')
|
24
|
+
File.open(output_file, 'w') do |output|
|
25
|
+
dicom = DICOM::DObject.read(file)
|
26
|
+
print_info dicom, output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
dicom_files.each do |file|
|
31
|
+
dicom = DICOM::DObject.read(file)
|
32
|
+
print_info dicom, output || STDOUT
|
33
|
+
end
|
34
|
+
output.close if output
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_info(dicom, output)
|
39
|
+
$stdout = output
|
40
|
+
dicom.print_all
|
41
|
+
$stdout = STDOUT
|
42
|
+
end
|
43
|
+
end
|
data/lib/dicoms/progress.rb
CHANGED
@@ -6,14 +6,19 @@ class DicomS
|
|
6
6
|
@progress = options[:initial_progress] || 0
|
7
7
|
# TODO: if filename == :console, show progress on the console
|
8
8
|
if filename
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
if description
|
10
|
+
init_options = {
|
11
|
+
override_contents: {
|
12
|
+
process: description,
|
13
|
+
subprocess: options[:subprocess],
|
14
|
+
progress: @progress,
|
15
|
+
error: nil
|
16
|
+
}
|
15
17
|
}
|
16
|
-
|
18
|
+
else
|
19
|
+
init_options = {}
|
20
|
+
end
|
21
|
+
@file = SharedSettings.new(filename, init_options)
|
17
22
|
else
|
18
23
|
@file = nil
|
19
24
|
end
|
@@ -22,6 +27,16 @@ class DicomS
|
|
22
27
|
|
23
28
|
attr_reader :progress
|
24
29
|
|
30
|
+
def persistent?
|
31
|
+
!!@file
|
32
|
+
end
|
33
|
+
|
34
|
+
def error!(code, message)
|
35
|
+
@file.update do |data|
|
36
|
+
data.merge error: code, error_message: message
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
25
40
|
def finished?
|
26
41
|
@progress >= 100
|
27
42
|
end
|
data/lib/dicoms/projection.rb
CHANGED
@@ -69,13 +69,14 @@ class DicomS
|
|
69
69
|
# volume = NArray.sint(maxx, maxy, maxz)
|
70
70
|
volume = NArray.int(maxx, maxy, maxz)
|
71
71
|
end
|
72
|
+
maxv = volume.max
|
72
73
|
keeping_path do
|
73
74
|
sequence.each do |dicom, z, file|
|
74
75
|
slice = sequence.dicom_pixels(dicom, unsigned: true)
|
75
76
|
volume[true, true, z] = slice
|
76
77
|
if center_slice_projection?(options[:axial])
|
77
78
|
minz_contents, maxz_contents = update_min_max_contents(
|
78
|
-
z, slice.max,
|
79
|
+
z, slice.max, maxv, minz_contents, maxz_contents
|
79
80
|
)
|
80
81
|
end
|
81
82
|
progress.update_subprocess z
|
@@ -85,7 +86,7 @@ class DicomS
|
|
85
86
|
if center_slice_projection?(options[:coronal])
|
86
87
|
(0...maxy).each do |y|
|
87
88
|
miny_contents, maxy_contents = update_min_max_contents(
|
88
|
-
y, volume[true, y, true].max,
|
89
|
+
y, volume[true, y, true].max, maxv, miny_contents, maxy_contents
|
89
90
|
)
|
90
91
|
end
|
91
92
|
end
|
@@ -93,7 +94,7 @@ class DicomS
|
|
93
94
|
if center_slice_projection?(options[:sagittal])
|
94
95
|
(0...maxz).each do |z|
|
95
96
|
minz_contents, maxz_contents = update_min_max_contents(
|
96
|
-
z, volume[true, true, z].max,
|
97
|
+
z, volume[true, true, z].max, maxv, minz_contents, maxz_contents
|
97
98
|
)
|
98
99
|
end
|
99
100
|
end
|
@@ -129,7 +130,7 @@ class DicomS
|
|
129
130
|
elsif middle_slice_projection?(options[:coronal])
|
130
131
|
coronal_ys = [[maxy/2, 'm']]
|
131
132
|
elsif full_projection?(options[:coronal])
|
132
|
-
coronal_ys = (0...
|
133
|
+
coronal_ys = (0...maxy)
|
133
134
|
else
|
134
135
|
coronal_ys = []
|
135
136
|
end
|
@@ -275,8 +276,7 @@ class DicomS
|
|
275
276
|
v.max(axis)
|
276
277
|
end
|
277
278
|
|
278
|
-
def accumulated_attenuation_projection(float_v, axis, max_output_level, max=500)
|
279
|
-
k = 0.02
|
279
|
+
def accumulated_attenuation_projection(float_v, axis, max_output_level, max=500, k = 0.02)
|
280
280
|
if ADJUST_AAP_FOR_WIDTH
|
281
281
|
k *= 500.0/max
|
282
282
|
end
|
@@ -284,8 +284,10 @@ class DicomS
|
|
284
284
|
v.mul! -k
|
285
285
|
v = NMath.exp(v)
|
286
286
|
# Invert result (from attenuation to transmission)
|
287
|
-
|
288
|
-
|
287
|
+
if max_output_level
|
288
|
+
v.mul! -max_output_level
|
289
|
+
v.add! max_output_level
|
290
|
+
end
|
289
291
|
v
|
290
292
|
end
|
291
293
|
|
@@ -322,8 +324,6 @@ class DicomS
|
|
322
324
|
end
|
323
325
|
|
324
326
|
def projection_scaling(data, options = {})
|
325
|
-
sx = sy = sz = 1
|
326
|
-
|
327
327
|
nx = data.nx
|
328
328
|
ny = data.ny
|
329
329
|
nz = data.nz
|
@@ -331,18 +331,37 @@ class DicomS
|
|
331
331
|
dy = data.dy
|
332
332
|
dz = data.dz
|
333
333
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
334
|
+
sx = sy = sz = 1
|
335
|
+
scaled_nx = nx
|
336
|
+
scaled_ny = ny
|
337
|
+
scaled_nz = nz
|
338
|
+
|
339
|
+
adjust = ->(ref) {
|
338
340
|
sx = dx/ref
|
339
341
|
sy = dy/ref
|
340
342
|
sz = dz/ref
|
341
|
-
|
343
|
+
scaled_nx = (nx*sx).round
|
344
|
+
scaled_ny = (ny*sy).round
|
345
|
+
scaled_nz = (nz*sz).round
|
346
|
+
}
|
347
|
+
|
348
|
+
# need to cope with different scales in different axes.
|
349
|
+
# will always produce shrink factors (<1)
|
350
|
+
adjust.call [dx, dy, dz].max.to_f
|
342
351
|
|
343
|
-
|
344
|
-
|
345
|
-
|
352
|
+
# Now we may have scaled down too much; we'll avoid going below
|
353
|
+
# requested minimum sizes for axes and/or image dimensions.
|
354
|
+
# Axis X is the columns of axial an coronal views
|
355
|
+
# Axis Y is the rows of axial and the columns of sagittal
|
356
|
+
# Axis Z is the rows of coronal and sagittal views
|
357
|
+
min_nx = [scaled_nx, options.min_x_pixels, options[:mincols]].compact.max
|
358
|
+
adjust[dx*nx.to_f/min_nx] if scaled_nx < min_nx
|
359
|
+
|
360
|
+
min_ny = [scaled_ny, options.min_y_pixels, options[:mincols], options[:minrows]].compact.max
|
361
|
+
adjust[dy*ny.to_f/min_ny] if scaled_ny < min_ny
|
362
|
+
|
363
|
+
min_nz = [scaled_nz, options.min_z_pixels, options[:minrows]].compact.max
|
364
|
+
adjust[dz*nz.to_f/min_nz] if scaled_nz > min_nz
|
346
365
|
|
347
366
|
# further shrinking may be needed to avoid any projection
|
348
367
|
# to be larger thant the maximum image size
|
@@ -350,22 +369,13 @@ class DicomS
|
|
350
369
|
# Axis Y is the rows of axial and the columns of sagittal
|
351
370
|
# Axis Z is the rows of coronal and sagittal views
|
352
371
|
max_nx = [scaled_nx, options.max_x_pixels, options[:maxcols]].compact.min
|
353
|
-
if scaled_nx > max_nx
|
354
|
-
scaled_nx = max_nx
|
355
|
-
sx = scaled_nx/nx.to_f
|
356
|
-
end
|
372
|
+
adjust[dx*nx.to_f/max_nx] if scaled_nx > max_nx
|
357
373
|
|
358
374
|
max_ny = [scaled_ny, options.max_y_pixels, options[:maxcols], options[:maxrows]].compact.min
|
359
|
-
if scaled_ny > max_ny
|
360
|
-
scaled_ny = max_ny
|
361
|
-
sy = scaled_ny/ny.to_f
|
362
|
-
end
|
375
|
+
adjust[dy*ny.to_f/max_ny] if scaled_ny > max_ny
|
363
376
|
|
364
377
|
max_nz = [scaled_nz, options.max_z_pixels, options[:maxrows]].compact.min
|
365
|
-
if scaled_nz > max_nz
|
366
|
-
scaled_nz = max_nz
|
367
|
-
sz = scaled_nz/nz.to_f
|
368
|
-
end
|
378
|
+
adjust[dz*nz.to_f/max_nz] if scaled_nz > max_nz
|
369
379
|
|
370
380
|
{
|
371
381
|
scale_x: sx, scale_y: sy, scale_z: sz,
|
data/lib/dicoms/sequence.rb
CHANGED
@@ -79,6 +79,9 @@ class DicomS
|
|
79
79
|
@strategy = options[:transfer]
|
80
80
|
@metadata = Settings[version: 'DSPACK1']
|
81
81
|
|
82
|
+
if @files.empty?
|
83
|
+
raise InvaliddDICOM, 'No DICOM files found'
|
84
|
+
end
|
82
85
|
# TODO: reuse existing metadata in options (via settings)
|
83
86
|
|
84
87
|
if options[:reorder]
|
@@ -131,7 +134,7 @@ class DicomS
|
|
131
134
|
dicom = DICOM::DObject.read(@files[i])
|
132
135
|
sop_class = dicom['0002,0002'].value
|
133
136
|
unless sop_class == '1.2.840.10008.5.1.4.1.1.2'
|
134
|
-
raise "Unsopported SOP Class #{sop_class}"
|
137
|
+
raise UnsupportedDICOM, "Unsopported SOP Class #{sop_class}"
|
135
138
|
end
|
136
139
|
# TODO: require known SOP Class:
|
137
140
|
# (in tag 0002,0002, Media Storage SOP Class UID)
|
@@ -244,7 +247,7 @@ class DicomS
|
|
244
247
|
end
|
245
248
|
if bits
|
246
249
|
if bits != dicom_bit_depth(dicom) || signed != dicom_signed?(dicom)
|
247
|
-
raise "Inconsistent slices"
|
250
|
+
raise InvalidDICOM, "Inconsistent slices"
|
248
251
|
end
|
249
252
|
else
|
250
253
|
bits = dicom_bit_depth(dicom)
|
@@ -252,7 +255,7 @@ class DicomS
|
|
252
255
|
end
|
253
256
|
if slope
|
254
257
|
if slope != dicom_rescale_slope(dicom) || intercept != dicom_rescale_intercept(dicom)
|
255
|
-
raise "Inconsitent slices"
|
258
|
+
raise InvalidDICOM, "Inconsitent slices"
|
256
259
|
end
|
257
260
|
else
|
258
261
|
slope = dicom_rescale_slope(dicom)
|
@@ -322,8 +325,6 @@ class DicomS
|
|
322
325
|
|
323
326
|
total_n = size
|
324
327
|
|
325
|
-
n = last_i - first_i
|
326
|
-
|
327
328
|
xaxis = decode_vector(first_md.xaxis)
|
328
329
|
yaxis = decode_vector(first_md.yaxis)
|
329
330
|
# assert xaxis == decode_vector(last_md.xaxis)
|
@@ -337,12 +338,16 @@ class DicomS
|
|
337
338
|
@metadata.merge! Settings[first_md]
|
338
339
|
@metadata.zaxis = encode_vector zaxis
|
339
340
|
@metadata.nz = total_n
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
341
|
+
# Position coordinates are assumed at the voxel's center
|
342
|
+
# note that we divide the coordinate span z_last - z_first
|
343
|
+
# by the number of slices (voxels) minus one
|
344
|
+
@metadata.dz = (last_md.slice_z - first_md.slice_z).abs/(last_i - first_i)
|
345
|
+
|
346
|
+
tolerance = 0.05
|
347
|
+
if (xaxis.map(&:abs) - Vector[1,0,0]).magnitude > tolerance ||
|
348
|
+
(yaxis.map(&:abs) - Vector[0,1,0]).magnitude > tolerance ||
|
349
|
+
(zaxis.map(&:abs) - Vector[0,0,1]).magnitude > tolerance
|
350
|
+
raise UnsupportedDICOM, "Unsupported orientation"
|
346
351
|
end
|
347
352
|
@metadata.reverse_x = (xaxis[0] < 0) ? 1 : 0
|
348
353
|
@metadata.reverse_y = (yaxis[1] < 0) ? 1 : 0
|
@@ -24,6 +24,13 @@ class DicomS
|
|
24
24
|
end
|
25
25
|
contents = options[:replace_contents]
|
26
26
|
write contents if contents
|
27
|
+
contents = options[:override_contents]
|
28
|
+
if contents
|
29
|
+
update do |data|
|
30
|
+
data ||= {}
|
31
|
+
data.merge contents
|
32
|
+
end
|
33
|
+
end
|
27
34
|
end
|
28
35
|
|
29
36
|
# Read a shared file and obtain a Settings object
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'solver'
|
2
|
+
require 'narray'
|
3
|
+
|
4
|
+
class DicomS
|
5
|
+
|
6
|
+
# Compute sigmoid function s(x) = 1 - exp(-((x - x0)**gamma)/k) for x >= x0;
|
7
|
+
# with s(x) = 0 for x < x0).
|
8
|
+
#
|
9
|
+
# 0 <= s(x) < 1 for all x
|
10
|
+
#
|
11
|
+
# Defined by two parameters:
|
12
|
+
#
|
13
|
+
# * w width of the sigmoid
|
14
|
+
# * xc center of the sigmoid (s(xc) = 1/2)
|
15
|
+
#
|
16
|
+
# The interpretation of w is established by a constant 0 < k0 << 1
|
17
|
+
# where y1 = s(x1) = k0 and y2 = s(x2) = 1 - k0 and
|
18
|
+
# x1 = xc - w/2 and x2 = xc + w/2
|
19
|
+
#
|
20
|
+
# The gamma factor should be fixed a priori, e.g. gamma = 4
|
21
|
+
#
|
22
|
+
# k0 = 0.01
|
23
|
+
# w = 3.0
|
24
|
+
# xc = 7.0
|
25
|
+
# sigmod = Sigmoid.new(k0, w, xc)
|
26
|
+
# 0.5 == sigmoid[xc]
|
27
|
+
# k0 == sigmod[xc - w/2]
|
28
|
+
# 1 - k0 == sigmod[xc + w/2]
|
29
|
+
class Sigmoid
|
30
|
+
|
31
|
+
def initialize(options = {})
|
32
|
+
k0 = options[:k0] || 0.05
|
33
|
+
w = options[:width]
|
34
|
+
xc = options[:center]
|
35
|
+
@gamma = options[:gamma] || 4.0
|
36
|
+
tolerance = options[:tolerance] || Flt::Tolerance(0.01, :relative)
|
37
|
+
|
38
|
+
# Compute the parameters x0 and gamma which define the sigmoid
|
39
|
+
# given k0, w and xc
|
40
|
+
|
41
|
+
yc = 0.5
|
42
|
+
x1 = xc - w/2
|
43
|
+
x2 = xc + w/2
|
44
|
+
y1 = k0
|
45
|
+
y2 = 1 - k0
|
46
|
+
@gamma = 4.0
|
47
|
+
|
48
|
+
# We have two equations to solve for x0, gamma:
|
49
|
+
# * yc == 1 - Math.exp(-((xc - x0)**gamma)/k)
|
50
|
+
# * y1 == 1 - Math.exp(-((x1 - x0)**gamma)/k)
|
51
|
+
# Equivalently we could substitute the latter for
|
52
|
+
# * y2 == 1 - Math.exp(-((x2 - x0)**gamma)/k)
|
53
|
+
#
|
54
|
+
# So, we use the first equation to substitute in the second
|
55
|
+
# one of these:
|
56
|
+
#
|
57
|
+
# * x0 = xc - (-k*Math.log(1 - yc))**(1/gamma)
|
58
|
+
# * k = - (xc - x0)**gamma / Math.log(1 - yc)
|
59
|
+
#
|
60
|
+
# * gamma = Math.log(-k*Math.log(1 - yc))/Math.log(xc - x0)
|
61
|
+
#
|
62
|
+
# So we could solve either this for k:
|
63
|
+
#
|
64
|
+
# * y1 == 1 - Math.exp(-((x1 - xc + (-k*Math.log(1 - yc))**(1/gamma))**gamma)/k)
|
65
|
+
#
|
66
|
+
# or this for x0
|
67
|
+
#
|
68
|
+
# * y1 == 1 - Math.exp(-((x1 - x0)**gamma)/(- (xc - x0)**gamma / Math.log(1 - yc)))
|
69
|
+
algorithm = Flt::Solver::RFSecantSolver
|
70
|
+
|
71
|
+
if true
|
72
|
+
g = @gamma
|
73
|
+
eq = ->(k) {
|
74
|
+
y1 - 1 + exp(-((x1 - xc + (-k*log(1 - yc))**(1/g))**g)/k) }
|
75
|
+
solver = algorithm.new(Float.context, tolerance, &eq)
|
76
|
+
@k = solver.root(1.0, 100.0)
|
77
|
+
@x0 = xc - (-@k*Math.log(1 - yc))**(1/@gamma)
|
78
|
+
else
|
79
|
+
g = @gamma
|
80
|
+
eq = ->(x0) { y1 - 1 + exp(-((x1 - x0)**g)/(- (xc - x0)**g / log(1 - yc))) }
|
81
|
+
solver = algorithm.new(Float.context, tolerance, &eq)
|
82
|
+
@x0 = solver.root(0.0, 100.0)
|
83
|
+
@k = - (xc - @x0)**@gamma / Math.log(1 - yc)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def [](x)
|
88
|
+
if x.is_a? NArray
|
89
|
+
narray_sigmoid x
|
90
|
+
else
|
91
|
+
if x <= @x0
|
92
|
+
0.0
|
93
|
+
else
|
94
|
+
1.0 - Math.exp(-((x - @x0)**@gamma)/@k)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
attr_reader :gamma, :k, :x0
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def narray_sigmoid(x)
|
104
|
+
x[x <= @x0] = 0
|
105
|
+
ne0 = x.ne(0)
|
106
|
+
y = x[ne0]
|
107
|
+
y.sbt! @x0
|
108
|
+
y = power(y, @gamma)
|
109
|
+
y.div! -@k
|
110
|
+
y = NMath.exp(y)
|
111
|
+
y.mul! -1
|
112
|
+
y.add! 1
|
113
|
+
x[ne0] = y
|
114
|
+
x
|
115
|
+
end
|
116
|
+
|
117
|
+
def power(x, y)
|
118
|
+
NMath.exp(y*NMath.log(x))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|