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.
- 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
|