sobel-ts
Version:
TypeScript implementation of Sobel edge detection algorithm for image processing
146 lines (145 loc) • 5.82 kB
JavaScript
import { getImageDataFactory } from './types.js';
/**
* Converts ImageData to grayscale, applies Sobel kernels, returns new ImageData
*
* This class performs edge detection using the Sobel operator.
* It processes a given ImageData object to extract horizontal and vertical gradients,
* then outputs a new ImageData with edge intensities encoded as grayscale values.
*
* This is a TypeScript implementation with enhancements including:
* - Variable kernel sizes (3x3, 5x5)
* - Multiple output formats (magnitude, x, y, direction)
*
* Current Maintainer/Developer: @catorch
*
* This work builds upon the original JavaScript version by Miguel Mota
* (https://github.com/miguelmota/sobel, MIT License).
*
*/
export class Sobel {
/**
* Constructor stores image dimensions, kernel size, and computes grayscale version
* @param imageData - original image data (browser ImageData or Node.js buffer)
* @param kernelSize - size of the Sobel kernel (3 or 5, defaults to 3)
*/
constructor(imageData, kernelSize = 3) {
this.imageData = imageData;
this.width = imageData.width;
this.height = imageData.height;
this.kernelSize = kernelSize;
this.grayscaleData = this.convertToGrayscale(imageData);
}
/**
* Safely retrieves the value of a specific pixel channel in an image array
* @param data - image pixel array
* @param x - horizontal coordinate
* @param y - vertical coordinate
* @param channel - 0=R, 1=G, 2=B, 3=A (default 0)
* @returns pixel channel value (0 if out of bounds)
*/
pixelAt(data, x, y, channel = 0) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height)
return 0;
return data[(y * this.width + x) * 4 + channel];
}
/**
* Converts RGBA input to grayscale (preserves alpha as 255)
* @param imageData - original image data
* @returns new Uint8ClampedArray of grayscale RGBA pixels
*/
convertToGrayscale(imageData) {
const gray = new Uint8ClampedArray(this.width * this.height * 4);
const src = imageData.data;
for (let i = 0; i < src.length; i += 4) {
// Average of R, G, B channels
const avg = (src[i] + src[i + 1] + src[i + 2]) / 3;
gray[i] = gray[i + 1] = gray[i + 2] = avg; // Set R, G, B to avg
gray[i + 3] = 255; // Alpha remains fully opaque
}
return gray;
}
/**
* Applies Sobel filter using the specified kernel size and output format
* @param format - Output format ('magnitude', 'x', 'y', or 'direction')
* @returns ImageData with edge intensities
*/
apply(format = 'magnitude') {
const output = new Uint8ClampedArray(this.width * this.height * 4);
const kernelRadius = Math.floor(this.kernelSize / 2);
// Select appropriate kernels based on size
const kernelX = this.kernelSize === 3 ? Sobel.kernelX3 : Sobel.kernelX5;
const kernelY = this.kernelSize === 3 ? Sobel.kernelY3 : Sobel.kernelY5;
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
let gx = 0;
let gy = 0;
// Adjust loop based on kernelRadius
for (let ky = -kernelRadius; ky <= kernelRadius; ky++) {
for (let kx = -kernelRadius; kx <= kernelRadius; kx++) {
const px = x + kx;
const py = y + ky;
// Adjust kernel index based on radius
const weightX = kernelX[ky + kernelRadius][kx + kernelRadius];
const weightY = kernelY[ky + kernelRadius][kx + kernelRadius];
const value = this.pixelAt(this.grayscaleData, px, py);
gx += value * weightX;
gy += value * weightY;
}
}
// Compute the output value based on the selected format
let value;
switch (format) {
case 'x':
value = Math.abs(gx);
break;
case 'y':
value = Math.abs(gy);
break;
case 'direction':
const angle = Math.atan2(gy, gx); // Angle in radians [-PI, PI]
value = ((angle + Math.PI) / (2 * Math.PI)) * 255; // Map to [0, 255]
break;
case 'magnitude':
default:
value = Math.sqrt(gx * gx + gy * gy);
break;
}
// Write the result as grayscale pixel in output
const index = (y * this.width + x) * 4;
output[index] = output[index + 1] = output[index + 2] = value;
output[index + 3] = 255; // Alpha remains opaque
}
}
// Create new ImageData object using the appropriate factory
return Sobel.imageDataFactory.create(output, this.width, this.height);
}
}
// ImageData factory for the current environment
Sobel.imageDataFactory = getImageDataFactory();
// Sobel kernel for detecting horizontal edges
Sobel.kernelX3 = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1],
];
// Sobel kernel for detecting vertical edges
Sobel.kernelY3 = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1],
];
// Add standard 5x5 Sobel kernels
Sobel.kernelX5 = [
[-1, -2, 0, 2, 1],
[-4, -8, 0, 8, 4],
[-6, -12, 0, 12, 6],
[-4, -8, 0, 8, 4],
[-1, -2, 0, 2, 1]
];
Sobel.kernelY5 = [
[-1, -4, -6, -4, -1],
[-2, -8, -12, -8, -2],
[0, 0, 0, 0, 0],
[2, 8, 12, 8, 2],
[1, 4, 6, 4, 1]
];