Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuta Hashimoto authored and Yuta Hashimoto committed Nov 23, 2014
1 parent 50b98c9 commit 88bf66c
Show file tree
Hide file tree
Showing 9 changed files with 894 additions and 0 deletions.
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Canny JS
A (client-side) JavaScript implementation of Canny Edge Detection based on HTML5 canvas API.

## Demo
Visit the [demo page](http://yuta1984.github.io/canny/examples/) to see it in action.

## Usage
Include `canny.min.js` in your html file:

```
<script src="js/canny.min.js"></script>
```

`CannyJS.canny` method loads the image data from a given canvas, and returns the resulting image data as a `GrayImageData` object. To show the resulting image, just call its `drawOn(canvas)` method.

```javascript
// get target canvas element
mycanvas = document.getElementById("myCanvas");
// perform edge detection
imageData = CannyJS.canny(canvas);
// overwrites the original canvas
image.drawOn(mycanvas);
```

## Options
You can give some optional parameters to `CannyJS.canny` method:

```javascript
CannyJS.canny(canvas, [ht=100], [lt=50], [sigmma=1.4], [kernelSize=5])
```

`ht` and `lt` represent high and low threshold values that will be used in hysteresis thresholding procedure. Both `sigmma` and `kernalSize` are parameters used in Gaussian blur process (note that `kernelSize` must be an odd number).

## Other API
You can also call methods that perform each step of Canny edge detection: gaussian blur, sobel filtering, non-maximum suppression and hysteresis thresholding. Since these methods all receive and return `GrayImageData` objects, you first need to build an instance and make it load image data:

```javascript
var canvas = document.getElementById("myCanvas");
// construct a new GrayImageData object
var imageData = new GrayImageData(canvas.width, canvas.height)
// load image data from canvas
imageData.loadCanvas(canavs);
```

Available methods are as follows:

```javascript
// apply Gaussian filter
CannyJS.gaussianBlur(imageData, [sigmma=1.4], [kernelSize=5]);
blur.drawOn(canvas);
// apply sobel filter
CannyJS.sobel(blur);
// apply non-Maximum suppression
CannyJS.nonMaximumSuppression(sobel)
// apply hysteresis thresholding
CannyJS.hysteresis(nms, [ht=100], [lt=50])
```

## Performance
From waht I tested CannyJS takes 3-4 seconds to perform edge-detection on an image with size 600x400 (tested on Chrome 38 on MacBookAir). Because I wrote this library in CoffeeScript I have difficulties in optimizing the generated code for better performances. Any suggestion or fix will be appreciated (perhaps I better rewrite it in native JavaScript?).

## License
MIT License.
305 changes: 305 additions & 0 deletions canny.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
###*
# Utility object
###
Util = {}

Util.generateMatrix = (w, h, initialValue) ->
matrix=[]
for x in [0..w-1]
matrix[x] = []
for y in [0..h-1]
matrix[x][y] = initialValue
matrix

###*
# Class that represents gray-scaled image data
###
class GrayImageData

###*
# construct a new image data
# @param {number} width of the image
# @param {number} height of the image
###
constructor: (width, height) ->
@width = width
@height = height
@data = Util.generateMatrix(@width, @height, 0)
@

###*
# load image data from canvas and store it as a matrix of gray-scaled pixels
# @param {object} canvas object
###
loadCanvas: (canvas) ->
ctx = canvas.getContext('2d')
rawdata = ctx.getImageData(0, 0, canvas.width, canvas.height).data
x = 0
y = 0
for d, i in rawdata by 4
r = rawdata[i]
g = rawdata[i+1]
b = rawdata[i+2]
@data[x][y] = Math.round(0.298*r + 0.586*g + 0.114*b)
if x is @width-1
x = 0
y+= 1
else
x+=1
@

###*
# get the neighbor of a given point
# @param {number} x corrdinate of the point
# @param {number} y corrdinate of the point
# @param {number} size of the neighbors
# @return {array} matrix of the neighbor of the point
###
getNeighbors: (x, y, size) ->
neighbors = Util.generateMatrix(size, size, 0)
for i in [0..size-1]
neighbors[i] = []
for j in [0..size-1]
trnsX = x-(size-1)/2+i
trnsY = y-(size-1)/2+j
if @data[trnsX] and @data[trnsX][trnsY]
neighbors[i][j] = @data[trnsX][trnsY]
else
neighbors[i][j] = 0
neighbors

###*
# iterate all the pixel in the image data
# @param {number} size of the neighbors given to
# @param {function} function that will applied to the pixel
###
eachPixel: (neighborSize, func) ->
for x in [0..@width-1]
for y in [0..@height-1]
current = @data[x][y]
neighbors = @getNeighbors(x, y, neighborSize)
func(x, y, current, neighbors)
# x = 0
# while x < @width-1
# y = 0
# while y < @height-1
# current = @data[x][y]
# neighbors = @getNeighbors(x, y, neighborSize)
# func(x, y, current, neighbors)
# y++
# x++
@

###*
# return linear array of the image data
# @return {array} array of the pixel color data
###
toImageDataArray: ->
ary = []
for y in [0..@height-1]
for x in [0..@width-1]
ary.push @data[x][y] for i in [0..2]
ary.push 255
ary

###*
# return a deep copy of this object
# @return {object} the copy of this object
###
copy: ->
copied = new GrayImageData(@width, @height)
for x in [0..@width-1]
for y in [0..@height-1]
copied.data[x][y] = @data[x][y]
copied.width = @width
copied.height = @height
copied

###*
# draw the image on a given canvas
# @param {object} target canvas object
###
drawOn: (canvas) ->
ctx = canvas.getContext('2d')
imgData = ctx.createImageData(canvas.width, canvas.height)
for color, i in @toImageDataArray()
imgData.data[i] = color
ctx.putImageData(imgData, 0, 0)

###*
# fill the image with given color
# @param {number} color to fill
###
fill: (color) ->
for y in [0..@height-1]
for x in [0..@width-1]
@data[x][y] = color


###*
# object that holds methods for image processing
###
CannyJS = {}

###*
# apply gaussian blur to the image data
# @param {object} GrayImageData object
# @param {number} [sigmma=1.4] value of sigmma of gauss function
# @param {number} [size=3] size of the kernel (must be an odd number)
# @return {object} GrayImageData object
###
CannyJS.gaussianBlur= (imgData, sigmma=1.4, size=3) ->
kernel = CannyJS.generateKernel(sigmma, size)
copy = imgData.copy()
copy.fill 0
imgData.eachPixel size, (x, y, current, neighbors) ->
## this for-loop is too slow
# for i in [0..size-1]
# for j in [0..size-1]
# copy.data[x][y] += neighbors[i][j] * kernel[i][j]
i = 0
while i <= size-1
j = 0
while j <= size-1
copy.data[x][y] += neighbors[i][j] * kernel[i][j]
j++
i++
copy

###*
# generate kernel matrix
# @param {number} [sigmma] value of sigmma of gauss function
# @param {number} [size] size of the kernel (must be an odd number)
# @return {array} kernel matrix
###
CannyJS.generateKernel= (sigmma, size) ->
s = sigmma
e = 2.718
kernel = Util.generateMatrix(size, size, 0)
sum = 0
for i in [0..size-1]
x = -(size-1)/2 + i # calculate the local x coordinate of neighbor
for j in [0..size-1]
y = -(size-1)/2 + j # calculate the local y coordinate of neighbor
gaussian = (1/(2*Math.PI*s*s)) * Math.pow(e, -(x*x+y*y)/(2*s*s))
kernel[i][j] = gaussian
sum += gaussian
# normalization
for i in [0..size-1]
for j in [0..size-1]
kernel[i][j] = (kernel[i][j]/sum).toFixed(3)
console.log "kernel",kernel
kernel

###*
# appy sobel filter to image data
# @param {object} GrayImageData object
# @return {object} GrayImageData object
###
CannyJS.sobel= (imgData) ->
yFiler = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
]
xFiler = [
[-1, -2, -1],
[ 0, 0, 0],
[1, 2, 1]
]
copy = imgData.copy()
copy.fill 0
imgData.eachPixel 3, (x, y, current, neighbors) ->
ghs=0
gvs=0
for i in [0..2]
for j in [0..2]
ghs += yFiler[i][j]*neighbors[i][j]
gvs += xFiler[i][j]*neighbors[i][j]
copy.data[x][y] = Math.sqrt(ghs*ghs+gvs*gvs)
copy

