rfits 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/Rakefile.rb +92 -0
- data/lib/rfits.rb +1 -0
- data/lib/rfits/ext/extconf.rb +7 -0
- data/lib/rfits/ext/rfitsio.c +2797 -0
- data/lib/rfits/rfits.rb +1328 -0
- data/test/rfits_test.rb +310 -0
- data/test/rfitsio_test.rb +315 -0
- data/test/test.1.fits +7522 -2
- data/test/test.2.fits +1796 -0
- metadata +55 -0
data/lib/rfits/rfits.rb
ADDED
@@ -0,0 +1,1328 @@
|
|
1
|
+
require 'ftools'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
# RFits is a library for parsing the Flexible Image Transport System (FITS) files widely used in astronomy.
|
6
|
+
#
|
7
|
+
# ==== Installing
|
8
|
+
#
|
9
|
+
# RFits requires the C library CFITSIO (http://heasarc.gsfc.nasa.gov/docs/software/fitsio/fitsio.html)
|
10
|
+
# to be installed. In most cases this is as simple as:
|
11
|
+
#
|
12
|
+
# > tar zxvf cfitsioxxxx.tar.gz
|
13
|
+
# > cd cfitsio
|
14
|
+
# > ./configure
|
15
|
+
# > make shared
|
16
|
+
# > sudo make install
|
17
|
+
#
|
18
|
+
# You can then install via rubygems in the usual way:
|
19
|
+
#
|
20
|
+
# > gem install rfits
|
21
|
+
#
|
22
|
+
# ==== Using
|
23
|
+
#
|
24
|
+
# require 'rubygems'
|
25
|
+
# require 'rfits'
|
26
|
+
#
|
27
|
+
# RFits::File.open('m31.fits', 'rw') do |fits| # m31.fits exists
|
28
|
+
# # first extension is an image
|
29
|
+
# img = fits[0]
|
30
|
+
#
|
31
|
+
# # retrieve/write header values using hash syntax
|
32
|
+
# header = img.header
|
33
|
+
# puts header['TELESCOP'] #=> 'KPNO 4.0 meter telescope'
|
34
|
+
# puts header['TELEQUIN'] #=> 2000.0
|
35
|
+
# puts header['SIMPLE'] #=> true
|
36
|
+
# puts header['TELFOCUS'] #=> -9998
|
37
|
+
# header['MY_HDR1'] = 'A nice value'
|
38
|
+
# header['MY_HDR2'] = 10
|
39
|
+
# header['MY_HDR3'] = 9.1
|
40
|
+
# header['MY_HDR4'] = Complex.new(4, 1) # yes, you can do this
|
41
|
+
#
|
42
|
+
# # retrieve/write pixel values using array syntax
|
43
|
+
# pixels = img.data
|
44
|
+
# first_pixel = pixels[0] # the first pixel
|
45
|
+
# pixel_list = pixels[0..10] # pixels 0 through 10 as an array
|
46
|
+
# pixel_list = pixels[[0, 0], [10, 10]] # pixels between the points [0, 0] and [10, 10]
|
47
|
+
# pixels[10] = 5 # the ninth pixel value is set to 5
|
48
|
+
# pixels[3..7] = [1, 4, 2, 8] # pixels 3 through 7 are set
|
49
|
+
#
|
50
|
+
# # second extension is a binary table
|
51
|
+
# tbl = fits[1]
|
52
|
+
#
|
53
|
+
# # access a table using array syntax
|
54
|
+
# data = tbl.data
|
55
|
+
# row = data[0] # first row as a hetergeneous array
|
56
|
+
# col = data.column(2) # third column
|
57
|
+
# data[1] = [1, 1.2, 'blah', Complex.new(1, 1)] # second row is set
|
58
|
+
# data.set_column(1, [1.3, 4.5, 7.8, 2.2]) # the second column is set
|
59
|
+
# col << [1, 1.2, 'blah', Complex.new(1, 1)] # append a row
|
60
|
+
#
|
61
|
+
# # you can find out things about the table columns
|
62
|
+
# metadata = tbl.column_information
|
63
|
+
# puts metadata[0].data_type #=> :short
|
64
|
+
# puts metadata[0].name #=> 'catalog_id'
|
65
|
+
#
|
66
|
+
# # and add new ones
|
67
|
+
# metadata << {:name => 'new_column1', :format => 'A10'} # append a new string column
|
68
|
+
# metadata[3] = {:name => 'new_column2', :format => 'I'} # insert an integer column as the 4th column
|
69
|
+
# end
|
70
|
+
module RFits
|
71
|
+
require 'rfits/ext/rfitsio'
|
72
|
+
|
73
|
+
# A class representing a FITS file.
|
74
|
+
class File
|
75
|
+
include Enumerable
|
76
|
+
|
77
|
+
attr_reader :io
|
78
|
+
|
79
|
+
# Open an existing FITS file or create a new one. The mode parameter
|
80
|
+
# may be on of:
|
81
|
+
# * r opens a file for reading. If it doesn't exist, it creates it.
|
82
|
+
# * rw opens afile for reading and writing. If it doesn't exist, it creates it.
|
83
|
+
# * rw+ opens a file for reading and writing. If it exists, it truncates it.
|
84
|
+
#
|
85
|
+
# fits = RFits::File.new('m31.fits', 'rw') # this is probably what you want
|
86
|
+
def initialize(path, mode="rw")
|
87
|
+
iomode = mode.match('w') ? IO::Proxy::READWRITE : IO::Proxy::READONLY
|
88
|
+
truncate = mode.match('\+') ? true : false
|
89
|
+
|
90
|
+
if truncate and mode.match('w')
|
91
|
+
@io = IO::Proxy::fits_create_file("!#{path}")
|
92
|
+
else
|
93
|
+
@io = ::File.exists?(path) ? IO::Proxy.fits_open_file(path, iomode) : IO::Proxy::fits_create_file(path)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Works as RFits::File#new but accepts a block. The FITS file is automatically
|
98
|
+
# closed after execution of the block.
|
99
|
+
#
|
100
|
+
# RFits::File.open('m31.fits', 'rw') do |fits|
|
101
|
+
# # do things to the FITS file, fits.close gets called automatically
|
102
|
+
# end
|
103
|
+
def self.open(path, mode="rw")
|
104
|
+
fits = File.new(path, mode)
|
105
|
+
if block_given?
|
106
|
+
yield fits
|
107
|
+
fits.close
|
108
|
+
else
|
109
|
+
fits
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Close the FITS file.
|
114
|
+
# file.close()
|
115
|
+
def close
|
116
|
+
IO::Proxy::fits_close_file(self.io)
|
117
|
+
end
|
118
|
+
|
119
|
+
# The filesystem path to the FITS file.
|
120
|
+
# puts file.path # m31.fits
|
121
|
+
def path
|
122
|
+
IO::Proxy::fits_file_name(self.io)
|
123
|
+
end
|
124
|
+
|
125
|
+
# The IO mode of the FITS file (i.e. IO::Proxy::READWRITE or IO::Proxy::READONLY)
|
126
|
+
# puts file.mode # 1 = IO::Proxy::READWRITE
|
127
|
+
def mode
|
128
|
+
IO::Proxy.fits_file_mode(self.io)
|
129
|
+
end
|
130
|
+
|
131
|
+
# The type of URL of the FITS file (i.e. file:// or ftp://).
|
132
|
+
# puts file.url_type # file://
|
133
|
+
def url_type
|
134
|
+
IO::Proxy.fits_url_type(self.io)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get the specified HDU associated with the FITS file. Contrary
|
138
|
+
# to FITS conventions this is *zero-based* access. So:
|
139
|
+
# primary_hdu = fits[0] # primary array
|
140
|
+
# hdu = fits[2] # third HDU
|
141
|
+
def [](extpos)
|
142
|
+
extpos = extpos < 0 ? (self.length + extpos) : extpos
|
143
|
+
|
144
|
+
set_position(extpos)
|
145
|
+
hdu = case IO::Proxy.fits_get_hdu_type(self.io)
|
146
|
+
when IO::Proxy::IMAGE_HDU then Image.new(self, extpos, nil, nil)
|
147
|
+
when IO::Proxy::ASCII_TBL then AsciiTable.new(self, extpos, nil, nil)
|
148
|
+
when IO::Proxy::BINARY_TBL then BinaryTable.new(self, extpos, nil, nil)
|
149
|
+
else HDU.new(self, extpos)
|
150
|
+
end
|
151
|
+
|
152
|
+
hdu
|
153
|
+
end
|
154
|
+
|
155
|
+
# Get the last HDU in the file.
|
156
|
+
def last
|
157
|
+
self[-1]
|
158
|
+
end
|
159
|
+
|
160
|
+
# Get the first HDU in the file.
|
161
|
+
def first
|
162
|
+
self[0]
|
163
|
+
end
|
164
|
+
|
165
|
+
# Iterates through each HDU in the FITS file.
|
166
|
+
# fits.each do |hdu|
|
167
|
+
# # do something...
|
168
|
+
# end
|
169
|
+
def each
|
170
|
+
(0...self.length).each do |i|
|
171
|
+
yield self[i]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# The number of HDUs in the FITS file.
|
176
|
+
def length
|
177
|
+
IO::Proxy.fits_get_num_hdus(self.io)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Delete the HDU at the specified position. Again, this is
|
181
|
+
# zero-based access.
|
182
|
+
# fits.delete_at(2) # delete the third HDU
|
183
|
+
def delete_at(extpos)
|
184
|
+
set_position(extpos)
|
185
|
+
Proxy.fits_delete_hdu(self.io)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Delete the specified HDU.
|
189
|
+
# fits.delete(hdu)
|
190
|
+
def delete(hdu)
|
191
|
+
delete_at(hdu.position)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Append a new HDU to the file.
|
195
|
+
# fits << RFits::Image.new(@fits, nil, RFits::IO::Proxy::LONG_IMG, [4, 4]) # notice you can leave the position nil
|
196
|
+
def <<(hdu, *args)
|
197
|
+
hdu.create(self.length, *args)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Insert a new HDU at the specified position.
|
201
|
+
# fits[1] = RFits::Image.new(@fits, nil, RFits::IO::Proxy::LONG_IMG, [2, 3]) # the old hdu at pos 1 gets pushed down
|
202
|
+
def []=(pos, *args)
|
203
|
+
hdu = args[0]
|
204
|
+
hdu.create(pos, *args[1..-1])
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_position(extpos) #:nodoc:
|
208
|
+
IO::Proxy.fits_movabs_hdu(self.io, extpos + 1)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Represents the header keyword information of an HDU. Behaves
|
213
|
+
# much like a hash.
|
214
|
+
class Header
|
215
|
+
include Enumerable
|
216
|
+
|
217
|
+
# The HDU associated with the header.
|
218
|
+
attr_reader :hdu
|
219
|
+
|
220
|
+
# Initialize the header. This doesn't actually write bytes to file--use Header#create for that.
|
221
|
+
# header = Header.new(hdu)
|
222
|
+
def initialize(hdu)
|
223
|
+
@hdu = hdu
|
224
|
+
end
|
225
|
+
|
226
|
+
# Retrieve the value of the specified keyword.
|
227
|
+
# If _include_comment_ is true, the returned value will be a two-element
|
228
|
+
# array with the value as the first element, and the comment as the second.
|
229
|
+
# The return type of the value will be determined automatically--this is
|
230
|
+
# almost always what you want. However, if necessary, you may specify the type using _as_,
|
231
|
+
# which may be one of: "C" (string), "L" (logical), "I" (integer), "F" (float) or "C" (complex)
|
232
|
+
# and the library will try to do the right thing.
|
233
|
+
#
|
234
|
+
# puts header['OBJNAME'] # M31
|
235
|
+
# puts header['OBJNAME', true] # ['M31', 'The name of the object']
|
236
|
+
# puts header['BITPIX'] # 8
|
237
|
+
# puts header['BITPIX', false, 'F'] # 8.0
|
238
|
+
def [](keyname, include_comment=false, as=nil)
|
239
|
+
self.hdu.reset_position()
|
240
|
+
IO::Proxy.fits_read_keyn(self.hdu.file.io, 0)
|
241
|
+
|
242
|
+
begin
|
243
|
+
keytype = as ? as : IO::Proxy.fits_get_keytype(IO::Proxy.fits_read_keyword(self.hdu.file.io, keyname).first) rescue IO::Exception
|
244
|
+
key = case keytype
|
245
|
+
when 'C' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TSTRING, keyname)
|
246
|
+
when 'L' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TLOGICAL, keyname)
|
247
|
+
when 'I' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TLONG, keyname)
|
248
|
+
when 'F' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TDOUBLE, keyname)
|
249
|
+
when 'X' then IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TDBLCOMPLEX, keyname)
|
250
|
+
else IO::Proxy.fits_read_key(self.hdu.file.io, IO::Proxy::TSTRING, keyname)
|
251
|
+
end
|
252
|
+
include_comment ? key : key.first
|
253
|
+
rescue IO::Exception
|
254
|
+
nil
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Set the value of the keyword. If _keyvalue_ is an array, the first
|
259
|
+
# element of that array is assumed to be the value, and the second the
|
260
|
+
# comment. The value may be a: String, boolean, Fixnum, Float or Complex
|
261
|
+
# object. Anything else will be serialized using it's to_s method and
|
262
|
+
# saved as a string.
|
263
|
+
#
|
264
|
+
# header['TEST1'] = 123 # set keyword value to 123
|
265
|
+
# header['TEST1'] = [123, 'a test integer'] # set keyword value to 123 with "a test integer" as the comment
|
266
|
+
def []=(keyname, keyvalue)
|
267
|
+
self.hdu.reset_position()
|
268
|
+
|
269
|
+
value, comment = keyvalue.is_a?(Array) ? keyvalue : [keyvalue, nil]
|
270
|
+
|
271
|
+
dtype = case value
|
272
|
+
when String then IO::Proxy::TSTRING
|
273
|
+
when TrueClass then IO::Proxy::TLOGICAL
|
274
|
+
when FalseClass then IO::Proxy::TLOGICAL
|
275
|
+
when Fixnum then IO::Proxy::TLONG
|
276
|
+
when Float then IO::Proxy::TDOUBLE
|
277
|
+
when Complex then IO::Proxy::TDBLCOMPLEX
|
278
|
+
else
|
279
|
+
value = value.to_s
|
280
|
+
IO::Proxy::TSTRING
|
281
|
+
end
|
282
|
+
|
283
|
+
IO::Proxy.fits_update_key(self.hdu.file.io, dtype, keyname, value, comment)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Iterate through each keyword pair in the header. By default,
|
287
|
+
# values will be converted to their appropriate types, but this can
|
288
|
+
# be overridden by setting _typecast_ to false. Additionally,
|
289
|
+
# COMMENT and HISTORY keywords are ignored unless _comment_keywords_
|
290
|
+
# is set to true.
|
291
|
+
#
|
292
|
+
# header.each do |name, value, comment|
|
293
|
+
# puts "#{name} = #{value}"
|
294
|
+
# end
|
295
|
+
def each(typecast=true, comment_keywords=false)
|
296
|
+
self.hdu.reset_position()
|
297
|
+
|
298
|
+
(1..self.length).each do |i|
|
299
|
+
keyname = value = comment = nil
|
300
|
+
if typecast
|
301
|
+
keyname = IO::Proxy.fits_read_keyn(self.hdu.file.io, i).first
|
302
|
+
value, comment = self[keyname, true]
|
303
|
+
else
|
304
|
+
keyname, value, comment = IO::Proxy.fits_read_keyn(self.hdu.file.io, i)
|
305
|
+
end
|
306
|
+
|
307
|
+
yield(keyname, value, comment) unless (keyname == "COMMENT" or keyname == "HISTORY") and !comment_keywords
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Delete the specified keyword.
|
312
|
+
# header.delete('OBJNAME')
|
313
|
+
def delete(keyname)
|
314
|
+
self.hdu.reset_position()
|
315
|
+
IO::Proxy.fits_delete_key(self.hdu.file.io, keyname)
|
316
|
+
end
|
317
|
+
|
318
|
+
# The number of keyword pairs in the header.
|
319
|
+
# puts header.length # 62
|
320
|
+
def length
|
321
|
+
self.hdu.reset_position()
|
322
|
+
IO::Proxy.fits_get_hdrspace(self.hdu.file.io).first
|
323
|
+
end
|
324
|
+
|
325
|
+
# Retrieve COMMENT keywords as a string.
|
326
|
+
# puts header.comments # This file is part of the EUVE Science Archive. It contains...
|
327
|
+
def comment
|
328
|
+
self.hdu.reset_position()
|
329
|
+
|
330
|
+
statements = []
|
331
|
+
self.each(false, true) do |keyname, keyvalue, keycomment|
|
332
|
+
statements << keycomment.strip.gsub(/^\'/, '').gsub(/\'$/, '') if keyname == 'COMMENT'
|
333
|
+
end
|
334
|
+
|
335
|
+
statements.join("\n")
|
336
|
+
end
|
337
|
+
|
338
|
+
# Append a COMMENT keyword.
|
339
|
+
# header.comment = "My comment"
|
340
|
+
def comment=(comment)
|
341
|
+
self.hdu.reset_position()
|
342
|
+
IO::Proxy.fits_write_comment(self.hdu.file.io, comment)
|
343
|
+
end
|
344
|
+
|
345
|
+
# Retrieve HISTORY keywords as a strong.
|
346
|
+
# puts header.history
|
347
|
+
def history
|
348
|
+
self.hdu.reset_position()
|
349
|
+
|
350
|
+
statements = []
|
351
|
+
self.each(false, true) do |keyname, keyvalue, keycomment|
|
352
|
+
statements << keycomment.strip.gsub(/^\'/, '').gsub(/\'$/, '') if keyname == 'HISTORY'
|
353
|
+
end
|
354
|
+
|
355
|
+
statements.join("\n")
|
356
|
+
end
|
357
|
+
|
358
|
+
# Append a HISTORY comment.
|
359
|
+
# header.history = "My history"
|
360
|
+
def history=(history)
|
361
|
+
self.hdu.reset_position()
|
362
|
+
IO::Proxy.fits_write_history(self.hdu.file.io, history)
|
363
|
+
end
|
364
|
+
|
365
|
+
# Convert the header to a string.
|
366
|
+
def to_s
|
367
|
+
self.hdu.reset_position()
|
368
|
+
IO::Proxy.fits_hdr2str(self.hdu.file.io, false, [], 0)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# Represents the actual pixels in a FITS file.
|
373
|
+
class ImageData
|
374
|
+
include Enumerable
|
375
|
+
|
376
|
+
BITPIX_IMG_MAP = {
|
377
|
+
IO::Proxy::BYTE_IMG => IO::Proxy::TBYTE,
|
378
|
+
IO::Proxy::SHORT_IMG => IO::Proxy::TSHORT,
|
379
|
+
IO::Proxy::LONG_IMG => IO::Proxy::TLONG,
|
380
|
+
IO::Proxy::LONGLONG_IMG => IO::Proxy::TLONGLONG,
|
381
|
+
IO::Proxy::FLOAT_IMG => IO::Proxy::TFLOAT,
|
382
|
+
IO::Proxy::DOUBLE_IMG => IO::Proxy::TDOUBLE,
|
383
|
+
IO::Proxy::SBYTE_IMG => IO::Proxy::TSBYTE,
|
384
|
+
IO::Proxy::USHORT_IMG => IO::Proxy::TUSHORT,
|
385
|
+
IO::Proxy::ULONG_IMG => IO::Proxy::TULONG
|
386
|
+
}
|
387
|
+
|
388
|
+
# The image the data belongs to.
|
389
|
+
attr_reader :image
|
390
|
+
|
391
|
+
# Create an ImageData object associated with the specified image.
|
392
|
+
def initialize(image)
|
393
|
+
@image = image
|
394
|
+
end
|
395
|
+
|
396
|
+
# An alias for ImageData#pixels, but automatically chooses the
|
397
|
+
# datatype for you based on the type of image (i.e. BITPIX).
|
398
|
+
def [](*args)
|
399
|
+
self.pixels(BITPIX_IMG_MAP[self.image.bitpix], *args)
|
400
|
+
end
|
401
|
+
|
402
|
+
# An alias for ImageData#set_pixels, but automatically chooses the
|
403
|
+
# datatype for you based on the type of image (i.e. BITPIX).
|
404
|
+
def []=(*args)
|
405
|
+
self.set_pixels(BITPIX_IMG_MAP[self.image.bitpix], *args)
|
406
|
+
end
|
407
|
+
|
408
|
+
# Retrieve the value of a pixel, or a range of pixels. The value
|
409
|
+
# is coerced into an appropriate class according to the datatype provided.
|
410
|
+
# Can be used in a number of ways. The simplest plucks the nth pixel from the image array:
|
411
|
+
#
|
412
|
+
# image.data.pixels(IO::Proxy::TSHORT, 0) # first pixel in the image (remember zero-based indexing?)
|
413
|
+
# image.data.pixels(IO::Proxy::TSHORT, image.data[image.data.size - 1]) # last pixel in the image
|
414
|
+
#
|
415
|
+
# You can also choose a range of contiguous pixels, in which case an array of values is returned:
|
416
|
+
#
|
417
|
+
# image.data.pixels(IO::Proxy::TSHORT, 0..10) # retrieve first 10 pixels
|
418
|
+
#
|
419
|
+
# or pick the single pixel at a particular coordinate:
|
420
|
+
#
|
421
|
+
# image.data.pixels(IO::Proxy::TSHORT, 4, 5)
|
422
|
+
#
|
423
|
+
# or equivalently:
|
424
|
+
#
|
425
|
+
# image.data.pixels(IO::Proxy::TSHORT, [4, 5])
|
426
|
+
#
|
427
|
+
# In addition, you can give an (x,y) coordinate and a number of pixels, assuming the coordinates are zero-based:
|
428
|
+
#
|
429
|
+
# image.data.pixels(IO::Proxy::TSHORT, [4, 5], 10) # 10 pixels starting at the point [4, 5]
|
430
|
+
#
|
431
|
+
# or get all the pixels between two points:
|
432
|
+
#
|
433
|
+
# image.data.pixels(IO::Proxy::TSHORT, [4, 5], [6, 6]) # all the pixels between the points [4, 5] and [6, 6]
|
434
|
+
def pixels(datatype, *args)
|
435
|
+
self.image.reset_position()
|
436
|
+
|
437
|
+
pixnum = nelem = nil
|
438
|
+
pixel_values = case args.size
|
439
|
+
when 1 # 1 argument
|
440
|
+
if args.first.is_a?(Range) # a range of pixels
|
441
|
+
pixnum = args.first.first.to_i < 0 ? self.length + args.first.first.to_i : args.first.first.to_i + 1
|
442
|
+
nelem = args.first.exclude_end? ?
|
443
|
+
args.first.last.to_i - args.first.first.to_i + 1:
|
444
|
+
args.first.last.to_i - args.first.first.to_i
|
445
|
+
elsif args.first.is_a?(Fixnum) # the nth pixel
|
446
|
+
pixnum = args.first.to_i < 0 ? self.length + args.first.to_i : args.first.to_i + 1
|
447
|
+
nelem = 1
|
448
|
+
elsif args.first.is_a?(Array) # array representing single pixel
|
449
|
+
return(IO::Proxy.fits_read_pix(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, 1, nil).first.first)
|
450
|
+
else
|
451
|
+
raise ArgumentError, "Argument should be an integer, an integer range, or an array of integers."
|
452
|
+
end
|
453
|
+
|
454
|
+
IO::Proxy.fits_read_img(self.image.file.io, datatype, pixnum, nelem, nil).first
|
455
|
+
when 2 # 2 arguments
|
456
|
+
if args.first.is_a?(Fixnum) and args.last.is_a?(Fixnum) # the pixel at the coordinate
|
457
|
+
IO::Proxy.fits_read_pix(self.image.file.io, datatype, [args.first.to_i+1, args.last.to_i+1], 1, nil).first
|
458
|
+
elsif args.first.is_a?(Array) and args.last.is_a?(Fixnum) # a starting coordinate + some number of pixels
|
459
|
+
fpixel = args.first.collect{ |p| p + 1 }
|
460
|
+
IO::Proxy.fits_read_pix(self.image.file.io, datatype, fpixel, args.last.to_i, nil).first
|
461
|
+
elsif args.first.is_a?(Array) and args.last.is_a?(Array) # a starting coordinate + ending coordinate
|
462
|
+
fpixel = args.first.collect{ |p| p + 1 }
|
463
|
+
lpixel = args.last.collect{ |p| p + 1 }
|
464
|
+
IO::Proxy.fits_read_subset(self.image.file.io, datatype, fpixel, lpixel, args.first.collect{ |dim| 1 }, nil).first
|
465
|
+
else
|
466
|
+
raise ArgumentError, "Arguments should be two fixnums, an array and a fixnum, or two arrays."
|
467
|
+
end
|
468
|
+
else
|
469
|
+
raise ArgumentError, "Arguments should be an integer, an integer range, an array of integers, two fixnums, an array and a fixnum, or two arrays."
|
470
|
+
end
|
471
|
+
|
472
|
+
pixel_values.size == 1 ? pixel_values.first : pixel_values
|
473
|
+
end
|
474
|
+
|
475
|
+
# Set the value of a pixel, or a range of pixels. The value
|
476
|
+
# is coerced into an appropriate class according to the datatype provided.
|
477
|
+
# Can be used in a number of ways. The simplest sets the nth pixel in the image array:
|
478
|
+
#
|
479
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, 0, 5) # first pixel in the image is set to 5
|
480
|
+
#
|
481
|
+
# You can also set a range of contiguous pixels:
|
482
|
+
#
|
483
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, 0..10, [5, 5, 3, 1, 0, 9, 5, 6, 7, 1]) # set first 10 pixels
|
484
|
+
#
|
485
|
+
# or set the single pixel at a particular coordinate:
|
486
|
+
#
|
487
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, 4, 5, 10) # set pixel at [4, 5] to 10
|
488
|
+
#
|
489
|
+
# or equivalently:
|
490
|
+
#
|
491
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, [4, 5], 10)
|
492
|
+
#
|
493
|
+
# In addition, you can give an (x,y) coordinate and a list of pixels, assuming the coordinates are zero-based:
|
494
|
+
#
|
495
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, [4, 5], [1, 2, 3, 4]) # four pixels with values 1, 2, 3, 4 starting at [4, 5]
|
496
|
+
#
|
497
|
+
# or set the pixels between two points:
|
498
|
+
#
|
499
|
+
# image.data.set_pixels(IO::Proxy::TSHORT, [0, 0], [1, 1], [5, 6, 7, 8]) # set pixels to (5, 6, 7, 8) between [0, 0] and [1, 1]
|
500
|
+
def set_pixels(datatype, *args)
|
501
|
+
self.image.reset_position()
|
502
|
+
|
503
|
+
val = args.last.is_a?(Array) ? args.last : [args.last]
|
504
|
+
case args.size
|
505
|
+
when 2
|
506
|
+
if args.first.is_a?(Fixnum)
|
507
|
+
pixnum = args.first.to_i < 0 ? self.length + args.first.to_i : args.first.to_i + 1
|
508
|
+
IO::Proxy.fits_write_img(self.image.file.io, datatype, pixnum, val.size, val)
|
509
|
+
elsif args.first.is_a?(Range)
|
510
|
+
pixnum = args.first.first.to_i < 0 ? self.length + args.first.first.to_i : args.first.first.to_i + 1
|
511
|
+
nelem = args.first.exclude_end? ?
|
512
|
+
args.first.last.to_i - args.first.first.to_i + 1:
|
513
|
+
args.first.last.to_i - args.first.first.to_i
|
514
|
+
IO::Proxy.fits_write_img(self.image.file.io, datatype, pixnum, nelem, val)
|
515
|
+
elsif(args.first.is_a?(Array))
|
516
|
+
IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, val.size, val, nil)
|
517
|
+
else
|
518
|
+
raise ArgumentError, "Selector should be an integer, an integer range, or an array of integers."
|
519
|
+
end
|
520
|
+
when 3
|
521
|
+
if args.first.is_a?(Fixnum) and args[-2].is_a?(Fixnum)
|
522
|
+
IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args[0..-2].collect{ |c| c + 1 }, val.size, val, nil)
|
523
|
+
elsif args.first.is_a?(Array) and args[-2].is_a?(Fixnum)
|
524
|
+
IO::Proxy.fits_write_pixnull(self.image.file.io, datatype, args.first.collect{ |c| c + 1 }, args[-2], val, nil)
|
525
|
+
elsif args.first.is_a?(Array) and args[-2].is_a?(Array)
|
526
|
+
IO::Proxy.fits_write_subset(self.image.file.io, datatype,
|
527
|
+
args.first.collect{ |c| c + 1 }, args[1].collect{ |c| c + 1 },
|
528
|
+
val)
|
529
|
+
else
|
530
|
+
raise ArgumentError, "Selectors should be two fixnums, an array and a fixnum, or two arrays."
|
531
|
+
end
|
532
|
+
else
|
533
|
+
raise ArgumentError, "Selectors should be an integer, an integer range, an array of integers, two fixnums, an array and a fixnum, or two arrays."
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
# The number of pixels in the image array.
|
538
|
+
def length
|
539
|
+
self.image.naxes.inject(1){ |product, n| product *= n }
|
540
|
+
end
|
541
|
+
|
542
|
+
# Iterate through each pixel in the array.
|
543
|
+
def each
|
544
|
+
(0...self.length).each do |i|
|
545
|
+
yield self[i]
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
# Retrieve the ith (where i is zero-based) row of the image array.
|
550
|
+
# Assumes a two-dimensional image.
|
551
|
+
def row(i)
|
552
|
+
self[[0, i], self.image.naxes.first]
|
553
|
+
end
|
554
|
+
|
555
|
+
# Set the ith row of a 2D image to the specified pixels.
|
556
|
+
def set_row(i, vals)
|
557
|
+
self[[0, i], self.image.naxes.first] = vals
|
558
|
+
end
|
559
|
+
|
560
|
+
# Iterate through each row of pixels in the array.
|
561
|
+
# Assumes a two-dimensional image.
|
562
|
+
def each_row
|
563
|
+
(0...self.image.naxes.last).each do |i|
|
564
|
+
yield(self.row(i), i)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# Retrieve the ith (where i is zero-based) column of the image array.
|
569
|
+
# Assumes a two-dimensional image.
|
570
|
+
def column(i)
|
571
|
+
pixel_list = []
|
572
|
+
(0...self.image.naxes.first).each do |row|
|
573
|
+
pixel_list << self[i, row]
|
574
|
+
end
|
575
|
+
|
576
|
+
pixel_list
|
577
|
+
end
|
578
|
+
|
579
|
+
# Set the ith column of a 2D image to the specified pixels.
|
580
|
+
def set_column(i, vals)
|
581
|
+
(0...self.image.naxes.first).each do |row|
|
582
|
+
self[i, row] = vals[row]
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
# Iterate through each column of pixels in the array.
|
587
|
+
# Assumes a two-dimensional image.
|
588
|
+
def each_column
|
589
|
+
(0...self.image.naxes.last).each do |i|
|
590
|
+
yield(self.column(i), i)
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
# The first pixel in the image array.
|
595
|
+
def first
|
596
|
+
self[0]
|
597
|
+
end
|
598
|
+
|
599
|
+
# The last pixel in the image array.
|
600
|
+
def last
|
601
|
+
self[self.length - 1]
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
# Represents tabular data in a FITS file.
|
606
|
+
class TableData
|
607
|
+
include Enumerable
|
608
|
+
|
609
|
+
# The table the data lives in.
|
610
|
+
attr_reader :table
|
611
|
+
|
612
|
+
# Create a TableData object that is associated with the specifed table.
|
613
|
+
# data = TableData.new(table)
|
614
|
+
def initialize(table)
|
615
|
+
@table = table
|
616
|
+
end
|
617
|
+
|
618
|
+
# Set the ith columns to the specified values.
|
619
|
+
# data.set_column(2, [1, 2, 3, 4, 5]) # set the 3rd column (which is an integer column)
|
620
|
+
def set_column(i, values)
|
621
|
+
self.table.reset_position
|
622
|
+
|
623
|
+
IO::Proxy.fits_write_colnull(self.table.file.io,
|
624
|
+
self.table.column_information[i].data_type(true), i+1, 1, 1, values.size,
|
625
|
+
values, nil)
|
626
|
+
end
|
627
|
+
|
628
|
+
# Retrieve the ith column.
|
629
|
+
# data.column(2) # [1, 2, 3, 4, 5]
|
630
|
+
def column(i)
|
631
|
+
self.table.reset_position
|
632
|
+
|
633
|
+
IO::Proxy.fits_read_col(self.table.file.io,
|
634
|
+
self.table.column_information[i].data_type(true), i+1, 1, 1, self.size, nil).first
|
635
|
+
end
|
636
|
+
|
637
|
+
# Retrieve the ith row of data.
|
638
|
+
# data[2] # ['a1', 1.1, 3, Complex.new(3, 4)]
|
639
|
+
# or the value of the cell at the specified row and column.
|
640
|
+
# data[2, 3] # Complex.new(3, 4) # the 4th cell in the 3rd row
|
641
|
+
def [](*args)
|
642
|
+
self.table.reset_position
|
643
|
+
|
644
|
+
result = case args.length
|
645
|
+
when 1 # retrieve the whole row
|
646
|
+
row = []
|
647
|
+
(0...self.table.column_information.size).each do |col|
|
648
|
+
row << IO::Proxy.fits_read_col(self.table.file.io,
|
649
|
+
self.table.column_information[col].data_type(true),
|
650
|
+
col+1, (args.first.to_i+1), 1, 1, nil).first.first
|
651
|
+
end
|
652
|
+
row
|
653
|
+
when 2 # retrieve the value of the cell
|
654
|
+
IO::Proxy.fits_read_col(self.table.file.io,
|
655
|
+
self.table.column_information[args.last].data_type(true),
|
656
|
+
(args.last.to_i)+1, (args.first.to_i)+1, 1, 1, nil).first.first
|
657
|
+
else
|
658
|
+
raise ArgumentError, "Selector should be an integer indicating the row, or two integers indicating the row and column."
|
659
|
+
end
|
660
|
+
|
661
|
+
result
|
662
|
+
end
|
663
|
+
|
664
|
+
# Alias for TableData#[]
|
665
|
+
def row(i)
|
666
|
+
self[i]
|
667
|
+
end
|
668
|
+
|
669
|
+
# Set the ith row to the specified values
|
670
|
+
# data[4] = ['new', 2.2, 7, Complex.new(5, 5)]
|
671
|
+
# or specify the value of a particular cell
|
672
|
+
# data[4, 1] = 2.2 # the second column of the 5th row
|
673
|
+
def []=(*args)
|
674
|
+
self.table.reset_position
|
675
|
+
|
676
|
+
i = args.first
|
677
|
+
|
678
|
+
case args.length
|
679
|
+
when 2
|
680
|
+
values = args[1..-1].first
|
681
|
+
|
682
|
+
(0...values.size).each do |col|
|
683
|
+
IO::Proxy.fits_write_colnull(self.table.file.io,
|
684
|
+
self.table.column_information[col].data_type(true), col+1, i+1, 1, 1,
|
685
|
+
[values[col]], nil)
|
686
|
+
end
|
687
|
+
when 3
|
688
|
+
col = args[1]
|
689
|
+
values = args[2..-1]
|
690
|
+
|
691
|
+
IO::Proxy.fits_write_colnull(self.table.file.io,
|
692
|
+
self.table.column_information[col].data_type(true), col+1, i+1, 1, 1,
|
693
|
+
values, nil)
|
694
|
+
else
|
695
|
+
raise ArgumentError, "Selector should be an integer indicating the row, or two integers indicating the row and column."
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
# Append a new row.
|
700
|
+
# data << ['new', 2.2, 7, Complex.new(5, 5)]
|
701
|
+
def <<(values)
|
702
|
+
self[self.size] = values
|
703
|
+
end
|
704
|
+
|
705
|
+
# Alias for TableData#[]=
|
706
|
+
def set_row(i, values)
|
707
|
+
self[i] = values
|
708
|
+
end
|
709
|
+
|
710
|
+
# The number of rows in the table.
|
711
|
+
# table.length # 5
|
712
|
+
def length
|
713
|
+
self.table.reset_position
|
714
|
+
IO::Proxy.fits_get_num_rows(self.table.file.io)
|
715
|
+
end
|
716
|
+
|
717
|
+
# Alias for TableData#length
|
718
|
+
def size
|
719
|
+
self.length
|
720
|
+
end
|
721
|
+
|
722
|
+
# Delete the ith row.
|
723
|
+
# table.delete(2) # delete the 3rd row
|
724
|
+
def delete(i)
|
725
|
+
self.table.reset_position
|
726
|
+
IO::Proxy.fits_delete_rows(self.table.file.io, i+1, 1)
|
727
|
+
end
|
728
|
+
|
729
|
+
# Alias for TableData#each.
|
730
|
+
def each_row
|
731
|
+
self.each do |row|
|
732
|
+
yield row
|
733
|
+
end
|
734
|
+
end
|
735
|
+
|
736
|
+
# Iterate through each row in the table.
|
737
|
+
def each
|
738
|
+
self.table.reset_position
|
739
|
+
|
740
|
+
(0...self.size).each do |i|
|
741
|
+
yield self[i]
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
# Iterate through each column in the table.
|
746
|
+
def each_column
|
747
|
+
(0...self.table.column_information.size).each do |i|
|
748
|
+
yield self.column(i)
|
749
|
+
end
|
750
|
+
end
|
751
|
+
|
752
|
+
# Retrieve the first row in the table.
|
753
|
+
# table.first # ['blah', 23.1, 9, Complex.new(4, 5)]
|
754
|
+
def first
|
755
|
+
self[0]
|
756
|
+
end
|
757
|
+
|
758
|
+
# Retrieve the last row in the table.
|
759
|
+
# table.last # ['new', 2.2, 7, Complex.new(5, 5)]
|
760
|
+
def last
|
761
|
+
self[self.size - 1]
|
762
|
+
end
|
763
|
+
|
764
|
+
# Convert the rows to a string. Currently an alias to TableData#to_csv
|
765
|
+
def to_s
|
766
|
+
self.to_csv
|
767
|
+
end
|
768
|
+
|
769
|
+
# Convert the rows in the table to CSV.
|
770
|
+
def to_csv
|
771
|
+
buffer = ''
|
772
|
+
|
773
|
+
self.each_row do |row|
|
774
|
+
CSV.generate_row(row.collect{ |cell| cell.to_s }, row.size, buffer)
|
775
|
+
end
|
776
|
+
|
777
|
+
buffer
|
778
|
+
end
|
779
|
+
end
|
780
|
+
|
781
|
+
# The base class for all HDUs. It's possible to use this on its
|
782
|
+
# own, but really it's intended to be abstract.
|
783
|
+
class HDU
|
784
|
+
HDU_TYPE_MAP = {
|
785
|
+
IO::Proxy::IMAGE_HDU => :image,
|
786
|
+
IO::Proxy::ASCII_TBL => :ascii_tbl,
|
787
|
+
IO::Proxy::BINARY_TBL => :binary_tbl,
|
788
|
+
IO::Proxy::ANY_HDU => :other
|
789
|
+
}
|
790
|
+
|
791
|
+
# The file object associated with the HDU.
|
792
|
+
attr_reader :file
|
793
|
+
|
794
|
+
# The header associated with the HDU.
|
795
|
+
attr_reader :header
|
796
|
+
|
797
|
+
# The data associated with the HDU.
|
798
|
+
attr_reader :data
|
799
|
+
|
800
|
+
# Initialize a new HDU in the specified file at the specified position (zero-based).
|
801
|
+
# This doesn't actually write bytes to file--use HDU#create for that.
|
802
|
+
# fits = File.open('m31.fits', 'rw') do |f|
|
803
|
+
# hdu = HDU.new(f, 2)
|
804
|
+
# end
|
805
|
+
def initialize(file, extpos)
|
806
|
+
@file = file
|
807
|
+
@extpos = extpos
|
808
|
+
|
809
|
+
@header = Header.new(self)
|
810
|
+
@data = ImageData.new(self)
|
811
|
+
end
|
812
|
+
|
813
|
+
# Get the type of HDU. Possibilities are: :image, :ascii_tbl, :binary_tbl, :other
|
814
|
+
def hdu_type
|
815
|
+
reset_position()
|
816
|
+
HDU_TYPE_MAP[IO::Proxy.fits_get_hdu_type(self.file.io)] || :unknown
|
817
|
+
end
|
818
|
+
|
819
|
+
# Get the position of the HDU.
|
820
|
+
# puts hdu.position # 2 -> this is the third HDU in the file
|
821
|
+
def position
|
822
|
+
reset_position()
|
823
|
+
IO::Proxy.fits_get_hdu_num(self.file.io) - 1
|
824
|
+
end
|
825
|
+
|
826
|
+
def reset_position
|
827
|
+
IO::Proxy.fits_movabs_hdu(self.file.io, @extpos + 1)
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
# A set of routines for setting and retrieving compression characterstics.
|
832
|
+
module Compressible
|
833
|
+
COMPRESSION_TYPE_MAP = {
|
834
|
+
IO::Proxy::RICE_1 => :rice,
|
835
|
+
IO::Proxy::GZIP_1 => :gzip,
|
836
|
+
IO::Proxy::PLIO_1 => :plio,
|
837
|
+
IO::Proxy::HCOMPRESS_1 => :hcompress
|
838
|
+
}
|
839
|
+
|
840
|
+
COMPRESSION_DIM_KEY = 'ZNAXIS'
|
841
|
+
COMPRESSION_TYPE_KEY = 'ZCOMPTYPE'
|
842
|
+
COMPRESSION_TILE_KEY_ROOT = 'ZTILE'
|
843
|
+
COMPRESSION_VAR_KEY_ROOT = 'ZNAME'
|
844
|
+
COMPRESSION_VAR_VAL_ROOT = 'ZVAL'
|
845
|
+
COMPRESSION_NOISE_BIT_NAME_VALUE = 'NOISEBIT'
|
846
|
+
COMPRESSION_SCALE_NAME_VALUE = 'SCALE'
|
847
|
+
COMPRESSION_SMOOTH_NAME_VALUE = 'SMOOTH'
|
848
|
+
|
849
|
+
# Activate compression on the image.
|
850
|
+
# "options" is a hash with the following keys:
|
851
|
+
# [:compression_type] the type of compression (:rice, :gzip, :plio, :hcompress). Required.
|
852
|
+
# [:tile_dim] the size of the tiles used in compression. Optional.
|
853
|
+
# [:noise_bits] applicable for floating point images only. Optional.
|
854
|
+
# [:hcomp_scale] scale factor for use in hcompress compression. Optional.
|
855
|
+
# [:hcomp_smooth] smoothing factor for use in hcompress compression. Optional.
|
856
|
+
def activate_compression
|
857
|
+
self.compression_type = self.compression_options[:compression_type] if self.compression_options[:compression_type]
|
858
|
+
self.tile_dim = self.compression_options[:tile_dim] if self.compression_options[:tile_dim]
|
859
|
+
self.noise_bits = self.compression_options[:noise_bits] if self.compression_options[:noise_bits]
|
860
|
+
self.hcomp_scale = self.compression_options[:hcomp_scale] if self.compression_options[:hcomp_scale]
|
861
|
+
self.hcomp_smooth = self.compression_options[:hcomp_smooth] if self.compression_options[:hcomp_smooth]
|
862
|
+
end
|
863
|
+
|
864
|
+
# Turn off compression for the image.
|
865
|
+
def deactivate_compression
|
866
|
+
self.compression_type = 0
|
867
|
+
end
|
868
|
+
|
869
|
+
# Retrieve the compression type of the image. Possible values: :rice, :gzip, :plio, :hcompress
|
870
|
+
def compression_type
|
871
|
+
reset_position()
|
872
|
+
|
873
|
+
ctype = case self.header['ZCMPTYPE']
|
874
|
+
when 'GZIP_1' then IO::Proxy::GZIP_1
|
875
|
+
when 'RICE_1' then IO::Proxy::RICE_1
|
876
|
+
when 'HCOMPRESS_1' then IO::Proxy::HCOMPRESS_1
|
877
|
+
when 'PLIO_1' then IO::Proxy::PLIO_1
|
878
|
+
else
|
879
|
+
raise "Unknown compression type #{self.header[COMPRESSION_TYPE_KEY]}."
|
880
|
+
end
|
881
|
+
|
882
|
+
COMPRESSION_TYPE_MAP[ctype]
|
883
|
+
end
|
884
|
+
|
885
|
+
# Set the compression type of the image. Possible values: :rice, :gzip, :plio, :hcompress
|
886
|
+
# image.compression_type = :gzip
|
887
|
+
def compression_type=(ctype)
|
888
|
+
ctype ? IO::Proxy.fits_set_compression_type(self.file.io, COMPRESSION_TYPE_MAP.invert[ctype.to_sym] || ctype) :
|
889
|
+
IO::Proxy.fits_set_compression_type(self.file.io, nil)
|
890
|
+
end
|
891
|
+
|
892
|
+
# Get the tile dimensions for the compressed image.
|
893
|
+
def tile_dim
|
894
|
+
reset_position()
|
895
|
+
|
896
|
+
tile_dims = []
|
897
|
+
(1..self.header[COMPRESSION_DIM_KEY]).each do |i|
|
898
|
+
tile_dims << self.header["#{COMPRESSION_TILE_KEY_ROOT}#{i}"]
|
899
|
+
end
|
900
|
+
|
901
|
+
tile_dims
|
902
|
+
end
|
903
|
+
|
904
|
+
# Set the tile dimensions for the compressed image.
|
905
|
+
# image.tile_dim = [5, 5]
|
906
|
+
def tile_dim=(tilesizes)
|
907
|
+
IO::Proxy.fits_set_tile_dim(self.file.io, tilesizes.size, tilesizes)
|
908
|
+
end
|
909
|
+
|
910
|
+
# Get the noise bits for the compressed image.
|
911
|
+
def noise_bits
|
912
|
+
reset_position()
|
913
|
+
|
914
|
+
zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
|
915
|
+
name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_NOISE_BIT_NAME_VALUE
|
916
|
+
}
|
917
|
+
zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
|
918
|
+
end
|
919
|
+
|
920
|
+
# Set the noise bits for the compressed image.
|
921
|
+
def noise_bits=(bits)
|
922
|
+
IO::Proxy.fits_set_noise_bits(self.file.io, bits.to_i)
|
923
|
+
end
|
924
|
+
|
925
|
+
# Get the hcompress scale if the image is compressed using hcompress.
|
926
|
+
def hcomp_scale
|
927
|
+
reset_position()
|
928
|
+
|
929
|
+
zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
|
930
|
+
name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_SCALE_NAME_VALUE
|
931
|
+
}
|
932
|
+
zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
|
933
|
+
end
|
934
|
+
|
935
|
+
# Set the hcompress scale if the image is compressed using hcompress.
|
936
|
+
def hcomp_scale=(scale)
|
937
|
+
IO::Proxy.fits_set_hcomp_scale(self.file.io, scale.to_i)
|
938
|
+
end
|
939
|
+
|
940
|
+
# Get the hcompress smoothing factor if the image is compressed using hcompress.
|
941
|
+
def hcomp_smooth
|
942
|
+
reset_position()
|
943
|
+
|
944
|
+
zname_name, zname_value, zname_comment = self.header.find { |name, value, comment|
|
945
|
+
name.match(/^#{COMPRESSION_VAR_KEY_ROOT}/) and value == COMPRESSION_SMOOTH_NAME_VALUE
|
946
|
+
}
|
947
|
+
zname_name ? self.header["#{COMPRESSION_VAR_VAL_ROOT}#{zname_name.match(/^#{COMPRESSION_VAR_KEY_ROOT}(\d+)$/)[1]}"] : nil
|
948
|
+
end
|
949
|
+
|
950
|
+
# Set the hcompress smoothing factor if the image is compressed using hcompress.
|
951
|
+
def hcomp_smooth=(smooth)
|
952
|
+
IO::Proxy.fits_set_hcomp_smooth(self.file.io, smooth.to_i)
|
953
|
+
end
|
954
|
+
end
|
955
|
+
|
956
|
+
# An Image HDU.
|
957
|
+
class Image < HDU
|
958
|
+
include Compressible
|
959
|
+
|
960
|
+
attr_reader :compression_options
|
961
|
+
|
962
|
+
IMG_TYPE_MAP = {
|
963
|
+
IO::Proxy::BYTE_IMG => :byte,
|
964
|
+
IO::Proxy::SHORT_IMG => :short,
|
965
|
+
IO::Proxy::LONG_IMG => :long,
|
966
|
+
IO::Proxy::LONGLONG_IMG => :longlong,
|
967
|
+
IO::Proxy::FLOAT_IMG => :float,
|
968
|
+
IO::Proxy::DOUBLE_IMG => :double
|
969
|
+
}
|
970
|
+
|
971
|
+
# Instantiate a new image in the specified file at the specified position,
|
972
|
+
# of a certain data type and size.
|
973
|
+
# "coptions" is optional, but if present see Compressible#activate_compression for allowed format.
|
974
|
+
#
|
975
|
+
# If you do activate compression for an image there is one "gotcha" that might take you by surprise:
|
976
|
+
# if there are no existing HDUs in the file and you add a compressed image, an extra image HDU at the
|
977
|
+
# start of the file will be created. In other words, your new compressed image will be at fits[1]
|
978
|
+
# *not* fits[0]. This is because cfitsio compresses the image by wrapping it in a binary table,
|
979
|
+
# but the FITS specification mandates that the primary HDU must always be an image array.
|
980
|
+
def initialize(file, extpos, bitpix, naxes, coptions=nil)
|
981
|
+
super(file, extpos)
|
982
|
+
|
983
|
+
@bitpix = bitpix
|
984
|
+
@naxes = naxes
|
985
|
+
|
986
|
+
@compression_options = coptions
|
987
|
+
end
|
988
|
+
|
989
|
+
# Actually create a physical image (i.e. write bytes).
|
990
|
+
# "at" is the position at which to insert the image.
|
991
|
+
def create(at)
|
992
|
+
self.deactivate_compression
|
993
|
+
|
994
|
+
# A dummy image gets added if to the top of the file if
|
995
|
+
# the first image added is compressed.
|
996
|
+
@extpos = (self.file.length == 0 and self.compression_options) ? at + 1 : at
|
997
|
+
|
998
|
+
self.file.set_position(at - 1 < 0 ? 0 : at - 1)
|
999
|
+
self.activate_compression() if self.compression_options
|
1000
|
+
IO::Proxy.fits_insert_img(self.file.io, @bitpix, @naxes.size, @naxes)
|
1001
|
+
|
1002
|
+
# This just makes sure something is written out right away.
|
1003
|
+
self.header['RFITS1234'] = 'temp'
|
1004
|
+
self.header.delete('RFITS1234')
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# Alias for Image#dim
|
1008
|
+
def naxis
|
1009
|
+
self.dim
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
# The number of dimensions of the image (i.e. 2).
|
1013
|
+
def dim
|
1014
|
+
reset_position()
|
1015
|
+
IO::Proxy.fits_get_img_dim(self.file.io)
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
# The size of each dimension (i.e. [512, 512]).
|
1019
|
+
def size
|
1020
|
+
reset_position()
|
1021
|
+
IO::Proxy.fits_get_img_size(self.file.io, self.naxis)
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
# Alias for Image#size
|
1025
|
+
def naxes
|
1026
|
+
self.size
|
1027
|
+
end
|
1028
|
+
|
1029
|
+
# Alter image dimensions to the specified size.
|
1030
|
+
# img.naxes = [1000, 200]
|
1031
|
+
def naxes=(naxes)
|
1032
|
+
reset_position()
|
1033
|
+
IO::Proxy.fits_resize_img(self.file.io, self.bitpix, naxes.size, naxes)
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
# The type of image. Possibilities include: :byte, :short, :long, :longlong, :float, :double
|
1037
|
+
def image_type
|
1038
|
+
reset_position()
|
1039
|
+
etype = IO::Proxy.fits_get_img_equivtype(self.file.io)
|
1040
|
+
IMG_TYPE_MAP[etype] || etype
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
# Number of bits per data pixel. By default returns the "equivalent type" unless _equiv_ is set to false.
|
1044
|
+
def bitpix(equiv=true)
|
1045
|
+
reset_position()
|
1046
|
+
equiv ? IO::Proxy.fits_get_img_equivtype(self.file.io) : IO::Proxy.fits_get_img_type(self.file.io)
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# Alter the datatype of the image.
|
1050
|
+
# img.bitpix = IO::Proxy::LONG_IMG
|
1051
|
+
def bitpix=(bitpix)
|
1052
|
+
reset_position()
|
1053
|
+
IO::Proxy.fits_resize_img(self.file.io, bitpix, self.naxis, self.naxes)
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
# Resize the image to the specified dimensions and data type.
|
1057
|
+
# img.resize(IO::Proxy::LONG_IMG, [1000, 200])
|
1058
|
+
def resize(bitpix, naxes)
|
1059
|
+
reset_position()
|
1060
|
+
IO::Proxy.fits_resize_img(self.file.io, bitpix, naxes.size, naxes)
|
1061
|
+
end
|
1062
|
+
|
1063
|
+
# Returns true if the image is compressed, false otherwise.
|
1064
|
+
def compressed?
|
1065
|
+
reset_position()
|
1066
|
+
IO::Proxy.fits_is_compressed_image(self.file.io) == 1 ? true : false
|
1067
|
+
end
|
1068
|
+
end
|
1069
|
+
|
1070
|
+
# A class that encapsulates knowledge about the name and type of a column in a table.
|
1071
|
+
class ColumnInformation
|
1072
|
+
COLUMN_TYPE_MAP = {
|
1073
|
+
IO::Proxy::TBIT => :bit,
|
1074
|
+
IO::Proxy::TBYTE => :byte,
|
1075
|
+
IO::Proxy::TLOGICAL => :logical,
|
1076
|
+
IO::Proxy::TSTRING => :string,
|
1077
|
+
IO::Proxy::TSHORT => :short,
|
1078
|
+
IO::Proxy::TLONG => :long,
|
1079
|
+
IO::Proxy::TFLOAT => :float,
|
1080
|
+
IO::Proxy::TDOUBLE => :double,
|
1081
|
+
IO::Proxy::TCOMPLEX => :complex,
|
1082
|
+
IO::Proxy::TDBLCOMPLEX => :dblcomplex
|
1083
|
+
}
|
1084
|
+
|
1085
|
+
# The table the column belongs to.
|
1086
|
+
attr_reader :table
|
1087
|
+
|
1088
|
+
# The position of the column in the table.
|
1089
|
+
attr_reader :position
|
1090
|
+
|
1091
|
+
# Create a new ColumnInformation object on the specified table (of type AsciiTable or BinaryTable)
|
1092
|
+
# at the specified location (a zero-based index).
|
1093
|
+
# ci = ColumnInformation.new(tbl, 2) # the third column in the table
|
1094
|
+
def initialize(table, position)
|
1095
|
+
@table = table
|
1096
|
+
@position = position
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
# The name of the column.
|
1100
|
+
# ci.name # 'column_3'
|
1101
|
+
def name
|
1102
|
+
self.table.reset_position
|
1103
|
+
IO::Proxy.fits_get_colname(self.table.file.io, IO::Proxy::CASEINSEN, "#{self.position + 1}").first
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
# The data type of the column. May be one of:
|
1107
|
+
# :bit, :byte, :logical, :string, :short, :long, :float, :double, :complex, :dblcomplex
|
1108
|
+
# If the data type is unrecognized, an integer corresponding to the cfitsio data type
|
1109
|
+
# will be returned instead.
|
1110
|
+
# ci.data_type # :string
|
1111
|
+
def data_type(as_int=false)
|
1112
|
+
self.table.reset_position
|
1113
|
+
type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
|
1114
|
+
|
1115
|
+
as_int ? type_info[0] : COLUMN_TYPE_MAP[type_info[0]] || type_info[0]
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
# The vector repeat count of the column. An AsciiTable will always have a repeat of 1.
|
1119
|
+
# ci.repeat # 10
|
1120
|
+
def repeat
|
1121
|
+
self.table.reset_position
|
1122
|
+
type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
|
1123
|
+
|
1124
|
+
type_info[1]
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
# The width, in bytes, of the column.
|
1128
|
+
# ci.width # 10
|
1129
|
+
def width
|
1130
|
+
self.table.reset_position
|
1131
|
+
type_info = IO::Proxy.fits_get_coltype(self.table.file.io, self.position+1)
|
1132
|
+
|
1133
|
+
type_info[2]
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
# The display width (i.e. the width of the column if it were to be converted into it's string representation).
|
1137
|
+
# ci.display_width # 10
|
1138
|
+
def display_width
|
1139
|
+
self.table.reset_position
|
1140
|
+
IO::Proxy.fits_get_col_display_width(self.table.file.io, self.position+1)
|
1141
|
+
end
|
1142
|
+
|
1143
|
+
# The number of dimensions of the column. "maxdim" is the maximum number of dimensions
|
1144
|
+
# to return and defaults to 1.
|
1145
|
+
# ci.dim # 1
|
1146
|
+
def dim(maxdim=1)
|
1147
|
+
self.table.reset_position
|
1148
|
+
dim_info = IO::Proxy.fits_read_tdim(self.table.file.io, self.position+1, 1)
|
1149
|
+
|
1150
|
+
dim_info[0]
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
# An alias for ColumnInformation#dim
|
1154
|
+
def naxis(*args)
|
1155
|
+
self.dim(*args)
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
# The size of the dimensions of the column. "maxdim" is the maximum number of dimensions
|
1159
|
+
# to return and defaults to 1.
|
1160
|
+
# ci.size # [1]
|
1161
|
+
def size(maxdim=1)
|
1162
|
+
self.table.reset_position
|
1163
|
+
dim_info = IO::Proxy.fits_read_tdim(self.table.file.io, self.position+1, maxdim)
|
1164
|
+
|
1165
|
+
return dim_info[1]
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
# An alias for ColumnInformation#size
|
1169
|
+
def naxes(*args)
|
1170
|
+
self.size(*args)
|
1171
|
+
end
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
# A list of ColumnInformation objects representing the set
|
1175
|
+
# of column definitions. Behaves much like an array.
|
1176
|
+
class ColumnInformationList
|
1177
|
+
include Enumerable
|
1178
|
+
|
1179
|
+
# The table the columns belong to.
|
1180
|
+
attr_reader :table
|
1181
|
+
|
1182
|
+
# Create a list of ColumnInformation from the specified table.
|
1183
|
+
# cil = ColumnInformationList.new(table)
|
1184
|
+
def initialize(table)
|
1185
|
+
@table = table
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
# Retrieve the ith column's information.
|
1189
|
+
# cil[1] # the second column's information
|
1190
|
+
def [](i)
|
1191
|
+
self.table.reset_position
|
1192
|
+
ColumnInformation.new(self.table, i)
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
# Insert a column into the ith position.
|
1196
|
+
# "options" is a hash with two keys:
|
1197
|
+
#
|
1198
|
+
# [:name] the name of the column
|
1199
|
+
# [:format] the format of the column as explained in the cfitsio documentation
|
1200
|
+
#
|
1201
|
+
# You can also insert multiple columns at the same time by passing an array
|
1202
|
+
# of hashes of the format explained above.
|
1203
|
+
# cil[1] = {:name => 'new_column_1', :format => 'A10'}
|
1204
|
+
# cil[1] = [{:name => 'new_column_1', :format => 'A10'}, {:name => 'new_column_2', :format => 'I'}]
|
1205
|
+
def []=(i, options)
|
1206
|
+
self.table.reset_position
|
1207
|
+
|
1208
|
+
names = []
|
1209
|
+
formats = []
|
1210
|
+
if options.is_a?(Array)
|
1211
|
+
options.each do |spec|
|
1212
|
+
names << spec[:name]
|
1213
|
+
formats << spec[:format]
|
1214
|
+
end
|
1215
|
+
else
|
1216
|
+
names = [options[:name]]
|
1217
|
+
formats = [options[:format]]
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
IO::Proxy.fits_insert_cols(self.table.file.io, i+1, names.size, names, formats)
|
1221
|
+
end
|
1222
|
+
|
1223
|
+
# Iterate through each ColumnInformation object.
|
1224
|
+
def each
|
1225
|
+
(0...self.size).each do |i|
|
1226
|
+
yield self[i]
|
1227
|
+
end
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
# Append a new column to the table. See ColumnInformationList#[]= for details
|
1231
|
+
# on what "options" can look like.
|
1232
|
+
# cil << {:name => 'new_column_1', :format => 'A10'}
|
1233
|
+
# cil << [{:name => 'new_column_1', :format => 'A10'}, {:name => 'new_column_2', :format => 'I'}]
|
1234
|
+
def <<(options)
|
1235
|
+
self[self.size] = options
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
# Delete the ith column.
|
1239
|
+
# cil.delete(1) # delete the second column
|
1240
|
+
def delete(i)
|
1241
|
+
self.table.reset_position
|
1242
|
+
IO::Proxy.fits_delete_col(self.table.file.io, i+1)
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
# The number of columns in the table.
|
1246
|
+
# cil.size # 4
|
1247
|
+
def size
|
1248
|
+
self.table.reset_position
|
1249
|
+
IO::Proxy.fits_get_num_cols(self.table.file.io)
|
1250
|
+
end
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
# A class representing a generic table extension. Is intended to be abstract.
|
1254
|
+
class Table < HDU
|
1255
|
+
# Metadata for each column.
|
1256
|
+
attr_reader :column_information
|
1257
|
+
|
1258
|
+
# Tabular data.
|
1259
|
+
attr_reader :data
|
1260
|
+
|
1261
|
+
def initialize(file, extpos, names, formats, extname=nil, units=nil)
|
1262
|
+
super(file, extpos)
|
1263
|
+
|
1264
|
+
@column_names = names
|
1265
|
+
@column_formats = formats
|
1266
|
+
@extname = extname
|
1267
|
+
@column_units = units
|
1268
|
+
|
1269
|
+
@column_information = ColumnInformationList.new(self)
|
1270
|
+
@data = TableData.new(self)
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
# Convert a table to CSV.
|
1274
|
+
def to_csv
|
1275
|
+
col_names = self.column_information.collect{ |ci| ci.name }
|
1276
|
+
|
1277
|
+
buffer = ''
|
1278
|
+
CSV.generate_row(col_names, col_names.size, buffer)
|
1279
|
+
buffer << self.data.to_csv
|
1280
|
+
|
1281
|
+
buffer
|
1282
|
+
end
|
1283
|
+
|
1284
|
+
# Convert a table to a string. Currently an alias for Table#to_csv.
|
1285
|
+
def to_s
|
1286
|
+
self.to_csv
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
# A class representing an ASCII table.
|
1291
|
+
class AsciiTable < Table
|
1292
|
+
def create(at)
|
1293
|
+
self.file.set_position(at - 1 < 0 ? 0 : at - 1)
|
1294
|
+
|
1295
|
+
IO::Proxy.fits_insert_atbl(self.file.io, 0, 0, @column_names.size,
|
1296
|
+
@column_names,
|
1297
|
+
nil,
|
1298
|
+
@column_formats,
|
1299
|
+
@column_units,
|
1300
|
+
@extname, 0)
|
1301
|
+
|
1302
|
+
@extpos = at
|
1303
|
+
|
1304
|
+
# This just makes sure something is written out right away.
|
1305
|
+
self.header['RFITS1234'] = 'temp'
|
1306
|
+
self.header.delete('RFITS1234')
|
1307
|
+
end
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
# A class representing a binary table.
|
1311
|
+
class BinaryTable < Table
|
1312
|
+
def create(at)
|
1313
|
+
self.file.set_position(at - 1 < 0 ? 0 : at - 1)
|
1314
|
+
|
1315
|
+
IO::Proxy.fits_insert_btbl(self.file.io, 0, @column_names.size,
|
1316
|
+
@column_names,
|
1317
|
+
@column_formats,
|
1318
|
+
@column_units,
|
1319
|
+
@extname, 0)
|
1320
|
+
|
1321
|
+
@extpos = at
|
1322
|
+
|
1323
|
+
# This just makes sure something is written out right away.
|
1324
|
+
self.header['RFITS1234'] = 'temp'
|
1325
|
+
self.header.delete('RFITS1234')
|
1326
|
+
end
|
1327
|
+
end
|
1328
|
+
end
|