Last active
January 16, 2020 16:59
-
-
Save jonahaung/eccc7c01c2e836de8a866401d46cee92 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Quadrilateral.swift | |
// Myanmar Lens | |
// | |
// Created by Aung Ko Min on 14/1/20. | |
// Copyright © 2020 Aung Ko Min. All rights reserved. | |
// | |
// | |
// Quadrilateral.swift | |
// WeScan | |
// | |
// Created by Boris Emorine on 2/8/18. | |
// Copyright © 2018 WeTransfer. All rights reserved. | |
// | |
import UIKit | |
import AVFoundation | |
import Vision | |
/// A data structure representing a quadrilateral and its position. This class exists to bypass the fact that CIRectangleFeature is read-only. | |
public struct Quadrilateral: Transformable { | |
/// A point that specifies the top left corner of the quadrilateral. | |
public var topLeft: CGPoint | |
/// A point that specifies the top right corner of the quadrilateral. | |
public var topRight: CGPoint | |
/// A point that specifies the bottom right corner of the quadrilateral. | |
public var bottomRight: CGPoint | |
/// A point that specifies the bottom left corner of the quadrilateral. | |
public var bottomLeft: CGPoint | |
init(rectangleFeature: CIRectangleFeature) { | |
self.topLeft = rectangleFeature.topLeft | |
self.topRight = rectangleFeature.topRight | |
self.bottomLeft = rectangleFeature.bottomLeft | |
self.bottomRight = rectangleFeature.bottomRight | |
} | |
@available(iOS 11.0, *) | |
init(rectangleObservation: VNRectangleObservation) { | |
self.topLeft = rectangleObservation.topLeft | |
self.topRight = rectangleObservation.topRight | |
self.bottomLeft = rectangleObservation.bottomLeft | |
self.bottomRight = rectangleObservation.bottomRight | |
} | |
init(topLeft: CGPoint, topRight: CGPoint, bottomRight: CGPoint, bottomLeft: CGPoint) { | |
self.topLeft = topLeft | |
self.topRight = topRight | |
self.bottomRight = bottomRight | |
self.bottomLeft = bottomLeft | |
} | |
public var description: String { | |
return "topLeft: \(topLeft), topRight: \(topRight), bottomRight: \(bottomRight), bottomLeft: \(bottomLeft)" | |
} | |
/// The path of the Quadrilateral as a `UIBezierPath` | |
var path: UIBezierPath { | |
let path = UIBezierPath() | |
path.move(to: topLeft) | |
path.addLine(to: topRight) | |
path.addLine(to: bottomRight) | |
path.addLine(to: bottomLeft) | |
path.close() | |
return path | |
} | |
/// The perimeter of the Quadrilateral | |
var perimeter: Double { | |
let perimeter = topLeft.distanceTo(point: topRight) + topRight.distanceTo(point: bottomRight) + bottomRight.distanceTo(point: bottomLeft) + bottomLeft.distanceTo(point: topLeft) | |
return Double(perimeter) | |
} | |
/// Applies a `CGAffineTransform` to the quadrilateral. | |
/// | |
/// - Parameters: | |
/// - t: the transform to apply. | |
/// - Returns: The transformed quadrilateral. | |
func applying(_ transform: CGAffineTransform) -> Quadrilateral { | |
let quadrilateral = Quadrilateral(topLeft: topLeft.applying(transform), topRight: topRight.applying(transform), bottomRight: bottomRight.applying(transform), bottomLeft: bottomLeft.applying(transform)) | |
return quadrilateral | |
} | |
/// Checks whether the quadrilateral is withing a given distance of another quadrilateral. | |
/// | |
/// - Parameters: | |
/// - distance: The distance (threshold) to use for the condition to be met. | |
/// - rectangleFeature: The other rectangle to compare this instance with. | |
/// - Returns: True if the given rectangle is within the given distance of this rectangle instance. | |
func isWithin(_ distance: CGFloat, ofRectangleFeature rectangleFeature: Quadrilateral) -> Bool { | |
let topLeftRect = topLeft.surroundingSquare(withSize: distance) | |
if !topLeftRect.contains(rectangleFeature.topLeft) { | |
return false | |
} | |
let topRightRect = topRight.surroundingSquare(withSize: distance) | |
if !topRightRect.contains(rectangleFeature.topRight) { | |
return false | |
} | |
let bottomRightRect = bottomRight.surroundingSquare(withSize: distance) | |
if !bottomRightRect.contains(rectangleFeature.bottomRight) { | |
return false | |
} | |
let bottomLeftRect = bottomLeft.surroundingSquare(withSize: distance) | |
if !bottomLeftRect.contains(rectangleFeature.bottomLeft) { | |
return false | |
} | |
return true | |
} | |
/// Reorganizes the current quadrilateal, making sure that the points are at their appropriate positions. For example, it ensures that the top left point is actually the top and left point point of the quadrilateral. | |
mutating func reorganize() { | |
let points = [topLeft, topRight, bottomRight, bottomLeft] | |
let ySortedPoints = sortPointsByYValue(points) | |
guard ySortedPoints.count == 4 else { | |
return | |
} | |
let topMostPoints = Array(ySortedPoints[0..<2]) | |
let bottomMostPoints = Array(ySortedPoints[2..<4]) | |
let xSortedTopMostPoints = sortPointsByXValue(topMostPoints) | |
let xSortedBottomMostPoints = sortPointsByXValue(bottomMostPoints) | |
guard xSortedTopMostPoints.count > 1, | |
xSortedBottomMostPoints.count > 1 else { | |
return | |
} | |
topLeft = xSortedTopMostPoints[0] | |
topRight = xSortedTopMostPoints[1] | |
bottomRight = xSortedBottomMostPoints[1] | |
bottomLeft = xSortedBottomMostPoints[0] | |
} | |
/// Scales the quadrilateral based on the ratio of two given sizes, and optionaly applies a rotation. | |
/// | |
/// - Parameters: | |
/// - fromSize: The size the quadrilateral is currently related to. | |
/// - toSize: The size to scale the quadrilateral to. | |
/// - rotationAngle: The optional rotation to apply. | |
/// - Returns: The newly scaled and potentially rotated quadrilateral. | |
func scale(_ fromSize: CGSize, _ toSize: CGSize, withRotationAngle rotationAngle: CGFloat = 0.0) -> Quadrilateral { | |
var invertedfromSize = fromSize | |
let rotated = rotationAngle != 0.0 | |
if rotated && rotationAngle != CGFloat.pi { | |
invertedfromSize = CGSize(width: fromSize.height, height: fromSize.width) | |
} | |
var transformedQuad = self | |
let invertedFromSizeWidth = invertedfromSize.width == 0 ? .leastNormalMagnitude : invertedfromSize.width | |
let scale = toSize.width / invertedFromSizeWidth | |
let scaledTransform = CGAffineTransform(scaleX: scale, y: scale) | |
transformedQuad = transformedQuad.applying(scaledTransform) | |
if rotated { | |
let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle) | |
let fromImageBounds = CGRect(origin: .zero, size: fromSize).applying(scaledTransform).applying(rotationTransform) | |
let toImageBounds = CGRect(origin: .zero, size: toSize) | |
let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: fromImageBounds, toCenterOfRect: toImageBounds) | |
transformedQuad = transformedQuad.applyTransforms([rotationTransform, translationTransform]) | |
} | |
return transformedQuad | |
} | |
// Convenience functions | |
/// Sorts the given `CGPoints` based on their y value. | |
/// - Parameters: | |
/// - points: The poinmts to sort. | |
/// - Returns: The points sorted based on their y value. | |
private func sortPointsByYValue(_ points: [CGPoint]) -> [CGPoint] { | |
return points.sorted { (point1, point2) -> Bool in | |
point1.y < point2.y | |
} | |
} | |
/// Sorts the given `CGPoints` based on their x value. | |
/// - Parameters: | |
/// - points: The points to sort. | |
/// - Returns: The points sorted based on their x value. | |
private func sortPointsByXValue(_ points: [CGPoint]) -> [CGPoint] { | |
return points.sorted { (point1, point2) -> Bool in | |
point1.x < point2.x | |
} | |
} | |
} | |
extension Quadrilateral { | |
/// Converts the current to the cartesian coordinate system (where 0 on the y axis is at the bottom). | |
/// | |
/// - Parameters: | |
/// - height: The height of the rect containing the quadrilateral. | |
/// - Returns: The same quadrilateral in the cartesian corrdinate system. | |
func toCartesian(withHeight height: CGFloat) -> Quadrilateral { | |
let topLeft = self.topLeft.cartesian(withHeight: height) | |
let topRight = self.topRight.cartesian(withHeight: height) | |
let bottomRight = self.bottomRight.cartesian(withHeight: height) | |
let bottomLeft = self.bottomLeft.cartesian(withHeight: height) | |
return Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) | |
} | |
} | |
extension Quadrilateral: Equatable { | |
public static func == (lhs: Quadrilateral, rhs: Quadrilateral) -> Bool { | |
return lhs.topLeft == rhs.topLeft && lhs.topRight == rhs.topRight && lhs.bottomRight == rhs.bottomRight && lhs.bottomLeft == rhs.bottomLeft | |
} | |
} | |
/// Objects that conform to the Transformable protocol are capable of being transformed with a `CGAffineTransform`. | |
protocol Transformable { | |
/// Applies the given `CGAffineTransform`. | |
/// | |
/// - Parameters: | |
/// - t: The transform to apply | |
/// - Returns: The same object transformed by the passed in `CGAffineTransform`. | |
func applying(_ transform: CGAffineTransform) -> Self | |
} | |
extension Transformable { | |
/// Applies multiple given transforms in the given order. | |
/// | |
/// - Parameters: | |
/// - transforms: The transforms to apply. | |
/// - Returns: The same object transformed by the passed in `CGAffineTransform`s. | |
func applyTransforms(_ transforms: [CGAffineTransform]) -> Self { | |
var transformableObject = self | |
transforms.forEach { (transform) in | |
transformableObject = transformableObject.applying(transform) | |
} | |
return transformableObject | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment