Нарисуйте идеальный круг от прикосновения пользователя
У меня есть этот практический проект, который позволяет пользователю рисовать на экране, когда они касаются пальцами. Очень простое приложение, которое я сделал, как осуществить обратный путь. Мой маленький двоюродный брат взял на себя смелость рисовать вещи пальцем с моим iPad в этом приложении (детские рисунки: круг, линии и т. д., Все, что пришло ему на ум). Затем он начал рисовать круги, а затем попросил меня сделать это "хороший круг" (из моего понимания: сделайте нарисованный круг идеально круглым, как мы знаем независимо от того, насколько стабильны мы попробуйте нарисовать что-то пальцем на экране, круг никогда не бывает таким закругленным, как должен быть круг).
Итак, мой вопрос здесь заключается в том, есть ли способ в коде, где мы можем сначала обнаружить линию, нарисованную пользователем, которая образует круг и генерирует примерно такой же размер круга, делая его идеально круглым на экране. Создание не такой прямой прямой - это то, что я знаю, как это сделать, но что касается круга, я не совсем знаю, как это сделать с кварцем или другими методами.
мое рассуждение заключается в том, что начало и конечная точка линии должны касаться или пересекать друг друга после того, как пользователь поднимает палец, чтобы оправдать тот факт, что он пытался фактически нарисовать круг.
7 ответов:
иногда действительно полезно потратить некоторое время на изобретение колеса. Как вы, возможно, уже заметили, есть много фреймворков, но это не так сложно реализовать простое, но все же полезное решение без введения всей этой сложности. (Пожалуйста, не поймите меня неправильно, для любой серьезной цели лучше использовать некоторые зрелые и проверенные стабильные рамки).
сначала я представлю свои результаты, а затем объясню простую и понятную идею, стоящую за ними.
вы увидите, что в моей реализации нет необходимости анализировать каждую отдельную точку и делать сложные вычисления. Идея состоит в том, чтобы определить некоторую ценную метаинформацию. Я буду использовать касательной пример:
давайте определим простой и понятный шаблон, типичный для выбранной формы:
так что это не так сложно реализовать обнаружение круга механизм, основанный на этой идее. См. рабочую демонстрацию ниже (извините, я использую 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; }
Пожалуйста, обратите внимание на значение по умолчанию есть. Простой поиск по доступным "частям" фигуры снимет это ограничение. Обратите внимание, что вам нужно будет использовать круговой буфер для обнаружения полной формы:
по часовой стрелке и против часовой стрелки
In чтобы поддерживать оба режима, вам нужно будет использовать круговой буфер из предыдущего улучшения и искать в обоих направлениях:
нарисовать эллипс
у вас есть все необходимое в
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); }
классический метод компьютерного зрения для обнаружения формы является преобразование Хафа. Одна из приятных вещей о преобразовании Hough заключается в том, что он очень терпим к частичным данным, несовершенным данным и шуму. Используя Хафа для круга: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process
учитывая, что ваш круг нарисован вручную, я думаю, что преобразование Хафа может быть хорошим совпадением для вас.
вот "упрощенное" объяснение, прошу прощения ибо на самом деле все не так просто. Большая часть этого из школьного проекта, который я сделал много лет назад.
преобразование Хафа-это схема голосования. Выделяется двумерный массив целых чисел, и все элементы равны нулю. Каждый элемент соответствует одному пикселю в анализируемом изображении. Этот массив называется накопительным массивом, так как каждый элемент будет накапливать информацию, голоса, указывающие на возможность того, что пиксель может находиться в начале окружности или дуга.
к изображению применяется детектор краев градиентного оператора, и записываются краевые пиксели или ребра. В edgel-это пиксель, который имеет разную интенсивность или цвет по отношению к его соседям. Степень различия называется величиной градиента. Для каждого edgel достаточной величины, схема голосования применяется, что позволит прирастить элементов аккумулятора массива. Элементы, которые увеличиваются (голосовали за), соответствуют возможным истокам кругов, которые проходят через рассматриваемый эджель. Желаемый результат заключается в том, что если дуга существует, то истинное происхождение получит больше голосов, чем ложное происхождение.
обратите внимание, что элементы аккумулятора массива побывал на голосование круг вокруг edgel на рассмотрении. Вычисление координат x, y для голосования совпадает с вычислением координат x,y круга, который вы рисуете.
в вашем рисованном изображении вы можете использовать набор (цветной) пиксели напрямую, а не вычислять Эджели.
теперь с несовершенно расположенными пикселями вы не обязательно получите один элемент массива аккумулятора с наибольшим количеством голосов. Вы можете получить коллекцию соседних элементов массива с кучей голосов, кластер. Центр тяжести этого кластера может обеспечить хорошее приближение к источнику.
обратите внимание, что вам может потребоваться выполнить преобразование Хафа для разных значений радиуса R. тот, который производит более плотный кластер голосов-это "лучше" подходит.
существуют различные методы, которые можно использовать для уменьшения голосов за ложное происхождение. Например, одним из преимуществ использования edgels является то, что они не только имеют величину, но и направление. При голосовании нам нужно только проголосовать за возможные истоки в соответствующем направлении. Места, получающие голоса, будут образовывать дугу, а не полный круг.
вот пример. Мы начнем с окружности единичного радиуса и аккумулятор инициализации массива. Поскольку каждый пиксель считается потенциальным источником, за него голосуют. Истинное происхождение получает наибольшее количество голосов, которое в данном случае составляет четыре.
. 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 секунды.
(и не принимайте мои номера за Евангелие, потому что я просто вытащил их из своей шляпы. Экспериментируйте и находите лучшие значения.)
во-вторых, проанализировать образцы.
вы хотите получить два факта. Во-первых, центр фигуры, который (IIRC) должен быть просто средним из всех точек. Во-вторых, средний радиус каждого образца от этого центра.
Если, как предположил @user1118321, вы хотите поддерживать полигоны, то остальная часть анализа состоит в принятии этого решения: хочет ли пользователь нарисовать круг или полигон. Вы можете посмотреть на образцы как на полигон, чтобы начать с этого определения.
есть несколько критериев, вы можете использование:
- время: если пользователь парит дольше в некоторых точках, чем в других (которые, если образцы находятся на постоянном интервале, будут отображаться как кластер последовательных образцов рядом друг с другом в пространстве), это могут быть углы. Вы должны сделать свой угловой порог маленьким, чтобы пользователь мог сделать это бессознательно, а не намеренно останавливаться на каждом углу.
- угол: круг будет иметь примерно такой же угол от одного образца к следующему полностью вокруг. Многоугольник будет иметь несколько углов, Соединенных отрезками прямой линии; углы-это углы. Для правильного многоугольника (окружность к эллипсу неправильного многоугольника) угловые углы должны быть примерно одинаковыми; неправильный многоугольник будет иметь разные угловые углы.
- интервал: углы правильного многоугольника будут равны расстоянию друг от друга в пределах углового размера, а радиус будет постоянным. Неправильного многоугольника будет иметь нерегулярные угловые интервалы и/или не-постоянного радиуса.
третий и последний шаг заключается в создании фигуры, центрированной на ранее определенной центральной точке, с ранее определенным радиусом.
нет гарантий, что все, что я сказал выше, будет работать или быть эффективным, но я надеюсь, что это, по крайней мере, поможет вам на правильном пути-и, пожалуйста, если кто-то, кто знает больше о распознавании формы, чем я (что является очень низкой планкой), видит это, не стесняйтесь оставлять комментарий или свой собственный ответ.
Мне очень повезло с правильно обученным распознавателем $1 (http://depts.washington.edu/aimgroup/proj/dollar/). я использовал его для кругов, линий, треугольников и квадратов.
Это было давно, до UIGestureRecognizer, но я думаю, что должно быть легко создать правильные подклассы UIGestureRecognizer.
после того, как вы определили, что пользователь закончил рисовать свою форму там, где они начали, вы можете взять образец координат, которые они нарисовали, и попытаться установить их в круг.
здесь есть решение MATLAB для этой проблемы: http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m
который основан на бумаге наименьшие квадраты подгонки кругов и эллипсов Вальтер Гандер, Джин Х. Голуб и Рольф Штребель: http://www.emis.de/journals/BBMS/Bulletin/sup962/gander.pdf
доктор Ян КУП из Университета Кентербери, Новая Зеландия опубликовал статью с резюме:
задача определения круга наилучшего соответствия множеству точек в плоскости (или очевидное обобщение на n-размеры) легко сформулирована как нелинейная общая задача наименьших квадратов, которая может быть решается с использованием алгоритма минимизации Гаусса-Ньютона. Этот показано, что прямой подход является неэффективным и крайне неэффективным. чувствителен к присутствию выбросов. Альтернативная формулировка позволяет свести задачу к линейной задаче наименьших квадратов что тривиально решается. Рекомендуется показано дополнительное преимущество заключается в том, что он гораздо менее чувствителен к выбросам, чем нелинейные наименьшие квадраты подход.
http://link.springer.com/article/10.1007%2FBF00939613
файл 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
поместите некоторые UIViews на" X " местах и проверить их на попадание ( в последовательности ) . Если они все попадут в последовательность, я думаю, что было бы справедливо позволить пользователю сказать: "Хорошо, что вы нарисовали круг"
звучит хорошо? (и просто )