#!/bin/bash # # Developed by Fred Weinhaus 6/12/2010 .......... 11/12/2017 # # ------------------------------------------------------------------------------ # # 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: stainedglass [-k kind] [-s size] [-o offset] [-n ncolors] [-b bright] # [-e ecolor] [-t thick] [-r rseed] [-a] infile outfile # USAGE: stainedglass [-h or -help] # # OPTIONS: # # -k kind kind of stainedglass cell shape; choices are: square # (or s), hexagon (or h), random (or r); default=random # -s size size of cell; integer>0; default=16 # -o offset random offset amount; integer>=0; default=6; # only applies to kind=random # -n ncolors number of desired reduced colors for the output; # integer>1; default is no color reduction # -b bright brightness value in percent for output image; # integer>=0; default=100 # -e ecolor color for edge or border around each cell; any valid # IM color; default=black # -t thick thickness for edge or border around each cell; # integer>=0; default=1; zero means no edge or border # -r rseed random number seed value; integer>=0; if seed provided, # then image will reproduce; default is no seed, so that # each image will be randomly different; only applies # to kind=random # -a use average color of cell rather than color at center # of cell; default is center color # ### # # NAME: STAINEDGLASS # # PURPOSE: Applies a stained glass cell effect to an image. # # DESCRIPTION: STAINEDGLASS applies a stained glass cell effect to an image. The # choices of cell shapes are hexagon, square and randomized square. The cell # size and border around the cell can be specified. # # # OPTIONS: # # -k kind ... KIND of stainedglass cell shape; choices are: square (or s), # hexagon (or h), random (or r). The latter is a square with each corner # randomly offset. The default=random. # # -s size ... SIZE of stained glass cells. Values are integers>=0. The # default=16. # # -o offset ... OFFSET is the random offset amount for the case of kind=random. # Values are integers>=0. The default=6. # # -n ncolors ... NCOLORS is the number of desired reduced colors in the output. # Values are integers>1. The default is no color reduction. Larger number of # colors takes more time to color reduce. # # -b bright ... BRIGHTNESS value in percent for the output image. Values are # integers>=0. The default=100 means no change in brightness. # # -e ecolor ... ECOLOR is the color for the edge or border around each cell. # Any valid IM color is allowed. The default=black. # # -t thick ... THICK is the thickness for the edge or border around each cell. # Values are integers>=0. The default=1. A value of zero means no edge or # border will be included. # # -r rseed ... RSEED is the random number seed value for kind=random. Values # are integers>=0. If a seed is provided, then the resulting image will be # reproducable. The default is no seed. In that case, each resulting image # will be randomly different. # # -a ... use AVERAGE color of cell rather than color at center of shell; # default is center color. The average value will be accurate only for odd # square shapes with IM 6.5.9.0 or higher. All others cases will use only an # approximate average. # # Thanks to Anthony Thyssen for critiqing the original version and for # several useful suggestions for improvement. # # NOTE: This script will be slow prior to IM 6.8.3.10 due to the need to # extract color values for each cell center point across the input image. # A progress meter is therefore provided to the terminal. A speed-up is # available via a -process function, getColors. To obtain getColors, # contact me. It requires IM 6.6.2-10 or higher. # # IMPORTANT: This script will fail due to an unintended restriction in the # txt: format starting with IM 6.9.9.1 and IM 7.0.6.2. It has been fixed at # IM 6.9.9.23 and IM 7.0.7.11. # # REQUIREMENTS: Does not produce a proper set of edges/borders around each # cell under Q8, due to insufficient graylevel resolution (0 and 255) # to handle more that 255 cells. # # 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 kind="random" # random, square, hexagon size=16 # cell size offset=6 # pixel amount to randomly add or subtract to square corners ncolors="" # number of output colors bright=100 # brightness adjust ecolor="black" # edge color thick=1 # edge thickness rseed="" # seed for random average="no" # preprocess for cell average # 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 19 ] 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 ;; -k) # get kind shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID KIND SPECIFICATION ---" checkMinus "$1" kind=`echo "$1" | tr '[A-Z]' '[a-z]'` case "$kind" in hexagon|h) kind="hexagon" ;; square|s) kind="square" ;; random|r) kind="random" ;; *) errMsg "--- KIND=$kind IS AN INVALID VALUE ---" ;; esac ;; -s) # get size shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID SIZE SPECIFICATION ---" checkMinus "$1" size=`expr "$1" : '\([0-9]*\)'` [ "$size" = "" ] && errMsg "--- SIZE=$size MUST BE A NON-NEGATIVE INTEGER (with no sign) ---" test1=`echo "$size < 1" | bc` [ $test1 -eq 1 ] && errMsg "--- SIZE=$size MUST BE A POSITIVE INTEGER ---" ;; -o) # get offset shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID OFFSET SPECIFICATION ---" checkMinus "$1" offset=`expr "$1" : '\([0-9]*\)'` [ "$offset" = "" ] && errMsg "--- OFFSET=$offset MUST BE A NON-NEGATIVE INTEGER ---" ;; -n) # get ncolors shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID NCOLORS SPECIFICATION ---" checkMinus "$1" ncolors=`expr "$1" : '\([0-9]*\)'` [ "$ncolors" = "" ] && errMsg "--- NCOLORS=$ncolors MUST BE A NON-NEGATIVE INTEGER (with no sign) ---" test1=`echo "$ncolors < 2" | bc` [ $test1 -eq 1 ] && errMsg "--- NCOLORS=$ncolors MUST BE AN GREATER THAN 1 ---" ;; -b) # get bright shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID BRIGHT SPECIFICATION ---" checkMinus "$1" bright=`expr "$1" : '\([0-9]*\)'` [ "$bright" = "" ] && errMsg "--- BRIGHT=$bright MUST BE A NON-NEGATIVE INTEGER ---" ;; -e) # get ecolor shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID ECOLOR SPECIFICATION ---" checkMinus "$1" ecolor="$1" ;; -t) # get thick shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID THICK SPECIFICATION ---" checkMinus "$1" thick=`expr "$1" : '\([0-9]*\)'` [ "$thick" = "" ] && errMsg "--- THICK=$thick MUST BE A NON-NEGATIVE INTEGER ---" ;; -r) # get rseed shift # to get the next parameter # test if parameter starts with minus sign errorMsg="--- INVALID RSEED SPECIFICATION ---" checkMinus "$1" rseed=`expr "$1" : '\([0-9]*\)'` [ "$rseed" = "" ] && errMsg "--- RSEED=$rseed MUST BE A NON-NEGATIVE INTEGER ---" ;; -a) # get average average="yes" ;; -) # 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 and auto delete upon exit tmpA1="$dir/stainedglass_1_$$.mpc" tmpB1="$dir/stainedglass_1_$$.cache" tmpA2="$dir/stainedglass_2_$$.mpc" tmpB2="$dir/stainedglass_2_$$.cache" tmpA3="$dir/stainedglass_3_$$.mpc" tmpB3="$dir/stainedglass_3_$$.cache" tmpC="$dir/stainedglass_C_$$.txt" tmpG="$dir/stainedglass_G_$$.txt" trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG;" 0 trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG; exit 1" 1 2 3 15 # does not seem to produce output with the following???? #trap "rm -f $tmpA1 $tmpB1 $tmpA2 $tmpB2 $tmpA3 $tmpB3 $tmpC $tmpG; exit 1" ERR # set up color reduction if [ "$ncolors" != "" ]; then reduce="-monitor +dither -colors $ncolors +monitor" echo "" echo "Reducing Colors:" else reduce="" fi 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 swapped at IM 6.7.5.5, but not properly fixed until 6.7.6.6 # before swap verbose info reported colorspace=RGB after colorspace=sRGB if [ "$im_version" -ge "06070606" ]; then cspace1="sRGB" cspace2="sRGBA" else cspace1="RGB" cspace2="RGBA" fi # no need for setcspace for grayscale or channels after 6.8.5.4 if [ "$im_version" -gt "06080504" ]; then cspace1="" cspace2="" fi # get colorspace colorspace=`convert $infile -ping -format "%[colorspace]" info:` # set up for modulate # note there seems to be a change in -modulate (for HSL) between IM 6.8.4.6 and 6.8.4.7 that is noticeable in the output if [ "$bright" != "100" -a "$colorspace" = "CMYK" ]; then modulation="-colorspace $cspace1 -modulate ${bright},100,100 -colorspace cmyk" elif [ "$bright" != "100" -a "$colorspace" = "CMYKA" ]; then modulation="-colorspace $cspace2 -modulate ${bright},100,100 -colorspace cmyka" elif [ "$bright" != "100" ]; then modulation="-modulate ${bright},100,100" else modulation="" fi # read the input image and filter image into the temp files and test validity. convert -quiet "$infile" $reduce $modulation +repage "$tmpA1" || errMsg "--- FILE $infile DOES NOT EXIST OR IS NOT AN ORDINARY FILE, NOT READABLE OR HAS ZERO SIZE ---" # preprocess for average color if [ "$average" = "yes" -a "$im_version" -ge "06050900" -a "$kind" = "square" ]; then dim=`convert xc: -format "%[fx:round(($size-1)/2)]" info:` convert $tmpA1 -define convolve:scale=! -morphology convolve square:$dim $tmpA1 elif [ "$average" = "yes" -a "$im_version" -ge "06050900" -a "$kind" != "square" ]; then dim=`convert xc: -format "%[fx:round(($size-1)/2)+0.5]" info:` convert $tmpA1 -define convolve:scale=! -morphology convolve disk:$dim $tmpA1 elif [ "$average" = "yes" ]; then dim=`convert xc: -format "%[fx:round(($size-1)/2)]" info:` convert $tmpA1 -blur ${dim}x65000 $tmpA1 fi # test if -process module getcolors exists if [ "$im_version" -ge "06050210" ]; then process_test=`convert -list module | grep "getColors"` fi #echo "process_test=$process_test;" ww=`convert $tmpA1 -ping -format "%w" info:` hh=`convert $tmpA1 -ping -format "%h" info:` ww1=$(($ww-1)) hh1=$(($hh-1)) ww2=`convert xc: -format "%[fx:$ww1+round($size/2)]" info:` hh2=`convert xc: -format "%[fx:$hh1+round($size/2)]" info:` #echo "ww=$ww; hh=$hh; ww1=$ww1; hh1=$hh1; ww2=$ww2; hh2=$hh2;" # get qrange qrange=`convert xc: -format "%[fx:quantumrange]" info:` # init colors file echo "# ImageMagick pixel enumeration: $ww,$hh,255,rgb" > $tmpC # init increment grays file touch $tmpG if [ "$kind" = "random" ]; then # need to add 1 to offset as awk rand is exclusive between 0 and 1 offset=$(($offset+1)) awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v offset="$offset" -v rseed="$rseed" ' BEGIN { if (rseed=="") {srand();} else {srand(rseed);} y=0; while ( y < hh2 ) { x=0; while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} rx=rand(); if (rx<0.5) {signx=-1;} else {signx=1;} offx=int(signx*rand()*offset); xx=x+offx; if (xx<0) {xx=0}; if (xx>ww1) {xx=ww1}; ry=rand(); if (ry<0.5) {signy=-1;} else {signy=1;} offy=int(signy*rand()*offset); yy=y+offy; if (yy<0) {yy=0}; if (yy>hh1) {yy=hh1}; print xx","yy": (255,255,255)"; x=x+size; } y=y+size; } }' >> $tmpC awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" -v offset="$offset" -v rseed="$rseed" ' BEGIN { if (rseed=="") {srand();} else {srand(rseed);} k=0; y=0; while ( y < hh2 ) { x=0; while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} rx=rand(); if (rx<0.5) {signx=-1;} else {signx=1;} offx=int(signx*rand()*offset); xx=x+offx; if (xx<0) {xx=0}; if (xx>ww1) {xx=ww1}; ry=rand(); if (ry<0.5) {signy=-1;} else {signy=1;} offy=int(signy*rand()*offset); yy=y+offy; if (yy<0) {yy=0}; if (yy>hh1) {yy=hh1}; g=(k % 256); print xx,yy" gray("g")"; k++; x=x+size; } y=y+size; } }' >> $tmpG elif [ "$kind" = "hexagon" ]; then awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" ' BEGIN { j=0; y=0; while ( y < hh2 ) { if (j%2==0) {x=int((size+0.5)/2);} else {x=0;} while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} print x","y": (255,255,255)"; x=x+size; } j++; y=y+size; } }' >> $tmpC awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" ' BEGIN { j=0; k=0; y=0; while ( y < hh2 ) { if (j%2==0) {x=int((size+0.5)/2);} else {x=0;} while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} g=(k % 256); print x,y" gray("g")"; k++; x=x+size; } j++; y=y+size; } }' >> $tmpG elif [ "$kind" = "square" ]; then awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" ' BEGIN { y=0; while ( y < hh2 ) { x=0; while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} print x","y": (255,255,255)"; x=x+size; } y=y+size; } }' >> $tmpC awk -v size="$size" -v ww1="$ww1" -v hh1="$hh1" -v ww2="$ww2" -v hh2="$hh2" -v qrange="$qrange" ' BEGIN { k=0; y=0; while ( y < hh2 ) { x=0; while (x < ww2 ) { if (x>ww1) {x=ww1;} if (y>hh1) {y=hh1;} g=(k % 256); print x,y" gray("g")"; k++; x=x+size; } y=y+size; } }' >> $tmpG fi if [ "$thick" = "0" ]; then if [ "$im_version" -ge "06080310" ]; then convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite \ sparse-color:- |\ convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' \ "$outfile" elif [ "$im_version" -lt "06060210" -o "$process_test" != "getColors" ]; then echo "" echo "Progress:" echo "" convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite -monitor \ txt:- |\ sed '1d; / 0) /d; s/:.* /,/;' |\ convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' \ +monitor "$outfile" echo "" else convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite $tmpA1 convert $tmpA1 -alpha on -process "getColors" null: > $tmpC convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpC" \ "$outfile" fi else if [ "$im_version" -ge "06080310" ]; then convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite \ sparse-color:- |\ convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' $tmpA2 convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \ -auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3 convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \ -compose over -background $ecolor -flatten \ "$outfile" elif [ "$im_version" -lt "06060210" -o "$process_test" != "getColors" ]; then echo "" echo "Progress:" echo "" convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite -monitor \ txt:- |\ sed '1d; / 0) /d; s/:.* /,/;' |\ convert -size ${ww}x${hh} xc: -sparse-color voronoi '@-' +monitor $tmpA2 convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \ -auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3 convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \ -compose over -background $ecolor -flatten \ "$outfile" echo "" else convert $tmpA1 \( -background black $tmpC \) \ -alpha off -compose copy_opacity -composite $tmpA1 convert $tmpA1 -alpha on -process "getColors" null: > $tmpC convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpC" $tmpA2 convert -size ${ww}x${hh} xc: -sparse-color voronoi "@$tmpG" \ -auto-level -morphology edge diamond:$thick -threshold 0 -negate $tmpA3 convert $tmpA2 $tmpA3 -alpha off -compose copy_opacity -composite \ -compose over -background $ecolor -flatten \ "$outfile" fi fi exit 0