J'ai ce projet de pratique qui permet à l'utilisateur de dessiner sur l'écran lorsqu'il touche avec ses doigts. App très simple que j'ai fait comme exercice. Mon petit cousin a pris la liberté de dessiner des choses avec son doigt avec mon iPad sur cette appli (dessins d'enfants: cercle, lignes, etc., tout ce qui lui venait à l'esprit). Puis il a commencé à dessiner des cercles, puis il m'a demandé de le faire "bon cercle" (d'après ma compréhension: faire le cercle dessiné parfaitement rond, comme nous savons quelle que soit la stabilité, nous essayons de dessiner quelque chose avec notre doigt sur l'écran, un cercle n'est jamais vraiment aussi arrondi qu'un cercle devrait l'être).
Donc ma question ici est que, existe-t-il un moyen dans le code où nous pouvons d'abord détecter une ligne dessinée par l'utilisateur qui forme un cercle et générer environ la même taille du cercle en le rendant parfaitement rond à l'écran. Faire une ligne droite pas si droite est quelque chose que je saurais faire, mais en ce qui concerne le cercle, je ne sais pas trop comment le faire avec du Quartz ou d'autres méthodes.
Mon raisonnement est que le début et la fin de la ligne doivent se toucher ou se croiser après que l'utilisateur a levé le doigt pour justifier le fait qu'il essayait de dessiner un cercle.
Parfois, il est vraiment utile de passer un peu de temps à réinventer la roue. Comme vous l'avez peut-être déjà remarqué, il existe de nombreux frameworks, mais il n'est pas si difficile d'implémenter une solution simple mais néanmoins utile sans introduire toute cette complexité. (S'il vous plaît, ne vous méprenez pas, il est préférable d'utiliser un cadre mature et éprouvé pour un usage sérieux).
Je vais d'abord présenter mes résultats, puis expliquer l'idée simple et directe qui les sous-tend.
Vous verrez dans mon implémentation qu'il n'est pas nécessaire d'analyser chaque point et de faire des calculs complexes. L'idée est de repérer des méta-informations précieuses. Je vais utiliser tangent comme exemple:
Identifions un motif simple et direct, typique de la forme sélectionnée:
Il n'est donc pas si difficile de mettre en œuvre un mécanisme de détection de cercle basé sur cette idée. Voir la démo de travail ci-dessous (Désolé, j'utilise Java comme le moyen le plus rapide de fournir cet exemple rapide et un peu sale):
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);
}
});
}
}
La mise en œuvre d'un comportement similaire sur iOS ne devrait pas poser de problème, car vous n'avez besoin que de plusieurs événements et coordonnées. Quelque chose comme ceci (voir exemple ):
- (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];
}
Plusieurs améliorations sont possibles.
Commencez à tout moment
La condition actuelle consiste à commencer à dessiner un cercle à partir du point milieu supérieur en raison de la simplification suivante:
if(type == null || type != newType) {
if(newType != shape[index]) {
break;
}
bounds[index] = current;
detected[index++] = newType;
}
Veuillez noter que la valeur par défaut de index
est utilisée. Une simple recherche parmi les "parties" disponibles de la forme supprimera cette limitation. Veuillez noter que vous devrez utiliser un tampon circulaire afin de détecter une forme complète:
Dans le sens horaire et antihoraire
Afin de prendre en charge les deux modes, vous devrez utiliser le tampon circulaire de l'amélioration précédente et rechercher dans les deux directions:
Dessinez une ellipse
Vous avez déjà tout ce dont vous avez besoin dans le tableau bounds
.
Utilisez simplement ces données:
cWidth = bounds[2].y - bounds[0].y;
cHeight = bounds[3].y - bounds[1].y;
Autres gestes (facultatif)
Enfin, il vous suffit de gérer correctement une situation où dx
(ou dy
) est égal à zéro afin de prendre en charge d'autres gestes:
Mise à jour
Ce petit PoC a attiré une grande attention, j'ai donc mis à jour un peu le code afin de le faire fonctionner correctement et de fournir des conseils de dessin, de mettre en évidence les points de support, etc.:
Voici le code:
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);
}
Une technique de vision par ordinateur classique pour détecter une forme est la transformation de Hough. Une des bonnes choses à propos de la transformation de Hough est qu'elle est très tolérante aux données partielles, aux données imparfaites et au bruit. Utiliser Hough pour un cercle: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process
Étant donné que votre cercle est dessiné à la main, je pense que la transformation de Hough peut vous convenir.
Voici une explication "simplifiée", je m'excuse car ce n'est pas vraiment si simple. Une grande partie provient d'un projet scolaire que j'ai réalisé il y a de nombreuses années.
La transformation de Hough est un système de vote. Un tableau bidimensionnel d'entiers est alloué et tous les éléments sont mis à zéro. Chaque élément correspond à un seul pixel de l'image analysée. Ce tableau est appelé le tableau accumulateur puisque chaque élément accumulera des informations, des votes, indiquant la possibilité qu'un pixel puisse être à l'origine d'un cercle ou d'un arc.
Un détecteur de bord d'opérateur de gradient est appliqué à l'image et les pixels de bord, ou bords, sont enregistrés. Un edgel est un pixel qui a une intensité ou une couleur différente par rapport à ses voisins. Le degré de différence est appelé amplitude du gradient. Pour chaque edgel de magnitude suffisante, un schéma de vote est appliqué qui incrémentera les éléments du réseau d'accumulateurs. Les éléments incrémentés (votés) correspondent aux origines possibles des cercles qui traversent l'édgel considéré. Le résultat souhaité est que si un arc existe, la véritable origine recevra plus de votes que les fausses origines.
Notez que les éléments du réseau d'accumulateurs visités pour voter forment un cercle autour de l'edgel considéré. Le calcul des coordonnées x, y pour lesquelles voter est le même que le calcul des coordonnées x, y d'un cercle que vous dessinez.
Dans votre image dessinée à la main, vous pourrez peut-être utiliser directement les pixels définis (colorés) plutôt que de calculer les bords.
Maintenant, avec des pixels mal situés, vous n'obtiendrez pas nécessairement un seul élément de tableau d'accumulateurs avec le plus grand nombre de votes. Vous pouvez obtenir une collection d'éléments de tableau voisins avec un tas de votes, un cluster. Le centre de gravité de cet amas peut offrir une bonne approximation pour l'origine.
Notez que vous devrez peut-être exécuter la transformation de Hough pour différentes valeurs de rayon R. Celui qui produit le groupe de votes le plus dense est le "meilleur" ajustement.
Il existe différentes techniques à utiliser pour réduire les votes pour les fausses origines. Par exemple, l'un des avantages de l'utilisation des bords est qu'ils ont non seulement une amplitude mais aussi une direction. Lors du vote, nous devons seulement voter pour les origines possibles dans la direction appropriée. Les emplacements recevant des votes formeraient un arc plutôt qu'un cercle complet.
Voici un exemple. Nous commençons par un cercle de rayon un et un réseau d'accumulateurs initialisé. Comme chaque pixel est considéré, les origines potentielles sont votées. La véritable origine reçoit le plus de voix, qui dans ce cas est de quatre.
. 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
Voici une autre façon. Utilisation de UIView touchesBegan, touchesMoved, touchesEnded et ajout de points à un tableau. Vous divisez le tableau en deux et testez si chaque point d'un tableau a à peu près le même diamètre que son homologue dans l'autre tableau que toutes les autres paires.
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);
}
Ça sonne bien? :)
Je ne suis pas un expert en reconnaissance de forme, mais voici comment je pourrais aborder le problème.
Tout d'abord, tout en affichant le chemin de l'utilisateur à main levée, accumulez secrètement une liste d'échantillons ponctuels (x, y) ainsi que des temps. Vous pouvez obtenir les deux faits à partir de vos événements de glissement, les envelopper dans un objet modèle simple et les empiler dans un tableau modifiable.
Vous voudrez probablement prélever des échantillons assez fréquemment, disons toutes les 0,1 secondes. Une autre possibilité serait de commencer vraiment fréquemment, peut-être toutes les 0,05 secondes, et de regarder combien de temps l'utilisateur traîne; s'ils traînent plus longtemps qu'un certain temps, réduisez la fréquence d'échantillonnage (et déposez tous les échantillons qui auraient été manqués) à quelque chose comme 0,2 seconde.
(Et ne prenez pas mes chiffres pour l'évangile, parce que je les ai juste sortis de mon chapeau. Expérimentez et trouvez de meilleures valeurs.)
Deuxièmement, analysez les échantillons.
Vous voudrez dériver deux faits. Tout d'abord, le centre de la forme, qui (IIRC) devrait juste être la moyenne de tous les points. Deuxièmement, le rayon moyen de chaque échantillon de ce centre.
Si, comme @ user1118321 l'a deviné, vous voulez prendre en charge les polygones, le reste de l'analyse consiste à prendre cette décision: si l'utilisateur veut dessiner un cercle ou un polygone. Vous pouvez regarder les échantillons comme un polygone pour commencer pour faire cette détermination.
Vous pouvez utiliser plusieurs critères:
La troisième et dernière étape consiste à créer la forme, centrée sur le point central précédemment déterminé, avec le rayon précédemment déterminé.
Aucune garantie que tout ce que j'ai dit ci-dessus fonctionnera ou sera efficace, mais j'espère que cela vous mettra au moins sur la bonne voie - et s'il vous plaît, si quelqu'un qui en sait plus sur la reconnaissance de forme que moi (qui est une barre très basse) voit cela, n'hésitez pas à poster un commentaire ou votre propre réponse.
J'ai eu de la chance avec un logiciel de reconnaissance de $ 1 correctement formé ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Je l'ai utilisé pour les cercles, les lignes, les triangles et les carrés.
C'était il y a longtemps, avant UIGestureRecognizer, mais je pense qu'il devrait être facile de créer des sous-classes UIGestureRecognizer appropriées.
Une fois que vous avez déterminé que l'utilisateur a fini de dessiner sa forme là où il a commencé, vous pouvez prendre un échantillon des coordonnées qu'il a dessinées et essayer de les ajuster à un cercle.
Il existe une solution MATLAB à ce problème ici: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m
Qui est basé sur l'article Ajustement des moindres carrés des cercles et des ellipses de Walter Gander, Gene H. Golub et Rolf Strebel: http: //www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf
Le Dr Ian Coope de l'Université de Canterbury, NZ, a publié un article avec le résumé:
Le problème de la détermination du cercle le mieux ajusté à un ensemble de points dans le plan (ou la généralisation évidente à n dimensions) est facilement formulé comme un problème de moindres carrés totaux non linéaire qui peut être résolu en utilisant un algorithme de minimisation de Gauss-Newton. Cette approche directe se révèle inefficace et extrêmement sensible à la présence de valeurs aberrantes. Une formulation alternative permet de réduire le problème à un problème des moindres carrés linéaires qui est trivialement résolu. L'approche recommandée présente l'avantage supplémentaire d'être beaucoup moins sensible aux valeurs aberrantes que l'approche des moindres carrés non linéaires.
http://link.springer.com/article/10.1007%2FBF0093961
Le fichier MATLAB peut calculer à la fois le problème TLS non linéaire et le problème LLS linéaire.
Voici une manière assez simple d'utiliser:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
en supposant cette grille matricielle:
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
Placez quelques UIViews sur les emplacements "X" et testez-les pour devenir frappé (en séquence). S'ils sont tous touchés en séquence, je pense qu'il serait juste de laisser l'utilisateur dire "Bravo, vous avez dessiné un cercle"
Ça vous va? (et simple)