关于ios:通过用户的触摸画出一个完美的圆圈

Draw a perfect circle from user's touch

我有一个练习项目,允许用户在用手指触摸时在屏幕上绘制。我做的非常简单的App就是锻炼方式。
我的小堂兄在用我的iPad在这个App上自由地用手指画东西(孩子画的东西:圆,线等,无论他想到什么)。
然后他开始画圆,然后他问我将其设为"好圆"(根据我的理解:如我们所知,使绘制的圆完美圆
无论我们尝试用手指在屏幕上画些东西多么稳定,圆形永远都不会像圆形那样真实地变圆。

因此,我的问题是,代码中是否有任何方法可以首先检测用户绘制的形成圆的线,并通过使其在屏幕上完美地变圆来生成近似相同大小的圆。使一条不太直线的直线是我会知道的方法,但是对于圆形,我不太了解如何使用Quartz或其他方法进行。

我的理由是,在用户抬起手指以证明他实际上想画圆的事实成立之后,直线的起点和终点必须彼此接触或交叉。


有时花一些时间重新发明轮子真的很有用。正如您可能已经注意到的,有很多框架,但是在不引入所有复杂性的情况下实现简单但有用的解决方案并不难。 (请不要误会我的意思,出于任何严肃的目的,最好使用一些成熟且经过验证的稳定框架)。

我将首先介绍我的结果,然后解释其背后的简单明了的想法。

enter image description here

您将在我的实现中看到不需要分析每个点并进行复杂的计算。这个想法是发现一些有价值的元信息。我将以切线为例:

enter image description here

让我们确定一个简单明了的图案,通常适用于所选形状:

enter image description here

因此,基于该思想实现圆检测机制并不难。请参阅下面的工作演示(很抱歉,我使用Java作为提供此快速且有点脏的示例的最快方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class CircleGestureDemo extends JFrame implements MouseListener, MouseMotionListener {

    enum Type {
        RIGHT_DOWN,
        LEFT_DOWN,
        LEFT_UP,
        RIGHT_UP,
        UNDEFINED
    }

    private static final Type[] circleShape = {
        Type.RIGHT_DOWN,
        Type.LEFT_DOWN,
        Type.LEFT_UP,
        Type.RIGHT_UP};

    private boolean editing = false;
    private Point[] bounds;
    private Point last = new Point(0, 0);
    private List<Point> points = new ArrayList<>();

    public CircleGestureDemo() throws HeadlessException {
        super("Detect Circle");

        addMouseListener(this);
        addMouseMotionListener(this);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        setPreferredSize(new Dimension(800, 600));
        pack();
    }

    @Override
    public void paint(Graphics graphics) {
        Dimension d = getSize();
        Graphics2D g = (Graphics2D) graphics;

        super.paint(g);

        RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g.setRenderingHints(qualityHints);

        g.setColor(Color.RED);
        if (cD == 0) {
            Point b = null;
            for (Point e : points) {
                if (null != b) {
                    g.drawLine(b.x, b.y, e.x, e.y);
                }
                b = e;
            }
        }else if (cD > 0){
            g.setColor(Color.BLUE);
            g.setStroke(new BasicStroke(3));
            g.drawOval(cX, cY, cD, cD);
        }else{
            g.drawString("Uknown",30,50);
        }
    }


    private Type getType(int dx, int dy) {
        Type result = Type.UNDEFINED;

        if (dx > 0 && dy < 0) {
            result = Type.RIGHT_DOWN;
        } else if (dx < 0 && dy < 0) {
            result = Type.LEFT_DOWN;
        } else if (dx < 0 && dy > 0) {
            result = Type.LEFT_UP;
        } else if (dx > 0 && dy > 0) {
            result = Type.RIGHT_UP;
        }

        return result;
    }

    private boolean isCircle(List<Point> points) {
        boolean result = false;
        Type[] shape = circleShape;
        Type[] detected = new Type[shape.length];
        bounds = new Point[shape.length];

        final int STEP = 5;

        int index = 0;        
        Point current = points.get(0);
        Type type = null;

        for (int i = STEP; i < points.size(); i += STEP) {
            Point next = points.get(i);
            int dx = next.x - current.x;
            int dy = -(next.y - current.y);

            if(dx == 0 || dy == 0) {
                continue;
            }

            Type newType = getType(dx, dy);
            if(type == null || type != newType) {
                if(newType != shape[index]) {
                    break;
                }
                bounds[index] = current;
                detected[index++] = newType;
            }
            type = newType;            
            current = next;

            if (index >= shape.length) {
                result = true;
                break;
            }
        }

        return result;
    }

    @Override
    public void mousePressed(MouseEvent e) {
        cD = 0;
        points.clear();
        editing = true;
    }

    private int cX;
    private int cY;
    private int cD;

    @Override
    public void mouseReleased(MouseEvent e) {
        editing = false;
        if(points.size() > 0) {
            if(isCircle(points)) {
                cX = bounds[0].x + Math.abs((bounds[2].x - bounds[0].x)/2);
                cY = bounds[0].y;
                cD = bounds[2].y - bounds[0].y;
                cX = cX - cD/2;

                System.out.println("circle");
            }else{
                cD = -1;
                System.out.println("unknown");
            }
            repaint();
        }
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        Point newPoint = e.getPoint();
        if (editing && !last.equals(newPoint)) {
            points.add(newPoint);
            last = newPoint;
            repaint();
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                CircleGestureDemo t = new CircleGestureDemo();
                t.setVisible(true);
            }
        });
    }
}

