Рейтинг@Mail.ru

Слежение за объектом на EV3

Автор: Alex. Опубликовано в Копилка . просмотров: 28210

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

С помощью веб-камеры и образовательного набора конструктора LEGO MINDSTORMS Education EV3 (45544) вполне можно сделать робота, отслеживающего двигающийся объект. Робот сможет не только поворачивать камеру в сторону объекта, но и выдерживать определённую дистанцию до него, т.е. подъехать поближе, если объект удаляется от камеры, или отъехать подальше, если объект приближается. О том, как это сделать поговорим в этой статье.

Слежение за объектом на EV3

Подготовка

Итак, давайте разбираться по порядку, что нам понадобится. Кроме самого набора LEGO MINDSTORMS Education EV3 (45544), нам нужна самая простая веб-камера с USB-подключением, не тяжёлая, которую вы сможете закрепить на роботе. Разрешение роли не играет, т.к. мощности EV3 всё равно не хватит на обработку больших изображений.

Чтобы EV3 смог работать с веб-камерой мы будем использовать альтернативную прошивку leJOS, вместо стандартной. О том, что такое leJOS, как подготовить загрузочную SD-карту и как программировать, используя leJOS, можете прочитать в статье «Программируем робота LEGO Mindstorms EV3 на Java». Обработку изображений, полученных с камеры, мы будем делать с помощью библиотеки компьютерного зрения OpenCV, которая появилась в составе leJOS в версии 0.9.1, поэтому эту версию и будем использовать.

После того как вы подготовили модуль EV3 для работы с leJOS, желательно проверить работает ли веб-камера с EV3 с помощью простых примеров, которые вы можете найти в статье «Программируем робота LEGO Mindstorms EV3 на Java» в разделе «Использование веб-камеры в leJOS EV3 для захвата изображения». Если камера работает, можно идти дальше.

Конструкцию робота можете придумать свою или использовать моего «Исследователя EV3», главное, чтобы робот смог не только поворачивать камеру в сторону объекта, но и двигаться к нему или от него. Конструкцию поворачивающейся рамки, к которой крепится камера, измените в зависимости от формы вашей камеры. Мне удалось закрепить камеру только с помощью деталей конструктора, но вы можете использовать резинки, верёвки и другие приспособления. Главное, чтобы камера держалась на рамке прочно и не болталась. Кстати, как робот работает можно посмотреть на видео:

Вот инструкция для сборки «Исследователя EV3»:

Исследователь EV3

Файлы:
Инструкция для сборки исследователя EV3 Версия:2

Инструкция для сборки робота исследователя EV3 из базового образовательного набора конструктора LEGO Mindstorms Education EV3 (45544).

В версии 2: рамка закреплена прочнее и не отваливается.

Дата 04.06.2016 Размер файла 4.95 MB Закачек 14086

После того как робот готов, можно переходить к программированию. Я сразу дам текст программы с комментариями, а ниже дам пояснения, как она работает и какие команды понимает.

package ru.proghouse.ev3.project;
 
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.List;
import java.util.Vector;
 
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.highgui.Highgui;
import org.opencv.highgui.VideoCapture;
import org.opencv.imgproc.Imgproc;
 
import lejos.hardware.BrickFinder;
import lejos.hardware.Button;
import lejos.hardware.lcd.GraphicsLCD;
import lejos.hardware.lcd.LCD;
import lejos.hardware.motor.EV3MediumRegulatedMotor;
import lejos.robotics.RegulatedMotor;
import lejos.utility.Delay;
 
public class BallTrackingBot {
 
