#! /bin/bash # imfuse - Combines focus stackshot images to one overall sharp image. Version="0.9.14-beta" ### Information usage() { echo "imfuse: Combines focus stackshot images to one overall sharp image. Usage: imfuse [OPTIONS] -- IMAGES... imfuse assumes that the alphanumerical order of the input images goes from front to back; for the other way around use option --revert. imfuse results suffer a lot from JPG compression. If you have JPG images as a source only, first convert them to TIF or PNG before doing anything else. Tool stackprepare can do that. Dependencies: ImageMagick 7 Mandatory (command 'magick'). exiftool Optional, needed to set and transfer image metadata. enfuse Optional, needed for options --enfuse and --bg=enfuse. geeqie Optional, needed for option -V to show progress.. feh Optional, needed for option -W to show result image. focus-stack Optional, needed for option --align. https://github.com/PetteriAimonen/focus-stack General options: --align Align images. Rather use tool stackalign, because this option is not well integrated in cache reusage. -B, --basename [=NAME] Base name [+path] for output image. If empty, the name of current directory is used. -C, --cache [=DIR] Store generated masks and images in directory DIR. They can be re-used in later runs of imfuse. See also option --rmcache. Default DIR: ~/.cache/imfuse For best performance the cache should be on an SSD. --cacheformat=FORMAT Store cache files in format FORMAT. Default: $(maskarg_defaultvalue cacheformat word1) Should be a lossless format like tif or png. mpc is optimized for fast access by ImageMagick, but needs lots of disk space. In case you run out of disk space, use 'tif'. --exif [=IMAGE] Transfer exif meta data from IMAGE or first image. -f, --force Force imfuse to run even if output image exists. --format=FORMAT Store result in image format FORMAT. Default: $(maskarg_defaultvalue format word1) -h, --help Show this help and exit. --layered Store backgrounds and substacks in layered TIF. --license Show license (GPLv2) and exit. --limit-memory=ARG Limit amount of used memory. Default: 80% ARG can be a % value or an absolute value for MB. -L, --longname Create long filename containing all options. -o, --output=FILE Result image file name. See also --basename. --revert Revert order of source images. --rmcache Clean cache and exit. --version Show imfuse version and exit. --video Generate a video of shown intermediate images. -V Show intermediate images with image viewer geeqie. -W Show result image with image viewer feh. For keyboard shortcuts see 'man feh'. Examples: arrow left/right: switch images arrow up/down: zoom in/out /: zoom to window size *: zoom 100% i: show/hide imfuse options d: show/hide file name -X Store [and show] mask and depth map, too. The options below can take additionally arguments [=ARG]. Multiple arguments are comma-separated. Example: --morphology=r3,blur Arguments taken by all mask generating options: (Mostly you only need w). w Weight of mask. Percent value from 0 to 100. Of interest if specifying more than one mask generation method. Example: --morphology --darkness=w15 This will generate two masks. The saturation mask will only have noteable effect where the morphology mask strength is below 25%. mask [=yes|no] Apply --mask* options. Default is yes except for channel and image comparision options. C= Colorspace to use instead of default $(maskarg_defaultvalue colorspace colorspace). Compare --colorspace. c Colorspace channel to use. Counting up from 0 for first channel. Example: C=HSL,c2 will use the saturation channel from HSL. I= Image creation and comparision. For possible args see --background. Example: --statistic=E=max will generate a mask from an evaluated max image that was compared with the source image. Compare step 2b: Mask generation - image comparision masks. diff [=yes|no] Use difference of command result to source image. Normally not neded to be set manually. neg Negate / invert the mask. Most of interest for color channels. t Threshold (removal) of low contrast areas. T Threshold (removal) of high contrast areas. level [=yes|no] Equalize histogram distribution to get compareable masks. Default is yes except for image comparision options and for some colorspace channel options. Arguments additionally taken by some options: r Radius. Must be an integer value. R Second radius. s Sigma. Takes also non-integer values like 0.75 S Second Sigma. p A percent value. P Second percent value n An integer value. N Second integer value. (word) Some options take one or two word arguments. Generally spoken, increased radius or sigma enhances contrast, reduces noise, looses details and increases undesired seams. There's always a tradeoff. The default values are set rather low and mostly profit from increasing. As a metapher, think of radius and sigma as of brush sizes. Step 1: Source image adjustment These option do not modify the original files, but only adjust them in RAM. In general you can skip this step. --colorspace [=ARG] Colorspace in which to generate the masks. Normally you don't need to adjust this option. ARG takes arguments: c channel (word) colorspace Default: $(maskarg_defaultvalue colorspace colorspace) colorspace can be one of 'magick -list colorspace'. Some of interest: sRGB, CMYK, RGB https://legacy.imagemagick.org/Usage/color_basics --prepresize [=ARG] Resize images before mask generation. The result will have the original size nonetheless. ARG takes arguments: p percent Default: $(maskarg_defaultvalue prepresize percent1) (word) interpolation Default: $(maskarg_defaultvalue prepresize word1) percent values >100% can make sense to get more detail. Values <100% can speed up processing of large images at the cost of precision. word must be one of 'magick -list interpolate' and adjusts resize interpolation. Note that you would need to adjust radius and sigma in mask options proportinally for similar results. Percent values like in --fft are less affected. Step 2: Mask generation Options can be specified multiple times. The order does not matter. The generated masks will be composed into one mask containing them all. Most recommended: --blur, --fft, --wavelet and --diffstat for fine detail. --statistic and --morphology for strong edges. Contrast masks: The contrast mask options detect edges that indicate focused areas. --blur [=ARG] Difference of blur. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue cutblur radius1) s sigma1 Default: $(maskarg_defaultvalue cutblur sigma1) S sigma2 Default: 1.6 * sigma1 (word) gaussian Adjust only sigma, radius 0 is adjusted automatically. If sigma2 is set to 0, --blur will compare the source with a blurred version of itself directly. gaussian enables slower but more accurate gaussian blur. Compare quite similar but slower option --dog. --cmd [=ARG] Custom ImageMagick option to create a mask. Compare --cmddiff. ARG additionally takes argument: (word) string string can be e.g. '-edge 2'. Avoid comma , in string. There's also a use case without a command at all but setting arguments C=,c only. https://imagemagick.org/script/command-line-options.php --cmddiff [=ARG] Custom ImageMagick option to create a mask. The resulting mask will be a '-compose Difference' comparision from command result with source image. There's also a use case without a command at all but setting arguments C=,c or I= only. ARG additionally takes argument: (word) string string can be e.g. '-blur 0x5'. Avoid comma , in string. https://imagemagick.org/script/command-line-options.php --comet [=ARG] Comet edge detection. Compares rotated mean areas. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue comet radius1) s sigma Default: $(maskarg_defaultvalue comet sigma1) (word) mode Default: $(maskarg_defaultvalue comet word1) mode can be one of 'magick -list compose' Recommended: Lighten, Darken. Adjust only sigma, radius 0 is adjusted automatically. --compass [=ARG] Compass edge detection. ARG additionally takes argument: (word) mode Default: $(maskarg_defaultvalue compass word1) mode can be one of 'magick -list compose' --diffstat [=ARG] Compares two local statistics of source images. Uses ImageMagick option -statistic. Compare --statistic. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue diffstat radius1) (word) mode1 Default: $(maskarg_defaultvalue diffstat word1) (word) mode2 Default: $(maskarg_defaultvalue diffstat word2) mode1 and mode2 can be two of 'magick -list evaluate'. --dog [=ARG] Difference of Gaussian. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue cutblur radius1) s sigma1 Default: $(maskarg_defaultvalue cutblur sigma1) S sigma2 Default: 1.6 * sigma1 Adjust only sigma, radius 0 is adjusted automatically. Compare https://www.imagemagick.org/Usage/convolve/#dog Quite similar but faster is option --blur. --fft [=ARG] Discrete fourier transformation. This option is in development and might change in future. The arguments configure a mask that is multiplied with the fourier magnitude part. ARG additionally takes arguments: r radius1 Default: $(maskarg_defaultvalue fft radius1) R radius2 s sigma1 p percent Default: $(maskarg_defaultvalue fft percent1) N number2 Default: $(maskarg_defaultvalue fft number2) (word1) shape conversion Default: $(maskarg_defaultvalue fft word1) (word2) shape form Default: $(maskarg_defaultvalue fft word2) radius1 sets the size of the mask shape. Low values increase contrast, high values increase detail. The radius is a percent value relative to image size. radius2 creates a second result compared with the first. word1 defines the shape conversion. Possible: gradient gaussian logVALUE powVALUE solid For VALUE replace VALUE with a value like 2 10 100 1000, e.g. log2, log100, log1000, pow2, pow0.5 word2 is the mask shape. Possible word2: circle square rhombus cross crossx sigma applies -sigmoidal-contrast to the shape. Negative values apply a +sigmoidal-contrast. For 'solid' shape it defines a blur sigma instead. percent1 sets the mid-point for -sigmoidal-contrast. No effect for 'solid' shapes. number2 gives the choice between low and high pass filter: 0 low pass filter 1 high pass filter --freichen [=ARG] Frei-Chen edge detection. ARG additionally takes arguments: n mode number Default: $(maskarg_defaultvalue freichen number1) (word1) compose mode Default: $(maskarg_defaultvalue freichen word1) Possible mode numbers at: http://www.imagemagick.org/Usage/convolve/#freichen Composen mode can be one of 'magick -list compose'. No useful settings known other than default. --kirsch [=ARG] Kirsch edge detection. --laplacian [=ARG] Laplacian edge detection. ARG additionally takes argument: n mode number Default: $(maskarg_defaultvalue laplacian number1) Possible mode numbers at: http://www.imagemagick.org/Usage/convolve/#laplacian --log [=ARG] Laplacian of Gaussian. Needs rework, not useful yet. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue log radius1) s sigma1 Default: $(maskarg_defaultvalue log sigma1) S sigma2 Adjust only sigma, radius 0 is adjusted automatically. If sigma2 is set, -log gives the difference of two log results with sigma1 and sigma2. --morphology [=ARG] Morphology edge detection. ARG additionally takes arguments: r radius Default: $(maskarg_defaultvalue morphology radius1) R kernel iteration Default: $(maskarg_defaultvalue morphology radius2) (word1) mode Default: $(maskarg_defaultvalue morphology word1) (word2) kernel Default: $(maskarg_defaultvalue morphology word2) mode can be one of 'magick -list morphology'. try: edge, edgein, edgeout, dilate, smooth. kernel can be one of 'magick -list kernel'. Compare: https://imagemagick.org/Usage/morphology --prewitt [=ARG] Prewitt edge detection. --resize [=ARG] Resize image to percent and back to normal size. p percent Default: $(maskarg_defaultvalue resize percent1)% (word) interpolation Default: $(maskarg_defaultvalue resize word1) interpolation can be one of 'magick -list interpolate'. --roberts [=ARG] Roberts edge detection. --sobel [=ARG] Sobel edge detection. --statistic [=ARG] ImageMagick option -statistic. ARG additionally takes arguments: r radius1 Default: $(maskarg_defaultvalue statistic radius1) R radius2 (word) mode Default: $(maskarg_defaultvalue statistic word1) mode can be one of 'magick -list statistic'. If radius2 is set, --statistic gives the difference of two statistic results with radius1 and radius2. --wavelet [=ARG] Based on ImageMagick option -wavelet-denoise. ARG additionally takes arguments: p percent1 Default: $(maskarg_defaultvalue wavelet percent1)% P percent2 For percent value compare option -wavelet-denoise. If percent2 is set, --wavelet gives the difference of two wavelet results with percent1 and percent2. Colorspace channel masks: All channel options are variations of --channel. Adding some of them with a rather low weight between w5..w25 can help to close gaps where edges to detect focus are rare. Most useful are --darkness and --saturation. Changing argument level is worth a try. --channel [=ARG] Use a colorspace channel as mask. ARG takes the general mask arguments: w weight C= colorspace c colorspace channel neg negate level leveling Default: $(maskarg_defaultvalue channel level) mask --mask* Default: $(maskarg_defaultvalue channel mask) Colorspace can be one of 'magick -list colorspace' The color channel is a number from 0..31. Compare 'magick -list colorspace'. --chroma [=ARG] Chroma, color strength. Same as --channel=C=HCL,c1 ARG takes the general mask arguments: w weight level leveling Default: $(maskarg_defaultvalue chroma level) mask --mask* Default: $(maskarg_defaultvalue channel mask) --darkness [=ARG] Darkness. Same as --channel=C=HSL,c2,neg ARG takes the general mask arguments: w weight level leveling Default: $(maskarg_defaultvalue darkness level) mask --mask* Default: $(maskarg_defaultvalue channel mask) --lightness [=ARG] Lightness. Same as --channel=C=HSL,c2 ARG takes the general mask arguments: w weight level leveling Default: $(maskarg_defaultvalue lightness level) mask --mask* Default: $(maskarg_defaultvalue channel mask) --saturation [=ARG] Color saturation. Same as --channel=C=HSL,c1 ARG takes the general mask arguments: w weight level leveling Default: $(maskarg_defaultvalue saturation level) mask --mask* Default: $(maskarg_defaultvalue channel mask) Image comparision masks: Adding --compose with e.g. =Overlay with a rather low weight between w5..w25 can help to close gaps, especially in large dark areas. --compose [=ARG] Composes evaluated min,max of source images with MODE. ARG takes the general mask arguments: level leveling Default: $(maskarg_defaultvalue compose level) mask --mask* Default: $(maskarg_defaultvalue compose mask) ARG additionally takes argument: (word) mode Default: $(maskarg_defaultvalue compose word1) Adding a '2', e.g. '--compose=overlay2' swaps min,max to max,min and can give a different result. mode can be one of 'magick -list compose' Some modes of interest: overlay interpolate colordodge hardlight reflect softburn softlight linearlight pegtoplight --depthmap [=ARG] Use a previously created depth map to get focus areas. ARG takes the general mask arguments: level leveling Default: $(maskarg_defaultvalue depthmap level) mask --mask* Default: $(maskarg_defaultvalue depthmap mask) ARG additionally takes argument: (word) filename Yet works well only if no --cut* options except --cutless has been used for the depth map. Sample use: imfuse creates a depth map with -X, you can change it in an image editor, and use this option to create the focus image based on the adjusted depth map. --enfuse [=ARG] Image comparision with a basic enfuse result. ARG takes the general mask arguments: level leveling Default: $(maskarg_defaultvalue enfuse level) mask --mask* Default: $(maskarg_defaultvalue enfuse mask) --evaluate [=ARG] Evaluates from all source images with mode MODE and compares the result with each source image. ARG takes the general mask arguments: level leveling Default: $(maskarg_defaultvalue evaluate level) mask --mask* Default: $(maskarg_defaultvalue evaluate mask) ARG additionally takes arguments: (word) mode Default: max mode can be one of 'magick -list evaluate' Compare --max, --min, --mean, --median. --max [=ARG] Same as --evaluate=max. Brightest pixels of stack. Result is similar to --lightness. ARG takes the general mask arguments: level leveling Default: $(maskarg_defaultvalue max level) mask --mask* Default: $(maskarg_defaultvalue max mask) --mean [=ARG] Same as --evaluate=mean. Average of pixels in stack. level leveling Default: $(maskarg_defaultvalue mean level) mask --mask* Default: $(maskarg_defaultvalue mean mask) --median [=ARG] Same as --evaluate=median. Median pixel of stack. level leveling Default: $(maskarg_defaultvalue median level) mask --mask* Default: $(maskarg_defaultvalue median mask) --min [=ARG] Same as --evaluate=min. Darkest pixels of stack. Result is similar to --darkness. level leveling Default: $(maskarg_defaultvalue min level) mask --mask* Default: $(maskarg_defaultvalue min mask) Step 3: Mask merging The contrast masks generated in step 2 are merged into one per image. The merged result can be adjusted with --mask* options. Most recommended: --maskblur and --maskwave. These options can be specified multiple times, the order matters: --maskblur [=ARG] Blur masks to enhance contrast and close minor gaps. ARG takes argument: r radius Default: $(maskarg_defaultvalue cutblur radius1) s sigma Default: $(maskarg_defaultvalue cutblur sigma1) p percent1 Default: 100% P percent2 Default: 100-percent1% (word) gaussian Adjust only sigma, radius 0 is adjusted automatically. percent1 defines how strong the blur will be applied. percent2 defines how strong the original we remain. gaussian enables slower but more accurate gaussian blur. Compare ImageMagick option -blur. --maskcmd [=ARG] Custom ImageMagick option to apply on merged masks. ARG takes argument: (word) string string can be e.g. '-blur 0x5'. Avoid comma , in string. https://imagemagick.org/script/command-line-options.php --maskenhance [=ARG] Remove noise from mask. ARG takes argument: n iterations Default: $(maskarg_defaultvalue maskenhance number1) Compare ImageMagick option -enhance. --maskdespeckle [=ARG] Remove noise from mask. ARG takes argument: n iterations Default: $(maskarg_defaultvalue maskdespeckle number1) Compare ImageMagick option -despeckle. --maskfft [=ARG] Discrete fourier transformation. This option is in development and might change in future. ARG additionally takes arguments: r radius1 Default: $(maskarg_defaultvalue fft radius1) s sigma1 p percent Default: $(maskarg_defaultvalue fft percent1) N number2 Default: $(maskarg_defaultvalue fft number2) (word1) shape conversion Default: $(maskarg_defaultvalue fft word1) (word2) shape form Default: $(maskarg_defaultvalue fft word2) See option --fft for arguments. --maskkuwahara [=ARG] Noise removal with kuwahara method. ARG takes argument: r radius Default: $(maskarg_defaultvalue maskkuwahara radius1) Compare ImageMagick option -kuwahara. --maskmorph [=ARG] Change shape of mask. ARG takes arguments: r radius Default: $(maskarg_defaultvalue maskmorph radius1) R kernel iteration Default: $(maskarg_defaultvalue maskmorph radius2) (word1) mode Default: $(maskarg_defaultvalue maskmorph word1) (word2) kernel Default: $(maskarg_defaultvalue maskmorph word2) radius is the kernel radius. kernel iteration multiplies the kernel radius. mode can be one of 'magick -list morphology'. try: open, close, erode, dilate, smooth. kernel can be one of 'magick -list kernel'. CPU expensive option. Iterating the kernel is cheaper than using a greater radius. Compare: https://imagemagick.org/Usage/morphology --maskstat [=ARG] Apply a statistic method for each mask pixel to adjust it according to its neighborhood. Option is applied to contrast masks only. ARG takes arguments: r radius Default: $(maskarg_defaultvalue maskstat radius1) (word) statistic mode Default: $(maskarg_defaultvalue maskstat word1) word1 can be one of 'magick -list statistic'. Modes of interest: mean, median, gradient. Mode mean softens the result and closes gaps. Mode median sharpens the result and opens gaps. Mode gradient sharpens the result, closes gaps and adds noise in weak areas. Compare ImageMagick option -statistic. --maskthreshold [=ARG] Remove low (or high) contrast area. ARG takes arguments: t threshold1 Default: $(maskarg_defaultvalue maskthreshold threshold1)% T threshold2 Default: $(maskarg_defaultvalue maskthreshold threshold2)% threshold1 removes low contrast areas below a percent. threshold2 removes high contrast areas above a percent. --maskwave [=ARG] Remove noise in masks. Can close gaps, but can also create artefacts in very low contrast areas. ARG takes arguments: p percent Default: $(maskarg_defaultvalue maskwave percent1)% Compare ImageMagick option -wavelet-denoise. These options can be specified only once, order does not matter: --maskmerge [=ARG] Compose mode to merge multiple masks. ARG takes arguments: (word) mode Default: $(maskarg_defaultvalue maskmerge word1) mode can be one of 'magick -list compose'. Of interest: Plus Interpolate Multiply Exclusion Blend --masklevel [=ARG] Levels merged masks into visible spectrum from 0%..100% to provide the following focus and postprocessing option arguments a full range from 0%..100%. Enabled by default. ARG takes argument: t round threshold Default: $(maskarg_defaultvalue masklevel threshold1)% p min P max (word) mode Default: $(maskarg_defaultvalue masklevel word1) mode can be one of: all: Level all masks. substack: Level only merged masks in current substack. off: Do not level masks. The round threshold rounds up or down to the next matching rounded percent value. This helps to get same results for cropped image parts as well as for whole images. mode 'substack' should only be used to speed up test runs with different mask merging options within one single substack. Only mode 'all' reliably provides valid values. percent values min and max allow to set absolute values. This option is always executed as the last one of --mask*. Step 4: Stack fusion The source images are processed with the merged masks, the overall sharp image will be generated. For each source image a cut mask is generated based on the merged masks and the following --cut* options. The order of --cut* options matters, use --cutless always first. All --cut* options except --cutless can be specified multiple times. Recommended combination: --cutwave --cutbg --cutalpha To strengthen foreground objects use --cutless. --cutalpha [=ARG] Make cut semitransparent according to mask. Nice result if used after --cutwave. Use --cutbg before. ARG takes arguments: p percent1 Default: $(maskarg_defaultvalue cutalpha percent1)% P percent2 Default: $(maskarg_defaultvalue cutalpha percent2)% For percent arguments compare --finalalpha. If the result is too soft, try 'p-25,P150'. --cutbg [=ARG] Create a background from current cuts. To be used before --cutthreshold or --cutalpha. ARG takes argument: (word) mode Default: $(maskarg_defaultvalue cutbg word1)% Possible modes: background Use as background, applied after --final*. compose Add to result before --final* options. off Store background, but do not show it. --cutblur [=ARG] Blur cut masks in final focus montage. ARG takes argument: r radius Default: $(maskarg_defaultvalue cutblur radius1) s sigma Default: $(maskarg_defaultvalue cutblur sigma1) p percent1 Default: 100% P percent2 Default: 100-percent1% (word) gaussian Adjust only sigma, radius 0 is adjusted automatically. percent1 defines how strong the blur will be applied. percent2 defines how strong the original we remain. gaussian enables slower but more accurate gaussian blur. --cutcmd [=ARG] Custom ImageMagick option to apply on cut mask. ARG takes argument: (word) string string can be e.g. '-blur 0x5'. Avoid comma , in string. https://imagemagick.org/script/command-line-options.php --cutless [=ARG] Strenghtens less contrasted objects in front of strong contrasted background objects. ARG takes argument: p percent Default: $(maskarg_defaultvalue cutless percent1)% Percent is the minimal intermediate contrast difference between background and foreground object. If unsharp areas appear, use a greater percent value. If the foreground object still has holes, use a lower percent value. Useful percent values heavily depend on previous options. Can be specified only once, the order matters. --cutmax [=ARG] Remove parts of current cut mask that are weaker than current max in result mask. ARG takes argument: p percent Default: 0 The percent value preserves some weak cut mask parts. --cutmorph [=ARG] Change shape of cut mask. Default mode 'erode' shrinks the cut mask to eliminate undesired fringes. ARG takes arguments: r radius Default: $(maskarg_defaultvalue cutmorph radius1) R kernel iteration Default: $(maskarg_defaultvalue cutmorph radius2) (word1) mode Default: $(maskarg_defaultvalue cutmorph word1) (word2) kernel Default: $(maskarg_defaultvalue cutmorph word2) radius is the kernel radius. kernel iteration multiplies the kernel radius. mode can be one of 'magick -list morphology'. try: open, close, erode, dilate, smooth. kernel can be one of 'magick -list kernel'. CPU expensive option. Iterating the kernel is cheaper than using a greater radius. Compare: https://imagemagick.org/Usage/morphology --cutsoft [=ARG] Similar to --cutblur, but paint default sharp cut mask over blurred cut mask. ARG takes argument: s sigma Default: $(maskarg_defaultvalue cutsoft sigma1) --cutthreshold [=ARG] Cut of parts of cut mask where current source mask is weaker than threshold. Removes undesired seams, but also the background. Preserve background with --cutbg before. ARG takes arguments: t threshold Default: $(maskarg_defaultvalue cutthreshold threshold1)% --cutwave [=ARG] Soften the cut mask. Result looses a bit of sharpness, but looks more friendly overall, and covers small issues. ARG takes arguments: p percent Default: $(maskarg_defaultvalue cutwave percent1)% For percent value compare option -wavelet-denoise. --substacks [=ARG] Split stack into different stacks (called substacks) and fuse them separately. Can be specified multiple times. ARG takes arguments: r radius Default: 5% p percent1 Default: 14% P percent2 n number1 N number2 (word) kurt You can specify single substacks or generate a set of substacks. Please specify either percents or numbers. - Given only one of percent or number, imfuse will generate a set of substacks accordingly. - word 'kurt' starts all substacks with image 1. - If you specify two of percents or numbers, one single substack within the given range of images is generated. - radius specifies how many images neigboured substacks should share. A percent value for radius is allowed. Use --threshold or --alpha for transparent substacks. The substacks will be composed over each other onto a background (to specify with --background). See also --layered to store the substacks as layers. That allows manual rework of the results in gimp. Step 5: Post processing The order of the options on cli matters. Noteably --alpha with percent values different from =0%,100% affects the following percent arguments.nd to have a good start for --finalalpha. All options can be specified multiple times. --finalalpha [=ARG] Generate transparent image using the contrast mask as alpha channel (=transparency channel). The semitransparent result is composed over --background. Compare similar but often better option --cutalpa. ARG takes arguments: p percent1 Default: $(maskarg_defaultvalue finalalpha percent1)% P percent2 Default: $(maskarg_defaultvalue finalalpha percent2)% (word) off percent1 affects the low contrast areas with a useful range of -100%..+50%. percent2 affects the high contrast areas with a useful range of +100%..+200%. Higher values make less transparent. The percent values are applied as '+level p%,P%' on the transparency alpha channel. If word 'off' is set, the alpha channel is disabled without further action. --finalblur [=ARG] Soft blur of bokeh in low contrast areas. ARG takes arguments: t threshold Default: $(maskarg_defaultvalue finalblur threshold1)% s sigma1 Default: $(maskarg_defaultvalue finalblur sigma1) S sigma2 Default: $(maskarg_defaultvalue finalblur sigma2) Blurs low contrast areas below threshold with sigma1. If sigma2 is given, the cut border between bokeh and foreground is blurred for a soft transition. --finalblur2 [=ARG] Soft blur of bokeh in low contrast areas. Other than --finalblur it blurs according to the contrast mask strengh. Low contrast is blurred more than high. ARG takes arguments: r radius1 Default: $(maskarg_defaultvalue finalblur2 radius1) t threshold Default: $(maskarg_defaultvalue finalblur2 threshold1)% A greater radius1 blures more. threshold restricts blur to contrast mask strength. --finalcmd [=ARG] Custom ImageMagick option to apply on result image. ARG takes argument: (word) string Default: $(maskarg_defaultvalue finalcmd word1) string can be e.g. '-auto-level'. Avoid comma , in string. https://imagemagick.org/script/command-line-options.php --finalgamma Set gamma of image. Can improve contrast. (word) value/auto Default: $(maskarg_defaultvalue finalgamma word1) Gamma less than 1.0 darkens the image, and gamma greater than 1.0 lightens the image. 'auto' sets gamma automatically. Compare ImageMagick options -gamma and -auto-gamma. --finalsharpen [=ARG] Sharpen result image. ARG takes arguments: r radius Default: $(maskarg_defaultvalue finalsharpen radius1) s sigma Default: $(maskarg_defaultvalue finalsharpen sigma1) Adjust only sigma, radius 0 is adjusted automatically. Compare ImageMagick option -adaptive-sharpen. --finalthreshold [=ARG] Make low (or high) contrast area transparent. ARG takes arguments: t threshold1 Default: $(maskarg_defaultvalue finalthreshold threshold1)% T threshold2 Default: $(maskarg_defaultvalue finalthreshold threshold2)% s sigma Default: $(maskarg_defaultvalue finalthreshold sigma1) threshold1 removes low contrast areas below a percent. threshold2 removes high contrast areas above a percent. sigma blurs the cut border. Step 6: Background --bg, --background [=ARG] Specify a background to paint on. Useful for (semi-)transparent results or as a result on its own. ARG takes argument: (word) background Default: $(maskarg_defaultvalue bg word1) background can be one out of: wave wavedark enfuse transparent magick -list color magick -list evaluate (compare --evaluate) magick -list compose (compare --compose) Try: enfuse mean max min colorize colorize2 pinlight pinlight2 interpolate overlay pegtoplight reflect Can be specified multiple times for option --layered. Much thanks to the developers and supporters of ImageMagick! Without them this project would not have been possible. A first start to get an idea of the stackshot image fusing workflow was inspired by Alan Gibson's focStack.bat at https://im.snibgo.com/focstack.htm . imfuse also contains code from Fred Weinhaus with his friendly permisson. Version: imfuse v$Version Author: Martin Viereck, Germany License: GPLv2 https://www.gnu.org/licenses/old-licenses/gpl-2.0.html#SEC1 Website: https://github.com/mviereck/microscopy-tools " } license() { echo " imfuse - Combines focus stackshot images to one overall sharp image. Copyright (C) 2022 Martin Viereck This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. imfuse website and contact: https://github.com/mviereck/microscopy-tools " } ### Messages error() { echo " imfuse$Subprocess ERROR: $* " >&2 kill -s SIGINT "$Imfusepid" } note() { echo "imfuse$Subprocess: $*" >&2 return 0 } verbose() { [ "$Verbose" = "yes" ] && echo "imfuse$Subprocess: $*" >&2 return 0 } showimage() { local Frame [ "$Showimageprocessing" = "yes" ] && geeqie -t -r --File:"${1:-}" 2>/dev/null & disown $! [ "$Video" ] && { # Videoframecount="$((Videoframecount+1))" Videoframecount="$(ls "${Cachedir}"/videoframe????.* | sort -V | tail -n1)" Videoframecount="$(basename "$Videoframecount")" Videoframecount="${Videoframecount//[^0-9]/}" Videoframecount="$(sed "s/^0*//" <<< "$Videoframecount")" Videoframecount="$(calc "$Videoframecount+1")" Frame="${Cachedir}/videoframe$(printnum "$Videoframecount").tif" #ln -s "${1:-}" "$Frame" cp "${1:-}" "$Frame" ### FIXME ln -s where possible } return 0 } showviewnior() { local W H [ "$Viewnior" ] && { #viewnior "$@" >/dev/null 2>&1 & W=900 H=900 compare "$Imagewidth" -lt "$W" && W="$Imagewidth" compare "$Imageheight" -lt "$H" && H="$Imageheight" nohup feh --draw-tinted --zoom="max" --keep-zoom-vp \ --geometry="${W}x${H}+37+0" \ --title="$Sourcemd5: $Parsedoptions" \ --info="exiftool -imagedescription %F | cut -d: -f2- | fold -s -w $((W/7))" \ "$@" /dev/null 2>&1 & disown $! } return 0 } showresult() { showimage "$Resultimage" #command -v xclip >/dev/null && echo -n "$Resultimage" | xclip -i -selection clipboard notify-send "imfuse is ready" 2>/dev/null ||: case "$Extendedsave" in yes) showviewnior "$Resultimage" "$Resultmask" "$Resultdepthmap" ;; no) showviewnior "$Resultimage" ;; esac echo "$Resultimage" } traperror() { error "traperror($Subprocess): Command at Line ${2:-} returned with error code ${1:-}: ${4:-} ${3:-} - ${5:-}" } ### Misc calc() { # float calculation of $* with awk # first awk calculates, second awk removes trailing zeros. LC_ALL=C awk "BEGIN { OFMT=\"%f\"; print $**1 ; }" | awk '{ if ($0 ~ /\./){ sub("0*$","",$0); sub ("\\.$","",$0);} print }' } checkmagicklist() { local Check [ "${3:-}" = "print" ] && Check="" || Check="-q" $Magickbin -list "${1:-}" | grep -w -i "^${2:-XXX}" | head -n1 | awk '{print $1}' | grep $Check -w -i "^${2:-XXX}" } compare() { # compare floating number with < > = local Arg1 Arg2 Operator Arg1="${1:-}" Operator="${2}" Arg2="${3:-}" case "$Operator" in "<"|"lt"|"-lt") Operator="<" ;; ">"|"gt"|"-gt") Operator=">" ;; "="|"eq"|"-eq"|"==") Operator="==" ;; esac [ -n "$(LC_ALL=C awk "BEGIN{if ($Arg1 $Operator $Arg2) print \"yes\"}")" ] } digitonly() { #sed "s/[^0-9.]//g" <<< "${1:-}" echo "${1//[^0-9.]/}" } digitrm() { #sed "s/[0-9.]//g" <<< "${1:-}" echo "${1//[0-9.-]/}" } generate_key() { # generate a unique key value from current time and nanoseconds date +%s.%N } lowercase() { # Usage: lowercase "string" printf '%s\n' "${1,,}" } md5cut() { # print last 6 digits grep -v "#" <<< "$*" | tr "\n" " " | md5sum | cut -c27-32 } numberofpercent() { Number="${1:-}" Number="${Number//%}" Number="$((Number*Sourceimagenumber/100))" [ "$Number" -lt "1" ] && Number=1 [ "$Number" -gt "$Sourceimagenumber" ] && Number="$Sourceimagenumber" echo "$Number" } percentrm() { # remove % from string #sed s/%//g <<< "${1:-}" echo "${1//%/}" } printnum(){ # print number $1 with leading zeroes. # $1 number # $2 digits. Default: 4 [ "${1:-}" = "NUMBER" ] && echo "NUMBER" && return 0 printf "%0${2:-4}d" "${1:-0}" } printsameline() { # print $1 without newline at begin of current line echo -ne "${1:-}\033[0K\r" >&2 } printtotalmemory() { # print total memory including zram local Memory Line Zram Memory="$(LC_ALL=C free | grep "Mem:" | LC_ALL=C awk '{print $2}')" while read Line; do Zram="$(LC_ALL=C awk 'BEGIN {OFMT = "%.0f"} {print $3}' <<< "$Line")" Zram="$(LC_ALL=C awk 'BEGIN {OFMT = "%.0f"} {print $1 / 1000}' <<< "$Zram")" Memory="$((Memory + Zram))" done < <(/sbin/swapon --bytes | grep zram ||:) echo $Memory } unspecialstring() { # Replace all characters except those described in tr string with a '-'. printf %s "${1:-}" | LC_ALL=C tr -c "a-zA-Z0-9.,=-_" "-" } ### Debugging helpers forcemask() { note "Forcing mask generation" rm -f "${Cachedir}"/${Sourcemd5}.mask.* } forcelevel() { note "Forcing leveling" rm -f "${Cachedir}"/level* } forcemerge() { note "Forcing merge" rm -f "${Cachedir}"/${Sourcemd5}.mask.merge* } forcefocus() { note "Forcing focus" rm -f "${Cachedir}"/${Sourcemd5}.focus* rm -f "${Cachedir}"/${Sourcemd5}.substack* } forcepost() { note "Forcing focus postprocessing" rm -f "${Cachedir}"/${Sourcemd5}.substack*.final.* } #### run commands on all CPUs multicore() { # Run multiple processes in parallel, but not more than $Multicore_maxprocesses # $1 Command # $2 Image to show if $1 is finished # $3 Memory needed by the process # Run multicore_wait afterwards to wait for the last processes to finish. local Process Command local Mem_needed Zram [ "${1:-}" = "-t1" ] && { shift [ "$Multicore_processcount" -gt 0 ] && { multicore_wait || return 1 } } [ "$Multicore_processcount" = "$Multicore_maxprocesses" ] && { multicore_wait || return 1 } [ "$Multicore_processcount" = "0" ] && { Multicore_memorymax="$(printfreememory)" Multicore_memorymax="$((Multicore_memorymax*8/10))" } Mem_needed=0 for Process in $(seq $Multicore_maxprocesses); do Mem_needed="$(awk "BEGIN {print $Mem_needed + ${Multicore_memory[$Process]:-0} }" )" done Mem_needed="$((Mem_needed+${3:-0}))" [ "$Mem_needed" -gt "$Multicore_memorymax" ] && { note "multicore: Low memory. Waiting for $Multicore_processcount running processes to finish. Need: $(( ${3:-0}/1000 )) (overall $((Mem_needed/1000))) MB, Available: $((Multicore_memorymax/1000)) MB" [ "$Mem_needed" -gt "$Multicore_memorymax" ] && [ "$Multicore_processcount" = "0" ] && note "multicore: Likely hard disk cache will be used and slow down the calculation." multicore_wait || return 1 } ifcmdbreak && return 1 Multicore_processcount=$((Multicore_processcount +1)) Command="$(cut -d ' ' -f1 <<< "${1:-}")" case $(type -t "$Command") in file) Command="nice ${1:-}" ;; *) Command="${1:-}" ;; esac #verbose "multicore: ${1:-}" eval "$Command &" Multicore_process[Multicore_processcount]=$! Multicore_image[Multicore_processcount]="${2:-}" Multicore_memory[Multicore_processcount]="${3:-0}" return 0 } multicore_wait() { local Process Error= for Process in $(seq ${Multicore_maxprocesses:-0}); do [ "${Multicore_process[$Process]}" ] && { multicore_waitprocess "${Multicore_process[$Process]}" || { multicore_break Error=1 } [ "$Error" ] && break [ "${Multicore_image[$Process]}" ] && showimage "${Multicore_image[$Process]}" } Multicore_process[$Process]="" Multicore_image[$Process]="" Multicore_memory[$Process]="0" done [ "$Error" ] && return 1 Multicore_processcount=0 return 0 } multicore_waitprocess() { local Error= while sleep 0.2 ; do ps -p "${1:-}" >/dev/null || break ifcmdbreak && Error=1 [ "$Error" ] && break done [ "$Error" ] && return 1 wait "${1:-}" return $? } multicore_break() { local Process for Process in $(seq ${Multicore_maxprocesses:-0}); do [ "${Multicore_process[$Process]}" ] && { verbose "multicore_break: Sending SIGINT to $(ps -p ${Multicore_process[$Process]})" kill "${Multicore_process[$Process]}" wait "${Multicore_process[$Process]}" Multicore_process[$Process]="" Multicore_image[$Process]="" Multicore_memory[$Process]="0" } done } multicore_init() { # declare global variables local Process Multicore_maxprocesses="$(nproc)" Multicore_maxprocesses="${Multicore_maxprocesses:-1}" Multicore_maxprocesses="$((Multicore_maxprocesses * 2))" for Process in $(seq $Multicore_maxprocesses); do Multicore_process[$Process]="" Multicore_image[$Process]="" Multicore_memory[$Process]="0" done Multicore_processcount=0 Multicore_minram=250000 Multicore_maxprocesses=$Multicore_maxprocesses } ifcmdbreak() { [ -e "${Cachedir}/exit" ] #return 1 } printfreememory() { # print current free memory including zram local Memory Line Zram Memory="$(LC_ALL=C free | grep "Mem:" | LC_ALL=C awk '{print $7}')" while read Line; do Zram="$(LC_ALL=C awk 'BEGIN {OFMT = "%.0f"} {print ($3 - $4)}' <<< "$Line")" Zram="$(LC_ALL=C awk 'BEGIN {OFMT = "%.0f"} {print $1 / 1000}' <<< "$Zram")" #Memory="$((Memory + Zram))" done < <(/sbin/swapon --bytes | grep zram ||:) echo "$Memory" } ### Files check_exifstring() { # Generate parsed option string for EXIF meta data local Count Parsedoptions="" [ "$Align" = "yes" ] && Parsedoptions="$Parsedoptions --align" # step source image preparation maskarg_single "colorspace" && Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" maskarg_single "prepresize" && Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" # step mask generation for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in mask) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done # step merge for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in merge) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done maskarg_single "maskmerge" && Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" maskarg_single "masklevel" && Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" # step focus for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in focus) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done # step substacks [ "$Substackautoall" = "no" ] && for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in substack) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done # step postprocessing for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in postfocus) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done # step background for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in background) Parsedoptions="$Parsedoptions $(maskarg_short "$Argcount")" ;; esac done # misc [ -n "$Revertimagelist" ] && Parsedoptions="$Parsedoptions --revert" [ "$Storelayered" = "yes" ] && Parsedoptions="$Parsedoptions --layered" [ -n "$Testsetup" ] && Parsedoptions="$Parsedoptions --test=$Testarg" Parsedoptions="${Parsedoptions#" "}" } check_outputname() { for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in background|postfocus) ;; *) Substackmd5="$Substackmd5 ${Imoptions[$Count]}=${Imarguments[$Count]}" ;; esac case "$Argtype" in background) ;; *) Finalmd5="$Finalmd5 ${Imoptions[$Count]}=${Imarguments[$Count]}" ;; esac done Substackmd5="$(md5cut "$Sourcemd5 $Substackmd5")" Finalmd5="$(md5cut "$Sourcemd5 $Finalmd5")" Optionmd5="$(md5cut "${Imoptions[*]} ${Imarguments[*]} $Testsetup$Testarg")" Resultbasename="${Outputbasename}${Sourcemd5}_${Optionmd5}" Resultsearchmask="*${Sourcemd5}_${Optionmd5}*" [ "$Storelayered" = "yes" ] && Resultbasename="${Resultbasename}_layered" [ -n "$Resultimage" ] && Resultbasename="$(basename "$Resultimage")" [ -z "${Outputdir:-}" ] && Outputdir="." [ "$Longname" = "yes" ] && Longname="${Parsedoptions// /}" || Longname="" case "$Extendedsave" in yes) Resultdepthmap="${Outputdir}/${Resultbasename}${Longname}.depthmap.${Imageformat}" Resultmask="${Outputdir}/${Resultbasename}${Longname}.mask.${Imageformat}" ;; no) ### FIXME Resultdepthmap="${Cachedir}/${Resultbasename}${Longname}.depthmap.tif" Resultmask="${Cachedir}/${Resultbasename}${Longname}.mask.tif" ;; esac [ "$Video" ] && Video="${Outputdir}/${Resultbasename}${Longname}.video.webm" [ -z "$Resultimage" ] && Resultimage="${Outputdir}/${Resultbasename}${Longname}.result.${Imageformat}" Sourceimagepath="$(realpath "${Sourceimagelist[1]}")" Sourceimagepath="$(dirname "$Sourceimagepath")" Sourceimagepath="${Sourceimagepath/"$HOME"/"~"}" return 0 } load_sourceimages() { # Load source images into imagemagick registry # (Can also happen in align()) local Sourceimage Count Number Command local Firstimage Lastimage Firstimage="${1:-1}" Lastimage="${2:-"$Sourceimagenumber"}" sendmagickmessage "STOPWATCH" for Count in $(seq "$Lastimage" -1 "$Firstimage"); do Number="$(printnum "$Count")" case "$Loadsourceimages" in yes) [ -z "${Imsourceimagelist[$Count]:-}" ] && { Imsourceimagelist[$Count]="mpr:sourceimage.$(printnum "$Count")" sendmagickmessage "PROGRESS:Loading source image: ${Sourceimagelist[$Count]} ETA:$Count" Command=" '${Sourceimagelist[$Count]}' -alpha off -depth 16 -write '$(sourceimagename "$Count")' -delete 0" cmd "$Command" } ;; no) Imsourceimagelist[$Count]="${Sourceimagelist[$Count]}" ;; esac done [ "$Loadsourceimages" = "yes" ] && sendmagickmessage "/PROGRESS" cmd_waitforready return 0 } maskarray() { local Basename local -n List="${4:-}" local Firstimage Lastimage Firstimage="${2:-1}" Lastimage="${3:-$Sourceimagenumber}" Basename="${1:-}" for Count in $(seq "$Firstimage" "$Lastimage"); do List[$Count]="$(maskname "$Basename" "$Count")" done } masklist() { local Basename List= local Firstimage Lastimage Basename="${1:-}" Firstimage="${2:-1}" Lastimage="${3:-$Sourceimagenumber}" for Count in $(seq "$Firstimage" "$Lastimage"); do List="$List '$(maskname "$Basename" "$Count")'" done echo "$List" } sourceimagename() { # print source image name number $1. # might be an mpr: or a file depending on $Loadsourceimages. echo "${Imsourceimagelist[${1:-}]}" } maskexist() { local Count local Firstimage Lastimage= Firstimage="${2:-1}" [ -n "${2:-}" ] && [ -z "${3:-}" ] && Lastimage="$Firstimage" [ -z "$Lastimage" ] && Lastimage="${3:-$Sourceimagenumber}" case "$Masktocache" in yes) for Count in $(seq "$Firstimage" "$Lastimage"); do [ -e "$(maskname "${1:-}" "$Count")" ] || return 1 done return 0 ;; no) return 1 ;; esac } maskname() { [ -z "${1:-}" ] && error "maskname(): maskbasename is empty: ${1:-} ${2:-}" [ -z "${2:-}" ] && error "maskname(): number not given: ${1:-} ${2:-}" case "$Masktocache" in yes) echo "${Cachedir}/${Sourcemd5}.mask.${1:-}.$(printnum "${2:-}").${Cacheformat}" ;; no) echo "mpr:${1:-}.$(printnum "${2:-}")" ;; esac } ### image processing helpers align() { # align with focus-stack # https://github.com/PetteriAimonen/focus-stack local Log Line Image Count Command local X Y W H Lmax=0 Tmax=0 Rmin=10000000 Bmin=10000000 local Aligndir Log="${Cachedir}/align.log" Aligndir="${Cachedir}/aligned.$Sourcemd5" mkdir -p "$Aligndir" note "Aligning source images with external tool focus-stack." focus-stack --verbose --output="$Aligndir/" --align-only --no-contrast --no-whitebalance "${Sourceimagelist[@]}" >"$Log" || return 1 # calculate smallest valid area common to all aligned images while read Line; do Line="$(cut -d' ' -f4- <<< "$Line")" X="$(digitonly "$(cut -d, -f1 <<< "$Line")" )" Y="$(digitonly "$(cut -d, -f2 <<< "$Line")" )" W="$(digitonly "$(cut -d, -f3 <<< "$Line")" )" H="$(digitonly "$(cut -d, -f4 <<< "$Line")" )" [ "$X" -gt "$Lmax" ] && Lmax="$X" [ "$Y" -gt "$Tmax" ] && Tmax="$Y" [ "$((X+W))" -lt "$Rmin" ] && Rmin="$((X+W))" [ "$((Y+H))" -lt "$Bmin" ] && Bmin="$((X+W))" done < <(grep "valid area" "$Log" ||:) X="$Lmax" Y="$Tmax" W="$((Rmin-Lmax-1))" H="$((Bmin-Tmax-1))" # crop aligned images to valid area, store as mpr source images sendmagickmessage "STOPWATCH" for Count in $(seq "$Sourceimagenumber" -1 1); do Image="${Sourceimagelist[$Count]}" Number="$(printnum "$Count")" Image="$Aligndir/$(basename "$Image")" Sourceimagelist[$Count]="$Image" case "$Loadsourceimages" in yes) Imsourceimagelist[$Count]="mpr:sourceimage.$(printnum "$Count")" ;; no) Imsourceimagelist[$Count]="$Image" ;; esac sendmagickmessage "PROGRESS:Cropping aligned images ${W}x${H}+${X}+${Y}: $Image ETA:$Count" Command=" '$Image' -crop ${W}x${H}+${X}+${Y} $Tifstorealpha -write '$(sourceimagename "$Count")' $(showimagecode "$(sourceimagename "$Count")") -delete 0" cmd "$Command" done sendmagickmessage "/PROGRESS" cmd_waitforready Sourcemd5="$(md5cut "$(ls -f -l --full-time "${Sourceimagelist[@]}")" )" return 0 } alphalevel() { local Command Image Mask Percent1 Percent2 local Longoptions Parsedoptions Longoptions="image:,mask:,percent1:,percent2:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --image) Image="${2:-}" ; shift ;; --mask) Mask="${2:-}" ; shift ;; --percent1) Percent1="${2:-}" ; shift ;; --percent2) Percent2="${2:-}" ; shift ;; esac shift done [ -z "$Image" ] && return 1 Percent1="${Percent1:-0}" Percent2="${Percent2:-100}" Command=" ## alphalevel() '$Image' -write mpr:image -delete 0" case "$Mask" in "") Command="$Command mpr:image -channel alpha -separate -write mpr:mask -delete 0" ;; *) Command="$Command '$Mask' -write mpr:mask -delete 0" ;; esac Command="$Command mpr:mask +level ${Percent1}%x${Percent2}% -write mpr:mask -delete 0" Command="$Command mpr:image mpr:mask -alpha off -compose CopyOpacity -composite $Tifstorealpha -write '$Image' -delete 0" Command="$Command +set registry:image +set registry:mask" Command="$Command ## /alphalevel()" cmd "$Command" cmd_waitforready return 0 } clut_gaussian() { # create gaussian clut image $1 with sigma $2 local Command Clutimage Sigma Clutimage="${1:-}" Sigma="${2:-1}" Command=" -size 1x1 xc:white -bordercolor Black -border 2x0 -filter gaussian -define filter:sigma=$Sigma -resize 512x256! -crop 50%x100%+0+0 -auto-level -write '$Clutimage' -delete -1" cmd "$Command" cmd_waitforready } enfuse_split() { local Resultimage Enfuseoptions local Command Count local Firstimage Lastimage local Mem_free Mem_needed local Splitimagelist= Splitimagename Splitresult= Splitcache Split Splits Splitheight local Startzeit Resultimage="${1:-}" Firstimage="${2:-1}" Lastimage="${3:-$Sourceimagenumber}" Mem_free="$(printfreememory)" Mem_needed="$(( (Lastimage-Firstimage+1) * Imagewidth*Imageheight * 2 * 32 / (8*1024)))" Enfuseoptions="--contrast-weight=1 --saturation-weight=0 --exposure-weight=0 --hard-mask" sendmagickmessage "NOTE:Generating image with external tool enfuse" Startzeit="$(date +%s)" Splits="$((Mem_needed / Mem_free +1))" case "$Splits" in 1) ### Single run attempt #nice enfuse $Enfuseoptions -o "$Resultimage" "${Sourceimagelist[@]:$Firstimage:$Lastimage}" 2>&1 | grep -v -E "loading next image|assuming all pixels should contribute|does not have an alpha channel|TIFFDecoder" || error "Failed to generate enfuse image" nice enfuse $Enfuseoptions -o "$Resultimage" "${Sourceimagelist[@]:$Firstimage:$Lastimage}" || error "Failed to generate enfuse image" ;; *) ### Splitting attempt note "enfuse: Splitting source images due to low memory." load_sourceimages "$Firstimage" "$Lastimage" Splitheight=$((Imageheight/Splits)) Splitcache="${Cachedir}/enfuse.split" mkdir -p "$Splitcache" for Split in $(seq $Splits); do Splitgeometry[$Split]="${Imagewidth}x$((Splitheight + $([ "$Split" -lt "$Splits" ] && echo 20 || echo 0) ))+0+$(( (Split-1) * Splitheight ))" done sendmagickmessage "STOPWATCH" for Count in $(seq "$Firstimage" "$Lastimage"); do sendmagickmessage "PROGRESS:enfuse: Splitting source images into $Splits parts: ${Sourceimagelist[$Count]} ETA:$Count" Command=" '${Sourceimagelist[$Count]}' -write mpr:sourceimage -delete 0" for Split in $(seq "$Splits"); do Splitimagename="$Splitcache/split.$(printnum "$Count").${Split}.png" Command="$Command mpr:sourceimage -crop ${Splitgeometry[$Split]} +repage $Tifstore -write '${Splitimagename}' -delete 0" Splitimagelist[$Split]="${Splitimagelist[$Split]:-} $Splitimagename" done #cmd "$Command" multicore "$Magickbin $(tr -d "\n" <<< "$Command") -exit" done multicore_wait sendmagickmessage "/PROGRESS" #cmd_waitforready for Split in $(seq $Splits); do note "Running enfuse step $Split / $Splits" Splitresult[$Split]="$Splitcache/splitresult${Split}.png" #nice enfuse $Enfuseoptions -o "${Splitresult[$Split]}" "${Splitimagelist[$Split]}" 2>&1 | grep -v -E "loading next image|assuming all pixels should contribute|does not have an alpha channel|TIFFDecoder" || error "Failed to generate enfuse image" nice enfuse $Enfuseoptions -o "${Splitresult[$Split]}" "${Splitimagelist[$Split]}" || error "Failed to generate enfuse image" showimage "${Splitresult[$Split]}" done sendmagickmessage "NOTE:enfuse: Appending splits to result" Command=" mpr:black -write mpr:append -delete 0" for Split in $(seq $Splits); do Command="$Command mpr:append +repage -gravity NorthWest '${Splitresult[$Split]}' +repage -gravity NorthWest -geometry +0+$(( (Split -1) * Splitheight )) -compose Over -composite -write mpr:append -delete 0" done Command="$Command mpr:append $Tifstore -write '$Resultimage' $(showimagecode "$Resultimage") -delete 0 +set registry:append" cmd "$Command" cmd_waitforready rm -r "$Splitcache" ;; esac note "enfuse_split()[$Splits] ready after: $(date -u -d @$(($(date +%s)-Startzeit)) +"%T")" return 0 } evaluate() { local Imagelist Imagenumber=0 Resultimage Mode local Command Mode="${1:-}" ; shift Resultimage="${1:-}" ; shift while [ $# -gt 0 ]; do Imagenumber="$((Imagenumber+1))" Imagelist[$Imagenumber]="${1:-}" shift done sendmagickmessage "NOTE:Evaluating $Mode: $Resultimage" [ "$Loadsourceimages" = "no" ] && { case "${Mode,,}" in max|min|mean) #evaluate_splitlist "$Mode" "$Resultimage" "${Imagelist[@]}" evaluate_iterative "$Mode" "$Resultimage" "${Imagelist[@]}" return 0 ;; esac } Command=" # evaluate() $Mode: $Resultimage $(printf "'%s' " "${Imagelist[@]}") -alpha off -evaluate-sequence $Mode $Tifstore -write '$Resultimage' $(showimagecode "$Resultimage") -delete 0" cmd "$Command" cmd_waitforready return 0 } evaluate_iterative() { # supports min, max, mean # loads images one by one instead all of them local Imagelist Imagenumber=0 Resultimage Mode local Command Mode="${1:-}" ; shift Resultimage="${1:-}" ; shift while [ $# -gt 0 ]; do Imagenumber="$((Imagenumber+1))" Imagelist[$Imagenumber]="${1:-}" shift done case "${Mode,,}" in max) Mode="Lighten" ;; min) Mode="Darken" ;; esac sendmagickmessage "STOPWATCH" Command=" '${Imagelist[$Imagenumber]}'" case "${Mode,,}" in darken|lighten) for Count in $(seq $((Imagenumber-1)) -1 1); do Command="$Command '${Imagelist[$Count]}' -format 'PROGRESS:Compose $Mode ETA:$Count/$Imagenumber\n' -write info: -compose $Mode -composite $(showimagecode)" done ;; mean) for Count in $(seq $Imagenumber -1 1); do Command="$Command '${Imagelist[$Count]}' -alpha off -format 'PROGRESS:Compose Blend ETA:$Count/$Imagenumber\n' -write info: -compose blend -set option:compose:args $(calc 100/$((Imagenumber-Count+1)) ) -composite $(showimagecode)" done ;; *) error "evaluate_iterative(): Unsupported mode: $Mode" ;; esac Command="$Command $Tifstorealpha -write '$Resultimage' -delete 0" cmd "$Command" cmd_waitforready sendmagickmessage "/PROGRESS" return 0 } evaluate_splitlist() { local Imagelist Imagenumber=0 Resultimage Mode local Command local Split Splits Splitresult Splitfirstimage Splitlastimage local Mem_free Mem_needed Mode="${1:-}" ; shift Resultimage="${1:-}" ; shift while [ $# -gt 0 ]; do Imagenumber="$((Imagenumber+1))" Imagelist[$Imagenumber]="${1:-}" shift done Splits="$(nproc)" Mem_free="$(printfreememory)" Mem_free="$((Mem_free * 8/10))" Mem_needed="$((Imagenumber * Imagememsize))" while [ "$((Mem_needed / Splits))" -gt "$Mem_free" ]; do Splits="$((Splits +1))" done [ "$Splits" -lt "1" ] && Splits=1 Splits="$((Splits+1))" for Split in $(seq $Splits); do Splitfirstimage[$Split]="$(( (Split-1)*Imagenumber/Splits +1))" Splitlastimage[$Split]="$(( (Split)*Imagenumber/Splits +1))" [ "${Splitlastimage[$Split]}" -gt "$Imagenumber" ] && Splitlastimage[$Split]="$Imagenumber" Splitresult[$Split]="${Cachedir}/evalsplit_$Mode.$Split.${Cacheformat}" done sendmagickmessage "NOTE:evaluating in $Splits tasks outside of script" for Split in $(seq $Splits); do # -limit memory ${Mem_free}KB # -define registry:temporary-path='${Cachedir}' Command="$Magickbin $(printf "'%s' " "${Imagelist[@]:${Splitfirstimage[$Split]}:${Splitlastimage[$Split]}}") -evaluate-sequence $Mode $Tifstore -write '${Splitresult[$Split]}' -delete 0 -exit" multicore "$(tr -d "\n" <<< "$Command")" "${Splitresult[$Split]}" "$(( Imagememsize * (${Splitlastimage[$Split]}-${Splitfirstimage[$Split]}+1) ))" done multicore_wait Command=" ${Splitresult[*]} -evaluate-sequence $Mode $Tifstorealpha -write '$Resultimage' -delete 0" cmd "$Command" cmd_waitforready } exiftransfer() { # Transfer exif data from image $1 to image $2 # Does not transfer image size information. # Sets orientation tag to horizontal / no rotation. # Several warnings are supressed. local Sourceimage Destinationimage Exifargs Sourceimage="${1:-}" Destinationimage="${2:-}" Exifargs="$(exiftool -a -u -g1 -args "$Sourceimage")" Exifargs="$(LC_ALL=C grep -v -E -- \ '-ExifTool|-System:|-File:|ImageWidth|ImageHeight|ImageSize|Compression|Orientation|Resolution' <<< "$Exifargs" \ | sed "s/'/'\"'\"'/g ; s/=/='/ ; s/\$/'/" )" eval exiftool -ignoreMinorErrors -overwrite_original_in_place $Exifargs -Orientation=Horizontal "$Destinationimage" 2>&1 | grep -v -E 'Warning|files updated' } finalblur() { local Image Mask Threshold Sigma1 Sigma2 Command local Longoptions Parsedoptions Longoptions="image:,mask:,threshold:,sigma1:,sigma2:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --image) Image="${2:-}" ; shift ;; --mask) Mask="${2:-}" ; shift ;; --threshold) Threshold="${2:-}" ; shift ;; --sigma1) Sigma1="${2:-}" ; shift;; --sigma2) Sigma2="${2:-}" ; shift ;; --) ;; esac shift done Sigma1="${Sigma1:-1}" Sigma2="${Sigma2:-0}" Threshold="${Threshold:-50}" Command=" # --finalblur '$Image' -write mpr:image -delete 0 '$Mask' -write mpr:maxmask -delete 0 mpr:maxmask -threshold ${Threshold}% -negate -write mpr:cutmask -delete 0" Command="$Command mpr:image -alpha off -blur 0x$Sigma1 -write mpr:bokeh -delete 0 mpr:bokeh ( mpr:cutmask -blur 0x$Sigma2 ) -alpha off -compose CopyOpacity -composite -write mpr:partsource_blurred -delete 0 mpr:image mpr:partsource_blurred -compose Over -composite $Tifstorealpha -write '$Image' $(showimagecode) -delete 0 +set registry:bokeh +set registry:cutmask +set registry:image +set registry:maxmask +set registry:partsource_blurred" cmd "$Command" cmd_waitforready return 0 } finalblur2() { # --finalblur2 local Longoptions Parsedoptions local Image Threshold Blur Mask local Command Longoptions="image:,mask:,threshold:,radius1:,radius2:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --image) Image="${2:-}" ; shift ;; --mask) Mask="${2:-}" ; shift ;; --radius1) Radius1="${2:-}" ; shift ;; --radius2) Radius2="${2:-}" ; shift ;; --threshold) Threshold="${2:-}" ; shift ;; --) ;; esac shift done Command=" # --finalblur2 '$Mask' -white-threshold ${Threshold}% -negate" Command="$Command -write mpr:mask -delete 0 '$Image' mpr:mask -set option:compose:args ${Radius1}x${Radius1} -compose Blur -composite $Tifstorealpha -write '$Image' $(showimagecode "$Image") -delete 0 " cmd "$Command" cmd_waitforready } generate_fftshape() { local Shape Virtualsize Virtualborder Radius Negate Sigmoidalcontrast Sigmoidalmidpoint local Fftcenter Shapemodearg Outputname Command= local Longoptions Parsedoptions Longoptions="shape:,size:,conversion:,radius:,negate:,sigmoidal:,sigmoidalmidpoint:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --shape) Shape="${2:-}" ; shift ;; #--virtualborder) Virtualborder="${2:-}" ; shift ;; --conversion) Shapemode="${2:-}" ; shift ;; --radius) Radius="${2:-}" ; shift ;; --negate) Negate="${2:-}" ; shift ;; --sigmoidal) Sigmoidalcontrast="${2:-}" ; shift ;; --sigmoidalmidpoint) Sigmoidalmidpoint="${2:-}" ; shift ;; --) ;; esac shift done Shape="${Shape:-circle}" Virtualsize="${Virtualsize:-$Fftsize}" Radius="${Radius:-100}" Shapemode="${Shapemode:-gradient}" Negate="${Negate:-0}" Sigmoidalcontrast="${Sigmoidalcontrast:-}" Sigmoidalmidpoint="${Sigmoidalmidpoint:-50}" Fftcenter="$((Virtualsize/2))" Radius="$((Virtualsize*Radius/200))" Shapemodearg="${Shapemode//[a-zA-Z]/}" case "$Shape" in circle) case "$Shapemode" in solid) Command=" -size ${Virtualsize}x${Virtualsize} xc:black -fill white -draw 'circle ${Fftcenter},${Fftcenter},${Fftcenter},$((Fftcenter+Radius))'" ;; *) Command=" -size $((Radius*2))x$((Radius*2)) radial-gradient:" ;; esac ;; cross|xcross) case "$Shapemode" in solid) Command=" -size $(calc "$Virtualsize*1.5")x$(calc "$Virtualsize*1.5") xc:black -background white -gravity center -splice $((Radius*2))x$((Radius1*2))" ;; *) Command=" -size $(calc "$Virtualsize*2")x$Radius gradient: -rotate 180 ( -clone 0 -flip ) -append -write mpr:grad1 -rotate 90 -write mpr:grad2 -delete 0 -size $(calc "$Virtualsize*1.5")x$(calc "$Virtualsize*1.5") xc:black mpr:grad1 -gravity center -compose lighten -composite mpr:grad2 -compose lighten -composite" ;; esac ;; square|rhombus) case "$Shapemode" in solid) Command=" -size $(calc "$Virtualsize*1.5")x$(calc "$Virtualsize*1.5") xc:black -fill white -draw 'rectangle $((Fftcenter-Radius)),$((Fftcenter-Radius)) \ $((Fftcenter+Radius)),$((Fftcenter+Radius))'" ;; *) Command=" -size ${Argradius1}x${Argradius1} gradient: -rotate 180 ( gradient: -rotate 90 ) -compose Darken -composite ( -clone 0 -flip ) -append ( -clone 0 -flop ) +append" ;; esac ;; esac case "$Shape" in rhombus|xcross) Command="$Command -background black -rotate 45 +repage" ;; esac Command="$Command -size ${Virtualsize}x${Virtualsize} xc:black -swap 0,1 -gravity Center -compose Blend -composite +gravity +repage" case "$Shapemode" in cos*) Command="$Command -evaluate cos ${Shapemodearg:-0.5} -negate" ;; gauss|gaussian) Command="$Command -negate -fx 'exp(-(u^2)/(sqrt(2*pi)))' -auto-level" ;; gradient) ;; inverselog*) Command="$Command -negate -evaluate inverselog ${Shapemodearg:-10} -negate -auto-level" ;; log*) Command="$Command -negate -evaluate log ${Shapemodearg:-10} -negate -auto-level" ;; pow*) Command="$Command -evaluate pow ${Shapemodearg:-2} -auto-level" ;; solid) ;; esac [ -n "$Sigmoidalcontrast" ] && case "$Shapemode" in solid) Command="$Command -blur 0x$Sigmoidalcontrast" ;; *) compare "$Sigmoidalcontrast" -gt "0" && Command="$Command -sigmoidal-contrast ${Sigmoidalcontrast}x${Sigmoidalmidpoint}%" || Command="$Command +sigmoidal-contrast $(calc "-1*$Sigmoidalcontrast")x${Sigmoidalmidpoint}%" ;; esac [ "$Negate" = "1" ] && Command="$Command -negate" Outputname="mpr:fftshape$(md5cut "$Command")-$Shape-r$Radius" Command="$Command -alpha off -write '$Outputname' $(showimagecode "$Outputname") -delete 0" cmd "$Command" echo "$Outputname" } generate_image() { local Mode Imagename= Swap= Imagetype= Showname= Showmode= Command Size local Generalimage= Skip= Subimfuse local Longoptions Parsedoptions local Firstimage Lastimage Longoptions="first:,last:,mode:,name:,showname,showmode" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --first) Firstimage="${2:-}" ; shift ;; --last) Lastimage="${2:-}" ; shift ;; --mode) Mode="${2:-}" ; shift ;; --name) Imagename="${2:-}" ; shift ;; --showname) Showname="yes" ;; --showmode) Showmode="yes" ;; esac shift done Firstimage="${Firstimage:-1}" Lastimage="${Lastimage:-$Sourceimagenumber}" Size="${Imagewidth}x${Imageheight}" grep -q "2" <<< "$Mode" && Swap="-swap 0,1" || Swap="" Mode="$(tr -d "2" <<< "$Mode")" checkmagicklist color "$Mode" && Imagetype="color" && Mode="$(checkmagicklist "$Imagetype" "$Mode" print)" checkmagicklist evaluate "$Mode" && Imagetype="evaluate" && Mode="$(checkmagicklist "$Imagetype" "$Mode" print)" checkmagicklist compose "$(tr -d "2" <<< "$Mode")" && Imagetype="compose" && Mode="$(checkmagicklist "$Imagetype" "$Mode" print)" case "$Mode" in enfuse) Imagetype="enfuse" ;; wave) Imagetype="wave" ;; wavedark) Imagetype="wavedark" ;; esac [ -z "$Imagename" ] && { Generalimage="yes" case "$Imagetype" in enfuse) Imagename="${Cachedir}/${Sourcemd5}.enfuse.${Firstimage}..${Lastimage}.tif" ;; wave) Imagename="${Cachedir}/${Sourcemd5}.wave.${Firstimage}..${Lastimage}.${Cacheformat}" ;; wavedark) Imagename="${Cachedir}/${Sourcemd5}.wavedark.${Firstimage}..${Lastimage}.${Cacheformat}" ;; evaluate) Imagename="${Cachedir}/${Sourcemd5}.evaluate.${Firstimage}..${Lastimage}.${Mode}.${Cacheformat}" ;; compose) Imagename="${Cachedir}/${Sourcemd5}.compose.${Firstimage}..${Lastimage}.${Mode}$([ "$Swap" ] && echo 2 ||:).${Cacheformat}" ;; color) Imagename="mpr:${Mode}" ;; esac } Subimfuse="imfuse --cache=${Cachedir} --output=$Imagename --sub=bg-wave" [ "$Showimageprocessing" = "yes" ] && Subimfuse="$Subimfuse -V" [ -z "$Imagename" ] && { note "generate_image() ERROR: No image name given and none created based on possibly unknown image mode: '$Mode'" return 1 } [ "$Showmode" ] && { echo "$Mode$([ "$Swap" ] && echo 2 ||:)" return 0 } [ "$Showname" ] && { echo "$Imagename" return 0 } [ "$Generalimage" = "yes" ] && case $Imagetype in color) ;; *) [ -e "$Imagename" ] && Skip="yes" ;; esac [ "$Skip" = "yes" ] && { sendmagickmessage "NOTE:Skipping image generation, already exists: $Imagename" return 0 } sendmagickmessage "NOTE:Generating image $Imagename" case "$Imagetype" in color) Command=" -size $Size canvas:$Mode $Tifstorealpha -write '$Imagename' -delete 0" cmd "$Command" cmd_waitforready ;; compose) generate_image --mode min --first "$Firstimage" --last "$Lastimage" generate_image --mode max --first "$Firstimage" --last "$Lastimage" Command=" '$(generate_image --showname --mode min --first "$Firstimage" --last "$Lastimage")' '$(generate_image --showname --mode max --first "$Firstimage" --last "$Lastimage")' -alpha off $Swap -compose $Mode -composite $Tifstorealpha -write '$Imagename' $(showimagecode "$Imagename") -delete 0" cmd "$Command" cmd_waitforready ;; enfuse) enfuse_split "$Imagename" "${Firstimage}" "${Lastimage}" ;; evaluate) load_sourceimages evaluate "$Mode" "$Imagename" "${Imsourceimagelist[@]:$Firstimage:$Lastimage}" ;; wave) $Subimfuse --format="tif" --wavelet=p100 --cutwave=p100 --finalalpha=off "${Sourceimagelist[@]:$Firstimage:$Lastimage}" ;; wavedark) $Subimfuse --format="tif" --wavelet=p100 --cutwave=p100 --darkness=w25 --finalalpha=off "${Sourceimagelist[@]:$Firstimage:$Lastimage}" ;; esac return 0 } generate_video() { note "Generating video $Video" Videoframerate=5 #nice ffmpeg -y -hide_banner -nostdin -r $Videoframerate -f image2 -start_number 1 -i $Framedir/frame%04d.$Imageformat $Destinationfile || note "ERROR in video generation" [ -f "$Video" ] && rm "$Video" nice ffmpeg -y -hide_banner -nostdin -r $Videoframerate -f image2 -start_number 1 -i "${Cachedir}/videoframe%04d.$Imageformat" "$Video" || note "ERROR in video generation" rm "${Cachedir}"/videoframe* ffplay "$Video" } level_masks() { local Maskbasename= Maskmax Maskmin Maskmd5 Clutimage Clut= Round= local Count Mask Levelmin= Levelmax= Checkmin= Checkmax= local Longoptions Parsedoptions local Firstimage Lastimage Masknumber=0 Sourcemask Destmask Outputbasename local Masklist Longoptions="first:,last:,max::,min::,name:,output:,clut::,round::" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --first) Firstimage="${2:-}" ; shift ;; --last) Lastimage="${2:-}" ; shift ;; --name) Maskbasename="${2:-}" ; shift ;; --output) Outputbasename="${2:-}" ; shift ;; --clut) Clut="${2:-yes}" ; shift ;; --round) Round="${2:-5}" ; shift ;; --min) Levelmin="${2:-}" ; shift ;; --max) Levelmax="${2:-}" ; shift ;; --) ;; *) Masknumber="$((Masknumber+1))" Masklist[$Masknumber]="${1:-}" esac shift done Firstimage="${Firstimage:-1}" Lastimage="${Lastimage:-$Sourceimagenumber}" Outputbasename="${Outputbasename:-"$Maskbasename"}" Clut="${Clut:-no}" [ "$Round" = "0" ] && Round="" case "$Masknumber" in "0") maskarray "$Maskbasename" "$Firstimage" "$Lastimage" Masklist ;; *) Firstimage="1" Lastimage="$Masknumber" ;; esac Maskmax="mpr:maskmax" Maskmin="mpr:maskmin" Maskmaxmin="mpr:maskmaxmin" Clutimage="mpr:clut" #Clutimage=clut.tif [ -z "$Levelmax" ] && Checkmax="yes" [ -z "$Levelmin" ] && Checkmin="yes" sendmagickmessage "NOTE:Leveling $Maskbasename" case "0" in 0) [ "$Checkmax" ] && { evaluate max "$Maskmax" "${Masklist[@]}" Levelmax="$(getmagickinfo "$Maskmax" "%[fx:maxima*100]")" } [ "$Checkmin" ] && { evaluate min "$Maskmin" "${Masklist[@]}" Levelmin="$(getmagickinfo "$Maskmin" "%[fx:minima*100]")" } ;; 1) sendmagickmessage "STOPWATCH" [ -z "$Levelmin$Levelmax" ] && { [ "$Checkmin" ] && Levelmin=100 [ "$Checkmax" ] && Levelmax=0 for Count in $(seq $Lastimage -1 $Firstimage); do eval "$(getmagickinfo "${Masklist[$Count]}" " Imageminlevel=%[fx:minima*100] Imagemaxlevel=%[fx:maxima*100] ")" Imageminlevel="$(calc "$Imageminlevel")" Imagemaxlevel="$(calc "$Imagemaxlevel")" [ "$Checkmin" ] && compare "$Imageminlevel" -lt "$Levelmin" && Levelmin="$Imageminlevel" [ "$Checkmax" ] && compare "$Imagemaxlevel" -gt "$Levelmax" && Levelmax="$Imagemaxlevel" sendmagickmessage "PROGRESS:Calculating min:$Levelmin%, max:$Levelmax%, ETA:$Count" [ -n "$Round" ] && compare "$Levelmin" -lt "$Round" && compare "$Levelmax" -gt "$((100-Round))" && { sendmagickmessage "PROGRESS:Stopped calculating min/max because min<$Round%, max>$((100-Round))%" break } done sendmagickmessage "/PROGRESS" } ;; esac [ -n "$Round" ] && { Count="100" while compare "$Count" -gt "0" ; do Count="$((Count-Round))" compare "$Count" -lt "$Levelmax" && Levelmax="$((Count+Round))" && break done } [ -n "$Round" ] && { Count="0" while compare "$Count" -lt "100" ; do Count="$((Count+Round))" compare "$Count" -gt "$Levelmin" && Levelmin="$((Count-Round))" && break done } [ "$Clut" = "no" ] && compare "$Levelmin" lt "1" && compare "$Levelmax" gt "99" && [ "$Maskbasename" = "$Outputbasename" ] && { sendmagickmessage "NOTE:Skipping level, range is greater than 1%..99%" return 0 } [ "$Clut" = "yes" ] && { evaluate max "$Maskmax" "${Masklist[@]}" Command=" '$Maskmax' -separate -append -level ${Levelmin}%,${Levelmax}% -clamp $Tifstore -write '$Maskmax' -delete 0" cmd "$Command" cmd_waitforready mkclut_uniform "$Maskmax" "$Clutimage" } sendmagickmessage "STOPWATCH" for Count in $(seq $Lastimage -1 $Firstimage); do sendmagickmessage "PROGRESS:Mask leveling ${Levelmin}%,${Levelmax}%: $Maskbasename ETA:$Count" Sourcemask="${Masklist[$Count]}" Destmask="${Masklist[$Count]}" [ -n "$Outputbasename" ] && Destmask="$(maskname "$Outputbasename" "$Count")" Command=" '$Sourcemask' -level ${Levelmin}%,${Levelmax}%" [ "$Clut" = "yes" ] && Command="$Command '$Clutimage' -clut" Command="$Command $Tifstore -write '$Destmask' $(showimagecode "$Destmask") -delete 0" cmd "$Command" done sendmagickmessage "/PROGRESS" cmd_waitforready return 0 } mkclut_uniform() { # Generate a clut image to uniform distribution in level_masks() # Contains code from http://www.fmwconcepts.com/imagemagick/redist with friendly permission from Fred Weinhaus. local Referenceimage Clutimage Netpbm Netpbmdata local Histogramarray Colorcountarray Totalpixel Lutlist Quantumrange Factor local Command Key1 Key2 Count Referenceimage="${1:-}" Clutimage="${2:-}" # get cumulative histogram Key1="$(generate_key)" Key2="$(generate_key)" Command=" '$Referenceimage' -colorspace gray -depth 8 -format '\n$Key1\n%c$Key2\n' -write histogram:info:- -delete 0" cmd "$Command" cmd_waitforready Histogramarray="$(sed -n "/$Key1/,/$Key2/p" "$Magickfifolog" | sed '1d ; $d')" Histogramarray=($(echo "$Histogramarray" | sed -n 's/[ ]*\([0-9]*\).*gray[(]\([0-9]*\).*$/\1 \2/p' \ | LC_ALL=C awk '# AWK to generate a zero filled histogram { bin[int($2)] += $1; } END { for (i=0;i<256;i++) {hist = bin[i]+0; print hist; } } ' ) ) Colorcountarray=($(echo ${Histogramarray[*]} | LC_ALL=C awk '# AWK to generate a cumulative histogram { split($0,count," ") } END { for (i=0;i<256;i++) { cum += count[i]; print cum } }' )) Totalpixel="${Colorcountarray[255]}" # generate lut for Uniform Distribution which is just the cumulative histogram normalized to max value # the uniform distribution has an integral which is f(x)=x # this means that the cumulative distribution of the image is its own lut and only needs to be scaled # get raw cumulative histogram Quantumrange="$(getmagickinfo "$Referenceimage" "%[fx:quantumrange]")" Factor="$(getmagickinfo rose: "%[fx:$Quantumrange/$Totalpixel]")" Lutlist="$(for ((Count=0; Count<256; Count++)); do echo "${Colorcountarray[$Count]}" done | LC_ALL=C awk -v Factor="$Factor" '{ print int(Factor*$1); }')" # convert lutArr into lut image Netpbm="P2 256 1 $Quantumrange $Lutlist" Netpbmdata="$(base64 <<< "$Netpbm")" sendmagickmessage "Content of encoded NetPBM clut image: $(echo "$Netpbm" | tr "\n" " ")" Command=" 'inline:data:image/netpbm;base64, $Netpbmdata ' -scale 256x1\! -write '$Clutimage' -delete 0" cmd "$Command" cmd_waitforready return 0 } threshold() { local Mask Image= Blackthreshold Whitethreshold Thresholdblur Cutmask= Cutimage= local Command local Longoptions Parsedoptions Longoptions="image:,mask:,threshold1:,threshold2:,sigma1:,cutmask::,cutimage::" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --mask) Mask="${2:-}" ; shift ;; --image) Image="${2:-}" ; shift ;; --sigma1) Thresholdblur="${2:-}" ; shift ;; --threshold1) Blackthreshold="${2:-}" ; shift ;; --threshold2) Whitethreshold="${2:-}" ; shift ;; --cutmask) Cutmask="${2:-"soft"}" ; shift ;; --cutimage) Cutimage="${2:-"soft"}" ; shift ;; esac shift done Blackthreshold="${Blackthreshold:-0}" Whitethreshold="${Whitethreshold:-100}" Thresholdblur="${Thresholdblur:-0}" Command=" '$Mask' -alpha off -write mpr:mask -delete 0 mpr:mask -threshold ${Blackthreshold}% -write mpr:blackthreshold -delete 0 mpr:mask -threshold $((Whitethreshold))% -negate -write mpr:whitethreshold -delete 0 mpr:blackthreshold mpr:whitethreshold -compose Darken -composite -write mpr:threshold -delete 0" [ "$Thresholdblur" -gt "0" ] && Command="$Command mpr:threshold -blur 0x$Thresholdblur -write mpr:threshold -delete 0" case "$Cutmask" in soft) Command="$Command mpr:mask mpr:threshold -compose Darken -composite -write mpr:mask -delete 0 mpr:mask $Tifstore -write '$Mask' -delete 0" ;; hard) Command="$Command mpr:threshold $Tifstore -write '$Mask' -delete 0" ;; esac case "$Cutimage" in soft) Command="$Command mpr:mask mpr:threshold -compose Lighten -composite -write mpr:mask -delete 0 '$Image' -alpha extract mpr:mask -compose Darken -composite -write mpr:mask -delete 0 '$Image' mpr:mask -alpha off -compose CopyOpacity -composite $Tifstorealpha -write '$Image' -delete 0" ;; hard) Command="$Command '$Image' mpr:threshold -alpha off -compose CopyOpacity -composite $Tifstorealpha -write '$Image' -delete 0" ;; esac Command="$Command +set registry:mask +set registry:threshold +set registry:whitethreshold +set registry:blackthreshold" cmd "$Command" cmd_waitforready return 0 } ### core focus stack routines focus_maskmethod() { # Mask generating ImageMagick options # Most of them are based on edge detection: http://www.imagemagick.org/Usage/convolve/#edgedet local Sourceimage local Maskgenerator= local Firstimage Lastimage local Fftmaskname1 Fftmaskname2 Sourceimage="${1:-1}" Firstimage="${2:-1}" Lastimage="${3:-$Sourceimagenumber}" Maskgenerator=" # --$Argmethod=${Argoptions%,TYPE*}" case $Argmethod in blur) case "$Argsigma2" in "") Maskgenerator="$Maskgenerator $Sourceimage" case "$Argword1" in ""|"blur") Maskgenerator="$Maskgenerator -blur ${Argradius1}x${Argsigma1}" ;; "gaussian") Maskgenerator="$Maskgenerator -gaussian-blur ${Argradius1}x${Argsigma1}" ;; esac ;; *) case "$Argword1" in ""|"blur") Maskgenerator="$Maskgenerator ( $Sourceimage -blur ${Argradius1},${Argsigma1} ) ( $Sourceimage -blur ${Argradius1},${Argsigma2} ) -compose Difference -composite" ;; "gaussian") Maskgenerator="$Maskgenerator ( $Sourceimage -gaussian-blur ${Argradius1},${Argsigma1} ) ( $Sourceimage -gaussian-blur ${Argradius1},${Argsigma2} ) -compose Difference -composite" ;; esac ;; esac ;; cmd|channel|chroma|darkness|lightness|saturation|enfuse|compose|evaluate|max|min|mean|median) Maskgenerator="$Maskgenerator $Sourceimage $Argword1" ;; cmddiff) Maskgenerator="$Maskgenerator $Sourceimage $Argword1" ;; comet) Maskgenerator="$Maskgenerator $Sourceimage -define morphology:compose=${Argword1} -morphology Convolve Comet:${Argradius1}x${Argsigma1}:>" ;; compass) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=50%! -define morphology:compose=${Argword1} -define convolve:bias=50% -morphology Convolve Compass:>" ;; depthmap) Md5="md$(md5cut $(ls -f -l --full-time "$Argword1")).tif" cmd " # --depthmap='$Argword1' $Argword1 -write mpr:$Md5 -alpha extract -write mpr:${Md5}mask -delete 0 mpr:$Md5 -alpha off mpr:${Md5}mask -compose Darken -composite -write mpr:$Md5 -delete 0" Maskgenerator="$Maskgenerator mpr:black +level CLUTLEVEL% mpr:rainbow -clut mpr:$Md5 -compose Difference -composite -negate mpr:${Md5}mask -compose Darken -composite" ;; diffstat) Maskgenerator="$Maskgenerator ( $Sourceimage -statistic ${Argword1} ${Argradius1} ) ( $Sourceimage -statistic ${Argword2} ${Argradius2} ) -compose Difference -composite" ;; dog) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:bias=50% -define convolve:scale=100% -morphology Convolve DoG:${Argradius1},${Argsigma1},${Argsigma2}" ;; experimental) Maskgenerator="$Maskgenerator $Sourceimage -grayscale RMS ( $Sourceimage ) -compose Difference -composite " ;; experimentalX) Maskgenerator="$Maskgenerator $Sourceimage ( -clone 0 -colorspace HSL -channel 1 -separate +channel ) ( -clone 0 -colorspace HSB -channel 1 -separate +channel ) -delete 0 -compose Difference -composite " ;; fft) Fftmaskname1="$(generate_fftshape --shape "$Argword2" --conversion "${Argword1}" --radius "$Argradius1" \ --sigmoidal "$Argsigma1" --sigmoidalmidpoint "$Argpercent1" \ --negate "$Argnumber2" )" [ "${Argradius2:-0}" -gt "0" ] && { Fftmaskname2="$(generate_fftshape --shape "$Argword2" --conversion "${Argword1}" --radius "$Argradius2" \ --sigmoidal "$Argsigma1" --sigmoidalmidpoint "$Argpercent1" \ --negate "$Argnumber2" )" } Maskgenerator="$Maskgenerator -size ${Fftsize}x${Fftsize} xc:black $Sourceimage #-virtual-pixel Mirror -fx 'v.p[-$(( (Fftsize-Maskwidth)/2 )),-$(( (Fftsize-Maskheight)/2 ))]' -fft ( -clone 0 -write mpr:fft0 -delete 0 ) ( -clone 1 -write mpr:fft1 -delete 0 ) -delete 0,1" Maskgenerator="$Maskgenerator mpr:fft0 '$Fftmaskname1' -compose Multiply -composite -write mpr:fft0-radius1 -delete 0" [ -n "$Argradius2" ] && Maskgenerator="$Maskgenerator mpr:fft0 '$Fftmaskname2' -compose Multiply -composite -write mpr:fft0-radius2 -delete 0" Maskgenerator="$Maskgenerator mpr:fft0-radius1 mpr:fft1 -ift -gravity center -crop ${Maskwidth}x${Maskheight}+0+0 +repage +gravity" [ -n "$Argradius2" ] && Maskgenerator="$Maskgenerator ( mpr:fft0-radius2 mpr:fft1 -ift -gravity center -crop ${Maskwidth}x${Maskheight}+0+0 +repage +gravity ) -alpha off -compose Difference -composite" ;; freichen) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=100%! -define morphology:compose=${Argword1} -define convolve:bias=10% -morphology Convolve FreiChen:${Argnumber1}>" ;; kirsch) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=50%! -define morphology:compose=Screen -define convolve:bias=50% -morphology Convolve Kirsch:>" ;; laplacian) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:bias=50% -define convolve:scale=! -morphology Convolve Laplacian:${Argnumber1}" ;; log) # LoG: Laplacian of Gaussian Maskgenerator="$Maskgenerator $Sourceimage -define convolve:bias=50% -define convolve:scale=100%! -morphology Convolve LoG:${Argradius1}x${Argsigma1}" [ -n "$Argsigma2" ] && Maskgenerator="$Maskgenerator ( $Sourceimage -define convolve:scale=100% -morphology Convolve LoG:${Argradius1}x${Argsigma2} ) -compose Difference -composite" ;; morphology) Maskgenerator="$Maskgenerator $Sourceimage -morphology ${Argword1}:${Argradius2} ${Argword2}:${Argradius1}" ;; prewitt) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=100%! -define morphology:compose=Screen -define convolve:bias=10% -morphology Convolve Prewitt:>" ;; resize) Maskgenerator="$Maskgenerator $Sourceimage -interpolate $Argword1 -interpolative-resize ${Argpercent1}% -interpolative-resize ${Maskwidth}x${Maskheight} +interpolate" ;; roberts) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=100% -define morphology:compose=Screen -define convolve:bias=10% -morphology Convolve Roberts:@" ;; sobel) Maskgenerator="$Maskgenerator $Sourceimage -define convolve:scale=100%! -define convolve:bias=10% -define morphology:compose=Screen -morphology Convolve Sobel:>" ;; statistic) case "$Argradius2" in "") Maskgenerator="$Maskgenerator $Sourceimage -statistic ${Argword1} ${Argradius1}x${Argradius1}" ;; *) Maskgenerator="$Maskgenerator ( $Sourceimage -statistic ${Argword1} ${Argradius1} ) ( $Sourceimage -statistic ${Argword1} ${Argradius2} ) -compose Difference -composite" ;; esac ;; wavelet) Maskgenerator="$Maskgenerator $Sourceimage -wavelet-denoise ${Argpercent1}%" [ -n "$Argpercent2" ] && Maskgenerator="$Maskgenerator ( $Sourceimage -wavelet-denoise ${Argpercent2}% ) -compose Difference -composite" ;; esac echo "$Maskgenerator" } focus_generate_masks() { local Maskgenerator Maskgenerator_all= Basenamelist= Maskready= Maskmax Mode Line Imagename Command= Maskmd5 Count Sourceimagecode Sourceimagepreparation= local Longoptions Parsedoptions local Firstimage Lastimage local Levelmin Levelmax Longoptions="first:,last:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --first) Firstimage="${2:-}" ; shift ;; --last) Lastimage="${2:-}" ; shift ;; esac shift done Firstimage="${Firstimage:-1}" Lastimage="${Lastimage:-$Sourceimagenumber}" sendmagickmessage "NOTE:Step 2: Generating masks" # Generate code to prepare source image Sourceimagepreparation=" mpr:sourceimage" # --prepresize Maskwidth="$Imagewidth" Maskheight="$Imageheight" maskarg_single "prepresize" && { Sourceimagepreparation="$Sourceimagepreparation -interpolate $Argword1 -interpolative-resize ${Argpercent1}% +interpolate" Maskwidth="$(magick -size ${Imagewidth}x10 xc: -resize "$Argpercent1"% -format "%w" info:)" Maskheight="$(magick -size 10x${Imageheight} xc: -resize "$Argpercent1"% -format "%h" info:)" } [ "$Maskwidth" -gt "$Maskheight" ] && Fftsize="$Maskwidth" || Fftsize="$Maskheight" Fftsize="$(calc "$Fftsize*1.1" | cut -d. -f1)" [ "$(( 2* (Fftsize/2) ))" != "$Fftsize" ] && Fftsize="$((Fftsize+1))" [ "$Testsetup" ] && Sourceimagepreparation="$Sourceimagepreparation -contrast-stretch 0 -sharpen 0x4" Sourceimagepreparation="$Sourceimagepreparation -write mpr:sourceimage -delete 0" # Generate Code for mask generation for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" [ "$Argtype" = "mask" ] && { Sourceimagecode=" mpr:sourceimage -alpha off # --colorspace -colorspace $Colorspace" [ -n "$Colorspacechannel" ] && Sourceimagecode="$Sourceimagecode -channel $Colorspacechannel -separate +channel" [ -n "$Argcolorspace" ] || [ -n "$Argchannel" ] && { Sourceimagecode=" mpr:sourceimage -alpha off # =C -colorspace ${Argcolorspace:-"$Colorspace"}" [ -n "$Argchannel" ] && Sourceimagecode="$Sourceimagecode # =c -channel $Argchannel -separate +channel" } [ -n "$Argimage" ] && { ### FIXME colorspace generate_image --mode "$Argimage" --first "$Firstimage" --last "$Lastimage" Sourceimagecode=" # =E mpr:sourceimage $(generate_image --mode "$Argimage" --showname --first "$Firstimage" --last "$Lastimage") -compose Difference -composite -negate" } Maskgenerator=" $(focus_maskmethod "$Sourceimagecode" "$Firstimage" "$Lastimage")" [ "$Argdiff" = "yes" ] && Maskgenerator="$Maskgenerator # =diff ( $Sourceimagecode ) -compose Difference -composite" [ "$Argnegate" ] && Maskgenerator="$Maskgenerator # =neg -negate" [ -z "$Argchannel" ] && Maskgenerator="$Maskgenerator # combine colorspace channels -colorspace $Colorspace -evaluate Divide 4 -separate -evaluate-sequence add" Maskmd5="$(md5cut "$Arglevel $Sourceimagepreparation $Maskgenerator $Sourcemd5")" Argbasename="$Argmethod.$Maskmd5" #Argbasename="${Argmethod}${Argoptions}.$Sourcemd5" maskarg_store "$Count" Maskgenerator="$Maskgenerator # level range -format '$Argbasename:MAX=%[fx:maxima*100]\n' -write info: -format '$Argbasename:MIN=%[fx:minima*100]\n' -write info:" ## Check if mask already exists Maskready="no" maskexist "$Argbasename" "$Firstimage" "$Lastimage" && { Maskready="yes" sendmagickmessage "NOTE:Skipping mask generation, already exists: $Argbasename" } grep -q -x "$Argbasename" <<< "$Basenamelist" && Maskready="yes" Basenamelist="$Basenamelist $Argbasename" [ "$Maskready" = "no" ] && { case "$Argmethod" in compose|evaluate|min|max|mean|median|enfuse) generate_image --mode "$Argimage" --first "$Firstimage" --last "$Lastimage" ;; esac Maskgenerator_all="$Maskgenerator_all $Maskgenerator $Tifstore -write '$(maskname "$Argbasename" NUMBER)' $(showimagecode "$(maskname "$Argbasename" NUMBER)") -delete 0" } } done # Generate masks [ -n "$Maskgenerator_all" ] && { grep -q "NUMBER" <<< "$Maskgenerator_all" && load_sourceimages "$Firstimage" "$Lastimage" sendmagickmessage "STOPWATCH" for Count in $(seq "$Lastimage" -1 "$Firstimage"); do sendmagickmessage "PROGRESS:Mask generation ETA:$Count" Command=" '$(sourceimagename "$Count")' -write mpr:sourceimage -delete 0 ${Sourceimagepreparation} ${Maskgenerator_all}" Command="${Command//NUMBER/$(printnum "$Count")}" Command="${Command//CLUTLEVEL/$(calc "100*($Sourceimagenumber-$Count)/$Sourceimagenumber")}" cmd "$Command" done sendmagickmessage "/PROGRESS" cmd_waitforready Command=" +set registry:sourceimage" cmd "$Command" cmd_waitforready } # level and uniform masks for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" [ "$Argtype" = "mask" ] && { [ "$Arglevel" = "yes" ] && { [ "$Masktocache" = "yes" ] && [ -e "${Cachedir}/${Sourcemd5}.mask.${Argbasename}.levelclut${Argclut}" ] && { sendmagickmessage "NOTE:Skipping mask leveling, already done: $Argbasename" } || { Levelmax="$(grep "$Argbasename:MAX" "$Magickfifolog" | cut -d= -f2 | LC_ALL=C awk '{ OFMT="%f"; print $1*1 }' | sort -n | tail -n1)" Levelmin="$(grep "$Argbasename:MIN" "$Magickfifolog" | cut -d= -f2 | LC_ALL=C awk '{ OFMT="%f"; print $1*1 }' | sort -n | head -n1)" level_masks --name="$Argbasename" --first="$Firstimage" --last="$Lastimage" --clut="${Argclut:-yes}" --min="$Levelmin" --max="$Levelmax" [ "$Masktocache" = "yes" ] && :> "${Cachedir}/${Sourcemd5}.mask.${Argbasename}.levelclut${Argclut}" } } } done return 0 } focus_merge_masks() { # Merge different masks. local Mergemask Mergecode Methodnumber= Mergemethod local Command Count local Levelmin Levelmax local Firstimage Lastimage [ "$Maskmethodnumber" = "0" ] && { verbose "Skipping merge, no mask options are specified." return 0 } sendmagickmessage "NOTE:Step 3: Merging masks" Firstimage="${1:-1}" Lastimage="${2:-$Sourceimagenumber}" # --masklevel maskarg_single "masklevel" && { case "$Argword1" in all) Firstimage="1" Lastimage="$Sourceimagenumber" ;; substack) ;; esac } Mergemaskbasename="merge" maskarg_single "maskmerge" ||: Mergemethod="${Argword1:-"Screen"}" case "${Mergemethod,,}" in ### FIXME check further modes multiply|darken) Mergecode=" mpr:white -alpha off" ;; blend|lighten|screen) Mergecode=" mpr:black -alpha off" ;; *) Mergecode=" mpr:gray50 -alpha off" ;; esac Mergecode="$Mergecode -resize ${Maskwidth}x${Maskheight}" ### code for mask merging except those not to be postprocessed for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in mask) [ "${Argmask:-}" != "no" ] && { Mergecode="$Mergecode ( $(maskname "$Argbasename" NUMBER)" [ -n "$Argthreshold1" ] && Mergecode="$Mergecode # =t -black-threshold ${Argthreshold1:-0}%" [ -n "$Argthreshold2" ] && Mergecode="$Mergecode # =T -negate -black-threshold $((100-${Argthreshold2:-100}))% -negate" [ "$Argweight" != "100" ] && Mergecode="$Mergecode # =w +level 0%,${Argweight}%" Mergecode="$Mergecode ) -compose $Mergemethod -composite" } ;; esac done Mergecode="$Mergecode -write mpr:mergemask -delete 0" ### code for mask postprocessing Mergecode="$Mergecode mpr:mergemask" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in merge) Mergecode="$Mergecode # --$Argmethod=${Argoptions%,TYPE*}" case "${Imoptions[$Count]}" in maskblur) Mergecode="$Mergecode -write mpr:mergemask" case "$Argword1" in ""|"blur") Mergecode="$Mergecode -blur ${Argradius1}x${Argsigma1}" ;; "gaussian") Mergecode="$Mergecode -gaussian-blur ${Argradius1}x${Argsigma1}" ;; esac [ "$Argpercent1,$Argpercent2" = "100,0" ] || { Mergecode="$Mergecode mpr:mergemask -compose Blend -define compose:args=$Argpercent2%,$Argpercent1% -composite" } ;; maskcmd) Mergecode="$Mergecode $Argword1" ;; maskdespeckle) for Count in $(seq "$Argnumber1"); do Mergecode="$Mergecode -despeckle" done ;; maskenhance) for Count in $(seq "$Argnumber1"); do Mergecode="$Mergecode -enhance" done ;; maskfft) Fftmask="$(generate_fftshape --shape "$Argword2" --conversion "${Argword1}" --radius "$Argradius1" \ --sigmoidal "$Argsigma1" --sigmoidalmidpoint "$Argpercent1" \ --negate "$Argnumber2" )" Mergecode="$Mergecode -write mpr:mergemask -size ${Fftsize}x${Fftsize} xc:black -swap 0,1 -fx 'v.p[-$(( (Fftsize-Maskwidth)/2 )),-$(( (Fftsize-Maskheight)/2 ))]' -fft ( -clone 0 -write mpr:fft0 -delete 0 ) ( -clone 1 -write mpr:fft1 -delete 0 ) -delete 0,1 mpr:fft0 '$Fftmask' -compose Multiply -composite mpr:fft1 -ift -gravity center -crop ${Maskwidth}x${Maskheight}+0+0 +gravity +repage" ;; maskkuwahara) Mergecode="$Mergecode -kuwahara $Argradius1" ;; maskmorph) Mergecode="$Mergecode -morphology $Argword1:$Argradius2 ${Argword2:-Octagon}:$Argradius1" ;; maskstat) Mergecode="$Mergecode -write mpr:mergemask -statistic $Argword1 ${Argradius1}x${Argradius1}" case "${Argword1,,}" in contrast|mode|standarddeviationX) Mergecode="$Mergecode mpr:mergemask -alpha off -compose Difference -composite" ;; esac ;; masktest) Mergecode="$Mergecode -interpolate Spline -interpolative-resize ${Argpercent1:-50}% -interpolative-resize ${Maskwidth}x${Maskheight} +interpolate" ;; maskthreshold) Mergecode="$Mergecode -black-threshold ${Argthreshold1}% -negate -black-threshold $((100-Argthreshold2))% -negate" ;; maskwave) case "$Argpercent2" in "") Mergecode="$Mergecode -wavelet-denoise ${Argpercent1}%" ;; *) Mergecode="$Mergecode -write mpr:mergemask ( mpr:mergemask -wavelet-denoise ${Argpercent1}% ) ( mpr:mergemask -wavelet-denoise ${Argpercent2}% ) -compose Difference -composite" ;; esac ;; esac ;; esac done ### code for mask merging for those not to be postprocessed for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in mask) [ "${Argmask:-}" = "no" ] && { Mergecode="$Mergecode ( $(maskname "$Argbasename" NUMBER)" [ -n "$Argthreshold1" ] && Mergecode="$Mergecode # =t -black-threshold ${Argthreshold1:-0}%" [ -n "$Argthreshold2" ] && Mergecode="$Mergecode # =T -negate -black-threshold $((100-${Argthreshold2:-100}))% -negate" [ "$Argweight" != "100" ] && Mergecode="$Mergecode # =w +level 0%,${Argweight}%" Mergecode="$Mergecode ) -compose $Mergemethod -composite" } ;; esac done Mergecode="$Mergecode -write mpr:mergemask -delete 0" false && [ "$Testsetup" ] && Mergecode="$Mergecode ./face--bg=Difference_md3a7ecc.tif -grayscale RMS mpr:mergemask -compose Multiply -composite -write mpr:mergemask -delete 0" ### run Mergemaskbasename="$Mergemaskbasename.$(md5cut "$Mergecode $Sourcemd5" )" #Mergemaskbasename="$(unspecialstring "$Mergemaskbasename")" maskexist "$Mergemaskbasename" "$Firstimage" "$Lastimage" && { verbose "Skipping merge, $Mergemaskbasename $Firstimage..$Lastimage already exists" } || { # merge masks sendmagickmessage "STOPWATCH" for Count in $(seq "$Lastimage" -1 "$Firstimage"); do Mergemask="$(maskname "$Mergemaskbasename" "$Count")" sendmagickmessage "PROGRESS:Mask merging $Mergemaskbasename ETA:$Count" #$(sed "s/NUMBER/$(printnum $Count)/g" <<< "$Mergecode" ) Command=" ${Mergecode//"NUMBER"/"$(printnum "$Count")"} mpr:mergemask -clamp -format '$Mergemaskbasename:MAX=%[fx:maxima*100]\n' -write info: -format '$Mergemaskbasename:MIN=%[fx:minima*100]\n' -write info: $Tifstore -write '$Mergemask' $(showimagecode "$Mergemask") -delete 0" [ -f "$(maskname "$Mergemaskbasename" "$Count")" ] || cmd "$Command" done sendmagickmessage "/PROGRESS" cmd_waitforready } # --masklevel Levelmax="$(grep "$Mergemaskbasename:MAX" "$Magickfifolog" | cut -d= -f2 | LC_ALL=C awk '{ OFMT="%f"; print $1*1 }' | sort -n | tail -n1)" Levelmin="$(grep "$Mergemaskbasename:MIN" "$Magickfifolog" | cut -d= -f2 | LC_ALL=C awk '{ OFMT="%f"; print $1*1 }' | sort -n | head -n1)" maskarg_single "masklevel" && [ "$Argword1" != "off" ] && { Newbasename="${Sourcemd5}.mask.${Mergemaskbasename}.${Firstimage}..${Lastimage}.level-t${Argthreshold1}-${Argword1}" [ "$Masktocache" = "yes" ] && [ -e "${Cachedir}/$Newbasename" ] && { sendmagickmessage "NOTE:--masklevel: Skipping, mask is already leveled." } || { level_masks --name="$Mergemaskbasename" --round="$Argthreshold1" --min="${Argpercent1:-$Levelmin}" --max="${Argpercent2:-$Levelmax}" --output="$Newbasename" --first="$Firstimage" --last="$Lastimage" [ "$Masktocache" = "yes" ] && :> "${Cachedir}/$Newbasename" } Mergemaskbasename="$Newbasename" } return 0 } focus_montage() { # Finally: focus stacking local Resultcopy Maskmaxcopy= Depthmap= local Firstimage Lastimage local Focusimagebasename #local Focusimagefile Focusimagemaskfile Focusimagedepthfile local Command Count local Longoptions Parsedoptions local Focuscode= Interpolate Longoptions="maskmax:,depthmap:,output:,firstimage:,lastimage:" Parsedoptions="$(getopt --options="" --longoptions "$Longoptions" -- "$@")" eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --output) Resultcopy="${2:-}" ; shift ;; --maskmax) Maskmaxcopy="${2:-}" ; shift ;; --depthmap) Depthmap="${2:-}" ; shift ;; --firstimage) Firstimage="${2:-}" ; shift ;; --lastimage) Lastimage="${2:-}" ; shift ;; --) ;; esac shift done Firstimage="${Firstimage:-1}" Lastimage="${Lastimage:-$Sourceimagenumber}" # Code for --cut* options Focuscode=" mpr:cutmask -alpha off -write mpr:cutmask_hard" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in focus) Focuscode="$Focuscode # --${Argmethod}=${Argoptions}" case "$Argmethod" in cutalpha) Focuscode="$Focuscode ( mpr:mergemask +level ${Argpercent1}%x${Argpercent2}% ) -compose Darken -composite" ;; cutbg) Argword2="${Sourcemd5}.cutbg.${Argcount}.$(md5cut "$Focuscode $Mergemaskbasename").${Cacheformat}" maskarg_store "$Argcount" generate_image --mode "transparent" --name "mpr:$Argword2" case "$Argword1" in background) Backgroundnumber="$((Backgroundnumber+1))" Backgroundimage[$Backgroundnumber]="${Cachedir}/$Argword2" ;; esac Focuscode="$Focuscode -write mpr:cutmask -delete 0 mpr:sourceimage mpr:cutmask -alpha off -compose CopyOpacity -composite mpr:$Argword2 -compose DstOver -composite -write mpr:$Argword2 -delete 0 mpr:cutmask" ;; cutblur) Focuscode="$Focuscode -write mpr:cutmask" case "$Argword1" in ""|"blur") Focuscode="$Focuscode -blur ${Argradius1}x${Argsigma1}" ;; "gaussian") Focuscode="$Focuscode -gaussian-blur ${Argradius1}x${Argsigma1}" ;; esac [ "$Argpercent1,$Argpercent2" = "100,0" ] || { Focuscode="$Focuscode mpr:cutmask -compose Blend -define compose:args=$Argpercent2%,$Argpercent1% -composite" } ;; cutcmd) Focuscode="$Focuscode $Argword1" ;; cutless) Focuscode="$Focuscode ### --cutless -write mpr:cutmask_max -delete 0 # get part of actual maskmax mpr:focusmask mpr:cutmask_max -alpha off -compose CopyOpacity -composite -write mpr:maskmax_part -delete 0 # paint actual mergemask part into lessmask mpr:lessthanmax mpr:maskmax_part -compose Over -composite -write mpr:lessthanmax -delete 0 # get area of merge mask that is stronger (+offset) than lessmask) mpr:mergemask ( mpr:lessthanmax -evaluate Add ${Argpercent1}% ) -compose MinusSrc -composite -fill white +opaque black -write mpr:cutmask_less -delete 0" [ -n "$Argthreshold1" ] && Focuscode="$Focuscode mpr:cutmask_less -threshold ${Argthreshold1}% -write mpr:cutmask_less -delete 0" Focuscode="$Focuscode # reduce max in focus mask where --lessthanmax takes over mpr:mergemask mpr:cutmask_less -alpha off -compose CopyOpacity -composite -write mpr:mergemask_part -delete 0 mpr:focusmask mpr:mergemask_part -compose Over -composite -write mpr:focusmask -delete 0 # darken lessmask with current merge mask to get a mask with decreasing sharpness after a max peak mpr:lessthanmax mpr:mergemask -alpha off -compose Darken -composite -write mpr:lessthanmax -delete 0 # get area of merge mask equal or sharper than current max (same code as above, but now with less-adjusted maskmax. mpr:mergemask mpr:focusmask -alpha off -compose MinusDst -composite -fill white +opaque black -negate -write mpr:cutmask -delete 0" Focuscode="$Focuscode # drop areas with strength 0 mpr:focusmask -threshold 0% mpr:cutmask -compose Darken -composite -write mpr:cutmask -delete 0" Focuscode="$Focuscode mpr:cutmask ###/ --cutless" ;; cutmax) Focuscode="$Focuscode ### --cutmax -write mpr:cutmask -delete 0 # get part of mergemask that fits current cutmask mpr:mergemask mpr:cutmask -alpha off -compose CopyOpacity -composite -write mpr:mergemask_part -delete 0 # get part of focus mask that fits current cutmask mpr:focusmask mpr:cutmask -alpha off -compose CopyOpacity -composite -write mpr:focusmask_part -delete 0 # get area where cutmask area is stronger than focus mask mpr:mergemask_part ( mpr:focusmask_part -evaluate Subtract $Argpercent1% ) -alpha off -compose Minus -composite -fill white +opaque black -negate -write mpr:cutmask_part -delete 0 # restrict cutmask to area where it is stronger than focus mask mpr:cutmask mpr:cutmask_part -alpha off -compose Darken -composite -write mpr:cutmask -delete 0 mpr:cutmask ###/ --cutmax" ;; cutmorph) Focuscode="$Focuscode -morphology ${Argword1}:${Argradius2} ${Argword2:-Octagon}:${Argradius1}" ;; cutsoft) Focuscode="$Focuscode -blur 0x$Argsigma1 mpr:cutmask -compose Lighten -composite" ;; cuttest) # --cutmore: don't paint already painted area below threshold Focuscode="$Focuscode -write mpr:cutmask -delete 0 mpr:focusimage -alpha extract -write mpr:focusalpha -delete 0 mpr:mergemask -threshold ${Argthreshold1:-5}% ( mpr:focusalpha -negate ) -compose Lighten -composite mpr:cutmask -compose Darken -composite" ;; cutthreshold) Focuscode="$Focuscode ( mpr:mergemask -threshold ${Argthreshold1}% ) -compose Darken -composite" ;; cutwave) Focuscode="$Focuscode -wavelet-denoise ${Argpercent1}%" ;; esac ;; esac done Focuscode="$Focuscode -write mpr:cutmask -delete 0" # $(showimagecode mpr:cutmask) sendmagickmessage "NOTE:Step 4: Focus cut montage" Focusimagebasename="$Sourcemd5.focus.$Firstimage..$Lastimage.$(md5cut "$(masklist "$Mergemaskbasename") $Focuscode")" Focusimagefile="${Cachedir}/$Focusimagebasename.result.tif" Focusimagemaskfile="${Cachedir}/$Focusimagebasename.mask.tif" Focusimagedepthfile="${Cachedir}/$Focusimagebasename.depthmap.tif" [ -e "$Focusimagefile" ] && { note $Focusimagefile $Mergemaskbasename sendmagickmessage "NOTE:Skipping focus cut montage, image already exists." Command="" [ -n "$Maskmaxcopy" ] && [ -f "$Focusimagemaskfile" ] && Command="$Command '$Focusimagemaskfile' $Tifstore -write '$Maskmaxcopy' -delete 0" [ -n "$Depthmap" ] && [ -f "$Focusimagedepthfile" ] && Command="$Command '$Focusimagedepthfile' $Tifstorealpha -write '$Depthmap' -delete 0" Command="$Command '$Focusimagefile' -write mpr:focusimage -delete 0" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argmethod" in cutbg) case "$Argword1" in "compose") Command="$Command # --cutbg ${Cachedir}/$Argword2 mpr:focusimage -compose Over -composite -write mpr:focusimage -delete 0" ;; esac ;; esac done Command="$Command mpr:focusimage $Tifstorealpha -write '$Resultcopy' -delete 0" cmd "$Command" cmd_waitforready return 0 } load_sourceimages "$Firstimage" "$Lastimage" Command=" mpr:transparent -write mpr:focusimage -delete 0 mpr:black -alpha off -write mpr:focusmask -delete 0 mpr:transparent -write mpr:depthmap -delete 0 mpr:black -write mpr:depthmapalpha -delete 0 mpr:transparent -write mpr:lessthanmax -delete 0" cmd "$Command" maskarg_single "prepresize" && Interpolate="$Argword1" || Interpolate="Spline" sendmagickmessage "STOPWATCH" for Count in $(seq "$Lastimage" -1 "$Firstimage"); do sendmagickmessage "PROGRESS:Focus montage ETA:$Count" Command=" # Load images '$(sourceimagename "$Count")' -alpha off -write mpr:sourceimage -delete 0 '$(maskname "$Mergemaskbasename" "$Count")' -alpha off -interpolate $Interpolate -interpolative-resize ${Imagewidth}x${Imageheight} +interpolate -write mpr:mergemask -delete 0 # get area of merge mask equal or sharper than current max mpr:mergemask mpr:focusmask -alpha off -compose Minus -composite -fill white +opaque black -negate -write mpr:cutmask -delete 0 # increase max in focusmask mpr:mergemask mpr:focusmask -compose Lighten -composite -write mpr:focusmask -delete 0" Command="$Command # drop areas with strength 0 mpr:focusmask -threshold 0% mpr:cutmask -compose Darken -composite -write mpr:cutmask -delete 0" # add --cut* options Command="$Command $Focuscode" Command="$Command # get part of source image mpr:sourceimage mpr:cutmask -alpha off -compose CopyOpacity -composite -write mpr:sourcepart $(showimagecode mpr:sourcepart) -delete 0 # add part of source to result mpr:focusimage mpr:sourcepart -compose Over -composite -write mpr:focusimage -delete 0" # $(showimagecode "mpr:focusimage") [ -n "$Depthmap" ] && Command="$Command # depth map mpr:black +level $(calc "100*($Sourceimagenumber-$Count)/$Sourceimagenumber")% mpr:cutmask #-alpha off -compose CopyOpacity -composite -write mpr:depthmappart -delete 0 mpr:depthmap mpr:depthmappart -compose Over -composite -write mpr:depthmap -delete 0" # mpr:depthmapalpha # mpr:cutmask # -compose Lighten -composite # -write mpr:depthmapalpha # -delete 0" cmd "$Command" done sendmagickmessage "/PROGRESS" Command="" Command="$Command # store result image and mask mpr:focusimage $Tifstorealpha -write '$Focusimagefile' -delete 0 mpr:focusmask $Tifstore -write '$Focusimagemaskfile' -delete 0" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argmethod" in cutbg) Command="$Command # --cutbg mpr:$Argword2 -write ${Cachedir}/$Argword2 -delete 0" case "$Argword1" in "compose") # add background to result Command="$Command mpr:$Argword2 mpr:focusimage -compose Over -composite -write mpr:focusimage -delete 0" ;; esac ;; esac done Command="$Command # write to output image mpr:focusimage $Tifstorealpha -write '$Resultcopy' -delete 0" [ -n "$Depthmap" ] && Command="$Command mpr:depthmap # mpr:depthmapalpha # -compose CopyOpacity -composite $Tifstorealpha -write '$Focusimagedepthfile' -write '$Depthmap' -delete 0" [ -n "$Maskmaxcopy" ] && Command="$Command # copy max mask mpr:focusmask $Tifstore -write '$Maskmaxcopy' -delete 0" cmd "$Command" cmd_waitforready Command=" +set registry:cutmask +set registry:cutmask_less +set registry:cutmask_max +set registry:depthmap +set registry:focusimage +set registry:focusmask +set registry:lessthanmax +set registry:maskmax +set registry:maskmax_part +set registry:sourceimage +set registry:mergemask +set registry:mergemask_part +set registry:sourcepart" cmd "$Command" cmd_waitforready #+set registry:resultimage return 0 } focus_main() { local Substackstep Substackbasename local Command Count Startzeit local Backgroundimage local Transparentlayers= sendmagickmessage "NOTE:Step 1: Generating background images" # helper images generate_image --mode "black" generate_image --mode "white" generate_image --mode "transparent" generate_image --mode "gray50" # --background Backgroundnumber="0" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" case "$Argtype" in background) sendmagickmessage "NOTE:Processing --${Imoptions[$Count]}=${Imarguments["$Count"]%,TYPE*}" Backgroundnumber="$((Backgroundnumber+1))" Backgroundimage[$Backgroundnumber]="$(generate_image --showname --mode "$Argword1")" generate_image --mode "$Argword1" ;; esac done # Generate masks Startzeit="$(date +%s)" focus_generate_masks focus_merge_masks sendmagickmessage "VERBOSE:Generating masks ready after: $(date -u -d @$(($(date +%s)-Startzeit)) +"%T")" Command=" # provide transparent mpr:resultimage to paint on mpr:transparent -write mpr:resultimage -delete 0" cmd "$Command" # Focus stacking [ "$Maskmethodnumber" -gt "0" ] && { for Substackstep in $(seq "$Substacknumber" -1 1); do Substackbasename="${Cachedir}/${Sourcemd5}.substack${Substackstep}.${Substackfirstimage[$Substackstep]}..${Substacklastimage[$Substackstep]}.${Substackmd5}" Substackresult[$Substackstep]="${Substackbasename}.result.${Cacheformat}" Substackmask[$Substackstep]="${Substackbasename}.mask.${Cacheformat}" Substackdepthmap[$Substackstep]="${Substackbasename}.depthmap.${Cacheformat}" sendmagickmessage "NEWLINE" sendmagickmessage "NOTE:Generating substack $Substackstep / $Substacknumber: ${Substackfirstimage[$Substackstep]}..${Substacklastimage[$Substackstep]}" # focus Startzeit="$(date +%s)" case "$Extendedsave" in yes) focus_montage --maskmax "${Substackmask[$Substackstep]}" --output "${Substackresult[$Substackstep]}" \ --firstimage="${Substackfirstimage[$Substackstep]}" --lastimage="${Substacklastimage[$Substackstep]}" \ --depthmap "${Substackdepthmap[$Substackstep]}" ;; no) focus_montage --maskmax "${Substackmask[$Substackstep]}" --output "${Substackresult[$Substackstep]}" \ --firstimage="${Substackfirstimage[$Substackstep]}" --lastimage="${Substacklastimage[$Substackstep]}" ;; esac sendmagickmessage "VERBOSE:focus ready after: $(date -u -d @$(($(date +%s)-Startzeit)) +"%T")" done Startzeit="$(date +%s)" for Substackstep in $(seq "$Substacknumber" -1 1); do sendmagickmessage "NOTE:Step 5: Final postprocessing substack $Substackstep / $Substacknumber: ${Substackfirstimage[$Substackstep]}..${Substacklastimage[$Substackstep]}" Substackbasename="${Cachedir}/${Sourcemd5}.substack${Substackstep}.${Substackfirstimage[$Substackstep]}..${Substacklastimage[$Substackstep]}.${Finalmd5}" Substackresultpost[$Substackstep]="${Substackbasename}.result.final.${Cacheformat}" Substackmaskpost[$Substackstep]="${Substackbasename}.mask.final.${Cacheformat}" Substackdepthmappost[$Substackstep]="${Substackbasename}.depthmap.final.${Cacheformat}" #[ -e "${Substackresultpost[$Substackstep]}" ] && { # sendmagickmessage "NOTE:Skipping --final* options, already done." #} || { { Command=" '${Substackresult[$Substackstep]}' $Tifstorealpha -write '${Substackresultpost[$Substackstep]}' -delete 0 '${Substackmask[$Substackstep]}' $Tifstore -write '${Substackmaskpost[$Substackstep]}' -delete 0" case "$Extendedsave" in "yes") Command="$Command '${Substackdepthmap[$Substackstep]}' $Tifstorealpha -write '${Substackdepthmappost[$Substackstep]}' -delete 0" ;; esac cmd "$Command" for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" Option="${Imoptions[$Count]}" case "$Argtype" in postfocus) sendmagickmessage "NOTE:Applying --${Imoptions[$Count]}=${Imarguments["$Count"]%,TYPE*}" case "$Option" in finalalpha) case "$Argword1" in "") alphalevel --image "${Substackresultpost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --percent1 "${Argpercent1}" --percent2 "${Argpercent2}" case "$Extendedsave" in yes) alphalevel --image "${Substackdepthmappost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --percent1 "${Argpercent1}" --percent2 "${Argpercent2}" ;; esac ;; off) Command=" '${Substackresultpost[$Substackstep]}' -alpha off $Tifstore -write '${Substackresultpost[$Substackstep]}' -delete 0" [ "$Extendedsave" = "yes" ] && Command="$Command ${Substackdepthmappost[$Substackstep]} -alpha off $Tifstore -write '${Substackdepthmappost[$Substackstep]}' -delete 0" cmd "$Command" ;; esac ;; finalblur) finalblur --image "${Substackresultpost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --threshold "$Argthreshold1" --sigma1 "$Argsigma1" --sigma2 "$Argsigma2" ;; finalblur2) finalblur2 --image "${Substackresultpost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --threshold "$Argthreshold1" --radius1 "$Argradius1" --radius2 "$Argradius2" ;; finalcmd) Command=" ${Substackresultpost[$Substackstep]} $Argword1 $Tifstorealpha -write ${Substackresultpost[$Substackstep]} -delete 0" cmd "$Command" ;; finalgamma) case "$Argword1" in auto) Command=" ${Substackresultpost[$Substackstep]} -auto-gamma" ;; *) Command=" ${Substackresultpost[$Substackstep]} -gamma $Argword1" ;; esac Command="$Command $Tifstorealpha -write ${Substackresultpost[$Substackstep]} -delete 0" cmd "$Command" ;; finalsharpen) Command=" ${Substackresultpost[$Substackstep]} -adaptive-sharpen ${Argradius1}x${Argsigma1} $Tifstorealpha -write ${Substackresultpost[$Substackstep]} -delete 0" cmd "$Command" ;; finaltest) Command=" ${Substackdepthmappost[$Substackstep]} -threshold 5% ( ${Substackmaskpost[$Substackstep]} -threshold 10% )" ;; finalthreshold) threshold --image "${Substackresultpost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --threshold1 "$Argthreshold1" --threshold2 "$Argthreshold2" --sigma1 "$Argsigma1" \ --cutimage="soft" --cutmask="soft" case "$Extendedsave" in yes) threshold --image "${Substackdepthmappost[$Substackstep]}" --mask "${Substackmaskpost[$Substackstep]}" \ --threshold1 "$Argthreshold1" --threshold2 "$Argthreshold2" \ --cutimage="soft" ;; esac ;; esac ;; esac done } done cmd_waitforready sendmagickmessage "NOTE:Final postprocessing ready after: $(date -u -d @$(($(date +%s)-Startzeit)) +"%T")" Command=" # composing substack results mpr:resultimage" for Substackstep in $(seq "$Substacknumber" -1 1); do Command="$Command '${Substackresultpost[$Substackstep]}' -compose Over -composite" done Command="$Command -write mpr:resultimage $(showimagecode) -delete 0" cmd "$Command" cmd_waitforready } # Generate result image case "$Storelayered" in yes) # --layered sendmagickmessage "NOTE:Generating layered tif" { maskarg_single "threshold" || maskarg_single "finalalpha" ; } && Transparentlayers="yes" generate_image --mode cyan generate_image --mode magenta generate_image --mode yellow generate_image --mode green Command=" # --layered mpr:resultimage mpr:cyan mpr:magenta mpr:yellow mpr:green" for Count in $(seq "$Backgroundnumber"); do Command="$Command '${Backgroundimage[$Count]}'" done for Substackstep in $(seq "$Substacknumber" -1 1); do [ "$Transparentlayers" = "yes" ] && Command="$Command '${Substackresult[$Substackstep]}'" Command="$Command '${Substackresultpost[$Substackstep]}'" done Command="$Command $Tifstorealpha #-define tiff:write-layers=yes -write '$Resultimage' $(showimagecode "$Resultimage") -delete 0" ;; no) Command=" mpr:transparent" for Count in $(seq "$Backgroundnumber"); do Command="$Command '${Backgroundimage[$Count]}' -compose Over -composite" done Command="$Command mpr:resultimage -compose Over -composite -write mpr:resultimage -delete 0 mpr:resultimage $Tifstorealpha -write '$Resultimage' -delete 0" ;; esac cmd "$Command" cmd_waitforready [ "$Maskmethodnumber" = "0" ] && [ "$Backgroundnumber" = "0" ] && { note "No mask option and no background has been specified. imfuse has generated an empty transparent result image. Suggestion: Try at least '--background=enfuse', or short: --bg" return 0 } # -X: Save mask and colorize depth map [ "$Extendedsave" = "yes" ] && [ "$Substacknumber" -gt "0" ] && { case "$Substacknumber" in 1) Command=" '${Substackmaskpost[1]}' $Tifstorealpha -write '$Resultmask' -delete 0 '${Substackdepthmappost[1]}' -write mpr:depthmap -delete 0" ;; *) evaluate max "$Resultmask" "${Substackmaskpost[@]}" Command=" # composing substack depth maps mpr:transparent" for Substackstep in $(seq "$Substacknumber" -1 1); do Command="$Command '${Substackdepthmappost[$Substackstep]}' -compose Over -composite" done Command="$Command -write mpr:depthmap -delete 0" ;; esac Command="$Command mpr:depthmap -alpha extract -write mpr:alpha -delete 0 mpr:depthmap mpr:rainbow -clut mpr:alpha -compose CopyOpacity -composite $Tifstorealpha -write '$Resultdepthmap' $(showimagecode "$Resultdepthmap") -delete 0" cmd "$Command" cmd_waitforready } false && [ "$Testsetup" ] && { Command=" $Resultdepthmap ( $Resultdepthmap -blur 0x0.5 \) -compose Difference -composite ( $Resultmask -negate ) -compose Multiply -composite -negate -statistic mean 10x10 -write show: -threshold ${Testarg:-90}% -write mpr:filter -delete 0 $Resultimage mpr:filter -compose CopyOpacity -composite -write $Resultimage -delete 0" cmd "$Command" cmd_waitforready } return 0 } ### mask args maskarg_add() { Imoptionsnumber="$((Imoptionsnumber+1))" Argmethod="${1#--}" [ "$Argmethod" = "background" ] && Argmethod="bg" Imoptions[$Imoptionsnumber]="$Argmethod" Imarguments[$Imoptionsnumber]="${2:-}" maskarg_parse "$Imoptionsnumber" maskarg_check "$Imoptionsnumber" maskarg_store "$Imoptionsnumber" case "$Argtype" in mask) Maskmethodnumber="$((Maskmethodnumber+1))" ;; esac # to add a new option: # parse_options(): add to $Longoptions # parse_options(): add it to big collection entry) # maskarg_defaultvalue(): specify type # maskarg_defaultvalue(): add default values (if any) # maskarg_checkallowed(): add name to all allowed arguments # check_outputname(): type single needs a custom entry } maskarg_check() { # check for allowed values, set defaults. # maskarg_parse() must run beforehand. # use maskarg_store() afterwards. Argtype="$(maskarg_defaultvalue "$Argmethod" type)" [ -z "$Argchannel" ] && Argchannel="$(maskarg_defaultvalue "$Argmethod" channel)" [ -z "$Argcolorspace" ] && Argcolorspace="$(maskarg_defaultvalue "$Argmethod" colorspace)" [ -z "$Argimage" ] && Argimage="$(maskarg_defaultvalue "$Argmethod" image)" [ -z "$Arglevel" ] && Arglevel="$(maskarg_defaultvalue "$Argmethod" level)" [ -z "$Argclut" ] && Argclut="$(maskarg_defaultvalue "$Argmethod" clut)" [ -z "$Argnegate" ] && Argnegate="$(maskarg_defaultvalue "$Argmethod" negate)" [ -z "$Argmask" ] && Argmask="$(maskarg_defaultvalue "$Argmethod" mask)" [ -z "$Argnumber1" ] && Argnumber1="$(maskarg_defaultvalue "$Argmethod" number1)" [ -z "$Argnumber2" ] && Argnumber2="$(maskarg_defaultvalue "$Argmethod" number2)" [ -z "$Argpercent1" ] && Argpercent1="$(maskarg_defaultvalue "$Argmethod" percent1)" [ -z "$Argpercent2" ] && Argpercent2="$(maskarg_defaultvalue "$Argmethod" percent2)" [ -z "$Argradius1" ] && Argradius1="$(maskarg_defaultvalue "$Argmethod" radius1)" [ -z "$Argradius2" ] && Argradius2="$(maskarg_defaultvalue "$Argmethod" radius2)" [ -z "$Argsigma1" ] && Argsigma1="$(maskarg_defaultvalue "$Argmethod" sigma1)" [ -z "$Argsigma2" ] && Argsigma2="$(maskarg_defaultvalue "$Argmethod" sigma2)" [ -z "$Argthreshold1" ] && Argthreshold1="$(maskarg_defaultvalue "$Argmethod" threshold1)" [ -z "$Argthreshold2" ] && Argthreshold2="$(maskarg_defaultvalue "$Argmethod" threshold2)" [ -z "$Argword1" ] && Argword1="$(maskarg_defaultvalue "$Argmethod" word1)" [ -z "$Argword2" ] && Argword2="$(maskarg_defaultvalue "$Argmethod" word2)" [ -z "$Argdiff" ] && Argdiff="$(maskarg_defaultvalue "$Argmethod" diff)" [ -n "$Argchannel" ] && maskarg_checkallowed "$Argmethod" channel "$Argchannel" [ -n "$Argcolorspace" ] && maskarg_checkallowed "$Argmethod" colorspace "$Argcolorspace" [ -n "$Argimage" ] && maskarg_checkallowed "$Argmethod" image "$Argimage" [ -n "$Arglevel" ] && maskarg_checkallowed "$Argmethod" level "$Arglevel" [ -n "$Argclut" ] && maskarg_checkallowed "$Argmethod" clut "$Argclut" [ -n "$Argmask" ] && maskarg_checkallowed "$Argmethod" mask "$Argmask" [ -n "$Argnegate" ] && maskarg_checkallowed "$Argmethod" negate "$Argnegate" [ -n "$Argnumber1" ] && maskarg_checkallowed "$Argmethod" number1 "$Argnumber1" [ -n "$Argnumber2" ] && maskarg_checkallowed "$Argmethod" number2 "$Argnumber2" [ -n "$Argradius1" ] && maskarg_checkallowed "$Argmethod" radius1 "$Argradius1" [ -n "$Argradius2" ] && maskarg_checkallowed "$Argmethod" radius2 "$Argradius2" [ -n "$Argsigma1" ] && maskarg_checkallowed "$Argmethod" sigma1 "$Argsigma1" [ -n "$Argsigma2" ] && maskarg_checkallowed "$Argmethod" sigma2 "$Argsigma2" [ -n "$Argpercent1" ] && maskarg_checkallowed "$Argmethod" percent1 "$Argpercent1" [ -n "$Argpercent2" ] && maskarg_checkallowed "$Argmethod" percent2 "$Argpercent2" [ -n "$Argthreshold1" ] && maskarg_checkallowed "$Argmethod" threshold1 "$Argthreshold1" [ -n "$Argthreshold2" ] && maskarg_checkallowed "$Argmethod" threshold2 "$Argthreshold2" [ -n "$Argweight" ] && maskarg_checkallowed "$Argmethod" weight "$Argweight" [ -n "$Argword1" ] && maskarg_checkallowed "$Argmethod" word1 "$Argword1" [ -n "$Argword2" ] && maskarg_checkallowed "$Argmethod" word2 "$Argword2" [ -n "$Argdiff" ] && maskarg_checkallowed "$Argmethod" diff "$Argdiff" case "$Argtype" in single) grep -q -w "$Argmethod" <<< "$Singleoptionlist" && error "Option --$Argmethod can be specified only once." Singleoptionlist="$Singleoptionlist $Argmethod" ;; esac case $Argmethod in compose|evaluate) Argimage="${Argimage:-$Argword1}" Argword1="" ;; esac } maskarg_parse() { local Key Line Value eval $Arglist Argcount="${1:-}" Argmethod="${Imoptions[$Argcount]}" Argoptions="${Imarguments[$Argcount]}" while read -d, Line; do [[ "$Line" == *"="* ]] && { Value="${Line#*"="}" Key="${Line%"=${Value}"}" } || { Key="${Line//[0-9-+.]/}" # remove digits including +-. [ "$Key" = "%" ] && Key="p" Key="${Key%"%"}" #Value="${Line#${Key:-NOKEY}}" Value="${Line/${Key:-NOKEY}/}" } Value="${Value//%/}" Value="${Value//+/}" case $Key in "w") Argweight="$Value" ;; "r") [ -z "$Argradius1" ] && Argradius1="$Value" || Argradius2="$Value" ;; "R") Argradius2="${Value}" ;; "s") [ -z "$Argsigma1" ] && Argsigma1="$Value" || Argsigma2="$Value" ;; "S") Argsigma2="${Value}" ;; "p") [ -z "$Argpercent1" ] && Argpercent1="$Value" || Argpercent2="$Value" ;; "P") Argpercent2="$Value" ;; "t") Argthreshold1="$Value" ;; "T") Argthreshold2="$Value" ;; "n") [ -z "$Argnumber1" ] && Argnumber1="$Value" || Argnumber2="$Value" ;; "N") Argnumber2="$Value" ;; "I") Argimage="$Value" ;; "C") Argcolorspace="$Value" ;; "c") Argchannel="$Value" ;; "mask") Argmask="${Value:-yes}" ;; "diff") Argdiff="${Value:-yes}" ;; "neg") Argnegate="${Value:-yes}" ;; "level") Arglevel="${Value:-yes}" ;; "clut") Argclut="${Value:-yes}" ;; "TYPE") Argtype="$Value" ;; "AUTOWEIGHT") Argautoweight="$Value" ;; "BASENAME") Argbasename="$Value" ;; "W1"|"WORD1") Argword1="$Value" ;; "W2"|"WORD2") Argword2="$Value" ;; *) [ -z "$Argword1" ] && Argword1="$Line" || Argword2="$Line" ;; esac done <<< "$Argoptions," return 0 } maskarg_store() { Argoptions="" Argcount="${1:-}" [ "$Argweight" ] && Argoptions="$Argoptions,w$Argweight" [ "$Argmask" ] && Argoptions="$Argoptions,mask=$Argmask" [ "$Argradius1" ] && Argoptions="$Argoptions,r$Argradius1" [ "$Argradius2" ] && Argoptions="$Argoptions,R$Argradius2" [ "$Argsigma1" ] && Argoptions="$Argoptions,s$Argsigma1" [ "$Argsigma2" ] && Argoptions="$Argoptions,S$Argsigma2" [ "$Argpercent1" ] && Argoptions="$Argoptions,p$Argpercent1" [ "$Argpercent2" ] && Argoptions="$Argoptions,P$Argpercent2" [ "$Argnumber1" ] && Argoptions="$Argoptions,n$Argnumber1" [ "$Argnumber2" ] && Argoptions="$Argoptions,N$Argnumber2" [ "$Argimage" ] && Argoptions="$Argoptions,I=$Argimage" [ "$Argcolorspace" ] && Argoptions="$Argoptions,C=$Argcolorspace" [ "$Argchannel" ] && Argoptions="$Argoptions,c$Argchannel" [ "$Argdiff" ] && Argoptions="$Argoptions,diff=$Argdiff" [ "$Argnegate" ] && Argoptions="$Argoptions,neg=$Argnegate" [ "$Arglevel" ] && Argoptions="$Argoptions,level=$Arglevel" [ "$Argclut" ] && Argoptions="$Argoptions,clut=$Argclut" [ "$Argthreshold1" ] && Argoptions="$Argoptions,t$Argthreshold1" [ "$Argthreshold2" ] && Argoptions="$Argoptions,T$Argthreshold2" [ "$Argword1" ] && Argoptions="$Argoptions,W1=$Argword1" [ "$Argword2" ] && Argoptions="$Argoptions,W2=$Argword2" [ "$Argtype" ] && Argoptions="$Argoptions,TYPE=$Argtype" [ "$Argautoweight" ] && Argoptions="$Argoptions,AUTOWEIGHT=yes" [ "$Argbasename" ] && Argoptions="$Argoptions,BASENAME=$Argbasename" Argoptions="${Argoptions#,}" Imarguments[$Argcount]="$Argoptions" } maskarg_checkallowed() { local Argmethod Option Arg Argmethod="${1:-}" Option="${2:-}" Arg="${3:-}" case "$Argmethod" in experimental|cuttest|masktest) return 0 ;; esac case "$Option" in channel) case "$Argtype" in mask) ;; *) case "${1:-}" in colorspace) ;; *) error "--$Argmethod does not take argument colorspace channel: c$Arg" ;; esac ;; esac ;; clut|diff|level|mask|negate) case "$Argtype" in mask) case "$Arg" in yes|no) ;; *) error "--$Argmethod: Argument $Option does not take value '$Arg'. Allowed values are yes|no" ;; esac ;; *) error "--$Argmethod does not take argument '$Option'." ;; esac ;; colorspace) case "$Argtype" in mask) checkmagicklist colorspace "$Arg" || error "--$Argmethod: unknown colorspace for C=: $Arg Please choose one out of 'magick -list colorspace'" Argcolorspace="$(checkmagicklist colorspace "$Arg" print)" ;; *) case "${1:-}" in colorspace) checkmagicklist colorspace "$Arg" || error "--$Argmethod: unknown colorspace for C=: $Arg Please choose one out of 'magick -list colorspace'" Argcolorspace="$(checkmagicklist colorspace "$Arg" print)" ;; *) error "--$Argmethod does not take argument colorspace: C=$Arg" ;; esac ;; esac ;; image) case "$Argtype" in mask) generate_image --mode "$Arg" --showname >/dev/null || error "--$Argmethod: unknown image generation format for Option -I='$Arg'. Please choose one out of: enfuse max min mean median 'magick -list evaluate' 'magick -list compose' 'magick -list color" Argimage="$(generate_image --mode "$Argimage" --showmode)" ;; *) error "--$Argmethod does not take argument image comparision: I=$Arg" ;; esac ;; number1) case "$Argmethod" in freichen|laplacian|maskdespeckle|maskenhance|substacks) ;; *) error "--$Argmethod does not take argument number1: n$Arg" ;; esac ;; number2) case "$Argmethod" in fft|maskfft) ;; substacks) ;; *) error "--$Argmethod does not take argument number2: n$Arg" ;; esac ;; percent1) case "$Argmethod" in cutalpha|cutblur|cutless|cutmax|cutwave|fft|finalalpha|maskblur|maskfft|masklevel|maskwave|prepresize|resize|substacks|wavelet) ;; *) error "--$Argmethod does not take argument percent1: p$Arg" ;; esac ;; percent2) case "$Argmethod" in cutalpha|cutblur|finalalpha|maskblur|masklevel|maskwave|substacks|wavelet) ;; *) error "--$Argmethod does not take argument percent2: P$Arg" ;; esac ;; radius1) case "$Argmethod" in blur|comet|cutblur|dog|fft|finalblur2|finalsharpen|log|maskblur|maskfft|maskkuwahara) ;; morphology|cutmorph|maskmorph) ;; diffstat|statistic|maskstat) ;; substacks) ;; *) error "--$Argmethod does not take argument radius1: r$Arg" ;; esac ;; radius2) case "$Argmethod" in diffstat|fft|statistic) ;; morphology|cutmorph|maskmorph) ;; *) error "--$Argmethod does not take argument radius2: R$Arg" ;; esac ;; short) ;; sigma1) case "$Argmethod" in blur|comet|cutblur|cutsoft|dog|fft|finalblur|finalsharpen|finalthreshold|log|maskblur|maskfft) ;; *) error "--$Argmethod does not take argument sigma1: s$Arg" ;; esac ;; sigma2) case "$Argmethod" in blur) [ "$Arg" = "0" ] && Argsigma2="" ;; finalblur|dog|log) ;; *) error "--$Argmethod does not take argument sigma2: S$Arg" ;; esac ;; threshold1) case "$Argtype" in mask) ;; *) case "$Argmethod" in cutless|cutthreshold|finalblur|finalblur2|finalthreshold|masklevel|maskthreshold) ;; *) error "--$Argmethod does not take argument threshold1: t$Arg" ;; esac ;; esac ;; threshold2) case "$Argtype" in mask) ;; *) case "$Argmethod" in finalthreshold|maskthreshold) ;; *) error "--$Argmethod does not take argument threshold2: T$Arg" ;; esac ;; esac ;; weight) case "$Argtype" in mask) ;; *) error "--$Argmethod does not take argument weight: w$Arg" ;; esac ;; word1) case "$Argmethod" in bg) generate_image --mode "$Arg" --showname >/dev/null || error "--background: unknown image generation mode: '$Arg'. Please choose one out of: enfuse max min mean median 'magick -list evaluate' 'magick -list compose' 'magick -list color" Argword1="$(generate_image --mode "$Arg" --showmode)" ;; blur|maskblur|cutblur) Argword1="${Argword1,,}" case "$Argword1" in ""|"gaussian") ;; *) error "--$Argmethod does not know argument: $Argword1" ;; esac ;; channel|colorspace) checkmagicklist colorspace "$Arg" || error "--$Argmethod: unknown colorspace: $Arg Please choose one out of 'magick -list colorspace'" Argcolorspace="$(checkmagicklist colorspace "$Arg" print)" Argword1="" ;; cmd|cmddiff|cutcmd|finalcmd|maskcmd) ;; cutbg) case "$Argword1" in background|compose|off) ;; *) error "--$Argmethod: unknown argument for word1: $Arg" ;; esac ;; depthmap) [ -f "$Arg" ] || error "--$Argmethod: File not found: $Arg" ;; compose|comet|compass|freichen|maskmerge) checkmagicklist compose "${Arg%2}" || error "--$Argmethod: unknown argument for word1: $Arg Please choose one out of 'magick -list compose'" Argword1="$(checkmagicklist compose "${Arg%2}" print)$(tr -d -c "2" <<< "$Arg")" ;; evaluate) checkmagicklist evaluate "$Arg" || error "--$Argmethod: unknown argument for word1: $Arg Please choose one out of 'magick -list evaluate'" Argword1="$(checkmagicklist evaluate "$Arg" print)" ;; fft|maskfft) case "$Argword1" in cos*|gaussian|gradient|inverselog*|log*|pow*|solid) ;; *) error "--$Argmethod: unknown argument for word1: $Arg" ;; esac ;; finalalpha) case "$Argword1" in ""|"off") ;; *) error "--$Argmethod: unknown argument for word1: $Arg" ;; esac ;; finalgamma) ;; masklevel) case "$Arg" in all|off|substack) ;; *) error "--$Argmethod does not take argument: $Arg" ;; esac ;; morphology|cutmorph|maskmorph) checkmagicklist "morphology" "$(tr -d '2' <<< "$Arg")" || error "--$Argmethod: unknown argument for word1: $Arg Please choose one out of 'magick -list morphology'" Argword1="$(checkmagicklist morphology "$Arg" print)" ;; prepresize|resize) checkmagicklist interpolate "$Arg" || error "--$Argmethod: unknown argument for word1: $Arg Please choose one out of 'magick -list interpolate'" Argword1="$(checkmagicklist interpolate "$Arg" print)" ;; substacks) case "$Arg" in kurt) ;; *) error "--$Argmethod does not take argument: $Arg" ;; esac ;; statistic|diffstat|maskstat) case "${Arg,,}" in stdev|dev) Arg="StandardDeviation" ;; min) Arg="Minimum" ;; max) Arg="Maximum" ;; grad) Arg="Gradient" ;; esac checkmagicklist statistic "$Arg" || error "--$Argmethod: unknown argument for word1: $Arg Please choose out of 'magick -list statistic'" Argword1="$(checkmagicklist statistic "$Arg" print)" ;; *) error "--$Argmethod does not take argument word1: $Arg" ;; esac ;; word2) case "$Argmethod" in channel|colorspace) Argchannel="$Arg" Argword2="" ;; diffstat) case "${Arg,,}" in stdev|dev) Arg="StandardDeviation" ;; min) Arg="Minimum" ;; max) Arg="Maximum" ;; grad) Arg="Gradient" ;; esac checkmagicklist statistic "$Arg" || error "--$Argmethod: unknown argument for word2: $Arg Please choose out of 'magick -list statistic'" Argword2="$(checkmagicklist statistic "$Arg" print)" ;; fft|maskfft) case "$Argword2" in circle|cross|xcross|rhombus|square) ;; *) error "--$Argmethod: unknown argument for word2: $Arg" ;; esac ;; morphology|cutmorph|maskmorph) checkmagicklist kernel "$Arg" || error "--$Argmethod: unknown kernel: $Arg Please choose out of 'magick -list kernel'" Argword2="$(checkmagicklist kernel "$Arg" print)" ;; *) error "--$Argmethod does not take argument word2: $Arg" ;; esac ;; esac return 0 } maskarg_defaultvalue() { local Argmethod Argument Output= Argmethod="${1:-}" Argument="${2:-}" case "$Argument" in channel) case "$Argmethod" in channel) Output="" ;; chroma) Output="1" ;; colorspace) Output="" ;; darkness) Output="2" ;; lightness) Output="2" ;; saturation) Output="1" ;; esac ;; colorspace) case "$Argmethod" in channel) Output="" ;; chroma) Output="HCL" ;; colorspace) Output="sRGB" ;; darkness) Output="HSL" ;; lightness) Output="HSL" ;; saturation) Output="HSL" ;; esac ;; clut) ;; diff) case "$Argmethod" in blur) [ -z "$Argsigma2" ] && Output="yes" ;; cmddiff) Output="yes" ;; comet) Output="yes" ;; fft) case "${Argnumber2,,}" in 0) Output="yes" ;; 1) Output="no" ;; esac [ -n "$Argradius2" ] && Output="no" ;; morphology) case ${Argword1,,} in close|edge|edgein|edgeout|bottomhat|tophat) ;; *) Output="yes" ;; ### FIXME really? esac ;; resize) Output="yes" ;; statistic) case ${Argword1,,} in gradient|standarddeviation) ;; *) Output="yes" ;; esac ;; wavelet) [ -z "$Argpercent2" ] && Output="yes" ;; *) case "$Argtype" in mask) Output="no" ;; esac ;; esac ;; image) case "$Argmethod" in enfuse) Output="enfuse" ;; max) Output="Max" ;; mean) Output="Mean" ;; median) Output="Median" ;; min) Output="Min" ;; esac ;; level) case "$Argmethod" in channel) Output="no" ;; chroma) Output="no" ;; compose) Output="no" ;; darkness) Output="no" ;; depthmap) Output="no" ;; enfuse) Output="no" ;; evaluate) Output="no" ;; lightness) Output="no" ;; max) Output="no" ;; mean) Output="no" ;; median) Output="no" ;; min) Output="no" ;; saturation) Output="no" ;; *) case "$Argtype" in mask) Output="yes" ;; esac ;; esac ;; mask) case "$Argmethod" in channel) Output="no" ;; chroma) Output="no" ;; compose) Output="no" ;; darkness) Output="no" ;; depthmap) Output="no" ;; enfuse) Output="no" ;; evaluate) Output="no" ;; lightness) Output="no" ;; max) Output="no" ;; mean) Output="no" ;; median) Output="no" ;; min) Output="no" ;; saturation) Output="no" ;; *) case "$Argtype" in mask) Output="yes" ;; esac ;; esac ;; negate) case "$Argmethod" in darkness) Output="yes" ;; esac ;; number1) case "$Argmethod" in freichen) Output="0" ;; laplacian) Output="3" ;; maskdespeckle|maskenhance) Output="1" ;; esac ;; number2) case "$Argmethod" in fft|maskfft) Output="0" ;; esac ;; percent1) case "$Argmethod" in cutalpha) Output="0" ;; cutblur) Output="100" ;; cutless) Output="20" ;; cutmax) Output="0" ;; cutwave) Output="25" ;; fft) Output="50" ;; finalalpha) Output="0" ;; maskblur) Output="100" ;; maskfft) Output="50" ;; masklevel) Output="" ;; maskwave) Output="20" ;; prepresize) Output="50" ;; resize) Output="50" ;; wavelet) Output="5" ;; esac ;; percent2) case "$Argmethod" in cutalpha) Output="100" ;; cutblur) Output="$((100-Argpercent1))" ;; finalalpha) Output="100" ;; maskblur) Output="$((100-Argpercent1))" ;; masklevel) Output="" ;; wavelet) Output="" ;; esac ;; radius1) case "$Argmethod" in blur) Output="0" ;; comet) Output="0" ;; cutblur) Output="0" ;; cutmorph) Output="1" ;; diffstat) Output="3" ;; dog) Output="0" ;; fft) Output="75" ;; finalblur2) Output="4" ;; finalsharpen) Output="0" ;; log) Output="0" ;; maskblur) Output="0" ;; maskfft) Output="25" ;; maskkuwahara) Output="4" ;; maskmorph) Output="1" ;; maskstat) Output="5" ;; morphology) Output="1" ;; statistic) Output="2" ;; esac ;; radius2) case "$Argmethod" in cutmorph) Output="1" ;; diffstat) Output="$Argradius1" ;; fft) Output="" ;; maskmorph) Output="1" ;; morphology) Output="1" ;; statistic) ;; esac ;; sigma1) case "$Argmethod" in blur) Output="0.3" ;; comet) Output="5" ;; cutblur) Output="1" ;; cutsoft) Output="1" ;; dog) Output="0.3" ;; fft) Output="" ;; finalblur) Output="5" ;; finalsharpen) Output="1" ;; finalthreshold) Output="0" ;; log) Output="0.2" ;; maskblur) Output="1" ;; maskfft) Output="" ;; esac ;; sigma2) case "$Argmethod" in blur) Output="$(calc "$Argsigma1 * 1.6")" ;; finalblur) Output="2" ;; dog) Output="$(calc "$Argsigma1 * 1.6")" ;; log) Output="" ;; esac ;; threshold1) case "$Argmethod" in cutthreshold) Output="20" ;; finalblur) Output="20" ;; finalblur2) Output="20" ;; finalthreshold) Output="20" ;; masklevel) Output="5" ;; maskthreshold) Output="50" ;; esac ;; threshold2) case "$Argmethod" in finalthreshold) Output="100" ;; maskthreshold) Output="100" ;; esac ;; type) case "$Argmethod" in blur|cmd|cmddiff|comet|compass|diffstat|dog|fft|freichen|kirsch|laplacian|log|morphology|prewitt|resize|roberts|sobel|statistic|wavelet) Output="mask" ;; channel|chroma|darkness|lightness|saturation) Output="mask" ;; compose|depthmap|enfuse|evaluate|max|mean|median|min) Output="mask" ;; experimental) Output="mask" ;; maskblur|maskcmd|maskdespeckle|maskenhance|maskfft|maskkuwahara|maskmorph|maskstat|masktest|maskthreshold|maskwave) Output="merge" ;; cutalpha|cutbg|cutblur|cutcmd|cutless|cutmax|cutmorph|cutsoft|cuttest|cutthreshold|cutwave) Output="focus" ;; finalalpha|finalblur|finalblur2|finalcmd|finalgamma|finalsharpen|finalthreshold) Output="postfocus" ;; bg) Output="background" ;; colorspace|masklevel|maskmerge|prepresize) Output="single" ;; substacks) Output="substack" ;; *) error "maskarg_defaultvalue(): Unknown type for method $Argmethod" ;; esac ;; word1) case "$Argmethod" in bg) Output="enfuse" ;; blur) Output="" ;; cmd|maskcmd|cutcmd|finalcmd) Output="" ;; comet) Output="Lighten" ;; compass) Output="Lighten" ;; compose) Output="Overlay" ;; cutbg) Output="compose" ;; cutblur) Output="" ;; cutmorph) Output="Erode" ;; depthmap) Output="NO_FILE_SPECIFIED" ;; diffstat) Output="Median" ;; evaluate) Output="Max" ;; fft) Output="gradient" ;; finalgamma) Output="auto" ;; freichen) Output="Screen" ;; maskblur) Output="" ;; maskfft) Output="log1000" ;; masklevel) Output="all" ;; maskmerge) Output="Screen" ;; maskmorph) Output="Close" ;; maskstat) Output="Mean" ;; morphology) Output="Edge" ;; prepresize) Output="Spline" ;; resize) Output="Spline" ;; statistic) Output="StandardDeviation" ;; format) Output="tif" ;; cacheformat) Output="mpc" ;; esac ;; word2) case "$Argmethod" in diffstat) Output="Mean" ;; fft|maskfft) Output="circle" ;; morphology|cutmorph|maskmorph) Output="Octagon" ;; esac ;; esac echo "$Output" } maskarg_single() { # provide arguments of an option $1 that can be specified only once local Count= eval $Arglist for Count in $(seq "$Imoptionsnumber"); do [ "${Imoptions[$Count]}" = "${1:-}" ] && maskarg_parse "$Count" && break done [ "${Imoptions[$Count]:-}" = "${1:-}" ] } maskarg_short() { local Output= Argument Append maskarg_parse "${1:-}" for Argument in $Arglist; do Argument="${Argument%'=""'}" Argument="${Argument#Arg}" Append="" case "$Argument" in weight) #[ -n "$Argweight" ] && [ "$Argweight" != "100" ] && Append=",w$Argweight" [ -n "$Argweight" ] && [ -z "$Argautoweight" ] && Append=",w$Argweight" ;; radius1) [ -n "$Argradius1" ] && Append=",r$Argradius1" [ "$Argradius1" = "$(maskarg_defaultvalue "$Argmethod" radius1)" ] && case "$Argmethod" in blur|comet|cutblur|dog|finalsharpen|log|maskblur) Append="" ;; esac ;; radius2) [ -n "$Argradius2" ] && Append=",R$Argradius2" [ "$Argradius2" = "$(maskarg_defaultvalue "$Argmethod" radius2)" ] && case "$Argmethod" in diffstat) Append="" ;; esac ;; sigma1) [ -n "$Argsigma1" ] && Append=",s$Argsigma1" [ "$Argsigma1" = "$(maskarg_defaultvalue "$Argmethod" sigma1)" ] && case "$Argmethod" in #fft) Append="" ;; esac ;; sigma2) [ -n "$Argsigma2" ] && Append=",S$Argsigma2" ;; percent1) [ -n "$Argpercent1" ] && Append=",p$Argpercent1" [ "$Argpercent1" = "$(maskarg_defaultvalue "$Argmethod" percent1)" ] && case "$Argmethod" in maskblur|cutalpha|cutblur|fft|finalalpha|maskfft) Append="" ;; esac ;; percent2) [ -n "$Argpercent2" ] && Append=",P$Argpercent2" [ "$Argpercent2" = "$(maskarg_defaultvalue "$Argmethod" percent2)" ] && case "$Argmethod" in cutalpha|cutblur|finalalpha|maskblur) Append="" ;; esac ;; number1) [ -n "$Argnumber1" ] && Append=",n$Argnumber1" [ "$Argnumber1" = "$(maskarg_defaultvalue "$Argmethod" number1)" ] && case "$Argmethod" in fft|freichen) Append="" ;; esac ;; number2) [ -n "$Argnumber2" ] && Append=",N$Argnumber2" [ "$Argnumber2" = "$(maskarg_defaultvalue "$Argmethod" number2)" ] && case "$Argmethod" in fft|maskfft) Append="" ;; esac ;; image) [ -n "$Argimage" ] && Append=",I=$Argimage" [ "$Argimage" = "$(maskarg_defaultvalue "$Argmethod" image)" ] && case "$Argmethod" in enfuse|max|min|mean|median) Append="" ;; esac ;; colorspace) [ -n "$Argcolorspace" ] && Append=",C=$Argcolorspace" [ "$Argcolorspace" = "$(maskarg_defaultvalue "$Argmethod" colorspace)" ] && case "$Argmethod" in chroma|darkness|lightness|saturation) Append="" ;; esac case "$Argmethod" in colorspace) Append=",${Append#,C=}" ;; esac ;; channel) [ -n "$Argchannel" ] && Append=",c$Argchannel" [ "$Argchannel" = "$(maskarg_defaultvalue "$Argmethod" channel)" ] && case "$Argmethod" in chroma|darkness|lightness|saturation) Append="" ;; esac ;; diff) [ "$Argdiff" = "yes" ] && Append=",diff" [ "$Argdiff" = "no" ] && Append=",diff=no" [ "$Argdiff" = "$(maskarg_defaultvalue "$Argmethod" diff)" ] && case "$Argmethod" in *) Append="" ;; esac ;; negate) [ "$Argnegate" = "yes" ] && Append=",neg" [ "$Argnegate" = "no" ] && Append=",neg=no" [ "$Argnegate" = "$(maskarg_defaultvalue "$Argmethod" negate)" ] && case "$Argmethod" in darkness) Append="" ;; esac ;; word1) [ -n "$Argword1" ] && case "$Argmethod" in #cmd|maskcmd|cutcmd|depthmap|finalcmd) # Append=",'$Argword1'" ;; *) Append=",$Argword1" ;; esac #[ "${Argword1,,}" = "standarddeviation" ] && Append=",StDev" [ "$Argword1" = "$(maskarg_defaultvalue "$Argmethod" word1)" ] && case "$Argmethod" in comet|compass|cutbg|diffstat|fft|finalgamma|freichen|statistic|masklevel|maskmerge) Append="" ;; esac ;; word2) [ -n "$Argword2" ] && Append=",W2=$Argword2" [ "$Argword2" = "$(maskarg_defaultvalue "$Argmethod" word2)" ] && case "$Argmethod" in cutmorph|diffstat|fft|maskfft|maskmorph|morphology) Append="" ;; esac ;; level) [ -n "$Arglevel" ] && case "$Arglevel" in yes) Append=",level" ;; no) Append=",level=no" ;; esac [ "$Arglevel" = "$(maskarg_defaultvalue "$Argmethod" level)" ] && case "$Argmethod" in enfuse|evaluate|max|mean|median|min|compose) Append="" ;; channel|chroma|darkness|lightness|saturation) Append="" ;; depthmap) Append="" ;; *) Append="" ;; esac ;; clut) [ -n "$Argclut" ] && [ -n "$Arglevel" ] && case "$Argclut" in yes) Append=",clut" ;; no) Append=",clut=no" ;; esac ;; threshold1) [ -n "$Argthreshold1" ] && Append=",t$Argthreshold1" ;; threshold2) [ -n "$Argthreshold2" ] && Append=",T$Argthreshold2" [ "$Argthreshold2" = "$(maskarg_defaultvalue "$Argmethod" threshold2)" ] && case "$Argmethod" in finalthreshold|maskthreshold) Append="" ;; esac ;; mask) [ "$Argmask" = "yes" ] && Append=",mask" [ "$Argmask" = "no" ] && Append=",mask=no" [ "$Argmask" = "$(maskarg_defaultvalue "$Argmethod" mask)" ] && case "$Argmethod" in *) Append="" ;; esac ;; esac Output="$Output$Append" done Output="${Output#,}" [ -n "$Output" ] && Output="=${Output}" Output="--${Argmethod}${Output}" echo "$Output" } ### magick -script interaction cmd() { local Command Command="${1:-}" [ -n "$Magickfifo" ] && { echo "$Command" >> "$Magickfifo" echo "$Command" >> "$Magickscriptlog" echo "$Command" >> "/tmp/imfuse.log" } return } cmd_waitforkeyvalue() { # wait for keyword $1 in output of magick. Prints output after first : local Key Line Key="${1:-}" tail -f "$Magickfifolog" | while read Line; do grep -q "$Key" <<< "$Line" && break done echo "$(cut -d: -f2- <<< "$Line")" return 0 } cmd_waitforready() { # wait for magick command toolchain to be ready local Key Key="$(generate_key)" sendmagickmessage "$Key" cmd_waitforkeyvalue "$Key" >/dev/null return } readmagickmessage() { # read and parse messages from magick local Line Code Content local Stopwatch Etacount Etanumber Etaduration Etatime Etaline Etapos local Esc Colnorm Colgreenbg Esc="$(printf '\033')" Colnorm="${Esc}[0m" Colgreenbg="${Esc}[42m" tail -f "$Magickfifolog" | while read Line; do Code="$(cut -d: -f1 <<< "$Line")" Content="$(cut -d: -f2- <<< "$Line")" Columns="${COLUMNS:-80}" grep -q "ETA:" <<< "$Content" && { Columns="$((Columns))" Etacount="${Content#*ETA:}" grep -q "/" <<< "$Etacount" && Etanumber="${Etacount#*/}" Etacount="${Etacount%/*}" Etanumber="${Etanumber:-$Sourceimagenumber}" Etaduration="$(( $(date +%s) - Stopwatch ))" Etatime="$(( Etacount * Etaduration / (Etanumber-Etacount+1) ))" Etaline="$Etacount/$Etanumber ETA: $(date -u -d @$Etatime +"%T"), DUR: $(date -u -d @$Etaduration +"%T")" Content="${Content%ETA:*}$Etaline" Content="$(printf "%-${Columns}s" "$Content")" Content="$(cut -c1-${Columns} <<< "imfuse$Subprocess: $Content")" Etapos="$((Columns*Etacount/Etanumber))" Content="${Colgreenbg}${Content:0:$Etapos}${Colnorm}${Content:$Etapos}" } case "$Code" in PROGRESS) printsameline "$Content" ;; /PROGRESS|NEWLINE) echo "$Colnorm" >&2 ;; NOTE) note "$Content" ;; VERBOSE) verbose "$Content" ;; SHOW) showimage "$Content" ;; STOPWATCH) Stopwatch="$(date +%s)" ;; esac case "$Line" in *"@ warning"*) note "magick WARNING: $Line" ;; *"@ error"*) error "magick ERROR: $Line" break ;; esac done } sendmagickmessage() { # send message $1 to output of magick local Command= case "$(cut -d: -f1 <<< "${1:-}")" in PROGRESS) Command="$Command # $(cut -d: -f2- <<< "${1:-}")" ;; NOTE) Command="$Command #### $(cut -d: -f2- <<< "${1:-}") #### #" ;; *) Command="$Command # sending message ${1:-}" ;; esac Command="$Command rose: -format '${1:-}\n' -write info: -delete -1" cmd "$Command" return 0 } showimagecode() { local Image Imageformat # magick code to store current image and print a showimage message for readmagickmessage() [ "$Showimageprocessing" = "yes" ] && { Image="${1:-}" Imageformat="$(rev <<< "$Image" | cut -d. -f1 | rev)" [ "$(cut -c1-4 <<< "$Image")" = "mpr:" ] && Imageformat="mpr" case "$Imageformat" in ""|"mpr"|"mpc"|"miff") echo "# show image -compress none -depth 8 -write '$Showimage' -format 'SHOW:$Showimage\n' -write info:" ;; *) sendmagickmessage "SHOW:$Image" ;; esac } return 0 } getmagickinfo() { # get info output for image $1 in format $2 local Key1 Key2 Command Key1="$(generate_key)" Key2="$(generate_key)" # get info ${2:-} about image ${1:-} Command=" '${1:-}' -format '$Key1\n${2:-}\n$Key2\n' -write info: -delete -1" cmd "$Command" cmd_waitforready cmd_waitforkeyvalue "$Key2" >/dev/null sed -n "/$Key1/,/$Key2/p" "$Magickfifolog" | sed '1d ; $d' } ### main trap_sigint() { local Count=0 trap - ERR set +x [ "$$" = "$Imfusepid" ] && { note "Received SIGINT" [ -n "$Magickscriptpid" ] && kill "$Magickscriptpid" multicore_break finish 1 : } || { note "Received SIGINT in subshell $SHLVL: $$" kill -s SIGINT "$Imfusepid" trap - EXIT exit 130 } } finish() { local Count=0 trap - ERR set +x multicore_wait break 2>/dev/null cmd '-exit' while ps -p "$Magickscriptpid" >/dev/null 2>&1; do sleep 1 Count="$((Count+1))" [ "$Count" -gt "3" ] && printsameline "Waiting infinitely since $Count seconds for magick pid $Magickscriptpid to terminate" done echo "" >&2 # magick -script [ -n "$Magickmessagepid" ] && kill "$Magickmessagepid" exec 3>&- rm -f "$Magickfifo" "$Magickfifolog" >/dev/null 2>&1 #[ -f "$Magickscriptlog" ] && cp "$Magickscriptlog" . # cache [ -f "${Showimage:-}" ] && rm "$Showimage" [ "$Keepcache" = "no" ] && [ -d "${Cachedir}" ] && rm -R "${Cachedir}" [ -n "$Cachebasedir" ] && case "$Singleaction" in --rmcache) note "Option --rmcache: Cleaning cache: $Cachebasedir" [ -d "$Cachebasedir" ] && rm -R "$Cachebasedir" ;; *) note "Current cache size: $(du -hs "$Cachebasedir" 2>/dev/null | tr "\t" " " || echo "0") You can clean the cache folder with option --rmcache." ;; esac # jobs trap - SIGINT trap - EXIT jobs -l -r wait exit "${1:-0}" } declare_variables() { Arglist=' Argweight="" Argradius1="" Argradius2="" Argsigma1="" Argsigma2="" Argnumber1="" Argnumber2="" Argpercent1="" Argpercent2="" Argimage="" Argcolorspace="" Argchannel="" Argnegate="" Argthreshold1="" Argthreshold2="" Argword1="" Argword2="" Argcount="" Argdiff="" Arglevel="" Argclut="" Argmask="" Argmethod="" Argoptions="" Argtype="" Argautoweight="" Argbasename="" ' eval $Arglist Align="" Cachebasedir="" Cachedir="" Colorspace="" Colorspacechannel="" #Exiflist="" Exifsourceimage="" Extendedsave="no" Fftsize="" Finalmd5="" Firstimage="" Force="" Freemem="" Image="" Imageformat="" Imageheight="" Imagelistmemsize="" Imagememsize="" Imagewidth="" #Imarguments="" Imfusepid="$$" #Imoptions="" Imoptionsnumber="0" #Imsourceimagelist="" Keepcache="no" Limitmemory="" Line="" Loadsourceimages="" Longname="" Magickbin="" Magickfifo="" Magickfifolog="" Magickmessagepid="" Magickpixelmemory="" Magickscriptlog="" Magickscriptpid="" Magickversion="" Maskmethodnumber="0" Cacheformat="" Masktocache="no" Mergemaskbasename="" Optionmd5="" Outputbasename="" Outputdir="" Parsedoptions="" Revertimagelist="" Resultbasename="" Resultdepthmap="" Resultimage="" Resultmask="" Resultsearchmask="" Resulttimestamp="" Showimage="" Showimageprocessing="no" Singleaction="" Singleoptionlist="" Softmode="no" Sourceimage="" #Sourceimagelist="" Sourceimagenumber="0" Sourceimagepath="" Sourcemd5="" Startzeit="$(date +%s)" Storelayered="no" Subprocess="" Substackautoall="no" Substackfirstimage="" Substacklastimage="" #Substackmask="" #Substackmaskpost="" Substackmd5="" #Substackresult="" #Substackresultpost="" Substacknumber="0" Substacksize="" Substackstep="" Testarg="" Testimage="" Testsetup="" Tifstore="-alpha off +repage -depth 16 -quality 100% -compress lzw -type optimize" Tifstorealpha="+repage -depth 16 -quality 100% -compress lzw -type TrueColorAlpha" Verbose="" Video="" Videoframecount="" Viewnior="" return 0 } parse_options() { local Shortoptions Longoptions Parsedoptions Shortoptions="BCfhLo:vVWX" Longoptions="cache::,help,license,limit-memory::,mask2cache,cacheformat::,rmcache,sub::,test::,verbose,version" Longoptions="$Longoptions,basename::,exif::,force::,format::,longname,output:,showname,video" Longoptions="$Longoptions,align,revert" Longoptions="$Longoptions,colorspace::,prepresize::" Longoptions="$Longoptions,blur::,comet::,compass::,diffstat::,dog::,fft::,freichen::,kirsch::,laplacian::,log::,morphology::,prewitt::,resize::,roberts::,sobel::,statistic::,wavelet::" Longoptions="$Longoptions,compose::,depthmap::,enfuse::,evaluate::,max::,mean::,median::,min::" Longoptions="$Longoptions,channel::,chroma::,darkness::,lightness::,saturation::" Longoptions="$Longoptions,cmd::,cmddiff::,experimental::" Longoptions="$Longoptions,maskblur::,maskcmd::,maskdespeckle::,maskenhance::,maskfft::,maskkuwahara::,masklevel::,maskmerge::,maskmorph::,maskstat::,masktest::,maskthreshold::,maskwave::" Longoptions="$Longoptions,cutalpha::,cutbg::,cutblur::,cutcmd::,cutless::,cutmax::,cutmorph::,cutsoft::,cuttest::,cutthreshold::,cutwave::" Longoptions="$Longoptions,substacks::" Longoptions="$Longoptions,finalalpha::,finalblur::,finalblur2::,finalcmd::,finalgamma::,finalsharpen::,finalthreshold::" Longoptions="$Longoptions,background::,bg::,layered" Parsedoptions="$(getopt --options "$Shortoptions" --longoptions "$Longoptions" --name "$0" -- "$@")" || error "Error while parsing options." eval set -- "$Parsedoptions" while [ $# -gt 0 ]; do case "${1:-}" in --align) Align="yes" ;; -B) Outputbasename="auto" ;; --basename) Outputbasename="${2:-auto}" ; shift ;; -C) Cachedir="auto" ;; --cache) Cachedir="${2:-auto}" ; shift ;; --cacheformat) Cacheformat="${2:-mpc}" ; shift ;; --exif) Exifsourceimage="${2:-auto}" ; shift ;; -f) Force="yes" ;; --force) Force="${2:-yes}" ; shift ;; --format) Imageformat="${2:-tif}" ; shift ;; --layered) Storelayered="yes" ;; --limit-memory) Limitmemory="${2:-"80%"}" ; shift ;; -L|--longname) Longname="yes" ;; --mask2cache) Masktocache="yes" ;; -o|--output) Resultimage="${2:-}" ; shift ;; --revert) Revertimagelist="-r" ;; --sub) Subprocess=" ${2:-sub}" ; shift ;; --test) Testsetup="test" ; Testarg="${2:-}" ; shift ;; -v|--verbose) Verbose="yes" ;; --video) Video="auto" ;; -V) Showimageprocessing="yes" ;; -W) Viewnior="yes" ;; -X) Extendedsave="yes" ;; -h|--help|--license|--rmcache|--showname|--version) Singleaction="${1:-}" ;; --colorspace|--prepresize|\ --cmd|--cmddiff|--experimental|\ --blur|--comet|--compass|--diffstat|--dog|--fft|--freichen|--kirsch|--laplacian|--log|--morphology|--prewitt|--resize|--roberts|--sobel|--statistic|--wavelet|\ --channel|--chroma|--darkness|--lightness|--saturation|\ --compose|--depthmap|--enfuse|--evaluate|--max|--mean|--median|--min|\ --maskblur|--maskcmd|--maskdespeckle|--maskenhance|--maskfft|--maskkuwahara|--masklevel|--maskmerge|--maskmorph|--maskstat|--masktest|--maskthreshold|--maskwave|\ --cutalpha|--cutbg|--cutblur|--cutcmd|--cutless|--cutmax|--cutmorph|--cutsoft|--cuttest|--cutthreshold|--cutwave|\ --finalalpha|--finalblur|--finalblur2|--finalcmd|--finalgamma|--finalsharpen|--finalthreshold|\ --bg|--background|\ --substacks) maskarg_add "${1:-}" "${2:-}" ; shift ;; --) ;; *) [ -f "${1:-}" ] || error "File not found: ${1:-}" Sourceimagenumber="$((Sourceimagenumber+1))" Sourceimagelist[$Sourceimagenumber]="${1:-}" ;; esac shift done #IFS=$'\n' Sourceimagelist=( '' $(sort -V $Revertimagelist <<< "${Sourceimagelist[@]:-}") ) ; unset IFS IFS=$'\n' Sourceimagelist=( '' $(sort -V $Revertimagelist <<< "${Sourceimagelist[*]:-}")) ; unset IFS unset Sourceimagelist[0] return 0 } check_options() { local Arg Weightsum= Weightrest Masknoweightcount= local Firstimage Lastimage Substacksize Position # w, weight for Count in $(seq $Imoptionsnumber); do maskarg_parse "$Count" case $Argtype in mask) Weightsum="$((Weightsum + Argweight))" [ -z "$Argweight" ] && Masknoweightcount="$((Masknoweightcount+1))" ;; esac done Weightrest="$((100-Weightsum))" LC_ALL=C awk 'BEGIN {exit !('${Weightsum:-100}' > 100)}' && error "Sum of weight arguments exceed 100%." for Count in $(seq $Imoptionsnumber); do maskarg_parse "$Count" case $Argtype in mask) [ -z "$Argweight" ] && { LC_ALL=C awk 'BEGIN {exit !('${Weightrest:-0}' <= 0)}' && error "Sum of weight arguments exceed 100%, nothing left for --$Argmethod." Argweight="$(calc "$Weightrest / $Masknoweightcount ")" Argweight="${Argweight%.*}" Argautoweight="yes" } maskarg_store "$Count" ;; esac done # --align [ "$Align" = "yes" ] && { command -v focus-stack >/dev/null || error "--align: focus-stack not found. https://github.com/PetteriAimonen/focus-stack" } # --basename case $Outputbasename in auto) Outputbasename="$(basename "$(pwd)")" ;; esac [ -d "$Outputbasename" ] && { Outputdir="$Outputbasename" Outputbasename="" } || { [[ "$Outputbasename" = *"/"* ]] && { Outputdir="$(dirname "$Outputbasename")" Outputbasename="$(basename "$Outputbasename")" } } # --cacheformat Cacheformat="${Cacheformat:-"$(maskarg_defaultvalue cacheformat word1)"}" # --colorspace maskarg_single "colorspace" && { Colorspace="${Argcolorspace:-}" Colorspacechannel="${Argchannel:-}" } Colorspace="${Colorspace:-"$(maskarg_defaultvalue colorspace colorspace)"}" # --exif [ "$Exifsourceimage" ] && { [ "$Exifsourceimage" = "auto" ] && { Exifsourceimage="${Sourceimagelist[1]}" [ "$Revertimagelist" ] && Exifsourceimage="${Sourceimagelist[$Imagenumber]}" } command -v exiftool >/dev/null || error "Option --exif: exiftool not found." } # --format Imageformat="${Imageformat:-"$(maskarg_defaultvalue format word1)"}" # --masklevel maskarg_single "masklevel" || { grep -q "TYPE=mask" <<< "${Imarguments[*]}" && maskarg_add "masklevel" } # --substacks maskarg_single "substacks" || { [ "$Maskmethodnumber" -gt "0" ] && { maskarg_add "substacks" "n1,N${Sourceimagenumber}" Substackautoall="yes" } } for Count in $(seq "$Imoptionsnumber"); do maskarg_parse "$Count" [ "$Argtype" = "substack" ] && { [ -z "$Argnumber1" ] && [ -n "$Argpercent1" ] && Argnumber1="$(numberofpercent "$Argpercent1")" [ -z "$Argnumber2" ] && [ -n "$Argpercent2" ] && Argnumber2="$(numberofpercent "$Argpercent2")" grep -q "%" <<< "$Argradius1" && Argradius1="$(numberofpercent "$Argradius1")" [ -z "$Argnumber2" ] && [ -z "$Argradius1" ] && Argradius1="$(numberofpercent "5")" # default radius 5% [ -z "$Argnumber1" ] && Argnumber1="$(numberofpercent "14")" # default ~7 substacks maskarg_store "$Count" # single substack [ -n "$Argnumber1" ] && [ -n "$Argnumber2" ] && { Argradius1="${Argradius1:-0}" Firstimage="$((Argnumber1-Argradius1))" Lastimage="$((Argnumber2+Argradius1))" [ "$Firstimage" -lt "1" ] && Firstimage="1" [ "$Lastimage" -gt "$Sourceimagenumber" ] && Lastimage="$Sourceimagenumber" Substacknumber="$((Substacknumber+1))" Substackfirstimage[$Substacknumber]="$Firstimage" Substacklastimage[$Substacknumber]="$Lastimage" } # generate set of substacks [ -n "$Argnumber1" ] && [ -z "$Argnumber2" ] && { Substacksize="$Argnumber1" Position="0" while [ "${Lastimage:-0}" -lt "$Sourceimagenumber" ]; do Firstimage="$((Position-Argradius1))" Lastimage="$((Position+Substacksize+Argradius1))" [ "$Firstimage" -lt "1" ] && Firstimage="1" [ "$Argword1" = "kurt" ] && Firstimage="1" [ "$Lastimage" -gt "$Sourceimagenumber" ] && Lastimage="$Sourceimagenumber" Substacknumber="$((Substacknumber+1))" Substackfirstimage[$Substacknumber]="$Firstimage" Substacklastimage[$Substacknumber]="$Lastimage" Position="$((Position+Substacksize))" done } } done # -V [ "$Showimageprocessing" = "yes" ] && { command -v geeqie >/dev/null || error "Option -V: geeqie not found." } # -W [ "$Viewnior" ] && { command -v feh >/dev/null || error "Option -W: image viewer feh not found." } ### FIXME dependency checks for enfuse return 0 } check_magick() { # magick Magickbin="$(command -v magick)" false && [ -e "/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.6" ] && { # https://imagemagick.org/script/openmp.php # https://goog-perftools.sourceforge.net/doc/tcmalloc.html Magickbin="env LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libtcmalloc_minimal.so.4.5.6 $Magickbin" } [ -z "$Magickbin" ] && error "Command 'magick' not found. Please install ImageMagick version 7." # check magick version Magickversion="$($Magickbin -version)" # Bytesperpixel*(channels)*unknownfactor grep -q " Q8 " <<< "$Magickversion" && Magickpixelmemory="$(calc "1*(5) *1.2")" grep -q " Q8-HDRI " <<< "$Magickversion" && Magickpixelmemory="$(calc "2*(5) *1.2")" grep -q " Q16 " <<< "$Magickversion" && Magickpixelmemory="$(calc "2*(5) *1.2")" grep -q " Q16-HDRI " <<< "$Magickversion" && Magickpixelmemory="$(calc "4*(5) *1.2")" grep -q " Q32 " <<< "$Magickversion" && Magickpixelmemory="$(calc "4*(5) *1.2")" grep -q " Q32-HDRI " <<< "$Magickversion" && Magickpixelmemory="$(calc "8*(5) *1.2")" Magickversion="$(head -n1 <<< "$Magickversion" | cut -d' ' -f3)" } main_setup() { local Resultimage_check # --cache Cachebasedir="$HOME/.cache/imfuse" [ -n "${Cachedir}" ] && { Masktocache="yes" Keepcache="yes" [ "${Cachedir}" = "auto" ] && Cachedir="" } Cachedir="${Cachedir//"~"/"$HOME"}" [ -n "${Cachedir}" ] && Cachebasedir="${Cachedir}" case "$Masktocache" in yes) [ -z "${Cachedir}" ] && Cachedir="$Cachebasedir" ;; no) [ -z "${Cachedir}" ] && Cachedir="$Cachebasedir/$Imfusepid" ;; esac mkdir -p "${Cachedir}" || error "Error creating cache folder ${Cachedir}" Testimage="${Cachedir}/test.png" Showimage="${XDG_RUNTIME_DIR:-${Cachedir}}/imfuse.showimage.tif" # check RAM Freemem="$(printfreememory)" [ -z "$Freemem" ] && { note "WARNING: failed to estimate free memory. Blindly guessing 1GB." Freemem="1000000" } Limitmemory="${Limitmemory:-"80%"}" grep -q "%" <<< "$Limitmemory" && { Limitmemory="$(tr -d "%" <<< "$Limitmemory")" Limitmemory="$((Freemem*Limitmemory/100))" } || { Limitmemory="$((Limitmemory*1000))" } # Image properties [ "$Sourceimagenumber" -gt "0" ] && { Image="${Sourceimagelist[1]}" Imagewidth="$($Magickbin identify -format '%w' "$Image")" Imageheight="$($Magickbin identify -format '%h' "$Image")" Imagememsize="$((Imagewidth*Imageheight*Magickpixelmemory/1000))" Imagememsize="$((Imagememsize*125/100))" # by observation. Alpha channel? Imagelistmemsize="$((Sourceimagenumber*Imagememsize))" Sourcemd5="$(md5cut "$( ls -l --full-time "${Sourceimagelist[@]}" )" )" # --output, --basename check_exifstring check_outputname } # check if source images should be loaded to RAM case "$Masktocache" in yes) compare "$((Imagelistmemsize))" -lt "$(calc "$Limitmemory*0.9")" && Loadsourceimages="yes" || Loadsourceimages="no" ;; no) compare "$((Imagelistmemsize))" -lt "$(calc "$Limitmemory*0.3")" && Loadsourceimages="yes" || Loadsourceimages="no" ;; esac verbose " imfuse Version: $Version Imagemagick version: $Magickversion Image number: $Sourceimagenumber Image width: $Imagewidth px Image height: $Imageheight px Image memory size (estimated): $Imagememsize KB Image list memory size (estimated): $((Imagelistmemsize/1024)) MB Memory total (including zram): $(($(printtotalmemory)/1000)) MB Memory currently free: $((Freemem/1000)) MB Memory limit for imagemagick: $((Limitmemory/1000)) MB Loading source images to RAM: $Loadsourceimages" note "Parsed image processing options: $Parsedoptions" # start magick -script in background Magickfifo="${Cachedir}/magickfifo.$Imfusepid" Magickfifolog="${Cachedir}/magickmessage.$Imfusepid.log" Magickscriptlog="${Cachedir}/magickscript.log" :> "$Magickscriptlog" :> /tmp/imfuse.log mkfifo "$Magickfifo" exec 3<>"$Magickfifo" nice magick -limit memory ${Limitmemory}KB -define registry:temporary-path="${Cachedir}" -script - <&3 >"$Magickfifolog" 2>&1 & Magickscriptpid="$!" readmagickmessage & Magickmessagepid="$!" Command=" #! /usr/bin/magick -script # Generated by imfuse $Version # Date: $(date) # Image: $Resultbasename # Options: $Parsedoptions ( wizard: -colorspace $Colorspace" [ -n "$Colorspacechannel" ] && Command="$Command -channel $Colorspacechannel -separate +channel" Command="$Command ) -statistic StandardDeviation 2 -evaluate Divide 3 -separate -evaluate-sequence Add -auto-level $Tifstorealpha -write '$Showimage' -delete 0 # clut for depth map -size 1x512 gradient:white-red gradient:red-orange gradient:orange-yellow gradient:yellow-green gradient:green-blue gradient:blue-indigo #gradient:indigo-black -append -rotate 90 -flip -write mpr:rainbow -delete 0 " cmd "$Command" cmd_waitforready # start geeqie [ "$Showimageprocessing" = "yes" ] && nohup geeqie -t -r --File:"$Showimage" /dev/null 2>&1 # --force case "$Force" in "") Resultimage_check="$(find . -name "${Resultsearchmask}.result.*" | tail -n1)" [ -n "$Resultimage_check" ] && { Resultimage="$Resultimage_check" Resultdepthmap="$(find . -name "${Resultsearchmask}.depthmap.*" | tail -n1)" Resultmask="$(find . -name "${Resultsearchmask}.mask.*" | tail -n1)" note "Output image already exists with matching md5sum. You can force imfuse to run nonetheless with option --force." showresult finish } ;; mask) forcemask forcelevel forcemerge forcefocus forcepost ;; level) forcelevel forcemerge forcefocus forcepost ;; merge) forcemerge forcefocus forcepost ;; focus) forcefocus forcepost ;; post|postfocus) forcepost ;; esac return 0 } main() { set -Eu trap trap_sigint SIGINT trap finish EXIT trap 'traperror $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[@]})' ERR shopt -s lastpipe declare_variables check_magick parse_options "$@" check_options case "$Singleaction" in -h|--help) usage finish ;; --license) license finish ;; --version) note "$Version" finish ;; esac [ "$Sourceimagenumber" = "0" ] && [ -z "$Singleaction" ] && error "No source images provided." main_setup multicore_init case "$Singleaction" in --rmcache) finish ;; --showname) echo "$Resultimage" finish ;; esac # --align [ "$Align" = "yes" ] && { align || error "Error in align()." } # run focus_main # --exif: transfer EXIF metadata from first image to result, store imfuse version and parsed options [ "$Exifsourceimage" ] && exiftransfer "$Exifsourceimage" "$Resultimage" command -v exiftool && { case "$Extendedsave" in yes) Exiflist=("$Resultimage" "$Resultmask" "$Resultdepthmap") #exiftool -overwrite_original -Software="imfuse v$Version" -ImageDescription="$Parsedoptions" -Make="$Sourceimagepath" "$Resultimage" "$Resultmask" "$Resultdepthmap" ;; no) Exiflist=("$Resultimage") #exiftool -overwrite_original -Software="imfuse v$Version" -ImageDescription="$Parsedoptions" -Make="$Sourceimagepath" "$Resultimage" ;; esac for Image in "${Exiflist[@]}"; do [ -f "$Image" ] && exiftool -overwrite_original -Software="imfuse v$Version" -ImageDescription="$Parsedoptions" -Make="$Sourceimagepath" "$Image" done : } || { note "Failed to store metadata in output image. Is exiftool installed?" } # throw out cmd "-exit" note "Ready after $(date -u -d @$(($(date +%s)-Startzeit)) +"%T")" showresult # --video [ "$Video" ] && generate_video return 0 } main "$@" finish