วาดวงกลมที่สมบูรณ์แบบจากการสัมผัสของผู้ใช้


176

ฉันมีโครงการฝึกหัดนี้ที่ให้ผู้ใช้สามารถวาดบนหน้าจอขณะที่พวกเขาสัมผัสด้วยนิ้วของพวกเขา แอพที่เรียบง่ายมากฉันกลับไปออกกำลังกายเหมือนเดิม ลูกพี่ลูกน้องตัวน้อยของฉันใช้เสรีภาพในการวาดสิ่งต่าง ๆ ด้วยนิ้วมือของเขากับ iPad ของฉันบนแอพนี้ (ภาพวาดสำหรับเด็ก: วงกลมเส้น ฯลฯ อะไรก็ตามที่อยู่ในใจของเขา) จากนั้นเขาก็เริ่มวาดวงกลมและจากนั้นเขาขอให้ฉันทำให้มันเป็น "วงกลมที่ดี" (จากความเข้าใจของฉัน: ทำให้วงกลมที่ถูกวาดกลมอย่างสมบูรณ์แบบเพราะเรารู้ว่าไม่ว่าเราจะพยายามวาดบางสิ่งด้วยนิ้วของเราบนหน้าจอ วงกลมไม่เคยถูกปัดเศษอย่างที่ควรจะเป็น)

ดังนั้นคำถามของฉันที่นี่คือมีวิธีใดในรหัสที่เราสามารถตรวจจับเส้นที่วาดโดยผู้ใช้ที่สร้างเป็นวงกลมและสร้างขนาดประมาณวงกลมเดียวกันโดยการทำให้มันกลมอย่างสมบูรณ์บนหน้าจอ การสร้างเส้นตรงที่ไม่ตรงนั้นเป็นสิ่งที่ฉันรู้ว่าต้องทำอย่างไร แต่สำหรับวงกลมฉันไม่รู้ว่าจะทำอย่างไรกับ Quartz หรือวิธีอื่น ๆ

เหตุผลของฉันคือจุดเริ่มต้นและจุดสิ้นสุดของบรรทัดต้องแตะหรือข้ามซึ่งกันและกันหลังจากผู้ใช้ยกนิ้วของเขาเพื่อพิสูจน์ความจริงที่ว่าเขาพยายามวาดวงกลมจริงๆ


2
เป็นการยากที่จะบอกความแตกต่างระหว่างวงกลมกับรูปหลายเหลี่ยมในสถานการณ์นี้ วิธีการเกี่ยวกับ "เครื่องมือวงกลม" ที่ผู้ใช้คลิกเพื่อกำหนดศูนย์กลางหรือมุมหนึ่งของสี่เหลี่ยมที่มีขอบเขตและลากเพื่อเปลี่ยนรัศมีหรือตั้งค่ามุมตรงข้าม?
user1118321

2
@ user1118321: สิ่งนี้เอาชนะแนวคิดของเพียงแค่ความสามารถในการวาดวงกลมและมีวงกลมที่สมบูรณ์แบบ ตามหลักการแล้วแอปควรจดจำจากรูปวาดของผู้ใช้เพียงอย่างเดียวไม่ว่าผู้ใช้จะวาดวงกลม (มากหรือน้อย) วงรีหรือรูปหลายเหลี่ยม (เพิ่มเติมรูปหลายเหลี่ยมอาจไม่อยู่ในขอบเขตสำหรับแอปนี้ - อาจเป็นเพียงวงกลมหรือเส้น)
Peter Hosey

ดังนั้นคุณคิดว่าคำตอบใดที่ฉันควรให้รางวัล? ฉันเห็นผู้สมัครที่ดีมากมาย
Peter Hosey

@ Unheilig: ฉันไม่มีความเชี่ยวชาญในเรื่องใดเลยนอกเหนือจากความเข้าใจในเรื่องตรีโกณมิติ ที่กล่าวว่าคำตอบที่แสดงให้ฉันเห็นว่าเป็นไปได้มากที่สุดคือstackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461อาจเป็นstackoverflow.com/a/ 18992200/30461และของฉันเอง นี่คือสิ่งที่ฉันจะลองก่อน ฉันออกคำสั่งให้คุณ
Peter Hosey

