Vẽ một vòng tròn hoàn hảo từ liên lạc của người dùng


176

Tôi có dự án thực hành này cho phép người dùng vẽ trên màn hình khi họ chạm bằng ngón tay. Ứng dụng rất đơn giản tôi đã làm như cách tập thể dục trở lại. Em họ của tôi đã tự do vẽ mọi thứ bằng ngón tay của mình bằng iPad của tôi trên Ứng dụng này (Hình vẽ của trẻ em: hình tròn, đường kẻ, v.v., bất cứ điều gì xuất hiện trong đầu anh ấy). Sau đó, anh ấy bắt đầu vẽ các vòng tròn và sau đó anh ấy yêu cầu tôi làm cho nó trở thành "vòng tròn tốt" (theo cách hiểu của tôi: làm cho vòng tròn được vẽ hoàn hảo, vì chúng tôi biết dù chúng tôi có cố gắng vẽ một cái gì đó bằng ngón tay trên màn hình, ổn định như thế nào hình tròn không bao giờ thực sự tròn như hình tròn).

Vì vậy, câu hỏi của tôi ở đây là, có cách nào trong mã mà trước tiên chúng ta có thể phát hiện một đường được vẽ bởi người dùng tạo thành một vòng tròn và tạo ra kích thước tương đương của vòng tròn bằng cách làm cho nó tròn hoàn hảo trên màn hình. Tạo một đường thẳng không quá thẳng là điều tôi sẽ biết cách thực hiện, nhưng đối với vòng tròn, tôi hoàn toàn không biết cách thực hiện bằng Quartz hoặc các phương pháp khác.

Lý do của tôi là, điểm bắt đầu và điểm cuối của đường phải chạm hoặc giao nhau sau khi người dùng nhấc ngón tay lên để chứng minh thực tế rằng anh ta đang cố gắng thực sự vẽ một vòng tròn.


2
Có thể khó phân biệt sự khác biệt giữa hình tròn và hình đa giác trong kịch bản này. Làm thế nào về việc có một "Công cụ hình tròn" nơi người dùng nhấp để xác định tâm hoặc một góc của hình chữ nhật giới hạn và kéo để thay đổi bán kính hoặc đặt góc đối diện?
dùng1118321

2
@ user1118321: Điều này đánh bại khái niệm chỉ có thể vẽ một vòng tròn và có một vòng tròn hoàn hảo. Lý tưởng nhất, ứng dụng sẽ nhận ra từ bản vẽ của người dùng cho dù người dùng đã vẽ một vòng tròn (nhiều hay ít), hình elip hoặc đa giác. (Ngoài ra, đa giác có thể không nằm trong phạm vi của ứng dụng này, nó có thể chỉ là hình tròn hoặc đường kẻ.)
Peter Hosey

Vì vậy, câu trả lời nào bạn nghĩ rằng tôi nên đưa tiền thưởng? Tôi thấy nhiều ứng cử viên tốt.
Peter Hosey

@Unheilig: Tôi không có chuyên môn về chủ đề này, ngoài sự hiểu biết mới về trig. Điều đó nói rằng, các câu trả lời cho thấy tiềm năng nhất đối với tôi là stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , có thể stackoverflow.com/a/ 18992200/30461 , và của riêng tôi. Đó là những cái tôi sẽ thử đầu tiên. Tôi để lại thứ tự cho bạn.
Peter Hosey

1
@Gene: Có lẽ bạn có thể tóm tắt các thông tin liên quan và liên kết đến nhiều chi tiết hơn, trong một câu trả lời.
Peter Hosey

Câu trả lời:


381

Đôi khi nó thực sự hữu ích để dành một chút thời gian để phát minh lại bánh xe. Như bạn có thể đã nhận thấy có rất nhiều khung công tác, nhưng không khó để thực hiện một giải pháp đơn giản nhưng hữu ích mà không giới thiệu tất cả sự phức tạp đó. (Xin đừng hiểu sai ý tôi, vì bất kỳ mục đích nghiêm túc nào, tốt hơn là sử dụng một số khung trưởng thành và được chứng minh là ổn định).

Tôi sẽ trình bày kết quả của mình trước và sau đó giải thích ý tưởng đơn giản và dễ hiểu đằng sau chúng.

nhập mô tả hình ảnh ở đây

Bạn sẽ thấy trong quá trình thực hiện của tôi, không cần phải phân tích từng điểm một và thực hiện các tính toán phức tạp. Ý tưởng là để phát hiện một số thông tin meta có giá trị. Tôi sẽ sử dụng tiếp tuyến làm ví dụ:

