nifti 0.0.1 → 0.0.2
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.
- 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
|