洪水法实现Region RLE的fill_up算法

效果图

实现原理

核心算法:反向洪水填充(背景可达性分析)

核心思想

孔洞的本质是「无法从图像 / 区域边界连通的背景区域」。该算法不直接找孔洞,而是先标记所有能从边界连通的背景(外部背景),剩余未被标记的背景就是「孔洞」,最后将这些孔洞填充为前景。

洪水填充的实现方式: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>
相关推荐
2401_841495642 小时前
自然语言处理实战——基于BP神经网络的命名实体识别
人工智能·python·神经网络·算法·机器学习·自然语言处理·命名实体识别
wjykp2 小时前
88~93感知机f
人工智能·算法
良木生香2 小时前
【数据结构-初阶】二叉树---链式存储
c语言·数据结构·c++·算法·蓝桥杯·深度优先
Binky6782 小时前
力扣--贪心(2)+动规(1)
算法·leetcode·职场和发展
雾喔2 小时前
1970. 你能穿过矩阵的最后一天 + 今年总结
线性代数·算法·矩阵
长安er11 小时前
LeetCode215/347/295 堆相关理论与题目
java·数据结构·算法·leetcode·
元亓亓亓11 小时前
LeetCode热题100--62. 不同路径--中等
算法·leetcode·职场和发展
小白菜又菜11 小时前
Leetcode 1925. Count Square Sum Triples
算法·leetcode
登山人在路上12 小时前
Nginx三种会话保持算法对比
算法·哈希算法·散列表