Намалюйте ідеальне коло від дотику користувача


176

У мене є такий практичний проект, який дозволяє користувачеві малювати на екрані, коли він торкається пальцями. Дуже просте додаток я зробив як вправу назад. Мій двоюрідний двоюрідний брат брав на себе сміття малювати речі пальцем за допомогою мого iPad у цьому додатку (Дитячі малюнки: коло, лінії тощо), що б йому не спадало на думку). Потім він почав малювати кола, а потім попросив мене зробити це «хорошим колом» (з мого розуміння: зробити намальоване коло ідеально круглим, оскільки ми знаємо, незалежно від того, наскільки стійкими ми намагаємось намалювати щось пальцем на екрані, коло ніколи насправді не таке округлене, як має бути коло).

Отже, моє запитання полягає в тому, чи є в коді спосіб, де ми можемо спочатку виявити лінію, намальовану користувачем, яка формує коло, і генерувати приблизно однаковий розмір кола, зробивши його ідеально круглим на екрані. Зробити не настільки пряму пряму - це те, що я б знав, як це зробити, але що стосується кола, я не знаю, як це зробити за допомогою кварцу чи інших методів.

Моє міркування полягає в тому, що початок і кінцева точка рядка повинні торкатися або перетинати один одного після того, як користувач підніме палець, щоб виправдати факт, що він намагався насправді намалювати коло.


2
Бути важко сказати різницю між колом та багатокутником у цьому сценарії. Як щодо того, щоб мати "Інструмент кола", де користувач клацає, щоб визначити центр, або один кут обмежувального прямокутника, і тягне, щоб змінити радіус або встановити протилежний кут?
користувач1118321

2
@ user1118321: Це перемагає концепцію просто вміти малювати коло та мати ідеальне коло. В ідеалі додаток повинен розпізнавати лише на малюнку користувача, чи намалював користувач коло (більше чи менше), еліпс чи багатокутник. (Крім того, для цього додатка можуть не бути багатокутники - це можуть бути просто кола або лінії.)
Пітер Хосей,

Отже, на яку відповідь, на вашу думку, я повинен дати винагороду? Я бачу багато хороших кандидатів.
Пітер Хосей

@Unheilig: Я не маю ніяких знань з цього питання, окрім як народжуваного розуміння тригма. Однак, відповіді, які представляють для мене найбільший потенціал, - stackoverflow.com/a/19071980/30461 , stackoverflow.com/a/19055873/30461 , stackoverflow.com/a/18995771/30461 , можливо, stackoverflow.com/a/ 18992200/30461 , і моя власна. Це я спробую спочатку. Я залишаю вам замовлення.
Пітер Хосей

1
@Gene: Можливо, ви зможете узагальнити відповідну інформацію та посилання на більш детальну інформацію у відповідь.
Пітер Хосей

Відповіді:


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

27
Більш загально: стисле, зрозуміле пояснення ТА діаграми та анімоване демонстраційне повідомлення І код І варіації? Це ідеальна відповідь на переповнення стека.
Пітер Хосей

11
Це така гарна відповідь, я майже можу пробачити, що він робить комп’ютерну графіку на Java! ;)
Ніколас Міарі

4
Чи буде більше дивовижних оновлень (тобто, більше фігур тощо) на це Різдво, Санта-Ренат? :-)
Unheilig

1
Ого. Тур де сила.
wogsland

14

Класична техніка комп’ютерного бачення для виявлення фігури - трансформація Хоф. Одна з приємних речей щодо Hough Transform - це те, що вона дуже терпима до часткових даних, недосконалих даних та шуму. Використання Hough для кола: http://en.wikipedia.org/wiki/Hough_transform#Circle_detection_process

Зважаючи на те, що ваше коло намальовано рукою, я думаю, що трансформація Хауфа може бути для вас хорошим.

Ось "спрощене" пояснення, я вибачаюся за те, що насправді це не так просто. Багато чого з шкільного проекту, який я робив багато років тому.

Трансформація Хауфа - це схема голосування. Виділяється двомірний масив цілих чисел і всі елементи встановлюються в нуль. Кожному елементу відповідає один піксель у аналізованому зображенні. Цей масив називається масивом акумулятора, оскільки кожен елемент буде накопичувати інформацію, голоси, що вказує на можливість того, що піксель може бути у початку кола або дуги.

Градієнтний детектор краю градієнта застосовується до зображення, а пікселі ребер або ребра записуються. Енгель - це піксель, який має різну інтенсивність або колір відносно сусідів. Ступінь різниці називається величиною градієнта. Для кожного крана достатньої величини застосовується схема голосування, що збільшуватиме елементи акумуляторного масиву. Елементи, що збільшуються (проголосували за), відповідають можливим джерелам кіл, які проходять через розглянутий край. Бажаний результат полягає в тому, що якщо дуга існує, то справжнє походження отримає більше голосів, ніж помилкове походження.

Зауважте, що елементи масиву акумулятора, який відвідується для голосування, утворюють коло навколо розглянутого крана. Обчислення координат x, y для голосування - те саме, що обчислення координат x, y кола, яке ви малюєте.

У намальованому вами зображенні ви можете використовувати прямі (кольорові) пікселі безпосередньо, а не обчислювати ребра.

Тепер із недосконало розташованими пікселями ви не обов’язково отримаєте один елемент масиву акумулятора з найбільшою кількістю голосів. Ви можете отримати колекцію сусідніх елементів масиву з купою голосів, кластером. Центр ваги цього скупчення може запропонувати гарне наближення до походження.

Зауважте, що, можливо, вам доведеться запустити трансформацію Хоф для різних значень радіуса R. Той, що створює щільніший кластер голосів, є "кращим" пристосуванням.

Існують різні методики, щоб зменшити кількість голосів за помилкове походження. Наприклад, однією з переваг використання ребер є те, що вони мають не тільки величину, але й напрямок. Голосуючи, нам потрібно голосувати лише за можливі джерела у відповідному напрямку. Місця, що отримують голоси, формували б дугу, а не повне коло.

Ось приклад. Почнемо з кола радіуса один і ініціалізованого масиву акумулятора. Оскільки за кожен піксель вважається потенційне джерело, проголосується за. Справжнє походження отримує найбільше голосів, у цьому випадку - чотири.

.  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, touvesMoved, touchEnded та додавання точок до масиву. Ви ділите масив на половинки і перевіряєте, чи кожна точка одного масиву має приблизно такий же діаметр від його аналога в іншому масиві, як і всі інші пари.

    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

Я не фахівець з розпізнавання форми, але ось як я можу підійти до проблеми.

По-перше, показуючи шлях користувача як вільний від руки, таємно накопичуйте список точкових (х, у) зразків разом із часом. Ви можете отримати обидва факти з подій перетягування, обернути їх у простий об’єкт моделі та скласти їх у змінний масив.

Напевно, ви хочете брати зразки досить часто - скажімо, кожні 0,1 секунди. Ще одна можливість - почати дійсно часто, можливо кожні 0,05 секунди, і спостерігати за тим, як довго тягне користувач; якщо вони перетягуються довше деякої кількості часу, тоді зменшіть частоту вибірки (і відкиньте будь-які зразки, які були б пропущені), приблизно на 0,2 секунди.

(І не бери мої номери за євангелією, тому що я щойно витягнув їх із капелюха. Експериментуй і знайди кращі значення.)

По-друге, проаналізуйте зразки.

Ви хочете отримати два факти. По-перше, центр фігури, який (IIRC) повинен бути просто середнім серед усіх точок. По-друге, середній радіус кожного зразка з цього центру.

Якщо ви, як @ user1118321 здогадалися, хочете підтримати багатокутники, тоді решта аналізу складається з прийняття цього рішення: чи хоче користувач намалювати коло чи багатокутник. Ви можете розглядати зразки як багатокутник, для початку, щоб визначити це.

