ユーザーのタッチから真円を描く


176

私は、ユーザーが指でタッチしたときに画面に描画できるようにするこの練習プロジェクトを持っています。とてもシンプルなアプリです。私のいとこは、このアプリのiPadで指で物を描く自由を取りました(子供の絵:円、線など、思いついたものは何でも)。それから彼は円を描き始め、それを「良い円」にするように私に頼みました(私の理解から:描いた円を完全に丸くします。円は実際には円のように丸められることはありません)。

だから私の質問は、ユーザーが描いた線を最初に検出して、画面上で完全に円形にすることで、ほぼ同じサイズの円を生成できるコードに方法があるということです。直線ではない直線を作る方法は知っていますが、円に関しては、クォーツや他の方法でそれを行う方法がわかりません。

私の考えでは、ユーザーが指を離した後、線の始点と終点が互いに接触または交差して、実際に円を描こうとしているという事実を正当化する必要があります。


2
このシナリオでは、円と多角形の違いを見分けるのは難しい場合があります。ユーザーがクリックして中心または外接する四角形の1つのコーナーを定義し、ドラッグして半径を変更するか、反対側のコーナーを設定する「円ツール」を使用してみませんか?
user1118321 2013

2
@ user1118321:これは、円を描き、完全な円を作ることができるという概念を打ち破ります。理想的には、アプリはユーザーの描画のみから、ユーザーが描いた円(多かれ少なかれ)、楕円、または多角形を認識する必要があります。(さらに、ポリゴンはこのアプリのスコープにはない可能性があります。それは単に円または線である可能性があります。)
Peter Hosey 2013

それで、あなたは私に賞金を与えるべきだと思いますか?良い候補者がたくさんいます。
Peter Hosey 2013

@Unheilig:トリガーについての初期の理解を超えて、この件に関する専門知識はありません。そうは言っても、私に最も可能性を示す答えは、stackoverflow.com / a/ 19071980 / 30461stackoverflow.com/ a / 19055873 / 30461stackoverflow.com / a / 18995771 / 30461、おそらくstackoverflow.com/a/です。 18992200/30461、そして私自身。それらは私が最初に試すものです。注文はお任せします。
Peter Hosey 2013

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)がゼロに等しい状況を適切に処理する必要があります。

ここに画像の説明を入力してください

更新

この小さな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
壮観な答えレナ。アプローチの明確な説明、プロセスを文書化した画像、アニメーションも。また、最も一般化された堅牢なソリューションのようです。接線は本当に賢いアイデアのように聞こえます-初期の(現在の?)手書き認識技術とよく似ています。この答えのためにブックマークされた質問。:)
enhzflep 2013年

27
より一般的には、簡潔で理解しやすい説明と図、そしてアニメーションのデモとコードとバリエーションです。これは理想的なスタックオーバーフローの回答です。
Peter Hosey 2013年

11
これは良い答えです。彼がJavaでコンピュータグラフィックスをやっているのはほぼ許せます。;)
Nicolas Miari、2016年

4
このクリスマス、サンタ・レナトには、意外なアップデート(つまり、より多くのシェイプなど)がありますか?:-)
Unheilig '12

1
ワオ。ツールドフォース。
ウォグスランド2017年

14

形状を検出するための古典的なコンピュータービジョン技術は、ハフ変換です。ハフ変換の優れた点の1つは、部分データ、不完全なデータ、ノイズに非常に耐性があることです。円にハフを使用する:http : //en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

あなたの円が手描きであることを考えると、ハフ変換はあなたにぴったりだと思います。

ここに「単純化された」説明があります。本当に単純ではないことをお詫びします。その多くは、私が何年も前に行った学校のプロジェクトからのものです。

ハフ変換は投票方式です。整数の2次元配列が割り当てられ、すべての要素がゼロに設定されます。各要素は、分析中の画像の1つのピクセルに対応します。この配列はアキュムレータ配列と呼ばれます。これは、各要素が情報や投票を蓄積し、ピクセルが円または円弧の原点にある可能性を示しているためです。

