Commits

Michele Bini committed 77c38dc

Initial commit.

Comments (0)

Files changed (1)

+#!/usr/bin/perl -w
+
+# Copyright (c) 2009, 2010, 2012 by Michele Bini <michele.bini@gmail.com>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the version 3 of the GNU General Public License
+# as published by the Free Software Foundation.
+
+# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use strict;
+BEGIN {
+  push @INC, "$ENV{HOME}/dev/perl/";
+}
+package PhotoCrop;
+
+# Functional interface
+
+# Objects:
+# Image I<w, h> with w and h respectively as the horizontal and vertical dimensions.
+# r: represents the image radius in pixels, the length of the radius of a circle enclosing the image; r <= ((w/2)^2 + (h/2)^2)
+# r is used as the unit for distances
+
+# Traditional (infinite field) crop: C:<x, y> {D<x, y>| s, b}
+# C_x is the horizontal offset of the crop center from the image center, in s units
+# C_y is respectivel the vertical offset,
+# D<w, h> the new image dimension in r units
+# s: is the radius of the output image
+# b: is the aspect ratio of the output image: width in pixels/width in height
+
+# Traditional crop corrected: C, s, b, {O, d|T}
+# d is the field depth in s units, the distance between the camera and the image plane, may be expressed accurately as atan(d)/(atan(1)*2) with rage (0..1)
+# O<x, y> is the offset of the image from the focal center
+# T is a 3D vector <O_x, O_y, d>
+
+# Photocrop: K, s, b, T
+# K is a quaternion representing the 3d rotation of the camera
+
+# TODO:
+# * add angle measurements
+# * a one-pixel offset was detected in a picture between the photocrop window and the povray-rendered image
+# * grid for correct perspective adjustment
+
+use Math::Quat qw(i j k);
+
+# apt-get install libsdl-perl libimage-info-perl
+# photocrop dscf0008.jpg --debug --delay 10 --size 800x600 --angle "0.3 rad" --nometa
+# photocrop im002382.jpg --debug --delay 10 --size 320x200 --angle 60deg --nometa --rotation -56.88deg --tilt -14.45deg --turn -9.89deg --viewangley 33deg
+
+use SDL::OpenGL;
+
+sub PhotoCrop::Texture::new {
+  my ($p, $w, $h, $d) = @_;
+  my $textures = glGenTextures(1);
+  die "Could not generate textures" unless $textures->[0];
+  glBindTexture(GL_TEXTURE_2D, $textures->[0]);
+  glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+  glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+  glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+  glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+  #vec(substr($d, int(rand(length($d))), 1), 0, 8) ^= int(rand(0x100)) for (0..5000);
+  glTexImage2D(GL_TEXTURE_2D, 0, 3, $w, $h, 0, GL_RGB, GL_UNSIGNED_BYTE, $d);
+  die "Problem setting up 2D texture" if glGetError();
+  #die "Problem setting up 2d Texture (dimensions not a power of 2?)):".glErrorString(glGetError())."\n" if glGetError();
+  warn length($d) . " bytes" if $PhotoCrop::debug;
+  warn "texture: " . $textures->[0] if $PhotoCrop::debug;
+  bless [ $textures->[0] ], $p
+}
+
+sub PhotoCrop::Texture::nr { shift->[0] }
+
+use Image::Info qw(image_info);
+
+sub PhotoCrop::Photo::new {
+  my $i = shift;
+  my $fn = shift;
+  my %o = @_;
+  my ($ax, $ay) = $o{nometadata} ? () : do {
+    my @i = image_info($fn);
+    my %i = %{ $i[0] };
+    do { use Data::Dumper qw(Dumper); print STDERR Dumper(@i) if $o{verbose} };
+    my ($h, $w, $l, $u, $x, $y) = @i{qw(height width FocalLength FocalPlaneResolutionUnit FocalPlaneXResolution FocalPlaneYResolution)};
+    if (!defined($u)) {
+      warn "FocalPlaneResolutionUnit not defined";
+    } elsif ($u ne 'dpcm') {
+      warn "FocalPlaneResolutionUnit not supported: $u";
+    } else {
+      warn "x: atan(($w/$x)/2*$l) y: atan(($h/$y)/2*$l)";
+      no integer;
+      2 * atan2(($w/$x), 2*$l), # should be 2 * atan(d/(2*l))
+      2 * atan2(($h/$y), 2*$l)
+    }
+  };
+  warn "detected angle of vision on the x dimension: $ax rad" if defined($ax);
+  warn "detected angle of vision on the y dimension: $ay rad" if defined($ay);
+  if (defined($ax) && defined $ay) {
+    my $sx = $o{scaledetectedangle} || $o{scaledetectedanglex};
+    my $sy = $o{scaledetectedangle} || $o{scaledetectedangley};
+    $ax *= $sx if $sx; $ay *= $sy if $sy;
+    #my $aov = ($ax > $ay) ? $ax : $ay;
+    $o{angle} = ($ax > $ay) ? $ax : $ay;
+  } else {
+    $o{angle} = 2*atan2(1,2);
+  }
+  # $o{aov} = [ $ax, $ay ] if defined($ax) && defined($ay);
+  $o{filename} = $fn;
+  bless \%o, $i
+}
+
+sub splitpixels {
+  my ($pixels, $s, $fillpix) = @_;
+  my @r;
+  my $i = 0;
+  my $l = length($pixels);
+  my $n;
+  while (($n = $l - $i) > 0) {
+    if ($n < $s) {
+      push @r, substr($pixels, $i, $n) .
+      ($fillpix x (($s - $n)/length($fillpix)));
+      last;
+    } else {
+      push @r, substr($pixels, $i, $s);
+      $i += $s;
+    }
+  }
+  #warn int(@r) . " parts" if $PhotoCrop::debug;
+  @r
+}
+
+sub splithpixels {
+  my ($pixels, $pitch, $s, $fillpix) = @_;
+  my @rows = map { [ splitpixels($_, $s, $fillpix) ] } splitpixels($pixels, $pitch, $fillpix);
+  my @cols = map { [ ] } @{ $rows[0] };
+  my $r;
+  while (defined($r = pop @rows)) {
+    for (@cols) {
+      push @$_, pop(@$r);
+    }
+  }
+  map { join('', @$_) } @cols
+}
+
+sub pickrand { $_[int(rand(int(@_)))] }
+
+use POSIX qw(tan fmod);
+
+my $pi2 = atan2(1, 0)*4;
+my $rad_per_deg = atan2(1,1)/45.0;
+my $deg_per_rad = 45.0/atan2(1,1);
+
+sub rad2deg { no integer; shift()*$deg_per_rad }
+sub deg2rad { no integer; shift()*$rad_per_deg }
+sub deg2deg { fmod(shift(), 360) }
+sub rad2rad { fmod(shift(), $pi2) }
+
+sub canvas {
+  my $i = shift;
+  
+  my ($ax, $ay) = defined($i->{aov}) ? @{ $i->{aov} } : ();
+  if (!defined($ax) || !defined($ay)) {
+    no integer;
+    my ($w, $h) = ($i->{width}, $i->{height});
+    my ($a1, $a2, $d1, $d2) = ($w > $h) ?
+    (\$ay, \$ax, $h, $w) :
+    (\$ax, \$ay, $w, $h);
+    $$a1 = $i->{angle};
+    $$a2 = 2*atan2(tan($$a1/2)*($d2/$d1), 1);
+    $i->{aov} = [ $ax, $ay ];
+  }
+
+  map { tan($_/2) } $ax, $ay;
+}
+
+sub PhotoCrop::Photo::gldraw {
+  my $i = shift;
+  my $notile = $i->{notile};
+  my $texs = $i->{texs} ||= $notile || do {
+    warn "Loading image" if $PhotoCrop::debug;
+    my $surf = SDL::Surface->new("-name" => $i->{filename});
+    die "Could not open file: $i->{filename}" unless defined $surf;
+    my ($width, $height, $pitch, $bytespp, $pixels) =
+    ($surf->width, $surf->height, $surf->pitch,
+     $surf->bytes_per_pixel, $surf->pixels);
+    $i->{width} = $width;
+    $i->{height} = $height;
+    #use Data::Dumper; warn "pixels: " . Data::Dumper::Dumper($pixels);
+    warn "splitting pixels into textures" if $PhotoCrop::debug;
+    my $texdim = $i->{texdim} ||= 256;
+    my $filler = "\x00" x $bytespp;
+    [
+    map {
+       [ reverse map { PhotoCrop::Texture->new($texdim, $texdim, $_) } splithpixels($_, $pitch, $texdim*$bytespp, $filler) ]
+     } splitpixels($pixels, $texdim*$pitch, $filler) ]
+  };
+  my $tex;
+  $tex = $i->{tex} ||= do {
+    warn "Loading image" if $PhotoCrop::debug;
+    my $surf = SDL::Surface->new("-name" => $i->{filename});
+    my ($width, $height, $pitch, $bytespp, $pixels) =
+    ($surf->width, $surf->height, $surf->pitch,
+     $surf->bytes_per_pixel, $surf->pixels);
+    $i->{width} = $width;
+    $i->{height} = $height;
+    warn "Internal consistency error: width=$width, pitch=$pitch, bytespp=$bytespp" if ($width != $pitch / $bytespp);
+    PhotoCrop::Texture->new($width, $height, $pixels)
+  } if $notile;
+  if (0) {
+    use bytes;
+    my $s = 16;
+    my $d = "\x00" x (3 * $s * $s);
+    my ($x, $y);
+    for $x (0..$s-1) {
+      for $y (0..$s-1) {
+	vec(substr($d, ($x+$y*$s)*3, 1), 0, 8) = $x*(256/$s);
+	vec(substr($d, ($x+$y*$s)*3+1, 1), 0, 8) = $y*(256/$s);
+      }
+    }
+    my $textures = glGenTextures(1);
+    die "Could not generate textures" unless $textures->[0];
+    glBindTexture(GL_TEXTURE_2D, $textures->[0]);
+    glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+    glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
+    glTexParameter(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
+    #vec(substr($d, int(rand(length($d))), 1), 0, 8) ^= int(rand(0x100)) for (0..5000);
+    #vec(substr($d, int(rand(length($d))), 1), 0, 8) ^= int(rand(0x100)) for (0..5000);
+    glTexImage2D(GL_TEXTURE_2D, 0, 3, $s, $s, 0, GL_RGB, GL_UNSIGNED_BYTE, $d);
+    $textures->[0];
+  };
+  #glTranslate(0,0,-5);
+  glBindTexture(GL_TEXTURE_2D, $notile? $tex->nr : pickrand(@{ pickrand(@$texs) })->nr);
+  #warn "tex: $tex";
+  #glBindTexture(GL_TEXTURE_2D, $tex);
+
+  my ($bx, $by) = canvas($i);
+  if (!$notile) {
+    my $texdim = 256;
+    # FIXME: this isn't the correct approach since border pixels are split
+    my $pw = $i->{width}; # Image width in pixels
+    my $ph = $i->{height}; # Image height in pixels
+    # Should take into account the following:
+    # -sx +sx : extents in world coordinates
+    # pw: image dimensions in pixels
+    no integer;
+    my $sy = $by;
+    my $y = $ph;
+    map {
+      my $ly = $by*((($y-$texdim)*2 - $ph) / $ph);
+      my $sx = -$bx;
+      my $x = 0;
+      map {
+	my $lx = $bx*((($x+$texdim)*2 - $pw) / $pw);
+	glBindTexture(GL_TEXTURE_2D, $_->nr);
+	glBegin(GL_QUADS);
+	glTexCoord(0.0, 1.0); glVertex($sx, $sy, -1);
+	glTexCoord(1.0, 1.0); glVertex($lx, $sy, -1);
+	glTexCoord(1.0, 0.0); glVertex($lx, $ly, -1);
+	glTexCoord(0.0, 0.0); glVertex($sx, $ly, -1);
+	glEnd;
+	$sx = $lx; $x+=$texdim;
+      } @$_;
+      $sy = $ly; $y-=$texdim;
+    } @$texs;
+  } else {
+    my ($w, $h) = @$i{qw(width height)};
+    # Ditto
+    glBegin(GL_QUADS);
+    glTexCoord(0.0, 1.0); glVertex(-$bx, -$by, -1);
+    glTexCoord(1.0, 1.0); glVertex(+$bx, -$by, -1);
+    glTexCoord(1.0, 0.0); glVertex(+$bx, +$by, -1);
+    glTexCoord(0.0, 0.0); glVertex(-$bx, +$by, -1);
+    glEnd;
+  }
+}
+
+sub PhotoCrop::Photo::angle {
+  my ($i, $a) = @_;
+  my $o = $i->{angle};
+  if (defined $a) {
+    $i->{angle} = $a;
+    warn "Here $a!" if $PhotoCrop::debug;
+    delete $i->{aov};
+  }
+  $o
+}
+
+sub PhotoCrop::KeyControl::new { bless { p => {} }, shift }
+
+sub PhotoCrop::KeyControl::keyup {
+  my ($i, $k) = @_;
+  my $a = $i->{"u$k"};
+  delete $i->{p}->{$k};
+  $a->() if defined $a;
+}
+
+sub PhotoCrop::KeyControl::keydown {
+  my ($i, $k) = @_;
+  my $a = $i->{"d$k"};
+  $a->() if defined $a;
+  $i->{p}->{$k} = 1;
+}
+
+sub PhotoCrop::KeyControl::keyaction {
+  my ($i, $k, $a) = @_;
+  $i->{"d$k"} = $a
+}
+
+sub PhotoCrop::KeyControl::frame {
+  my $i = shift;
+  my $q;
+  for (keys %{ $i->{p} }) {
+    if (defined($q = $i->{"f$_"})) {
+      $q->();
+    }
+  }
+}
+
+sub PhotoCrop::KeyControl::needanim { %{ shift->{p} } && 1 }
+
+sub PhotoCrop::KeyControl::control {
+  my ($i, $step, $p, $n, $a, %o) = @_;
+  my $control = 0.0;
+  my $scale = $step;
+  my $geom = ($o{geom} || 1.1);
+  my $x;
+  $x = sub { $control+=$scale; $control = $a->($control) };
+  $i->{"d$_"} = $x for @$p;
+  $x = sub { $control+=$scale; $scale+=$step; $scale *= $geom; $control = $a->($control) };
+  $i->{"f$_"} = $x for @$p;
+  $x = sub { $scale = $step };
+  $i->{"u$_"} = $x for @$p;
+  $x = sub { $control-=$scale; $control = $a->($control) };
+  $i->{"d$_"} = $x for @$n;
+  $x = sub { $control-=$scale; $scale+=$step; $scale *= $geom; $control = $a->($control) };
+  $i->{"f$_"} = $x for @$n;
+  $x = sub { $scale = $step; $step = $step; };
+  $i->{"u$_"} = $x for @$n;
+}
+
+
+use strict;
+use Getopt::Long qw(GetOptions);
+
+my $VERSION = "0.0.101";
+
+use SDL::App;
+use SDL::OpenGL;
+use SDL::Event;
+use SDL::Constants;
+
+my $scaledetectedangle;
+my $scaledetectedanglex;
+my $scaledetectedangley;
+my $screenw = 640;
+my $screenh = 480;
+my $delay = 50;
+my $fullscreen;
+my $verbose;
+my $angle;
+my $nometa;
+my $texdim;
+my $noscreenresize;
+my $notile;
+my $viewangle = 30;
+#my $viewangley = 30;
+#my $viewrotation; # unsupported at the moment
+my $turn = 0;
+my $tilt = 0;
+my $rotation = 0;
+my $viewrotation = 0;
+my $savebmp;
+my $savepovray;
+my $scaleup=2;
+my $savepng;
+my $rungimp;
+my $crosshair = 1;
+our $debug = 0;
+die "Unrecognized option. Try $0 --help for more information" unless
+GetOptions
+"help" => sub {
+  print STDERR
+      "photocrop - Perspective-correct photo cropping
+
+$0 [--size <width>x<height>] [--fullscreen] [--verbose] [--debug] image.jpg\n  --delay <ms> - time delay for each frame (1/1000 of a second)\n
+
+Vieving options:
+  --angle ANGLE
+  --viewangle ANGLE      initial angle of perspective view
+  --turn ANGLE           initial right-side turn
+  --tilt ANGLE           initial up-tilt
+  --rotation ANGLE       initial image rotation
+  --viewrotation ANGLE   view rotation
+  --savebmp FILE         Output a screenshot to FILE before exiting
+  --savepovray FILE      Output a povray scene to FILE of the current view before exiting
+  --scaledetectedangle RATIO   # Scale detected aperture angle by RATIO
+  --scaledetectedanglex RATIO  # Same for the X coordinate
+  --scaledetectedangley RATIO  # Same for the Y coordinate
+  ANGLE: <decimal-number> <deg|rad>
+
+Display:
+  --fullscreen
+
+Output:
+  --scaleup X       scales up X times the image rendered by povray
+  --savepng FILE    save to file the view rendered with povray
+  --savebmp  FILE   save displayed image to file on exit (low quality,not recommended)
+  --savepovray FILE  save to povray file on exit
+
+Interactive options
+  --[no-]crosshair   [de]activate a blinking, angle measuring crosshair
+
+Program parameters:
+  --screensize WxH   set window/screen size
+  --noscreenresize   Don't allow window resize
+  --rungimp          run gimp on generated file
+  --delay MS         delay frames MS milliseconds
+  --notile           do not tile main texture!
+  --texdim S         use size S for texture tiles (future option)
+  --verbose
+  --debug
+";
+},
+"scaledetectedanglex=s" => \$scaledetectedanglex,
+"scaledetectedangley=s" => \$scaledetectedangley,
+"scaledetectedangle=s" => \$scaledetectedangle,
+"rungimp!" => \$rungimp,
+"scaleup=s" => \$scaleup,
+"savepng=s" => \$savepng,
+"savepovray=s" => \$savepovray,
+"savebmp=s" => \$savebmp,
+"viewangle=s" => \$viewangle,
+"turn=s" => \$turn,
+"tilt=s" => \$tilt,
+"rotation=s" => \$rotation,
+"viewrotation=s" => \$viewrotation,
+"angle=s" => \$angle,
+"delay=n" => \$delay,
+"fullscreen!" => \$fullscreen,
+"verbose!" => \$verbose,
+"nometa!" => \$nometa,
+"texdim=s" => \$texdim,
+"screensize=s" => sub {
+  ($screenw, $screenh) = ($1, $2)
+  if $_[1] =~ /([0-9]+)x([0-9]+)/
+},
+"noscreenresize" => \$noscreenresize,
+"notile" => \$notile,
+"debug!" => \$debug,
+"debug-level=s" => \$debug,
+;
+
+#die "1 rad = " . rad2deg(1) . " deg" if $debug;
+
+sub parseangle {
+  my $angle = shift;
+  unless ($angle =~ s/ *rad(ians?)?$//i) {
+    $angle =~ s/ *deg(rees?)?$//;
+    $angle = deg2rad($angle);
+  }
+  $angle
+}
+
+sub parseangletodeg {
+  my $angle = shift;
+  if ($angle =~ s/ *rad(ians?)?$//i) {
+    $angle = rad2deg($angle);
+  } else {
+    $angle =~ s/ *deg(rees?)?//;
+  }
+  $angle
+}
+
+$angle = parseangle($angle) if defined $angle;
+$viewangle = parseangletodeg($viewangle) if defined $viewangle;
+defined($$_) && ($$_ = parseangletodeg($$_)) for (\$rotation, \$tilt, \$turn, \$viewrotation);
+
+die "You should specify one image file.  Try $0 --help for more info."
+if int(@ARGV) != 1;
+
+my $filename = $ARGV[0];
+
+my $photo = PhotoCrop::Photo->new(
+  $filename,
+  (defined($scaledetectedangle)  ? (scaledetectedangle => $scaledetectedangle) : ()),
+  (defined($scaledetectedanglex)  ? (scaledetectedangle => $scaledetectedanglex) : ()),
+  (defined($scaledetectedangley)  ? (scaledetectedangle => $scaledetectedangley) : ()),
+  (defined($angle)  ? (angle => $angle) : ()),
+  (defined($nometa) ? (nometadata => 1) : ()),
+  (defined($texdim) ? (texdim => $texdim) : ()),
+  (defined($notile) ? (notile => $notile) : ())
+);
+
+$angle = $photo->angle;
+
+my $app = SDL::App->new( -title => "$filename - Photocrop $VERSION", -width => $screenw, -height => $screenh, -gl => 1, -fullscreen => ($fullscreen?1:0),
+			 ($noscreenresize?():( -resizeable => 1 ))
+);
+
+SDL::ShowCursor(0);
+
+my $event = SDL::Event->new;
+$event->set(SDL_SYSWMEVENT, SDL_IGNORE);
+
+sub setviewport {
+  glViewport(0, 0, $screenw, $screenh);
+}
+
+sub setperspective {
+  glMatrixMode(GL_PROJECTION);
+  glLoadIdentity();
+  warn "screen size after resize: $screenw x $screenh" if $debug;
+  if (1) {
+    no integer;
+    my $c = 0.01;
+    my $i = ($screenw<$screenh);
+    ($screenw, $screenh) = ($screenh, $screenw) if $i;
+    my $y = tan(deg2rad($viewangle)/2)*$c;
+    my $x = $y*($screenw/$screenh);
+    ($screenw, $screenh, $x, $y) = ($screenh, $screenw, $y, $x) if $i;
+    warn "setperspective angle:$viewangle x:$x y:$y" if $debug > 3;
+    glFrustum(-$x, $x, -$y, $y, $c, 100);
+  } else {
+    no integer;
+    gluPerspective($viewangle, $screenw/$screenh, 0.01, 100.0);
+  }
+  glMatrixMode(GL_MODELVIEW);
+}
+
+sub setview {
+  setviewport();
+  glEnable(GL_TEXTURE_2D);
+  glClearColor(0.0, 0.0, 0.2, 0.0);
+  glClearDepth(1.0);
+  glDepthFunc(GL_LESS);
+  glEnable(GL_DEPTH_TEST);
+  glShadeModel(GL_SMOOTH);
+  setperspective();
+}
+
+sub Math::Quat::unit { Math::Quat::mkquat(1,0,0,0) }
+
+sub rotation_by_axis {
+  my ($a, $x, $y, $z) = @_; $a *= 0.5; my $s = sin($a);
+  my $m = sqrt($x**2 + $y**2 + $z**2);
+  #map { $$_ /= $m } (\$x, \$y, \$z);
+  $x /= $m; $y /= $m; $z /= $m;
+  Math::Quat->new(cos($a), $x*$s, $y*$s, $z*$s)
+}
+
+sub Math::Quat::rotate_by_axis { shift() * rotation_by_axis(@_) }
+
+sub Math::Quat::matrix3x3 {
+  my ($w, $x, $y, $z) = @{ shift() };
+  die "Internal error (matrix3x3)" unless defined($w) && defined($x) && defined($y) && defined($z);
+  my ($x2, $y2, $z2) = map { ($_**2)*2 } ($x, $y, $z);
+  my $xy = ($x*$y)*2;
+  my $xz = ($x*$z)*2;
+  my $yz = ($y*$z)*2;
+  my $wx = ($w*$x)*2;
+  my $wz = ($w*$z)*2;
+  my $wy = ($w*$y)*2;
+  1 - $y2 - $z2, $xy + $wz, $xy - $wy,
+  $xy - $wz, 1 - $x2 - $z2, $yz + $wx,
+  $xz + $wy, $yz - $wx, 1 - $x2 - $y2
+}
+
+sub Math::Quat::matrix4x4 {
+  @_ = (shift->Math::Quat::matrix3x3);
+  $_[0], $_[1], $_[2], 0,
+  $_[3], $_[4], $_[5], 0,
+  $_[6], $_[7], $_[8], 0,
+  0,0,0,1
+}
+
+sub UNIVERSAL::peekobj { require Data::Dumper; print STDERR caller() . ": " . Data::Dumper::Dumper($_[0]); $_[0] }
+sub UNIVERSAL::peek { require Data::Dumper; print STDERR caller() . ": $_[0]\n"; $_[0] }
+
+setview();
+
+use POSIX qw(fmod);
+
+sub outputpovray {
+    my $filetype = $filename;
+    ($filetype =~ /[.]([a-z0-9]+)$/) && ($filetype = $1);
+    $filetype = lc $filetype;
+    $filetype =~ s/jpg/jpeg/;
+    $filetype =~ s/tif/tiff/;
+    my ($bx, $by) = canvas($photo);
+    my $viewanglew = ($screenh >= $screenw) ? $viewangle : do {
+      no integer;
+      (rad2deg(atan2(
+		 tan(deg2rad($viewangle/2)) * ($screenw/$screenh),
+		 1)*2));
+    };
+    print "
+#include \"colors.inc\"
+background { color Gray }
+global_settings { ambient_light rgb<10, 10, 10> } // This should be correct for color fidelity
+
+camera {
+  location <0, 0, 0>
+  look_at <0, 0, -1>
+  right x*$screenw/$screenh
+  // angle ($viewangle // this is ok when screenw==screenh
+  angle $viewanglew // angle in povray refers to the horizontal angle, while viewangle is the vertical one if screenw>screenh
+  // angle ".($viewangle*(($by>$bx)?($by/$bx):($bx/$by)))." // viewangle
+  rotate ".$viewrotation." // viewrotation
+  rotate ".$turn."*y //turn
+  rotate ".$tilt."*x //tilt
+  rotate ".$rotation."*z // rotation
+}
+
+box {
+  <".$bx.",".$by.",-1>, <".(-$bx).",".(-$by).",-1>
+  texture {
+      pigment {
+        image_map {
+          $filetype \"".$filename."\"
+          interpolate 2
+        }
+      }
+    translate <-0.5, -0.5, 0>
+    scale <-2, 2, 2>
+    scale <$bx, $by, 1>
+    // scale <0.7, 0.7, 1>
+    // scale <".(($bx>$by)? ("1, ".($by/$bx)) : (($bx/$by).", 1")).", 1>
+  }
+}
+
+// angle ".$angle."
+// render with: povray +W".($screenw*$scaleup)." +H".($screenh*$scaleup)."
+"  
+}
+
+sub savepovray {
+  local *X;
+  my $f = shift;
+    if (open X, ">", $f) {
+      select(*X);
+      outputpovray;
+      select(*STDOUT);
+    } else {
+      warn "Could not open file to save povray scene: $f";
+    }
+}
+
+use File::Temp qw(tempfile);
+
+sub safefname {
+  shift() =~ m|[^-_/a-z0-9]|i
+}
+
+sub mysystem {
+  my $s = $_[0];
+  warn "Execute: $s";
+  system $s;
+}
+
+my ($done, $anim);
+sub quit {
+  return if $done;
+  select(*STDOUT);
+  print "
+  rotation $rotation deg
+  tilt $tilt deg
+  turn $turn deg
+  viewrotation $viewrotation deg
+  viewangle $viewangle rad\n";
+  print "  angle $angle deg\n" if defined $angle;
+  $done = 1
+}
+
+sub save {
+  savepovray($savepovray) if defined $savepovray;
+  if (defined($savepng) || $rungimp) {
+    if (defined($savepovray) && safefname($savepovray)) {
+      warn  "filename $savepovray may contain unsafe characters, regenerating to new temp location" if $debug;
+      undef $savepovray;
+    }
+    my ($fh, $fh2);
+    my $f = $savepovray;
+    ($fh, $f) = tempfile(SUFFIX => ".pov", UNLINK => 0) unless defined $f;
+    savepovray($f) if !defined($savepovray);
+    ($fh2, $savepng) = tempfile(SUFFIX => ".png", UNLINK => 0) unless defined $savepng;
+    warn "Filename $savepng may not be safe!" if !safefname($savepng);
+    my $gimp = $rungimp? " && gimp $savepng ": "";
+    mysystem("gnome-terminal -x sh -c \"povray +Q9 -D +A0.01 +R5 +W".($screenw*$scaleup)." +H".($screenh*$scaleup)." +O$savepng $f $gimp\" &");
+    #unlink($f) if !defined($savepovray);
+  }
+  if (defined $savebmp) {
+    my $l = $app->lock;
+    my $s = SDL::Surface->new(-from => $app, -width => $app->width, -height => $app->height, -depth => $app->bpp, -Rmask => $app->Rmask, -Gmask => $app->Gmask, -B => $app->Bmask, -A => $app->Amask);
+    $app->blit(undef, $s, undef);
+    $app->unlock if $l;
+    unless ($s->save_bmp($savebmp)) {
+      warn "Could not save screeen!";	
+    }
+  }
+}
+
+my $keyboard = PhotoCrop::KeyControl->new;
+do {
+  $keyboard->keyaction(SDLK_q, \&quit);
+  $keyboard->keyaction(SDLK_ESCAPE, \&quit);
+  $keyboard->keyaction(SDLK_RETURN, \&save);
+  $keyboard->keyaction(SDLK_f, sub { $app->fullscreen($fullscreen = !$fullscreen) });
+  my $oldviewangle = $viewangle;
+  my $viewanglecontrol = 0;
+  $keyboard->control(1, [SDLK_MINUS, SDLK_z], [SDLK_PLUS, SDLK_a], sub {
+    my $control = shift;
+    if (0) {
+      $viewangle = $oldviewangle * (1.0001 ** $control);
+      if (($viewangle > 0) && ($viewangle < 180)) {
+	$viewanglecontrol = $control;
+      } else {
+	$control = $viewanglecontrol;
+      }
+    } else {
+      $viewangle = rad2deg(atan2(tan(deg2rad($oldviewangle)/2) * (1.0001 ** $control), 1) * 2);
+    }
+    setperspective();
+    $control
+		     });
+  if (defined $angle) {
+    my $oldangle = $angle; $keyboard->control(0.01, [SDLK_e], [SDLK_n], sub { my $i = shift; $angle = atan2(tan($oldangle/2) * (1.001 ** $i), 1) * 2; $photo->angle($angle); $i });
+  }
+  my $oldrotation = $rotation; $keyboard->control(0.01, [SDLK_w], [SDLK_c], sub { my $i = shift; $rotation = $oldrotation + $i; deg2deg($i) });
+  my $oldtilt = $tilt; $keyboard->control(0.01, [SDLK_u, SDLK_UP], [SDLK_d, SDLK_DOWN], sub { my $i = shift; $tilt = $oldtilt + $i; deg2deg($i) });
+  my $oldturn = $turn; $keyboard->control(0.01, [SDLK_r, SDLK_RIGHT], [SDLK_l, SDLK_LEFT], sub { my $i = shift; $turn = $oldturn + $i; deg2deg($i) });
+  my $oldviewrotation = $viewrotation; $keyboard->control(0.01, [SDLK_k], [SDLK_v], sub { my $i = shift; $viewrotation = $oldviewrotation + $i; deg2deg($i) });
+};
+my $frame = 0;
+my %e = (SDL_QUIT() => \&quit,
+	 SDL_KEYUP() => sub { $keyboard->keyup($event->key_sym) },
+	 SDL_KEYDOWN() => sub { $keyboard->keydown($event->key_sym) },
+	 SDL_VIDEORESIZE() => sub { my $e = shift; return if $noscreenresize; $app->resize($screenw = $e->resize_w, $screenh = $e->resize_h); setview() }
+	 );
+while (!$done) {
+  warn "Frame $frame" if $debug;
+  warn "Redrawing!" if $debug > 1;
+  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+  my $time = int(time);
+  if ($crosshair && ($time & 1)) {
+    glEnable(GL_LINE_SMOOTH);
+    glLoadIdentity();
+    # my $w = deg2rad(0.2);
+    glLineWidth(1);
+    if (1) {
+      my @c = (($time&2)?(255,0,0):(0,255,255));
+      glColor(@c, 255);
+      # glSecondaryColor(@c);
+    };
+    glBegin(GL_LINES);
+    no integer;
+    my $b = deg2rad(30);
+    my $a = deg2rad(90);
+    my $amin = deg2rad(1/60);
+    while (1) {
+      $a /= 3; $b /= 3;
+      my ($x, $y, $z) = (sin($a), -cos($a), sin($b));
+      glVertex($z, $x, $y); glVertex(-$z, $x, $y);
+      glVertex($z, -$x, $y); glVertex(-$z, -$x, $y);
+      glVertex($x, $z, $y); glVertex($x, -$z, $y);
+      glVertex(-$x, $z, $y); glVertex(-$x, -$z, $y);
+      ($x, $y) = (sin($a * 2), -cos($a * 2));
+      glVertex($z, $x, $y); glVertex(-$z, $x, $y);
+      glVertex($z, -$x, $y); glVertex(-$z, -$x, $y);
+      glVertex($x, $z, $y); glVertex($x, -$z, $y);
+      glVertex(-$x, $z, $y); glVertex(-$x, -$z, $y);
+      last if $a < $amin;
+    }
+    glEnd;
+    glColor(1,1,1);
+    glLineWidth(1);
+  }
+  if (1) {
+    glLoadMatrix(Math::Quat->unit
+		 ->rotate_by_axis(deg2rad($viewrotation),0,0,1)
+		 ->rotate_by_axis(deg2rad($turn),0,1,0)
+		 ->rotate_by_axis(deg2rad($tilt),-1,0,0)
+		 ->rotate_by_axis(deg2rad($rotation),0,0,1)
+		 ->matrix4x4);
+  } elsif (1) {
+    #my $q = rotation_by_axis(deg2rad($turn),0,1,0);
+    #$q->rotate_by_axis(deg2rad($tilt),-1,0,0);
+    #$q->rotate_by_axis(deg2rad($rotation),0,0,1);
+    #glLoadMatrix([$q->matrix4x4]);
+    #glLoadMatrix(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1);
+    glLoadMatrix(Math::Quat->unit
+		 ->rotate_by_axis(deg2rad($turn),0,1,0)
+		 ->rotate_by_axis(deg2rad($tilt),-1,0,0)
+		 ->rotate_by_axis(deg2rad($rotation),0,0,1)
+		 ->matrix4x4);
+  } else {
+    glLoadIdentity();
+    #glColor(1,1,1);
+    glRotate($turn,0.0,1.0,0.0) if $turn;
+    glRotate($tilt,-1.0,0.0,0.0) if $tilt;
+    glRotate($rotation,0.0,0.0,1.0) if $rotation;
+  }
+  glLineWidth(1); # not sure why this happens
+  $photo->gldraw;
+  #glTranslate(0,0,-6);
+  #glBegin(GL_QUADS);
+  #glVertex(-1.0,+1.0,+0.0);
+  #glVertex(+1.0,+1.0,+0.0);
+  #glVertex(+1.0,-1.0,+0.0);
+  #glVertex(-1.0,-1.0,+0.0);
+  #glEnd();
+  #glColor(1,1,0);
+  #glTranslate(-1.5,0.0,0);
+  glColor(1,0,1,1);
+  glBegin(GL_TRIANGLES);
+  glVertex(+0.0,+10.0,-10.0);
+  glVertex(+0.0,+0.0,-10.0);
+  glVertex(+10.0,+0.0,-10.0);
+  glEnd();
+  glColor(1,1,1,1);
+  #glTranslate(3.0,0.0,0.0);
+  #glBegin(GL_QUADS);
+  #glVertex(-1.0,+1.0,+0.0);
+  #glVertex(+1.0,+1.0,+0.0);
+  #glVertex(+1.0,-1.0,+0.0);
+  #glVertex(-1.0,-1.0,+0.0);
+  #glEnd();
+
+  $app->sync();
+  $app->delay($delay);
+  warn "Processing events" if $debug;
+  $event->pump;
+  while ($anim ? $event->poll : $event->wait) {
+    my $type = $event->type;
+    my $a = $e{$type};
+    $a->($event) if defined $a;
+    redo if $event->poll;
+    last
+  }
+  $keyboard->frame;
+  $anim = $keyboard->needanim;
+  $frame++;
+}