Cropping An Image (CIImage) To A UIBezierPath - swift

I have seen this question asked in varying ways on Stack Overflow, but I have not seen an implementation that leverages anything but UIKit to develop an approach. For my use case, I have a CIImage and UIBezierPath (rather, a set of points from a machine learning feature detection model) that I would like to use a "mask" for the CIImage.
For example, I may have an image of a person's face. Subsequently, I may have a UIBezierPath that creates a closed shape around one of their eyes. Ideally, I would like to "crop" my CIImage to encompass the UIBezierPath. I recognize that since the final image will need to be square/rectangle, I would like to have any area outside of the UIBezierPath either black or transparent so it can be used as a mask for a future composition.
At present, I have code set up as such;
func doCrop(image: CIImage) -> CIImage {
// The drawing context (here to show I need to use CIContext for another use later on in this function.
let ciContext = CIContext()
// The original source image
let source = image
// Setup the path
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 400, y: 0))
path.addLine(to: CGPoint(x: 600, y: 200))
path.addLine(to: CGPoint(x: 400, y: 400))
path.addLine(to: CGPoint(x: 200, y: 400))
path.addLine(to: CGPoint(x: 100, y: 200))
path.close()
// Test
let cropped = source.cropped(to: path.bounds)
}
To be honest, I'm really unsure where to go from here. From what I have ascertained online, I see solutions that either implement UIKit to create an image context and draw a path, or solutions that only mask a UIImageView (which is not relevant for my needs, as I will need to draw this into a CIContext and pass through a Metal shader at a later point).

Related

Single shadow for multiple objects in Swift?

I want to draw a dialog bubble so I need 2 objects:
arrow (can be drawn with UIBezierPath only)
rounded rect (can be drawn with UIBezierPath or UIView+corner radius)
Example of code generated by paintcode:
//// Rectangle Drawing
let rectanglePath = UIBezierPath(roundedRect: CGRect(x: 68, y: 38, width: 101, height: 38), cornerRadius: 5)
UIColor.gray.setFill()
rectanglePath.fill()
//// Polygon Drawing
let polygonPath = UIBezierPath()
polygonPath.move(to: CGPoint(x: 118, y: 25.5))
polygonPath.addLine(to: CGPoint(x: 125.36, y: 38.25))
polygonPath.addLine(to: CGPoint(x: 110.64, y: 38.25))
polygonPath.close()
UIColor.gray.setFill()
polygonPath.fill()
The problem is if I add shadow to this code then it is applied twice and may overlap the objects.
I could replace 2 objects with one but the complexity of code is increased significantly (drawing rect and 3 points will be replaced with ~50 points including "control points").
So is it possible to join rectanglePath and polygonPath to draw single shadow somehow without this complexity?

Is it possible to create an instance of a particular location on the screen (using auto layout)?

I have 4 playing cards on the screen. At the press of a button, I want one of the cards at random to move to the middle of the top half of the screen. Is it possible to create an instance of a constraint (eg: centerXAnchor with constant 0, and centerYAnchor with constant -200) so that I can use CGAffineTransform and move the random image to this point?
Ive tried creating an instance of a CGRect Frame:
let destination = CGPoint(x: 10, y: 10)
but this does not move evenly across devices.
An affine transformation matrix is used to rotate, scale, translate, or skew the objects you draw in a graphics context.
I don't think CGAffineTransform is the ideal thing to use for this task. You aren't doing any the above things (rotate, scale, translate, or skew).
I think you would likely be best using UIView.animateWithDuration
let cardSize = CGSize(width: 100, height: 100)
let card = UIView(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: cardSize))
UIView.animate(withDuration: 1.0) {
card.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: cardSize)
}

ARKit: Occlude everything in a plane except reference image

I have an image reference that is triggering an experience on ARKit2, and I want to create an occlusion plane that covers everything around my trigger, except the trigger itself. Representatively, it is something like this (placed the image below the plane to show better, but it is in reality on the same level):
As of now, I have no issue with making an occlusion on a regular shape like so:
func setOcclusionMaterial() {
let occlusionPlane = SCNPlane(width: imageReference!.physicalSize.width, height: imageReference!.physicalSize.height)
let holdout = SCNMaterial()
holdout.isDoubleSided = true
holdout.diffuse.contents = CIColor.black
holdout.colorBufferWriteMask = SCNColorMask(rawValue: 0)
occlusionPlane.firstMaterial? = holdout
let occlusionNode = SCNNode()
occlusionNode.eulerAngles.x = -.pi / 2
occlusionNode.geometry = occlusionPlane
node!.addChildNode(occlusionNode)
}
However, I'm not sure how I could create a plane that excludes the image area (a circle).
What I'm trying to do is to place a 3D element that appears to be below my trigger but only visible if seen top-down or with a slight angle.
Hope this is clear enough and thanks for your help if you have any ideas.
Your code does not render an occlusion for me, at least not in the Playground I quickly threw together. Maybe you can create one if my answer does not actually solve your problem.
You have to options here. Either create a custom geometry, in a 3D modelling tool like blender or manual via SCNGeometrySource but that is expansive because you will need a lot of triangles to make it work.
You can try to apply a CALayer as the plane's geometry:
Create a CGPath that is the rectangle with the circular cutout (or any form you'd like) and create a CAShapeLayer from the path.
var path = CGMutablePath()
path.addRect(CGRect(x: 0, y: 0, width: 100, height: 100))
path.addEllipse(in: CGRect(x: 25, y: 25, width: 50, height: 50))
let layer = CAShapeLayer()
layer.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
layer.path = path
layer.fillColor = UIColor.white.cgColor
layer.fillRule = .evenOdd
I could not confirm this actually worked because your occlusion is not quite working for me.

Cut a circle out of a UIView using mask [duplicate]

This question already has answers here:
How can I 'cut' a transparent hole in a UIImage?
(4 answers)
Closed 6 years ago.
In my app I have a square UIView and I want to cut a hole/notch out of the top of. All the tutorials online are all the same and seemed quite straightforward but every single one of them always delivered the exact opposite of what I wanted.
For example this is the code for the custom UIView:
class BottomOverlayView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}
fileprivate func drawCircle(){
let circleRadius: CGFloat = 80
let topMidRectangle = CGRect(x: 0, y: 0, width: circleRadius*2, height: circleRadius*2)
let circle: CAShapeLayer = CAShapeLayer()
circle.position = CGPoint(x: (frame.width/2)-circleRadius, y: 0-circleRadius)
circle.fillColor = UIColor.black.cgColor
circle.path = UIBezierPath(ovalIn: topMidRectangle).cgPath
circle.fillRule = kCAFillRuleEvenOdd
self.layer.mask = circle
self.clipsToBounds = true
}
}
Here is what I hope to achieve (the light blue is the UIView, the dark blue is the background):
But here is what I get instead. (Every single time no matter what I try)
I'm not sure how I would achieve this, aside from making a mask that is already the exact shape that I need. But if I was able to do that then I wouldn't be having this issue in the first place. Does anyone have any tips on how to achieve this?
EDIT: The question that this is supposedly a duplicate of I had already attempted and was not able to get working. Perhaps I was doing it wrong or using it in the wrong context. I wasn't familiar with any of the given methods and also the use of pointers made it seem a bit outdated. The accepted answer does a much better job of explaining how this can be implemented using much more widely used UIBezierPaths and also within the context of a custom UIView.
I'd suggest drawing a path for your mask, e.g. in Swift 3
// BottomOverlayView.swift
import UIKit
#IBDesignable
class BottomOverlayView: UIView {
#IBInspectable
var radius: CGFloat = 100 { didSet { updateMask() } }
override func layoutSubviews() {
super.layoutSubviews()
updateMask()
}
private func updateMask() {
let path = UIBezierPath()
path.move(to: bounds.origin)
let center = CGPoint(x: bounds.midX, y: bounds.minY)
path.addArc(withCenter: center, radius: radius, startAngle: .pi, endAngle: 0, clockwise: false)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
Note, I tweaked this to set the mask in two places:
From layoutSubviews: That way if the frame changes, for example as a result of auto layout (or by manually changing the frame or whatever), it will update accordingly; and
If you update radius: That way, if you're using this in a storyboard or if you change the radius programmatically, it will reflect that change.
So, you can overlay a half height, light blue BottomOverlayView on top of a dark blue UIView, like so:
That yields:
If you wanted to use the "cut a hole" technique suggested in the duplicative answer, the updateMask method would be:
private func updateMask() {
let center = CGPoint(x: bounds.midX, y: bounds.minY)
let path = UIBezierPath(rect: bounds)
path.addArc(withCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
let mask = CAShapeLayer()
mask.fillRule = .evenOdd
mask.path = path.cgPath
layer.mask = mask
}
I personally find the path within a path with even-odd rule to be a bit counter-intuitive. Where I can (such as this case), I just prefer to just draw the path of the mask. But if you need a mask that has a cut-out, this even-odd fill rule approach can be useful.

Creating transparent gradient and use it as an alpha-mask in SpriteKit

I am trying to make a gradient and use it as an alpha mask. Right now, I am able to make an image similar to this (from black to transparent):
This is the code which I use to make all this:
private func createImage(width: CGFloat, height: CGFloat) -> CGImageRef?{
if let ciFilter = CIFilter(name: "CILinearGradient"){
let ciContext = CIContext()
ciFilter.setDefaults()
let startColor = CIColor(red: 0, green: 0, blue: 0, alpha: 0)
let endColor = CIColor(red: 0, green: 0, blue: 0, alpha: 1)
let startVector = CIVector(x: 0, y: height-10)
let endVector = CIVector(x: 0, y: height-22)
ciFilter.setValue(startColor, forKey: "inputColor0")
ciFilter.setValue(endColor, forKey: "inputColor1")
ciFilter.setValue(startVector, forKey: "inputPoint0")
ciFilter.setValue(endVector, forKey: "inputPoint1")
if let outputImage = ciFilter.outputImage {
let cgImage:CGImageRef = ciContext.createCGImage(outputImage, fromRect: CGRect(x: 0, y: 0, width: width, height: height))
return cgImage
}
}
return nil
}
For me, this way works pretty much perfect because I create a mask only once in my code (when GUI element is created) so performance is not affected at all.
The thing is that I actually need a resulting image (gradient texture) to look like this (gradients should have fixed height, but size of black area may vary):
Because CILinearGradient filter doesn't have an inputImage parameter, I can't chain two filters one after another. But rather I have to create an image, and apply a filter, then to create another image, and apply a new filter.
I solved this just like described above. Also I had to extend a process in three steps (create upper gradient, create middle, black area and create lower gradient) and then to combine all that into one image.
I wonder is there anything about CILinearGradient filter I am not aware of ? Maybe there is an easier way to attack this problem and make a complex gradient?
Note that I am using SpriteKit and solving this with CAGradientLayer is not a way I want to go.
I'm not sure Core Image is the right tool for the job here — you'd probably be better off using CGGradient functions to draw your image.
Aside: What you're asking would be possible but cumbersome in CoreImage. Think of it by analogy to what you'd do to create these images as a user of Photoshop or other common graphics software... the CoreImage way would be something like this:
Create a layer for the upper gradient, fill the entire canvas with your gradient, and transform the layer to the proper position and size
Create a layer for the middle black portion, fill the entire canvas with black, and transform the layer to the proper position and size
Ditto #1, but for the bottom gradient.
You could do this with a combination of generator filters, transform filters, and compositing filters...
gradient -> transform -\
}-> composite -\
solid color -> transform -/ \
}-> composite
gradient -> transform ————————————————--/
But setting that up would be ugly.
Likewise, in Photoshop you could use selection tools and fill/gradient tools to create your gradient-fill-gradient design completely within a single layer... that's what drawing a single image using CoreGraphics is analogous to.
So, how to do that single-pass CG drawing? Something like this...
func createImage(width: CGFloat, _ height: CGFloat) -> CGImage {
let gradientOffset: CGFloat = 10 // white at bottom and top before/after gradient
let gradientLength: CGFloat = 12 // height of gradient region
// create colors and gradient for drawing with
let space = CGColorSpaceCreateDeviceRGB()
let startColor = UIColor.whiteColor().CGColor
let endColor = UIColor.blackColor().CGColor
let colors: CFArray = [startColor, endColor]
let gradient = CGGradientCreateWithColors(space, colors, [0,1])
// start an image context
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext()
// fill the whole thing with white (that'll show through at the ends when done)
CGContextSetFillColorWithColor(context, startColor)
CGContextFillRect(context, CGRect(x: 0, y: 0, width: width, height: height))
// draw top gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: gradientOffset), CGPoint(x: 0, y: gradientOffset + gradientLength), [])
// fill solid black middle
CGContextSetFillColorWithColor(context, endColor)
CGContextFillRect(context, CGRect(x: 0, y: gradientOffset + gradientLength, width: width, height: height - 2 * gradientOffset - 2 * gradientLength))
// draw bottom gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: height - gradientOffset), CGPoint(x: 0, y: height - gradientOffset - gradientLength), [])
return CGBitmapContextCreateImage(context)!
}