snibgo's ImageMagick pages

Focus stack

A set of photographs can be taken of an object at different focus settings. These can be automatically combined, selecting the best pixels from each image for the final output.

See also Wikipedia: Focus stacking.

Photography

Use a tripod.

Decide on the approximate number of exposures, and how far to refocus between exposures. Lens DOF (depth of field) markings, and a viewfinder DOF preview, may assist.

With a good old-fashioned camera, where the lens barrel is twisted for focus, twist roughly equally between exposures.

Ensure the photos have the same (or very similar) exposures.

The script assumes the images are sorted, with the closest focus first.

The images should be sorted, either from front to back or back to front. (But does this really make a difference?)

If you don't care how this works, skip stright to Using the script.

Sample image set

The samples were taken with a Micro-Nikkor 105/2.8 on a Nikon D800. The exposure was 1/100s @ f/8, ASA 100.

I took 25 photos at the same exposure, and have used the in-camera jpegs, with no processing. (I overshot the focus on the penultimate frame.)

All operations were performed on full-size images, about 7500x5000 pixels, and the results were reduced or cropped for display on this web page.

Here are the first, third and fifth images, resized for the web:

rem goto skip

set WEB_SIZE=-resize 500x500

set SRCDIR=\pictures\20140619

set SRC1=%SRCDIR%\AGA_1867.jpg
set SRC2=%SRCDIR%\AGA_1869.jpg
set SRC3=%SRCDIR%\AGA_1871.jpg

%IM%convert %SRC1% %WEB_SIZE% fs_src1.jpg
%IM%convert %SRC2% %WEB_SIZE% fs_src2.jpg
%IM%convert %SRC3% %WEB_SIZE% fs_src3.jpg
fs_src1.jpg fs_src2.jpg fs_src3.jpg

The page will walk through the stages of creating one image from these three photos, before using a single script to process all the 25 photos.

The method

  1. Resize the photos and align them.
  2. For images #1 and #2, make a mask showing which image contributes to the result, and make the resulting image.
  3. For the result of step 2 and image #3, repeat step 2.
  4. Repeat for all images.

For (n) images we make (n-1) masks.

The mask is black where the first of two images is the sharpest, and white where the second is sharpest. So a masked "-compose Over" generates each result.

Various techniques could measure sharpness. See Details, details. The script uses whichPixel.bat, which uses a blurred difference of two blurs. This gives a "detail mask" that is lighter where there is plenty of detail and darker where there is less detail. To find which of two images contains most detail, we compare the two detail masks. For each pixel, When we have two images that measure detail, where lighter means more detail, we compare them with " -compose MinusSrc -composite -fill White +opaque Black", and this results in the mask.

Step 1: resize and align

Cameras generally focus by moving the lens forwards or backwards. For closer objects, the lens is moved forwards. This changes the angle of view.

An alternative technique for focusing, particularly for macro photography, is to move the entire camera forwards or backwards. This keeps the angle of view constant but changes perspective, thus changing the relative size of distant and near objects. The method described here might cope with this focusing technique.

Finding the scale takes about 5 minutes per photo. This is horribly slow, when we might have 25 photos. I hope to improve this.

for /F "usebackq" %%L ^
in (`%IM%identify ^
  -format "PREC=%%[fx:1+1/max(w,h)]" %SRC1%`) ^
do set %%L

call %PICTBAT%whatScalePrec %SRC1% %SRC2% 0.9 1.1 10 %PREC%
if ERRORLEVEL 1 exit /B 1

echo SCALE=%wspSCALE% FACT=%wspFACT% DODGY=%wspDODGY% 
set SCALE1=%wspSCALE%

call %PICTBAT%whatScalePrec %SRC2% %SRC3% 0.9 1.1 10 %PREC%
if ERRORLEVEL 1 exit /B 1

echo SCALE=%wspSCALE% FACT=%wspFACT% DODGY=%wspDODGY% 
set SCALE2=%wspSCALE%

echo SCALE1=%SCALE1% SCALE2=%SCALE2% 
SCALE=0.979424792 FACT=1.0000822 DODGY=0 
SCALE=0.980764083 FACT=1.0000822 DODGY=0 
SCALE1=0.979424792 SCALE2=0.980764083 

To scale the first two images to match the third, we need to scale the first image (SRC1) by SCALE1*SCALE2, and the second image (SRC2) by SCALE2.

(It would be more convenient to directly use whatScale on the first and last images. But in the general case, the first and last images will be too different from each other to find the scale.)

:skip

for /F "usebackq" %%L ^
in (`%IM%identify ^
  -precision 9 ^
  -format "f1=%%[fx:%SCALE1%*%SCALE2%]" xc:`) ^
do set %%L

%IM%convert ^
  %SRC1% ^
  -virtual-pixel Edge -distort SRT %f1%,0 ^
  -write fs_s1.tiff ^
  %WEB_SIZE% ^
  fs_s1_sm.jpg

%IM%convert ^
  %SRC2% ^
  -virtual-pixel Edge -distort SRT %SCALE2%,0 ^
  -write fs_s2.tiff ^
  %WEB_SIZE% ^
  fs_s2_sm.jpg

Here are the first two images, resized:

fs_s1_sm.jpg fs_s2_sm.jpg

Virtual-pixel has smeared the edges. From this, we can see that the first image has shrunk more than the second, as expected. Using "-virtual-pixel Edge" would have looked better, but the method would then think that the sharp edge with the black border represented real-work detail.

Camera mechanics are never perfect. Changing the focus may also shift the optical axis of the lens. So we also need to align the photos. I assume that only a simple translation is required, with no rotation, shear or perspective.

As before, in the general case we can't find the direct translation to the final image, but must find the alignment from each photo to the next.

call %PICTBAT%whatTrans fs_s1.tiff fs_s2.tiff
echo wtX=%wtX% wtY=%wtY% wtSCORE=%wtSCORE% wtDODGY=%wtDODGY% 
set wtX1=%wtX%
set wtY1=%wtY%

call %PICTBAT%whatTrans fs_s2.tiff %SRC3%
echo wtX=%wtX% wtY=%wtY% wtSCORE=%wtSCORE% wtDODGY=%wtDODGY% 
set wtX2=%wtX%
set wtY2=%wtY%
set /A wtX1+=%wtX%
set /A wtY1+=%wtY%

echo wtX1=%wtX1% wtY1=%wtY1%  wtX2=%wtX2% wtY2=%wtY2% 
wtX=-8 wtY=1 wtSCORE=0.0500568 wtDODGY=0 
wtX=9 wtY=-12 wtSCORE=0.0377895 wtDODGY=0 
wtX1=1 wtY1=-11  wtX2=9 wtY2=-12 

Build the strings for "-distort SRT", and apply them:

call %PICTBAT%mSRTdelta %SRC1% %wtX1% %wtY1%
set SRT1=%x0%,%y0%,%f1%,0,%x1%,%y1%
call %PICTBAT%mSRTdelta %SRC2% %wtX2% %wtY2%
set SRT2=%x0%,%y0%,%SCALE2%,0,%x1%,%y1%

%IM%convert ^
  %SRC1% ^
  -virtual-pixel Edge -distort SRT %SRT1% ^
  -write fs_s1b.tiff ^
  %WEB_SIZE% ^
  fs_s1b_sm.jpg

%IM%convert ^
  %SRC2% ^
  -virtual-pixel Edge -distort SRT %SRT2% ^
  -write fs_s2b.tiff ^
  %WEB_SIZE% ^
  fs_s2b_sm.jpg

Here are the first two images, resized and aligned:

fs_s1b_sm.jpg fs_s2b_sm.jpg

Step 2: make and apply mask

:skip

set wpDEBUG=1
call %PICTBAT%whichPix fs_s1b.tiff fs_s2b.tiff fs_wp1.tiff

%IM%convert ^
  %wpDEBUG_MASK% ^
  %WEB_SIZE% ^
  fs_mask1_sm.png

%IM%convert ^
  fs_wp1.tiff ^
  %WEB_SIZE% ^
  fs_wp1_sm.jpg
fs_mask1_sm.png fs_wp1_sm.jpg

The mask has a vertical white stripe that shows where the second photo is more in focus than the first. White specks on the left are false results. Bad bokeh? Hmm, needs investigating. The edges, as noted above, are false.

Merge this result with third and final (unmodified) image.