nhập mô tả hình ảnh ở đây

Hãy xác định một mẫu đơn giản và dễ hiểu, điển hình cho hình dạng đã chọn:

nhập mô tả hình ảnh ở đây

Vì vậy, không khó để thực hiện cơ chế phát hiện vòng tròn dựa trên ý tưởng đó. Xem bản demo hoạt động bên dưới (Xin lỗi, tôi đang sử dụng Java là cách nhanh nhất để cung cấp ví dụ nhanh và hơi bẩn này):

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

Nó không phải là một vấn đề để thực hiện hành vi tương tự trên iOS, vì bạn chỉ cần một vài sự kiện và tọa độ. Một cái gì đó như sau (xem ví dụ ):

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

Có một số cải tiến có thể.

Bắt đầu bất cứ lúc nào

Yêu cầu hiện tại là bắt đầu vẽ một vòng tròn từ điểm giữa trên cùng do sự đơn giản hóa sau:

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

Xin lưu ý giá trị mặc định indexđược sử dụng. Một tìm kiếm đơn giản thông qua các "bộ phận" có sẵn của hình dạng sẽ loại bỏ giới hạn đó. Xin lưu ý rằng bạn sẽ cần sử dụng bộ đệm tròn để phát hiện hình dạng đầy đủ:

nhập mô tả hình ảnh ở đây

Theo chiều kim đồng hồ và ngược chiều kim đồng hồ

Để hỗ trợ cả hai chế độ, bạn sẽ cần sử dụng bộ đệm tròn từ cải tiến trước đó và tìm kiếm theo cả hai hướng:

nhập mô tả hình ảnh ở đây

Vẽ một hình elip

Bạn có mọi thứ bạn cần đã có trong boundsmảng.

nhập mô tả hình ảnh ở đây

Chỉ cần sử dụng dữ liệu đó:

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

Cử chỉ khác (tùy chọn)

Cuối cùng, bạn chỉ cần xử lý đúng một tình huống khi dx(hoặc dy) bằng 0 để hỗ trợ các cử chỉ khác:

nhập mô tả hình ảnh ở đây

Cập nhật

PoC nhỏ này nhận được sự chú ý khá cao, vì vậy tôi đã cập nhật mã một chút để làm cho nó hoạt động trơn tru và cung cấp một số gợi ý vẽ, làm nổi bật các điểm hỗ trợ, v.v .:

nhập mô tả hình ảnh ở đây

Đây là mã:

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
Câu trả lời ngoạn mục Renat. Mô tả rõ ràng về cách tiếp cận, hình ảnh tài liệu quá trình, hình ảnh động quá. Cũng có vẻ là giải pháp tổng quát nhất, mạnh mẽ. Tangents nghe có vẻ là một ý tưởng thực sự thông minh - giống như các kỹ thuật nhận dạng chữ viết tay ban đầu (hiện tại?). Câu hỏi được đánh dấu vì lợi ích của câu trả lời này. :)
boostzflep

27
Tổng quát hơn: Một lời giải thích ngắn gọn, dễ hiểu VÀ sơ đồ VÀ một bản demo hoạt hình VÀ mã VÀ các biến thể? Đây là một câu trả lời Stack Overflow lý tưởng.
Peter Hosey

11
Đây là một câu trả lời tốt, tôi gần như có thể tha thứ cho anh ta đang làm đồ họa máy tính trong Java! ;)
Nicolas Miari

4
Sẽ có những cập nhật đáng ngạc nhiên nữa (tức là nhiều hình dạng hơn, v.v.) cho Giáng sinh này, Santa Renat? :-)
Unheilig

1
Ồ Tour de lực lượng.
Wogsland

14

Một kỹ thuật Computer Vision cổ điển để phát hiện hình dạng là Hough Transform. Một trong những điều tốt đẹp về Hough Transform là nó rất khoan dung với dữ liệu một phần, dữ liệu không hoàn hảo và tiếng ồn. Sử dụng Hough cho một vòng tròn: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_ process

Cho rằng vòng tròn của bạn được vẽ bằng tay, tôi nghĩ rằng biến đổi Hough có thể phù hợp với bạn.

Đây là một lời giải thích "đơn giản hóa", tôi xin lỗi vì nó không thực sự đơn giản. Phần lớn là từ một dự án trường học mà tôi đã làm nhiều năm trước.

The Hough Transform là một chương trình bỏ phiếu. Mảng số nguyên hai chiều được phân bổ và tất cả các phần tử được đặt thành không. Mỗi yếu tố tương ứng với một pixel trong ảnh được phân tích. Mảng này được gọi là mảng tích lũy vì mỗi phần tử sẽ tích lũy thông tin, phiếu bầu, cho thấy khả năng một pixel có thể ở gốc của một vòng tròn hoặc vòng cung.

