package fr.unistra.pelican.algorithms.io; import java.io.DataInputStream; import java.io.FileInputStream; import java.io.IOException; import fr.unistra.pelican.Algorithm; import fr.unistra.pelican.DoubleImage; import fr.unistra.pelican.Image; import fr.unistra.pelican.InvalidFileFormatException; /** * Loads fits images.EXTENSIONs are NOT and will NOT be supported * - float64 works fine<br> - float32 works ok <br> - byte works ok<br> - * int32 works ok (? needs more testing)<br> - int16 works ok (? needs more * testing)<br> - calibration works ok<br> - multiband works ok<br> * * <br> * * TODO * - still in need of a way of telling apart byte order - there is no official * way (the NASA engineers have disappointed me...).<br> - buffered * reading...do we really need it ?<br> - "BLANK pixel to min" operation not * tested<br> - add InvalidFileFormatException support<br> - add 64bit float * and double support<br> - add image extension support<br> * * <br> * * NOTES * * Fits files can have up to 999 axes.<br> * The so-called multiband support of this class takes into account only * NAXIS3...<br> * thus supporting ONLY a total of 3 (THREE) axes...but of course NAXIS3 can * have<br> * any dimension it desires...in EA's case NAXIS3 = 6<br> * * @author Erchan Aptoula * */ public class FitsImageLoad extends Algorithm { /** * Input parameter */ public String filename; /** * Output parameter */ public Image output; private DataInputStream dis; private int bitPix; // pixel coding type private int bytesPerPixel; private int width = 0; private int height = 0; private int band = 1; private double bscale = 1.0; private double bzero = 0.0; private boolean scaled = false; // private boolean intelByteOrder = false; private boolean thereAreBlanks = false; private int blankVal = 0; // add support for customizable blank fill // (black or white) static final int BYTE = 1; static final int INT16_SIGNED = 2; static final int INT32 = 3; static final int INT64 = 4; static final int FLOAT32 = 5; static final int FLOAT64 = 6; static final int RECORD_SIZE = 80; static final int HEADER_SIZE = 2880; /** * Constructor * */ public FitsImageLoad() { super(); super.inputs = "filename"; super.outputs = "output"; } public void launch() { try { getHeaders(); double[] pixels = readPixels(); flip(pixels); if (thereAreBlanks == true) fillInTheBlanks(pixels); // ALWAYS before calibration if (scaled == true) calibrate(pixels); DoubleImage doubleOutput = new DoubleImage(width, height, 1, 1, band); for (int b = 0; b < band; b++) { for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { double tmp = pixels[b * width * height + j * width + i]; doubleOutput.setPixelXYBDouble(i, j, b, tmp); } } } //doubleOutput = doubleOutput.scaleToZeroOne(); this.output = doubleOutput; dis.close(); } catch (Exception e) { e.printStackTrace(); } } private void fillInTheBlanks(double[] pixels) { // get the min or max according to fill type... // black backgrounds are natural for astronomical images => min // BLANK used primarily with positive BITPIX => no reals... System.err.println("Blanks detected : filling in..."); double min = Double.MAX_VALUE; for (int i = 0; i < pixels.length; i++) if (pixels[i] < min) min = pixels[i]; for (int i = 0; i < pixels.length; i++) if (pixels[i] == (double) blankVal) pixels[i] = min; } private void calibrate(double[] pixels) { System.err.println("[Debug] FitsImageLoad : calibrating with bscale " + bscale + " and bzero " + bzero); for (int i = 0; i < pixels.length; i++) pixels[i] = bscale * pixels[i] + bzero; System.err.println("[Debug] FitsImageLoad : calibration is over"); } private void flip(double[] pixels) { int index1, index2; double tmp; for (int b = 0; b < band; b++) { for (int y = 0; y < height / 2; y++) { index1 = y * width + b * width * height; index2 = (height - 1 - y) * width + b * width * height; for (int i = 0; i < width; i++) { tmp = pixels[index1]; pixels[index1++] = pixels[index2]; pixels[index2++] = tmp; } } } } private double[] readPixels() throws IOException { int pixelCount = width * height * band; int byteCount = pixelCount * bytesPerPixel; byte[] buffer = new byte[byteCount]; double[] pixels = new double[pixelCount]; int tmp = 0; long longTmp = 0; dis.read(buffer); // default : BIG-ENDIAN switch (bitPix) { case INT32: for (int i = 0, j = 0; i < pixelCount; i++, j += 4) { tmp = (int) (((buffer[j] & 0xff) << 24) | ((buffer[j + 1] & 0xff) << 16) | ((buffer[j + 2] & 0xff) << 8) | (buffer[j + 3] & 0xff)); pixels[i] = (new Integer((int) (tmp & 0xffffffffL))) .doubleValue(); } break; case INT64: for (int i = 0, j = 0; i < pixelCount; i++, j += 8) { longTmp = 0; longTmp = longTmp | (int) (buffer[j] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 1] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 2] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 3] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 4] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 5] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 6] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 7] & 0xff); pixels[i] = (new Long((int) (longTmp & 0xffffffffffffffffL))) .doubleValue(); } break; case INT16_SIGNED: for (int i = 0, j = 0; i < pixelCount; i++, j += 2) pixels[i] = (new Short( (short) ((((buffer[j] & 0xff) << 8) | (buffer[j + 1] & 0xff))))) .doubleValue(); break; case FLOAT32: for (int i = 0, j = 0; i < pixelCount; i++, j += 4) { tmp = (int) (((buffer[j] & 0xff) << 24) | ((buffer[j + 1] & 0xff) << 16) | ((buffer[j + 2] & 0xff) << 8) | (buffer[j + 3] & 0xff)); pixels[i] = (new Float(Float.intBitsToFloat(tmp))) .doubleValue(); } break; case FLOAT64: for (int i = 0, j = 0; i < pixelCount; i++, j += 8) { longTmp = 0; longTmp = longTmp | (int) (buffer[j] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 1] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 2] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 3] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 4] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 5] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 6] & 0xff); longTmp = longTmp << 8; longTmp = longTmp | (int) (buffer[j + 7] & 0xff); pixels[i] = Double.longBitsToDouble(longTmp); } break; case BYTE: for (int i = 0; i < pixelCount; i++) pixels[i] = (new Byte(buffer[i])).doubleValue(); break; default: return null; } return pixels; } private void getHeaders() throws IOException, InvalidFileFormatException { // Each header unit consists of any number of 80-character keyword // records int count = 1; dis = new DataInputStream(new FileInputStream(filename)); String s = getString(RECORD_SIZE); if (s.charAt(29) != 'T') { // SIMPLE = T (conforming to standard) of F // (non conforming) dis.close(); throw new InvalidFileFormatException("SIMPLE = F" + this.filename + " does not conform to FITS standard"); } do { count++; s = getString(RECORD_SIZE); if (s.startsWith("BITPIX")) { // System.err.println("[Debug] FitsImageLoad : header " + count // + " " + s); switch (getInteger(s)) { case 8: bitPix = BYTE; bytesPerPixel = 1; break; case 16: bitPix = INT16_SIGNED; bytesPerPixel = 2; break; case 32: bitPix = INT32; bytesPerPixel = 4; break; case -32: bitPix = FLOAT32; bytesPerPixel = 4; break; case 64: bitPix = INT64; bytesPerPixel = 8; break; case -64: bitPix = FLOAT64; bytesPerPixel = 8; break; default: // throw new InvalidFileFormatException("BITPIX must be 8, // 16, 32 or -32"); System.err.println("BITPIX must be 8, 16, 32, -32 or -64"); dis.close(); return; } } else if (s.startsWith("NAXIS ")) { int axes = getInteger(s); if (axes < 2 || axes > 3) throw new InvalidFileFormatException( "Not supported FITS variant (NAXIS = " + axes + ")"); } else if (s.startsWith("NAXIS1")) width = getInteger(s); else if (s.startsWith("NAXIS2")) height = getInteger(s); // multi-frame fits...3 axes // This keyword gives the number of frames in the 3rd axe else if (s.startsWith("NAXIS3")) band = getInteger(s); else if (s.startsWith("BSCALE")) { bscale = getFloat(s); if (bscale != 1.0) scaled = true; } else if (s.startsWith("BZERO")) { bzero = getFloat(s); if (bzero != 0.0) scaled = true; } else if (s.startsWith("BLANK")) { thereAreBlanks = true; blankVal = getInteger(s); } } while (!s.startsWith("END")); if (width == 0) { dis.close(); throw new InvalidFileFormatException("No visual data"); } // skip until the end of a multiple of 2880 bytes... // ...and there thou shall encounter what thou seek -> "the image" int skipCount = (count * RECORD_SIZE - 1) / 2880; skipCount = (skipCount + 1) * HEADER_SIZE - count * RECORD_SIZE; dis.skip(skipCount); } private int getInteger(String s) { int tmp = s.indexOf("/"); if (tmp != -1) s = s.substring(10, tmp); else s = s.substring(10); s = s.trim(); return Integer.parseInt(s); } private String getString(int length) throws IOException { byte[] b = new byte[length]; dis.read(b); return new String(b); } private double getFloat(String s) { int tmp = s.indexOf("/"); if (tmp != -1) s = s.substring(10, tmp); else s = s.substring(10); return Double.parseDouble(s); } /** * Loads fits images. * * @param filename Filename of the fits image. * @return the Fits image. */ public static Image exec(String filename) { return (Image) new FitsImageLoad().process(filename); } }