#!/bin/bash
#
# Developed by Fred Weinhaus 10/21/2014 .......... revised 6/18/2016
#
# ------------------------------------------------------------------------------
# 
# 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: space2 [-a autolevel] [-b bri] [-c con] [-s sat] [-m maxgain ] [-d]
# [-w window ] infile outfile
# USAGE: space2 [-h or -help]
#
# OPTIONS:
#
# -a              autolevel      autolevel as preprocessing; options are: 
#                                off (o), together (t), separately (s); 
#                                default=together
# -b              bri            brightness adjustment factor; float>0; 
#                                default=1 (no change)                             
# -c              con            contrast adjustment factor; float>0; 
#                                default=1 (no change)                             
# -s              sat            saturation adjustment percent; float; 
#                                -45<=sat<=45; default is no change 
# -m              maxgain        maximum gain; controls amount of detail;
#                                float>0; default=2.5
#                                larger values will allow more detail;
# -d                             set the desired standard deviation (dstd) 
#                                to an internally set constant value; 
#                                default is to use the image's actual std                                
# -w              window         windowing size as percentage of image;
#                                nominally between 5 and 20; 2<=float<=50;
#                                default=8
# -h or -help                    get help
#
###
#
# NAME: SPACE2
# 
# PURPOSE: To change the contrast and/or brightness in an image adaptively. 
# 
# DESCRIPTION: SPACE2 is a locally adaptive technique to enhance an image's 
# brightness and contrast. SPACE is an abbreviation for SPacially Adaptive 
# Contrast Enhancement.
#
# The adaptive formula R = M + G*(I-M). Here R is the resulting image. I is
# the input image. M is a mean image, which is a low pass filtered version of
# the input image. It is generated by a block resize of the input image to
# some fraction of the input size. The resize amount is computed from the
# window size. This image is then re-expanded to its original size. The
# resizing technique is fast way to apply a large block size moving window
# average. The term (I-M) is a high pass filtered version of the input image.
# G is a gain image, which involves S, the standard deviation of the input
# image, generated by the same resizing technique as used to create the mean
# image, M. G also includes the desired standard deviation (dstd) and a max
# gain factor. The gain image, G, is used to set the amount of detail
# (sharpness) in the output as well as to limit runaway gain. Max gain is
# typically on the order of 1-10, with a default of 2.5. The block window size
# typically is on the order of 5-20% of the image size and nominally 8%. There
# are times when one may want to push the max gain value higher than the
# default, for example, when trying to pull information out of a hazy picture.
# The algorithm has options to control the brightness, contrast and
# saturation. The brightness and contrast have automatically computed defaults
# that are image dependent. They are applied via sigmoidal non-linear
# functions. The saturation is a linear adjustment in LAB colorspace and the
# default is no change.
# 
# 
# Arguments: 
#
# -a autolevel --- autolevel as preprocessing step. The options are: 
# off (o), together (t), separately (s). The default=together. Note, 
# for some images, especially with haze, the option separately can cause 
# severe color shifts.
# 
# -b bri --- bri is a brightness adjustment factor multiplied by the  
# automatically computed default. It is a float>0. Values larger then 1 will 
# increase the brightness and values smaller than 1 will decrease the 
# brightness. The default is 1 or no change from the automatically computed 
# value. 
# 
# -c con --- con is a contrast adjustment factor multiplied by the  
# automatically computed default. It is a float>0. Values larger then 1 will 
# increase the contrast and values smaller than 1 will decrease the contrast. 
# The default is 1 or no change from the automatically computed value. 
# 
# -s sat --- sat is a saturation factor. It is used in a post processing step. 
# It is a float in the range of -45 to 45 (percent). The default is 0 or no 
# change and skips saturation processing. 
# 
# -m maxgain --- maxgain is the maximum gain. It controls the amount of detail 
# (sharpness). Values are floats>0. The default is 2.5. Larger values allow 
# more detail to show and may be useful for very hazy images. Too large a  
# value may over-exaggerate detail, especially when there is no haze.
# 
# -d --- set the desired standard deviation (dstd) to an internally set 
# constant value. The default is to use the image's actual std for the dstd.                             
# Both results are close. It becomes a user's preference. The results are 
# image dependent. Some images become slightly sharper with one method and 
# other images are sharper with the other method.
# 
# -w window --- the moving window size as a percentage of the input image for the 
# local averaging and standard deviation operations. Values are 2<=float<=50. 
# Typical values are between 5 and 20. The default is 8 (%).
# 
# REQUIREMENTS: If not using HDRI mode, this script can be slow due to the 
# use of -fx. Also IM 6.5.5-1 or higher due to the use of -auto-level. For 
# best results use IM 6.7.9-5 or higher due to changes in the operation of 
# -sigmoidal-contrast.
#
# 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; 
autolevel="together"	# autolevel preprocess
bri=1					# auto compute
con=1					# auto compute
sat=0					# skips saturation processing; no change
dflag="false"			# desired std flag; default is to use image's std
maxgain=2.5				# max gain
window=8					# window size for averaging
stats="false"

# internal arguments for default computation of bri and con
dmean=0.5		# nominal desired mean
dstd=0.2		# nominal desired std


# set directory for temporary files
# use tmpdir="." or replace with tmpdir="/tmp"
tmpdir="."


# 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 [ $# -gt 15 ]
	then
	errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
	while [ $# -gt 0 ]
		do
		# get parameters
		case "$1" in
	  -h|-help)    # help information
				   echo ""
				   usage2
				   ;;
			-a)    # get  autolevel
				   shift  # to get the next parameter
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID AUTOLEVEL SPECIFICATION ---"
				   checkMinus "$1"
				   autolevel=`echo "$1" | tr '[A-Z]' '[a-z]'`
				   case "$autolevel" in 
						o|off) autolevel="off" ;;
						together|t) autolevel="together" ;;
						separately|s) autolevel="separately" ;;
						*) errMsg "--- AUTOLEVEL=$autolevel IS AN INVALID VALUE ---" ;;
					esac
				   ;;
			-b)    # bri
				   shift  # to get the next parameter - bri
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID BRI SPECIFICATION ---"
				   checkMinus "$1"
				   bri=`expr "$1" : '\([.0-9]*\)'`
				   [ "$bri" = "" ] && errMsg "--- BRI=$bri MUST BE A NON-NEGATIVE FLOATING POINT VALUE (with no sign) ---"
				   britest=`echo "$bri <= 0" | bc`
				   [ $britest -eq 1 ] && errMsg "--- BRI=$bri MUST BE A POSITIVE FLOATING POINT VALUE ---"
				   ;;
			-c)    # con
				   shift  # to get the next parameter - con
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID CON SPECIFICATION ---"
				   checkMinus "$1"
				   con=`expr "$1" : '\([.0-9]*\)'`
				   [ "$con" = "" ] && errMsg "--- CON=$con MUST BE A NON-NEGATIVE FLOATING POINT VALUE (with no sign) ---"
				   contest=`echo "$con <= 0" | bc`
				   [ $contest -eq 1 ] && errMsg "--- CON=$con MUST BE A POSITIVE FLOATING POINT VALUE ---"
				   ;;
			-s)    # sat
				   shift  # to get the next parameter - sat
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID SAT SPECIFICATION ---"
				   sat=`expr "$1" : '\([-.0-9]*\)'`
				   [ "$sat" = "" ] && errMsg "--- SAT=$sat MUST BE A NON-NEGATIVE FLOATING POINT VALUE (with no sign) ---"
				   sattestA=`echo "$sat > 45" | bc`
				   sattestB=`echo "$sat < 45" | bc`
				   [ $sattestA -eq 1 -o $sattestB -eq 1 ] && errMsg "--- SAT=$sat MUST BE A FLOATING POINT VALUE BETWEEN -45 AND 45 ---"
				   ;;
			-w)    # window
				   shift  # to get the next parameter - window
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID WINDOW SPECIFICATION ---"
				   checkMinus "$1"
				   window=`expr "$1" : '\([.0-9]*\)'`
				   [ "$window" = "" ] && errMsg "--- WINDOW=$window MUST BE A POSITIVE FLOATING POINT VALUE (with no sign) ---"
				   windowtestA=`echo "$window < 2" | bc`
				   windowtestB=`echo "$window > 50" | bc`
				   [ $windowtestA -eq 1 -o $windowtestB -eq 1 ] && errMsg "--- WINDOW=$window MUST BE A FLOATING POINT VALUE BETWEEN 2 AND 50 ---"
				   ;;
			-m)    # maxgain
				   shift  # to get the next parameter - maxgain
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID GAIN SPECIFICATION ---"
				   checkMinus "$1"
				   maxgain=`expr "$1" : '\([.0-9]*\)'`
				   [ "$maxgain" = "" ] && errMsg "--- GAIN=$maxgain MUST BE A POSITIVE FLOATING POINT VALUE (with no sign) ---"
				   maxgaintest=`echo "$maxgain <= 0" | bc`
				   [ $maxgaintest -eq 1 ] && errMsg "--- GAIN=$maxgain MUST BE A POSITIVE FLOATING POINT VALUE ---"
				   ;;
		    -d)    # dflag
				   dflag="true"
				   ;;
			 -)    # 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"
	[ "$stats" = "false" ] && outfile="$2"
fi


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

# test that outfile provided
if [ "$stats" = "false" ]
	then
	[ "$outfile" = "" ] && errMsg "NO OUTPUT FILE SPECIFIED"
fi