Một máy dò cạnh toán tử gradient được áp dụng cho các pixel hình ảnh và cạnh, hoặc edgels, được ghi lại. Một edgel là một pixel có cường độ hoặc màu khác nhau so với các hàng xóm của nó. Mức độ khác biệt được gọi là độ lớn độ dốc. Đối với mỗi edgel có cường độ đủ lớn, một sơ đồ bỏ phiếu được áp dụng sẽ làm tăng các phần tử của mảng tích lũy. Các yếu tố được tăng lên (bầu chọn) tương ứng với nguồn gốc có thể có của các vòng tròn đi qua edgel đang được xem xét. Kết quả mong muốn là nếu một vòng cung tồn tại thì nguồn gốc thực sự sẽ nhận được nhiều phiếu hơn so với nguồn gốc sai.

Lưu ý rằng các yếu tố của mảng tích lũy đang được truy cập để bỏ phiếu tạo thành một vòng tròn xung quanh edgel đang được xem xét. Tính toán tọa độ x, y để bỏ phiếu giống như tính toán tọa độ x, y của một vòng tròn mà bạn đang vẽ.

Trong hình vẽ tay của bạn, bạn có thể có thể sử dụng các pixel (màu) được đặt trực tiếp thay vì tính toán các sắc độ.

Bây giờ với các pixel được định vị không hoàn hảo, bạn sẽ không nhất thiết phải có một phần tử mảng tích lũy duy nhất với số phiếu bầu lớn nhất. Bạn có thể nhận được một tập hợp các phần tử mảng lân cận với một loạt phiếu bầu, một cụm. Trọng tâm của cụm này có thể cung cấp một xấp xỉ tốt cho nguồn gốc.

Lưu ý rằng bạn có thể phải chạy Biến đổi Hough cho các giá trị bán kính khác nhau R. Nhóm tạo ra cụm phiếu dày đặc hơn là phù hợp "tốt hơn".

Có nhiều kỹ thuật khác nhau để sử dụng để giảm phiếu cho nguồn gốc sai. Ví dụ, một lợi thế của việc sử dụng edgels là chúng không chỉ có độ lớn mà chúng còn có hướng. Khi bỏ phiếu chúng ta chỉ cần bỏ phiếu cho nguồn gốc có thể theo hướng thích hợp. Các vị trí nhận được phiếu bầu sẽ tạo thành một vòng cung chứ không phải là một vòng tròn hoàn chỉnh.

Đây là một ví dụ. Chúng tôi bắt đầu với một vòng tròn bán kính một và một mảng tích lũy khởi tạo. Vì mỗi pixel được coi là nguồn gốc tiềm năng được bình chọn. Nguồn gốc thực sự nhận được nhiều phiếu nhất trong trường hợp này là bốn.

.  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

Đây là một cách khác. Sử dụng UIView touchesBegan, touchesMoved, touchEnded và thêm điểm vào một mảng. Bạn chia mảng thành hai nửa và kiểm tra xem mọi điểm trong một mảng có đường kính gần bằng với đối tác của nó trong mảng khác như tất cả các cặp khác hay không.

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

}

Nghe có ổn không? :)


3

Tôi không phải là chuyên gia nhận dạng hình dạng, nhưng đây là cách tôi có thể tiếp cận vấn đề.

Đầu tiên, trong khi hiển thị đường dẫn của người dùng dưới dạng tự do, bí mật tích lũy danh sách các mẫu điểm (x, y) cùng với thời gian. Bạn có thể lấy cả hai sự kiện từ các sự kiện kéo của mình, bọc chúng vào một đối tượng mô hình đơn giản và chồng chúng thành một mảng có thể thay đổi.

Bạn có thể muốn lấy các mẫu khá thường xuyên, nói cứ sau 0,1 giây. Một khả năng khác là bắt đầu thực sự thường xuyên, có thể cứ sau 0,05 giây và xem người dùng kéo dài bao lâu; nếu chúng kéo dài hơn một khoảng thời gian, sau đó hạ tần số mẫu (và giảm bất kỳ mẫu nào đã bị bỏ qua) xuống khoảng 0,2 giây.

(Và đừng lấy số của tôi cho phúc âm, vì tôi vừa rút chúng ra khỏi mũ. Thử nghiệm và tìm giá trị tốt hơn.)

Thứ hai, phân tích các mẫu.

