Ilya Laktyushin 02e06779ef Drawing
2022-12-03 21:57:32 +04:00

242 lines
7.7 KiB
Swift

import Foundation
import UIKit
private let pointsCount: Int = 64
private let squareSize: Double = 250.0
private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize)
private let halfDiagonal = diagonal * 0.5
private let angleRange: Double = .pi / 4.0
private let anglePrecision: Double = .pi / 90.0
class Unistroke {
let points: [CGPoint]
init(points: [CGPoint]) {
var points = resample(points: points, totalPoints: pointsCount)
let radians = indicativeAngle(points: points)
points = rotate(points: points, byRadians: -radians)
points = scale(points: points, toSize: squareSize)
points = translate(points: points, to: .zero)
self.points = points
}
func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? {
var bestDistance = Double.infinity
var bestTemplate: UnistrokeTemplate?
for template in templates {
let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision)
if templateDistance < bestDistance {
bestDistance = templateDistance
bestTemplate = template
}
}
if let bestTemplate = bestTemplate {
bestDistance = 1.0 - bestDistance / halfDiagonal
if bestDistance < minThreshold {
return nil
}
return bestTemplate.name
} else {
return nil
}
}
}
class UnistrokeTemplate : Unistroke {
var name: String
init(name: String, points: [CGPoint]) {
self.name = name
super.init(points: points)
}
}
private struct Edge {
var minX: Double
var minY: Double
var maxX: Double
var maxY: Double
init(minX: Double, maxX: Double, minY: Double, maxY: Double) {
self.minX = minX
self.minY = minY
self.maxX = maxX
self.maxY = maxY
}
mutating func addPoint(value: CGPoint) {
self.minX = min(self.minX,value.x)
self.maxX = max(self.maxX,value.x)
self.minY = min(self.minY,value.y)
self.maxY = max(self.maxY,value.y)
}
}
private extension Double {
func toRadians() -> Double {
let res = self * .pi / 180.0
return res
}
}
private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] {
var initialPoints = points
let interval = pathLength(points: initialPoints) / Double(totalPoints - 1)
var totalLength: Double = 0.0
var newPoints: [CGPoint] = [points[0]]
for i in 1 ..< initialPoints.count {
let currentLength = initialPoints[i - 1].distance(to: initialPoints[i])
if totalLength + currentLength >= interval {
let newPoint = CGPoint(
x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x),
y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y)
)
newPoints.append(newPoint)
initialPoints.insert(newPoint, at: i)
totalLength = 0.0
} else {
totalLength += currentLength
}
}
if newPoints.count == totalPoints - 1 {
newPoints.append(points.last!)
}
return newPoints
}
private func pathLength(points: [CGPoint]) -> Double {
var distance: Double = 0.0
for index in 1 ..< points.count {
distance += points[index - 1].distance(to: points[index])
}
return distance
}
private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double {
var d: Double = 0.0
for idx in 0 ..< min(path1.count, path2.count) {
d += path1[idx].distance(to: path2[idx])
}
return d / Double(path1.count)
}
private func centroid(points: [CGPoint]) -> CGPoint {
var centroidPoint: CGPoint = .zero
for point in points {
centroidPoint.x = centroidPoint.x + point.x
centroidPoint.y = centroidPoint.y + point.y
}
centroidPoint.x = (centroidPoint.x / Double(points.count))
centroidPoint.y = (centroidPoint.y / Double(points.count))
return centroidPoint
}
private func boundingBox(points: [CGPoint]) -> CGRect {
var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity)
for point in points {
edge.addPoint(value: point)
}
return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY))
}
private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] {
let centroid = centroid(points: points)
let cosinus = cos(radians)
let sinus = sin(radians)
var result: [CGPoint] = []
for point in points {
result.append(
CGPoint(
x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x,
y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y
)
)
}
return result
}
private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] {
let boundingBox = boundingBox(points: points)
var result: [CGPoint] = []
for point in points {
result.append(
CGPoint(
x: point.x * (size / boundingBox.width),
y: point.y * (size / boundingBox.height)
)
)
}
return result
}
private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] {
let centroidPoint = centroid(points: points)
var newPoints: [CGPoint] = []
for point in points {
newPoints.append(
CGPoint(
x: point.x + pt.x - centroidPoint.x,
y: point.y + pt.y - centroidPoint.y
)
)
}
return newPoints
}
private func vectorize(points: [CGPoint]) -> [Double] {
var sum: Double = 0.0
var vector: [Double] = []
for point in points {
vector.append(point.x)
vector.append(point.y)
sum += (point.x * point.x) + (point.y * point.y)
}
let magnitude = sqrt(sum)
for i in 0 ..< vector.count {
vector[i] = vector[i] / magnitude
}
return vector
}
private func indicativeAngle(points: [CGPoint]) -> Double {
let centroid = centroid(points: points)
return atan2(centroid.y - points[0].y, centroid.x - points[0].x)
}
private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double {
func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double {
let rotatedPoints = rotate(points: points, byRadians: radians)
return pathDistance(path1: rotatedPoints, path2: strokeTemplate)
}
let phi: Double = (0.5 * (-1.0 + sqrt(5.0)))
var fromAngle = fromAngle
var toAngle = toAngle
var x1 = phi * fromAngle + (1.0 - phi) * toAngle
var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
var x2 = (1.0 - phi) * fromAngle + phi * toAngle
var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
while abs(toAngle - fromAngle) > threshold {
if f1 < f2 {
toAngle = x2
x2 = x1
f2 = f1
x1 = phi * fromAngle + (1.0 - phi) * toAngle
f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
} else {
fromAngle = x1
x1 = x2
f1 = f2
x2 = (1.0 - phi) * fromAngle + phi * toAngle
f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
}
}
return min(f1, f2)
}