UNPKG

id-scanner-lib

Version:

Browser-based ID card, QR code, and face recognition scanner with liveness detection

233 lines (212 loc) 9.41 kB
/** * @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; } }