# get im 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`


# test for hdri enabled
# NOTE: must put grep before trap using ERR in case it does not find a match
if [ "$im_version" -ge "07000000" ]; then
	hdri_on=`convert -version | grep "HDRI"`	
else
	hdri_on=`convert -list configure | grep "enable-hdri"`
fi


# setup temporary images and auto delete upon exit
# create temp directory
dir="$tmpdir/SPACE2.$$"
mkdir "$dir" || {
  echo >&2 "UNABLE TO CREATE WORKING DIR \"$dir\" -- ABORTING"
  exit 10
}
trap "rm -rf $dir;" 0
trap "rm -rf $dir; exit 1" 1 2 3 10 15
trap "rm -rf $dir; exit 1" ERR


# colorspace RGB and sRGB swapped between 6.7.5.5 and 6.7.6.7 
# though probably not resolved until the latter
# then -colorspace gray changed to linear between 6.7.6.7 and 6.7.8.2 
# then -separate converted to linear gray channels between 6.7.6.7 and 6.7.8.2,
# though probably not resolved until the latter
# so -colorspace HSL/HSB -separate and -colorspace gray became linear
# but we need to use -set colorspace RGB before using them at appropriate times
# so that results stay as in original script
# The following was determined from various version tests using space.
# with IM 6.7.4.10, 6.7.6.10, 6.7.9.0
if [ "$im_version" -lt "06070607" -o "$im_version" -gt "06070707" ]; then
	setcspace="-set colorspace RGB"
else
	setcspace=""
fi
if [ "$im_version" -lt "06070606" -o "$im_version" -gt "06070707" ]; then
	cspace="RGB"
else
	cspace="sRGB"
fi
# no need for setcspace for grayscale or channels after 6.8.5.4
if [ "$im_version" -gt "06080504" ]; then
	setcspace=""
	cspace="sRGB"
fi


# set up for autolevel
if [ "$autolevel" = "separately" ]; then
	aproc="-channel rgb -auto-level"
elif [ "$autolevel" = "together" ]; then
	aproc="-auto-level"
elif [ "$autolevel" = "off" ]; then
	aproc=""
fi


# read input and test if valid
if ! convert -quiet "$infile" $aproc +repage "$dir/tmpA.mpc"; then
	errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---"
fi


convert $dir/tmpA.mpc $setcspace -colorspace Gray $dir/tmp0.mpc
mean=`convert $dir/tmp0.mpc -format "%[fx:mean]" info:`
std=`convert $dir/tmp0.mpc -format "%[fx:standard_deviation]" info:`
#echo "mean=$mean; std=$std;"


if [ "$stats" = "true" ]; then
	mean100=`convert xc: -format "%[fx:$mean*100]" info:`
	std100=`convert xc: -format "%[fx:$std*100]" info:`
	echo ""
	echo "Mean (0-100) = $mean100"
	echo "Std (0-...) = $std100"
	echo ""
	exit 0
fi


#compute brightness and contrast amounts
bri=`convert xc: -format "%[fx:$bri*pow($dmean/$mean,2.5)]" info:`
con=`convert xc: -format "%[fx:$con*($dstd/$std)]" info:`
echo "bri=$bri; con=$con;"


# test bri and con if zero, positive or negative
# note values less than 0.0001 are essentially zero, but -sigmoidal-contrast fails at exactly zero
britest=`convert xc: -format "%[fx:abs($bri)<0.0001?0:($bri>0?1:-1)]" info:`
contest=`convert xc: -format "%[fx:abs($con)<0.0001?0:($con>0?1:-1)]" info:`
echo "britest=$britest; contest=$contest;"


if [ $britest -eq 0 -a $contest -eq 1 ]; then
	# positive contrast only
	bproc=""
	cproc="-sigmoidal-contrast $con,50%"

elif [ $britest -eq 0 -a $contest -eq -1 ]; then
	bproc=""
	cproc="+sigmoidal-contrast $con,50%"

elif [ $britest -eq 1 -a $contest -eq 0 ]; then
	# positive brightness only
	bproc="-sigmoidal-contrast $bri,0%"
	cproc=""

elif [ $britest -eq -1 -a $contest -eq 0 ]; then
	# negative brightness only
	bproc="+sigmoidal-contrast $bri,0%"
	cproc=""

elif [ $britest -eq 1 -a $contest -eq 1 ]; then
	# positive brightness and positive contrast
	bproc="-sigmoidal-contrast $bri,0%"
	cproc="-sigmoidal-contrast $con,50%"

elif [ $britest -eq 1 -a $contest -eq -1 ]; then
	# positive brightness and negative contrast
	bproc="-sigmoidal-contrast $bri,0%"
	cproc="+sigmoidal-contrast $con,50%"

elif [ $britest -eq -1 -a $contest -eq 1 ]; then
	# negative brightness and positive contrast
	bproc="+sigmoidal-contrast $bri,0%"
	cproc="-sigmoidal-contrast $con,50%"

elif [ $britest -eq -1 -a $contest -eq -1 ]; then
	# negative brightness and negative contrast
	bproc="+sigmoidal-contrast $bri,0%"
	cproc="+sigmoidal-contrast $con,50%"
fi


# apply bri and con as preprocess before getting mean and std images
# also get new std value
# only small difference if use old std value
std=`convert $dir/tmpA.mpc $cproc $bproc -write $dir/tmpA.mpc \
	$setcspace -colorspace Gray -format "%[fx:standard_deviation]" info:`
#echo "new std=$std;"


# convert window to a resize factor
rf=`convert xc: -format "%[fx:100/$window]" info:`

# get reduced mean image
convert $dir/tmpA.mpc -filter box -resize $rf% $dir/tmpM.mpc

# get reduced standard deviation image
# get std = sqrt( ave(x^2) - ave(x)^2 )
# -gamma 2 is equivalent to sqrt
convert \( $dir/tmpA.mpc $dir/tmpA.mpc -compose multiply -composite -filter box -resize $rf% \) \
	\( $dir/tmpM.mpc $dir/tmpM.mpc -compose multiply -composite \) +swap \
	-compose minus -composite -gamma 2 $dir/tmpS.mpc


# expand mean and std images
dim=`convert $dir/tmpA.mpc -format "%wx%h" info:`
convert $dir/tmpM.mpc -resize $dim! $dir/tmpME.mpc
convert $dir/tmpS.mpc -resize $dim! $dir/tmpSE.mpc


# using dstd rather than std is very close.
# kind of a toss-up depending upon user preference
if [ "$dflag" = "false" ]; then
	dstd=`convert xc: -format "%[fx:$std]" info:`
fi

if [ "$hdri_on" = "" ]; then
	dsdmaxgain=`convert xc: -format "%[fx:$std/$maxgain]" info:`
#	echo "dstd=$dstd; dsdmaxgain=$dsdmaxgain"
	convert $dir/tmpA.mpc $dir/tmpME.mpc $dir/tmpSE.mpc -monitor \
		-fx "u[1]+$dstd*(u[0]-u[1])/(u[2]+$dsdmaxgain)" +monitor $dir/tmp3.mpc
else
	dsdmaxgain=`convert xc: -format "%[fx:100*$std/$maxgain]" info:`
#	echo "dstd=$dstd; dsdmaxgain=$dsdmaxgain"

	# first line -- read input image modified by sigmoidal brightness and contrast and the mean and std images
	# second line -- u[2]+$dsdmaxgain; apply second fixed sigmoidal contrast to mean image and add dsdmaxgain
	# third line -- $dstd*(u[0]-u[1]); compute (image - mean) and multiply by dstd
	# fourth line -- $dstd*(u[0]-u[1])/(u[2]+$dsdmaxgain)
	# fifth line -- (u[1])+$dstd*(u[0]-u[1])/(u[2]+$dsdmaxgain)

	convert $dir/tmpA.mpc $dir/tmpME.mpc $dir/tmpSE.mpc \
	\( -clone 2 -evaluate add $dsdmaxgain% \) \
	\( -clone 0 -clone 1 +swap -define compose:clamp=off -compose minus -composite -evaluate multiply $dstd \) \
	\(  -clone 4 -clone 3 +swap -define compose:clamp=off -compose divide -composite \) \
	-delete 0,2,3,4 -define compose:clamp=off -compose plus -composite $dir/tmp3.mpc
fi
	
# modify saturation if desired
if [ "$sat" != "0" ]
	then
	bp=$sat
	wp=`convert xc: -format "%[fx:(100-$sat)]" info:`		
	convert $dir/tmp3.mpc $dir/tmpA.mpc
	convert $dir/tmpA.mpc $setcspace -colorspace LAB -channel R -separate $dir/tmp0.mpc
	convert $dir/tmpA.mpc $setcspace -colorspace LAB -channel G -separate $dir/tmp1.mpc
	convert $dir/tmpA.mpc $setcspace -colorspace LAB -channel B -separate $dir/tmp2.mpc
	convert $dir/tmp1.mpc -level $bp,$wp% $dir/tmp1.mpc
	convert $dir/tmp2.mpc -level $bp,$wp% $dir/tmp2.mpc
	convert $dir/tmp0.mpc -colorspace LAB \
		$dir/tmp0.mpc -compose CopyRed -composite \
		$dir/tmp1.mpc -compose CopyGreen -composite \
		$dir/tmp2.mpc -compose CopyBlue -composite \
		-colorspace $cspace $dir/tmp3.mpc
fi


# write output
convert $dir/tmp3.mpc "$outfile"

exit 0