Ви можете використовувати декілька критеріїв:

  • Час: Якщо користувач лежить довше в деяких точках, ніж інші (які, якщо зразки знаходяться з постійним інтервалом, з'являться як скупчення послідовних зразків поруч один з одним у просторі), це можуть бути кути. Ви повинні зробити поріг кута невеликим, щоб користувач міг це робити несвідомо, а не свідомо робити паузи на кожному куті.
  • Кут: Коло матиме приблизно однаковий кут від однієї вибірки до другої на всій стороні. Багатокутник матиме кілька кутів, з'єднаних прямими відрізками; кути - кути. Для звичайного багатокутника (коло до еліпса неправильного багатокутника) кутові кути повинні бути приблизно однаковими; неправильний багатокутник матиме різні кутові кути.
  • Інтервал: кути регулярного багатокутника будуть рівними між собою кутовими розмірами, а радіус буде постійним. Неправильний багатокутник матиме неправильні кутові проміжки та / або нестабільний радіус.

Третій і останній крок - створити форму, зосереджену на раніше визначеній центральній точці, із визначеним раніше радіусом.

Ніяких гарантій, що все, що я говорив вище, буде працювати або бути ефективним, але я сподіваюся, що принаймні приведе вас на правильний шлях - і, будь ласка, якщо хтось, хто знає більше про розпізнавання форми, ніж я (це дуже низька смуга), побачить це, сміливо публікуйте коментар або власну відповідь.


+1 Привіт, дякую за вклад. Дуже інформативний. Так само я хочу, щоб супермен iOS / "розпізнавання форми" якось побачив цю публікацію і просвітить нас далі.
Unheilig

1
@Unheilig: Гарна ідея. Зроблено.
Пітер Хосей

1
Ваш алгоритм звучить добре. Я б додав перевірку того, наскільки відстань шляху користувача від ідеального кола / багатокутника. (Наприклад, відсоткове середнє квадратичне відхилення.) Якщо він занадто великий, користувач може не захотіти ідеальної форми. Для кваліфікованого каракулі обрізання буде меншим, ніж для неохайного каракуля. Це дозволило б програмі надати художню свободу художникам, але допомогти початківцям.
dmm

@ user2654818: Як би ти це виміряв?
Пітер Хосей

1
@PeterHosey: Пояснення для кіл: Отримавши ідеальне коло, ви отримаєте центр і радіус. Отже, ви берете кожну намальовану точку і обчислюєте її квадратну відстань від центру, що становить ((x-x0) ^ 2 + (y-y0) ^ 2). Відніміть це від радіуса в квадрат. (Я уникаю безлічі квадратних коренів, щоб зберегти обчислення.) Назвіть, що помилка квадрата для намальованої точки. Середнє значення похибки квадрата для всіх намальованих точок, потім квадратний корінь, а потім ділити на радіус. Це ваша середня відсоткова розбіжність. (Математика / статистика, напевно, гідна, але це спрацює на практиці.)
dmm,

2

Мені пощастило з правильно навченим розпізнавачем $ 1 ( http://depts.washington.edu/aimgroup/proj/dollar/ ). Я використовував це для кіл, ліній, трикутників і квадратів.

Це було дуже давно, перш ніж UIGestureRecognizer, але я думаю, що створити належні підкласи UIGestureRecognizer слід легко.


2

Після того як ви визначите, що користувач закінчив малювати їх форму там, де він почав, ви можете взяти зразок координат, які вони прокреслили, і спробувати прилаштувати їх до кола.

Тут є рішення 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.


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, щоб виявити дотики, правда? І як би ви визначили розмір одного одного UIView, якщо полотно розміром наприклад iPad? Здається, що якщо коло невелике і якщо розмір одного UIView більший, в цьому випадку ми не зможемо перевірити послідовність, оскільки всі намальовані точки лежатимуть в одному єдиному UIView.
Unheilig

Так - це, ймовірно, працює лише в тому випадку, якщо ви виправите полотно на щось на зразок 300x300, а потім поруч із ним розмістите "приклад" з розміром кола, якого ви бажаєте намалювати. Якщо так, я б пішов з квадратиками 50x50 * 6, вам також потрібно лише відобразити перегляди, які вас цікавлять, щоб потрапити у правильні місця, а не всі 6 * 6 (36) або 8 * 8 (64)
dijipiji

@Unheilig: Саме це і робить це рішення. Все, що достатньо кругового, щоб пройти правильну послідовність переглядів (а ви потенційно могли б дозволити деяку максимальну кількість об’їздів для додаткового нахилу) буде відповідати як коло. Потім ви прив'язуєте його до ідеального кола, зосередженого в центрі всіх тих поглядів, радіус яких досягає всіх (або принаймні більшості) з них.
Пітер Хосей

@PeterHosey Добре, дозвольте мені спробувати обійти це питання. Буду вдячний, якщо хтось із вас може надати якийсь код, щоб отримати цю інформацію. Тим часом я також спробую обійти цю голову, і згодом я зроблю те саме з кодуючою частиною. Дякую.
Unheilig

Щойно подав для вас ще один спосіб, який, думаю, може бути кращим
dijipiji
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.