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,61 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class DicomS
|
5
|
+
|
6
|
+
# Shared files can be concurrently accessed by diferent processes.
|
7
|
+
# They should never be large files, and update operations (reading,
|
8
|
+
# then writing) should always be quick, because processes trying to
|
9
|
+
# read the file while an update is going on are blocked.
|
10
|
+
#
|
11
|
+
# Example
|
12
|
+
#
|
13
|
+
# counter = SharedFile.new('counter')
|
14
|
+
#
|
15
|
+
# counter.update do |contents|
|
16
|
+
# contents.to_i + 1
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# counter = counter.read.to_i
|
20
|
+
#
|
21
|
+
class SharedFile
|
22
|
+
def initialize(name, options = {})
|
23
|
+
@name = name
|
24
|
+
raise "A directory exists with that name" if File.directory?(@name)
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :name
|
28
|
+
|
29
|
+
def exists?
|
30
|
+
File.exists?(@name)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Update a file safely.
|
34
|
+
def update(&blk)
|
35
|
+
File.open(@name, File::RDWR|File::CREAT, 0644) do |file|
|
36
|
+
file.flock File::LOCK_EX
|
37
|
+
if blk.arity == 1
|
38
|
+
new_contents = blk.call(file.read)
|
39
|
+
else
|
40
|
+
new_contents = blk.call
|
41
|
+
end
|
42
|
+
file.rewind
|
43
|
+
file.write new_contents
|
44
|
+
file.flush
|
45
|
+
file.truncate file.pos
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Read a file safely
|
50
|
+
def read
|
51
|
+
File.open(@name, "r") do |file|
|
52
|
+
file.flock File::LOCK_SH # this blocks until available
|
53
|
+
file.read
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def write(contents)
|
58
|
+
update { contents }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
class DicomS
|
5
|
+
|
6
|
+
# Shared Settings are Settings stored in a SharedFile so
|
7
|
+
# they can be used concurrently from different processes.
|
8
|
+
class SharedSettings
|
9
|
+
def initialize(name, options = {})
|
10
|
+
@file = SharedFile.new(name)
|
11
|
+
@format = options[:format]
|
12
|
+
@compact = options[:compact]
|
13
|
+
unless @format
|
14
|
+
if File.extname(@file.name) == '.json'
|
15
|
+
@format = :json
|
16
|
+
else
|
17
|
+
@format = :yaml
|
18
|
+
end
|
19
|
+
end
|
20
|
+
contents = options[:initial_contents]
|
21
|
+
if contents && !@file.exists?
|
22
|
+
# Create with given initial contents
|
23
|
+
write contents
|
24
|
+
end
|
25
|
+
contents = options[:replace_contents]
|
26
|
+
write contents if contents
|
27
|
+
end
|
28
|
+
|
29
|
+
# Read a shared file and obtain a Settings object
|
30
|
+
#
|
31
|
+
# counter = shared_settings.read.counter
|
32
|
+
#
|
33
|
+
def read
|
34
|
+
decode @file.read
|
35
|
+
end
|
36
|
+
|
37
|
+
# To make sure contents are not changed between reading and writing
|
38
|
+
# use update:
|
39
|
+
#
|
40
|
+
# shared_settings.update do |data|
|
41
|
+
# # modify data and return modified data
|
42
|
+
# data.counter += 1
|
43
|
+
# data
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
def update(&blk)
|
47
|
+
@file.update do |data|
|
48
|
+
encode blk.call(decode(data))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Use this only if the contents written is independet of
|
53
|
+
# the previous content (i.e. no need to read, the change the data
|
54
|
+
# and write it back)
|
55
|
+
#
|
56
|
+
# shared_settings.write Setting[counter: 0
|
57
|
+
#
|
58
|
+
def write(data)
|
59
|
+
@file.write encode(data)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def encode(data)
|
65
|
+
if data.is_a?(Settings) || data.is_a?(Hash)
|
66
|
+
# Specially for YAML, we don't want to use symbols for
|
67
|
+
# hash keys because if read with languages other than
|
68
|
+
# Ruby that may cause some troubles.
|
69
|
+
data = stringify_keys(data.to_h)
|
70
|
+
end
|
71
|
+
case @format
|
72
|
+
when :json
|
73
|
+
if @compact
|
74
|
+
JSON.dump data
|
75
|
+
else
|
76
|
+
JSON.pretty_generate data
|
77
|
+
end
|
78
|
+
when :yaml
|
79
|
+
data.to_yaml
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def decode(data)
|
84
|
+
case @format
|
85
|
+
when :json
|
86
|
+
data = JSON.load(data)
|
87
|
+
when :yaml
|
88
|
+
data = YAML.load(data)
|
89
|
+
end
|
90
|
+
if data.is_a?(Hash)
|
91
|
+
Settings[data]
|
92
|
+
else
|
93
|
+
data
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Convert symbolic keys to strings in a hash recursively
|
98
|
+
def stringify_keys(data)
|
99
|
+
if data.is_a?(Hash)
|
100
|
+
Hash[
|
101
|
+
data.map do |k, v|
|
102
|
+
[k.respond_to?(:to_sym) ? k.to_s : k, stringify_keys(v)]
|
103
|
+
end
|
104
|
+
]
|
105
|
+
else
|
106
|
+
data
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/lib/dicoms/stats.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
class DicomS
|
2
|
+
def stats(dicom_directory, options = {})
|
3
|
+
# TODO: compute histogram of levels
|
4
|
+
dicom_files = find_dicom_files(dicom_directory)
|
5
|
+
if dicom_files.empty?
|
6
|
+
raise "ERROR: no se han encontrado archivos DICOM en: \n #{dicom_directory}"
|
7
|
+
end
|
8
|
+
|
9
|
+
mins = []
|
10
|
+
maxs = []
|
11
|
+
next_mins = []
|
12
|
+
n = 0
|
13
|
+
|
14
|
+
dicom_files.each do |file|
|
15
|
+
n += 1
|
16
|
+
d = DICOM::DObject.read(file)
|
17
|
+
data = dicom_narray(d)
|
18
|
+
min = data.min
|
19
|
+
mins << min
|
20
|
+
maxs << data.max
|
21
|
+
next_mins << data[data > min].min
|
22
|
+
end
|
23
|
+
{
|
24
|
+
n: n,
|
25
|
+
min: mins.min,
|
26
|
+
next_min: next_mins.min,
|
27
|
+
max: maxs.max
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,349 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
USE_SLICE_Z = false
|
5
|
+
METADATA_TYPES = {
|
6
|
+
# Note: for axisx, axisy, axisz decode_vector should be used
|
7
|
+
dx: :to_f, dy: :to_f, dz: :to_f,
|
8
|
+
nx: :to_i, ny: :to_i, nz: :to_i,
|
9
|
+
max: :to_i, min: :to_i,
|
10
|
+
lim_min: :to_i, lim_max: :to_i,
|
11
|
+
rescaled: :to_i, # 0-false 1-true
|
12
|
+
slope: :to_f, intercept: :to_f,
|
13
|
+
bits: :to_i,
|
14
|
+
signed: :to_i, # 0-false 1-true
|
15
|
+
firstx: :to_i, firsty: :to_i, firstz: :to_i,
|
16
|
+
lastx: :to_i, lasty: :to_i, lastz: :to_i,
|
17
|
+
study_id: :to_s, series_id: :to_i,
|
18
|
+
x: :to_f, y: :to_f, z: :to_f,
|
19
|
+
slize_z: :to_f,
|
20
|
+
reverse_x: :to_i, # 0-false 1-true
|
21
|
+
reverse_y: :to_i, # 0-false 1-true
|
22
|
+
reverse_z: :to_i, # 0-false 1-true
|
23
|
+
axial_sx: :to_f,
|
24
|
+
axial_sy: :to_f,
|
25
|
+
coronal_sx: :to_f,
|
26
|
+
coronal_sy: :to_f,
|
27
|
+
sagittal_sx: :to_f,
|
28
|
+
sagittal_sy: :to_f
|
29
|
+
}
|
30
|
+
|
31
|
+
module Support
|
32
|
+
def cast_metadata(metadata)
|
33
|
+
metadata = Hash[metadata.to_h.to_a.map { |key, value|
|
34
|
+
key = key.to_s.downcase.to_sym
|
35
|
+
trans = METADATA_TYPES[key]
|
36
|
+
value = value.send(trans) if trans
|
37
|
+
[key, value]
|
38
|
+
}]
|
39
|
+
Settings[metadata]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Code that use images should be wrapped with this.
|
43
|
+
#
|
44
|
+
# Reason: if RMagick is used by DICOM to handle images,
|
45
|
+
# then the first time it is needed, 'rmagick' will be required.
|
46
|
+
# This has the effect of placing the path of ImageMagick
|
47
|
+
# in front of the PATH.
|
48
|
+
# On Windows, ImageMagick includes FFMPeg in its path and we
|
49
|
+
# may require a later version than the bundled with IM,
|
50
|
+
# so we keep the original path rbefore RMagick alters it.
|
51
|
+
# We may be less dependant on the FFMpeg version is we avoid
|
52
|
+
# using the start_number option by renumbering the extracted
|
53
|
+
# images...
|
54
|
+
def keeping_path
|
55
|
+
path = ENV['PATH']
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
ENV['PATH'] = path
|
59
|
+
end
|
60
|
+
|
61
|
+
# Replace ALT_SEPARATOR in pathname (Windows)
|
62
|
+
def normalized_path(path)
|
63
|
+
if File::ALT_SEPARATOR
|
64
|
+
path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
|
65
|
+
else
|
66
|
+
path
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def dicom?(file)
|
71
|
+
ok = false
|
72
|
+
if File.file?(file)
|
73
|
+
File.open(file, 'rb') do |data|
|
74
|
+
data.seek 128, IO::SEEK_SET # skip preamble
|
75
|
+
ok = (data.read(4) == 'DICM')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
ok
|
79
|
+
end
|
80
|
+
|
81
|
+
# Find DICOM files in a directory;
|
82
|
+
# Return the file names in an array.
|
83
|
+
# DICOM files with a numeric part in the name are returned first, ordered
|
84
|
+
# by the numeric value.
|
85
|
+
# DICOM files with non-numeric names are returned last ordered by name.
|
86
|
+
def find_dicom_files(dicom_directory)
|
87
|
+
# TODO: look recursively inside nested directories
|
88
|
+
if File.directory?(dicom_directory)
|
89
|
+
dicom_directory = normalized_path(dicom_directory)
|
90
|
+
files = Dir.glob(File.join(dicom_directory, '*')).select{|f| dicom?(f)}
|
91
|
+
elsif File.file?(dicom_directory) && dicom?(dicom_directory)
|
92
|
+
files = [dicom_directory]
|
93
|
+
else
|
94
|
+
files = []
|
95
|
+
end
|
96
|
+
non_numeric = []
|
97
|
+
numeric_files = []
|
98
|
+
files.each do |name|
|
99
|
+
base = File.basename(name)
|
100
|
+
match = /\d+/.match(base)
|
101
|
+
if match
|
102
|
+
number = match[0]
|
103
|
+
if base =~ /\AI\d\d\d\d\d\d\d\Z/
|
104
|
+
# funny scheme found in some DICOMS:
|
105
|
+
# the I is followed by the instance number (unpadded), then right
|
106
|
+
# padded with zeros, then increased (which affects the last digit)
|
107
|
+
# while it coincides with some prior value.
|
108
|
+
match = /I(\d\d\d\d)/.match(base)
|
109
|
+
number = match[1]
|
110
|
+
number = number[0...-1] while number.size > 1 && number[-1] == '0'
|
111
|
+
number_zeros = name[-1].to_i
|
112
|
+
number << '0'*number_zeros
|
113
|
+
end
|
114
|
+
numeric_files << [number, name]
|
115
|
+
else
|
116
|
+
non_numeric << name
|
117
|
+
end
|
118
|
+
end
|
119
|
+
numeric_files.sort_by{ |text, name| text.to_i }.map(&:last) + non_numeric.sort
|
120
|
+
end
|
121
|
+
|
122
|
+
def single_dicom_metadata(dicom)
|
123
|
+
# 0028,0030 Pixel Spacing:
|
124
|
+
dx, dy = dicom.pixel_spacing.value.split('\\').map(&:to_f)
|
125
|
+
# 0020,0032 Image Position (Patient):
|
126
|
+
x, y, z = dicom.image_position_patient.value.split('\\').map(&:to_f)
|
127
|
+
# 0020,0037 Image Orientation (Patient):
|
128
|
+
xx, xy, xz, yx, yy, yz = dicom.image_orientation_patient.value.split('\\').map(&:to_f)
|
129
|
+
if USE_SLICE_Z
|
130
|
+
# according to http://www.vtk.org/Wiki/VTK/FAQ#The_spacing_in_my_DICOM_files_are_wrong
|
131
|
+
# this is not reliable
|
132
|
+
# 0020,1041 Slice Location:
|
133
|
+
slice_z = dicom.slice_location.value.to_f
|
134
|
+
else
|
135
|
+
slice_z = z
|
136
|
+
end
|
137
|
+
|
138
|
+
# 0028,0011 Columns :
|
139
|
+
nx = dicom.num_cols # dicom.columns.value.to_i
|
140
|
+
# 0028,0010 Rows:
|
141
|
+
ny = dicom.num_rows # dicom.rows.value.to_i
|
142
|
+
|
143
|
+
unless dicom.samples_per_pixel.value.to_i == 1
|
144
|
+
raise "Invalid DICOM format"
|
145
|
+
end
|
146
|
+
Settings[
|
147
|
+
dx: dx, dy: dy, x: x, y: y, z: z,
|
148
|
+
slice_z: slice_z, nx: nx, ny: ny,
|
149
|
+
xaxis: encode_vector([xx,xy,xz]),
|
150
|
+
yaxis: encode_vector([yx,yy,yz])
|
151
|
+
# TODO: + min, max (original values corresponding to 0, 255)
|
152
|
+
]
|
153
|
+
end
|
154
|
+
|
155
|
+
def encode_vector(v)
|
156
|
+
v.to_a*','
|
157
|
+
end
|
158
|
+
|
159
|
+
def decode_vector(v)
|
160
|
+
Vector[*v.split(',').map(&:to_f)]
|
161
|
+
end
|
162
|
+
|
163
|
+
def output_file_name(dir, prefix, name, ext = '.jpg')
|
164
|
+
File.join dir, "#{prefix}#{File.basename(name,'.dcm')}#{ext}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def dicom_name_pattern(name, output_dir)
|
168
|
+
dir = File.dirname(name)
|
169
|
+
file = File.basename(name)
|
170
|
+
number_pattern = /\d+/
|
171
|
+
match = number_pattern.match(file)
|
172
|
+
raise "Invalid DICOM file name" unless match
|
173
|
+
number = match[0]
|
174
|
+
file = file.sub(number_pattern, "%d")
|
175
|
+
if match.begin(0) == 0
|
176
|
+
# ffmpeg has troubles with filename patterns starting with digits, so we'll add a prefix
|
177
|
+
prefix = "d-"
|
178
|
+
else
|
179
|
+
prefix = nil
|
180
|
+
end
|
181
|
+
pattern = output_file_name(output_dir, prefix, file)
|
182
|
+
[prefix, pattern, number]
|
183
|
+
end
|
184
|
+
|
185
|
+
def define_transfer(options, *defaults)
|
186
|
+
strategy, params = Array(options[:transfer])
|
187
|
+
|
188
|
+
unless defaults.first.is_a?(Hash)
|
189
|
+
default_strategy = defaults.shift.to_sym
|
190
|
+
end
|
191
|
+
defautl_strategy ||= :sample
|
192
|
+
default_params = defaults.shift || {}
|
193
|
+
raise "Invalid number of parametrs" unless defaults.empty?
|
194
|
+
Transfer.strategy strategy || default_strategy, default_params.merge((params || {}).to_h)
|
195
|
+
end
|
196
|
+
|
197
|
+
def pixel_value_range(num_bits, signed)
|
198
|
+
num_values = (1 << num_bits) # 2**num_bits
|
199
|
+
if signed
|
200
|
+
[-num_values/2, num_values/2-1]
|
201
|
+
else
|
202
|
+
[0, num_values-1]
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def dicom_element_value(dicom, tag, options = {})
|
207
|
+
if dicom.exists?(tag)
|
208
|
+
value = dicom[tag].value
|
209
|
+
if options[:first]
|
210
|
+
if value.is_a?(String)
|
211
|
+
value = value.split('\\').first
|
212
|
+
elsif value.is_a?(Array)
|
213
|
+
value = value.first
|
214
|
+
end
|
215
|
+
end
|
216
|
+
value = value.send(options[:convert]) if options[:convert]
|
217
|
+
value
|
218
|
+
else
|
219
|
+
options[:default]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# WL (window level)
|
224
|
+
def dicom_window_center(dicom)
|
225
|
+
# dicom.window_center.value.to_i
|
226
|
+
dicom_element_value(dicom, '0028,1050', convert: :to_f, first: true)
|
227
|
+
end
|
228
|
+
|
229
|
+
# WW (window width)
|
230
|
+
def dicom_window_width(dicom)
|
231
|
+
# dicom.window_center.value.to_i
|
232
|
+
dicom_element_value(dicom, '0028,1051', convert: :to_f, first: true)
|
233
|
+
end
|
234
|
+
|
235
|
+
def dicom_rescale_intercept(dicom)
|
236
|
+
dicom_element_value(dicom, '0028,1052', convert: :to_f, default: 0)
|
237
|
+
end
|
238
|
+
|
239
|
+
def dicom_rescale_slope(dicom)
|
240
|
+
dicom_element_value(dicom, '0028,1053', convert: :to_f, default: 1)
|
241
|
+
end
|
242
|
+
|
243
|
+
def dicom_bit_depth(dicom)
|
244
|
+
# dicom.send(:bit_depth)
|
245
|
+
dicom_element_value dicom, '0028,0100', convert: :to_i
|
246
|
+
end
|
247
|
+
|
248
|
+
def dicom_signed?(dicom)
|
249
|
+
# dicom.send(:signed_pixels?)
|
250
|
+
case dicom_element_value(dicom, '0028,0103', convert: :to_i)
|
251
|
+
when 1
|
252
|
+
true
|
253
|
+
when 0
|
254
|
+
false
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def dicom_stored_bits(dicom)
|
259
|
+
# dicom.bits_stored.value.to_i
|
260
|
+
dicom_element_value dicom, '0028,0101', convert: :to_i
|
261
|
+
end
|
262
|
+
|
263
|
+
def dicom_narray(dicom, options = {})
|
264
|
+
if dicom.compression?
|
265
|
+
img = dicom.image
|
266
|
+
pixels = dicom.export_pixels(img, dicom.send(:photometry))
|
267
|
+
na = NArray.to_na(pixels).reshape!(dicom.num_cols, dicom.num_rows)
|
268
|
+
bits = dicom_bit_depth(dicom)
|
269
|
+
signed = dicom_signed?(dicom)
|
270
|
+
stored_bits = dicom_stored_bits(dicom)
|
271
|
+
if stored_bits != Magick::MAGICKCORE_QUANTUM_DEPTH
|
272
|
+
use_float = stored_bits < Magick::MAGICKCORE_QUANTUM_DEPTH
|
273
|
+
if use_float
|
274
|
+
na = na.to_type(NArray::SFLOAT)
|
275
|
+
na.mul! 2.0**(stored_bits - Magick::MAGICKCORE_QUANTUM_DEPTH)
|
276
|
+
na = na.to_type(NArray::INT)
|
277
|
+
else
|
278
|
+
na.mul! (1 << (stored_bits - Magick::MAGICKCORE_QUANTUM_DEPTH))
|
279
|
+
end
|
280
|
+
end
|
281
|
+
if remap = options[:remap] || level = options[:level]
|
282
|
+
intercept = dicom_rescale_intercept(dicom)
|
283
|
+
slope = dicom_rescale_slope(dicom)
|
284
|
+
if intercept != 0 || slope != 1
|
285
|
+
na.mul! slope
|
286
|
+
na.add! intercept
|
287
|
+
end
|
288
|
+
if level
|
289
|
+
if level.is_a?(Array)
|
290
|
+
center, width = level
|
291
|
+
else
|
292
|
+
center = dicom_window_center(dicom)
|
293
|
+
width = dicom_window_width(dicom)
|
294
|
+
end
|
295
|
+
if center && width
|
296
|
+
low = center - width/2
|
297
|
+
high = center + width/2
|
298
|
+
na[na < low] = low
|
299
|
+
na[na > high] = high
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Now we limit the output values range.
|
304
|
+
# Note that we don't use:
|
305
|
+
# min, max = pixel_value_range(bits, signed)
|
306
|
+
# because thats the limits for the stored values, but not for
|
307
|
+
# the representation values we're computing here (which are
|
308
|
+
# typically signed even if the storage is unsigned)
|
309
|
+
# We coud use this, but that would have to be
|
310
|
+
# min, max = pixel_value_range(stored_bits, false)
|
311
|
+
# min = -max
|
312
|
+
# but that requires some reviewing.
|
313
|
+
# Maybe this shold be parameterized.
|
314
|
+
min, max = -65535, 65535
|
315
|
+
min_pixel_value = na.min
|
316
|
+
if min
|
317
|
+
if min_pixel_value < min
|
318
|
+
offset = min_pixel_value.abs
|
319
|
+
na.add! offset
|
320
|
+
end
|
321
|
+
end
|
322
|
+
max_pixel_value = na.max
|
323
|
+
if max
|
324
|
+
if max_pixel_value > max
|
325
|
+
factor = (max_pixel_value.to_f/max.to_f).ceil
|
326
|
+
na.div! factor
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
na
|
332
|
+
else
|
333
|
+
dicom.narray options
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def assign_dicom_pixels(dicom, pixels)
|
338
|
+
if dicom.compression?
|
339
|
+
dicom.delete DICOM::PIXEL_TAG
|
340
|
+
end
|
341
|
+
dicom.pixels = pixels
|
342
|
+
end
|
343
|
+
|
344
|
+
def dicom_compression(dicom)
|
345
|
+
ts = DICOM::LIBRARY.uid(dicom.transfer_syntax)
|
346
|
+
ts.name if ts.compressed_pixels?
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|