    public static void main(String[] args) throws Exception {
        //Загружаем библиотеку OpenCV.
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        //Создаём вспомогательный класс для захвата видео.
        VideoCapture videoCapture = new VideoCapture(0);
        //Задаём ширину и высоту кадра, с которым будем работать. 
        final int WIDTH = 160, HEIGHT = 120;
        videoCapture.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, WIDTH);
        videoCapture.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, HEIGHT);
        //Задаём ширину и высоту центральной области.
        //Когда за рамки этой области будет выходить объект, будем двигать робота.
        final int CENTER_WIDTH = 50, CENTER_HEIGHT = 50;
        //Считаем координаты центра кадра.
        Point imageCenter = new Point(WIDTH / 2, HEIGHT / 2);
        //Захватываем камеру для работы с видео.
        if (videoCapture.open(0))
            //Выдаём сообщение, что камера готова.
            LCD.drawString("Camera is ready.", 0, 0);
        else {
            //Выдаём сообщение, что камера не готова. Может быть не подключена?
            LCD.drawString("Camera is not ready.", 0, 0);
            Delay.msDelay(5000);
            return;
        }
 
        final int MOTOR_A_SPEED = 100; //Скорость вращения мотора A.
        final int MOTOR_BC_SPEED = 100; //Скорость вращения моторов B и C.
 
        //Робот двигается шажками, поэтому задаём здесь размеры шагов.
        final int MOTOR_A_STEP = 20; //Размер шага для мотора A.
        final int MOTOR_BC_STEP_ROTATION = 20; //Размер шага для моторов B и C при вращении робота.
        final int MOTOR_BC_STEP_MOVE = 90; //Размер шага для моторов B и C при движении робота.
 
        //Инициализируем моторы.
        RegulatedMotor motorA = new EV3MediumRegulatedMotor(BrickFinder.getDefault().getPort("A"));
        RegulatedMotor motorB = new EV3MediumRegulatedMotor(BrickFinder.getDefault().getPort("B"));
        RegulatedMotor motorC = new EV3MediumRegulatedMotor(BrickFinder.getDefault().getPort("C"));
 
        //Задаём скорость вращения моторов.
        motorA.setSpeed(MOTOR_A_SPEED);
        motorB.setSpeed(MOTOR_BC_SPEED);
        motorC.setSpeed(MOTOR_BC_SPEED);
 
        //Выводим на экран приглашение выбрать режим (обычный или отладка).
        LCD.drawString("Choose the mode:", 0, 1);
        LCD.drawString("Up - normal;", 0, 2);
        LCD.drawString("Down - debug.", 0, 3);
 
        boolean debug = false;
        int debugMode = 0;
 
        //Пока ждём выбор режима, считываем изображения с камеры.
        Mat image = new Mat();
        while (true) {
            videoCapture.read(image);
            if (Button.ESCAPE.isDown())
                return; //Esc - выход из программы.
            else if (Button.UP.isDown())
                break; //Кнопка "вверх" - выбран обычный режим.
            else if (Button.DOWN.isDown()) {
                debug = true; //Кнопка "вниз" - выбран режим отладки.
                break;
            }
        }
 
        //Берём пробу цвета в центре кадра.
        double[] sampleColor = getSampleColor(videoCapture, WIDTH, HEIGHT);
        //Выводим на экран результаты взятия пробы цвета.
        LCD.drawString("Sample color:", 0, 4);
        LCD.drawString(" " + Double.toString(sampleColor[0])
                + " " + Double.toString(sampleColor[1])
                + " " + Double.toString(sampleColor[2]), 0, 5);
        //Определяем цвет по цветовому тону и выводим его на экран.
        double h = sampleColor[0];
        String colorName = "";
        if (h < 22)
            colorName = "orange";
        else if (h < 38)
            colorName = "yellow";
        else if (h < 75)
            colorName = "green";
        else if (h < 130)
            colorName = "blue";
        else if (h < 160)
            colorName = "violet";
        else
            colorName = "red";
        LCD.drawString("Is it " + colorName + "?", 0, 6);
 
        Delay.msDelay(1000);
 
        final Scalar RED = new Scalar(0, 0, 255);
 
        ServerSocket serverSocket = null;
        Socket socket = null;
        String boundary = "Thats it folks!";
        try {
            if (debug) {
                //Выводим на экран приглашение подключиться к EV3 с помощью браузера.
                LCD.clear();
                LCD.drawString("Connect to", 0, 0);
                LCD.drawString("http://10.0.1.1:8080", 0, 1);
                //Создаём серверный сокет.
                serverSocket = new ServerSocket(8080);
                //Ждём подключения к серверу.
                socket = serverSocket.accept();
                //Клиент (браузер) подключен, выводим информацию об этом на экран.
                LCD.drawString("Connected!", 0, 2);
                //Отдаём клиенту (браузеру) HTTP-заголовок.
                writeHeader(socket.getOutputStream(), boundary);
            }
 
            //Вычисляем интервал цветов, близких к цвету пробы.
            double hMin = sampleColor[0] - 10;
            double hMax = sampleColor[0] + 10;
            if (hMin < 0)
                hMin = 0;
            if (hMax > 179)
                hMax = 179;
            double sMin = sampleColor[1] - 50;
            double sMax = sampleColor[1] + 50;
            if (sMin < 0)
                sMin = 0;
            if (sMax > 255)
                sMax = 255;
            double vMin = sampleColor[2] - 50;
            double vMax = sampleColor[2] + 50;
            if (vMin < 0)
                vMin = 0;
            if (vMax > 255)
                vMax = 255;
            Scalar hsvMin = new Scalar(hMin, sMin, vMin);
            Scalar hsvMax = new Scalar(hMax, sMax, vMax);
 
            GraphicsLCD lcd = BrickFinder.getDefault().getGraphicsLCD();
 
            Mat hsv = new Mat();
            Mat mask = new Mat();
            Mat hierarchy = new Mat();
            Size ksize  = new Size(5, 5);
            List<MatOfPoint> contours = new Vector<MatOfPoint>();
            double contourArea, maxArea;
            MatOfPoint biggestContour;
            MatOfPoint2f contours2f = new MatOfPoint2f();
            Point center = new Point();
            float[] radius = new float[1];
            MatOfByte buffer = new MatOfByte();
 
            boolean motorBCMoving = true;
            boolean motorAMoving = true;
 
            while (!Button.ESCAPE.isDown()) {
                //Меняем режим отладки, если нажата клавиша Enter на модуле EV3.
                if (Button.ENTER.isDown()) {
                    debugMode++;
                    if (debugMode >= 4)
                        debugMode = 0;
                }
                //Запоминаем, крутятся ли моторы перед поиском объекта.
                motorBCMoving = motorB.isMoving() || motorC.isMoving();
                motorAMoving = motorA.isMoving();
 
                //Считываем кадр с камеры.
                if (videoCapture.read(image) && !image.empty()) {
 
                    //Если включена отладка и режим отладки 2 (source),
                    //отдаём полученный кадр браузеру.
                    if (debug && debugMode == 2) {
                        Highgui.imencode(".jpeg", image, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
 
                    //Imgproc.blur(image, image, ksize); //Размытие отключено, для ускорения.
                    //Полученный кадр формата BGR. Переводим его в формат HSV.
                    Imgproc.cvtColor(image, hsv, Imgproc.COLOR_BGR2HSV);
                    //Ищем цвета в нужном нам интервале. В результате получится чёрно-белое изображение,
                    //где белый цвет обозначает цвет, попавший в интервал, а чёрный - всё остальное.
                    Core.inRange(hsv, hsvMin, hsvMax, mask);
 
                    //Если включена отладка и режим отладки 1 (mask),
                    //отдаём результат работы функции inRange браузеру.
                    if (debug && debugMode == 1) {
                        Highgui.imencode(".jpeg", mask, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
 
                    //Ищем контуры, т.е. объекты нужного нам цвета.
                    contours.clear();
                    Imgproc.findContours(mask, contours, hierarchy,
                            Imgproc.RETR_LIST, Imgproc.CHAIN_APPROX_SIMPLE);
 
                    //Среди найденных контуров ищем контур наибольшего размера.
                    biggestContour = null;
                    maxArea = 0;
                    for (MatOfPoint contour : contours) {
                        contourArea = Imgproc.contourArea(contour);
                        if (contourArea > maxArea) {
                            biggestContour = contour;
                            maxArea = contourArea;
                        }
                    }
 
                    //Очищаем экран.
                    lcd.clear();
 
                    //Если контур есть и его размер больше или равен 100...
                    if (biggestContour != null && maxArea >= 100) {
 
                        //1-й способ поиска круга на изображении.
                        //Здесь ищется окружность в указанном контуре.
                        //В результате получаем координату центра окружности и её радиус.
                        //Этот способ работает лучше, чем 2-й способ, но медленнее.
                        //Чтобы работал этот способ откомментируйте его и закомментируйте 2-й.
                        /*biggestContour.convertTo(contours2f, CvType.CV_32FC2);
                        Imgproc.minEnclosingCircle(contours2f, center, radius);*/
 
                        //2-й способ поиска круга на изображении.
                        //На самом деле здесь круг не ищется.
                        //Здесь просто считаем, что радиус - это наибольший размер контура / 2,
                        //а центр контура - это центр нашего круга.
                        Rect rect = Imgproc.boundingRect(biggestContour);
                        center.x = rect.x + rect.width / 2;
                        center.y = rect.y + rect.height / 2;
                        radius[0] = Math.max(rect.width, rect.height) / 2;
 
                        //Если включен режим отладки, выводим на экран радиус круга и режим отладки.
                        if (debug) {
                            LCD.drawString("Radius: " + Double.toString(radius[0]), 0, 6);
                            LCD.drawString("Debug mode:"
                                    + (debugMode == 0 ? "contour"
                                     : debugMode == 1 ? "mask"
                                     : debugMode == 2 ? "source"
                                     : "none"), 0, 7);
                        }
                        //Рисуем найденный круг на экране EV3.
                        lcd.drawRoundRect((int)center.x - (int)radius[0],
                                (int)center.y - (int)radius[0], 
                                (int)radius[0] * 2, (int)radius[0] * 2,
                                (int)radius[0] * 2, (int)radius[0] * 2);
 
                        //Считаем смещение центра круга от центра кадра.
                        double dy = center.y - imageCenter.y;
                        //Если мотор не двигается сейчас и не двигался перед взятием кадра,
                        //можно двигать камеру.
                        if ((!motorA.isMoving()) && (!motorAMoving)) {
                            //Если круг вышел за центральную область двигаем камеру.
                            if (dy < -CENTER_HEIGHT / 2) 
                                motorA.rotate(-MOTOR_A_STEP, true);
                            else if (dy > CENTER_HEIGHT / 2)
                                motorA.rotate(MOTOR_A_STEP, true);
                        }
 
                        //Считаем смещение центра круга от центра кадра.
                        double dx = center.x - imageCenter.x;
                        //Если моторы не двигаются сейчас и не двигались перед взятием кадра,
                        //можно двигать робота.
                        if ((!motorB.isMoving()) && (!motorC.isMoving()) && (!motorBCMoving)) {
                            //Если круг вышел за центральную область, поворачиваем робота.
                            if (dx < -CENTER_WIDTH / 2) {
                                motorB.rotate(-MOTOR_BC_STEP_ROTATION, true);
                                motorC.rotate(MOTOR_BC_STEP_ROTATION, true);
                            } else if (dx > CENTER_WIDTH / 2) {
                                motorB.rotate(MOTOR_BC_STEP_ROTATION, true);
                                motorC.rotate(-MOTOR_BC_STEP_ROTATION, true);
                            }
                            //Если круг слишком мал, подъезжаем ближе.
                            else if (radius[0] < 20) {
                                motorB.rotate(MOTOR_BC_STEP_MOVE, true);
                                motorC.rotate(MOTOR_BC_STEP_MOVE, true);
                            }
                            //Если круг слишком велик, отъезжаем дальше.
                            else if (radius[0] > 30) {
                                motorB.rotate(-MOTOR_BC_STEP_MOVE, true);
                                motorC.rotate(-MOTOR_BC_STEP_MOVE, true);
                            }
 
                        }
 
                        //Если включена отладка и режим отладки 0 (contour)...
                        if (debug && debugMode == 0) {
                            contours.clear();
                            contours.add(biggestContour);
                            //Рисуем найденный контур на кадре.
                            Imgproc.drawContours(image, contours, 0, RED);
                            //Рисуем найденный круг на кадре.
                            Core.circle(image, center, (int)radius[0], RED);
                        }
                    }
                    else { //Если контур не найден или его размер меньше 100, останавливаем двигатели.
                        if (motorA.isMoving())
                            motorA.stop(true);
                        if (motorB.isMoving() || motorC.isMoving()) {
                            motorB.stop(true);
                            motorC.stop(true);
                        }
                    }
 
                    //Если включена отладка и режим отладки 0 (contour),
                    //отдаём браузеру кадр, полученный с камеры,
                    //с подрисованными контуром и кругом.
                    if (debug && debugMode == 0) {
                        Highgui.imencode(".jpeg", image, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
                }
            }
            lcd.clear();
        } finally {
            //Закрываем сокеты.
            if (socket != null)
                socket.close();
            if (serverSocket != null)
                serverSocket.close();
        }
    }
 
    //Функция получает пробу цвета с камеры.
    private static double[] getSampleColor(VideoCapture videoCapture,
            int imageWidth, int imageHeight) {
        Mat image = new Mat();
        Size ksize  = new Size(5, 5);
        double h = 0, s = 0, v = 0;
        int sampleCount = 7; //Количество проб.
        for (int i = 0; i < sampleCount; ) {
            if (videoCapture.read(image) && !image.empty()) {
                Rect snippetROI = new Rect(image.width() / 2 - 5,
                        image.height() / 2 - 5, 10, 10);
                Mat snippet    = image.submat(snippetROI);
                Imgproc.blur(snippet, snippet, ksize);
                Imgproc.cvtColor(snippet, snippet, Imgproc.COLOR_BGR2HSV);
                double[] color = snippet.get(5, 5);
                h += color[0];
                s += color[1];
                v += color[2];
                i++;
                Delay.msDelay(100);
            }
        }
        return new double[] {Math.round(h / sampleCount),
                Math.round(s / sampleCount),
                Math.round(v / sampleCount)};
    }
 
    //Функция отдаёт браузеру HTTP-заголовок.
    private static void writeHeader(OutputStream stream, String boundary) throws IOException {
        stream.write(("HTTP/1.0 200 OK\r\n" +
                "Connection: close\r\n" +
                "Max-Age: 0\r\n" +
                "Expires: 0\r\n" +
                "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" +
                "Pragma: no-cache\r\n" + 
                "Content-Type: multipart/x-mixed-replace; " +
                "boundary=" + boundary + "\r\n" +
                "\r\n" +
                "--" + boundary + "\r\n").getBytes());
    }
 
    //Функция отдаёт браузеру изображение.
    private static void writeJpg(OutputStream stream, MatOfByte image, String boundary) throws IOException {
        byte[] imageBytes = image.toArray();
        stream.write(("Content-type: image/jpeg\r\n" +
                "Content-Length: " + imageBytes.length + "\r\n" +
                "\r\n").getBytes());
        stream.write(imageBytes);
        stream.write(("\r\n--" + boundary + "\r\n").getBytes());
    }
 
}

Описание работы программы

После запуска программы, если всё в порядке, на экране появляются следующие надписи:

Camera is ready.
Choose the mode:
Up - normal;
Down - debug.

Первая строчка говорит о том, что камера готова. Следующая запись приглашает выбрать режим работы: нормальный или отладка. Следующие 2 строчки – подсказки. В этот момент программа ждёт вашего решения. Вы можете остановить программу, нажав Esc на модуле EV3 или нажать кнопку «вверх», чтобы выбрать нормальный режим, или кнопку «вниз» - для отладки.

Перед тем как выбрать режим подставьте к камере объект, который вы будете отслеживать. При этом старайтесь, чтобы объект был освещён так же, как и при дальнейшем отслеживании. Это не обязательно должен быть шарик. Главное, чтобы объект был яркого насыщенного цвета. Лучше всего использовать синий или зелёный цвета.

Затем нажмите кнопку вниз или вверх, в зависимости от выбранного режима, и робот возьмёт пробу цвета. На самом деле проба берётся 7 раз с небольшим интервалом времени, и конечный цвет будет вычислен как среднее арифметическое от этих 7-ми проб. Это позволяет снизить зависимость от мерцаний и подстройки баланса белого. На экране появится надпись:

Sample color:
 109.0 208.0 251.0
Is it blue?

Здесь робот сообщает, какой цвет получен с камеры в цветовой модели HSV. Первая цифра – это цветовой тон (Hue), вторая – насыщенность (Saturation) и третья – значение или яркость (Value). Цветовой тон как раз и определяет цвет. В OpenCV он может принимать значения от 0 до 179, см. рисунок ниже.

Шкала цветового тона в цветовой модели HSV в OpenCV

Именно по цветовому тону робот пытается определить название цвета и высвечивает вопрос Is it blue? (Это синий?). Отвечать на него конечно не нужно, это робот сомневается:)

Насыщенность и яркость могут быть в пределах от 0 до 255. Чем меньше насыщенность, тем ближе к белому или серому будет цвет, чем меньше яркость – тем ближе к чёрному. Из чего мы можем сделать вывод, что чем больше значения насыщенности и яркости, тем проще будет определять цвет и отслеживать объект.

Дальше программа будет работать по-разному в зависимости от режима. Режим отладки отличается от нормального режима тем, что вы сможете видеть то, что видит камера через браузер. Конечно, это замедляет работу программы, но зато вы сможете оценить, как происходит работа с изображением. Подробнее об отладке поговорим чуть ниже, а сейчас посмотрим, что будет, если выбрать нормальный режим.

После взятия пробы, действия программы по-простому можно описать так: программа будет в цикле брать кадры с камеры, искать на них объекты такого же цвета, что и цвет пробы, с небольшим допуском, и двигать робота так, чтобы отслеживаемый объект был примерно в центре кадра и был примерно нужного размера. В каждой итерации на экране отрисовывается окружность на экране EV3, в том месте где на кадре обнаружен объект и того размера который удалось определить. Как только робот теряет объект, он останавливается и ждёт, пока объект появится снова.

К сожалению, все описанные действия микрокомпьютер EV3 делает медленно, и о плавности движений робота можно забыть, т.к. идёт большая задержка при обработке изображения. Поэтому моя программа двигает робота пошагово, на это можно обратить внимание на видео: нашли объект на кадре – крутим моторы робота на один шаг (т.е. угол), ищем объект снова и опять двигаем робота на один шаг, и так далее. Пока робот двигается, брать пробы не имеет смысла, т.к. это заведомо будут устаревшие сведения.

Чтобы остановить программу, нажмите и чуть подержите кнопку Esc.

Поиск объекта с помощью OpenCV

Здесь нужно сделать остановку и отдельно написать, как же я буду искать объект на изображениях, полученных с камеры. Делается это очень просто: мы будем искать на изображении пятно определённого цвета – это и будет наш объект.

Если расписывать в деталях, то делается это так. Сначала переводим полученную с камеры картинку в формат HSV с помощью функции cvtColor. В этом формате легче найти заданный цвет, ведь он определяется всего одной цифрой в интервале от 0 до 179, см. выше.

Затем ищем на картинке интересующий нас интервал цвета в кадре. Дело в том, что тот цвет, который изначально запомнил робот, может меняться в зависимости от освещения и теней, падающих на объект. Кроме того шарик, как его видит камера, покрашен неоднородно: светлее посередине и темнее к краям. Именно поэтому в программе мы используем интервал. Однако интервал не должен быть слишком большим, иначе у нас будет много ошибок.

В программе я использую интервал тона -10 <= h <= +10, где h – это значение цветового тона снятое во время первой пробы, и интервалы насыщенности и яркости, соответственно, -50 <= s <= 50 и -50 <= v <= 50. Но не больше предельных значений. Т.е. если проба цвета была 109, 208, 251, то искомый интервал будет 99-119, 198-218, 241-255.

Интервал ищется с помощью функции inRange. В результате работы функции получается чёрно белая картинка, где белыми точками обозначены точки, цвет которых попал в заданный интервал, а чёрными – все остальные.

После этого с помощью функции findContours находим контуры белых фигур, т.е. объекты. Конечно, здесь могут оказаться несколько объектов, но здесь уже ничего не поделаешь. В этом случае я выбираю наибольший найденный объект.

Вот собственно и всё. У найденного контура я могу узнать центр и размер. И, в зависимости от того, где этот контур, т.е. объект, расположен на картинке, я могу сделать вывод, куда нужно поворачивать камеру и двигать робота.

Для улучшения работы этого алгоритма лучше использовать размытие (функция blur) и поиск окружностей (функция minEnclosingCircle), но они замедляют поиск объектов. Поэтому в коде программы они закомментированы. Вы можете попробовать, как работает программа с ними, просто убрав комментарии.

Режим отладки

С нормальным режимом разобрались, теперь напишу, как работает программа в режиме отладки. Когда вы нажали кнопку «Вниз» при выборе режима – вы запустили режим отладки. В этот момент робот точно так же возьмёт пробу цвета, а затем появится следующая надпись:

Connect to
http://10.0.1.1:8080

Это призыв к тому, чтобы вы подключились к EV3 с помощью браузера. Программа в этой точке будет висеть, пока вы этого не сделаете. Для подключения к EV3 создайте файл с расширением .html со следующим содержимым:

<html>
<body>
<img src="http://10.0.1.1:8080"/>
</body>
</html>

Откройте его с помощью браузера Chrome или Firefox. Как только вы откроете созданный файл, робот оживёт, а в браузере появится потоковое видео с камеры, где будет обведён найденный контур:

Найденный шарик с помощью библиотеки OpenCV

Картинка, как видите, маленькая. Размером всего 160x120. Поверьте, даже с таким небольшим размером вы почувствуете тормоза. Однако такого размера вполне хватает для отслеживания объекта. В браузере вы можете увеличить масштаб, чтобы в картинку не нужно было вглядываться. Вот как это выглядит в масштабе 200%:

Поиск объекта с помощью OpenCV

Одна красная линия обводит найденный с помощью библиотеки OpenCV контур, а вторая - это окружность, диаметр которой равен наибольшему размеру контура с центром в центре контура.

На экране EV3 в это время пишется следующий текст:

Radius: 21.4
Debug mode: contour

В верхней строке, как вы можете догадаться – радиус найденного объекта, а в нижней строке – режим отладки. Всего их 4: contour, mask, source и none. Если во время отладки вы будете нажимать на кнопку Enter на модуле EV3, вы будете переключать режим отладки. По умолчанию – это contour (контур). В этом режиме отображается видео с камеры, и сверху рисуются линии контура и окружность, про которые написано выше.

Следующий режим – mask. В этом режиме вы увидите результат работы функции inRange библиотеки OpenCV.

Результат поиска интервала цветов функцией inRange

Следующий режим – source – это видео, полученное с камеры без обработки, см. картинку ниже.

Изображение, полученное с веб-камеры и переданное из EV3 в браузер

Режим – none – это режим, в котором картинки в браузер не отправляются. Этот режим работает так, как будто отладки нет.

Итог

Итак, я написал все мысли, которые у меня были по поводу эксперимента с отслеживанием объекта с помощью библиотеки OpenCV.

При разработке этого эксперимента я использовал следующие статьи и исходники:

1. Справка по OpenCV
2. A color tracker for an Omnicamera
3. openCV web streaming
4. Color Detection & Object Tracking
5. Ball Tracking with OpenCV

Читайте также статью «Обнаружение лица на EV3».

Tags: OpenCV leJOS Учебники по программированию Инструкции LEGO Mindstorms EV3 LEGO Mindstorms Education EV3

Комментарии   

Gulzhan
+1 #1 Gulzhan 03.12.2019 13:29
а где программировать эту программу
:-?
Цитировать
Alex
+1 #2 Alex 04.12.2019 16:32
Цитирую Gulzhan:
а где программировать эту программу
:-?

Программировать в Eclipse. В статье "Программируем робота LEGO Mindstorms EV3 на Java" подробно написано: www.proghouse.ru/article-box/55-lejos-ev3
Цитировать
VFirstov
0 #3 VFirstov 12.01.2021 14:50
Здорово! А вы не пробовали использовать два кирпича, чтобы меньше подтормаживало? Как думаете, можно такой вариант реализовать и поможет ли?
Цитировать
Alex
0 #4 Alex 12.01.2021 21:38
Цитирую VFirstov:
Здорово! А вы не пробовали использовать два кирпича, чтобы меньше подтормаживало? Как думаете, можно такой вариант реализовать и поможет ли?

А что будет делать второй кирпич? Анализировать каждую вторую картинку? С ним ещё надо будет обмениваться данными... Будет очень сложно и будет ещё больше тормозов.
Цитировать

Добавить комментарий