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
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# DicomS: DICOM Series toolkit
|
2
|
+
|
3
|
+
DicomS is a Ruby toolkit for working with DICOM (CT/MRI) Series
|
4
|
+
(image sequences that compose a volume of density information).
|
5
|
+
|
6
|
+
It can be used through a command line interface
|
7
|
+
by using the `dicoms` executable script, or
|
8
|
+
from a Ruby program through the 'DicomS' class interface (API).
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'dicoms'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install dicoms
|
25
|
+
|
26
|
+
### Requirements
|
27
|
+
|
28
|
+
* [FFmpeg](https://www.ffmpeg.org/) (the command line tools).
|
29
|
+
* [ImageMagick](http://www.imagemagick.org/) (the library whichs is used by RMagick)
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
The `dicoms` executable provides the following commands:
|
34
|
+
|
35
|
+
* Extract images: `dicoms extract DICOM-DIR ...`
|
36
|
+
* Generate projected images (on axial, sagittal and coronal planes):
|
37
|
+
`dicoms project DICOM-DIR ...`
|
38
|
+
* Pack a DICOM series in compact form: `dicoms pack DICOM-DIR ...`
|
39
|
+
* Unpack a packed DICOM series: `dicoms unpack PACKED-FILE ...`
|
40
|
+
|
41
|
+
Use the command to get further help.
|
42
|
+
|
43
|
+
## License
|
44
|
+
|
45
|
+
Copyright (c) 2015 Javier Goizueta
|
46
|
+
|
47
|
+
This software is licensed under the
|
48
|
+
[GNU General Public License](./LICENSE.md) version 3.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
t.libs << "test"
|
6
|
+
t.libs << "lib"
|
7
|
+
t.test_files = FileList['test/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'rdoc/task'
|
11
|
+
Rake::RDocTask.new do |rdoc|
|
12
|
+
version = DicomS::VERSION
|
13
|
+
|
14
|
+
rdoc.rdoc_dir = 'rdoc'
|
15
|
+
rdoc.title = "DicomS #{version}"
|
16
|
+
rdoc.main = "README.md"
|
17
|
+
rdoc.rdoc_files.include('README*')
|
18
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
19
|
+
rdoc.markup = 'markdown' if rdoc.respond_to?(:markup)
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default => :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dicompack"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/dicoms.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dicoms/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dicoms"
|
8
|
+
spec.version = DicomS::VERSION
|
9
|
+
spec.authors = ["Javier Goizueta"]
|
10
|
+
spec.email = ["jgoizueta@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{DICOM Series toolkit}
|
13
|
+
spec.description = %q{Toolkit for working with DICOM image sequences}
|
14
|
+
spec.homepage = "https://gitlab.com/jgoizueta/dicompacker"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'dicom'
|
22
|
+
# spec.add_dependency 'dicom', 'mini_magick'
|
23
|
+
spec.add_dependency 'rmagick', '~> 2.14'
|
24
|
+
spec.add_dependency 'sys_cmd', '>= 0.2.1'
|
25
|
+
spec.add_dependency 'modalsettings', '~> 1.0.1'
|
26
|
+
spec.add_dependency 'narray', '~> 0.6'
|
27
|
+
spec.add_dependency 'thor', '~> 0.19'
|
28
|
+
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "minitest"
|
33
|
+
end
|
data/exe/dicoms
ADDED
data/lib/dicoms.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'dicom'
|
3
|
+
require 'modalsettings'
|
4
|
+
require 'sys_cmd'
|
5
|
+
require 'narray'
|
6
|
+
|
7
|
+
require "dicoms/version"
|
8
|
+
require "dicoms/meta_codec"
|
9
|
+
require "dicoms/support"
|
10
|
+
require "dicoms/shared_files"
|
11
|
+
require "dicoms/shared_settings"
|
12
|
+
require "dicoms/progress"
|
13
|
+
require "dicoms/command_options"
|
14
|
+
require "dicoms/sequence"
|
15
|
+
require "dicoms/transfer"
|
16
|
+
require "dicoms/extract"
|
17
|
+
require "dicoms/pack"
|
18
|
+
require "dicoms/unpack"
|
19
|
+
require "dicoms/stats"
|
20
|
+
require "dicoms/projection"
|
21
|
+
require "dicoms/remap"
|
22
|
+
|
23
|
+
class DicomS
|
24
|
+
|
25
|
+
def initialize(options = {})
|
26
|
+
@settings = Settings[options]
|
27
|
+
|
28
|
+
if @settings.image_processor
|
29
|
+
DICOM.image_processor = @settings.image_processor.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
@ffmpeg_options = { 'ffmpeg' => @settings.ffmpeg }
|
33
|
+
# TODO: use quality level settings
|
34
|
+
# TODO: temporary strategy option (:current_dir, :system_tmp, ...)
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :settings
|
38
|
+
|
39
|
+
extend Support
|
40
|
+
include Support
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def meta_codec
|
45
|
+
MetaCodec.new
|
46
|
+
end
|
47
|
+
|
48
|
+
# def optimize_dynamic_range(data, output_min, output_max, options = {})
|
49
|
+
# minimum, maximum = options[:range]
|
50
|
+
# r = (maximum - minimum).to_f
|
51
|
+
# data -= minimum
|
52
|
+
# data *= (output_max - output_min)/r
|
53
|
+
# data += output_min
|
54
|
+
# data[data < output_min] = output_min
|
55
|
+
# data[data > output_max] = output_max
|
56
|
+
# data
|
57
|
+
# end
|
58
|
+
|
59
|
+
def check_command(command)
|
60
|
+
unless command.success?
|
61
|
+
puts "Error executing:"
|
62
|
+
puts " #{command}"
|
63
|
+
puts command.error_output
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/dicoms/cli.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
class DicomS
|
4
|
+
class CLI < Thor
|
5
|
+
check_unknown_options!
|
6
|
+
|
7
|
+
def self.exit_on_failure?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'version', "Display DicomS version"
|
12
|
+
map %w(-v --version) => :version
|
13
|
+
def version
|
14
|
+
say "dicoms #{VERSION}"
|
15
|
+
end
|
16
|
+
|
17
|
+
class_option 'verbose', type: :boolean, default: false
|
18
|
+
class_option 'settings', type: :string, desc: 'settings (read-only) file'
|
19
|
+
class_option 'settings_io', type: :string, desc: 'settings file'
|
20
|
+
|
21
|
+
desc "pack DICOM-DIR", "pack a DICOM directory"
|
22
|
+
option :output, desc: 'output file', aliases: '-o'
|
23
|
+
option :tmp, desc: 'temporary directory'
|
24
|
+
option :transfer, desc: 'transfer method', aliases: '-t', default: 'sample'
|
25
|
+
option :center, desc: 'center (window transfer)', aliases: '-c'
|
26
|
+
option :width, desc: 'window (window transfer)', aliases: '-w'
|
27
|
+
option :ignore_min, desc: 'ignore minimum (global/first/sample transfer)', aliases: '-i'
|
28
|
+
option :samples, desc: 'number of samples (sample transfer)', aliases: '-s'
|
29
|
+
option :min, desc: 'minimum value (fixed transfer)'
|
30
|
+
option :max, desc: 'maximum value (fixed transfer)'
|
31
|
+
option :reorder, desc: 'reorder slices based on instance number'
|
32
|
+
def pack(dicom_dir)
|
33
|
+
DICOM.logger.level = Logger::FATAL
|
34
|
+
strategy_parameters = {
|
35
|
+
ignore_min: true
|
36
|
+
}
|
37
|
+
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
|
+
cmd_options = CommandOptions[
|
43
|
+
settings: options.settings,
|
44
|
+
settings_io: options.settings_io,
|
45
|
+
output: options.output,
|
46
|
+
tmp: options.tmp,
|
47
|
+
reorder: options.reorder,
|
48
|
+
dicom_metadata: true
|
49
|
+
]
|
50
|
+
packer = DicomS.new(settings)
|
51
|
+
packer.pack dicom_dir, cmd_options
|
52
|
+
# rescue => raise Error?
|
53
|
+
0
|
54
|
+
end
|
55
|
+
|
56
|
+
desc "unpack dspack", "unpack a dspack file"
|
57
|
+
option :output, desc: 'output directory', aliases: '-o'
|
58
|
+
option :dicom, desc: 'dicom format output directory', aliases: '-d'
|
59
|
+
# TODO: parameters for dicom regeneration
|
60
|
+
def unpack(dspack)
|
61
|
+
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
|
+
settings = {} # TODO: ...
|
67
|
+
packer = DicomS.new(settings)
|
68
|
+
packer.unpack(
|
69
|
+
dspack,
|
70
|
+
settings: options.settings,
|
71
|
+
settings_io: options.settings_io,
|
72
|
+
output: options.output,
|
73
|
+
dicom_output: options.dicom
|
74
|
+
)
|
75
|
+
# rescue => raise Error?
|
76
|
+
0
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "extract DICOM-DIR", "extract images from a set of DICOM files"
|
80
|
+
option :output, desc: 'output directory', aliases: '-o'
|
81
|
+
option :transfer, desc: 'transfer method', aliases: '-t', default: 'window'
|
82
|
+
option :center, desc: 'center (window transfer)', aliases: '-c'
|
83
|
+
option :width, desc: 'window (window transfer)', aliases: '-w'
|
84
|
+
option :ignore_min, desc: 'ignore minimum (global/first/sample transfer)', aliases: '-i'
|
85
|
+
option :samples, desc: 'number of samples (sample transfer)', aliases: '-s'
|
86
|
+
option :min, desc: 'minimum value (fixed transfer)'
|
87
|
+
option :max, desc: 'maximum value (fixed transfer)'
|
88
|
+
option :raw, desc: 'generate raw output', aliases: '-r'
|
89
|
+
option :big, desc: 'big-endian raw output'
|
90
|
+
option :little, desc: 'little-endian raw output'
|
91
|
+
def extract(dicom_dir)000
|
92
|
+
DICOM.logger.level = Logger::FATAL
|
93
|
+
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
|
+
|
99
|
+
raw = options.raw
|
100
|
+
if options.big
|
101
|
+
raw = true
|
102
|
+
big_endian = true
|
103
|
+
elsif options.little
|
104
|
+
raw = true
|
105
|
+
little_endian = true
|
106
|
+
end
|
107
|
+
|
108
|
+
packer = DicomS.new(settings)
|
109
|
+
packer.extract(
|
110
|
+
dicom_dir,
|
111
|
+
transfer: DicomS.transfer_options(options),
|
112
|
+
output: options.output,
|
113
|
+
raw: raw, big_endian: big_endian, little_endian: little_endian
|
114
|
+
)
|
115
|
+
# rescue => raise Error?
|
116
|
+
0
|
117
|
+
end
|
118
|
+
|
119
|
+
desc "Level stats", "Level limits of one or more DICOM files"
|
120
|
+
def stats(dicom_dir)
|
121
|
+
DICOM.logger.level = Logger::FATAL
|
122
|
+
settings = {} # TODO: ...
|
123
|
+
dicoms = DicomS.new(settings)
|
124
|
+
stats = dicoms.stats dicom_dir
|
125
|
+
puts "Aggregate values for #{stats[:n]} DICOM files:"
|
126
|
+
puts " Minimum level: #{stats[:min]}"
|
127
|
+
puts " Next minimum level: #{stats[:next_min]}"
|
128
|
+
puts " Maximum level: #{stats[:max]}"
|
129
|
+
0
|
130
|
+
end
|
131
|
+
|
132
|
+
desc "projection DICOM-DIR", "extract projected images from a DICOM sequence"
|
133
|
+
option :output, desc: 'output directory', aliases: '-o'
|
134
|
+
option :axial, desc: 'N for single slice, * all, C center, mip or aap for volumetric aggregation'
|
135
|
+
option :sagittal, desc: 'N for single slice, * all, C center, mip or aap for volumetric aggregation'
|
136
|
+
option :coronal, desc: 'N for single slice, * all, C center, mip or aap for volumetric aggregation'
|
137
|
+
option :transfer, desc: 'transfer method', aliases: '-t', default: 'window'
|
138
|
+
# option :byte, desc: 'transfer as bytes', aliases: '-b'
|
139
|
+
option :center, desc: 'center (window transfer)', aliases: '-c'
|
140
|
+
option :width, desc: 'window (window transfer)', aliases: '-w'
|
141
|
+
option :ignore_min, desc: 'ignore minimum (global/first/sample transfer)', aliases: '-i'
|
142
|
+
option :samples, desc: 'number of samples (sample transfer)', aliases: '-s'
|
143
|
+
option :min, desc: 'minimum value (fixed transfer)'
|
144
|
+
option :max, desc: 'maximum value (fixed transfer)'
|
145
|
+
option :max_x_pixels, desc: 'maximum number of pixels in the X direction'
|
146
|
+
option :max_y_pixels, desc: 'maximum number of pixels in the Y direction'
|
147
|
+
option :max_z_pixels, desc: 'maximum number of pixels in the Z direction'
|
148
|
+
option :reorder, desc: 'reorder slices based on instance number'
|
149
|
+
def projection(dicom_dir)
|
150
|
+
DICOM.logger.level = Logger::FATAL
|
151
|
+
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
|
+
if options.settings_io || options.settings
|
157
|
+
cmd_options = CommandOptions[
|
158
|
+
settings: options.settings,
|
159
|
+
settings_io: options.settings_io,
|
160
|
+
output: options.output,
|
161
|
+
max_x_pixels: options.max_x_pixels && options.max_x_pixels.to_i,
|
162
|
+
max_y_pixels: options.max_y_pixels && options.max_y_pixels.to_i,
|
163
|
+
max_z_pixels: options.max_z_pixels && options.max_z_pixels.to_i,
|
164
|
+
reorder: options.reorder,
|
165
|
+
]
|
166
|
+
else
|
167
|
+
cmd_options = CommandOptions[
|
168
|
+
transfer: DicomS.transfer_options(options),
|
169
|
+
output: options.output,
|
170
|
+
axial: options.axial == 'axial' ? 'mip' : options.axial,
|
171
|
+
sagittal: options.sagittal == 'sagittal' ? 'mip' : options.sagittal,
|
172
|
+
coronal: options.coronal == 'coronal' ? 'mip' : options.coronal,
|
173
|
+
max_x_pixels: options.max_x_pixels && options.max_x_pixels.to_i,
|
174
|
+
max_y_pixels: options.max_y_pixels && options.max_y_pixels.to_i,
|
175
|
+
max_z_pixels: options.max_z_pixels && options.max_z_pixels.to_i,
|
176
|
+
reorder: options.reorder,
|
177
|
+
]
|
178
|
+
end
|
179
|
+
unless cmd_options.axial || options.sagittal || options.coronal
|
180
|
+
raise Error, "Must specify at least one projection (axial/sagittal/coronal)"
|
181
|
+
end
|
182
|
+
packer = DicomS.new(settings)
|
183
|
+
packer.projection(dicom_dir, cmd_options)
|
184
|
+
# rescue => raise Error?
|
185
|
+
0
|
186
|
+
end
|
187
|
+
|
188
|
+
desc "Remap DICOM-DIR", "convert DICOM pixel values"
|
189
|
+
option :output, desc: 'output directory', aliases: '-o'
|
190
|
+
option :transfer, desc: 'transfer method', aliases: '-t', default: 'identity'
|
191
|
+
option :unsigned, desc: 'transfer as unsigned', aliases: '-u'
|
192
|
+
# option :byte, desc: 'transfer as bytes', aliases: '-b'
|
193
|
+
option :center, desc: 'center (window transfer)', aliases: '-c'
|
194
|
+
option :width, desc: 'window (window transfer)', aliases: '-w'
|
195
|
+
option :ignore_min, desc: 'ignore minimum (global/first/sample transfer)', aliases: '-i'
|
196
|
+
option :samples, desc: 'number of samples (sample transfer)', aliases: '-s'
|
197
|
+
option :min, desc: 'minimum value (fixed transfer)'
|
198
|
+
option :max, desc: 'maximum value (fixed transfer)'
|
199
|
+
def remap(dicom_dir)
|
200
|
+
DICOM.logger.level = Logger::FATAL
|
201
|
+
settings = {} # TODO: ...
|
202
|
+
unless File.directory?(dicom_dir)
|
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,
|
209
|
+
transfer: DicomS.transfer_options(options),
|
210
|
+
output: options.output
|
211
|
+
)
|
212
|
+
# rescue => raise Error?
|
213
|
+
0
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class <<self
|
218
|
+
def transfer_options(options)
|
219
|
+
strategy = options.transfer.to_sym
|
220
|
+
params = {}
|
221
|
+
params[:output] = :unsigned if options.unsigned
|
222
|
+
params[:output] = :byte if options.byte
|
223
|
+
params[:center] = options.center.to_f if options.center
|
224
|
+
params[:width] = options.width.to_f if options.width
|
225
|
+
if options.ignore_min
|
226
|
+
params[:ignore_min] = true
|
227
|
+
elsif [:global, :first, :sample].include?(strategy)
|
228
|
+
params[:ignore_min] = false
|
229
|
+
end
|
230
|
+
params[:max_files] = options.samples if options.samples
|
231
|
+
params[:min] = options[:min].to_f if options[:min]
|
232
|
+
params[:max] = options[:max].to_f if options[:max]
|
233
|
+
|
234
|
+
if params.empty?
|
235
|
+
strategy
|
236
|
+
else
|
237
|
+
[strategy, params]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|