Isolating Characters in an Image in JavaScript

The JavaScript program below first draws horizontal separators between lines of text in an imported image, and then draws separators between individual characters in each line. Along with a few other recent posts, it is intended as the beginnings of a system for optical character recognition. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.


<html>
<body>

<!-- ui -->

<div id="divUI">

	<label><b>Image Characterizer</b></label>
	<p>Load a file, specify a background color, and click the button to automatically split into lines and characters.</p>
	
	<div>
		<label>Image to Find Characters in:</label>
		<input type="file" onchange="inputImageToCharacterize_Changed(this);"></input>
		<div id="divDisplayImageToCharacterize">[none]</div>
	</div>

	<div>
		</div>
			<label>Background Color RGB:</label>
			<input id="inputColorBackground" value="255,255,255"></input>
		</div>
		<div>
			<label>Color Difference Threshold:</label>
			<input id="inputColorDifferenceThreshold" type="number" value="256"></input>
		</div>
		<button onclick="buttonImageCharacterize_Clicked();">Find Characters</button>
	</div>

	<div>
		<label>Image with Characters Highlighted:</label>
		<div id="divDisplayImageCharacterized">[none]</div>
	</div>

</div>

<!-- ui ends-->

<script type="text/javascript">

// ui events

function buttonImageCharacterize_Clicked()
{
	var divDisplayImageToCharacterize = 
		document.getElementById("divDisplayImageToCharacterize");

	var imageToCharacterizeAsCanvas = 
		divDisplayImageToCharacterize.getElementsByTagName("canvas")[0];

	if (imageToCharacterizeAsCanvas == null)
	{
		alert("No image loaded!");
		return;
	}

	var inputColorBackground = 
		document.getElementById("inputColorBackground");
	var inputColorDifferenceThreshold = 
		document.getElementById("inputColorDifferenceThreshold");

	var colorBackground = inputColorBackground.value;
	var colorDifferenceThreshold = inputColorDifferenceThreshold.value;

	var imageCharacterizedAsCanvas = new ImageAutoCharacterizer().findCharactersInCanvas
	(
		imageToCharacterizeAsCanvas,
		colorBackground,
		colorDifferenceThreshold
	);
	imageCharacterizedAsCanvas.style = "border:1px solid";

	var divDisplayImageCharacterized = 
		document.getElementById("divDisplayImageCharacterized");
	divDisplayImageCharacterized.innerHTML = "";
	divDisplayImageCharacterized.appendChild(imageCharacterizedAsCanvas);
}

function inputImageToCharacterize_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(eventFileLoaded)
	{
		var imageAsDataURL = eventFileLoaded.target.result;
		var imageAsDOMElement = document.createElement("img");
		imageAsDOMElement.onload = function(eventImageLoaded)
		{
			var imageAsCanvas = document.createElement("canvas");
			imageAsCanvas.style = "border:1px solid";
			imageAsCanvas.width = imageAsDOMElement.width;
			imageAsCanvas.height = imageAsDOMElement.height;

			var graphics = imageAsCanvas.getContext("2d");
			graphics.drawImage(imageAsDOMElement, 0, 0);

			imageAsCanvas.onmousedown = function(mouseEvent)
			{
				var x = mouseEvent.x;
				var y = mouseEvent.y;

				var pixelRGBA = graphics.getImageData
				(
					x, y, 1, 1
				).data;

				var pixelAsString = 
					+ pixelRGBA[0] + ","
					+ pixelRGBA[1] + ","
					+ pixelRGBA[2]

				var inputColorBackground = 
					document.getElementById("inputColorBackground");

				inputColorBackground.value = pixelAsString;
			}

			var divDisplayImageToCharacterize = document.getElementById
			(
				"divDisplayImageToCharacterize"
			);
			divDisplayImageToCharacterize.innerHTML = "";
			divDisplayImageToCharacterize.appendChild
			(
				imageAsCanvas
			);
		}
		imageAsDOMElement.src = imageAsDataURL;
	}
	fileReader.readAsDataURL(file);
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
}
{
	Bounds.prototype.size = function()
	{
		return this.max.clone().subtract(this.min);
	}
}

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

	Coords.prototype.dimension = function(dimensionIndex, valueToSet)
	{
		return (dimensionIndex == 0 ? this.x : this.y);
	}

	Coords.prototype.dimensionSet = function(dimensionIndex, valueToSet)
	{
		if (dimensionIndex == 0)
		{
			this.x = valueToSet;
		}
		else
		{
			this.y = valueToSet;
		}

		return this;
	}

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