Bạn sẽ muốn rút ra hai sự thật. Đầu tiên, tâm của hình, mà (IIRC) chỉ nên là trung bình của tất cả các điểm. Thứ hai, bán kính trung bình của mỗi mẫu từ trung tâm đó.

Nếu, như @ user1118321 đoán, bạn muốn hỗ trợ đa giác, thì phần còn lại của phân tích bao gồm việc đưa ra quyết định: người dùng muốn vẽ một vòng tròn hay đa giác. Bạn có thể xem các mẫu như một đa giác để bắt đầu xác định.

Có một số tiêu chí bạn có thể sử dụng:

  • Thời gian: Nếu người dùng di chuyển lâu hơn tại một số điểm so với những điểm khác (mà nếu các mẫu ở một khoảng không đổi, sẽ xuất hiện dưới dạng một cụm các mẫu liên tiếp gần nhau trong không gian), thì đó có thể là các góc. Bạn nên làm cho ngưỡng góc của bạn nhỏ để người dùng có thể thực hiện việc này một cách vô thức, thay vì phải cố tình tạm dừng ở mỗi góc.
  • Góc: Một vòng tròn sẽ có cùng một góc từ mẫu này đến mẫu tiếp theo trong suốt quãng đường. Một đa giác sẽ có một số góc được nối bởi các đoạn thẳng; các góc là các góc. Đối với một đa giác thông thường (hình tròn đến hình elip của hình đa giác không đều), tất cả các góc đều gần giống nhau; một đa giác không đều sẽ có các góc góc khác nhau.
  • Khoảng thời gian: Các góc của đa giác thông thường sẽ cách nhau không gian bằng nhau trong kích thước góc và bán kính sẽ không đổi. Một đa giác không đều sẽ có các khoảng góc không đều và / hoặc bán kính không cố định.

Bước thứ ba và cuối cùng là tạo hình, tập trung vào điểm trung tâm được xác định trước đó, với bán kính được xác định trước đó.

Không đảm bảo rằng bất cứ điều gì tôi đã nói ở trên sẽ hoạt động hoặc hiệu quả, nhưng tôi hy vọng điều đó ít nhất giúp bạn đi đúng hướng và xin vui lòng, nếu bất cứ ai biết nhiều về nhận dạng hình dạng hơn tôi (đó là một thanh rất thấp) nhìn thấy này, hãy gửi bình luận hoặc câu trả lời của riêng bạn.


+1 Xin chào, cảm ơn về đầu vào. Rất nhiều thông tin. Tương tự như vậy, tôi ước rằng siêu nhân iOS / "nhận dạng hình dạng" bằng cách nào đó sẽ thấy bài đăng này và khai sáng chúng tôi hơn nữa.
Unheilig

1
@Unheilig: Ý kiến ​​hay. Làm xong.
Peter Hosey

1
Thuật toán của bạn nghe có vẻ tốt. Tôi sẽ thêm một kiểm tra về khoảng cách đường dẫn của người dùng được chuyển hướng từ một vòng tròn / đa giác hoàn hảo. (Ví dụ: phần trăm có nghĩa là độ lệch vuông.) Nếu nó quá lớn, người dùng có thể không muốn hình dạng lý tưởng. Đối với một người vẽ nguệch ngoạc lành nghề, điểm cắt sẽ nhỏ hơn so với người vẽ nguệch ngoạc. Có được điều này sẽ cho phép chương trình mang lại tự do nghệ thuật cho các nghệ sĩ nhưng rất nhiều sự giúp đỡ cho người mới bắt đầu.
dmm

@ user2654818: Bạn sẽ đo lường điều đó như thế nào?
Peter Hosey

1
@PeterHosey: Giải thích cho các vòng kết nối: Khi bạn có vòng tròn lý tưởng, bạn đã có tâm và bán kính. Vì vậy, bạn lấy mọi điểm đã vẽ và tính khoảng cách vuông của nó từ tâm, đó là ((x-x0) ^ 2 + (y-y0) ^ 2). Trừ đi mà từ bán kính bình phương. (Tôi đang tránh rất nhiều căn bậc hai để lưu tính toán.) Gọi đó là lỗi bình phương cho một điểm được vẽ. Trung bình sai số bình phương cho tất cả các điểm được vẽ, sau đó căn bậc hai, sau đó chia cho bán kính. Đó là phân kỳ phần trăm trung bình của bạn. (Toán học / số liệu thống kê có thể đáng tin cậy, nhưng nó sẽ hoạt động trong thực tế.)
dmm

2

