#!/bin/sh
# 
# Fred Weinhaus ................................ revised 4/25/2015 
# Code Improvements by Anthony Thyssen  ................ 7/7/2008
# Developed by Fred Weinhaus 7/6/2008 .......... revised 4/25/2015
#
# ------------------------------------------------------------------------------
# 
# Licensing:
# 
# Copyright © Fred Weinhaus
# 
# My scripts are available free of charge for non-commercial use, ONLY.
# 
# For use of my scripts in commercial (for-profit) environments or 
# non-free applications, please contact me (Fred Weinhaus) for 
# licensing arrangements. My email address is fmw at alink dot net.
# 
# If you: 1) redistribute, 2) incorporate any of these scripts into other 
# free applications or 3) reprogram them in another scripting language, 
# then you must contact me for permission, especially if the result might 
# be used in a commercial or for-profit environment.
# 
# My scripts are also subject, in a subordinate manner, to the ImageMagick 
# license, which can be found at: http://www.imagemagick.org/script/license.php
# 
# ------------------------------------------------------------------------------
# 
####
#
# USAGE: shapemorph [-f frames] [-d delay] [-p pause] [-r] [-m] "x1,y1 x2,y2" infile1 infile2 outfile
#        shapemorph [-h|-help]
# 
# OPTIONS:
# 
#   -f  frames     number of frames in animation; frames>1; default=20
#   -d  delay      delay between frames; delay>0; default=10
#   -p  pause      pause delay for two undistorted input images;
#                  pause>0; default=100
#   -r             reverse the animation sequence and append it to the end
#   -m             enable progress monitoring of the generation of each frame
#   "x1,y1 x2,y2"  Control point location in infile1 and infile2, respectively;
#                  MUST be specified just prior to infile1 and outfile1
#                  consist of four, comma or space seperated numbers
# 
###
#
# NAME: SHAPEMORPH
# 
# PURPOSE: To create a shape morphing animation sequence between two images.
# 
# DESCRIPTION: SHAPEMORPH creates a shape morphing animation sequence between
# two images using one corresponding control point specified from each of the
# input images. The control point along with the fixed corners actually form
# 5 control points that are used to fill out X and Y displacement maps (images)
# that are then used to transform the geometry of each image to the other. The
# corresponding frames from the transformation of each image are then blended
# proportional to the progression of frames.
# 
# OPTIONS:
# 
# -f frames ... FRAMES is the total number of frames in the animation (including
# infile1 and infile2 as the start and end frames. Values are integers > 1. The
# default is 20.
# 
# -d delay ... DELAY between frames. Values are integers>0. The default=10
# 
# -p pause ... PAUSE is the delay to use for the first and last frame of the
# animation, i.e. the delay for each of the input images. The default=100
# 
# -r ... If supplied, then reverse the animation sequence, remove the first and
# last frames of the reversed sequence and append these reversed frames to
# the end of the animation.
# 
# -m ... If supplied, then enable monitoring of -fx as it creates each frame.
# 
# "x1,y1 x2,y2" ... The corresponding control point locations in infile1 and
# infile2 respectively. Only one control point in each image may be used. These
# coordinates MUST be provided after all the optional arguments and just prior
# to the declaration of infile1 infile2.
# 
# NOTE: Thanks to Anthony Thyssen's improvements, the script has a 10x 
# speed increase for IM version prior to 6.4.2-4 and is now even faster 
# for IM version after 6.4.2-4 due to the use of the new -distort shepards.
# 
# CAVEAT: No guarantee that this script will work on all platforms,
# nor that trapping of inconsistent parameters is complete and
# foolproof. Use At Your Own Risk.
# 
######
# 

# set default values
frames=20
delay=10
pause=100
reverse="no"
monitor=""
coords=""

# set directory for temporary files
tmpdir="/tmp"

# set up functions to report Usage and Usage with Description
PROGNAME=`type $0 | awk '{print $3}'`  # search for executable on path
PROGDIR=`dirname $PROGNAME`            # extract directory of program
PROGNAME=`basename $PROGNAME`          # base name of program
usage1() 
	{
	echo >&2 ""
	echo >&2 "$PROGNAME:" "$@"
	sed >&2 -e '1,/^####/d;  /^###/g;  /^#/!q;  s/^#//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
	}
