Gambarlah lingkaran yang sempurna dari sentuhan pengguna

176

Saya memiliki proyek latihan ini yang memungkinkan pengguna untuk menggambar di layar saat mereka menyentuh dengan jari-jari mereka. App yang sangat sederhana saya lakukan sebagai latihan jalan kembali. Sepupu kecil saya mengambil kebebasan menggambar dengan jarinya dengan iPad saya di Aplikasi ini (Gambar anak-anak: lingkaran, garis, dll, apa pun yang terlintas dalam pikirannya). Kemudian dia mulai menggambar lingkaran dan kemudian dia meminta saya untuk menjadikannya "lingkaran yang baik" (dari pemahaman saya: membuat lingkaran yang digambar menjadi bulat sempurna, seperti yang kita ketahui tidak masalah seberapa stabil kita mencoba menggambar sesuatu dengan jari kita di layar, sebuah lingkaran tidak pernah benar-benar bulat seperti seharusnya lingkaran).

Jadi pertanyaan saya di sini adalah, apakah ada cara dalam kode di mana kita pertama kali dapat mendeteksi garis yang ditarik oleh pengguna yang membentuk lingkaran dan menghasilkan ukuran lingkaran yang kira-kira sama dengan membuatnya bulat sempurna di layar. Membuat garis yang tidak terlalu lurus adalah sesuatu yang saya tahu caranya, tetapi untuk lingkaran, saya tidak tahu bagaimana cara melakukannya dengan Quartz atau metode lain.

Alasan saya adalah bahwa, titik awal dan akhir dari garis harus menyentuh atau saling silang setelah pengguna mengangkat jarinya untuk membenarkan fakta bahwa ia mencoba untuk benar-benar menggambar lingkaran.

Unheilig
sumber
2
Mungkin sulit untuk membedakan antara lingkaran dan poligon dalam skenario ini. Bagaimana dengan memiliki "Alat Lingkaran" di mana pengguna mengklik untuk menentukan pusat, atau satu sudut persegi panjang yang terikat, dan menyeret untuk mengubah radius atau mengatur sudut yang berlawanan?
user1118321
2
@ user1118321: Ini mengalahkan konsep hanya bisa menggambar lingkaran dan memiliki lingkaran sempurna. Idealnya, aplikasi harus mengenali dari gambar pengguna sendiri apakah pengguna menggambar lingkaran (kurang lebih), elips, atau poligon. (Plus, poligon mungkin tidak dalam ruang lingkup untuk aplikasi ini — itu mungkin hanya lingkaran atau garis.)
Peter Hosey
Jadi, menurut Anda jawaban apa yang harus saya berikan hadiah? Saya melihat banyak kandidat yang baik.
Peter Hosey
@Unheilig: Saya tidak memiliki keahlian dalam bidang ini, di luar pemahaman yang baru tentang trigonometri. Yang mengatakan, jawaban yang menunjukkan potensi paling bagi saya adalah stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , mungkin stackoverflow.com/a/ 18992200/30461 , dan saya sendiri. Itulah yang saya coba dulu. Saya meninggalkan pesanan untuk Anda.
Peter Hosey
1
@ Gen: Mungkin Anda bisa meringkas informasi yang relevan, dan menautkan ke rincian lebih lanjut, dalam jawaban.
Peter Hosey

Jawaban:

381

Terkadang sangat berguna untuk meluangkan waktu menciptakan kembali roda. Seperti yang mungkin sudah Anda perhatikan ada banyak kerangka kerja, tetapi tidak sulit untuk mengimplementasikan solusi sederhana, namun bermanfaat tanpa memperkenalkan semua kompleksitas itu. (Tolong jangan salah paham, untuk tujuan serius apa pun lebih baik menggunakan kerangka kerja yang matang dan terbukti stabil).

Saya akan mempresentasikan hasil saya pertama dan kemudian menjelaskan ide sederhana dan langsung di belakang mereka.

masukkan deskripsi gambar di sini

Anda akan melihat dalam implementasi saya tidak perlu menganalisis setiap titik dan melakukan perhitungan yang kompleks. Idenya adalah untuk menemukan beberapa informasi meta yang berharga. Saya akan menggunakan tangen sebagai contoh:

