Memory leak when resizing UIImage - ios

I've read through multiple threads concerning the topic but my problem still persists.
When I'm resizing an Image with following code:
extension UIImage {
func thumbnailWithMaxSize(image:UIImage, maxSize: CGFloat) -> UIImage {
let width = image.size.width
let height = image.size.height
var sizeX: CGFloat = 0
var sizeY: CGFloat = 0
if width > height {
sizeX = maxSize
sizeY = maxSize * height/width
}
else {
sizeY = maxSize
sizeX = maxSize * width/height
}
UIGraphicsBeginImageContext(CGSize(width: sizeX, height: sizeY))
let rect = CGRect(x: 0.0, y: 0.0, width: sizeX, height: sizeY)
UIGraphicsBeginImageContext(rect.size)
draw(in: rect)
let thumbnail = UIGraphicsGetImageFromCurrentImageContext()!;
UIGraphicsEndImageContext()
return thumbnail
}
override func viewDidLoad() {
super.viewDidLoad()
let lionImage = UIImage(named: "lion.jpg")!
var thumb = UIImage()
autoreleasepool {
thumb = lionImage.thumbnailWithMaxSize(image: lionImage, maxSize: 2000)
}
myImageView.image = thumb
}
...the memory is not released. So when I navigate through multiple ViewControllers (e.g. with a PageViewController) I end up getting memory warnings and the app eventually crashes.
I also tried to load the image via UIImage(contentsOfFile: path) without success.
Any suggestions?

I noticed your code beginning two contexts but only ending one.
Here's my extension, which is basically the same as your's. Since I'm not having memory issues, it looks like that may be the issue.
extension UIImage {
public func resizeToRect(_ size : CGSize) -> UIImage {
UIGraphicsBeginImageContext(size)
self.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
return resizedImage!
}
}

The problem is this:
UIGraphicsGetImageFromCurrentImageContext()
returns an autoreleased UIImage. The autorelease pool holds on to this image until your code returns control to the runloop, which you do not do for a long time. To solve this problem, make thumb = nil after using it.
var thumb = UIImage()
autoreleasepool {
thumb = lionImage.thumbnailWithMaxSize(image: lionImage, maxSize: 2000)
let myImage:UIImage = UIImage(UIImagePNGRepresentation(thumb));
thumb = nil
}
myImageView.image = myImage

Related

Cropping AVCapturePhoto to overlay rectangle displayed on screen

I am trying to take a picture of a thin piece of metal, cropped to the outline displayed on the screen. I have seen almost every other post on here, but nothing has got it for me yet. This image will then be used for analysis by a library. I can get some cropping to happen, but never to the rectangle displayed. I have tried rotating the image before cropping, and calculating the rect based on the rectangle on screen.
Here is my capture code. PreviewView is the container, videoLayer is for the AVCapture video.
// Photo capture delegate
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
guard let imgData = photo.fileDataRepresentation(), let uiImg = UIImage(data: imgData), let cgImg = uiImg.cgImage else {
return
}
print("Original image size: ", uiImg.size, "\nCGHeight: ", cgImg.height, " width: ", cgImg.width)
print("Orientation: ", uiImg.imageOrientation.rawValue)
guard let img = cropImage(image: uiImg) else {
return
}
showImage(image: img)
}
func cropImage(image: UIImage) -> UIImage? {
print("Image size before crop: ", image.size)
//Get the croppedRect from function below
let croppedRect = calculateRect(image: image)
guard let imgRet = image.cgImage?.cropping(to: croppedRect) else {
return nil
}
return UIImage(cgImage: imgRet)
}
func calculateRect(image: UIImage) -> CGRect {
let originalSize: CGSize
let visibleLayerFrame = self.rectangleView.bounds
// Calculate the rect from the rectangleview to translate to the image
let metaRect = (self.videoLayer.metadataOutputRectConverted(fromLayerRect: visibleLayerFrame))
print("MetaRect: ", metaRect)
// check orientation
if (image.imageOrientation == UIImage.Orientation.left || image.imageOrientation == UIImage.Orientation.right) {
originalSize = CGSize(width: image.size.height, height: image.size.width)
} else {
originalSize = image.size
}
let cropRect: CGRect = CGRect(x: metaRect.origin.x * originalSize.width, y: metaRect.origin.y * originalSize.height, width: metaRect.size.width * originalSize.width, height: metaRect.size.height * originalSize.height).integral
print("Calculated Rect: ", cropRect)
return cropRect
}
func showImage(image: UIImage) {
if takenImage != nil {
takenImage = nil
}
takenImage = UIImageView(image: image)
takenImage.frame = CGRect(x: 10, y: 50, width: 400, height: 1080)
takenImage.contentMode = .scaleAspectFit
print("Cropped Image Size: ", image.size)
self.previewView.addSubview(takenImage)
}
And here is along the line of what I keep getting.
What am I screwing up?
I managed to solve the issue for my use case.
private func cropToPreviewLayer(from originalImage: UIImage, toSizeOf rect: CGRect) -> UIImage? {
guard let cgImage = originalImage.cgImage else { return nil }
// This previewLayer is the AVCaptureVideoPreviewLayer which the resizeAspectFill and videoOrientation portrait has been set.
let outputRect = previewLayer.metadataOutputRectConverted(fromLayerRect: rect)
let width = CGFloat(cgImage.width)
let height = CGFloat(cgImage.height)
let cropRect = CGRect(x: (outputRect.origin.x * width), y: (outputRect.origin.y * height), width: (outputRect.size.width * width), height: (outputRect.size.height * height))
if let croppedCGImage = cgImage.cropping(to: cropRect) {
return UIImage(cgImage: croppedCGImage, scale: 1.0, orientation: originalImage.imageOrientation)
}
return nil
}
usage of the piece of code for my case:
let rect = CGRect(x: 25, y: 150, width: 325, height: 230)
let croppedImage = self.cropToPreviewLayer(from: image, toSizeOf: rect)
self.imageView.image = croppedImage
The world of UIKit has the TOP LEFT corner as 0,0.
The 0,0 point in the AVFoundation world is the BOTTOM LEFT corner.
So you have to translate by rotating 90 degrees.
That's why your image is bonkers.
Also remember that because of the origin translation the following rules apply:
X is actually up and down
Y is actually left and right
width and height are swapped
Also be aware that the UIImageView content mode setting WILL impact how your image scales. You might want to use .scaleAspectFill and NOT AspectFit if you really want to see how your image looks in the UIView.
I used this code snippet to see what was behind the curtain:
// figure out how to cut/crop this
let realImageRect = AVMakeRect(aspectRatio: image.size, insideRect: (self.cameraPreview?.frame)!)
NSLog("real image rectangle = \(realImageRect.debugDescription)")
The 'cameraPreview' reference above is the control you're using for your AV Capture Session.
Good luck!

UIImageJpgRepresentation doubles image resolution

I am trying to save an image coming from the iPhone camera to a file. I use the following code:
try UIImageJPEGRepresentation(toWrite, 0.8)?.write(to: tempURL, options: NSData.WritingOptions.atomicWrite)
This results in a file double the resolution of the toWrite UIImage. I confirmed in the watch expressions that creating a new UIImage from UIImageJPEGRepresentation doubles its resolution
-> toWrite.size CGSize (width = 3264, height = 2448)
-> UIImage(data: UIImageJPEGRepresentation(toWrite, 0.8)).size CGSize? (width = 6528, height = 4896)
Any idea why this would happen, and how to avoid it?
Thanks
Your initial image has scale factor = 2, but when you init your image from data you will get image with scale factor = 1. Your way to solve it is to control the scale and init the image with scale property:
#available(iOS 6.0, *)
public init?(data: Data, scale: CGFloat)
Playground code that represents the way you can set scale
extension UIImage {
class func with(color: UIColor, size: CGSize) -> UIImage? {
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(size, true, 2.0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.setFillColor(color.cgColor)
context.fill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
let image = UIImage.with(color: UIColor.orange, size: CGSize(width: 100, height: 100))
if let image = image {
let scale = image.scale
if let data = UIImageJPEGRepresentation(image, 0.8) {
if let newImage = UIImage(data: data, scale: scale) {
debugPrint(newImage?.size)
}
}
}

After cropping images in Swift I'm getting results tilted with 90 degrees - why?

I'm using a nice github plugin for Swift https://github.com/budidino/ShittyImageCrop responsible for cropping the image.
I need aspect ratio 4:3, so I call this controller like this:
let shittyVC = ShittyImageCropVC(frame: (self.navigationController?.view.frame)!, image: image!, aspectWidth: 3, aspectHeight: 4)
self.navigationController?.present(shittyVC, animated: true, completion: nil)
Now, when I provide horizontal image (wider than taller) - cropped result is fine - I see a photo with aspect ratio 4:3 as an output.
But when I provide vertical image and try to cropp it - I'm seeing tilted output. So for example, when normal photo is like this:
vertical - and tilted - one looks like this:
(sorry for low res here). Why does it get shifted to one side?
I suspect the problem might be somewhere in the logic of the crop-button:
func tappedCrop() {
print("tapped crop")
var imgX: CGFloat = 0
if scrollView.contentOffset.x > 0 {
imgX = scrollView.contentOffset.x / scrollView.zoomScale
}
let gapToTheHole = view.frame.height/2 - holeRect.height/2
var imgY: CGFloat = 0
if scrollView.contentOffset.y + gapToTheHole > 0 {
imgY = (scrollView.contentOffset.y + gapToTheHole) / scrollView.zoomScale
}
let imgW = holeRect.width / scrollView.zoomScale
let imgH = holeRect.height / scrollView.zoomScale
print("IMG x: \(imgX) y: \(imgY) w: \(imgW) h: \(imgH)")
let cropRect = CGRect(x: imgX, y: imgY, width: imgW, height: imgH)
let imageRef = img.cgImage!.cropping(to: cropRect)
let croppedImage = UIImage(cgImage: imageRef!)
var path:String = NSTemporaryDirectory() + "tempFile.jpeg"
if let data = UIImageJPEGRepresentation(croppedImage, 0.95) { //0.4 - compression quality
//print("low compression is here")
try? data.write(to: URL(fileURLWithPath: path), options: [.atomic])
}
self.dismiss(animated: true, completion: nil)
}
ShittyImageCrop saves cropped images directly to your album and I couldn't replicate your issue using vertical images.
I see you used UIImageJPEGRepresentation compared to UIImageWriteToSavedPhotosAlbum from ShittyImageCrop and it seems other people also have problems with image rotation after using UIImageJPEGRepresentation.
Look up iOS UIImagePickerController result image orientation after upload and iOS JPEG images rotated 90 degrees
EDIT
try implementing fixOrientation() from https://stackoverflow.com/a/27775741/611879
add fixOrientation():
func fixOrientation(img:UIImage) -> UIImage {
if (img.imageOrientation == UIImageOrientation.Up) {
return img
}
UIGraphicsBeginImageContextWithOptions(img.size, false, img.scale)
let rect = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
img.drawInRect(rect)
let normalizedImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return normalizedImage
}
and then do it before using UIImageJPEGRepresentation:
if let data = UIImageJPEGRepresentation(fixOrientation(croppedImage), 0.95) {
try? data.write(to: URL(fileURLWithPath: path), options: [.atomic])
}
EDIT 2
please edit the init method of ShittyImageCrop by replacing img = image with:
if (image.imageOrientation != .up) {
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
var rect = CGRect.zero
rect.size = image.size
image.draw(in: rect)
img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
} else {
img = image
}

Cropping the same UIImage with the same CGRect gives different results

I have the following functional in the app:
the user takes (or chooses) an image (hereinafter originalImage).
the originalImage is sent to some external API, which returns the array of coordinates of dots that I need to add to originalImage.
Since the dots are always located in one area (face), I want to crop the originalImage close to the face borders and display to the user only the result of crop.
After the crop result is displayed I'm adding dots to it one by one.
Here is the code that does the job (except sending image, let's say it has already happened)
class ScanResultViewController{
#IBOutlet weak var scanPreviewImageView: UIImageView!
let originalImage = ORIGINAL_IMAGE //meaning we already have it
let scanDots = [["x":123, "y":123], ["x":234, "y":234]]//total 68 coordinates
var cropRect:CGRect!
override func viewDidLoad() {
super.viewDidLoad()
self.setScanImage()
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.animateScan(0)
}
func setScanImage(){
self.cropRect = self.getCropRect(self.scanDots, sourceImage:self.originalImage)
let croppedImage = self.originalImage.imageAtRect(self.cropRect)
self.scanPreviewImageView.image = croppedImage
self.scanPreviewImageView.contentMode = .ScaleAspectFill
}
func animateScan(index:Int){
let i = index
self.originalImage = self.addOnePointToImage(self.originalImage, pointImage: GREEN_DOT!, point: self.scanDots[i])
let croppedImage = self.originalImage.imageAtRect(self.cropRect)
self.scanPreviewImageView.image = croppedImage
self.scanPreviewImageView.contentMode = .ScaleAspectFill
if i < self.scanDots.count-1{
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
self.animateScan(i+1)
}
}
}
func addOnePointToImage(sourceImage:UIImage, pointImage:UIImage, point: Dictionary<String,CGFloat>)->UIImage{
let rect = CGRect(x: 0, y: 0, width: sourceImage.size.width, height: sourceImage.size.height)
UIGraphicsBeginImageContextWithOptions(sourceImage.size, true, 0)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)
CGContextFillRect(context, rect)
sourceImage.drawInRect(rect, blendMode: .Normal, alpha: 1)
let pointWidth = sourceImage.size.width/66.7
pointImage.drawInRect(CGRectMake(point["x"]!-pointWidth/2, point["y"]!-pointWidth/2, pointWidth, pointWidth), blendMode: .Normal, alpha: 1)
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
func getCropRect(points: Array<Dictionary<String,CGFloat>>, sourceImage:UIImage)->CGRect{
var topLeft:CGPoint = CGPoint(x: points[0]["x"]!, y: points[0]["y"]!)
var topRight:CGPoint = CGPoint(x: points[0]["x"]!, y: points[0]["y"]!)
var bottomLeft:CGPoint = CGPoint(x: points[0]["x"]!, y: points[0]["y"]!)
var bottomRight:CGPoint = CGPoint(x: points[0]["x"]!, y: points[0]["y"]!)
for p in points{
if p["x"]<topLeft.x {topLeft.x = p["x"]!}
if p["y"]<topLeft.y {topLeft.y = p["y"]!}
if p["x"]>topRight.x {topRight.x = p["x"]!}
if p["y"]<topRight.y {topRight.y = p["y"]!}
if p["x"]<bottomLeft.x {bottomLeft.x = p["x"]!}
if p["y"]>bottomLeft.y {bottomLeft.y = p["y"]!}
if p["x"]>bottomRight.x {bottomRight.x = p["x"]!}
if p["y"]>bottomRight.y {bottomRight.y = p["y"]!}
}
let rect = CGRect(x: topLeft.x, y: topLeft.y, width: (topRight.x-topLeft.x), height: (bottomLeft.y-topLeft.y))
return rect
}
}
extension UIImage{
public func imageAtRect(rect: CGRect) -> UIImage {
let imageRef: CGImageRef = CGImageCreateWithImageInRect(self.CGImage, rect)!
let subImage: UIImage = UIImage(CGImage: imageRef)
return subImage
}
}
The problem is that in setScanImage the desired area is accurately cropped and displayed, but when animateScan method is called a different area of the same image is cropped (and displayed) though cropRect is the same and the size of originalImage is totally the same.
Any ideas, guys?
By the way if I display originalImage without cropping it everything works smoothly.
So finally after approximately 10 hours net time (and a lot of help of the stackoverflow community:-) I managed to fix the problem:
In the function addOnePointToImage you need to change the following:
In this line:
UIGraphicsBeginImageContextWithOptions(sourceImage.size, true, 0)
you need to change the last argument (which stands for scale) to 1:
UIGraphicsBeginImageContextWithOptions(sourceImage.size, true, 1)
That totally resolves the issue.

How to apply a tintColor to a UIImage?

I have a UIImage that is a small symbol that is all black. The UIImage is getting set in a custom UIButton subclass I have. Is it possible to have the image to apply the tintColor to it, so instead of the black image it changes colors to whatever the tintColor is?
I'm just trying to avoid creating new assets.
// here I want defaultImageName (that is black) to use the tintColor (that is white)
[self setImage:[UIImage imageNamed:defaultImageName] forState:UIControlStateNormal];
If you are just supporting iOS 7 you can use tintColor and UIImageRenderingModeAlwaysTemplate
This article covers that:
https://www.captechconsulting.com/blogs/ios-7-tutorial-series-tint-color-and-easy-app-theming
If you need to support an earlier version you may want to consider this thread
How would I tint an image programmatically on the iPhone?
Swift 4, copy-paste solution
#IBOutlet weak var iconImageView: UIImageView!
iconImageView.image = UIImage(imageLiteralResourceName: "myImageName").withRenderingMode(.alwaysTemplate)
iconImageView.tintColor = UIColor.red
On iOS 13+ you can use the following:
UIImage(named: "img_name")?.withTintColor(.red)
https://developer.apple.com/documentation/uikit/uiimage/3327300-withtintcolor
Try this:
func tinted(with color: UIColor) -> UIImage? {
defer { UIGraphicsEndImageContext() }
UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
color.set()
self.withRenderingMode(.alwaysTemplate).draw(in: CGRect(origin: .zero, size: self.size))
return UIGraphicsGetImageFromCurrentImageContext()
}
For example:
button.setImage(UIImage(systemName: "checkmark.circle")?.tinted(with: .systemGray), for: .normal)
Here's how I use tint colors and opacities in IOS 9 with Swift -
//apply a color to an image
//ref - http://stackoverflow.com/questions/28427935/how-can-i-change-image-tintcolor
//ref - https://www.captechconsulting.com/blogs/ios-7-tutorial-series-tint-color-and-easy-app-theming
func getTintedImage() -> UIImageView {
var image :UIImage
var imageView :UIImageView
image = UIImage(named: "someAsset")!
let size : CGSize = image.size
let frame : CGRect = CGRectMake((UIScreen.mainScreen().bounds.width-86)/2, 600, size.width, size.height)
let redCover : UIView = UIView(frame: frame)
redCover.backgroundColor = UIColor.redColor()
redCover.layer.opacity = 0.75
imageView = UIImageView();
imageView.image = image.imageWithRenderingMode(UIImageRenderingMode.Automatic)
imageView.addSubview(redCover)
return imageView
}
Why not use image filtering
btn.setImage(image, for: UIControl.State.normal)
btn.setImage(image.disabled, for: UIControl.State.disabled)
Use CoreImage to do image filter
extension UIImage
{
/// Create a grayscale image with alpha channel. Is 5 times faster than grayscaleImage().
/// - Returns: The grayscale image of self if available.
var disabled: UIImage?
{
// Create image rectangle with current image width/height * scale
let pixelSize = CGSize(width: self.size.width * self.scale, height: self.size.height * self.scale)
let imageRect = CGRect(origin: CGPoint.zero, size: pixelSize)
// Grayscale color space
let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceGray()
// Create bitmap content with current image size and grayscale colorspace
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
if let context: CGContext = CGContext(data: nil, width: Int(pixelSize.width), height: Int(pixelSize.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
{
// Draw image into current context, with specified rectangle
// using previously defined context (with grayscale colorspace)
guard let cg = self.cgImage else{
return nil
}
context.draw(cg, in: imageRect)
// Create bitmap image info from pixel data in current context
if let imageRef: CGImage = context.makeImage(){
let bitmapInfoAlphaOnly = CGBitmapInfo(rawValue: CGImageAlphaInfo.alphaOnly.rawValue)
guard let context = CGContext(data: nil, width: Int(pixelSize.width), height: Int(pixelSize.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfoAlphaOnly.rawValue) else{
return nil
}
context.draw(cg, in: imageRect)
if let mask: CGImage = context.makeImage() {
// Create a new UIImage object
if let newCGImage = imageRef.masking(mask){
// Return the new grayscale image
return UIImage(cgImage: newCGImage, scale: self.scale, orientation: self.imageOrientation)
}
}
}
}
// A required variable was unexpected nil
return nil
}
}
Of course, in Swift 5
Swift5 Extension
extension UIImage {
var template: UIImage? {
return self.withRenderingMode(.alwaysTemplate)
}
}
Usage:
UIImageView
let imgView = UIImageView()
imgView.tintColor = UIColor.red
imgView.image = UIImage(named: "IMAGE_NAME_HERE")?.template
UIButton
let button = UIButton(type: .custom)
button.tintColor = UIColor.red
button.setImage(UIImage(named: "IMAGE_NAME_HERE")?.template, for: .normal)
Use this simple extension to UIImageView
#IBInspectable var tintedColor: UIColor{
get{
return tintColor
}
set{
image = image?.withRenderingMode(.alwaysTemplate)
tintColor = newValue
}
}

Resources