/* * Filename: percept.c * * Author: Martin Reddy, - 20 Dec 2000 * * Purpose: * This program reads an input image (binary PPM format) and processes * this to create another image showing the degree of detail actually * visible to the human eye, assuming that the image is presented * across a certain extent of our eyes' field of view which defaults * to the entire visual field. * * Essentially, we calculate the highest spatial frequency of each * pixel given its eccentricity (degree into the visual periphery) and * a constant user-specified velocity (degrees per second). We then * blur this pixel using an equivalently-sized Gaussian filter. * * The model used to evaluate spatial frequency limit is described in: * * M. Reddy (1997). "Perceptually Modulated Level of Detail for Virtual * Environments". Phd Thesis. University of Edinburgh. * * Status: * This code is released as Open Source under the GNU General Public * License (GPL), http://www.opensource.org/licenses/gpl-license.html * * (c) 2000,2001 Copyright Martin Reddy. All rights reserved. * */ #include #include #include #include #include #define BUFSIZE 256 #define max(a,b) (((a)>(b))?(a):(b)) #define min(a,b) (((a)<(b))?(a):(b)) /* just the one global variable... to control output of debug information */ int debug = 0; /* structure to hold an image and all parameters */ typedef unsigned char uchar; typedef struct { uchar *buffer; /* bitmap for the image */ int w, h, c; /* height, width, and components */ float hfov, vfov, hoff, voff; /* field of view and fovea offset (deg) */ } Image; typedef struct { float r, g, b; /* red, green, blue components [0..255] */ } Pixel; /* * log_debug * * Outputs a printf() style message to the log file, percept.log, but * only if the debug flag is on. This routine will manage opening the * file the first time it is called, and will close the file if passed * a NULL format string * */ void log_debug( const char *format, ... ) { static FILE *fp = NULL; va_list arg; if ( ! debug ) return; if ( ! format ) { if ( fp ) fclose( fp ); fp = NULL; return; } if ( ! fp ) { if ( ! ( fp = fopen( "percept.log", "w" ) ) ) { fprintf( stderr, "Cannot create debug log file!\n" ); debug = 0; return; } } va_start( arg, format ); vfprintf( fp, format, arg ); fflush( fp ); va_end( arg ); } /* * alloc_image( w, h ) * * Allocates an Image structure with a specified width and height * and allocates the memory for an RGB bitmap to fit this size * */ Image * alloc_image( int w, int h ) { Image *image = calloc( sizeof(Image), 1 ); if ( ! image ) return NULL; image->w = w; image->h = h; image->c = 3; /* binocular visual field from "Useful Numbers from Vision Science" */ image->hfov = 200.0; /* binocular horizontal visual field */ image->vfov = 135.0; /* binocular vertical visual field */ image->hoff = 0.0; image->voff = 0.0; image->buffer = calloc( w * h, 3 ); if ( ! image->buffer ) { free( image ); return NULL; } return image; } /* * get_pixel( image, x, y ) * * Returns the RGB pixel at the specified (x,y) pixel coordinate, * or returns NULL if the coordinate is invalid. * */ Pixel * get_pixel( Image *image, int x, int y ) { static Pixel pixel; int offset = ( y * image->w * image->c ) + ( x * image->c ); if ( image == NULL || image->buffer == NULL ) return NULL; if ( x < 0 || x >= image->w ) return NULL; if ( y < 0 || y >= image->h ) return NULL; pixel.r = (float) image->buffer[offset]; pixel.g = (float) image->buffer[offset+1]; pixel.b = (float) image->buffer[offset+2]; return &pixel; } /* * set_pixel( image, x, y, pixel ) * * Takes the specified RGB value and writes it to the pixel at * coordinate (x,y) in the image. No action is taken if an invalid * pixel coordinate or image are specified. * */ void set_pixel( Image *image, int x, int y, Pixel *pixel ) { int offset = ( y * image->w * image->c ) + ( x * image->c ); if ( pixel == NULL || image == NULL || image->buffer == NULL ) return; if ( x < 0 || x >= image->w ) return; if ( y < 0 || y >= image->h ) return; image->buffer[offset] = (uchar) pixel->r; image->buffer[offset+1] = (uchar) pixel->g; image->buffer[offset+2] = (uchar) pixel->b; } /* * read_image( filename ) * * Takes the filename for a binary PPM image, reads this image into * memory, and returns an Image structure all filled out. Returns * NULL if an error was encountered * */ Image * read_image( char *filename ) { FILE *fp = fopen( filename, "r" ); Image *image; char buffer[BUFSIZE]; int w, h; if ( fp == NULL ) { fprintf( stderr, "Couldn't open image %s\n", filename ); return NULL; } fgets( buffer, BUFSIZE, fp ); if ( strcmp( buffer, "P6\n" ) != 0 ) { fprintf( stderr, "File is not a binary PPM %s\n", filename ); fclose( fp ); return NULL; } do { fgets( buffer, BUFSIZE, fp ); } while ( buffer[0] == '#' ); sscanf( buffer, "%d %d", &w, &h ); fgets( buffer, BUFSIZE, fp ); if ( ( image = alloc_image( w, h ) ) == NULL ) { fprintf( stderr, "Couldn't allocate memory for image\n" ); fclose( fp ); return NULL; } if ( fread( image->buffer, 1, w*h*3, fp ) <= 0 ) { fclose( fp ); return NULL; } fclose( fp ); return image; } /* * write_image( image, filename, vel, no_ecc ) * * Takes an Image structure and writes out the RGB bitmap to a * binary PPM image using the specified filename. The vel and no_ecc * parameters are used to create a comment in the PPM header describing * the velocity and eccentricity settings appropriate to this image. * */ void write_image( Image *image, char *filename, float vel, int no_ecc ) { FILE *fp = fopen( filename, "w" ); if ( ! fp ) return; fprintf( fp, "P6\n# percept: vel %g, ecc %s, hfov %g vfov %g", vel, (no_ecc)?"off":"on", image->hfov, image->vfov ); fprintf( fp, ", hoff %g, voff %g\n", image->hoff, image->voff ); fprintf( fp, "%d %d\n255\n", image->w, image->h ); if ( fwrite( image->buffer, 1, image->w * image->h * image->c, fp ) <= 0 ) fprintf( stderr, "Error while writing image!\n" ); fclose( fp ); } /* * get_arg_float( argc, argv, opt, value ) * get_arg_string( argc, argv, opt, value ) * get_arg( argc, argv, opt ) * * A number of functions for parsing command line options. * */ void get_arg_float( int argc, char **argv, char *opt, float *value ) { int i; for ( i = 1; i < argc; i++ ) { if ( strcmp( argv[i], opt ) == 0 && i < argc-1 ) { *value = (float) atof( argv[++i] ); return; } } } void get_arg_string( int argc, char **argv, char *opt, char **value ) { int i; for ( i = 1; i < argc; i++ ) { if ( strcmp( argv[i], opt ) == 0 && i < argc-1 ) { *value = argv[++i]; return; } } } int get_arg( int argc, char **argv, char *opt ) { int i; for ( i = 1; i < argc; i++ ) if ( strcmp( argv[i], opt ) == 0 ) return 1; return 0; } /* * gaussian2d( xvalue, yvalue, xsize, ysize ) * * calculates a 2D Gaussian scaling factor. You specify a location * (xvalue, yvalue) inside an (xsize, ysize) area, and the function * will return a scaling factor from 0 to 1.0, where 1.0 is at the * center of the area. * */ float gaussian2d( float xvalue, float yvalue, float xsize, float ysize ) { float xgauss, ygauss; xsize /= 3.0; ysize /= 3.0; xgauss = (float) exp( -( xvalue * xvalue ) / ( xsize * xsize ) ); ygauss = (float) exp( -( yvalue * yvalue ) / ( ysize * ysize ) ); return xgauss * ygauss; } /* * calc_pixel( image, x, y, deg_size, value ) * * Calculates the RGB value for a pixel at (x, y) given a visibility * limit of deg_size. This degree extent is used to calculate the * size of a Gaussian kernel to apply a low-pass filter at that * pixel location. The result is to blur the pixel by smoothing across * a number of pixels in propotion to the visiblity limit * */ void calc_pixel( Image *image, int x, int y, float deg_size, Pixel *value ) { float xspanf = deg_size * image->w / image->hfov; float yspanf = deg_size * image->h / image->vfov; int xspan = (int) ceil( xspanf / 2.0f ); int yspan = (int) ceil( yspanf / 2.0f ); float total = 0.0, gauss; int i, j; Pixel *pixel; value->r = 0.0f; value->g = 0.0f; value->b = 0.0f; log_debug( "calc_pixel(%d, %d), deg_size = %1.4f, span(%1.1f, %1.1f)\n", x, y, deg_size, xspanf, yspanf ); for ( i = -yspan; i <= yspan; i++ ) { for ( j = -xspan; j <= xspan; j++ ) { pixel = get_pixel( image, x+j, y+i ); if ( ! pixel ) continue; gauss = gaussian2d( (float) j, (float) i, xspanf, yspanf ); pixel->r *= gauss; pixel->g *= gauss; pixel->b *= gauss; total += gauss; value->r += pixel->r; value->g += pixel->g; value->b += pixel->b; } } value->r = min( max( value->r / total, 0.0f ), 255.0f ); value->g = min( max( value->g / total, 0.0f ), 255.0f ); value->b = min( max( value->b / total, 0.0f ), 255.0f ); log_debug( "gaussian(%d, %d; %1.4f deg) gives (%1.0f, %1.0f, %1.0f)\n", x, y, deg_size, value->r, value->g, value->b ); } /* * usage() * * Displays the program usage to stdout and exits * */ void usage( void ) { puts( "Percept V1.0 - Martin Reddy, 30 Dec 2000\n" ); puts( "usage: percept [options]" ); puts( "where," ); puts( " -vfov = vertical field of view (deg)" ); puts( " -hfov = horizontal field of view (deg)" ); puts( " -voff = vertical offset of sweet spot (deg)" ); puts( " -hoff = horizontal offset of sweet spot (deg)" ); puts( " -vel = velocity (deg/s)" ); puts( " -noecc = assume constant eccentricity of 0 deg throughout" ); puts( " -debug = output debug details to percept.log" ); puts( " -o = output ppm filename (default is percept.ppm)" ); puts( "" ); exit( 0 ); } /* * show_percentage( image, x, y ) * * Displays a progress report with a percentage done readout * */ void show_percentage( Image *image, int x, int y ) { static float last_perc = -2.0f; float pixels = (float) image->w * image->h; float perc = 100.0f * ( ( y * image->w ) + x ) / pixels; if ( perc > last_perc + 1.0f ) { fprintf( stderr, "Processing (%1.0f%%)... \r", perc ); last_perc = perc; } } /* * threshold_spatial_frequency( ecc, vel ) * * Calculates the threshold spatial frequency of the human visual * system to a stimulus at a set eccentricity (deg) and moving at a * specified velocity (deg/s). The result is in units of c/deg. * */ float threshold_spatial_frequency( float ecc, float vel ) { float motionSF = 60.0f, cortMag = 1.0f; if ( vel > 0.825f ) motionSF = -27.78f * log10( vel ) + 57.69f; if ( ecc > 5.79f ) { float temp = ( 0.3f * ecc ) + 1.0f; cortMag = 7.49f / ( temp * temp ); } return motionSF * cortMag; } /* * max_size_at_pixel( image, x, y, vel, no_ecc ) * * Calculates the maxium size of stimulus (deg) that can be resolved * at the specified pixel coordinate. A velocity parameter can be * specified, and the eccentricity parameter can be selectively ignored. * */ float max_size_at_pixel( Image *image, int x, int y, float vel, int no_ecc ) { float xps, yps, xdoff, ydoff, ecc, sf; if ( no_ecc ) ecc = 0.0; else { /* calculate the pixel coordinate of the sweet spot */ xps = ( image->w / 2.0f ) + ( image->hoff * image->w ) / image->hfov; yps = ( image->h / 2.0f ) + ( image->voff * image->h ) / image->vfov; /* calculate the degree offset from the sweet spot */ xdoff = ( x - xps ) * image->hfov / image->w; ydoff = ( y - yps ) * image->vfov / image->h; /* calculate the degree distance from the sweet spot (eccentricity) */ ecc = (float) sqrt( ( xdoff * xdoff ) + ( ydoff * ydoff ) ); } /* calculate the threshold spatial frequency at this ecc and vel */ sf = threshold_spatial_frequency( ecc, vel ); log_debug( "pixel(%d, %d), ecc = %1.2f, vel = %1.2f, spatfreq = %1.4f\n", x, y, ecc, vel, sf ); /* return the threshold stimulus size in degrees */ return ( sf == 0.0f ) ? 0.0f : 1.0f / ( sf * 2.0f ); } /* * main( argc, argv ) * * The main program which parses the command line arguments, reads the * input image, applies the perceptually modulated Gaussian blurring, * and outputs the transformed image. * */ int main( int argc, char **argv ) { Image *in_image, *out_image; float vel = 0.0f, size; Pixel pixel; int x, y, no_ecc = 0; char *out_name = "percept.ppm"; /* read the input image */ if ( argc < 2 ) usage(); if ( ( in_image = read_image( argv[1] ) ) == NULL ) { fprintf( stderr, "Could not read the inputs image\n" ); exit( 1 ); } /* parse the user's command line arguments */ if ( get_arg( argc, argv, "-noecc" ) ) no_ecc = 1; if ( get_arg( argc, argv, "-debug" ) ) debug = 1; get_arg_float( argc, argv, "-hfov", &in_image->hfov ); get_arg_float( argc, argv, "-vfov", &in_image->vfov ); get_arg_float( argc, argv, "-hoff", &in_image->hoff ); get_arg_float( argc, argv, "-voff", &in_image->voff ); get_arg_float( argc, argv, "-vel", &vel ); get_arg_string( argc, argv, "-o", &out_name ); if ( get_arg( argc, argv, "-calc" ) ) { float ecc = 0.0, sf; get_arg_float( argc, argv, "-calc", &ecc ); sf = threshold_spatial_frequency( ecc, vel ); printf( "threshold sf at %g deg (%g deg/s) = %g c/deg\n", ecc, vel, sf ); exit( 0 ); } /* create some header debug output */ log_debug( "Debug output for percept utility\n\n" ); log_debug( "image dimensions = %d x %d pixels\n", in_image->w, in_image->h ); log_debug( "field of view = %1.1f x %1.1f deg\n", in_image->hfov, in_image->vfov ); log_debug( "sweet spot offset = %1.1f x %1.1f deg\n", in_image->hoff, in_image->voff ); log_debug( "eccentricity degradation = %s\n", ( no_ecc ) ? "off" : "on" ); log_debug( "global velocity = %1.1f deg/s\n", vel ); log_debug( "pixel frequency = %1.4f x %1.4f c/deg\n\n", in_image->hfov / in_image->w,in_image->vfov / in_image->h ); /* create the output bitmap */ if ( ! ( out_image = alloc_image( in_image->w, in_image->h ) ) ) { fprintf( stderr, "Could not allocate output bitmap\n" ); exit( 1 ); } /* go through each pixel and work out how much we need to blur it */ for ( y = 0; y < in_image->h; y++ ) { for ( x = 0; x < in_image->w; x++ ) { show_percentage( in_image, x, y ); size = max_size_at_pixel( in_image, x, y, vel, no_ecc ); calc_pixel( in_image, x, y, size, &pixel ); set_pixel( out_image, x, y, &pixel ); } } /* close the log file */ log_debug( NULL ); /* write out the processed image */ write_image( out_image, out_name, vel, no_ecc ); printf( "Saved image to %s.\n", out_name ); return 0; } /* EOF: percept.c */