Обнаружение лица на EV3

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

Рейтинг:  5 / 5

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

Используя конструктор LEGO MINDSTORMS EV3 и веб-камеру, вы сможете провести эксперимент по обнаружению лиц в помещении. Для эксперимента подойдёт любой колёсный робот EV3, который умеет вращаться на месте, и на который вы сможете закрепить веб камеру. Робот будет сканировать помещение, поворачиваясь вокруг, а, увидев лица, будет останавливаться и дёргаться столько раз, сколько лиц увидел.

Обнаружение лица на EV3

Подготовка к эксперименту

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

Кроме того, для работы программы вам понадобится файл lbpcascade_frontalface.xml. Найти его вы можете в архиве с библиотекой OpenCV в папке data\lbpcascades. Скачать библиотеку OpenCV можно на официальном сайте здесь. Скачивать лучше ту же версию, которая используется в leJOS (в leJOS 0.9.1 используется OpenCV 2.4.11). Вообще, посмотреть версию OpenCV используемую в leJOS можно, когда вы создадите проект, в пакете org.opencv.core в файле Core.class. Файл lbpcascade_frontalface.xml нужно закачать в EV3 с помощью центра управления (EV3 Control Center) в папку с программами.

Теперь что касается робота. Будем считать, что простой колёсный робот у вас уже собран. Я буду использовать своего «Исследователя EV3», собранного из образовательного набора LEGO MINDSTORMS Education EV3 (45544). Вот инструкция для сборки «Исследователя EV3»:

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

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

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

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

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

Закрепите камеру на роботе и подключите её к модулю EV3 с помощью USB-кабеля. Как выглядит и работает мой готовый робот, вы можете посмотреть на видео:

Как создать проект в Eclipse и как запустить программу, написано в статье «Программируем робота LEGO Mindstorms EV3 на Java». Поэтому здесь я просто приведу код программы, а ниже дам подробные объяснения.

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.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.highgui.Highgui;
import org.opencv.highgui.VideoCapture;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
 
import lejos.hardware.BrickFinder;
import lejos.hardware.Button;
import lejos.hardware.lcd.GraphicsLCD;
import lejos.hardware.lcd.LCD;
import lejos.hardware.motor.EV3LargeRegulatedMotor;
import lejos.robotics.RegulatedMotor;
import lejos.utility.Delay;
 
public class FaceDetectionBot implements Runnable {
 
