通过用户的触摸画出完美的圆圈


178

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

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

我的理由是,在用户抬起手指以证明他实际上试图绘制一个圆这一事实之后,直线的起点和终点必须彼此接触或交叉。


2
在这种情况下,很难说出圆形和多边形之间的区别。在用户单击以定义中心或边界矩形的一个角并拖动以更改半径或设置相反角的情况下,使用“圆形工具”怎么样?
user1118321 2013年

2
@ user1118321:这打破了只能画一个圆并具有一个完美圆的概念。理想情况下,应用程序应该仅从用户的绘图中识别出用户是绘制了一个圆形(或多或少),一个椭圆形还是一个多边形。(此外,此应用程序可能不在多边形范围内,可能只是圆形或直线。)
Peter Hosey 2013年

那么,您认为我应该给哪个答案赏金?我看到很多优秀的候选人。
Peter Hosey 2013年

@Unheilig:除了对trig的初步了解之外,我没有任何专业知识。这就是说,这显示出最有潜力给我的答案是stackoverflow.com/a/19071980/30461stackoverflow.com/a/19055873/30461stackoverflow.com/a/18995771/30461,也许stackoverflow.com/a/ 18992200/30461和我自己的。这些是我会首先尝试的。我把订单留给你。
Peter Hosey 2013年

1
@Gene:也许您可以在回答中总结相关信息,并链接到更多详细信息。
彼得·霍西

Answers:


386

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

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

在此处输入图片说明

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

在此处输入图片说明

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

在此处输入图片说明

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

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上实现类似行为应该不是问题,因为您只需要几个事件和坐标即可。类似于以下内容(请参见示例):

- (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];    
}

有几种可能的增强功能。

随时开始

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

        if(type == null || type != newType) {
            if(newType != shape[index]) {
                break;
            }
            bounds[index] = current;
            detected[index++] = newType;
        }

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

在此处输入图片说明

顺时针和逆时针

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

在此处输入图片说明

画一个椭圆

您已经在bounds阵列中拥有了所需的一切。

在此处输入图片说明

只需使用该数据:

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

其他手势(可选)

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

在此处输入图片说明

更新资料

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

在此处输入图片说明

这是代码:

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);
}

77
壮观的回答雷纳特。方法的清晰描述,记录过程的图像以及动画。似乎也是最通用,最可靠的解决方案。切线听起来像一个非常聪明的主意-很像最初的(当前的)手写识别技术。为这个答案加上了书签的问题。:)
enhzflep

28
更笼统地说:简明易懂的说明和图表,动画演示和代码以及变化形式?这是一个理想的堆栈溢出答案。
彼得·霍西

11
这是一个很好的答案,我几乎可以原谅他正在用Java做计算机图形!;)
Nicolas Miari

4
圣雷纳特(Santa Renat)这个圣诞节是否还会有令人惊讶的更新(例如,更多形状等等)?:-)
Unheilig

1
哇。环法自行车赛。
wogsland

14

用于检测形状的经典计算机视觉技术是霍夫变换。霍夫变换的优点之一是它可以很好地容忍部分数据,不完善的数据和噪声。使用霍夫转一圈:http : //en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

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

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

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

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

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

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

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

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

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

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

.  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

5

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

    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);

}

听起来还好吗?:)


3

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

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

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

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

其次,分析样本。

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

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

您可以使用几个条件:

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

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

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


+1您好,感谢您的输入。非常丰富。同样,我希望iOS /“形状识别”超人能以某种方式看到这篇文章并进一步启发我们。
Unheilig 2013年

1
@Unheilig:好主意。做完了
Peter Hosey 2013年

1
您的算法听起来不错。我将添加一条检查,以了解用户的路径与理想的圆/多边形之间的距离。(例如,百分比均方偏差。)如果太大,则用户可能不想要理想的形状。对于熟练的涂鸦者,截止频率将比草率的涂鸦者小。有了这个功能,程序就可以给艺术家以艺术上的自由,但对初学者则有很多帮助。
dmm

@ user2654818:您将如何衡量?
Peter Hosey 2013年

1
@PeterHosey:圆的解释:有了理想的圆之后,便有了中心和半径。因此,您获取每个绘制的点,并计算其与中心的平方距离,即((x-x0)^ 2 +(y-y0)^ 2)。从半径平方中减去该值。(为了避免计算,我避免了很多平方根。)称绘制点的平方误差。对所有绘制点的平方误差求平均值,然后将其平方根,然后将其除以半径。这就是您的平均差异百分比。(数学/统计信息可能很有价值,但实际上可以使用。)
dmm 2013年


2

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

这里有一个针对这个问题的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博士发表了一篇论文,摘要如下:

确定最适合平面中一组点的圆的问题(或明显地概括为n维)很容易地表述为非线性总最小二乘问题,可以使用高斯-牛顿最小化算法解决。事实证明,这种简单的方法效率低下,并且对异常值的存在极为敏感。一种替代的公式使问题可以简化为线性最小二乘问题,这是很容易解决的。结果表明,与非线性最小二乘法相比,推荐的方法具有对异常值不敏感得多的优点。

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

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


0

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

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

假设此矩阵网格为:

 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”位置,并测试它们是否被命中(依次)。如果他们都按顺序被击中,我认为让用户说“你画了圆好吗”可能是公平的。

听起来还好吗?(简单)


嗨柠檬 有充分的理由,但是在上述情况下,这意味着我们将需要64个UIView来检测触摸,对吗?如果画布是iPad的大小,那么如何定义单个UIView的大小?看来,如果圆圈很小,并且单个UIView的大小较大,则在这种情况下,我们将无法检查序列,因为所有绘制的点都将位于单个UIView内。
Unheilig

是的-仅当您将画布固定为300x300之类,然后在其旁边有一个“示例”画布且您正在寻找用户绘制的圆形大小时,此画布才可能起作用。如果是这样,我将使用50x50的正方形* 6,那么您还只需要渲染您感兴趣的视图即可将其击中正确的位置,而不是全部显示6 * 6(36)或8 * 8(64)
dijipiji 2013年

@Unheilig:这就是该解决方案的作用。足以通过正确视图序列的任何圆形(并且您可能允许最大弯路次数以增加倾斜度)将匹配为一个圆形。然后,将其捕捉到以所有这些视图的中心为中心的完美圆,其半径达到所有(或至少大多数)视图的半径。
Peter Hosey 2013年

@PeterHosey好的,让我尝试着解决这个问题。如果您能提供一些代码来完成此工作,我将不胜感激。同时,我还将尝试绕过这个问题,然后再对编码部分进行相同的处理。谢谢。
Unheilig 2013年

刚刚为您提交了另一种方法,我认为可能会更好
dijipiji 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.