nifti 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -3
- data/CHANGELOG +10 -0
- data/LICENSE +165 -0
- data/README.markdown +30 -34
- data/Rakefile +16 -0
- data/features/n_image.feature +18 -0
- data/features/step_definitions/n_image_steps.rb +33 -0
- data/features/support/env.rb +11 -0
- data/features/support/fixtures/brain_dti.nii.gz +0 -0
- data/features/support/hooks.rb +3 -0
- data/lib/nifti.rb +1 -0
- data/lib/nifti/n_image.rb +110 -0
- data/lib/nifti/n_object.rb +17 -10
- data/lib/nifti/n_read.rb +52 -46
- data/lib/nifti/n_write.rb +29 -14
- data/lib/nifti/version.rb +1 -1
- data/nifti.gemspec +6 -3
- data/spec/custom_matchers.rb +1 -0
- data/spec/fixtures/3plLoc.nii.gz +0 -0
- data/spec/nifti/n_image_spec.rb +76 -0
- data/spec/nifti/n_object_spec.rb +23 -18
- data/spec/nifti/n_read_spec.rb +126 -60
- data/spec/nifti/n_write_spec.rb +71 -26
- data/spec/spec_helper.rb +19 -0
- metadata +111 -78
- data/COPYING +0 -674
- data/Gemfile.lock +0 -30
data/lib/nifti/n_object.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module NIFTI
|
2
2
|
# The NObject class is the main class for interacting with the NIFTI object.
|
3
3
|
# Reading from and writing to files is executed from instances of this class.
|
4
|
-
#
|
4
|
+
#
|
5
5
|
class NObject
|
6
6
|
# An array which contain any notices/warnings/errors that have been recorded for the NObject instance.
|
7
7
|
attr_reader :errors
|
@@ -65,9 +65,16 @@ module NIFTI
|
|
65
65
|
elsif not string == nil
|
66
66
|
raise ArgumentError, "Invalid argument. Expected String (or nil), got #{string.class}."
|
67
67
|
end
|
68
|
-
|
69
68
|
end
|
70
|
-
|
69
|
+
|
70
|
+
# Reopen the NIFTI File and retrieve image data, returning, if the retrieval was successful, it as an NImage object
|
71
|
+
def get_nimage
|
72
|
+
image = self.get_image
|
73
|
+
if !image.nil?
|
74
|
+
NImage.new(image, self.header["dim"])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
71
78
|
# Reopen the NIFTI File and retrieve image data
|
72
79
|
def get_image
|
73
80
|
r = NRead.new(@string, :image => true)
|
@@ -75,7 +82,7 @@ module NIFTI
|
|
75
82
|
@image = r.image_rubyarray
|
76
83
|
end
|
77
84
|
end
|
78
|
-
|
85
|
+
|
79
86
|
# Passes the NObject to the DWrite class, which writes out the header and image to the specified file.
|
80
87
|
#
|
81
88
|
# === Parameters
|
@@ -101,10 +108,10 @@ module NIFTI
|
|
101
108
|
raise ArgumentError, "Invalid file_name. Expected String, got #{file_name.class}."
|
102
109
|
end
|
103
110
|
end
|
104
|
-
|
111
|
+
|
105
112
|
# Following methods are private:
|
106
|
-
private
|
107
|
-
|
113
|
+
private
|
114
|
+
|
108
115
|
# Returns a NIFTI object by reading and parsing the specified file.
|
109
116
|
# This is accomplished by initializing the NRead class, which loads NIFTI information.
|
110
117
|
#
|
@@ -112,7 +119,7 @@ module NIFTI
|
|
112
119
|
#
|
113
120
|
# This method is called automatically when initializing the NObject class with a file parameter,
|
114
121
|
# and in practice should not be called by users.
|
115
|
-
#
|
122
|
+
#
|
116
123
|
def read(string, options={})
|
117
124
|
if string.is_a?(String)
|
118
125
|
@string = string
|
@@ -124,7 +131,7 @@ module NIFTI
|
|
124
131
|
@header = r.hdr
|
125
132
|
@extended_header = r.extended_header
|
126
133
|
if r.image_narray
|
127
|
-
@image = r.image_narray
|
134
|
+
@image = r.image_narray
|
128
135
|
elsif r.image_rubyarray
|
129
136
|
@image = r.image_rubyarray
|
130
137
|
end
|
@@ -150,6 +157,6 @@ module NIFTI
|
|
150
157
|
@errors << msg
|
151
158
|
@errors.flatten
|
152
159
|
end
|
153
|
-
|
160
|
+
|
154
161
|
end
|
155
162
|
end
|
data/lib/nifti/n_read.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
1
3
|
module NIFTI
|
2
4
|
# The NRead class parses the NIFTI data from a binary string.
|
3
|
-
#
|
5
|
+
#
|
4
6
|
class NRead
|
5
7
|
# An array which records any status messages that are generated while parsing the DICOM string.
|
6
8
|
attr_reader :msg
|
@@ -14,14 +16,14 @@ module NIFTI
|
|
14
16
|
attr_reader :image_rubyarray
|
15
17
|
# A narray of image values reshapred to image dimensions
|
16
18
|
attr_reader :image_narray
|
17
|
-
|
19
|
+
|
18
20
|
# Valid Magic codes for the NIFTI Header
|
19
21
|
MAGIC = %w{ni1 n+1}
|
20
|
-
|
22
|
+
|
21
23
|
# Create a NRead object to parse a nifti file or binary string and set header and image info instance variables.
|
22
24
|
#
|
23
25
|
# The nifti header will be checked for validity (header size and magic number) and will raise an IOError if invalid.
|
24
|
-
#
|
26
|
+
#
|
25
27
|
# NIFTI header extensions are not yet supported and are not included in the header.
|
26
28
|
#
|
27
29
|
# The header and image are accessible via the hdr and image instance variables. An optional narray matrix may also be available in image_narray if desired by passing in :narray => true as an option.
|
@@ -43,18 +45,18 @@ module NIFTI
|
|
43
45
|
@success = false
|
44
46
|
set_stream(source, options)
|
45
47
|
parse_header(options)
|
46
|
-
|
48
|
+
|
47
49
|
return self
|
48
50
|
end
|
49
|
-
|
51
|
+
|
50
52
|
# Unpack an image array from vox_offset to the end of a nifti file.
|
51
|
-
#
|
53
|
+
#
|
52
54
|
# === Parameters
|
53
55
|
#
|
54
56
|
# There are no parameters - this reads from the binary string in the @string instance variable.
|
55
|
-
#
|
57
|
+
#
|
56
58
|
# This sets @image_rubyarray to the image data vector and also returns it.
|
57
|
-
#
|
59
|
+
#
|
58
60
|
def read_image
|
59
61
|
raw_image = []
|
60
62
|
@stream.index = @hdr['vox_offset']
|
@@ -62,25 +64,25 @@ module NIFTI
|
|
62
64
|
format = @stream.format[type]
|
63
65
|
@image_rubyarray = @stream.decode(@stream.rest_length, type)
|
64
66
|
end
|
65
|
-
|
66
|
-
# Create an narray if the NArray is available
|
67
|
+
|
68
|
+
# Create an narray if the NArray is available
|
67
69
|
# Tests if a file is readable, and if so, opens it.
|
68
70
|
#
|
69
71
|
# === Parameters
|
70
72
|
#
|
71
73
|
# * <tt>image_array</tt> -- Array. A vector of image data.
|
72
74
|
# * <tt>dim</tt> -- Array. The dim array from the nifti header, specifing number of dimensions (dim[0]) and dimension length of other dimensions to reshape narray into.
|
73
|
-
#
|
75
|
+
#
|
74
76
|
def get_image_narray(image_array, dim)
|
75
|
-
if
|
77
|
+
if Object.const_defined?('NArray')
|
76
78
|
@image_narray = pixel_data = NArray.to_na(image_array).reshape!(*dim[1..dim[0]])
|
77
79
|
else
|
78
80
|
add_msg "Can't find NArray, no image_narray created. Please `gem install narray`"
|
79
81
|
end
|
80
82
|
end
|
81
|
-
|
83
|
+
|
82
84
|
private
|
83
|
-
|
85
|
+
|
84
86
|
# Initializes @stream from a binary string or filename
|
85
87
|
def set_stream(source, options)
|
86
88
|
# Are we going to read from a file, or read from a binary string?
|
@@ -101,65 +103,65 @@ module NIFTI
|
|
101
103
|
@file.close
|
102
104
|
end
|
103
105
|
end
|
104
|
-
|
106
|
+
|
105
107
|
# Create a Stream instance to handle the decoding of content from this binary string:
|
106
108
|
@file_endian = false
|
107
109
|
@stream = Stream.new(@str, false)
|
108
110
|
end
|
109
|
-
|
111
|
+
|
110
112
|
# Parse the NIFTI Header.
|
111
113
|
def parse_header(options = {})
|
112
114
|
check_header
|
113
115
|
@hdr = parse_basic_header
|
114
116
|
@extended_header = parse_extended_header
|
115
|
-
|
117
|
+
|
116
118
|
# Optional image gathering
|
117
|
-
read_image if options[:image]
|
119
|
+
read_image if options[:image]
|
118
120
|
get_image_narray(@image_rubyarray, @hdr['dim']) if options[:narray]
|
119
|
-
|
121
|
+
|
120
122
|
@success = true
|
121
123
|
end
|
122
|
-
|
124
|
+
|
123
125
|
# NIFTI uses the header length (first 4 bytes) to be 348 number of "ni1\0"
|
124
126
|
# or "n+1\0" as the last 4 bytes to be magic numbers to validate the header.
|
125
|
-
#
|
127
|
+
#
|
126
128
|
# The header is usually checked before any data is read, but can be
|
127
129
|
# checked at any point in the process as the stream index is reset to its
|
128
130
|
# original position after validation.
|
129
|
-
#
|
130
|
-
# There are no options - the method will raise an IOError if any of the
|
131
|
+
#
|
132
|
+
# There are no options - the method will raise an IOError if any of the
|
131
133
|
# magic numbers are not valid.
|
132
134
|
def check_header
|
133
135
|
begin
|
134
136
|
starting_index = @stream.index
|
135
|
-
|
137
|
+
|
136
138
|
# Check sizeof_hdr
|
137
|
-
@stream.index = 0;
|
139
|
+
@stream.index = 0;
|
138
140
|
sizeof_hdr = @stream.decode(4, "UL")
|
139
141
|
raise IOError, "Bad Header Length #{sizeof_hdr}" unless sizeof_hdr == 348
|
140
|
-
|
142
|
+
|
141
143
|
# Check magic
|
142
|
-
@stream.index = 344;
|
144
|
+
@stream.index = 344;
|
143
145
|
magic = @stream.decode(4, "STR")
|
144
146
|
raise IOError, "Bad Magic Code #{magic} (should be ni1 or n+1)" unless MAGIC.include?(magic)
|
145
|
-
|
147
|
+
|
146
148
|
rescue IOError => e
|
147
149
|
raise IOError, "Header appears to be malformed: #{e}"
|
148
150
|
else
|
149
151
|
@stream.index = starting_index
|
150
|
-
end
|
152
|
+
end
|
151
153
|
end
|
152
154
|
|
153
155
|
# Read the nifti header according to its byte signature.
|
154
|
-
# The file stream will be left open and should be positioned at the end of the 348 byte header.
|
156
|
+
# The file stream will be left open and should be positioned at the end of the 348 byte header.
|
155
157
|
def parse_basic_header
|
156
158
|
# The HEADER_SIGNATURE is defined in NIFTI::Constants and used for both reading and writing.
|
157
159
|
header = {}
|
158
|
-
HEADER_SIGNATURE.each do |header_item|
|
160
|
+
HEADER_SIGNATURE.each do |header_item|
|
159
161
|
name, length, type = *header_item
|
160
162
|
header[name] = @stream.decode(length, type)
|
161
163
|
end
|
162
|
-
|
164
|
+
|
163
165
|
# Extract Freq, Phase & Slice Dimensions from diminfo
|
164
166
|
if header['dim_info']
|
165
167
|
header['freq_dim'] = dim_info_to_freq_dim(header['dim_info'])
|
@@ -168,7 +170,7 @@ module NIFTI
|
|
168
170
|
end
|
169
171
|
header['sform_code_descr'] = XFORM_CODES[header['sform_code']]
|
170
172
|
header['qform_code_descr'] = XFORM_CODES[header['qform_code']]
|
171
|
-
|
173
|
+
|
172
174
|
return header
|
173
175
|
end
|
174
176
|
|
@@ -201,7 +203,7 @@ module NIFTI
|
|
201
203
|
end
|
202
204
|
return extended
|
203
205
|
end
|
204
|
-
|
206
|
+
|
205
207
|
# Tests if a file is readable, and if so, opens it.
|
206
208
|
#
|
207
209
|
# === Parameters
|
@@ -213,7 +215,11 @@ module NIFTI
|
|
213
215
|
if File.readable?(file)
|
214
216
|
if not File.directory?(file)
|
215
217
|
if File.size(file) > 8
|
216
|
-
|
218
|
+
begin
|
219
|
+
@file = Zlib::GzipReader.new(File.new(file, "rb"))
|
220
|
+
rescue Zlib::GzipFile::Error
|
221
|
+
@file = File.new(file, "rb")
|
222
|
+
end
|
217
223
|
else
|
218
224
|
@msg << "Error! File is too small to contain DICOM information (#{file})."
|
219
225
|
end
|
@@ -227,38 +233,38 @@ module NIFTI
|
|
227
233
|
@msg << "Error! The file you have supplied does not exist (#{file})."
|
228
234
|
end
|
229
235
|
end
|
230
|
-
|
236
|
+
|
231
237
|
# Bitwise Operator to extract Frequency Dimension
|
232
238
|
def dim_info_to_freq_dim(dim_info)
|
233
239
|
extract_dim_info(dim_info, 0)
|
234
240
|
end
|
235
|
-
|
236
|
-
# Bitwise Operator to extract Phase Dimension
|
241
|
+
|
242
|
+
# Bitwise Operator to extract Phase Dimension
|
237
243
|
def dim_info_to_phase_dim(dim_info)
|
238
244
|
extract_dim_info(dim_info, 2)
|
239
245
|
end
|
240
|
-
|
246
|
+
|
241
247
|
# Bitwise Operator to extract Slice Dimension
|
242
248
|
def dim_info_to_slice_dim(dim_info)
|
243
249
|
extract_dim_info(dim_info, 4)
|
244
250
|
end
|
245
|
-
|
251
|
+
|
246
252
|
# Bitwise Operator to extract Frequency, Phase & Slice Dimensions from 2byte diminfo
|
247
253
|
def extract_dim_info(dim_info, offset = 0)
|
248
254
|
(dim_info >> offset) & 0x03
|
249
255
|
end
|
250
|
-
|
256
|
+
|
251
257
|
# Bitwise Operator to encode Freq, Phase & Slice into Diminfo
|
252
258
|
def fps_into_dim_info(frequency_dim, phase_dim, slice_dim)
|
253
|
-
((frequency_dim & 0x03 ) << 0 ) |
|
254
|
-
((phase_dim & 0x03) << 2 ) |
|
259
|
+
((frequency_dim & 0x03 ) << 0 ) |
|
260
|
+
((phase_dim & 0x03) << 2 ) |
|
255
261
|
((slice_dim & 0x03) << 4 )
|
256
262
|
end
|
257
|
-
|
263
|
+
|
258
264
|
# Add a message (TODO: and maybe print to screen if verbose)
|
259
265
|
def add_msg(msg)
|
260
266
|
@msg << msg
|
261
267
|
end
|
262
|
-
|
268
|
+
|
263
269
|
end
|
264
270
|
end
|
data/lib/nifti/n_write.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
1
3
|
module NIFTI
|
2
4
|
|
3
5
|
# The NWrite class handles the encoding of an NObject instance to a valid NIFTI string.
|
@@ -9,7 +11,7 @@ module NIFTI
|
|
9
11
|
attr_reader :msg
|
10
12
|
# A boolean which reports whether the DICOM string was encoded/written successfully (true) or not (false).
|
11
13
|
attr_reader :success
|
12
|
-
|
14
|
+
|
13
15
|
# Creates an NWrite instance.
|
14
16
|
#
|
15
17
|
# === Parameters
|
@@ -26,7 +28,7 @@ module NIFTI
|
|
26
28
|
# Array for storing error/warning messages:
|
27
29
|
@msg = Array.new
|
28
30
|
end
|
29
|
-
|
31
|
+
|
30
32
|
# Handles the encoding of NIfTI information to string as well as writing it to file.
|
31
33
|
def write
|
32
34
|
# Check if we are able to create given file:
|
@@ -40,7 +42,7 @@ module NIFTI
|
|
40
42
|
@stream = Stream.new(nil, @file_endian)
|
41
43
|
# Tell the Stream instance which file to write to:
|
42
44
|
@stream.set_file(@file)
|
43
|
-
|
45
|
+
|
44
46
|
# Write Header and Image
|
45
47
|
write_basic_header
|
46
48
|
write_extended_header
|
@@ -51,9 +53,9 @@ module NIFTI
|
|
51
53
|
# Mark this write session as successful:
|
52
54
|
@success = true
|
53
55
|
end
|
54
|
-
|
56
|
+
|
55
57
|
end
|
56
|
-
|
58
|
+
|
57
59
|
# Write Basic Header
|
58
60
|
def write_basic_header
|
59
61
|
HEADER_SIGNATURE.each do |header_item|
|
@@ -72,7 +74,7 @@ module NIFTI
|
|
72
74
|
end
|
73
75
|
end
|
74
76
|
end
|
75
|
-
|
77
|
+
|
76
78
|
# Write Extended Header
|
77
79
|
def write_extended_header
|
78
80
|
unless @obj.extended_header.empty?
|
@@ -85,16 +87,14 @@ module NIFTI
|
|
85
87
|
else
|
86
88
|
@stream.write @stream.encode([0,0,0,0], "BY")
|
87
89
|
end
|
88
|
-
|
89
|
-
|
90
90
|
end
|
91
|
-
|
91
|
+
|
92
92
|
# Write Image
|
93
93
|
def write_image
|
94
94
|
type = NIFTI_DATATYPES[@obj.header['datatype']]
|
95
95
|
@stream.write @stream.encode(@obj.image, type)
|
96
96
|
end
|
97
|
-
|
97
|
+
|
98
98
|
# Tests if the path/file is writable, creates any folders if necessary, and opens the file for writing.
|
99
99
|
#
|
100
100
|
# === Parameters
|
@@ -106,7 +106,7 @@ module NIFTI
|
|
106
106
|
if File.exist?(file)
|
107
107
|
# Is it writable?
|
108
108
|
if File.writable?(file)
|
109
|
-
@file =
|
109
|
+
@file = get_new_file_writer(file)
|
110
110
|
else
|
111
111
|
# Existing file is not writable:
|
112
112
|
@msg << "Error! The program does not have permission or resources to create the file you specified: (#{file})"
|
@@ -127,16 +127,31 @@ module NIFTI
|
|
127
127
|
end
|
128
128
|
end
|
129
129
|
# The path to this non-existing file is verified, and we can proceed to create the file:
|
130
|
-
@file =
|
130
|
+
@file = get_new_file_writer(file)
|
131
131
|
end
|
132
132
|
end
|
133
|
-
|
133
|
+
|
134
134
|
# Creates various variables used when encoding the DICOM string.
|
135
135
|
#
|
136
136
|
def init_variables
|
137
137
|
# Until a DICOM write has completed successfully the status is 'unsuccessful':
|
138
138
|
@success = false
|
139
139
|
end
|
140
|
-
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Opens the file according to it's extension (gziped or uncompressed)
|
144
|
+
#
|
145
|
+
# === Parameters
|
146
|
+
#
|
147
|
+
# * <tt>file</tt> -- A path/file string.
|
148
|
+
#
|
149
|
+
def get_new_file_writer(file)
|
150
|
+
if File.extname(file) == '.gz'
|
151
|
+
Zlib::GzipWriter.new(File.new(file, 'wb'))
|
152
|
+
else
|
153
|
+
File.new(file, 'wb')
|
154
|
+
end
|
155
|
+
end
|
141
156
|
end
|
142
157
|
end
|
data/lib/nifti/version.rb
CHANGED
data/nifti.gemspec
CHANGED
@@ -7,18 +7,21 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = NIFTI::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Erik Kastman"]
|
10
|
-
s.email = ["
|
11
|
-
s.homepage = ""
|
10
|
+
s.email = ["erik.kastman@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/brainmap/nifti"
|
12
12
|
s.summary = %q{A pure Ruby API to the NIfTI Neuroimaging Format}
|
13
13
|
s.description = %q{A pure Ruby API to the NIfTI Neuroimaging Format}
|
14
14
|
|
15
|
-
s.
|
15
|
+
s.required_ruby_version = '>= 1.8'
|
16
16
|
|
17
17
|
s.files = `git ls-files`.split("\n")
|
18
18
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
|
+
s.license = 'LGPLv3'
|
21
22
|
s.add_development_dependency "rspec"
|
22
23
|
s.add_development_dependency "mocha"
|
24
|
+
s.add_development_dependency "cucumber"
|
23
25
|
s.add_development_dependency "narray"
|
26
|
+
s.add_development_dependency "simplecov"
|
24
27
|
end
|