usage2() 
	{
	echo >&2 ""
	echo >&2 "$PROGNAME:" "$@"
	sed >&2 -e '1,/^####/d;  /^######/g;  /^#/!q;  s/^#*//;  s/^ //;  4,$p' "$PROGDIR/$PROGNAME"
	}


# function to test for minus at start of value of second part of option 1 or 2
checkMinus() {
  test=`echo "$1" | grep -c '^-.*$'`   # returns 1 if match; 0 otherwise
  [ $test -eq 1 ] && usage "$errorMsg"
}

# test for correct number of arguments and get values
[ $# -eq 0 ] && usage_verbose
[ $# -gt 12 ] && usage "Too Many Arguments"

while [ $# -gt 0 ]; do
  # get parameter values
  case "$1" in
    -h|-help) usage_verbose ;;  # verbose help message
    -r) reverse="yes" ;;        # set frame reversal append
    -m) monitor="-monitor" ;;   # enable monitoring of frame creation
    -f) # get frames
        shift  # to get the next parameter - frames
        # test if parameter starts with minus sign
        errorMsg="Invalid Frames Specification"
        checkMinus "$1"
        frames=`expr "$1" : '\([0-9]*\)'`
        [ "$frames" = "" ] && usage "Frames=\"$frames\" must be an integer"
        framestest=`echo "$frames <= 1" | bc`
        [ $framestest -eq 1 ] &&
           usage "Frames=\"$frames\" must be an integer greater than 1"
        ;;
    -d) # get delay
        shift  # to get the next parameter - delay
        # test if parameter starts with minus sign
        errorMsg="Invalid Delay specification"
        checkMinus "$1"
        delay=`expr "$1" : '\([0-9]*\)'`
        [ "$delay" = "" ] && usage "delay=\"$delay\" must be an integer"
        delaytest=`echo "$delay < 1" | bc`
        [ $delaytest -eq 1 ] &&
           usage "delay=\"$delay\" must be a positive integer"
        ;;
    -p) # get pause
        shift  # to get the next parameter - pause
        # test if parameter starts with minus sign
        errorMsg="Invalid pause specification"
        checkMinus "$1"
        pause=`expr "$1" : '\([0-9]*\)'`
        [ "$pause" = "" ] &&
           usage "pause=\"$pause\" must be a non-negative integer"
        ;;
    --) shift; break ;;                    # End of options
    -)  break ;;                           # STDIN and end of arguments
    -*) usage "Unknown Option \"$1\"" ;;
    *)  break ;;
  esac
  shift   # next option
done

# get coords, infile1, infile2 and outfile
coords="$1"
infile1="$2"
infile2="$3"
outfile="$4"

# test that coordinates are specified correctly
if [ "$coords" = "" ]; then
  usage "--- NO CONTROL POINTS WERE SPECIFIED ---"
