iosswiftalgorithmpixelflood-fill

Floodfill Scanline pixel in iOS Swift


I am using floodFillScanline https://lodev.org/cgtutor/floodfill.html algorithm in swift but its not working as expected.

Also performance wise it looks bit slow currently I am getting

Runtime: 0.33456450200174004s

func floodFillScanlineStackV3(x: Int, y: Int,
                            color_Grid: [[Int]], newColor: Int, oldColor: Int,
                            height: Int, width: Int) -> [[Int]] {
    var colorGrid = color_Grid
    
    assert(x < width, "p.x \(x) out of range, must be < \(width)")
    assert(y < height, "p.y \(y) out of range, must be < \(height)")
    
    if oldColor == newColor { return [] }
    
    var stack : [(Int, Int)] = [(x, y)] // 0 is X, 1 is Y
    
    while let pp = stack.popLast() {
        
        var x1 = pp.0
        
        while(x1 >= 0 && colorGrid[pp.1][x1] == oldColor) {
            x1 -= 1
        }
        
        x1 += 1
        
        var spanAbove = false
        var spanBelow = false
        
        while(x1 < width && compareColor(oldColor, colorGrid[pp.1][pp.0])) {
            colorGrid[x1][pp.1] = newColor
            
            let north = (x1, pp.1-1)
            let south = (x1, pp.1+1)
            
            if spanAbove == false && pp.1 > 0 &&  compareColor(colorGrid[x1][pp.1-1], oldColor) {
                stack.append(north)
                spanAbove = true
            } else if spanAbove && pp.1 > 0 && !compareColor(colorGrid[x1][pp.1-1], oldColor) {
                spanAbove = false
            } else if spanBelow == false && pp.1 < height - 1 && compareColor(colorGrid[x1][pp.1+1], oldColor) {
                stack.append(south)
                spanBelow = true
            } else if spanBelow && pp.1 < height - 1 && compareColor(colorGrid[x1][pp.1+1], oldColor) { //!_colorIsEqual(south, pixelBuffer, rgba) {
                spanBelow = false
            }
            
            x1 += 1
        }
    }
    
    return colorGrid
}

Before flood fill enter image description here

After flood fill enter image description here