###*
# appy non-maximum suppression to image data
# @param {object} GrayImageData object
# @return {object} GrayImageData object
###
CannyJS.nonMaximumSuppression = (imgData) ->
copy = imgData.copy()
copy.fill 0
# discard non-local maximum
imgData.eachPixel 3, (x, y, c, n) ->
if n[1][1] > n[0][1] and n[1][1] > n[2][1]
copy.data[x][y] = n[1][1]
else
copy.data[x][y] = 0
if n[1][1] > n[0][2] and n[1][1] > n[2][0]
copy.data[x][y] = n[1][1]
else
copy.data[x][y] = 0
if n[1][1] > n[1][0] and n[1][1] > n[1][2]
copy.data[x][y] = n[1][1]
else
copy.data[x][y] = 0
if n[1][1] > n[0][0] and n[1][1] > n[2][2]
copy.data[x][y] = n[1][1]
else
copy.data[x][y] = 0
copy


###*
# appy hysteresis threshold to image data
# @param {object} GrayImageData object
# @param {number} [ht=150] value of high threshold
# @param {number} [lt=100] value of low threshold
# @return {object} GrayImageData object
###
CannyJS.hysteresis= (imgData, ht, lt) ->
copy = imgData.copy()
isStrong = (edge) -> edge > ht
isCandidate = (edge) -> edge <= ht and edge >= lt
isWeak = (edge) -> edge < lt
# discard weak edges, pick up strong ones
imgData.eachPixel 3, (x, y, current, neighbors) ->
if isStrong(current)
copy.data[x][y] = 255
else if isWeak(current) or isCandidate(current)
copy.data[x][y] = 0
# traverse over candidate edges connected to strong ones
traverseEdge = (x, y) ->
return if x is 0 or y is 0 or x is imgData.width-1 or y is imgData.height-1
if isStrong(copy.data[x][y])
neighbors = copy.getNeighbors(x, y, 3)
for i in [0..2]
for j in [0..2]
if isCandidate(neighbors[i][j])
copy.data[x-1+i][y-1+j] = 255
traverseEdge(x-1+i,y-1+j)
copy.eachPixel 3, (x, y) -> traverseEdge(x, y)
# discard others
copy.eachPixel 1, (x, y, current) ->
copy.data[x][y] = 0 unless isStrong(current)
copy

###*
# appy canny edge detection algorithm to canvas
# @param {object} canvas object
# @param {number} [ht=100] value of high threshold
# @param {number} [lt=50] value of low threshold
# @param {number} [sigmma=1.4] value of sigmma of gauss function
# @param {number} [size=3] size of the kernel (must be an odd number)
# @return {object} GrayImageData object
###
CannyJS.canny = (canvas, ht=100, lt=50, sigmma=1.4, kernelSize=3) ->
imgData = new GrayImageData(canvas.width, canvas.height)
imgData.loadCanvas(canvas)
blur = CannyJS.gaussianBlur(imgData, sigmma, kernelSize)
sobel = CannyJS.sobel(blur)
nms = CannyJS.nonMaximumSuppression(sobel)
CannyJS.hysteresis(nms, ht, lt)

window.CannyJS = CannyJS
window.GrayImageData = GrayImageData


Loading

0 comments on commit 88bf66c

Please sign in to comment.