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