Introduction
This article has three parts.
- Converting FITS files to more commonly known image formats
- Show the converted images with a dynamic transparency
- 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.
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;
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();
} } while(basichdu != null);
for(int i = 0; i < fits.NumberOfHDUs; i++){
basichdu = fits.getHDU(i);
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;
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;
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);
} }
int offset = (int)(totalmin < 0.0 ? (-1)*totalmin : 0.0);
for(int j = array.Length-1; j >= 0; j--){
idx_x = 0;
idx_y++;
int[] row = (int[])array[j];
for(int k = 0; k < row.Length; k++){
double val = row[k];
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;
} 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);
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++;
}
}
} catch(Exception ex){
string error = ex.Message;
} } } 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.
private static System.Drawing.Color WeighRGBValue(System.Drawing.Color color, int wavelength){
System.Drawing.Color c;
int alpha, red, green, blue;
red = green = blue = 255;
alpha = color.A;
switch(wavelength){
case 94: red = 0;
green = color.G;
blue = 0;
break;
case 131: red = 0;
green = color.G;
blue = color.B;
break;
case 304: red = color.R;
green = color.G / 5;
blue = color.B / 5;
break;
case 335: red = (color.R / 5);
green = (color.G / 5);
blue = color.B;
break;
case 171: red = color.R;
green = (int)((double)color.G / 255.0 * 215.0);
blue = 0;
break;
case 193: red = color.R; green = (int)((double)color.G / 255.0 * 115.0); blue = (int)((double)color.B / 255.0 * 51.0); break;
case 211: red = color.R;
green = 0;
blue = color.B;
break;
case 1600: red = (int)((double)color.R / 255.0 * 187.0); green = (int)((double)color.G / 255.0 * 187.0); blue = 0;
break;
case 1700: red = color.R;
green = (int)((double)color.G / 255.0 * 192.0);
blue = (int)((double)color.B / 255.0 * 203.0);
break;
case 4500: red = color.R;
green = color.G;
blue = color.B;
break;
default: red = 0;
green = 0;
blue = 0;
break;
} 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:
- 304
- 131
- 171
- 193
- 211
- 335
- 1600
- 1700
- 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:
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