在iOS上实现类似的行为应该不是问题,因为您只需要几个事件和坐标即可。类似于以下内容(请参见示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch* touch = [[event allTouches] anyObject];
}

- (void)handleTouch:(UIEvent *)event {
    UITouch* touch = [[event allTouches] anyObject];
    CGPoint location = [touch locationInView:self];

}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self handleTouch: event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self handleTouch: event];    
}

有几种可能的增强功能。

随时开始

由于以下简化,当前的要求是从顶部中间点开始画一个圆:

1
2
3
4
5
6
7
        if(type == null || type != newType) {
            if(newType != shape[index]) {
                break;
            }
            bounds[index] = current;
            detected[index++] = newType;
        }

请注意,使用的是默认值index。简单搜索形状的可用"部分"将消除该限制。请注意,您需要使用圆形缓冲区才能检测完整形状:

enter image description here

顺时针和逆时针

为了支持两种模式,您将需要使用先前增强功能中的循环缓冲区并在两个方向上进行搜索:

enter image description here

画一个椭圆

bounds数组中已经具有所需的一切。

enter image description here

只需使用该数据:

1
2
cWidth = bounds[2].y - bounds[0].y;
cHeight = bounds[3].y - bounds[1].y;

其他手势(可选)

最后,您只需要正确处理dx(或dy)等于零的情况即可支持其他手势:

enter image description here

更新资料

这个小的PoC引起了很高的关注,因此我做了一些更新代码,以使其平稳运行并提供一些绘图提示,突出支持点等:

enter image description here