function ImageAutoCharacterizer()
{
	// do nothing
}
{
	ImageAutoCharacterizer.prototype.findCharactersInCanvas = function
	(
		imageToFindCharactersInAsCanvas,
		colorBackground,
		colorDifferenceThreshold,
	)
	{
		// Draw the image itself.

		var imageCharacterizedAsCanvas = document.createElement("canvas");

		var imageSize = new Coords
		(
			imageToFindCharactersInAsCanvas.width,
			imageToFindCharactersInAsCanvas.height
		);

		imageCharacterizedAsCanvas.width = imageSize.x;
		imageCharacterizedAsCanvas.height = imageSize.y;

		graphics = imageCharacterizedAsCanvas.getContext("2d");
		graphics.drawImage
		(
			imageToFindCharactersInAsCanvas, 
			0, 0
		);

		// Locate the spaces between lines of text.

		var spacesBetweenLinesOfText = this.findSpacesInCanvasInBounds
		(
			imageToFindCharactersInAsCanvas,
			new Bounds(new Coords(0, 0), imageSize),
			colorBackground,
			colorDifferenceThreshold,
			1 // axisI = y
		);

		var lineGroups = 
		[
			spacesBetweenLinesOfText
		];

		for (var i = 0; i < spacesBetweenLinesOfText.length - 1; i++)
		{
			var lineTop = spacesBetweenLinesOfText[i];
			var lineBottom = spacesBetweenLinesOfText[i + 1];

			var spacesBetweenCharactersInLine = this.findSpacesInCanvasInBounds
			(
				imageToFindCharactersInAsCanvas,
				new Bounds(lineTop.fromPos, lineBottom.toPos),
				colorBackground,
				colorDifferenceThreshold,
				0 // axisI = x
			);

			lineGroups.push(spacesBetweenCharactersInLine);			
		}		

		// Draw all the lines.

		graphics.strokeStyle = "Cyan";

		for (var g = 0; g < lineGroups.length; g++)
		{
			var linesInGroup = lineGroups[g];

			for (var i = 0; i < linesInGroup.length; i++)
			{
				var line = linesInGroup[i];

				line.drawToGraphics(graphics);
			}
		}

		return imageCharacterizedAsCanvas;

	}

	ImageAutoCharacterizer.prototype.findSpacesInCanvasInBounds = function
	(	
		imageToSplitAsCanvas,
		bounds,
		colorBackground,
		colorDifferenceThreshold,
		axisI
	)
	{			
		var colorBackgroundRGB = colorBackground.split(",");

		var graphics = imageToSplitAsCanvas.getContext("2d");

		var imageToSplitSize = new Coords
		(
			imageToSplitAsCanvas.width,
			imageToSplitAsCanvas.height
		);

		var pixelPos = new Coords();

		var linesOpen = [];

		var axisJ = 1 - axisI;

		var min = bounds.min;
		var max = bounds.max;

		var iMin = min.dimension(axisI);
		var iMax = max.dimension(axisI);

		var jMin = min.dimension(axisJ);
		var jMax = max.dimension(axisJ);

		for (var i = iMin; i < iMax; i++)
		{
			pixelPos.dimensionSet(axisI, i);

			var isLineOpen = true;
				
			for (var direction = 0; direction < 2; direction++)
			{
				var jStart = (direction == 0 ? jMin : jMax - 1);
				var jEnd = (direction == 0 ? jMax : jMin - 1);
				var jStep = (direction == 0 ? 1 : -1);

				for (var j = jStart; j != jEnd; j += jStep)
				{
 					pixelPos.dimensionSet(axisJ, j);
	
					var isPixelWithinThreshold = 
						this.isPixelWithinThreshold
						(
							graphics, 
							colorBackgroundRGB, 
							colorDifferenceThreshold, 
							pixelPos
						);

					if (isPixelWithinThreshold == false)
					{
						isLineOpen = false;

						break;
					}

				} // end for j

				if (isLineOpen == false)
				{
					break;
				}

			} // end for d

			if (isLineOpen == true)
			{
				var lineOpen = new Line
				(
					new Coords().dimensionSet
					(
						axisJ, jMin
					).dimensionSet
					(
						axisI, i
					),
					new Coords().dimensionSet
					(
						axisJ, jMax
					).dimensionSet
					(
						axisI, i
					)
				);

				linesOpen.push(lineOpen);
			}
		
		} // end for i

		var lineGroupCurrent = [];
		var lineGroups = [];
		var linePrev = null;

		for (var i = 0; i < linesOpen.length; i++)
		{
			var line = linesOpen[i];

			if (linePrev != null)
			{
				var distanceBetweenLineAndPrev = 
					line.fromPos.dimension(axisI) 
					- linePrev.fromPos.dimension(axisI);

				if (distanceBetweenLineAndPrev > 1)
				{
					lineGroups.push(lineGroupCurrent);
					lineGroupCurrent = [];	
				}
			}

			lineGroupCurrent.push(line);

			linePrev = line;
		}

		lineGroups.push(lineGroupCurrent);

		var linesSpaced = [];

		for (var g = 0; g < lineGroups.length; g++)
		{
			var lineGroup = lineGroups[g];

			var indexOfLineAtCenterOfGroup 
				= Math.floor(lineGroup.length / 2);

			var lineAtCenterOfGroup = lineGroup[indexOfLineAtCenterOfGroup];

			linesSpaced.push(lineAtCenterOfGroup);
		}

		var linesForSpaces = linesSpaced;

		return linesForSpaces;
	}

	ImageAutoCharacterizer.prototype.isPixelWithinThreshold = function
	(
		graphics, colorBackgroundRGB, colorDifferenceThreshold, pixelPos
	)
	{
		var isPixelWithinThreshold = false;

		var pixelRGB = graphics.getImageData
		(
			pixelPos.x, pixelPos.y, 1, 1
		).data;

		var pixelDifference = 0;
		var numberOfColorComponents = 3; // rgb

		for (var c = 0; c < numberOfColorComponents; c++)
		{
			var componentDifference = Math.abs
			(
				pixelRGB[c] - colorBackgroundRGB[c]
			);
			pixelDifference += componentDifference;
		}

		var isPixelWithinThreshold =  
			(pixelDifference <= colorDifferenceThreshold);

		return isPixelWithinThreshold;
	}
}

function Line(fromPos, toPos)
{
	this.fromPos = fromPos;
	this.toPos = toPos;
}
{
	Line.prototype.drawToGraphics = function(graphics)
	{
		graphics.beginPath();
		graphics.moveTo(this.fromPos.x, this.fromPos.y);
		graphics.lineTo(this.toPos.x, this.toPos.y);
		graphics.stroke();
	}
}

</script>

</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , , . 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