Tôi đã khá may mắn với một công cụ nhận dạng $ 1 được đào tạo đúng cách ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Tôi đã sử dụng nó cho hình tròn, đường thẳng, hình tam giác và hình vuông.

Cách đây đã lâu, trước khi có UIGestureRecognizer, nhưng tôi nghĩ việc tạo ra các lớp con UIGestureRecognizer thích hợp là điều dễ dàng.


2

Khi bạn xác định người dùng đã vẽ xong hình dạng của họ tại nơi họ bắt đầu, bạn có thể lấy một mẫu tọa độ mà họ đã vẽ và thử khớp chúng với một vòng tròn.

Có một giải pháp MATLAB cho vấn đề này ở đây: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

Dựa trên bài viết Least-Squares Ghép các vòng tròn và Ellipses của Walter Gander, Gene H. Golub và Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

Tiến sĩ Ian Coope từ Đại học Canterbury, New Zealand đã xuất bản một bài báo với bản tóm tắt:

Vấn đề xác định đường tròn phù hợp nhất với một tập hợp các điểm trong mặt phẳng (hoặc khái quát hóa rõ ràng cho kích thước n) có thể dễ dàng được coi là một bài toán tổng bình phương nhỏ nhất phi tuyến có thể được giải bằng thuật toán tối thiểu hóa Gauss-Newton. Cách tiếp cận đơn giản này được chứng minh là không hiệu quả và cực kỳ nhạy cảm với sự hiện diện của các ngoại lệ. Một công thức thay thế cho phép giảm vấn đề thành vấn đề bình phương tuyến tính nhỏ nhất được giải quyết một cách tầm thường. Cách tiếp cận được đề xuất cho thấy có thêm lợi thế là ít nhạy cảm hơn với các ngoại lệ so với phương pháp bình phương nhỏ nhất phi tuyến.

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

Tệp MATLAB có thể tính toán cả bài toán TLS phi tuyến và bài toán LLS tuyến tính.


0

Đây là một cách khá đơn giản bằng cách sử dụng:

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

giả sử lưới ma trận này:

 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

Đặt một số UIView vào vị trí "X" và kiểm tra xem chúng có bị tấn công không (theo trình tự). Nếu tất cả đều bị tấn công theo trình tự, tôi nghĩ có thể công bằng khi để người dùng nói "Làm tốt lắm, bạn đã vẽ một vòng tròn"

Âm thanh ổn chứ? (và đơn giản)


Chào, Lemon. Lý luận tốt, nhưng trong kịch bản trên, điều đó có nghĩa là chúng ta sẽ cần phải có 64 UIView để phát hiện các cú chạm, phải không? Và làm thế nào bạn có thể xác định kích thước cho một UIView duy nhất nếu khung vẽ là kích thước của iPad chẳng hạn? Có vẻ như nếu vòng tròn nhỏ và nếu kích thước của một UIView lớn hơn, thì trong trường hợp này, chúng tôi không thể kiểm tra chuỗi vì tất cả các điểm được vẽ sẽ nằm trong một UIView duy nhất.
Unheilig

Đúng - cái này có lẽ chỉ hoạt động nếu bạn sửa canvas thành một cái gì đó như 300x300 và sau đó có một canvas "ví dụ" bên cạnh nó với kích thước vòng tròn bạn đang tìm kiếm để người dùng vẽ. Nếu vậy tôi sẽ đi với các ô vuông 50x50 * 6, bạn cũng chỉ cần hiển thị các Chế độ xem mà bạn quan tâm để đánh đúng vị trí, không phải tất cả 6 * 6 (36) hoặc 8 * 8 (64)
dijipiji

@Unheilig: Đó là những gì giải pháp này làm. Bất cứ điều gì đủ vòng tròn để đi qua một chuỗi các khung nhìn chính xác (và bạn có khả năng có thể cho phép một số đường vòng tối đa cho độ dốc thêm) sẽ khớp với một vòng tròn. Sau đó, bạn chụp nó thành một vòng tròn hoàn hảo tập trung ở trung tâm của tất cả các chế độ xem đó, có bán kính đạt tới tất cả (hoặc ít nhất là).
Peter Hosey

@PeterHosey Ok, hãy để tôi thử suy nghĩ về điều này. Tôi sẽ đánh giá cao nếu bất kỳ ai trong số bạn có thể cung cấp một số mã để có được điều này. Trong khi đó, tôi cũng sẽ cố gắng để hoàn thành công việc này và sau đó tôi sẽ làm tương tự với phần mã hóa. Cảm ơn.
Unheilig

Chỉ cần gửi một cách khác cho bạn mà tôi nghĩ có thể tốt hơn
dijipiji
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.