According to The FITS Support Office at NASA/GSFC 'Flexible Image Transport System' (FITS) is a standard data format used in astronomy.

Introduction

Back when I attended University, the campus lent me an Orion StarShoot Pro V2.0, which I used with an 8" aperture telescope for Astrophotography. The software provided with the camera was called MaximDL Essential, which was a version of the software with most of it's features stripped. The software, like most other astrophotography software, produces images with the open standard called Flexible Image Transport System (FITS).

FITS is mostly, only, used in the scientific community for it's ability to not only store image data but also information in the form of binary or ASCII tables. This ability gives the format great modularity, but it also decreases the support for some configurations of the format. For instance, in the Java-based image processing software created by a Dev at the NIH, if an RGB FITS image is opened, it is recognized as three different images. MaximDL, which opens the FITS images correctly, is only available if purchased, starting, at 300$.

Anyway, I decided to create a program which will correctly display the FITS images produced by the StarShoot Pro. This, for me, will be an on going project, as to gradually increase support for FITS images, add features, and, even, give it the ability to interface with the StarShoot Pro to directly take exposures.

Now, I chose Fast Light Toolkit (FLTK) since it's a cross-platform Graphic User Interface (GUI) C++ library. It is written in C++ in such a way that it can be compiled in Linux, Windows, and MacOS X. The library I chose to interface with the FITS images is cfitsio since it provides high-level functions for reading and writing FITS files.

The Image Class

After looking through the documentation and some of the source code, I noticed that the creation of a GUI which imported some of the more well known image file types, like JPEG and PNG, were made simple by the use of classes Fl_JPEG_Image and Fl_PNG_Image which inherit the class Fl_RGB_Image. These sub-classes' only task is to add the functionality to Fl_Image to import the certain file types, like JPEG and PNG. For Instance, if we look at the header for the class Fl_BMP_Image you will notice that it only has one non-inherited member function.

// /\/\    more code    /\/\
class FL_EXPORT Fl_BMP_Image : public Fl_RGB_Image {

    public:
    
    Fl_BMP_Image(const char* filename);
};
// \/\/    more code    \/\/

So, it is optimal to follow in their foot steps, but if we take a look at Fl_RGB_Image we also notice that the perfered way of storing the image is in a uchar array; uchar is an 8-bit integer. If we want to accept images which are 16-bit, 32-bit, etc. then we need to import the image as a double array and squeeze the n-bit information into the 8-bit space. Another caveat is the fact that RGB images in FITS file are stored in a planar format and the FLTK library requires an interleaved format.

// include/Fl_FITS_Image.H
#ifndef Fl_FITS_Image_H
#define Fl_FITS_Image_H
#include "FL/Fl_Image.H"

class FL_EXPORT Fl_FITS_Image : public Rl_RGB_Image {
public:
  Fl_FITS_Image(const char* filename);
private:
  /* copies and converts a double array to a uchar array */ 
  void copy_double_to_uchar(uchar* output, double* input, long long size);
  /* converts from planar format to interleaved format */
  void planar_to_rgb(uchar* ar, long long size);
};
#endif // Fl_FITS_Image_h

Let's start building our source for the header by including some of the libraries we need, including the cfitsio, fltk, and some standard libraries.

// src/Fl_FITS_Image.cxx

#include <Fl_FITS_Image.H>
#include <FL/Fl_Shared_Image.H>
#include <FL/Fl.H>
#include <stdio.h>
#include <stlib.h>
#include <algorithm>

extern "C"
{
#include <fitsio.h>
}

In order to compress the information from a larger space stored in the double array, first of all, we need to get the space that the information spans, then, using this, we scale the data.

void Fl_FITS_Image::copy_double_to_uchar(uchar* output, double* input, long long size)
{
  /* Find largest and smallest number in array */
  auto ele = std::minmax_element(input, input+size);
  double max_ele = ele.first - input;
  double min_ele = ele.second - input;
  
  /* Scale Image to uchar based on the largest and smallest number in array */
  for (long long i = 0; i < size; i++)
    output[i] = static_cast<uchar>( (input[i] - min_ele) * 255.0 / (max_ele - min_ele) );
}

