Image Processing in p5.js


Edge Detection

Edge detection is an image processing technique for finding the boundaries of objects within images and it's implementation is a type of Kernel Convolution. It consists of trying to find the regions where the image has a sharp change in intensity. A high value indicates a steep change and a low value indicates shallow change. A very common way to perform this is by using the Sobel Operator. In this tutorial we will explain the logic behind Detecting Edges in an image.

Budapest Hotel Budapest Hotel Sobel


For a more detailed explanation of what Kernel Convolution is, read our Blur Logic tutorial. We highly recommend that you have a look at it before moving forward!

  1. Logic Behind Edge Detection
  2. X-Direction Kernel
  3. Y-Direction Kernel
  4. Sobel Operator

Logic Behind Edge Detection

The basic idea is to approximate the change in light intensity. We do this by comparing the value of pixels on the right side and left side (x-direction kernel), followed by comparing the upper side and lower side (y-direction kernel). We use two 3x3 kernels, one for each x and y direction.

The change in light intensity is the gradient magnitude of the edge, which can be computed by the following formula:

| G | = sqrt ( (Gx*Gx) + (Gy*Gy) )

Gx = Gradient in x-direction

Gy = Gradient in y-direction


X-Direction Kernel

The X-Direction Kernel will compute the vertically oriented edges. We preserve the center pixels and try to find the difference between the left and right regions. The kernel for x-direction has negative numbers on the left side and positive numbers on the right side.

Visual Gx Visual


Our program will multiply the current pixel and its neighbors by the numbers on the matrix and add them. This will be repeated for every pixel on the canvas, consequently checking for all vertical edges the image contains. For the sake of simplicity, on the example below we only use the green channel of each pixel and set each channel to the calculated sum. The sum could result in either negative or positive value. We stretch those results to values between 0 and 255 using the map() method and just output it into a greyscale image.

Budapest Hotel Budapest Hotel Sobel



var srcimg, dstimg;

function preload() {
  srcimg = loadImage(imglist.bosch);
}

function setup() {
  createCanvas(srcimg.width, srcimg.height);
	pixelDensity(1);
	dstimg = createImage(srcimg.width, srcimg.height);
}

