/* pnmsirds.c - transforms a portable anymap to a single image random dot
                stereogram (SIRDS) or a single image stereogram (SIS)
**
** Copyright (C) 1994 by Martin Momberg (momberg@dik.maschinenbau.th-darmstadt.de)
**               1993 by Thimbleby, Inglis and Witten (Basic algorithm)
**               1994 by Claussen and Poepsel, c't (Texture mapping and minor improvements)
**               1990 by Jef Poskanzer (Quantization formula for color to grayscale)
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation.  This software is provided "as is" without express or
** implied warranty.
*/

#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif

#define VERSION "1.0"

/* SIRDS type */
#define blackwhite 0
#define grayscale  1
#define color      2
#define texture    3

#define round(X) (int) ((X) + 0.5)

/* Quantization formula for transforming color to gray pixels
   taken from ppmtopgm */
#define quantize(xel) ( round( 0.299 * PPM_GETR( xel ) + \
                              0.587 * PPM_GETG( xel ) + \
                              0.114 * PPM_GETB( xel ) ) )

/* Include files */
#include "limits.h"
#include "pnm.h"
#include "ppmdraw.h"

/* Prototypes */
static void
makesamearray ARGS(( xel* xelrow, int samearray[], int cols, float mu, int E,
                    int invert, xelval maxval, int format ));
static void
drawcircle ARGS(( xel** xelarray, int cols, int rows, xelval newmaxval,
                 int x, int y, int d, xel* colorxel ));

#if __STDC__
int
main( int argc, char* argv[] )
#else /*__STDC__*/
int
main( argc, argv )
    int argc;
    char* argv[];
