Dessinez un cercle parfait au contact de l'utilisateur


176

J'ai ce projet pratique qui permet à l'utilisateur de dessiner sur l'écran en se touchant avec ses doigts. Application très simple que j'ai faite comme exercice. Mon petit cousin a pris la liberté de dessiner des choses avec son doigt avec mon iPad sur cette appli (dessins d'enfants: cercle, lignes, etc., tout ce qui lui venait à l'esprit). Puis il a commencé à dessiner des cercles, puis il m'a demandé de faire un "bon cercle" (d'après ce que j'ai compris: faites le cercle dessiné parfaitement rond, car nous savons quelle que soit la stabilité que nous essayons de dessiner avec notre doigt sur l'écran, un un cercle n'est jamais vraiment aussi arrondi qu'un cercle devrait l'être).

Ma question ici est donc la suivante: y a-t-il un moyen dans le code où nous pouvons d'abord détecter une ligne dessinée par l'utilisateur qui forme un cercle et générer approximativement la même taille du cercle en le rendant parfaitement rond sur l'écran. Faire une ligne droite pas si droite est quelque chose que je saurais faire, mais en ce qui concerne le cercle, je ne sais pas trop comment le faire avec Quartz ou d'autres méthodes.

Mon raisonnement est que le point de départ et le point final de la ligne doivent se toucher ou se croiser après que l'utilisateur lève son doigt pour justifier le fait qu'il essayait réellement de dessiner un cercle.


2
Il peut être difficile de faire la différence entre un cercle et un polygone dans ce scénario. Que diriez-vous d'avoir un "Outil Cercle" où l'utilisateur clique pour définir le centre, ou un coin d'un rectangle de délimitation, et fait glisser pour modifier le rayon ou définir le coin opposé?
user1118321

2
@ user1118321: Cela va à l'encontre du concept de simplement pouvoir dessiner un cercle et avoir un cercle parfait. Idéalement, l'application devrait reconnaître uniquement à partir du dessin de l'utilisateur si l'utilisateur a dessiné un cercle (plus ou moins), une ellipse ou un polygone. (De plus, les polygones peuvent ne pas faire partie de la portée de cette application, il peut s'agir simplement de cercles ou de lignes.)
Peter Hosey

Alors, à quelle réponse pensez-vous que je devrais donner la prime? Je vois beaucoup de bons candidats.
Peter Hosey

@Unheilig: Je n'ai aucune expertise sur le sujet, au-delà d'une compréhension naissante du trig. Cela dit, les réponses qui me montrent le plus de potentiel sont stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , peut-être stackoverflow.com/a/ 18992200/30461 , et la mienne. Ce sont ceux que j'essaierais en premier. Je vous laisse l'ordre.
Peter Hosey

1
@Gene: Vous pourriez peut-être résumer les informations pertinentes et créer un lien vers plus de détails dans une réponse.
Peter Hosey

Réponses:


381

Parfois, il est vraiment utile de passer du temps à réinventer la roue. Comme vous l'avez peut-être déjà remarqué, il existe de nombreux frameworks, mais il n'est pas si difficile d'implémenter une solution simple mais utile sans introduire toute cette complexité. (S'il vous plaît ne vous méprenez pas, pour tout objectif sérieux, il est préférable d'utiliser un cadre mature et éprouvé pour être stable).

Je vais d'abord présenter mes résultats, puis expliquer l'idée simple et directe qui les sous-tend.

entrez la description de l'image ici

Vous verrez dans mon implémentation qu'il n'est pas nécessaire d'analyser chaque point et de faire des calculs complexes. L'idée est de repérer des méta-informations précieuses. J'utiliserai la tangente comme exemple:

entrez la description de l'image ici

Identifions un motif simple et direct, typique de la forme sélectionnée:

entrez la description de l'image ici

Il n'est donc pas si difficile de mettre en œuvre un mécanisme de détection de cercle basé sur cette idée. Voir la démo de travail ci-dessous (Désolé, j'utilise Java comme le moyen le plus rapide de fournir cet exemple rapide et un peu sale):

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

L'implémentation d'un comportement similaire sur iOS ne devrait pas poser de problème, car vous n'avez besoin que de plusieurs événements et coordonnées. Quelque chose comme ce qui suit (voir exemple ):

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

Il existe plusieurs améliorations possibles.

Commencez à tout moment

L'exigence actuelle est de commencer à dessiner un cercle à partir du point central supérieur en raison de la simplification suivante:

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

Veuillez noter que la valeur par défaut de indexest utilisée. Une simple recherche dans les «parties» disponibles de la forme supprimera cette limitation. Veuillez noter que vous devrez utiliser un tampon circulaire pour détecter une forme complète:

entrez la description de l'image ici

Dans le sens horaire et antihoraire

Afin de prendre en charge les deux modes, vous devrez utiliser le tampon circulaire de l'amélioration précédente et rechercher dans les deux sens:

entrez la description de l'image ici

Dessinez une ellipse

Vous avez déjà tout ce dont vous avez besoin dans le boundstableau.

entrez la description de l'image ici

Utilisez simplement ces données:

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

Autres gestes (facultatifs)

Enfin, il vous suffit de gérer correctement une situation où dx(ou dy) est égal à zéro afin de prendre en charge d'autres gestes:

entrez la description de l'image ici

Mettre à jour

Ce petit PoC a attiré une grande attention, j'ai donc mis à jour un peu le code afin de le faire fonctionner correctement et de fournir des conseils de dessin, mettre en évidence les points de support, etc.:

entrez la description de l'image ici

Voici le code:

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

76
Réponse spectaculaire Renat. Description claire de l'approche, images qui documentent le processus, animations aussi. Semble aussi la solution la plus généralisée et la plus robuste. Les tangentes semblent être une idée vraiment intelligente - un peu comme les techniques de reconnaissance de l'écriture manuscrite initiale (actuelle?). Question mise en signet pour cette réponse. :)
enhzflep

27
Plus généralement: Une explication ET des diagrammes concis et compréhensibles ET une démo animée ET un code ET des variations? C'est une réponse idéale de Stack Overflow.
Peter Hosey

11
C'est une si bonne réponse, je peux presque pardonner qu'il fait de l'infographie en Java! ;)
Nicolas Miari

4
Y aura-t-il des mises à jour plus surprenantes (c'est-à-dire plus de formes, etc.) pour ce Noël, Santa Renat? :-)
Unheilig

1
Sensationnel. Tour de force.
wogsland

14

Une technique classique de vision par ordinateur pour détecter une forme est la transformation de Hough. L'un des avantages de la transformation de Hough est qu'elle tolère très bien les données partielles, les données imparfaites et le bruit. Utilisation de Hough pour un cercle: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

Étant donné que votre cercle est dessiné à la main, je pense que la transformation de Hough peut vous convenir.

Voici une explication "simplifiée", je m'excuse de ne pas vraiment être aussi simple. Une grande partie provient d'un projet scolaire que j'ai réalisé il y a de nombreuses années.

La transformation de Hough est un système de vote. Un tableau à deux dimensions d'entiers est alloué et tous les éléments sont mis à zéro. Chaque élément correspond à un seul pixel de l'image en cours d'analyse. Ce tableau est appelé tableau accumulateur puisque chaque élément va accumuler des informations, des votes, indiquant la possibilité qu'un pixel puisse être à l'origine d'un cercle ou d'un arc.

Un détecteur de bord d'opérateur de gradient est appliqué à l'image et les pixels de bord, ou bords, sont enregistrés. Un edgel est un pixel qui a une intensité ou une couleur différente par rapport à ses voisins. Le degré de différence est appelé la magnitude du gradient. Pour chaque edgel d'une amplitude suffisante, un schéma de vote est appliqué qui incrémentera les éléments du tableau d'accumulateurs. Les éléments incrémentés (votés pour) correspondent aux origines possibles des cercles qui traversent l'edgel considéré. Le résultat souhaité est que si un arc existe, la véritable origine recevra plus de votes que les fausses origines.

Notez que les éléments du tableau d'accumulateurs visités pour voter forment un cercle autour de l'edgel considéré. Le calcul des coordonnées x, y pour lesquelles voter est le même que celui des coordonnées x, y d'un cercle que vous dessinez.

Dans votre image dessinée à la main, vous pourrez peut-être utiliser directement les pixels (colorés) définis plutôt que de calculer les bords.

Désormais, avec des pixels imparfaits, vous n'obtiendrez pas nécessairement un seul élément de tableau accumulateur avec le plus grand nombre de votes. Vous pouvez obtenir une collection d'éléments de tableau voisins avec un tas de votes, un cluster. Le centre de gravité de cet amas peut offrir une bonne approximation de l'origine.

Notez que vous devrez peut-être exécuter la transformation de Hough pour différentes valeurs de rayon R. Celui qui produit le groupe de votes le plus dense est le «meilleur» ajustement.

Il existe différentes techniques à utiliser pour réduire les votes pour les fausses origines. Par exemple, un avantage de l'utilisation des bordures est qu'ils ont non seulement une grandeur mais aussi une direction. Lors du vote, il suffit de voter pour les origines possibles dans la direction appropriée. Les lieux recevant des votes formeraient un arc plutôt qu'un cercle complet.

Voici un exemple. Nous commençons par un cercle de rayon un et un tableau d'accumulateurs initialisé. Comme chaque pixel est considéré, les origines potentielles sont votées. La véritable origine reçoit le plus de voix, ce qui dans ce cas est de quatre.

.  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

Voici une autre manière. Utilisation de UIView touchesBegan, touchesMoved, touchesEnded et ajout de points à un tableau. Vous divisez le tableau en deux et testez si chaque point d'un tableau a à peu près le même diamètre que son homologue de l'autre tableau que toutes les autres paires.

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

}

Ça vous va? :)


3

Je ne suis pas un expert en reconnaissance de formes, mais voici comment je pourrais aborder le problème.

Tout d'abord, tout en affichant le chemin de l'utilisateur à main levée, accumulez secrètement une liste d'échantillons ponctuels (x, y) avec les temps. Vous pouvez obtenir les deux faits à partir de vos événements de glissement, les envelopper dans un objet modèle simple et les empiler dans un tableau mutable.

Vous voudrez probablement prélever les échantillons assez fréquemment, disons toutes les 0,1 seconde. Une autre possibilité serait de commencer vraiment fréquemment, peut-être toutes les 0,05 secondes, et de regarder combien de temps l'utilisateur traîne; s'ils traînent plus longtemps qu'un certain temps, abaissez la fréquence d'échantillonnage (et déposez tous les échantillons qui auraient été manqués) à quelque chose comme 0,2 seconde.

(Et ne prenez pas mes chiffres pour un évangile, parce que je les ai simplement sortis de mon chapeau. Expérimentez et trouvez de meilleures valeurs.)

Deuxièmement, analysez les échantillons.

Vous voudrez tirer deux faits. Tout d'abord, le centre de la forme, qui (IIRC) devrait être juste la moyenne de tous les points. Deuxièmement, le rayon moyen de chaque échantillon à partir de ce centre.

Si, comme @ user1118321 l'a deviné, vous souhaitez prendre en charge les polygones, le reste de l'analyse consiste à prendre cette décision: si l'utilisateur souhaite dessiner un cercle ou un polygone. Vous pouvez commencer par regarder les échantillons comme un polygone pour effectuer cette détermination.

Vous pouvez utiliser plusieurs critères:

  • Temps: Si l'utilisateur survole plus longtemps à certains points que d'autres (qui, si les échantillons sont à intervalle constant, apparaîtra comme un groupe d'échantillons consécutifs proches les uns des autres dans l'espace), il peut s'agir de coins. Vous devez réduire votre seuil de coin afin que l'utilisateur puisse le faire inconsciemment, plutôt que de devoir délibérément faire une pause à chaque coin.
  • Angle: Un cercle aura à peu près le même angle d'un échantillon à l'autre tout autour. Un polygone aura plusieurs angles joints par des segments de ligne droite; les angles sont les coins. Pour un polygone régulier (du cercle à l'ellipse d'un polygone irrégulier), les angles de coin doivent tous être à peu près les mêmes; un polygone irrégulier aura des angles de coin différents.
  • Intervalle: les coins d'un polygone régulier seront espacés de manière égale dans la dimension angulaire et le rayon sera constant. Un polygone irrégulier aura des intervalles angulaires irréguliers et / ou un rayon non constant.

La troisième et dernière étape consiste à créer la forme, centrée sur le point central précédemment déterminé, avec le rayon précédemment déterminé.

Rien ne garantit que tout ce que j'ai dit ci-dessus fonctionnera ou sera efficace, mais j'espère que cela vous mettra au moins sur la bonne voie - et s'il vous plaît, si quelqu'un qui en sait plus sur la reconnaissance de forme que moi (qui est une barre très basse) voit cela, n'hésitez pas à poster un commentaire ou votre propre réponse.


+1 Salut, merci pour l'entrée. Très instructif. De même, je souhaite que le surhomme iOS / "reconnaissance de forme" verra d'une manière ou d'une autre ce message et nous éclairera davantage.
Unheilig

1
@Unheilig: Bonne idée. Terminé.
Peter Hosey

1
Votre algorithme sonne bien. J'ajouterais un contrôle sur la distance entre le chemin de l'utilisateur et un cercle / polygone parfait. (Par exemple, écart carré moyen en pourcentage.) S'il est trop grand, l'utilisateur peut ne pas vouloir la forme idéale. Pour un doodler expérimenté, la limite serait plus petite que pour un doodler bâclé. Avoir cela permettrait au programme de donner la liberté artistique aux artistes mais beaucoup d'aide aux débutants.
dmm

@ user2654818: Comment mesureriez-vous cela?
Peter Hosey

1
@PeterHosey: Explication des cercles: une fois que vous avez le cercle idéal, vous avez le centre et le rayon. Donc, vous prenez chaque point dessiné et calculez sa distance carrée du centre, qui est ((x-x0) ^ 2 + (y-y0) ^ 2). Soustrayez cela du rayon au carré. (J'évite beaucoup de racines carrées pour enregistrer le calcul.) Appelez cela l'erreur quadratique pour un point dessiné. Moyenne de l'erreur quadratique pour tous les points dessinés, puis racine carrée, puis divisez-la par le rayon. C'est votre pourcentage moyen de divergence. (Les mathématiques / statistiques sont probablement dignes de grincer des dents, mais cela fonctionnerait dans la pratique.)
dmm

2

J'ai eu assez de chance avec un module de reconnaissance à 1 $ correctement formé ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Je l'ai utilisé pour les cercles, les lignes, les triangles et les carrés.

C'était il y a longtemps, avant UIGestureRecognizer, mais je pense qu'il devrait être facile de créer des sous-classes UIGestureRecognizer appropriées.


2

Une fois que vous avez déterminé que l'utilisateur a fini de dessiner sa forme là où il a commencé, vous pouvez prendre un échantillon des coordonnées qu'il a dessinées et essayer de les ajuster à un cercle.

Il existe une solution MATLAB à ce problème ici: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

Ce qui est basé sur l'article Ajustement des moindres carrés de cercles et d'ellipses par Walter Gander, Gene H. Golub et Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

Le Dr Ian Coope de l'Université de Canterbury, NZ a publié un article avec le résumé:

Le problème de la détermination du cercle de meilleur ajustement à un ensemble de points dans le plan (ou la généralisation évidente aux n dimensions) est facilement formulé comme un problème des moindres carrés totaux non linéaire qui peut être résolu en utilisant un algorithme de minimisation de Gauss-Newton. Cette approche simple se révèle inefficace et extrêmement sensible à la présence de valeurs aberrantes. Une formulation alternative permet de réduire le problème à un problème linéaire des moindres carrés qui est résolu de manière triviale. L'approche recommandée présente l'avantage supplémentaire d'être beaucoup moins sensible aux valeurs aberrantes que l'approche des moindres carrés non linéaires.

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

Le fichier MATLAB peut calculer à la fois le problème TLS non linéaire et LLS linéaire.


0

Voici un moyen assez simple d'utiliser:

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

en supposant cette grille matricielle:

 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

Placez quelques UIViews sur les emplacements "X" et testez-les pour devenir touchés (dans l'ordre). S'ils sont tous touchés dans l'ordre, je pense qu'il pourrait être juste de laisser l'utilisateur dire "Bravo, vous avez dessiné un cercle"

Ça vous va? (et simple)


Salut, Lemon. Bon raisonnement, mais dans le scénario ci-dessus, cela signifie que nous aurions besoin de 64 UIViews pour détecter les touches, non? Et comment définiriez-vous la taille d'un seul UIView si le canevas a la taille d'un iPad par exemple? Il semble que si le cercle est petit et si la taille d'un seul UIView est plus grande, dans ce cas nous ne pourrions pas vérifier la séquence car tous les points dessinés se trouveraient dans un seul UIView.
Unheilig du

Oui - celui-ci ne fonctionne probablement que si vous fixez le canevas à quelque chose comme 300x300 et que vous avez ensuite un canevas "exemple" à côté avec la taille du cercle que vous cherchez à dessiner par l'utilisateur. Si c'est le cas, j'irais avec des carrés 50x50 * 6, vous n'avez également besoin que de rendre les vues que vous souhaitez atteindre aux bons endroits, pas tous 6 * 6 (36) ou 8 * 8 (64)
dijipiji

@Unheilig: C'est ce que fait cette solution. Tout ce qui est assez circulaire pour passer à travers une séquence correcte de vues (et vous pourriez éventuellement autoriser un nombre maximum de détours pour une pente supplémentaire) correspondra à un cercle. Vous l'alignez ensuite sur un cercle parfait centré au centre de toutes ces vues, dont le rayon atteint toutes (ou au moins la plupart) d'entre elles.
Peter Hosey

@PeterHosey Ok, laissez-moi essayer de comprendre ça. J'apprécierais si l'un de vous pouvait fournir du code pour faire avancer les choses. En attendant, je vais également essayer de comprendre cela et ensuite je ferai de même avec la partie codage. Merci.
Unheilig du

Je viens de soumettre une autre façon pour vous qui, je pense, pourrait être meilleure
dijipiji
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.