#!/bin/bash
#
# Developed by Fred Weinhaus 6/25/2009 .......... revised 10/8/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: bcmatch [-c colormode] [-s satmode] infile1 infile2 outfile
# USAGE: bcmatch [-h or -help]
#
# OPTIONS:
#
# -c      colormode       colorspace/channel to use to compute 
#                         brightness and contrast statistics; 
#                         choices are: gray, intensity, luminance, 
#                         lightness, brightness, average, magnitude, 
#                         rgb; default=gray
# -s      satmode         colorspace mode to use to modify saturation;
#                         choices are: HSB or HSL; default is no 
#                         change in saturation.
# 
# infile2 will be modified to match infile1
# 
###
#
# NAME: BCMATCH 
# 
# PURPOSE: To modify one image to try to match its brightness, contrast and
# optionally saturation to that of another image.
# 
# DESCRIPTION: BCMATCH modifies one image to try to match its brightness and 
# contrast and optionally saturation to that of another image. The second 
# image will be modified to match the first image. Saturation changes 
# only apply to color images, not grayscale ones. The choice of colormode 
# determines what colorspace or channel will be used to measure the mean 
# and standard deviation of the two images. The differences in mean and 
# standard deviation are equated to brightness and contrast changes, which 
# then are converted to intercept and slope of a linear transformation. 
# This linear transformation is then applied to the second image to make 
# the output. If colormode=rgb, then each channel of the second image will 
# be processed idependently. For all other colormodes, a common transformation 
# will be applied to all channels of the second image.
#
# IMPORTANT: As this is a linear transformation, it may not do justice 
# to any non-linear differences between the two images, such as gamma 
# modifications. Furthermore, once information is lost, especially due 
# to too much contrast change, it generally cannot be adequately recovered.
# 
# OPTIONS: 
# 
# -c colormode ... COLORMODE is the colorspace/channel to use to compute
# the brightness and contrast values of the two images. The choices are: 
# gray, intensity, luminance, lightness, brightness, average, magnitude 
# and rgb. Values of gray and intensity are equivalent. If colormode=rgb, 
# then each channel of the image will be processed independently. The 
# default is gray.
# 
# Gray or Intensity uses statistics from -colorspace Gray.
# Luminance uses statistics from -colorspace Rec709Luma.
# Lightness uses statistics from the lightness channel of -colorspace HSL.
# Brightness uses statistics from the brightness channel of -colorspace HSB.
# Average uses statistics from the first channel of -colorspace OHTA.
# Magnitude uses aggregate statistics from all the channels.
# RGB uses statistics independently from each channel of -colorspace sRGB/RGB.
# See definitions at: 
# http://www.imagemagick.org/script/command-line-options.php#colorspace
# 
# Note: generally there are only slight differences between the various 
# non-rgb colormode results. Colormode=rgb can cause color balance shifts.
# 
# -s satmode ... SATMODE is the choice of saturation colorspace to use 
# when modifying the saturation. Choices are: HSB and HSL. The default 
# is to make no change in saturation. If used, I recommend HSB. This 
# argument is only applicable when both images are color (not grayscale).
# 
# REQUIRES: IM version 6.4.8-9 or higher due to the use of -function polynomial
# 
# 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
colormode="gray"	# various choices: gray, luminance ... and rgb
satmode=""			# HSB or HSL


# 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"
	}

getSlopeIntercept()
	{
	img1="$1"
	img2="$2"
	# y=ax+b; a=slope, b=intercept; x and y in range 0 to 1
	# bri=fractional change in mean values
	# con=fractional change in std values
	# brightness change => y=(x+0.5*bri)
	# contrast change => y=(x-0.5)*(1+con)+0.5; so slope pivots about 0.5
	# use 0.5*bri as range is from assumed midrange 
	# thus bri=-+1 makes y go between 0 and 1 when x=0.5
	# con=1 makes slope double
	# combine as: y=(x+0.5*bri-0.5)*(1+con)+0.5
	# a=(1+con)
	# bb=bri
	# thus y==(x+0.5*bb-0.5)*a+0.5=a*x+(0.5*a*bb-0.5*a+0.5)=a*x+0.5*(a*bb-a+1)
	# so that intercept b=0.5*(a*bb-a+1)
	m1=`convert $img1 -format "%[mean]" info:`
	m2=`convert $img2 -format "%[mean]" info:`
	s1=`convert $img1 -format "%[standard-deviation]" info:`
	s2=`convert $img2 -format "%[standard-deviation]" info:`
	a=`convert xc: -format "%[fx:1+($s1-$s2)/$s1]" info:`
	bb=`convert xc: -format "%[fx:($m1-$m2)/$m1]" info:`
	b=`convert xc: -format "%[fx:0.5*($a*$bb-$a+1)]" info:`
	#echo "m1=$m1; m2=$m2; s1=$s1; s2=$s2; a=$a; bb=$bb; b=$b"
	}

# test for correct number of arguments and get values
if [ $# -eq 0 ]
	then
	# help information
   echo ""
   usage2
   exit 0
elif [ $# -gt 7 ]
	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
					   ;;
				-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) ;;
					   		intensity) ;;
					   		luminance) ;;
					   		lightness) ;;
					   		brightness) ;;
					   		average) ;;
					   		magnitude) ;;
					   		rgb) ;;
					   		*) errMsg "--- COLORMODE=$colormode IS AN INVALID VALUE ---" 
					   	esac
					   ;;
				-s)    # get  satmode
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID SATMODE SPECIFICATION ---"
					   checkMinus "$1"
					   satmode=`echo "$1" | tr '[A-Z]' '[a-z]'`
					   case "$satmode" in 
					   		hsb) ;;
					   		hsl) ;;
					   		*) errMsg "--- SATMODE=$satmode IS AN INVALID VALUE ---" 
					   	esac
					   ;;
				 -)    # 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
	infile1="$1"
	infile2="$2"
	outfile="$3"
fi

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

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

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


# test for minimum IM version required
# IM 6.4.8.9 or higher to conform to -function polynomial
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`
[ "$im_version" -lt "06040809" ] && errMsg "--- REQUIRES IM VERSION 6.4.6-9 OR HIGHER ---"


# test if infiles are grayscale
# NOTE: must put grep before trap using ERR in case it does not find a match
# NOTE: removed -ping; cause %r to report gray rather than srgb for truecolor images 
gray1="no"
gray2="no"
grayscale1=`convert "$infile1" -format "%[colorspace]" info:`
typegray1=`convert "$infile1" -format '%r' info: | grep 'Gray'`
grayscale2=`convert "$infile2" -format "%[colorspace]" info:`
typegray2=`convert "$infile2" -format '%r' info: | grep 'Gray'`
[ "$grayscale1" = "Gray" -o "$typegray1" != "" ] && gray1="yes"
[ "$grayscale2" = "Gray" -o "$typegray2" != "" ] && gray2="yes"
#echo "gray1=$gray1; gray2=$gray2"


# set up temporary files
tmpA1="$dir/bcmatch_1_$$.mpc"
tmpB1="$dir/bcmatch_1_$$.cache"
tmpA2="$dir/bcmatch_2_$$.mpc"
tmpB2="$dir/bcmatch_2_$$.cache"
tmpIA1="$dir/bcmatch_I1_$$.mpc"
tmpIB1="$dir/bcmatch_I1_$$.cache"
tmpIA2="$dir/bcmatch_I2_$$.mpc"
tmpIB2="$dir/bcmatch_I2_$$.cache"
tmpRA1="$dir/bcmatch_R1_$$.mpc"
tmpRB1="$dir/bcmatch_R1_$$.cache"
tmpRA2="$dir/bcmatch_R2_$$.mpc"
tmpRB2="$dir/bcmatch_R2_$$.cache"
tmpGA1="$dir/bcmatch_G1_$$.mpc"
tmpGB1="$dir/bcmatch_G1_$$.cache"
tmpGA2="$dir/bcmatch_G2_$$.mpc"
tmpGB2="$dir/bcmatch_G2_$$.cache"
tmpBA1="$dir/bcmatch_B1_$$.mpc"
tmpBB1="$dir/bcmatch_B1_$$.cache"
tmpBA2="$dir/bcmatch_B2_$$.mpc"
tmpBB2="$dir/bcmatch_B2_$$.cache"
tmpSA1="$dir/bcmatch_S1_$$.mpc"
tmpSB1="$dir/bcmatch_S1_$$.cache"
tmpSA2="$dir/bcmatch_S2_$$.mpc"
tmpSB2="$dir/bcmatch_S2_$$.cache"
tmpOA2="$dir/bcmatch_O2_$$.mpc"
tmpOB2="$dir/bcmatch_O2_$$.cache"
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpIA1 $tmpIB1 $tmpIA2 $tmpIB2 $tmpRA1 $tmpRB1 $tmpRA2 $tmpRB2 $tmpGA1 $tmpGB1 $tmpGA2 $tmpGB2 $tmpBA1 $tmpBB1 $tmpBA2 $tmpBB2 $tmpSA1 $tmpSB1 $tmpSA2 $tmpSB2 $tmpOA2 $tmpOB2;" 0
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpIA1 $tmpIB1 $tmpIA2 $tmpIB2 $tmpRA1 $tmpRB1 $tmpRA2 $tmpRB2 $tmpGA1 $tmpGB1 $tmpGA2 $tmpGB2 $tmpBA1 $tmpBB1 $tmpBA2 $tmpBB2 $tmpSA1 $tmpSB1 $tmpSA2 $tmpSB2 $tmpOA2 $tmpOB2; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpIA1 $tmpIB1 $tmpIA2 $tmpIB2 $tmpRA1 $tmpRB1 $tmpRA2 $tmpRB2 $tmpGA1 $tmpGB1 $tmpGA2 $tmpGB2 $tmpBA1 $tmpBB1 $tmpBA2 $tmpBB2 $tmpSA1 $tmpSB1 $tmpSA2 $tmpSB2 $tmpOA2 $tmpOB2; exit 1" ERR

# read the input images into the temp files and test validity.
convert -quiet "$infile1" +repage "$tmpA1" ||
	errMsg "--- FILE $infile1 DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE  ---"

convert -quiet "$infile2" +repage "$tmpA2" ||
	errMsg "--- FILE $infil2 DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE  ---"


# NO LONGER USED
: <<COMMENT
# extract alpha channel if exists
alpha=`convert $tmpA2 -format "%A" info:`
if [ "$alpha" = "True" ]; then
	convert $tmpA2 -alpha extract $tmpOA2
fi
#echo "alpha=$alpha"

# test if infile2 is PseudoClass and RGB or Gray and if matte
type2=`convert $tmpA2 -format '%r' info:`
if [ "$type2" = "PseudoClassRGB" ]; then
	reduction="-type Palette"
elif [ "$type2" = "PseudoClassRGBMatte" ]; then
	reduction="-type Palette $tmpOA2 -compose copy_opacity -composite"
elif [ "$type2" = "PseudoClassGray" ]; then
	reduction="-type Grayscale"
elif [ "$type2" = "PseudoClassGrayMatte" ]; then
	reduction="-type Grayscale $tmpOA2 -compose copy_opacity -composite"
else
	reduction=""
fi
#echo "type2=$type2; reduction=$reduction"
COMMENT
reduction=""

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

# 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 autolevel.
# with IM 6.7.4.10, 6.7.6.10, 6.7.8.6
if [ "$im_version" -lt "06070606" -o "$im_version" -gt "06070707" ]; then
	cspace="RGB"
else
	cspace="sRGB"
fi
if [ "$im_version" -lt "06070607" -o "$im_version" -gt "06070707" ]; then
	setcspace="-set colorspace RGB"
else
	setcspace=""
fi
# no need for setcspace for grayscale or channels after 6.8.5.4
if [ "$im_version" -gt "06080504" ]; then
	setcspace=""
	cspace="sRGB"
fi


# convert infile1 to colormode if not already gray
if [ "$gray1" = "no" ]; then
	if [ "$colormode" = "intensity" -o "$colormode" = "gray" ]; then
		convert $tmpA1 $setcspace -colorspace Gray $tmpIA1
	elif [ "$colormode" = "luminance" -a "$im_version" -ge "07000000" ]; then
		convert $tmpA1 $setcspace -grayscale Rec709Luma $tmpI1
	elif [ "$colormode" = "luminance" -a "$im_version" -lt "07000000" ]; then
		convert $tmpA1 $setcspace -colorspace Rec709Luma $tmpI1
	elif [ "$colormode" = "lightness" ]; then
		convert $tmpA1 $setcspace -colorspace HSL -channel B -separate $tmpIA1
	elif [ "$colormode" = "brightness" ]; then
		convert $tmpA1 $setcspace -colorspace HSB -channel B -separate $tmpIA1
	elif [ "$colormode" = "average" ]; then
		convert $tmpA1 $setcspace -colorspace OHTA -channel R -separate $tmpIA1
	elif [ "$colormode" = "magnitude" ]; then
		convert $tmpA1 $tmpIA1
	elif [ "$colormode" = "rgb" ]; then
		convert $tmpA1 $setcspace -channel R -separate $tmpRA1
		convert $tmpA1 $setcspace -channel G -separate $tmpGA1
		convert $tmpA1 $setcspace -channel B -separate $tmpBA1
	fi
	if [ "$satmode" != "" ]; then
		convert $tmpA1 $setcspace -colorspace $satmode -channel G -separate $tmpSA1
	fi	
fi

# convert infile2 to colormode if not already gray
if [ "$gray2" = "no" ]; then
	if [ "$colormode" = "intensity" -o "$colormode" = "gray" ]; then
		convert $tmpA2 $setcspace -colorspace Gray $tmpIA2
	elif [ "$colormode" = "luminance" ]; then
		convert $tmpA2 $setcspace -colorspace Rec709Luma $tmpIA2
	elif [ "$colormode" = "lightness" ]; then
		convert $tmpA2 $setcspace -colorspace HSL -channel B -separate $tmpIA2
	elif [ "$colormode" = "brightness" ]; then
		convert $tmpA2 $setcspace -colorspace HSB -channel B -separate $tmpIA2
	elif [ "$colormode" = "average" ]; then
		convert $tmpA2 $setcspace -colorspace OHTA -channel R -separate $tmpIA2
	elif [ "$colormode" = "magnitude" ]; then
		convert $tmpA2 $tmpIA2
	elif [ "$colormode" = "rgb" ]; then
		convert $tmpA2 $setcspace -channel R -separate $tmpRA2
		convert $tmpA2 $setcspace -channel G -separate $tmpGA2
		convert $tmpA2 $setcspace -channel B -separate $tmpBA2
	fi
	if [ "$satmode" != "" ]; then
		convert $tmpA2 $setcspace -colorspace $satmode -channel G -separate $tmpSA2
	fi	
fi	

# process images
if [ "$gray1" = "yes" -a "$gray2" = "yes" ]; then
	getSlopeIntercept "$tmpA1" "$tmpA2"
	convert $tmpA2 -function polynomial "$a,$b" $reduction $tmpA2
elif [ "$gray1" = "no" -a "$gray2" = "yes" -a "$colormode" != "rgb" ]; then
	getSlopeIntercept "$tmpIA1" "$tmpA2"
	convert $tmpA2 -function polynomial "$a,$b" $reduction $tmpA2
elif [ "$gray1" = "yes" -a "$gray2" = "no" -a "$colormode" != "rgb" ]; then
	getSlopeIntercept "$tmpA1" "$tmpIA2"
	convert $tmpA2 -function polynomial "$a,$b" $reduction $tmpA2
elif [ "$colormode" != "rgb" ]; then
	getSlopeIntercept "$tmpIA1" "$tmpIA2"
	convert $tmpA2 -function polynomial "$a,$b" $reduction $tmpA2
else
	getSlopeIntercept "$tmpRA1" "$tmpRA2"
	convert $tmpRA2 -function polynomial "$a,$b" $tmpRA2
	getSlopeIntercept "$tmpGA1" "$tmpGA2"
	convert $tmpGA2 -function polynomial "$a,$b" $tmpGA2
	getSlopeIntercept "$tmpBA1" "$tmpBA2"
	convert $tmpBA2 -function polynomial "$a,$b" $tmpBA2
	convert $tmpRA2 $tmpGA2 $tmpBA2 -combine -colorspace $cspace $reduction $tmpA2
fi

if [ "$satmode" != "" ]; then
	getSlopeIntercept "$tmpSA1" "$tmpSA2"
	satpct=`convert xc: -format "%[fx:100*(1+$bb)]" info:`
	convert $tmpA2 -set option:modulate:colorspace $satmode \
		-modulate 100,${satpct} "$outfile"
else
	convert $tmpA2 $outfile
fi
exit 0