#endif /*__STDC__*/
{

      int   argn;                   /* argument index */


      /* File handles for input and texture file */
      FILE* ifp;
      FILE* tfp;

      /* Pixel variables for the source and the SIRDS anymaps */
      xel**  xelarray;
      xel*   xelrow;
      xel    xelcol;
      int    cols, rows, format, newformat;
      xelval maxval, newmaxval;

      /* Commandline parameters */
      float eyedist   = 2.5;        /* eye distance is assumed to be 2.5 inch */
      float mu        = 1/3.0;      /* depth of the field as fraction of the
                                       viewing distance */
      float dpi       = 75.0;       /* picture resolution 75 dot per inch */
      int   render    = blackwhite; /* generate a black&white SIRDS by default */
      int   renderset = FALSE;
      int   allowmag  = FALSE;      /* don't allow magnification of the texture
                                       by default */
      int   complementary = FALSE;  /* don't use the complementary color for the
                                       constrained pixel */
      float condots   = 0.0;        /* set convergence dots diameter to 0
                                       by default */
      int   invert    = FALSE;      /* don't invert the depth values by default */
      int   rescale   = FALSE;      /* don't rescale depth values by default */
      int   seed      = 0;          /* use current time as seed for the
                                       random number generator by default */
      xelval usermaxval = 0;        /* use the default maxval for the
                                       SIRDS anymap */
      int   verbose   = FALSE;      /* be more verbose what is being done */

      /* To check the correctness of usermaxval */
      int   power;

      /* Rescaling */
      xelval infval, supval, currval;
      float scalval;

      /* Convergence dots */
      int   realcondots;
      xel    blackxel;

      /* Some parameters needed in the computation of the SIRDS */
      int   far, near;              /* separation for the far and near planes */

      /* Index variables for loops */
      int   row, col;

      /* The array for the pixel constraints */
      /* Invariant: x <= y <=> same[x] <= y  */
      /* Semantics: the pixel in column x must receive the same color as the pixel in
                    column same[x]. If same[x] == x, the color may be chosen randomly */
      int   *samearray;

      /* Texture variables */
      char  texturefile[FILENAME_MAX]; /* texture pnm filename */
      xel** texturearray;              /* texture anymap */
      int   tcols, trows, tformat;     /* texture parameters */
      xelval tmaxval;
      float texstep;
      int   xtex, ytex;

      /* The usage of pnmsirds */
      char* usage = "[-eyedistance <in>] \n \
                [-dpi <n>] \n \
                [-fielddepth <fraction>] \n \
                [-grayscale | -color [-complementary] | \n \
                 -texture [-allowmag] [-complementary] <pnmfile>] \n \
                [-convergencedots [<diain>]] \n \
                [-invert] \n \
                [-rescale] \n \
                [-seed <n>] \n \
                [-maxval <n>] \n \
                [-verbose] \n \
                [pnmfile]";

      /* ------ Start of code ------ */

      /* Package initialization */
      pnm_init( &argc, argv );

      /* Argument scanning */
      argn = 1;
      while ( argn < argc && argv[argn][0] == '-' && argv[argn][1] != '\0' )
        {
          if ( pm_keymatch( argv[argn], "-eyedistance", 1 ) )
            {
              ++argn;
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%f", &eyedist ) != 1 )
                    pm_error( "Malformed eyedistance value" );
                }
              else
                pm_error( "distance value in inch is missing" );
              if ( eyedist <= 0.0 )
                pm_error( "Eye distance must be greater than 0.0" );
            }
          else if ( pm_keymatch( argv[argn], "-dpi", 1 ) )
            {
              ++argn;
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%f", &dpi ) != 1 )
                    pm_error( "Malformed dpi value" );
                }
              else
                pm_error( "Dpi value is missing" );
              if ( dpi <= 0.0 )
                pm_error( "Dpi must be greater than 0.0" );
            }
          else if ( pm_keymatch( argv[argn], "-fielddepth", 1 ) )
            {
              ++argn;
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%f", &mu ) != 1 )
                    pm_error( "Malformed field depth value" );
                }
              else
                pm_error( "Field depth value is missing" );
              if ( mu <= 0.0 || mu > 1.0 )
                pm_error( "Field depth must be between 0 and 1 (including)" );
            }
          else if ( pm_keymatch( argv[argn], "-grayscale", 1 ) )
            {
              if ( renderset )
                pm_error( "You must choose either grayscale, color, texture, or nothing" );
              render = grayscale;
              renderset = TRUE;
            }
          else if ( pm_keymatch( argv[argn], "-color", 3 ) )
            {
              if ( renderset )
                pm_error( "You must choose either grayscale, color, texture, or nothing" );
              render = color;
              renderset = TRUE;
              if ( argn < argc - 1 && pm_keymatch( argv[argn + 1], "-complementary", 3 ) )
                {
                  ++argn;
                  complementary = TRUE;
                }
            }
          else if ( pm_keymatch( argv[argn], "-texture", 1 ) )
            {
              if ( renderset )
                pm_error( "You must choose either grayscale, color, texture, or nothing" );
              render = texture;
              renderset = TRUE;
              ++argn;
              if ( argn < argc && pm_keymatch( argv[argn], "-allowmag", 1 ) )
                {
                  allowmag = TRUE;
                  ++argn;
                }
              if ( argn < argc && pm_keymatch( argv[argn], "-complementary", 3 ) )
                {
                  complementary = TRUE;
                  ++argn;
                }
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%s", texturefile ) != 1 )
                    pm_error( "Malformed texture filename" );
                }
              else
                pm_error( "Texture filename is missing" );
            }
          else if ( pm_keymatch( argv[argn], "-convergencedots", 3 ) )
            {
              if ( argn < argc - 1 &&
                  ( argv[argn + 1][0] >= '0' && argv[argn + 1][0] <= '9' ||
                  argv[argn + 1][0] == '.' ) )
                {
                  ++argn;
                  if ( sscanf( argv[argn], "%f", &condots ) != 1 )
                    pm_error( "Malformed condots diameter value" );
                  if ( condots <= 0.0 )
                    pm_error( "Convergence dots diameter must be greater than 0.0" );
                }
              else
                condots = 0.2;
            }
          else if ( pm_keymatch( argv[argn], "-invert", 1 ) )
            invert = TRUE;
          else if ( pm_keymatch( argv[argn], "-rescale", 1 ) )
            rescale = TRUE;
          else if ( pm_keymatch( argv[argn], "-seed", 1 ) )
            {
              ++argn;
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%d", &seed ) != 1 )
                    pm_error( "Malformed seed value" );
                }
              else
                pm_error( "Seed value is missing" );
              if ( seed <= 0 )
                pm_error( "Seed must be greater than 0" );
            }
          else if ( pm_keymatch( argv[argn], "-maxval", 1 ) )
            {
              ++argn;
              if ( argn < argc )
                {
                  if ( sscanf( argv[argn], "%hd", &usermaxval ) != 1 )
                    pm_error( "Malformed maxval value" );
                }
              else
                pm_error( "Maxval value is missing" );
              if ( usermaxval <= 0 )
                pm_error( "Maxval must be greater than 0" );
              power = 2;
              while ( power < INT_MAX / 2 && power - 1 != usermaxval ) power *= 2;
              if ( power >= INT_MAX / 2 && power - 1 != usermaxval )
                pm_error( "Maxval must be equal to a power of two minus 1" );
            }
          else if ( pm_keymatch( argv[argn], "-verbose", 1 ) )
            verbose = TRUE;
          else
            pm_usage( usage );
          ++argn;
        }

      /* Get source file or read from stdin */
      if ( argn != argc )
        {
          ifp = pm_openr( argv[argn] );
          ++argn;
        }
      else
        ifp = stdin;

      if ( argn != argc ) pm_usage( usage );

      if ( verbose ) pm_message( "Version %s", VERSION );

      /* Complementary colors don't work with the current algorithm
         I will fix it as soon as I get the correct algorithm */
      if ( complementary )
        {
          pm_message( "Using complementary colors isn't implemented yet." );
          pm_error( "It will be included in the next version." );
        }

      /* Read source anymap */
      pnm_pbmmaxval = PNM_MAXMAXVAL;  /* use larger value for better results */

      if ( verbose ) pm_message( "Reading source anymap..." );

      xelarray = pnm_readpnm( ifp, &cols, &rows, &maxval, &format );

      if ( verbose )
        pm_message( "with %d cols, %d rows, %d as maxval and %2c as format",
                   cols, rows, maxval, format);

      /* Seems to result in an error
         pm_close( ifp );
         */

      /* Read texture if available */
      if ( render == texture )
        {
          /* If seed mentioned in the parameter list, give a hint to that */
          if ( seed > 0 )
            pm_message( "Choosing a seed is meaningless, if a texture is used" );

          tfp = pm_openr( texturefile );

          if ( verbose ) pm_message( "Reading the texture...");

          texturearray = pnm_readpnm( tfp, &tcols, &trows, &tmaxval, &tformat );

          if ( verbose )
            pm_message( "with %d cols, %d rows, %d as maxval and %2c as format",
                       tcols, trows, tmaxval, tformat);

          /* Seems to result in an error
             pm_close( tfp );
             */
        }

      /* Rescale soure image, if desired, to get the full depth impression */
      /* First, get the minimal and maximal depth values of the source */
      if ( rescale )
        {
          if ( verbose ) pm_message( "Rescaling: getting min/max..." );

          supval = 0; infval = maxval;
          for ( row = 0; row < rows; ++row )
            for (col = 0; col < cols; ++col )
              {
                xelcol = xelarray[row][col];
                if ( PNM_FORMAT_TYPE(format) == PPM_TYPE )
                  currval = quantize( xelcol );
                else
                  currval = PNM_GET1( xelcol );

                if ( currval < infval ) infval = currval;
                if ( currval > supval ) supval = currval;
              }

          /* Second, rescale the source image, putting a colored image to grayscale */

          if ( verbose ) pm_message( "Rescaling: min = %d, max = %d...",
                                    infval, supval );

          scalval = ( float ) maxval / ( supval - infval );
          for ( row = 0; row < rows; ++row )
            for (col = 0; col < cols; ++col )
              {
                xelcol = xelarray[row][col];
                if ( PNM_FORMAT_TYPE(format) == PPM_TYPE )
                  {
                    currval = round( scalval * quantize( xelcol ) - infval );
                    PPM_ASSIGN( xelarray[row][col],
                               currval,
                               currval,
                               currval );
                  }
                else
                  PNM_ASSIGN1( xelarray[row][col],
                              round( scalval * PNM_GET1( xelcol ) - infval ) );
              }
        }

      /* Define the parameters of the SIRDS anymap */
      switch ( render )
        {
        case blackwhite:
          newformat = PBM_TYPE;
          newmaxval = ( usermaxval > 0 ? usermaxval : PNM_MAXMAXVAL );
          if ( verbose )
            pm_message( "Constructing a black and white SIRDS with maxval %d",
                       newmaxval );
          break;
        case grayscale:
          newformat = PGM_TYPE;
          newmaxval = ( usermaxval > 0 ? usermaxval : PGM_MAXMAXVAL );
          if ( verbose )
            pm_message( "Constructing a grayscale SIRDS with maxval %d",
                       newmaxval );
          break;
        case color:
          newformat = PPM_TYPE;
          newmaxval = ( usermaxval > 0 ? usermaxval : PPM_MAXMAXVAL );
          if ( verbose )
            pm_message( "Constructing a color SIRDS with maxval %d",
                       newmaxval );
          break;
        case texture:
          newformat = tformat;
          newmaxval = tmaxval;
          if ( verbose )
            pm_message( "Constructing a textured SIS with maxval %d",
                       newmaxval );
          break;
        }

      /* Alloc memory for the samearray */
      if ( ( samearray = calloc( cols, sizeof( int ) ) ) == NULL )
        pm_error( "Can't allocate memory for the samearray." );

      /* Randomize the random number generator */
      if ( seed == 0 )
        srandom( time( NULL ) );
      else
        srandom( seed );

      /* Calculate some parameters and write them out, if desired */
      far = round( eyedist * dpi * 0.5 );
      near = round( ( 1.0 - mu ) * eyedist * dpi / ( 2.0 - mu ) );

      if ( verbose )
        {
          pm_message( "Far plane separation   = %d pixels", far );
          pm_message( "Near plane separation  = %d pixels", near );
          pm_message( "Number of depth planes = %d", far - near + 1 );
          pm_message( "The distance between near and far plane is 1/%f of", 1.0 / mu);
          pm_message( "the distance between eyes and image or image and far plane.");
        }

      /* Do what has to be done */

      if ( verbose ) pm_message( "Processing Image" );

      for ( row = 0; row < rows; ++row )
        {
          /* Not very clean because I didn't use pm_message, I know :-) */
          if ( verbose )
            if ( row % 10 == 0 )
              fprintf( stderr, "\rpnmsirds: Processing row %4d", row );

          xelrow = xelarray[row];

          /* Compute the constraints for the row pixel colors */
          makesamearray( xelrow, samearray, cols, mu, round( eyedist * dpi ),
                        invert, maxval, format );

          /* Choose the pixel colors randomly, if they are not constrained
             by another pixel's color, take the color of the latter, otherwise
             The texture mapping algorithm was taken from
             Claussen, Ute ; Poepsel, Josef: Im Rausch der Tiefe. In:
             c't - Magazin fuer Computertechnik, Hannover, Germany : Heise Verlag,
             July 1994. */
          switch ( render )
            {
            case blackwhite:
              for ( col = cols - 1; col >= 0; --col)
                {
                  if ( samearray[col] == col )
                    PNM_ASSIGN1( xelrow[col], random() & 1 );
                  else
                    PNM_ASSIGN1( xelrow[col],
                               PNM_GET1( xelrow[ samearray[ col ] ] ));
                }
              break;
            case grayscale:
              for ( col = cols - 1; col >= 0; --col)
                {
                  if ( samearray[col] == col )
                    PNM_ASSIGN1( xelrow[col], random() & newmaxval );
                  else
                    PNM_ASSIGN1( xelrow[col],
                               PNM_GET1( xelrow[ samearray[ col ] ] ));
                }
              break;
            case color:
              for ( col = cols - 1; col >= 0; --col)
                {
                  if ( samearray[col] == col )
                    PPM_ASSIGN( xelrow[col],
                               random() & newmaxval,
                               random() & newmaxval,
                               random() & newmaxval );
                  else
                    {
                      xelcol = xelrow[ samearray[ col ] ];
                      if ( complementary )
                        PPM_ASSIGN( xelrow[col],
                                   newmaxval - PPM_GETR( xelcol ),
                                   newmaxval - PPM_GETG( xelcol ),
                                   newmaxval - PPM_GETB( xelcol ) );
                      else
                        PPM_ASSIGN( xelrow[col],
                                   PPM_GETR( xelcol ),
                                   PPM_GETG( xelcol ),
                                   PPM_GETB( xelcol ) );
                    }
                }
              break;
            case texture:
              texstep = tcols / far;
              if ( ! allowmag ) if ( texstep < 1.0 ) texstep = 1.0;
              ytex = round( row * texstep ) % trows;
              for ( col = cols - 1; col >= 0; --col)
                {
                  if ( samearray[col] == col )
                    {
                      xtex = round( col * texstep ) % tcols;
                      xelcol = texturearray[ytex][xtex];
                      if ( PNM_FORMAT_TYPE(newformat) == PPM_TYPE )
                        PPM_ASSIGN( xelrow[col],
                                   PPM_GETR( xelcol ),
                                   PPM_GETG( xelcol ),
                                   PPM_GETB( xelcol ) );
                      else
                        PNM_ASSIGN1( xelrow[col],
                                   PNM_GET1( xelcol ));
                    }
                  else
                    {
                      xelcol = xelrow[ samearray[ col ] ];
                      if ( PNM_FORMAT_TYPE(newformat) == PPM_TYPE )
                        if ( complementary )
                          PPM_ASSIGN( xelrow[col],
                                     newmaxval - PPM_GETR( xelcol ),
                                     newmaxval - PPM_GETG( xelcol ),
                                     newmaxval - PPM_GETB( xelcol ) );
                        else
                          PPM_ASSIGN( xelrow[col],
                                     PPM_GETR( xelcol ),
                                     PPM_GETG( xelcol ),
                                     PPM_GETB( xelcol ) );
                      else
                        PNM_ASSIGN1( xelrow[col],
                                   PNM_GET1( xelcol ));
                    }
                }
              break;
            }
        }

      /* Same as above :-/ */
      if ( verbose ) fprintf( stderr, "\r" );

      /* Draw convergence dots for faster adaption of the eyes */
      if ( condots > 0.0 )
        {
          realcondots = round( condots * dpi < rows / 12 ? condots * dpi :
                              rows / 12 );

          if ( verbose ) pm_message( "Drawing convergence dots with %f inch diameter", realcondots / dpi);

          blackxel = pnm_blackxel( newmaxval, newformat );

          drawcircle( xelarray, cols, rows, newmaxval,
                     cols / 2 - far / 2, rows * 19 / 20, realcondots, &blackxel );
          drawcircle( xelarray, cols, rows, newmaxval,
                     cols / 2 + far / 2, rows * 19 / 20, realcondots, &blackxel );

        }

      /* Write the SIRDS anymap */
      if ( verbose ) pm_message( "Writing SIRDS anymap..." );
      pnm_writepnm( stdout, xelarray, cols, rows, newmaxval, newformat, 0 );
      pm_close( stdout );
      if ( verbose )
        pm_message( "with %d cols, %d rows, %d as maxval and %2c as format.",
                   cols, rows, newmaxval, newformat);

      /* Free memory */
      pnm_freearray( xelarray, rows );
      if ( render == texture ) pnm_freearray( texturearray, trows );
      free( samearray );

      exit( 0 );
    }

/* This algorithm uses the depth information stored as grayscale values in the
   xelrow and computes from it the pixel constraints that lead to the stereoscopic
   impression.
   The algorithm was taken from

   Thimbleby, Harold W. ; Inglis, Stuart ; Witten, Ian H.:
     Displaying 3D Images: Algorithms for Single Image Random Dot Stereograms

   available as katz.anu.edu.au:/pub/stereograms/papers/SIRDS-paper.ps.Z.
   Some minor improvements of the algorithm concerning the different ranges of
   the depth values in the original algorithm and in the pnm format are taken from
   Claussen, Ute ; Poepsel, Josef: Im Rausch der Tiefe. In:
   c't - Magazin fuer Computertechnik, Hannover, Germany : Heise Verlag,
   July 1994.
   The algorithm was intentionally left uncommented: the best description is contained
   the former mentioned articles.

   The original algorithm contained a tiny bug (or is it a feature ;-) )
   which leads to leaving a pixel unconstrained that should be constrained.
   The bug is fixed below. (Thanks to Marcus Reigl and Robert Graeb).
*/
#if __STDC__
static void
makesamearray( xel* xelrow, int samearray[], int cols, float mu, int E,
              int invert, xelval maxval, int format )
#else /*__STDC__*/
static void
makesamearray( xelrow, samearray, cols, mu, E, invert, maxval, format )
     xel* xelrow;
     int  samearray[];
     int  cols;
     float mu;
     int  E;
     int  invert;
     xelval maxval;
     int  format;
#endif /*__STDC__*/
{
  xel  xelcol;
  int  col;
  int  left, right, l;
  int  s, olds;
  int  t, zt;
  int  visible;
  int  Zorg, Zorgl, Zorgr;
  float Z;
  float ft;

  float sh;

  for ( col = 0; col < cols; ++col )
    samearray[col] = col;

  /* See comment below */
  olds = 0;

  ft = 2 * maxval / mu / E;

  for ( col = 0; col < cols; ++col )
    {
      xelcol = xelrow[col];
      if ( PNM_FORMAT_TYPE(format) == PPM_TYPE )
        Zorg = quantize( xelcol );
      else
        Zorg = PNM_GET1( xelcol );

      if ( invert ) Zorg = maxval - Zorg;

      Z = ( ( float ) Zorg ) / maxval;

      s = round( ( 1.0 - mu * Z ) * E / (  2.0 - mu * Z ) );

      left = col - s / 2; right = left + s;

      if ( 0 <= left && right < cols )
        {
         t = 1;
         do {
           zt = Zorg + round( ( 2.0 - mu * Z) * t * ft );

           xelcol = xelrow[col - t];
           if ( PNM_FORMAT_TYPE(format) == PPM_TYPE )
             Zorgl = quantize( xelcol );
           else
             Zorgl = PNM_GET1( xelcol );

           if ( invert ) Zorgl = maxval - Zorgl;

           visible = Zorgl < zt;
           if ( visible )
             {
               xelcol = xelrow[col + t];
               if ( PNM_FORMAT_TYPE(format) == PPM_TYPE )
                 Zorgr = quantize( xelcol );
               else
                 Zorgr = PNM_GET1( xelcol );

               if ( invert ) Zorgr = maxval - Zorgr;

               visible = Zorgr < zt;
             }

           t++;
         } while ( visible && zt < maxval );
         if ( visible )
           {
             /* if s has decreased by 1 from even to odd,
                left will increase by 2 which leads to
                an unconstrained pixel yielding image errors
                especially in textured SIRDS. Therefore we
                choose a dependance of left - 1 on left which
                corresponds to the expected separation for
                left - 1. */
             if ( s == olds - 1 && (olds / 2) * 2 == olds )
               samearray[left - 1] = left;
             olds = s;

             l = samearray[left];
             while ( l != left && l != right )
               if (l < left )
                 {
                   left = l;
                   l = samearray[left];
                 }
               else
                 {
                   samearray[left] = right;
                   left = right;
                   l = samearray[left];
                   right = l;
                 }
             samearray[left] = right;
           }
       }
    }
  return;
}

/* This routine currently draws a circle
   using the ppmdraw routines */
#if __STDC__
static void
drawcircle( xel** xelarray, int cols, int rows, xelval maxval,
           int x, int y, int d, xel* colorxel )
#else /*__STDC__*/
static void
drawcircle( xelarray, cols, rows, maxval, x, y, d, colorxel )
     xel** xelarray;
     int   cols;
     int   rows;
     xelval maxval;
     int   x;
     int   y;
     int   d;
     xel*  colorxel;
#endif /*__STDC__*/
{
  char* fillhandle;

  fillhandle = ppmd_fill_init();
  ppmd_circle( xelarray, cols, rows, maxval, x, y, round( d / 2 ), ppmd_fill_drawproc,
              fillhandle );
  ppmd_fill( xelarray, cols, rows, maxval, fillhandle, PPMD_NULLDRAWPROC,
            ( char* ) colorxel );

  return;
}