这是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class CircleGestureDemo extends JFrame {

    enum Type {

        RIGHT_DOWN,
        LEFT_DOWN,
        LEFT_UP,
        RIGHT_UP,
        UNDEFINED
    }

    private static final Type[] circleShape = {
        Type.RIGHT_DOWN,
        Type.LEFT_DOWN,
        Type.LEFT_UP,
        Type.RIGHT_UP};

    public CircleGestureDemo() throws HeadlessException {
        super("Circle gesture");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        add(BorderLayout.CENTER, new GesturePanel());
        setPreferredSize(new Dimension(800, 600));
        pack();
    }

    public static class GesturePanel extends JPanel implements MouseListener, MouseMotionListener {

        private boolean editing = false;
        private Point[] bounds;
        private Point last = new Point(0, 0);
        private final List<Point> points = new ArrayList<>();

        public GesturePanel() {
            super(true);
            addMouseListener(this);
            addMouseMotionListener(this);
        }

        @Override
        public void paint(Graphics graphics) {
            super.paint(graphics);

            Dimension d = getSize();
            Graphics2D g = (Graphics2D) graphics;

            RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);

            g.setRenderingHints(qualityHints);

            if (!points.isEmpty() && cD == 0) {
                isCircle(points, g);
                g.setColor(HINT_COLOR);
                if (bounds[2] != null) {
                    int r = (bounds[2].y - bounds[0].y) / 2;
                    g.setStroke(new BasicStroke(r / 3 + 1));
                    g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
                } else if (bounds[1] != null) {
                    int r = bounds[1].x - bounds[0].x;
                    g.setStroke(new BasicStroke(r / 3 + 1));
                    g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
                }
            }

            g.setStroke(new BasicStroke(2));
            g.setColor(Color.RED);

            if (cD == 0) {
                Point b = null;
                for (Point e : points) {
                    if (null != b) {
                        g.drawLine(b.x, b.y, e.x, e.y);
                    }
                    b = e;
                }

            } else if (cD > 0) {
                g.setColor(Color.BLUE);
                g.setStroke(new BasicStroke(3));
                g.drawOval(cX, cY, cD, cD);
            } else {
                g.drawString("Uknown", 30, 50);
            }
        }

        private Type getType(int dx, int dy) {
            Type result = Type.UNDEFINED;

            if (dx > 0 && dy < 0) {
                result = Type.RIGHT_DOWN;
            } else if (dx < 0 && dy < 0) {
                result = Type.LEFT_DOWN;
            } else if (dx < 0 && dy > 0) {
                result = Type.LEFT_UP;
            } else if (dx > 0 && dy > 0) {
                result = Type.RIGHT_UP;
            }

            return result;
        }

        private boolean isCircle(List<Point> points, Graphics2D g) {
            boolean result = false;
            Type[] shape = circleShape;
            bounds = new Point[shape.length];

            final int STEP = 5;
            int index = 0;
            int initial = 0;
            Point current = points.get(0);
            Type type = null;

            for (int i = STEP; i < points.size(); i += STEP) {
                final Point next = points.get(i);
                final int dx = next.x - current.x;
                final int dy = -(next.y - current.y);

                if (dx == 0 || dy == 0) {
                    continue;
                }

                final int marker = 8;
                if (null != g) {
                    g.setColor(Color.BLACK);
                    g.setStroke(new BasicStroke(2));
                    g.drawOval(current.x - marker/2,
                               current.y - marker/2,
                               marker, marker);
                }

                Type newType = getType(dx, dy);
                if (type == null || type != newType) {
                    if (newType != shape[index]) {
                        break;
                    }
                    bounds[index++] = current;
                }

                type = newType;
                current = next;
                initial = i;

                if (index >= shape.length) {
                    result = true;
                    break;
                }
            }
            return result;
        }

        @Override
        public void mousePressed(MouseEvent e) {
            cD = 0;
            points.clear();
            editing = true;
        }

        private int cX;
        private int cY;
        private int cD;

        @Override
        public void mouseReleased(MouseEvent e) {
            editing = false;
            if (points.size() > 0) {
                if (isCircle(points, null)) {
                    int r = Math.abs((bounds[2].y - bounds[0].y) / 2);
                    cX = bounds[0].x - r;
                    cY = bounds[0].y;
                    cD = 2 * r;
                } else {
                    cD = -1;
                }
                repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            Point newPoint = e.getPoint();
            if (editing && !last.equals(newPoint)) {
                points.add(newPoint);
                last = newPoint;
                repaint();
            }
        }

        @Override
        public void mouseMoved(MouseEvent e) {
        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {
        }

        @Override
        public void mouseClicked(MouseEvent e) {
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                CircleGestureDemo t = new CircleGestureDemo();
                t.setVisible(true);
            }
        });
    }

    final static Color HINT_COLOR = new Color(0x55888888, true);
}


用于检测形状的经典计算机视觉技术是霍夫变换。关于霍夫变换的好处之一是它对部分数据,不完善的数据和噪声非常容忍。用霍夫围圈:
http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

考虑到您的圆圈是手绘的,我认为霍夫变换可能对您来说是一个不错的选择。

这是一个"简化"的解释,对于它并不是那么简单,我深表歉意。其中大部分来自我多年前所做的学校项目。

霍夫变换是一种投票方案。分配了一个二维整数数组,并将所有元素设置为零。每个元素对应于要分析的图像中的单个像素。此数组称为累加器数组,因为每个元素都会累积信息(票数),表明像素可能位于圆或弧的原点。

将梯度算子边缘检测器应用于图像,并记录边缘像素或edgels。边缘l是相对于其邻居具有不同强度或颜色的像素。差异程度称为梯度幅度。对于足够幅度的每个边缘1,应用表决方案,该表决方案将累加累加器阵列的元素。递增(投票)的元素对应于通过考虑中的edgel的圆的可能原点。期望的结果是,如果存在弧,则真实起点将比假起点获得更多的选票。

