AngryDuck
AngryDuck

Reputation: 4607

Change White Pixels in a UIImage

I have a UIImage which is an outline, and one which is filled, both were created from OpenCV, one from grab cut and the other from structuring element.

my two images are like this:

enter image description here

enter image description here

I am trying to change all of the white pixels in the outlined image because I want to merge the two images together so I end up with a red outline and a white filled area in the middle. I am using this to merge the two together, and I am aware instead of red it will be a pink kind of colour and a grey instead of white since I am just blending them together with alpha.

// Start an image context
UIGraphicsBeginImageContext(grabcutResult.size)
        
// Create rect for this draw session
let rect = CGRect(x: 0.0, y: 0.0, width: grabcutResult.size.width, height: grabcutResult.size.height)
grabcutResult.draw(in: rect)
redGrabcutOutline.draw(in: rect, blendMode: .normal, alpha: 0.5)

let finalImage = UIGraphicsGetImageFromCurrentImageContext()

and the idea is it should look something like this.

enter image description here

I want to be able to complete this operation quickly but the only solutions I have found are either for ImageView (which only affects how stuff is rendered not the underlying UIImage) or they involve looping over the entire image pixel by pixel.

I am trying to find a solution that will just mask all the white pixels in the outline to red without having to loop over the entire image pixel by pixel as it is too slow.

Ideally it would be good if I could get openCV to just return a red outline instead of white but I don't think its possible to change this (maybe im wrong).

Using swift btw... but any Help is appreciated, thanks in advance.

Upvotes: 3

Views: 1482

Answers (3)

DonMag
DonMag

Reputation: 77672

This may work for you - using only Swift code...

extension UIImage {

    func maskWithColor(color: UIColor) -> UIImage? {

        let maskingColors: [CGFloat] = [1, 255, 1, 255, 1, 255]
        let bounds = CGRect(origin: .zero, size: size)

        let maskImage = cgImage!
        var returnImage: UIImage?

        // make sure image has no alpha channel
        let rFormat = UIGraphicsImageRendererFormat()
        rFormat.opaque = true
        let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
        let noAlphaImage = renderer.image {
            (context) in
            self.draw(at: .zero)
        }

        let noAlphaCGRef = noAlphaImage.cgImage

        if let imgRefCopy = noAlphaCGRef?.copy(maskingColorComponents: maskingColors) {

            let rFormat = UIGraphicsImageRendererFormat()
            rFormat.opaque = false
            let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
            returnImage = renderer.image {
                (context) in
                context.cgContext.clip(to: bounds, mask: maskImage)
                context.cgContext.setFillColor(color.cgColor)
                context.cgContext.fill(bounds)
                context.cgContext.draw(imgRefCopy, in: bounds)
            }

        }
        return returnImage
    }

}

This extension returns a UIImage with white replaced with the passed UIColor, and the black "background" changed to transparent.

Use it in this manner:

// change filled white star to gray with transparent background
let modFilledImage = filledImage.maskWithColor(color: UIColor(red: 200, green: 200, blue: 200))

// change outlined white star to red with transparent background
let modOutlineImage = outlineImage.maskWithColor(color: UIColor.red)

// combine the images on a black background

Here is a full example, using your two original images (most of the code is setting up image views to show the results):

extension UIImage {

    func maskWithColor(color: UIColor) -> UIImage? {

        let maskingColors: [CGFloat] = [1, 255, 1, 255, 1, 255]
        let bounds = CGRect(origin: .zero, size: size)

        let maskImage = cgImage!
        var returnImage: UIImage?

        // make sure image has no alpha channel
        let rFormat = UIGraphicsImageRendererFormat()
        rFormat.opaque = true
        let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
        let noAlphaImage = renderer.image {
            (context) in
            self.draw(at: .zero)
        }

        let noAlphaCGRef = noAlphaImage.cgImage

        if let imgRefCopy = noAlphaCGRef?.copy(maskingColorComponents: maskingColors) {

            let rFormat = UIGraphicsImageRendererFormat()
            rFormat.opaque = false
            let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
            returnImage = renderer.image {
                (context) in
                context.cgContext.clip(to: bounds, mask: maskImage)
                context.cgContext.setFillColor(color.cgColor)
                context.cgContext.fill(bounds)
                context.cgContext.draw(imgRefCopy, in: bounds)
            }

        }
        return returnImage
    }

}

class MaskWorkViewController: UIViewController {

