snibgo's ImageMagick pages

Canny edge detection

Canny (and other types of) edges can segment images.

See Wikipedia: Canny edge detector.

The syntax is "-canny radiusxsigma+lower_percent+upper_percent". radiusxsigma applies an initial Gausian blur.

Wedge test

Make a double wedge for testing.

%IM%convert ^
  -size 40x400 gradient: -rotate 90 ^
  ( +clone -flop ) ^
  -append ^
  ca_wedge.png
ca_wedge.png

The values at the centre are:

%IM%convert ^
  ca_wedge.png ^
  -crop 6x2+197+39 +repage ^
  txt: 
# ImageMagick pixel enumeration: 6,2,65535,gray
0,0: (32357,32357,32357)  #7E657E657E65  gray(49.3736%)
1,0: (32521,32521,32521)  #7F097F097F09  gray(49.6239%)
2,0: (32685,32685,32685)  #7FAD7FAD7FAD  gray(49.8741%)
3,0: (32850,32850,32850)  #805280528052  gray(50.1259%)
4,0: (33014,33014,33014)  #80F680F680F6  gray(50.3761%)
5,0: (33178,33178,33178)  #819A819A819A  gray(50.6264%)
0,1: (33178,33178,33178)  #819A819A819A  gray(50.6264%)
1,1: (33014,33014,33014)  #80F680F680F6  gray(50.3761%)
2,1: (32850,32850,32850)  #805280528052  gray(50.1259%)
3,1: (32685,32685,32685)  #7FAD7FAD7FAD  gray(49.8741%)
4,1: (32521,32521,32521)  #7F097F097F09  gray(49.6239%)
5,1: (32357,32357,32357)  #7E657E657E65  gray(49.3736%)

We can see that each value in the upper half is different to the corresponding value in the lower half. However, at their closest, they are as close as adjacent values in either side.

%IM%convert ^
  ca_wedge.png ^
  -canny 0x20+5%%+30%% ^
  ca_w1.png
ca_w1.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x4+5%%+30%% ^
  ca_w1a.png
ca_w1a.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x2+5%%+30%% ^
  ca_w1b.png
ca_w1b.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x2+1%%+30%% ^
  ca_w2.png
ca_w2.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x2+0.8%%+30%% ^
  ca_w3.png
ca_w3.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x2+0%%+30%% ^
  ca_w4.png
ca_w4.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x1+1%%+30%% ^
  ca_w5.png
ca_w5.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+1%%+30%% ^
  ca_w5.png
ca_w5.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+0.5%%+30%% ^
  ca_w6.png
ca_w6.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+0.4%%+30%% ^
  ca_w7.png
ca_w7.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+0.3%%+30%% ^
  ca_w8.png
ca_w8.png

When the low threshold is set very low, spurious edges are found within each wedge. Even so, the central point still isn't closed.

%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+0.2%%+30%% ^
  ca_w9.png
ca_w9.png
%IM%convert ^
  ca_wedge.png ^
  -canny 0x0+0.01%%+30%% ^
  ca_w10.png
ca_w10.png
%IM%convert ^
  ca_wedge.png ^
  -edge 1 ^
  ca_e1.png
ca_e1.png
call %PICTBAT%twoBlrDiff ca_wedge.png
ca_wedge_2bd.png
%IM%convert ^
  ca_wedge_2bd.png ^
  -blur 0x4 ^
  ca_2bdb.png
ca_2bdb.png

Segmenting

We can segment an image with the Canny edge detector.

A source image.

set WEB_SIZE=-resize 600x600

%IM%convert dt_src.tiff ca_src.tiff

%IM%convert ^
  ca_src.tiff ^
  -crop 600x600+2244+2789 +repage ^
  ca_src_cr.png

set CA_SRC=ca_src_cr.png
ca_src_cr.pngjpg

Try in various colorspaces.

for /F "usebackq" %%C in (`%IM%convert -list colorspace`) do (
  %IM%convert ^
    %CA_SRC% ^
    -colorspace %%C ^
    -separate ^
    -set colorspace sRGB ^
    -auto-level ^
    -canny 0x4+5%%+30%% ^
    -evaluate-sequence Max ^
    -background Blue -fill Yellow ^
    -pointsize 20 -gravity SouthEast ^
    -annotate 0 " %%C   \n" ^
    ca_cs_%%C.png
)

