2026/5/21 12:48:36
网站建设
项目流程
数字展厅网站建设,.net网站,百度店铺,网站开发与设计实训报告1000字效果图 实现原理
核心算法#xff1a;反向洪水填充#xff08;背景可达性分析#xff09;
核心思想
孔洞的本质是「无法从图像 / 区域边界连通的背景区域」。该算法不直接找孔洞#xff0c;而是先标记所有能从边界连通的背景#xff08;外部背景#xff09;#xff0…效果图实现原理核心算法反向洪水填充背景可达性分析核心思想孔洞的本质是「无法从图像 / 区域边界连通的背景区域」。该算法不直接找孔洞而是先标记所有能从边界连通的背景外部背景剩余未被标记的背景就是「孔洞」最后将这些孔洞填充为前景。洪水填充的实现方式DFS深度优先代码中用stack栈实现洪水填充弹出栈顶元素处理→属于DFS深度优先搜索若改用queue队列则是 BFS广度优先两者核心逻辑一致仅遍历顺序不同。只处理上下左右四个方向是 4 连通规则若添加对角线如{dx:1, dy:1}则是 8 连通反向填充的优势传统种子填充法需要手动选 “孔洞种子”而该算法无需人工选种子自动从边界取种子只标记「外部背景」反向锁定孔洞避免漏判 / 误判适配任意形状的前景区域无需拓扑分析。核心算法是RLE→像素矩阵转换 边界种子初始化 4 连通 DFS 洪水填充标记外部背景 反向填充孔洞 像素矩阵→RLE 还原。核心可视化代码!DOCTYPE html html langen head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title增强版画布绘图与RLE编码工具/title style body { font-family: Arial, sans-serif; margin: 20px; } .canvas-row { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px; } .canvas-container { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; } canvas { border: 1px solid #000; cursor: crosshair; } .controls { margin: 10px 0; display: flex; gap: 10px; flex-wrap: wrap; } button { padding: 8px 16px; cursor: pointer; } textarea { width: 800px; height: 200px; font-family: monospace; } .canvas-label { font-weight: bold; text-align: center; } /style /head body h1增强版画布绘图与RLE编码工具/h1 div classcanvas-row div classcanvas-container div classcanvas-label绘图区域/div canvas iddrawing-canvas width500 height500/canvas /div div classcanvas-container div classcanvas-label二值化结果 (0-128)/div canvas idbinary-canvas width500 height500/canvas /div /div div classcanvas-row div classcanvas-container div classcanvas-labelRLE可视化/div canvas idrle-canvas width500 height500/canvas /div div classcanvas-container div classcanvas-labelFillUp结果/div canvas idfillup-canvas width500 height500/canvas /div /div div classcontrols button idclear-btn清除画布/button button idbinarize-btn二值化并生成RLE/button button idfillup-btn执行FillUp/button div label forbrush-size画笔大小:/label input typerange idbrush-size min1 max50 value20 span idbrush-size-value20/span /div /div div h3RLE编码结果:/h3 textarea idrle-output readonly/textarea /div script // 获取所有画布和上下文 const drawingCanvas document.getElementById(drawing-canvas); const drawingCtx drawingCanvas.getContext(2d); const binaryCanvas document.getElementById(binary-canvas); const binaryCtx binaryCanvas.getContext(2d); const rleCanvas document.getElementById(rle-canvas); const rleCtx rleCanvas.getContext(2d); const fillupCanvas document.getElementById(fillup-canvas); const fillupCtx fillupCanvas.getContext(2d); // 获取控制元素 const clearBtn document.getElementById(clear-btn); const binarizeBtn document.getElementById(binarize-btn); const fillupBtn document.getElementById(fillup-btn); const rleOutput document.getElementById(rle-output); const brushSizeInput document.getElementById(brush-size); const brushSizeValue document.getElementById(brush-size-value); // 绘图状态 let isDrawing false; let brushSize 20; let brushColor #000000; // 初始化画布 function initCanvas(ctx, canvas) { ctx.fillStyle #ffffff; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle brushColor; } initCanvas(drawingCtx, drawingCanvas); initCanvas(binaryCtx, binaryCanvas); initCanvas(rleCtx, rleCanvas); initCanvas(fillupCtx, fillupCanvas); // 画笔大小控制 brushSizeInput.addEventListener(input, () { brushSize parseInt(brushSizeInput.value); brushSizeValue.textContent brushSize; }); // 绘图功能 drawingCanvas.addEventListener(mousedown, startDrawing); drawingCanvas.addEventListener(mousemove, draw); drawingCanvas.addEventListener(mouseup, stopDrawing); drawingCanvas.addEventListener(mouseout, stopDrawing); function startDrawing(e) { isDrawing true; draw(e); } function draw(e) { if (!isDrawing) return; const rect drawingCanvas.getBoundingClientRect(); const x e.clientX - rect.left; const y e.clientY - rect.top; drawingCtx.beginPath(); drawingCtx.arc(x, y, brushSize, 0, Math.PI * 2); drawingCtx.fill(); } function stopDrawing() { isDrawing false; } // 清除画布 clearBtn.addEventListener(click, () { initCanvas(drawingCtx, drawingCanvas); initCanvas(binaryCtx, binaryCanvas); initCanvas(rleCtx, rleCanvas); initCanvas(fillupCtx, fillupCanvas); rleOutput.value ; }); // 二值化并生成RLE binarizeBtn.addEventListener(click, () { // 获取绘图区域图像数据 const imageData drawingCtx.getImageData(0, 0, drawingCanvas.width, drawingCanvas.height); const data imageData.data; const width drawingCanvas.width; const height drawingCanvas.height; // 创建二维数组表示二值图像 const binaryImage new Array(height); for (let y 0; y height; y) { binaryImage[y] new Array(width).fill(0); } // 二值化处理 (0-128为1其余为0) for (let i 0; i data.length; i 4) { const y Math.floor((i / 4) / width); const x (i / 4) % width; const brightness 0.299 * data[i] 0.587 * data[i 1] 0.114 * data[i 2]; binaryImage[y][x] brightness 128 ? 1 : 0; } // 显示二值化结果 displayBinaryImage(binaryImage, binaryCtx, binaryCanvas); // 生成RLE编码 const rleSegments generateRLE(binaryImage); displayRLE(rleSegments); // 显示RLE可视化 displayRLEVisualization(rleSegments, rleCtx, rleCanvas); }); // 显示二值化图像 function displayBinaryImage(binaryImage, ctx, canvas) { ctx.fillStyle #ffffff; ctx.fillRect(0, 0, canvas.width, canvas.height); for (let y 0; y binaryImage.length; y) { for (let x 0; x binaryImage[y].length; x) { if (binaryImage[y][x] 1) { ctx.fillStyle #000000; ctx.fillRect(x, y, 1, 1); } } } } // 生成RLE编码 function generateRLE(binaryImage) { const segments []; const height binaryImage.length; const width binaryImage[0].length; let currentLabel 1; for (let y 0; y height; y) { let x 0; while (x width) { if (binaryImage[y][x] 1) { let xStart x; while (x width binaryImage[y][x] 1) { x; } const xEnd x - 1; const area xEnd - xStart 1; segments.push({ y, x_start: xStart, x_end: xEnd, label: currentLabel, area }); } else { x; } } } return segments; } // 显示RLE编码 function displayRLE(segments) { let output RLE Segments \n; output Format: yY, x[X1,X2], labelL, areaA\n\n; segments.forEach(segment { output y${segment.y}, x[${segment.x_start},${segment.x_end}], label${segment.label}, area${segment.area}\n; }); rleOutput.value output; } // 显示RLE可视化 function displayRLEVisualization(segments, ctx, canvas) { // 清空画布 ctx.fillStyle #ffffff; ctx.fillRect(0, 0, canvas.width, canvas.height); // 定义多个颜色 const colors [ #ff0000, // 红色 // #00aa00, // 绿色 // #0000ff, // 蓝色 // #ff9900, // 橙色 // #9900ff, // 紫色 // #00ffff, // 青色 // #ff00ff, // 品红 // #666600, // 深黄绿 ]; // 绘制RLE段 segments.forEach((segment, index) { const segmentWidth segment.x_end - segment.x_start 1; const y segment.y; // 轮换使用颜色 const color colors[index % colors.length]; ctx.fillStyle color; ctx.fillRect(segment.x_start, y, segmentWidth, 1); }); } // FillUp算法实现 fillupBtn.addEventListener(click, () { // 获取二值化图像数据 const imageData binaryCtx.getImageData(0, 0, binaryCanvas.width, binaryCanvas.height); const data imageData.data; const width binaryCanvas.width; const height binaryCanvas.height; // 创建二维数组表示二值图像 const binaryImage new Array(height); for (let y 0; y height; y) { binaryImage[y] new Array(width).fill(0); } // 从二值化画布读取数据 for (let i 0; i data.length; i 4) { const y Math.floor((i / 4) / width); const x (i / 4) % width; binaryImage[y][x] data[i] 0 ? 1 : 0; // 黑色为1白色为0 } // 生成初始RLE let rleSegments generateRLE(binaryImage); // 执行FillUp算法 rleSegments fillUpRLE2(rleSegments, width, height); // 显示FillUp结果 displayRLEVisualization(rleSegments, fillupCtx, fillupCanvas); // 更新RLE文本 displayRLE(rleSegments); }); // FillUp算法实现 (类似Halcon的fill_up) function fillUpRLE(segments, width, height) { // 创建标记数组 const labelMap new Array(height); for (let y 0; y height; y) { labelMap[y] new Array(width).fill(0); } // 应用初始标签 segments.forEach(segment { for (let x segment.x_start; x segment.x_end; x) { labelMap[segment.y][x] segment.label; } }); // 按标签分组处理 const labels new Set(segments.map(s s.label)); const newSegments []; labels.forEach(label { // 找到当前标签的所有原始段 const originalSegments segments.filter(s s.label label); // 如果没有任何段跳过 if (originalSegments.length 0) return; // 找到当前标签的区域边界框 let minX width, maxX 0, minY height, maxY 0; for (let segment of originalSegments) { minX Math.min(minX, segment.x_start); maxX Math.max(maxX, segment.x_end); minY Math.min(minY, segment.y); maxY Math.max(maxY, segment.y); } // 扩展边界框为填充留出空间 const expand 1; minX Math.max(0, minX - expand); maxX Math.min(width - 1, maxX expand); minY Math.max(0, minY - expand); maxY Math.min(height - 1, maxY expand); // 为当前标签创建处理区域 const regionWidth maxX - minX 1; const regionHeight maxY - minY 1; // 创建标记数组 const visited new Array(regionHeight); const isFilled new Array(regionHeight); for (let i 0; i regionHeight; i) { visited[i] new Array(regionWidth).fill(false); isFilled[i] new Array(regionWidth).fill(false); } // 标记原始区域 for (let segment of originalSegments) { const y segment.y - minY; for (let x segment.x_start; x segment.x_end; x) { const relX x - minX; isFilled[y][relX] true; } } // 使用BFS/DFS找到所有从边界可达的点 const stack []; // 从边界开始标记可到达的点 for (let y 0; y regionHeight; y) { // 左边界 if (!isFilled[y][0] !visited[y][0]) { stack.push({x: 0, y: y}); visited[y][0] true; } // 右边界 if (!isFilled[y][regionWidth-1] !visited[y][regionWidth-1]) { stack.push({x: regionWidth-1, y: y}); visited[y][regionWidth-1] true; } } for (let x 0; x regionWidth; x) { // 上边界 if (!isFilled[0][x] !visited[0][x]) { stack.push({x: x, y: 0}); visited[0][x] true; } // 下边界 if (!isFilled[regionHeight-1][x] !visited[regionHeight-1][x]) { stack.push({x: x, y: regionHeight-1}); visited[regionHeight-1][x] true; } } // 执行洪水填充 const directions [ {dx: 1, dy: 0}, {dx: -1, dy: 0}, {dx: 0, dy: 1}, {dx: 0, dy: -1} ]; while (stack.length 0) { const {x, y} stack.pop(); for (const dir of directions) { const nx x dir.dx; const ny y dir.dy; if (nx 0 nx regionWidth ny 0 ny regionHeight) { if (!isFilled[ny][nx] !visited[ny][nx]) { visited[ny][nx] true; stack.push({x: nx, y: ny}); } } } } // 填充所有未被访问且未被填充的点即闭合空洞 for (let y 0; y regionHeight; y) { for (let x 0; x regionWidth; x) { if (!isFilled[y][x] !visited[y][x]) { isFilled[y][x] true; } } } // 从isFilled生成新的RLE段 for (let y 0; y regionHeight; y) { const absY y minY; let x 0; while (x regionWidth) { if (isFilled[y][x]) { let xStart x; while (x regionWidth isFilled[y][x]) { x; } const xEnd x - 1; newSegments.push({ y: absY, x_start: xStart minX, x_end: xEnd minX, label: label, area: xEnd - xStart 1 }); } else { x; } } } }); // 按y和x_start排序 newSegments.sort((a, b) { if (a.y ! b.y) return a.y - b.y; return a.x_start - b.x_start; }); return newSegments; } /script /body /html