【发布时间】:2014-12-23 14:29:14
【问题描述】:
我想为绘图应用程序实现自动完成功能。一旦绘制了徒手对象,我想检测对象的类型(圆形/矩形/三角形),并根据结果绘制相应的对象。
我已经阅读了一些关于 OpenCV 的信息,但是我需要将用户绘图实时转换为图像。我正在记录触摸绘制/跟踪的点数,并生成相应路径的UIBeizerPath。如何检测形状类型?
【问题讨论】:
标签: ios xcode detection shapes uibezierpath
我想为绘图应用程序实现自动完成功能。一旦绘制了徒手对象,我想检测对象的类型(圆形/矩形/三角形),并根据结果绘制相应的对象。
我已经阅读了一些关于 OpenCV 的信息,但是我需要将用户绘图实时转换为图像。我正在记录触摸绘制/跟踪的点数,并生成相应路径的UIBeizerPath。如何检测形状类型?
【问题讨论】:
标签: ios xcode detection shapes uibezierpath
您需要先分割数据点。谷歌上“笔画分割”找到相关文章。一种简单快速的算法是计算每个数据点的正向斜率和反向斜率,然后计算正向斜率和反向斜率之间的转角。如果转弯角度大于某个角度阈值,那么您可以假设您的路径在那里急转弯。根据计算出的急转弯数,您可以推断这些点是代表三角形(有 2 个急转弯)、四边形(有 3 个急转弯)还是其他东西。要推断数据点表示圆形或矩形,您需要进行额外的计算。例如,如果根本没有急转弯,则对数据点进行圆拟合,以查看拟合圆的最大误差是否小于某个容差。要输出一个矩形,您必须将直线拟合到每个数据点段,并检查拟合线是否或多或少相互正交。
【讨论】:
您可以使用CGPathApply(..) 方法遍历UIBezierPath 点。以Look here 为例。
但您应该以某种方式确定形状类型 - 这是数学任务和方法取决于您的输入数据。
【讨论】:
也许是一个更简单的解决方案,至少对于完美的形状:假设您知道您要查找的形状,您可以在用户的 UIBezierPath 范围内创建该形状的路径,然后针对描边版本执行命中测试这条路。
例如,这里有一个 Swift 5 方法,用于检测路径是否类似于椭圆。我还加入了一个从另一篇文章改编的方法,用于提取 UIBezierPath 的所有元素,除了子路径的闭包:
extension UIBezierPath {
/// Indicates whether a manually drawn UIBézierPath resembles an oval.
func resemblesOval() -> Bool {
// Get all the path points
let pathPoints = self.getPathElements().map({$0.point})
// Create the path of the perfect oval of the same bounds to perform a hit test against
let perfectOvalPath = UIBezierPath(ovalIn: self.bounds)
// Choose a hit test ribbon that is a quarter of the average of the bounds' width and height
let hitWidth = (self.bounds.width + self.bounds.height)/2 * 0.25
// Stroke the path of the desired perfect oval to create hit test criteria
let hitPath = perfectOvalPath.cgPath.copy(strokingWithWidth: hitWidth,
lineCap: .round,
lineJoin: .miter,
miterLimit: 0)
// Create an array to collect points that succeed in the hit test
var validPathPoints = [CGPoint]()
// Hit test for each point of our tested path
for point in pathPoints {
guard point != nil else { continue }
if hitPath.contains(point!) {
validPathPoints.append(point!)
}
}
// If 90% or more were successful, then it is an oval
if 10 * validPathPoints.count >= 9 * pathPoints.count {
return true
}
return false
}
/// An enum containing possible UIBezierPath element types. Use in conjunction with the `getPathElements` method.
enum PathElementType {
case move
case addLine
case addQuadCurve
case addCurve
}
/// Extracts all of the path elements, their points and their control points. Expected returned types belong to the enum `PathElementType`.
func getPathElements() -> [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] {
let initialPath = UIBezierPath(cgPath: self.cgPath)
var bezierPoints = NSMutableArray()
initialPath.cgPath.apply(info: &bezierPoints, function: { info, element in
guard let resultingPoints = info?.assumingMemoryBound(to: NSMutableArray.self) else {
return
}
let points = element.pointee.points
let type = element.pointee.type
switch type {
case .moveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("move"))
case .addLineToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("addLine"))
case .addQuadCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add(NSString("addQuadCurve"))
case .addCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[2].x)), NSNumber(value: Float(points[2].y))])
resultingPoints.pointee.add(NSString("addCurve"))
case .closeSubpath:
break
@unknown default:
break
}
})
let elementsTypes : [String] = bezierPoints.compactMap { $0 as? String }
let elementsCGFloats : [[CGFloat]] = bezierPoints.compactMap { $0 as? [CGFloat] }
var elementsCGPoints : [CGPoint] = elementsCGFloats.map { CGPoint(x: $0[0], y: $0[1]) }
var returnValue : [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] = []
for i in 0..<elementsTypes.count {
switch elementsTypes[i] {
case "move":
returnValue.append((type: .move, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addLine":
returnValue.append((type: .addLine, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addQuadCurve":
let controlPoint = elementsCGPoints.removeFirst()
returnValue.append((type: .addQuadCurve, point: elementsCGPoints.removeFirst(), controlPoint: controlPoint, controlPoint1: nil, controlPoint2: nil))
case "addCurve":
let controlPoint1 = elementsCGPoints.removeFirst()
let controlPoint2 = elementsCGPoints.removeFirst()
returnValue.append((type: .addCurve, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: controlPoint1, controlPoint2: controlPoint2))
default:
returnValue.append((type: nil, point: nil, controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
}
}
return returnValue
}
}
【讨论】: