#!/bin/bash
#
# Developed by Fred Weinhaus 10/2/2017 .......... revised 12/9/2018
#
# ------------------------------------------------------------------------------
# 
# 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: adaptivegamma [-t type] [-c colormode] [-a autolevel] [-s sigma] [-m mix ] 
# [-b base] [-g gain] [-T thresh] infile outfile
# USAGE: adaptivegamma [-h or -help]
#
# OPTIONS:
#
# -t     type          type of adaptive gamma enhancement; choices are lcc, pnae, hybrid; 
#                      default=hybrid
# -c     colormode     colorspace for processing; choices are gray, srgb, lab, hcl, hsl, 
#                      hsv, hsi, ycbcr and ohta; default=gray
# -a     autolevel     autolevel as preprocessing; options are: off, together and 
#                      separately; default=together
# -s     sigma         sigma value for blurring the intensity-like channel; integer>=0;
#                      default=20
# -m     mix           mixing percent for the hybrid blend of lcc and pnae; 
#                      0<=integer<= 100; default=50; 0 is pure lcc, 100 is pure pnae
# -b     base          base value for lcc method; float>0; default=2
# -g     gain          gain value for pnae method; float>=0; default is computed 
#                      automatically from the mean and standard deviation of the image;
#                      nominal value is about 0.5
# -T     thresh        threshold to determine if image is bright or dark; 0<=float<=1; 
#                      default=0.5. If bright, the image will be negated before and 
#                      after processing
#
###
#
# NAME: ADAPTIVEGAMMA
# 
# PURPOSE: To enhance the contrast/brightness in an image using an adaptive gamma method. 
# 
# DESCRIPTION: ADAPTIVEGAMMA is a locally/spatially adaptive gamma technique to enhance 
# an image's brightness and contrast. There are 3 types of adaptive gamma enhancement
# that can be used. The first is the local contrast correction (lcc) method. The second 
# is the parallel nonlinear adaptive enhancement (pnae) method. The third is a hybrid 
# blend of the other two.
#
# These adaptive gamma methods are power law adjustments of the image as defined by 
# OUT=pow(IN,exponent); where exponent = 1/gamma. However, here the exponent is a value 
# that changes spatially according to the local mean M(x,y) of the intensity-like 
# channel of the image.
# 
# In the lcc method, exponent=[base]*M(x,y)-1, where M is an image of the local mean of the
# intensity-like channel achieve by blurring the image and base is nominally 2.
# 
# In the pnae method, exponent=const*[M(x,y)-epsilon]/[1-M(x,y)+epsilon] + gain. The 
# const and epsilon values are constants where const=0.1 and epsilon=0.01. The gain is 
# essentially a global (inverse) gamma value and is nominally about 0.5. But here the 
# default is computed automatically as (log(0.5)/log(M))*($maxstd/min($maxstd,S)), 
# where M is the global mean of the image and S is the global standard deviation of 
# the image and maxstd is a maximum value for the standard deviation above which the 
# standard deviation term equals unity and so does not affect the first mean term.
# 
# Arguments: 
#
# -t type ... TYPE of adaptive gamma enhancement. The choices are lcc (l), pnae (p)
# and hybrid (h). The default=hybrid.
# 
# -c colormode ... COLORMODE is the colorspace for processing. The choices are gray, 
# srgb, lab, hcl, hsl, hsv, hsi, ycbcr and ohta. The default=gray.
# 
# -a autolevel ... AUTOLEVEL as preprocessing step. The choices are: off (o), 
# together (t) and separately (s). The default=together
# 
# -s sigma ... SIGMA value for blurring the intensity-like channel. Values are 
# integer>=0. The default=20. Larger values give slightly more contrast.
# 
# -m mix ... MIX is the mixing percent for the hybrid blend of the lcc and pnae types.
# Values are 0<=integers<= 100. The default=50. 0 is pure lcc and 100 is pure pnae.
# 
# -b base .. BASE value for lcc method. Values are floats>0. The default=2. Larger 
# values reduce the contrast and smaller values increase the contrast.
# 
# -g gain ... GAIN value for pnae method. Values aare float>=0. The default is computed 
# automatically from the global mean and standard deviation of the intensity-like 
# channel of the image. The nominal value is around 0.5.
# 
# -T thresh ... THRESH is a threshold to determine if the image is bright or dark. 
# Values are 0<=float<=1. The default=0.5. If bright, the image will be negated before  
# and after processing.
#
# REFERENCES:
# http://www.ipol.im/pub/art/2011/gl_lcc/article_lr.pdf 
# https://asp-eurasipjournals.springeropen.com/track/pdf/10.1186/1687-6180-2014-70?site=asp.eurasipjournals.springeropen.com
# https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&uact=8&ved=0ahUKEwigiqGb1sjWAhXrlVQKHSM3Bf4QFggrMAE&url=http%3A%2F%2Fwww.ivl.disco.unimib.it%2Fdownload%2Fcapra2010contrast-image.pdf&usg=AFQjCNFcvsunTdhTjNTd6goxo3wZCPvEYQ
# https://en.wikipedia.org/wiki/Gamma_correction
# 
# NOTE: This script may be a bit slow due to the use of -fx.
#
# 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
# external args
type="hybrid"			#lcc, pnae, hybrid
colormode="gray"		# gray, srgb, lab, hcl, ycbcr, ohta
autolevel="together"	# together or separate or off
sigma=20				# blur sigma
mix=0.5					# mixing of lcc and pnae for hybrid (mix=1 is pnae, mix=0 is lcc)
base=2					# for lcc and hybrid
gain=""					# nominal 0.5, default computed from mean and midgray
thresh=0.5              # threshold to determine if image is bright or dark

# internal args
midgray=0.5			# for lcc or hybrid
maxstd=0.15			# for lcc or hybrid
const=0.1			# for pnae or hybrid
eps=0.01			# for pnae or hybrid


# 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 -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 18 ]
	then
	errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
	while [ $# -gt 0 ]
		do
		# get parameters
		case "$1" in
	  -h|-help)    # help information
				   echo ""
				   usage2
				   ;;
			-t)    # get  type
				   shift  # to get the next parameter
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID TYPE SPECIFICATION ---"
				   checkMinus "$1"
				   type=`echo "$1" | tr '[A-Z]' '[a-z]'`
				   case "$type" in 
						lcc|l) type="lcc" ;;
						pnae|p) type="pnae" ;;
						hybrid|h) type="hybrid" ;;
						*) errMsg "--- TYPE=$type IS AN INVALID VALUE ---" ;;
					esac
				   ;;
			-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 
						off|o) autolevel="off" ;;
						together|t) autolevel="together" ;;
						separately|s) autolevel="separately" ;;
						*) errMsg "--- AUTOLEVEL=$autolevel IS AN INVALID VALUE ---" ;;
					esac
				   ;;
			-c)    # get  colormode
				   shift  # to get the next parameter
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID COLORMODE SPECIFICATION ---"
				   checkMinus "$1"
				   colormode=`echo "$1" | tr '[A-Z]' '[a-z]'`
				   case "$colormode" in 
						gray|g) colormode="gray" ;;
						srgb|s|r) colormode="srgb" ;;
						lab|l) colormode="lab" ;;
						hcl) colormode="hcl" ;;
						hsl) colormode="hsl" ;;
						hsv) colormode="hsv" ;;
						hsi) colormode="hsi" ;;
						ycbcr|y) colormode="ycbcr" ;;
						ohta|o) colormode="ohta" ;;
						*) errMsg "--- COLORMODE=$colormode IS AN INVALID VALUE ---" ;;
					esac
				   ;;
			-s)    # get sigma
				   shift  # to get the next parameter
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID SIGMA SPECIFICATION ---"
				   checkMinus "$1"
				   sigma=`expr "$1" : '\([0-9]*\)'`
				   [ "$sigma" = "" ] && errMsg "--- SIGMA=$sigma MUST BE A NON-NEGATIVE INTEGER ---"
				   ;;
			-m)    # get mix
				   shift  # to get the next parameter
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID MIX SPECIFICATION ---"
				   checkMinus "$1"
				   mix=`expr "$1" : '\([0-9]*\)'`
				   [ "$mix" = "" ] && errMsg "--- MIX=$mix MUST BE A NON-NEGATIVE INTEGER ---"
				   testA=`echo "$mix > 100" | bc`
				   [ $testA -eq 1 ] && errMsg "--- MIX=$mix MUST BE AN INTEGER BETWEEN 0 AND 100 ---"
				   ;;
			-b)    # base
				   shift  # to get the next parameter - maxgain
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID BASE SPECIFICATION ---"
				   checkMinus "$1"
				   base=`expr "$1" : '\([.0-9]*\)'`
				   [ "$base" = "" ] && errMsg "--- BASE=$base MUST BE A POSITIVE FLOATING POINT VALUE (with no sign) ---"
				   testA=`echo "$base == 0" | bc`
				   [ $testA -eq 1 ] && errMsg "--- BASE=$base MUST BE A POSITIVE FLOATING POINT VALUE ---"
				   ;;
			-g)    # gain
				   shift  # to get the next parameter - maxgain
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID GAIN SPECIFICATION ---"
				   checkMinus "$1"
				   gain=`expr "$1" : '\([.0-9]*\)'`
				   [ "$gain" = "" ] && errMsg "--- GAIN=$gain MUST BE A POSITIVE FLOATING POINT VALUE (with no sign) ---"
				   ;;
			-T)    # thresh
				   shift  # to get the next parameter - maxgain
				   # test if parameter starts with minus sign 
				   errorMsg="--- INVALID THRESH SPECIFICATION ---"
				   checkMinus "$1"
				   thresh=`expr "$1" : '\([.0-9]*\)'`
				   [ "$thresh" = "" ] && errMsg "--- THRESH=$thresh MUST BE A POSITIVE FLOATING POINT VALUE (with no sign) ---"
				   testA=`echo "$thresh > 1" | bc`
				   [ $testA -eq 1 ] && errMsg "--- THRESH=$thresh MUST BE A POSITIVE FLOATING POINT VALUE BETWEEN 0 AND 1 ---"
				   ;;
			 -)    # 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 temporary images
tmpA1="$dir/space3_1_$$.mpc"
tmpB1="$dir/space3_1_$$.cache"
trap "rm -f $tmpA1 $tmpB1; exit 0" 0
trap "rm -f $tmpA1 $tmpB1; exit 1" 1 2 3 15

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


# test threshold for negating if image is too bright
test=`convert "$infile" -format "%[fx:mean>$thresh?1:0]" info:`
if [ $test -eq 1 ]; then
	negating="-negate"
else
	negating=""
fi

# read the input image into the temporary cached image and test if valid
convert -quiet -regard-warnings "$infile" $negating $aproc +repage $tmpA1 ||
	echo "--- 1 FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO size  ---"

# set up gain
if [ "$type" != "lcc" -a "$gain" = "" ]; then
	# gain=inverse gamma with boost for very low standard deviations (very low contrast)
	#gain=`convert $tmpA1 -format "%[fx:log(0.5)/log(mean)]" info:`
	gain=`convert $tmpA1 -format "%[fx:(log($midgray)/log(mean))*($maxstd/min($maxstd,standard_deviation))]" info:`
fi
#echo "gain=$gain"

# convert mix to fraction
mix=`convert xc: -format "%[fx:$mix/100]" info:`


# set up channel for colormode
[ "$colormode" = "lab" -o "$colormode" = "ycbcr" -o "$colormode" = "ohta" ] && channel=0
[ "$colormode" = "hcl" -o "$colormode" = "hsl" -o "$colormode" = "hsv" -o "$colormode" = "hsi" ] && channel=2


if [ "$type" = "lcc" ] || [ "$type" = "hybrid" -a "$mix" = "0" ]; then
	if [ "$colormode" = "gray" ]; then
		convert $tmpA1 \( +clone -colorspace $colormode -blur 0x$sigma \) -monitor \
		-fx "pow(u,pow($base,2*v-1))" +monitor $negating "$outfile"

	elif [ "$colormode" = "srgb" ]; then
		convert $tmpA1 -colorspace sRGB -separate \
			\( -clone 0 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,pow($base,2*v-1))" +monitor \) \
			\( -clone 1 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,pow($base,2*v-1))" +monitor \) \
			\( -clone 2 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,pow($base,2*v-1))" +monitor \) \
			-delete 0-2 -set colorspace sRGB -combine -colorspace sRGB $negating \
			"$outfile"

	else
		convert $tmpA1 -colorspace $colormode -separate \
			\( -clone $channel \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,pow($base,2*v-1))" +monitor \) \
			-swap ${channel},3 +delete -set colorspace $colormode -combine -colorspace sRGB $negating \
			"$outfile"
	fi

elif [ "$type" = "pnae" ] || [ "$type" = "hybrid" -a "$mix" = "1" ]; then

	if [ "$colormode" = "gray" ]; then
		convert $tmpA1 \( +clone -colorspace $colormode -blur 0x$sigma \) -monitor \
		-fx "pow(u,$const*(v+$eps)/(1-v+$eps)+$gain)" +monitor $negating "$outfile"

	elif [ "$colormode" = "srgb" ]; then
		convert $tmpA1 -colorspace sRGB -separate \
			\( -clone 0 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$const*(v+$eps)/(1-v+$eps)+$gain)" +monitor \) \
			\( -clone 1 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$const*(v+$eps)/(1-v+$eps)+$gain)" +monitor \) \
			\( -clone 2 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$const*(v+$eps)/(1-v+$eps)+$gain)" +monitor \) \
			-delete 0-2 -set colorspace sRGB -combine -colorspace sRGB $negating \
			"$outfile"

	else
		convert $tmpA1 -colorspace $colormode -separate \
			\( -clone $channel \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$const*(v+$eps)/(1-v+$eps)+$gain)" +monitor \) \
			-swap ${channel},3 +delete -set colorspace $colormode -combine -colorspace sRGB $negating \
			"$outfile"
	fi
	

elif [ "$type" = "hybrid" ]; then

	if [ "$colormode" = "gray" ]; then
		convert $tmpA1 \( +clone -colorspace $colormode -blur 0x$sigma \) -monitor \
		-fx "pow(u,$mix*($const*(v+$eps)/(1-v+$eps)+$gain)+(1-$mix)*pow($base,2*v-1))" +monitor $negating "$outfile"

	elif [ "$colormode" = "srgb" ]; then
		convert $tmpA1 -colorspace sRGB -separate \
			\( -clone 0 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$mix*($const*(v+$eps)/(1-v+$eps)+$gain)+(1-$mix)*pow($base,2*v-1))" +monitor \) \
			\( -clone 1 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$mix*($const*(v+$eps)/(1-v+$eps)+$gain)+(1-$mix)*pow($base,2*v-1))" +monitor \) \
			\( -clone 2 \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$mix*($const*(v+$eps)/(1-v+$eps)+$gain)+(1-$mix)*pow($base,2*v-1))" +monitor \) \
			-delete 0-2 -set colorspace sRGB -combine -colorspace sRGB $negating \
			"$outfile"

	else
		convert $tmpA1 -colorspace $colormode -separate \
			\( -clone $channel \( +clone -blur 0x$sigma \) -monitor -fx "pow(u,$mix*($const*(v+$eps)/(1-v+$eps)+$gain)+(1-$mix)*pow($base,2*v-1))" +monitor \) \
			-swap ${channel},3 +delete -set colorspace $colormode -combine -colorspace sRGB $negating \
			"$outfile"
	fi

fi

exit 0


