この練習プロジェクトでは、ユーザーが指で触れたときに画面に描画できるようにします。非常にシンプルなアプリです。私の小さないとこは、このアプリで私のiPadで指で物事を描く自由を取りました(子供の描画:円、線など、彼の心に浮かんだものは何でも)。それから彼は円を描き始め、それから「良い円」にするように頼まれました(私の理解から:描かれた円を完全に丸くしてください。円のように丸くなることはありません)。
だからここで私の質問は、円を形成するユーザーによって描かれた線を最初に検出し、画面上で完全に丸くすることでほぼ同じサイズの円を生成できる方法がありますか?それほど直線ではない直線を作ることは私が行う方法を知っていることですが、円に関しては、Quartzまたは他の方法でそれを行う方法をまったく知りません。
私の推論では、ユーザーが実際に円を描いているという事実を正当化するために、ユーザーが指を離した後、線の始点と終点が互いに接触または交差する必要があります。
車輪の再発明に時間を費やすことが本当に役立つ場合があります。既にお気づきかもしれませんが、多くのフレームワークがありますが、その複雑さをすべて導入することなく、シンプルでありながら有用なソリューションを実装することはそれほど難しくありません。 (誤解しないでください。深刻な目的のためには、安定したフレームワークであることが証明されている成熟したものを使用する方が良いでしょう)。
最初に結果を示し、次にそれらの背後にある単純でわかりやすいアイデアを説明します。
私の実装では、すべての単一ポイントを分析して複雑な計算を行う必要がないことがわかります。アイデアは、いくつかの貴重なメタ情報を見つけることです。例として 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
)がゼロに等しい状況を適切に処理する必要があります。
更新
この小さな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);
}
形状を検出するための古典的なコンピュータービジョン手法は、ハフ変換です。ハフ変換の素晴らしい点の1つは、部分データ、不完全なデータ、ノイズに対して非常に寛容であることです。円にハフを使用: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process
あなたの円が手書きであることを考えると、ハフ変換はあなたにぴったりだと思います。
ここに「単純化された」説明がありますが、本当にそれほど単純ではないことをおaびします。その多くは、私が何年も前に行った学校プロジェクトからのものです。
ハフ変換は投票方式です。整数の2次元配列が割り当てられ、すべての要素がゼロに設定されます。各要素は、分析中の画像の単一ピクセルに対応します。この配列はアキュムレータ配列と呼ばれます。各要素は、ピクセルが円または円弧の原点にある可能性を示す情報、投票を蓄積するためです。
勾配演算子のエッジ検出器が画像に適用され、エッジピクセル、またはエッジが記録されます。 edgelは、隣接するピクセルとは異なる強度または色を持つピクセルです。差異の程度は、勾配の大きさと呼ばれます。十分な大きさのエッジごとに、アキュムレータ配列の要素をインクリメントする投票スキームが適用されます。インクリメント(投票)される要素は、検討中のエッジを通過する円の可能な原点に対応します。望ましい結果は、アークが存在する場合、真のオリジンが偽のオリジンよりも多くの票を獲得することです。
投票のために訪問されるアキュムレータ配列の要素は、検討中のエッジを囲む円を形成することに注意してください。投票するX、Y座標の計算は、描画している円のX、Y座標の計算と同じです。
手描きの画像では、edgelsを計算するのではなく、セット(色付き)ピクセルを直接使用できる場合があります。
不完全に配置されたピクセルでは、最大の投票数を持つ単一のアキュムレータ配列要素を必ずしも取得できません。多数の票を集めたクラスターである、隣接する配列要素のコレクションを取得できます。このクラスターの重心は、原点の適切な近似値を提供する場合があります。
半径Rのさまざまな値に対してハフ変換を実行する必要がある場合があることに注意してください。より密集した投票クラスタを生成するものは、「より良い」適合です。
偽の発信元に対する投票を減らすために使用するさまざまな手法があります。たとえば、edgelsを使用する利点の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
別の方法があります。 UIView touchesBegan、touchesMoved、touchesEndedを使用して、配列にポイントを追加します。配列を半分に分割し、ある配列のすべての点が、他の配列の対応する部分と他のすべてのペアとほぼ同じ直径であるかどうかをテストします。
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);
}
大丈夫? :)
私は形状認識の専門家ではありませんが、ここで問題に取り組む方法を示します。
最初に、ユーザーのパスをフリーハンドとして表示しながら、時間とともにポイント(x、y)サンプルのリストを密かに蓄積します。ドラッグイベントから両方のファクトを取得し、それらを単純なモデルオブジェクトにラップし、それらを可変配列に積み上げることができます。
サンプルをかなり頻繁に、たとえば0.1秒ごとに取得することをお勧めします。別の可能性としては、実際に頻繁に、おそらく0.05秒ごとに開始し、ユーザーがドラッグする時間を監視することです。一定の時間よりも長くドラッグする場合、サンプル頻度を0.2秒程度に下げます(見逃したサンプルはすべてドロップします)。
(そして、福音のために数字を受け取らないでください。帽子から数字を取り出しただけです。実験して、より良い価値を見つけてください。)
次に、サンプルを分析します。
2つの事実を導き出します。まず、形状の中心。これは(IIRC)すべてのポイントの平均である必要があります。第二に、その中心からの各サンプルの平均半径。
@ user1118321が推測したように、ポリゴンをサポートする場合、残りの分析は、ユーザーが円を描くかポリゴンを描くかを決定することで構成されます。その決定を行うために、サンプルを多角形として見ることができます。
使用できる基準はいくつかあります。
3番目の最後のステップは、以前に決定した半径で、以前に決定した中心点を中心とした形状を作成することです。
上で述べたことがすべて機能するか、効率的であるという保証はありませんが、少なくともあなたが正しい軌道に乗れることを望みます。そして、私よりも形状認識について詳しく知っている人(非常に低いバー)が見ればこれ、コメントやあなた自身の答えを投稿してください。
適切に訓練された1ドルのレコグナイザーでかなり幸運でした( http://depts.washington.edu/aimgroup/proj/dollar/ )。円、線、三角形、正方形に使用しました。
UIGestureRecognizerよりもずっと前のことでしたが、適切なUIGestureRecognizerサブクラスを作成するのは簡単だと思います。
ユーザーが開始位置から図形の描画を完了したと判断したら、ユーザーが描いた座標のサンプルを取得して、円にフィットさせてみます。
この問題に対する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
NZカンタベリー大学のIan Coope博士は、アブストラクト付きの論文を発表しました。
平面内の点のセットに最適な円を決定する問題(またはn次元への明らかな一般化)は、ガウス・ニュートン最小化アルゴリズムを使用して解決できる非線形総最小二乗問題として簡単に定式化できます。この単純なアプローチは、非効率的であり、外れ値の存在に非常に敏感であることが示されています。別の定式化により、問題を線形最小二乗問題に減らすことができ、これは簡単に解決できます。推奨されるアプローチには、非線形最小二乗アプローチよりも外れ値に対する感度がはるかに低いという利点があることが示されています。
http://link.springer.com/article/10.1007%2FBF0093961
MATLABファイルは、非線形TLSと線形LLS問題の両方を計算できます。
以下は使用するかなり簡単な方法です:
- (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を配置し、ヒットするかどうかをテストします(順番に)。それらがすべて順番にヒットした場合、ユーザーに「よくやった、円を描いた」と言うのは公平だと思う
大丈夫? (そしてシンプル)