사용자의 터치로 완벽한 원 그리기


176

이 연습 프로젝트를 통해 사용자가 손가락으로 터치 할 때 화면에 그릴 수 있습니다. 내가 운동 방법으로 다시했던 매우 간단한 응용 프로그램. 내 작은 사촌 이이 응용 프로그램에서 내 iPad로 손가락으로 물건을 그리는 자유를 얻었습니다 (어린이 그림 : 원, 선 등 그의 마음에 들었던 것). 그런 다음 그는 원을 그리기 시작한 후 "좋은 원"으로 만들 것을 요청했습니다. 원은 절대 원처럼 반올림되지 않습니다.

그래서 제 질문은 코드에서 원을 형성하는 사용자가 그린 선을 먼저 감지하고 화면에서 완전히 둥글게하여 원과 거의 같은 크기의 원을 생성 할 수있는 방법이 있습니까? 그렇게 직선이 아닌 직선을 만드는 것은 내가해야 할 일이지만 원은 석영이나 다른 방법으로 어떻게 해야하는지 잘 모릅니다.

내 추론은 사용자가 실제로 원을 그리려고한다는 사실을 정당화하기 위해 사용자가 손가락을 든 후에 선의 시작과 끝이 서로 닿거나 교차해야한다는 것입니다.


2
이 시나리오에서는 원과 다각형의 차이를 말하기가 어려울 수 있습니다. 사용자가 클릭하여 경계 사각형의 중심 또는 한 모서리를 정의하고 드래그하여 반경을 변경하거나 반대쪽 모서리를 설정하는 "원 도구"를 사용하는 것은 어떻습니까?
user1118321

2
@ user1118321 : 이것은 단지 원을 그리고 완벽한 원을 가질 수 있다는 개념을 무너 뜨립니다. 이상적으로, 앱은 사용자가 원 (더 많거나 적은), 타원 또는 다각형을 그렸는지 여부를 사용자의 그림만으로 인식해야합니다. (플러스, 다각형이에 대한 범위에되지 않을 수도 있습니다 앱 그냥 원이나 줄 수 있습니다.)
피터 Hosey

그래서, 나는 어떤 현상에 대해 현상금을 주어야한다고 생각합니까? 좋은 후보자가 많이 있습니다.
피터 호세이

@Unheilig : 저는 Trig에 대한 초기 이해를 넘어서는 주제에 대한 전문 지식이 없습니다. 즉, 저에게 가장 가능성이 높은 답변은 stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , 아마도 stackoverflow.com/a/ 18992200/30461 및 내 자신의. 그것들은 내가 먼저 시도한 것들입니다. 나는 당신에게 명령을 남깁니다.
Peter Hosey

1
@Gene : 아마도 관련 정보를 요약하고 더 자세한 정보로 연결될 수 있습니다.
Peter Hosey 2013 년

답변:


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)가 0 인 상황을 올바르게 처리하면됩니다 .

여기에 이미지 설명을 입력하십시오

최신 정보

이 작은 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
더 일반적으로 : 간결하고 이해하기 쉬운 설명 및 다이어그램과 애니메이션 데모 및 코드 및 변형? 이상적인 스택 오버플로 답변입니다.
Peter Hosey

11
이것은 좋은 대답입니다. 그가 Java로 컴퓨터 그래픽을하고 있다고 거의 용서할 수 있습니다! ;)
Nicolas Miari

4
이 크리스마스, 산타 레나 트에 대해 더 이상 놀라운 업데이트 (예 : 더 많은 모양 등)가 있습니까? :-)
Unheilig

1
와. 투르 드 포스.
wogsland

14

모양을 감지하는 고전적인 컴퓨터 비전 기술은 Hough Transform입니다. Hough Transform의 장점 중 하나는 부분 데이터, 불완전한 데이터 및 노이즈에 매우 관대하다는 것입니다. 서클에 Hough 사용 : http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

귀하의 원이 손으로 그려 졌으므로 Hough 변환이 당신에게 잘 어울릴 것입니다.

여기에 "간단한"설명이 있습니다. 실제로 그렇게 단순하지 않은 점에 대해 사과드립니다. 그것의 대부분은 몇 년 전에 내가 한 학교 프로젝트에서 나온 것입니다.

허프 변환은 투표 방식입니다. 2 차원 정수 배열이 할당되고 모든 요소가 0으로 설정됩니다. 각 요소는 분석중인 이미지의 단일 픽셀에 해당합니다. 이 배열은 각 요소가 정보, 투표권을 축적하므로 픽셀이 원 또는 호의 원점에있을 가능성을 나타 내기 때문에 누산기 배열이라고합니다.

그라디언트 연산자 엣지 검출기가 이미지에 적용되고 엣지 픽셀 또는 엣지가 기록됩니다. Edgel은 주변에 대해 다른 강도 또는 색상을 갖는 픽셀입니다. 차이의 정도를 그래디언트 크기라고합니다. 충분한 크기의 각 edge1에 대해 어큐뮬레이터 어레이의 요소를 증가시키는 투표 방식이 적용됩니다. 증가 (투표 용)되는 요소는 고려중인 edgel을 통과하는 가능한 원의 원점에 해당합니다. 원하는 결과는 호가 존재하면 실제 원점이 잘못된 원점보다 더 많은 투표를 받게됩니다.

투표를 위해 방문되는 누산기 배열의 요소는 고려중인 가장자리 주위에 원을 형성합니다. 투표 할 x, y 좌표를 계산하는 것은 그리는 원의 x, y 좌표를 계산하는 것과 같습니다.

손으로 그린 ​​이미지에서 edgels를 계산하는 대신 세트 (컬러) 픽셀을 직접 사용할 수 있습니다.

이제 불완전하게 위치한 픽셀로 인해 가장 많은 표를 가진 단일 누산기 배열 요소를 얻을 필요는 없습니다. 여러 개의 투표권과 클러스터가있는 이웃 배열 요소 모음을 얻을 수 있습니다. 이 군집의 무게 중심은 원점에 대한 근사치를 제공 할 수 있습니다.

반경 R의 다른 값에 대해 Hough Transform을 실행해야 할 수도 있습니다. 더 밀집된 투표 클러스터를 생성하는 것은 "더 나은"적합입니다.

허위 기원에 대한 투표를 줄이기 위해 사용할 수있는 다양한 기술이 있습니다. 예를 들어, edgels를 사용하는 것의 장점은 크기뿐만 아니라 방향도 가지고 있다는 것입니다. 투표 할 때 적절한 방향으로 가능한 출처에 대해서만 투표하면됩니다. 투표를받는 위치는 완전한 원이 아닌 호를 형성합니다.

다음은 예입니다. 우리는 반지름 1의 원과 초기화 된 누산기 배열로 시작합니다. 각 픽셀이 고려 될 때 잠재적 원점이 투표됩니다. 진정한 원산지는이 경우 4 인 가장 많은 표를받습니다.

.  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 터치를 사용하여 시작, 터치, 이동, 종료 및 배열에 포인트 추가. 배열을 반으로 나누고 한 배열의 모든 점이 다른 배열의 다른 점과 다른 지름과 거의 같은 지름인지 테스트합니다.

    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
알고리즘이 잘 들립니다. 사용자의 경로가 완벽한 원 / 다각형에서 얼마나 멀리 떨어져 있는지 확인합니다. (예를 들어 퍼센트는 제곱 편차를 의미합니다.) 너무 큰 경우 사용자는 이상적인 모양을 원하지 않을 수 있습니다. 숙련 된 기념일 로고의 경우 컷오프는 조잡한 기념일 로고보다 작습니다. 이를 통해 프로그램은 예술가에게 예술적 자유를 줄 수 있지만 초보자에게는 많은 도움을 줄 수 있습니다.
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

이 논문 은 Walter Gander, Gene H. Golub 및 Rolf Strebel가 작성한 최소 제곱 원의 원과 타원 피팅을 기반으로합니다 . .

뉴질랜드 캔터베리 대학교 (University of Canterbury)의 Ian Coope 박사는 초록과 함께 논문을 발표했습니다.

평면의 점 집합에 가장 잘 맞는 원을 결정하는 문제 (또는 n 차원에 대한 명백한 일반화)는 Gauss-Newton 최소화 알고리즘을 사용하여 해결할 수있는 비선형 총 최소 제곱 문제로 쉽게 공식화됩니다. 이 간단한 접근 방식은 비효율적이며 특이 치의 존재에 매우 민감합니다. 대안적인 제형은 문제가 사소한 해결되는 선형 최소 제곱 문제로 감소 될 수있게한다. 권장 접근법은 비선형 최소 제곱 접근법보다 특이 치에 훨씬 덜 민감하다는 추가 이점을 갖는 것으로 나타났습니다.

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

"X"위치에 UIView를 배치하고 적중 (순서대로)되는지 테스트하십시오. 모두 순서대로 맞으면 사용자가 "원을 그리셨습니까?"라고 말하게하는 것이 공정하다고 생각합니다.

괜찮아? (간단한)


안녕, 레몬 좋은 추론이지만 위의 시나리오에서 터치를 감지하려면 64 개의 UIView가 필요합니다. 캔버스가 iPad의 크기 인 경우 단일 UIView의 크기를 어떻게 정의합니까? 원이 작고 단일 UIView의 크기가 큰 경우이 경우 모든 그려진 점이 단일 UIView 내에 있기 때문에 시퀀스를 확인할 수없는 것 같습니다.
Unheilig

그렇습니다-이것은 캔버스를 300x300과 같은 것으로 고친 다음 사용자가 그릴 원의 크기가있는 캔버스 옆에 "예제"캔버스가있는 경우에만 작동합니다. 그렇다면 50x50 squares * 6을 사용한다면 6 * 6 (36) 또는 8 * 8 (64)가 아니라 올바른 위치에 타격을
가하려는 뷰만 렌더링하면됩니다.

@ Unheilig : 이것이이 솔루션이하는 일입니다. 올바른 순서의 뷰를 통과하기에 충분한 원형 (및 추가 경사면에 대해 최대 수의 우회를 허용 할 수 있음)은 원과 일치합니다. 그런 다음 반지름이 모두 (또는 적어도) 도달하는 모든 뷰의 중심을 중심으로 한 완벽한 원에 스냅합니다.
Peter Hosey

@PeterHosey Ok,이 문제를 해결하려고 노력하겠습니다. 이 롤링을 위해 코드를 제공 할 수 있다면 감사하겠습니다. 한편, 나는 또한 내 머리를 둥글게하려고 노력하고 나중에 코딩 부분과 똑같이 할 것입니다. 감사.
Unheilig

그냥 내가 더 나은 것 같아요 당신을위한 다른 방법으로 제출
dijipiji
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.