dicoms 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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