Dibuja un círculo perfecto desde el toque del usuario

176

Tengo este proyecto de práctica que permite al usuario dibujar en la pantalla mientras toca con los dedos. Aplicación muy simple que hice como ejercicio hace mucho tiempo. Mi primo se tomó la libertad de dibujar cosas con su dedo con mi iPad en esta aplicación (dibujos para niños: círculos, líneas, etc., lo que se le ocurriera). Luego comenzó a dibujar círculos y luego me pidió que lo hiciera "buen círculo" (según tengo entendido: haga que el círculo dibujado sea perfectamente redondo, ya que sabemos que no importa cuán estable tratemos de dibujar algo con nuestro dedo en la pantalla, un el círculo nunca es realmente tan redondeado como debería ser un círculo).

Entonces, mi pregunta aquí es: ¿hay alguna forma en el código en la que podamos detectar primero una línea dibujada por el usuario que forme un círculo y genere aproximadamente el mismo tamaño del círculo haciéndolo perfectamente redondo en la pantalla? Hacer una línea no tan recta es algo que sabría hacer, pero en cuanto al círculo, no sé cómo hacerlo con Quartz u otros métodos.

Mi razonamiento es que, el punto inicial y final de la línea debe tocarse o cruzarse después de que el usuario levanta el dedo para justificar el hecho de que estaba tratando de dibujar un círculo.

Unheilig
fuente
2
Puede ser difícil distinguir entre un círculo y un polígono en este escenario. ¿Qué tal tener una "Herramienta de círculo" donde el usuario hace clic para definir el centro, o una esquina de un rectángulo delimitador, y arrastra para cambiar el radio o establecer la esquina opuesta?
user1118321
2
@ user1118321: Esto frustra el concepto de poder dibujar un círculo y tener un círculo perfecto. Idealmente, la aplicación debería reconocer solo en el dibujo del usuario si el usuario dibujó un círculo (más o menos), una elipse o un polígono. (Además, es posible que los polígonos no estén dentro del alcance de esta aplicación; pueden ser solo círculos o líneas).
Peter Hosey
Entonces, ¿a qué respuesta crees que debería darle la recompensa? Veo muchos buenos candidatos.
Peter Hosey
@Unheilig: No tengo ninguna experiencia en el tema, más allá de una comprensión incipiente de trigonometría. Dicho esto, las respuestas que muestran el mayor potencial para mí son stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , tal vez stackoverflow.com/a/ 18992200/30461 , y la mía. Esos son los que probaría primero. Te dejo la orden.
Peter Hosey
1
@Gene: Quizás podría resumir la información relevante y vincular a más detalles, en una respuesta.
Peter Hosey el

Respuestas:

381

A veces es realmente útil pasar un tiempo reinventando la rueda. Como ya habrás notado, hay muchos frameworks, pero no es tan difícil implementar una solución simple pero útil sin introducir toda esa complejidad. (Por favor, no me malinterpreten, para cualquier propósito serio es mejor usar un marco estable y probado).

Presentaré mis resultados primero y luego explicaré la idea simple y directa detrás de ellos.

ingrese la descripción de la imagen aquí

Verá en mi implementación que no hay necesidad de analizar cada punto y hacer cálculos complejos. La idea es detectar alguna metainformación valiosa. Voy a utilizar tangente como un ejemplo:

ingrese la descripción de la imagen aquí

Identifiquemos un patrón simple y directo, típico de la forma seleccionada:

ingrese la descripción de la imagen aquí

Por lo tanto, no es tan difícil implementar un mecanismo de detección de círculo basado en esa idea. Vea la demostración de trabajo a continuación (Lo siento, estoy usando Java como la forma más rápida de proporcionar este ejemplo rápido y un poco sucio):

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

Implementar un comportamiento similar en iOS no debería ser un problema, ya que solo necesita varios eventos y coordenadas. Algo como lo siguiente (ver ejemplo ):

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

Hay varias mejoras posibles.

Comience en cualquier punto

El requisito actual es comenzar a dibujar un círculo desde el punto medio superior debido a la siguiente simplificación:

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

Tenga en cuenta que indexse utiliza el valor predeterminado de . Una simple búsqueda a través de las "partes" disponibles de la forma eliminará esa limitación. Tenga en cuenta que necesitará usar un búfer circular para detectar una forma completa:

ingrese la descripción de la imagen aquí

En sentido horario y antihorario

