nifti 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/CHANGELOG +13 -0
- data/COPYING +674 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +30 -0
- data/README.markdown +126 -0
- data/Rakefile +2 -0
- data/lib/nifti/constants.rb +223 -0
- data/lib/nifti/n_object.rb +155 -0
- data/lib/nifti/n_read.rb +264 -0
- data/lib/nifti/n_write.rb +142 -0
- data/lib/nifti/stream.rb +373 -0
- data/lib/nifti/version.rb +4 -0
- data/lib/nifti.rb +23 -0
- data/nifti.gemspec +24 -0
- data/spec/custom_matchers.rb +13 -0
- data/spec/fixtures/3plLoc.nii +0 -0
- data/spec/interactive/compare.rb +43 -0
- data/spec/nifti/n_object_spec.rb +111 -0
- data/spec/nifti/n_read_spec.rb +89 -0
- data/spec/nifti/n_write_spec.rb +56 -0
- data/spec/nifti/stream_spec.rb +44 -0
- data/spec/spec_helper.rb +13 -0
- metadata +138 -0
data/lib/nifti/n_read.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
module NIFTI
|
2
|
+
# The NRead class parses the NIFTI data from a binary string.
|
3
|
+
#
|
4
|
+
class NRead
|
5
|
+
# An array which records any status messages that are generated while parsing the DICOM string.
|
6
|
+
attr_reader :msg
|
7
|
+
# A boolean which reports whether the NIFTI string was parsed successfully (true) or not (false).
|
8
|
+
attr_reader :success
|
9
|
+
# A hash containing header attributes.
|
10
|
+
attr_reader :hdr
|
11
|
+
# An array of nifti header extension hashes with keys esize, ecode and data.
|
12
|
+
attr_reader :extended_header
|
13
|
+
# An array of decoded image values
|
14
|
+
attr_reader :image_rubyarray
|
15
|
+
# A narray of image values reshapred to image dimensions
|
16
|
+
attr_reader :image_narray
|
17
|
+
|
18
|
+
# Valid Magic codes for the NIFTI Header
|
19
|
+
MAGIC = %w{ni1 n+1}
|
20
|
+
|
21
|
+
# Create a NRead object to parse a nifti file or binary string and set header and image info instance variables.
|
22
|
+
#
|
23
|
+
# The nifti header will be checked for validity (header size and magic number) and will raise an IOError if invalid.
|
24
|
+
#
|
25
|
+
# NIFTI header extensions are not yet supported and are not included in the header.
|
26
|
+
#
|
27
|
+
# 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.
|
28
|
+
#
|
29
|
+
# === Parameters
|
30
|
+
#
|
31
|
+
# * <tt>source</tt> -- A string which specifies either the path of a NIFTI file to be loaded, or a binary NIFTI string to be parsed.
|
32
|
+
# * <tt>options</tt> -- A hash of parameters.
|
33
|
+
#
|
34
|
+
# === Options
|
35
|
+
#
|
36
|
+
# * <tt>:bin</tt> -- Boolean. If set to true, string parameter will be interpreted as a binary NIFTI string, and not a path string, which is the default behaviour.
|
37
|
+
# * <tt>:image</tt> -- Boolean. If set to true, automatically load the image into @image, otherwise only a header is collected and you can get an image
|
38
|
+
# * <tt>:narray</tt> -- Boolean. If set to true, a properly shaped narray matrix will be set in the instance variable @image_narray. Automatically sets :image => true
|
39
|
+
#
|
40
|
+
def initialize(source=nil, options={})
|
41
|
+
options[:image] = true if options[:narray]
|
42
|
+
@msg = []
|
43
|
+
@success = false
|
44
|
+
set_stream(source, options)
|
45
|
+
parse_header(options)
|
46
|
+
|
47
|
+
return self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Unpack an image array from vox_offset to the end of a nifti file.
|
51
|
+
#
|
52
|
+
# === Parameters
|
53
|
+
#
|
54
|
+
# There are no parameters - this reads from the binary string in the @string instance variable.
|
55
|
+
#
|
56
|
+
# This sets @image_rubyarray to the image data vector and also returns it.
|
57
|
+
#
|
58
|
+
def read_image
|
59
|
+
raw_image = []
|
60
|
+
@stream.index = @hdr['vox_offset']
|
61
|
+
type = NIFTI_DATATYPES[@hdr['datatype']]
|
62
|
+
format = @stream.format[type]
|
63
|
+
@image_rubyarray = @stream.decode(@stream.rest_length, type)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Create an narray if the NArray is available
|
67
|
+
# Tests if a file is readable, and if so, opens it.
|
68
|
+
#
|
69
|
+
# === Parameters
|
70
|
+
#
|
71
|
+
# * <tt>image_array</tt> -- Array. A vector of image data.
|
72
|
+
# * <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
|
+
#
|
74
|
+
def get_image_narray(image_array, dim)
|
75
|
+
if defined? NArray
|
76
|
+
@image_narray = pixel_data = NArray.to_na(image_array).reshape!(*dim[1..dim[0]])
|
77
|
+
else
|
78
|
+
add_msg "Can't find NArray, no image_narray created. Please `gem install narray`"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Initializes @stream from a binary string or filename
|
85
|
+
def set_stream(source, options)
|
86
|
+
# Are we going to read from a file, or read from a binary string?
|
87
|
+
if options[:bin]
|
88
|
+
# Read from the provided binary string:
|
89
|
+
@str = source
|
90
|
+
else
|
91
|
+
# Read from file:
|
92
|
+
open_file(source)
|
93
|
+
# Read the initial header of the file:
|
94
|
+
if @file == nil
|
95
|
+
# File is not readable, so we return:
|
96
|
+
@success = false
|
97
|
+
return
|
98
|
+
else
|
99
|
+
# Extract the content of the file to a binary string:
|
100
|
+
@str = @file.read
|
101
|
+
@file.close
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Create a Stream instance to handle the decoding of content from this binary string:
|
106
|
+
@file_endian = false
|
107
|
+
@stream = Stream.new(@str, false)
|
108
|
+
end
|
109
|
+
|
110
|
+
# Parse the NIFTI Header.
|
111
|
+
def parse_header(options = {})
|
112
|
+
check_header
|
113
|
+
@hdr = parse_basic_header
|
114
|
+
@extended_header = parse_extended_header
|
115
|
+
|
116
|
+
# Optional image gathering
|
117
|
+
read_image if options[:image]
|
118
|
+
get_image_narray(@image_rubyarray, @hdr['dim']) if options[:narray]
|
119
|
+
|
120
|
+
@success = true
|
121
|
+
end
|
122
|
+
|
123
|
+
# NIFTI uses the header length (first 4 bytes) to be 348 number of "ni1\0"
|
124
|
+
# or "n+1\0" as the last 4 bytes to be magic numbers to validate the header.
|
125
|
+
#
|
126
|
+
# The header is usually checked before any data is read, but can be
|
127
|
+
# checked at any point in the process as the stream index is reset to its
|
128
|
+
# original position after validation.
|
129
|
+
#
|
130
|
+
# There are no options - the method will raise an IOError if any of the
|
131
|
+
# magic numbers are not valid.
|
132
|
+
def check_header
|
133
|
+
begin
|
134
|
+
starting_index = @stream.index
|
135
|
+
|
136
|
+
# Check sizeof_hdr
|
137
|
+
@stream.index = 0;
|
138
|
+
sizeof_hdr = @stream.decode(4, "UL")
|
139
|
+
raise IOError, "Bad Header Length #{sizeof_hdr}" unless sizeof_hdr == 348
|
140
|
+
|
141
|
+
# Check magic
|
142
|
+
@stream.index = 344;
|
143
|
+
magic = @stream.decode(4, "STR")
|
144
|
+
raise IOError, "Bad Magic Code #{magic} (should be ni1 or n+1)" unless MAGIC.include?(magic)
|
145
|
+
|
146
|
+
rescue IOError => e
|
147
|
+
raise IOError, "Header appears to be malformed: #{e}"
|
148
|
+
else
|
149
|
+
@stream.index = starting_index
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# 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.
|
155
|
+
def parse_basic_header
|
156
|
+
# The HEADER_SIGNATURE is defined in NIFTI::Constants and used for both reading and writing.
|
157
|
+
header = {}
|
158
|
+
HEADER_SIGNATURE.each do |header_item|
|
159
|
+
name, length, type = *header_item
|
160
|
+
header[name] = @stream.decode(length, type)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Extract Freq, Phase & Slice Dimensions from diminfo
|
164
|
+
if header['dim_info']
|
165
|
+
header['freq_dim'] = dim_info_to_freq_dim(header['dim_info'])
|
166
|
+
header['phase_dim'] = dim_info_to_phase_dim(header['dim_info'])
|
167
|
+
header['slice_dim'] = dim_info_to_slice_dim(header['dim_info'])
|
168
|
+
end
|
169
|
+
header['sform_code_descr'] = XFORM_CODES[header['sform_code']]
|
170
|
+
header['qform_code_descr'] = XFORM_CODES[header['qform_code']]
|
171
|
+
|
172
|
+
return header
|
173
|
+
end
|
174
|
+
|
175
|
+
# Read any extended header information.
|
176
|
+
# The file stream will be left at imaging data itself, taking vox_offset into account for NIFTI Header Extended Attributes.
|
177
|
+
# Pass in the voxel offset so the extended header knows when to stop reading.
|
178
|
+
|
179
|
+
def parse_extended_header
|
180
|
+
extended = []
|
181
|
+
extension = @stream.decode(4, "BY")
|
182
|
+
|
183
|
+
# "After the end of the 348 byte header (e.g., after the magic field),
|
184
|
+
# the next 4 bytes are an byte array field named extension. By default,
|
185
|
+
# all 4 bytes of this array should be set to zero. In a .nii file, these 4
|
186
|
+
# bytes will always be present, since the earliest start point for the
|
187
|
+
# image data is byte #352. In a separate .hdr file, these bytes may or may
|
188
|
+
# not be present (i.e., a .hdr file may only be 348 bytes long). If not
|
189
|
+
# present, then a NIfTI-1.1 compliant program should use the default value
|
190
|
+
# of extension={0,0,0,0}. The first byte (extension[0]) is the only value
|
191
|
+
# of this array that is specified at present. The other 3 bytes are
|
192
|
+
# reserved for future use."
|
193
|
+
if extension[0] != 0
|
194
|
+
while @stream.index < @hdr['vox_offset']
|
195
|
+
esize, ecode = *@stream.decode(8, "UL")
|
196
|
+
data = @stream.decode(esize - 8, "STR")
|
197
|
+
extended << {:esize => esize, :ecode => ecode, :data => data}
|
198
|
+
end
|
199
|
+
# stream.decode(header['vox_offset'] - stream.index, "STR")
|
200
|
+
# stream.skip header['vox_offset'] - stream.index
|
201
|
+
end
|
202
|
+
return extended
|
203
|
+
end
|
204
|
+
|
205
|
+
# Tests if a file is readable, and if so, opens it.
|
206
|
+
#
|
207
|
+
# === Parameters
|
208
|
+
#
|
209
|
+
# * <tt>file</tt> -- A path/file string.
|
210
|
+
#
|
211
|
+
def open_file(file)
|
212
|
+
if File.exist?(file)
|
213
|
+
if File.readable?(file)
|
214
|
+
if not File.directory?(file)
|
215
|
+
if File.size(file) > 8
|
216
|
+
@file = File.new(file, "rb")
|
217
|
+
else
|
218
|
+
@msg << "Error! File is too small to contain DICOM information (#{file})."
|
219
|
+
end
|
220
|
+
else
|
221
|
+
@msg << "Error! File is a directory (#{file})."
|
222
|
+
end
|
223
|
+
else
|
224
|
+
@msg << "Error! File exists but I don't have permission to read it (#{file})."
|
225
|
+
end
|
226
|
+
else
|
227
|
+
@msg << "Error! The file you have supplied does not exist (#{file})."
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Bitwise Operator to extract Frequency Dimension
|
232
|
+
def dim_info_to_freq_dim(dim_info)
|
233
|
+
extract_dim_info(dim_info, 0)
|
234
|
+
end
|
235
|
+
|
236
|
+
# Bitwise Operator to extract Phase Dimension
|
237
|
+
def dim_info_to_phase_dim(dim_info)
|
238
|
+
extract_dim_info(dim_info, 2)
|
239
|
+
end
|
240
|
+
|
241
|
+
# Bitwise Operator to extract Slice Dimension
|
242
|
+
def dim_info_to_slice_dim(dim_info)
|
243
|
+
extract_dim_info(dim_info, 4)
|
244
|
+
end
|
245
|
+
|
246
|
+
# Bitwise Operator to extract Frequency, Phase & Slice Dimensions from 2byte diminfo
|
247
|
+
def extract_dim_info(dim_info, offset = 0)
|
248
|
+
(dim_info >> offset) & 0x03
|
249
|
+
end
|
250
|
+
|
251
|
+
# Bitwise Operator to encode Freq, Phase & Slice into Diminfo
|
252
|
+
def fps_into_dim_info(frequency_dim, phase_dim, slice_dim)
|
253
|
+
((frequency_dim & 0x03 ) << 0 ) |
|
254
|
+
((phase_dim & 0x03) << 2 ) |
|
255
|
+
((slice_dim & 0x03) << 4 )
|
256
|
+
end
|
257
|
+
|
258
|
+
# Add a message (TODO: and maybe print to screen if verbose)
|
259
|
+
def add_msg(msg)
|
260
|
+
@msg << msg
|
261
|
+
end
|
262
|
+
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module NIFTI
|
2
|
+
|
3
|
+
# The NWrite class handles the encoding of an NObject instance to a valid NIFTI string.
|
4
|
+
# The String is then written to file.
|
5
|
+
#
|
6
|
+
class NWrite
|
7
|
+
|
8
|
+
# An array which records any status messages that are generated while encoding/writing the DICOM string.
|
9
|
+
attr_reader :msg
|
10
|
+
# A boolean which reports whether the DICOM string was encoded/written successfully (true) or not (false).
|
11
|
+
attr_reader :success
|
12
|
+
|
13
|
+
# Creates an NWrite instance.
|
14
|
+
#
|
15
|
+
# === Parameters
|
16
|
+
#
|
17
|
+
# * <tt>obj</tt> -- A NObject instance which will be used to encode a NIfTI string.
|
18
|
+
# * <tt>file_name</tt> -- A string, either specifying the path of a DICOM file to be loaded, or a binary DICOM string to be parsed.
|
19
|
+
# * <tt>options</tt> -- A hash of parameters.
|
20
|
+
#
|
21
|
+
# === Options
|
22
|
+
#
|
23
|
+
def initialize(obj, file_name, options = {})
|
24
|
+
@obj = obj
|
25
|
+
@file_name = file_name
|
26
|
+
# Array for storing error/warning messages:
|
27
|
+
@msg = Array.new
|
28
|
+
end
|
29
|
+
|
30
|
+
# Handles the encoding of NIfTI information to string as well as writing it to file.
|
31
|
+
def write
|
32
|
+
# Check if we are able to create given file:
|
33
|
+
open_file(@file_name)
|
34
|
+
# Go ahead and write if the file was opened successfully:
|
35
|
+
if @file
|
36
|
+
# Initiate necessary variables:
|
37
|
+
init_variables
|
38
|
+
@file_endian = false
|
39
|
+
# Create a Stream instance to handle the encoding of content to a binary string:
|
40
|
+
@stream = Stream.new(nil, @file_endian)
|
41
|
+
# Tell the Stream instance which file to write to:
|
42
|
+
@stream.set_file(@file)
|
43
|
+
|
44
|
+
# Write Header and Image
|
45
|
+
write_basic_header
|
46
|
+
write_extended_header
|
47
|
+
write_image
|
48
|
+
|
49
|
+
# As file has been written successfully, it can be closed.
|
50
|
+
@file.close
|
51
|
+
# Mark this write session as successful:
|
52
|
+
@success = true
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
# Write Basic Header
|
58
|
+
def write_basic_header
|
59
|
+
HEADER_SIGNATURE.each do |header_item|
|
60
|
+
begin
|
61
|
+
name, length, type = *header_item
|
62
|
+
str = @stream.encode(@obj.header[name], type)
|
63
|
+
padded_str = @stream.encode_string_to_length(str, length)
|
64
|
+
# puts @stream.index, name, str.unpack(@stream.vr_to_str(type))
|
65
|
+
# pp padded_str.unpack(@stream.vr_to_str(type))
|
66
|
+
|
67
|
+
@stream.write padded_str
|
68
|
+
@stream.skip length
|
69
|
+
rescue StandardError => e
|
70
|
+
puts name, length, type, e
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Write Extended Header
|
77
|
+
def write_extended_header
|
78
|
+
unless @obj.extended_header.empty?
|
79
|
+
@stream.write @stream.encode([1,0,0,0], "BY")
|
80
|
+
@obj.extended_header.each do |extension|
|
81
|
+
@stream.write @stream.encode extension[:esize], "UL"
|
82
|
+
@stream.write @stream.encode extension[:ecode], "UL"
|
83
|
+
@stream.write @stream.encode_string_to_length(@stream.encode(extension[:data], "STR"), extension[:esize] - 8)
|
84
|
+
end
|
85
|
+
else
|
86
|
+
@stream.write @stream.encode([0,0,0,0], "BY")
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
# Write Image
|
93
|
+
def write_image
|
94
|
+
type = NIFTI_DATATYPES[@obj.header['datatype']]
|
95
|
+
@stream.write @stream.encode(@obj.image, type)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Tests if the path/file is writable, creates any folders if necessary, and opens the file for writing.
|
99
|
+
#
|
100
|
+
# === Parameters
|
101
|
+
#
|
102
|
+
# * <tt>file</tt> -- A path/file string.
|
103
|
+
#
|
104
|
+
def open_file(file)
|
105
|
+
# Check if file already exists:
|
106
|
+
if File.exist?(file)
|
107
|
+
# Is it writable?
|
108
|
+
if File.writable?(file)
|
109
|
+
@file = File.new(file, "wb")
|
110
|
+
else
|
111
|
+
# Existing file is not writable:
|
112
|
+
@msg << "Error! The program does not have permission or resources to create the file you specified: (#{file})"
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# File does not exist.
|
116
|
+
# Check if this file's path contains a folder that does not exist, and therefore needs to be created:
|
117
|
+
folders = file.split(File::SEPARATOR)
|
118
|
+
if folders.length > 1
|
119
|
+
# Remove last element (which should be the file string):
|
120
|
+
folders.pop
|
121
|
+
path = folders.join(File::SEPARATOR)
|
122
|
+
# Check if this path exists:
|
123
|
+
unless File.directory?(path)
|
124
|
+
# We need to create (parts of) this path:
|
125
|
+
require 'fileutils'
|
126
|
+
FileUtils.mkdir_p path
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# The path to this non-existing file is verified, and we can proceed to create the file:
|
130
|
+
@file = File.new(file, "wb")
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Creates various variables used when encoding the DICOM string.
|
135
|
+
#
|
136
|
+
def init_variables
|
137
|
+
# Until a DICOM write has completed successfully the status is 'unsuccessful':
|
138
|
+
@success = false
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|