Reputation: 4607
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:
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.
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
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:
Upvotes: 3
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
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
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