Image File Processing Fits And Android

The Flexible Image Transport System (FITS) was designed specifically for astronomy images. In this article we'll look at how you can write programs to read the data and recreate the picture stored within it.

A detailed description of the FITS file format can be found on the NASA site. You can get quite complicated files, with multiple images and tables of data; but we’re going to use very basic versions that include a single image. These basic files are then actually easier to work with than more common types such as png or jpeg, which include various methods of compression (ways of reducing the file size).

We'll be cutting some corners to make things as simple as possible. If you see a red box, then you can click on it to see information about the shortcut that's been taken; but feel free to ignore them until after you have the program working.

Easy Ways to Work with FITS

We'll be creating a basic, low-level program to work with FITS files. If you want to work with more complicated data, or are more interested in creating a nice-looking image, then you would be better off using one of the existing tools. The NASA page lists various tools that you could use; one of which is the ESA/ESO/NASA FITS Liberator plugin for PhotoShop. There's a nice (but old) tutorial on using FITS Liberator that shows you how to combine Hubble images to create stunning multi-colour images. Even if you are writing your own software there are many libraries that you can use to make things easier - such as in the astropy library for Python.

What is FITS?

All of the files that we're going to work with come from the MicroObservatory image directory. You could use any file, but it will be easier to follow this article if you use the image of the moon provided below.

Click here to download the Moon FITS file

Download the file, and then open it in a text editor – such as Notepad or TextEdit. You might need to open the application first, and then choose File > Open, and choose Any Type as the file format so that you can see the FITS file.

As you scroll through the file you should notice two different types of text. The top part should be readable (even if you’re not sure what it means), and the bottom part that looks like gibberish. The structure of FITS files is essentially like this:

two main sections: header unit and data unit Diagram of FITS file structure

On this first pass we’re going to cheat and read the descriptive data ourselves. Look out for important keywords:

NAXIS How many axis of data – should be 2 for X and Y
NAXIS1 How much data along axis 1 – i.e. width of the image in this case
NAXIS2 How much data along axis 2 – i.e. height of the image in this case
BITPIX How many bits (binary 1’s and 0’s) are used to store the brightness of each pixel. It should be 16.

There’s some other information in there, but none that we need at the moment. Feel free to have a look through, see what you can make out, and move on when you’re ready.

Using Android

Why Android? - Click to expand
If you weren't questioning the use of Android, then let me explain why it's a bad idea. If you're working with images then you probably want two things: a large screen so that you can see the details, and a fast processor to work with the pixel data. A mobile device is not a great choice; but I'm using Android here simply because there are other apps that I want to produce tutorials for, and I wouldn't want to force people to download and learn multiple programming environments. Plus Android can handle what we're asking it to do, and we won't be doing anything too language specific (so you could recreate it in another language if you wanted to).

We'll be using Android Studio. I'll assume you know how to set-up a new project including a blank activity. If not then the full Android Studio setup is explained in another post.


Android wants to use dp instead of pixels - Click to expand
Using dp (device pixels) helps to make your designs look similar across the huge range of screen sizes and resolutions that exist. However in this case we need an exact pixel size. There are ways of achieving both, but let's keep it simple.

That’s all we’re adding to the view for now.

Including the File

Our application needs to find the FITS file before we can process it. Let’s add it to our project.

Now that we have a suitable folder, we can copy the fits file into it.

Adding an assets folder to the Android project Adding an assets folder to the Android project

Now you know where the assets folder is on your computer. Copy the FITS file into it.

Loading Files - Click to expand
If we leave it this way, then it will be a fairly useless program that only ever processes a single file. In a future article we might load the file that you want from a location on the phone instead.

Simple / 5

If it isn't open already, then find and open (double click) the MainActivity file:

Look for the OnCreate method. To begin with we’ll put the code in there, so that it runs as soon as the app starts.

Running lots of code in onCreate - Click to expand
This will add a delay to our app starting, and could look as though nothing is happening. In fact we wouldn't really want to run a big chunk of processing code within the main program anyway. Something like an AsyncTask would be a better choice - for a later date :).

The basic structure of our code is going to be:

  1. Load the file.
  2. Loop through header unit: even though we won’t look at it initially, it’s still there.
  3. Loop over the data unit - drawing one pixel at a time.