1
@ ยีน: บางทีคุณสามารถสรุปข้อมูลที่เกี่ยวข้องและเชื่อมโยงไปยังรายละเอียดเพิ่มเติมได้ในคำตอบ
ปีเตอร์โฮเชย์

คำตอบ:


381

บางครั้งมันมีประโยชน์มากที่จะใช้เวลาสร้างล้อ อย่างที่คุณอาจสังเกตเห็นแล้วว่ามีเฟรมเวิร์กมากมาย แต่ก็ไม่ยากที่จะใช้โซลูชันที่เรียบง่าย แต่มีประโยชน์โดยไม่ต้องแนะนำความซับซ้อนทั้งหมดนั้น (โปรดอย่าเข้าใจฉันผิดเพื่อจุดประสงค์ที่จริงจังจะเป็นการดีกว่าหากใช้บางอย่างที่เป็นผู้ใหญ่และพิสูจน์แล้วว่าเป็นเฟรมเวิร์กที่เสถียร)

ฉันจะนำเสนอผลลัพธ์ของฉันก่อนแล้วจึงอธิบายแนวคิดที่เรียบง่ายและตรงไปตรงมา

ป้อนคำอธิบายรูปภาพที่นี่

คุณจะเห็นในการใช้งานของฉันไม่จำเป็นต้องวิเคราะห์ทุก ๆ จุดและทำการคำนวณที่ซับซ้อน แนวคิดคือการสังเกตข้อมูลเมตาที่มีค่าบางอย่าง ฉันจะใช้แทนเจนต์เป็นตัวอย่าง:

ป้อนคำอธิบายรูปภาพที่นี่

ลองระบุรูปแบบที่เรียบง่ายและตรงไปตรงมาโดยทั่วไปสำหรับรูปร่างที่เลือก:

ป้อนคำอธิบายรูปภาพที่นี่

ดังนั้นจึงไม่ยากที่จะใช้กลไกการตรวจจับแบบวงกลมตามแนวคิดนั้น ดูตัวอย่างการทำงานด้านล่าง (ขออภัยฉันใช้ Java เป็นวิธีที่เร็วที่สุดในการให้ตัวอย่างรวดเร็วและสกปรก):

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

มันไม่ควรเป็นปัญหาที่จะใช้พฤติกรรมที่คล้ายกันบน iOS เนื่องจากคุณเพียงแค่ต้องการเหตุการณ์และพิกัดหลายอย่าง สิ่งต่อไปนี้ (ดูตัวอย่าง ):

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

มีการปรับปรุงหลายอย่างที่เป็นไปได้

เริ่มที่จุดใดก็ได้

ข้อกำหนดปัจจุบันคือการเริ่มวาดวงกลมจากจุดกึ่งกลางด้านบนเนื่องจากการทำให้เข้าใจง่ายดังต่อไปนี้:

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

กรุณาแจ้งให้ทราบค่าเริ่มต้นของindexถูกนำมาใช้ การค้นหาอย่างง่ายผ่าน "ส่วน" ที่มีอยู่ของรูปร่างจะลบข้อ จำกัด นั้น โปรดทราบว่าคุณจะต้องใช้บัฟเฟอร์วงกลมเพื่อตรวจจับรูปร่างเต็ม:

ป้อนคำอธิบายรูปภาพที่นี่

ตามเข็มนาฬิกาและทวนเข็มนาฬิกา

ในการรองรับทั้งสองโหมดคุณจะต้องใช้บัฟเฟอร์แบบวงกลมจากการปรับปรุงก่อนหน้านี้และค้นหาทั้งสองทิศทาง:

ป้อนคำอธิบายรูปภาพที่นี่

วาดวงรี

คุณมีทุกสิ่งที่คุณต้องการอยู่ในboundsอาเรย์แล้ว

ป้อนคำอธิบายรูปภาพที่นี่

ใช้ข้อมูลนั้นเพียง:

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

ท่าทางอื่น ๆ (ไม่บังคับ)

สุดท้ายคุณเพียงแค่ต้องจัดการสถานการณ์อย่างถูกต้องเมื่อdx(หรือdy) เท่ากับศูนย์เพื่อสนับสนุนท่าทางอื่น ๆ :

