Disegna un cerchio perfetto dal tocco dell'utente


176

Ho questo progetto di pratica che consente all'utente di disegnare sullo schermo mentre si toccano con le dita. App molto semplice che ho fatto come esercizio molto tempo fa. Mio cugino si è preso la libertà di disegnare le cose con il dito con il mio iPad su questa app (disegni per bambini: cerchio, linee, ecc., Qualunque cosa gli venisse in mente). Poi ha iniziato a disegnare cerchi e poi mi ha chiesto di renderlo "buon cerchio" (dalla mia comprensione: rendere il cerchio disegnato perfettamente rotondo, poiché sappiamo non importa quanto stabile cerchiamo di disegnare qualcosa con il dito sullo schermo, un il cerchio non è mai veramente arrotondato come dovrebbe essere un cerchio).

Quindi la mia domanda qui è che, c'è un modo nel codice in cui possiamo prima rilevare una linea tracciata dall'utente che forma un cerchio e generare approssimativamente la stessa dimensione del cerchio rendendolo perfettamente rotondo sullo schermo. Fare una retta non così dritta è qualcosa che vorrei sapere come fare, ma per quanto riguarda il cerchio, non so proprio come procedere con Quartz o altri metodi.

Il mio ragionamento è che, il punto iniziale e quello finale della linea devono toccarsi o incrociarsi dopo che l'utente ha alzato il dito per giustificare il fatto che stesse cercando di disegnare effettivamente un cerchio.


2
In questo scenario può essere difficile distinguere tra un cerchio e un poligono. Che ne dici di avere uno "Strumento cerchio" in cui l'utente fa clic per definire il centro, o un angolo di un rettangolo di delimitazione, e trascina per cambiare il raggio o impostare l'angolo opposto?
user1118321

2
@ user1118321: Questo sconfigge il concetto di poter solo disegnare un cerchio e avere un cerchio perfetto. Idealmente, l'app dovrebbe riconoscere dal solo disegno dell'utente se l'utente ha disegnato un cerchio (più o meno), un'ellisse o un poligono. (Inoltre, i poligoni potrebbero non essere nell'ambito di questa app, potrebbero essere solo cerchi o linee.)
Peter Hosey,

Quindi, a quale risposta pensi che dovrei dare la generosità? Vedo molti buoni candidati.
Peter Hosey,

@Unheilig: non ho alcuna competenza in materia, al di là di una comprensione nascente del trig. Detto questo, le risposte che mostrano il maggior potenziale per me sono stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , forse stackoverflow.com/a/ 18992200/30461 , e il mio. Quelli sono quelli che proverei per primi. Lascio l'ordine a te.
Peter Hosey,

1
@Gene: forse potresti riassumere le informazioni pertinenti e collegarti a maggiori dettagli, in una risposta.
Peter Hosey,

Risposte:


381

A volte è davvero utile passare un po 'di tempo a reinventare la ruota. Come avrai già notato, ci sono molti framework, ma non è così difficile implementare una soluzione semplice, ma utile senza introdurre tutta quella complessità. (Per favore, non fraintendetemi, per qualsiasi scopo serio è meglio usare un quadro maturo e dimostrato di essere stabile).

Presenterò prima i miei risultati e poi spiegherò l'idea semplice e diretta dietro di loro.

inserisci qui la descrizione dell'immagine

Vedrai nella mia implementazione che non è necessario analizzare ogni singolo punto e fare calcoli complessi. L'idea è di individuare alcune preziose meta informazioni. Userò tangente come un esempio:

inserisci qui la descrizione dell'immagine

Identifichiamo un modello semplice e diretto, tipico per la forma selezionata:

inserisci qui la descrizione dell'immagine

Quindi non è così difficile implementare un meccanismo di rilevazione del cerchio basato su quell'idea. Vedi la demo funzionante di seguito (mi dispiace, sto usando Java come il modo più veloce per fornire questo esempio veloce e un po 'sporco):

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

Non dovrebbe essere un problema implementare un comportamento simile su iOS, dal momento che hai solo bisogno di diversi eventi e coordinate. Qualcosa di simile al seguente (vedi esempio ):

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

Esistono diversi miglioramenti possibili.

Inizia in qualsiasi momento

Il requisito attuale è iniziare a disegnare un cerchio dal punto medio superiore a causa della seguente semplificazione:

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

Si noti che indexviene utilizzato il valore predefinito di . Una semplice ricerca tra le "parti" disponibili della forma rimuoverà tale limitazione. Si noti che è necessario utilizzare un buffer circolare per rilevare una forma completa:

inserisci qui la descrizione dell'immagine

In senso orario e antiorario