Para admitir ambos modos, deberá utilizar el búfer circular de la mejora anterior y buscar en ambas direcciones:

ingrese la descripción de la imagen aquí

Dibujar una elipse

Ya tienes todo lo que necesitas en la boundsmatriz.

ingrese la descripción de la imagen aquí

Simplemente use esos datos:

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

Otros gestos (opcional)

Finalmente, solo necesita manejar adecuadamente una situación cuando dx(o dy) es igual a cero para admitir otros gestos:

ingrese la descripción de la imagen aquí

Actualizar

Este pequeño PoC recibió bastante atención, por lo que actualicé un poco el código para que funcione sin problemas y proporcione algunas sugerencias de dibujo, resalte puntos de apoyo, etc.

ingrese la descripción de la imagen aquí

Aquí está el código:

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);
}
Renat Gilmanov
fuente
76
Espectacular respuesta Renat. Descripción clara del enfoque, imágenes que documentan el proceso, animaciones también. También parece la solución más generalizada y robusta. Las tangentes suenan como una idea realmente inteligente, muy parecida a las técnicas iniciales de reconocimiento de escritura a mano. Pregunta marcada por el bien de esta respuesta. :)
enhzflep 01 de
27
Más en general: ¿Una explicación Y diagramas concisos y comprensibles Y una demostración animada Y un código Y variaciones? Esta es una respuesta ideal para el desbordamiento de pila.
Peter Hosey
11
Esta es una buena respuesta, casi puedo perdonar que esté haciendo gráficos por computadora en Java. ;)
Nicolas Miari
44
¿Habrá más actualizaciones sorprendentes (es decir, más formas, etc.) para esta Navidad, Santa Renat? :-)
Unheilig
1
Guau. Proeza.
wogsland
14

Una técnica clásica de visión por computadora para detectar una forma es la Transformación de Hough. Una de las cosas buenas de la Transformación Hough es que es muy tolerante con los datos parciales, los datos imperfectos y el ruido. Usando Hough para un círculo: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

Dado que su círculo está dibujado a mano, creo que la transformación de Hough puede ser una buena combinación para usted.

Aquí hay una explicación "simplificada", me disculpo por no ser tan simple. Gran parte es de un proyecto escolar que hice hace muchos años.

La transformación de Hough es un esquema de votación. Se asigna una matriz bidimensional de enteros y todos los elementos se establecen en cero. Cada elemento corresponde a un solo píxel en la imagen que se analiza. Este conjunto se denomina conjunto de acumuladores, ya que cada elemento acumulará información, votos, lo que indica la posibilidad de que un píxel pueda estar en el origen de un círculo o arco.

Se aplica un detector de borde de operador de gradiente a la imagen y se registran los píxeles de borde, o edgels. Un edgel es un píxel que tiene una intensidad o color diferente con respecto a sus vecinos. El grado de diferencia se llama la magnitud del gradiente. Para cada edgel de magnitud suficiente se aplica un esquema de votación que incrementará los elementos del conjunto de acumuladores. Los elementos que se incrementan (votan) corresponden a los posibles orígenes de los círculos que pasan por el edgel en consideración. El resultado deseado es que si existe un arco, el origen verdadero recibirá más votos que los orígenes falsos.

Tenga en cuenta que los elementos del conjunto de acumuladores que se visitan para votar forman un círculo alrededor del edgel en consideración. Calcular las coordenadas x, y para votar es lo mismo que calcular las coordenadas x, y de un círculo que está dibujando.

En su imagen dibujada a mano, puede usar los píxeles del conjunto (de color) directamente en lugar de calcular edgels.

Ahora, con píxeles ubicados de manera imperfecta, no necesariamente obtendrá un solo elemento de matriz de acumulador con el mayor número de votos. Puede obtener una colección de elementos de matriz vecinos con un montón de votos, un clúster. El centro de gravedad de este grupo puede ofrecer una buena aproximación para el origen.

Tenga en cuenta que es posible que deba ejecutar la Transformación de Hough para diferentes valores de radio R. El que produce el grupo de votos más denso es el ajuste "mejor".

Existen varias técnicas para reducir votos por orígenes falsos. Por ejemplo, una ventaja de usar edgels es que no solo tienen una magnitud sino que también tienen una dirección. Al votar solo necesitamos votar por posibles orígenes en la dirección apropiada. Los lugares que reciben votos formarían un arco en lugar de un círculo completo.