勾配演算子エッジ検出器が画像に適用され、エッジピクセルまたはエッジルが記録されます。Edgelは、隣接するピクセルとは異なる強度または色を持つピクセルです。差異の程度は、勾配の大きさと呼ばれます。十分な大きさの各エッジに、アキュムレータアレイの要素をインクリメントする投票方式が適用されます。増分される(投票される)要素は、考慮中のエッジルを通過する円の考えられる原点に対応します。望ましい結果は、アークが存在する場合、真の起源は偽の起源よりも多くの票を受け取るということです。

投票のためにアクセスされるアキュムレータ配列の要素は、検討中のエッジルの周りに円を形成することに注意してください。投票するX、Y座標の計算は、描画している円のX、Y座標の計算と同じです。

手描きの画像では、エッジルを計算するのではなく、セット(カラー)ピクセルを直接使用できる場合があります。

不完全に配置されたピクセルがあるため、投票数が最も多い単一のアキュムレータ配列要素を取得する必要はありません。多数の票、クラスターを持つ隣接する配列要素のコレクションを取得できます。このクラスターの重心は、原点を適切に近似している可能性があります。

半径Rの異なる値に対してハフ変換を実行する必要がある場合があることに注意してください。投票のより密集したクラスターを生成するものは、「より良い」適合です。

偽の起源に対する投票を減らすために使用するさまざまな手法があります。たとえば、エッジルを使用する利点の1つは、エッジがあるだけでなく、方向もあるということです。投票するときは、適切な方向で可能な起源について投票するだけです。投票を受けた場所は、完全な円ではなく円弧を形成します。

ここに例があります。半径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 touchesBegan、touchesMoved、touchesEndedを使用して、配列にポイントを追加します。配列を半分に分割し、1つの配列のすべての点が、他の配列のすべての点と、他の配列の対応する点の直径とほぼ同じかどうかをテストします。

    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秒程度に下げます。

(そして私が福音のために私の数字をとらないでください。私は帽子からそれらを引き出しただけだからです。実験してより良い値を見つけてください。)

次に、サンプルを分析します。

2つの事実を導き出す必要があります。最初に、形状の中心、つまり(IIRC)はすべてのポイントの平均である必要があります。次に、その中心からの各サンプルの平均半径。

@ user1118321が推測したように、ポリゴンをサポートする必要がある場合、残りの分析は、ユーザーが円を描くかポリゴンを描くかを決定することから成ります。サンプルを多角形として見て、その決定を始めることができます。

使用できる基準はいくつかあります。

  • 時間:ユーザーが他の場所よりも長い間ホバーする場合(サンプルが一定の間隔である場合、空間内で互いに近くに連続するサンプルのクラスターとして表示されます)、それらはコーナーになる可能性があります。コーナーのしきい値を小さくして、ユーザーが意図的に各コーナーで一時停止するのではなく、無意識にこれを実行できるようにする必要があります。
  • 角度:円は、あるサンプルから次のサンプルまで、ほぼ同じ角度になります。ポリゴンには、直線セグメントで結合されたいくつかの角度があります。角度は角です。正多角形(円から不規則な多角形の楕円まで)の場合、角の角度はすべてほぼ同じでなければなりません。不規則なポリゴンでは、コーナー角度が異なります。
  • 間隔:通常のポリゴンのコーナーは、角度寸法内で等間隔に配置され、半径は一定になります。不規則なポリゴンは、不規則な角度間隔や非一定の半径を持っています。

最後の3番目の手順は、以前に決定された中心点を中心とし、以前に決定された半径で形状を作成することです。

私が上で述べたすべてが機能するか効率的である保証はありませんが、少なくとも正しい軌道に乗ることができれば幸いです。そして、私よりも形状認識について知っている人(これは非常に低いバーです)が見た場合これ、コメントやあなた自身の答えを投稿してください。


+1こんにちは、入力いただきありがとうございます。非常に有益です。同様に、iOS /「形状認識」のスーパーマンがこの投稿をどうにか見て、私たちをさらに啓蒙してくれることを願っています。
Unheilig 2013

1
@Unheilig:いい考えです。できました。
Peter Hosey 2013

1
あなたのアルゴリズムはいいですね。ユーザーのパスが完全な円/多角形からどれだけ離れているかについてのチェックを追加します。(たとえば、パーセント平均二乗偏差。)大きすぎる場合、ユーザーは理想的な形状を望まない場合があります。熟練したDoodleの場合、カットオフはずさんなDoodleの場合よりも小さくなります。これがあれば、プログラムはアーティストに芸術的な自由を与えることができますが、初心者には多くの助けを与えます。
DMM

@ user2654818:どのように測定しますか?
Peter Hosey 2013

1
@PeterHosey:円の説明:理想的な円ができたら、中心と半径を取得します。したがって、描画されたすべての点を取り、中心からの二乗距離を計算します。これは((x-x0)^ 2 +(y-y0)^ 2)です。二乗した半径からそれを差し引きます。(私は計算を節約するために多くの平方根を避けています。)描画された点の二乗誤差を呼び出します。描画されたすべてのポイントの二乗誤差を平均し、それを平方根し、それを半径で割ります。それはあなたの平均パーセントの相違です。(数学/統計は恐ろしい価値がありますが、実際には機能します。)
dmm


2

ユーザーが始点で形状の描画を終了したと判断したら、ユーザーが描いた座標のサンプルを取り、それらを円にフィットさせることができます。

この問題に対するMATLABソリューションがここにあります。 http //www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

これは、Walter Gander、Gene H. Golub、Rolf Strebelによる紙の最小二乗フィッティングの円と楕円に基づいています。 。http

ニュージーランドのカンタベリー大学のIan Coope博士が、次の要約を含む論文を発表しました。

平面内の点のセットに最適な円を決定する問題(またはn次元への明らかな一般化)は、ガウスニュートン最小化アルゴリズムを使用して解決できる非線形最小二乗問題として簡単に定式化されます。この直接的なアプローチは、非効率であり、外れ値の存在に非常に敏感であることが示されています。代替の定式化により、問題を簡単に解決できる線形最小二乗問題に減らすことができます。推奨されるアプローチには、非線形最小二乗アプローチよりも外れ値に対する感度がはるかに低いという追加の利点があることが示されています。

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

いくつかのUIViewを「X」の場所に配置し、ヒットするかどうかを(順番に)テストします。それらすべてが順番にヒットした場合、ユーザーに「円を描きました」と言ってもよいと思います。

大丈夫ですか?(そしてシンプル)


こんにちは、レモン。適切な推論ですが、上記のシナリオでは、タッチを検出するために64個のUIViewが必要になることを意味しますよね?たとえば、キャンバスがiPadのサイズである場合、1つのUIViewのサイズをどのように定義しますか?円が小さく、単一のUIViewのサイズが大きい場合、この場合、描画されたすべてのポイントが単一のUIView内にあるため、シーケンスを確認できなかったようです。
Unheilig 2013

うん-これはおそらく、キャンバスを300x300のようなものに固定し、その隣に「サンプル」のキャンバスがあり、ユーザーが描く円のサイズを指定している場合にのみ機能します。その場合、50x50の正方形* 6を使用します。6* 6(36)または8 * 8(64)のすべてではなく、正しい場所でヒットすることに関心のあるビューのみをレンダリングする必要があります
dijipiji

@Unheilig:これがこのソリューションの機能です。正しいビューのシーケンスを通過するのに十分なほど円形である場合(そして、余分なスロップに対して最大数の迂回路を許可する可能性がある場合)は、円として一致します。次に、それらすべてのビューの中心に中心を置く完全な円に半径をスナップします。その半径はビューのすべて(または少なくともほとんど)に達します。
Peter Hosey 2013

@PeterHoseyわかりました、これについて頭を動かしましょう。これを実現するためのコードを提供していただければ幸いです。その間、私もこれについて頭を回そうとします、そしてその後、私はコーディング部分で同じことをします。ありがとう。
Unheilig 2013

私はあなたのためにもっと良いかもしれないと思う別の方法を提出しました
dijipiji
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.