dicoms 1.0.0 → 1.2.0

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.
@@ -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
@@ -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
@@ -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
- @file = SharedSettings.new(
10
- filename,
11
- replace_contents: {
12
- process: description,
13
- subprocess: options[:subprocess],
14
- progress: @progress
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
@@ -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, maxy, minz_contents, maxz_contents
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, maxy, miny_contents, maxy_contents
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, maxz, minz_contents, maxz_contents
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...maxx)
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
- v.mul! -max_output_level
288
- v.add! max_output_level
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
- unless dx == dy && dy == dz
335
- # need to cope with different scales in different axes.
336
- # will always produce shrink factors (<1)
337
- ref = [dx, dy, dz].max.to_f
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
- end
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
- scaled_nx = (nx*sx).round
344
- scaled_ny = (ny*sy).round
345
- scaled_nz = (nz*sz).round
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,
@@ -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
- @metadata.dz = (last_md.slice_z - first_md.slice_z).abs/n
341
-
342
- if xaxis[0].abs != 1 || xaxis[1] != 0 || xaxis[2] != 0 ||
343
- yaxis[0] != 0 || yaxis[1] != 1 || yaxis[2] != 0 ||
344
- zaxis[0] != 0 || zaxis[1] != 0 || zaxis[2].abs != 1
345
- raise Error, "Unsupported orientation"
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