【问题标题】:Detect if drawing a path is a circle/rectangle in xcode检测绘制路径是否是xcode中的圆形/矩形
【发布时间】:2014-12-23 14:29:14
【问题描述】:

我想为绘图应用程序实现自动完成功能。一旦绘制了徒手对象,我想检测对象的类型(圆形/矩形/三角形),并根据结果绘制相应的对象。

我已经阅读了一些关于 OpenCV 的信息,但是我需要将用户绘图实时转换为图像。我正在记录触摸绘制/跟踪的点数,并生成相应路径的UIBeizerPath。如何检测形状类型?

【问题讨论】:

    标签: ios xcode detection shapes uibezierpath


    【解决方案1】:

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

    【讨论】:

    • 有趣!我遵循了您的方法,但不幸的是,我得到的点序列有轻微的偏移(即它们不在直线上),因此应用该公式给了我很多急转弯。对于一个三角形,我得到了 70 个急转弯,然后我减小了样本大小,但仍然得到了大约 18 个急转弯。
    • 计算后向/前向斜率时,使用当前点前后的几个点。例如,在计算点 P(i) 的后/前斜率时,使用点 P(i-m) 和 P(i+m)。根据点密度选择 m 的值。这样,您应该能够避免由于较小的数据噪声而导致的急转弯。
    • 是的,在发表评论后,我认为我应该减少我的抽样规模并这样做了。现在我似乎得到了正确的值,尽管对于三角形我一直得到 3 圈而不是 2 圈,而矩形我有时会得到 2 圈。我猜转向角阈值需要做更多的工作。
    • 通常我使用 60 度作为角度阈值。请注意,转动角度为 180 - 角度(P(i-m),P(i),P(i+m))。所以,对于一个看起来像等边三角形的笔画,你应该得到两个急转弯,每个急转弯大约是 120 度,而不是 60 度。
    • 我已经优化了您的方法,使其具有某种可行的算法,尽管它远非完美。所以我会接受这个答案作为正确的答案。
    【解决方案2】:

    您可以使用CGPathApply(..) 方法遍历UIBezierPath 点。以Look here 为例。

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

    【讨论】:

    • 我认为它隐藏在我的问题中,但我已经有了路径的要点。我正在研究如何检测形状的数学。
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 2022-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多