ป้อนคำอธิบายรูปภาพที่นี่

ปรับปรุง

PoC ขนาดเล็กนี้ได้รับความสนใจค่อนข้างสูงดังนั้นฉันจึงอัปเดตรหัสเล็กน้อยเพื่อให้ทำงานได้อย่างราบรื่นและให้คำแนะนำการวาดภาพไฮไลต์จุดสนับสนุน ฯลฯ :

ป้อนคำอธิบายรูปภาพที่นี่

นี่คือรหัส:

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
คำตอบที่งดงาม Renat คำอธิบายที่ชัดเจนเกี่ยวกับวิธีการภาพที่บันทึกกระบวนการภาพเคลื่อนไหวด้วย ดูเหมือนว่ายังเป็นวิธีการแก้ปัญหาทั่วไปที่แข็งแกร่งที่สุด ฟังก์ชั่นแทนเจนต์ดูเหมือนความคิดที่ฉลาด - เหมือนเทคนิคการรู้จำลายมือเริ่มต้น (ปัจจุบัน) คำถามที่คั่นไว้เพื่อประโยชน์ของคำตอบนี้ :)
enhzflep

27
เพิ่มเติมโดยทั่วไป: คำอธิบายและไดอะแกรมที่รัดกุมและเข้าใจง่ายและตัวอย่างแอนนิเมชั่นและโค้ดและรูปแบบต่างๆ? นี่เป็นคำตอบที่เหมาะสำหรับ Stack Overflow
Peter Hosey

11
นี่เป็นคำตอบที่ดีมากฉันเกือบจะให้อภัยเขากำลังทำกราฟิกคอมพิวเตอร์ใน Java! ;)
Nicolas Miari

4
จะมีการอัปเดตที่น่าแปลกใจอีกต่อไป (เช่นรูปร่างเพิ่มเติม ฯลฯ ) สำหรับคริสต์มาสซานต้าเรนาตนี้หรือไม่? :-)
Unheilig

1
ว้าว. ตูร์เดอบังคับ
wogsland

14

เทคนิค Computer Vision แบบคลาสสิกสำหรับตรวจจับรูปร่างคือ Hough Transform หนึ่งในสิ่งที่ดีเกี่ยวกับ Hough Transform คือมันทนทานต่อข้อมูลบางส่วนข้อมูลที่ไม่สมบูรณ์และเสียงรบกวน การใช้ Hough เป็นวงกลม: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

เนื่องจากวงกลมของคุณถูกวาดด้วยมือฉันคิดว่าการแปลง Hough อาจเป็นการจับคู่ที่ดีสำหรับคุณ

ต่อไปนี้เป็นคำอธิบายที่ "เข้าใจง่าย" ฉันขอโทษเพราะมันไม่ได้ง่ายขนาดนั้น ส่วนใหญ่มาจากโครงการโรงเรียนที่ฉันทำเมื่อหลายปีก่อน

Hough Transform เป็นรูปแบบการลงคะแนน อาร์เรย์จำนวนสองมิติของจำนวนเต็มถูกจัดสรรและองค์ประกอบทั้งหมดถูกตั้งค่าเป็นศูนย์ แต่ละองค์ประกอบสอดคล้องกับพิกเซลเดียวในภาพที่กำลังวิเคราะห์ อาเรย์นี้เรียกว่าอาเรย์แอคคิวมูเลเตอร์เนื่องจากแต่ละอิลิเมนต์จะรวบรวมข้อมูลโหวตเพื่อระบุความเป็นไปได้ที่พิกเซลอาจจะอยู่ที่จุดกำเนิดของวงกลมหรือส่วนโค้ง

ตัวตรวจจับขอบตัวดำเนินการไล่ระดับสีจะถูกนำไปใช้กับภาพและพิกเซลขอบหรือขอบภาพจะถูกบันทึกไว้ edgel เป็นพิกเซลที่มีความเข้มหรือสีต่างกันเมื่อเทียบกับประเทศเพื่อนบ้าน ระดับของความแตกต่างเรียกว่าขนาดไล่ระดับสี สำหรับแต่ละ edgel ที่มีขนาดเพียงพอจะมีการใช้รูปแบบการลงคะแนนซึ่งจะเป็นการเพิ่มองค์ประกอบของอาร์เรย์ตัวสะสม องค์ประกอบที่เพิ่มขึ้น (โหวตให้) สอดคล้องกับต้นกำเนิดของวงกลมที่ผ่าน edgel ภายใต้การพิจารณา ผลลัพธ์ที่ต้องการคือถ้าส่วนโค้งมีอยู่แหล่งกำเนิดที่แท้จริงจะได้รับคะแนนเสียงมากกว่าต้นกำเนิดที่ผิดพลาด

โปรดทราบว่าองค์ประกอบของอาร์เรย์แอคคิวมูเลเตอร์ที่ถูกเยี่ยมชมสำหรับการลงคะแนนจะสร้างวงกลมรอบ edgel ที่อยู่ระหว่างการพิจารณา การคำนวณพิกัด x, y ที่จะลงคะแนนเสียงนั้นเหมือนกับการคำนวณพิกัด x, y ของวงกลมที่คุณกำลังวาด

ในภาพที่วาดด้วยมือคุณอาจใช้พิกเซลชุด (สี) โดยตรงแทนที่จะคำนวณ edgels

ตอนนี้ด้วยพิกเซลที่ตั้งไม่ถูกต้องคุณจะไม่จำเป็นต้องมีองค์ประกอบอาเรย์แอคคิวมูเลเตอร์เดียวที่มีจำนวนโหวตมากที่สุด คุณอาจได้รับชุดขององค์ประกอบอาเรย์เพื่อนบ้านที่มีคะแนนโหวตกลุ่ม จุดศูนย์ถ่วงของกระจุกดาวนี้อาจเสนอจุดกำเนิดที่ดีสำหรับการประมาณ

โปรดทราบว่าคุณอาจต้องใช้การแปลง Hough สำหรับค่ารัศมีที่แตกต่างกัน R ที่สร้างกลุ่มหนาแน่นของการลงคะแนนคือแบบที่ "ดีกว่า"

มีเทคนิคต่าง ๆ ที่ใช้เพื่อลดการลงคะแนนสำหรับต้นกำเนิดที่ผิด ตัวอย่างเช่นข้อดีอย่างหนึ่งของการใช้ edgels คือพวกเขาไม่เพียงมีขนาด แต่ยังมีทิศทางด้วย เมื่อลงคะแนนเราต้องลงคะแนนเฉพาะสำหรับต้นกำเนิดที่เป็นไปได้ในทิศทางที่เหมาะสม สถานที่ที่ได้รับคะแนนโหวตจะสร้างส่วนโค้งแทนที่จะเป็นวงกลมที่สมบูรณ์

นี่คือตัวอย่าง เราเริ่มต้นด้วยวงกลมรัศมีหนึ่งและอาร์เรย์สะสมเริ่มต้น เนื่องจากแต่ละพิกเซลถือว่าเป็นจุดกำเนิดที่มีศักยภาพ ต้นกำเนิดที่แท้จริงได้รับคะแนนเสียงมากที่สุดซึ่งในกรณีนี้คือสี่

.  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

นี่คือวิธีอื่น การใช้ UIView touchegegan แตะ Moved แตะที่ End และเพิ่มจุดลงในอาร์เรย์ คุณแบ่งอาร์เรย์ออกเป็นครึ่งหนึ่งและทดสอบว่าทุกจุดในหนึ่งแถวนั้นมีเส้นผ่านศูนย์กลางเท่ากันจากคู่ในแถวอื่น ๆ เป็นคู่อื่น ๆ ทั้งหมดหรือไม่

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

}

เสียงนั้นโอเคไหม :)


3

ฉันไม่มีผู้เชี่ยวชาญด้านการจดจำรูปร่าง แต่นี่คือวิธีที่ฉันจะแก้ไขปัญหา

ขั้นแรกในขณะที่แสดงเส้นทางของผู้ใช้ด้วยมือเปล่าให้รวบรวมตัวอย่างรายการจุด (x, y) อย่างลับๆพร้อมกับเวลา คุณสามารถรับข้อมูลทั้งสองจากเหตุการณ์การลากของคุณห่อมันลงในวัตถุแบบง่าย ๆ และรวบรวมสิ่งเหล่านั้นในอาร์เรย์ที่ไม่แน่นอน

คุณอาจต้องการสุ่มตัวอย่างค่อนข้างบ่อย - พูดทุก ๆ 0.1 วินาที ความเป็นไปได้อีกอย่างหนึ่งก็คือการเริ่มต้นบ่อยครั้งมากๆ อาจจะทุกๆ 0.05 วินาทีและดูว่าผู้ใช้ลากไปนานแค่ไหน หากพวกเขาลากนานกว่าระยะเวลาหนึ่งให้ลดความถี่ของตัวอย่าง (และปล่อยตัวอย่างใด ๆ ที่พลาดไป) เป็น 0.2 วินาที

(และอย่านำตัวเลขของฉันไปใช้ในข่าวประเสริฐเพราะฉันเพิ่งดึงพวกเขาออกจากหมวกของฉันทดลองและหาค่าที่ดีกว่า)

ประการที่สองวิเคราะห์ตัวอย่าง

คุณจะต้องได้รับข้อเท็จจริงสองประการ ก่อนอื่นจุดศูนย์กลางของรูปร่างซึ่ง (IIRC) ควรเป็นค่าเฉลี่ยของคะแนนทั้งหมด ประการที่สองคือรัศมีเฉลี่ยของแต่ละตัวอย่างจากศูนย์นั้น

หากตามที่ @ user1118321 คาดเดาคุณต้องการสนับสนุนรูปหลายเหลี่ยมจากนั้นการวิเคราะห์ที่เหลือประกอบด้วยการตัดสินใจว่า: ผู้ใช้ต้องการวาดรูปวงกลมหรือรูปหลายเหลี่ยม คุณสามารถดูตัวอย่างเป็นรูปหลายเหลี่ยมที่เริ่มต้นด้วยการตัดสินใจ

มีหลายเกณฑ์ที่คุณสามารถใช้ได้:

  • เวลา: หากผู้ใช้วนเวียนอยู่นานกว่าบางจุด (ซึ่งหากตัวอย่างอยู่ในช่วงเวลาคงที่จะปรากฏเป็นกลุ่มตัวอย่างต่อเนื่องที่อยู่ใกล้กันในอวกาศ) ซึ่งอาจเป็นมุม คุณควรทำให้ขีด จำกัด มุมของคุณเล็กเพื่อให้ผู้ใช้สามารถทำสิ่งนี้โดยไม่รู้ตัวแทนที่จะต้องหยุดชั่วคราวในแต่ละมุม
  • มุม: วงกลมจะมีมุมเท่ากันจากตัวอย่างหนึ่งไปยังอีกมุมหนึ่ง รูปหลายเหลี่ยมจะมีหลายมุมรวมกันโดยส่วนของเส้นตรง มุมเป็นมุม สำหรับรูปหลายเหลี่ยมปกติ (วงกลมไปยังวงรีของรูปหลายเหลี่ยมที่ผิดปกติ) มุมของมุมควรจะเท่ากัน รูปหลายเหลี่ยมที่ผิดปกติจะมีมุมที่แตกต่างกัน
  • ช่วงเวลา: มุมของรูปหลายเหลี่ยมปกติจะมีพื้นที่เท่ากันภายในมิติเชิงมุมและรัศมีจะคงที่ รูปหลายเหลี่ยมที่ผิดปกติจะมีช่วงเชิงมุมที่ผิดปกติและ / หรือรัศมีที่ไม่คงที่

ขั้นตอนที่สามและขั้นสุดท้ายคือการสร้างรูปร่างโดยมีศูนย์กลางที่จุดศูนย์กลางที่กำหนดไว้ก่อนหน้านี้โดยมีรัศมีที่กำหนดไว้ก่อนหน้านี้

ไม่รับประกันว่าสิ่งที่ฉันได้กล่าวไว้ข้างต้นจะได้ผลหรือมีประสิทธิภาพ แต่ฉันหวังว่าอย่างน้อยคุณก็จะได้รับสิ่งที่ถูกต้อง - และโปรดถ้าใครก็ตามที่รู้จักการจดจำรูปร่างมากกว่าฉัน (ซึ่งเป็นแถบที่ต่ำมาก) สิ่งนี้รู้สึกฟรีเพื่อแสดงความคิดเห็นหรือคำตอบของคุณเอง


+1 สวัสดีขอบคุณสำหรับการป้อนข้อมูล ข้อมูลมาก ในทำนองเดียวกันฉันหวังว่าซูเปอร์แมน iOS / "การจดจำรูปร่าง" จะเห็นโพสต์นี้และสอนเราต่อไป
Unheilig

1
@ Unheilig: ความคิดที่ดี เสร็จสิ้น
Peter Hosey

1
อัลกอริทึมของคุณฟังดูดี ฉันจะเพิ่มการตรวจสอบว่าเส้นทางของผู้ใช้แยกจากวงกลม / รูปหลายเหลี่ยมที่สมบูรณ์ (ตัวอย่างเช่นเปอร์เซ็นต์หมายถึงการเบี่ยงเบนกำลังสอง) หากมีขนาดใหญ่เกินไปผู้ใช้อาจไม่ต้องการรูปร่างในอุดมคติ สำหรับ doodler ที่มีทักษะการตัดจะเล็กกว่าสำหรับ doodler ที่เลอะเทอะ การมีสิ่งนี้จะช่วยให้โปรแกรมสามารถให้อิสระทางศิลปะแก่ศิลปิน แต่มีความช่วยเหลือมากมายสำหรับผู้เริ่มต้น
dmm

@ user2654818: คุณวัดได้อย่างไร
Peter Hosey

1
@PeterHosey: คำอธิบายสำหรับแวดวง: เมื่อคุณมีวงกลมในอุดมคติแล้วคุณจะมีศูนย์กลางและรัศมี คุณเอาจุดที่วาดทุกอันแล้วคำนวณระยะห่างจากจุดศูนย์กลางซึ่งก็คือ ((x-x0) ^ 2 + (y-y0) ^ 2) ลบออกจากรัศมีกำลังสอง (ฉันกำลังหลีกเลี่ยงสแควร์รูทจำนวนมากเพื่อบันทึกการคำนวณ) เรียกว่าข้อผิดพลาดกำลังสองสำหรับจุดที่วาด หาค่าเฉลี่ยของข้อผิดพลาดกำลังสองสำหรับคะแนนที่ดึงออกทั้งหมดจากนั้นให้ทำการลบรากที่สองแล้วหารด้วยรัศมี นั่นคือเปอร์เซ็นต์การเบี่ยงเบนเฉลี่ยของคุณ (คณิตศาสตร์ / สถิติน่าจะมีค่าพอสมควร แต่มันจะใช้งานได้จริง)
dmm

2

