﻿

# By FFT, what rotation?

Using FFT, we find relative rotation of images invariant to scale, translation, or other factors.

The page What rotation? gives a method for finding the angle of rotation between two images, assuming:

1. The two images are the same size.
2. One image is the rotation of the other, through the centres. That is, the centres of the images align with each other.

However, when we have photographs or frames from a video, the camera has probably rotated through multiple axes, so the second condition is false, and the problem is more difficult.

The problem can be solved with Fast Fourier Transformations (FFT), finding the relative rotation even when there is also scaling and translation. A byproduct of this method also finds the best translation, after applying the rotation.

In priciple, the method is simple: find the FFT spectrum of each image, and apply the What rotation? method to those. But there are some complications.

## Sample inputs

For a worked example, we use two images from the What rotation? page.

 wr_src1.png wr_src2.png

## The method

Find the FFT spectrum of each image. Ensure the same level adjustments are made to both.

 ```call %PICTBAT%fwdFft ^ wr_src1.png ^ wrf_s1_XX.miff ^ wrf_s1_``` ```call %PICTBAT%fwdFft ^ wr_src2.png ^ wrf_s2_XX.miff ^ wrf_s2_ ^ %wrf_s1__max% ^ %wrf_s1__mean%```

The dominant features look suspiciously like artefacts from the image boundaries. Adust the images so they both self-tile.

 ```call %PICTBAT%deEdgeForFft3 ^ wr_src1.png wrf_s1_de.miff . 4c``` ```call %PICTBAT%deEdgeForFft3 ^ wr_src2.png wrf_s2_de.miff . 4c```

Do the FFT on these de-edged versions:

 ```call %PICTBAT%fwdFft ^ wrf_s1_de.miff ^ wrf_s1de_XX.miff ^ wrf_s1_``` ```call %PICTBAT%fwdFft ^ wrf_s2_de.miff ^ wrf_s2de_XX.miff ^ wrf_s2_ ^ %wrf_s1__max% ^ %wrf_s1__mean%```

That's better. Now apply the whatRot method on the spectrum images. (Beware: whatRot.bat finds the rotation of the inscribed circles of the spectrums, so it discards some high-frequency data.) As mentioned in What rotation: False results, we need siDIV_DIM.

```set siDIV_DIM=40

call %PICTBAT%whatRot wrf_s1de_spec.miff wrf_s2de_spec.miff
if ERRORLEVEL 1 exit /B 1

set siDIV_DIM=

echo wrANGLE=%wrANGLE% wrANGLE_ERR=%wrANGLE_ERR% wrSCORE=%wrSCORE% wrSCORE_SUB=%wrSCORE_SUB% wrDODGY=%wrDODGY% ```
`wrANGLE=279.9 wrANGLE_ERR=0.3 wrSCORE=0.0559544 wrSCORE_SUB=0.0629347 wrDODGY=0 `

The result is 280°, which is 180° away from the correct answer of 100°. This is fair enough; a Fourier transform of a line at 23° is the same as one at 180°+23°. By trying out both possibilities, we readily find which one is correct. The centre of these source images align. If they didn't, we would need to find the translation first.

 ```%IM%convert ^ wr_src1.png ^ -rotate %wrANGLE% ^ +repage ^ wr_src2.png ^ -gravity Center ^ -compose Difference -composite ^ wrf_diff.miff``` ```for /F "usebackq" %%L in (`%IM%identify ^ -format "angp180=%%[fx:%wrANGLE%+180]" ^ xc:`) do set %%L %IM%convert ^ wr_src1.png ^ -rotate %angp180% ^ +repage ^ wr_src2.png ^ -gravity Center ^ -compose Difference -composite ^ wrf_diff2.miff```

In a script, we could take a suitable crop and find the average lightness. The result that is closest to black is the correct one.

However, we would generally need to use the script whatTrans.bat, and this can do a rotation first and return a score.

```call %PICTBAT%whatTrans wr_src1.png wr_src2.png 1 %wrANGLE%

if ERRORLEVEL 1 exit /B 1

echo wtX=%wtX% wtY=%wtY% wtXY=%wtXY% wtXYneg=%wtXYneg% wtSCORE=%wtSCORE% wtDODGY=%wtDODGY% ```
`wtX=-3 wtY=1 wtXY=-3+1 wtXYneg=+3-1 wtSCORE=0.124719 wtDODGY=0 `
```call %PICTBAT%whatTrans wr_src1.png wr_src2.png 1 %angp180%

if ERRORLEVEL 1 exit /B 1

echo wtX=%wtX% wtY=%wtY% wtXY=%wtXY% wtXYneg=%wtXYneg% wtSCORE=%wtSCORE% wtDODGY=%wtDODGY% ```
`wtX=0 wtY=0 wtXY=+0+0 wtXYneg=+0+0 wtSCORE=0.00649212 wtDODGY=0 `

The second score is lower, so %angp180% is the correct angle and wtX,wtY give the displacements of the centres.

 ```%IM%convert ^ wr_src1.png ^ -rotate %angp180% ^ +repage ^ wr_src2.png ^ -gravity Center ^ -geometry %wtXY% ^ -compose Difference -composite ^ wrf_diff3.miff```

## The script

We implement the above in the script whatRotFft.bat. This calls whatRot.bat on the spectrums to find the rotation, then calls whatTrans.bat on the original images to find the best translation, and which of the two possible rotations is the correct one.

We will create debugging images:

`set wrfDEBUG=1`

We try the script with example images from the What translation? page.

 ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src2.png wrf_ret12. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret12.DEBUG_FILE% wrf_dbg12.jpg set wrf_ret12. ``` ```wrf_ret12.ANGLE=0 wrf_ret12.ANGLE_ERR=0.3 wrf_ret12.DEBUG_FILE=wrf_a_dbg.miff wrf_ret12.DODGY=0 wrf_ret12.SCORE=0.004911 wrf_ret12.X=-72 wrf_ret12.XY=-72-42 wrf_ret12.XYneg=+72+42 wrf_ret12.Y=-42``` This is the correct solution. ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src3.png wrf_ret13. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret13.DEBUG_FILE% wrf_dbg13.jpg set wrf_ret13. ``` ```wrf_ret13.ANGLE=3.9 wrf_ret13.ANGLE_ERR=0.3 wrf_ret13.DEBUG_FILE=wrf_a_dbg.miff wrf_ret13.DODGY=0 wrf_ret13.SCORE=0.134195 wrf_ret13.X=-67 wrf_ret13.XY=-67+26 wrf_ret13.XYneg=+67-26 wrf_ret13.Y=26``` This is NOT the correct solution. ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src4.png wrf_ret14. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret14.DEBUG_FILE% wrf_dbg14.jpg set wrf_ret14. ``` ```wrf_ret14.ANGLE=345 wrf_ret14.ANGLE_ERR=0.3 wrf_ret14.DEBUG_FILE=wrf_a_dbg.miff wrf_ret14.DODGY=0 wrf_ret14.SCORE=0.134907 wrf_ret14.X=-68 wrf_ret14.XY=-68-46 wrf_ret14.XYneg=+68+46 wrf_ret14.Y=-46``` This is NOT the correct solution.

The images have a lot of noisy non-aligned high-frequency detail, and images #3 and #4 overlap #1 by only about 50%. This has cased the wrong solution to be found. By setting wrCROP_PC, we reduce the influence of high-frequency data on the search.

`set wrCROP_PC=25`
 ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src2.png wrf_ret12_cr. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret12_cr.DEBUG_FILE% wrf_dbg12_cr.jpg set wrf_ret12_cr. ``` ```wrf_ret12_cr.ANGLE=0 wrf_ret12_cr.ANGLE_ERR=1.2 wrf_ret12_cr.DEBUG_FILE=wrf_a_dbg.miff wrf_ret12_cr.DODGY=0 wrf_ret12_cr.SCORE=0.004911 wrf_ret12_cr.X=-72 wrf_ret12_cr.XY=-72-42 wrf_ret12_cr.XYneg=+72+42 wrf_ret12_cr.Y=-42``` ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src3.png wrf_ret13_cr. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret13.DEBUG_FILE% wrf_dbg13_cr.jpg set wrf_ret13_cr. ``` ```wrf_ret13_cr.ANGLE=100.8 wrf_ret13_cr.ANGLE_ERR=1.2 wrf_ret13_cr.DEBUG_FILE=wrf_a_dbg.miff wrf_ret13_cr.DODGY=1 wrf_ret13_cr.SCORE=0.1216 wrf_ret13_cr.X=-75 wrf_ret13_cr.XY=-75-41 wrf_ret13_cr.XYneg=+75+41 wrf_ret13_cr.Y=-41``` ```call %PICTBAT%whatRotFft ^ wt_src1.png wt_src4.png wrf_ret14_cr. if ERRORLEVEL 1 goto :error %IM%convert %wrf_ret14.DEBUG_FILE% wrf_dbg14_cr.jpg set wrf_ret14_cr. ``` ```wrf_ret14_cr.ANGLE=261.6 wrf_ret14_cr.ANGLE_ERR=1.2 wrf_ret14_cr.DEBUG_FILE=wrf_a_dbg.miff wrf_ret14_cr.DODGY=0 wrf_ret14_cr.SCORE=0.123699 wrf_ret14_cr.X=-73 wrf_ret14_cr.XY=-73-44 wrf_ret14_cr.XYneg=+73+44 wrf_ret14_cr.Y=-44```

This has found the correct solutions, more or less.

We see by eye that the results are improved. But the scores have not improved by much, so they they are not a reliable indicator of whether there was a problem or whether cropping the spectrum has solved it.

A possible solution: blur or downsize the image and examine the standard deviaton. If it is below a certain threshold, we have this problem. I haven't worked out the details.

Unset the cropping:

`set wrCROP_PC=`

## Normalising inputs

Here are crops from two photographs of a panorama sequence. The crops are of the overlapping area, more or less.

 ```%IM%convert ^ -delay 100 -loop 0 ^ ( op_3-4_ra_sm.png ^ -bordercolor Red -border 2 ^ ) ^ ( op_3-4_rb_sm.png ^ -bordercolor Green -border 2 ^ ) ^ -layers optimize ^ wrf_clds.gif```

The flicker comparison shows that the first image (with red border) needs rotating slightly clockwise to align with the second (with green border). The axis of rotation is not at the centre of the images, but somewhat lower and to the left.

We can find the rotation and translation to align the images.

```call %PICTBAT%whatRotFft op_3-4_ra_sm.png op_3-4_rb_sm.png wrf_clds.
if ERRORLEVEL 1 goto error
set wrf_clds. ```
```wrf_clds.ANGLE=0
wrf_clds.ANGLE_ERR=0.2
wrf_clds.DEBUG_FILE=wrf_a_dbg.miff
wrf_clds.DODGY=0
wrf_clds.SCORE=0.0281182
wrf_clds.X=-71
wrf_clds.XY=-71-19
wrf_clds.XYneg=+71+19
wrf_clds.Y=-19```

The script thinks the best alignment is with zero rotation. The problem is that the very soft image detail is swamped by the edge effects. We could soften the edges further, but there is no obvious way of calculating the required degree of softening. Instead, we will increase the contrast in the images.

We normalise both images so all channels have a mean of 0.5 and standard deviation of 0.17. (SD=0.17 is a reasonable value for "ordinary" photographs.)

 ```call %PICTBAT%imgMnSd ^ op_3-4_ra_sm.png ^ 0.5 0.17 ^ wrf_clds_na.miff``` ```call %PICTBAT%imgMnSd ^ op_3-4_rb_sm.png ^ 0.5 0.17 ^ wrf_clds_nb.miff``` ```%IM%convert ^ -delay 100 -loop 0 ^ ( wrf_clds_na.miff ^ -bordercolor Red -border 2 ^ ) ^ ( wrf_clds_nb.miff ^ -bordercolor Green -border 2 ^ ) ^ -layers optimize ^ wrf_clds_nab.gif```

Find the rotation and translation of those normalised images:

```call %PICTBAT%whatRotFft wrf_clds_na.miff wrf_clds_nb.miff wrf_clds_n.
if ERRORLEVEL 1 goto error
set wrf_clds_n. ```
```wrf_clds_n.ANGLE=8.4
wrf_clds_n.ANGLE_ERR=0.2
wrf_clds_n.DEBUG_FILE=wrf_a_dbg.miff
wrf_clds_n.DODGY=0
wrf_clds_n.SCORE=0.0492858
wrf_clds_n.X=8
wrf_clds_n.XY=+8+10
wrf_clds_n.XYneg=-8-10
wrf_clds_n.Y=10```

The numbers look more sensible. A blink-comparison shows the result. As well as writing the animated GIF, we write a multi-image MIFF.

 -repage has "!" to add extra offset to the offset caused by -rotate. ```%IM%convert ^ -delay 100 -loop 0 ^ ( \prose\pictures\wrf_clds_na.miff ^ -bordercolor Red -border 2 ^ -rotate %wrf_clds_n.ANGLE% ^ -repage "%wrf_clds_n.XY%^!" ^ ) ^ ( \prose\pictures\wrf_clds_nb.miff ^ -bordercolor Green -border 2 ^ ) ^ -layers trim-bounds ^ +write wrf_clds2.miff ^ wrf_clds2.gif```

The result looks good. It can't be improved by further rotation or translation. Would scaling improve the result? We use whatScale.bat on crops of the multi-image MIFF.

```%IM%convert ^
wrf_clds2.miff ^
-layers coalesce ^
-gravity Center -crop 50%%x50%%+0+0 +repage ^
wrf_clds2c.miff

call %PICTBAT%whatScale wrf_clds2c.miff[0] wrf_clds2c.miff[1]
if ERRORLEVEL 1 goto error
set ws ```
```wsDODGY=0
wsFACT=1.01104669
wsSCALE=0.998978955
wsSCORE=0.0376923
wsSCORE_SUB=0.0549892```

The result, wsSCALE, is more or less 1.0. (wsSCALE/wsFACT <= 1.0 and wsSCALE*wsFACT >= 1.0.) So scaling won't improve the result.

## Conclusion

For ordinary but "de-edged" photographs, the method works well. After finding the rotation x°, we also need to find the translation in order to resolve whether the angle is x or (x+180)°.

The examples on this page are more difficult; the input images of stony concrete slabs and clouds have very little low-frequency detail. The clouds have very little high-frequency data either. For the slabs, this is corrected by removing low-level detail (with wrCROP=25). For the clouds, the images are first normalised to a more sensible standard deviation with the script imgMnSd.bat.

Further tests suggest that setting wrCROP=25 doesn't harm accuracy (though it does reduce precision).

## Future

Here's a tricky problem: given two rectangles that overlap by scale, rotation and translation, suppose we want each output pixel to come from one or other input. Where should the minimum error boundary cuts (MEBCs), aka darkpaths, be?

The edges of the two rectangles intersect at up to eight locations. We might have eight darkpaths between these locations and some central location.

## Scripts

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

### whatRot.bat

```rem Given same-size images %1 and %2,
rem finds angle of rotation for %1 to best match %2, and a score.
@rem
@rem Also uses:
@rem   wrDO_SUPER      if ==0, no supersampling will be done.
@rem   wrDO_SCORE_SUB  if ==0, wrSCORE_SUB will not be found.
@rem   wrMETRIC        default RMSE
@rem   wrDEBUG         if 1, creates images for debugging
@rem   wrDELTEMP       if not 0, will remove temporary file.
@rem   wrCROP_PC       percentage for central crops before unrolling [100].
@rem
@rem Returns:
@rem   wrANGLE     (degrees) rotate first image by this to match second.
@rem   wrANGLE_ERR (degrees) margin of error
@rem   wrSCORE     (0.0 to 1.0, more or less).
@rem   wrSCORE_SUB (0.0 to 1.0, more or less).
@rem   wrDODGY     0 or 1. 1 means the result is unreliable.
@rem
@rem Updated:
@rem   10-August-2022 for IM v7.
@rem

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

@setlocal enabledelayedexpansion

@call echoOffSave

if "%wrCROP_PC%"=="" set wrCROP_PC=100

set TMPEXT=.miff

call %PICTBAT%setInOut %1 wr
set IN_A=%INFILE%
set TEMP_A=%TEMP%\%~n1_wr_x2%TMPEXT%

call %PICTBAT%setInOut %2 wr
set IN_B=%INFILE%
set TEMP_B=%TEMP%\%~n2_wr%TMPEXT%
if %IN_B%==%IN_A% set TEMP_B=%TEMP%\%~n2_B_wr%TMPEXT%

set DEBUG_FILE=wr_%~n1_%~n2_dbg%EXT%

if not "%wrDELTEMP%"=="0" (
del %TEMP_A% 2>nul
del %TEMP_B% 2>nul
)

if "%wrMETRIC%"=="" set wrMETRIC=RMSE

for /F "usebackq" %%L in (`%IMG7%magick identify ^
-format "WW1=%%[fx:w*%wrCROP_PC%/100]\nHH1=%%[fx:h*%wrCROP_PC%/100]\nIS_SMALL=%%[fx:max(w,h)*%wrCROP_PC%/100>1000?0:1]" ^
%IN_A%`) do set %%L

if "%wrDO_SCORE_SUB%"=="" set wrDO_SCORE_SUB=%IS_SMALL%

if "%wrDO_SUPER%"=="" set wrDO_SUPER=%IS_SMALL%

if %wrDO_SUPER%==1 (
set SUPER_UP=-set option:distort:scale 4
set SUPER_DN=-resize 100x25%%
set SUPER_FACT=4
) else (
set SUPER_UP=
set SUPER_DN=
set SUPER_FACT=1
)

echo %0: SUPER_FACT=%SUPER_FACT% WW1=%WW1% HH1=%HH1%

if %wrCROP_PC%==100 (
set sCROP=
) else (
set sCROP=-gravity center -crop %wrCROP_PC%%%x%wrCROP_PC%%%+0+0 +repage
)

if not exist %TEMP_A% %IMG7%magick ^
%IN_A% ^
%sCROP% ^
%SUPER_UP% ^
-distort DePolar 0 ^
%SUPER_DN% ^
^( +clone ^) +append +repage ^
+depth ^
%TEMP_A%

if not exist %TEMP_B% %IMG7%magick convert ^
%IN_B% ^
%sCROP% ^
%SUPER_UP% ^
-distort DePolar 0 ^
%SUPER_DN% ^
+depth ^
%TEMP_B%

call %PICTBAT%srchImg %TEMP_A% %TEMP_B%
if ERRORLEVEL 1 exit /B 1

set DODGY=0

for /F "usebackq" %%L in (`%IMG7%magick identify ^
-precision 9 ^
-format "ANGLE=%%[fx:%siCOMP_XW%*360/(%SUPER_FACT%*%WW1%)]\nANGLE_ERR=%%[fx:360/(%SUPER_FACT%*%WW1%)]" ^
xc:`) do set %%L

if %ANGLE%==360 set ANGLE=0

if %wrDO_SCORE_SUB%==1 (

for /F "usebackq" %%L in (`%IMG7%magick identify ^
-format "DIM_2=%%[fx:int(%WW1%<%HH1%?%WW1%/2:%HH1%/2)]" ^
xc:`) do set %%L

for /F "usebackq" %%L in (`%IMG7%magick ^
%IN_A% ^
-rotate %ANGLE% +repage ^
%IN_B% ^
-gravity Center ^
-crop !DIM_2!x!DIM_2!+0+0 +repage ^
-metric %wrMETRIC% -format "ScoreSub=%%[distortion]" -compare ^
info:`) do set %%L
)

if "%wrDEBUG%"=="1" (

for /F "usebackq" %%L in (`%IMG7%magick identify ^
-format "DIM_2=%%[fx:int(%WW1%<%HH1%?%WW1%/2:%HH1%/2)]" ^
xc:`) do set %%L

%IMG7%magick ^
%IN_A% ^
^( +clone -alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #f00 -frame 1x1 ^
-channel A -evaluate Multiply 0.75 ^
^) ^
-compose Over -composite ^
-virtual-pixel None ^
+distort SRT 1,%ANGLE% ^
^( %IN_B% ^
^( +clone -alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #0f0 -frame 1x1 ^
-channel A -evaluate Multiply 0.75 ^
^) ^
-composite ^
^) ^
^( -clone -1 ^
-gravity Center ^
-crop !DIM_2!x!DIM_2!+0+0 ^
-alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #ff0 -frame 1x1 ^
-channel A -evaluate Multiply 0.75 ^
^) ^
-layers merge ^
+repage ^
%DEBUG_FILE%
)

if not "%wrDELTEMP%"=="0" (
del %TEMP_A% 2>nul
del %TEMP_B% 2>nul
)

call echoRestore

endlocal & set wrANGLE=%ANGLE%& set wrANGLE_ERR=%ANGLE_ERR%& set wrSCORE=%siCOMP_FLT%& set wrSCORE_SUB=%ScoreSub%& set wrDODGY=%DODGY%```

### fwdFft.bat

```rem From input image %1,
rem makes outputs %2 (must contain XX)
rem   _mag _ph and _spec.
rem Also creates environment variable prefix %3 with suffixes
rem   _ww  input image width
rem   _hh  input image height
rem   _sqdim width and height of outputs.
rem   _max maximum value of magnitude component
rem   _scale scale
rem %4 Assumed maximum value of magnitude.
rem %5 Assumed mean value of magnitude.
@rem
@rem Also uses:
@rem   fftPOST_SPEC eg -resize "600x600>"
@rem

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 fft

set OUTFILE=%2
if "%OUTFILE%"=="." set OUTFILE=
if "%OUTFILE%"=="" set OUTFILE=%BASENAME%_fft_XX%EXT%

set ENV_PREF=%3
if "%ENV_PREF%"=="." set ENV_PREF=
if "%ENV_PREF%"=="" set ENV_PREF=fft

set ASS_MAX=%4
if "%ASS_MAX%"=="." set ASS_MAX=

set ASS_MEAN=%5
if "%ASS_MEAN%"=="." set ASS_MEAN=

set OUT_MAG=%OUTFILE:XX=mag%
set OUT_PH=%OUTFILE:XX=ph%
set OUT_SPEC=%OUTFILE:XX=spec%

if "%OUT_MAG%"=="%OUT_PH%" (
echo Bad OUTFILE [%OUTFILE%] -- should contain XX
exit /B 1
)

set WW=
for /F "usebackq" %%L in (`%IM%identify ^
-format "WW=%%w\nHH=%%h" ^
%INFILE%`) do set %%L
if "%WW%"=="" exit /B 1

%IMDEV%convert ^
%INFILE% ^
-fft ^
-define quantum:format=floating-point ^
( -clone 0 -write %OUT_MAG% +delete ) ^
-delete 0 ^
%OUT_PH%

if ERRORLEVEL 1 (
echo No FFT
exit /B 1
)

for /F "usebackq" %%L in (`%IMDEV%convert ^
%OUT_MAG% ^
-precision 19 ^
-format "SCALE=%%[fx:exp(log(mean)/log(0.5))]\nWH=%%w\nMEAN=%%[fx:mean]\nMIN=%%[fx:minima]\nMAX=%%[fx:maxima]" ^
info:`) do set %%L

if not "%ASS_MAX%"=="" set MAX=%ASS_MAX%
if not "%ASS_MEAN%"=="" set MEAN=%ASS_MEAN%

echo %0: SCALE=%SCALE% MIN=%MIN% MEAN=%MEAN% MAX=%MAX%

for /F "usebackq" %%L in (`%IMDEV%identify ^
-precision 19 ^
-format "SCALE=%%[fx:exp(log(%MEAN%/%MAX%)/log(0.5))]" ^
xc:`) do set %%L

echo %0: SCALE=%SCALE% MIN=%MIN% MEAN=%MEAN% MAX=%MAX%

%IMDEV%convert ^
%OUT_MAG% ^
-evaluate divide %MAX% ^
-evaluate log %SCALE% ^
%fftPOST_SPEC% ^
%OUT_SPEC%

call echoRestore

endlocal & set %ENV_PREF%_ww=%WW%&^
set %ENV_PREF%_hh=%HH%&^
set %ENV_PREF%_sqdim=%WH%&^
set %ENV_PREF%_max=%MAX%&^
set %ENV_PREF%_scale=%SCALE%&^
set %ENV_PREF%_mean=%MEAN%```

### deEdgeForFft3.bat

```rem Given %1 is an ordinary image,
rem makes output %2
rem with edges fade to colour %3 [gray(50%)],
rem up to distance %4 from edges [4c].
rem %5 is any of LTRB, any case. [LTRB]
@rem
@rem %4 is pixels,
@rem   or can be suffixed with p or % or c, for proportion or percentage of minimum dimension.
@rem
@rem Also uses:
@rem
@rem      -function sinusoid 0.5,-90,0.5,0.5
@rem

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 deff

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

set EDGE_COL=%3
if "%EDGE_COL%"=="." set EDGE_COL=
if "%EDGE_COL%"=="" set EDGE_COL=gray(50%%)

set EDGE_DIST=%4
if "%EDGE_DIST%"=="." set EDGE_DIST=
if "%EDGE_DIST%"=="" set EDGE_DIST=4c

set EDGES=%5
if "%EDGES%"=="." set EDGES=
if "%EDGES%"=="" set EDGES=LTRB
call %PICTBAT%getLtrb %EDGES%
if %ltrbERR%==1 exit /B 1

set WW=
for /F "usebackq" %%L in (`%IM%convert ^
%INFILE% ^
-format "WW=%%w\nHH=%%h\nMID_DIM=%%[fx:min(w,h)]" ^
info:`) do set %%L
if "%WW%"=="" exit /B 1

set ED_LAST=%EDGE_DIST:~-1%

if "%ED_LAST%"=="^%" set ED_LAST=c

if /I "%ED_LAST%"=="c" (

for /F "usebackq" %%L in (`%IM%identify ^
-format "nED=%%[fx:%MID_DIM%*%EDGE_DIST:~0,-1%/100]" ^
xc:`) do set %%L

) else if /I "%ED_LAST%"=="p" (

for /F "usebackq" %%L in (`%IM%identify ^
-format "nED=%%[fx:%MID_DIM%*%EDGE_DIST:~0,-1%]" ^
xc:`) do set %%L

) else (
set nED=%EDGE_DIST%
)

set sL=
set sT=
set sR=
set sB=

if %ltrbL%==1 set sL=( gradient:white-black -rotate -90 %deefGRAD_MOD% ) -gravity West -composite

if %ltrbR%==1 set sR=( gradient:white-black -rotate 90 %deefGRAD_MOD% ) -gravity East -composite

%IM%convert ^
%INFILE% ^
( -size %WW%x%HH% ^
xc:%EDGE_COL% ^
) ^
( -size %WW%x%HH% ^
xc:Black ^
-compose Lighten ^
-size %WW%x%nED% ^
%sT% ^
%sB% ^
-size %HH%x%nED% ^
%sL% ^
%sR% ^
) ^
-compose Over -composite ^
%OUTFILE%

if ERRORLEVEL 1 exit /B 1

call echoRestore

@endlocal```

### whatTrans.bat

```rem Given same-sized images %1 and %2,
rem scales %1 by %3 and rotates by %4, and crops it.
rem Returns location of resultimg image in %2 as offset from centres,
rem ie (0,0) means scaled and rotated %1 is not displaced over %2.
rem %5 is prefix for environment variables for numeric results. [wt]

@rem Also uses:
@rem   wtMETRIC        default RMSE
@rem   wtTUNE_CROP     if 1, crop percentage will be adjusted for the given scale
@rem   wtDEBUG         if 1, creates image for debugging
@rem   wtDEBUG_FILE    name of debugging image file
@rem   wtDELTEMP       if not 0, will remove temporary file.
@rem
@rem Returns:
@rem   wtX, wtY    integer pixel offsets.
@rem                 Positive values mean the first image needs moving
@rem                 down and right in order to register with the second image.
@rem   wtXY        the previous, concatenated in the format +X+Y or -X-Y etc.
@rem   wtXYneg     like wtXY but with negated values.
@rem   wtSCORE     0.0 to 1.0, more or less
@rem   wtDEBUG_OUTFILE  name of debugging image file
@rem
@rem Last updated:
@rem   6-August-2016 Cleaned up.
@rem   10-August-2022 for IM v7.

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

@setlocal enabledelayedexpansion

@call echoOffSave

set SCALE=%3
if "%SCALE%"=="." set SCALE=
if "%SCALE%"=="" set SCALE=1

set ANGLE=%4
if "%ANGLE%"=="." set ANGLE=
if "%ANGLE%"=="" set ANGLE=0

set ENV_PREF=%5
if "%ENV_PREF%"=="." set ENV_PREF=
if "%ENV_PREF%"=="" set ENV_PREF=wt

set TMPEXT=.miff

if "%wtMETRIC%"=="" set wtMETRIC=RMSE

if "%wtTUNE_CROP%"=="" set wtTUNE_CROP=0

call %PICTBAT%setInOut %1 wt
set IN_A=%INFILE%
set TEMP_A=%TEMP%\%~n1_%SCALE%_%ANGLE%_%wtTUNE_CROP%_wt%TMPEXT%

call %PICTBAT%setInOut %2 wt
set IN_B=%INFILE%

if not "%wtDELTEMP%"=="0" (
del %TEMP_A% 2>nul
)

if "%wtDEBUG_OUTFILE%"=="" (
set DEBUG_FILE=wt_%~n1_%~n2_dbg%EXT%
) else (
set DEBUG_FILE=%wtDEBUG_OUTFILE%
)

for /F "usebackq" %%L ^
in (`%IMG7%magick identify -format "WW2=%%w\nHH2=%%h" %IN_B%`) ^
do set %%L

set /A CROP_W=WW2/2
set /A CROP_H=HH2/2

rem Maybe do next only if SCALE < 1.
rem If done when scale near 2, there is no wriggle-room for search.
if %wtTUNE_CROP%==1 for /F "usebackq" %%L in (`%IM%identify ^
-format "CROP_W=%%[fx:%WW2%*%SCALE%/2]\nCROP_H=%%[fx:%HH2%*%SCALE%/2]" ^
xc:`) do set %%L

rem Ensure CROP_X has same parity (least significant bit) as WW2,
rem and CROP_Y has same parity as HH2.
set /A CROP_W-=CROP_W%%2^^WW2%%2
set /A CROP_H-=CROP_H%%2^^HH2%%2

echo %0: SCALE=%SCALE% ANGLE=%ANGLE% CROP_W=%CROP_W% CROP_H=%CROP_H%

%IMG7%magick ^
%IN_A% ^
-distort SRT %SCALE%,%ANGLE% -gravity center -crop %CROP_W%x%CROP_H%+0+0 +repage ^
%TEMP_A%

for /F "usebackq" %%L in (`%IM%convert ^
-ping %TEMP_A% ^
-format "WW1=%%w\nHH1=%%h\nW_2=%%[fx:(%WW2%-w)/2]\nH_2=%%[fx:(%HH2%-h)/2]" ^
info:`) do set %%L

set /A diffW=WW2-WW1
set /A diffH=HH2-HH1

set siDEBUG=
call %PICTBAT%srchImg %IN_B% %TEMP_A%
if ERRORLEVEL 1 exit /B 1

set /A RESULT_XI=siCOMP_XW-diffW/2
set /A RESULT_YI=siCOMP_YW-diffH/2

echo %0: COMP_FLT=%siCOMP_FLT% COMP_XW=%siCOMP_XW% COMP_YW=%siCOMP_YW%

set DODGY=0
if %siCOMP_XW%==0 set DODGY=1
if %siCOMP_YW%==0 set DODGY=1
if %siCOMP_XW%==%diffW% set DODGY=1
if %siCOMP_YW%==%diffH% set DODGY=1

if "%wtDEBUG%"=="1" (

for /F "usebackq" %%L in (`%IM%convert ^
-ping %IN_A% ^
-format "W_2=%%[fx:w/2]\nH_2=%%[fx:h/2]\nnewX=%%[fx:w/2+(%RESULT_XI%)]\nnewY=%%[fx:h/2+(%RESULT_YI%)]" ^
info:`) do set %%L

%IM%convert ^
%IN_A% ^
^( +clone -alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #f00 -frame 1x1 ^
-channel A -evaluate Multiply 0.75 ^
^) ^
-composite ^
-virtual-pixel None ^
+distort SRT !W_2!,!H_2!,%SCALE%,%ANGLE%,!newX!,!newY! ^
^( %IN_B% ^
^( +clone -alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #0f0 -frame 1x1 ^
-channel A -evaluate Multiply 0.75 ^
^) ^
-composite ^
^) ^
^( %TEMP_A% ^
^( +clone -alpha Transparent ^
-shave 1x1 ^
-bordercolor None ^
-mattecolor #ff0 -frame 1x1 ^
^) ^
-composite ^
-alpha set ^
-channel A -evaluate Multiply 0.5 ^
-repage +%siCOMP_XW%+%siCOMP_YW% ^
^) ^
-layers merge ^
+repage ^
%DEBUG_FILE%
)

if not "%wtDELTEMP%"=="0" (
del %TEMP_A% 2>nul
)

set SX=
set SY=
if %RESULT_XI% GEQ 0 set SX=+
if %RESULT_YI% GEQ 0 set SY=+
set wtXY=%SX%%RESULT_XI%%SY%%RESULT_YI%

set /A SXn=-%RESULT_XI%
set /A SYn=-%RESULT_YI%
set SX=
set SY=
if %SXn% GEQ 0 set SX=+
if %SYn% GEQ 0 set SY=+
set wtXYneg=%SX%%SXn%%SY%%SYn%

echo %0: RESULT_XI=%RESULT_XI%  RESULT_YI=%RESULT_YI% wtXY=%wtXY%

call echoRestore

endlocal &set %ENV_PREF%X=%RESULT_XI%&^
set %ENV_PREF%Y=%RESULT_YI%&^
set %ENV_PREF%SCORE=%siCOMP_FLT%&^
set %ENV_PREF%DODGY=%DODGY%&^
set %ENV_PREF%DEBUG_FILE=%DEBUG_FILE%&^
set %ENV_PREF%XY=%wtXY%&^
set %ENV_PREF%XYneg=%wtXYneg%```

### whatRotFft.bat

```rem Given same-size images %1 and %2,
rem finds angle of rotation for %1 to best match %2, and a score, by FFT.
rem Also calls whatRot.bat to disambiguate the angle and returns those results.
rem %3 is prefix for environment variables for numeric results. [wt]
@rem
@rem Also uses:
@rem   wrfDEBUG  if 1, creates image for debugging
@rem
@rem Returns:
@rem   All the returns from whatTrans.bat.

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

@setlocal enabledelayedexpansion

@call echoOffSave

set ENV_PREF=%3
if "%ENV_PREF%"=="." set ENV_PREF=
if "%ENV_PREF%"=="" set ENV_PREF=wrf.

set TMPEXT=.miff
set TMP_DIR=wrftmp

if not exist %TMP_DIR% md %TMP_DIR%

call %PICTBAT%setInOut %1 wrf
set IN_A=%INFILE%
set TEMP_A=%TMP_DIR%\%~n1_wrf_x2%TMPEXT%
set FFT_A=%TMP_DIR%\%~n1_wrf_XX_x2%TMPEXT%

call %PICTBAT%setInOut %2 wrf
set IN_B=%INFILE%
set TEMP_B=%TMP_DIR%\%~n2_wrf%TMPEXT%
if %IN_B%==%IN_A% set TEMP_B=%TMP_DIR%\%~n2_B_wrf%TMPEXT%
set FFT_B=%TMP_DIR%\%~n2_wrf_XX%TMPEXT%
if %IN_B%==%IN_A% set FFT_B=%TMP_DIR%\%~n2_B_wrf_XX%TMPEXT%

set SPEC_A=%FFT_A:XX=spec%
set SPEC_B=%FFT_B:XX=spec%

rem set DEBUG_FILE=wrf_%~n1_%~n2_dbg%EXT%

if not "%wrDELTEMP%"=="0" (
del %TEMP_A% 2>nul
del %TEMP_B% 2>nul
)

call %PICTBAT%deEdgeForFft3 %IN_A% %TEMP_A%
if ERRORLEVEL 1 exit /B 1

call %PICTBAT%deEdgeForFft3 %IN_B% %TEMP_B%
if ERRORLEVEL 1 exit /B 1

call %PICTBAT%fwdFft ^
%TEMP_A% ^
%FFT_A% ^
wrf_s1_

if ERRORLEVEL 1 exit /B 1

call %PICTBAT%fwdFft ^
%TEMP_B% ^
%FFT_B% ^
wrf_s2_ ^
%wrf_s1__max% ^
%wrf_s1__mean%

if ERRORLEVEL 1 exit /B 1

set siDIV_DIM=100

set wrDEBUG=0
call %PICTBAT%whatRot %SPEC_A% %SPEC_B%
if ERRORLEVEL 1 exit /B 1

set siDIV_DIM=

echo %0: wrANGLE=%wrANGLE% wrANGLE_ERR=%wrANGLE_ERR% wrSCORE=%wrSCORE% wrSCORE_SUB=%wrSCORE_SUB% wrDODGY=%wrDODGY%

for /F "usebackq" %%L in (`%IM%identify ^
-format "angm180=%%[fx:%wrANGLE%-180]" ^
xc:`) do set %%L

set wtDEBUG=%wrfDEBUG%
set wtDEBUG_OUTFILE=wrf_a_dbg.miff
call %PICTBAT%whatTrans %IN_A% %IN_B% 1 %wrANGLE% wrfA.
if ERRORLEVEL 1 exit /B 1

set wtDEBUG_OUTFILE=wrf_b_dbg.miff
call %PICTBAT%whatTrans %IN_A% %IN_B% 1 %angm180% out.
if ERRORLEVEL 1 exit /B 1

for /F "usebackq" %%L in (`%IM%identify ^
-format "IsFirst=%%[fx:%wrfA.SCORE%<%out.SCORE%?1:0]" ^
xc:`) do set %%L

rem FIXME: keep the first debug file.

if %IsFirst%==1 (
set out.X=%wrfA.X%
set out.Y=%wrfA.Y%
set out.XY=%wrfA.XY%
set out.XYneg=%wrfA.XYneg%
set out.SCORE=%wrfA.SCORE%
set out.DODGY=%wrfA.DODGY%
set out.DEBUG_FILE=wrf_a_dbg.miff
set out.ANGLE=%wrANGLE%
) else (
set out.DEBUG_FILE=wrf_b_dbg.miff
set out.ANGLE=%angm180%
)

set out.ANGLE_ERR=%wrANGLE_ERR%

set out.

call echoRestore

endlocal &set %ENV_PREF%X=%out.X%&^
set %ENV_PREF%Y=%out.Y%&^
set %ENV_PREF%XY=%out.XY%&^
set %ENV_PREF%XYneg=%out.XYneg%&^
set %ENV_PREF%SCORE=%out.SCORE%&^
set %ENV_PREF%DODGY=%out.DODGY%&^
set %ENV_PREF%ANGLE=%out.ANGLE%&^
set %ENV_PREF%ANGLE_ERR=%out.ANGLE_ERR%&^
set %ENV_PREF%DEBUG_FILE=%out.DEBUG_FILE%```

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

`%IM%identify -version`
```Version: ImageMagick 6.9.9-50 Q16 x64 2018-06-02 http://www.imagemagick.org
Visual C++: 180040629
Features: Cipher DPC Modules OpenMP
Delegates (built-in): bzlib cairo flif freetype gslib heic jng jp2 jpeg lcms lqr lzma openexr pangocairo png ps raw rsvg tiff webp xml zlib```

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

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

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.