function draw() {
	// X-Direction Kernel
	var k1 = [[-1, 0, 1],
		  [-2, 0, 2],
		  [-1, 0, 1]];
	
	srcimg.loadPixels();
	dstimg.loadPixels();
	
	var w = srcimg.width;
	var h = srcimg.height;
	for (var x = 0; x < w; x++) {
    	for (var y = 0; y < h; y++) {
		
			// INDEX POSITION IN PIXEL LIST
			var ul = ((x-1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER LEFT
			var uc = ((x-0+w)%w + w*((y-1+h)%h))*4; // location of the UPPER MID
			var ur = ((x+1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER RIGHT
			var ml = ((x-1+w)%w + w*((y+0+h)%h))*4; // location of the LEFT
			var mc = ((x-0+w)%w + w*((y+0+h)%h))*4; // location of the CENTER PIXEL
			var mr = ((x+1+w)%w + w*((y+0+h)%h))*4; // location of the RIGHT
			var ll = ((x-1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER LEFT
			var lc = ((x-0+w)%w + w*((y+1+h)%h))*4; // location of the LOWER MID
			var lr = ((x+1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER RIGHT

			// green channel only
			var p0 = srcimg.pixels[ul+1]*k1[0][0]; // upper left
			var p1 = srcimg.pixels[uc+1]*k1[0][1]; // upper mid
			var p2 = srcimg.pixels[ur+1]*k1[0][2]; // upper right
			var p3 = srcimg.pixels[ml+1]*k1[1][0]; // left
			var p4 = srcimg.pixels[mc+1]*k1[1][1]; // center pixel
			var p5 = srcimg.pixels[mr+1]*k1[1][2]; // right
			var p6 = srcimg.pixels[ll+1]*k1[2][0]; // lower left
			var p7 = srcimg.pixels[lc+1]*k1[2][1]; // lower mid
			var p8 = srcimg.pixels[lr+1]*k1[2][2]; // lower right
			var r1 = p0+p1+p2+p3+p4+p5+p6+p7+p8;

			// -1000 is the minimum value the sum could result in and 1000 is the maximum
			var result = map(r1, -1000, 1000, 0, 255);
			
			// write pixels into destination image:
			dstimg.pixels[mc] = result; 
			dstimg.pixels[mc+1] = result; 
			dstimg.pixels[mc+2] = result; 
			dstimg.pixels[mc+3] = 255; 	
    	}
  	}	
	// update and display the pixel buffer
	dstimg.updatePixels();
	image(dstimg, 0, 0, dstimg.width, dstimg.height);
}

Another way of doing this would be to o use the function image.filter(GRAY) at the beginning and then perfom the same calculations for each channel (red, green, and blue) and writing the results into the proper channels on the destination image. In this case, we need ne image to be black and white in the beginning to make sure our edges are all the same instead of containing multiple colors.

Budapest Hotel Budapest Hotel Sobel


Y-Direction Kernel

The Y-Direction Kernel differs form the X-Direction Kernel in that it will compute the horizontally oriented edges. Again, we preserve the center pixels and try to find the difference between the upper and lower regions. The kernel for y-direction has negative numbers at the top and positive numbers at the bottom.

Visual Gy Visual


Our program will multiply the current pixel and its neighbors by the numbers on the matrix and add them. This will be repeated for every pixel on the canvas, consequently checking for all horizontal edges the image contains. For the sake of simplicity, on the example below we only use the green channel of each pixel and set each channel to the calculated sum. The sum could result in either negative or positive value. We stretch those results to values between 0 and 255 using the map() method and just output it into a greyscale image.

Budapest Hotel Budapest Hotel Sobel



var srcimg, dstimg;

function preload() {
  srcimg = loadImage(imglist.bosch);
}

function setup() {
  createCanvas(srcimg.width, srcimg.height);
	pixelDensity(1);
	dstimg = createImage(srcimg.width, srcimg.height);
}

function draw() {
	// Y-Direction Kernel
	var k2 = [[-1, -2, -1],
		   [0, 0, 0],
	           [1, 2, 1]];
	
	srcimg.loadPixels();
	dstimg.loadPixels();
	
	var w = srcimg.width;
	var h = srcimg.height;
	for (var x = 0; x < w; x++) {
    	for (var y = 0; y < h; y++) {

			// INDEX POSITION IN PIXEL LIST
			var ul = ((x-1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER LEFT
			var uc = ((x-0+w)%w + w*((y-1+h)%h))*4; // location of the UPPER MID
			var ur = ((x+1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER RIGHT
			var ml = ((x-1+w)%w + w*((y+0+h)%h))*4; // location of the LEFT
			var mc = ((x-0+w)%w + w*((y+0+h)%h))*4; // location of the CENTER PIXEL
			var mr = ((x+1+w)%w + w*((y+0+h)%h))*4; // location of the RIGHT
			var ll = ((x-1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER LEFT
			var lc = ((x-0+w)%w + w*((y+1+h)%h))*4; // location of the LOWER MID
			var lr = ((x+1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER RIGHT

			// green channel only
			var p0 = srcimg.pixels[ul+1]*k2[0][0]; // upper left
			var p1 = srcimg.pixels[uc+1]*k2[0][1]; // upper mid
			var p2 = srcimg.pixels[ur+1]*k2[0][2]; // upper right
			var p3 = srcimg.pixels[ml+1]*k2[1][0]; // left
			var p4 = srcimg.pixels[mc+1]*k2[1][1]; // center pixel
			var p5 = srcimg.pixels[mr+1]*k2[1][2]; // right
			var p6 = srcimg.pixels[ll+1]*k2[2][0]; // lower left
			var p7 = srcimg.pixels[lc+1]*k2[2][1]; // lower mid
			var p8 = srcimg.pixels[lr+1]*k2[2][2]; // lower right
			var r2 = p0+p1+p2+p3+p4+p5+p6+p7+p8;

			// -1000 is the minimum value the sum could result in and 1000 is the maximum
			var result = map(r2, -1000, 1000, 0, 255);

			// write pixels into destination image:
			dstimg.pixels[mc] = result; 
			dstimg.pixels[mc+1] = result; 
			dstimg.pixels[mc+2] = result; 
			dstimg.pixels[mc+3] = 255; 		
    	}
  	}	
	// update and display the pixel buffer
	dstimg.updatePixels(); 
	image(dstimg, 0, 0, dstimg.width, dstimg.height);
}

Another way of doing this would be to o use the function image.filter(GRAY) at the beginning and then perfom the same calculations for each channel (red, green, and blue) and writing the results into the proper channels on the destination image. In this case, we need ne image to be black and white in the beginning to make sure our edges are all the same instead of containing multiple colors.

Budapest Hotel Budapest Hotel Sobel


Sobel Operator

The Sobel Operator is going to check, each pixel for both horizontal and vertical edges, sum the results for x and y separately, and then take the square root of Gx squared + Gy squared. In this situation, all values will be positive, but for more accuracy we should still map the values so we get the correct insensity of each line in our image. If we don't map it, all of our edges will be opaque white.

| G | = map( sqrt (Gx*Gx+Gy*Gy), 0, 1414, 0, 255 );

Gx = Gradient in x-direction

Gy = Gradient in y-direction

0 = minumim possible result

1414 = maximum possible result


Budapest Hotel Budapest Hotel Sobel



// convolution kernel example

var srcimg, dstimg;

function preload() {
  srcimg = loadImage(imglist.budapest); // Load the image
}

function setup() {
  createCanvas(srcimg.width, srcimg.height);
	pixelDensity(1);
	dstimg = createImage(srcimg.width, srcimg.height);
}

function draw() {
	processImage(srcimg, dstimg);
	image(dstimg, 0, 0, dstimg.width, dstimg.height);
}

function processImage(_srcimg, _dstimg, _kernel)
{
	_srcimg.filter(GRAY);
	var k1 = [[-1, 0, 1],
							[-2, 0, 2],
							[-1, 0, 1]];
	var k2 = [[-1, -2, -1],
							[0, 0, 0],
							[1, 2, 1]];
	_srcimg.loadPixels(); // convert the entire canvas to a pixel buffer
	_dstimg.loadPixels(); // convert the entire canvas to a pixel buffer
	
	var w = _srcimg.width;
	var h = _srcimg.height;
	for (var x = 0; x < w; x++) {
    	for (var y = 0; y < h; y++) {
		
			// INDEX POSITION IN PIXEL LIST
			var ul = ((x-1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER LEFT
			var uc = ((x-0+w)%w + w*((y-1+h)%h))*4; // location of the UPPER MID
			var ur = ((x+1+w)%w + w*((y-1+h)%h))*4; // location of the UPPER RIGHT
			var ml = ((x-1+w)%w + w*((y+0+h)%h))*4; // location of the LEFT
			var mc = ((x-0+w)%w + w*((y+0+h)%h))*4; // location of the CENTER PIXEL
			var mr = ((x+1+w)%w + w*((y+0+h)%h))*4; // location of the RIGHT
			var ll = ((x-1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER LEFT
			var lc = ((x-0+w)%w + w*((y+1+h)%h))*4; // location of the LOWER MID
			var lr = ((x+1+w)%w + w*((y+1+h)%h))*4; // location of the LOWER RIGHT
			
			// green channel only
			var p0 = _srcimg.pixels[ul+1]*k1[0][0]; // upper left
			var p1 = _srcimg.pixels[uc+1]*k1[0][1]; // upper mid
			var p2 = _srcimg.pixels[ur+1]*k1[0][2]; // upper right
			var p3 = _srcimg.pixels[ml+1]*k1[1][0]; // left
			var p4 = _srcimg.pixels[mc+1]*k1[1][1]; // center pixel
			var p5 = _srcimg.pixels[mr+1]*k1[1][2]; // right
			var p6 = _srcimg.pixels[ll+1]*k1[2][0]; // lower left
			var p7 = _srcimg.pixels[lc+1]*k1[2][1]; // lower mid
			var p8 = _srcimg.pixels[lr+1]*k1[2][2]; // lower right
			var r1 = p0+p1+p2+p3+p4+p5+p6+p7+p8; 

			var p0 = _srcimg.pixels[ul+1]*k2[0][0]; // upper left
			var p1 = _srcimg.pixels[uc+1]*k2[0][1]; // upper mid
			var p2 = _srcimg.pixels[ur+1]*k2[0][2]; // upper right
			var p3 = _srcimg.pixels[ml+1]*k2[1][0]; // left
			var p4 = _srcimg.pixels[mc+1]*k2[1][1]; // center pixel
			var p5 = _srcimg.pixels[mr+1]*k2[1][2]; // right
			var p6 = _srcimg.pixels[ll+1]*k2[2][0]; // lower left
			var p7 = _srcimg.pixels[lc+1]*k2[2][1]; // lower mid
			var p8 = _srcimg.pixels[lr+1]*k2[2][2]; // lower right
			var r2 = p0+p1+p2+p3+p4+p5+p6+p7+p8; 

			// 0 is the minimum value the sum could result in and 1414 is the maximum
			var result = map(sqrt(r1*r1+r2*r2),0,1414,0,255);
			
			// write pixels into destination image:
			_dstimg.pixels[mc] = result; 
			_dstimg.pixels[mc+1] = result; 
			_dstimg.pixels[mc+2] = result; 
			_dstimg.pixels[mc+3] = 255; 
			
		}
	}	
	
	_dstimg.updatePixels(); // update and display the pixel buffer

}


Credit to Crystal Chen and Paolla Bruno Dutra. Thanks to R. DuBois Luke and Tega Brain at NYU for mentoring :)