id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
233 lines (212 loc) • 9.41 kB
text/typescript
/**
* @file 边缘检测器
* @description 提供边缘检测算法(Sobel、Canny等)
* @module utils/edge-detector
*/
/**
* 边缘检测器类
* 提供各种边缘检测算法用于图像处理
*/
export class EdgeDetector {
/**
* 使用Sobel算子进行边缘检测
* @param imageData 灰度图像数据
* @param threshold 边缘阈值,默认为30
* @returns 检测到边缘的图像数据
*/
static detectEdges(imageData: ImageData, threshold: number = 30): ImageData {
const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
const width = grayscaleImage.width;
const height = grayscaleImage.height;
const inputData = grayscaleImage.data;
const outputData = new Uint8ClampedArray(inputData.length);
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0, gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixelPos = ((y + ky) * width + (x + kx)) * 4;
const pixelVal = inputData[pixelPos];
const kernelIdx = (ky + 1) * 3 + (kx + 1);
gx += pixelVal * sobelX[kernelIdx];
gy += pixelVal * sobelY[kernelIdx];
}
}
let magnitude = Math.sqrt(gx * gx + gy * gy);
magnitude = magnitude > threshold ? 255 : 0;
const pos = (y * width + x) * 4;
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = magnitude;
outputData[pos + 3] = 255;
}
}
// 处理边缘
for (let i = 0; i < width * 4; i++) {
outputData[i] = 0;
outputData[(height - 1) * width * 4 + i] = 0;
}
for (let i = 0; i < height; i++) {
const leftPos = i * width * 4;
const rightPos = (i * width + width - 1) * 4;
for (let j = 0; j < 4; j++) {
outputData[leftPos + j] = 0;
outputData[rightPos + j] = 0;
}
}
return new ImageData(outputData, width, height);
}
/**
* 卡尼-德里奇边缘检测
*/
static cannyEdgeDetection(imageData: ImageData, lowThreshold: number = 20, highThreshold: number = 50): ImageData {
const grayscaleImage = this.toGrayscale(new ImageData(new Uint8ClampedArray(imageData.data), imageData.width, imageData.height));
const blurredImage = this.gaussianBlur(grayscaleImage, 1.5);
const { gradientMagnitude, gradientDirection } = this.computeGradients(blurredImage);
const nonMaxSuppressed = this.nonMaxSuppression(gradientMagnitude, gradientDirection, blurredImage.width, blurredImage.height);
const thresholdResult = this.hysteresisThresholding(nonMaxSuppressed, blurredImage.width, blurredImage.height, lowThreshold, highThreshold);
const outputData = new Uint8ClampedArray(imageData.data.length);
for (let i = 0; i < thresholdResult.length; i++) {
const pos = i * 4;
const value = thresholdResult[i] ? 255 : 0;
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
outputData[pos + 3] = 255;
}
return new ImageData(outputData, blurredImage.width, blurredImage.height);
}
private static toGrayscale(imageData: ImageData): ImageData {
const srcData = imageData.data;
const destData = new Uint8ClampedArray(srcData);
for (let i = 0; i < srcData.length; i += 4) {
const gray = srcData[i] * 0.3 + srcData[i + 1] * 0.59 + srcData[i + 2] * 0.11;
destData[i] = destData[i + 1] = destData[i + 2] = gray;
destData[i + 3] = srcData[i + 3];
}
return new ImageData(destData, imageData.width, imageData.height);
}
private static gaussianBlur(imageData: ImageData, sigma: number = 1.5): ImageData {
const width = imageData.width, height = imageData.height;
const inputData = imageData.data, outputData = new Uint8ClampedArray(inputData.length);
const kernelSize = Math.max(3, Math.floor(sigma * 3) * 2 + 1);
const halfKernel = Math.floor(kernelSize / 2);
const kernel = this.generateGaussianKernel(kernelSize, sigma);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let sum = 0, weightSum = 0;
for (let ky = -halfKernel; ky <= halfKernel; ky++) {
for (let kx = -halfKernel; kx <= halfKernel; kx++) {
const pixelY = Math.min(Math.max(y + ky, 0), height - 1);
const pixelX = Math.min(Math.max(x + kx, 0), width - 1);
const pixelPos = (pixelY * width + pixelX) * 4;
const kernelY = ky + halfKernel, kernelX = kx + halfKernel;
const weight = kernel[kernelY * kernelSize + kernelX];
sum += inputData[pixelPos] * weight;
weightSum += weight;
}
}
const pos = (y * width + x) * 4;
const value = Math.round(sum / weightSum);
outputData[pos] = outputData[pos + 1] = outputData[pos + 2] = value;
outputData[pos + 3] = 255;
}
}
return new ImageData(outputData, width, height);
}
private static generateGaussianKernel(size: number, sigma: number): number[] {
const kernel = new Array(size * size);
const center = Math.floor(size / 2);
let sum = 0;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const distance = Math.sqrt((x - center) ** 2 + (y - center) ** 2);
kernel[y * size + x] = Math.exp(-(distance ** 2) / (2 * sigma ** 2));
sum += kernel[y * size + x];
}
}
for (let i = 0; i < kernel.length; i++) kernel[i] /= sum;
return kernel;
}
private static computeGradients(imageData: ImageData): { gradientMagnitude: number[], gradientDirection: number[] } {
const width = imageData.width, height = imageData.height;
const inputData = imageData.data;
const gradientMagnitude = new Array(width * height);
const gradientDirection = new Array(width * height);
const sobelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const sobelY = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0, gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixelPos = ((y + ky) * width + (x + kx)) * 4;
const pixelVal = inputData[pixelPos];
const kernelIdx = (ky + 1) * 3 + (kx + 1);
gx += pixelVal * sobelX[kernelIdx];
gy += pixelVal * sobelY[kernelIdx];
}
}
const idx = y * width + x;
gradientMagnitude[idx] = Math.sqrt(gx * gx + gy * gy);
gradientDirection[idx] = Math.atan2(gy, gx);
}
}
return { gradientMagnitude, gradientDirection };
}
private static nonMaxSuppression(gradientMagnitude: number[], gradientDirection: number[], width: number, height: number): number[] {
const result = new Array(width * height).fill(0);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
const magnitude = gradientMagnitude[idx];
const direction = gradientDirection[idx];
const degrees = (direction * 180 / Math.PI + 180) % 180;
let neighbor1Idx: number, neighbor2Idx: number;
if ((degrees >= 0 && degrees < 22.5) || (degrees >= 157.5 && degrees <= 180)) {
neighbor1Idx = idx - 1; neighbor2Idx = idx + 1;
} else if (degrees >= 22.5 && degrees < 67.5) {
neighbor1Idx = (y - 1) * width + (x + 1); neighbor2Idx = (y + 1) * width + (x - 1);
} else if (degrees >= 67.5 && degrees < 112.5) {
neighbor1Idx = (y - 1) * width + x; neighbor2Idx = (y + 1) * width + x;
} else {
neighbor1Idx = (y - 1) * width + (x - 1); neighbor2Idx = (y + 1) * width + (x + 1);
}
if (magnitude >= gradientMagnitude[neighbor1Idx] && magnitude >= gradientMagnitude[neighbor2Idx]) {
result[idx] = magnitude;
}
}
}
return result;
}
private static hysteresisThresholding(nonMaxSuppressed: number[], width: number, height: number, lowThreshold: number, highThreshold: number): boolean[] {
const result = new Array(width * height).fill(false);
const visited = new Array(width * height).fill(false);
const stack: number[] = [];
for (let i = 0; i < nonMaxSuppressed.length; i++) {
if (nonMaxSuppressed[i] >= highThreshold) {
result[i] = true;
stack.push(i);
visited[i] = true;
}
}
const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
while (stack.length > 0) {
const currentIdx = stack.pop()!;
const currentX = currentIdx % width;
const currentY = Math.floor(currentIdx / width);
for (let i = 0; i < 8; i++) {
const newX = currentX + dx[i];
const newY = currentY + dy[i];
if (newX >= 0 && newX < width && newY >= 0 && newY < height) {
const newIdx = newY * width + newX;
if (!visited[newIdx] && nonMaxSuppressed[newIdx] >= lowThreshold) {
result[newIdx] = true;
stack.push(newIdx);
visited[newIdx] = true;
}
}
}
}
return result;
}
}