注意,被访问以进行投票的累加器阵列的元素在所考虑的edgel周围形成一个圆圈。计算要投票的x,y坐标与计算要绘制的圆的x,y坐标相同。

在手绘图像中,您可以直接使用设置的(彩色)像素,而不用计算edgels。

现在,如果像素位置不正确,您将不一定会获得投票数最多的单个累加器数组元素。您可能会得到一堆带有一组投票的相邻数组元素的集合。这个星团的重心可以为原点提供一个很好的近似值。

请注意,您可能必须针对不同的半径R值运行霍夫变换。产生更密集的一组投票的是"更好"的拟合。

有多种技术可用于减少虚假来源的选票。例如,使用edgels的一个优点是它们不仅具有幅度,而且还具有方向。投票时,我们只需要为适当方向的可能来源投票。获得选票的地点将形成弧形,而不是一个完整的圆圈。

这是一个例子。我们从半径为1的圆和一个初始化的累加器数组开始。由于每个像素都被认为是潜在原点。真正的原点获得最多的选票,在这种情况下为四票。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.  empty pixel
X  drawn pixel
*  drawn pixel currently being considered

. . . . .   0 0 0 0 0
. . X . .   0 0 0 0 0
. X . X .   0 0 0 0 0
. . X . .   0 0 0 0 0
. . . . .   0 0 0 0 0

. . . . .   0 0 0 0 0
. . X . .   0 1 0 0 0
. * . X .   1 0 1 0 0
. . X . .   0 1 0 0 0
. . . . .   0 0 0 0 0

. . . . .   0 0 0 0 0
. . X . .   0 1 0 0 0
. X . X .   1 0 2 0 0
. . * . .   0 2 0 1 0
. . . . .   0 0 1 0 0

. . . . .   0 0 0 0 0
. . X . .   0 1 0 1 0
. X . * .   1 0 3 0 1
. . X . .   0 2 0 2 0
. . . . .   0 0 1 0 0

. . . . .   0 0 1 0 0
. . * . .   0 2 0 2 0
. X . X .   1 0 4 0 1
. . X . .   0 2 0 2 0
. . . . .   0 0 1 0 0


这是另一种方式。使用UIView touchesBegan,touchesMoved,touchesEnded并向数组添加点。您将阵列分为两半,并测试一个阵列中每个点的直径与另一个阵列中对应点的直径是否与其他所有对都大致相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
    NSMutableArray * pointStack;

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        // Detect touch anywhere
    UITouch *touch = [touches anyObject];


    pointStack = [[NSMutableArray alloc]init];

    CGPoint touchDownPoint = [touch locationInView:touch.view];


    [pointStack addObject:touchDownPoint];

    }


    /**
     *
     */
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    {

            UITouch* touch = [touches anyObject];
            CGPoint touchDownPoint = [touch locationInView:touch.view];

            [pointStack addObject:touchDownPoint];  

    }

    /**
     * So now you have an array of lots of points
     * All you have to do is find what should be the diameter
     * Then compare opposite points to see if the reach a similar diameter
     */
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    {
            uint pointCount = [pointStack count];

    //assume the circle was drawn a constant rate and the half way point will serve to calculate or diameter
    CGPoint startPoint = [pointStack objectAtIndex:0];
    CGPoint halfWayPoint = [pointStack objectAtIndex:floor(pointCount/2)];

    float dx = startPoint.x - halfWayPoint.x;
    float dy = startPoint.y - halfWayPoint.y;


    float diameter = sqrt((dx*dx) + (dy*dy));

    bool isCircle = YES;// try to prove false!

    uint indexStep=10; // jump every 10 points, reduce to be more granular

    // okay now compare matches
    // e.g. compare indexes against their opposites and see if they have the same diameter
    //
      for (uint i=indexStep;i<floor(pointCount/2);i+=indexStep)
      {

      CGPoint testPointA = [pointStack objectAtIndex:i];
      CGPoint testPointB = [pointStack objectAtIndex:floor(pointCount/2)+i];

      dx = testPointA.x - testPointB.x;
      dy = testPointA.y - testPointB.y;


      float testDiameter = sqrt((dx*dx) + (dy*dy));

      if(testDiameter>=(diameter-10) && testDiameter<=(diameter+10)) // +/- 10 ( or whatever degree of variance you want )
      {
      //all good
      }
      else
      {
      isCircle=NO;
      }

    }//end for loop

    NSLog(@"iCircle=%i",isCircle);

}