    let origFilledImgView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.contentMode = .center
        return v
    }()

    let origOutlineImgView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.contentMode = .center
        return v
    }()

    let modifiedFilledImgView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.contentMode = .center
        return v
    }()

    let modifiedOutlineImgView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.contentMode = .center
        return v
    }()

    let combinedImgView: UIImageView = {
        let v = UIImageView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.contentMode = .center
        return v
    }()

    let origStack: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .horizontal
        v.spacing = 20
        return v
    }()

    let modifiedStack: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .horizontal
        v.spacing = 20
        return v
    }()

    let mainStack: UIStackView = {
        let v = UIStackView()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.axis = .vertical
        v.alignment = .center
        v.spacing = 10
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let filledImage = UIImage(named: "StarFill"),
            let outlineImage = UIImage(named: "StarEdge") else {
                return
        }

        var modifiedFilledImage: UIImage = UIImage()
        var modifiedOutlineImage: UIImage = UIImage()
        var combinedImage: UIImage = UIImage()

        // for both original images, replace white with color
        // and make black transparent
        if let modFilledImage = filledImage.maskWithColor(color: UIColor(red: 200, green: 200, blue: 200)),
            let modOutlineImage = outlineImage.maskWithColor(color: UIColor.red) {

            modifiedFilledImage = modFilledImage
            modifiedOutlineImage = modOutlineImage

            let rFormat = UIGraphicsImageRendererFormat()
            rFormat.opaque = true

            let renderer = UIGraphicsImageRenderer(size: modifiedFilledImage.size, format: rFormat)

            // combine modified images on black background
            combinedImage = renderer.image {
                (context) in
                context.cgContext.setFillColor(UIColor.black.cgColor)
                context.cgContext.fill(CGRect(origin: .zero, size: modifiedFilledImage.size))
                modifiedFilledImage.draw(at: .zero)
                modifiedOutlineImage.draw(at: .zero)
            }

        }

        // setup image views and set .image properties
        setupUI(filledImage.size)

        origFilledImgView.image = filledImage
        origOutlineImgView.image = outlineImage

        modifiedFilledImgView.image = modifiedFilledImage
        modifiedOutlineImgView.image = modifiedOutlineImage

        combinedImgView.image = combinedImage

    }

    func setupUI(_ imageSize: CGSize) -> Void {

        origStack.addArrangedSubview(origFilledImgView)
        origStack.addArrangedSubview(origOutlineImgView)

        modifiedStack.addArrangedSubview(modifiedFilledImgView)
        modifiedStack.addArrangedSubview(modifiedOutlineImgView)

        var lbl = UILabel()
        lbl.textAlignment = .center
        lbl.text = "Original Images"

        mainStack.addArrangedSubview(lbl)

        mainStack.addArrangedSubview(origStack)

        lbl = UILabel()
        lbl.textAlignment = .center
        lbl.numberOfLines = 0
        lbl.text = "Modified Images\n(UIImageViews have Green Background)"

        mainStack.addArrangedSubview(lbl)

        mainStack.addArrangedSubview(modifiedStack)

        lbl = UILabel()
        lbl.textAlignment = .center
        lbl.text = "Combined on Black Background"

        mainStack.addArrangedSubview(lbl)

        mainStack.addArrangedSubview(combinedImgView)

        view.addSubview(mainStack)

        NSLayoutConstraint.activate([

            mainStack.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            mainStack.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),

        ])

        [origFilledImgView, origOutlineImgView, modifiedFilledImgView, modifiedOutlineImgView, combinedImgView].forEach {

            $0.backgroundColor = .green

            NSLayoutConstraint.activate([

                $0.widthAnchor.constraint(equalToConstant: imageSize.width),
                $0.heightAnchor.constraint(equalToConstant: imageSize.height),

            ])

        }

    }

}

And the result, showing the original, modified and final combined image... Image views have green backgrounds to show the transparent areas:

enter image description here

Upvotes: 3

nathancy
nathancy

Reputation: 46660

The idea is to bitwise-or the two masks together which "merges" the two masks. Since this new combined grayscale image is still a single (1-channel) image, we need to convert it to a 3-channel so we can apply color to the image. Finally, we color the outline mask with red to get our result

enter image description here

I implemented it in Python OpenCV but you can adapt the same idea with Swift

import cv2

# Read in images as grayscale
full = cv2.imread('1.png', 0)
outline = cv2.imread('2.png', 0)

# Bitwise-or masks
combine = cv2.bitwise_or(full, outline)

# Combine to 3 color channel and color outline red
combine = cv2.merge([combine, combine, combine])
combine[outline > 120] = (57,0,204)

cv2.imshow('combine', combine)
cv2.waitKey()

Benchmarks using IPython

In [3]: %timeit combine()
782 µs ± 10.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Using the Vectorized feature of Numpy, it seems to be pretty fast

Upvotes: 1

Chris
Chris

Reputation: 8126

try this:

public func maskWithColor2( color:UIColor) -> UIImage {

    UIGraphicsBeginImageContextWithOptions(self.size, false, UIScreen.main.scale)
    let context = UIGraphicsGetCurrentContext()!

    color.setFill()

    context.translateBy(x: 0, y: self.size.height)
    context.scaleBy(x: 1.0, y: -1.0)

    let rect = CGRect(x: 0.0, y: 0.0, width: self.size.width, height: self.size.height)
    context.draw(self.cgImage!, in: rect)

    context.setBlendMode(CGBlendMode.sourceIn)
    context.addRect(rect)
    context.drawPath(using: CGPathDrawingMode.fill)

    let coloredImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return coloredImage!
}

Upvotes: -1

Related Questions