#!/bin/bash
#
# Developed by Fred Weinhaus 3/31/2010 .......... revised 11/22/2020
#
# ------------------------------------------------------------------------------
# 
# 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: innercrop [-o ocolor] [-p prep] [-f fuzzval] [-t thresholds] [-b biases] [-u ucolor] [-m mode] [-g gcolor] [-s swidth] [-i is360] [-r rmse] infile outfile
# USAGE: innercrop [-help]
#
# OPTIONS:
#
# -o      ocolor	       	color of outer region to be cropped out; 
#                           Any valid IM color; default=black
# -p      prep              preprocessing trim type; choices are 1 or 2;
#                           1=fuzzy trim; 2=fuzzy floodfill trim; default=1
# -f      fuzzval           fuzz value in percent to use for preprocessing;
#                           0<=float<=100; default=0
# -t      thresholds        four optional threshold values, one for each 
#                           processing pass; comma separate list; values 
#                           are floats>=0; default=",,,," means all four 
#                           are computed automatically
# -b      biases            four optional biases applied as multipliers of 
#                           automatic thresholds, only; values are floats>=0; 
#                           default="1,1,1,1"
# -u      ucolor	       	unused color in image to use when converting 
#                           image to a binary mask; Any valid IM color;
#                           default="none"
# -m      mode              mode for output image; choices are crop and box;
#                           crop means to crop the input image; box means 
#                           to draw a rectangular box showing where the image
#                           would be cropped; default=crop
# -g      gcolor	       	color to use when mode=box; Any valid IM color;
#                           default=red
# -s      swidth            strokewidth to use when mode=box; integer>0;
#                           default=1
# -i      is360             test if cropped image is a 360 panorama; 
#                           yes means do test; no means do not do test;
#                           default=no
# -r      rmse              rmse threshold value for determining if is 360 
#                           panorama; float>0; default=.05
#
###
#
# NAME: INNERCROP 
# 
# PURPOSE: To crop an image to a rectangle that will just trim 
# any irregularly shaped outer boundary color.
# 
# DESCRIPTION: INNERCROP crops an image to a rectangle that will 
# just trim any irregularly shaped outer boundary color. The image is first 
# trimmed and then converted to a binary mask. The mask is processed to one 
# column and one row and thresholded to find the first and last occurences of 
# white, which determine the top and bottom and left an right sides for 
# cropping. This is repeated a second time to get the final crop coordinates.
# 
# 
# OPTIONS: 
# 
# -o ocolor ... OCOLOR is the color of the outer region that is to be cropped 
# out of the image. Any valid IM color is allowed.
# 
# -p prep ... PREP is the preprocessing trim type to be used. Choices are 
# either 1 or 2. A value of 1 means to simply do a fuzzy trim. A value of 2 
# means to do a fuzzy floodfill trim. The latter is useful when the image 
# itself contains the ocolor interior to the outer region. The default=1
# 
# -f fuzzval ... FUZZVAL is the fuzz factor in percent used in the 
# preprocessing step. The choice of value is important if the outer color 
# is not uniform as may be the case with jpeg compression. Typical values 
# in those cases are a few percent. Over cropping can occur if the fuzzval 
# is too high. Values are 0<=float<=100. The default=0
# 
# -t thresholds ... THRESHOLDS are four comma separate combinations of 
# non-negative floating point values or null values (no spaces). 
# The default=",,," which indicates that each of the four thresholds is to 
# be computed automatically. Non-null values may be useful if the 
# automatic thresholds do not provide a good result and one desires to 
# try to improve the result.
# 
# -b biases ... BIASES are four comma separate non-negative floating point 
# values that are used as multipliers of the automatically computed thresholds.
# The corresponding bias is not used when a non-null value is proved for a 
# given threshold. The default="1,1,1,1". The biases may be useful if the 
# automatic thresholds do not provide a good result and one desires to 
# try to improve the result.
# 
# -u ucolor ... UCOLOR is any unused (safe) color in the image that will be 
# used to convert the image into a mask. Any valid IM color is allowd. The 
# default=none (i.e. transparent) and assumes the image is fully opaque.
# 
# -m mode ... MODE is the output mode. The choices are: crop and box. When 
# mode=crop, the ouput image will be the cropped version of the input image. 
# When mode=box, a graphic box outline will be drawn on the input image showing 
# where the cropping would take place. The default=crop.
# 
# -g gcolor ... GCOLOR is the color of the graphic box outline to be used 
# when mode=box. Any valid IM color is allowed. The default=red.
# 
# -s swidth ... SWIDTH is the strokewidth to use when drawing the graphic 
# box outline. Values are integers>0. The default=1.
# 
# -i is360 ... IS360 is a flag to perform a test to determine if the image 
# is a 360 degree panorama. The choices are yes or no. The default=no, which 
# means do not test for 360 degree panorama.
# 
# -r rmse ... RMSE is the root mean squared error threshold used to compare 
# the first and last columns to determine if they are similar enough. Values 
# are floats>0. The default is 0.05. If the compare test has an rmse value 
# less than this threshold, then the image is determined to be a 360 degree 
# panorama.
# 
# 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
ocolor="black"		# color of outer region;
prep=1				# 1=fuzzy trim, 2=fuzzy floodfill and trim
fuzzval=0			# percent fuzz for background black
thresh=",,,"		# defaults for y,x,y2,x2 thresholds; can use ty1,tx1,ty2,tx2; default=automatic
bias="1,1,1,1"		# multiplier bias for thresholds by,bx,by2,bx2 relative to automatic thresholds
ucolor="none"		# unused safe color
mode="crop"			# output image mode: crop or show graphic box
gcolor="red"      	# graphic box color
swidth="1"			# strokewidth
is360test="no"		# test if pano is 360 or not; yes or no
rmse=".05"			# rmse threshold for determining if is 360 pano

# 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 24 ]
	then
	errMsg "--- TOO MANY ARGUMENTS WERE PROVIDED ---"
else
	while [ $# -gt 0 ]
		do
			# get parameter values
			case "$1" in
		     -help)    # help information
					   echo ""
					   usage2
					   exit 0
					   ;;
				-o)    # get ocolor
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID OCOLOR SPECIFICATION ---"
					   checkMinus "$1"
					   ocolor="$1"
					   ;;
				-p)    # get prep
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID PREP SPECIFICATION ---"
					   checkMinus "$1"
					   prep="$1"
					   [ $prep -ne 1 -a $prep -ne 2 ] && errMsg "--- PREP=$prep MUST BE EITHER 1 OR 2 ---"
					   ;;
				-f)    # get fuzzval
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID FUZZVAL SPECIFICATION ---"
					   checkMinus "$1"
					   fuzzval=`expr "$1" : '\([.0-9]*\)'`
					   fuzzvaltestA=`echo "$fuzzval < 0" | bc`
					   fuzzvaltestB=`echo "$fuzzval > 100" | bc`
					   [ $fuzzvaltestA -eq 1 -o $fuzzvaltestB -eq 1 ] && errMsg "--- FUZZVAL=$fuzzval MUST BE A FLOAT BETWEEN 0 AND 100 ---"
					   ;;
				-t)    # get thresh
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID THRESHOLDS SPECIFICATION ---"
					   checkMinus "$1"
					   thresh=`expr "$1" : '\([,.0-9]*\)'`
					   [ "$thresh" = "" ] && errMsg "--- THRESHOLDS=$thresh MUST CONTAIN ONLY DIGITS, PERIODS AND COMMAS ---"
					   ;;
				-b)    # get bias
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID BIASES SPECIFICATION ---"
					   checkMinus "$1"
					   bias=`expr "$1" : '\([,.0-9]*\)'`
					   [ "$bias" = "" ] && errMsg "--- BIASES=$bias MUST CONTAIN ONLY DIGITS, PERIODS AND COMMAS ---"
					   ;;
				-u)    # get ucolor
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID UCOLOR SPECIFICATION ---"
					   checkMinus "$1"
					   ucolor="$1"
					   ;;
				-m)    # get mode
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID MODE SPECIFICATION ---"
					   checkMinus "$1"
					   mode=`echo "$1" | tr '[A-Z]' '[a-z]'`
					   [ "$mode" != "crop" -a "$mode" != "box" ] && errMsg "--- MODE=$mode MUST BE EITHER CROP OR BOX ---"
					   ;;
				-g)    # get gcolor
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID GCOLOR SPECIFICATION ---"
					   checkMinus "$1"
					   gcolor="$1"
					   ;;
				-s)    # get swidth
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID SWIDTH SPECIFICATION ---"
					   checkMinus "$1"
					   swidth=`expr "$1" : '\([0-9]*\)'`
					   [ "$swidth" = "" ] && errMsg "--- SWIDTH=$swidth MUST BE AN INTEGER ---"
					   swidthtest=`echo "$swidth < 1" | bc`
					   [ $swidthtest -eq 1 ] && errMsg "--- SWIDTH=$swidth MUST BE A POSITIVE INTEGER ---"
					   ;;
				-i)    # get is360test
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID IS360 SPECIFICATION ---"
					   checkMinus "$1"
					   is360test=`echo "$1" | tr '[A-Z]' '[a-z]'`
					   [ "$is360test" != "yes" -a "$is360test" != "no" ] && errMsg "--- IS360=$is360test MUST BE EITHER YES OR NO ---"
					   ;;
				-r)    # get rmse
					   shift  # to get the next parameter
					   # test if parameter starts with minus sign 
					   errorMsg="--- INVALID RMSE SPECIFICATION ---"
					   checkMinus "$1"
					   rmse=`expr "$1" : '\([.0-9]*\)'`
					   rmsetest=`echo "$rmse <= 0" | bc`
					   [ $rmsetest -eq 1 ] && errMsg "--- RMSE=$rmse MUST BE A POSITIVE FLOAT ---"
					   ;;
				 -)    # 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/innercrop_1_$$.mpc"
tmpB1="$dir/innercrop_1_$$.cache"
tmpA2="$dir/innercrop_2_$$.mpc"
tmpB2="$dir/innercrop_2_$$.cache"
tmpA3="$dir/innercrop_3_$$.mpc"
tmpB3="$dir/innercrop_3_$$.cache"
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3;" 0
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3; exit 1" 1 2 3 15
trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3; exit 1" ERR

# 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 innercrop.
# with IM 6.7.4.10, 6.7.6.10, 6.7.8.7
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=""
fi
# NOTE: need this to convert colornames from gray(255,255,255) to white after IM 6.7.7.7
if [ "$im_version" -gt "06070707" ]; then
	setcspace2="-set colorspace sRGB"
else
	setcspace2=""
fi

# read and test input image
if [ $prep -eq 1 ]; then
	convert -quiet "$infile" -fuzz ${fuzzval}% -trim "$tmpA1" || 
		errMsg  "--- FILE $imagefile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE  ---"
elif [ $prep -eq 2 ]; then
#echo "fuzzval=${fuzzval}%"
	convert -quiet "$infile" -bordercolor "$ocolor" -border 2 \
		-fuzz ${fuzzval}% -fill "$ocolor" -draw "color 0,0 floodfill" -trim "$tmpA1" || 
		errMsg  "--- FILE $imagefile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE  ---"
fi

# get virtual canvas offsets
pagex=`convert $tmpA1 -ping -format "%X" info:`
pagey=`convert $tmpA1 -ping -format "%Y" info:`
#echo "pagex=$pagex; pagey=$pagey"

# remove virtual canvas info
convert $tmpA1 +repage $setcspace $tmpA1

# get trimmed image dimensions
ww=`convert $tmpA1 -ping -format "%w" info:`
hh=`convert $tmpA1 -ping -format "%h" info:`
ww1=`convert xc: -format "%[fx:$ww-1]" info:`
hh1=`convert xc: -format "%[fx:$hh-1]" info:`
#echo "ww=$ww; hh=$hh"

# extract thresholds
threshy=`echo $thresh | cut -d, -f1`
threshx=`echo $thresh | cut -d, -f2`
threshy2=`echo $thresh | cut -d, -f3`
threshx2=`echo $thresh | cut -d, -f4`
#echo "threshy=$threshy; threshx=$threshx; threshy2=$threshy2; threshx2=$threshx2"


# extract biases
biasy=`echo $bias | cut -d, -f1`
biasx=`echo $bias | cut -d, -f2`
biasy2=`echo $bias | cut -d, -f3`
biasx2=`echo $bias | cut -d, -f4`
[ "$biasy" = "" -o "$biasx" = "" -o "$biasy2" = "" -o "$biasx2" = "" ] && errMsg "--- BIASES CANNOT BE EMPTY VALUES ---"
#echo "biasy=$biasy; biasx=$biasx; biasy2=$biasy2; biasx2=$biasx2"

# create black and white mask
if [ $prep -eq 1 ]; then
#echo "got here"
#echo "fuzzval=$fuzzval;"
#	convert $tmpA1 -fuzz ${fuzzval}% -fill black -opaque "$ocolor" +fuzz -fill white +opaque black $tmpA2
	convert $tmpA1 -channel rgba -alpha on -fuzz ${fuzzval}% -fill "$ucolor" -opaque "$ocolor" +fuzz \
		-fill white +opaque "$ucolor" -fill black -opaque "$ucolor" -alpha off $tmpA2
elif [ $prep -eq 2 ]; then
#	convert $tmpA1 -fill black -opaque "$ocolor" -fill white +opaque black $tmpA2
	convert $tmpA1 -channel rgba -alpha on -fill "$ucolor" -opaque "$ocolor" \
		-fill white +opaque "$ucolor" -fill black -opaque "$ucolor" -alpha off $tmpA2
fi


#function to process mask vertically or horizontally for y1,y2 or x1,x2
procVertHoriz()
	{
	direction=$1
	width=$2
	height=$3
	xoff=$4
	yoff=$5
	thresh1=$6
	bias=$7

	# use mean as threshold for processing
	if [ "$thresh1" = "" ]; then
		thresh1=`convert $tmpA2[${width}x${height}+$xoff+$yoff] -format "%[fx:max(0.001,100*(1-mean)*$bias)]" info:`
#echo "thresh1=$thresh1; bias=$bias;"
	fi
	
	# compute complement threshold for actual processing
	thresh2=`convert xc: -format "%[fx:100-$thresh1]" info:`
#echo "thresh2=$thresh2"
	
	# get thresholded row or column from potentially cropped mask
	# also compute length for array
	if [ "$direction" = "vertical" ]; then
		convert $tmpA2[${width}x${height}+$xoff+$yoff] -scale 1x${height}! -threshold ${thresh2}% $setcspace2 $tmpA3
		len=$height
		len1=`convert xc: -format "%[fx:$height-1]" info:`
	elif [ "$direction" = "horizontal" ]; then
		convert $tmpA2[${width}x${height}+$xoff+$yoff] -scale ${width}x1! -threshold ${thresh2}% $setcspace2 $tmpA3
		len=$width
		len1=`convert xc: -format "%[fx:$width-1]" info:`
	fi
#echo "len=$len; len1=$len1;"
		
	# convert row or column to text and find first and last white pixel for v1 and v2	
	rcArr=(`convert $tmpA3 txt: | tail -n +2 | sed -n 's/^.*#[^ ]* \([a-z]*\)/\1/p'`)
#echo "${#rcArr[*]}"
	for ((i=0;i<=len;i+=1)); do
		if [ "${rcArr[$i]}" = "white" ]; then
			v1=$i
			break
		fi
	done
	for ((i=len1;i>=0;i-=1)); do
		if [ "${rcArr[$i]}" = "white" ]; then
			v2=$i
			break
		fi
	done
	}