call %PICTBAT%whichPix fs_wp1.tiff %SRC3% fs_wp2.tiff

%IM%convert ^
  %wpDEBUG_MASK% ^
  %WEB_SIZE% ^
  fs_mask2_sm.png

%IM%convert ^
  fs_wp2.tiff ^
  %WEB_SIZE% ^
  fs_wp2_sm.jpg
fs_mask2_sm.png fs_wp2_sm.jpg

The result has successfully combined the three photos.

Using the script

List the photographs into a text file, one photo per line. (I overshot the focus on the penultimate frame, so removed it from the list; the stack has 24 photos.) For example, focStackList.txt:

F:\pictures\20140619\AGA_1867.JPG
F:\pictures\20140619\AGA_1868.JPG
F:\pictures\20140619\AGA_1869.JPG
F:\pictures\20140619\AGA_1870.JPG
F:\pictures\20140619\AGA_1871.JPG
F:\pictures\20140619\AGA_1872.JPG
F:\pictures\20140619\AGA_1873.JPG
F:\pictures\20140619\AGA_1874.JPG
F:\pictures\20140619\AGA_1875.JPG
F:\pictures\20140619\AGA_1876.JPG
F:\pictures\20140619\AGA_1877.JPG
F:\pictures\20140619\AGA_1878.JPG
F:\pictures\20140619\AGA_1879.JPG
F:\pictures\20140619\AGA_1880.JPG
F:\pictures\20140619\AGA_1881.JPG
F:\pictures\20140619\AGA_1882.JPG
F:\pictures\20140619\AGA_1883.JPG
F:\pictures\20140619\AGA_1884.JPG
F:\pictures\20140619\AGA_1885.JPG
F:\pictures\20140619\AGA_1886.JPG
F:\pictures\20140619\AGA_1887.JPG
F:\pictures\20140619\AGA_1888.JPG
F:\pictures\20140619\AGA_1889.JPG
F:\pictures\20140619\AGA_1891.JPG

Run the script:

call %PICTBAT%focStack focStackList.txt fs_stacked.tiff

Make a small version for the web:

%IM%convert ^
  fs_stacked.tiff ^
  %WEB_SIZE% ^
  fs_stacked_sm.jpg
fs_stacked_sm.jpg

There is smearing around the edges from "-virtual-pixel edge". It is widest where detail comes from close-up photos. I intend to add something to the script so we can crop this off.

On the full-size image, I can see vertical waves of sharpness along the brick wall.

I might also modify the script so we can skip stages already performed.

The script takes about two hours for this stack of 24 photos. The most expensive operation, finding the relative scale of adjacent photos, takes most of that time. This creates and uses temporary files which are not deleted. If these files already exist, the script takes about 30 minutes.

Scripts

For convenience, scripts are available in a single zip file. See Zipped BAT files.

focStack.bat

rem From %1, a list of focus-stack images, makes a composite image.
rem If %2 is given, that will be the output image file.
@rem
@rem Also uses:
@rem   fsMAKE_VP_MASK if 0, don't make virtual-pixel mask.
@rem



@rem fsFILES.Cnt
@rem Structure of fsFILES[n]
@rem   .SrcFile
@rem   .Aligned
@rem   .Scale
@rem   .DX
@rem   .DY
@rem   .CumulScale
@rem   .CumulDX
@rem   .CumulDY
@rem
@rem

@if "%1"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

rem @call echoOffSave

call %PICTBAT%setInOut %1 fs


if not exist %INFILE% (
  echo Can't find INFILE [%INFILE%]
  exit /B 1
)

if not "%2"=="" set OUTFILE=%2

set TMPDIR=%TEMP%
set TMPEXT=.miff
set TMPOUT=%TMPDIR%\%INNAME%_fs_out%TMPEXT%
set DUMP_DATA=%TMPDIR%\%INNAME%_fs_data.lis
set VP_MASK=%TMPDIR%\%INNAME%_fs_vpmask.miff


rem ------------------------------------------
echo Read the list of input photos.

set i=0
for /F "tokens=*" %%F in (%INFILE%) do (
  echo %%F
  set fsFILES[!i!].SrcFile=%%F
  set /A i+=1
)
set /A fsFILES.Cnt=%i%

if %fsFILES.Cnt% LSS 2 (
  echo Need at least 2 entries in INFILE [%INFILE%]
  exit /B 1
)

set /A LastNum=fsFILES.Cnt-2

if not "%fsMAKE_VP_MASK%"=="0" %IM%convert ^
  %fsFILES[0].SrcFile% ^
  -fill White -colorize 100 ^
  %VP_MASK%

if ERRORLEVEL 1 exit /B 1


rem ------------------------------------------
echo Find the scale of each against the next.

for /F "usebackq" %%L ^
in (`%IM%identify ^
  -format "PREC=%%[fx:1+1/max(w,h)]" %fsFILES[0].SrcFile%`) ^
do set %%L

echo PREC=%PREC%

set MIN_SCALE=0.9
set MAX_SCALE=1.1

for /L %%i in (0,1,%LastNum%) do (
  echo Find scale of %%i
  set /A j=%%i+1

  call :NextSrcFile !j!

  call %PICTBAT%whatScalePrec ^
    !fsFILES[%%i].SrcFile! ^
    !NextSrcFile! ^
    %MIN_SCALE% %MAX_SCALE% 10 %PREC%

  if ERRORLEVEL 1 exit /B 1

  set fsFILES[%%i].Scale=!wspSCALE!
)


rem -------------------------------------
echo Calculate cumulative scales.

set /A CntM1=fsFILES.Cnt-1
set fsFILES[%CntM1%].CumulScale=1

for /L %%i in (%LastNum%,-1,0) do (
  set /A j=%%i+1
  call :NextCumulScale !j!

  for /F "usebackq" %%L in (`%IM%identify ^
    -precision 9 ^
    -format "CumulScale=%%[fx:!NextCumulScale!*!fsFILES[%%i].Scale!]" ^
    xc:`) do set %%L

  set fsFILES[%%i].CumulScale=!CumulScale!
)

rem --------------------------------------
echo Scale each image.
for /L %%i in (0,1,%LastNum%) do (
  echo Scale %%i

  set OutScale=%TMPDIR%\%INNAME%_fs_%%i%TMPEXT%

  %IM%convert ^
    !fsFILES[%%i].SrcFile! ^
    -virtual-pixel Edge -distort SRT !fsFILES[%%i].CumulScale!,0 ^
    !OutScale!

  set fsFILES[%%i].Aligned=!OutScale!
)

set fsFILES[%CntM1%].Aligned=!fsFILES[%CntM1%].SrcFile!

rem ----------------------------------------
echo Find translations between scaled files.

for /L %%i in (0,1,%LastNum%) do (
  echo Find translation %%i

  set /A j=%%i+1
  call :NextAligned !j!

  call %PICTBAT%whatTrans ^
    !fsFILES[%%i].Aligned! ^
    !NextAligned!

  set fsFILES[%%i].DX=!wtX!
  set fsFILES[%%i].DY=!wtY!
)

rem ----------------------------------------
echo Calculate cumulative translation.

set fsFILES[%CntM1%].CumulDX=0
set fsFILES[%CntM1%].CumulDY=0

for /L %%i in (%LastNum%,-1,0) do (
  set /A j=%%i+1

  set /A fsFILES[%%i].CumulDX=fsFILES[%%i].DX+fsFILES[!j!].CumulDX
  set /A fsFILES[%%i].CumulDY=fsFILES[%%i].DY+fsFILES[!j!].CumulDY
)


rem ----------------------------------------
echo Scale and translate images.

rem FIXME: The translate will be an integer number of pixels,
rem so we can save time by not repeating the scale.

for /F "usebackq" %%L ^
in (`%IM%convert -ping !fsFILES[0].SrcFile! ^
  -format "x0=%%[fx:int(w/2)]\ny0=%%[fx:int(h/2)]" ^
  info:`) ^
do set %%L

