snibgo's ImageMagick pages

De-barrel distortion

I love GoPro cameras, including the fisheye effect (heavy barrel distortion). If we want, we can make the image rectilinear (removing this distortion).

Two techniques are explored here. The first uses "-distort Barrel". This is simple to understand, but not easy to get good results for image with massive distortion.

The second technique ( De-fisheye) is more complicated but gives excellent results.

Experiments with "-distort Barrel"

This example photo was taken through a window looking directly at the sun. The bottom of the windowsill and the brick surround can be seen. In real life, they are both straight. The camera is reflected in the glass window. The original is 4000x3000 pixels. For web purposes, we shrink the image. The values for the parameters don't depend on the size.

set SRC=db_source.png

if not exist %SRC% %IM%convert ^
  \pictures\20140115\GOPR0077.jpg ^
  -resize 300x400 ^
  -strip ^
  %SRC%
db_source.png

All images on this page are made from this small image. Naturally, creating images from the full-size original would give better quality.

To remove barrel distortion we can use -distort Barrel with three or four parameters. The output size will be the same as the input size. For each pixel in the output image, IM calculates the radius (Rdst) from the centre of the image, normalised so Rdst=1 at the centre of the longest sides. To find which source pixel should be copied here, it applies the formula:

Rsrc = A.Rdst4 + B.Rdst3 + C.Rdst2 + D.Rdst

The values A, B and C must be supplied to -distort Barrel. If D is not supplied, it defaults to 1 - (A + B + C).

Suitable values for A, B and C can be found automatically from calibrate_lens_gui.exe, part of the Hugin toolset. (However, that tool optimises for zero distortion at the centre of the long edge, D = 1 - (A + B + C).) Or they can be found by trial-and-error.

For the GoPro Hero 3 Black, reasonable values are A = 0.10, B = -0.32, C = 0. This gives the default value for D = 1.22.

%IM%convert ^
  %SRC% ^
  -distort Barrel 0.10,-0.32,0 ^
  db_plain.png
db_plain.pngjpg

Another set of parameters that work:

%IM%convert ^
  %SRC% ^
  -distort Barrel 0.001,0,-0.31 ^
  db_plain2.png
db_plain2.pngjpg

Yet another set of parameters that work:

%IM%convert ^
  %SRC% ^
  -distort Barrel 0.007,-0.007,-0.33 ^
  db_plain3.png
db_plain3.pngjpg

Yet another set of parameters, this time from Hugin optimizer:

%IM%convert ^
  %SRC% ^
  -distort Barrel -0.081,0.192,-0.393 ^
  db_plain4.png

The result is lousy.

db_plain4.pngjpg

At this default value for D:

Rsrc = A.Rdst4 + B.Rdst3 + C.Rdst2 + D.Rdst Rsrc = A.Rdst4 + B.Rdst3 + C.Rdst2 + (1 - (A + B + C)).Rdst

When Rdst==1:

Rsrc = A.14 + B.13 + C.12 + (1 - (A + B + C)).1
Rsrc = A + B + C + (1 - (A + B + C))
Rsrc = 1

So pixels at Rdst=1 won't move. By taking the difference, we see a black circle where the image not been distorted.

%IM%convert ^
  %SRC% ^
  db_plain.png ^
  -compose Difference -composite ^
  -auto-level -auto-gamma ^
  db_plain_diff.png
db_plain_diff.pngjpg

By choosing a suitable value for D, we can set this radius of non-distortion (Rnd) at any desired value.

Rnd = A.Rnd4 + B.Rnd3 + C.Rnd2 + D.Rnd 
1 = A.Rnd3 + B.Rnd2 + C.Rnd + D
D = 1 - (A.Rnd3 + B.Rnd2 + C.Rnd)

For no distortion at the centre, we need Rnd = 0, hence D = 1.

%IM%convert ^
  %SRC% ^
  ( +clone ^
    -virtual-pixel Black ^
    -distort Barrel 0.10,-0.32,0,1 ^
    -write db_cent.png ^
  ) ^
  -compose Difference -composite ^
  -auto-level -auto-gamma ^
  db_cent_diff.png
db_cent.pngjpg db_cent_diff.pngjpg

Compared to db_plain.png above, this image is "zoomed in", and we have lost pixels around the edges.

Another special case is with zero distortion at the centre of the short edges. This image has an aspect ratio of 4:3, so Rnd = 4/3 = 1.3333.

Substituting values A = 0.10, B = -0.32, C = 0:

D = 1 - (A.Rnd3 + B.Rnd2 + C.Rnd)
D = 1 - (0.1*(4/3)3 - 0.32*(4/3)2 + 0)
D = 1.3319

To highlight the added border, I use "-virtual-pixel Black". This darkens pixels at the edge of the boundary. If we are cropping within the boundary, it is better to use "-virtual-pixel Edge".

%IM%convert ^
  %SRC% ^
  ( +clone ^
    -virtual-pixel Black ^
    -distort Barrel 0.10,-0.32,0,1.3319 ^
    -write db_centSht.png ^
  ) ^
  -compose Difference -composite ^
  -auto-level -auto-gamma ^
  db_centSht_diff.png
db_centSht.pngjpg db_centSht_diff.pngjpg
%IM%convert ^
  %SRC% ^
  ( +clone ^
    -virtual-pixel Black ^
    -distort Barrel 0.02,-0.03,-0.45,1.6059 ^
    -write db_centSht2.png ^
  ) ^
  -compose Difference -composite ^
  -auto-level -auto-gamma ^
  db_centSht2_diff.png
db_centSht2.pngjpg db_centSht2_diff.pngjpg

As a final special case, we may want zero distortion in the corners. This gives all the pixels from the source image, but with extra virtual pixels. Pixels near centre will be heavily squashed. (Or, if the image is enlarged, pixels near the corners will be heavily stretched.) With an aspect ratio of 4:3, the diagonal is 5 (we have a Pythagorean triangle 3:4:5), so Rnd = 5/3 = 1.6667.

D = 1 - (A.Rnd3 + B.Rnd2 + C.Rnd)
D = 1 - (0.1*(5/3)3 - 0.32*(5/3)2 + 0)
D = 1.4259
%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.10,-0.32,0,1.4259 ^
  db_corn.png
db_corn.pngjpg

However, lines that should be straight are now curved. Huh?

Multiply A, B and C by 1.4259/1.22:

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.1169,-0.3740,0,1.4259 ^
  db_corn2.png
db_corn2.pngjpg

But then D no longer keeps zero distortion in the corners. Should be 1.497. Then A = 0.1 * 1.497/1.22 = 0.1227 and B = 0.32 * 1.497/1.22 = 0.3927. Then D = 1.5228. Etc.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.1227,-0.3927,0,1.5229 ^
  db_corn3.png
db_corn3.pngjpg
%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.40,-0.95,0,1.787 ^
  db_corn4.png
db_corn4.pngjpg

D such that corners don't move

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.155,-0.26,-0.55,1.9213 ^
  db_corn5.png
db_corn5.pngjpg

D such that A+B+C+D=1

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Barrel 0.155,-0.26,-0.55,1.655 ^
  db_cent5.png
db_cent5.pngjpg

Asymetric transformations

Blah ...

Resizing from "-barrel"

We can de-barrel and and enlarge in a single operation. This is quicker and higher quality than doing two separate operations.

From db_corn5.png above, scaled by 1.5.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -define distort:scale=1.5 ^
  -distort Barrel 0.155,-0.26,-0.55,1.9213 ^
  db_corn5_sc1.png
db_corn5_sc1.pngjpg

If we choose scale to the same value as D, the scale at the centre will be one, i.e. the same as the undistorted source. Pixels near the corners will be heavily stretched.

From db_corn5.png above, scaled by 1.9213.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -background Khaki ^
  -define distort:scale=1.9213 ^
  -distort Barrel 0.155,-0.26,-0.55,1.9213 ^
  db_corn5_sc2.png
db_corn5_sc2.pngjpg

Or we can shrink.

From db_corn5.png above, multiplied by 0.5.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -background Khaki ^
  -define distort:scale=0.5 ^
  -distort Barrel 0.155,-0.26,-0.55,1.9213 ^
  db_corn5_sc3.png
db_corn5_sc3.pngjpg

We can apply a viewport to the distort. This is like "-crop", but gravity has no effect, nor does "%". Whatever the order, "distort:viewport" is applied first, then "distort:scale".

From db_corn5.png above, scaled by 1.5, with viewport.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -gravity East ^
  -define distort:viewport=50%%x50%% ^
  -define distort:scale=1.5 ^
  -distort Barrel 0.155,-0.26,-0.55,1.9213 ^
  db_corn5_sc4.png
db_corn5_sc4.pngjpg

An alternative method of resizing is to use "-extent". We make it (n) times larger by using "-extent" at n*100%, and dividing each of A, B, C and D by (n). In this example, virtual pixels won't be used, so the border colour is taken from "-background".

From db_corn5.png above, multiplied by 1.5.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -background Khaki ^
  -gravity Center -extent 150%% ^
  -distort Barrel 0.10333,-0.17333,-0.36667,1.28087 ^
  db_corn5_exp.png
db_corn5_exp.pngjpg

If we choose (n) to be the value of D (so the new value of D will be one), the scale at the centre will be one, i.e. the same as the undistorted source. Pixels near the corners will be heavily stretched.

From db_corn5.png above, multiplied by 1.9213.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -background Khaki ^
  -gravity Center -extent 192.13%% ^
  -distort Barrel 0.080675,-0.13533,-0.28626,1 ^
  db_corn5_one.png
db_corn5_one.pngjpg

Although "-extent" less than 100% is possible, it doesn't give the desired result.

Cropping from "-barrel"

We can crop the result in the usual way. One crop in particular is useful: a letterbox crop from the de-barrel with zero distortion at the centre of the short lines, removing the two groups of virtual pixels. The result takes the greatest advantage of the camera's wide angle.

If a script hard-codes the ABCD parameters, it might as well also hard-code the crop parameters.

In the general case, we need to find the crop parameters from arbitrary ABCD.

To find the crop parameters, we can walk along each pixel in the longest centre-line, from the edge towards the centre, calculating (r) and hence Rsrc. We take a note when Rsrc becomes less than the semi-width or semi-height. This gives us the required crop width and height. For the letterbox crop, we are interested only when Rsrc becomes less than half the shortest dimension. The housekeeping is a bit messy, so we use a script, deBarrelCrop.bat. It returns the crop width and height, in pixels.

call %PICTBAT%deBarrelCrop db_centSht2.png 0.02,-0.03,-0.45,1.6059
echo %dbcWIDTHpix% %dbcHEIGHTpix% %dbcOFFS_X% %dbcOFFS_Y% 
300 183 0 21 
%IM%convert ^
  db_centSht2.png ^
  -gravity Center -crop %dbcWIDTHpix%x%dbcHEIGHTpix%+0+0 +repage ^
  db_centSht2_cr.png
db_centSht2_cr.pngjpg

When we have the crop parameters from deBarrelCrop, we can use this in a viewport for distort.

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -define distort:viewport=%dbcWIDTHpix%x%dbcHEIGHTpix%+%dbcOFFS_X%+%dbcOFFS_Y% ^
  -distort Barrel 0.02,-0.03,-0.45,1.6059 ^
  +repage ^
  db_centSht2_cr2.png

Compare this to the result from "-crop":

%IM%compare -metric RMSE db_centSht2_cr.png db_centSht2_cr2.png NULL: 
cmd /c exit /B 0
0 (0)
db_centSht2_cr2.pngjpg

Displacement maps from "-barrel"

For the basics, see my main page on Displacement maps.

We can create a relative displacement map for a given debarrel operation, and apply the same map to many images.

Create the map. In a single command, we create a null absolute map, apply barrel distortion to this, and convert it to a relative displacement map.

set DEBARREL=-distort Barrel 0.02,-0.03,-0.45,1.6059

FOR /F "usebackq" %%L IN (`%IM%identify -format "WW=%%w\nHH=%%h\nWm1=%%[fx:w-1]\nHm1=%%[fx:h-1]" %SRC%`) DO set %%L

%IM%convert ^
  ( -size %HH%x%WW% gradient: -rotate 90 ) ^
  ( -size %WW%x%HH% gradient: -flip ) ^
  -size %WW%x%HH% xc:Black ^
  -combine ^
  -evaluate Divide 2 ^
  -set colorspace sRGB ^
  ( +clone -virtual-pixel Edge %DEBARREL% ) ^
  -compose ModulusSubtract -composite ^
  -evaluate AddModulus 50%% ^
  db_map_rel.miff

Apply the map. We use "+depth" for precision in the comparison.

%IM%convert ^
  %SRC% ^
  db_map_rel.miff ^
  -compose Displace ^
    -set option:compose:args %Wm1%x%Hm1% ^
    -composite ^
  +depth ^
  db_deb_by_map.png
db_deb_by_map.png

"-virtual-pixel" doesn't seem to work. The map needs tweaking? The centres of the short sides are naff.

For comparison, debarrel directly ...

%IM%convert ^
  %SRC% ^
  %DEBARREL% ^
  +depth ^
  db_deb_direct.png
db_deb_direct.png

... and compare ...

%IM%compare ^
  -metric RMSE ^
  db_deb_by_map.png ^
  db_deb_direct.png ^
  NULL: 

cmd /c exit /B 0
240.499 (0.00366978)

If we want to use a constant colour (such as black) for virtual pixels, using a displacement map, with proper anti-aliasing, the method becomes more complicated. The mask has the same colors as before, but with an anti-aliased alpha channel.

set DEBARREL=-distort Barrel 0.02,-0.03,-0.45,1.6059

FOR /F "usebackq" %%L IN (`%IM%identify ^
  -format "WW=%%w\nHH=%%h\nWm1=%%[fx:w-1]\nHm1=%%[fx:h-1]" ^
  %SRC%`) DO set %%L

%IM%convert ^
  ( -size %HH%x%WW% gradient: -rotate 90 ) ^
  ( -size %WW%x%HH% gradient: -flip ) ^
  -size %WW%x%HH% xc:Black ^
  -combine ^
  -evaluate Divide 2 ^
  -set colorspace sRGB ^
+write x0.png ^
  ( +clone -virtual-pixel None %DEBARREL% -write mpr:MASK +write x1.png +delete ) ^
  ( +clone -virtual-pixel Edge %DEBARREL% ) ^
  -compose ModulusSubtract -composite ^
  -evaluate AddModulus 50%% ^
  mpr:MASK ^
  -compose CopyOpacity -composite ^
  db_map_rel2.miff

set sSPARSE=^
0,0,#000,^
%Wm1%,0,#f00,^
0,%Hm1%,#0f0,^
%Wm1%,%Hm1%,#ff0

%IM%convert ^
  -size %WW%x%HH% xc:Black ^
  ( +clone -virtual-pixel None %DEBARREL% -write mpr:MASK +write x1.png +delete ) ^
  -sparse-color Bilinear %sSPARSE% ^
+write x0.png ^
  ( +clone -virtual-pixel Edge %DEBARREL% ) ^
  -compose Mathematics -define compose:args=0,0.5,-0.5,0.5 -composite ^
  mpr:MASK ^
  -alpha set ^
  -compose CopyOpacity -composite ^
  db_map_rel2.miff

Apply the map. We displace according to the colours in the usual way, but then we copy opacity from the map and flatten against the required background colour.

%IM%convert ^
  %SRC% ^
  db_map_rel2.miff ^
  ( -clone 0-1 ^
    -compose Displace ^
      -set option:compose:args %Wm1%x%Hm1% ^
      -composite ^
  ) ^
  -delete 0 +swap ^
  -compose CopyOpacity -composite ^
  -background #000 -compose Over -flatten ^
  +depth ^
  db_deb_by_map2.png
db_deb_by_map2.png

Applying this map is expensive. We displace and copy opacity and flatten. An alternative would be to create a mask of the black for the vitual pixels and transparent elsewhere, and compose that over the image, saving one operation, at the cost of a reading an extra image.

For comparison, debarrel directly ...

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  %DEBARREL% ^
  +depth ^
  db_deb_direct2.png
db_deb_direct2.png

... and compare ...

%IM%compare ^
  -metric RMSE ^
  db_deb_by_map2.png ^
  db_deb_direct2.png ^
  NULL: 

cmd /c exit /B 0
229.102 (0.00349588)

"-barrel" with Viewports

The "-barrel" distortions above actually create pincusion results, moving the corners outwards. Even when the radius of no distortion, Rnd, is set to 1.0 so the short dimension remains the same, we might want to see what happens in the corners. To do this, we can use a viewport. This takes the usual WxH+X+Y geometry argument, where WxH will be the desired output size, generally larger than the input, and the offsets should be half the input dimension minus the output dimension. (Thus, the offsets will usually be negative.)

The script barrelTest.bat calls the script barrelSize.bat with the image dimensions and the barrel parameters. barrelSize.bat makes a dummy white image, guesses how large the output should be, distorts the white image, and trims the result. The size of the result tells us what viewport dimensions we need, and barrelTest.bat uses this to distort the actual image.

call %PICTBAT%barrelTest ^
  %SRC% ^
  db_plain_bt.jpg ^
  "0.10,-0.32,0" ^
  2
db_plain_bt.jpg
call %PICTBAT%barrelTest ^
  %SRC% ^
  db_plain_bt2.jpg ^
  "0.10,-0.32,0" ^
  9
db_plain_bt2.jpg

Each destination pixel is derived from a single source pixel. However, each source pixel is distorted to up to four pixels.

call %PICTBAT%barrelTest ^
  %SRC% ^
  db_plain_bt3.jpg ^
  "-0.10,-0.32,0" ^
  2
db_plain_bt3.jpg
call %PICTBAT%barrelTest ^
  %SRC% ^
  db_plain_bt4.jpg ^
  "-0.10,-0.32,-0.19" ^
  5
db_plain_bt4.jpg
barrelTest "-0.010,-0.032,-0.19" 7

Depolar/Polar with displacement maps

We can "-distort Depolar" to unwrap the image so the y-dimension is the radius, then distort in the y-dimension, then "-distort Polar". The basic scheme could work like this:

FOR /F "usebackq" %%L ^
IN (`%IM%identify ^
  -format "WW=%%w\nHH=%%h\nWm1=%%[fx:w-1]\nHm1=%%[fx:h-1]" ^
  %SRC%`) ^
DO set %%L

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -distort Depolar -1 ^
  -write db_depol.png ^
  ( ^
    -size 1x%HH% ^
    gradient:black-white ^
    ( +clone -gamma 1 ) ^
    -compose Mathematics ^
      -define compose:args=0,0.5,-0.5,0.5 ^
      -composite ^
    -scale "%WW%x%HH%^!" ^
  ) ^
  -compose Displace ^
    -set option:compose:args 0x%Hm1% ^
    -composite ^
  -background Pink -virtual-pixel Background ^
  -distort Polar -1 ^
  db_depol_pol.png

%IM%compare -metric RMSE %SRC% db_depol_pol.png NULL: 
cmd /c exit /B 0
1336.99 (0.0204012)
db_depol.pngjpg db_depol_pol.pngjpg

The example above uses "-gamma 1" as a no-op. Any transformation could be used. The displacement map is black at the top (representing a zero distance from the centre) to white at the bottom (representing the maximum distance, from the centre to the corners).

In the depolared image, pixels near the centre of the source have been spread out, while those near the edge have been squished together. We can improve the quality by supersampling with "distort:scale" at each end and (crudely) resizing the map. The intermediate result is large, so we don't save or show it.

FOR /F "usebackq" %%L ^
IN (`%IM%identify ^
  -format "WW=%%w\nHH=%%h\nWm1=%%[fx:w-1]\nHm1=%%[fx:h-1]" ^
  %SRC%`) ^
DO set %%L

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -define distort:scale=4 ^
  -distort Depolar -1 ^
  ( ^
    -size 1x%HH% ^
    gradient:black-white ^
    ( +clone -gamma 1 ) ^
    -compose Mathematics ^
      -define compose:args=0,0.5,-0.5,0.5 ^
      -composite ^
    -scale "%WW%x%HH%^!" ^
    -resize 400%% ^
  ) ^
  -compose Displace ^
    -set option:compose:args 0x%Hm1% ^
    -composite ^
  -background Pink -virtual-pixel Background ^
  -define distort:scale=0.25 ^
  -distort Polar -1 ^
  db_depol_pol2.png

%IM%compare -metric RMSE %SRC% db_depol_pol2.png NULL: 
cmd /c exit /B 0
565.253 (0.00862522)
db_depol_pol2.pngjpg

A bit of maths for "-barrel"

The basic fourth-order polynomial de-barrel equation is:

Rsrc = A.Rdst4 + B.Rdst3 + C.Rdst2 + D.Rdst

At the radius of non-distortion, Rsrc = Rdst = Rnd.

Rnd = A.Rnd4 + B.Rnd3 + C.Rnd2 + D.Rnd 
1 = A.Rnd3 + B.Rnd2 + C.Rnd + D
D = 1 - (A.Rnd3 + B.Rnd2 + C.Rnd)

So:

Rsrc = A.r4 + B.r3 + C.r2 + r - r.(A.Rnd3 + B.Rnd2 + C.Rnd)
     = Rdst.[A.Rdst3 + B.Rdst2 + C.Rdst + 1 - A.Rnd3 - B.Rnd2 - C.Rnd]
     = Rdst.[A.(Rdst3-Rnd3) + B.(Rdst2-Rnd2) + C.(Rdst-Rnd) + 1]

