#! /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 - generate images from minc files =head1 SYNOPSIS B [options] .mnc [] mincpik generates image files from MINC volumes using the Imagemagick convert utility. Use -help or -man for more information and examples =head1 DESCRIPTION B generates image files from MINC volumes using the Imagemagick B utility. For a complete list of output file types see the B 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 =head3 Image range and lookup table options =over =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 =head3 Slicing options =over =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 =head3 Triplanar options =over =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 =cuts