How to Crop UIImage considering screen scale - ios

I'm trying to crop a UIImage using a crop box UIView that a user can drag around anywhere in the image view to crop. The logic I'm using to compute the crop rect is as follows:
extension UIImageView {
public func computeCropRect(for sourceFrame : CGRect) -> CGRect {
let widthScale = bounds.size.width / image!.size.width
let heightScale = bounds.size.height / image!.size.height
var x : CGFloat = 0
var y : CGFloat = 0
var width : CGFloat = 0
var height : CGFloat = 0
var offSet : CGFloat = 0
if widthScale < heightScale {
offSet = (bounds.size.height - (image!.size.height * widthScale))/2
x = sourceFrame.origin.x / widthScale
y = (sourceFrame.origin.y - offSet) / widthScale
width = sourceFrame.size.width / widthScale
height = sourceFrame.size.height / widthScale
} else {
offSet = (bounds.size.width - (image!.size.width * heightScale))/2
x = (sourceFrame.origin.x - offSet) / heightScale
y = sourceFrame.origin.y / heightScale
width = sourceFrame.size.width / heightScale
height = sourceFrame.size.height / heightScale
}
return CGRect(x: x, y: y, width: width, height: height)
}
}
The crop box frame looks like this and is positionable anywhere in the frame of the image view by dragging it:
This crop code works just fine until I combine this with another feature I'm trying to support which is the ability to let the user draw using their finger inside of the UIImageView. The code for that looks like this:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touchPoint = touches.first {
let currentPoint = touchPoint.location(in: self)
UIGraphicsBeginImageContextWithOptions(frame.size, false, UIScreen.main.scale)
if let context = UIGraphicsGetCurrentContext() {
image?.draw(in: imageEffectsService.computeAspectFitFrameFor(containerSize: frame.size, imageSize: image!.size), blendMode: .normal, alpha: CGFloat(imageOpacity))
drawLineAt(startPoint: lastTouchPoint, endPoint: currentPoint, currentContext: context, strokeColor: drawColor)
UIGraphicsEndImageContext()
}
}
}
private func drawLineAt(startPoint : CGPoint, endPoint : CGPoint, currentContext : CGContext, strokeColor : UIColor) {
currentContext.beginPath()
currentContext.setLineCap(CGLineCap.round)
currentContext.setLineWidth(brushSize)
currentContext.setStrokeColor(strokeColor.cgColor)
currentContext.move(to: startPoint)
currentContext.addLine(to: endPoint)
currentContext.strokePath()
image = UIGraphicsGetImageFromCurrentImageContext()
}
The crop method loses accuracy once I apply a drawing particularly because of this line:
UIGraphicsBeginImageContextWithOptions(frame.size, false, UIScreen.main.scale)
If instead I use:
UIGraphicsBeginImageContext(frame.size)
My crop code will be accurate but the drawing fidelity will look grainy and low quality because I am not accounting for retina screen devices. My question is how would I modify my crop function to account for the UIScreen.main.scale?

Related

Image Transformation [Mostly Scaling] issue with Google Cloud Textrecognizer when the Text have multiple blocks and lines