Per supportare entrambe le modalità dovrai usare il buffer circolare del miglioramento precedente e cercare in entrambe le direzioni:

inserisci qui la descrizione dell'immagine

Disegna un'ellisse

Hai già tutto ciò di cui hai bisogno boundsnell'array.

inserisci qui la descrizione dell'immagine

Usa semplicemente quei dati:

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

Altri gesti (facoltativo)

Infine, devi solo gestire correttamente una situazione in cui dx(o dy) è uguale a zero per supportare altri gesti:

inserisci qui la descrizione dell'immagine

Aggiornare

Questo piccolo PoC ha ricevuto un'attenzione piuttosto elevata, quindi ho aggiornato un po 'il codice per farlo funzionare senza problemi e fornire alcuni suggerimenti di disegno, evidenziare i punti di supporto, ecc:

inserisci qui la descrizione dell'immagine

Ecco il codice:

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
Risposta spettacolare Renat. Chiara descrizione dell'approccio, immagini che documentano il processo, anche animazioni. Sembra anche la soluzione più generalizzata e robusta. Le tangenti sembrano un'idea davvero intelligente - proprio come le tecniche di riconoscimento della scrittura iniziale (attuale?). Domanda aggiunta ai segnalibri per il bene di questa risposta. :)
enhzflep

27
Più in generale: una spiegazione concisa e comprensibile E diagrammi E una demo animata E codice E variazioni? Questa è una risposta Stack Overflow ideale.
Peter Hosey,

11
Questa è una buona risposta, posso quasi perdonare che sta facendo grafica al computer in Java! ;)
Nicolas Miari,

4
Ci saranno più aggiornamenti sorprendenti (ovvero, più forme, ecc.) Per questo Natale, Santa Renat? :-)
Unheilig,

1
Wow. Tour de force.
wogsland,

14

Una tecnica classica di Computer Vision per rilevare una forma è la trasformazione di Hough. Una delle cose belle della trasformazione di Hough è che è molto tollerante nei confronti di dati parziali, dati imperfetti e rumore. Utilizzo di Hough per una cerchia: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

Dato che il tuo cerchio è disegnato a mano, penso che la trasformazione di Hough potrebbe essere una buona corrispondenza per te.

Ecco una spiegazione "semplificata", mi scuso per non essere così semplice. Gran parte di esso proviene da un progetto scolastico che ho fatto molti anni fa.

La trasformazione di Hough è uno schema di voto. Viene allocata una matrice bidimensionale di numeri interi e tutti gli elementi vengono impostati su zero. Ogni elemento corrisponde a un singolo pixel nell'immagine che viene analizzata. Questo array viene chiamato array di accumulatori poiché ogni elemento accumulerà informazioni, voti, indicando la possibilità che un pixel possa trovarsi all'origine di un cerchio o di un arco.

Un rilevatore di bordi dell'operatore con gradiente viene applicato all'immagine e vengono registrati pixel di bordo o bordi. Un bordo è un pixel che ha un'intensità o un colore diversi rispetto ai suoi vicini. Il grado di differenza è chiamato magnitudine del gradiente. Per ogni bordo di grandezza sufficiente viene applicato uno schema di voto che incrementa gli elementi dell'array accumulatore. Gli elementi che vengono incrementati (votati) corrispondono alle possibili origini dei cerchi che attraversano il bordo in esame. Il risultato desiderato è che se esiste un arco, la vera origine riceverà più voti delle false origini.

Si noti che gli elementi dell'array di accumulatori visitati per il voto formano un cerchio attorno al bordo in esame. Il calcolo delle coordinate x, y per cui votare è lo stesso del calcolo delle coordinate x, y di un cerchio che stai disegnando.

Nell'immagine disegnata a mano potresti essere in grado di utilizzare direttamente i pixel impostati (colorati) anziché calcolare i bordi.

Ora con pixel posizionati in modo imperfetto non otterrai necessariamente un singolo elemento dell'array di accumulatori con il maggior numero di voti. È possibile ottenere una raccolta di elementi array vicini con un gruppo di voti, un cluster. Il centro di gravità di questo gruppo può offrire una buona approssimazione per l'origine.

Si noti che potrebbe essere necessario eseguire la trasformazione di Hough per diversi valori di raggio R. Quello che produce il gruppo di voti più denso è l'adattamento "migliore".

Esistono varie tecniche da utilizzare per ridurre i voti per false origini. Ad esempio, uno dei vantaggi dell'utilizzo dei bordi è che non solo hanno una grandezza ma hanno anche una direzione. Al momento del voto, dobbiamo solo votare per possibili origini nella direzione appropriata. Le posizioni che ricevono i voti formerebbero un arco anziché un cerchio completo.

Ecco un esempio Iniziamo con un cerchio di raggio uno e un array di accumulatori inizializzato. Poiché ogni pixel è considerato vengono votate le origini potenziali. La vera origine riceve il maggior numero di voti, che in questo caso sono quattro.

.  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

Ecco un altro modo. Usando UIView touchBegan, touchMoved, touchEnded e aggiunta di punti a un array. Dividi l'array in metà e verifica se ogni punto in un array ha approssimativamente lo stesso diametro dalla sua controparte nell'altro array di tutte le altre coppie.

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

}

Suona bene? :)


3

Non sono un esperto del riconoscimento delle forme, ma ecco come potrei affrontare il problema.

Innanzitutto, mentre visualizzi il percorso dell'utente come a mano libera, accumula segretamente un elenco di punti (x, y) con i tempi. Puoi ottenere entrambi i fatti dai tuoi eventi di trascinamento, avvolgerli in un semplice oggetto modello e raggrupparli in un array mutabile.

Probabilmente vuoi prelevare i campioni abbastanza frequentemente, ad esempio ogni 0,1 secondi. Un'altra possibilità sarebbe quella di iniziare molto frequentemente, forse ogni 0,05 secondi, e vedere per quanto tempo trascina l'utente; se trascinano più a lungo di un certo periodo di tempo, quindi abbassano la frequenza di campionamento (e rilasciano tutti i campioni che sarebbero stati persi) a qualcosa come 0,2 secondi.

(E non prendere i miei numeri per il Vangelo, perché li ho appena tolti dal cappello. Sperimenta e trova valori migliori.)

In secondo luogo, analizzare i campioni.

Ti consigliamo di ricavare due fatti. Innanzitutto, il centro della forma, che (IIRC) dovrebbe essere solo la media di tutti i punti. In secondo luogo, il raggio medio di ciascun campione da quel centro.

Se, come indovinato @utente1118321, vuoi supportare poligoni, il resto dell'analisi consiste nel prendere quella decisione: se l'utente vuole disegnare un cerchio o un poligono. Puoi guardare i campioni come un poligono per iniziare con questa decisione.

Esistono diversi criteri che è possibile utilizzare:

  • Tempo: se l'utente si ferma più a lungo in alcuni punti rispetto ad altri (che, se i campioni sono ad intervalli costanti, appariranno come un gruppo di campioni consecutivi uno vicino all'altro nello spazio), questi potrebbero essere angoli. Dovresti ridurre la soglia del tuo angolo in modo che l'utente possa farlo inconsciamente, piuttosto che fare una pausa deliberata ad ogni angolo.
  • Angolo: un cerchio avrà all'incirca lo stesso angolo da un campione all'altro tutto intorno. Un poligono avrà diversi angoli uniti da segmenti di linea retta; gli angoli sono gli angoli. Per un poligono regolare (dal cerchio all'ellisse di un poligono irregolare), gli angoli degli angoli dovrebbero essere più o meno gli stessi; un poligono irregolare avrà angoli angolari diversi.
  • Intervallo: gli angoli di un poligono regolare avranno lo stesso spazio all'interno della dimensione angolare e il raggio sarà costante. Un poligono irregolare avrà intervalli angolari irregolari e / o un raggio non costante.

Il terzo e ultimo passaggio consiste nel creare la forma, centrata sul punto centrale precedentemente determinato, con il raggio precedentemente determinato.

Nessuna garanzia che tutto ciò che ho detto sopra funzionerà o sarà efficiente, ma spero che almeno ti porti sulla strada giusta, e per favore, se qualcuno che conosce più il riconoscimento della forma di me (che è una barra molto bassa) vede questo, sentiti libero di pubblicare un commento o la tua risposta.


+1 Ciao, grazie per l'input. Molto informativo. Allo stesso modo, vorrei che il superman iOS / "riconoscimento della forma" vedesse in qualche modo questo post e ci illuminasse ulteriormente.
Unheilig,

1
@Unheilig: buona idea. Fatto.
Peter Hosey,

1
Il tuo algoritmo suona bene. Vorrei aggiungere un controllo su quanto il percorso dell'utente si è discostato da un cerchio / poligono perfetto. (Ad esempio, percentuale significa deviazione quadrata.) Se è troppo grande, l'utente potrebbe non desiderare la forma ideale. Per uno scarabocchiatore esperto, il taglio sarebbe più piccolo di quello di uno scarabocchio sciatto. Avere questo consentirebbe al programma di dare libertà artistica agli artisti ma molto aiuto ai principianti.
dmm,

@ user2654818: come lo misureresti?
Peter Hosey,

1
@PeterHosey: Spiegazione per i cerchi: una volta che hai il cerchio ideale, hai il centro e il raggio. Quindi prendi ogni punto disegnato e calcoli la sua distanza quadrata dal centro, che è ((x-x0) ^ 2 + (y-y0) ^ 2). Sottrai quello dal raggio al quadrato. (Sto evitando molte radici quadrate per salvare il calcolo.) Chiama l'errore quadratico per un punto disegnato. Media dell'errore al quadrato per tutti i punti disegnati, quindi radice quadrata, quindi dividerlo per il raggio. Questa è la tua divergenza percentuale media. (La matematica / statistica è probabilmente meritevole, ma funzionerebbe in pratica.)
dmm,

2

Ho avuto abbastanza fortuna con un riconoscitore da $ 1 adeguatamente addestrato ( http://depts.washington.edu/aimgroup/proj/dollar/ ). L'ho usato per cerchi, linee, triangoli e quadrati.

È passato molto tempo prima di UIGestureRecognizer, ma penso che dovrebbe essere facile creare sottoclassi UIGestureRecognizer appropriate.


2

Una volta stabilito che l'utente ha finito di disegnare la sua forma nel punto in cui ha iniziato, puoi prendere un campione delle coordinate che ha tracciato e provare a inserirle in un cerchio.

Esiste una soluzione MATLAB a questo problema qui: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

Che si basa sull'articolo Least-Squares Fitting of Circles and Ellipses di Walter Gander, Gene H. Golub e Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

Il dott. Ian Coope dell'Università di Canterbury, in Nuova Zelanda, ha pubblicato un documento con l'abstract:

Il problema di determinare il cerchio più adatto a un insieme di punti nel piano (o l'evidente generalizzazione alle n-dimensioni) è facilmente formulato come un problema dei minimi quadrati totali non lineari che può essere risolto usando un algoritmo di minimizzazione di Gauss-Newton. Questo approccio diretto è dimostrato inefficiente ed estremamente sensibile alla presenza di valori anomali. Una formulazione alternativa consente di ridurre il problema a un problema di minimi quadrati lineari che è banalmente risolto. L'approccio raccomandato ha il vantaggio di essere molto meno sensibile agli outlier rispetto all'approccio dei minimi quadrati non lineari.

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

Il file MATLAB può calcolare sia il problema TLL non lineare che il problema LLS lineare.


0

Ecco un modo abbastanza semplice usando:

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

assumendo questa griglia matriciale:

 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

Posiziona alcune UIVview sulle posizioni "X" e testale per essere colpite (in sequenza). Se tutti vengono colpiti in sequenza, penso che potrebbe essere giusto lasciare che l'utente dica "Ben fatto, hai disegnato un cerchio"

Suona bene? (e semplice)


Ciao Lemon. Buon ragionamento, ma nello scenario sopra, significa che avremmo bisogno di avere 64 UIVview per rilevare i tocchi, giusto? E come definiresti la dimensione di una singola vista UIV, ad esempio se l'area di disegno ha le dimensioni di un iPad? Sembra che se il cerchio è piccolo e se le dimensioni di un singolo UIView sono più grandi, in questo caso non potremmo controllare la sequenza perché tutti i punti disegnati si troverebbero all'interno di un singolo UIView.
Unheilig,

Sì - questo probabilmente funziona solo se fissi la tela su qualcosa come 300x300 e poi hai una tela "di esempio" accanto ad essa con le dimensioni del cerchio che stai cercando l'utente di disegnare. In tal caso, andrei con 50x50 quadrati * 6, devi anche renderizzare le viste che ti interessano colpire nelle posizioni corrette, non tutte 6 * 6 (36) o 8 * 8 (64)
dijipiji,

@Unheilig: ecco cosa fa questa soluzione. Qualunque cosa abbastanza circolare da passare attraverso una corretta sequenza di viste (e potresti potenzialmente consentire un numero massimo di deviazioni per una maggiore inclinazione) corrisponderà a un cerchio. Quindi lo fai scattare in un cerchio perfetto centrato al centro di tutte quelle viste, il cui raggio raggiunge tutte (o almeno la maggior parte) di esse.
Peter Hosey,

@PeterHosey Ok, fammi provare a pensarci bene. Gradirei se qualcuno di voi potesse fornire un po 'di codice per ottenere questo risultato. Nel frattempo, proverò anche a farmi girare la testa e poi farò lo stesso con la parte di programmazione. Grazie.
Unheilig,

Ho appena presentato un altro modo per te che penso potrebbe essere migliore
Dijipiji,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.