Represent CIRectangleFeature with UIBezierPath - Swift Represent CIRectangleFeature with UIBezierPath - Swift ios ios

Represent CIRectangleFeature with UIBezierPath - Swift


If you only need to display the path then it's a bit easier to draw the path in a CAShapeLayer.

  1. Add a CAShapeLayer to the preview image.
  2. Calculate the rectangle.
  3. Create a UIBezierPath for the feature.
  4. Transform the path to match the source image.
  5. Set the path to the CAShapeLayer

Some complications arise in step 4 if you need to support scaled images, or images with orientation (i.e. anything from the user's camera).

Below is an example. This supports This code assumes that the image is displayed in a UIImageView with a contentMode of AspectFit, AspectFill, ScaleToFill, or Centre. It also supports images with an orientation Up, Down, Right and Left.

// Extension for calculating the image scale in an image view.// See: http://stackoverflow.com/questions/6856879/iphone-getting-the-size-of-an-image-after-aspectftextension UIImageView {    var imageScale: CGSize? {        guard let image = image else {            return nil        }        let sx = Double(self.frame.size.width / image.size.width)        let sy = Double(self.frame.size.height / image.size.height)        var s = 1.0        switch (self.contentMode) {        case .ScaleAspectFit:            s = fmin(sx, sy)            return CGSize (width: s, height: s)        case .ScaleAspectFill:            s = fmax(sx, sy)            return CGSize(width:s, height:s)        case .ScaleToFill:            return CGSize(width:sx, height:sy)        default:            return CGSize(width:s, height:s)        }    }}// Extension which provides a transform to rotate the image based on it's orientation metadata. extension UIImageView {    var normalizedTransformForOrientation: CGAffineTransform? {        guard let image = image else {            return nil        }        let r: CGFloat        switch image.imageOrientation {        case .Up:            r = 0        case .Down:            r = +1.0        case .Left:            r = -0.5        case .Right:            r = +0.5        default:            fatalError()        }        let cx = CGRectGetMidX(bounds)        let cy = CGRectGetMidY(bounds)        var transform = CGAffineTransformIdentity        transform = CGAffineTransformTranslate(transform, cx, cy)        transform = CGAffineTransformRotate(transform, CGFloat(M_PI) * r)        transform = CGAffineTransformTranslate(transform, -cx, -cy)        return transform    }}class ViewController: UIViewController {    // Shape layer for displaying the path.    let pathLayer: CAShapeLayer = {        let layer = CAShapeLayer()        layer.fillColor = UIColor.greenColor().colorWithAlphaComponent(0.3).CGColor        layer.strokeColor = UIColor.greenColor().colorWithAlphaComponent(0.9).CGColor        layer.lineWidth = 2.0        return layer    }()    // Image view where the preview and path overlay will be displayed.    @IBOutlet var imageView: UIImageView?    override func viewDidLoad() {        super.viewDidLoad()        // Add the path overlay to the image view.        imageView?.layer.addSublayer(pathLayer)        // Load a sample image from the assets.        selectImage(UIImage(named: "sample"))    }    func selectImage(image: UIImage?) {        imageView?.image = image        if let image = image {            processImage(image)        }    }    // Detect rectangles in image, and draw the path on the screen.    func processImage(input: UIImage) {        let path = pathsForRectanglesInImage(input)        let transform = pathTransformForImageView(imageView!)        path?.applyTransform(transform)        pathLayer.path = path?.CGPath    }    // Detect rectangles in an image and return a UIBezierPath.    func pathsForRectanglesInImage(input: UIImage) -> UIBezierPath? {        guard let sourceImage = CIImage(image: input) else {            return nil        }        let features = performRectangleDetection(sourceImage)        return pathForFeatures(features)    }    // Detect rectangles in image.    func performRectangleDetection(image: CIImage) -> [CIFeature] {        let detector:CIDetector = CIDetector(            ofType: CIDetectorTypeRectangle,            context: nil,            options: [CIDetectorAccuracy : CIDetectorAccuracyHigh]        )        let features = detector.featuresInImage(image)        return features    }    // Compose a UIBezierPath from CIRectangleFeatures.     func pathForFeatures(features: [CIFeature]) -> UIBezierPath {        let path = UIBezierPath()        for feature in features {            guard let rect = feature as? CIRectangleFeature else {                continue            }            path.moveToPoint(rect.topLeft)            path.addLineToPoint(rect.topRight)            path.addLineToPoint(rect.bottomRight)            path.addLineToPoint(rect.bottomLeft)            path.closePath()        }        return path    }    // Calculate the transform to orient the preview path to the image shown inside the image view.    func pathTransformForImageView(imageView: UIImageView) -> CGAffineTransform {        guard let image = imageView.image else {            return CGAffineTransformIdentity        }        guard let imageScale = imageView.imageScale else {            return CGAffineTransformIdentity        }        guard let imageTransform = imageView.normalizedTransformForOrientation else {            return CGAffineTransformIdentity        }        let frame = imageView.frame        let imageWidth = image.size.width * imageScale.width        let imageHeight = image.size.height * imageScale.height        var transform = CGAffineTransformIdentity        // Rotate to match the image orientation.        transform = CGAffineTransformConcat(imageTransform, transform)        // Flip vertically (flipped in CIDetector).        transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(frame))        transform = CGAffineTransformScale(transform, 1.0, -1.0)        // Centre align.        let tx: CGFloat = (CGRectGetWidth(frame) - imageWidth) * 0.5        let ty: CGFloat = (CGRectGetHeight(frame) - imageHeight) * 0.5        transform = CGAffineTransformTranslate(transform, tx, ty)        // Scale to match UIImageView scaling.        transform = CGAffineTransformScale(transform, imageScale.width, imageScale.height)        return transform    }}

Detected rectangle with stroked overlay


I've been struggling with the same problem for a few days, and this is how I overcame the problem:

I made a custom class to store the points and add some helper functions:

////  ObyRectangleFeature.swift////  Created by 4oby on 5/20/16.//  Copyright © 2016 cvv. All rights reserved.//import Foundationimport UIKitextension CGPoint {    func scalePointByCeficient(ƒ_x: CGFloat, ƒ_y: CGFloat) -> CGPoint {        return CGPoint(x: self.x/ƒ_x, y: self.y/ƒ_y) //original image    }    func reversePointCoordinates() -> CGPoint {        return CGPoint(x: self.y, y: self.x)    }    func sumPointCoordinates(add: CGPoint) -> CGPoint {        return CGPoint(x: self.x + add.x, y: self.y + add.y)    }    func substractPointCoordinates(sub: CGPoint) -> CGPoint {        return CGPoint(x: self.x - sub.x, y: self.y - sub.y)    }}class ObyRectangleFeature : NSObject {    var topLeft: CGPoint!    var topRight: CGPoint!    var bottomLeft: CGPoint!    var bottomRight: CGPoint!    var centerPoint : CGPoint{        get {            let centerX = ((topLeft.x + bottomLeft.x)/2 + (topRight.x + bottomRight.x)/2)/2            let centerY = ((topRight.y + topLeft.y)/2 + (bottomRight.y + bottomLeft.y)/2)/2            return CGPoint(x: centerX, y: centerY)        }    }    convenience init(_ rectangleFeature: CIRectangleFeature) {        self.init()        topLeft = rectangleFeature.topLeft        topRight = rectangleFeature.topRight        bottomLeft = rectangleFeature.bottomLeft        bottomRight = rectangleFeature.bottomRight    }    override init() {        super.init()    }    func rotate90Degree() -> Void {        let centerPoint =  self.centerPoint//        /rotate cos(90)=0, sin(90)=1        topLeft = CGPoint(x: centerPoint.x + (topLeft.y - centerPoint.y), y: centerPoint.y + (topLeft.x - centerPoint.x))        topRight = CGPoint(x: centerPoint.x + (topRight.y - centerPoint.y), y: centerPoint.y + (topRight.x - centerPoint.x))        bottomLeft = CGPoint(x: centerPoint.x + (bottomLeft.y - centerPoint.y), y: centerPoint.y + (bottomLeft.x - centerPoint.x))        bottomRight = CGPoint(x: centerPoint.x + (bottomRight.y - centerPoint.y), y: centerPoint.y + (bottomRight.x - centerPoint.x))    }    func  scaleRectWithCoeficient(ƒ_x: CGFloat, ƒ_y: CGFloat) -> Void {        topLeft =  topLeft.scalePointByCeficient(ƒ_x, ƒ_y: ƒ_y)        topRight = topRight.scalePointByCeficient(ƒ_x, ƒ_y: ƒ_y)        bottomLeft = bottomLeft.scalePointByCeficient(ƒ_x, ƒ_y: ƒ_y)        bottomRight = bottomRight.scalePointByCeficient(ƒ_x, ƒ_y: ƒ_y)    }    func correctOriginPoints() -> Void {        let deltaCenter = self.centerPoint.reversePointCoordinates().substractPointCoordinates(self.centerPoint)        let TL = topLeft        let TR = topRight        let BL = bottomLeft        let BR = bottomRight        topLeft = BL.sumPointCoordinates(deltaCenter)        topRight = TL.sumPointCoordinates(deltaCenter)        bottomLeft = BR.sumPointCoordinates(deltaCenter)        bottomRight = TR.sumPointCoordinates(deltaCenter)    }}

And this is the initialization code :

let scalatedRect : ObyRectangleFeature = ObyRectangleFeature(rectangleFeature)        // fromSize -> Initial size of the CIImage        // toSize -> the size of the scaled Image        let ƒ_x = (fromSize.width/toSize.width)        let ƒ_y = (fromSize.height/toSize.height)        /*the coeficients are interchange intentionally cause of the different        coordinate system used by CIImage and UIImage, you could rotate before         scaling, to preserve the order, but if you do, the result will be offCenter*/        scalatedRect.scaleRectWithCoeficient(ƒ_y, ƒ_y: ƒ_x)        scalatedRect.rotate90Degree()        scalatedRect.correctOriginPoints()

At this point scaleRect is ready to be drawn any way you like.