Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 199 additions & 88 deletions negfix8
Original file line number Diff line number Diff line change
@@ -1,161 +1,272 @@
#!/bin/bash
# negfix8 ver 1.2
# negfix8 ver 1.4b
# GNU General Public License v2
# (C) 2011-2012 JaZ99 - http://www.flickr.com/photos/jaz99/
# (C) 2011-2013 JaZ99 - http://www.flickr.com/photos/jaz99/
# (C) 2014 mszargar

# This little script converts negative (scanned as 16-bit _linear_
# positive, i.e. gamma=1.0), either C-41 color negative or B&W negative,
# positive, i.e. gamma=1.0), either C-41 color negative or B&W negative,
# into the posivite image. Output image might require some
# minor tweaking (gamma, contrast, exposition, etc.).
# Reguires ImageMagick Q16 version (http://www.imagemagick.org/).

# Newest version is always available here:
# https://sites.google.com/site/negfix/

# IMPORTANT: For best result, scanned image MUST NOT contain holder fragments!

# You may wish to fine-tune the default settings:
COMMENT="Processed with negfix8"
GAMMA=2.15
PROF_DIR=$HOME/.negfix8
MEM="-quiet -limit memory 512M" # max phisical memory used
MEM="-quiet -limit memory 512M" # max physical memory used
ZIP="-compress zip"
CS=""
BINNING=""
PROF_NAME=""
SEPARATE=""
NORM=""
AUTOLEVEL=""
UNSHARP=""
TRIM=""
IR=0
MIRROR=""
ROTATE=""
MASKING_CMD=""
MAGICK_TEMPORARY_PATH="/var/tmp" # ImageMagick's temporary directory

# Uncomment below, if you want 20% saturation boost
#SATURATION="-modulate 100,120"

# Starting with ImageMagick 6.8.0-5, there is -poly option
# available, which speeds things up considerably.
# If you have latest ImageMagick installed, comment out
# the next 'convert' command to avoid unnecesary
# testing
HAS_POLY=1
convert -quiet rose: -poly "0,1" - > /dev/null 2>&1 || HAS_POLY=0

# Additional speeding up is possible:
# Locate and delete all occurances of "-evaluate Add 1".
# Those are the safeguards against divide by zero error.
# If you do scan properly, they are not needed at all.
# comment out the SAFE variable.
# It is the safeguard against divide by zero error.
# If you do scan properly, it is not needed at all.
SAFE="-evaluate Add 1"

# End of fine-tune section

CSPACE="-set colorspace RGB -strip"

PROF_CMD="convert $MEM \"\${F}\" $CSPACE \$SEPARATE \$BINNING -shave 10x10 -blur 3x3 -format \""\
"%[fx: minima.r] "\
"%[fx: minima.g] "\
"%[fx: minima.b] "\
"%[fx: ((minima.r/maxima.r)^(1/\$GAMMA))*QuantumRange*0.95] "\
"%[fx: log(maxima.g/minima.g)/log(maxima.r/minima.r)] "\
"%[fx: log(maxima.b/minima.b)/log(maxima.r/minima.r)] "\
"\" info:"

MAKE_PROF=0
if [ ! -d "$MAGICK_TEMPORARY_PATH" ]; then
MAGICK_TEMPORARY_PATH=/tmp
fi
export MAGICK_TEMPORARY_PATH