Aquí hay un ejemplo. Comenzamos con un círculo de radio uno y una matriz de acumulador inicializada. Como cada píxel se considera, se votan los orígenes potenciales. El verdadero origen recibe la mayoría de los votos, que en este caso son cuatro.

.  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
perpenso
fuente
5

Aquí hay otra manera. Usar UIView touchesBegan, touchesMoved, touchesEnded y agregar puntos a una matriz. Divide la matriz en mitades y prueba si cada punto de una matriz tiene aproximadamente el mismo diámetro que su contraparte en la otra matriz como todos los otros pares.

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

}

Eso suena bien? :)

dijipiji
fuente
3

No soy un experto en reconocimiento de formas, pero así es como podría abordar el problema.

Primero, mientras muestra la ruta del usuario como a mano alzada, acumule secretamente una lista de muestras de puntos (x, y) junto con los tiempos. Puede obtener ambos hechos de sus eventos de arrastre, envolverlos en un objeto modelo simple y agruparlos en una matriz mutable.

Probablemente quiera tomar las muestras con bastante frecuencia, por ejemplo, cada 0.1 segundos. Otra posibilidad sería comenzar con mucha frecuencia, tal vez cada 0.05 segundos, y observar cuánto tiempo arrastra el usuario; si arrastran más de una cantidad de tiempo, reduzca la frecuencia de muestreo (y suelte las muestras que se habrían perdido) a algo así como 0.2 segundos.

(Y no tome mis números como evangelio, porque los saqué de mi sombrero. Experimente y encuentre mejores valores).

Segundo, analizar las muestras.

Querrás derivar dos hechos. Primero, el centro de la forma, que (IIRC) debería ser el promedio de todos los puntos. Segundo, el radio promedio de cada muestra desde ese centro.

Si, como adivinó @ user1118321, desea admitir polígonos, el resto del análisis consiste en tomar esa decisión: si el usuario quiere dibujar un círculo o un polígono. Puede comenzar a ver las muestras como un polígono para hacer esa determinación.

Hay varios criterios que puede usar:

  • Tiempo: si el usuario pasa más tiempo en algunos puntos que en otros (que, si las muestras están en un intervalo constante, aparecerán como un grupo de muestras consecutivas cerca del otro en el espacio), pueden ser esquinas. Debe hacer que su umbral de esquina sea pequeño para que el usuario pueda hacer esto inconscientemente, en lugar de tener que pausar deliberadamente en cada esquina.
  • Ángulo: un círculo tendrá aproximadamente el mismo ángulo de una muestra a la siguiente completamente. Un polígono tendrá varios ángulos unidos por segmentos de línea recta; Los ángulos son las esquinas. Para un polígono regular (el círculo a la elipse de un polígono irregular), los ángulos de las esquinas deben ser aproximadamente iguales; Un polígono irregular tendrá diferentes ángulos de esquina.
  • Intervalo: las esquinas de un polígono regular estarán separadas por el mismo espacio dentro de la dimensión angular, y el radio será constante. Un polígono irregular tendrá intervalos angulares irregulares y / o un radio no constante.

El tercer y último paso es crear la forma, centrada en el punto central previamente determinado, con el radio previamente determinado.

No hay garantías de que nada de lo que dije anteriormente funcione o sea eficiente, pero espero que al menos lo lleve por el camino correcto, y por favor, si alguien que sabe más sobre el reconocimiento de formas que yo (que es una barra muy baja) ve esto, no dude en publicar un comentario o su propia respuesta.

Peter Hosey
fuente
+1 Hola, gracias por el aporte. Muy informativo. Del mismo modo, deseo que el superhombre iOS / "reconocimiento de formas" vea de alguna manera esta publicación y nos ilumine aún más.
Unheilig
1
@Unheilig: Buena idea. Hecho.
Peter Hosey
1
Tu algoritmo suena bien. Agregaría una verificación de cuán lejos la ruta del usuario divergió de un círculo / polígono perfecto. (Por ejemplo, porcentaje de desviación cuadrática media). Si es demasiado grande, el usuario podría no querer la forma ideal. Para un garabato experto, el límite sería menor que para un garabato descuidado. Tener esto permitiría al programa dar libertad artística a los artistas pero mucha ayuda a los principiantes.
dmm
@ user2654818: ¿Cómo medirías eso?
Peter Hosey
1
@ PeterHosey: Explicación para los círculos: una vez que tienes el círculo ideal, tienes el centro y el radio. Entonces, toma cada punto dibujado y calcula su distancia cuadrada desde el centro, que es ((x-x0) ^ 2 + (y-y0) ^ 2). Resta eso del radio al cuadrado. (Estoy evitando muchas raíces cuadradas para guardar el cálculo). Llame a eso el error al cuadrado para un punto dibujado. Promedio del error al cuadrado para todos los puntos dibujados, luego raíz cuadrada, luego divídalo por el radio. Ese es su porcentaje promedio de divergencia. (Las matemáticas / estadísticas probablemente sean muy difíciles, pero funcionaría en la práctica.)
dmm
2

