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.
@@ -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