Solution

  • I think you'll have a much better chance of translating that code to Swift by keeping it closer to the original.

    func floodFillScanlineStackV4(x: Int, y: Int,
                                  color_Grid: [Int],
                                  newColor: Int, oldColor: Int,
                                  h: Int, w: Int) -> [Int] {
        
        var screenBuffer = color_Grid
        
        assert(x < w, "p.x \(x) out of range, must be < \(w)")
        assert(y < h, "p.y \(y) out of range, must be < \(h)")
        
        if oldColor == newColor { return [] }
        
        var x1: Int = 0
        
        var stack : [(Int, Int)] = [(x, y)] // 0 is X, 1 is Y
        
        while let pp = stack.popLast() {
            
            x1 = pp.0
            let y = pp.1
            
            while(x1 >= 0 && screenBuffer[y * w + x1] == oldColor) {
                x1 -= 1
            }
            x1 += 1
            
            var spanAbove = false
            var spanBelow = false
            
            while(x1 < w && screenBuffer[y * w + x1] == oldColor) {
                screenBuffer[y * w + x1] = newColor;
                
                if(!spanAbove && y > 0 && screenBuffer[(y - 1) * w + x1] == oldColor) {
                    stack.append((x1, y - 1))
                    spanAbove = true
                }
                else if(spanAbove && y > 0 && screenBuffer[(y - 1) * w + x1] != oldColor) {
                    spanAbove = false
                }
                if(!spanBelow && y < h - 1 && screenBuffer[(y + 1) * w + x1] == oldColor) {
                    stack.append((x1, y + 1))
                    spanBelow = true
                }
                else if(spanBelow && y < h - 1 && screenBuffer[(y + 1) * w + x1] != oldColor) {
                    spanBelow = false
                }
                x1 += 1
            }
        }
        
        return screenBuffer
    }
    
    func compareColor(_ v1: Int, _ v2: Int) -> Bool {
        return v1 == v2
    }
    

    To do that, instead of using a two-dimensional array of Int, use a one-dimensional array.

    Here's some example code. Note that this is Example Code only, with minimal error checking, and is not intended to be considered "Production Ready":

    class FloodVC: UIViewController {
        
        let gridWidth: Int = 12
        let gridHeight: Int = 12
        var bufLength: Int = 0
        
        let gridSpacing: CGFloat = 1
        
        let colors: [UIColor] = [
            UIColor(red: 1.00, green: 0.60, blue: 0.60, alpha: 1.0),
            UIColor(red: 0.60, green: 1.00, blue: 0.60, alpha: 1.0),
            UIColor(red: 0.20, green: 0.85, blue: 1.00, alpha: 1.0),
            UIColor(red: 1.00, green: 1.00, blue: 0.60, alpha: 1.0),
            UIColor(red: 0.60, green: 1.00, blue: 1.00, alpha: 1.0),
            UIColor(red: 1.00, green: 0.60, blue: 1.00, alpha: 1.0),
        ]
        
        var grid: [Int] = []
        
        var newColor: Int = 2
        
        lazy var gridStack: UIStackView = {
            let v = UIStackView()
            v.axis = .vertical
            v.distribution = .fillEqually
            v.spacing = gridSpacing
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let colorStack: UIStackView = {
            let v = UIStackView()
            v.spacing = 8
            v.distribution = .fillEqually
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        let infoLabel: UILabel = {
            let v = UILabel()
            v.textAlignment = .center
            v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
            v.translatesAutoresizingMaskIntoConstraints = false
            return v
        }()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            bufLength = gridWidth * gridHeight
            
            // init array to all Zeroes
            grid = Array(repeating: 0, count: bufLength)
            
            for _ in 0..<gridHeight {
                let rs = UIStackView()
                rs.distribution = .fillEqually
                rs.spacing = gridSpacing
                for _ in 0..<gridWidth {
                    let v = UILabel()
                    v.font = .systemFont(ofSize: 10, weight: .light)
                    v.textAlignment = .center
                    v.isUserInteractionEnabled = true
                    let g = UITapGestureRecognizer(target: self, action: #selector(cellTap(_:)))
                    v.addGestureRecognizer(g)
                    rs.addArrangedSubview(v)
                }
                gridStack.addArrangedSubview(rs)
            }
            
            view.addSubview(gridStack)
            
            for i in 0..<colors.count {
                let v = UILabel()
                v.font = .systemFont(ofSize: 10, weight: .light)
                v.textAlignment = .center
                v.text = "\(i)"
                v.backgroundColor = colors[i]
                v.isUserInteractionEnabled = true
                v.heightAnchor.constraint(equalToConstant: 32).isActive = true
                v.layer.borderColor = UIColor.red.cgColor
                if i == newColor {
                    v.layer.borderWidth = 1
                }
                let g = UITapGestureRecognizer(target: self, action: #selector(newColorTap(_:)))
                v.addGestureRecognizer(g)
                colorStack.addArrangedSubview(v)
            }
            
            let shapeStack: UIStackView = {
                let v = UIStackView()
                v.spacing = 8
                v.distribution = .fillEqually
                v.translatesAutoresizingMaskIntoConstraints = false
                return v
            }()
            
            ["Square", "Cross", "Triangle", "Random"].forEach { str in
                let v = UIButton()
                v.titleLabel?.font = .systemFont(ofSize: 13, weight: .light)
                v.setTitle(str, for: [])
                v.backgroundColor = .systemBlue
                v.setTitleColor(.white, for: .normal)
                v.setTitleColor(.lightGray, for: .highlighted)
                v.layer.borderColor = UIColor.blue.cgColor
                v.layer.borderWidth = 1
                v.layer.cornerRadius = 6
                v.addTarget(self, action: #selector(setupShape(_:)), for: .touchUpInside)
                shapeStack.addArrangedSubview(v)
            }
            
            let optionsStack: UIStackView = {
                let v = UIStackView()
                v.axis = .vertical
                v.spacing = 8
                v.translatesAutoresizingMaskIntoConstraints = false
                return v
            }()
            
            optionsStack.addArrangedSubview(shapeStack)
            optionsStack.addArrangedSubview(colorStack)
            
            view.addSubview(optionsStack)
            view.addSubview(infoLabel)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                shapeStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                shapeStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16),
                shapeStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16),
                
                gridStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8),
                gridStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8),
                gridStack.heightAnchor.constraint(equalTo: gridStack.widthAnchor),
                gridStack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
                
                infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16),
                infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16),
                infoLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
    
            ])
            
            setupSquare()
        }
        
        @objc func newColorTap(_ g: UITapGestureRecognizer) {
            
            guard let v = g.view as? UILabel,
                  let t = v.text,
                  let idx = Int(t)
            else { return }
            
            for i in 0..<colorStack.arrangedSubviews.count {
                colorStack.arrangedSubviews[i].layer.borderWidth = i == idx ? 1 : 0
            }
            
            newColor = idx
            
        }
        
        @objc func cellTap(_ g: UITapGestureRecognizer) {
            
            guard let v = g.view as? UILabel,
                  let t = v.text,
                  let oldColor = Int(t),
                  let rowStack = v.superview as? UIStackView,
                  let c = rowStack.arrangedSubviews.firstIndex(of: v),
                  let r = gridStack.arrangedSubviews.firstIndex(of: rowStack)
            else { return }
            
    
            let st = CFAbsoluteTimeGetCurrent()
    
            let newGrid = floodFillScanlineStackV4(x: c, y: r, color_Grid: grid, newColor: newColor, oldColor: oldColor, h: gridHeight, w: gridWidth)
            
            let elapsed = CFAbsoluteTimeGetCurrent() - st
    
            let nf = NumberFormatter()
            nf.maximumFractionDigits = 8
    
            infoLabel.text = "Elapsed Time: " + nf.string(from: NSNumber(value: elapsed))! + " seconds"
    
            // if new color equals tapped cell color,
            //  newGrid will be empty
            if newGrid.count != 0 {
                grid = newGrid
                refreshGrid(grid)
            }
        }
        
        func refreshGrid(_ g: [Int]) {
            
            for i in 0..<bufLength {
                let r = i / gridWidth
                guard let rs = gridStack.arrangedSubviews[r] as? UIStackView
                else { fatalError("bad setup") }
                for c in 0..<gridWidth {
                    guard let v = rs.arrangedSubviews[c] as? UILabel
                    else { fatalError("bad setup") }
                    let p = r * gridWidth + c
                    v.text = "\(g[p])"
                    v.backgroundColor = colors[g[p]]
                }
            }
            
        }
        
        func floodFillScanlineStackV4(x: Int, y: Int,
                                      color_Grid: [Int],
                                      newColor: Int, oldColor: Int,
                                      h: Int, w: Int) -> [Int] {
            
            var screenBuffer = color_Grid
            
            assert(x < w, "p.x \(x) out of range, must be < \(w)")
            assert(y < h, "p.y \(y) out of range, must be < \(h)")
            
            if oldColor == newColor { return [] }
            
            var x1: Int = 0
            
            var stack : [(Int, Int)] = [(x, y)] // 0 is X, 1 is Y
            
            while let pp = stack.popLast() {
                
                x1 = pp.0
                let y = pp.1
                
                while(x1 >= 0 && screenBuffer[y * w + x1] == oldColor) {
                    x1 -= 1
                }
                x1 += 1
                
                var spanAbove = false
                var spanBelow = false
                
                while(x1 < w && screenBuffer[y * w + x1] == oldColor) {
                    screenBuffer[y * w + x1] = newColor;
                    
                    if(!spanAbove && y > 0 && screenBuffer[(y - 1) * w + x1] == oldColor) {
                        stack.append((x1, y - 1))
                        spanAbove = true
                    }
                    else if(spanAbove && y > 0 && screenBuffer[(y - 1) * w + x1] != oldColor) {
                        spanAbove = false
                    }
                    if(!spanBelow && y < h - 1 && screenBuffer[(y + 1) * w + x1] == oldColor) {
                        stack.append((x1, y + 1))
                        spanBelow = true
                    }
                    else if(spanBelow && y < h - 1 && screenBuffer[(y + 1) * w + x1] != oldColor) {
                        spanBelow = false
                    }
                    x1 += 1
                }
            }
            
            return screenBuffer
        }
        
        func compareColor(_ v1: Int, _ v2: Int) -> Bool {
            return v1 == v2
        }
    
        // MARK: grid setups
        @objc func setupShape(_ sender: Any?) {
            var t: String = "square"
            if let btn = sender as? UIButton {
                t = btn.currentTitle ?? "square"
            }
            switch t {
            case "Square":
                setupSquare()
                ()
            case "Cross":
                setupCross()
                ()
            case "Triangle":
                setupTriangle()
                ()
            default:
                setupRandom()
                ()
            }
        }
        
        @objc func setupSquare() {
            
            // init array to all Zeroes
            grid = Array(repeating: 0, count: bufLength)
            
            let row1: Int = 2
            let row2: Int = gridHeight - (row1 + 1)
            let col1: Int = 2
            let col2: Int = gridWidth - (col1 + 1)
            
            for r in row1...row2 {
                for c in col1...col2 {
                    let p = r * gridWidth + c
                    grid[p] = 1
                }
            }
            
            refreshGrid(grid)
            
        }
        
        @objc func setupCross() {
            
            // init array to all Zeroes
            grid = Array(repeating: 0, count: bufLength)
            
            var row1: Int = 2
            var row2: Int = gridHeight - (row1 + 1)
            var col1: Int = gridWidth / 2 - 1
            var col2: Int = col1 + 1
            for r in row1...row2 {
                for c in col1...col2 {
                    let p = r * gridWidth + c
                    grid[p] = 1
                }
            }
            row1 = gridHeight / 2 - 1
            row2 = row1 + 1
            col1 = 2
            col2 = gridWidth - (col1 + 1)
            for r in row1...row2 {
                for c in col1...col2 {
                    let p = r * gridWidth + c
                    grid[p] = 1
                }
            }
            
            refreshGrid(grid)
            
        }
        
        @objc func setupTriangle() {
            
            // init array to all Zeroes
            grid = Array(repeating: 0, count: bufLength)
            
            var row: Int = 1
            
            var col1: Int = gridWidth / 2 - 1
            var col2: Int = col1 + 1
            
            let p: Int = row * gridWidth + col1
            grid[p] = 1
            grid[p + 1] = 1
            row += 1
            col1 -= 1
            col2 += 1
            
            while col1 > 0 {
                var p1: Int = row * gridWidth + col1
                var p2: Int = row * gridWidth + col2
                grid[p1] = 1
                grid[p2] = 1
                row += 1
                
                p1 = row * gridWidth + col1
                p2 = row * gridWidth + col2
                grid[p1] = 1
                grid[p2] = 1
                row += 1
                
                col1 -= 1
                col2 += 1
            }
            for c in col1...col2 {
                let p: Int = row * gridWidth + c
                grid[p] = 1
            }
            
            refreshGrid(grid)
            
        }
        
        @objc func setupRandom() {
            
            // init array to all Zeroes
            grid = Array(repeating: 0, count: bufLength)
            
            // we'll fill grid with random excluding
            //  the first color, to make it easier to see
            //  the changes
            for r in 0..<gridHeight {
                for c in 0..<gridWidth {
                    let p = r * gridWidth + c
                    grid[p] = Int.random(in: 1..<colors.count)
                }
            }
            
            let idx: Int = 0
            
            for i in 0..<colorStack.arrangedSubviews.count {
                colorStack.arrangedSubviews[i].layer.borderWidth = i == idx ? 1 : 0
            }
            
            newColor = idx
            
            refreshGrid(grid)
            
        }
        
    }
    

    It looks like this when running:

    enter image description here

    Tapping a color-box at the top will select the "replacement color".

    Tapping a box inside the grid will perform the Flood Fill.

    Since this would ideally be used directly on a buffer of image data, the "Elapsed Time" value is for the Int Array process only -- it does not include refreshing the UI (the grid of colors/numbers).

    Tapping on the labeled buttons sets up an initial pattern:

    enter image description here

    enter image description here

    Use Random and then select various contiguous squares to try out a winding path:

    enter image description here