He tenido bastante suerte con un reconocedor de $ 1 debidamente entrenado ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Lo usé para círculos, líneas, triángulos y cuadrados.

Fue hace mucho tiempo, antes de UIGestureRecognizer, pero creo que debería ser fácil crear subclases de UIGestureRecognizer adecuadas.

Martin Adoue
fuente
2

Una vez que determine que el usuario terminó de dibujar su forma donde comenzó, puede tomar una muestra de las coordenadas que dibujaron e intentar ajustarlas a un círculo.

Aquí hay una solución MATLAB para este problema: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

El cual se basa en el ajuste de mínimos cuadrados en papel de círculos y elipses de Walter Gander, Gene H. Golub y Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

El Dr. Ian Coope de la Universidad de Canterbury, Nueva Zelanda, publicó un artículo con el resumen:

El problema de determinar el círculo de mejor ajuste a un conjunto de puntos en el plano (o la generalización obvia a n-dimensiones) se formula fácilmente como un problema de mínimos cuadrados totales no lineales que puede resolverse utilizando un algoritmo de minimización de Gauss-Newton. Este enfoque directo se muestra ineficiente y extremadamente sensible a la presencia de valores atípicos. Una formulación alternativa permite que el problema se reduzca a un problema lineal de mínimos cuadrados que se resuelve trivialmente. Se muestra que el enfoque recomendado tiene la ventaja adicional de ser mucho menos sensible a los valores atípicos que el enfoque de mínimos cuadrados no lineales.

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

El archivo MATLAB puede calcular tanto el problema TLS no lineal como el problema LLS lineal.

David Lawson
fuente
0

Aquí hay una manera bastante simple de usar:

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

asumiendo esta cuadrícula matricial:

 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

Coloque algunas UIViews en las ubicaciones "X" y pruébelas para que sean golpeadas (en secuencia). Si todos son golpeados en secuencia, creo que sería justo dejar que el usuario diga "Bien hecho, dibujaste un círculo"

Suena bien? (y simple)

dijipiji
fuente
Hola limon Buen razonamiento, pero en el escenario anterior, significa que necesitaríamos tener 64 UIViews para detectar los toques, ¿verdad? ¿Y cómo definiría el tamaño de una sola UIView si el lienzo es del tamaño de un iPad, por ejemplo? Parece que si el círculo es pequeño y si el tamaño de una sola UIView es más grande, en este caso no podríamos verificar la secuencia porque todos los puntos dibujados estarían dentro de una sola UIView.
Unheilig
Sí, este probablemente solo funcione si fija el lienzo a algo así como 300x300 y luego tiene un lienzo de "ejemplo" al lado con el tamaño del círculo que está buscando que dibuje el usuario. Si es así, elegiría 50x50 cuadrados * 6, también solo necesita renderizar las Vistas que le interesan en las ubicaciones correctas, no todas 6 * 6 (36) u 8 * 8 (64)
dijipiji
@Unheilig: Eso es lo que hace esta solución. Cualquier cosa lo suficientemente circular como para pasar por una secuencia correcta de vistas (y podría permitir un número máximo de desvíos para una pendiente adicional) coincidirá como un círculo. Luego lo ajusta en un círculo perfecto centrado en el centro de todas esas vistas, cuyo radio alcanza todas (o al menos la mayoría) de ellas.
Peter Hosey
@ PeterHosey Ok, déjame tratar de entender esto. Agradecería si alguno de ustedes pudiera proporcionar algún código para que esto funcione. Mientras tanto, también trataré de entender esto y luego haré lo mismo con la parte de codificación. Gracias.
Unheilig
Acabo de presentar otra forma para ti que creo que podría ser mejor
Dijipiji