Some of these:

ca_cs_Lab.png ca_cs_LCH.png ca_cs_LCHuv.png ca_cs_OHTA.png ca_cs_YCC.png ca_cs_YIQ.png

We can combine edges from multiple colorspaces.

%IM%convert ^
  %CA_SRC% ^
  ( -clone 0 -colorspace Lab -separate ) ^
  ( -clone 0 -colorspace YIQ -separate ) ^
  ( -clone 0 -colorspace YCC -separate ) ^
  -delete 0 ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x4+5%%+30%% ^
  -evaluate-sequence Max ^
  ca_cs_LabYIQ.png
)
ca_cs_LabYIQ.png

Four Canny edges:

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -separate ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x4+5%%+30%% ^
  -write ca_xlab.png ^
  -evaluate-sequence Max ^
  -write ca_xlabf.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -separate ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x10+5%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf2.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab2.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -separate ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x2+20%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf2a.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab2a.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -separate ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x2+0.01%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf0.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab0.png
ca_lab.png ca_lab2.png ca_lab2a.png ca_lab0.png

The same four Canny edges, but RMS of channels instead of separating:

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -grayscale RMS ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x4+5%%+30%% ^
  -write ca_xlabG.png ^
  -evaluate-sequence Max ^
  -write ca_xlabfG.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_labG.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -grayscale RMS ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x10+5%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf2G.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab2G.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -grayscale RMS ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x2+20%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf2aG.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab2aG.png

%IM%convert ^
  %CA_SRC% ^
  -colorspace LAB ^
  -separate ^
  -set colorspace sRGB ^
  -auto-level ^
  -canny 0x2+0.01%%+30%% ^
  -evaluate-sequence Max ^
  -write ca_xlabf0G.png ^
  %WEB_SIZE% ^
  -auto-level ^
  ca_lab0G.png
ca_labG.png ca_lab2G.png ca_lab2aG.png ca_lab0G.png

Blah ...

call %PICTBAT%twoBlrDiff %CA_SRC%

%IM%convert ^
  ca_src_cr_2bd.png ^
  -blur 0x8 ^
  -auto-level ^
  ca_src_cr_2bd.png

%IM%convert ^
  ca_src_cr_2bd.png ^
  -morphology Erode Diamond ^
  -morphology Thinning:-1 Skeleton ^
  -morphology Thinning:-1 LineEnds ^
  ca_skel.png
ca_src_cr_2bd.pngjpg ca_skel.png

See Morphology: Smooth.

Increasing the disk size closes gaps we want closed,
but also makes lines rougher.

Skeleton:3 would break lines I don't want broken,
so I use the default.

%IM%convert ^
  ca_cs_LabYIQ.png ^
  -morphology Close Disk:3.0 ^
  -write ca_cls.png ^
  -morphology Thinning:-1 Skeleton:3 ^
  -write ca_skel.png ^
  -morphology Thinning:-1 LineEnds ^
  -morphology Thinning:-1 Diagonals ^
  -morphology Thinning Corners ^
  -write ca_skel4.png ^
  -morphology Thinning LineEnds:4 ^
  ca_smth.png

%IM%convert ^
  %CA_SRC% ^
  ( ca_smth.png -alpha set -channel A -evaluate set 50%% +channel ) ^
  -composite ^
  ca_smtha.png
ca_cls.png ca_skel.png ca_skel4.png ca_smth.png ca_smtha.pngjpg

We can smooth the lines by (a) bluring, thresholding and repeating in IM or (b) using potrace. Blurring also shrinks loops, closing some of them.

%IM%convert ^
  ca_smth.png ^
  -blur 0x2 ^
  -fill #fff +opaque #000 ^
  -write ca_cls2.png ^
  -morphology Thinning:-1 Skeleton ^
  -write ca_skel2.png ^
  -morphology Thinning:-1 LineEnds ^
  -morphology Thinning:-1 Diagonals ^
  -morphology Thinning Corners ^
  -write ca_skel42.png ^
  -morphology Thinning LineEnds:4 ^
  ca_smth2.png

%IM%convert ^
  %CA_SRC% ^
  ( ca_smth2.png -alpha set -channel A -evaluate set 50%% +channel ) ^
  -composite ^
  ca_smtha2.png
