dicoms 1.0.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 +7 -0
- data/.gitignore +15 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.md +598 -0
- data/README.md +48 -0
- data/Rakefile +22 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/dicoms.gemspec +33 -0
- data/exe/dicoms +8 -0
- data/lib/dicoms.rb +67 -0
- data/lib/dicoms/cli.rb +241 -0
- data/lib/dicoms/command_options.rb +40 -0
- data/lib/dicoms/extract.rb +87 -0
- data/lib/dicoms/meta_codec.rb +131 -0
- data/lib/dicoms/pack.rb +82 -0
- data/lib/dicoms/progress.rb +80 -0
- data/lib/dicoms/projection.rb +422 -0
- data/lib/dicoms/remap.rb +46 -0
- data/lib/dicoms/sequence.rb +415 -0
- data/lib/dicoms/shared_files.rb +61 -0
- data/lib/dicoms/shared_settings.rb +111 -0
- data/lib/dicoms/stats.rb +30 -0
- data/lib/dicoms/support.rb +349 -0
- data/lib/dicoms/transfer.rb +339 -0
- data/lib/dicoms/unpack.rb +209 -0
- data/lib/dicoms/version.rb +3 -0
- metadata +200 -0
@@ -0,0 +1,339 @@
|
|
1
|
+
class DicomS
|
2
|
+
# Base class for Transfer strategy classes that define how
|
3
|
+
# the values of DICOM pixels are scales to be used as image pixels
|
4
|
+
# or to be processed as data (generic presentation values).
|
5
|
+
#
|
6
|
+
# Different strategies determine how much of the original
|
7
|
+
# data dynamic range is preserved.
|
8
|
+
#
|
9
|
+
# All the Transfer-derived classes can pass an :output option to the base
|
10
|
+
# which changes output range limits from what is stored in the DICOM.
|
11
|
+
# Two values are supported:
|
12
|
+
#
|
13
|
+
# * :byte Output consist of single byte values (0-255)
|
14
|
+
# * :unsigned Output is always unsigned
|
15
|
+
#
|
16
|
+
# The method min_max(sequence) of each class returns the minimum and maximum
|
17
|
+
# values which are mapped to the output limits.
|
18
|
+
# These values may be raw or rescaled depending on the min_max_rescaled? method
|
19
|
+
#
|
20
|
+
class Transfer
|
21
|
+
USE_DATA = false
|
22
|
+
|
23
|
+
include Support
|
24
|
+
extend Support
|
25
|
+
|
26
|
+
def initialize(options = {})
|
27
|
+
@output = options[:output]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Remapped DICOM pixel values as an Image
|
31
|
+
def image(dicom, min, max)
|
32
|
+
assign_dicom_pixels dicom, pixels(dicom, min, max)
|
33
|
+
dicom.image
|
34
|
+
end
|
35
|
+
|
36
|
+
# Remapped DICOM pixel values as an NArray
|
37
|
+
def pixels(dicom, min, max)
|
38
|
+
processed_data(dicom, min, max)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.strategy(strategy, options = {})
|
42
|
+
if strategy.is_a?(Array) && options.empty?
|
43
|
+
strategy, options = strategy
|
44
|
+
end
|
45
|
+
return nil if strategy.nil?
|
46
|
+
case strategy.to_sym
|
47
|
+
when :fixed
|
48
|
+
strategy_class = FixedTransfer
|
49
|
+
when :window
|
50
|
+
strategy_class = WindowTransfer
|
51
|
+
when :first
|
52
|
+
strategy_class = FirstTransfer
|
53
|
+
when :global
|
54
|
+
strategy_class = GlobalTransfer
|
55
|
+
when :sample
|
56
|
+
strategy_class = SampleTransfer
|
57
|
+
when :identity
|
58
|
+
strategy_class = IdentityTransfer
|
59
|
+
else
|
60
|
+
raise "INVALID: #{strategy.inspect}"
|
61
|
+
end
|
62
|
+
strategy_class.new options
|
63
|
+
end
|
64
|
+
|
65
|
+
# absolute output limits of the range (raw, not rescaled)
|
66
|
+
def min_max_limits(dicom)
|
67
|
+
case @output
|
68
|
+
when :byte
|
69
|
+
[0, 255]
|
70
|
+
when :unsigned
|
71
|
+
min, max = Transfer.min_max_limits(dicom)
|
72
|
+
if min < 0
|
73
|
+
min = 0
|
74
|
+
max -= min
|
75
|
+
end
|
76
|
+
[min, max]
|
77
|
+
else
|
78
|
+
Transfer.min_max_limits(dicom)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.min_max_limits(dicom)
|
83
|
+
num_bits = dicom_bit_depth(dicom)
|
84
|
+
signed = dicom_signed?(dicom)
|
85
|
+
pixel_value_range(num_bits, signed)
|
86
|
+
end
|
87
|
+
|
88
|
+
FLOAT_MAPPING = true
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
# Apply window-clipping; also
|
93
|
+
# always apply rescale (remap)
|
94
|
+
class WindowTransfer < Transfer
|
95
|
+
|
96
|
+
def initialize(options = {})
|
97
|
+
@center = options[:center]
|
98
|
+
@width = options[:width]
|
99
|
+
super options
|
100
|
+
end
|
101
|
+
|
102
|
+
def min_max(sequence)
|
103
|
+
# TODO: use options to sample/take first/take all?
|
104
|
+
dicom = sequence.first
|
105
|
+
data_range dicom
|
106
|
+
end
|
107
|
+
|
108
|
+
def min_max_rescaled?
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
112
|
+
def processed_data(dicom, min, max)
|
113
|
+
center = (min + max)/2
|
114
|
+
width = max - min
|
115
|
+
data = dicom_narray(dicom, level: [center, width])
|
116
|
+
map_to_output dicom, data, min, max
|
117
|
+
end
|
118
|
+
|
119
|
+
# def image(dicom, min, max)
|
120
|
+
# center = (min + max)/2
|
121
|
+
# width = max - min
|
122
|
+
# dicom.image(level: [center, width]).normalize
|
123
|
+
# end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def map_to_output(dicom, data, min, max)
|
128
|
+
output_min, output_max = min_max_limits(dicom)
|
129
|
+
output_range = output_max - output_min
|
130
|
+
input_range = max - min
|
131
|
+
float_arith = FLOAT_MAPPING || output_range < input_range
|
132
|
+
data_type = data.typecode
|
133
|
+
data = data.to_type(NArray::SFLOAT) if float_arith
|
134
|
+
data.sbt! min
|
135
|
+
data.mul! (output_range).to_f/(input_range)
|
136
|
+
data.add! output_min
|
137
|
+
data = data.to_type(data_type) if float_arith
|
138
|
+
data
|
139
|
+
end
|
140
|
+
|
141
|
+
def data_range(dicom)
|
142
|
+
if USE_DATA
|
143
|
+
if @center && @width
|
144
|
+
level = [@center, @width]
|
145
|
+
else
|
146
|
+
level = true
|
147
|
+
end
|
148
|
+
data = dicom_narray(dicom, level: level)
|
149
|
+
[data.min, data.max]
|
150
|
+
else
|
151
|
+
center = @center || dicom_window_center(dicom)
|
152
|
+
width = @width || dicom_window_width(dicom)
|
153
|
+
low = center - width/2
|
154
|
+
high = center + width/2
|
155
|
+
[low, high]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
# These strategies
|
162
|
+
# have optional dropping of the lowest level (base),
|
163
|
+
# photometric rescaling, extension factor
|
164
|
+
# and map the minimum/maximum input values
|
165
|
+
# (determined by the particular strategy and files)
|
166
|
+
# to the minimum/maximum output levels (black/white)
|
167
|
+
#
|
168
|
+
# +:ignore_min+ is used to ignore the minimum level present in the data
|
169
|
+
# and look for the next minimum value. Usually the absolute minimum
|
170
|
+
# corresponds to parts of the image outside the registered area and has
|
171
|
+
# typically the value -2048 for CT images. The next minimum value
|
172
|
+
# usually corresponds to air and is what's taken as the image's minimum
|
173
|
+
# when this option is set to +true+.
|
174
|
+
#
|
175
|
+
class RangeTransfer < Transfer
|
176
|
+
|
177
|
+
def initialize(options = {})
|
178
|
+
options = { ignore_min: true }.merge(options)
|
179
|
+
@rescale = options[:rescale]
|
180
|
+
@ignore_min = options[:ignore_min]
|
181
|
+
@extension_factor = options[:extend] || 0.0
|
182
|
+
super options
|
183
|
+
end
|
184
|
+
|
185
|
+
def min_max(sequence)
|
186
|
+
v0 = minimum = maximum = nil
|
187
|
+
select_dicoms(sequence) do |d|
|
188
|
+
d_v0, d_min, d_max = data_range(d)
|
189
|
+
v0 ||= d_v0
|
190
|
+
minimum ||= d_min
|
191
|
+
maximum ||= d_max
|
192
|
+
v0 = d_v0 if v0 && d_v0 && v0 > d_v0
|
193
|
+
minimum = d_min if minimum > d_min
|
194
|
+
maximum = d_max if maximum < d_max
|
195
|
+
end
|
196
|
+
[minimum, maximum]
|
197
|
+
end
|
198
|
+
|
199
|
+
def min_max_rescaled?
|
200
|
+
@rescale
|
201
|
+
end
|
202
|
+
|
203
|
+
def processed_data(dicom, min, max)
|
204
|
+
output_min, output_max = min_max_limits(dicom)
|
205
|
+
output_range = output_max - output_min
|
206
|
+
input_range = max - min
|
207
|
+
float_arith = FLOAT_MAPPING || output_range < input_range
|
208
|
+
data = dicom_narray(dicom, level: false, remap: @rescale)
|
209
|
+
data_type = data.typecode
|
210
|
+
data = data.to_type(NArray::SFLOAT) if float_arith
|
211
|
+
data.sbt! min
|
212
|
+
data.mul! output_range/input_range.to_f
|
213
|
+
data.add! output_min
|
214
|
+
data[data < output_min] = output_min
|
215
|
+
data[data > output_max] = output_max
|
216
|
+
data = data.to_type(data_type) if float_arith
|
217
|
+
data
|
218
|
+
end
|
219
|
+
|
220
|
+
private
|
221
|
+
|
222
|
+
def data_range(dicom)
|
223
|
+
data = dicom_narray(dicom, level: false, remap: @rescale)
|
224
|
+
base = nil
|
225
|
+
minimum = data.min
|
226
|
+
maximum = data.max
|
227
|
+
if @ignore_min
|
228
|
+
base = minimum
|
229
|
+
minimum = (data[data > base].min)
|
230
|
+
end
|
231
|
+
if @extension_factor != 0
|
232
|
+
# extend the range
|
233
|
+
minimum, maximum = extend_data_range(@extension_factor, base, minimum, maximum)
|
234
|
+
end
|
235
|
+
[base, minimum, maximum]
|
236
|
+
end
|
237
|
+
|
238
|
+
def extend_data_range(k, base, minimum, maximum)
|
239
|
+
k += 1.0
|
240
|
+
c = (maximum + minimum)/2
|
241
|
+
minimum = (c + k*(minimum - c)).round
|
242
|
+
maximum = (c + k*(maximum - c)).round
|
243
|
+
if base
|
244
|
+
minimum = base + 1 if minimum <= base
|
245
|
+
end
|
246
|
+
[minimum, maximum]
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
|
251
|
+
class FixedTransfer < RangeTransfer
|
252
|
+
|
253
|
+
def initialize(options = {})
|
254
|
+
@fixed_min = options[:min] || -2048
|
255
|
+
@fixed_max = options[:max] || +2048
|
256
|
+
options[:ignore_min] = false
|
257
|
+
options[:extend] = nil
|
258
|
+
unless options.key?(:rescale)
|
259
|
+
options[:rescale] = true
|
260
|
+
end
|
261
|
+
super options
|
262
|
+
end
|
263
|
+
|
264
|
+
def min_max(sequence)
|
265
|
+
# TODO: set default min, max regarding dicom data type
|
266
|
+
[@fixed_min, @fixed_max]
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
class GlobalTransfer < RangeTransfer
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
def select_dicoms(sequence, &blk)
|
276
|
+
sequence.each &blk
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
class FirstTransfer < RangeTransfer
|
282
|
+
|
283
|
+
def initialize(options = {})
|
284
|
+
extend = options[:extend] || 0.3
|
285
|
+
super options.merge(extend: extend)
|
286
|
+
end
|
287
|
+
|
288
|
+
private
|
289
|
+
|
290
|
+
def select_dicoms(sequence, &blk)
|
291
|
+
blk[sequence.first] if sequence.size > 0
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
class SampleTransfer < RangeTransfer
|
297
|
+
|
298
|
+
def initialize(options = {})
|
299
|
+
@max_files = options[:max_files] || 8
|
300
|
+
super options
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
|
305
|
+
def select_dicoms(sequence, &blk)
|
306
|
+
n = [sequence.size, @max_files].min
|
307
|
+
(0...sequence.size).to_a.sample(n).sort.each do |i|
|
308
|
+
blk[sequence.dicom(i)]
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
end
|
313
|
+
|
314
|
+
# Preserve internal values.
|
315
|
+
# Can be used with the output: :unsinged option
|
316
|
+
# to convert signed values to unsinged.
|
317
|
+
class IdentityTransfer < FixedTransfer
|
318
|
+
def initialize(options = {})
|
319
|
+
super options
|
320
|
+
end
|
321
|
+
|
322
|
+
def min_max(sequence)
|
323
|
+
min_max_limits(sequence.first)
|
324
|
+
end
|
325
|
+
|
326
|
+
def map_to_output(dicom, data, min, max)
|
327
|
+
if @rescale
|
328
|
+
intercept = dicom_rescale_intercept(dicom)
|
329
|
+
slope = dicom_rescale_slope(dicom)
|
330
|
+
if slope != 1 || intercept != 0
|
331
|
+
return super
|
332
|
+
end
|
333
|
+
end
|
334
|
+
data = dicom_narray(dicom)
|
335
|
+
data.add! -min if min < 0
|
336
|
+
data
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
class DicomS
|
2
|
+
ELEMENTS_TO_REMOVE = %w(0028,2110 0028,2112 0018,1151 0018,1152 0028,1055 7FE0,0000 7FE0,0010)
|
3
|
+
|
4
|
+
# When generating DICOMs back:
|
5
|
+
# Elements that must be replaced (because image format is not preserved)
|
6
|
+
# 0002,0010 Transfer Syntax UID
|
7
|
+
# replace by 1.2.840.10008.1.2.1 Implicit VR Little Endian
|
8
|
+
# or maybe 1.2.840.10008.1.2: Implicit VR Little Endian: Default Transfer Syntax for DICOM
|
9
|
+
# these should be removed if present:
|
10
|
+
# 0028,2110 Lossy Image Compression
|
11
|
+
# 0028,2112 Lossy Image Compression Ratio
|
12
|
+
# we need to adjust metadata elements (which refer to first slice)
|
13
|
+
# to each slice.
|
14
|
+
# elements that vary from slice to slice but whose variation probably doesn't matter:
|
15
|
+
# 0008,0033 Content Time
|
16
|
+
# elements that vary and should be removed:
|
17
|
+
# 0018,1151 X-Ray Tube Current
|
18
|
+
# 0018,1152 Exposure
|
19
|
+
# elements that should be adjusted:
|
20
|
+
# 0020,0013 Instance Number # increment by 1 for each slice
|
21
|
+
# 0020,0032 Image Position (Patient) # should be computed from additional metadata (dz)
|
22
|
+
# 0020,1041 Slice Location # should be computed from additional metadata (dz)
|
23
|
+
# elements that may need adjusting depending on value restoration method:
|
24
|
+
# 0028,0100 Bits Allocated (keep if value size is maintained)
|
25
|
+
# 0028,0101 Bits Stored make = Bits Allocated
|
26
|
+
# 0028,0102 High Bit make = BIts Stored - 1
|
27
|
+
# 0028,0103 Pixel Representation 0-unsigned 1-signed
|
28
|
+
# 0028,1050 Window Center
|
29
|
+
# 0028,1051 Window Width
|
30
|
+
# 0028,1052 Rescale Intercept
|
31
|
+
# 0028,1053 Rescale Slope
|
32
|
+
# 0028,1054 Rescale Type
|
33
|
+
# 0028,1055 Window Center & Width Explanation - can be removed if present
|
34
|
+
# elements that shouldn't vary:
|
35
|
+
# 0028,0002 Samples per Pixel = 1
|
36
|
+
# 0028,0004 Photometric Interpretation = MONOCHROME2
|
37
|
+
# also, these element should be removed (because Pixel data is set by assigning an image)
|
38
|
+
# 7FE0,0000 Group Length - can be omitted
|
39
|
+
# 7FE0,0010 Pixel Data - will be assigned by assigning an image
|
40
|
+
# other varying elements that need further study: (vary in some studies)
|
41
|
+
# 0002,0000 File Meta Information Group Length # drop this and 0002,0001?
|
42
|
+
# 0002,0003 Media Storage SOP Instance UID
|
43
|
+
# 0008,0018 SOP Instance UID
|
44
|
+
|
45
|
+
def unpack(pack_file, options = {})
|
46
|
+
options = CommandOptions[options]
|
47
|
+
|
48
|
+
progress = Progress.new('unpacking', options)
|
49
|
+
|
50
|
+
unpack_dir = options.path_option(:output,
|
51
|
+
File.basename(pack_file, '.mkv')
|
52
|
+
)
|
53
|
+
FileUtils.mkdir_p unpack_dir
|
54
|
+
|
55
|
+
prefix = File.basename(pack_file, '.mkv')
|
56
|
+
output_file_pattern = File.join(unpack_dir, "#{prefix}-%3d.jpeg")
|
57
|
+
|
58
|
+
progress.begin_subprocess 'extracting_images', -70
|
59
|
+
ffmpeg = SysCmd.command('ffmpeg', @ffmpeg_options) do
|
60
|
+
option '-y' # overwrite existing files
|
61
|
+
option '-hide_banner'
|
62
|
+
option '-loglevel', 'quiet'
|
63
|
+
option '-i', file: pack_file
|
64
|
+
option '-q:v', 2
|
65
|
+
file output_file_pattern
|
66
|
+
end
|
67
|
+
ffmpeg.run error_output: :separate
|
68
|
+
check_command ffmpeg
|
69
|
+
|
70
|
+
progress.begin_subprocess 'extracting_metadata', -10
|
71
|
+
metadata_file = File.join(unpack_dir, 'metadata.txt')
|
72
|
+
ffmpeg = SysCmd.command('ffmpeg', @ffmpeg_options) do
|
73
|
+
option '-y' # overwrite existing files
|
74
|
+
option '-hide_banner'
|
75
|
+
option '-loglevel', 'quiet'
|
76
|
+
option '-i', file: pack_file
|
77
|
+
option '-f', 'ffmetadata'
|
78
|
+
file metadata_file
|
79
|
+
end
|
80
|
+
ffmpeg.run error_output: :separate
|
81
|
+
check_command ffmpeg
|
82
|
+
|
83
|
+
dicom_elements, metadata = meta_codec.read_metadata(metadata_file)
|
84
|
+
metadata = cast_metadata(metadata)
|
85
|
+
|
86
|
+
metadata_yaml = File.join(unpack_dir, 'metadata.yml')
|
87
|
+
File.open(metadata_yaml, 'w') do |yaml|
|
88
|
+
yaml.write metadata.to_yaml
|
89
|
+
end
|
90
|
+
|
91
|
+
if options[:dicom_output]
|
92
|
+
dicom_directory = options.path_option(:dicom_output,
|
93
|
+
'DICOM'
|
94
|
+
)
|
95
|
+
img_files = File.join(unpack_dir, "#{prefix}-*.jpeg")
|
96
|
+
if options.roi
|
97
|
+
firstx, lastx, firsty, lasty, firstz, lastz = options.roi
|
98
|
+
columns = lastx - firstx + 1
|
99
|
+
rows = lasty - firsty + 1
|
100
|
+
else
|
101
|
+
rows = metadata.ny
|
102
|
+
columns = metadata.nx
|
103
|
+
end
|
104
|
+
progress.begin_subprocess 'generating_dicoms', 100, img_files.size
|
105
|
+
count = 0
|
106
|
+
pos = 0.0
|
107
|
+
slice_pos = 0.0
|
108
|
+
Dir[img_files].each do |fn, i|
|
109
|
+
count += 1
|
110
|
+
dicom_file = File.join(dicom_directory, File.basename(fn, '.jpeg')+'.dcm')
|
111
|
+
image = Magick::Image::read(fn).first
|
112
|
+
image_columns = image.columns
|
113
|
+
image_rows = image.rows
|
114
|
+
if image_columns != columns || image_rows != rows
|
115
|
+
raise "Inconsistent image size"
|
116
|
+
end
|
117
|
+
dicom = DICOM::DObject.new
|
118
|
+
dicom_elements.each do |element|
|
119
|
+
case element.tag
|
120
|
+
when '0002,0010'
|
121
|
+
# TODO: replace value by 1.2.840.10008.1.2.1
|
122
|
+
when *ELEMENTS_TO_REMOVE
|
123
|
+
element = nil
|
124
|
+
when '0020,0013'
|
125
|
+
element.value = count
|
126
|
+
when '0020,0032'
|
127
|
+
if count == 1
|
128
|
+
pos = element.value.split('\\').map(&:to_f)
|
129
|
+
else
|
130
|
+
pos[2] += metadata.dz
|
131
|
+
element.value = pos.join('\\')
|
132
|
+
end
|
133
|
+
when '0020,1041'
|
134
|
+
if count == 1
|
135
|
+
slice_pos = element.value.to_f
|
136
|
+
else
|
137
|
+
slice_pos += metadata.dz
|
138
|
+
element.value = slice_pos.to_f
|
139
|
+
end
|
140
|
+
when '0028,0010'
|
141
|
+
element.value = image_rows
|
142
|
+
when '0028,0011'
|
143
|
+
element.value = image_columns
|
144
|
+
when '0028,0101'
|
145
|
+
element.value = metadata.bits
|
146
|
+
when '0028,0102'
|
147
|
+
element.value = metadata.bits - 1
|
148
|
+
when '0028,0103'
|
149
|
+
element.value = metadata.signed
|
150
|
+
end
|
151
|
+
if element
|
152
|
+
DICOM::Element.new(element.tag, element.value, :parent => dicom)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
dicom.pixels = image_to_dicom_pixels(metadata, image)
|
156
|
+
dicom.write dicom_file
|
157
|
+
progress.update_subprocess count
|
158
|
+
end
|
159
|
+
end
|
160
|
+
progress.finish
|
161
|
+
end
|
162
|
+
|
163
|
+
def image_to_dicom_pixels(metadata, image)
|
164
|
+
min_v = metadata.min # value assigned to black
|
165
|
+
max_v = metadata.max # value assigned to white
|
166
|
+
if metadata.rescaled.to_i == 1
|
167
|
+
slope = metadata.slope
|
168
|
+
intercept = metadata.intercept
|
169
|
+
if slope != 1 || intercept != 0
|
170
|
+
# unscale
|
171
|
+
min_v = (min_v - intercept)/slope
|
172
|
+
max_v = (max_v - intercept)/slope
|
173
|
+
end
|
174
|
+
end
|
175
|
+
pixels = image.export_pixels(0, 0, image.columns, image.rows, 'I')
|
176
|
+
pixels = NArray.to_na(pixels).reshape!(image.columns, image.rows)
|
177
|
+
pixels = pixels.to_type(NArray::SFLOAT)
|
178
|
+
q = Magick::MAGICKCORE_QUANTUM_DEPTH
|
179
|
+
min_p, max_p = pixel_value_range(q, false)
|
180
|
+
# min_p => min_v; max_p => max_v
|
181
|
+
# pixels.sbt! min_p # not needed, min_p should be 0
|
182
|
+
pixels.mul! (max_v - min_v).to_f/(max_p - min_p)
|
183
|
+
pixels.add! min_v
|
184
|
+
bits = metadata.bits # original bit depth, in accordance with '0028,0100' if dicom metatada present
|
185
|
+
signed = metadata.signed == 1 ? true : false # in accordance with 0028,0103 if dicom metadata
|
186
|
+
if true
|
187
|
+
pixels = pixels.to_i
|
188
|
+
else
|
189
|
+
if bits > 16
|
190
|
+
if signed
|
191
|
+
pixels = pixels.to_type(3) # sint (signed, four bytes)
|
192
|
+
else
|
193
|
+
pixels = pixels.to_type(4) # sfloat (single precision float)
|
194
|
+
end
|
195
|
+
elsif bits > 8
|
196
|
+
if signed
|
197
|
+
pixels = pixels.to_type(2) # int (signed, two bytes)
|
198
|
+
else
|
199
|
+
pixels = pixels.to_type(3) # sint (signed, four bytes)
|
200
|
+
end
|
201
|
+
elsif signed
|
202
|
+
pixels = pixels.to_type(2) # sint (signed, two bytes)
|
203
|
+
else
|
204
|
+
pixels = pixels.to_type(1) # byte (unsiged)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
pixels
|
208
|
+
end
|
209
|
+
end
|