if [ $# -eq 0 ]; then
# ugly hack to avoid using the function...
set -- `/bin/echo "-h"`
fi

CSPACE="-set colorspace RGB"
MAKE_PROF=1
PROF_NAME="-"

while [ $# -gt 0 ]; do
case $1 in
-m) MIRROR="-flop"; shift;;
-s) SEPARATE="-channel $2 -separate"; shift; shift;;
-m) MIRROR="-flop"; shift;;
-o) ROTATE="-rotate $2"; shift; shift;;
-s) SEPARATE="-channel $2 -separate"; shift; shift;;
-cs) CS="-contrast-stretch 0"; shift;;
-r) BINNING=$2; shift; shift;;
-n) NORM="-normalize"; shift;;
-al) AUTOLEVEL="-auto-level"; shift;;
-usm) UNSHARP="-unsharp 1.5x1.2+1.0+0.01"; shift;;
-r) BINNING=$2; shift; shift;;
-t) TRIM="-bordercolor black -border 1x1 -fuzz $20% -trim -shave $20x$20"; shift; shift;;
-g) GAMMA=$2; shift; shift;;
-c) MAKE_PROF=1
PROF_NAME="$2"
shift; shift;;
-u|+u) PROF_NAME="$2";
HALF_PROF=0
-ir) IR=1; shift;;
-c) PROF_NAME="$2"
shift; shift;;
-u) MAKE_PROF=0
PROF_NAME="$2";
if [ ! -r "$PROF_DIR"/"$PROF_NAME" ]; then
echo "The profile $PROF_DIR/$PROF_NAME does not exist!"
exit 98
fi
shift; shift;;
-h|-?) cat << EOF
Usage: negfix8 [-m] [-g G] [-cs] [-r {2|3|4}] [-u prof] [-s {R|G|B}] input_file [output_file]
negfix8 [-g G] [-r {2|3|4}] [-s {R|G|B}] -c prof input_file
Usage: negfix8 [-m] [-g G] [-ir] [-cs] [-r {2|3|4}] [-t T={1..9}] [-o O={-180..180}] [-u frame_prof] [-s {R|G|B}] input_file [output_file]
negfix8 [-m] [-g G] [-ir] [-cs] [-r {2|3|4}] [-t T={1..9}] [-o O={-180..180}] [-u frame_prof] [-s {R|G|B}] input_file [input_file...]
negfix8 -c frame_prof input_file [input_file...]

where: -g - set image gamma to specified value (default: $GAMMA)
-cs - perform contrast stretch operation
-n - perform normalization on image
-al - perform auto levels on image
-r - perform X-fold image size reduction, with binning*
-c - create profile from input file
-u - use created profile
-t - trim image by a factor of T (in most cases 1 is sufficient)
-c - create frame profile from input file or files, then exit
-u - use created frame profile
-s - separate given channel to make B&W image
-o - rotate image by O degrees (use negative value for counterclockwise)
-m - mirror image horizontally
-ir - create an alpha channel from infrared channel (only HDRi files)
-usm - perfrom unsharp masking

* Binning: http://www.imagemagick.org/Usage/photos/binning/sample3.html

