I found this site and this site both really helpful for actually implementing the Canny Edge Detetor. Definitely refer to both of them for more examples and pictures.
I did use Python/NumPy for generating the Gaussian Filter kernel files that were needed for the ArcGIS Focal Statistics function. The following code will generate a properly formatted kernel file based on the kernel size and sigma value.
import os import numpy as np def gaussian_kernel(workspace, size=5, sigma=1.4): sigma_str = str(round(sigma,1)).replace('.', 'p') file_name = 'gaussian_{0}_{1}.txt'.format(size, sigma_str) size = int(size) i,j = np.mgrid[0:size,0:size] i -= (size/2) j -= (size/2) g = np.exp(-(i**2+j**2)/(2*sigma**2)) g /= g.sum() f = open(os.path.join(workspace, file_name), 'wb') f.write('{0} {0}\n'.format(size)) for row in g: print ' {0}'.format(' '.join(['{0:0.6f}'.format(x) for x in row])) f.write('{0}\n'.format(' '.join(['{0:0.6f}'.format(x) for x in row]))) f.close()I then had to build kernel files for the Sobel Operators.
sobel_x.txt | sobel_y.txt |
---|---|
3 3 |
3 3 |
d0.txt | d1.txt | d2.txt | d3.txt |
---|---|---|---|
3 3 |
3 3 |
3 3 |
3 3 |
temp_obj.save(temp_raster)
calls. I didn't include all of my input checking and some names/values are hardwired.import os import arcpy from arcpy import env import arcpy.sa as sa import numpy as np def canny_edge(workspace, raster_name, filter_name, t_low, t_high): #### Set ArcGIS environment parameters arcpy.CheckOutExtension("Spatial") env.overwriteOutput = True env.workspace = workspace env.scratchWorkspace = workspace env.cellSize = raster_path env.extent = raster_path env.snapRaster = raster_path #### Hardwired file names for kernels sobel_x_path = os.path.join(workspace, 'sobel_x.txt') sobel_y_path = os.path.join(workspace, 'sobel_y.txt') d0_path = os.path.join(workspace, 'd0.txt') d1_path = os.path.join(workspace, 'd1.txt') d2_path = os.path.join(workspace, 'd2.txt') d3_path = os.path.join(workspace, 'd3.txt') d4_path = os.path.join(workspace, 'd4.txt') d5_path = os.path.join(workspace, 'd5.txt') d6_path = os.path.join(workspace, 'd6.txt') d7_path = os.path.join(workspace, 'd7.txt') #### Canny Edge Detector Workflow #### Apply gaussian filter filter_obj = sa.FocalStatistics( raster_path, sa.NbrWeight(filter_path), "SUM", "NODATA") #### Calculate 1st derivative in horizontal and vertical direction grad_x_obj = sa.FocalStatistics( filter_obj, sa.NbrWeight(sobel_x_path), "SUM", "DATA") grad_y_obj = sa.FocalStatistics( filter_obj, sa.NbrWeight(sobel_y_path), "SUM", "DATA") del filter_obj #### Calculate gradient and direction grad_obj = sa.SquareRoot( sa.Power(grad_x_obj,2) + sa.Power(grad_y_obj,2)) angle_obj = sa.ATan2(grad_y_obj, grad_x_obj) del grad_x_obj, grad_y_obj #### Reclassify angle into four directions #### See http://suraj.lums.edu.pk/~cs436a02/CannyImplementation.htm reclass_remap = sa.RemapRange( [[-3.141593, -2.748894, 0], [-2.748894, -1.963495, 1], [-1.963495, -1.178097, 2], [-1.178097, -0.392699, 3], [-0.392699, 0.392699, 0], [ 0.392699, 1.178097, 1], [ 1.178097, 1.963495, 2], [ 1.963495, 2.748894, 3], [ 2.748894, 3.141593, 0]]) zone_obj = sa.Reclassify( angle_obj, "Value", reclass_remap, "NODATA") del angle_obj, reclass_remap #### Non-maxima suppresion #### Shift raster 1 cell in each of the 8 possible directions #### This will get neighboring cells for non-maxima suppression #### Keep cells that are greater than the two neighboring cells #### along the gradient direction #### Calculate separate raster for each primary direction then sum d0_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d0_path), "SUM", "DATA") d4_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d4_path), "SUM", "DATA") grad_04_obj = sa.Con( (zone_obj == 0) & (grad_obj > d0_obj) & (grad_obj > d4_obj), grad_obj, 0) del d0_obj, d4_obj d1_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d1_path), "SUM", "DATA") d5_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d5_path), "SUM", "DATA") grad_15_obj = sa.Con( (zone_obj == 1) & (grad_obj > d1_obj) & (grad_obj > d5_obj), grad_obj, 0) del d1_obj, d5_obj d2_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d2_path), "SUM", "DATA") d6_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d6_path), "SUM", "DATA") grad_26_obj = sa.Con( (zone_obj == 2) & (grad_obj > d2_obj) & (grad_obj > d6_obj), grad_obj, 0) del d2_obj, d6_obj d3_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d3_path), "SUM", "DATA") d7_obj = sa.FocalStatistics( grad_obj, sa.NbrWeight(d7_path), "SUM", "DATA") grad_37_obj = sa.Con( (zone_obj == 3) & (grad_obj > d3_obj) & (grad_obj > d7_obj), grad_obj, 0) del d3_obj, d7_obj del grad_obj, zone_obj print "Sum Directions" nms_obj = (grad_04_obj + grad_15_obj + grad_26_obj + grad_37_obj) del grad_04_obj, grad_15_obj, grad_26_obj, grad_37_obj nms_obj.save(raster_path.replace('.img', '_nms.img')) #### Hysteresis Thresholding t_low_obj = sa.RegionGroup( sa.Con(nms_obj > t_low, 1), "EIGHT", "CROSS", "NO_LINK") t_zs_obj = sa.ZonalStatistics( t_low_obj, "value", nms_obj, "MAXIMUM", "DATA") del t_low_obj t_high_obj = sa.Con((t_zs_obj >= t_high), nms_obj) del t_zs_obj, nms_obj t_high_obj.save(raster_path.replace('.img', '_edges.img'))There are probably issues at the edges of the rasters because I noticed that the Focal Statistics was not properly handling nodata using the weighting option.