#!/bin/bash
#
# Developed by Fred Weinhaus 6/20/2009 .......... revised 1/23/2013
#
# USAGE: spherize [-d diameter(s)] [-a amp] [-z zoom] [-b bcolor ] [-s] [-t] infile outfile
# USAGE: spherize [-h or -help]
#
# OPTIONS:
#
# -d diameter(s) diameters; comma separate x and y diameters;
# integers>0 and less than or equal to image
# dimensions; if only one value provided,
# it will be used for both; default is the
# image "width,height"
# -a amp distortion amplification factor; float>=0;
# default=0
# -z zoom zoom factor; scales the input image on the
# sphere; float>=0; default=1
# -b bcolor background color; any valid IM color or
# "none" for transparent or "image" for input
# image background; default=black
# -s make result with both diameters equal to the
# smaller of the x or y diameters
# -t trim image to diameters
#
###
#
# NAME: SPHERIZE
#
# PURPOSE: To warp an image onto a (hemi-)sphere.
#
# DESCRIPTION: UNROTATE warps an image onto a (hemi-)sphere and views it
# orthographically. The x and y diameters can be specified along with the
# distortion amplification and input image zoom. The background around
# the sphere is also user controlled. This is a limited version of my
# bubblewarp script that is not limited by the use of the IM -fx function
# and therefore is very fast.
#
#
# Arguments:
#
# -d diameter(s) --- DIAMETER(S) is a comma separate list of the desired
# x and y diameters of the sphere. If only one value is provided, then it
# will be used for both diameters. Values are integers greater than zero
# and equal to or smaller than the dimensions of the image. The default
# values are the size of the image. Thus if the image is square, then a
# spherical effect will be produced. But if the image is not square, then
# elliptical effect will be produced.
#
# -a amp --- AMP is the distortion amplification factor. Values are floats
# greater than or equal to zero. The default=1. Values larger than one will
# make more distortion and values less than one will make less distortion.
#
# -z zoom --- ZOOM is the zoom factor for the input image before warping it
# onto the sphere. Values are floats greater than or equal to zero. The
# default=1. Values larger than one will magnify the input image on the sphere.
# Values less than one will minify the input image on the sphere.
#
# -b bcolor ... BCOLOR is the color for the background outside the sphere.
# Any valid IM color is allowed or one may specify "none" for a transparent
# background or "image" to have the input image as the background. The default
# is black.
#
# -s ... Make the result with both diameters equal to the smaller of the
# x or y diameters.
#
# -t ... Trim the output image to the diameters.
#
# NOTE: This script is a limited version of my bubblewarp script, but is
# much faster, since it does not use -fx.
#
# NOTE: This script requires IM 6.5.3-9 or higher due to the use of
# -compose distort. Many thanks to Anthony Thyssen for his hard work
# developing it.
#
# 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;
dx="" # x diameter
dy="" # y diameter
amp=1 # distortion amplification
zoom=1 # input image zoom factor
bcolor="black" # background color; none for transparency; or "image"
symmetry="no" # sphere with both diameters equal to min of x and y diameters
trim="no" # trim output to diameters

# set directory for temporary files
dir="." # suggestions are dir="." or dir="/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 -n '/^###/q; /^#/!q; s/^#//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}
usage2()
{
echo >&2 ""
echo >&2 "$PROGNAME:" "$@"
sed >&2 -n '/^######/q; /^#/!q; s/^#*//; s/^ //; 4,$p' "$PROGDIR/$PROGNAME"
}

# function to report error messages
errMsg()
{
echo ""
echo $1
echo ""
usage1
exit 1
}

# 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 ] && errMsg "$errorMsg"
}

