Last active
January 15, 2020 02:12
-
-
Save alexpersian/909786d0095ff00c8a2e2fcd85186f35 to your computer and use it in GitHub Desktop.
ViewController class that will render a shape and then on-tap will draw a bounding box around that shape.
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
// | |
// ViewController with a single shape drawn inside it. | |
// Tapping on the shape will cause a red minimum-bounding box to be drawn | |
// around the shape's bounds. | |
// | |
// Included as a single monofile for the sake of including it all within a single gist. | |
// Core logic centers around the Flood Fill algorithm with an interative, BFS approach used. | |
// | |
import UIKit | |
import CoreGraphics | |
final class ViewController: UIViewController { | |
private var screen: UIImage? | |
private var maxX: CGFloat { return view.frame.width } | |
private var maxY: CGFloat { return view.frame.height } | |
private var visited: Set<CGPoint> = Set() | |
private var boundingPoints: [CGPoint] = [] | |
private var topLeftBound: CGPoint = .zero | |
private var bottomRightBound: CGPoint = .zero | |
override func viewDidAppear(_ animated: Bool) { | |
super.viewDidAppear(animated) | |
let polyFrame = CGRect(x: view.frame.midX - 100, | |
y: view.frame.midY - 100, | |
width: 200, | |
height: 200) | |
let polygon = PolygonView(frame: polyFrame) | |
view.addSubview(polygon) | |
// We take a snapshot of the screen to analyze instead of checking | |
// the actual screen data every iteration of floodFill. | |
screen = captureScreen() | |
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(start)) | |
polygon.addGestureRecognizer(tapGesture) | |
} | |
@objc private func start(gestureRecognizer: UITapGestureRecognizer) { | |
let startPoint = gestureRecognizer.location(in: view) | |
floodFill(start: startPoint) | |
findBoundingCoordinates() | |
drawBoundingBox() | |
} | |
private func captureScreen() -> UIImage? { | |
defer { UIGraphicsEndImageContext() } | |
UIGraphicsBeginImageContext(self.view.frame.size) | |
if let ctx = UIGraphicsGetCurrentContext() { | |
self.view.layer.render(in: ctx) | |
return UIGraphicsGetImageFromCurrentImageContext() | |
} else { | |
return nil | |
} | |
} | |
private func floodFill(start: CGPoint) { | |
var pointQueue: [CGPoint] = [] | |
pointQueue.append(start) | |
while !pointQueue.isEmpty { | |
let point = pointQueue.removeFirst() | |
// Make sure we havne't visited this point already | |
guard !visited.contains(point) else { continue } | |
if colorIsBlack(at: point) { // We are still within the object | |
// Add all of its neighbors | |
pointQueue.append(CGPoint(x: point.x - 1, y: point.y)) | |
pointQueue.append(CGPoint(x: point.x + 1, y: point.y)) | |
pointQueue.append(CGPoint(x: point.x, y: point.y - 1)) | |
pointQueue.append(CGPoint(x: point.x, y: point.y + 1)) | |
} else { // We've reached the boundary | |
// Add the point to our list of boundary points | |
boundingPoints.append(point) | |
} | |
// Mark the point as visited | |
visited.insert(point) | |
} | |
} | |
private func colorIsBlack(at point: CGPoint) -> Bool { | |
if let color = screen?.color(at: point), color.equals(.black) { | |
return true | |
} else { | |
return false | |
} | |
} | |
private func findBoundingCoordinates() { | |
boundingPoints.sort { $0.x < $1.x } | |
topLeftBound.x = boundingPoints.first?.x ?? 0 | |
bottomRightBound.x = boundingPoints.last?.x ?? 0 | |
boundingPoints.sort { $0.y < $1.y } | |
topLeftBound.y = boundingPoints.first?.y ?? 0 | |
bottomRightBound.y = boundingPoints.last?.y ?? 0 | |
} | |
private func drawBoundingBox() { | |
let boundingRect = CGRect(x: topLeftBound.x, | |
y: topLeftBound.y, | |
width: bottomRightBound.x - topLeftBound.x, | |
height: bottomRightBound.y - topLeftBound.y) | |
let boundingView = UIView(frame: boundingRect) | |
boundingView.backgroundColor = .none | |
boundingView.layer.borderColor = CGColor(srgbRed: 1.0, green: 0, blue: 0, alpha: 1.0) | |
boundingView.layer.borderWidth = 2.0 | |
self.view.addSubview(boundingView) | |
} | |
} | |
// Borrowed from https://stackoverflow.com/a/50624060/3434244 | |
extension UIImage { | |
func color(at point: CGPoint) -> UIColor? { | |
if point.x < 0 || point.x > self.size.width || point.y < 0 || point.y > self.size.height { return nil } | |
guard | |
let provider = self.cgImage?.dataProvider, | |
let providerData = provider.data, | |
let data = CFDataGetBytePtr(providerData) | |
else { return nil } | |
let numberOfComponents = 4 | |
let pixelData = Int((size.width * point.y) + point.x) * numberOfComponents | |
let r = CGFloat(data[pixelData]) / 255.0 | |
let g = CGFloat(data[pixelData + 1]) / 255.0 | |
let b = CGFloat(data[pixelData + 2]) / 255.0 | |
let a = CGFloat(data[pixelData + 3]) / 255.0 | |
return UIColor(red: r, green: g, blue: b, alpha: a) | |
} | |
} | |
// Borrowed from https://stackoverflow.com/a/40486973/3434244 | |
extension UIColor { | |
func equals(_ rhs: UIColor) -> Bool { | |
var lhsR: CGFloat = 0 | |
var lhsG: CGFloat = 0 | |
var lhsB: CGFloat = 0 | |
var lhsA: CGFloat = 0 | |
self.getRed(&lhsR, green: &lhsG, blue: &lhsB, alpha: &lhsA) | |
var rhsR: CGFloat = 0 | |
var rhsG: CGFloat = 0 | |
var rhsB: CGFloat = 0 | |
var rhsA: CGFloat = 0 | |
rhs.getRed(&rhsR, green: &rhsG, blue: &rhsB, alpha: &rhsA) | |
return lhsR == rhsR && | |
lhsG == rhsG && | |
lhsB == rhsB && | |
lhsA == rhsA | |
} | |
} | |
extension CGPoint: Hashable { | |
public func hash(into hasher: inout Hasher) { | |
hasher.combine(x) | |
hasher.combine(y) | |
} | |
static func == (lhs: CGPoint, rhs: CGPoint) -> Bool { | |
return lhs.x == rhs.x && lhs.y == rhs.y | |
} | |
} | |
final class PolygonView : UIView { | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
backgroundColor = UIColor.clear | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
backgroundColor = UIColor.clear | |
} | |
override func draw(_ rect: CGRect) { | |
let size = self.bounds.size | |
let origin = self.bounds.origin | |
let h1 = size.height * 0.25 | |
let h2 = size.height * 0.50 | |
// Calculate the 5 points of the polygon | |
let p1 = CGPoint(x: origin.x + (size.width / 2), y: origin.y) | |
let p2 = CGPoint(x: origin.x, y: p1.y + h1) | |
let p3 = CGPoint(x: p2.x, y: p2.y + h2) | |
let p4 = CGPoint(x: p1.x, y: size.height) | |
let p5 = CGPoint(x: size.width, y: p3.y) | |
let p6 = CGPoint(x: size.width, y: p2.y) | |
// Create the path | |
let path = UIBezierPath() | |
path.move(to: p1) | |
path.addLine(to: p2) | |
path.addLine(to: p3) | |
path.addLine(to: p4) | |
path.addLine(to: p5) | |
path.addLine(to: p6) | |
path.close() | |
// Fill the path with color | |
UIColor.black.set() | |
path.fill() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment