MTKView displaying CIImage goes wrong when image changes size - ios

I'm using a fairly standard subclass of MTKView that displays a CIImage, for the purposes of rapidly updating using CIFilters. Code for the draw() function is below.
The problem I have is that only the portions of the view that are covered by the image, in this case scaledImage in the code below, are actually redrawn. This means that if the new image is smaller than the previous image, I can still see portions of the old image poking out from behind it.
Is there a way to clear the content of the drawable's texture when the size of the contained image changes, so that this does not happen?
override func draw() {
guard let image = image,
let targetTexture = currentDrawable?.texture,
let commandBuffer = commandQueue.makeCommandBuffer()
else { return }
let bounds = CGRect(origin: CGPoint.zero, size: drawableSize)
let scaleX = drawableSize.width / image.extent.width
let scaleY = drawableSize.height / image.extent.height
let scale = min(scaleX, scaleY)
let width = image.extent.width * scale
let height = image.extent.height * scale
let originX = (bounds.width - width) / 2
let originY = (bounds.height - height) / 2
let scaledImage = image
.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
.transformed(by: CGAffineTransform(translationX: originX, y: originY))
ciContext.render(
scaledImage,
to: targetTexture,
commandBuffer: commandBuffer,
bounds: bounds,
colorSpace: colorSpace
)
guard let drawable = currentDrawable else { return }
commandBuffer.present(drawable)
commandBuffer.commit()
super.draw()
}

MTKView has clearColor property which you can use like so:
clearColor = MTLClearColorMake(1, 1, 1, 1)
Alternatively, if you'd like to use CIImage, you can just create one with CIColor like so:
CIImage(color: CIColor(red: 1, green: 1, blue: 1)).cropped(to: CGRect(origin: .zero, size: drawableSize))

What you can do is tell the ciContext to clear the texture before you draw to it.
So before you do this,
ciContext.render(
scaledImage,
to: targetTexture,
commandBuffer: commandBuffer,
bounds: bounds,
colorSpace: colorSpace)
clear the context by first.
let renderDestination = CIRenderDestination(
width: Int(sizeToClear.width),
height: Int(sizeToClear.height),
pixelFormat: colorPixelFormat,
commandBuffer: nil) { () -> MTLTexture in
return drawable.texture
}
do {
try ciContext.startTask(toClear: renderDestination)
} catch {
}
For more info see the documentation

I have the following solution which works, but I'm not very pleased with it as it feels hacky so please, somebody tell me a better way of doing it!
What I'm doing is tracking when the size of the image changes and setting a flag called needsRefresh. During the view's draw() function I then check to see if this flag is set and, if it is, I create a new CIImage from the view's clearColor that's the same size as the drawable and render this before continuing to the image I actually want to render.
This seems really inefficient, so better solutions are welcome!
Here's the additional drawing code:
if needsRefresh {
let color = UIColor(mtkColor: clearColor)
let clearImage = CIImage(cgImage: UIImage.withColor(color, size: bounds.size)!.cgImage!)
ciContext.render(
clearImage,
to: targetTexture,
commandBuffer: commandBuffer,
bounds: bounds,
colorSpace: colorSpace
)
needsRefresh = false
}
Here's the UIColor extension for converting the clear color:
extension UIColor {
convenience init(mtkColor: MTLClearColor) {
let red = CGFloat(mtkColor.red)
let green = CGFloat(mtkColor.green)
let blue = CGFloat(mtkColor.blue)
let alpha = CGFloat(mtkColor.alpha)
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
}
And finally, a small extension for creating CIImages from a given background colour:
extension UIImage {
static func withColor(_ color: UIColor, size: CGSize = CGSize(width: 10, height: 10)) -> UIImage? {
UIGraphicsBeginImageContext(size)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: .zero, size: size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}

Related

Sharing PKCanvasView with a subview as an Image

I have a PKCanvasView with a subview of a UIImageView.
I'd like to share the view as a UIImage. This would be the UIImageView with the PKCanvasView.drawing that is above it.
Here is how I am getting the drawing portion. I am just unsure how to get the UIImageView with it.
private func image(from canvas: PKCanvasView) -> UIImage {
let drawing = canvas.drawing
let visibleRect = canvas.bounds
let image = drawing.image(from: visibleRect, scale: UIScreen.main.scale)
return image
}
Update:
Here is what I am currently trying:
let format = UIGraphicsImageRendererFormat.default()
format.opaque = false
let visibleRect = CGRect(x: 0, y: 0, width: self.imageView.frame.size.width, height: self.imageView.frame.size.height)
let image = UIGraphicsImageRenderer(size: self.imageView.frame.size, format: format).image { context in
imageView.image?.draw(in: visibleRect, blendMode: .normal, alpha: 1.0)
canvasView.drawing.image(from: visibleRect, scale: UIScreen.main.scale).draw(at: .zero)
}
The problem I am facing with this is my image is being stretched from top to bottom. Some images more than others.
You will need to create a new graphics context, draw your image into it and then draw your drawing on top of it. Your code should look something like this:
let format = UIGraphicsImageRendererFormat.default()
format.opaque = false
let image = UIGraphicsImageRenderer(size: imageView.frame.size, format: format).image { context in
imageView.image?.draw(at: .zero)
canvas.drawing.image(from: imageView.frame, scale: UIScreen.main.scale).draw(at: .zero)
}

Visible Artifacts Compositing two UIImages with UIGraphicsContext using Hard Light transfer mode

I am compositing two UIImages together using a UIGraphicsContext.
It works for the most part, but using the Hard Light CGBlendMode, I am getting strange artifacts that are not present if I use a Hard Light transfer mode in Photoshop.
I would try to use Core Image, but I need to be able to control the opacity of the top layer being transferred with Hard Light, and as far as I can tell that is not possible.
Here is the image created with a UIGraphicsContext:
Here it is using the same layers and opacity in Photoshop:
Here is the base image:
And the layer being composited with 60% opacity using Hard Light:
I have tried setting the context interpolation quality to high, using a transparency layer, but nothing has made any improvement.
My code is below. Does anyone know how to fix this, or alternative ways of achieving the same results that I am getting in Photoshop?
public extension UIImage {
public func compImagesWithImage(_ topImg: UIImage, blendMode: CGBlendMode, opacity: CGFloat) -> UIImage? {
let baseImg = self
if let cgImage = baseImg.cgImage {
let baseW = CGFloat(baseImg.cgImage!.width as size_t)
let baseH = CGFloat(baseImg.cgImage!.height as size_t)
UIGraphicsBeginImageContext(CGSize(width: baseW, height: baseH))
if let context = UIGraphicsGetCurrentContext() {
context.saveGState();
context.interpolationQuality = .high
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: baseH)
context.concatenate(flipVertical)
context.setAlpha(1.0)
context.setBlendMode(CGBlendMode.normal)
context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: CGSize(width: CGFloat(baseW), height: CGFloat(baseH))))
context.setAlpha(opacity)
context.setBlendMode(blendMode)
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.draw(topImg.cgImage!, in: CGRect(origin: CGPoint.zero, size: CGSize(width: CGFloat(baseW), height: CGFloat(baseH))))
context.endTransparencyLayer()
context.restoreGState();
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage
}
}
return nil
}
}
This fixed it: Higlight Artifacts When Using Hard Light Blend Mode
I just had to set the alpha of the base layer to 0.99.
Here is the updated code:
public func compImagesWithImage(_ topImg: UIImage, blendMode: CGBlendMode, opacity: CGFloat) -> UIImage? {
let baseImg = self
if let cgImage = baseImg.cgImage {
let baseW = CGFloat(baseImg.cgImage!.width as size_t)
let baseH = CGFloat(baseImg.cgImage!.height as size_t)
UIGraphicsBeginImageContext(CGSize(width: baseW, height: baseH))
if let context = UIGraphicsGetCurrentContext() {
context.saveGState();
context.interpolationQuality = .high
let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: baseH)
context.concatenate(flipVertical)
context.setAlpha(0.99)
context.setBlendMode(CGBlendMode.normal)
context.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: CGSize(width: CGFloat(baseW), height: CGFloat(baseH))))
context.setAlpha(opacity)
context.setBlendMode(blendMode)
context.beginTransparencyLayer(auxiliaryInfo: nil)
context.draw(topImg.cgImage!, in: CGRect(origin: CGPoint.zero, size: CGSize(width: CGFloat(baseW), height: CGFloat(baseH))))
context.endTransparencyLayer()
context.restoreGState();
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage
}
}

Convert Square UIImage to round UIIMage for use with CAEmitterCell.contents

I have a custom UI component that produces an image of a round ball with some labels superimposed over top of it.
I am taking a snapshot of the component using the following UIView extension I found here on StackOverflow.
I am taking the resulting UIImage and using it in a CAEmitterCell.
My problem is that the snapshot image is square - my round ball on a white background. I would like the background to be clear when it is emitted but I can't seem to find a way to do it.
Is there any way I can modify the UIImage to make its corners transparent?
Thanks.
extension UIView {
/// Create snapshot
///
/// - parameter rect: The `CGRect` of the portion of the view to return. If `nil` (or omitted),
/// return snapshot of the whole view.
///
/// - returns: Returns `UIImage` of the specified portion of the view.
func snapshot(of rect: CGRect? = nil) -> UIImage? {
// snapshot entire view
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
drawHierarchy(in: bounds, afterScreenUpdates: true)
let wholeImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// if no `rect` provided, return image of whole view
guard let image = wholeImage, let rect = rect else { return wholeImage }
// otherwise, grab specified `rect` of image
let scale = image.scale
let scaledRect = CGRect(x: rect.origin.x * scale, y: rect.origin.y * scale, width: rect.size.width * scale, height: rect.size.height * scale)
guard let cgImage = image.cgImage?.cropping(to: scaledRect) else { return nil }
return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
}
}
After figuring out how to word my question here, I thought of another way to search for my answer and found a solution.
Someone posted an extension to turn white backgrounds transparent as an answer to a previous question. White didn't work for me, but a simple edit and a name change made the extension work for black backgrounds instead of white.
extension UIImage {
func imageByMakingBlackBackgroundTransparent() -> UIImage? {
let image = UIImage(data: UIImageJPEGRepresentation(self, 1.0)!)!
let rawImageRef: CGImage = image.cgImage!
let colorMasking: [CGFloat] = [0, 0, 0, 0, 0, 0]
UIGraphicsBeginImageContext(image.size);
let maskedImageRef = rawImageRef.copy(maskingColorComponents: colorMasking)
UIGraphicsGetCurrentContext()?.translateBy(x: 0.0,y: image.size.height)
UIGraphicsGetCurrentContext()?.scaleBy(x: 1.0, y: -1.0)
UIGraphicsGetCurrentContext()?.draw(maskedImageRef!, in: CGRect.init(x: 0, y: 0, width: image.size.width, height: image.size.height))
let result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result
}
}
My image was originally on a white background, but there was also white in the important part of the image. I temporarily changed my background to black, took a snapshot, then converted black to transparent, then changed my background back to white.
The end result is that my problem is fixed.
Thanks to anyone who took any time to read or think about this problem.

How get round layer mask?

I have a square CIImage.
I want to add round layer of a mask and to impose it on this image.
Should I use CIFilter?
To do it entirely with CoreImage you can follow this recipe:
let image = CIImage(contentsOf: URL(string: "https://s3.amazonaws.com/uploads.hipchat.com/27364/1957261/bJERqq8O2V3NVok/upload.jpg")!)
// Construct a circle
let circle = CIFilter(name: "CIRadialGradient", withInputParameters:[
"inputRadius0":100,
"inputRadius1":100,
"inputColor0":CIColor(red: 1, green: 1, blue: 1, alpha:1),
"inputColor1":CIColor(red: 0, green: 0, blue: 0, alpha:0)
])?.outputImage
// Turn the circle into an alpha mask
let mask = CIFilter(name: "CIMaskToAlpha", withInputParameters: [kCIInputImageKey:circle!])?.outputImage
// Apply the mask to the input image
let combine = CIFilter(name: "CIBlendWithAlphaMask", withInputParameters:[
kCIInputMaskImageKey:mask!,
kCIInputImageKey:image!
])
let output = combine?.outputImage
Although this only works for circles
Use like this , For round image with border
self.profileImageView.layer.cornerRadius = self.profileImageView.frame.size.width / 2
self.profileImageView.clipsToBounds = YES;
self.profileImageView.layer.borderWidth = 3.0f;
self.profileImageView.layer.borderColor = UIColor.whiteColor.CGColor
maybe this can help to create a round image using core graphic.. they create a round blur image using core graphic.
Blur specific part of an image (rectangular, circular)?
I solve my problem :).
I use extension.
override var outputImage : CIImage!
{
if let inputImage = inputImage {
//making UIImage from CIImage
let uiImage = UIImage(ciImage: inputImage)
//use extension
let roundImage = uiImage.roundedRectImageFromImage(image:uiImage, imageSize: CGSize.init(width: uiImage.size.width/2, height: uiImage.size.width/2), cornerRadius: uiImage.size.width/2)
//get back CIImage
let ciImage = CIImage(image: roundImage)
///There other code.....
}
extension UIImage {
func roundedRectImageFromImage(image:UIImage,imageSize:CGSize,cornerRadius:CGFloat)->UIImage{
UIGraphicsBeginImageContextWithOptions(imageSize,false,0.0)
let bounds=CGRect(origin: CGPoint.zero, size: imageSize)
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
image.draw(in: bounds)
let finalImage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage!
}
}

Make UIImage a Circle without Losing quality? (swift)

The image becomes blurry once applying roundImage:
Making a UIImage to a circle form
extension UIImage
{
func roundImage() -> UIImage
{
let newImage = self.copy() as! UIImage
let cornerRadius = self.size.height/2
UIGraphicsBeginImageContextWithOptions(self.size, false, 1.0)
let bounds = CGRect(origin: CGPointZero, size: self.size)
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
newImage.drawInRect(bounds)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return finalImage
}
}
Why are you using bezierpath? Just set cornerradius for uiimageview.
If your image is larger than the imageview then you have to resize your image to your imageview size and then set cornerradius for that uiimageview.
It will work. Works for me
Replace the following line
UIGraphicsBeginImageContextWithOptions(self.size, false, 1.0)
with
UIGraphicsBeginImageContextWithOptions(self.size, view.opaque , 0.0)
try this one
let image = UIImageView(frame: CGRectMake(0, 0, 100, 100))
I recommend that you can use AlamofireImage (https://github.com/Alamofire/AlamofireImage)
It's very easily to make rounded image or circle image without losing quality.
just like this:
let image = UIImage(named: "unicorn")!
let radius: CGFloat = 20.0
let roundedImage = image.af_imageWithRoundedCornerRadius(radius)
let circularImage = image.af_imageRoundedIntoCircle()
Voila!
Your issue is that you are using scale 1, which is the lowest "quality".
Setting the scale to 0 will use the device scale, which just uses the image as is.
A side note: Functions inside a class that return a new instance of that class can be implemented as class functions. This makes it very clear what the function does. It does not manipulate the existing image. It returns a new one.
Since you were talking about circles, I also corrected your code so it will now make a circle of any image and crop it. You might want to center this.
extension UIImage {
class func roundImage(image : UIImage) -> UIImage? {
// copy
guard let newImage = image.copy() as? UIImage else {
return nil
}
// start context
UIGraphicsBeginImageContextWithOptions(newImage.size, false, 0.0)
// bounds
let cornerRadius = newImage.size.height / 2
let minDim = min(newImage.size.height, newImage.size.width)
let bounds = CGRect(origin: CGPointZero, size: CGSize(width: minDim, height: minDim))
UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).addClip()
// new image
newImage.drawInRect(bounds)
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// crop
let maybeCrop = UIImage.crop(finalImage, cropRect: bounds)
return maybeCrop
}
class func crop(image: UIImage, cropRect : CGRect) -> UIImage? {
guard let imgRef = CGImageCreateWithImageInRect(image.CGImage, cropRect) else {
return nil
}
return UIImage(CGImage: imgRef)
}
}

Resources