Suppose Rdst = t.Rnd. Then:

Rsrc = Rdst.[A.(Rdst3-Rnd3) + B.(Rdst2-Rnd2) + C.(Rdst-Rnd) + 1]
     = t.Rnd.[A.Rnd3.(t3-1) + B.Rnd2.(t2-1) + C.Rnd.(t-1) + 1]

At t==0, Rsrc=0. At t==1, Rsrc=Rnd.

Inside the circle of non-distortion, t < 1. Outside, t > 1. When t < 1, all Rnd terms are < 0, so Rsrc < t.Rnd. When t > 1, all Rnd terms are > 0, so Rsrc > t.Rnd.

Beyond the usual

We usually want 0 <= Rnd <= semi-diagonal/semi-short-side.

De-fisheye

After some experimentation, it turns out that the lens is close to an ideal equidistant fisheye (see external page PanoTools Fisheye Projection), so we use arctan to return the projection to rectilinear.

FOR /F "usebackq" %%L ^
IN (`%IM%identify ^
  -format "WW4=%%[fx:4*w]\nHH4=%%[fx:4*h]\nW4m1=%%[fx:4*w-1]\nH4m1=%%[fx:4*h-1]" ^
  %SRC%`) ^
DO set %%L

set RAT=3.1

%IM%convert ^
  -size 1x%HH4% ^
  gradient:black-white ^
  ( +clone ^
    -fx "atan(%RAT%*u)/atan(%RAT%)" ^
  ) ^
  -compose Mathematics -define compose:args=0,0.5,-0.5,0.5 -composite ^
  -scale "%WW4%x%HH4%^!" ^
  pol_map_rel4.png

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -define distort:scale=4 ^
  -distort Depolar -1 ^
  pol_map_rel4.png ^
  -compose Displace ^
    -set option:compose:args 0x%H4m1% ^
    -composite ^
  -background Pink -virtual-pixel Background ^
  -define distort:scale=0.25 ^
  -distort Polar -1 ^
  db_depol_pol3.png

Repeat the distortion in RGB space to see if there is a difference.

%IM%convert ^
  %SRC% ^
  -colorspace RGB -set colorspace sRGB ^
  -virtual-pixel Black ^
  -define distort:scale=4 ^
  -distort Depolar -1 ^
  pol_map_rel4.png ^
  -compose Displace ^
    -set option:compose:args 0x%H4m1% ^
    -composite ^
  -background Pink -virtual-pixel Background ^
  -define distort:scale=0.25 ^
  -distort Polar -1 ^
  -set colorspace RGB -colorspace sRGB ^
  db_depol_pol4.png

%IM%compare -metric RMSE db_depol_pol3.png db_depol_pol4.png NULL: 
cmd /c exit /B 0
664.013 (0.0101322)

No significant difference.

db_depol_pol3.pngjpg db_depol_pol4.pngjpg

As usual (see Displacement Maps), we can create an identity (mis-named "null") displacement map, give it the depolar/atan/polar transformation, and apply that to many photographs.

We implement this in a script, rectGoPro.bat.

Finding the inner border is easy. When Rsrc is the distance from the centre in fisheye space; r is the distance from the centre in rectlinear space:

Rsrc = atan (k.r) / atan (k)

where r is one at the corners, not at the centre of the long sides, so:

r = tan (Rsrc.atan(k)) / k

For this image, aspect ratio 4:3, the centres of the long sides are at Rsrc = 3/5 of the semi-diagonal. Taking k as 3.1:

r = tan(3/5 * atan(3.1))/3.1 = 0.303695

or r = 759.2.7 pixels from the centre for a 4000x3000 image. Hence the maximum height available with no virtual pixels is 1518 pixels.

The centres of the short sides are at Rsrc = 4/5, so:

r = tan(4/5 * atan(3.1))/3.1 = 0.51021

or r = 1275.5 pixels from the centre for a 4000x3000 image. Hence the maximum width available with no virtual pixels is 2551 pixels.

atan(3.1) is 72.1°, so the camera records 144.2° from corner to opposite corner. If the lens is a true equidistant (linear) fisheye then the horizontal angle is 4000/5000 * 144.2 deg = 115.4°, and the vertical angle is 144.2 * 3000/5000 = 86.5°.

Conclusions

There are four main transformations for removing barrel distortion, but an infinite number of others.

Here are the summary results for my GoPro Hero 3 Black.

Rnd Unchanged scale at... Extent A B C D Code Image Crop
0 ... image centre. none 0.10 -0.32 0 1
1 ... centre of long edges. none 0.007 -0.007 -0.33
longest/shortest = 4/3 ... centre of short edges. none 0.02 -0.03 -0.45 1.6059
diagonal/shortest = 5/3 ... corners. none 0.155 -0.26 -0.55 1.9213
0 ... image centres. Keeps all image pixels. 192.13% 0.080675 -0.13533 -0.28626 1

The depolar/displace-arctan/polar method gives the best result. It is fast, provided the displacement map has already been created. We use a script, rectGoPro.bat.

call %PICTBAT%rectGoPro %SRC%
if ERRORLEVEL 1 goto error

%IM%convert ^
  db_source_rgp.png ^
  db_rgp1.jpg
db_rgp1.jpg
call %PICTBAT%rectGoPro %SRC% 0 Black
if ERRORLEVEL 1 goto error

%IM%convert ^
  db_source_rgp.png ^
  db_rgp2.jpg
db_rgp2.jpg
call %PICTBAT%rectGoPro %SRC% 1
if ERRORLEVEL 1 goto error

%IM%convert ^
  db_source_rgp.png ^
  db_rgp3.jpg
db_rgp3.jpg
call %PICTBAT%rectGoPro %SRC% 1 0 1
if ERRORLEVEL 1 goto error

%IM%convert ^
  db_source_rgp.png ^
  db_rgp4.jpg
db_rgp4.jpg

Scaling: I mostly want the output image to be a certain size, or the best quality. If I cared about keeping a certain scale at the centre, then I can calculate that atan(0.0001*3.1)/atan(3.1)/0.0001 = 2.4627524 so the default script settings will shrink the centre by a factor of about 2.46. I could enlarge the result by that amount or (better) set rgpSUP_SAMP=4 before running the script then enlarge by 2.46/4.

The script assumes that images can be enlarged by rgpSUP_SAMP and then reduced by the same, giving the original number of pixels. The default value of 2 is generally fine. A value of 4 gives better quality, but takes longer and also eats time, memory and disk, as the images are 16000x12000 pixels.

A smooth transition can be made between the fisheye image made by the camera and the rectilinear version by varying rgpRAT between a small positive value (eg 0.0001) and the usual value of 3.1.

Scripts

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

deBarrelCrop.bat

rem For image %1, debarrel ABCD parameters %2 %3 %4 %5,
rem find crop parameters.

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

@setlocal

call echoOffSave

call %PICTBAT%setInOut %1 dbc

set A=%2
set B=%3
set C=%4
set D=%5

for /F "usebackq" %%L ^
in (`%IM%identify -format "WW=%%w\nHH=%%h\nIsPort=%%[fx:w>h?0:1]\nLL_2=%%[fx:w>h?w/2:h/2]\nSS_2=%%[fx:w<h?w/2:h/2]" %INFILE%`) ^
do set %%L

for /F "usebackq" %%L ^
in (`%IM%identify -format "LL_2i=%%[fx:int(%LL_2%)]\nSS_2i=%%[fx:int(%SS_2%)]" %INFILE%`) ^
do set %%L

for /F "usebackq" %%L ^
in (`%IM%identify -format "LL_2F=%%[fx:%LL_2%/%SS_2%]\nODD_LL=%%[fx:%LL_2%%%2>0?1:0]\nODD_SS=%%[fx:%SS_2%%%2>0?1:0]" xc:`) ^
do set %%L

echo ODD_LL=%ODD_LL%  ODD_SS=%ODD_SS%

rem Beware fractional semi-length.

set OFFS_SS=0
set OFFS_LL=0

for /L %%I in (%LL_2%,-1,1) do (
  if !OFFS_SS!==0 (
    for /F "usebackq" %%L ^
in (`%IM%identify -format "rDest=%%[fx:%%I/%SS_2%]" %SRC%`) ^
do set %%L

    for /F "usebackq" %%L ^
in (`%IM%identify ^
  -format "rSrc=%%[fx:!rDest!*((%A%)*!rDest!*!rDest!*!rDest!+(%B%)*!rDest!*!rDest!+(%C%)*!rDest!+(%D%))]" ^
  %SRC%`) ^
do set %%L

    echo I=%%I  rDest=!rDest!  rSrc=!rSrc!

    for /F "usebackq" %%L ^
in (`%IM%identify -format "LSS_LL=%%[fx:!rSrc!<%LL_2F%?1:0]\nLSS_SS=%%[fx:!rSrc!<1?1:0]" %SRC%`) ^
do set %%L

    if !LSS_LL!==1 if !OFFS_LL!==0 set OFFS_LL=%%I
    if !LSS_SS!==1 if !OFFS_SS!==0 set OFFS_SS=%%I
  )
)

echo OFFS_LL=%OFFS_LL%  OFFS_SS=%OFFS_SS%

if %IsPort%==1 (
  set /A OFFS_X=%SS_2i%-%OFFS_SS%
  set /A OFFS_Y=%LL_2i%-%OFFS_LL%
  set /A WIDTHpix=%OFFS_SS%+%OFFS_SS%+%ODD_SS%
  set /A HEIGHTpix=%OFFS_LL%+%OFFS_LL%+%ODD_LL%
) else (
  set /A OFFS_X=%LL_2i%-%OFFS_LL%
  set /A OFFS_Y=%SS_2i%-%OFFS_SS%
  set /A WIDTHpix=%OFFS_LL%+%OFFS_LL%+%ODD_LL%
  set /A HEIGHTpix=%OFFS_SS%+%OFFS_SS%+%ODD_SS%
)

call echoRestore

endlocal & set dbcOFFS_X=%OFFS_X%& set dbcOFFS_Y=%OFFS_Y%& set dbcWIDTHpix=%WIDTHpix%& set dbcHEIGHTpix=%HEIGHTpix%

rem %IM%convert %1 -gravity Center -crop %dbcWIDTHpix%x%dbcHEIGHTpix%+0+0 d.png

rectGoPro.bat

rem For image %1, from a GoPro, makes rectilinear version.
@rem %2 is whether to crop within virtual pixels. Default = 0 = don't crop
@rem %3 is whether to overlay virtual pixels (and what colour). Default = 0 = no overlay; none=make them transparent, otherwise colour.
@rem %4 is whether to leave at supersampled size. (Supersamples by rgpSUP_SAMP.) Default = 0 = reduce size down.

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

@setlocal

call echoOffSave

call %PICTBAT%setInOut %1 rgp