The interleaving of planar data is an interesting mathematical problem that has a recursize algorithm which can interleave the data in place. But, due to time constraints I chose to use the naive approach of splitting the array. So, in memory the array is stored as RRRRGGGGBBBB but we want it store as RGBRGBRGBRGB.

void Fl_FITS_Image::planar_to_rgb(uchar* ar, long long size)
{
  long long j;
  long long trd = size / 3;
  
  uchar f[trd];
  uchar s[trd];
  uchar t[trd];
  
  /* split */
  for (j = 0; j < trd; j++)
  {
    f[j] = *(ar+j);
    s[j] = *(ar+trd+j);
    f[j] = *(ar+2*trd+j);
  }
  
  /* Shuffle */
  for (j = 0; j < trd; j++)
  {
    *(ar+(j*3)) = f[j];
    *(ar+(j*3)+1) = s[j];
    *(ar+(j*30+2) = t[j];
  }
}

Now, to get to the meat of the class, I used the program called imcopy whose source code is posted on the nasa library website. Firstly, lets declare some temporary variables

Fl_FITS_Image::Fl_FITS_Image(const char* filename): Fl_RGB_Image(0,0,0)
{
  fitsfile *fptr; /* FITS file pointer, defined in fitsio.h */
  int status = 0; /* CFITSIO status value MUST be initialized to zero! */
  int bitpix, naxis; /* bits per pixel and number of dimensions */
  /* naxes will store image size and fpixel is a temprary array */
  long naxes[3] = {1, 1, 1}, fpixel[3] = {1, 1, 1};

The fits_open_file function will open the image file and the fits_get_img_param will get the number of bits per pixel, the number of dimension, and the size of the dimensions.

/* Open Fits File */
if (!fits_open_file(&fptr, filename, READONLY, &status))
{
  /* Get Fits paramters */
  if (!fits_get_img_param(fptr, 3, &bitpix, &naxis, naxes, &status))
  {
    // TODO
  }
  /* Close Fits file */
  fits_close_file(fptr, &status);
  
  /* If error occurred, print out error message */
  if (status)
  {
    char toshow[30];
    fits_get_errstatus(status, toshow);
    Fl::warning("Error: %s\n", toshow);
  }
}

Now, we need to make sure the image data is either 2D for gray scale or 3D for RGB image. Where the todo comment is add

if (naxis != 2 && naxis != 3)
  Fl::warning("Error: only 2D or 3D images are supported, it is %dD\n", axis);
else
{
  // TODO
}

Now that we have some checks in place, we can start by setting the dimensions with some of the inherited member functions. According to the documentation, we set the d function to 1 if gray scale and 3 if rgb. Where the todo comment add

/* Set dimensions */
w(naxes[0]);
h(naxes[1]);
d((naxis == 2) ? 1 : 3);

A check that is required and apparent from looking at the source code for Fl_JPEG_Image is that for the size of the image. We need to make sure that it is not too big and reset all the vars if it is.

/* Check if Size is too Big! */
if (((size_t)w()) * h() * d() > max_size() ) {
  Fl::warning("JPEG file \"%s\" is too large or contains errors!\n", filename);

  /* close fits file if File too big */
  fits_close_file(fptr, &status);

  /* set dimensions to 0 */
  w(0);
  h(0);
  d(0);

  /* delete array */
  if (array) {
    delete[] const_cast<uchar*>(array);
    array = 0;
    alloc_array = 0;
  }

  /* throw error */
  ld(ERR_FORMAT);
  return;
}

Lets allocate the array to the inherited member uchar* and set alloc_array to 1 to indicate that it has been allocated.

/* allocate array */
long long pn = w() * h() * d();
array = new uchar[pn];
alloc_array = 1;

If the image that we are getting is already 8-bit then there is no need to go through the double step described above we can just call fits_read_pix to get the image into the array.

if (bitpix == BYTE_IMG)
  fits_read_pix(fptr, TBYTE, fpixel, pn, NULL, const_cast<uchar*>(array), NULL, &status);

For the other image types we make the use of a double array

else
{
  /* Create a double array, to compress any kind of image into 8bit space */
  double* tmp = new double[pn];
  
  /* Read image into double array */
  fits_read_pix(fptr, TDOUBLE, fpixel, pn, NULL, tmp, NULL, &status);
  
  /* Compress image into uchar array */
  copy_double_to_uchar(const_cast<uchar*>(array), tmp, pn);
  
  /* delete temporary array */
  delete [] tmp;
}

If the image is RGB then we need to interleave it.

/* FITS Images are store in a planar format but FLTK requires an interleaved format */	
/* RRRRGGGGBBBB -> RGBRGBRGBRGB */
if (d() == 3)
  planar_to_rgb(const_cast<uchar*>(array), pn);

The Window Class

I've found when programming with the GTKmm library, a C++ linux centric GUI library, that inheriting the class which defines the window and adding any widgets to it is good way of tackling the building of a GUI. So, in this case, we inherit the Fl_Window and add our widgets to it. To keep it simple, I will only add a menu bar and a box which will display our image. In the menu bar, we will have an "Open" option that will pull up the Fl_Native_File_Chooser class and grab a string with our image file.
In the design of our GUI, we will have a menu bar that takes up the first 30 pixels from the top. The window, menu bar, and image box will resize dynamically depending on the size of the image with a small border around the image of a couple of pixels. So, on top of the widget objects we will have functions that will get and set the size of the boundary. In order for a function to be called by the "Open" button in the menu bar, the function has to be a static function.

// include/Main_Window.H
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Menu_Bar.H>

class Main_Window : public Fl_Window
{
public:
  /* Constructor and Desctructor */
  Main_Window();
  ~Main_Window() {}
  
  /* FLTK Widgets */
  Fl_Menu_Bar *menuBar;
  Fl_Box *imgBox;
  
  /* funcs to access _boundary */
  int Get_Boundary() { return _boundary; }
  void Set_Boundary(int boundary) { _boundary = boundary; }
private:
  /* boundary around image */
  int _boundary = 10;
  /* Called by Open buton in menu bar */
  static void OpenCallback(Fl_Widget* w, void* data);
  /* Called by Quit button in menu bar */
  static void QuitCallback(Fl_Widget* w, void* data);
  /* Loads the image into Fl_Box */
  static void Load_Image(const char* filename, void* data);
  /* Figures out perfered size for image to be resized */
  static void suggest_size(int src_w, int src_h, int& img_w, int& img_h, int boundary);
};
#endif // MAINWINDOW_H

In the source file for the class we will include the Fl_FITS_Image class we created as well as some necessary includes.

// src/Main_Window.cxx
#include "Main_Window"
#include <iostream>
#include <FL/Fl.H>
#include <FL/fl_ask.H>
#include <FL/Fl_Native_File_Chooser.H>
#include <Fl_FITS_Image.H>

In the constructor for the class we have to call the constructor for the class that we are inheriting, the Fl_Window class. We then must create the objects for which the menuBar and imgBox will be pointing at, as well as, point our menuBar items to the callback functions.

Main_Window::Main_Window() : Fl_Window(400, 200, "StarShootView")
{
  /* Choose the FLTK Theme */
  Fl::scheme("gtk+");
  
  /* Create the menu bar */
  menuBar = new Fl_Menu_Bar(0, 0, 400, 30);
  
  /* Create the quit/open button and attach the callback functions */
  menuBar->add("&File/&Open", "^o", OpenCallback, (void*)this); // Pass (this) as a pointer
  menuBar->add("&File/&Quit", "^q", QuitCallback);
  
  /* Create the Box which will contain our images */
  imgBox = new Fl_Box(_boundary, _boundary+30, 400-_boundary, 200-_boundary);
}

The OpenCallback will create the Fl_Native_File_Chooser object, show it, then call the Load_Image function.

void Main_Window::OpenCallback(Fl_Widget* w, void* data)
{
  Fl_Native_File_Chooser fnfc;
  
  /* set the title */
  fnfc.title("Pick a FITS file");
  fnfc.type(Fl_Native_File_Chooser::BROWSE_FILE);
  
  /* This will restrict the kinds of file that show up */
  fnfc.filter("FITS\t*.{fits,fit,fts}");
  
  // show native chooser
  switch( fnfc.show() ) {
    case -1: fl_alert("ERROR: %s", fnfc.errmsg()); break;
    case  1: break;
    default: Load_Image(fnfc.filename(), data);
  }
}

We can now start to build the call back functions. The QuitCallback function will simply call the exit function.

void Main_Window::QuitCallback(Fl_Widget* w, void* data)
{
  exit(0);
}

Finally, for the function that will load the image, Load_Image. We must first cast the void pointer to a Main_Window pointer.

void Main_Window::Load_Image(const char* filename, void* data)
{
  /* Set void pointer as Main_Window pointer */
  Main_Window *win = (Main_Window*)data;

In order to find the right dimensions for the window, box, and menu bar with respect to the image, we need to get the current screen size, image size, window position, and the boundary size.

/* Get screen size, image size, position, and window position */
int img_w = fits.w(), img_h = fits.h();           // Image Size
int src_w = Fl::w(), src_h = Fl::h();             // Screen Size
int x_pos = win->x_root(), y_pos = win->y_root(); // Window Position
int boundary = win->Get_Boundary();               // Boundary Size

Use the function we have yet to write called suggest_size to get the new image size. The new image size will be used to resize the image as well as all the other aspects of the window.

/* Calculate a good image size to scale to */
suggest_size(src_w, src_h, img_w, img_h, boundary);

According to the documentation, we can use the copy function into a Fl_Image file to resize the image.

/* Resize image */
Fl_Image* new_fits = fits.copy(img_w, img_h);

Lastly, update the window size, menu bar size, image box size, update the image, and redraw.

/* Update image, box size, window size, menu bar size, and redraw */
win->resize(x_pos, y_pos, img_w + boundary, img_h + boundary + 30);
win->menuBar->resize(0, 0, img_w + boundary, 30);
win->imgBox->resize(boundary/2, boundary/2 + 30, img_w, img_h);
win->imgBox->image(new_fits);
win->redraw();

Now, I would like to keep the aspect ratio of the image, so it would be best to scale both the width and height by the same amount I called scale_amount.

void Main_Window::suggest_size(int src_w, int src_h, int& img_w, int& img_h, int boundary)
{
  // Amount to Scale 
  float scale_amount = 1.0;

Firstly, we need to check if the width of the image is larger then the width of the image; Then, figure out how much to scale.

if (img_w > src_w)
  scale_amount = scr_w * 1.0f / img_w;

Next, let's check if the image width is larger than the screen width, but, if it is, then we must also check if the amount to scale for the width is larger than the height. Then, apply the scale.

if (img_h > src_h)
{
  float sc = src_h * 1.0f / img_w;
  if (sc < scale_amount) scale_amount = sc;
}
img_w = img_w * scale_amount - boundary;
img_h = img_h * scale_amount - boundary - 40;

Main Function

The inhertience of the Fl_Window really simplifies the main function. All that is needed is to create the Main_Window object.

#include <FL/Fl.H>
#include "Main_Window.H"

int main() {
  Main_Window win;
  win.show();
  return Fl::run();
}

Results

In Fedora or Ubuntu, you can use their respective project managers to obtain the required two package, libfltk and libcfitsio. To build with the libraries installed, enter this into the terminal

git clone https://github.com/anhydrous99/StarShootView
cd StarShootView/build
cmake ..
make

On windows, you can obtain or build the two respective library obtained from this website for cfitsio and this website for FLTK. Then use the CMake GUI or the generated Visual Studio Project to link and build these libraries.

StarShootView with RGB FITS image displayed

Link to part 2