nifti 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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