ฉันโชคดีมากที่ได้รับการรับรองอย่างถูกต้อง $ 1 ( http://depts.washington.edu/aimgroup/proj/dollar/ ) ฉันใช้มันเป็นวงกลมเส้นสามเหลี่ยมและสี่เหลี่ยมจัตุรัส

เป็นเวลานานก่อนที่จะถึง UIGestureRecognizer แต่ฉันคิดว่ามันควรจะง่ายในการสร้างคลาสย่อยของ UIGestureRecognizer ที่เหมาะสม


2

เมื่อคุณกำหนดให้ผู้ใช้วาดรูปร่างของพวกเขาเสร็จสิ้นเมื่อเริ่มต้นคุณสามารถนำตัวอย่างของพิกัดที่พวกเขาเข้ามาและลองปรับให้เข้ากับวงกลม

มีวิธีแก้ไขปัญหาของ MATLAB ที่นี่: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

ซึ่งมีพื้นฐานมาจากกระดาษLeast-Squares Fitting ของ Circles และ Ellipsesโดย Walter Gander, Gene H. Golub และ Rolf Strebel: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

Dr Ian Coope จาก University of Canterbury, NZ ตีพิมพ์บทความที่มีนามธรรม:

ปัญหาของการกำหนดวงกลมที่เหมาะสมที่สุดสำหรับชุดของจุดในระนาบ (หรือการวางนัยทั่วไปให้เป็น n-Dimensions) นั้นถูกกำหนดขึ้นมาอย่างง่ายดายในรูปของปัญหากำลังสองน้อยที่สุดที่ไม่เป็นเชิงเส้นซึ่งอาจแก้ไขได้โดยใช้ วิธีการตรงไปข้างหน้านี้แสดงให้เห็นว่าไม่มีประสิทธิภาพและไวต่อการปรากฏตัวของค่าผิดปกติอย่างมาก การกำหนดทางเลือกช่วยให้ปัญหาลดลงเป็นปัญหากำลังสองน้อยที่สุดในแนวตรงซึ่งแก้ไขได้เล็กน้อย วิธีการที่แนะนำนั้นแสดงให้เห็นว่ามีความได้เปรียบที่เพิ่มขึ้นของความอ่อนไหวต่อค่าผิดปกติน้อยกว่าวิธีที่ไม่เชิงเส้นกำลังสองน้อยที่สุด

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

ไฟล์ MATLAB สามารถคำนวณได้ทั้งปัญหา TLS เชิงเส้นและเชิงเส้น LLS


0

นี่เป็นวิธีที่ค่อนข้างง่ายโดยใช้:

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

สมมติว่าตารางเมทริกซ์นี้:

 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

วาง UIViews บางส่วนไว้ในตำแหน่ง "X" และทดสอบเพื่อหา Hit (ตามลำดับ) หากพวกเขาได้รับความนิยมอย่างต่อเนื่องฉันคิดว่ามันยุติธรรมที่จะให้ผู้ใช้พูดว่า "คุณวาดวงกลมได้ดี"

ฟังดูโอเคไหม (และเรียบง่าย)


สวัสดีเลมอน เหตุผลที่ดี แต่ในสถานการณ์ข้างต้นหมายความว่าเราต้องมี 64 UIV วิวเพื่อตรวจจับการสัมผัสใช่ไหม? และคุณจะกำหนดขนาดของ UIView เดียวได้อย่างไรถ้าผืนผ้าใบมีขนาดเท่ากับ iPad ดูเหมือนว่าถ้าวงกลมมีขนาดเล็กและถ้าขนาดของ UIView เดียวมีขนาดใหญ่กว่าในกรณีนี้เราไม่สามารถตรวจสอบลำดับได้เนื่องจากคะแนนที่วาดทั้งหมดจะอยู่ใน UIView เดียว
Unheilig

อ๋อ - อันนี้อาจใช้ได้เฉพาะในกรณีที่คุณแก้ไขผืนผ้าใบเป็นอย่างเช่น 300x300 แล้วมีผืนผ้าใบ "ตัวอย่าง" ถัดจากขนาดของวงกลมที่คุณต้องการให้ผู้ใช้วาด ถ้าเป็นเช่นนั้นฉันจะไปด้วยสี่เหลี่ยมจัตุรัสขนาด 50x50 * 6 คุณเพียง แต่ต้องแสดงจำนวนการดูที่คุณสนใจในตำแหน่งที่ถูกต้องไม่ใช่ทั้งหมด 6 * 6 (36) หรือ 8 * 8 (64)
dijipiji

@ Unheilig: นั่นคือสิ่งที่วิธีการแก้ปัญหานี้ สิ่งใดก็ตามที่วนเวียนมากพอที่จะผ่านลำดับการดูที่ถูกต้อง (และคุณอาจอนุญาตให้มีการแวะจำนวนสูงสุดสำหรับการลดระดับพิเศษ) จะจับคู่เป็นวงกลม จากนั้นคุณถ่ายภาพให้เป็นวงกลมที่สมบูรณ์แบบซึ่งอยู่กึ่งกลางที่ศูนย์กลางของมุมมองทั้งหมดที่มีรัศมีถึง (หรืออย่างน้อยที่สุด)
Peter Hosey

@ PeterHosey ตกลงให้ฉันลองรับรอบนี้ ฉันจะซาบซึ้งถ้ามีคุณสามารถให้รหัสบางอย่างเพื่อรับการกลิ้งนี้ ในขณะเดียวกันฉันจะพยายามทำให้หัวของฉันรอบนี้และฉันจะทำเช่นเดียวกันกับส่วนการเข้ารหัส ขอบคุณ
Unheilig

เพิ่งส่งวิธีอื่นให้คุณซึ่งฉันคิดว่าน่าจะดีกว่า
dijipiji
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.