set MASK_DIR=%PICTBAT%rectGoPro\
set DISP_DIR=%PICTBAT%rectGoPro\
set WORK_EXT=mpc

if not exist %MASK_DIR% md %MASK_DIR%
if not exist %DISP_DIR% md %DISP_DIR%

set DO_CROP=%2
if "%DO_CROP%"=="" set DO_CROP=0

set BORD_COL=%3
if "%BORD_COL%"=="" set BORD_COL=0

set SUP_SAMP=%4
if "%SUP_SAMP%"=="" set SUP_SAMP=0

if "%rgpSUP_SAMP%"=="" set rgpSUP_SAMP=2

set /A SUP_SAMPpc = %rgpSUP_SAMP%*100


FOR /F "usebackq" %%L ^
IN (`%IM%identify ^
  -format "WW=%%w\nHH=%%h\nIS_PORT=%%[fx:w<h?1:0]\nSUP_SAMPinvPc=%%[fx:100/%rgpSUP_SAMP%]\nSUP_SAMPinv=%%[fx:1/%rgpSUP_SAMP%]" ^
  %INFILE%`) ^
DO set %%L

if %IS_PORT%==0 (
  set ROT_FOR=
  set ROT_REV=
) else (
  set ROT_FOR=-rotate 90
  set ROT_REV=-rotate -90
  set t=%WW%
  set WW=%HH%
  set HH=!t!
)

rem We need to find good values of RAT for other image sizes. (3 still and 9 video sizes.)

if not %WW%==4000 (
  echo Dimension needs to be 4000x3000 or 3000x4000.
rem  exit /B 1
)

if not %HH%==3000 (
  echo Dimension needs to be 4000x3000 or 3000x4000.
rem  exit /B 1
)

if "%rgpRAT%"=="" (
  set RAT=3.1
) else (
  set RAT=%rgpRAT%
)


set sRAT=%RAT:.=-%

set SUFFIX=%WW%_%HH%_%sRAT%

set /A WWss=%rgpSUP_SAMP%*%WW%
set /A HHss=%rgpSUP_SAMP%*%HH%
set /A Hm1=%HH%-1
set /A Wm1=%WW%-1
set /A Wssm1=%WWss%-1
set /A Hssm1=%HHss%-1


rem Make displacement maps, if we need them and don't have them.

set sBIG_MAP=%DISP_DIR%rgp_disp_map_%SUFFIX%_%rgpSUP_SAMP%.%WORK_EXT%
set sSML_MAP=%DISP_DIR%rgp_disp_map_%SUFFIX%.%WORK_EXT%

set MAKE_BIG_MAP=0
set MAKE_SML_MAP=0

if not exist %sSML_MAP% if %SUP_SAMP%==0 set MAKE_SML_MAP=1

if not exist %sBIG_MAP% (
  if %SUP_SAMP%==1 set MAKE_BIG_MAP=1
  if %MAKE_SML_MAP%==1 set MAKE_BIG_MAP=1
)

if %MAKE_BIG_MAP%==1 %IM%convert ^
  ( -size %HHss%x%WWss% gradient: -rotate 90 ) ^
  ( -size %WWss%x%HHss% gradient: -flip ) ^
  -size %WWss%x%HHss% xc:Black ^
  -combine ^
  -set colorspace sRGB ^
  ( +clone ^
    -virtual-pixel Edge ^
    -distort Depolar -1 ^
    ( ^
      -size 1x%HHss% ^
      gradient:black-white ^
      ( +clone ^
        -fx "atan(%RAT%*u)/atan(%RAT%)" ^
      ) ^
      -compose Mathematics -define compose:args=0,0.5,-0.5,0.5 -composite ^
      -scale "%WWss%x%HHss%^!" ^
    ) ^
    -compose Displace ^
      -set option:compose:args 0x%Hssm1% ^
      -composite ^
    -write drx0.jpg ^
    -background Pink -virtual-pixel Background ^
    -distort Polar -1 ^
  ) ^
  -compose Mathematics -define compose:args=0,0.5,-0.5,0.5 -composite ^
  %sBIG_MAP%

if %MAKE_SML_MAP%==1 %IM%convert ^
  %sBIG_MAP% ^
  -scale %SUP_SAMPinvPc%%% ^
  %sSML_MAP%


rem Make borders to compose over virtual pixels, if we need them and don't have them.

set sBIG_BORD=%MASK_DIR%rgp_bord_%SUFFIX%_%rgpSUP_SAMP%.%WORK_EXT%
set sSML_BORD=%MASK_DIR%rgp_bord_%SUFFIX%.%WORK_EXT%

set MAKE_BIG_BORD=0
set MAKE_SML_BORD=0

if not exist %sSML_BORD% if %SUP_SAMP%==0 set MAKE_SML_BORD=1

rem FIXME: large border
rem FIXME: polmap_rel.png

if %MAKE_SML_BORD%==1 (
%IM%convert ^
  -size %WW%x%HH% ^
  xc:#fff ^
  -virtual-pixel Black ^
  -define distort:scale=%rgpSUP_SAMP% ^
  -distort Depolar -1 ^
  ^( ^
    -size 1x%HHss% ^
    gradient:black-white ^
    ^( +clone ^
      -fx "atan(%RAT%*u)/atan(%RAT%)" ^
    ^) ^
    -compose Mathematics -define compose:args=0,0.5,-0.5,0.5 -composite ^
    -scale "%WWss%x%HHss%^!" ^
  ^) ^
  -compose Displace ^
    -set option:compose:args 0x%Hssm1% ^
    -composite ^
  ^( +clone ^
    -define distort:scale=1 ^
    -distort Polar -1 ^
    -negate -alpha Copy -evaluate Set 0 ^
    -write %sBIG_BORD% ^
    +delete ^
  ^) ^
  -define distort:scale=%SUP_SAMPinv% ^
  -distort Polar -1 ^
  -negate -alpha Copy -evaluate Set 0 ^
  %sSML_BORD%
)