听起来还好吗? :)


我不是形状识别专家,但是这是解决问题的方法。

首先,在以徒手方式显示用户路径的同时,秘密地累积点(x,y)样本列表以及时间。您可以从拖动事件中获得两个事实,将它们包装到一个简单的模型对象中,然后将它们堆积在可变数组中。

您可能想要相当频繁地取样(例如,每0.1秒取样一次)。另一种可能性是开始真正频繁,可能每0.05秒开始一次,并观察用户拖动多长时间。如果拖动时间超过一定时间,则将采样频率降低(并将所有可能遗漏的采样降低)到0.2秒左右。

(也不要把我的数字当作福音,因为我只是从帽子里拿出来。尝试并找到更好的价值。)

其次,分析样本。

您将要得出两个事实。首先,形状的中心(IIRC)应该只是所有点的平均值。其次,每个样本从该中心开始的平均半径。

如@ user1118321所猜测的,如果您想要支持多边形,那么其余的分析就是要做出以下决定:用户是要绘制圆形还是多边形。您可以将样品视为一个多边形来开始进行

您可以使用几个条件:

  • 时间:如果用户在某些点上的停留时间比在其他点上的停留时间更长(如果采样间隔固定,则它们将显示为一组连续的采样,彼此在空间上彼此靠近),这些点可能是角落。您应该将拐角阈值设置得较小,以便用户可以无意识地执行此操作,而不必刻意在每个拐角处暂停。
  • 角度:从一个样本到下一个样本,圆始终具有大致相同的角度。多边形将具有多个由直线段相连的角度;角就是角。对于规则多边形(到不规则多边形的椭圆的圆),拐角角度应大致相同;不规则多边形将具有不同的角角。
  • 间隔:规则多边形的角在角度尺寸内的间隔相等,并且半径将恒定。不规则多边形将具有不规则角度间隔和/或不恒定半径。

第三步,也是最后一步,就是以预先确定的中心点为中心,以预先确定的半径创建形状。

无法保证我上面所说的一切都会奏效或有效,但我希望它至少能使您走上正确的轨道-如果有人比我更了解形状识别(这是一个非常低的标准),请注意为此,请随时发表评论或您自己的答案。


一旦确定用户在其起点处完成绘制形状,就可以对他们绘制的坐标进行采样,然后尝试将其拟合到一个圆上。

这里有一个针对这个问题的MATLAB解决方案:
http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

它基于Walter Gander,Gene H. Golub和Rolf Strebel的论文《圆和椭圆的最小二乘拟合》:
http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

新西兰坎特伯雷大学的Ian Coope博士发表了一篇论文,摘要如下:

The problem of determining the circle of best fit to a set of points
in the plane (or the obvious generalization to n-dimensions) is easily
formulated as a nonlinear total least-squares problem which may be
solved using a Gauss-Newton minimization algorithm. This
straight-forward approach is shown to be inefficient and extremely
sensitive to the presence of outliers. An alternative formulation
allows the problem to be reduced to a linear least squares problem
which is trivially solved. The recommended approach is shown to have
the added advantage of being much less sensitive to outliers than the
nonlinear least squares approach.

http://link.springer.com/article/10.1007%2FBF00939613

MATLAB文件可以计算非线性TLS和线性LLS问题。


我有一个受过良好训练的$ 1识别器,运气很好(http://depts.washington.edu/aimgroup/proj/dollar/)。我将其用于圆形,直线,三角形和正方形。

早在UIGestureRecognizer之前,但我认为创建适当的UIGestureRecognizer子类应该很容易。


这是一个相当简单的使用方法:

1
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

假设此矩阵网格为:

1
2
3
4
5
6
7
8
9
 A B C D E F G H
1      X X
2    X     X
3  X         X
4  X         X
5    X     X
6      X X
7
8

将一些UIView放在" X"位置,并测试它们是否被命中(按顺序)。如果他们都按顺序被击中,我认为让用户说"你画了圆好吗"可能是公平的。

听起来还好吗? (简单)