#!/bin/bash
# 
# Developed by Fred Weinhaus 9/24/2007 .......... 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: derivative [-w width] [-a angle] [t thresh] [-m mix] infile outfile
# USAGE: derivative [-h or -help]
# 
# OPTIONS:
# 
# -w        width      width of filter; width=3, 5, 7; default=3
# -a        angle      angle=0 to 360 (clockwise from x axis);
#                      default=0
# -t        thresh     threshold percent for binarization; 
#                      thresh=integer 0 to 100; default="" (none)
# -m        mix        mixing percent with original image; 
#                      mix=integer 0 to 100; default=100
# -h                   get help information
# -help                get help information
# 
###
# 
# NAME: DERIVATIVE 
# 
# PURPOSE: To apply a first directional derivative filter to an image. 
# 
# DESCRIPTION: DERIVATIVE generates an output image which is a user defined mix 
# or blend of the original image and the first directional derivative convolution  
# of the image. The directional derivative is generated by combining the 
# individual x and y component derivative kernels at the desired angle. Then 
# the resulting directional derivative kernel is optionally mixed with the 
# the identity kernel representing the original image.
# 
# The basic blended high pass filtering formula is F = (1-m)*I + m*D, where I
# is the original image, D is the directional derivative high pass filtered
# image and m = mix/100. When m=0, we get only the original image and when
# m=1, we get only the high pass derivative filtered image. For intermediate
# value of m, we get a blend of the image and the derivative high pass
# filtered image. Now, we can consider both I and D as a convolution of some
# kernel with the original image, namely I = i x I and D = d x I, where x
# means convolution. Note that a convolution is simply a weighted average of
# all the pixels in some neighborhood of a give pixel. Usually an odd sized
# neighborhood, such as 3x3, etc is used to prevent having the resulting
# image be shifted a fractional pixel. The convolution kernel values are
# simply the weights for the average. So here, i is the identity kernel,
# which is all zeroes, except the center of the kernel which has a value of
# 1. Similarly, d is one of the directional derivative kernels. They are
# different forms of a high pass filter. Thus we can consider the final
# filtered image, F = f x I, where f = (1-m)*i + m*d. Consequently, we only
# have to do one convolution using the convolution kernel, f. Note, that all
# pure high pass filter convolution kernels will have weights that sum to 0.
# 
# 
# OPTIONS: 
# 
# -w width determines the size and form of the filter. Three different size 
# filters are allowed. So width=3, 5 or 7. The default is width=3.
#  
# width=3
# DX
# -1 0 1
# -1 0 1
# -1 0 1
#
# DY
# 1 1 1
# 0 0 0
# -1 -1 -1
#
#
# width=5
# DX
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
#
# DY
# 2 2 2 2 2
# 1 1 1 1 1
# 0 0 0 0 0
# -1 -1 -1 -1 -1
# -2 -2 -2 -2 -2
#
#
# width=7
# DX
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
#
# DY
# 3 3 3 3 3 3 3
# 2 2 2 2 2 2 2
# 1 1 1 1 1 1 1
# 0 0 0 0 0 0 0
# -1 -1 -1 -1 -1 -1 -1
# -2 -2 -2 -2 -2 -2 -2
# -3 -3 -3 -3 -3 -3 -3
#
#
# -a angle is the orientation of the directional derivative. Values are integers 
# ranging from 0 to 360 degrees clockwise from the x axis. Results for values of
# 0 and 360 will be identical. The default=0.
#
# -t thresh is the thresholding percentage used to create a binary Laplacian
# edge image. Values range from 0 to 100. A higher value will result in 
# fewer edges in the resulting image.
#
# -m mix is the percentage mixing factor used to blend the Laplacian with 
# the original image. A value of mix=0, results in the original image. A 
# value of mix=100 results in a pure Laplacian filtered image. 
#
# 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
deriv=1
width=3
ang=0
mix=100
thresh=""

# set directory for temporary files
dir="."    # suggestions are dir="." or dir="/tmp"

# First Derivatives

# width=3
# DX
# -1 0 1
# -1 0 1
# -1 0 1
d1x3="-1,0,1,-1,0,1,-1,0,1"
# DY
# 1 1 1
# 0 0 0
# -1 -1 -1
d1y3="1,1,1,0,0,0,-1,-1,-1"

# width=5
# DX
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
# -2 -1 0 1 2
d1x5="-2,-1,0,1,2,-2,-1,0,1,2,-2,-1,0,1,2,-2,-1,0,1,2,-2,-1,0,1,2"
# DY
# 2 2 2 2 2
# 1 1 1 1 1
# 0 0 0 0 0
# -1 -1 -1 -1 -1
# -2 -2 -2 -2 -2
d1y5="2,2,2,2,2,1,1,1,1,1,0,0,0,0,0,-1,-1,-1,-1,-1,-2,-2,-2,-2,-2"

# width=7
# DX
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
# -3 -2 -1 0 1 2 3
d1x7="-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3,-3,-2,-1,0,1,2,3"
# DY
# 3 3 3 3 3 3 3
# 2 2 2 2 2 2 2
# 1 1 1 1 1 1 1
# 0 0 0 0 0 0 0
# -1 -1 -1 -1 -1 -1 -1
# -2 -2 -2 -2 -2 -2 -2
# -3 -3 -3 -3 -3 -3 -3
d1y7="3 3 3 3 3 3 3 2 2 2 2 2 2 2 1 1 1 1 1 1 1 0 0 0 0 0 0 0 -1 -1 -1 -1 -1 -1 -1 -2 -2 -2 -2 -2 -2 -2 -3 -3 -3 -3 -3 -3 -3"


# 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 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 [ $# -eq 3 -o $# -eq 5 -o $# -eq 7 -o $# -eq 9 -o $# -gt 10 ]
	then
	errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
	while [ $# -gt 0 ]
		do
			# get parameter values
			case "$1" in
		  -h|-help)    # help information
					   echo ""
					   usage2
					   exit 0
					   ;;
				-w)    # get width
					   shift  # to get the next parameter - width
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID FILTER WIDTH SPECIFIED ---"
					   checkMinus "$1"
					   width="$1"
					   # test filter values
					   [ $width -ne 3 -a $width -ne 5 -a $width -ne 7 ] && errMsg "--- WIDTH=$width IS NOT A VALID VALUE ---"
					   ;;
				-a)    # get angle
					   shift  # to get the next parameter - angle
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID ANGLE SPECIFICATION ---"
					   checkMinus "$1"
					   # test thresh values
					   ang=`expr "$1" : '\([0-9]*\)'`
					   [ "$ang" = "" ] && errMsg "--- ANGLE=$ang MUST BE AN INTEGER ---"
					   angtestA=`echo "$ang < 0" | bc`
					   angtestB=`echo "$ang > 360" | bc`
					   [ $angtestA -eq 1 -o $angtestB -eq 1 ] && errMsg "--- ANGLE=$ang MUST BE AN INTEGER BETWEEN 0 AND 360 ---"
					   ;;
				-t)    # get thresh
					   shift  # to get the next parameter - thresh
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID THRESHOLD SPECIFICATION ---"
					   checkMinus "$1"
					   # test thresh values
					   thresh=`expr "$1" : '\([0-9]*\)'`
					   [ "$thresh" = "" ] && errMsg "--- THRESH=$thresh MUST BE AN INTEGER ---"
					   threshtestA=`echo "$mix < 0" | bc`
					   threshtestB=`echo "$mix > 100" | bc`
					   [ $threshtestA -eq 1 -o $threshtestB -eq 1 ] && errMsg "--- THRESH=$thresh MUST BE AN INTEGER BETWEEN 0 AND 100 ---"
					   ;;
				-m)    # get mix
					   shift  # to get the next parameter - mix
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID MIX SPECIFICATION ---"
					   checkMinus "$1"
					   # test mix values
					   mix=`expr "$1" : '\([0-9]*\)'`
					   [ "$mix" = "" ] && errMsg "--- MIX=$mix MUST BE AN INTEGER ---"
					   mixtestA=`echo "$mix < 0" | bc`
					   mixtestB=`echo "$mix > 100" | bc`
					   [ $mixtestA -eq 1 -o $mixtestB -eq 1 ] && errMsg "--- MIX=$mix MUST BE AN INTEGER BETWEEN 0 AND 100 ---"
					   ;;
				 -)    # 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 ---"

# test if image an ordinary, readable and non-zero size
if [ -f $infile -a -r $infile -a -s $infile ]
	then
	: 'do nothing - proceed'
	else
		errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
		exit 1
fi

# setup temporary images and auto delete upon exit
tmp0="$dir/derivative_0_$$.miff"
trap "rm -f $tmp0;" 0
trap "rm -f $tmp0; exit 1" 1 2 3 15
trap "rm -f $tmp0; exit 1" ERR

# compute pi
pi=`echo "scale=10; 4*a(1)" | bc -l`

# compute ang (positive clockwise from x axis)
echo "ang=$ang"
ang=`echo "scale=10; - $pi * $ang / 180" | bc`
sinang=`echo "scale=10; s($ang)" | bc -l`
cosang=`echo "scale=10; c($ang)" | bc -l`


# get derivative kernels
if [ $width -eq 3 ]
	then
	dx=$d1x3
	dy=$d1y3
elif [ $width -eq 5 ]
	then
	dx=$d1x5
	dy=$d1y5
elif [ $width -eq 7 ]
	then
	dx=$d1x7
	dy=$d1y7
else
	errMsg "--- INVALID DERIVATIVE FILTER ---"
fi

# convert 1D derivatives to array 
kernx=`echo $dx | sed 's/,/ /g'`
kernxArr=($kernx)
kerny=`echo $dy | sed 's/,/ /g'`
kernyArr=($kerny)
num=`expr $width \* $width`
num2=`echo "scale=0; ($num - 1) / 2" | bc`

# compute 1D mixed directional derivative
i=0
while [ $i -lt $num ]
	do
	kern1Arr[$i]=`echo "scale=5; $cosang * ${kernxArr[$i]} / 1" | bc`
	kern2Arr[$i]=`echo "scale=5; $sinang * ${kernyArr[$i]} / 1" | bc`
	kernArr[$i]=`echo "scale=3; $mix * (${kern1Arr[$i]} + ${kern2Arr[$i]}) / 100" | bc`
	i=`expr $i + 1`
done
kernArr[$num2]=`echo "scale=3; (((100 - $mix) / 100) + ${kernArr[$num2]})" | bc`


# print 2D directional derivative
i=0
k=0
while [ $i -lt $width ]
	do
	j=0
	krn=""
	while [ $j -lt $width ]
		do
		krn="$krn ${kernArr[$k]}"
		kernel[$i]=$krn
		k=`expr $k + 1`
		j=`expr $j + 1`
	done
	i=`expr $i + 1`	
done
echo ""
echo "2D Directional Derivative Kernel"
i=0
while [ $i -lt $width ]
	do
	printf %-10s ${kernel[$i]}
	echo ""
	i=`expr $i + 1`
done

# print 1D IM Final Laplacian Kernel
echo ""
echo "IM Final Directional Derivative Kernel"
kernIM=${kernArr[*]}
kernIM=`echo $kernIM | sed 's/ /,/g'`
echo $kernIM
echo ""


if [ "$thresh" != "" ]
	then
	threshoption="-threshold $thresh%"
else
	threshoption=""
fi

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`
	
# IM7 uses correlate in place of convolve to achieve the same result
# they vary by 180 degree rotation
if [ "$im_version" -ge "07000000" ]; then
	proc="-morphology correlate"
else
	proc="-convolve"
fi

#echo "proc=$proc; threshoption=$threshoption;"

# NOTE: improper nomalization for -convolve for zero sum (edge) filters between IM 6.7.6.6 and 6.7.8.7 (fixed 6.7.8.8)

convert $infile $proc "$kernIM" $threshoption $tmp0
#[ $mix -lt 100 ] && composite -blend $mix $tmp0 $infile $tmp0
[ $mix -lt 100 ] && convert $infile $tmp0 -define compose:args=$mix -compose blend -composite $tmp0
convert $tmp0 "$outfile"
exit 0