ca_cls2.png ca_skel2.png ca_skel42.png ca_smth2.png ca_smtha2.pngjpg

Lines that previously hit an edge at an angle are now more perpendicular to the edge, sadly. Similarly, where three lines met at a T-junction, lines are now curved so they meet at 120°.

Using potrace:

call %PICTBAT%setInkPath

%IM%convert ^
  ca_smth.png ^
  ca_smth.pnm

%POTRACEDIR%potrace -r 90 -s -o ca_smth.svg ca_smth.pnm
del ca_smth.pnm


%IM%convert ^
  %CA_SRC% ^
  ( ^
    -density 90 -units PixelsPerInch ^
    ca_smth.svg ^
    -alpha set -channel A -evaluate set 50%% +channel ^
  ) ^
  -composite ^
  ca_svgo.png
ca_svgo.pngjpg

Using potrace to smooth lines, the angles between lines, and between each line and an edge, have been unchanged.

The segment image (white lines on black background) divides the source image into segments. Some attributes of each segment are:

For example, the script listSegments.bat lists the first three of those:

call %PICTBAT%listSegments ca_smth2.png ca_segs.lis

%IM%convert %lsTEMPIMG% ca_segs.png
0,0,50536,IsEdge 
0,523,23444,IsEdge 
352,0,18430,IsEdge 
577,0,189,IsEdge 
599,53,7219,IsEdge 
599,204,7825,IsEdge 
599,251,463,IsEdge 
599,298,2705,IsEdge 
599,454,13033,IsEdge 
599,548,51802,IsEdge 
93,599,677,IsEdge 
253,32,132324,NotEdge 
498,49,1656,NotEdge 
462,79,7931,NotEdge 
438,110,432,NotEdge 
428,136,5522,NotEdge 
563,153,346,NotEdge 
427,303,6006,NotEdge 
369,328,1,NotEdge 
370,329,1,NotEdge 
371,330,1,NotEdge 
359,343,804,NotEdge 
372,365,2,NotEdge 
391,365,1096,NotEdge 
82,371,20817,NotEdge 
462,372,570,NotEdge 
418,399,1822,NotEdge 
387,409,539,NotEdge 
ca_segs.png

The sum of the pixels in the segments will be less than the image size, as the white lines are not counted as in any segment.

More complex processing could be performed. For example, the average colours of each segment could be compared in order to correlate them. For this, reducing the image to just the hue can be useful. Real-world objects that are low in saturation (black, gray, white) are very good at picking up colours from surrounding objects (the surrounding objects reflect coloured light).

%IM%convert ^
  %CA_SRC% ^
  -colorspace HSL ^
  -channel GB -evaluate set 50%% ^
  -colorspace sRGB ^
  ca_justHue.png
ca_justHue.pngjpg

This does a fairly good job of distinguishing between skin, grass and "black" straps, and has correctly picked up the grass and skin colour on the bottom edge, near the left side. Sadly, the black strap in the palm of the hand has picked up colour from the skin.

For an alternative see GrowCut segmentation.

Selecting segments

IM doesn't (but really should) have a read mask, so finding the average colour of each segment is awkward. We could use methods from Inner Trim to crop a rectangle from each segment.

We can select only the segments that don't touch an image edge by specifying the colours to listSegments (black for the edge segments and white for the centre segments), then pruning lines.

set lsLEFT_COL=#f00
set lsTOP_COL=#f00
set lsRIGHT_COL=#f00
set lsBOTTOM_COL=#f00
set lsCENT_COL=#fff

call %PICTBAT%listSegments ca_smth2.png ca_segs2.lis

set lsLEFT_COL=
set lsTOP_COL=
set lsRIGHT_COL=
set lsBOTTOM_COL=
set lsCENT_COL=

%IM%convert ^
  %lsTEMPIMG% ^
  -fill #000 +opaque #fff ^
  -bordercolor #000 -border 1 ^
  -morphology Thinning:-1 LineEnds ^
  -morphology Thinning LineEnds:4 ^
  ca_segs2.png
ca_segs2.png

Feathering

We probably want to feather the edges before using this as a transparency mask.

call %PICTBAT%featherEdge ca_segs2.png 4 0

%IM%convert ^
  %CA_SRC% ^
  %feOUTFILE% ^
  -compose CopyOpacity -composite ^
  ca_ftr.png
ca_ftr.pngjpg

Scripts

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

listSegments.bat

rem From %1 a segment image file (white lines on black background)
rem lists segments to %2 CSV file.

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 ls

set INFILE=%1
set OUTCSV=%2

del %OUTCSV% >nul

set TEMPEXT=.miff
set TEMPDIR=%TEMP%
set TEMPIMG=%TEMPDIR%\%~n1_ls%TEMPEXT%

if "%lsLEFT_COL%"=="" set lsLEFT_COL=#088
if "%lsTOP_COL%"=="" set lsTOP_COL=#00f
if "%lsRIGHT_COL%"=="" set lsRIGHT_COL=#f0f
if "%lsBOTTOM_COL%"=="" set lsBOTTOM_COL=#0f0
if "%lsCENT_COL%"=="" set lsCENT_COL=#f00

%IM%convert %INFILE% -alpha off %TEMPIMG%

rem We find black pixels, and floodfill.

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


:loopLeft

set BLACK_X=

for /F "usebackq tokens=1-2 delims=, " %%X ^
in (`%IM%convert ^
  %TEMPIMG% ^
  -crop 1x%HH%+0+0 +repage ^
  +transparent #000 ^
  sparse-color:`) ^
do set BLACK_X=%%X& set BLACK_Y=%%Y

if not "%BLACK_X%"=="" (
  for /F "usebackq" %%L in (`%IM%convert ^
    %TEMPIMG% ^
    -fill %lsLEFT_COL% ^
    ^( +clone -draw "color %BLACK_X%,%BLACK_Y% floodfill" -write mpr:FILLED ^) ^
    -metric AE -compare -format "NUM_PIX=%%[distortion]" -write info: ^
    -delete 0 ^
    mpr:FILLED ^
    %TEMPIMG%`) do set %%L

  echo NUM_PIX=!NUM_PIX!

  echo %BLACK_X%,%BLACK_Y%,!NUM_PIX!,IsEdge >>%OUTCSV%

  goto loopLeft
)


:loopTop

set BLACK_X=

for /F "usebackq tokens=1-2 delims=, " %%X ^
in (`%IM%convert ^
  %TEMPIMG% ^
  -crop %WW%x1+0+0 +repage ^
  +transparent #000 ^
  sparse-color:`) ^
do set BLACK_X=%%X& set BLACK_Y=%%Y

if not "%BLACK_X%"=="" (
  for /F "usebackq" %%L in (`%IM%convert ^
    %TEMPIMG% ^
    -fill %lsTOP_COL% ^
    ^( +clone -draw "color %BLACK_X%,%BLACK_Y% floodfill" -write mpr:FILLED ^) ^
    -metric AE -compare -format "NUM_PIX=%%[distortion]" -write info: ^
    -delete 0 ^
    mpr:FILLED ^
    %TEMPIMG%`) do set %%L

  echo %BLACK_X%,%BLACK_Y%,!NUM_PIX!,IsEdge >>%OUTCSV%

  goto loopTop
)


:loopRight

set BLACK_X=

for /F "usebackq tokens=1-2 delims=, " %%X ^
in (`%IM%convert ^
  %TEMPIMG% ^
  -crop 1x%HH%+%Wm1%+0 +repage ^
  +transparent #000 ^
  sparse-color:`) ^
do set BLACK_X=%%X& set BLACK_Y=%%Y

if not "%BLACK_X%"=="" (
  set /A BLACK_X+=%Wm1%

  for /F "usebackq" %%L in (`%IM%convert ^
    %TEMPIMG% ^
    -fill %lsRIGHT_COL% ^
    ^( +clone -draw "color !BLACK_X!,%BLACK_Y% floodfill" -write mpr:FILLED ^) ^
    -metric AE -compare -format "NUM_PIX=%%[distortion]" -write info: ^
    -delete 0 ^
    mpr:FILLED ^
    %TEMPIMG%`) do set %%L

  echo NUM_PIX=!NUM_PIX!

  echo !BLACK_X!,%BLACK_Y%,!NUM_PIX!,IsEdge >>%OUTCSV%

  goto loopRight
)


:loopBottom

set BLACK_X=

for /F "usebackq tokens=1-2 delims=, " %%X ^
in (`%IM%convert ^
  %TEMPIMG% ^
  -crop %WW%x1+0+%Hm1% +repage ^
  +transparent #000 ^
  sparse-color:`) ^