else
  # Seperate out comma/space separated coordinates
  set - `echo "$coords" | sed 's/[, ][, ]*/ /g'`
  [ $# -ne 4 ] &&
    usage "--- TWO CONTROL POINTS (4 VALUES) NEED TO BE SPECIFIED ---"
  xx1="$1"
  yy1="$2"
  xx2="$3"
  yy2="$4"
fi

# test all images are provided
[ "$infile1" = "" ] && usage "No input file 1 specified ---"
[ "$infile2" = "" ] && usage "No input file 2 specified ---"
[ "$outfile" = "" ] && usage "No output file specified ---"

# Setup directory for temporary files
# On exit remove ALL the whole directory of temporary images
dir="$tmpdir/$PROGNAME.$$"
trap "rm -rf $dir;" 0
trap "rm -rf $dir; exit 1" 1 2 3 15
trap "rm -rf $dir; exit 1" ERR
mkdir "$dir" || {
  echo >&2 "$PROGNAME: Unable to create working dir \"$dir\" -- ABORTING"
  exit 10
}

convert -quiet -delay $delay "$infile1" +repage "$dir/A.mpc" ||
  usage "--- FAILED TO READ \"$infile1\" ---"

convert -quiet -delay $delay "$infile2" +repage "$dir/B.mpc" ||
  usage "--- FAILED TO READ \"$infile2\" ---"

# get image sizes and test if same size
aw=`convert $dir/A.mpc -format %w info:`
ah=`convert $dir/A.mpc -format %h info:`
bw=`convert $dir/B.mpc -format %w info:`
bh=`convert $dir/B.mpc -format %h info:`
if [ $aw -eq $bw -a $ah -eq $bh ]; then
  ww=$aw
  hh=$ah
else
  usage "--- INPUT IMAGES ARE NOT THE SAME SIZE ---"
fi

# get last pixel in image (for corners)
wm1=`expr $ww - 1`
hm1=`expr $hh - 1`

# get im version to use for switching methods depending upon version
im_version=`convert -list configure | \
	sed '/^LIB_VERSION_NUMBER */!d;  s//,/;  s/,/,0/g;  s/,0*\([0-9][0-9]\)/\1/g' | head -n 1`


if [ "$im_version" -ge "06040304" ]; then 
	# use -distort shepards technique
	
	# set corner control points as fixed
	corners="0,0 0,0  $wm1,0 $wm1,0  $wm1,$hm1 $wm1,$hm1  0,$hm1 0,$hm1"

	# do geometric warping and intensity blending
	echo ""
	echo "Processing $frames Frames:"
	
	# add infile1 as first (zeroth) frame
	echo "0 (start image)"
	convert -delay $pause $dir/A.mpc $dir/result.miff
	
	# create the in between frames
	iter=`expr $frames - 1`
	# initial coord for destination for both images
	cpx=$xx1
	cpy=$yy1
	# increment on current destination coordinates
	dx=`convert xc: -format "%[fx:($xx2-$xx1)/$iter]" info:`
	dy=`convert xc: -format "%[fx:($yy2-$yy1)/$iter]" info:`

	i=1
	while [ $i -lt $iter ]; do
		echo "$i"
		
		# set up iteration
		blend=`convert xc: -format "%[fx:100*$i/$iter]" info:`
		
		# new same location for a given iteration used for both images
		cpx=`convert xc: -format "%[fx:$cpx+$dx]" info:`
		cpy=`convert xc: -format "%[fx:$cpy+$dy]" info:`
		
		# interate new control point for infile 2 (B)
		cpointsb="$corners  $xx2,$yy2 $cpx,$cpy"
		
		# interate new control point for infile 1 (A)
		cpointsa="$corners  $xx1,$yy1 $cpx,$cpy"
		
		# NOTE: A multi-image miff is just a concatanation of images!
		# This makes it easy to form pipelined commands.
		
		if [ "$im_version" -ge "06050304" ]; then
			( 
			# ( ... ) is a subshell to allow two miff:- to be used as multi-image miff
			
			# Warp each image using -distort shepards.
			
			# transform B to A
			convert $dir/B.mpc -distort shepards "$cpointsb" miff:-
			
			# transform A to B
			convert $dir/A.mpc -distort shepards "$cpointsa" miff:-
			
			# blend the displaced images
			) | convert - -reverse -define compose:args=${blend}% -compose blend -composite miff:- >> $dir/result.miff

		else
				( 
			# ( ... ) is a subshell to allow two miff:- to be used as multi-image miff
			
			# Warp each image using -distort shepards.
			
			# transform B to A
			convert $dir/B.mpc -distort shepards "$cpointsb" miff:-
			
			# transform A to B
			convert $dir/A.mpc -distort shepards "$cpointsa" miff:-
			
			# blend the displaced images
			) | composite -blend ${blend}% - miff:- >> $dir/result.miff
		fi
			
		i=`expr $i + 1`
	done
	
	# add infile2 as last frame
	echo "$i (end image)"
	convert -delay $pause $dir/B.mpc miff:- >> $dir/result.miff
	
	# reverse and append if desired
	if [ "$reverse" = "yes" ]; then
	  convert $dir/result.miff \( -clone -2-1 \) -loop 0 "$outfile"
	else
	  convert $dir/result.miff -loop 0 "$outfile"
	fi

else
	# use displacement mapping technique
	
	# create x and y displacement maps for transforming geometry
	# from infile1 to infile2
	
	# As a speed up, all operations done only in the green
	# (for grey) channel which is then extracted
	
	# shepards method to interpolate control point value with zero at corners
	# functions to work out the inverse distance squared from corners and control
	da="da=1/max(1,i*i+j*j);"               		# upper-left
	db="db=1/max(1,(i-$wm1)^2+j*j);"         		# lower-left
	dc="dc=1/max(1,(i-$wm1)^2+(j-$hm1)^2);"   		# lower-right
	dd="dd=1/max(1,i*i+(j-$hm1)^2);"         		# upper-right
	de="de=1/max(1,(i-$xx2)^2+(j-$yy2)^2);"   		# control point
	ds="ds=(da+db+dc+dd+de);"               		# sum of inverse distance squared
	
	echo ""
	echo "Creating Displacement Map For Infile1"
	# all operations done only in the green for greyscale channel
	# use +level 50,100% to set displacement relative to mid gray and full dynamic range
	# by moving black (0 value) to 50% and keeping white at 100% as weights range from 0 to 1
	convert -size ${ww}x${hh} xc: -monitor -channel G \
			-fx "$da $db $dc $dd $de $ds de/ds" \
			+level 50,100%  -separate  $dir/Amap.mpc
	
	# Repeat shepards method for morphing in the other direction
	# We only need to change the control point and displacement direction
	de="de=1/max(1,(hypot((i-$xx1),(j-$yy1))^2));"  # control point
	
	echo ""
	echo "Creating Displacement Map For Infile2"
	# all operations done only in the green for greyscale channel
	# use +level 50,100% to set displacement relative to mid gray and full dynamic range
	# by moving black (0 value) to 50% and keeping white at 100% as weights range from 0 to 1
	convert -size ${ww}x${hh} xc: -monitor -channel G \
			-fx "$da $db $dc $dd $de $ds de/ds" \
			+level 50,100%  -separate  $dir/Bmap.mpc
	
	# do geometric warping and intensity blending
	echo ""
	echo "Processing $frames Frames:"
	
	# add infile1 as first (zeroth) frame
	echo "0 (start image)"
	convert -delay $pause $dir/A.mpc $dir/result.miff
	
	# dx,dy offset in pixels between moving control points.
	# note formula reversed from -distort shepards method above
	Adx=`convert xc: -format "%[fx:$xx1-$xx2]" info:`
	Ady=`convert xc: -format "%[fx:$yy1-$yy2]" info:`
	Bdx=`convert xc: -format "%[fx:-$Adx]" info:`
	Bdy=`convert xc: -format "%[fx:-$Ady]" info:`
	
	# create the in between frames
	iter=`expr $frames - 1`
	i=1
	while [ $i -lt $iter ]; do
		echo "$i"
		Awarp=`convert xc: -format "%[fx:$i/($iter)]" info:`
		Bwarp=`convert xc: -format "%[fx:1-$Awarp]" info:`
		blend=`convert xc: -format "%[fx:100*$Awarp]" info:`
		
		# NOTE: A multi-image miff is just a concatanation of images!
		# This makes it easy to form pipelined commands.
		
		( 
		# ( ... ) is a subshell to allow two miff:- to be used as multi-image miff
		
		# Warp each image using the pre-calculated displacement maps.
		
		# transform B to A
		dx=`convert xc: -format "%[fx:$Bdx*$Bwarp]" info:`
		dy=`convert xc: -format "%[fx:$Bdy*$Bwarp]" info:`
		composite -displace "$dx,$dy" $dir/Bmap.mpc $dir/B.mpc \
				  $monitor miff:-
		
		# transform A to B
		dx=`convert xc: -format "%[fx:$Adx*$Awarp]" info:`
		dy=`convert xc: -format "%[fx:$Ady*$Awarp]" info:`
		composite -displace "$dx,$dy" $dir/Amap.mpc $dir/A.mpc \
				  $monitor miff:-
		
		# blend the displaced images
		) | composite -blend ${blend}% - miff:- >> $dir/result.miff
		
		i=`expr $i + 1`
	done
	
	# add infile2 as last frame
	echo "$i (end image)"
	convert -delay $pause $dir/B.mpc miff:- >> $dir/result.miff
	
	# reverse and append if desired
	if [ "$reverse" = "yes" ]; then
	  convert $dir/result.miff \( -clone -2-1 \) -loop 0 "$outfile"
	else
	  convert $dir/result.miff -loop 0 "$outfile"
	fi
fi

exit 0