if %SUP_SAMP%==0 (
  set fBORD=%sSML_BORD%
  set CW=%WW%
  set CH=%HH%
) else (
  set fBORD=%sBIG_BORD%
  set CW=%WWss%
  set CH=%HHss%
)

rem Set crop.

if %DO_CROP%==1 (
  for /F "usebackq" %%L ^
in (`%IM%identify -format "DIAG=%%[fx:sqrt(%CW%*%CW%+%CH%*%CH%)]" xc:`) ^
do set %%L

  for /F "usebackq" %%L ^
in (`%IM%identify -format "C_WI=%%[fx:int(!DIAG!*tan(%CW%/!DIAG!*atan(%RAT%))/%RAT%)]\nC_HT=%%[fx:int(!DIAG!*tan(%CH%/!DIAG!*atan(%RAT%))/%RAT%)]" xc:`) ^
do set %%L

  set sCROP=-gravity Center -crop !C_WI!x!C_HT!+0+0 +repage
) else (
  set sCROP=
)

rem Set overlay. For non-black, quicker to pre-process.

if /I %BORD_COL%==0 (
  set sBORD=
) else if /I %BORD_COL%==none (
  set sBORD="(" %fBORD% -channel A -negate ")" -compose CopyOpacity -composite
) else if /I %BORD_COL%==black (
  set sBORD=%fBORD% -compose Over -composite
) else (
  set sBORD="(" %fBORD% -alpha off -fill %BORD_COL% -opaque #000 -alpha on ")" -compose Over -composite
)

if %SUP_SAMP%==1 (
  %IM%convert ^
    %INFILE% ^
    %ROT_FOR% ^
    -resize %SUP_SAMPpc%%% ^
    %sBIG_MAP% ^
    -virtual-pixel Black ^
    -compose Displace ^
      -set option:compose:args %Wssm1%x%Hssm1% ^
      -composite ^
    %sBORD% ^
    %sCROP% ^
    %ROT_REV% ^
    %COMPR% ^
    %OUTFILE%
) else (
  %IM%convert ^
    %INFILE% ^
    %ROT_FOR% ^
    %sSML_MAP% ^
    -virtual-pixel Black ^
    -compose Displace ^
      -set option:compose:args %Wm1%x%Hm1% ^
      -composite ^
    %sBORD% ^
    %sCROP% ^
    %ROT_REV% ^
    %COMPR% ^
    %OUTFILE%
)

%IM%identify %OUTFILE%

call echoRestore

barrelTest.bat

rem Given %1 is image,
rem %2 is output file
rem %3 is (quoted) set of barrel parameters
rem %4 is multiple of size for first guess, integer, [default 3]
rem 

setlocal

set SRC=\prose\pictures\db_source.png

set SRC=%1

set OUTFILE=%2

set DB=%3

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

call %PICTBAT%barrelSize %WW% %HH% %DB% %4

if ERRORLEVEL 1 exit /B 1

%IM%convert ^
  %SRC% ^
  -virtual-pixel Black ^
  -define distort:viewport=%bsVP% ^
  -distort barrel %DB% ^
  %OUTFILE%


endlocal

barrelSize.bat

rem Given %1 and %2 are width and height eg 267, 233
rem %3 is (quoted) set of barrel parameters
rem %4 is multiple of size for first guess, integer, [default 3]
rem finds output width and height that contains all the pixel data.

setlocal

set WW=%1
set HH=%2

set DP=%3

set MULT_SZ=%4
if "%MULT_SZ%"=="." set MULT_SZ=
if "%MULT_SZ%"=="" set MULT_SZ=3

set /A W_MULT=%MULT_SZ%*%WW%
set /A H_MULT=%MULT_SZ%*%HH%

set /A VX=(%W_MULT%-%WW%)/2
set /A VY=(%H_MULT%-%HH%)/2

echo %0: %W_MULT%x%H_MULT%-%VX%-%VY%

for /F "usebackq" %%L in (`%IM%convert ^
  -size %WW%x%HH% ^
  xc:White ^
  -virtual-pixel Black ^
  -define distort:viewport^=%W_MULT%x%H_MULT%-%VX%-%VY% ^
  -distort barrel %DP% ^
  -trim +repage ^
  -format "VW=%%w\nVH=%%h\nVX=%%[fx:int((w-%WW%)/2+0.5)]\nVY=%%[fx:int((h-%HH%)/2+0.5)]" ^
  info:`) do set %%L

if %VW%==%W3% exit /B 1
if %VH%==%H3% exit /B 1

endlocal & set bsVP=%VW%x%VH%-%VX%-%VY%

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

%IM%identify -version
Version: ImageMagick 6.9.5-3 Q16 x86 2016-07-22 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Visual C++: 180040629
Features: Cipher DPC Modules OpenMP 
Delegates (built-in): bzlib cairo flif freetype jng jp2 jpeg lcms lqr openexr pangocairo png ps rsvg tiff webp xml zlib

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


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.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 23-Feb-2014.

Page created 24-Nov-2016 14:31:10.

Copyright © 2016 Alan Gibson.