Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

FITS to image and image transparency

0.00/5 (No votes)
8 Apr 2016 1  
Convert FITS file to known image formats and use those images for a zooming functionality (image transparency)

Introduction

This article has three parts.  

  1. Converting FITS files to more commonly known image formats
  2. Show the converted images with a dynamic transparency
  3. Playing timelapse videos, while using the dynamic transparency.

Prerequisites

Background

The code in this article is pretty straight forward, but finding the code is somewhat harder.  At work we look at the sun, we try to understand it (research) and we try to make space weather predictions (operations).  

One of the best tools to look at the sun is the SDO satellite. (http://sdo.gsfc.nasa.gov/)  This satellite provides a viewing of the solar corona in different bandwidts (concerning the AIA instrument, there are other instruments as well)  The AIA imager presents the images in several bandwidths: 094, 131, 171, 193, 211, 304, 335, 1600, 1700 and 4500 Angstrom.  Each bandwith showing different features.  (Depending on heat and density).

The idea of this prototype is based on the idea that temperature rises further out the transition region/corona.  

Temperature in the corona

 

 

 

 

 

 

 

 

 

 

So by ordering the images according to base temperature (degrees Kelvin) we should get some sort of zoomable 3D image.

This proved not to be quite true, but nevertheless is this tool useful, because the images are all calibrated the same (aligned centers, aligned disk size, ...) so we can more easely track the features of the different bandwidths by "zooming".

About FITS

FITS stands for Flexible Image Transport System.  It is a standard file format often used in astronomy.  It's designed to handle tables and images (which are kind of tables as well).  There can be multiple tables and images in a single FITS file and therefore the size of such a file can also differ significantly, depending on the source.  Currently it is not that well supported in C#.  And the only library available does not allow many exotic features like idl or python libraries can.

Using the code

Reading FITS

Before we can even attempt to get something "image like" done in C# you need to convert the FITS file to something .Net can work with: bmp, jpg, png, tiff, ... 

This is rather difficult, because there is not much information on what the FITS file contains.  Each instrument eg.  can deliver such a file, but with different types of values.  As I discovered with the SDO AIA fits files, the pixel values where NOT RGB.  

The FITS file has HDU parts in them.  Header Data Units.  Each unit has two sections:  A data section containing the raw data and a header section containing metadata.  Some of the header keywords are mandatory, others are optional (or can be added freely)

This is the conversion method.  It takes the filepath to the FITS file.
(The conversion is specific for this SDO AIA format.)

public static System.Drawing.Image Convert(string pathfitsfile){
            int totalmax = 0;
            int totalmin = 0;
            int wavelength = 0;
            

            System.Drawing.Image result = null;
            //http://fits.gsfc.nasa.gov/fits_libraries.html#CSharpFITS
            //Read the fits file
            nom.tam.fits.Fits fits = new nom.tam.fits.Fits(pathfitsfile);
            nom.tam.fits.BasicHDU basichdu;
            do{
                basichdu = fits.readHDU();
                if(basichdu != null){
                    basichdu.Info();
                }                                            //end if
            }                                                //end do
            while(basichdu != null);

            //loop through the Header Data Units
            for(int i = 0; i < fits.NumberOfHDUs; i++){
                basichdu = fits.getHDU(i);
                //this retreives the current bandwidth
                string card = basichdu.Header.GetCard(20);
                wavelength = System.Convert.ToInt32(card.Replace("WAVELNTH=", "").Trim());
                if(basichdu.GetType().FullName == "nom.tam.fits.ImageHDU"){
                    try{
                        nom.tam.fits.ImageHDU imghdu = (nom.tam.fits.ImageHDU)basichdu;
                        //get out the pixel array and the dimensions
                        Array [] array = (Array[])imghdu.Data.DataArray;
                        int x = imghdu.Axes[0];
                        int y = imghdu.Axes[1];
                        result = new System.Drawing.Bitmap(x, y);
                        System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(result);
                        int idx_x = 0;
                        int idx_y = 0;
                        //get the real min/max values.
                        //We need it to get the values between 0 - 255.
                        for(int j = array.Length-1; j >= 0; j--){
                            int[] row = (int[])array[j];
                            for(int k = 0; k < row.Length; k++){
                                totalmax = (row[k] > totalmax ? row[k] : totalmax);
                                totalmin = (row[k] < totalmin ? row[k] : totalmin);
                            }                                //end for
                        }

                        //offset the values so everything is positive
                        int offset = (int)(totalmin < 0.0 ? (-1)*totalmin : 0.0);
                        //bottom is top, top is bottom.
                        for(int j = array.Length-1; j >= 0; j--){
                            //pixels are NOT RGB values so here we convert it to an RGB value.
                            idx_x = 0;
                            idx_y++;
                            int[] row = (int[])array[j];
                            for(int k = 0; k < row.Length; k++){
                                double val = row[k];
                                //This was trial and error.
                                switch(wavelength){
                                    case 335:    
                                    case 94:
                                    case 131:
                                    break;
                                    case 304:    val /= 3;
                                    break;    
                                    case 171:
                                    case 193: 
                                    case 211: 
                                    case 1600:    val /= 32;
                                    break;
                                    case 1700:    val /= 128;
                                    break;
                                    case 4500:    val /= 1024;
                                    break;
                                    default:
                                    break;
                                }                            //end switch
                                //make sure all pixels are in range 0 - 255
                                val = (val > 255.0 ? val = 255.0 : val);
                                val = (val < 0.0 ? val = 0.0 : val);
                                System.Drawing.Color c = System.Drawing.Color.FromArgb(2013265920 + (int)val);
                                //call this function to color the image depending on wavelength
                                c = WeighRGBValue(System.Drawing.Color.FromArgb(c.A, c.B, c.B, c.B), wavelength);
                                ((System.Drawing.Bitmap)result).SetPixel(idx_x, idx_y, c);
                                idx_x++;
                            }
                            
                        }

                    }                                        //end try
                    catch(Exception ex){
                        string error = ex.Message;
                    }                                        //end catch
                }                                            //end if
            }                                                //end for
            return result;
        }

Basically this code strips out the data part of the file (the pixel values) and converts those values to RGB.

When you do not use the WeighRGBValue method, the image is gray scaled (which is fine).  For zooming later on, however, you would like to detect which feature belongs to which image (or corresponding) bandwith.  Therefore we color it.  I tried to simulate the original SDO AIA colors as can be found on the website.

        //http://www.rapidtables.com/web/color/RGB_Color.htm
        private static System.Drawing.Color WeighRGBValue(System.Drawing.Color color, int wavelength){
            //Tried to get a similar color to the original site.
            System.Drawing.Color c;
            int alpha, red, green, blue;
            red = green = blue = 255;
            alpha = color.A;
            switch(wavelength){
                case 94:    //green : 00FF00
                            red = 0;
                            green = color.G;
                            blue = 0;
                break;
                case 131:    //teal : 008080
                            red = 0;
                            green = color.G;
                            blue = color.B;
                break;
                case 304:    //red : FF0000
                            red = color.R;
                            green = color.G / 5;
                            blue = color.B / 5;
                break;
                case 335:    //blue : 0000FF
                            red = (color.R / 5);
                            green = (color.G / 5);
                            blue = color.B;
                break;
                case 171:    //gold : FFD700
                            red = color.R;
                            green = (int)((double)color.G / 255.0 * 215.0);
                            blue = 0;
                break;
                case 193:    //copper : B87333
                            red = color.R;                                    //184 mapped to 255
                            green = (int)((double)color.G / 255.0 * 115.0);    //115 mapped to 255
                            blue = (int)((double)color.B / 255.0 * 51.0);    //50 mapped to 255
                break;
                case 211:    //purple : 800080
                            red = color.R;
                            green = 0;
                            blue = color.B;
                break;
                case 1600:    //ocher : BBBB00
                            red = (int)((double)color.R / 255.0 * 187.0);    //BB mapped to 255
                            green = (int)((double)color.G / 255.0 * 187.0);    //BB mapped to 255
                            blue = 0;
                break;
                case 1700:    //pink : FFC0CB
                            red = color.R;
                            green = (int)((double)color.G / 255.0 * 192.0);
                            blue = (int)((double)color.B / 255.0 * 203.0);
                break;
                case 4500:    //silver : C0C0C0
                            red = color.R;
                            green = color.G;
                            blue = color.B;
                break;
                default:    red = 0;
                            green = 0;
                            blue = 0;
                break;
            }                            //end switch
            c = System.Drawing.Color.FromArgb(alpha, red, green, blue);
            return c;
        }

 

Simply put, we're shifting the gray color to another color so it comes out right.  Some trial and error was involved here.  I tried to get as close to the original as possible, but also tried not to loose any features.

Image Transparency

Notice that you can also download the jpg images from the SDO website in case you want to skip the first section.

Once you have the images you can set them one on top of the other.  They have the same size, the solar center is the same and so is the disk size.  I added a line of images to the source of this project.

In WPF you create an Image for each bandwidth.  I use the temperature as base value to order my images:

  1. 304
  2. 131
  3. 171
  4. 193
  5. 211
  6. 335
  7. 1600
  8. 1700
  9. 4500

The temperature is expressed in degrees Kelvin (K)

You can find most that information on the internet, but here it is:
(somewhat incomplete, but sufficient for our needs)

Bandwith K (from) K (to) Order
0094 6300000   7
0131 400000 16000000 2
0171 630000   3
0193 1200000 20000000 4
0211 20000000   5
0304 50000   1
0335 25000000   6
1600      
1700      
4500      

 

 

 

 

The order is based on the K (from).  The values are mostly a range. (like 0131, 0193).

loading an image from disk you can do like this:

string fn = rootfolder + @"\" + basefilename.Replace(bw, "0094");
                images["0094"].BeginInit();
                images["0094"].UriSource = new Uri(fn, UriKind.Absolute);
                images["0094"].EndInit();
                img_0094.Source = images["0094"];
                img_0094.Stretch = Stretch.Uniform;

This code loads the 0094 image.  Repeat it for all bandwidths.  I manually added and styled the Image controls in XAML, but you could just as easely add in code and use a List or Array to hold the controls.

Notice that the filenames are similar for each bandwith (hence reusing the filename and inserting the bandwith part with a new bandwith).  You can also use the filename for changing the "time" parameter later in in the Timelapse Video section.

In order to zoom I added a slider and on its ValueChanged event and I added this code:

            //0 - 1000
            if(slider_zoom.Value >= 0 && slider_zoom.Value < 100){
                img_0304.Opacity = (100 - slider_zoom.Value)/100;
                img_0131.Opacity = (0 + slider_zoom.Value) / 100;
            }
            if(slider_zoom.Value >= 100 && slider_zoom.Value < 200){
                img_0131.Opacity = (100 - (slider_zoom.Value - 100))/100;
                img_0171.Opacity = (0 + (slider_zoom.Value - 100))/100;
            }
            if(slider_zoom.Value >= 200 && slider_zoom.Value < 300){
                img_0171.Opacity = (100 - (slider_zoom.Value - 200))/100;
                img_0193.Opacity = (0 + (slider_zoom.Value - 200))/100;
            }
            if(slider_zoom.Value >= 300 && slider_zoom.Value < 400){
                img_0193.Opacity = (100 - (slider_zoom.Value - 300))/100;
                img_0211.Opacity = (0 + (slider_zoom.Value - 300))/100;

            }
            if(slider_zoom.Value >= 400 && slider_zoom.Value < 500){
                img_0211.Opacity = (100 - (slider_zoom.Value - 400))/100;
                img_0335.Opacity = (0 + (slider_zoom.Value - 400))/100;
            }
            if(slider_zoom.Value >= 500 && slider_zoom.Value < 600){
                img_0335.Opacity = (100 - (slider_zoom.Value - 500))/100;
                img_0094.Opacity = (0 + (slider_zoom.Value - 500))/100;
            }
            if(slider_zoom.Value >= 600 && slider_zoom.Value < 700){
                img_0094.Opacity = (100 - (slider_zoom.Value - 600))/100;
                img_1600.Opacity = (0 + (slider_zoom.Value - 600))/100;
            }
            if(slider_zoom.Value >= 700 && slider_zoom.Value < 800){
                img_1600.Opacity = (100 - (slider_zoom.Value - 700))/100;
                img_1700.Opacity = (0 + (slider_zoom.Value - 700))/100;
            }
            if(slider_zoom.Value >= 800 && slider_zoom.Value < 900){
                img_1700.Opacity = (100 - (slider_zoom.Value - 800))/100;
                img_4500.Opacity = (0 + (slider_zoom.Value - 800))/100;
            }
            if(slider_zoom.Value >= 900 && slider_zoom.Value < 1000){
                img_4500.Opacity = (100 - (slider_zoom.Value - 900))/100;
            }

I must admit that was easier than I thought and it goes pretty smoothly.

Timelapse Video

This part is still under construction.  Of course are any ideas on this welcome.

Here are my thoughts:

Basically you need two rails per bandwith (double buffering), loading the next image in background while displaying the previous.  That means your playing with 20 Image controls, 10 showing and 10 in the background. (possible memory issues)

The problem, I think will be to change the Opacity property on the fly, while looping through the images.  Since this is definately multithreaded it might become an issue.

Needless to say, if I get it to work, I will post it in this section.

Points of Interest

The fun thing about this prototype project is that it is very visual.  Working with images always is.  You change the code a bit and you see the result immediately.

This is a "free time" project, so I'm unsure how much time I can spend on it, but I'll try to finish it.

Concerning the fits conversion

This proved quite a challenge.  The FITS libraries available for C# aren't the best and I learned that not all images consist of pixels with RGB values.  Getting it converted prooved quite a challenge.  The end result is "OK", but still not as you would see on the SDO webpage.  I'm pretty sure my trail and error can be improved, notably skipping (or setting) values above a treshold to black (before converting!) .  I saw some python code where they do this.  The effect is that the haze around the sun should disappear somewhat.  Also the arbitrary value we use to divide the FITS pixel value could be improved.  My guess is that there is some scaling factor inside the FITS header you could use, but until now I haven't found it.

Concerning the image transparency

At first I thought you would need to work on the image itself to make this work, instead of changing the opacity of the control.  Since it did not require much work, I just tried it and it came out very well and smooth.  Needless to say that this can also be used with different kinds of images where it might be useful to do something similar.

Timelapse Video

Creating a timelapse video should not be so hard in itself.  You create a timer and load/show the image one after the other.  Trying to control the images while running the timelapse will probably be another matter, but getting the 10 bandwidths running in sync while playing with the transparency will be quite the challenge.

References

  • "Courtesy of NASA/SDO and the AIA, EVE, and HMI science teams."

History

  • 2016-04-08: Version 1.0

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here