nifti 0.0.1
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 +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
|