# test for correct number of arguments and get values
if [ $# -eq 0 ]
then
# help information
echo ""
usage2
exit 0
elif [ $# -gt 12 ]
then
errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
while [ $# -gt 0 ]
do
# get parameters
case "$1" in
-h|-help) # help information
echo ""
usage2
;;
-d) # diameters
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID DIAMETERS SPECIFICATION ---"
checkMinus "$1"
diameters=$1
test=`echo "$1" | tr "," " " | wc -w`
[ $test -eq 1 -o $test -gt 2 ] && errMsg "--- INCORRECT NUMBER OF DIAMETERS SUPPLIED ---"
diameters=`expr "$1" : '\([0-9]*,[0-9]*\)'`
[ "$diameters" = "" ] && errMsg "--- DIAMETERS=$diameters MUST BE A PAIR OF NON-NEGATIVE INTEGERS SEPARATED BY A COMMA ---"
diameters="$1,"
dx=`echo "$diameters" | cut -d, -f1`
dy=`echo "$diameters" | cut -d, -f2`
# further testing done later
;;
-a) # amp
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID AMPLIFICATION SPECIFICATION ---"
checkMinus "$1"
amp=`expr "$1" : '\([.0-9]*\)'`
[ "$amp" = "" ] && errMsg "--- AMPLIFICATION=$amp MUST BE A NON-NEGATIVE FLOATING POINT VALUE (with no sign) ---"
;;
-z) # zoom
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID ZOOM SPECIFICATION ---"
checkMinus "$1"
zoom=`expr "$1" : '\([.0-9]*\)'`
[ "$zoom" = "" ] && errMsg "--- ZOOM=$zoom MUST BE A NON-NEGATIVE FLOATING POINT VALUE (with no sign) ---"
;;
-b) # get bcolor
shift # to get the next parameter
# test if parameter starts with minus sign
errorMsg="--- INVALID BACKGROUND COLOR SPECIFICATION ---"
checkMinus "$1"
bcolor="$1"
;;
-s) # set symmetry
symmetry="yes"
;;
-t) # set trim
trim="yes"
;;
-) # STDIN and end of arguments
break
;;
-*) # any other - argument
errMsg "--- UNKNOWN OPTION ---"
;;
*) # end of arguments
break
;;
esac
shift # next option
done
#
# get infile and outfile
infile=$1
outfile=$2
fi

# test that infile provided
[ "$infile" = "" ] && errMsg "NO INPUT FILE SPECIFIED"

# test that outfile provided
[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"

# setup temporaries
tmpA1="$dir/spherize_A_$$.mpc"
tmpA2="$dir/spherize_A_$$.cache"
tmpB1="$dir/spherize_B_$$.mpc"
tmpB2="$dir/spherize_B_$$.cache"
tmpF1="$dir/spherize_T_$$.mpc"
tmpF2="$dir/spherize_T_$$.cache"
tmpM1="$dir/spherize_M_$$.mpc"
tmpM2="$dir/spherize_M_$$.cache"
tmpX1="$dir/spherize_X_$$.mpc"
tmpX2="$dir/spherize_X_$$.cache"
tmpY1="$dir/spherize_Y_$$.mpc"
tmpY2="$dir/spherize_Y_$$.cache"
trap "rm -f $tmpA1 $tmpA2 $tmpB1 $tmpB2 $tmpF1 $tmpF2 $tmpM1 $tmpM2 $tmpX1 $tmpX2 $tmpY1 $tmpY2; exit 0" 0
trap "rm -f $tmpA1 $tmpA2 $tmpB1 $tmpB2 $tmpM1 $tmpM2 $tmpX1 $tmpX2 $tmpY1 $tmpY2; exit 1" 1 2 3 15


# read the input image into the TMP cached image.
/bin/convert -quiet -regard-warnings "$infile" +repage "$tmpA1" ||
errMsg "--- FILE $infile NOT READABLE OR HAS ZERO SIZE ---"

# test for minimum IM version required
# IM 6.5.3.9 or higher to conform to new -compose distort
im_version=`/bin/convert -list configure | \
sed '/^LIB_VERSION_NUMBER /!d; s//,/; s/,/,0/g; s/,0*\([0-9][0-9]\)/\1/g' | head -n 1`
[ "$im_version" -lt "06050309" ] && errMsg "--- REQUIRES IM VERSION 6.5.3-9 OR HIGHER ---"

# get image dimensions
ww=`/bin/convert $tmpA1 -ping -format "%w" info:`
hh=`/bin/convert $tmpA1 -ping -format "%h" info:`

# set default diameters
if [ "$dx" = "" ]; then
dx=$ww
fi
if [ "$dy" = "" ]; then
dy=$hh
fi

# compute min diameter
dm=`/bin/convert xc: -format "%[fx:min($dx,$dy)]" info:`
if [ "$symmetry" = "yes" ]; then
dx=$dm
dy=$dm
fi

# test diameters
[ $dx -eq 0 -o $dx -gt $ww ] && errMsg "--- X DIAMETER MUST BE GREATER THAN 0 AND LESS OR EQUAL TO IMAGE WIDTH ---"
[ $dy -eq 0 -o $dy -gt $hh ] && errMsg "--- Y DIAMETER MUST BE GREATER THAN 0 AND LESS OR EQUAL TO IMAGE HEIGHT ---"


# scale radius so that that image fits circle or ellipse at any diameter with zoom=1
# invert zoom so increases with larger values
# use absolute coord displacement
# xamount=(rx/zoom)*(ww/dx)=ww/(2*zoom); where rx = x radius
xamount=`/bin/convert xc: -format "%[fx:$ww/(2*$zoom+quantumscale)]" info:`
yamount=`/bin/convert xc: -format "%[fx:$hh/(2*$zoom+quantumscale)]" info:`


# create radial gradient
/bin/convert -size ${dm}x${dm} radial-gradient: -negate $tmpF1

# convert circular gradient to elliptical
if [ $dx -ne $dy ]; then
/bin/convert $tmpF1 -resize ${dx}x${dy}! $tmpF1
fi

# create mask
/bin/convert $tmpF1 -negate -gravity center -background black -extent ${ww}x${hh} -threshold 0 $tmpM1

# set up amplify for distortion
if [ "$amp" = "1" ]; then
amplify=""
else
amplify="-evaluate pow $amp"
fi

# compute spherize function F=(2/pi)*asin(rd)/rd
/bin/convert $tmpF1 \( +clone -function arcsin '2,0,2,0' $amplify \) \
-compose divide -composite $tmpF1

# set up x displacement
# compute x gradient increasing with i
/bin/convert -size ${dy}x${dx} gradient: -rotate 90 $tmpX1

# compute u*v=xd*asin(rd)/rd; u=gradient 0,1; v=asin(rd)/rd
# but where want xd in range -1,1; so must use (2*u-1)
# then modify to 0.5*u*v+0.5 to recenter at mid gray
# old fx method: 0.5*((2*u-1)*v)+0.5 = u*v - 0.5*v + 0.5
# convert $tmpX1 $tmpF1 -monitor -fx "u*v-0.5*v+0.5" $tmpX1

if [ "$im_version" -ge "06050403" ]; then
/bin/convert $tmpX1 $tmpF1 +swap \
-compose Mathematics -set option:compose:args "1,0,-0.5,0.5" \
-composite $tmpX1
else

# multiply arcsin(r)/(r) by abs of gradient relative to midgray as zero
# abs is computed as -solarize 50% -level 50,0%
# abs has V shape and ranges 0 to 1
# then scale so that range is 0.5 to 1
#
# convert gradient to sign of gradient
# sign is computed as -threshold 50%
# thus zero (for neg) on left and one (for pos) on right
# use sign as mask to separate the right (pos) side,
# then negate mask and arcsin-absgrad product
# to get the left (neg) side and then combine with right side
/bin/convert $tmpX1 $tmpF1 \
\( -clone 0 -solarize 50% -level 50,0% \) \
\( -clone 1,2 -compose multiply -composite -function polynomial "0.5,0.5" \) \
\( -clone 0 -threshold 50% \) \
\( -clone 3,4 -compose multiply -composite \) \
\( -clone 3,4 -negate -compose multiply -composite \) \
-delete 0-4 -compose plus -composite \
$tmpX1
fi

# y displacement
if [ $dx -eq $dy ]; then
# map is symmetric so just rotate x displacement
/bin/convert $tmpX1 -rotate 90 $tmpY1
else
# compute y gradient increasing with j
/bin/convert -size ${dx}x${dy} gradient: -negate $tmpY1

if [ "$im_version" -ge "06050403" ]; then
/bin/convert $tmpY1 $tmpF1 +swap \
-compose Mathematics -set option:compose:args "1,0,-0.5,0.5" \
-composite $tmpY1
else
/bin/convert $tmpY1 $tmpF1 \
\( -clone 0 -solarize 50% -level 50,0% \) \
\( -clone 1,2 -compose multiply -composite -function polynomial "0.5,0.5" \) \
\( -clone 0 -threshold 50% \) \
\( -clone 3,4 -compose multiply -composite \) \
\( -clone 3,4 -negate -compose multiply -composite \) \
-delete 0-4 -compose plus -composite \
$tmpY1
fi
fi

# displace image (center relative)
# absolute coord displacement
/bin/convert $tmpA1 $tmpX1 $tmpY1 \
-gravity center -compose distort \
-set option:compose:args ${xamount}x${yamount} -composite \
$tmpB1

# set up trim
if [ "$trim" = "yes" ]; then
trimming="-gravity center -crop ${dx}x${dy}+0+0 +repage"
else
trimming=""
fi

# composite mask
if [ "$bcolor" = "none" ]; then
# echo "xparent background"
/bin/convert $tmpB1 $tmpM1 \
-alpha off -compose copy_opacity -composite $trimming $outfile
elif [ "$bcolor" = "image" -a "$symmetry" = "no" ]; then
# echo "img background w/sym"
/bin/convert $tmpB1 $trimming $outfile
elif [ "$bcolor" = "image" -a "$symmetry" = "yes" ]; then
# echo "img background w/o sym"
/bin/convert $tmpA1 $tmpB1 $tmpM1 -compose over -composite $trimming $outfile
else
# echo "background color $bcolor"
/bin/convert \( $tmpB1 $tmpM1 \
-gravity center -alpha off -compose copy_opacity -composite \) \
\( -size ${ww}x${hh} xc:"$bcolor" \) +swap \
-gravity center -compose over -composite $trimming $outfile
fi

exit 0