Filtering Images with Convolution Masks in JavaScript

The JavaScript code below runs a given image through a few different filters and displays the results to the screen.

To see it in action, copy the code to an .html file, copy an image named “Test.png” to the same directory, and open the .html file in a web browser that runs JavaScript.  Note that if you’re not running it through a web server, it may be necessary to disable some security settings on the web browser.

ImageFilters

<html>
<body>

<script type='text/javascript'>

// main

function ImageFilterTest()
{
	this.main = function()
	{
		var filePathOfImageToFilter = "Test.png";
		var elementImgToFilter = document.createElement("img");
		elementImgToFilter.onload = this.main2;
		elementImgToFilter.src = filePathOfImageToFilter;
	}

	this.main2 = function(event)
	{
		var elementImgToFilter = event.srcElement;

		var elementTableRow = document.createElement("tr");

		var elementTableCell = document.createElement("td");
		var elementForOriginalLabel = document.createElement("p");
		elementForOriginalLabel.innerHTML = "Original:";
		elementTableCell.appendChild(elementForOriginalLabel);
		elementTableCell.appendChild(elementImgToFilter);
		elementTableRow.appendChild(elementTableCell);

		var filterBlur = new Filter
		(
			"Blur",	
			// convolutionMasks
			[
				[
					[.1, .1, .1],
					[.1, .2, .1],
					[.1, .1, .1],
				]
			]
		);

		var filterEdgeDetectSobel = new Filter
		(
			"Sobel Edge Detect",
			// convolutionMasks
			[
				[
					[1, 0, -1],
					[2, 0, -2],
					[1, 0, -1],
				],
				[
					[1, 2, 1],
					[0, 0, 0],
					[-1, -2, -1],
				],
			]

		);

		var filterNone = new Filter
		(
			"None",	
			// convolutionMasks
			[
				[
					[0, 0, 0],
					[0, 1, 0],
					[0, 0, 0],
				]
			]
		);

		var filtersToApply = 
		[
			filterNone,
			filterBlur,
			filterEdgeDetectSobel,
		];

		for (var i = 0; i < filtersToApply.length; i++)
		{
			var filter = filtersToApply[i];

			var elementImgFiltered = filter.filterImage
			(
				elementImgToFilter
			);

			var elementTableCell = document.createElement("td");

			var elementForFilterName = document.createElement("p");
			elementForFilterName.innerHTML = filter.name + ":";
			elementTableCell.appendChild(elementForFilterName);
			elementTableCell.appendChild(elementImgFiltered);

			elementTableRow.appendChild(elementTableCell);
		}

		var elementTable = document.createElement("table");
		elementTable.appendChild(elementTableRow);
		document.body.appendChild(elementTable);
	}
}

// classes

function Color(red, green, blue)
{
	this.red = red;
	this.green = green;
	this.blue = blue;
}
{
	// static methods

	Color.fromSystemColor = function(systemColor)
	{
		var returnValue = new Color
		(
			systemColor[0],
			systemColor[1],
			systemColor[2]
		);

		return returnValue;
	}

	// instance methods

	Color.prototype.add = function(other)
	{
		this.red += other.red;
		this.green += other.green;
		this.blue += other.blue;

		return this;
	}

	Color.prototype.multiplyScalar = function(scalar)
	{
		this.red *= scalar;
		this.green *= scalar;
		this.blue *= scalar;

		return this;
	}

	Color.prototype.toSystemColor = function()
	{
		var returnValue = 
			"rgb("
			+ Math.floor(this.red) + ","
			+ Math.floor(this.green) + "," 
			+ Math.floor(this.blue)
			+ ")";

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;

		return this;
	}

	Coords.prototype.isWithinRange = function(range)
	{
		var returnValue = 
		(
			this.x >= 0 
			&& this.x <= range.x
			&& this.y >= 0
			&& this.y <= range.y
		);

		return returnValue;
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;

		return this;
	}
}

function Filter(name, convolutionMasks)
{
	this.name = name;
	this.convolutionMasks = convolutionMasks;
}
{
	Filter.prototype.filterImage = function(imageToFilter)
	{
		for (var i = 0; i < this.convolutionMasks.length; i++)
		{
			var mask = this.convolutionMasks[i];

			imageToFilter = this.filterImage_Mask(imageToFilter, mask);
		}

		return imageToFilter;
	}

	Filter.prototype.filterImage_Mask = function(imageToFilter, convolutionMask)
	{
		var imageSizeInPixels = new Coords
		(
			imageToFilter.width,
			imageToFilter.height
		);

		var canvasSource = document.createElement("canvas");
		canvasSource.width = imageSizeInPixels.x;
		canvasSource.height = imageSizeInPixels.y;
		var graphicsSource = canvasSource.getContext("2d");
		graphicsSource.drawImage
		(
			imageToFilter, 
			0, 0, 
			imageSizeInPixels.x, 
			imageSizeInPixels.y
		);

		var canvasOutput = document.createElement("canvas");
		canvasOutput.width = imageToFilter.width;
		canvasOutput.height = imageToFilter.height;
		var graphicsOutput = canvasOutput.getContext("2d");

		var imageSizeInPixelsMinusOnes = imageSizeInPixels.clone().subtract
		(
			new Coords(1, 1)
		);

		var maskSizeInPixels = new Coords
		(
			convolutionMask[0].length, 
			convolutionMask.length
		);

		var maskSizeInPixelsMinusOnesHalved = maskSizeInPixels.clone().subtract
		(
			new Coords(1, 1)
		).divideScalar
		(
			2
		);

		var pixelPosToModify = new Coords(0, 0);
		var maskPixelOffset = new Coords(0, 0);
		var pixelPosCurrent = new Coords(0, 0);

		for (var y = 0; y < imageSizeInPixels.y; y++)
		{
			pixelPosToModify.y = y;

			for (var x = 0; x < imageSizeInPixels.x; x++)
			{
				pixelPosToModify.x = x;

				var colorSumOfMaskPixelsSoFar = new Color(0, 0, 0);

				for (var j = 0; j < maskSizeInPixels.y; j++)
				{
					maskPixelOffset.y = j - maskSizeInPixelsMinusOnesHalved.y;

					for (var i = 0; i < maskSizeInPixels.x; i++)
					{
						maskPixelOffset.x = i - maskSizeInPixelsMinusOnesHalved.x;

						var maskPixelMultiplier = convolutionMask[j][i];

						pixelPosCurrent.overwriteWith
						(
							pixelPosToModify
						).add
						(
							maskPixelOffset
						);

						if (pixelPosCurrent.isWithinRange(imageSizeInPixelsMinusOnes) == true)
						{
							var systemColorAtPixelPosCurrent = graphicsSource.getImageData
							(
								pixelPosCurrent.x, pixelPosCurrent.y,
								1, 1
							).data;

							var colorAtPixelPosCurrent = Color.fromSystemColor
							(
								systemColorAtPixelPosCurrent
							);

							colorSumOfMaskPixelsSoFar.add
							(
								colorAtPixelPosCurrent.multiplyScalar
								(
									maskPixelMultiplier
								)
							);
						}
					}
				}

				graphicsOutput.fillStyle = colorSumOfMaskPixelsSoFar.toSystemColor();

				graphicsOutput.fillRect
				(
					x, y, 1, 1 					
				);
			}
		}

		var returnValue = document.createElement("img");
		returnValue.src = canvasOutput.toDataURL("image/png");

		return returnValue;
	}
}


// run
new ImageFilterTest().main();

</script>
</body>
</html>
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s