do set BLACK_X=%%X& set BLACK_Y=%%Y

if not "%BLACK_X%"=="" (
  set /A BLACK_Y+=%Hm1%

  for /F "usebackq" %%L in (`%IM%convert ^
    %TEMPIMG% ^
    -fill %lsBOTTOM_COL% ^
    ^( +clone -draw "color %BLACK_X%,!BLACK_Y! floodfill" -write mpr:FILLED ^) ^
    -metric AE -compare -format "NUM_PIX=%%[distortion]" -write info: ^
    -delete 0 ^
    mpr:FILLED ^
    %TEMPIMG%`) do set %%L

  echo NUM_PIX=!NUM_PIX!

  echo %BLACK_X%,!BLACK_Y!,!NUM_PIX!,IsEdge >>%OUTCSV%

  goto loopBottom
)


:loopCent

set BLACK_X=

for /F "usebackq tokens=1-2 delims=, " %%X ^
in (`%IM%convert ^
  %TEMPIMG% ^
  +transparent #000 ^
  sparse-color:`) ^
do set BLACK_X=%%X& set BLACK_Y=%%Y

if not "%BLACK_X%"=="" (
  for /F "usebackq" %%L in (`%IM%convert ^
    %TEMPIMG% ^
    -fill %lsCENT_COL% ^
    ^( +clone -draw "color %BLACK_X%,%BLACK_Y% floodfill" -write mpr:FILLED ^) ^
    -metric AE -compare -format "NUM_PIX=%%[distortion]" -write info: ^
    -delete 0 ^
    mpr:FILLED ^
    %TEMPIMG%`) do set %%L

  echo %BLACK_X%,%BLACK_Y%,!NUM_PIX!,NotEdge >>%OUTCSV%

  goto loopCent
)


type %OUTCSV%

call @echoRestore

@endlocal & set lsTEMPIMG=%TEMPIMG%

featherEdge.bat

rem From image %1, assumed white image on black background,
rem feathers the edge %2 pixels inside and %3 pixels outside.
rem Optional %4 is output file.

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

@setlocal enabledelayedexpansion

@call echoOffSave

call %PICTBAT%setInOut %1 fe

if not "%4"=="" set OUTFILE=%4


set PIX_IN=%2
if "%PIX_IN%"=="" set PIX_IN=5

set PIX_OUT=%3
if "%PIX_OUT%"=="" set PIX_OUT=5

set BOTH=0
if not %PIX_OUT%==0 if not %PIX_IN%==0 set BOTH=1

set DIST_KNL=Euclidean:7

if %BOTH%==1 (
  set /A NUM_IN=%PIX_OUT%+%PIX_IN%

  %IM%convert ^
    %INFILE% ^
    -negate ^
      -morphology Distance "%DIST_KNL%,%PIX_OUT%^!" ^
      -fill #000 +opaque #fff ^
      -negate ^
    -morphology Distance "%DIST_KNL%,!NUM_IN!^!" ^
    %OUTFILE%
) else if not %PIX_OUT%==0 (
  %IM%convert ^
    %INFILE% ^
    -negate ^
      -morphology Distance "%DIST_KNL%,%PIX_OUT%^!" ^
      -negate ^
    %OUTFILE%
) else if not %PIX_IN%==0 (
  %IM%convert ^
    %INFILE% ^
    -morphology Distance "%DIST_KNL%,%PIX_IN%^!" ^
    %OUTFILE%
) else (
  %IM%convert ^
    %INFILE% ^
    %OUTFILE%
)


@call echoRestore

endlocal & set feOUTFILE=%OUTFILE%

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

%IM%identify -version
Version: ImageMagick 6.9.2-5 Q16 x64 2015-10-31 http://www.imagemagick.org
Copyright: Copyright (C) 1999-2015 ImageMagick Studio LLC
License: http://www.imagemagick.org/script/license.php
Visual C++: 180031101
Features: Cipher DPC Modules OpenMP 
Delegates (built-in): bzlib cairo freetype jng jp2 jpeg lcms lqr openexr 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 canny.h1. To re-create this web page, check that Inkscape is on the path, then execute "procH1 canny".


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 24-November-2014.

Page created 20-Jul-2016 21:07:23.

Copyright © 2016 Alan Gibson.