ios 스크립트 사용자의 터치로 완벽한 원 그리기



react native 다음 지도 (5)

나는이 연습 프로젝트를 통해 사용자가 손가락으로 터치 할 때 화면을 그릴 수있다. 아주 간단한 앱으로 운동 방법으로 돌아 왔습니다. 내 작은 사촌은이 앱에서 내 iPad로 손가락으로 그림을 그리는 자유를 누렸다 (키즈 드로잉 : 서클, 라인 등 무엇이든 마음에와). 그런 다음 그는 서클을 그리기 시작한 다음 나에게 "좋은 서클"을 만들 것을 요청했습니다. (내 이해에 따르면 : 그려진 서클을 완벽하게 둥글게 만듭니다. 화면에서 손가락으로 뭔가를 그리는 것이 얼마나 안정적이든 관계없이 원은 원형과 같이 결코 둥글게되지 않습니다.).

그래서 여기 내 질문은, 우리가 원을 형성하고 화면에서 완벽하게 둥글게함으로써 원의 크기를 거의 생성하는 사용자에 의해 그려진 선을 먼저 감지 할 수있는 코드에 어떤 방법이 있느냐입니다. 직선이 아닌 직선을 만드는 것은 내가하는 법을 알 수있는 것이지만, 원에 관해서는 Quartz 나 다른 방법을 사용하는 방법을 알지 못한다.

내 추론은 라인의 시작점과 끝점은 사용자가 실제로 원을 그리려한다는 사실을 정당화하기 위해 손가락을 들었을 때 서로 닿거나 교차해야한다는 것입니다.

https://ffff65535.com


나는 어떤 형태의인지 전문가가 아니지만 여기에 내가 어떻게 문제에 접근 할 수 있는지가 나와있다.

먼저, 사용자의 경로를 프리 핸드로 표시하는 동안 포인트 (x, y) 샘플 목록을 시간과 함께 비밀리에 누적하십시오. 드래그 이벤트에서 두 가지 사실을 모두 가져올 수 있고, 간단한 모델 오브젝트로 래핑하고, 변경 가능한 배열로 그 사실을 쌓을 수 있습니다.

0.1 초마다 샘플을 꽤 자주 가져 가고 싶을 것입니다. 또 다른 가능성은 정말로 자주 시작합니다. 아마 0.05 초마다 시작하고 사용자가 얼마나 길게 끌고 있는지 지켜보십시오. 일정 시간 이상 끌면 샘플 주파수를 낮추고 (놓친 모든 샘플을 떨어 뜨리십시오) 0.2 초 정도 걸립니다.

(복음에 대한 사람들의 수를 가져 가라. 나는 그들을 내 모자에서 꺼냈다. 실험하고 더 나은 가치를 발견한다.)

둘째, 샘플을 분석하십시오.

당신은 두 가지 사실을 도출하고자합니다. 첫째, (IIRC)가 모든 점의 평균이어야하는 모양의 중심. 둘째, 그 중심으로부터의 각 샘플의 평균 반지름.

@ user1118321이 추측 한대로 다각형을 지원하려는 경우 나머지 분석은 사용자가 원 또는 다각형을 그릴 지 여부와 같은 결정을 내리는 것으로 구성됩니다. 샘플을 다각형으로보고 시작하여 결정을 내릴 수 있습니다.

사용할 수있는 기준은 다음과 같습니다.

  • 시간 : 사용자가 어떤 지점에서 다른 지점보다 오랫동안 가리키면 (샘플이 일정한 간격을 유지하면 서로 인접한 샘플이 연속적으로 공간에 나타납니다) 모서리가 될 수 있습니다. 각 구석에서 고의로 일시 중지하지 않고 무의식적으로이를 수행 할 수 있도록 모서리 임계 값을 작게 만들어야합니다.
  • 각도 : 원은 한 샘플에서 다음 샘플까지 모든 각도에서 대략 동일한 각도를 갖습니다. 다각형은 직선으로 연결된 여러 각도를가집니다. 각은 모서리입니다. 규칙적인 다각형 (불규칙한 다각형의 타원까지의 원)의 경우, 코너 각도는 모두 대략 동일해야합니다. 불규칙한 다각형은 다른 코너 각도를 갖습니다.
  • 간격 : 일정한 다각형의 모서리는 각도 치수 내에서 동일한 간격으로 떨어져 있고 반지름은 일정합니다. 불규칙한 다각형에는 불규칙한 각도 간격 및 / 또는 일정하지 않은 반경이 있습니다.

세 번째 단계와 마지막 단계는 이전에 결정된 반경을 사용하여 이전에 결정된 중심점을 중심으로 형상을 작성하는 것입니다.

위에서 말한 모든 것이 효과가 있거나 작동 할 것이라는 보장은 없지만 적어도 올바른 방향으로 가면 좋을 것입니다. 그리고 나보다 모양 인식에 대해 더 잘 아는 사람 (매우 낮은 막대)이 본다면 이 말은 자유롭게 의견이나 자신의 답변을 게시 할 수 있습니다.


나는 제대로 훈련 된 $ 1 인식기 ( http://depts.washington.edu/aimgroup/proj/dollar/ )로 꽤 좋은 행운을 얻었습니다. 원, 선, 삼각형 및 사각형에 사용했습니다.

오래 전에 UIGestureRecognizer가 있었지만 적절한 UIGestureRecognizer 서브 클래스를 작성하는 것이 쉬워야한다고 생각합니다.


다음을 사용하는 것이 매우 간단합니다.

- (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

일부 UIView를 "X"위치에 놓고 순서대로 히트되도록 테스트합니다. 그들은 모두 순차적으로 공격한다면 나는 사용자가 "잘 했어. 서클을 그렸어."라고 말하면 공정하다고 생각한다.

괜찮은거야? (그리고 간단하다)


때로는 바퀴를 다시 만드는 데 약간의 시간을 투자하는 것이 유용합니다. 이미 알고 계시 겠지만 많은 프레임 워크가 있지만 단순하지만 유용한 솔루션을 구현하기는 어렵지 않습니다. (제발 잘못 이해하지 마세요. 어떤 진지한 목적이라면 성숙하고 안정적인 프레임 워크로 입증 된 것을 사용하는 것이 낫습니다.)

먼저 결과를 발표하고 그 뒤에 단순하고 솔직한 아이디어를 설명합니다.

제 구현에서 모든 단일 지점을 분석하고 복잡한 계산을 수행 할 필요가 없습니다. 아이디어는 중요한 메타 정보를 발견하는 것입니다. 예를 들어 tangent 을 사용하겠습니다.

선택한 모양에 대해 일반적으로 나타나는 간단하고 직선적 인 패턴을 확인해 보겠습니다.

따라서 그 아이디어에 기반한 원 감지 메커니즘을 구현하는 것은 그리 어렵지 않습니다. 아래의 작업 데모보기 (죄송합니다.이 빠른 방법과 더러운 예제를 제공하는 가장 빠른 방법으로 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에서 비슷한 동작을 구현하는 것은 문제가되지 않아야합니다. 여러 이벤트와 좌표가 필요하기 때문입니다. 다음과 같은 것 ( example 참조) :

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

사용자가 시작한 위치에서 모양을 완성한 것으로 판단한 후에는 그려진 좌표 샘플을 가져 와서 원에 맞추어 볼 수 있습니다.

이 문제에 대한 MATLAB 솔루션은 다음과 같습니다. http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

Walter Gander, Gene H. Golub 및 Rolf Strebel 의 원 및 타원의 최소 제곱합을 기반으로 한 논문을 기반으로합니다 : http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf

캔터베리 대학 (University of Canterbury)의 이안 쿠프 (Ian Coope) 박사는 "

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

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

MATLAB 파일은 비선형 TLS 및 선형 LLS 문제를 계산할 수 있습니다.





shape-recognition