I am trying to make boarders about the frames of lines[Got from the google cloud vision text-recognition] on image in iOS Swift.
I've got lines from block and got the frames of line by using line.frame code provided by google.
I saw some Transformation algo which I applied :
private func createScaledFrame(
featureFrame: CGRect,
imageSize: CGSize, viewFrame: CGRect)
-> CGRect {
let viewSize = viewFrame.size
// 2
let resolutionView = viewSize.width / viewSize.height
let resolutionImage = imageSize.width / imageSize.height
// 3
var scale: CGFloat
if resolutionView > resolutionImage {
scale = viewSize.height / imageSize.height
} else {
scale = viewSize.width / imageSize.width
}
// 4
let featureWidthScaled = featureFrame.size.width * scale
let featureHeightScaled = featureFrame.size.height * scale
// 5
let imageWidthScaled = imageSize.width * scale
let imageHeightScaled = imageSize.height * scale
let imagePointXScaled = (viewSize.width - imageWidthScaled) / 2
let imagePointYScaled = (viewSize.height - imageHeightScaled) / 2
// 6
let featurePointXScaled = imagePointXScaled + featureFrame.origin.x * scale
let featurePointYScaled = imagePointYScaled + featureFrame.origin.y * scale
// 7
return CGRect(x: featurePointXScaled,
y: featurePointYScaled,
width: featureWidthScaled,
height: featureHeightScaled)
}
This is causes when lines and blocks increases It becomes small like shown In Image. Problem with scale is about 2.0...2.25 I dont know where I am wrong the issue with the aspect or something i missed here.
The Code where I am applying the above method:
let r = self.createScaledFrame(featureFrame: line.frame, imageSize: imageSize, viewFrame: self.imageView.frame)
let lyr = self.createShapeLayer(frame: r)
self.imageView.layer.addSublayer(lyr)
Create shape layer method :
private func createShapeLayer(frame: CGRect) -> CAShapeLayer {
// 1
let bpath = UIBezierPath(rect: frame)
let shapeLayer = CAShapeLayer()
shapeLayer.path = bpath.cgPath
// 2
shapeLayer.strokeColor = Constants.lineColor
shapeLayer.fillColor = Constants.fillColor
shapeLayer.lineWidth = Constants.lineWidth
return shapeLayer
}
Thanks In advance

iOS: Cropping image in wrong position and size [duplicate]

This question already has answers here:
How to crop a UIImageView to a new UIImage in 'aspect fill' mode?
(2 answers)
Closed 2 years ago.
I'm trying to allow the user to mark an area with her hand and then the image will be cropped to that particular area. The problem is that i've got everything wrong with the cropping sizing, positioning and scaling.
I'm definitely missing something but am not sure what is it that I'm doing wrong?
Here is the original image along with the crop rectangle that the user can mark with his finger:
This is the broken outcome:
Here is my custom UIImageView where I intercept the touch events. This is just for the user to draw the rectangle...
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first?.preciseLocation(in: self){
self.newTouch = touch
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let currentPoint = touch.preciseLocation(in: self)
reDrawSelectionArea(fromPoint: newTouch, toPoint: currentPoint)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.newBoxSelected?(box.frame)
box.frame = CGRect.zero //reset overlay for next tap
}
func reDrawSelectionArea(fromPoint: CGPoint, toPoint: CGPoint) {
//Calculate rect from the original point and last known point
let rect = CGRect(x: min(fromPoint.x, toPoint.x),
y: min(fromPoint.y, toPoint.y),
width: abs(fromPoint.x - toPoint.x),
height: abs(fromPoint.y - toPoint.y));
box.frame = rect
}
This is the actual cropping logic. What am I doing wrong here?
func cropToBounds(image: UIImage, newFrame: CGRect) -> UIImage {
let xScaleFactor = (imageView.image?.size.width)! / (self.imageView.bounds.size.width)
let yScaleFactor = (imageView.image?.size.height)! / (self.imageView.bounds.size.height)
let contextImage: UIImage = UIImage(cgImage: image.cgImage!)
print("NewFrame is: \(newFrame)")
let xPos = newFrame.minX * xScaleFactor
let yPos = newFrame.minY * yScaleFactor
let width = newFrame.size.width * xScaleFactor
let height = newFrame.size.height * xScaleFactor
print("xScaleFactor: \(xScaleFactor)")
print("yScaleFactor: \(yScaleFactor)")
print("xPos: \(xPos)")
print("yPos: \(yPos)")
print("width: \(width)")
print("height: \(height)")
// let rect: CGRect = CGRect(x: xPos, y: yPos , width: width, height: height)
let rect: CGRect = CGRect(x: xPos, y: yPos , width: width, height: height)
// Create bitmap image from context using the rect
let imageRef: CGImage = contextImage.cgImage!.cropping(to: rect)!
// Create a new image based on the imageRef and rotate back to the original orientation
let image: UIImage = UIImage(cgImage: imageRef, scale: image.scale, orientation: image.imageOrientation)
return image
}
You can import AVFoundation and use func AVMakeRect(aspectRatio: CGSize, insideRect boundingRect: CGRect) -> CGRect to get the actual rectangle of the aspect-fit image:
// import this in your class
import AVFoundation
then:
guard let img = imageView.image else {
fatalError("imageView has no image!")
}
// Original size which you want to preserve the aspect ratio of
let aspect: CGSize = img.size
// Rect to fit that size within
let rect: CGRect = CGRect(x: 0, y: 0, width: imageView.bounds.size.width, height: imageView.bounds.size.height)
// resulting size
let resultSize: CGSize = AVMakeRect(aspectRatio: aspect, insideRect: rect).size
// get y-position (1/2 of (imageView height - resulting size)
let resultOrigin: CGPoint = CGPoint(x: 0.0, y: (imageView.bounds.size.height - resultSize.height) / 2.0)
// this is the actual rect for the aspect-fit image
let actualImageRect: CGRect = CGRect(origin: resultOrigin, size: resultSize)
print(actualImageRect)
// you now have the actual rectangle for the image
// on which you can base your scale calculations
}

how to get the rect of an image to crop it

I am building a app where you can crop multiple images. I am using this code directly from apple:
func cropImage(_ inputImage: UIImage, toRect cropRect: CGRect, viewWidth: CGFloat, viewHeight: CGFloat) -> UIImage?
{
let imageViewScale = max(inputImage.size.width / viewWidth,
inputImage.size.height / viewHeight)
// Scale cropRect to handle images larger than shown-on-screen size
let cropZone = CGRect(x:cropRect.origin.x * imageViewScale,
y:cropRect.origin.y * imageViewScale,
width:cropRect.size.width * imageViewScale,
height:cropRect.size.height * imageViewScale)
// Perform cropping in Core Graphics
guard let cutImageRef: CGImage = inputImage.cgImage?.cropping(to:cropZone)
else {
return nil
}
// Return image to UIImage
let croppedImage: UIImage = UIImage(cgImage: cutImageRef)
return croppedImage
}
to crop the image I need a cropRect. I found also a solution in the Internet that I implemented in my code:
func realImageRect() -> CGRect {
let imageViewSize = self.frame.size
let imgSize = self.image?.size
guard let imageSize = imgSize else {
return CGRect.zero
}
let scaleWidth = imageViewSize.width / imageSize.width
let scaleHeight = imageViewSize.height / imageSize.height
let aspect = fmin(scaleWidth, scaleHeight)
var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect)
// Center image
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2
// Add imageView offset
imageRect.origin.x += self.frame.origin.x
imageRect.origin.y += self.frame.origin.y
return imageRect
}
As I already said, the app can crop multiple images. The images are stored in a array. I also have a crop view, which you can drag around the image with a pan gesture
for i in 0..<imageContentView.count {
let cropRect = CGRect(x: croppedViewArray[i].frame.origin.x - imageContentView[i].realImageRect().origin.x, y: croppedViewArray[i].frame.origin.y - imageContentView[i].realImageRect().origin.y, width: croppedViewArray[i].frame.width, height: croppedViewArray[i].frame.height)
print("cropRect", cropRect)
let croppedImage = ImageCrophandler.sharedInstance.cropImage(imageContentView[i].image!, toRect: cropRect, viewWidth: imageContentView[i].frame.width, viewHeight: imageContentView[i].frame.height)
print("cheight", croppedImage!.size.height,"cwidth", croppedImage!.size.width)
arrayOfCropedImages.append(croppedImage!)
}
The problem what I have is, that every cropped image has a different height and widths, but the images should be all the same size.
I figured out that the size gets calculated on which position the crop view is located.