All the code that you need is included below. Simply replace your onCreate code with this. It might look a bit long and scary, but that's partly because there are so many comments explaining what's going on.

	  @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);

 // Create an asset manager to help load the file
 AssetManager assetManager = getAssets();

 // File access can fail for a number of reasons
 // - so it needs to go in a try-catch block
 try {

   // ==== 1. Load the file ====
 // Create an input stream to access the file
 InputStream input = assetManager.open("moon.FITS");

   // ==== 2. Loop through header data ====
 // - reading chunks of 2880 bits (360 characters)
 String chunk = "";
 do {
 byte[] buffer = new byte[360]; // Create an array of bytes
 input.read(buffer,0,360); // Read 360 bytes from the file
 chunk = new String(buffer); // Convert the bytes read into a string

 } while (!chunk.contains(" END ")); // Keep looping until we read the END identifier


 // Define the size of the image
 // - we should have loaded it from the file
 int axis1 = 650;
 int axis2 = 500;

 // This will hold the pixel value loaded from the file
 byte[] data = new byte[2];
 int val;

 // Create a new image bitmap and create a canvas from it
   // - Bitmaps are just another way of storing images
   // - The Canvas is something we can draw on
 Bitmap tempBitmap = Bitmap.createBitmap(axis1, axis2, Bitmap.Config.ARGB_8888);
 Canvas tempCanvas = new Canvas(tempBitmap);
 Paint p = new Paint();

 // Get a reference to the image view
 ImageView img = (ImageView) findViewById(R.id.imgDisplay);

   // ==== 3. Loop over the image data ====
 // Loop for every pixel
 for (int h = 0; h < axis2; h++) // loop for each row
 {
 for (int w = 0; w < axis1; w++) { // loop for each pixel in a row

 // Read two bytes from the file (8 + 8 = 16 bits)
 input.read(data, 0, 2);

 // In java, bytes are signed, but in FITS they aren't
 // - If the value is less than 0
 if (data[1] < 0) val = (data[0] * 256) + (256 + data[1]);
 else val = (data[0] * 256) + data[1];

     // Temporary fudge to put the values in a range we can use
 val = val/5;

     // Make sure the value can't go over 255
     // - 255 is the brightest a pixel can be (white)
 if (val>255) val=255;

     // Create a way of drawing in that colour
 p.setColor(Color.argb(255, val, val, val));

     // Actually draw a pixel of that colour
 tempCanvas.drawRect(w, h, w+1, h+1, p);
 }
 }
   // When we get here the image will be fully drawn

 img.setImageBitmap(tempBitmap); // Add the bitmap to the image view

 input.close(); // Close the file

 } catch (IOException e) { // If we end up here, then something went wrong
 e.printStackTrace(); // We can't do much other than say what went wrong
 }
 }

When you first paste it in you'll get a lot of underlined red - indicating errors. Hopefully it's only because you're using bits of code (classes) without saying that you're going to in advance (import). Luckily there’s an easy option to fix this in Android Studio. Just click on the red text, and you should see a message that you can press Alt+Enter to fix the problem. What it's actually doing is going to the top of the program and adding import statements.

Android Studio's offered solution to missing libraries Android Studio's offered solution to missing imports

Run the program, and hopefully you get an image of the Moon:

First loading of FITS file First loading of FITS file

Linear interpolation

At the moment the image will have very poor contrast. The darkest areas are dark grey, and the light areas are light grey. In this particular file the sky has a pixel value of around 590, and the brightest part of the Moon has a value of 1236. By dividing by 5 this gives a range of 118 to 247, which is much narrower than the range of possible pixel values that we could use (0 to 255).

It might be easier to think about the problem if we use some graphs. The two included below actually show the same values, but using different scales. Our image at the moment is a bit like the graph on the left, with most of the pixel values near the middle, but with a broad scale. If we change our scale so that the range is a better fit with our data, then the differences between each value will be easier to see.

two graphs with different scales Demonstration of how changing the scale can make the difference in values easier to see

The solution is to use a system called linear interpolation. Essentially we'll say that the lowest pixel value should be black, and the highest should be white. Everything else will fall on a scale between those two. However, that will mean needing to know what the highest and lowest values are - which means changing the structure of our program to be:

  1. Load the file.
  2. Loop through header unit: even though we won’t look at it initially, it’s still there.
  3. Loop over the data unit - this time just storing the pixels instead of drawing, and importantly keeping track of the biggest and smallest that we see.
  4. Loop over the stored pixels - scaling each pixel based on the biggest and smallest values seen, and drawing them one-by-one.

Again, you can just replace the onCreate method that you have at the moment with the one below.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Create an asset manager to help load the file
        AssetManager assetManager = getAssets();

        // File access can fail for a number of reasons
        // - so it needs to go in a try-catch block
        try {

			// ==== 1. Load the file ====
            // Create an input stream to access the file
            InputStream input = assetManager.open("moon.FITS");

			// ==== 2. Loop through header data ====
            // - reading chunks of 2880 bits (360 characters)
            String chunk = "";
            do {
                byte[] buffer = new byte[360];  // Create an array of bytes
                input.read(buffer,0,360);       // Read 360 bytes from the file
                chunk = new String(buffer);     // Convert the bytes read into a string

            } while (!chunk.contains(" END ")); // Keep looping until we read the END identifier


            // Define the size of the image
            // - we should have loaded it from the file
            int axis1 = 650;
            int axis2 = 500;
            int[][] imgData = new int[axis1][axis2];    // Create an 2D array for the pixel values

            // This will hold the pixel value loaded from the file
            byte[] data = new byte[2];
            int val;

            // To store the biggest and smallest values from the file
            int max = -1;
            int min = -1;

            // ==== 3. Loop over the image data ====
			// Loop for every pixel
            for (int h = 0; h < axis2; h++)         // loop for each row
            {
                for (int w = 0; w < axis1; w++) {   // loop for each pixel in a row

                    // Read two bytes from the file (8 + 8 = 16 bits)
                    input.read(data, 0, 2);

                    // In java, bytes are signed, but in FITS they aren't
                    // - If the value is less than
                    if (data[1] < 0) val = (data[0] * 256) + (256 + data[1]);
                    else val = (data[0] * 256) + data[1];

                    imgData[w][h] = val;    // Store the value in the array

                    // Keep track of minimum and maximum
                    if ((max == -1) || (max < val)) max = val;  // If bigger than max, or not set
                    if ((min == -1) || (min > val)) min = val;  // If smaller than min, or not set
                }
            }

            // Create a new image bitmap and create a canvas from it
			// - Bitmaps are just another way of storing images
			// - The Canvas is something we can draw on
            Bitmap tempBitmap = Bitmap.createBitmap(axis1, axis2, Bitmap.Config.ARGB_8888);
            Canvas tempCanvas = new Canvas(tempBitmap);
            Paint p = new Paint();

            // Get a reference to the image view
            ImageView img = (ImageView) findViewById(R.id.imgDisplay);

            // Interpolation variables
			// - Convert max and min to types that allow decimals
			// - find the gap between them.

double dmin = (double) min; double dmax = (double) max; double gap = dmax - dmin; double scaleVal; // ==== 4. Loop overy every pixel saved, calculating new value ==== // Loop for every pixel for (int h = 0; h < axis2; h++) // loop for each row { for (int w = 0; w < axis1; w++) { // loop for each pixel in a row scaleVal = imgData[w][h]; // Load value scaleVal = (scaleVal - dmin) / gap; // Linear interpolation scaleVal = scaleVal * 255; val = (int)scaleVal; // Convert scaled value to integer if (val>255) val=255; // Upper limit p.setColor(Color.argb(255, val, val, val)); // Set the pixel colour tempCanvas.drawRect(w, h, w+1, h+1, p); // Draw a pixel } } // When we get here the image will be fully drawn img.setImageBitmap(tempBitmap); // Add the bitmap to the image view input.close(); // Close the file } catch (IOException e) { // If we end up here, then something went wrong e.printStackTrace(); // We can't do much other than say what went wrong } }

Run the program again, and you should see a much more well defined image.

Moon image displayed using linear interpolation Moon image displayed using linear interpolation

Logarithmic Scale

The linear interpolation gave us a better result, but it isn't always the best choice for astronomy images. The massive contrast between the darkness of the night's sky and the brightness of stars, can mean that faint objects get lost. Let's try a different file.

Click here to download the Ring Nebula FITS file

If you view this file at the moment then you won't see very much. Again we can use graphs to make things clearer. Below left we see a graph where a single extreme value (e.g. a bright star) is stretching the scale and hiding detail in the lower values. One possible solution is shown in the middle. It has exactly the same values, but the scale has been cut-off at 35. It gives a better view of the values, but how would we know to cut off at 35? We could use some maths to make a good guess, but normally this would be achieved by allowing someone to dynamically change the value (e.g. with a slider) and seeing the result. Another option would be to use logarithms. I probably wouldn't do a very good job of explaining what they are (here's a Khan academy video on logarithms), but the result is shown in the graph on the right. You apply a certain equation to each value, and it narrows the range - reducing extremes. The result might not be as good, but we don't need to worry about picking arbitrary numbers or building an interface for the user - so it's what we'll use.

three graphs Examples of how we can deal with an extreme value. Cut-off point (centre) and logarithmic scale (right)

Switching to a logarithmic scaling only requires very minor changes to the program. In fact we only need to include Math.log10( ) in three different places. First, change the lines that store the minimum and maximum values that we found:

// Interpolation variables
// - Convert max and min to types that allow decimals
// - find the gap between them.

double dmin = Math.log10( (double) min ); double dmax = Math.log10( (double) max ); double gap = dmax - dmin; double scaleVal;

Next scale the actual pixel value:

scaleVal = Math.log10( imgData[w][h] );     // Load value

scaleVal = (scaleVal - dmin) / gap;    		// Linear interpolation (of logs)
scaleVal = scaleVal * 255;

You should get a result similar to that shown below. With logarithmic scaling applied on the right, you can see more stars and the faint ring nebula itself becomes visible (at the intersection of the two arrows).

Ring Nebula image with only linear interpolation (top) and logarithmic scaling (bottom) Ring Nebula image with only linear interpolation (top) and logarithmic scaling (bottom)

Wrap-Up

There is a lot more work that we could do to make the app more usable, and to follow good programming practices. However, we've covered a lot of the fundamental principles of loading and rendering images. Hopefully you've also seen that choices in how we display images can be as important as the stored data itself.