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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 280aba2d5fdbf5adf3b30d288486c7c756df21e0
|
4
|
+
data.tar.gz: fa01e90238b33269969363cf4142e9314b4a6873
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7aad5d1d1ab092b05cddc40b00f04be771b4335380b5e3b32576ed97ccdf5f1ba426f7dc416db36f3f099fbb466959d983caaf87f507370e9828fd97baf7698b
|
7
|
+
data.tar.gz: 03e9f692beed4c588ee7f83ca844448a041a2da9eb3453857396aadf6c2e748c18f04c73a4d3698d1ee2d55a7e3107220d4505fffbb061364dd799ca65681f05
|
data/LICENSE.md
CHANGED
data/dicoms.gemspec
CHANGED
@@ -25,7 +25,8 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_dependency 'modalsettings', '~> 1.0.1'
|
26
26
|
spec.add_dependency 'narray', '~> 0.6'
|
27
27
|
spec.add_dependency 'thor', '~> 0.19'
|
28
|
-
|
28
|
+
spec.add_dependency 'solver', '>= 0.2.0'
|
29
|
+
spec.add_dependency 'histogram', '~> 0.2.4'
|
29
30
|
|
30
31
|
spec.add_development_dependency "bundler", "~> 1.10"
|
31
32
|
spec.add_development_dependency "rake", "~> 10.0"
|
data/lib/dicoms.rb
CHANGED
data/lib/dicoms/cli.rb
CHANGED
@@ -1,6 +1,27 @@
|
|
1
1
|
require 'thor'
|
2
2
|
|
3
3
|
class DicomS
|
4
|
+
|
5
|
+
def self.handle_errors(options)
|
6
|
+
return yield if options.debug
|
7
|
+
options = CommandOptions[options]
|
8
|
+
progress = Progress.new(nil, options)
|
9
|
+
if progress.persistent?
|
10
|
+
begin
|
11
|
+
yield
|
12
|
+
0
|
13
|
+
rescue Error => error
|
14
|
+
progress.error! error.code, error.to_s
|
15
|
+
1
|
16
|
+
rescue => error
|
17
|
+
progress.error! 'unknown', error.to_s
|
18
|
+
2
|
19
|
+
end
|
20
|
+
else
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
4
25
|
class CLI < Thor
|
5
26
|
check_unknown_options!
|
6
27
|
|
@@ -17,6 +38,7 @@ class DicomS
|
|
17
38
|
class_option 'verbose', type: :boolean, default: false
|
18
39
|
class_option 'settings', type: :string, desc: 'settings (read-only) file'
|
19
40
|
class_option 'settings_io', type: :string, desc: 'settings file'
|
41
|
+
class_option 'debug', type: :boolean, default: false
|
20
42
|
|
21
43
|
desc "pack DICOM-DIR", "pack a DICOM directory"
|
22
44
|
option :output, desc: 'output file', aliases: '-o'
|
@@ -35,22 +57,23 @@ class DicomS
|
|
35
57
|
ignore_min: true
|
36
58
|
}
|
37
59
|
settings = {} # TODO: ...
|
38
|
-
unless File.directory?(dicom_dir)
|
39
|
-
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
40
|
-
say options
|
41
|
-
end
|
42
60
|
cmd_options = CommandOptions[
|
43
61
|
settings: options.settings,
|
44
62
|
settings_io: options.settings_io,
|
45
63
|
output: options.output,
|
46
64
|
tmp: options.tmp,
|
47
65
|
reorder: options.reorder,
|
48
|
-
dicom_metadata: true
|
66
|
+
dicom_metadata: true,
|
67
|
+
debug: options.debug
|
49
68
|
]
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
69
|
+
DicomS.handle_errors(cmd_options) do
|
70
|
+
packer = DicomS.new(settings)
|
71
|
+
unless File.directory?(dicom_dir)
|
72
|
+
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
73
|
+
say options
|
74
|
+
end
|
75
|
+
packer.pack dicom_dir, cmd_options
|
76
|
+
end
|
54
77
|
end
|
55
78
|
|
56
79
|
desc "unpack dspack", "unpack a dspack file"
|
@@ -59,20 +82,22 @@ class DicomS
|
|
59
82
|
# TODO: parameters for dicom regeneration
|
60
83
|
def unpack(dspack)
|
61
84
|
DICOM.logger.level = Logger::FATAL
|
62
|
-
unless File.file?(dspack)
|
63
|
-
raise Error, set_color("File not found: #{dspack}", :red)
|
64
|
-
say options
|
65
|
-
end
|
66
85
|
settings = {} # TODO: ...
|
67
|
-
|
68
|
-
packer.unpack(
|
69
|
-
dspack,
|
86
|
+
cmd_options = {
|
70
87
|
settings: options.settings,
|
71
88
|
settings_io: options.settings_io,
|
72
89
|
output: options.output,
|
73
|
-
dicom_output: options.dicom
|
74
|
-
|
75
|
-
|
90
|
+
dicom_output: options.dicom,
|
91
|
+
debug: options.debug
|
92
|
+
}
|
93
|
+
DicomS.handle_errors(cmd_options) do
|
94
|
+
packer = DicomS.new(settings)
|
95
|
+
unless File.file?(dspack)
|
96
|
+
raise Error, set_color("File not found: #{dspack}", :red)
|
97
|
+
say options
|
98
|
+
end
|
99
|
+
packer.unpack dspack, cmd_options
|
100
|
+
end
|
76
101
|
0
|
77
102
|
end
|
78
103
|
|
@@ -91,10 +116,6 @@ class DicomS
|
|
91
116
|
def extract(dicom_dir)000
|
92
117
|
DICOM.logger.level = Logger::FATAL
|
93
118
|
settings = {} # TODO: ...
|
94
|
-
unless File.exists?(dicom_dir)
|
95
|
-
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
96
|
-
say options
|
97
|
-
end
|
98
119
|
|
99
120
|
raw = options.raw
|
100
121
|
if options.big
|
@@ -105,15 +126,20 @@ class DicomS
|
|
105
126
|
little_endian = true
|
106
127
|
end
|
107
128
|
|
108
|
-
|
109
|
-
packer.extract(
|
110
|
-
dicom_dir,
|
129
|
+
cmd_options = {
|
111
130
|
transfer: DicomS.transfer_options(options),
|
112
131
|
output: options.output,
|
113
|
-
raw: raw, big_endian: big_endian, little_endian: little_endian
|
114
|
-
|
115
|
-
|
116
|
-
|
132
|
+
raw: raw, big_endian: big_endian, little_endian: little_endian,
|
133
|
+
debug: options.debug
|
134
|
+
}
|
135
|
+
DicomS.handle_errors(cmd_options) do
|
136
|
+
packer = DicomS.new(settings)
|
137
|
+
unless File.exists?(dicom_dir)
|
138
|
+
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
139
|
+
say options
|
140
|
+
end
|
141
|
+
packer.extract(dicom_dir, cmd_options)
|
142
|
+
end
|
117
143
|
end
|
118
144
|
|
119
145
|
desc "Level stats", "Level limits of one or more DICOM files"
|
@@ -126,6 +152,31 @@ class DicomS
|
|
126
152
|
puts " Minimum level: #{stats[:min]}"
|
127
153
|
puts " Next minimum level: #{stats[:next_min]}"
|
128
154
|
puts " Maximum level: #{stats[:max]}"
|
155
|
+
puts "Histogram:"
|
156
|
+
dicoms.print_histogram *stats[:histogram], compact: true
|
157
|
+
0
|
158
|
+
end
|
159
|
+
|
160
|
+
desc "Histogram", "Histogram of one or more DICOM files"
|
161
|
+
option :width, desc: 'bin width', aliases: '-w'
|
162
|
+
option :compact, desc: 'compact format', aliases: '-c'
|
163
|
+
def histogram(dicom_dir)
|
164
|
+
DICOM.logger.level = Logger::FATAL
|
165
|
+
settings = {} # TODO: ...
|
166
|
+
dicoms = DicomS.new(settings)
|
167
|
+
width = options.width && options.width.to_f
|
168
|
+
compact = !!options.compact
|
169
|
+
dicoms.histogram dicom_dir, bin_width: width, compact: compact
|
170
|
+
0
|
171
|
+
end
|
172
|
+
|
173
|
+
desc "Information", "Show DICOM metadata"
|
174
|
+
option :output, desc: 'output directory or file', aliases: '-o'
|
175
|
+
def info(dicom_dir)
|
176
|
+
DICOM.logger.level = Logger::FATAL
|
177
|
+
settings = {} # TODO: ...
|
178
|
+
dicoms = DicomS.new(settings)
|
179
|
+
dicoms.info dicom_dir, output: options.output
|
129
180
|
0
|
130
181
|
end
|
131
182
|
|
@@ -145,14 +196,14 @@ class DicomS
|
|
145
196
|
option :max_x_pixels, desc: 'maximum number of pixels in the X direction'
|
146
197
|
option :max_y_pixels, desc: 'maximum number of pixels in the Y direction'
|
147
198
|
option :max_z_pixels, desc: 'maximum number of pixels in the Z direction'
|
199
|
+
option :maxcols, desc: 'maximum number of image columns'
|
200
|
+
option :maxrows, desc: 'maximum number of image rows'
|
201
|
+
option :mincols, desc: 'minimum number of image columns'
|
202
|
+
option :minrows, desc: 'minimum number of image rows'
|
148
203
|
option :reorder, desc: 'reorder slices based on instance number'
|
149
204
|
def projection(dicom_dir)
|
150
205
|
DICOM.logger.level = Logger::FATAL
|
151
206
|
settings = {} # TODO: ...
|
152
|
-
unless File.directory?(dicom_dir)
|
153
|
-
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
154
|
-
say options
|
155
|
-
end
|
156
207
|
if options.settings_io || options.settings
|
157
208
|
cmd_options = CommandOptions[
|
158
209
|
settings: options.settings,
|
@@ -161,7 +212,12 @@ class DicomS
|
|
161
212
|
max_x_pixels: options.max_x_pixels && options.max_x_pixels.to_i,
|
162
213
|
max_y_pixels: options.max_y_pixels && options.max_y_pixels.to_i,
|
163
214
|
max_z_pixels: options.max_z_pixels && options.max_z_pixels.to_i,
|
215
|
+
maxrows: options.maxrows && options.maxrows.to_i,
|
216
|
+
maxcols: options.maxcols && options.maxcols.to_i,
|
217
|
+
minrows: options.minrows && options.minrows.to_i,
|
218
|
+
mincols: options.mincols && options.mincols.to_i,
|
164
219
|
reorder: options.reorder,
|
220
|
+
debug: options.debug
|
165
221
|
]
|
166
222
|
else
|
167
223
|
cmd_options = CommandOptions[
|
@@ -173,16 +229,84 @@ class DicomS
|
|
173
229
|
max_x_pixels: options.max_x_pixels && options.max_x_pixels.to_i,
|
174
230
|
max_y_pixels: options.max_y_pixels && options.max_y_pixels.to_i,
|
175
231
|
max_z_pixels: options.max_z_pixels && options.max_z_pixels.to_i,
|
232
|
+
maxrows: options.maxrows && options.maxrows.to_i,
|
233
|
+
maxcols: options.maxcols && options.maxcols.to_i,
|
234
|
+
minrows: options.minrows && options.minrows.to_i,
|
235
|
+
mincols: options.mincols && options.mincols.to_i,
|
176
236
|
reorder: options.reorder,
|
237
|
+
debug: options.debug
|
177
238
|
]
|
178
239
|
end
|
179
|
-
|
180
|
-
|
240
|
+
DicomS.handle_errors(cmd_options) do
|
241
|
+
unless File.directory?(dicom_dir)
|
242
|
+
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
243
|
+
say options
|
244
|
+
end
|
245
|
+
unless cmd_options.axial || options.sagittal || options.coronal
|
246
|
+
raise Error, "Must specify at least one projection (axial/sagittal/coronal)"
|
247
|
+
end
|
248
|
+
packer = DicomS.new(settings)
|
249
|
+
packer.projection(dicom_dir, cmd_options)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
desc "explode DICOM-DIR", "extract all projected images from a DICOM sequence"
|
254
|
+
option :output, desc: 'output directory', aliases: '-o'
|
255
|
+
option :transfer, desc: 'transfer method', aliases: '-t', default: 'window'
|
256
|
+
# TODO: add :mip_transfer
|
257
|
+
# option :byte, desc: 'transfer as bytes', aliases: '-b'
|
258
|
+
option :center, desc: 'center (window transfer)', aliases: '-c'
|
259
|
+
option :width, desc: 'window (window transfer)', aliases: '-w'
|
260
|
+
option :ignore_min, desc: 'ignore minimum (global/first/sample transfer)', aliases: '-i'
|
261
|
+
option :samples, desc: 'number of samples (sample transfer)', aliases: '-s'
|
262
|
+
option :min, desc: 'minimum value (fixed transfer)'
|
263
|
+
option :max, desc: 'maximum value (fixed transfer)'
|
264
|
+
option :max_x_pixels, desc: 'maximum number of pixels in the X direction'
|
265
|
+
option :max_y_pixels, desc: 'maximum number of pixels in the Y direction'
|
266
|
+
option :max_z_pixels, desc: 'maximum number of pixels in the Z direction'
|
267
|
+
option :maxcols, desc: 'maximum number of image columns'
|
268
|
+
option :maxrows, desc: 'maximum number of image rows'
|
269
|
+
option :mincols, desc: 'minimum number of image columns'
|
270
|
+
option :minrows, desc: 'minimum number of image rows'
|
271
|
+
option :reorder, desc: 'reorder slices based on instance number'
|
272
|
+
option :no_axial, desc: 'omit axial slices extraction', type: :boolean, default: false
|
273
|
+
option :no_coronal, desc: 'omit coronal slices extraction', type: :boolean, default: false
|
274
|
+
option :no_sagittal, desc: 'omit sagittal slices extraction', type: :boolean, default: false
|
275
|
+
option :no_mip, desc: 'omit mip projection', type: :boolean, default: false
|
276
|
+
option :no_aap, desc: 'omit aap projection', type: :boolean, default: false
|
277
|
+
def explode(dicom_dir)
|
278
|
+
DICOM.logger.level = Logger::FATAL
|
279
|
+
settings = {} # TODO: ...
|
280
|
+
unless File.directory?(dicom_dir)
|
281
|
+
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
282
|
+
say options
|
283
|
+
end
|
284
|
+
cmd_options = CommandOptions[
|
285
|
+
settings: options.settings,
|
286
|
+
settings_io: options.settings_io,
|
287
|
+
output: options.output,
|
288
|
+
max_x_pixels: options.max_x_pixels && options.max_x_pixels.to_i,
|
289
|
+
max_y_pixels: options.max_y_pixels && options.max_y_pixels.to_i,
|
290
|
+
max_z_pixels: options.max_z_pixels && options.max_z_pixels.to_i,
|
291
|
+
maxrows: options.maxrows && options.maxrows.to_i,
|
292
|
+
maxcols: options.maxcols && options.maxcols.to_i,
|
293
|
+
minrows: options.minrows && options.minrows.to_i,
|
294
|
+
mincols: options.mincols && options.mincols.to_i,
|
295
|
+
reorder: options.reorder,
|
296
|
+
no_axial: options.no_axial,
|
297
|
+
no_coronal: options.no_coronal,
|
298
|
+
no_sagittal: options.no_sagittal,
|
299
|
+
no_mip: options.no_mip,
|
300
|
+
no_aap: options.no_aap,
|
301
|
+
debug: options.debug
|
302
|
+
]
|
303
|
+
unless options.settings_io || options.settings
|
304
|
+
cmd_options.merge! transfer: DicomS.transfer_options(options)
|
305
|
+
end
|
306
|
+
DicomS.handle_errors(cmd_options) do
|
307
|
+
packer = DicomS.new(settings)
|
308
|
+
packer.explode(dicom_dir, cmd_options)
|
181
309
|
end
|
182
|
-
packer = DicomS.new(settings)
|
183
|
-
packer.projection(dicom_dir, cmd_options)
|
184
|
-
# rescue => raise Error?
|
185
|
-
0
|
186
310
|
end
|
187
311
|
|
188
312
|
desc "Remap DICOM-DIR", "convert DICOM pixel values"
|
@@ -199,18 +323,19 @@ class DicomS
|
|
199
323
|
def remap(dicom_dir)
|
200
324
|
DICOM.logger.level = Logger::FATAL
|
201
325
|
settings = {} # TODO: ...
|
202
|
-
|
203
|
-
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
204
|
-
say options
|
205
|
-
end
|
206
|
-
packer = DicomS.new(settings)
|
207
|
-
packer.remap(
|
208
|
-
dicom_dir,
|
326
|
+
cmd_options = {
|
209
327
|
transfer: DicomS.transfer_options(options),
|
210
|
-
output: options.output
|
211
|
-
|
212
|
-
|
213
|
-
|
328
|
+
output: options.output,
|
329
|
+
debug: options.debug
|
330
|
+
}
|
331
|
+
DicomS.handle_errors(cmd_options) do
|
332
|
+
unless File.directory?(dicom_dir)
|
333
|
+
raise Error, set_color("Directory not found: #{dicom_dir}", :red)
|
334
|
+
say options
|
335
|
+
end
|
336
|
+
packer = DicomS.new(settings)
|
337
|
+
packer.remap(dicom_dir, cmd_options)
|
338
|
+
end
|
214
339
|
end
|
215
340
|
end
|
216
341
|
|
@@ -0,0 +1,421 @@
|
|
1
|
+
require 'rmagick'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
# Extract all projected views (all slices in the three axis,
|
5
|
+
# plus aap and mip projectios)
|
6
|
+
def explode(dicom_directory, options = {})
|
7
|
+
options = CommandOptions[options]
|
8
|
+
|
9
|
+
progress = Progress.new('projecting', options)
|
10
|
+
progress.begin_subprocess 'reading_metadata', 1
|
11
|
+
|
12
|
+
# Create sequence without a transfer strategy
|
13
|
+
# (identity strategy with rescaling would also do)
|
14
|
+
# so that we're working with Hounsfield units
|
15
|
+
sequence = Sequence.new(
|
16
|
+
dicom_directory,
|
17
|
+
reorder: options[:reorder]
|
18
|
+
)
|
19
|
+
|
20
|
+
slice_transfer = define_transfer(
|
21
|
+
options,
|
22
|
+
:window,
|
23
|
+
output: :unsigned
|
24
|
+
)
|
25
|
+
mip_transfer = define_transfer(
|
26
|
+
{ transfer: options[:mip_transfer] },
|
27
|
+
:fixed,
|
28
|
+
min: -1000, max: 2000,
|
29
|
+
output: :unsigned
|
30
|
+
)
|
31
|
+
aap_transfer = Transfer.strategy :fixed, min: 0.0, max: 1.0, float: true, output: :unsigned
|
32
|
+
|
33
|
+
extract_dir = options.path_option(
|
34
|
+
:output, File.join(File.expand_path(dicom_directory), 'images')
|
35
|
+
)
|
36
|
+
FileUtils.mkdir_p extract_dir
|
37
|
+
|
38
|
+
if sequence.metadata.lim_max <= 255
|
39
|
+
bits = 8
|
40
|
+
else
|
41
|
+
bits = 16
|
42
|
+
end
|
43
|
+
|
44
|
+
scaling = projection_scaling(sequence.metadata, options)
|
45
|
+
sequence.metadata.merge! scaling
|
46
|
+
scaling = Settings[scaling]
|
47
|
+
|
48
|
+
reverse_x = sequence.metadata.reverse_x.to_i == 1
|
49
|
+
reverse_y = sequence.metadata.reverse_y.to_i == 1
|
50
|
+
reverse_z = sequence.metadata.reverse_z.to_i == 1
|
51
|
+
|
52
|
+
maxx = sequence.metadata.nx
|
53
|
+
maxy = sequence.metadata.ny
|
54
|
+
maxz = sequence.metadata.nz
|
55
|
+
|
56
|
+
# minimum and maximum slices with non-(almost)-blank contents
|
57
|
+
minx_contents = maxx
|
58
|
+
maxx_contents = 0
|
59
|
+
miny_contents = maxy
|
60
|
+
maxy_contents = 0
|
61
|
+
minz_contents = maxz
|
62
|
+
maxz_contents = 0
|
63
|
+
|
64
|
+
axial_zs = options.no_axial ? [] : (0...maxz)
|
65
|
+
sagittal_xs = options.no_sagittal ? [] : (0...maxx)
|
66
|
+
coronal_ys = options.no_coronal ? [] : (0...maxy)
|
67
|
+
|
68
|
+
# Will determine first and last slice with noticeable contents in each axis
|
69
|
+
# Slices outside the range won't be generated to save space
|
70
|
+
# This information will also be used for the app projection
|
71
|
+
minx_contents = maxx
|
72
|
+
maxx_contents = 0
|
73
|
+
miny_contents = maxy
|
74
|
+
maxy_contents = 0
|
75
|
+
minz_contents = maxz
|
76
|
+
maxz_contents = 0
|
77
|
+
|
78
|
+
n_slices = axial_zs.size + sagittal_xs.size + coronal_ys.size
|
79
|
+
n_projections = 0
|
80
|
+
n_projections += 3 unless options.no_mip
|
81
|
+
n_projections += 3 unless options.no_aap
|
82
|
+
|
83
|
+
progress.begin_subprocess 'generating_volume', 50, maxz
|
84
|
+
|
85
|
+
# Load all the slices into a floating point 3D array
|
86
|
+
volume = NArray.sfloat(maxx, maxy, maxz)
|
87
|
+
keeping_path do
|
88
|
+
sequence.each do |dicom, z, file|
|
89
|
+
slice = sequence.dicom_pixels(dicom)
|
90
|
+
volume[true, true, z] = slice
|
91
|
+
progress.update_subprocess z
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
maxv = volume.max
|
96
|
+
|
97
|
+
# Generate slices
|
98
|
+
slices_percent = n_projections > 0 ? -70 : 100
|
99
|
+
progress.begin_subprocess 'generating_slices', slices_percent, n_slices if n_slices > 0
|
100
|
+
axial_zs.each_with_index do |z, i|
|
101
|
+
slice = volume[true, true, z]
|
102
|
+
minz_contents, maxz_contents = update_min_max_contents(
|
103
|
+
z, slice.max, maxv, minz_contents, maxz_contents
|
104
|
+
)
|
105
|
+
next unless (minz_contents..maxz_contents).include?(z)
|
106
|
+
output_image = output_file_name(extract_dir, 'axial_', z.to_s)
|
107
|
+
save_transferred_pixels sequence, slice_transfer, slice, output_image,
|
108
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: reverse_y,
|
109
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_ny,
|
110
|
+
normalize: false
|
111
|
+
progress.update_subprocess i
|
112
|
+
end
|
113
|
+
sagittal_xs.each_with_index do |x, i|
|
114
|
+
slice = volume[x, true, true]
|
115
|
+
minx_contents, maxx_contents = update_min_max_contents(
|
116
|
+
x, slice.max, maxv, minx_contents, maxx_contents
|
117
|
+
)
|
118
|
+
next unless (minx_contents..maxx_contents).include?(x)
|
119
|
+
output_image = output_file_name(extract_dir, 'sagittal_', x.to_s)
|
120
|
+
save_transferred_pixels sequence, slice_transfer, slice, output_image,
|
121
|
+
bit_depth: bits, reverse_x: !reverse_y, reverse_y: !reverse_z,
|
122
|
+
cols: scaling.scaled_ny, rows: scaling.scaled_nz,
|
123
|
+
normalize: false
|
124
|
+
progress.update_subprocess axial_zs.size + i
|
125
|
+
end
|
126
|
+
coronal_ys.each_with_index do |y, i|
|
127
|
+
slice = volume[true, y, true]
|
128
|
+
miny_contents, maxy_contents = update_min_max_contents(
|
129
|
+
y, slice.max, maxv, miny_contents, maxy_contents
|
130
|
+
)
|
131
|
+
next unless (miny_contents..maxy_contents).include?(y)
|
132
|
+
output_image = output_file_name(extract_dir, 'coronal_', y.to_s)
|
133
|
+
save_transferred_pixels sequence, slice_transfer, slice, output_image,
|
134
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: !reverse_z,
|
135
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_nz,
|
136
|
+
normalize: false
|
137
|
+
progress.update_subprocess axial_zs.size + sagittal_xs.size + i
|
138
|
+
end
|
139
|
+
|
140
|
+
progress.begin_subprocess 'generating_projections', 100, n_projections
|
141
|
+
|
142
|
+
projection_count = 0
|
143
|
+
|
144
|
+
unless options.no_mip
|
145
|
+
# Generate MIP projections
|
146
|
+
slice = maximum_intensity_projection(volume, Z_AXIS)
|
147
|
+
output_image = output_file_name(extract_dir, 'axial_', 'mip')
|
148
|
+
save_transferred_pixels sequence, mip_transfer, slice, output_image,
|
149
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: reverse_y,
|
150
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_ny,
|
151
|
+
normalize: true
|
152
|
+
projection_count += 1
|
153
|
+
progress.update_subprocess projection_count
|
154
|
+
|
155
|
+
slice = maximum_intensity_projection(volume, Y_AXIS)
|
156
|
+
output_image = output_file_name(extract_dir, 'coronal_', 'mip')
|
157
|
+
save_transferred_pixels sequence, mip_transfer, slice, output_image,
|
158
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: !reverse_z,
|
159
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_nz,
|
160
|
+
normalize: true
|
161
|
+
projection_count += 1
|
162
|
+
progress.update_subprocess projection_count
|
163
|
+
|
164
|
+
slice = maximum_intensity_projection(volume, X_AXIS)
|
165
|
+
output_image = output_file_name(extract_dir, 'sagittal_', 'mip')
|
166
|
+
save_transferred_pixels sequence, mip_transfer, slice, output_image,
|
167
|
+
bit_depth: bits, reverse_x: !reverse_y, reverse_y: !reverse_z,
|
168
|
+
cols: scaling.scaled_ny, rows: scaling.scaled_nz,
|
169
|
+
normalize: true
|
170
|
+
projection_count += 1
|
171
|
+
progress.update_subprocess projection_count
|
172
|
+
end
|
173
|
+
|
174
|
+
unless options.no_aap
|
175
|
+
# Generate AAP Projections
|
176
|
+
c = dicom_window_center(sequence.first)
|
177
|
+
w = dicom_window_width(sequence.first)
|
178
|
+
dx = sequence.metadata.dx
|
179
|
+
dy = sequence.metadata.dy
|
180
|
+
dz = sequence.metadata.dz
|
181
|
+
numx = maxx_contents - minx_contents + 1 if minx_contents <= maxx_contents
|
182
|
+
numy = maxy_contents - miny_contents + 1 if miny_contents <= maxy_contents
|
183
|
+
numz = maxz_contents - minz_contents + 1 if minz_contents <= maxz_contents
|
184
|
+
daap = DynamicAap.new(
|
185
|
+
volume,
|
186
|
+
center: c, width: w, dx: dx, dy: dy, dz: dz,
|
187
|
+
numx: numx, numy: numy, numz: numz
|
188
|
+
)
|
189
|
+
slice = daap.view(Z_AXIS)
|
190
|
+
output_image = output_file_name(extract_dir, 'axial_', 'aap')
|
191
|
+
save_transferred_pixels sequence, aap_transfer, slice, output_image,
|
192
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: reverse_y,
|
193
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_ny,
|
194
|
+
normalize: true
|
195
|
+
projection_count += 1
|
196
|
+
progress.update_subprocess projection_count
|
197
|
+
|
198
|
+
slice = daap.view(Y_AXIS)
|
199
|
+
output_image = output_file_name(extract_dir, 'coronal_', 'aap')
|
200
|
+
save_transferred_pixels sequence, aap_transfer, slice, output_image,
|
201
|
+
bit_depth: bits, reverse_x: reverse_x, reverse_y: !reverse_z,
|
202
|
+
cols: scaling.scaled_nx, rows: scaling.scaled_nz,
|
203
|
+
normalize: true
|
204
|
+
projection_count += 1
|
205
|
+
progress.update_subprocess projection_count
|
206
|
+
|
207
|
+
slice = daap.view(X_AXIS)
|
208
|
+
output_image = output_file_name(extract_dir, 'sagittal_', 'aap')
|
209
|
+
save_transferred_pixels sequence, aap_transfer, slice, output_image,
|
210
|
+
bit_depth: bits, reverse_x: !reverse_y, reverse_y: !reverse_z,
|
211
|
+
cols: scaling.scaled_ny, rows: scaling.scaled_nz,
|
212
|
+
normalize: true
|
213
|
+
projection_count += 1
|
214
|
+
progress.update_subprocess projection_count
|
215
|
+
end
|
216
|
+
|
217
|
+
volume = nil
|
218
|
+
sequence.metadata.merge!(
|
219
|
+
axial_first: minz_contents, axial_last: maxz_contents,
|
220
|
+
coronal_first: miny_contents, coronal_last: maxy_contents,
|
221
|
+
sagittal_first: minx_contents, sagittal_last: maxx_contents
|
222
|
+
)
|
223
|
+
options.save_settings 'projection', sequence.metadata
|
224
|
+
progress.finish
|
225
|
+
end
|
226
|
+
|
227
|
+
def power(data, factor)
|
228
|
+
NMath.exp(factor*NMath.log(data))
|
229
|
+
end
|
230
|
+
|
231
|
+
def save_transferred_pixels(sequence, transfer, pixels, output_image, options)
|
232
|
+
dicom = sequence.first
|
233
|
+
min, max = transfer.min_max(sequence)
|
234
|
+
pixels = transfer.transfer_rescaled_pixels(dicom, pixels, min, max)
|
235
|
+
save_pixels pixels, output_image, options
|
236
|
+
end
|
237
|
+
|
238
|
+
class DynamicAap
|
239
|
+
|
240
|
+
PRE_GAMMA = 8
|
241
|
+
SUM_NORMALIZATION = false
|
242
|
+
IMAGE_GAMMA = nil
|
243
|
+
IMAGE_ADJUSTMENT = true
|
244
|
+
WINDOW_BY_DEFAULT = true
|
245
|
+
NO_WINDOW = true
|
246
|
+
IMAGE_CONTRAST = nil
|
247
|
+
|
248
|
+
def initialize(data, options)
|
249
|
+
center = options[:center]
|
250
|
+
width = options[:width]
|
251
|
+
if center && width && !NO_WINDOW
|
252
|
+
# 1. Window level normalization
|
253
|
+
if options[:window_sigmod_gamma]
|
254
|
+
# 1.a using sigmod
|
255
|
+
gamma = options[:window_sigmod_gamma] || 3.0
|
256
|
+
k0 = options[:window_sigmod_k0] || 0.06
|
257
|
+
sigmoid = Sigmoid.new(center: center, width: width, gamma: gamma, k0: k0)
|
258
|
+
data = sigmoid[data]
|
259
|
+
elsif options[:k0] || WINDOW_BY_DEFAULT
|
260
|
+
# 1.b simpler linear pseudo-sigmoid
|
261
|
+
max = data.max
|
262
|
+
min = data.min
|
263
|
+
k0 = options[:k0] || 0.1
|
264
|
+
v_lo = center - width*0.5
|
265
|
+
v_hi = center + width*0.5
|
266
|
+
low_part = (data < v_lo)
|
267
|
+
high_part = (data > v_hi)
|
268
|
+
mid_part = (low_part | high_part).not
|
269
|
+
|
270
|
+
data[low_part] -= min
|
271
|
+
data[low_part] *= k0/(v_lo - min)
|
272
|
+
|
273
|
+
data[high_part] -= v_hi
|
274
|
+
data[high_part] *= k0/(max - v_hi)
|
275
|
+
data[high_part] += 1.0 - k0
|
276
|
+
|
277
|
+
data[mid_part] -= v_lo
|
278
|
+
data[mid_part] *= (1.0 - 2*k0)/width
|
279
|
+
data[mid_part] += k0
|
280
|
+
else
|
281
|
+
# 1.c clip to window (like 1.b with k0=0)
|
282
|
+
max = data.max
|
283
|
+
min = data.min
|
284
|
+
k0 = options[:k0] || 0.02
|
285
|
+
v_lo = center - width*0.5
|
286
|
+
v_hi = center + width*0.5
|
287
|
+
|
288
|
+
low_part = (data < v_lo)
|
289
|
+
high_part = (data > v_hi)
|
290
|
+
mid_part = (low_part | high_part).not
|
291
|
+
|
292
|
+
data[low_part] = 0
|
293
|
+
data[high_part] = 1
|
294
|
+
|
295
|
+
data[mid_part] -= v_lo
|
296
|
+
data[mid_part] *= 1.0/width
|
297
|
+
end
|
298
|
+
else
|
299
|
+
# Normalize to 0-1
|
300
|
+
data.add! -data.min
|
301
|
+
data.mul! 1.0/data.max
|
302
|
+
end
|
303
|
+
|
304
|
+
if PRE_GAMMA
|
305
|
+
if [2, 4, 8].include?(PRE_GAMMA)
|
306
|
+
g = PRE_GAMMA
|
307
|
+
while g > 1
|
308
|
+
data.mul! data
|
309
|
+
g /= 2
|
310
|
+
end
|
311
|
+
else
|
312
|
+
data = power(data, PRE_GAMMA)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
@data = data
|
317
|
+
@ref_num = 512
|
318
|
+
@dx = options[:dx]
|
319
|
+
@dy = options[:dy]
|
320
|
+
@dz = options[:dz]
|
321
|
+
@numx = options[:numx] || @ref_num
|
322
|
+
@numy = options[:numy] || @ref_num
|
323
|
+
@numz = options[:numz] || @ref_num
|
324
|
+
@max_output_level = options[:max] || 1.0
|
325
|
+
end
|
326
|
+
|
327
|
+
def view(axis)
|
328
|
+
case axis
|
329
|
+
when Z_AXIS
|
330
|
+
d = @dz
|
331
|
+
num = @numz
|
332
|
+
when Y_AXIS
|
333
|
+
d = @dy
|
334
|
+
num = @numy
|
335
|
+
when X_AXIS
|
336
|
+
d = @dx
|
337
|
+
num = @numx
|
338
|
+
end
|
339
|
+
|
340
|
+
s = @data.sum(axis)
|
341
|
+
s.div! s.max if SUM_NORMALIZATION
|
342
|
+
s.mul! -d*@ref_num/num
|
343
|
+
s = NMath.exp(s)
|
344
|
+
|
345
|
+
if IMAGE_GAMMA
|
346
|
+
s.mul! -1
|
347
|
+
s.add! 1
|
348
|
+
s = power(s, IMAGE_GAMMA)
|
349
|
+
contrast! s, IMAGE_CONTRAST if IMAGE_CONTRAST
|
350
|
+
s.mul! @max_output_level if @max_output_level != 1
|
351
|
+
elsif IMAGE_ADJUSTMENT
|
352
|
+
s = adjust(s)
|
353
|
+
contrast! s, IMAGE_CONTRAST if IMAGE_CONTRAST
|
354
|
+
s.mul! @max_output_level if @max_output_level != 1
|
355
|
+
else
|
356
|
+
# since contrast is vertically symmetrical we can apply it
|
357
|
+
# to the negative image
|
358
|
+
contrast! s, IMAGE_CONTRAST if IMAGE_CONTRAST
|
359
|
+
s.mul! -@max_output_level
|
360
|
+
s.add! @max_output_level
|
361
|
+
end
|
362
|
+
s
|
363
|
+
end
|
364
|
+
|
365
|
+
private
|
366
|
+
|
367
|
+
def contrast!(data, factor)
|
368
|
+
# We use this piecewise sigmoid function:
|
369
|
+
#
|
370
|
+
# * f(x) for x <= 1/2
|
371
|
+
# * 1 - f(1-x) for x > 1/2
|
372
|
+
#
|
373
|
+
# With f(x) = pow(2, factor-1)*pow(x, factor)
|
374
|
+
# The pow() function will be computed as:
|
375
|
+
# pow(x, y) = exp(y*log(x))
|
376
|
+
#
|
377
|
+
# TODO: consider this alternative:
|
378
|
+
# f(x) = (factor*x - x)/(2*factor*x - factor - 1
|
379
|
+
#)
|
380
|
+
factor = factor.round
|
381
|
+
k = 2**(factor-1)
|
382
|
+
lo = data <= 0.5
|
383
|
+
hi = data > 0.5
|
384
|
+
data[lo] = NMath.exp(factor*NMath.log(data[lo]))*k
|
385
|
+
data[hi] = (-NMath.exp(NMath.log((-data[hi]) + 1)*factor))*k+1
|
386
|
+
data
|
387
|
+
end
|
388
|
+
|
389
|
+
def power(data, factor)
|
390
|
+
NMath.exp(factor*NMath.log(data))
|
391
|
+
end
|
392
|
+
|
393
|
+
def adjust(pixels)
|
394
|
+
min = pixels.min
|
395
|
+
max = pixels.max
|
396
|
+
avg = pixels.mean
|
397
|
+
# pixels.sbt! min
|
398
|
+
# pixels.mul! 1.0/(max - min)
|
399
|
+
pixels.sbt! max
|
400
|
+
pixels.div! min - max
|
401
|
+
|
402
|
+
# HUM: target 0.7; frac: target 0.2
|
403
|
+
discriminator = (pixels > 0.83).count_true.to_f / (pixels > 0.5).count_true.to_f
|
404
|
+
|
405
|
+
avg_target = 1.0 - discriminator**0.9
|
406
|
+
x0 = 0.67
|
407
|
+
y0 = 0.72
|
408
|
+
gamma = 3.0
|
409
|
+
k = - x0**gamma/Math.log(1-y0)
|
410
|
+
avg_target = 1.0 - Math.exp(-avg_target**gamma/k)
|
411
|
+
|
412
|
+
if avg_target > 0
|
413
|
+
g = Math.log(avg_target)/Math.log(avg)
|
414
|
+
power(pixels, g)
|
415
|
+
else
|
416
|
+
pixels
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|