How to get x and y position of UIImage in UIImageView?

I want to get original x and y position of UIImage when we set it in UIImageView with scaleAspectFill.
As we know in scaleAspectFill, some of the portion is clipped. So as per my requirement I want to get x and y value (it may be - value I don't know.).
Here is the original image from gallery
Now I am setting this above image to my app view.
So as above situation, I want to get it's hidden x, y position of image which are clipped.
Can any one tell how to get it?
Use following extension
extension UIImageView {
var imageRect: CGRect {
guard let imageSize = self.image?.size else { return self.frame }
let scale = UIScreen.main.scale
let imageWidth = (imageSize.width / scale).rounded()
let frameWidth = self.frame.width.rounded()
let imageHeight = (imageSize.height / scale).rounded()
let frameHeight = self.frame.height.rounded()
let ratio = max(frameWidth / imageWidth, frameHeight / imageHeight)
let newSize = CGSize(width: imageWidth * ratio, height: imageHeight * ratio)
let newOrigin = CGPoint(x: self.center.x - (newSize.width / 2), y: self.center.y - (newSize.height / 2))
return CGRect(origin: newOrigin, size: newSize)
}
}
Usage
let rect = imageView.imageRect
print(rect)
UI Test
let testView = UIView(frame: rect)
testView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
imageView.superview?.addSubview(testView)
Use below extension to find out accurate details of Image in ImageView.
extension UIImageView {
var contentRect: CGRect {
guard let image = image else { return bounds }
guard contentMode == .scaleAspectFit else { return bounds }
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
let scale: CGFloat
if image.size.width > image.size.height {
scale = bounds.width / image.size.width
} else {
scale = bounds.height / image.size.height
}
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let x = (bounds.width - size.width) / 2.0
let y = (bounds.height - size.height) / 2.0
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
}
How to test
let rect = imgTest.contentRect
print("Image rect:", rect)
Reference: https://www.hackingwithswift.com/example-code/uikit/how-to-find-an-aspect-fit-images-size-inside-an-image-view
If you want to show image like it shows in gallery then you can use contraints
"H:|[v0]|" and "V:|[v0]|" and in imageview use .aspectFit
And if you want the image size you can use imageView.image!.size and calculate the amount of image which is getting cut. In aspectFill the width is matched to screenwidth and accordingly the height gets increased. So I guess you can find how how much amount of image is getting cut.
Try this Library ImageCoordinateSpace
I am not sure if it works for you or not, but it has a feature to convert CGPoint from image coordinates to any view coordinates and vice versa.

UIImageView get the position of the showing Image

I have a UIImageView which shows an UIImage.
The UIImage may change to other UIImage in different size, and the position and the size of the UIImage inside will change according according to it.
My Problem is that i'm trying add a view that will be at the end of the UIImage (which change all the time) and all I can get is the frame of the UIImageView (which stay full screen all the time).
How can i get the "frame" of current showing UIImage ?
Swift 4.2 & 5.0
func calculateRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size
let imgSize = imageView.image?.size
guard let imageSize = imgSize else {
return CGRect.zero
}
let scaleWidth = imageViewSize.width / imageSize.width
let scaleHeight = imageViewSize.height / imageSize.height
let aspect = fmin(scaleWidth, scaleHeight)
var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect)
// Center image
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2
// Add imageView offset
imageRect.origin.x += imageView.frame.origin.x
imageRect.origin.y += imageView.frame.origin.y
return imageRect
}
Swift 3.0
// MARK: - Create Rect
func calculateRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size
let imgSize = imageView.image?.size
guard let imageSize = imgSize, imgSize != nil else {
return CGRect.zero
}
let scaleWidth = imageViewSize.width / imageSize.width
let scaleHeight = imageViewSize.height / imageSize.height
let aspect = fmin(scaleWidth, scaleHeight)
var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect)
// Center image
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2
// Add imageView offset
imageRect.origin.x += imageView.frame.origin.x
imageRect.origin.y += imageView.frame.origin.y
return imageRect
}
For Swift < 3.0
Here is the above method in Swift. Again, assuming that contentMode is set to .ScaleAspectFit If there is no image on the given imageView CGRectZero will be returned.
func calculateRectOfImageInImageView(imageView: UIImageView) -> CGRect {
let imageViewSize = imageView.frame.size
let imgSize = imageView.image?.size
guard let imageSize = imgSize where imgSize != nil else {
return CGRectZero
}
let scaleWidth = imageViewSize.width / imageSize.width
let scaleHeight = imageViewSize.height / imageSize.height
let aspect = fmin(scaleWidth, scaleHeight)
var imageRect = CGRect(x: 0, y: 0, width: imageSize.width * aspect, height: imageSize.height * aspect)
// Center image
imageRect.origin.x = (imageViewSize.width - imageRect.size.width) / 2
imageRect.origin.y = (imageViewSize.height - imageRect.size.height) / 2
// Add imageView offset
imageRect.origin.x += imageView.frame.origin.x
imageRect.origin.y += imageView.frame.origin.y
return imageRect
}
The following will answer your question, assuming your UIImageView used UIViewContentModeAspectFit:
You have to regard the image sizing of the image inside UIImageView. This depends on how you set the contentMode. According your description, I assume you are using UIViewContentModeAspectFit. The resulting image will also be centered in the UIImageView so you also have to consider this for the calculation.
-(CGRect )calculateClientRectOfImageInUIImageView:(UIImageView *)imgView
{
CGSize imgViewSize=imgView.frame.size; // Size of UIImageView
CGSize imgSize=imgView.image.size; // Size of the image, currently displayed
// Calculate the aspect, assuming imgView.contentMode==UIViewContentModeScaleAspectFit
CGFloat scaleW = imgViewSize.width / imgSize.width;
CGFloat scaleH = imgViewSize.height / imgSize.height;
CGFloat aspect=fmin(scaleW, scaleH);
CGRect imageRect={ {0,0} , { imgSize.width*=aspect, imgSize.height*=aspect } };
// Note: the above is the same as :
// CGRect imageRect=CGRectMake(0,0,imgSize.width*=aspect,imgSize.height*=aspect) I just like this notation better
// Center image
imageRect.origin.x=(imgViewSize.width-imageRect.size.width)/2;
imageRect.origin.y=(imgViewSize.height-imageRect.size.height)/2;
// Add imageView offset
imageRect.origin.x+=imgView.frame.origin.x;
imageRect.origin.y+=imgView.frame.origin.y;
return(imageRect);
}
For a better illustration of the differences between the three content modes, see below:
I recommend using built in function AVMakeRectWithAspectRatio.
func AVMakeRectWithAspectRatioInsideRect(_ aspectRatio: CGSize, _ boundingRect: CGRect) -> CGRect
Parameters:
aspectRatio:
The width and height ratio (aspect ratio) you want to maintain.
boundingRect:
The bounding rectangle you want to fit into.
Return Value
Returns a scaled CGRect that maintains the aspect ratio specified by aspectRatio that fits within bounding Rect.
let boundingBox = AVMakeRectWithAspectRatioInsideRect(backgroundImage.size, frame)
Based on the wonderfully simple solution from Janusz, here's what I did:
let visibleRect = AVMakeRect(aspectRatio: CGSize(width: image.size.width, height: image.size.height), insideRect: self.frame)
if visibleRect.contains(point) {
// Do something great here...
}
Swift 3.0
I know its quite late but might help someone in future. Its very simple and inbuilt solution provided by iOS. Just need to:
import AVFoundation
let imageRect = AVMakeRect(aspectRatio: image.size, insideRect: self.imageView.bounds)

Resources