# process vertically 
procVertHoriz "vertical" "$ww" "$hh" "0" "0" "$threshy" "$biasy"
y1=$v1
y2=$v2
threshy=$thresh1
#echo "y1=$y1; y2=$y2; threshy=$threshy;"


# process horizontally
# get cropped height
hhc=`convert xc: -format "%[fx:$y2-$y1+1]" info:`
procVertHoriz "horizontal" "$ww" "$hhc" "0" "$y1" "$threshx" "$biasx"
x1=$v1
x2=$v2
threshx=$thresh1
#echo "x1=$x1; x2=$x2; threshx=$threshx;"


# process vertically 
# get cropped width
wwc=`convert xc: -format "%[fx:$x2-$x1+1]" info:`
procVertHoriz "vertical" "$wwc" "$hhc" "$x1" "$y1" "$threshy2" "$biasy2"
yy1=$v1
yy2=$v2
threshy2=$thresh1
#echo "yy1=$yy1; yy2=$yy2; threshy2=$threshy2;"


# process horizontally
# get cropped height
hhcc=`convert xc: -format "%[fx:$yy2-$yy1+1]" info:`
# get new yoffset
yc=$(($y1+$yy1))
procVertHoriz "horizontal" "$wwc" "$hhcc" "$x1" "$yc" "$threshx2" "$biasx2"
xx1=$v1
xx2=$v2
threshx2=$thresh1
#echo "xx1=$xx1; xx2=$xx2; threshx2=$threshx2;"


# compute crop coord relative to original (untrimmed) image
xa=$(($x1+$xx1+$pagex))
ya=$(($y1+$yy1+$pagey))
xb=$(($x1+$xx2+$pagex))
yb=$(($y1+$yy2+$pagey))
width=`convert xc: -format "%[fx:$xb-$xa+1]" info:`
height=`convert xc: -format "%[fx:$yb-$ya+1]" info:`

echo ""
echo "Thresholds: $threshy,$threshx,$threshy2,$threshx2"

echo ""
echo "Image Crop Coordinates:"
echo "Upper Left Corner: $xa,$ya"
echo "Lower Right Corner: $xb,$yb"
echo "Width x Height: ${width}x${height}"


# generate ouput image
if [ "$mode" = "crop" ]; then
	convert "$infile[${width}x${height}+${xa}+${ya}]" "$outfile"
elif [ "$mode" = "box" ]; then
	convert "$infile" -fill none -stroke "$gcolor" -strokewidth "$swidth" \
		-draw "rectangle $xa,$ya $xb,$yb" "$outfile"
fi

# test if is 360 pano
if [ "$is360test" = "yes" ]; then
	if [ $x1 -eq 0 -a $x2 -eq $ww1 ]; then
		# get first and last columns of trimmed image and do compare
		rmseval=`compare -metric rmse -dissimilarity-threshold 1 \
			$tmpA1[1x${height}+${xa}+${ya}] $tmpA1[1x${height}+${xb}+${ya}] \
			null: 2>&1 | sed -n 's/^[^ ]* [(]\(.*\)[)]$/\1/p'`
		if [ "$rmseval" = "" ]; then
			is360="error"
		else
			test=`convert xc: -format "%[fx:$rmseval<=$rmse?1:0]" info:`
			[ $test -eq 1 ] && is360="true" || is360="false"
		fi
	else
	is360="false"
	fi
	echo "is360=$is360"
fi
echo ""

exit 0



