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.
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:
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
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!
}
Related
I'm trying to recreate Apple's festival lights image in SwiftUI (screenshot from Apple India's website). Expected result:
Here's what I've managed to achieve so far:
MY UNDERSTANDING SO FAR: Images are not Shapes, so we can't stroke their borders, but I also found that shadow() modifier places shadows on image borders just fine. So, I need a way to customize the shadow somehow and understand how it works.
WHAT I'VE TRIED SO FAR: Besides the code above, I tried to unsuccessfully convert a given SF Symbol to a Shape using Vision framework's contour detection, based on my understanding of this article: https://www.iosdevie.com/p/new-in-ios-14-vision-contour-detection
Can someone please guide me on how I would go about doing this, preferably using SF symbols only.
Looks like the Vision contour detection isn't a bad approach after all. I was just missing a few things, as helpfully pointed out by #DonMag. Here's my final answer using SwiftUI, in case someone's interested.
First, we create an InsettableShape:
struct MKSymbolShape: InsettableShape {
var insetAmount = 0.0
let systemName: String
var trimmedImage: UIImage {
let cfg = UIImage.SymbolConfiguration(pointSize: 256.0)
// get the symbol
guard let imgA = UIImage(systemName: systemName, withConfiguration: cfg)?.withTintColor(.black, renderingMode: .alwaysOriginal) else {
fatalError("Could not load SF Symbol: \(systemName)!")
}
// we want to "strip" the bounding box empty space
// get a cgRef from imgA
guard let cgRef = imgA.cgImage else {
fatalError("Could not get cgImage!")
}
// create imgB from the cgRef
let imgB = UIImage(cgImage: cgRef, scale: imgA.scale, orientation: imgA.imageOrientation)
.withTintColor(.black, renderingMode: .alwaysOriginal)
// now render it on a white background
let resultImage = UIGraphicsImageRenderer(size: imgB.size).image { ctx in
UIColor.white.setFill()
ctx.fill(CGRect(origin: .zero, size: imgB.size))
imgB.draw(at: .zero)
}
return resultImage
}
func path(in rect: CGRect) -> Path {
// cgPath returned from Vision will be in rect 0,0 1.0,1.0 coordinates
// so we want to scale the path to our view bounds
let inputImage = self.trimmedImage
guard let cgPath = detectVisionContours(from: inputImage) else { return Path() }
let scW: CGFloat = (rect.width - CGFloat(insetAmount)) / cgPath.boundingBox.width
let scH: CGFloat = (rect.height - CGFloat(insetAmount)) / cgPath.boundingBox.height
// we need to invert the Y-coordinate space
var transform = CGAffineTransform.identity
.scaledBy(x: scW, y: -scH)
.translatedBy(x: 0.0, y: -cgPath.boundingBox.height)
if let imagePath = cgPath.copy(using: &transform) {
return Path(imagePath)
} else {
return Path()
}
}
func inset(by amount: CGFloat) -> some InsettableShape {
var shape = self
shape.insetAmount += amount
return shape
}
func detectVisionContours(from sourceImage: UIImage) -> CGPath? {
let inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
let contourRequest = VNDetectContoursRequest()
contourRequest.revision = VNDetectContourRequestRevision1
contourRequest.contrastAdjustment = 1.0
contourRequest.maximumImageDimension = 512
let requestHandler = VNImageRequestHandler(ciImage: inputImage, options: [:])
try! requestHandler.perform([contourRequest])
if let contoursObservation = contourRequest.results?.first {
return contoursObservation.normalizedPath
}
return nil
}
}
Then we create our main view:
struct PreviewView: View {
var body: some View {
ZStack {
LinearGradient(colors: [.black, .purple], startPoint: .top, endPoint: .bottom)
.edgesIgnoringSafeArea(.all)
MKSymbolShape(systemName: "applelogo")
.stroke(LinearGradient(colors: [.yellow, .orange, .pink, .red], startPoint: .top, endPoint: .bottom), style: StrokeStyle(lineWidth: 8, lineCap: .round, dash: [2.0, 21.0]))
.aspectRatio(CGSize(width: 30, height: 36), contentMode: .fit)
.padding()
}
}
}
Final look:
We can use the Vision framework with VNDetectContourRequestRevision1 to get a cgPath:
func detectVisionContours(from sourceImage: UIImage) -> CGPath? {
let inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
let contourRequest = VNDetectContoursRequest.init()
contourRequest.revision = VNDetectContourRequestRevision1
contourRequest.contrastAdjustment = 1.0
contourRequest.maximumImageDimension = 512
let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:])
try! requestHandler.perform([contourRequest])
if let contoursObservation = contourRequest.results?.first {
return contoursObservation.normalizedPath
}
return nil
}
The path will be based on a 0,0 1.0,1.0 coordinate space, so to use it we need to scale the path to our desired size. It also uses inverted Y-coordinates, so we'll need to flip it also:
// cgPath returned from Vision will be in rect 0,0 1.0,1.0 coordinates
// so we want to scale the path to our view bounds
let scW: CGFloat = targetRect.bounds.width / cgPth.boundingBox.width
let scH: CGFloat = targetRect.bounds.height / cgPth.boundingBox.height
// we need to invert the Y-coordinate space
var transform = CGAffineTransform.identity
.scaledBy(x: scW, y: -scH)
.translatedBy(x: 0.0, y: -cgPth.boundingBox.height)
return cgPth.copy(using: &transform)
Couple notes...
When using UIImage(systemName: "applelogo"), we get an image with "font" characteristics - namely, empty space. See this https://stackoverflow.com/a/71743787/6257435 and this https://stackoverflow.com/a/66293917/6257435 for some discussion.
So, we could use it directly, but it makes the path scaling and translation a bit complex.
So, instead of this "default":
we can use a little code to "trim" the space for a more usable image:
Then we can use the path from Vision as the path of a CAShapeLayer, along with these layer properties: .lineCap = .round / .lineWidth = 8 / .lineDashPattern = [2.0, 20.0] (for example) to get a "dotted line" stroke:
Then we can use that same path on a shape layer as a mask on a gradient layer:
and finally remove the image view so we see only the view with the masked gradient layer:
Here's example code to produce that:
import UIKit
import Vision
class ViewController: UIViewController {
let myOutlineView = UIView()
let myGradientView = UIView()
let shapeLayer = CAShapeLayer()
let gradientLayer = CAGradientLayer()
let defaultImageView = UIImageView()
let trimmedImageView = UIImageView()
var defaultImage: UIImage!
var trimmedImage: UIImage!
var visionPath: CGPath!
// an information label
let infoLabel: UILabel = {
let v = UILabel()
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.textAlignment = .center
v.numberOfLines = 0
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
// get the system image at 240-points (so we can get a good path from Vision)
// experiment with different sizes if the path doesn't appear smooth
let cfg = UIImage.SymbolConfiguration(pointSize: 240.0)
// get "applelogo" symbol
guard let imgA = UIImage(systemName: "applelogo", withConfiguration: cfg)?.withTintColor(.black, renderingMode: .alwaysOriginal) else {
fatalError("Could not load SF Symbol: applelogo!")
}
// now render it on a white background
self.defaultImage = UIGraphicsImageRenderer(size: imgA.size).image { ctx in
UIColor.white.setFill()
ctx.fill(CGRect(origin: .zero, size: imgA.size))
imgA.draw(at: .zero)
}
// we want to "strip" the bounding box empty space
// get a cgRef from imgA
guard let cgRef = imgA.cgImage else {
fatalError("Could not get cgImage!")
}
// create imgB from the cgRef
let imgB = UIImage(cgImage: cgRef, scale: imgA.scale, orientation: imgA.imageOrientation)
.withTintColor(.black, renderingMode: .alwaysOriginal)
// now render it on a white background
self.trimmedImage = UIGraphicsImageRenderer(size: imgB.size).image { ctx in
UIColor.white.setFill()
ctx.fill(CGRect(origin: .zero, size: imgB.size))
imgB.draw(at: .zero)
}
defaultImageView.image = defaultImage
defaultImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(defaultImageView)
trimmedImageView.image = trimmedImage
trimmedImageView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(trimmedImageView)
myOutlineView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myOutlineView)
myGradientView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myGradientView)
// next step button
let btn = UIButton()
btn.setTitle("Next Step", for: [])
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.backgroundColor = .systemRed
btn.layer.cornerRadius = 8
btn.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(btn)
infoLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(infoLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// inset default image view 20-points on each side
// height proportional to the image
// near the top
defaultImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
defaultImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
defaultImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
defaultImageView.heightAnchor.constraint(equalTo: defaultImageView.widthAnchor, multiplier: defaultImage.size.height / defaultImage.size.width),
// inset trimmed image view 40-points on each side
// height proportional to the image
// centered vertically
trimmedImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
trimmedImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
trimmedImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
trimmedImageView.heightAnchor.constraint(equalTo: trimmedImageView.widthAnchor, multiplier: self.trimmedImage.size.height / self.trimmedImage.size.width),
// add outline view on top of trimmed image view
myOutlineView.topAnchor.constraint(equalTo: trimmedImageView.topAnchor, constant: 0.0),
myOutlineView.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
myOutlineView.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
myOutlineView.bottomAnchor.constraint(equalTo: trimmedImageView.bottomAnchor, constant: 0.0),
// add gradient view on top of trimmed image view
myGradientView.topAnchor.constraint(equalTo: trimmedImageView.topAnchor, constant: 0.0),
myGradientView.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
myGradientView.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
myGradientView.bottomAnchor.constraint(equalTo: trimmedImageView.bottomAnchor, constant: 0.0),
// button and info label below
btn.topAnchor.constraint(equalTo: defaultImageView.bottomAnchor, constant: 20.0),
btn.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
btn.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
infoLabel.topAnchor.constraint(equalTo: btn.bottomAnchor, constant: 20.0),
infoLabel.leadingAnchor.constraint(equalTo: trimmedImageView.leadingAnchor, constant: 0.0),
infoLabel.trailingAnchor.constraint(equalTo: trimmedImageView.trailingAnchor, constant: 0.0),
infoLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 60.0),
])
// setup the shape layer
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
// this will give use round dots for the shape layer's stroke
shapeLayer.lineCap = .round
shapeLayer.lineWidth = 8
shapeLayer.lineDashPattern = [2.0, 20.0]
// setup the gradient layer
let c1: UIColor = .init(red: 0.95, green: 0.73, blue: 0.32, alpha: 1.0)
let c2: UIColor = .init(red: 0.95, green: 0.25, blue: 0.45, alpha: 1.0)
gradientLayer.colors = [c1.cgColor, c2.cgColor]
myOutlineView.layer.addSublayer(shapeLayer)
myGradientView.layer.addSublayer(gradientLayer)
btn.addTarget(self, action: #selector(nextStep), for: .touchUpInside)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let pth = pathSetup()
else {
fatalError("Vision could not create path")
}
self.visionPath = pth
shapeLayer.path = pth
gradientLayer.frame = myGradientView.bounds.insetBy(dx: -8.0, dy: -8.0)
let gradMask = CAShapeLayer()
gradMask.strokeColor = UIColor.red.cgColor
gradMask.fillColor = UIColor.clear.cgColor
gradMask.lineCap = .round
gradMask.lineWidth = 8
gradMask.lineDashPattern = [2.0, 20.0]
gradMask.path = pth
gradMask.position.x += 8.0
gradMask.position.y += 8.0
gradientLayer.mask = gradMask
nextStep()
}
var idx: Int = -1
#objc func nextStep() {
idx += 1
switch idx % 5 {
case 1:
defaultImageView.isHidden = true
trimmedImageView.isHidden = false
infoLabel.text = "\"applelogo\" system image - with trimmed empty-space bounding-box."
case 2:
myOutlineView.isHidden = false
shapeLayer.opacity = 1.0
infoLabel.text = "Dotted outline shape using Vision detected path."
case 3:
myOutlineView.isHidden = true
myGradientView.isHidden = false
infoLabel.text = "Use Dotted outline shape as a gradient layer mask."
case 4:
trimmedImageView.isHidden = true
view.backgroundColor = .black
infoLabel.text = "View by itself with Dotted outline shape as a gradient layer mask."
default:
view.backgroundColor = .systemBlue
defaultImageView.isHidden = false
trimmedImageView.isHidden = true
myOutlineView.isHidden = true
myGradientView.isHidden = true
shapeLayer.opacity = 0.0
infoLabel.text = "Default \"applelogo\" system image - note empty-space bounding-box."
}
}
func pathSetup() -> CGPath? {
// get the cgPath from the image
guard let cgPth = detectVisionContours(from: self.trimmedImage)
else {
print("Failed to get path!")
return nil
}
// cgPath returned from Vision will be in rect 0,0 1.0,1.0 coordinates
// so we want to scale the path to our view bounds
let scW: CGFloat = myOutlineView.bounds.width / cgPth.boundingBox.width
let scH: CGFloat = myOutlineView.bounds.height / cgPth.boundingBox.height
// we need to invert the Y-coordinate space
var transform = CGAffineTransform.identity
.scaledBy(x: scW, y: -scH)
.translatedBy(x: 0.0, y: -cgPth.boundingBox.height)
return cgPth.copy(using: &transform)
}
func detectVisionContours(from sourceImage: UIImage) -> CGPath? {
let inputImage = CIImage.init(cgImage: sourceImage.cgImage!)
let contourRequest = VNDetectContoursRequest.init()
contourRequest.revision = VNDetectContourRequestRevision1
contourRequest.contrastAdjustment = 1.0
contourRequest.maximumImageDimension = 512
let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:])
try! requestHandler.perform([contourRequest])
if let contoursObservation = contourRequest.results?.first {
return contoursObservation.normalizedPath
}
return nil
}
}
I have created a progressview according to number images as you can see in below code.
let view = UIView()
view.backgroundColor = .clear
let progressView = UIProgressView(frame: CGRect(x: 0, y: 0, width: frameOfParentView.width/3 - 8, height: 30))
progressView.progressViewStyle = .default
progressView.progress = 0.0
progressView.tintColor = .red
progressView.trackTintColor = .gray
progressView.layoutIfNeeded()
view.addSubview(progressView)
self.arrayOfProgrssView.append(progressView)
As you can see in gif at starting point tintColor alpha is little bit less but when it tense to reach at 100% it is fully red.
I also tried with below code:-
progressView.progressTintColor = .red
but did not get expected result.
To perform animation,
DispatchQueue.main.asyncAfter(deadline: .now() + 0.001) {
UIView.animate(withDuration: self.animationInMS) {
progressView.setProgress(1, animated: true)
}
}
progressView.layoutIfNeeded()
Issue in iOS 15:-
As you see below result with other colour.
Note:- I have checked in iOS 12.4 it's working properly as you can see into image.
Please let me know is anything require from my side.
Thanks in advance
This does appear to be "new behavior" where the alpha value matches the percent completion -- although, after some quick searching I haven't found any documentation on it.
One option as a work-around: set the .progressImage instead of the tint color.
So, use your favorite code to generate a solid-color image, such as:
extension UIImage {
public static func withColor(_ color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
let image = UIGraphicsImageRenderer(size: size, format: format).image { rendererContext in
color.setFill()
rendererContext.fill(CGRect(origin: .zero, size: size))
}
return image
}
}
Then, instead of:
progressView.tintColor = .red
use:
let img = UIImage.withColor(.red)
progressView.progressImage = img
Not fully tested, but to avoid the need to change existing code, you might also try:
extension UIProgressView {
open override var tintColor: UIColor! {
didSet {
let img = UIImage.withColor(tintColor)
progressImage = img
}
}
}
Now you can keep your existing progressView.tintColor = .red
I want to display a semi transparent text field over the uiimageview. I can’t choose static color for textfield because some images are bright while other images are dark. I want to automatically adapt text field color based on the colors behind it. Is there an easy solution for that?
UPD:
The effect I want to achieve is:
If UIImage is dark where my textfield should be placed, set textfield background color to white with 0.5 opacity.
If UIImage is light where my textfield should be placed, set textfield background color to black with 0.5 opacity.
So I want to somehow calculate the average color of the uiimageview in place where I want to put my textfield and then check if it is light or dark. I don't know, how to get the screenshot of the particular part my uiimageview and get it's average color. I want it to be optimised. I guess working with UIGraphicsImageRenderer isn't a good choice, that's why I ask this question. I know how to do it with UIGraphicsImageRenderer, but I don't think that my way is good enough.
"Brightness" of an image is not a straight-forward thing. You may or may not find this suitable.
If you search for determine brightness of an image you'll find plenty of documentation on it - likely way more than you want.
One common way to calculate the "brightness" of a pixel is to use the sum of:
red component * 0.299
green component * 0.587
blue component * 0.114
This is because we perceive the different colors at different "brightness" levels.
So, you'll want to loop through each pixel in the area of the image where you want to place your label (or textField), get the average brightness, and then decide what's "dark" and what's "light".
As an example, using this background image:
I generated a 5 x 8 grid of labels, looped through getting the "brightness" of the image in the rect under each label's frame, and then set the background and text color based on the brightness calculation (values range from 0 to 255, so I used < 127 is dark, >= 127 is light):
This is the code I used:
extension CGImage {
var brightness: Double {
get {
// common formula to get "average brightness"
let bytesPerPixel = self.bitsPerPixel / self.bitsPerComponent
let imageData = self.dataProvider?.data
let ptr = CFDataGetBytePtr(imageData)
var x = 0
var p = 0
var result: Double = 0
for _ in 0..<self.height {
for _ in 0..<self.width {
let r = ptr![p+0]
let g = ptr![p+1]
let b = ptr![p+2]
result += (0.299 * Double(r) + 0.587 * Double(g) + 0.114 * Double(b))
p += bytesPerPixel
x += 1
}
}
let bright = result / Double (x)
return bright
}
}
}
extension UIImage {
// get the "brightness" of self (entire image)
var brightness: Double {
get {
return (self.cgImage?.brightness)!
}
}
// get the "brightness" in a sub-rect of self
func brightnessIn(_ rect: CGRect) -> Double {
guard let cgImage = self.cgImage else { return 0.0 }
guard let croppedCGImage = cgImage.cropping(to: rect) else { return 0.0 }
return croppedCGImage.brightness
}
}
class ImageBrightnessViewController: UIViewController {
let imgView: UIImageView = {
let v = UIImageView()
v.contentMode = .center
v.backgroundColor = .green
v.clipsToBounds = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
var labels: [UILabel] = []
override func viewDidLoad() {
super.viewDidLoad()
// load an image
guard let img = UIImage(named: "bkg640x360") else { return }
imgView.image = img
let w = img.size.width
let h = img.size.height
// set image view's width and height equal to img width and height
view.addSubview(imgView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
imgView.widthAnchor.constraint(equalToConstant: w),
imgView.heightAnchor.constraint(equalToConstant: h),
imgView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
imgView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// use stack views to create a 5 x 8 grid of labels
let outerStackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.spacing = 32
v.distribution = .fillEqually
return v
}()
for _ in 1...5 {
let vStack = UIStackView()
vStack.axis = .vertical
vStack.spacing = 12
vStack.distribution = .fillEqually
for _ in 1...8 {
let label = UILabel()
label.textAlignment = .center
vStack.addArrangedSubview(label)
labels.append(label)
}
outerStackView.addArrangedSubview(vStack)
}
let padding: CGFloat = 12.0
imgView.addSubview(outerStackView)
NSLayoutConstraint.activate([
outerStackView.topAnchor.constraint(equalTo: imgView.topAnchor, constant: padding),
outerStackView.leadingAnchor.constraint(equalTo: imgView.leadingAnchor, constant: padding),
outerStackView.trailingAnchor.constraint(equalTo: imgView.trailingAnchor, constant: -padding),
outerStackView.bottomAnchor.constraint(equalTo: imgView.bottomAnchor, constant: -padding),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
guard let img = imgView.image else {
return
}
labels.forEach { v in
if let sv = v.superview {
// convert label frame to imgView coordinate space
let rect = sv.convert(v.frame, to: imgView)
// get the "brightness" of that rect from the image
// it will be in the range of 0 - 255
let d = img.brightnessIn(rect)
// set the text of the label to that value
v.text = String(format: "%.2f", d)
// just using 50% here... adjust as desired
if d > 127.0 {
// if brightness is than 50%
// black translucent background with white text
v.backgroundColor = UIColor.black.withAlphaComponent(0.5)
v.textColor = .white
} else {
// if brightness is greater than or equal to 50%
// white translucent background with black text
v.backgroundColor = UIColor.white.withAlphaComponent(0.5)
v.textColor = .black
}
}
}
}
}
As you can see, when getting the average of a region of a photo, you'll quite often not be completely happy with the result. That's why it's much more common to see one or the other, with a contrasting order and either a shadow or glow around the frame.
I am trying image crop tutorial
I want to make app that crop image and add sticker.
subviews are imageview’s subview ( imageview.addsubview(subview) )
please check this Image link
Everything is fine, but I can’t make final image.
I want to make final image using original resolution but my fuction’s result is too low resolution
I lost 5 days about this
example)
scrollview’s rect 0, 0, 200, 400
imageview size 7680 x 4320
I tried below code.
but result quality is very low. because the resolution is 200x400(scrollview’s rect) .
func screenshot() -> UIImage {
guard let imageview = imageview else { return UIImage() }
UIGraphicsBeginImageContextWithOptions(self.scrollView.bounds.size, false, UIScreen.main.scale)
let offset = self.scrollView.contentOffset
guard let thisContext = UIGraphicsGetCurrentContext() else { return UIImage() }
thisContext.translateBy(x: -offset.x, y: -offset.y)
self.scrollView.layer.render(in: thisContext)
let visibleScrollViewImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return visibleScrollViewImage ?? UIImage()
}
please let me know how to high quality resolution final image including sticker subviews
how to get one high quality image using
Using that method, you get an image of what is rendered on the screen.
What you want to do is calculate the "sticker" size and placement relative to the scaled image and then combine the images.
You can use this extension to overlay one image on another:
extension UIImage {
func overlayWith(image: UIImage, posX: CGFloat, posY: CGFloat) -> UIImage {
let rFormat = UIGraphicsImageRendererFormat()
let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
let newImage = renderer.image {
(context) in
self.draw(at: .zero)
image.draw(at: CGPoint(x: posX, y: posY))
}
return newImage
}
}
Use it by calling (for example):
let combinedImage = bkgImage.overlayWith(image: stickerImage, posX: 1000.0, posY: 1000.0)
That says "Create a new UIImage by overlaying the sticker image onto the background image at x: 1000, y:1000"
Remember that you'll need to translate your coordinates... So, if you are showing your 7680 x 4320 image in a 200 x 400 scrollview (not taking any zooming into account here), the on-screen image size will be 711 x 400. If the user places the sticker at 100, 50, the actual position on the original size image will be:
let scaleFactor = bkgImage.size.height / 400.0
let x = 100.0 * scaleFactor
let y = 50.0 * scaleFactor
// scaleFactor equals 10.8
// x equals 100 * 10.8 == 1080
// y equals 50 * 10.8 == 540
let combinedImage = bkgImage.overlayWith(image: stickerImage, posX: x, posY: y)
Here is a basic sample you can try out.
It starts with a background image of 5120 x 2880:
and a "sticker" image at 512 x 512:
And the result, with the sticker placed at x: 1000, y:1000. Top image is original background (aspectFit), middle image is the combined image (again, aspectFit), and the bottom image is the actual size combined image in a scroll view:
Use this source code to run that example (all code, no #IBoutlet):
import UIKit
extension UIImage {
func overlayWith(image: UIImage, posX: CGFloat, posY: CGFloat) -> UIImage {
let rFormat = UIGraphicsImageRendererFormat()
let renderer = UIGraphicsImageRenderer(size: size, format: rFormat)
let newImage = renderer.image {
(context) in
self.draw(at: .zero)
image.draw(at: CGPoint(x: posX, y: posY))
}
return newImage
}
}
class ViewController: UIViewController {
let origImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFit
v.backgroundColor = .yellow
return v
}()
let modImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .scaleAspectFit
v.backgroundColor = .yellow
return v
}()
let actualSizeImageView: UIImageView = {
let v = UIImageView()
v.translatesAutoresizingMaskIntoConstraints = false
v.contentMode = .topLeft
v.backgroundColor = .yellow
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
guard let bkgImage = UIImage(named: "background"),
let stickerImage = UIImage(named: "sticker") else {
fatalError("missing images")
}
view.backgroundColor = .systemGreen
view.addSubview(origImageView)
view.addSubview(modImageView)
view.addSubview(scrollView)
scrollView.addSubview(actualSizeImageView)
let g = view.safeAreaLayoutGuide
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
origImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 10.0),
origImageView.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
origImageView.widthAnchor.constraint(equalToConstant: 200.0),
origImageView.heightAnchor.constraint(equalToConstant: 120.0),
modImageView.topAnchor.constraint(equalTo: origImageView.bottomAnchor, constant: 10.0),
modImageView.centerXAnchor.constraint(equalTo: origImageView.centerXAnchor, constant: 0.0),
modImageView.widthAnchor.constraint(equalTo: origImageView.widthAnchor),
modImageView.heightAnchor.constraint(equalTo: origImageView.heightAnchor),
scrollView.topAnchor.constraint(equalTo: modImageView.bottomAnchor, constant: 10.0),
scrollView.centerXAnchor.constraint(equalTo: origImageView.centerXAnchor, constant: 0.0),
scrollView.widthAnchor.constraint(equalTo: g.widthAnchor, constant: -10.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -10.0),
actualSizeImageView.topAnchor.constraint(equalTo: sg.topAnchor),
actualSizeImageView.bottomAnchor.constraint(equalTo: sg.bottomAnchor),
actualSizeImageView.leadingAnchor.constraint(equalTo: sg.leadingAnchor),
actualSizeImageView.trailingAnchor.constraint(equalTo: sg.trailingAnchor),
actualSizeImageView.widthAnchor.constraint(equalToConstant: bkgImage.size.width),
actualSizeImageView.heightAnchor.constraint(equalToConstant: bkgImage.size.height),
])
origImageView.image = bkgImage
let combinedImage = bkgImage.overlayWith(image: stickerImage, posX: 1000.0, posY: 1000.0)
modImageView.image = combinedImage
actualSizeImageView.image = combinedImage
}
}
I am attempting to do something that I thought would be possible, but have no idea how to start.
I have created a UIView and would like it to be filled with colour, but in the shape defined by an image mask. i.e. I have a PNG with alpha, and I would like the UIView I have created to be UIColor.blue in the shape of that PNG.
To show just how stupid I am, here is the absolute rubbish I have attempted so far - trying to generate just a simple square doesn't even work for me, hence it's all commented out.
let rocketColourList: [UIColor] = [UIColor.blue, UIColor.red, UIColor.green, UIColor.purple, UIColor.orange]
var rocketColourNum: Int = 0
var rocketAngleNum: Int = 0
var rocketAngle: Double {
return Double(rocketAngleNum) * Double.pi / 4
}
var rocketColour = UIColor.black
public func drawRocket (){
rocketColour.set()
self.clipsToBounds = true
self.backgroundColor = UIColor.red
imageView.image = UIImage(named: ("rocketMask"))
addSubview(imageView)
//maskimage.frame = self.frame
//let someRect = CGRect (x: 1, y: 1, width: 1000, height: 1000)
//let someRect = CGRect (self.frame)
//let fillPath = UIBezierPath(rect:someRect)
//fillPath.fill()
//setNeedsDisplay()
}
}
Set image as template, then set tint color for UIImageView. Something like that:
guard let let image = UIImage(named: "rocketMask")?.withRenderingMode(.alwaysTemplate) else { return }
imageView.image = image
imageView.tintColor = .blue
Also you can do that through the images assets: