效果图

实现原理
核心算法:反向洪水填充(背景可达性分析)
核心思想
孔洞的本质是「无法从图像 / 区域边界连通的背景区域」。该算法不直接找孔洞,而是先标记所有能从边界连通的背景(外部背景),剩余未被标记的背景就是「孔洞」,最后将这些孔洞填充为前景。
洪水填充的实现方式:DFS(深度优先)
代码中用stack(栈)实现洪水填充,弹出栈顶元素处理→属于DFS(深度优先搜索);若改用queue(队列)则是 BFS(广度优先),两者核心逻辑一致,仅遍历顺序不同。
只处理上下左右四个方向,是 4 连通规则;若添加对角线(如{dx:1, dy:1})则是 8 连通
反向填充的优势
传统种子填充法需要手动选 "孔洞种子",而该算法:
无需人工选种子,自动从边界取种子;
只标记「外部背景」,反向锁定孔洞,避免漏判 / 误判;
适配任意形状的前景区域,无需拓扑分析。
核心算法是:
RLE→像素矩阵转换 + 边界种子初始化 + 4 连通 DFS 洪水填充(标记外部背景) + 反向填充孔洞 + 像素矩阵→RLE 还原。
核心可视化代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.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 class="canvas-row">
<div class="canvas-container">
<div class="canvas-label">绘图区域</div>
<canvas id="drawing-canvas" width="500" height="500"></canvas>
</div>
<div class="canvas-container">
<div class="canvas-label">二值化结果 (0-128)</div>
<canvas id="binary-canvas" width="500" height="500"></canvas>
</div>
</div>
<div class="canvas-row">
<div class="canvas-container">
<div class="canvas-label">RLE可视化</div>
<canvas id="rle-canvas" width="500" height="500"></canvas>
</div>
<div class="canvas-container">
<div class="canvas-label">FillUp结果</div>
<canvas id="fillup-canvas" width="500" height="500"></canvas>
</div>
</div>
<div class="controls">
<button id="clear-btn">清除画布</button>
<button id="binarize-btn">二值化并生成RLE</button>
<button id="fillup-btn">执行FillUp</button>
<div>
<label for="brush-size">画笔大小:</label>
<input type="range" id="brush-size" min="1" max="50" value="20">
<span id="brush-size-value">20</span>
</div>
</div>
<div>
<h3>RLE编码结果:</h3>
<textarea id="rle-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: y=Y, x=[X1,X2], label=L, area=A\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>