for /L %%i in (0,1,%LastNum%) do (
  echo Scale and translate %%i

  set /A x1=%x0%+fsFILES[%%i].CumulDX
  set /A y1=%y0%+fsFILES[%%i].CumulDY

  echo fsFILES[%%i].CumulScale=!fsFILES[%%i].CumulScale!

  set sSRT=!x0!,!y0!,!fsFILES[%%i].CumulScale!,0,!x1!,!y1!

  %IM%convert ^
    !fsFILES[%%i].SrcFile! ^
    -virtual-pixel Edge -distort SRT !sSRT! ^
    !fsFILES[%%i].Aligned!

  if not "%fsMAKE_VP_MASK%"=="0" %IM%convert ^
    %VP_MASK% ^
    -virtual-pixel Black -distort SRT !sSRT! ^
    %VP_MASK%
)


rem ----------------------------------------
echo Make and apply masks

set wpDEBUG=1

rem Merge 0 with 1 ...

call %PICTBAT%whichPix ^
  !fsFILES[0].Aligned! ^
  !fsFILES[1].Aligned! ^
  %TMPOUT%

rem ... then merge result with 2, 3, ...CntM1

for /L %%i in (2,1,%CntM1%) do (
  echo Mask %%i

  call %PICTBAT%whichPix ^
    %TMPOUT% ^
    !fsFILES[%%i].Aligned! ^
    %TMPOUT%
)


rem ----------------------------------------
echo Wrap up.

%IM%convert %TMPOUT% %OUTFILE%

rem Dump the data to a file.
set fsFILES >%DUMP_DATA%


call echoRestore

@endlocal & set fsOUTFILE=%OUTFILE%&set fsVP_MASK=%VP_MASK%

goto :eof


rem ----------------------------------------
rem Subroutines.

:NextSrcFile
set NextSrcFile=!fsFILES[%1].SrcFile!
exit /B 0

:NextCumulScale
set NextCumulScale=!fsFILES[%1].CumulScale!
exit /B 0

:NextAligned
set NextAligned=!fsFILES[%1].Aligned!
exit /B 0

mSRTdelta.bat

rem From image %1, deltaX %2, deltaY %3,
rem returns the four XY parameters for "-distort SRT".

for /F "usebackq" %%L ^
in (`%IM%convert -ping %1 ^
  -format "x0=%%[fx:int(w/2)]\ny0=%%[fx:int(h/2)]\nx1=%%[fx:%2+int(w/2)]\ny1=%%[fx:%3+int(h/2)]" ^
  info:`) ^
do set %%L

whichPix.bat

rem From images %1 and %2, makes mask
rem   black where %1 is sharpest,
rem   white where %2 is sharpest.
rem and output image.
@rem
@rem If %3 is given, that will be the output file.
@rem
@rem Also uses:
@rem   wpDEBUG if 1, creates mask debugging image.


@if "%2"=="" findstr /B "rem @rem" %~f0 & exit /B 1

@setlocal

@call echoOffSave

call %PICTBAT%setInOut %1 wp

if not "%3"=="" set OUTFILE=%3

set MASK=%~dpn1_wp_mask_dbg%EXT%

if "%wpDEBUG%"=="1" (
  set SAVE_MASK=-write %MASK%
) else (
  set SAVE_MASK=
)

set tbdParams=4 6 6

set tbdAUTO=0
call %PICTBAT%twoBlrDiff %1 %tbdParams%
set DIFF1=%tbdOUTFILE%
call %PICTBAT%twoBlrDiff %2 %tbdParams%
set DIFF2=%tbdOUTFILE%

%IM%convert ^
  %DIFF1% ^
  %DIFF2% ^
  -compose MinusDst -composite -fill White +opaque Black ^
  %SAVE_MASK% ^
  %2 ^
  %1 ^
  -swap 0,2 ^
  -alpha off ^
  -compose Over -composite ^
  %OUTFILE%
  

call echoRestore

@endlocal & set wpOUTFILE=%OUTFILE%&set wpDEBUG_MASK=%MASK%

All images on this page were created by the commands shown, using:

%IM%identify -version
Version: ImageMagick 6.8.9-0 Q16 x64 2014-04-06 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2014 ImageMagick Studio LLC
Features: DPC OpenMP
Delegates: bzlib cairo freetype jbig jng jp2 jpeg lcms lqr pangocairo png ps rsvg tiff webp xml zlib

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG to JPG.

Source file for this web page is focstack.h1. To re-create this web page, execute "procH1 focstack".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.


Page version v1.0 19-June-2014.

Page created 21-Jun-2014 17:57:16.

Copyright © 2014 Alan Gibson.