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,373 @@
1
+ module NIFTI
2
+ # The Stream class handles string operations (encoding to and decoding from binary strings).
3
+ #
4
+ class Stream
5
+ # A boolean which reports the relationship between the endianness of the system and the instance string.
6
+ attr_reader :equal_endian
7
+ # Our current position in the instance string (used only for decoding).
8
+ attr_accessor :index
9
+ # The instance string.
10
+ attr_accessor :string
11
+ # The endianness of the instance string.
12
+ attr_reader :str_endian
13
+ # An array of warning/error messages that (may) have been accumulated.
14
+ attr_reader :errors
15
+ # A hash of proper strings (based on endianess) to use for unpacking binary strings.
16
+ attr_reader :format
17
+ # A File object to write to
18
+ attr_accessor :file
19
+
20
+ # Creates a Stream instance.
21
+ #
22
+ # === Parameters
23
+ #
24
+ # * <tt>binary</tt> -- A binary string.
25
+ # * <tt>string_endian</tt> -- Boolean. The endianness of the instance string (true for big endian, false for small endian).
26
+ # * <tt>options</tt> -- A hash of parameters.
27
+ #
28
+ # === Options
29
+ #
30
+ # * <tt>:index</tt> -- Fixnum. A position (offset) in the instance string where reading will start.
31
+ #
32
+ def initialize(binary, string_endian, options={})
33
+ @string = binary || ""
34
+ @index = options[:index] || 0
35
+ @errors = Array.new
36
+ self.endian = string_endian
37
+ end
38
+
39
+
40
+
41
+ # Decodes a section of the instance string and returns the formatted data.
42
+ # The instance index is offset in accordance with the length read.
43
+ #
44
+ # === Notes
45
+ #
46
+ # * If multiple numbers are decoded, these are returned in an array.
47
+ #
48
+ # === Parameters
49
+ #
50
+ # * <tt>length</tt> -- Fixnum. The string length which will be decoded.
51
+ # * <tt>type</tt> -- String. The type (vr) of data to decode.
52
+ #
53
+ def decode(length, type)
54
+ # Check if values are valid:
55
+ if (@index + length) > @string.length
56
+ # The index number is bigger then the length of the binary string.
57
+ # We have reached the end and will return nil.
58
+ value = nil
59
+ else
60
+ if type == "AT"
61
+ value = decode_tag
62
+ else
63
+ # Decode the binary string and return value:
64
+ value = @string.slice(@index, length).unpack(vr_to_str(type))
65
+ # If the result is an array of one element, return the element instead of the array.
66
+ # If result is contained in a multi-element array, the original array is returned.
67
+ if value.length == 1
68
+ value = value[0]
69
+ # If value is a string, strip away possible trailing whitespace:
70
+ # Do this using gsub instead of ruby-core #strip to keep trailing carriage
71
+ # returns, etc., because that is valid whitespace that we want to have included
72
+ # (i.e. in extended header data, AFNI writes a \n at the end of it's xml info,
73
+ # and that must be kept in order to not change the file on writing back out).
74
+ value.gsub!(/\000*$/, "") if value.respond_to? :gsub!
75
+ end
76
+ # Update our position in the string:
77
+ skip(length)
78
+ end
79
+ end
80
+ return value
81
+ end
82
+
83
+ # Returns the length of the binary instance string.
84
+ #
85
+ def length
86
+ return @string.length
87
+ end
88
+
89
+ # Calculates and returns the remaining length of the instance string (from the index position).
90
+ #
91
+ def rest_length
92
+ length = @string.length - @index
93
+ return length
94
+ end
95
+
96
+ # Extracts and returns the remaining part of the instance string (from the index position to the end of the string).
97
+ #
98
+ def rest_string
99
+ str = @string[@index..(@string.length-1)]
100
+ return str
101
+ end
102
+
103
+ # Resets the instance string and index.
104
+ #
105
+ def reset
106
+ @string = ""
107
+ @index = 0
108
+ end
109
+
110
+ # Resets the instance index.
111
+ #
112
+ def reset_index
113
+ @index = 0
114
+ end
115
+
116
+ # Sets an instance file variable.
117
+ #
118
+ # === Notes
119
+ #
120
+ # For performance reasons, we enable the Stream instance to write directly to file,
121
+ # to avoid expensive string operations which will otherwise slow down the write performance.
122
+ #
123
+ # === Parameters
124
+ #
125
+ # * <tt>file</tt> -- A File instance.
126
+ #
127
+ def set_file(file)
128
+ @file = file
129
+ end
130
+
131
+ # Sets a new instance string, and resets the index variable.
132
+ #
133
+ # === Parameters
134
+ #
135
+ # * <tt>binary</tt> -- A binary string.
136
+ #
137
+ def set_string(binary)
138
+ binary = binary[0] if binary.is_a?(Array)
139
+ @string = binary
140
+ @index = 0
141
+ end
142
+
143
+ # Applies an offset (positive or negative) to the instance index.
144
+ #
145
+ # === Parameters
146
+ #
147
+ # * <tt>offset</tt> -- Fixnum. The length to skip (positive) or rewind (negative).
148
+ #
149
+ def skip(offset)
150
+ @index += offset
151
+ end
152
+
153
+ # Converts a data type/vr to an encode/decode string used by the pack/unpack methods, which is returned.
154
+ #
155
+ # === Parameters
156
+ #
157
+ # * <tt>vr</tt> -- String. A data type (value representation).
158
+ #
159
+ def vr_to_str(vr)
160
+ unless @format[vr]
161
+ errors << "Warning: Element type #{vr} does not have a reading method assigned to it. Something is not implemented correctly or the DICOM data analyzed is invalid."
162
+ return @hex
163
+ else
164
+ return @format[vr]
165
+ end
166
+ end
167
+
168
+ # Sets an instance file variable.
169
+ #
170
+ # === Notes
171
+ #
172
+ # For performance reasons, we enable the Stream instance to write directly to file,
173
+ # to avoid expensive string operations which will otherwise slow down the write performance.
174
+ #
175
+ # === Parameters
176
+ #
177
+ # * <tt>file</tt> -- A File instance.
178
+ #
179
+ def set_file(file)
180
+ @file = file
181
+ end
182
+
183
+ # Writes a binary string to the File instance.
184
+ #
185
+ # === Parameters
186
+ #
187
+ # * <tt>binary</tt> -- A binary string.
188
+ #
189
+ def write(binary)
190
+ @file.write(binary)
191
+ end
192
+
193
+ # Encodes a value and returns the resulting binary string.
194
+ #
195
+ # === Parameters
196
+ #
197
+ # * <tt>value</tt> -- A custom value (String, Fixnum, etc..) or an array of numbers.
198
+ # * <tt>type</tt> -- String. The type (vr) of data to encode.
199
+ #
200
+ def encode(value, type)
201
+ value = [value] unless value.is_a?(Array)
202
+ return value.pack(vr_to_str(type))
203
+ end
204
+
205
+ # Appends a string with trailling spaces to achieve a target length, and encodes it to a binary string.
206
+ # Returns the binary string. Raises an error if pad option is different than :null or :spaces
207
+ #
208
+ # === Parameters
209
+ #
210
+ # * <tt>string</tt> -- A string to be processed.
211
+ # * <tt>target_length</tt> -- Fixnum. The target length of the string that is created.
212
+ # * <tt>pad</tt> -- Type of desired padding, either :null or :spaces
213
+ #
214
+ def encode_string_to_length(string, target_length, pad = :null)
215
+ if pad == :spaces
216
+ template = "A#{target_length}"
217
+ elsif pad == :null
218
+ template = "a#{target_length}"
219
+ else
220
+ raise StandardError, "Could not identify padding type #{pad}"
221
+ end
222
+
223
+ length = string.length
224
+ if length < target_length
225
+ return [string].pack(template)
226
+ elsif length == target_length
227
+ return [string].pack(@str)
228
+ else
229
+ raise "The specified string is longer than the allowed maximum length (String: #{string}, Target length: #{target_length})."
230
+ end
231
+ end
232
+
233
+ # Following methods are private:
234
+ private
235
+
236
+
237
+ # Determines the relationship between system and string endianness, and sets the instance endian variable.
238
+ #
239
+ def configure_endian
240
+ if CPU_ENDIAN == @str_endian
241
+ @equal_endian = true
242
+ else
243
+ @equal_endian = false
244
+ end
245
+ end
246
+
247
+ # Sets the pack/unpack format strings that is used for encoding/decoding.
248
+ # Some of these depends on the endianness of the system and the String.
249
+ #
250
+ #--
251
+ # Note: Surprisingly the Ruby pack/unpack methods lack a format for signed short
252
+ # and signed long in the network byte order. A hack has been implemented to to ensure
253
+ # correct behaviour in this case, but it is slower (~4 times slower than a normal pack/unpack).
254
+ #
255
+ def set_string_formats
256
+ if @equal_endian
257
+ # Native byte order:
258
+ @us = "S*" # Unsigned short (2 bytes)
259
+ @ss = "s*" # Signed short (2 bytes)
260
+ @ul = "I*" # Unsigned long (4 bytes)
261
+ @sl = "l*" # Signed long (4 bytes)
262
+ @fs = "e*" # Floating point single (4 bytes)
263
+ @fd = "E*" # Floating point double ( 8 bytes)
264
+ else
265
+ # Network byte order:
266
+ @us = "n*"
267
+ @ss = CUSTOM_SS # Custom string for our redefined pack/unpack.
268
+ @ul = "N*"
269
+ @sl = CUSTOM_SL # Custom string for our redefined pack/unpack.
270
+ @fs = "g*"
271
+ @fd = "G*"
272
+ end
273
+ # Format strings that are not dependent on endianness:
274
+ @by = "C*" # Unsigned char (1 byte)
275
+ @str = "a*"
276
+ @hex = "H*" # (this may be dependent on endianness(?))
277
+ end
278
+
279
+ # Sets the hash which is used to convert data element types (VR) to
280
+ # encode/decode strings accepted by the pack/unpack methods.
281
+ #
282
+ def set_format_hash
283
+ @format = {
284
+ "BY" => @by, # Byte/Character (1-byte integers)
285
+ "US" => @us, # Unsigned short (2 bytes)
286
+ "SS" => @ss, # Signed short (2 bytes)
287
+ "UL" => @ul, # Unsigned long (4 bytes)
288
+ "SL" => @sl, # Signed long (4 bytes)
289
+ "FL" => @fs, # Floating point single (4 bytes)
290
+ "FD" => @fd, # Floating point double (8 bytes)
291
+ "OB" => @by, # Other byte string (1-byte integers)
292
+ "OF" => @fs, # Other float string (4-byte floating point numbers)
293
+ "OW" => @us, # Other word string (2-byte integers)
294
+ "AT" => @hex, # Tag reference (4 bytes) NB: This may need to be revisited at some point...
295
+ "UN" => @hex, # Unknown information (header element is not recognized from local database)
296
+ "HEX" => @hex, # HEX
297
+ # We have a number of VRs that are decoded as string:
298
+ "AE" => @str,
299
+ "AS" => @str,
300
+ "CS" => @str,
301
+ "DA" => @str,
302
+ "DS" => @str,
303
+ "DT" => @str,
304
+ "IS" => @str,
305
+ "LO" => @str,
306
+ "LT" => @str,
307
+ "PN" => @str,
308
+ "SH" => @str,
309
+ "ST" => @str,
310
+ "TM" => @str,
311
+ "UI" => @str,
312
+ "UT" => @str,
313
+ "STR" => @str
314
+ }
315
+ end
316
+
317
+ # Sets the hash which is used to keep track of which bytes to use for padding
318
+ # data elements of various vr which have an odd value length.
319
+ #
320
+ def set_pad_byte
321
+ @pad_byte = {
322
+ # Space character:
323
+ "AE" => "\x20",
324
+ "AS" => "\x20",
325
+ "CS" => "\x20",
326
+ "DA" => "\x20",
327
+ "DS" => "\x20",
328
+ "DT" => "\x20",
329
+ "IS" => "\x20",
330
+ "LO" => "\x20",
331
+ "LT" => "\x20",
332
+ "PN" => "\x20",
333
+ "SH" => "\x20",
334
+ "ST" => "\x20",
335
+ "TM" => "\x20",
336
+ "UT" => "\x20",
337
+ # Zero byte:
338
+ "AT" => "\x00",
339
+ "FL" => "\x00",
340
+ "FD" => "\x00",
341
+ "OB" => "\x00",
342
+ "OF" => "\x00",
343
+ "OW" => "\x00",
344
+ "SL" => "\x00",
345
+ "SQ" => "\x00",
346
+ "SS" => "\x00",
347
+ "UI" => "\x00",
348
+ "UL" => "\x00",
349
+ "UN" => "\x00",
350
+ "US" => "\x00"
351
+ }
352
+ @pad_byte.default = "\20"
353
+ end
354
+
355
+
356
+ # Sets the endianness of the instance string. The relationship between the string endianness and
357
+ # the system endianness, determines which encoding/decoding flags to use.
358
+ #
359
+ # === Parameters
360
+ #
361
+ # * <tt>string_endian</tt> -- Boolean. The endianness of the instance string (true for big endian, false for small endian).
362
+ #
363
+ def endian=(string_endian)
364
+ @str_endian = string_endian
365
+ configure_endian
366
+ set_string_formats
367
+ set_format_hash
368
+ set_pad_byte
369
+ end
370
+
371
+ end
372
+
373
+ end
@@ -0,0 +1,4 @@
1
+ module NIFTI
2
+ # Current Version of NIFTI
3
+ VERSION = "0.0.1"
4
+ end
data/lib/nifti.rb ADDED
@@ -0,0 +1,23 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ # Loads the files that are used by Ruby NIFTI.
4
+ #
5
+ # The following classes are meant to be used by users of Ruby DICOM:
6
+ # * NObject - for reading, manipulating and writing DICOM files.
7
+
8
+ # NIFTI is the main namespace for all Ruby NIfTI classes, constants and methods.
9
+ module NIFTI; end
10
+
11
+ # Core library:
12
+ require 'nifti/n_object'
13
+ require 'nifti/n_read'
14
+ require 'nifti/n_write'
15
+ require 'nifti/stream'
16
+ require 'nifti/constants'
17
+
18
+ begin
19
+ require 'narray'
20
+ rescue LoadError => e
21
+ puts "NArray requried for some image visualization options."
22
+ puts "Run 'gem install narray' or 'bundle install' to get it."
23
+ end
data/nifti.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "nifti/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "nifti"
7
+ s.version = NIFTI::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Erik Kastman"]
10
+ s.email = ["ekk@medicine.wisc.edu"]
11
+ s.homepage = ""
12
+ s.summary = %q{A pure Ruby API to the NIfTI Neuroimaging Format}
13
+ s.description = %q{A pure Ruby API to the NIfTI Neuroimaging Format}
14
+
15
+ s.rubyforge_project = "nifti"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.add_development_dependency "rspec"
22
+ s.add_development_dependency "mocha"
23
+ s.add_development_dependency "narray"
24
+ end
@@ -0,0 +1,13 @@
1
+ require 'digest/md5'
2
+
3
+ # Source: Matt Wynne; https://gist.github.com/736421
4
+ RSpec::Matchers.define(:be_same_file_as) do |exected_file_path|
5
+ match do |actual_file_path|
6
+ md5_hash(actual_file_path).should == md5_hash(exected_file_path)
7
+ end
8
+
9
+ # Calculate an md5 hash from a file path
10
+ def md5_hash(file_path)
11
+ Digest::MD5.hexdigest(File.read(file_path))
12
+ end
13
+ end
Binary file
@@ -0,0 +1,43 @@
1
+ # Reopen File to add a diff calculator for investigating byte differences between fixtures and created files.
2
+ class File
3
+ # Checks if two files contain the same contents. Prints and returns a
4
+ # position and values of differences, or returns nil if there were no
5
+ # differnces.
6
+ def File.same_contents(p1, p2)
7
+ f1 = open(p1).read
8
+ f2 = open(p2).read
9
+
10
+ # Control variables
11
+ differences = []
12
+ read_length = 1
13
+ same = true
14
+ index = 0
15
+
16
+ while ((index + read_length) < f1.length) && ((index + read_length) < f2.length)
17
+ same = f1.slice(index, read_length) == f2.slice(index, read_length)
18
+ unless same
19
+ puts index
20
+ pp f1.slice(index, read_length).unpack("C*")
21
+ pp f2.slice(index, read_length).unpack("C*")
22
+ differences << {:index => index,
23
+ :f1_value => f1.slice(index, read_length).unpack("C*"),
24
+ :f2_value => f2.slice(index, read_length).unpack("C*")
25
+ }
26
+ end
27
+ index = index + read_length
28
+ differences
29
+ end
30
+ end
31
+ end
32
+
33
+ # # Original comparison from Ruby Cookbook by Carlson & Richardson
34
+ # open(p1) do |f1|
35
+ # open(p2) do |f2|
36
+ # puts blocksize = f1.lstat.blksize
37
+ # same = true
38
+ # while same && !f1.eof? && !f2.eof?
39
+ # same = f1.read(blocksize) == f2.read(blocksize)
40
+ # end
41
+ # return same
42
+ # end
43
+ # end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+
3
+ describe NIFTI::NObject do
4
+ before :all do
5
+ @string = File.open(NIFTI_TEST_FILE1, 'rb').read
6
+ @fixture_image_length = 983040
7
+ @fixture_afni_extension_length = 5661
8
+ @new_fixture_file_name = '5PlLoc.nii'
9
+ @valid_header = {
10
+ "xyzt_units"=>2, "pixdim"=>[1.0, 0.9375, 0.9375, 12.5, 0.0, 0.0, 0.0,
11
+ 0.0], "sform_code"=>1, "aux_file"=>"", "scl_slope"=>0.0,
12
+ "srow_x"=>[-0.9375, -0.0, -0.0, 119.53125], "glmin"=>0, "freq_dim"=>0,
13
+ "srow_y"=>[-0.0, -0.9375, -0.0, 159.531005859375], "qform_code"=>1,
14
+ "slice_duration"=>0.0, "cal_min"=>0.0, "db_name"=>"", "magic"=>"n+1",
15
+ "srow_z"=>[0.0, 0.0, 12.5, -25.0], "quatern_b"=>0.0, "data_type"=>"",
16
+ "qform_code_descr"=>"NIFTI_XFORM_SCANNER_ANAT",
17
+ "sform_code_descr"=>"NIFTI_XFORM_SCANNER_ANAT", "intent_name"=>"",
18
+ "quatern_c"=>0.0, "slice_end"=>0, "scl_inter"=>0.0, "quatern_d"=>1.0,
19
+ "slice_code"=>0, "sizeof_hdr"=>348, "slice_dim"=>0,
20
+ "qoffset_x"=>119.53125, "dim_info"=>0, "phase_dim"=>0,
21
+ "qoffset_y"=>159.531005859375, "descrip"=>"", "datatype"=>4,
22
+ "intent_p1"=>0.0, "dim"=>[3, 256, 256, 15, 1, 1, 1, 1],
23
+ "qoffset_z"=>-25.0, "glmax"=>0, "toffset"=>0.0, "bitpix"=>16,
24
+ "intent_code"=>0, "intent_p2"=>0.0, "session_error"=>0, "extents"=>0,
25
+ "cal_max"=>0.0, "vox_offset"=>6032.0, "slice_start"=>0,
26
+ "intent_p3"=>0.0, "regular"=>"r"
27
+ }
28
+ end
29
+
30
+ # Think of these more as integration tests, since the actual reading
31
+ # is done and tested in the NRead spec
32
+ it "should read a nifti file and correctly initialize header and image" do
33
+ obj = NObject.new(NIFTI_TEST_FILE1)
34
+
35
+ obj.header.should == @valid_header
36
+ obj.extended_header.should_not be_empty
37
+ obj.extended_header.first[:esize].should == 5680
38
+ obj.extended_header.first[:ecode].should == 4
39
+ obj.extended_header.first[:data].length.should == @fixture_afni_extension_length
40
+ obj.image.should be_nil
41
+
42
+ end
43
+
44
+ it "should read a binary string and correctly initialize header and image" do
45
+ obj = NObject.new(@string, :bin => true)
46
+
47
+ obj.header.should == @valid_header
48
+ obj.extended_header.should_not be_empty
49
+ obj.extended_header.first[:esize].should == 5680
50
+ obj.extended_header.first[:ecode].should == 4
51
+ obj.extended_header.first[:data].length.should == @fixture_afni_extension_length
52
+ obj.image.should be_nil
53
+
54
+ end
55
+
56
+ it "should read a nifti file with image" do
57
+ obj = NObject.new(NIFTI_TEST_FILE1, :image => true)
58
+
59
+ obj.header.should == @valid_header
60
+ obj.extended_header.should_not be_empty
61
+ obj.extended_header.first[:esize].should == 5680
62
+ obj.extended_header.first[:ecode].should == 4
63
+ obj.extended_header.first[:data].length.should == @fixture_afni_extension_length
64
+ obj.image.should_not be_nil
65
+ obj.image.length.should == @fixture_image_length
66
+
67
+ end
68
+
69
+ it "should read a nifti file with image as narray" do
70
+ obj = NObject.new(NIFTI_TEST_FILE1, :image => true, :narray => true)
71
+
72
+ obj.header.should == @valid_header
73
+ obj.extended_header.should_not be_empty
74
+ obj.extended_header.first[:esize].should == 5680
75
+ obj.extended_header.first[:ecode].should == 4
76
+ obj.extended_header.first[:data].length.should == @fixture_afni_extension_length
77
+ obj.image.should_not be_nil
78
+ obj.image.class.should == NArray
79
+ obj.image.dim.should == 3
80
+
81
+ end
82
+
83
+ it "should retrieve image data when requested" do
84
+ obj = NObject.new(NIFTI_TEST_FILE1)
85
+ obj.get_image.length.should == @fixture_image_length
86
+ end
87
+
88
+
89
+ it "should raise an error if initialized with bad argument" do
90
+ lambda {
91
+ NObject.new(12345)
92
+ }.should raise_error ArgumentError, /Invalid argument/
93
+ end
94
+
95
+ it "should sucessfully write a NIfTI file" do
96
+ obj = NObject.new(NIFTI_TEST_FILE1, :image => true)
97
+ obj.write(@new_fixture_file_name)
98
+ File.exist?(@new_fixture_file_name).should be_true
99
+ obj.write_success.should be_true
100
+ end
101
+
102
+ it "should be able to assign an image" do
103
+ obj = NObject.new(@string, :bin => true, :image => true)
104
+ obj.image = [0] * @fixture_image_length
105
+ end
106
+
107
+ after :each do
108
+ File.delete @new_fixture_file_name if File.exist? @new_fixture_file_name
109
+ end
110
+
111
+ end