воскресенье, 28 января 2018 г.

Voic3 Command3R

Юбилейный, сотый, проект от "Карандаша и Самоделкина" просто обязан быть чем-то особенным! В прошлых сериях мы успели поработать в области машинного зрения, и наш робот на базе LEGO Mindstorms EV3 смог различать простейшие образы и даже сыграть с нами в "Камень, ножницы, бумага". На этот раз мы решили освоить работу со звуком и решить одну из задач, связанных с "машинным слухом".

Сделать робота, управляемого голосом, не так уж и сложно - достаточно воспользоваться готовым API от того же Google. В этом случае распознаваемый фрагмент отсылается онлайн-сервису, который в ответ сообщает распознанную текстовую строку. Если роботу нужна подобная автономная функциональность, существуют и готовые оффлайновые решения, вроде PocketSphinx, правда работают они в условиях ограниченных ресурсов на роботе крайне задумчиво. Главной же фишкой нашего проекта станет то, что мы не будем использовать никакие вспомогательные инструменты для распознавания речи, а напишем свой собственный "движок" для голосового управления, пусть и простенький.


Перво-наперво нам понадобится устройство, способное дать возможность роботу физически ощущать звуковую волну для возможности преобразования ее в цифровую форму. В комплекте с наборами LEGO NXT первой версии поставлялся датчик звука (NXT Sound Sensor). C его помощью можно измерять звуковое давление в условных единицах. Такого датчика у нас нет, поэтому мы решили использовать USB-микрофон, подключенный в соответствующий порт блока EV3. Что касается датчика звука NXT, то по информации от его владельцев, скорость опроса этого датчика невелика, поэтому на него рассчитывать все равно не стоит.
В качестве USB-микрофона может выступать почти любая USB-звуковая плата, с подключенным к ней аналоговым микрофоном или USB-веб-камера, как правило имеющая встроенный микрофон. EV3-блок тоже имеет в своем составе звуковую плату, к выходу которой подключен встроенный динамик, а вот встроенного микрофона, увы, нет..Максимальная частота дискретизации этого устройства - 22кГц, поэтому качественно воспроизводить звук EV3-блок не может физически.

При подключении дополнительного USB звукового устройства появляется возможность работать с ними одновременно, например получая данные с микрофона внешней звуковой платы и воспроизводя звук по прежнему встроенным динамиком. Возможна и конфигурация, при которой звук будет воспроизводиться внешней USB звуковой платой, при этом появляется возможность подключить к роботу качественную или "громкую" акустику.
В проекте мы снова используем операционную систему ev3dev и будем программировать робота на языке Python. Для работы с звуковыми устройствами существует целый ряд python-модулей, мы остановили свой выбор на pyalsaaudio, обладающим высоким быстродействием в условиях ограниченных ресурсов. Для ускорения обработки данных и воспользуемся модулем NumPy. Хотя можно хранить данные и в традиционных для Python структурах, вроде списков, однако работа с звуковыми потоками, имеющими зачастую немалый объем, при таком подходе происходит крайне неторопливо.


Общий алгоритм работы нашего робота таков:
1) Стартуем процесс записи и анализируем приходящие со звуковой платы данные небольшими порциями, оценивая громкость. Как только она превысит заданный порог - начинаем запись заданной (избыточной для фразы) длительности, например 2 секунлы
2) Анализируем полученный звуковой отрезок (сэмпл) на предмет выделения на нем слова (фразы).


Так как мы начинали запись активацией по пороговому значению, слово всегда будет в начале сэмпла, а вот конец нужно обрезать:


3) Воспроизводим сэмпл для мониторинга корректности его записи и выделения слова.
4) Разбиваем звуковой фрагмент на небольшие интервалы, длительностью около 20 мс. Лучший результат получается если разбивать на интервалы, перекрывающиеся н 50%, в этом случае скорость произношения фразы не будет заметно влиять на результат распознавания.


5) Анализируем каждый отрезок, выделяя в нем частоты с максимальной "громкостью". Частотный анализ фрагмента можно выполнить используя встроенную в NumPy функция быстрого преобразования Фурье.


На рисунке выделено 8 таких частот.
6) Получаем двумерную матрицу, содержащую несколько наиболее выраженным частот в каждом отрезке:


7) Сохраняем матрицу в виде именованного образца. Для каждого слова (голосовой команды) нужно записать несколько таких образков, с разной интонацией, можно разными голосами, если это необходимо.
8) Чтобы распознать слово-команду необходимо выполнить наги 1-5 и сравнить полученную матрицу с сохраненными образцами. Здесь есть несколько вариантов:
  • Найти самый похожий образец
  • Найти группу образцов, к которой анализируемый ближе всего
  • Скомбинировать поиск по близости к образцу и по близости к группе образцов. Если, например, анализируемый образец ближе всего к группе с образцами "Влево" и, одновременно, ближе в образцу "Вправо", у робота появляется возможность ответить "Не понял команды, повторите!".
  • Использовать нейронную сеть, обученную на записанных образцах.
В проекте мы используем третий вариант, а четвертый в данный момент изучаем.

суббота, 27 января 2018 г.

MindSnake3D

Пару недель назад в нашей группе VK был запущен опрос, для чего предназначен робот на фото?


Подсказки были таковы:
1) проект носит развлекательный характер
2) робот использует трехмерные массивы
3) разработка проекта ведется на языке С, однако сам проект тесно связан с другим языком программирования, который нам очень нравится
4) Шелдону Куперу он наверняка пришелся бы по душе.


Пришла пора приоткрыть завесу тайны. На каникулах мы тоже любим отдыхать, поэтому наш очередной проект, о котором сегодня пойдет речь, действительно носит развлекательный характер. Наверное многие из вас играли в знаменитую игру Snake ("Змейка"), она портирована на множество различных платформ, а наибольшую популярность получила будучи предустановленной на телефонах Nokia (подсказка №3: Питон - змея).


Змейка ползает по полю, поедая пищу, удлинняясь от каждой съеденной порции. Если змейка врезается в ограждение поля или в свой хвост - она погибает и игра начинается сначала. Цель игры - вырастить как можно более длинную змейку.
Мы давно хотели сделать эту игру на платформе LEGO Mindstorms, но интересная реализация никак не вырисовывалась. Создавать очередной клон на экране NXT или EV3 блока нам не хотелось, а реализовать змейку в механике похоже не по зубам даже гуру LEGO-роботостроения. 
Наконец, когда пришла пора изучить трехмерные массивы - все сложилось и идея обрела законченную форму. Далеким прототипом стал наш наш проект 2015 года - EV3 Муха. В этой игре в уме (или "мыслилке", как мы называем такого типа развлечения) мы должны были двигать воображаемую муху по плоскому полю. 
В проекте MinSnake мы добавим пищи для ума в виде еще одного измерения. Наша змейка будет ползать в 3D-кубе, который игрок должен представить в уме, управление головой змеи мы реализовали в конструкции. Все игровые события робот озвучивает голосом, экран блока в геймплее не используется. Змейка как и в классическом варианте не должна выползти за пределы игрового поля (куба) и не должна врезаться сама в себя (в свой хвост). Заморочено, правла? Шелдону Куперу точно бы понравилось!



Исходные данные таковы:
- куб имеет размер 3x3 пиксела
- змейка на старте имеет длину = 1, стартует из центра (ячейка 2,2,2)
- стартовое направление в начале игры - направо
- в зависимости от выбранного уровня сложности на ход дается в среднем 5 секунд, за это время игрок должен выбрать одно из 6 направлений движения (вверх-вниз-влево-вправо-к себе-от себя), в соответствии с которым змейка передвинется по истечении времени хода. На каждый следующий ход дается на несколько миллисекунд меньше чем на предыдущий
- еда появляется в кубе случайным образом, на свободном от змейки месте. Робот называет ячейку где именно. Чтобы не путаться в координатах озвучивается слой сверху вниз (1-2-3) и номер ячейки 1..9 по телефонному принципу (см. рис)


- при совпадении головы змеи с ячейкой, в которой лежит еда, змейка удлинняется - при движении в такую ячейку хвост не подтягивается, за счет чего обеспечивается рост
- если змейка не успела за отведенное время доползти до ячейки с едой, пища исчезает и появляется в новом месте. Время зависит от текущего уровня сложности
- по достижению заданной на данном увроне длины змейки игра переходит на следующий уровень сложности (3 уровня).


Инструкцию по сборке нашего проекта в формате LEGO Digital Designer и программу к роботу вы можете скачать по ссылке.



Исходный код программы на языке NXC:
struct coord{
  int x;
  int y;
  int z;
};

coord head;
coord eda;
coord max;

int snake[3][3][3];

void food(){
  while(true){
    eda.x=Random(2999)/1000.0;
    eda.y=Random(2999)/1000.0;
    eda.z=Random(2999)/1000.0;
    if(snake[eda.x][eda.y][eda.z]==0){
      break;
    }
  }
}

int otkl(int a, int b)
{
  return ((a+36000)%360 + 540 - b)%360 - 180;
}

int direct(){
  if(abs(otkl(MotorRotationCount(OUT_B),0))<=45){
    return 4;
  }
  if(abs(otkl(MotorRotationCount(OUT_B),180))<=45){
    return 5;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),0))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 3;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),0))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 2;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),90))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 1;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),90))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 0;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),180))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 3;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),180))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 2;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),270))<=45 && abs(otkl(MotorRotationCount(OUT_B),90))<=45){
    return 1;
  }

  if(abs(otkl(MotorRotationCount(OUT_A),270))<=45 && abs(otkl(MotorRotationCount(OUT_B),270))<=45){
    return 0;
  }

  return -1;
}
int time_hod=5000;
int direction=4;//0-туда1-обратно2-вверх3-вниз4-вправо5-влево
int eda_farther=0;
int maxa=0;
int direction_old=4;
long ti;
int len=1;
int level=0;
int len_up;
task main()
{
  ClearScreen();
  //TextOut(0,16,"Easy");
  //TextOut(0,8,"Normal");
  //TextOut(0,0,"Hard");
  Wait(1000);
  while(ButtonPressed(BTNCENTER,0)
  && ButtonPressed(BTNLEFT,0)
  && ButtonPressed(BTNRIGHT,0)){

  }
  if(ButtonPressed(BTNCENTER,1)){
    level=2;
    len_up=9;
    time_hod=5000;
    ClearScreen();
  //  TextOut(0,0,"Normal");
  }
  if(ButtonPressed(BTNLEFT,1)){
    level=1;
    len_up=3;
    time_hod=5000;
    ClearScreen();
  //  TextOut(0,0,"Easy");
  }
  if(ButtonPressed(BTNRIGHT,1)){
    level=3;
    len_up=9;
    time_hod=3000;
    ClearScreen();
  //  TextOut(0,0,"Hard");
  }
  Wait(3000);

  eda.x=10;
  eda.y=10;
  eda.z=10;
  head.x=1;
  head.y=1;
  head.z=1;
  PlayFile("start.rso");
  PlayFile("center.rso");
  ResetRotationCount(OUT_AB);
  direction=direct();
  for(int i=0;i<=2;i++){
    for(int z=0;z<=2;z++){
      for(int q=0;q<=2;q++){
        snake[i][z][q]=0;
      }
    }
  }
  snake[1][1][1]=1;
  long time=CurrentTick();
  long time_eda=CurrentTick();
  while(true){
    if(len==len_up && level==3){
      break;
      PlayFile("levelup.rso");
    }
    if(len==len_up && level==2){
      level=3;
      len_up=9;
      time_hod=3000;
      PlayFile("levelup.rso");
      Wait(1000);
    }
    if(len==len_up && level==1){
      level=2;
      len_up=6;
      PlayFile("levelup.rso");
      Wait(1000);
    }

    direction_old=direction;
    direction=direct();

    if(direction!=direction_old){
      switch(direction)
      {
      case 0:
        PlayFile("farther.rso");
        Wait(1000);
        break;
       case 1:
         PlayFile("closer.rso");
         Wait(1000);
         break;
       case 2:
         PlayFile("up.rso");
         Wait(1000);
         break;
       case 3:
         PlayFile("down.rso");
         Wait(1000);
         break;
       case 4:
         PlayFile("right.rso");
         Wait(1000);
         break;
       case 5:
         PlayFile("left.rso");
         Wait(1000);
         break;
       default:
         break;
       }
    }
    if(CurrentTick()-time_eda>=20000){
      time_eda=CurrentTick();
      food();
      PlayFile("food.rso");
      Wait(1000);
      if(eda.y==0){
        PlayFile("3.rso");
        Wait(1000);
      }
      if(eda.y==1){
        PlayFile("2.rso");
        Wait(1000);
      }
      if(eda.y==2){
        PlayFile("1.rso");
        Wait(1000);
      }
      if(eda.z==2){
        if(eda.x==0){
          PlayFile("1.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("2.rso");
          Wait(1000);
        }
        if(eda.x==2){
          PlayFile("3.rso");
          Wait(1000);
        }
      }
      if(eda.z==1){
        if(eda.x==0){
          PlayFile("4.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("5.rso");
          Wait(1000);
        }
        if(eda.x==2)
        {
          PlayFile("6.rso");
          Wait(1000);
        }
      }
      if(eda.z==0){
        if(eda.x==0){
          PlayFile("7.rso");
          Wait(1000);
        }
        if(eda.x==1){
          PlayFile("8.rso");
          Wait(1000);
        }
        if(eda.x==2){
          PlayFile("9.rso");
          Wait(1000);
        }
      }

    }
    if(CurrentTick() - ti>=1000){
      ti=CurrentTick();
 //     ClearScreen();
     // NumOut(10,0,eda.z);
    // / NumOut(10,8,eda.y);
    //  NumOut(10,16,eda.x);
    //  NumOut(0,0,head.z);
   ///   NumOut(0,8,head.y);
    //  NumOut(0,16,head.x);
    //  NumOut(0,24,direction);
     // NumOut(5,0,len);
    }
    if(CurrentTick()-time>=time_hod){
      PlayTone(440,100);
      time_hod=time_hod-10;
     
      time=CurrentTick()
      if(head.x==2 && direction==4
      || head.x==0 && direction==5
      || head.y==2 && direction==2
      || head.y==0 && direction==3
      || head.z==2 && direction==0
      || head.z==0 && direction==1){
        break;
      }
   
      eda_farther=0;
   
      if(direction==0 && head.x==eda.x && head.y==eda.y && head.z+1==eda.z){
        eda_farther=1;
      }
      if(direction==1 && head.x==eda.x && head.y==eda.y && head.z-1==eda.z){
        eda_farther=1;
      }
      if(direction==2 && head.x==eda.x && head.y+1==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==3 && head.x==eda.x && head.y-1==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==4 && head.x+1==eda.x && head.y==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      if(direction==5 && head.x-1==eda.x && head.y==eda.y && head.z==eda.z){
        eda_farther=1;
      }
      for(int i=0;i<=2;i++){
        for(int z=0;z<=2;z++){
          for(int q=0;q<=2;q++){
            if(snake[i][z][q]!=0){
              snake[i][z][q]+=1;
            }
          }
        }
      }
      max=0;
      for(int i=0;i<=2;i++){
        for(int z=0;z<=2;z++){
          for(int q=0;q<=2;q++){
            if(snake[i][z][q]>maxa){
              maxa=snake[i][z][q];
              max.x=i;
              max.y=z;
              max.z=q;
            }
          }
        }
      }

      if(eda_farther==0){
        snake[max.x][max.y][max.z]=0;
      }
      else{
        len++;
        time_eda=CurrentTick();
        eda.x=10;
        eda.y=10;
        eda.z=10;
        PlayFile("len.rso");
        Wait(1000);
      }
      maxa=0;
      max.x=0;
      max.y=0;
      max.z=0;
      if(direction==0){
        if(snake[head.x][head.y][head.z+1]==0){
          snake[head.x][head.y][head.z+1]=1;
          head.z++;
        }
        else{
          break;
        }
      }
      if(direction==1){
        if(snake[head.x][head.y][head.z-1]==0){
          snake[head.x][head.y][head.z-1]=1;
          head.z--;
        }
        else{
          break;
        }
      }
      if(direction==2){
        if(snake[head.x][head.y+1][head.z]==0){
          snake[head.x][head.y+1][head.z]=1;
          head.y++;
        }
        else{
          break;
        }
      }
      if(direction==3){
        if(snake[head.x][head.y-1][head.z]==0){
          snake[head.x][head.y-1][head.z]=1;
          head.y--;
        }
        else{
          break;
        }
      }
      if(direction==4){
        if(snake[head.x+1][head.y][head.z]==0){
          snake[head.x+1][head.y][head.z]=1;
          head.x++;
        }
        else{
          break;
        }
      }
      if(direction==5){
        if(snake[head.x-1][head.y][head.z]==0){
          snake[head.x-1][head.y][head.z]=1;
          head.x--;
        }
        else{
          break;
        }
      }
      direction=direct();
    }
  }
  PlayFile("end.rso");
  Wait(1000);
}


Самое популярное