Alex
Implementing the Sobel image edge detection algorithm using WebAssembly
- WebAssembly
- image edge detection
I heard about WebAssembly a long time ago and have a general idea that it offers high execution speed, suitable for scenarios with high-performance requirements like gaming, image/video editing, and more.
So this time, I plan to try implementing the Sobel image edge detection algorithm separately using native JavaScript and WebAssembly to compare the performance difference between the two.
Here, I won't delve into the detailed implementation of the Sobel algorithm, but rather focus on how to implement it using WebAssembly.
Thanks to miguelmota, there is already a ready-made JavaScript version implementation of the Sobel algorithm, what I need to do is translate it into C++ and compile it into WebAssembly.
// sobel.c
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <emscripten/emscripten.h>
#include <stdint.h>
EMSCRIPTEN_KEEPALIVE
uint8_t *create_buffer(int width, int height)
{
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t *p)
{
free(p);
}
int result[1];
EMSCRIPTEN_KEEPALIVE
void sobel(uint8_t *img_in, int width, int height)
{
uint8_t *img_out;
img_out = (uint8_t *)malloc(width * height * 4 * sizeof(uint8_t));
const int Gx[3][3] = {
{-1, 0, 1},
{-2, 0, 2},
{-1, 0, 1}};
const int Gy[3][3] = {
{-1, -2, -1},
{0, 0, 0},
{1, 2, 1}};
for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int sumX = 0;
int sumY = 0;
for (int j = 0; j < 3; j++)
{
for (int i = 0; i < 3; i++)
{
const int pixelIndex = (y + j - 1) * width + (x + i - 1);
const int pixelValue = img_in[pixelIndex * 4];
sumX += pixelValue * Gx[j][i];
sumY += pixelValue * Gy[j][i];
}
}
int gradientMagnitude = sqrt(sumX * sumX + sumY * sumY);
const int outputPixelIndex = (y * width + x) * 4;
// Converting an int to uint8_t may result in overflow
if (gradientMagnitude > 255)
{
gradientMagnitude = 255;
}
img_out[outputPixelIndex] = gradientMagnitude;
img_out[outputPixelIndex + 1] = gradientMagnitude;
img_out[outputPixelIndex + 2] = gradientMagnitude;
// Alpha channel
img_out[outputPixelIndex + 3] = 255;
}
}
result[0] = (int)img_out;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t *result)
{
free(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer()
{
return result[0];
}Then, we can compile the C code into WebAssembly. I use Docker to run the command, and you can find a guide on how to install Emscripten here.
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
emscripten/emsdk emcc sobel.c -O3 -o sobel.js -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'After compilation, we got two files: sobel.js and sobel.wasm, where sobel.js is the glue code, and sobel.wasm is the WebAssembly code.
Then let's write the html and javascript to test the function.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
canvas {
border: 1px solid;
}
</style>
</head>
<body>
<div><input type="file" id="input-file" /></div>
<div>
<p>Original image</p>
<canvas id="canvas1"></canvas>
</div>
<div>
<p>WebAssembly sobel</p>
<canvas id="canvas2"></canvas>
</div>
<div>
<p>Javascript sobel</p>
<canvas id="canvas3"></canvas>
</div>
<script src="./sobel.js"></script>
<script src="./index.js"></script>
</body>
</html>const inputFile = document.getElementById("input-file");
const canvas1 = document.getElementById("canvas1");
const canvas2 = document.getElementById("canvas2");
const canvas3 = document.getElementById("canvas3");
const ctx1 = canvas1.getContext("2d", { willReadFrequently: true });
const ctx2 = canvas2.getContext("2d", { willReadFrequently: true });
const ctx3 = canvas3.getContext("2d", { willReadFrequently: true });
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then(resp => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
canvas1.width = img.width;
canvas1.height = img.height;
// Draw image
ctx1.drawImage(img, 0, 0);
return ctx1.getImageData(0, 0, img.width, img.height);
}
const Gx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
];
const Gy = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
];
// Sobel image edge detection algorithm in Javascript
function sobelAlgorithm(imageData) {
const width = imageData.width;
const height = imageData.height;
const outputData = new Uint8ClampedArray(imageData.data.length);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
for (let j = 0; j < 3; j++) {
for (let i = 0; i < 3; i++) {
const pixelIndex = (y + j - 1) * width + (x + i - 1);
const pixelValue = imageData.data[pixelIndex * 4];
sumX += pixelValue * Gx[j][i];
sumY += pixelValue * Gy[j][i];
}
}
const gradientMagnitude = Math.sqrt(sumX * sumX + sumY * sumY);
// Rounding to an integer
const normalizedMagnitude = gradientMagnitude >>> 0;
const outputPixelIndex = (y * width + x) * 4;
outputData[outputPixelIndex] = normalizedMagnitude;
outputData[outputPixelIndex + 1] = normalizedMagnitude;
outputData[outputPixelIndex + 2] = normalizedMagnitude;
outputData[outputPixelIndex + 3] = 255; // Alpha channel
}
}
return new ImageData(outputData, width, height);
}
// Detect image edges using the Sobel algorithm in both native JavaScript and WebAssembly, and draw them to canvas
function appleSobelDrawImageData(api, imageData) {
const { width, height } = imageData;
canvas2.width = width;
canvas2.height = height;
canvas3.width = width;
canvas3.height = height;
const p = api.create_buffer(width, height);
Module.HEAPU8.set(imageData.data, p);
console.time("codeExecution c++");
api.sobel(p, width, height);
console.timeEnd("codeExecution c++");
const resultPointer = api.get_result_pointer();
const resultView = new Uint8ClampedArray(
Module.HEAPU8.buffer,
resultPointer,
width * height * 4
);
api.free_result(resultPointer);
api.destroy_buffer(p);
const outImageData = new ImageData(resultView, width, height);
ctx2.putImageData(outImageData, 0, 0);
console.time("codeExecution javascript");
const sobelData = sobelAlgorithm(imageData);
console.timeEnd("codeExecution javascript");
ctx3.putImageData(sobelData, 0, 0);
}
Module.onRuntimeInitialized = _ => {
// Create wrapper functions for all the exported C functions
const api = {
create_buffer: Module.cwrap("create_buffer", "number", [
"number",
"number",
]),
destroy_buffer: Module.cwrap("destroy_buffer", "", ["number"]),
gray_scale: Module.cwrap("gray_scale", "", ["number", "number", "number"]),
sobel: Module.cwrap("sobel", "", ["number", "number", "number"]),
free_result: Module.cwrap("free_result", "", ["number"]),
get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
};
loadImage("bocchi.JPG").then(imageData => {
appleSobelDrawImageData(api, imageData);
});
inputFile.addEventListener("change", event => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const img = new Image();
img.onload = function () {
const { width, height } = img;
canvas1.width = width;
canvas1.height = height;
ctx1.drawImage(img, 0, 0);
const imageData = ctx1.getImageData(0, 0, width, height);
appleSobelDrawImageData(api, imageData);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
}
});
};And the result, pretty good.

Performance Comparison between JavaScript and WebAssembly, achieving nearly tenfold speed improvement.
codeExecution c++: 12.4970703125 ms
codeExecution javascript: 100.006103515625 msYou can find the source code for this example here, and you can try it out online here.The algorithm's execution time will be logged to the console.
Reference Links
- https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm
- https://developer.mozilla.org/en-US/docs/WebAssembly/existing_C_to_wasm
- https://web.dev/emscripting-a-c-library/
- https://github.com/GoogleChrome/samples/tree/gh-pages/webassembly
- https://www.cntofu.com/book/150/zh/ch2-c-js/ch2-04-data-exchange.md