Contents
Introduction
There is hardly any introduction to computer vision course that does not cover edge detection. The concept of using gradients to detect edges is extremely simple and light weight from a computational perspective. Classical edge detection algorithms, especially the one presented here, are certainly not state of the art when it comes to super robust edge detection. They are simply too dependent on choosing parameters compared to a data-driven approach. It doesn’t mean that they are completely pointless but their useful fields of application is a lot smaller than e.g. in the 1990s or early 2000s and 2010s.
Using OpenCV with Python has some drawbacks. The lack of strong typing and a lack of proper documentation can cause quite a few headaches as some of the algorithms used for edge detection require specific inputs. Such an issue is related to finding edges on binary masks. To many it might be a rare application of an edge detector and there certainly are other approaches but it is great to highlight one important thing. Usually inputs are 8 bit single channel for edge detection. Edge detection is likely to fail if a binary mask consists of values {0,1} as it assumes values of interval [0,255] meaning that a binary image needs to be restricted to values {0,255}.
Canny Edge Detection
Applying Canny edge detection is most likely the simplest and most robust thing we could do using OpenCV to detect edges in an image.
cv::Canny
can be used in two ways. One approach requires a pre-processed input image, min/max thresholds and parameters for the Sobel operator. This approach performs calculating gradients itself. The second approach requires derivatives (gradients) of each of the two axes (x,y) instead of requiring an input image. This is a practical approach of other gradients (e.g. Scharr or custom approaches) are used instead of Sobel gradients or in case the gradient are calculated with anisotropic settings.
In the first case a simple example would look like this.
# Python
img = cv2.imread('./lena.jpg')
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
edges_bgr = cv2.cvtColor(cv2.Canny(img, 100, 150, 3, False),cv2.COLOR_GRAY2BGR)
edges_gray = cv2.cvtColor(cv2.Canny(img_gray, 100, 150, 3, False),cv2.COLOR_GRAY2BGR)
cv2.putText(edges_bgr, "BGR", (40,40), cv2.FONT_HERSHEY_SIMPLEX, 1 , (0,0,255))
cv2.putText(edges_gray, "GRAY", (40,40), cv2.FONT_HERSHEY_SIMPLEX, 1 , (0,0,255))
img_gray = cv2.cvtColor(img_gray,cv2.COLOR_GRAY2BGR)
cv2.imwrite('./canny_edge_bgr_gray_comparison.jpg', cv2.vconcat((cv2.hconcat((img,\
edges_bgr)),cv2.hconcat((img_gray, edges_gray)))))
NB!: Canny edge detection can be applied on both grayscale as well as BGR images. Common examples concentrate on grayscale images as these are less noisy. It does not mean that applying Canny edge detection to grayscale images results better line recovery than BGR images. A rule of thumb might be that BGR images lead to more lines detected (true positives and false positives) whereas grayscale input images usually show less lines recovered when using the exact same settings:

Smoothing the image before calculating gradients, e.g. a applying Gaussian blur filter, usually reduces clutter and improves the quality of line detection.
The second way to use cv::Canny
is by providing gradients for both axes directly. This requires the gradients to be of singed 16 Bit integers instead of the usual unsigned 8 Bit integers.
# Python
# dx,dy are outputs of e.g. directional Sobel function applied
edges_br = cv2.Canny(dx, dy, 100, 150, False)
Comment on Gradients
A fundamental idea of “classical edge detection” is that any edge (or line) can be identified by a change of color which is the derivative or gradient. cv::Laplacian
, cv::Sobel
and cv::Scharr
are common functions to calculate gradients. Laplacian is not a ‘per axis’ calculation and therefore a bit trickier to use. Instead of calling cv::Sobel
twice, cv::spatialGradient
could be used to calculate and output two arrays for both axes (x,y,).
However, this classical approach has its disadvantages and certainly was very useful in the past with extremely limited amount of compute power available. A fundamental downside of this approach is that there are thresholds involved and they kind of cause success or failure. Unless compute power is extremely restricted and the overall setting is super simple, it often makes little to use a non deep learning based approach to detect e.g. lines and such more modern approaches allow some form of “end to end” approach compared to a step wise approach that is based on edges detected but these need to be processed as well to provide some useful output.