#define NFKEYS 11
#define NSTDKEYS 10
#define NAXES 3
/* the macros significantly speed-ups processing */
#define MAX(x,y) (x) > (y) ? (x) : (y)
#define MIN(x,y) (x) < (y) ? (x) : (y)
#define PRESCALE(flux,thresh,sense) ((flux - thresh) / sense)
#define CLIP(x,xmin,xmax) MIN(MAX(x,xmin),xmax)
/* scotopic normalisation Sn = Scotopic(Xn,Yn,Zn)/100 */
const float Sn = 0.81085;
/* white-point for Luv, D65 */
const float uw = 0.19783;
const float vw = 0.46832;
/* white-point for xyY, D65 */
const float xw = 0.31273;
const float yw = 0.32902;
/* radian */
#define RAD 57.29577951308232
/* Gamma table */
#define NGAMMA 65536
static const float gstep = 1.0 / (float)(NGAMMA - 1);
static int gtab[NGAMMA];
static int Gamma(float r)
{
int n = r / gstep;
if( n < 0 )
return gtab[0];
else if( n > NGAMMA-1 )
return gtab[NGAMMA-1];
else {
assert(0 <= n && n < NGAMMA);
return gtab[n];
}
}
float sRGBGamma(float r)
{
const float q = 1.0 / 2.4;
return r < 0.0031308f ? 12.92f*r : 1.055f*powf(r,q) - 0.055f;
}
void XYZ_sRGB(float X, float Y, float Z, int *rgb)
{
float x = X / 100.0f;
float y = Y / 100.0f;
float z = Z / 100.0f;
/* transform to RGB and apply gamma function */
rgb[0] = Gamma( 3.2406f*x - 1.5372f*y - 0.4986f*z);
rgb[1] = Gamma(-0.9689f*x + 1.8758f*y + 0.0415f*z);
rgb[2] = Gamma( 0.0557f*x - 0.2040f*y + 1.0570f*z);
}
/* http://en.wikipedia.org/wiki/CIELUV_color_space */
void XYZ_Luv(float X, float Y, float Z, float *L, float *u, float *v)
{
float s,u1,v1,y,t;
s = X + 15.0f*Y + 3.0f*Z;
if( s > 0.0f ) {
u1 = 4.0f*X/s;
v1 = 9.0f*Y/s;
}
else {
u1 = uw;
v1 = vw;
}
y = Y / 100.0f;
if( y > 0.0088565f /* = powf(6.0/29.0,3)*/ )
*L = 116.0f*cbrtf(y) - 16.0f;
else
*L = 903.30f*y; /* = pow(26.0/3.0,3) */
t = 13.0f*(*L);
*u = t*(u1 - uw);
*v = t*(v1 - vw);
}
void Luv_XYZ(float L, float u, float v, float *X, float *Y, float *Z)
{
float u1,v1,s,t,w;
s = 13.0f*L;
if( s > 0.0f ) {
u1 = u/s + uw;
v1 = v/s + vw;
}
else {
u1 = uw;
v1 = vw;
}
if( L <= 8.0f )
*Y = L*0.11071f; /* = 100*powf(3.0/29.0,3) */
else {
w = (L + 16.0f)/116.0f;
*Y = 100.0f*w*w*w;
}
t = *Y/(4.0f*v1);
*X = t*(9.0f*u1);
*Z = t*(12.0f - 3.0f*u1 - 20.0f*v1);
}
float itt_line(float r)
{
return r;
}
float itt_snlike(float r)
{
float t = 6.66f*r;
return t > 0.0f ? t / asinh(t) - 1.0f : 0.0f;
}
float itt_sqr(float r)
{
return r*r;
}
float itt_tanh(float r)
{
return tanh(r);
}
float itt_asinh(float r)
{
return asinh(r);
}
float itt_photo(float r)
{
return (1.0f + tanh(2.0f*(r - 1.0f)))/2.0f;
}
float Scotopic(float X, float Y, float Z)
{
return 0.36169f*Z + 1.18214f*Y - 0.80498f*X;
}
float fmean(const float *pic, int w, int h, int x, int y, int d)
{
int x1,y1,x2,y2;
int i, j, n;
float s;
s = 0;
y1 = y;
y2 = y + d;
if( y2 > h ) y2 = h;
x1 = x;
x2 = x + d;
if( x2 > w ) x2 = w;
n = (y2 - y1)*(x2 - x1);
for(j = y1; j < y2; j++) {
const float *nrow = pic + j*w;
for(i = x1; i < x2; i++)
s += nrow[i];
}
return n > 0 ? s/n : 0;
}
float fpixel(const float *pic, int w, int h, int x, int y)
{
assert((0 <= x && x < w) && (0 <= y && y < h));
return pic[y*w + x];
}
void grey_picture(const float *pic, int naxis, const long *naxes,
long width, long height, png_byte *image,
int scale, float thresh, float sense, int bytes_per_pixel,
float (*Itt)(float))
{
long i,j,m,n;
float f;
png_byte *pimage = image;
for(j = 0; j < height; j++ ) {
n = j*scale;
for(i = 0; i < width; i++ ) {
m = i*scale;
if( scale == 1 )
f = fpixel(pic,naxes[0],naxes[1],m,n);
else
f = fmean(pic,naxes[0],naxes[1],m,n,scale);
f = Itt(PRESCALE(f,thresh,sense));
/* save to the image stream */
if( bytes_per_pixel == 2 ) {
png_uint_16 imgpix = (png_uint_16) Gamma(f);
memcpy(pimage,&imgpix,bytes_per_pixel);
}
else {
*pimage = (png_byte) Gamma(f);
}
pimage += bytes_per_pixel;
} /* m */
} /* n */
}
void colour_picture(const float *pic, int naxis, const long *naxes,
long width, long height, png_byte *image,
int scale, float thresh, float sense, int bytes_per_pixel,
float satur, int scotop, float slevel,float swidth,
float (*Itt)(float))
{
long i,j,m,n;
float W, X, Y, Z, L, u, v, x, y;
int rgb[3];
png_byte *pimage = image;
int npix = naxes[0] * naxes[1];
const float *Zpic = pic;
const float *Ypic = pic + npix;
const float *Xpic = pic + 2*npix;
for(j = 0; j < height; j++ ) {
n = j*scale;
for(i = 0; i < width; i++ ) {
m = i*scale;
if( scale == 1 ) {
Z = fpixel(Zpic,naxes[0],naxes[1],m,n);
Y = fpixel(Ypic,naxes[0],naxes[1],m,n);
X = fpixel(Xpic,naxes[0],naxes[1],m,n);
}
else {
Z = fmean(Zpic,naxes[0],naxes[1],m,n,scale);
Y = fmean(Ypic,naxes[0],naxes[1],m,n,scale);
X = fmean(Xpic,naxes[0],naxes[1],m,n,scale);
}
if( Z < 0.0f ) Z = 0.0f;
if( Y < 0.0f ) Y = 0.0f;
if( X < 0.0f ) X = 0.0f;
if( Y > 0.0f ) {
/* linear intensity pre-scaling */
W = X + Y + Z;
if( W > 0.0 ) {
x = X / W;
y = Y / W;
/* mix colours with scotopic */
if( scotop ) {
const float xyw = xw / yw;
float r = (Y - slevel) / swidth;
float S = Scotopic(X,Y,Z) / Sn;
float w = (1.0f + tanh(r))/2.0f;
float Yw = Y*w;
float Sw = S*(1 - w);
float D = Yw/y + Sw/yw;
Y = Yw + Sw;
x = ((x/y)*Yw + xyw*Sw) / D;
y = Y / D;
}
/* pre-scale */
Y = 100*PRESCALE(Y,thresh,sense);
if( Y < 0.0 ) Y = 0;
float Q = Y / y;
X = Q*x;
Z = Q*(1.0f - x - y);
if( X < 0.0 ) X = 0;
if( Z < 0.0 ) Z = 0;
}
if( Y > 0.0 ) {
/* convert to Luv */
XYZ_Luv(X,Y,Z,&L,&u,&v);
/* polar coordinates in Luv -> chroma c, hue as an angle */
float c = hypotf(u,v);
float coshue = 0;
float sinhue = 0;
if( c > 0 ) {
coshue = u / c;
sinhue = v / c;
}
// keep colour saturation
float s = c / L;
/* Itt */
L = 100.0f*Itt(L/100.0f);
// recover the saturation
c = s*L;
// user saturation
c = satur*c;
// limit chroma to the valid gamut
int lum = round(L);
if( 0 <= lum && lum <= 100 && c > 0.0f ) {
float hue = atan2f(v,u);
int deg = round(RAD*hue);
assert(0 <= deg+180 && deg+180 < 361 && 0 <= lum && lum <= 100);
c = MIN(Luv_sRGBGamut[180+deg][lum],c);
}
else
c = 0;
u = c*coshue;
v = c*sinhue;
Luv_XYZ(L,u,v,&X,&Y,&Z);
} /* Y > 0 */
XYZ_sRGB(X,Y,Z,rgb);
}
else {
for(int k = 0; k < 3; k++)
rgb[k] = 0;
}
/* save to the image stream */
if( bytes_per_pixel == 2 ) {
png_uint_16 imgpix[3];
for(int k = 0; k < 3; k++)
imgpix[k] = (png_uint_16) rgb[k];
memcpy(pimage,imgpix,3*bytes_per_pixel);
}
else {
png_byte imgpix[3];
for(int k = 0; k < 3; k++)
imgpix[k] = (png_byte) rgb[k];
memcpy(pimage,imgpix,3);
}
pimage += 3*bytes_per_pixel;
} /* m */
} /* n */
}
int fitspng(char *fitsname, char *png, int bit_depth,
float qblack,float rsense, int scale,
float thresh, float sense, float sthresh, float swidth,
int ctype, float satur, int verb,
int set_abs, int set_rel, int scotop)
{
char *fkeys[NFKEYS] = {"OBJECT", "OBSERVER", "FILTER", "DATE-OBS",
"CAMTYPE", "EXPTIME", "SITE", "TEMPERAT",
"XFACTOR", "YFACTOR", "TELESCOP" };
char *stdkeys[NSTDKEYS] = {"Title","Author","Description","Copyright",
"Creation Time","Software","Disclaimer",
"Warning","Source","Comment"};
char *stdvalues[NSTDKEYS];
char *fval[NFKEYS];
long naxes[NAXES];
/* FITS input */
fitsfile *f;
int naxis,bitpix,status,nullval=0,fpixel=1;
int i,n,blim;
char line[FLEN_CARD];
float *pic;
/* data scaling */
float r;
/* png */
png_uint_32 height, width, bytes_per_pixel, color_bytes;
int srgb_intent;
png_byte *image;
FILE *fp;
png_structp png_ptr;
png_infop info_ptr;
png_text text_ptr[NSTDKEYS];
char buf[NFKEYS*FLEN_CARD + 100];
png_bytep *row_pointers;
char *tm[6], *c, *c0;
float (*Itt)(float);
/* --------------------------------------------------------------------*/
/* Part: Initilization */
status = 0;
/* setup bit limit */
blim = 1;
for( i = 0; i < bit_depth; i++)
blim = 2*blim;
blim = blim - 1;
/* compute Gamma table by selected colour space, Gamma() shadows global Gamma() */
for( i = 0; i < NGAMMA; i++) {
float x = i*gstep;
int g = blim*sRGBGamma(x) + 0.5;
gtab[i] = MIN(MAX(g,0),blim);
// printf("%f %d %d\n",x,gtab[i],(int)(blim*Gamma(x)+0.5));
}
switch(ctype) {
case 0: Itt = &itt_line; break;
case 1: Itt = &itt_asinh; break;
case 5: Itt = &itt_snlike; break;
case 6: Itt = &itt_sqr; break;
case 7: Itt = &itt_tanh; break;
case 8: Itt = &itt_photo; break;
default:Itt = &itt_line;
}
image = NULL;
pic = NULL;
for( i = 0; i < NFKEYS; i++)
fval[i] = NULL;
for( i = 0; i < NSTDKEYS; i++)
stdvalues[i] = NULL;
for( i = 0; i < 6; i++ )
tm[i] = NULL;
if( verb )
fprintf(stderr,"Initialisation has finished.\n");
/* --------------------------------------------------------------------*/
/* Part: Load input FITS */
if( verb )
fprintf(stderr,"FITS load has begun...\n");
/* check whatever filename has extension - usefull only for selecting
of single bands in colour images
*/
fits_open_image(&f,fitsname, READONLY, &status);
if( status )
goto finish;
fits_get_img_type(f,&bitpix,&status);
fits_get_img_dim(f,&naxis,&status);
if( status )
goto finish;
if( !(naxis == 2 || naxis == 3)) {
fprintf(stderr,"Crash: Only grey or colour FITS files are supported.\n");
goto finish;
}
if( naxis == 3 ) {
fits_read_key(f,TSTRING,"CSPACE",line,NULL,&status);
if( status || strstr(line,"XYZ") == NULL ) {
fprintf(stderr,"Crash: Only CIE 1931 XYZ colour-space is supported yet.\n");
goto finish;
}
}
/* keywords */
for(i = 0; i < NFKEYS; i++) {
fits_read_key(f,TSTRING,fkeys[i],line,NULL,&status);
if( status == 0 ) {
if( fval[i] == NULL )
fval[i] = strdup(line);
else {
if( verb )
fprintf(stderr,"Ignoring keywords: %s=%s\n",fkeys[i],fval[i]);
}
}
else
status = 0;
}
fits_get_img_size(f,NAXES,naxes,&status);
if( status )
goto finish;
n = 1;
for( i = 0; i < naxis; i++)
n = n*naxes[i];
pic = malloc(n*sizeof(float));
if( pic == NULL ) {
fprintf(stderr,"Crash: There is no room for an input image.\n");
goto finish;
}
fits_read_img(f,TFLOAT,fpixel,n,&nullval,pic,&i,&status);
if( status )
goto finish;
fits_close_file(f, &status);
if( verb )
fprintf(stderr,"FITS load has finished.\n");
/* --------------------------------------------------------------------*/
/* Part: Estimation of intensity parameters */
if( abs(bitpix) > 8 && bit_depth < 16 && set_abs != 1 ) {
int res;
int npix = naxes[0]*naxes[1];
float *Ypic = naxis == 2 ? pic : pic + npix;
if( naxis == 2 ) /* grey images, single plane */
Ypic = pic;
else /* Y component for colour images */
Ypic = pic + npix;
res = tone(npix,Ypic,rsense,&qblack,&thresh,&sense,naxis==3,set_rel,verb);
if( ! res ) {
fprintf(stderr,"Some bad constellation for estimation of scale.\n");
goto finish;
}
if( verb )
fprintf(stderr,"Scale parameters estimate has finished.\n");
}
/* --------------------------------------------------------------------*/
/* Part: Intensity conversion */
if( verb )
fprintf(stderr,"Intensity transformation has begun..\n");
/* fill an output array */
height = naxes[1]/scale;
width = naxes[0]/scale;
bytes_per_pixel = bit_depth / 8;
color_bytes = naxis == 2 ? 1 : 3;
n = width*height;
if( height < 1 || width < 1 ) {
fprintf(stderr,"Size of scaled image is zero.\n");
goto finish;
}
if( verb )
fprintf(stderr,"thresh=%f sense=%f colour bytes: %d"
" bytes per pixel: %d\n", thresh, sense, color_bytes, bytes_per_pixel);
if( (image = malloc(height*width*bytes_per_pixel*color_bytes)) == NULL ) {
fprintf(stderr,"Crash: There is no room for an output image.\n");
goto finish;
}
assert(pic && image && scale >= 1);
/* greyscale */
if( naxis == 2 )
grey_picture(pic,naxis,naxes,width,height,image,scale,thresh,sense,
bytes_per_pixel,Itt);
else /* if( naxis == 3 ), i.e. colour image */
colour_picture(pic,naxis,naxes,width,height,image,scale,thresh,sense,
bytes_per_pixel,satur,scotop,sthresh,swidth,Itt);
free(pic);
if( verb )
fprintf(stderr,"Intensity transformation has finished.\n");
/* --------------------------------------------------------------------*/
/* Part: Save to PNG */
if( verb )
fprintf(stderr,"PNG save has begun..\n");
if( png )
fp = fopen(png, "wb");
else
fp = stdout;
if (!fp) {
fprintf(stderr,"Crash: Initialising of an output file failed.\n");
goto finish;
}
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
if (!png_ptr) {
fclose(fp);
goto finish;
}
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
fclose(fp);
png_destroy_write_struct(&png_ptr,(png_infopp)NULL);
goto finish;
}
png_init_io(png_ptr, fp);
png_set_write_status_fn(png_ptr, NULL);
png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth,
color_bytes == 1 ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE);
srgb_intent = PNG_sRGB_INTENT_PERCEPTUAL;
png_set_sRGB(png_ptr, info_ptr,srgb_intent);
if( fval[0] )
stdvalues[0] = strdup(fval[0]);
else
stdvalues[0] = strdup("");
if( fval[1] )
stdvalues[1] = strdup(fval[1]);
else
stdvalues[1] = strdup("");
/* numerical constant in declaraton of buf must be greater than max. length
of sum of following string(s) */
strcpy(buf,"An image");
if( fval[0] )
sprintf(buf+strlen(buf)," of the %s",fval[0]);
if( fval[6] )
sprintf(buf+strlen(buf)," taken at %s observatory",fval[6]);
if( fval[1] )
sprintf(buf+strlen(buf)," by %s",fval[1]);
if( fval[4] )
sprintf(buf+strlen(buf)," by the %s instrument",fval[4]);
if( fval[10] )
sprintf(buf+strlen(buf)," of the %s telescope",fval[10]);
if( fval[3] )
sprintf(buf+strlen(buf)," at %s UT (start time)",fval[3]);
if( fval[5] )
sprintf(buf+strlen(buf)," of exposure %s sec",fval[5]);
if( fval[2] && color_bytes == 1 )
sprintf(buf+strlen(buf)," with the %s filter",fval[2]);
strcat(buf,".");
if( fval[7] )
sprintf(buf+strlen(buf)," The instrument temperature: %s.",fval[7]);
if( fval[8] )
sprintf(buf+strlen(buf)," XBinnig: %s.",fval[8]);
if( fval[9] )
sprintf(buf+strlen(buf)," YBinnig: %s.",fval[9]);
stdvalues[2] = strdup(buf);
stdvalues[3] = strdup("");
/* decode time (round fractional seconds and adds timezone),
Standard FITS headers must contains DATE-OBS as
YYYY-MM-DDTHH:MM:SSS.SSS, however the rule is violated
by many software writing obsolete YYYY-MM-DD keyword.
*/
i = 0;
if( fval[3] ) {
for( i = 0, c = fval[3], c0 = fval[3]; *c != '\0'; c++) {
if( *c == '-' || *c == 'T' || *c == ':' || *c == ' ' || *(c+1) == '\0') {
n = c - c0;
if( *(c+1) == '\0' )
n++;
tm[i++] = strndup(c0,n);
c0 = c + 1;
}
}
/* This loop is my own bugy implementation of bugy strtok
(https://sourceware.org/bugzilla/show_bug.cgi?id=16640) */
}
if( i == 6 ) {
if( sscanf(tm[5],"%f",&r) == 1 )
i = rint(r);
else
i = 0;
sprintf(buf,"%s-%s-%s %s:%s:%02d GMT",tm[0],tm[1],tm[2],tm[3],tm[4],i);
stdvalues[4] = strdup(buf);
} else if( i == 3 ) {
sprintf(buf,"%s-%s-%s",tm[0],tm[1],tm[2]);
stdvalues[4] = strdup(buf);
}
else
stdvalues[4] = strdup("");
stdvalues[5] = strdup("Created by FITSPNG.");
stdvalues[6] = strdup("");
stdvalues[7] = strdup("");
strcpy(buf,"");
if( fval[4] )
strcat(buf,fval[4]);
if( fval[10] ) {
strcat(buf,", ");
strcat(buf,fval[10]);
}
stdvalues[8] = strdup(buf);
strcpy(buf,"Converted from the original FITS image:");
sprintf(buf+strlen(buf)," %s",fitsname);
stdvalues[9] = strdup(buf);
for(i = 0; i < NSTDKEYS; i++ ) {
text_ptr[i].key = stdkeys[i];
text_ptr[i].text = stdvalues[i];
text_ptr[i].compression = PNG_TEXT_COMPRESSION_NONE;
}
png_set_text(png_ptr, info_ptr, text_ptr, NSTDKEYS);
png_write_info(png_ptr, info_ptr);
#ifdef WORDS_BIGENDIAN
;
#else
if( bit_depth == 16 )
png_set_swap(png_ptr);
#endif
if( (row_pointers = malloc(height*sizeof(row_pointers))) == NULL ) {
fprintf(stderr,"There is no room for all rows of image.\n");
png_destroy_write_struct(&png_ptr,(png_infopp)NULL);
goto finish;
}
for (i = 0; i < height; i++)
row_pointers[i] = image + (height-1-i)*width*bytes_per_pixel*color_bytes;
png_write_image(png_ptr,row_pointers);
png_write_end(png_ptr, NULL);
png_free(png_ptr,row_pointers);
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(fp);
if( verb )
fprintf(stderr,"PNG save has finished.\n");
finish:
fits_report_error(stderr, status);
for( i = 0; i < NFKEYS; i++)
free(fval[i]);
for( i = 0; i < NSTDKEYS; i++)
free(stdvalues[i]);
for( i = 0; i < 6; i++ )
free(tm[i]);
free(image);
return(status);
}
fitspng/fitspng.h 0000644 0040317 0001750 00000002027 14174601171 013350 0 ustar hroch hroch /*
FITSPNG
Copyright (C) 2006-2022 Filip Hroch, Masaryk University, Brno, CZ
Fitspng is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Fitspng is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Fitspng. If not, see .
*/
/* fitspng.c */
int fitspng(char *, char *,int,float,float,int,
float,float,float,float,int,float,
int,int,int,int);
/* ecdf.c */
int ecdf(long, const float *, float *, float *);
float quantile(long, const float *, const float *, float);
/* tone.c */
int tone(long, const float *, float, float*,float *, float *,int,int,int);
fitspng/fitspng.html 0000644 0040317 0001750 00000060750 14174601171 014074 0 ustar hroch hroch
Fitspng
Introduction
FITS format is a general purpose
astronomical format which stores measurements of physical quantities.
FITS images represents an angular distribution of captured photon events,
or intensity of light.
Fitspng provides visualisation of
FITS
images by converting them
to PNG format.
Tone mapping
Fitspng applies a
global tone mapping
technique to transform of a wide dynamical range of FITS files
(naturally including
HDR)
to a limited range of, so called, modern displaying devices and
image formats.
The tone mapping includes two steps:
a pre-scaling (normalisation), and optional application of an intensity
transfer function,
Pre-scaling
maps linearly the captured counts, related to optical
intensities
I (I ∈ ℝ, I ≥ 0 ), to the range
0 ≤ t ≤ 1 by the two parameter transformation:
t = (I - B) / s.
As the parameters, B – black level, and
s – sensitivity, has been selected.
B sets a background level in original data. It corresponds to zero
in transformed values, and finally to the black colour.
The sensitivity s > 0 adjusts
a range of FITS values visible on PNG pictures.
It simulates the artificial sensitivity of a detector:
high, or low, values leads to a dim, or highlight, images.
Sensitivity operates like a gain of an electronic amplifier of a detector.
This parameter is a reciprocal quantity to ISO value well-known in
classical photography.
The pre-scale transformation
should be followed by a cut-off, which assigns
all values below the level B to be black,
and over the interval to be white:
0 ≤ t ≤ 1
ITT scaling applies an
intensity transfer function f(t)
(realised by an intensity transfer table (ITT) in the past)
on the pre-scaled data t:
τ = f(t).
Various ITT functions are
introduced in ITT functions paragraph.
The described tone mapping is applied on all pixels of a frame;
one is applied on values for grey-scale,
and on the luminosity channel of colour images.
Both B, s parameters can be leaved unspecified:
they are estimated
by the included machine algorithm described in
Tone parameters estimation.
A manual setup of the pre-scale parameters is available in two modes:
- Absolute:
the parameters B, s are given directly by user or,
- Relative:
their values are given as multipliers of
internally estimated parameters.
The absolute scaling is suitable for fine tune by hand, while
the relative scaling for some batch processing.
Gamma correction
Finally, a light-sensitivity model, specific to a displaying device,
sRGB
is applied onto τ values by the colour space specific
gamma function Γ(τ):
p = { 255 Γ(τ), 0 ≤ p ≤ 255 },
where p is the final value stored in PNG.
Tone parameters estimation
A reliable initial setup of the scaling parameters B, s is crucial
for proper function of Fitspng. A novel approach on base of quantiles of
an empirical distribution function has been developed.
Background level is estimated as
25% quantile
QB of
empirical CDF of observed pixels.
It is close to CDF of
Normal
distribution for the sky, a star free background.
The black level is claimed as the quantile:
B = QB(¼)
The pixels included in determination of the CDF
are selected from a grid covering full frame
(over 30 thousands of data points). The one-quarter choice
is a result of empirical experiments.
Light upper level is estimated
as 95% quantile of the empirical CDF constructed on base of
pixels with values above ≥ 3 D
of the background CDF. The selected pixels contains high fraction
of star light and another star-gazing objects,
which provides the range for observed intensity values.
s0 = QL(95%)
Parameter D is a quantile estimation of dispersion
on base of ¼ and ¾ of QB
quantiles (QN(½) ≐ 0.6745 is 50% quantile of
Normal distribution):
D = [QB(¾) - QB(¼)] / [2 · QN(½)]
Relative parameters q, v
are defined by the way:
The relative parameter q ∈ (0,1) is the quantile of
QB(q). It is set on ¼ for grey images.
Colour images has both B and QB(q)
set such way to have guarantied B ≥ 0;
CIE 1931 XYZ
values are positive by definition.
Common properties of QB(q) are:
q = ½ is arithmetic mean,
q = ¼ (standard background level) is the mean of absolute deviations
under the mean, q=0 or q=1 are minimal and maximal values.
The parameter 0.001 < v < 1000 (mostly)
adjusts relative slope against to the pre-defined value
s0.
The empirical CDF for a perfectly ordinary picture of the sky,
a starry image having its principal part covered by a noise due background,
is plotted on the graph below.
The vertical axis shows quantiles, the horizontal axis of picture values.
The background noise follows Normal distribution up to Q = 0.8;
the star light is getting importance over the background above the level.
The empirical CDF of a perfectly ordinary picture of the sky,
see text for description.
The histogram of the same frame.
ITT functions
Intensity transfer (ITT) functions maps input and output
intensities with non-linear weights. It can help to emphasise,
or to surprise, desired objects on images. The choice of "right"
function depends on taken scene, on photographers intention,
and it is so unique as every photography is unique.
Fitspng implements these ITT functions:
- line: f(t) = t,
- S/N like: f(t) = at / asinh(at) - 1,
t > 0, t ≈ 6,
- square (sqr): f(t) = t2,
- tanh: f(t) = tanh(t),
- asinh: f(t) = asinh(t),
- photo: f(t) = ½ {1 + tanh[2(t-1)]}.
Line function does nothing,
so it is very universal, fast and the preferable first choice.
S/N like function is very rough approximation
of signal-to-noise ratio which offers a good image contrast.
For pure counts, the ratio is proportional
to reciprocal of relative error of observed values as t / √t,
but the square root has singularity in zero. Asinh is a safe approximation
of square root for 0 ≤ t ≤ 10.
Function square emphasises details in
large diffuse faint areas.
Function tanh simulates
logistics curve
with a smooth saturation.
Function asinh offers the most wide dynamical range.
It keeps faint details white the bright objects are still visible.
Photo emulates the gradation curve of
the classical photographic emulsion having a low dynamical range.
Colour tuning
Colour FITS frames, as defined by
Munipack,
are recognised.
Saturation
The colour saturation of images should be reduced when
the relative saturation parameter has value S < 1,
or enhanced when S > 1. The saturation of the final frame
is computed by formula
S c,
with the chroma c = √[(u-uw)2 +
(v-vw)2] specifying the white-point distance in
CIELUV colour-space.
Nite vision
The human eye sensitivity in high and low light conditions
can be artificially simulated by greying of faint parts of images.
The night vision has shifted spectral sensitivity into short wavelengths
which, together with its narrow broadband, simulates
Purkyně effect.
The blend of photopic (daily) and scotopic visions (darkness)
is modelled by the weighting function
w = ½ {1 + tanh [(Y - Ym) / wm]},
with night-day vision threshold Ym,
and width of wm of the mesopic vision region.
The function has been selected by heuristic. There is no a proof of validity.
To derive composition of colour and scotopic images,
CIE 1931 XYZ
values are converted into monochromatic scotopic channel S
S = 0.3617 Z + 1.1821 Y - 0.8050 X.
Mesopic intensity Ym
is a compound of intensities Y and S:
Yn =
wY + (1-w)S = Yw + Sw,
with Yw = Yw, Sw = S(1-w).
Colours are derived by chromaticity coordinates xy in xyY colour space
(CIE 1931 XYZ,
sec. CIE xy chromaticity diagram and the CIE xyY colour space)
by a blend of full colours with scotopic channel. Scotopic vision is
in degrees of grey having chromatic coordinates of the white-point
xw,yw:
xn =
[(x/y) Yw +
(xw/yw) Sw]
/ D
yn = Yn / D,
where the denominator is D =
Yw / y + Sw / yw.
The scotopic imaging is possible under conditions
Ym ≥ 0 and
wm > 0. Pictures becomes fully coloured
for Ym → 0, or complete in night vision for
Ym → ∞. The mesopic regime
parameters Ym, wm
should be determined empirically by a qualified estimate.
Image resize
An output image can be scaled-down by an integer factor. Every pixel
of the scaled image is computed as the arithmetical mean of a square
with side given by this factor.
The photometric information is preserved while a noise is suppressed
during the re-scaling. This sufficient fast method provides high quality images.
Exif information
FITS header meta-information is stored as an EXIF information
of PNG files: the EXIF text strings has free format and no formalised
form.
Fitspng invocation
Synopsis
fitspng [options] file(s)
Command line options
- -r q, v
-
Relative pre-scaling mode: q, v (0 ≤ q ≤ 1)
represents quantiles
of a background (the sky) and v relative sensitivity.
See Tone parameters estimation.
Parameters can be omitted leaving default values in row.
It is specially useful for colour images where q
should have its value slightly over zero. The exact text form is
"-r ,v" or "-r q,", and should have
the comma included to support unambiguous interpretation.
If a colour image is processed, q is determined as the quantile
for zero intensity, so it is good idea to leave it unset; just use
"-r ,v".
- -l B, s
-
Absolute pre-scaling mode: B, s are directly used
for scaling of the output frame.
This setup completely disables the internal parameters estimation.
See Tone mapping.
- -f [line | asinh | snlike | sqr | tanh | photo]
-
Select a function, see ITT functions section.
- -S S
-
The colour saturation (Colour FITS only).
- -n Ym, wm
-
When used, switch-on, and setup,
mode which emulates human night vision (Colour FITS only).
- -s s
-
Shrink image: scale-down the size of the image by the specified factor
s > 1: every output pixel is the arithmetic mean of
s2 input pixels.
- -o filename
-
Specify an output file name for a single file input only.
If this switch is omitted, the output filename is determined by
modification of the input filename(s): suffixes, like *.fits,
are replaced by *.png, and the directory
path is removed: /space/image.fits is converted
to image.png. The approach leaves original
data untouched, results are stored in the current working
directory (see also Examples).
- -B [8|16]
-
The bit depth of the output PNG frame: 8 (default) or 16 bites per pixel.
A post-processing can be affected by the choice; most utilities
doesn't work with 16-bit colour depth correctly.
- -v, --verbose
-
describes what Fitspng currently does.
- -h, --help, --version
-
prints summary of options; the current version.
Examples of usage
Convert a FITS image to PNG:
$ fitspng grey.fits
Convert a FITS image to PNG thumbnail:
$ fitspng -s 10 grey.fits
Emulate the classical photography sensitivity function (gradation curve):
$ fitspng -f photo photo.fits
Create a semi-grey image:
$ fitspng -S 0.2 -o reduced.png colour.fits
Emulate night vision:
$ fitspng -n 1e6,1 -o scotopic.png colour.fits
Generate thumbnails of FITS files in /space
storing files under the current directory:
fitspng -s 10 /space/*.fits
The same result should be emulated with help of shell scripting
(providing powerful file name manipulations):
$ for FITS in /space/*.fits; do
NAME=$(basename $FITS);
PNG=${NAME%fits}png;
fitspng -o $PNG $FITS;
done
Fitspng has no parallel run support included;
however, the execution time on multiprocessor systems should be significantly
reduced with help of some external utility:
$ ls /space/*.fits | xargs -n 1 -P 0 fitspng
Gallery
#1, Colour image in sRGB
#2, ITT photography tone applied
#3, Sensitivity increased
#4, Sensitivity decreased
#5, Reduced colour saturation
#6, Enhanced colour saturation
#7, Vanished colour saturation
#8, Night vision
Whole gallery is generated by processing of the reference raw photo
IMG_5952.CR2:
$ rawtran IMG_5952.CR2
$ fitspng -s 10 -o IMG_5952.png IMG_5952.fits #1
$ fitspng -f photo -l 0,4e3 -s 10 -o IMG_5952_photo.png IMG_5952.fits #2
$ fitspng -r 0,2 -s 10 -o IMG_5952_r02.png IMG_5952.fits #3
$ fitspng -r 0,0.5 -s 10 -o IMG_5952_r05.png IMG_5952.fits #4
$ fitspng -S 0.5 -s 10 -o IMG_5952_S05.png IMG_5952.fits #5
$ fitspng -S 1.5 -s 10 -o IMG_5952_S15.png IMG_5952.fits #6
$ fitspng -S 0 -s 10 -o IMG_5952_S0.png IMG_5952.fits #7
$ fitspng -n 3e10,1 -s 10 -o IMG_5952_nite.png IMG_5952.fits #8
Download and installation
The tar-ball,
or the development
repository, is freely available under GPL-3 licence.
Both
cfitsio and
libpng
libraries, including files required for development (headers, static libraries),
are necessary for building.
-
Fitspng runs under any Unix-like operating system (all flavours
of GNU/Linux and BSD, Solaris, Mac OS X, etc).
-
Fitspng packages can be found in
Debian and
Ubuntu.
-
Jean-Paul Godard reported successful compilation under W7 (64b) environment
and mingw.
-
Fitspng can be compiled under Windows 8 (64-bit) with mingw
compiler (libpng will be required).
A recommended way of installation under Unix-like system is:
$ tar zxf fitspng-U.V.tar.gz
$ cd fitspng/
$ ./configure CFLAGS="-O2 -DNDEBUG"
$ make
# make install
The final step should be executed by root.
All files are installed into /usr/local
tree.
It would be nice to keep the source directory, if uninstall is
a desired action.
See also
Munipack is a general utility
working with FITS images.
Development notes can be found in
my blog.
License
Fitspng is free
software licensed under the
GNU General Public License.
This gives you the freedom to use and modify Fitspng to suit your needs.
fitspng/fitspng.spec 0000644 0040317 0001750 00000002164 14174601171 014055 0 ustar hroch hroch Summary: FITS to PNG converter
Name: fitspng
Version: %(cat configure.ac | awk '{if(/AC_INIT/) {gsub("[\\[\\]\\,]"," "); print $3;}}')
Release: 1
License: GPL3
Group: Applications/File
URL: http://integral.physics.muni.cz/%{name}
Source0: ftp://integral.physics.muni.cz/pub/%{name}/%{name}-%{version}.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
%description
Fitspng is an utility intended to convert of the natural high
dynamic range of FITS images, directly representing measured data,
to the limited numerical range of PNG format widely used
in computer graphics. Fitspng implements a global tone mapping
technique by a set of tone functions using parameters provided
by user or by machine estimate on base of a robust count statistics.
Moreover, the conversion keeps an important FITS meta-information
as a text part of PNG header.
%prep
%setup -q
%build
%configure CPPFLAGS=-I/usr/include/cfitsio
make
%install
rm -rf $RPM_BUILD_ROOT
make install DESTDIR=$RPM_BUILD_ROOT
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root,-)
%doc %{_mandir}/man1/fitspng.1*
%{_bindir}/fitspng
%doc README COPYING NEWS
fitspng/images/ 0000755 0040317 0001750 00000000000 14174601302 012765 5 ustar hroch hroch fitspng/images/IMG_5952.png 0000644 0040317 0001750 00000403763 14174601171 014614 0 ustar hroch hroch PNG
IHDR ` _ tEXtTitle ' tEXtAuthor H TtEXtDescription An image at 2009-07-21T13:03:20 UT (start time) of exposure 0.00250 sec. -
tEXtCopyright : %tEXtCreation Time 2009-07-21 13:03:20 GMTHlk tEXtSoftware Created by FITSPNG.Bv tEXtDisclaimer tEXtWarning tEXtSource =tEXtComment Converted from the original FITS image: IMG_5952.fitsY IDATxd[,Kvgf="/nv9)P@|̓ zA#/9RᾖsQYYYm}f+?/kїhƀl-~x~z9=]>fwvҒIm.`mCLe)jUެ]=^}p#-^^=9~D3b >~_ח7
[/3faWa6I=#_a5N*V'On8I#FI<,p02|z\_