    public static void main(String[] args) throws Exception {
        //Загружаем библиотеку OpenCV.
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        //Создаём вспомогательный класс для захвата видео.
        VideoCapture videoCapture = new VideoCapture(0);
        //Задаём ширину и высоту кадра, с которым будем работать. 
        videoCapture.set(Highgui.CV_CAP_PROP_FRAME_WIDTH, 160);
        videoCapture.set(Highgui.CV_CAP_PROP_FRAME_HEIGHT, 120);
        //Захватываем камеру для работы с видео.
        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_BC_SPEED = 50; //Скорость вращения моторов B и C.
 
        //Инициализируем моторы.
        RegulatedMotor motorB = new EV3LargeRegulatedMotor(BrickFinder.getDefault().getPort("B"));
        RegulatedMotor motorC = new EV3LargeRegulatedMotor(BrickFinder.getDefault().getPort("C"));
 
        //Выводим на экран приглашение выбрать режим (обычный или отладка).
        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;
 
        //Запускаем поток, в котором будем получать изображение с камеры. 
        FaceDetectionBot bot = new FaceDetectionBot(videoCapture);
        new Thread(bot).start();
 
        //Ждём выбора режима.
        while (true) {
            if (Button.ESCAPE.isDown())
                return; //Esc - выход из программы.
            else if (Button.UP.isDown())
                break; //Кнопка "вверх" - выбран обычный режим.
            else if (Button.DOWN.isDown()) {
                debug = true; //Кнопка "вниз" - выбран режим отладки.
                break;
            }
        }
 
        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);
            }
 
            //Инициализируем каскадный классификатор.
            String classifierFileName = "lbpcascade_frontalface.xml";
            CascadeClassifier faceDetector = new CascadeClassifier(classifierFileName);
 
            MatOfRect faceDetections = new MatOfRect();
            Mat image = new Mat();
            Mat gray = new Mat();
            long lastImageId = 0;
            int detected = 0;
            MatOfByte buffer = new MatOfByte();
            GraphicsLCD lcd = BrickFinder.getDefault().getGraphicsLCD();
 
            //Задаём скорость вращения моторов и начинаем поворачивать робота.
            motorB.setSpeed(MOTOR_BC_SPEED);
            motorC.setSpeed(MOTOR_BC_SPEED);
            motorB.backward();
            motorC.forward();
 
            while (!Button.ESCAPE.isDown()) {
                //Меняем режим отладки, если нажата клавиша Enter на модуле EV3.
                if (Button.ENTER.isDown()) {
                    debugMode++;
                    if (debugMode >= 4)
                        debugMode = 0;
                }
 
                //Забираем кадр, полученный с камеры в другом потоке.
                //Это сделано для уменьшения задержек.
                long curImageId = bot.getLastImage(image);
                if (curImageId != lastImageId) {
                    lastImageId = curImageId;
 
                //Так можно получать кадр с камеры в этом же потоке.
                //if (videoCapture.read(image) && !image.empty()) {
 
                    //Если включена отладка и режим отладки 2 (source),
                    //отдаём полученный кадр браузеру.
                    if (debug && debugMode == 2) {
                        Highgui.imencode(".jpeg", image, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
 
                    //Полученный кадр формата BGR. Переводим его в оттенки серого.
                    Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);
                    faceDetector.detectMultiScale(gray, faceDetections);
 
                    //Если включена отладка и режим отладки 1 (gray),
                    //отдаём преобразованный в оттенки серого кадр браузеру.
                    if (debug && debugMode == 1) {
                        Highgui.imencode(".jpeg", gray, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
 
                    //Если включена отладка и режим отладки 0 (faces),
                    //отдаём полученный кадр и обозначенные найденные на нём лица браузеру.
                    if (debug && debugMode == 0) {
                        //Подрисовываем рамки вокруг найденных лиц.
                        for (Rect rect : faceDetections.toArray())
                            Core.rectangle(image, 
                                    new Point(rect.x, rect.y), 
                                    new Point(rect.x + rect.width, rect.y + rect.height), 
                                    new Scalar(0, 255, 0));
                        //Отдаём изображение браузеру.
                        Highgui.imencode(".jpeg", image, buffer);
                        writeJpg(socket.getOutputStream(), buffer, boundary);
                    }
 
                    //Очищаем экран.
                    lcd.clear();
 
                    //Выводим на экран количество найденных лиц.
                    LCD.drawString(String.format("Detected %s face(s)", faceDetections.toArray().length), 0, 6);
 
                    //Если включена отладка, выводим на экран режим отладки.
                    if (debug) {
                        LCD.drawString("Debug mode:"
                                + (debugMode == 0 ? "faces"
                                 : debugMode == 1 ? "gray"
                                 : debugMode == 2 ? "source"
                                 : "none"), 0, 7);
                    }
 
                    //Если лица обнаружены, чуть-чуть поворачиваемся обратно и дёргаемся столько раз, сколько лиц найдено.
                    if (faceDetections.toArray().length > 0) {
                        if (detected == 0) {
                            motorB.setSpeed(200);
                            motorC.setSpeed(200);
                            motorB.synchronizeWith(new RegulatedMotor[] {motorC});
                            motorB.rotate(40, true);
                            motorC.rotate(-40, true);
                            motorB.endSynchronization();
                            motorB.waitComplete();
                            motorB.setSpeed(740);
                            motorC.setSpeed(740);
                            for (int i = 0; i < faceDetections.toArray().length * 2; i++) {
                                int angle = i % 2 == 0 ? 10 : -10;
                                motorB.rotate(angle, true);
                                motorC.rotate(angle, true);
                                Thread.sleep(100);
                                if (angle < 0)
                                    Thread.sleep(100);
                            }
                            Thread.sleep(1000);
                        }
                        detected = 3;
                    }
                    else {
                        //Если лиц дольше не видим, опять начинаем поворачивать робота.
                        if (detected > 0) {
                            detected--;
                            if (detected == 0) {
                                motorB.setSpeed(MOTOR_BC_SPEED);
                                motorC.setSpeed(MOTOR_BC_SPEED);
                                motorB.backward();
                                motorC.forward();
                            }
                        }
                    }
 
                }
            }
            //Оповещаем поток, о завершении работы.
            bot.terminated = true;
        } finally {
            //Закрываем сокеты.
            if (socket != null)
                socket.close();
            if (serverSocket != null)
                serverSocket.close();
        }
    }
 
    volatile boolean terminated = false;
    long currentImageId = 1;
    VideoCapture videoCapture = null;
    int currentWritingImage = 0;
    List<Mat> images = new Vector<Mat>();
    long[] imageIds = new long[3];
    Object lock = new Object();
 
    private FaceDetectionBot(VideoCapture videoCapture) {
        this.videoCapture = videoCapture;
        for (int i = 0; i < 3; i++) {
            images.add(new Mat());
            imageIds[i] = 0;
        }
    }
 
    //Функция отдаёт копию считанного в потоке изображения.
    private long getLastImage(Mat image) {
        synchronized (lock) {
            int currentReadingImage = currentWritingImage - 1;
            if (currentReadingImage < 0)
                currentReadingImage = images.size() - 1;
            images.get(currentReadingImage).copyTo(image);
            return imageIds[currentReadingImage];
        }
    }
 
    @Override
    public void run() {
        //Здесь в отдельном потоке постоянно считываем изображения с камеры и сохраняем в массив.
        while (!terminated) {
            if (videoCapture.read(images.get(currentWritingImage))) {
                imageIds[currentWritingImage] = currentImageId++;
                synchronized (lock) {
                    currentWritingImage++;
                    if (currentWritingImage >= images.size())
                        currentWritingImage = 0;
                }
            }
        }
    }
 
    //Функция отдаёт браузеру 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.

Первая строка сообщает о том, что камера готова. Вторая строка приглашает выбрать режим работы. Следующие строки подсказывают, какие режимы работы есть, и какие кнопки на модуле EV3 нужно нажать для выбора. Нажмите кнопку вверх (Up) , чтобы выбрать нормальный режим работы, или кнопку вниз (Down), чтобы выбрать режим отладки. Давайте здесь рассмотрим, как работает программа в нормальном режиме, а про отладку поговорим чуть позже.

Нажмите кнопку вверх. Робот сразу начнёт медленно поворачиваться на месте и искать лица на изображениях, получаемых с камеры. Как только в камеру попадёт одно лицо или несколько лиц, робот остановится, слегка повернётся обратно и дёрнется столько раз, сколько лиц он обнаружил. После этого робот будет стоять неподвижно, пока на изображениях, получаемых с камеры, не исчезнут все лица. Как только это произойдёт, робот опять начнёт медленно вращаться и искать лица. И так далее, пока вы не остановите программу, нажав Esc на модуле EV3.

Обнаружение лиц с помощью OpenCV

Теперь давайте чуть подробнее поговорим о том, как происходит обнаружение лиц на кадрах, полученных с камеры, в нашей программе. Дело в том, что в библиотеке OpenCV, которую мы используем, есть класс CascadeClassifier (каскадный классификатор), который способен находить на изображении определённые объекты, используя признаки Хаара (Альфред Хаар - венгерский математик) или признаки LBP (Local Binary Pattern). Чтобы каскадный классификатор знал, какие объекты нужно искать его тренируют с помощью нескольких сотен простых изображений целевых объектов (например, лиц или машин) одинакового размера, называемых положительными примерами, и произвольных картинок такого же размера, называемых негативными примерами (подробности читайте в официальной документации здесь и здесь). Но в нашем эксперименте мы не будем заниматься тренировкой, а воспользуемся уже готовыми натренированными каскадными классификаторами.

Готовые настройки каскадных классификаторов вы можете найти в папке data в архиве с библиотекой OpenCV (скачать OpenCV можно здесь). Тут есть настройки для поиска лиц в анфас и в профиль, кошачих мордочек, глаз, автомобильных номеров и др. Я для эксперимента выбрал файл lbpcascade_frontalface.xml для обнаружения лиц в анфас, но ничего не мешает вам провести свои эксперименты и поискать, например, лица в профиль или кошачьи мордочки. Для этого закачайте в модуль EV3 в папку с программами нужный файл с помощью центра управления (EV3 Control Center) и поменяйте в программе строчку «String classifierFileName = "lbpcascade_frontalface.xml";» указав здесь необходимое имя файла.

Ещё вам нужно знать, что тренировка и определение объектов с помощью признаков LBP происходит намного быстрее, чем с помощью признаков Хаар.

Итак, после того как мы создали экземпляр класса CascadeClassifier, загрузив настройки из файла lbpcascade_frontalface.xml, нам достаточно вызывать функцию detectMultiScale для каждого изображения полученного с камеры, чтобы найти на нём лица. А дальше уже мы можем по-разному управлять нашим роботом в зависимости от результата.

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

//if (videoCapture.read(image) && !image.empty()) {

и закомментируйте строки

FaceDetectionBot bot = new FaceDetectionBot(videoCapture);
new Thread(bot).start();
 
…
 
long curImageId = bot.getLastImage(image);
if (curImageId != lastImageId) {
    lastImageId = curImageId;
 
…
 
bot.terminated = true;

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

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

Connect to
http://10.0.1.1:8080

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

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

и откройте этот файл в браузере Chrome, Firefox или Microsoft Edge. Как только вы это сделаете, робот начнёт поворачиваться на месте, а в браузере будет отображаться видео, полученное с камеры. Картинка маленькая, но этого вполне достаточно, чтобы обнаружить на ней лицо.

Обнаружение лица на EV3

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

Detected 1 face(s)
Debug mode: faces

Как вы уже поняли, здесь написано количество найденных лиц и режим отладки. Нажимая на центральную кнопку модуля EV3, вы можете переключать режим отладки. Кроме режима faces, в котором отображаются картинка с камеры и рамки, вокруг найденных лиц, есть ещё режимы gray, source и none. В режиме gray вы увидите картинку, переведённую в оттенки серого, а именно на картинке такого формата библиотека OpenCV ищет объекты. В режиме source – картинка, полученная с камеры без изменений. В режиме none – изображение не передаётся в браузер.

Итог

Вот и всё что мне хотелось написать про обнаружение лиц с помощью EV3. Если у вас есть вопросы, смело задавайте их в комментариях.

А вот литература, которая помогала мне делать этот эксперимент:

FACE TRACKING ON EV3 USING LEJOS 0.9.1
Документация по OpenCV, в том числе Cascade Classification и Cascade Classifier Training.

Читайте также статью «Слежение за объектом на EV3».

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

Комментарии   

imehanik
0 #1 imehanik 09.11.2016 15:33
Спасибо! Отличная статья! Пробуем повторить.
Цитировать

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


Защитный код
Обновить