diff --git a/extractstamp.js b/extractstamp.js new file mode 100644 index 0000000..3b1bbdb --- /dev/null +++ b/extractstamp.js @@ -0,0 +1,482 @@ +// 移除 import 语句,因为我们使用全局的 cv 对象 + +let cvReady = false; +const primaryColor = "#ff0000"; + +function initOpenCV(callback) { + if (typeof cv !== "undefined") { + cvReady = true; + console.log("OpenCV.js 已加载"); + callback && callback(true); + } else { + console.log("等待 OpenCV.js 加载..."); + document.addEventListener("opencv-ready", () => { + cvReady = true; + console.log("OpenCV.js 已加载"); + callback && callback(true); + }); + } +} + +/** + * 提取指定颜色的印章 + * @param img 要处理的图像 + * @param extractColor 要提取的颜色,十六进制格式,如 "#FF0000" + * @param setColor 设置提取区域的新颜色,十六进制格式,如 "#0000FF" + * @returns 处理后的图像 + * + * @example + * // 提取红色印章并将其设置为蓝色 + * const img = document.getElementById('myImage'); + * const extractedImg = extractStampWithColor(img, "#FF0000", "#0000FF"); + * + * // 提取绿色印章并将其设置为黄色 + * const greenStamp = document.querySelector('.stamp-image'); + * const yellowStamp = extractStampWithColor(greenStamp, "#00FF00", "#FFFF00"); + */ +function extractStampWithColorImpl( + img, + extractColor = "#ff0000", + setColor = "#ff0000" +) { + if (cvReady) { + // 获取图片的宽高 + const imgWidth = img.width; + const imgHeight = img.height; + console.log("图片宽度:", imgWidth, "图片高度:", imgHeight); + let src = cv.imread(img); + let dst = new cv.Mat(); + let mask = new cv.Mat(); + + // 转换为HSV颜色空间 + cv.cvtColor(src, dst, cv.COLOR_RGBA2RGB); + cv.cvtColor(dst, dst, cv.COLOR_RGB2HSV); + + // 将提取颜色转换为HSV + const extractColorRGB = hexToRgba(extractColor); + const extractColorHSV = cv.matFromArray(1, 1, cv.CV_8UC3, [ + extractColorRGB[0], + extractColorRGB[1], + extractColorRGB[2], + ]); + cv.cvtColor(extractColorHSV, extractColorHSV, cv.COLOR_RGB2HSV); + const hsvValues = extractColorHSV.data32F; + + // 定义提取颜色的HSV范围 + let lowColor, highColor; + if (extractColor.toLowerCase() === "#ff0000") { + // 红色的HSV范围 + lowColor = new cv.Mat(dst.rows, dst.cols, dst.type(), [0, 100, 100, 0]); + highColor = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [10, 255, 255, 255] + ); + } else if (extractColor.toLowerCase() === "#00ff00") { + // 绿色的HSV范围 + lowColor = new cv.Mat(dst.rows, dst.cols, dst.type(), [60, 100, 100, 0]); + highColor = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [80, 255, 255, 255] + ); + } else if (extractColor.toLowerCase() === "#000000") { + // 黑色的HSV范围 + lowColor = new cv.Mat(dst.rows, dst.cols, dst.type(), [0, 0, 0, 0]); + highColor = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [180, 255, 30, 255] + ); + } else { + // 其他颜色(包括随机颜色如#00ffff)使用动态计算的范围 + const hue = hsvValues[0]; + const hueRange = 10; // 色相范围,可以根据需要调整 + lowColor = new cv.Mat(dst.rows, dst.cols, dst.type(), [ + Math.max(0, hue - hueRange), + 100, + 100, + 0, + ]); + highColor = new cv.Mat(dst.rows, dst.cols, dst.type(), [ + Math.min(180, hue + hueRange), + 255, + 255, + 255, + ]); + } + + // 创建掩码 + cv.inRange(dst, lowColor, highColor, mask); + + // 将十六进制颜色值转换为RGBA + const dstColor = hexToRgba(setColor); + console.log("dstColor:", dstColor); + + // 创建带有 alpha 通道的目标图像 + let result = new cv.Mat(src.rows, src.cols, cv.CV_8UC4, [0, 0, 0, 0]); + + // 创建指定颜色的图像(带有 alpha 通道) + let colorMat = new cv.Mat(src.rows, src.cols, cv.CV_8UC4, [ + ...dstColor.slice(0, 3), + 255, + ]); + + // 使用掩码将提取的区域设置为指定颜色,非提取区域保持透明 + colorMat.copyTo(result, mask); + + // 创建隐藏的canvas用来保存提取后的图片 + const hiddenCanvas = document.createElement("canvas"); + hiddenCanvas.width = result.cols; + hiddenCanvas.height = result.rows; + cv.imshow(hiddenCanvas, result); + let dataURL = hiddenCanvas.toDataURL("image/png"); + + // 释放内存 + src.delete(); + dst.delete(); + mask.delete(); + lowColor.delete(); + highColor.delete(); + extractColorHSV.delete(); + colorMat.delete(); + result.delete(); + return dataURL; + } else { + console.error("OpenCV.js 未加载"); + return img; + } +} + +/** + * 提取指定颜色的印章 + * @param file 图片文件 + * @param extractColor 提取的颜色 + * @param setColor 设置的颜色,比如提取红色设置红色那么能够进行对印章的填充 + * @param isCircle 是否是圆形,如果是圆形,那么会进行圆形的裁剪,否则进行椭圆的裁剪 + * @returns + */ +function extractStampWithFile(file, extractColor, setColor, isCircle = true) { + return new Promise((resolve, reject) => { + const img = new Image(); + let distImgList = []; + img.onload = async () => { + let dstImg = extractStampWithColorImpl(img, extractColor, setColor); + let debugCircle = true; + if (debugCircle) { + // 将base64的图像数据转换为Image对象 + const base64ToImage = (base64) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = (error) => reject(error); + img.src = base64; + }); + }; + // 将base64转换回Image对象 + const resultRedImg = await base64ToImage(dstImg); + // 提取圆圈并获取结果 + distImgList = extractCircles(resultRedImg, isCircle); + } + resolve(distImgList); + }; + img.onerror = (error) => { + console.error("图片加载失败", error); + reject(new Error("图片加载失败")); + }; + img.src = URL.createObjectURL(file); + }); +} + +/** + * 检测图像中的圆形 + * @param dst 检测圆形的图像 + * @returns 检测到的圆形列表 + */ +function detectCircles(dst) { + // 创建一个新的Mat对象来存储检测到的圆形 + let circles = new cv.Mat(); + // 计算最小和最大半径,用于限制检测到的圆形大小 + let minRadius = Math.min(dst.rows, dst.cols) * 0.05; // 最小半径为图像最小边的5% + let maxRadius = Math.min(dst.rows, dst.cols) * 0.5; // 最大半径为图像最小边的50% + // 使用Hough变换检测圆形 + cv.HoughCircles( + dst, + circles, + cv.HOUGH_GRADIENT, + 1, // 两个圆心之间的最小距离 + dst.rows / 4, // 检测圆心之间的最小距离 + 100, // 检测圆形的阈值 + 50, // 检测圆形的阈值 + minRadius, // 检测圆形的最小半径 + maxRadius // 检测圆形的最大半径 + ); + + // 初始化一个空数组来存储检测到的圆形信息 + let detectedCircles = []; + // 遍历检测到的圆形 + for (let i = 0; i < circles.cols; i++) { + // 将检测到的圆形信息转换为对象形式 + detectedCircles.push({ + x: circles.data32F[i * 3], // 圆心x坐标 + y: circles.data32F[i * 3 + 1], // 圆心y坐标 + radius: circles.data32F[i * 3 + 2] // 半径 + }); + } + // 根据半径大小对检测到的圆形进行排序,确保最大的圆形排在前面 + detectedCircles.sort((a, b) => b.radius - a.radius); + + // 释放内存 + circles.delete(); + // 返回最大的3个圆形 + return detectedCircles.slice(0, 3); +} + + +/** + * 提取印章圆形 + * @param {*} img + * @param {*} isCircle + * @returns + */ +function extractCircles(img, isCircle = true) { + let src = cv.imread(img); + let dst = new cv.Mat(); + + // 转换为灰度图 + cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); + + // 应用高斯模糊以减少噪声 + cv.GaussianBlur(dst, dst, new cv.Size(5, 5), 2, 2); + + // 创建画布 + let canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + let ctx = canvas.getContext("2d"); + // 绘制原始图像 + ctx?.drawImage(img, 0, 0, canvas.width, canvas.height); + let croppedStamps = []; + if (isCircle) { + let circles = []; + // 检测圆形 + circles = detectCircles(dst); + console.log("circles:", circles); + circles.forEach((circle) => { + console.log("draw circle:", circle); + croppedStamps.push(cropAndDownloadCircle(img, circle)); + }); + } else { + let ellipses = []; + // 检测椭圆 + ellipses = detectEllipses(dst); + console.log("ellipses:", ellipses); + ellipses.forEach((ellipse) => { + console.log("draw ellipse:", ellipse); + croppedStamps.push(cropAndDownloadEllipse(img, ellipse)); + }); + } + + // 释放内存 + src.delete(); + dst.delete(); + + return croppedStamps; +} + +function cropAndDownloadCircle(img, circle) { + // 定义缩放因子,使裁剪范围比圆形大一些 + const scaleFactor = 1.2; // 增加20%的范围,您可以根据需要调整这个值 + // 计算新的半径和尺寸 + let newRadius = circle.radius * scaleFactor; + let size = newRadius * 2; + + // 创建一个新的canvas来裁剪圆形 + let cropCanvas = document.createElement("canvas"); + cropCanvas.width = size; + cropCanvas.height = size; + let ctx = cropCanvas.getContext("2d"); + + if (ctx) { + // 裁剪圆形区域 + ctx.beginPath(); + ctx.arc(newRadius, newRadius, newRadius, 0, Math.PI * 2); + ctx.closePath(); + ctx.clip(); + + // 计算源图像的裁剪区域 + let sx = circle.x - newRadius; + let sy = circle.y - newRadius; + let sWidth = size; + let sHeight = size; + + // 确保不会裁剪到图像边界外 + if (sx < 0) { + sWidth += sx; + sx = 0; + } + if (sy < 0) { + sHeight += sy; + sy = 0; + } + if (sx + sWidth > img.width) { + sWidth = img.width - sx; + } + if (sy + sHeight > img.height) { + sHeight = img.height - sy; + } + + // 绘制裁剪后的图像 + ctx.drawImage(img, sx, sy, sWidth, sHeight, 0, 0, size, size); + + // 将裁剪后的图像转换为数据URL + let dataURL = cropCanvas.toDataURL("image/png"); + return dataURL; + } +} + +/** + * 根据文件提取红色印章 + * @param file + * @returns + */ +function extractRedStampWithFile(file) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const dstImg = extractRedStampWithColor(img, primaryColor); + resolve(dstImg); + }; + img.onerror = (error) => { + console.error("图片加载失败", error); + reject(new Error("图片加载失败")); + }; + img.src = URL.createObjectURL(file); + }); +} + +/** + * 提取红色印章 + * @param img 原始图片 + * @returns + */ +function extractRedStamp(img) { + if (cvReady) { + const dstImg = extractRedStampWithColor(img, primaryColor); + return dstImg; + } else { + console.error("OpenCV.js 未加载"); + return null; + } +} + +/** + * 提取红色印章 + * @param img + * @param color + * @returns + */ +function extractRedStampWithColor(img, color = primaryColor) { + if (cvReady) { + // 获取图片的宽高 + const imgWidth = img.width; + const imgHeight = img.height; + console.log("图片宽度:", imgWidth, "图片高度:", imgHeight); + let src = cv.imread(img); + let dst = new cv.Mat(); + let mask = new cv.Mat(); + + // 转换为HSV颜色空间 + cv.cvtColor(src, dst, cv.COLOR_RGBA2RGB); + cv.cvtColor(dst, dst, cv.COLOR_RGB2HSV); + + // 定义红色的HSV范围 + let lowRedA = new cv.Mat(dst.rows, dst.cols, dst.type(), [0, 100, 100, 0]); + let highRedA = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [10, 255, 255, 255] + ); + let lowRedB = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [160, 100, 100, 0] + ); + let highRedB = new cv.Mat( + dst.rows, + dst.cols, + dst.type(), + [180, 255, 255, 255] + ); + + // 创建掩码 + let maskA = new cv.Mat(); + let maskB = new cv.Mat(); + cv.inRange(dst, lowRedA, highRedA, maskA); + cv.inRange(dst, lowRedB, highRedB, maskB); + cv.add(maskA, maskB, mask); + + // 将十六进制颜色值转换为RGB + const dstColor = hexToRgba(color); + console.log("dstColor:", dstColor); + // 创建纯红色图像 + let red = new cv.Mat(src.rows, src.cols, src.type(), dstColor); + + // 使用掩码将红色区域设置为纯红色 + red.copyTo(dst, mask); + + // 创建隐藏的canvas用来保存提取后的图片 + const hiddenCanvas = document.createElement("canvas"); + hiddenCanvas.width = dst.cols; + hiddenCanvas.height = dst.rows; + cv.imshow(hiddenCanvas, dst); + let dataURL = hiddenCanvas.toDataURL("image/png"); + let link = document.createElement("a"); + link.download = "extracted_red_image.png"; + link.href = dataURL; + link.click(); + + // 释放内存 + src.delete(); + dst.delete(); + mask.delete(); + maskA.delete(); + maskB.delete(); + lowRedA.delete(); + highRedA.delete(); + lowRedB.delete(); + highRedB.delete(); + red.delete(); + return dst; + } else { + console.error("OpenCV.js 未加载"); + return img; + } +} + +/** + * 将十六进制颜色值转换为RGBA + * @param hex + * @returns + */ +function hexToRgba(hex) { + let r = parseInt(hex.slice(1, 3), 16); + let g = parseInt(hex.slice(3, 5), 16); + let b = parseInt(hex.slice(5, 7), 16); + let a = 255; + if (hex.length === 9) { + a = parseInt(hex.slice(7, 9), 16); + } + return [r, g, b, a]; +} + +// 在文件末尾,将函数添加到全局作用域 +window.initOpenCV = initOpenCV; +window.extractRedStampWithFile = extractRedStampWithFile; +window.extractRedStamp = extractRedStamp; +window.extractStampWithFile = extractStampWithFile; diff --git a/index.html b/index.html new file mode 100644 index 0000000..df4a8b8 --- /dev/null +++ b/index.html @@ -0,0 +1,62 @@ + + +
+ + +