在Xcode中检测绘制路径是否为圆形/矩形

4
我想为绘画应用程序实现自动完成功能。 在手绘对象绘制完毕后,我想检测对象的类型(圆形/矩形/三角形),并根据结果绘制相应的对象。
我已经阅读了一些关于OpenCV的资料,但是我需要实时将用户绘制的内容转换成图像。 我正在记录触摸操作绘制/追踪的点数,并生成相应路径的UIBeizerPath。我该如何检测形状类型?
3个回答

2
也许对于完美的形状来说,有一个更简单的解决方案:假设您知道要查找的形状,您可以在用户的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
    }
}

1
你需要先对数据点进行分割。在谷歌上搜索“笔画分割”以查找相关文章。一个简单快速的算法是计算每个数据点的正向斜率和反向斜率,然后计算正向斜率和反向斜率之间的转角。如果转角大于某个角度阈值,则可以假设路径在那里发生了急转弯。通过计算急转弯的数量,可以推断出这些点是否表示三角形(有2个急转弯)、四边形(有3个急转弯)或其他形状。要推断数据点表示圆形或矩形,您需要进行额外的计算。例如,如果根本没有急转弯,则对数据点进行圆拟合,以查看最大误差是否小于某个容差。要输出矩形,您将需要对每个数据点段进行直线拟合,并检查拟合的直线是否更或少相互垂直。

有趣!我按照你的方法尝试了一下,但不幸的是,我得到的点序列存在轻微偏移(即它们不在一条直线上),因此应用公式会产生许多急转弯。对于一个三角形,我得到了70个急转弯,然后我减少了样本大小,但仍然得到了大约18个急转弯。 - B K
在计算向后/向前斜率时,使用当前点之前和之后的几个点。例如,在计算点P(i)的向后/向前斜率时,请使用点P(i-m)和P(i+m)。根据点密度选择m的值。这样,您应该能够避免由轻微数据噪声引起的急转弯。 - fang
是的,在发布评论后,我意识到应该减少采样量并进行了调整。现在似乎得到了正确的值,但对于三角形,我仍然得到3个转弯而不是2个,对于矩形,有时会得到2个转弯。我想需要更多关于旋转角度阈值的工作。 - B K
通常我使用60度作为角度阈值。请注意,转向角度是180 - angle(P(i-m),P(i),P(i+m))。因此,对于看起来像等边三角形的笔画,您应该得到两个锐角转弯,每个锐角转弯约为120度,而不是60度。 - fang
我已经优化了你的方法,使其具有某种工作算法,但它远远不完美。因此,我将接受这个答案作为正确的答案。 - B K

0

您可以使用CGPathApply(..)方法迭代遍历UIBezierPath点。在这里查看示例。

但是您需要以某种方式确定形状类型 - 这是数学任务,其方法取决于您的输入数据。


1
我认为这个问题已经隐藏在我的问题中了,但我已经有了路径的点数。我正在研究如何检测形状的数学方法。 - B K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接