masukkan deskripsi gambar di sini

Mari kita identifikasi pola yang sederhana dan langsung, khas untuk bentuk yang dipilih:

masukkan deskripsi gambar di sini

Jadi tidak sulit untuk menerapkan mekanisme deteksi lingkaran berdasarkan ide itu. Lihat demo yang berfungsi di bawah ini (Maaf, saya menggunakan Java sebagai cara tercepat untuk memberikan contoh cepat dan agak kotor ini):

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

Seharusnya tidak menjadi masalah untuk menerapkan perilaku serupa di iOS, karena Anda hanya perlu beberapa acara dan koordinat. Sesuatu seperti berikut ini (lihat contoh ):

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

Ada beberapa perangkat tambahan yang mungkin.

Mulai kapan saja

Persyaratan saat ini adalah mulai menggambar lingkaran dari titik tengah atas karena penyederhanaan berikut:

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

Mohon perhatikan nilai default dari indexyang digunakan. Pencarian sederhana melalui "bagian" bentuk yang tersedia akan menghapus batasan itu. Harap dicatat Anda harus menggunakan buffer bundar untuk mendeteksi bentuk penuh:

masukkan deskripsi gambar di sini

Searah jarum jam dan berlawanan arah jarum jam

Untuk mendukung kedua mode ini, Anda harus menggunakan buffer bundar dari perangkat tambahan sebelumnya dan mencari di kedua arah:

masukkan deskripsi gambar di sini

Gambarkan elips

Anda sudah memiliki semua yang Anda butuhkan di dalam boundsarray.

masukkan deskripsi gambar di sini

Cukup gunakan data itu:

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

Gerakan lain (opsional)

Terakhir, Anda hanya perlu menangani situasi dengan benar dx(atau dy) sama dengan nol untuk mendukung gerakan lain:

masukkan deskripsi gambar di sini

Memperbarui

PoC kecil ini mendapat perhatian yang cukup tinggi, jadi saya memperbarui sedikit kode untuk membuatnya bekerja dengan lancar dan memberikan beberapa petunjuk menggambar, menyorot poin pendukung, dll:

masukkan deskripsi gambar di sini

Ini kodenya:

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
sumber
76
Jawaban spektakuler Renat. Deskripsi yang jelas tentang pendekatan, gambar yang mendokumentasikan proses, animasi juga. Tampaknya juga solusi yang paling umum dan kuat. Garis singgung terdengar seperti ide yang sangat pintar - seperti teknik pengenalan tulisan tangan awal (saat ini?). Pertanyaan ditandai untuk jawaban ini. :)
enhzflep
27
Secara lebih umum: Penjelasan DAN diagram yang ringkas, dapat dipahami DAN demo animasi DAN kode DAN variasi? Ini adalah jawaban Stack Overflow yang ideal.
Peter Hosey
11
Ini adalah jawaban yang bagus, saya hampir bisa memaafkan dia mengerjakan grafik komputer di Jawa! ;)
Nicolas Miari
4
Apakah akan ada pembaruan mengejutkan lagi (yaitu, lebih banyak bentuk, dll) untuk Natal ini, Santa Renat? :-)
Unheilig
1
Wow. Tour de force.
wogsland
14

Teknik Visi Komputer klasik untuk mendeteksi bentuk adalah Hough Transform. Salah satu hal yang menyenangkan tentang Hough Transform adalah sangat toleran terhadap data parsial, data tidak sempurna, dan kebisingan. Menggunakan Hough untuk lingkaran: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

Mengingat bahwa lingkaran Anda digambar tangan, saya pikir transformasi Hough mungkin cocok untuk Anda.

Inilah penjelasan "disederhanakan", saya minta maaf karena itu tidak benar-benar sesederhana itu. Sebagian besar dari proyek sekolah yang saya lakukan bertahun-tahun yang lalu.

The Hough Transform adalah skema pemungutan suara. Array bilangan bulat dua dimensi dialokasikan dan semua elemen diatur ke nol. Setiap elemen terkait dengan satu piksel dalam gambar yang dianalisis. Array ini disebut sebagai array akumulator karena setiap elemen akan mengumpulkan informasi, suara, yang menunjukkan kemungkinan bahwa suatu pixel mungkin berasal dari asal-usul lingkaran atau busur.

Detektor tepi operator gradien diterapkan pada gambar dan piksel tepi, atau edgels, direkam. Edgel adalah piksel yang memiliki intensitas atau warna berbeda sehubungan dengan tetangganya. Tingkat perbedaan disebut gradient magnitude. Untuk setiap edgel dengan besaran yang cukup, skema pemungutan suara diterapkan yang akan menambah elemen array akumulator. Elemen-elemen yang ditambahkan (dipilih) sesuai dengan kemungkinan asal usul lingkaran yang melewati edgel yang sedang dipertimbangkan. Hasil yang diinginkan adalah bahwa jika ada busur maka asal yang benar akan menerima lebih banyak suara daripada asal yang salah.

Perhatikan bahwa elemen array akumulator yang dikunjungi untuk pemungutan suara membentuk lingkaran di sekitar edgel yang sedang dipertimbangkan. Menghitung koordinat x, y untuk memilih sama dengan menghitung koordinat x, y dari lingkaran yang Anda gambar.

Pada gambar yang digambar dengan tangan Anda, Anda mungkin dapat menggunakan piksel yang diset (berwarna) secara langsung daripada menghitung edgels.

Sekarang dengan piksel yang terletak tidak sempurna Anda tidak perlu mendapatkan elemen array akumulator tunggal dengan jumlah suara terbanyak. Anda bisa mendapatkan koleksi elemen array tetangga dengan sekelompok suara, sebuah cluster. Pusat gravitasi dari kluster ini dapat menawarkan perkiraan yang baik untuk asal usulnya.

Perhatikan bahwa Anda mungkin harus menjalankan Hough Transform untuk nilai jari-jari R. yang berbeda yang menghasilkan kumpulan suara yang lebih padat adalah kecocokan "lebih baik".

Ada berbagai teknik yang digunakan untuk mengurangi suara untuk asal-usul palsu. Sebagai contoh satu keuntungan menggunakan edgels adalah bahwa mereka tidak hanya memiliki magnitudo tetapi mereka juga memiliki arah. Saat memberikan suara, kita hanya perlu memilih kemungkinan asal ke arah yang sesuai. Lokasi yang menerima suara akan membentuk busur daripada lingkaran lengkap.

Ini sebuah contoh. Kita mulai dengan lingkaran jari-jari satu dan array akumulator yang diinisialisasi. Karena setiap piksel dianggap sebagai asal potensial dipilih untuk. Asal sebenarnya menerima suara terbanyak yang dalam hal ini adalah empat.

.  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
sumber
5

Ini cara lain. Menggunakan UIView touchesMulai, touchesMoved, touchEnded dan menambahkan poin ke array. Anda membagi array menjadi dua, dan menguji apakah setiap titik dalam satu array kira-kira memiliki diameter yang sama dari pasangannya di array lain seperti semua pasangan lainnya.

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

}

Kedengarannya oke? :)

dijipiji
sumber
3

Saya bukan ahli pengenalan bentuk, tapi inilah cara saya mendekati masalah.

Pertama, saat menampilkan jalur pengguna sebagai bebas tangan, diam-diam mengumpulkan daftar titik (x, y) sampel bersamaan dengan waktu. Anda bisa mendapatkan kedua fakta dari acara seret, membungkusnya menjadi objek model sederhana, dan menumpuknya dalam array yang bisa berubah.

Anda mungkin ingin mengambil sampel dengan cukup sering — katakanlah, setiap 0,1 detik. Kemungkinan lain adalah mulai benar-benar sering, mungkin setiap 0,05 detik, dan perhatikan berapa lama pengguna menyeret; jika mereka menyeret lebih lama dari jumlah waktu tertentu, maka turunkan frekuensi sampel (dan jatuhkan sampel apa pun yang terlewatkan) menjadi sekitar 0,2 detik.

(Dan jangan mengambil nomor saya untuk Injil, karena saya baru saja mengeluarkannya dari topi saya. Bereksperimen dan temukan nilai yang lebih baik.)

Kedua, menganalisis sampel.

Anda ingin mendapatkan dua fakta. Pertama, pusat bentuk, yang (IIRC) seharusnya hanya rata-rata dari semua titik. Kedua, radius rata-rata setiap sampel dari pusat itu.

Jika, seperti yang ditebak @ user1118321, Anda ingin mendukung poligon, maka sisa analisisnya terdiri dari membuat keputusan itu: apakah pengguna ingin menggambar lingkaran atau poligon. Anda dapat melihat sampel sebagai poligon untuk memulai dengan menentukan itu.

Ada beberapa kriteria yang dapat Anda gunakan:

  • Waktu: Jika pengguna melayang lebih lama di beberapa titik daripada yang lain (yang, jika sampel berada pada interval konstan, akan muncul sebagai sekelompok sampel berurutan berdekatan satu sama lain dalam ruang), itu mungkin sudut. Anda harus membuat ambang sudut Anda kecil sehingga pengguna dapat melakukan ini secara tidak sadar, daripada harus sengaja berhenti di setiap sudut.
  • Sudut: Sebuah lingkaran akan memiliki sudut yang kira-kira sama dari satu sampel ke yang berikutnya sepanjang jalan. Poligon akan memiliki beberapa sudut yang bergabung dengan segmen garis lurus; sudut adalah sudutnya. Untuk poligon beraturan (lingkaran ke elips poligon tidak beraturan), sudut sudut harus kira-kira sama; poligon tidak beraturan akan memiliki sudut sudut yang berbeda.
  • Interval: Pojok-pojok poligon beraturan akan memiliki jarak yang sama di dalam dimensi sudut, dan jari-jari akan konstan. Poligon yang tidak beraturan akan memiliki interval sudut yang tidak teratur dan / atau jari-jari yang tidak konstan.

Langkah ketiga dan terakhir adalah membuat bentuk, berpusat pada titik pusat yang telah ditentukan sebelumnya, dengan jari-jari yang ditentukan sebelumnya.

Tidak ada jaminan bahwa apa pun yang saya katakan di atas akan berfungsi atau menjadi efisien, tetapi saya harap itu setidaknya membuat Anda berada di jalur yang benar — dan tolong, jika ada orang yang tahu lebih banyak tentang pengenalan bentuk daripada saya (yang merupakan bilah yang sangat rendah) melihat ini, jangan ragu untuk mengirim komentar atau jawaban Anda sendiri.

Peter Hosey
sumber
+1 Hai, terima kasih atas masukannya. Sangat informatif. Demikian juga, saya berharap superman iOS / "bentuk pengenalan" entah bagaimana akan melihat posting ini dan mencerahkan kita lebih lanjut.
Unheilig
1
@Unheilig: Ide bagus. Selesai
Peter Hosey
1
Algoritma Anda terdengar bagus. Saya akan menambahkan tanda centang pada seberapa jauh jalur pengguna berbeda dari lingkaran / poligon sempurna. (Misalnya, persen berarti deviasi kuadrat.) Jika terlalu besar, pengguna mungkin tidak menginginkan bentuk yang ideal. Untuk doodler yang ahli, cutoff akan lebih kecil daripada untuk doodler yang ceroboh. Memiliki ini akan memungkinkan program untuk memberikan kebebasan artistik kepada seniman tetapi banyak bantuan untuk pemula.
dmm
@ user2654818: Bagaimana Anda mengukurnya?
Peter Hosey
1
@PeterHosey: Penjelasan untuk lingkaran: Setelah Anda memiliki lingkaran ideal, Anda memiliki pusat dan jari-jarinya. Jadi, Anda mengambil setiap titik yang ditarik, dan menghitung jarak kuadratnya dari pusat, yaitu ((x-x0) ^ 2 + (y-y0) ^ 2). Kurangi itu dari jari-jari kuadrat. (Saya menghindari banyak akar kuadrat untuk menyimpan perhitungan.) Sebut bahwa kuadrat kesalahan untuk titik yang ditarik. Rata-rata kesalahan kuadrat untuk semua titik yang diambil, lalu root kuadrat itu, lalu bagi dengan jari-jari. Itu perbedaan rata-rata persen Anda. (Matematika / statistik mungkin ngeri, tetapi itu akan berhasil dalam praktiknya.)
dmm
2

Saya sudah cukup beruntung dengan pengenal $ 1 yang terlatih dengan baik ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Saya menggunakannya untuk lingkaran, garis, segitiga dan kotak.

Sudah lama sekali, sebelum UIGestureRecognizer, tapi saya pikir itu harus mudah untuk membuat subclass UIGestureRecognizer yang tepat.

Martin Adoue
sumber
2

Setelah Anda menentukan pengguna selesai menggambar bentuk di mana mereka mulai, Anda dapat mengambil sampel dari koordinat yang mereka gambar dan coba paskan mereka ke lingkaran.

Ada solusi MATLAB untuk masalah ini di sini: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

Yang didasarkan pada makalah Least-Squares Fitting of Circles and Ellipses oleh Walter Gander, Gene H. Golub dan Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

Dr Ian Coope dari University of Canterbury, NZ menerbitkan sebuah makalah dengan abstrak:

Masalah menentukan lingkaran paling cocok untuk satu set poin di pesawat (atau generalisasi yang jelas ke dimensi-n) dengan mudah diformulasikan sebagai masalah total-kuadrat terkecil nonlinier yang dapat diselesaikan dengan menggunakan algoritma minimisasi Gauss-Newton. Pendekatan langsung ini terbukti tidak efisien dan sangat sensitif terhadap kehadiran pencilan. Formulasi alternatif memungkinkan masalah tersebut direduksi menjadi masalah linear kuadrat terkecil yang diselesaikan secara sepele. Pendekatan yang direkomendasikan terbukti memiliki keuntungan tambahan yaitu menjadi jauh kurang sensitif terhadap pencilan daripada pendekatan kuadrat terkecil nonlinier.

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

File MATLAB dapat menghitung masalah TLS nonlinear dan linear LLS.

David Lawson
sumber
0

Berikut cara yang cukup sederhana menggunakan:

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

dengan asumsi kisi matriks ini:

 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

Tempatkan beberapa UIViews di lokasi "X" dan uji mereka untuk menjadi hit (secara berurutan). Jika mereka semua terkena secara berurutan, saya pikir mungkin adil untuk membiarkan pengguna mengatakan "Bagus, Anda menggambar lingkaran"

Kedengarannya oke? (dan sederhana)

dijipiji
sumber
Hai, Lemon. Alasan yang bagus, tetapi dalam skenario di atas, itu berarti bahwa kita perlu memiliki 64 UIViews untuk mendeteksi sentuhan, bukan? Dan bagaimana Anda menentukan ukuran untuk satu UIView tunggal jika kanvas adalah ukuran iPad misalnya? Tampaknya jika lingkaran itu kecil dan jika ukuran satu UIView lebih besar, dalam hal ini kami tidak dapat memeriksa urutannya karena semua titik yang ditarik akan berada dalam satu UIView tunggal.
Unheilig
Yap - yang ini mungkin hanya berfungsi jika Anda memperbaiki kanvas ke sesuatu seperti 300x300 dan kemudian memiliki kanvas "contoh" di sebelahnya dengan ukuran lingkaran yang Anda cari pengguna untuk menggambar. Jika demikian saya akan menggunakan 50x50 kotak * 6, Anda juga hanya perlu membuat Tampilan yang Anda minati di lokasi yang benar, tidak semua 6 * 6 (36) atau 8 * 8 (64)
dijipiji
@Unheilig: Itulah yang dilakukan solusi ini. Apa pun yang cukup melingkar untuk melewati urutan tampilan yang benar (dan Anda mungkin bisa memungkinkan beberapa jalan memutar maksimum untuk slop ekstra) akan cocok sebagai lingkaran. Anda kemudian snap ke lingkaran sempurna yang berpusat di tengah semua pandangan itu, yang radiusnya mencapai semua (atau paling tidak sebagian besar) dari mereka.
Peter Hosey
@ PeterHosey Ok, izinkan saya mencoba menyiasati ini. Saya akan sangat menghargai jika ada di antara Anda dapat memberikan beberapa kode untuk mendapatkan rolling ini. Sementara itu, saya juga akan mencoba untuk menyelesaikan masalah ini dan setelah itu saya akan melakukan hal yang sama dengan bagian pengkodean. Terima kasih.
Unheilig
Cukup kirimkan cara lain untuk Anda yang saya pikir mungkin lebih baik
dijipiji