#! /usr/bin/env perl
#
# Copyright 2009
# Andrew Janke - a.janke@gmail.com
# The University of Queensland
#
# 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.  The
# author and the University make no representations about the
# suitability of this software for any purpose.  It is provided "as is"
# without express or implied warranty.


use strict;
use warnings "all";
use Getopt::Long;
use Pod::Usage;
use File::Basename;
use File::Temp qw/ tempdir /;

my($Help, $Usage, $me, @opt_table, $tmpdir, %opt);
my(@args, $args, $infile, $outfile, %ordering, $CODE);

# permutation 'matrix' for differing views
%ordering = (
   'zspace' => ['yspace', 'xspace'],
   'yspace' => ['zspace', 'xspace'],
   'xspace' => ['zspace', 'yspace'],
   );

$me = &basename($0);
%opt = (
   'help' => 0,
   'man' => 0,
   'verbose' => 0,
   'clobber' => 0,
   'fake' => 0,
   
   'scale' => 2,
   'width' => undef,
   'bitdepth' => 8,
   'range' => undef,
   'image_range' => undef,
   'auto_range' => 0,
   'lookup' => undef,
   
   'slice' => undef,
   'dirs' => ['zspace'],
   
   'triplanar' => 0,
   'tilesize' => 250,
   'title' => 0,
   'title_text' => undef,
   'title_size' => 16,
   'sagittal_offset' => undef,
   'sagittal_offset_perc' => undef,
   'orientation' => 'vertical',
   'anot_bar' => undef,
   );

# Check arguments
&GetOptions(
   'help|?' => \$opt{'help'},
   'man' => \$opt{'man'},
   'version' => sub { &print_version_info },
   'v|verbose' => \$opt{'verbose'},
   'c|clobber' => \$opt{'clobber'},
   'f|fake' => \$opt{'fake'},
   
   'scale=i' => \$opt{'scale'},
   'width=i' => \$opt{'width'},
   'depth=i' => \$opt{'bitdepth'},
   
   'title' => \$opt{'title'},
   'title_text=s' => \$opt{'title_text'},
   'title_size=i' => \$opt{'title_size'},
   'anot_bar=s' => \$opt{'anot_bar'},
   
   # Image range and lookup table options
   'range=f{2}' => \@{$opt{'range'}},
   'image_range=f{2}' => \@{$opt{'image_range'}},
   'auto_range' => \$opt{'auto_range'},
   'lookup=s' => \$opt{'lookup'},
   
   # Slicing options
   's|slice=i' => \$opt{'slice'},
   'z|axial|transverse' => sub { $opt{'dirs'} = ['zspace']; },
   'y|coronal' => sub { $opt{'dirs'} = ['yspace']; },
   'x|sagittal' => sub { $opt{'dirs'} = ['xspace']; },
   
   # triplanar options
   't|triplanar' => \$opt{'triplanar'},
   'tilesize=i' => \$opt{'tilesize'},
   'sagittal_offset=i' => \$opt{'sagittal_offset'},
   'sagittal_offset_perc=i' => \$opt{'sagittal_offset_perc'},
   'vertical' => \$opt{'orientation'},
   'horizontal' => \$opt{'orientation'},
   ) or pod2usage(-verbose => 1) && exit;

# handle -man, -help or missing args
pod2usage(-verbose => 1) if $opt{'help'};
pod2usage(-exitstatus => 0, -verbose => 2) if $opt{'man'};
pod2usage(-verbose => 0) && exit if ($#ARGV != 1);

# Check arguments
#&Getopt::Tabular::SetHelp ($Help, $Usage);
#&GetOptions (\@opt_table, \@ARGV) || exit 1;
#die $Usage if ($#ARGV < 0);

# create temporary directory
$tmpdir = &tempdir( "$me-XXXXXXXX", TMPDIR => 1, CLEANUP => 1 );

# set up file names and do a few checks
$infile = $ARGV[0];
$outfile = (defined($ARGV[1])) ? $ARGV[1] : 'PNG:-';

die "$me: Couldn't find $infile\n\n" if (!-e $infile);
if($outfile ne 'PNG:-' && -e $outfile && !$opt{'clobber'}){
   die "\n$me: $outfile exists, use -clobber to overwrite\n\n";
   }

if($opt{'bitdepth'} != 16 && $opt{bitdepth} != 8) {
   die "\n$me: Invalid bitdepth specified - $opt{bitdepth} instead of 8 or 16\n\n";
   }

# sanity check
if($opt{'auto_range'} && @{$opt{'image_range'}}){
   die "\n$me: only specify one of -auto_range and -image_range (not both)\n\n";
   }

# warn about -slice and -triplanar
if(defined($opt{'slice'}) && $opt{'triplanar'}){
   warn "\n$me: you probably don't want to use both -triplanar and -slice\n\n";
   } 

# warn about -sagittal_offset and -sagittal_offset_perc
if(defined($opt{'sagittal_offset'}) && $opt{'sagittal_offset_perc'}){
   warn "\n$me: only use one of -sagittal_offset -sagittal_offset_perc\n\n";
   } 

# set up directions for triplanar
if($opt{'triplanar'}){
   $opt{'dirs'} = ['zspace', 'xspace', 'yspace'];
   }

my ($space, $n_slices, $convert_infile, $imgfile,
    @extract_args, @convert_args,
    $img_x, $img_y,
    $img_step_x, $img_step_y, 
    $img_length_x, $img_length_y,
    $dim_names, $pipe_args, $dimorder,
    @mont_files);

# find the 5% to 95% PcT image range if -auto_range
if($opt{'auto_range'}){
   my $buf;
   
   print STDERR "Getting range of $infile\n" if $opt{'verbose'};
   
   chomp($buf = `mincstats -quiet -pctT 5 $infile`);
   $buf *= 1.0;
   @{$opt{'image_range'}}[0] = $buf;

   chomp($buf = `mincstats -quiet -pctT 95 $infile`);
   $buf *= 1.0;
   @{$opt{'image_range'}}[1] = $buf;

   # a bit of output
   if($opt{'verbose'}){
      print STDERR "Using image range of [@{$opt{image_range}}[0]:@{$opt{image_range}}[1]]\n";
      }
   }

# foreach slicing direction
foreach $space (@{$opt{'dirs'}}){

   print STDERR "Doing direction $space\n" if $opt{'verbose'};
   
   my($slice);
   $CODE = "GRAY";
   
   # set up the imgfile depending on triplanar and -title
   if($opt{'triplanar'}){
      $imgfile = "$tmpdir/trip-$space.png";
      push(@mont_files, $imgfile);
      }
   elsif($opt{'title'}){
      $imgfile = "$tmpdir/image.png";
      }
   else{
      $imgfile = $outfile;
      }
   
   # Get the info we need
   $args = "mincinfo ".
           "-dimlength $space ".
           "-dimlength $ordering{$space}[0] ".
           "-dimlength $ordering{$space}[1] ".
           "-attvalue $ordering{$space}[0]:step ". 
           "-attvalue $ordering{$space}[1]:step ".
           "-dimnames ".
           $infile;
   ($n_slices, $img_x, $img_y, $img_step_x, $img_step_y, $dim_names) = split("\n", `$args`);

   if(defined($opt{'width'})){ 
      $opt{'scale'} = $opt{'width'}/abs($img_step_y * $img_y);
      print STDERR "Auto-scaling width factor: $opt{'scale'}\n" if $opt{'verbose'}; 
      }
   $img_length_x = abs(int($img_step_x * $img_x * $opt{'scale'}));
   $img_length_y = abs(int($img_step_y * $img_y * $opt{'scale'}));
   
   # figure out the slice to get
   $slice = (!defined($opt{'slice'})) ? int($n_slices/2) : $opt{'slice'};
   
   # do the sagittal offset (only one of these should be done)
   if($space eq 'xspace'){

      # slice offset      
      if(defined($opt{'sagittal_offset'})){
         $slice += $opt{'sagittal_offset'};
         }

      # perc offset
      if(defined($opt{'sagittal_offset_perc'})){
         $slice += int($n_slices * $opt{'sagittal_offset_perc'} / 100);
         }
      }
   
   # check we didn't step of the edge
   if($slice >= $n_slices || $slice < 0){ 
      die "Slice $slice out of range (0-" . ($n_slices-1) . ")\n\n";
      }
   
   # check if we have a vector_dimension already
   if($dim_names =~ m/vector_dimension/){
      $CODE = 'RGB';
      }
  
   # take only the first timepoint if we have a time dimension
   my @time_res_args = ();
   if($dim_names =~ m/time/){
      @time_res_args = ('-dimrange', "time=0,0");
      }
    
   # do the reshaping
   $dimorder = join(',', $space, @{$ordering{$space}});
   if($CODE eq 'RGB'){
      $dimorder .= ',vector_dimension';
      }
   @args = ('mincreshape', '-clobber', '-quiet',
            '-normalize',
            '+direction',
            '-dimsize', "$space=-1",
            '-dimsize', "$ordering{$space}[0]=-1",
            '-dimsize', "$ordering{$space}[1]=-1",
            '-dimorder', $dimorder,
            '-dimrange', "$space=$slice,1",
            @time_res_args,
            $infile, "$tmpdir/reshaped.mnc");                  
   if(scalar(@{$opt{'range'}}) != 0){
      push(@args, '-valid_range', @{$opt{'range'}}[0], @{$opt{'range'}}[1]);
      }
   if(scalar(@{$opt{'image_range'}}) != 0){
      push(@args, '-image_range', @{$opt{'image_range'}}[0], @{$opt{'image_range'}}[1]);
      }
   &do_cmd(@args);
   
   # do the lookup if required
   $convert_infile = "$tmpdir/reshaped.mnc";
   if($opt{'lookup'}){
      if($CODE eq 'RGB'){
         warn "$me: Input is vector-valued already.  No colour lookup done.\n";
         }
      else{
         $convert_infile = "$tmpdir/lookup.mnc";
         $CODE = 'RGB';
         &do_cmd('minclookup', '-clobber', '-quiet', 
                 split(' ', $opt{'lookup'}), 
                 "$tmpdir/reshaped.mnc", $convert_infile);
         }
      }

   # set up mincextract command
   @extract_args = ('mincextract', $convert_infile,
                    '-normalize',
                    ($opt{'bitdepth'} == 16) ? ('-short', '-unsigned') : '-byte');
   
   # set up convert arguments
   # a flip is 'normal' due to the difference between mnc and most image co-ordinates
   @convert_args = ('convert',
                    '-depth', $opt{'bitdepth'},
                    '-flip', 
                    '-size', $img_y . 'x' . $img_x,
                    '-geometry', $img_length_y . 'x' . $img_length_x . '!',
                    "$CODE:-", $imgfile);
   
   # check if we are big or little endian for convert's MSB wierdity
   $pipe_args = '|';
   if($opt{'bitdepth'} == 16){
      if(unpack("c",substr(pack("s",1),0,1))){
         warn "$me: LSB machine, swapping bytes with dd and crossing fingers\n";
         $pipe_args .= ' dd conv=swab | ';
         }
      }
   
   &do_cmd(join(' ', @extract_args, $pipe_args, @convert_args));
   }

# do the triplanar if requested
if($opt{'triplanar'}){
   my @orient_args;

   if($opt{'title'}){
      $imgfile = "$tmpdir/mont.png";
      }
   else{
      $imgfile = $outfile;
      }
   
   if($opt{'orientation'} eq 'vertical'){
      @orient_args = ('-tile', ('1x' . ($#mont_files + 1)));
      }
   else{ # $opt{orientation} eq 'horizontal'
      @orient_args = ('-tile', (($#mont_files + 1) . 'x1'));
      }
   
   # do the montage
   &do_cmd('montage',
           @orient_args, 
           '-background', 'grey10',
           '-geometry', "$opt{tilesize}x$opt{tilesize}+1+1",
           @mont_files, $imgfile);
   }
               
# Add the title
if($opt{'title'}){
   
   # set up the title text
   if(!defined($opt{'title_text'})){
      $opt{'title_text'} = &basename($infile);
      $opt{'title_text'} =~ s/\.mnc$//;
      }
     
#   This really does not work all that well (but should), go figure
#   &do_cmd('convert', '-box', 'black', 
#           '-font', '7x13', 
#           '-fill', 'white',
#           '-draw', "text 4,12 \"$opt{'title_text'}\"",
#           $imgfile, $outfile);
#
   # use montage instead
   &do_cmd('montage', 
           '-geometry', '100x100%',  
           '-background', 'black',
           '-fill', 'white',
           '-label', $opt{'title_text'},
           '-pointsize', $opt{'title_size'},
           $imgfile, $outfile);
   }

# create the annotated bar if required
if(defined($opt{'anot_bar'})){
   
   my($min, $max, $pcode, 
      $q0, $q1, $q2, $q3, $q4, $bh, $bw, $bb, $ob, 
      $textbump, $i, @buf);
   
   # set up a few constants
   $textbump = 3;
   $pcode = '%6g';
   
   # text border, other border, height, width
   $bb = 50;
   $ob = 25;
   $bh = $img_length_y - ($ob*2);
   $bw = int($bh/10);
   
   # get range if not defined
   if(!defined($opt{'image_range'}[0])){
      print STDERR "Getting image range\n" if $opt{'verbose'};
      
      @buf = split(/\n/, `mincstats -min -max -quiet $infile`);
      @{$opt{'image_range'}}[0] = $buf[0] * 1.0;
      @{$opt{'image_range'}}[1] = $buf[1] * 1.0;
      }
   
   $min = @{$opt{'image_range'}}[0];
   $max = @{$opt{'image_range'}}[1];
   
   # set up the datafile
   my(@data) = undef;
   my($packstring) = '';
   for($i=0; $i<$bh; $i++){
      $data[$i] = $i;
      $packstring .= 'f';
      }
   
   open(FH, ">$tmpdir/tmp-float.raw");
   for($i=0; $i<$bw; $i++){
      syswrite(FH, pack($packstring, @data));
      }
   close(FH);
   
   # make minc file
   &do_cmd('rawtominc', '-clobber',
           '-float',
           '-input', "$tmpdir/tmp-float.raw",
           '-xstep',  1, '-ystep',  1, '-zstep',  1,
           '-xstart', 0, '-ystart', 0, '-zstart', 0,
           '-dimorder', 'xspace,yspace,zspace',
           "$tmpdir/bar.mnc", $bw, $bh, 1);
   

   # make .miff bar image (whoa... recursion!)
   &do_cmd('mincpik', '-clobber',
           '-scale', 1,
           (defined($opt{'lookup'})) ? ('-lookup', $opt{'lookup'}) : (),
           "$tmpdir/bar.mnc", "$tmpdir/bar.png");
   
   # set up the text
   $q0 = sprintf($pcode, $min);
   $q1 = sprintf($pcode, (($max-$min) * 0.25) + $min);
   $q2 = sprintf($pcode, (($max-$min) * 0.5)  + $min);
   $q3 = sprintf($pcode, (($max-$min) * 0.75) + $min);
   $q4 = sprintf($pcode, $max);
   
   
   # create the bar itself via convert
   &do_cmd('convert',
           '-bordercolor', 'white',
           '-border', $bb,

           # color for all the decorations
           '-fill', 'black',

           # bar border
           '-draw', "line " . join(',',  $bb,      $bb,      ($bb+$bw),  $bb     ),
           '-draw', "line " . join(',',  $bb,     ($bb+$bh), ($bb+$bw), ($bb+$bh)),
           '-draw', "line " . join(',',  $bb,      $bb,       $bb,      ($bb+$bh)),
           '-draw', "line " . join(',', ($bb+$bw), $bb,      ($bb+$bw), ($bb+$bh)),

           # 3 ticks at 1/4, 1/2 and 3/4
           '-draw', "line " . join(',', ($bb+($bw*3/4)), ($bb+($bh*1/4)), ($bb+$bw), ($bb+($bh*1/4))),
           '-draw', "line " . join(',', ($bb+($bw*3/4)), ($bb+($bh*2/4)), ($bb+$bw), ($bb+($bh*2/4))),
           '-draw', "line " . join(',', ($bb+($bw*3/4)), ($bb+($bh*3/4)), ($bb+$bw), ($bb+($bh*3/4))),

           # text
           '-draw', 'text ' . ($bb+$bw) . ',' . ($bb+($bh*0/4)+$textbump) . " '$q4'",
           '-draw', 'text ' . ($bb+$bw) . ',' . ($bb+($bh*1/4)+$textbump) . " '$q3'",
           '-draw', 'text ' . ($bb+$bw) . ',' . ($bb+($bh*2/4)+$textbump) . " '$q2'",
           '-draw', 'text ' . ($bb+$bw) . ',' . ($bb+($bh*3/4)+$textbump) . " '$q1'",
           '-draw', 'text ' . ($bb+$bw) . ',' . ($bb+($bh*4/4)+$textbump) . " '$q0'",

           # finally crop of the extra border
           '-crop', (($bb*2)+$bw-($bb-$ob)) . "x" . (($bb*2)+$bh-$bb) . "+" . ($bb-$ob) . "+" . ($bb-$ob),

           "$tmpdir/bar.png", $opt{'anot_bar'});
   }


sub do_cmd {
   print STDERR "@_\n" if $opt{'verbose'};
   if(!$opt{'fake'}){
      system(@_) == 0 or die "\n$me: Failed executing @_\n\n";
      }
   }

# print version information
sub print_version_info {
   my $PACKAGE = '@PACKAGE_NAME@';
   my $VERSION = '@PACKAGE_VERSION@';
   my $PACKAGE_BUGREPORT = '@PACKAGE_BUGREPORT@';
   
   print STDOUT "\n$PACKAGE version $VERSION\n".
                "Comments to $PACKAGE_BUGREPORT\n\n";
   exit 0;
   }

__END__

=head1 NAME

B<mincpik> - generate images from minc files

=head1 SYNOPSIS

B<mincpik> [options] <infile>.mnc [<image.type>]

mincpik generates image files from MINC volumes using the Imagemagick
convert utility. Use -help or -man for more information and examples

=head1 DESCRIPTION

B<mincpik> generates image files from MINC volumes using the Imagemagick
B<convert> utility. For a complete list of output file types see the
B<convert> man pages.

EXAMPLES:
To display a default view, axial (z) slicing, middle slice
using display. (display is part of the Imagemagick package)

   mincpik infile.mnc PNG:- | display -

To generate a PNG file of the 15th coronal slice

   mincpik -slice 15 -coronal infile.mnc outfile.png

To generate a JPG file using the hotmetal lookup table 
with the image range 0 to 100

   mincpik -lookup '-hotmetal' -image_range 0 100 infile.mnc outfile.jpg

ImageMagick:  http://www.wizards.dupont.com/cristy/ImageMagick.html
   NB: ImageMagick should be compiled without 16-bit quanta.

Currently if there is a time dimension in the file the image will
only produced from the first time point

Problems or comments should be sent to: a.janke\@gmail.com

=head1 OPTIONS

=over 4

=item B<-v>, B<--verbose>

Be noisy when doing things

=item B<--version>

Print version number and exit

=item B<-?>, B<--help>

Dump some quick help output

=item B<--man>

Dump a man page

=item B<-c> B<--clobber>

overwrite the output file if it exists already

=item B<-f> B<--fake>

do a dry run, (echo cmds only). This is usually used in combination with -verbose to echo commands only

=item B<--scale>

scaling factor for resulting image, by default images are output 
at twice their original resolution

=item B<--width>

autoscale the resulting image to have a fixed image width (in pixels)

=item B<--depth>

bitdepth for resulting image 8 or 16 (MSB machines only!)

=item B<--title>

add a title to the resulting image, if just this option is specified the text used for the title is the name of the input image file.

=item B<--title_text>

use the input string for the title [default: input-filename]. This option must be used in conjunction with -title

=item B<--title_size>

font point size for the title

=item B<--anot_bar>

create an annotated bar to match the image (use height of the output image)

=back


=head2 Image range and lookup table options

=over 4

=item B<--range>

valid range of values for MINC file

=item B<--image_range>

range of image values to use for pixel intensity

=item B<--auto_range>

automatically determine image range using a 5 and 95% PcT. (histogram)

=item B<--lookup>

arguments to pass to minclookup

=back


=head2 Slicing options

=over 4

=item B<-s> B<--slice>

slice number to get. (note this is in voxel co-ordinates)

=item B<-z> B<--axial> B<--transverse>

get an axial/transverse (z) slice

=item B<-y> B<--coronal>

get a coronal (y) slice

=item B<-x> B<--sagittal>

get a sagital (x) slice

=back


=head2 Triplanar options

=over 4

=item B<-t> B<--triplanar>

create a triplanar view of the input file

=item B<--tilesize>

pixel size for each image in a triplanar

=item B<--sagittal_offset>

offset the sagittal slice from the centre

=item B<--sagittal_offset_perc>

offset the sagittal slice by a percentage from the centre

=item B<--vertical>

create a vertical triplanar view (Default)

=item B<--horizontal>

create a horizontal triplanar view

=back


=head1 SEE ALSO

convert(1) mincextract(1) display(1)

=head1 AUTHOR

Andrew Janke - a.janke@gmail.com

=head1 COPYRIGHTS

Copyright 2012 by Andrew L Janke

=cut