Examples:
negfix8 -c test_profile scan001.tif
negfix8 -u test_profile -cs -r 3 -m scan002.tif scan002.jpg
negfix8 -cs scan003.tif
negfig8 -s G scan004.tif
negfix8 -cs -o 90 -t 1 scan003.tif
negfig8 -ir -s G scan004.tif
negfix8 -c Portra400 roll123/*.tif
negfix8 -u Portra400 -cs roll123/*.tif

EOF
[ $HAS_POLY -eq 0 ] && echo "Please upgrade ImageMagick to the newest version to speed up the processing."
exit 0;;
*) break;;
esac
done

if [ ! -f "${1}" ]; then
echo "Cannot open: ${1}"
exit 99
fi

F=_t_`basename "$1"`
F2=`basename "$1"`
OUTF=P_`basename "$1"`
OUTF="${2:-$OUTF}"

# IM bug workaround: create a copy without warnings
convert $MEM "$1[0]" "$F" 2> /dev/null || echo "Not enough disk space."

if [ ! -z $BINNING ]; then
set -- `convert $MEM "$F" -format "%[fx: $BINNING*floor(w/$BINNING)] %[fx: $BINNING*floor(h/$BINNING)] %[fx: floor(w/$BINNING)] %[fx: floor(h/$BINNING)]" info:`
BINNING="-crop ${1}x${2}+0+0 +repage -scale ${3}x${4}"
if [ $# -eq 2 -a ! -e "${2}" ]; then
WORKLIST[0]="$1"
OUTF="$2"
else
WORKLIST=("$@")
OUTF=""
fi

if [ $MAKE_PROF -eq 1 ]; then
mkdir -p "$PROF_DIR"
echo "Creating profile $PROF_NAME..."
eval $PROF_CMD > "$PROF_DIR"/"$PROF_NAME"
EX=$?
cat "$PROF_DIR"/"$PROF_NAME"
rm -f "$F"
exit $EX
fi

# Create temporary profile or read existing profile...
if [ -f "$PROF_DIR"/"$PROF_NAME" ]; then
echo "$F2: Profile: $PROF_NAME"
set -- `cat "$PROF_DIR"/"$PROF_NAME"`
VALUES=(1 1 1 0 0 0)
for i in "${WORKLIST[@]}"; do
if [ ! -f "$i" -o ! -r "$i" ]; then
echo "Cannot open: $i"
exit 95
fi
echo "Reading "`basename "$i"`
VALUES=(`convert $MEM "${i}[0]" +profile "icc,icm" $CSPACE \
-shave 10x10 -blur 3x3 -format "%[fx: min(minima.r?minima.r:${VALUES[0]},${VALUES[0]})] \
%[fx: min(minima.g?minima.g:${VALUES[1]},${VALUES[1]})] \
%[fx: min(minima.b?minima.b:${VALUES[2]},${VALUES[2]})] \
%[fx: max(maxima.r<1?maxima.r:${VALUES[3]},${VALUES[3]})] \
%[fx: max(maxima.g<1?maxima.g:${VALUES[4]},${VALUES[4]})] \
%[fx: max(maxima.b<1?maxima.b:${VALUES[5]},${VALUES[5]})]" info:`)
done
if [ "${VALUES[3]}" == "0" ]; then
echo "Cannot create the average frame profile"
exit 97
fi
PROF=`convert xc: -format "${VALUES[0]} ${VALUES[1]} ${VALUES[2]} \
%[fx: ((${VALUES[0]}/${VALUES[3]})^(1/$GAMMA))*QuantumRange*0.95] \
%[fx: log(${VALUES[4]}/${VALUES[1]})/log(${VALUES[3]}/${VALUES[0]})] \
%[fx: log(${VALUES[5]}/${VALUES[2]})/log(${VALUES[3]}/${VALUES[0]})]" info:`
echo "Profile $PROF_NAME: $PROF"
if [ "$PROF_NAME" != "-" ]; then
echo "$PROF $GAMMA" > "$PROF_DIR"/"$PROF_NAME"
EX=$?
if [ $EX -ne 0 ]; then
echo "Error saving profile, error $EX"
fi
exit $EX
fi
else
echo -n "$F2: "
set -- `eval $PROF_CMD`
echo $*
echo "Using frame profile: $PROF_NAME"
fi

echo -n "Writing $OUTF... "
for i in "${WORKLIST[@]}"; do
if [ ! -f "$i" -o ! -r "$i" ]; then
echo "Cannot open: $i"
continue
fi

F2=`basename "$i"`
F=_t_$F2
M=_i_$F2
[ -z "$OUTF" ] && OUTF=P_$F2

# Real work is done below
if [ "$5" = "1" -a "$6" = "1" -o ! -z "$SEPARATE" ]; then
echo -n "(B&W) "
convert $MEM "${F}" $CSPACE $SEPARATE $BINNING $MIRROR \
-evaluate Add 1 -poly "$1,-1" -gamma $GAMMA -evaluate Subtract $4 \
$CS -comment "$COMMENT" $ZIP $SEPARATE "$OUTF"
EX=$?
else
convert $MEM "${F}" $CSPACE $BINNING $MIRROR -separate -delete 3-5 \
\( -clone 0 -evaluate Add 1 -poly "$1,-1" \) \
\( -clone 1 -evaluate Add 1 -poly "$2,-1" -gamma $5 \) \
\( -clone 2 -evaluate Add 1 -poly "$3,-1" -gamma $6 \) \
-delete 0-2 -channel RGB,sync -combine -gamma $GAMMA \
-evaluate Subtract $4 $CS $SATURATION -comment "$COMMENT" $ZIP "$OUTF"
EX=$?
fi

if [ $EX -eq 0 ]; then
echo "Done."
else
echo "Error!"
fi
# Create a copy with unknown TIF tags and color profiles stripped
# and applied user's profiles, if any


convert $MEM "$i[0]" +profile "icc,icm" "$F" 2> /dev/null || { echo "Not enough disk space."; exit 98; }

if [ $IR -eq 1 ]; then
convert $MEM "$i[2]" -threshold 80% "$M" 2> /dev/null || { echo "Not enough disk space."; exit 98; }
MASKING_CMD="-alpha off $M -compose CopyOpacity -composite -channel RGBA"

fi

if [ ! -z $BINNING ]; then
set -- `convert $MEM "$F" -format "%[fx: $BINNING*floor(w/$BINNING)] %[fx: $BINNING*floor(h/$BINNING)] %[fx: floor(w/$BINNING)] %[fx: floor(h/$BINNING)]" info:`
BINNING_CMD="-crop ${1}x${2}+0+0 +repage -scale ${3}x${4}"
fi

rm -f "$F"
# Read calculated or existing profile...
if [ -n "$PROF" -a "$PROF_NAME"=="-" ]; then
set -- $PROF
else
set -- `cat "$PROF_DIR"/"$PROF_NAME"`
GAMMA=${7:-$GAMMA}
fi

# Real work is done below
if [ "$5" = "1" -a "$6" = "1" -o ! -z "$SEPARATE" ]; then
if [ $HAS_POLY -eq 1 ]; then
echo -n "Writing $OUTF (B&W).... "
convert $MEM "${F}" $CSPACE $SEPARATE $MASKING_CMD $BINNING_CMD $ROTATE $MIRROR \
$SAFE -poly "$1,-1" -gamma $GAMMA -evaluate Subtract $4 \
$TRIM $CS $NORM $AUTOLEVEL $UNSHARP -comment "$COMMENT" $ZIP $SEPARATE "$OUTF"
EX=$?
else
echo -n "Writing $OUTF (B&W)... "
convert $MEM "${F}" $CSPACE $SEPARATE $MASKING_CMD $BINNING_CMD $ROTATE $MIRROR \
$SAFE -fx "$1/u" -gamma $GAMMA -evaluate Subtract $4 \
$TRIM $CS $NORM $AUTOLEVEL $UNSHARP -comment "$COMMENT" $ZIP $SEPARATE "$OUTF"
EX=$?
fi
else
if [ $HAS_POLY -eq 1 ]; then
echo -n "Writing $OUTF.... "
if [ $IR -eq 1 ]; then
convert $MEM "${F}" $CSPACE $MASKING_CMD $BINNING_CMD $ROTATE $MIRROR $SAFE -separate -delete 4-6 \
\( -clone 0 -poly "$1,-1" \) -swap 0,4 -delete 4 \
\( -clone 1 -poly "$2,-1" -gamma $5 \) -swap 1,4 -delete 4 \
\( -clone 2 -poly "$3,-1" -gamma $6 \) -swap 2,4 -delete 4 \
\( -clone 3 \) -swap 3,4 -delete 4 \
-combine -type TrueColorAlpha +channel -gamma $GAMMA \
-evaluate Subtract $4 $TRIM $CS $NORM $AUTOLEVEL $SATURATION $UNSHARP -comment "$COMMENT" $ZIP "$OUTF"
else
convert $MEM "${F}" $CSPACE $BINNING_CMD $ROTATE $MIRROR $SAFE -separate -delete 3-5 \
\( -clone 0 -poly "$1,-1" \) -swap 0,3 -delete 3 \
\( -clone 1 -poly "$2,-1" -gamma $5 \) -swap 1,3 -delete 3 \
\( -clone 2 -poly "$3,-1" -gamma $6 \) -swap 2,3 -delete 3 \
-combine -type TrueColor +channel -gamma $GAMMA \
-evaluate Subtract $4 $TRIM $CS $NORM $AUTOLEVEL $SATURATION $UNSHARP -comment "$COMMENT" $ZIP "$OUTF"

fi

EX=$?
else
echo -n "Writing $OUTF... "
convert $MEM "${F}" $CSPACE $MASKING_CMD $BINNING_CMD $ROTATE $MIRROR $SAFE \
-channel R -fx "$1/u" \
-channel G -fx "$2/u" -gamma $5 \
-channel B -fx "$3/u" -gamma $6 \
+channel -gamma $GAMMA -evaluate Subtract $4 \
$TRIM $CS $NORM $AUTOLEVEL $SATURATION $UNSHARP -comment "$COMMENT" $ZIP "$OUTF"
EX=$?
fi
fi

rm -f "$F" "$M"

if [ $EX -eq 0 ]; then
echo "Done."
OUTF=""
else
echo "Error!"
exit $EX
fi

done

exit $EX