Используя конструктор LEGO MINDSTORMS 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 из базового образовательного набора конструктора LEGO Mindstorms Education EV3 (45544).
В версии 2: рамка закреплена прочнее и не отваливается.
04.06.2016 4.95 MB 14090
Закрепите камеру на роботе и подключите её к модулю 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;publicclass FaceDetectionBot implementsRunnable{publicstaticvoid main(String[] args)throwsException{//Загружаем библиотеку 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;
}finalint 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);
newThread(bot).start();
//Ждём выбора режима.while(true){if(Button.ESCAPE.isDown())return; //Esc - выход из программы.elseif(Button.UP.isDown())break; //Кнопка "вверх" - выбран обычный режим.elseif(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 = newServerSocket(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,
newPoint(rect.x, rect.y),
newPoint(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();
}}volatileboolean terminated = false;
long currentImageId = 1;
VideoCapture videoCapture = null;
int currentWritingImage = 0;
List<Mat> images = new Vector<Mat>();
long[] imageIds = newlong[3];
Object lock = newObject();
private FaceDetectionBot(VideoCapture videoCapture){this.videoCapture = videoCapture;
for(int i = 0; i < 3; i++){
images.add(new Mat());
imageIds[i] = 0;
}}//Функция отдаёт копию считанного в потоке изображения.privatelong 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
publicvoid run(){//Здесь в отдельном потоке постоянно считываем изображения с камеры и сохраняем в массив.while(!terminated){if(videoCapture.read(images.get(currentWritingImage))){
imageIds[currentWritingImage] = currentImageId++;
synchronized(lock){
currentWritingImage++;
if(currentWritingImage >= images.size())
currentWritingImage = 0;
}}}}//Функция отдаёт браузеру HTTP-заголовок.privatestaticvoid writeHeader(OutputStream stream, String boundary)throwsIOException{
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());
}//Функция отдаёт браузеру изображение.privatestaticvoid writeJpg(OutputStream stream, MatOfByte image, String boundary)throwsIOException{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 для каждого изображения полученного с камеры, чтобы найти на нём лица. А дальше уже мы можем по-разному управлять нашим роботом в зависимости от результата.
Кстати, чтобы хоть как-то повысить скорость получения изображения с камеры, я сделал это в отдельном потоке. Так работает немного быстрее. Но, если вы хотите получать видео в основном потоке, откомментируйте строку
Теперь расскажу о режиме отладки. Чтобы в него перейти, нужно после запуска программы нажать клавишу вверх на модуле EV3. После этого на экране EV3 появится следующая надпись:
Connect to
http://10.0.1.1:8080
После этого вы можете подключиться к EV3 с помощью вашего браузера. Сначала убедитесь, что ваш EV3 подключен к компьютеру через Bluetooth. Затем сделайте текстовый файл с расширением .html, напишите в нём следующий текст
и откройте этот файл в браузере Chrome, Firefox или Microsoft Edge. Как только вы это сделаете, робот начнёт поворачиваться на месте, а в браузере будет отображаться видео, полученное с камеры. Картинка маленькая, но этого вполне достаточно, чтобы обнаружить на ней лицо.
На экране в это время будет написано следующее:
Detected 1 face(s)
Debug mode: faces
Как вы уже поняли, здесь написано количество найденных лиц и режим отладки. Нажимая на центральную кнопку модуля EV3, вы можете переключать режим отладки. Кроме режима faces, в котором отображаются картинка с камеры и рамки, вокруг найденных лиц, есть ещё режимы gray, source и none. В режиме gray вы увидите картинку, переведённую в оттенки серого, а именно на картинке такого формата библиотека OpenCV ищет объекты. В режиме source – картинка, полученная с камеры без изменений. В режиме none – изображение не передаётся в браузер.
Итог
Вот и всё что мне хотелось написать про обнаружение лиц с помощью EV3. Если у вас есть вопросы, смело задавайте их в комментариях.
А вот литература, которая помогала мне делать этот эксперимент:
Комментарии
RSS лента комментариев этой записи