воскресенье, 26 ноября 2017 г.

GeneLINEr

Последнее время вокруг все больше разговоров об искусственном интеллекте, то там то тут звучат модные термины "нейросети" и "генетические алгоритмы". В прошлых проектах (НейроКачели, НейроБашня и N3uralV1s10n) мы уже создавали простейшие нейронные сети, разобрались с тем что это такое в первом приближении и как они работают. Похоже пришло время сделать тоже самое с генетическими алгоритмами.

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



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

Замечание: данный проект не несет в себе ни оттенка соревновательной составляющей. Наша основная цель не в том, чтобы "вывести" быстрого гонца по линии, мы хотим получить опыт использования алгоритмов генетического типа с целью дальнейшего их использования в близкой нам по духу хоббийной робототехнике.

Начнем с конструкции робота. Это традиционная двухмоторная тележка на базе LEGO Mindstorms NXT, в передней части установлено 4 датчика освещенности, два из которых (внутренних, подключенных к портам 2 и 3) используются ПИД-регулятором робота для движения по линии, а два внешних(подключенных соответственно к портам 1 и 4) - для контроля срыва с линии в процессе обучения. Инструкцию в формате LEGO Digital Designer можно скачать по ссылке.



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

Алгоритм в общем виде выглядит следующим образом:


Для создания первой популяции виртуальных роботов давайте, для начала, опишем пользовательскую структуру данных, которая будет использоваться в качестве шаблона для создания особей в популяции.

struct person
{
  // у каждой особи должно быть имя, хотя бы codename
  string name;
  // номер поколения, в котором родилась данная особь
  int generation;
  // энергичность (быстрота) особи
  int speed;
  // скорость реакции
  float reaction;
  // мудрость (память) особи
  float memory;
  // интуиция (проницательность)
  float intuition;
  // степень доминантности особи
  float dominance;
  // пройденный особью путь за отведенный на тестирование промежуток времени
  int path;
};

Теперь создадим первую популяцию на основе этого шаблона, в ней у нас будет 6 особей:

person robot[6];

Генерируем случайным образом свойства особей первой популяции:

for (int i=0;i<6;i++){
  robot[i].name = "GeneLINEr_"+NumToStr(Random(1000));
  robot[i].generation=1;
  // энергичность (быстрота) особи 0..100
  robot[i].speed = Random(70)+30;
  // скорость реакции 0..3
  robot[i].reaction = Random(3000)/1000;
  // мудрость (память) особи 0..0,1
  robot[i].memory = Random(100)/1000;
  // интуиция (проницательность) 0..3
  robot[i].intuition = Random(3000)/1000;
  // степень доминантности особи не понятна до тестирования
  robot[i].dominance = 0;
  // пройденный особью жизненный путь = 0
  robot[i].path = 0;
}

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


В роботе реализована функция ПИД-регулятора движения по линии, принимающий на вход параметры Kp, Ki, Kd, скорость робота, выполняющая 5 секундное движение по линии и возвращающая длину пройденного пути, сглаженного до криволинейной траектории.

long pid(float Pk,float Ik,float Dk,int speed){
  long B=0;
  long C=0;
  long path=0;
  long MC=MotorRotationCount(OUT_C);
  long MB=MotorRotationCount(OUT_B);
  long e = 0;
  int porog=28;
  float ERRo=0;
  float ERR=0;
  float u=0;
  float z1=0;
  float z2=0;
  long tmp=CurrentTick();
  int p;
  int i;
  int d;
  while(CurrentTick()-tmp<=5000){
    MC=MotorRotationCount(OUT_C);
    MB=MotorRotationCount(OUT_B);
    ERR=Sensor(IN_3)-Sensor(IN_2);
    p=Pk*ERR;
    d=Dk*(ERR-ERRo);
    i=Ik*e;
    if(i>10)i=10;
    if(i<-10)i= -10;
    u=p+i+d;
    z1=speed-u;
    z2=speed+u;
    if(speed-u>100)z1=100;
    if(speed-u<-100)z1=-100;
    if(speed+u>100)z2=100;
    if(speed+u<-100)z2=-100;
    OnFwd(OUT_B,z1);
    OnFwd(OUT_C,z2);
    if(Sensor(IN_1)<=porog){
      PlayTone(TONE_C5, MS_500);
      RotateMotorEx(OUT_BC, 35, 100, 100, true, true);
      go_to_line();
      break;
    }
    if(Sensor(IN_4)<=porog){
      PlayTone(TONE_C5, MS_500);
      RotateMotorEx(OUT_BC, 35, 100, -100, true, true);
      go_to_line();
      break;
    }
    ERRo=ERR;
    e+=ERR;
    B=MotorRotationCount(OUT_B)-MB;
    C=MotorRotationCount(OUT_C)-MC;
    if(B>0 && C>0){
      if(B<C){
        path+=B;
      }
      else{
        path+=C;
      }
    }
  }
  Off(OUT_BC);
  return path;
}

void go_to_line(){
  float P=1.0;
  float D=1.0;
  float ERRo=0;
  float ERR=0;
  float u=0;
  while(abs(Sensor(IN_2)-Sensor(IN_3))>5){
    int ERR=Sensor(IN_3)-Sensor(IN_2);
    int u=P*ERR+D*(ERR-ERRo);
    int z1=-u;
    int z2=+u;
    if(z1>100) z1=100;
    if(z1<-100) z1=-100;
    if(z2>100) z2=100;
    if(z2<-100) z2=-100;
    OnFwd(OUT_B,z1);
    OnFwd(OUT_C,z2);
  }

}

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

for (int i=5;i>=0;i--){
  robot[i].path = pid(robot[i].reaction,
    robot[i].memory,robot[i].intuition,robot[i].speed);
  PlayTone(TONE_A4, MS_500);
}


После испытания всех особей текущего поколения ранжируем их в порядке убывания пройденного за время тестирования пути. Так как время на тест для каждой особи фиксировано - 5 секунд, соответственно у особей убывает и скорость. Будем использовать пузырьковую сортировку и "временную особь", для перестановки пар при ранжировании.

person robot_tmp;

for (int i=0;i<5;i++){
  for (int j=0;j<(5-i);j++){
    if(robot[j].path < robot[j+1].path){
      robot_tmp = robot[j];
      robot[j]=robot[j+1];
      robot[j+1]= robot_tmp;
    }
  }
}

Чем выше у особи рейтинг, тем выше и доминантность данной особи, соответственно тем большую часть свойст данной особи унаследуют ее потомки (выше = ближе к 1):

for (int i=0;i<6;i++){
  robot[i].dominance=i+1;
}

Две особи, самые слабые в популяции (5 и 6 в ранжированном списке) умирают, остальные дают потомство, порождая 6 особей новой популяции, наследующих черты родительских особей. В скрещивании участвуют доминантные признаки особей, давая соотношение унаследованных признаков. Унаследованный признак новорожденного кроме этого подвергается колебанию в 20%, для ускорения эволюции.

person newborn(person male, person female){
  person newburn;
  male.dominance = 1 - (male.dominance/(male.dominance+female.dominance));
  female.dominance = 1 - male.dominance;
  newburn.name = "GeneLINEr_"+NumToStr(Random(1000));
  newburn.generation=male.generation+1;
  if(male.speed>female.speed){
    newburn.speed = male.speed;
  }
  else{
    newburn.speed=female.speed;
  }
  newburn.reaction = male.reaction * male.dominance + female.reaction * female.dominance;
  newburn.reaction =newburn.reaction *((Random(40)+80)/100.0);
  newburn.memory = male.memory * male.dominance + female.memory * female.dominance;
  newburn.memory =newburn.memory *((Random(40)+80)/100.0);
  newburn.intuition = male.intuition * male.dominance + female.intuition * female.dominance;
  newburn.intuition = newburn.intuition *((Random(40)+80)/100.0);
  newburn.path = 0;
  newburn.dominance = 0;
  if(newburn.speed>max_speed){
    max_speed=newburn.speed;
  }
  return newburn;
}

// новая популяция из 6 особей
person robot_next_generation[6];

robot_next_generation[0] = newborn(0,1);
robot_next_generation[1] = newborn(0,2);
robot_next_generation[2] = newborn(0,3);
robot_next_generation[3] = newborn(1,2);
robot_next_generation[4] = newborn(1,3);
robot_next_generation[5] = newborn(2,3);

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

// Мутация
void mutants(){
  int property=Random(3);
  int mutant=Random(5);;
  if(mutant<4){
    switch(property){
      case 0:
        robot_next_generation[mutant].speed = max_speed+5;
        break;
      case 1:
        robot_next_generation[mutant].reaction =robot_next_generation[mutant].reaction *((Random(40)+80)/100.0);
        break;
      case 2:
        robot_next_generation[mutant].memory =robot_next_generation[mutant].memory *((Random(40)+80)/100.0);
        break;
      case 3:
        robot_next_generation[mutant].intuition = robot_next_generation[mutant].intuition *((Random(40)+80)/100.0);
        break;
      default:
        break;
    }
  }
  else{
    switch(property) {
      case 0:
        robot_next_generation[mutant].speed = max_speed+5;
        break;
      case 1:
        robot_next_generation[mutant].reaction = Random(3000)/1000;
        break;
      case 2:
        robot_next_generation[mutant].memory = Random(100)/1000;
        break;
      case 3:
        robot_next_generation[mutant].intuition = Random(3000)/1000;
        break;
      default:
        break;
    }
  }
}

Теперь производим смену популяций. Дети сменяют родителей:

for(int i=0;i<6;i++){
  robot[i]=robot_next_generation[i];
}

Теперь снова пришла пора испытать новое поколение, более приспособленное к решению поставленной задачи, оценить возросшую скорость,, выявить сильнейших и так до тех пор, пока результат не будет нас устраивать.


На этапе ранжирования можно сохранять информацию о свойствах особей в каждой популяции в файл.

byte fh;
int msg_len;
string logstr;

for(int i=0;i<6;i++){
  logstr = robot[i].name;
  logstr = logstr + ";" + NumToStr(robot[i].generation);
  logstr = logstr + ";" + NumToStr(robot[i].reaction) +";"+NumToStr(robot[i].memory)+";"+NumToStr(robot[i].intuition);
  logstr = logstr + ";" + NumToStr(robot[i].path)+";"+NumToStr(robot[i].speed);
  msg_len = StrLen(logstr);
  WriteLnString(fh, logstr, msg_len);
}

По данным, накопленным в файле в процессе работы программы, можно представить ход эволюции в виде наглядного графика (ось X - номер поколения, ось Y - средняя скорость особи):

воскресенье, 15 октября 2017 г.

N3uralV1s10n

Камера, подключенная к LEGO Mindstorms EV3 уже давно не является чем-то необычным. Конечно, в комплекте с набором ее нет, да и стандартное ПО от LEGO лишено возможности ее использования, но с появлением "прошивок" от сторонних разработчиков, таких как ev3dev и leJOS, появилась возможность подключить практически любую современную веб-камеру с USB-интерфейсом.
В нашем сегодняшнем проекте мы будем использовать камеру в качестве элемента системы машинного зрения, запрограммировав на Python простейшую нейронную сеть для распознавания образов.


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

Для получения информации с камеры мы будем использовать легковесный (по сравнению с OpenCV) модуль PyGame. Он не установлен в ev3dev "из коробки", но его можно доустановить используя менеджер модулей pip.

Конструкция у робота незамысловатая, по сути это крепление для камеры и листа бумаги А4, но тем не менее мы традиционно выкладывает инструкцию по ее сборке в формате LEGO Digital Designer, скачать ее можно по ссылке.


Мы используем камеру Logitech C110, это простейшая веб-камера с разрешением 640x480, которая имеет поддержку со стороны Linux. В конструкции используется пара датчиков-кнопок подключенных к 1 и 4 портам - они используются для "поощрения" и "наказания" нейронной сети в процессе обучения, и означают, соответственно, "Да" и "Нет".

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

Иску́сственная нейро́нная се́ть  - математическая модель, а также её программное или аппаратное воплощение, построенная по принципу организации и функционирования биологических нейронных сетей - сетей нервных клеток живого организма. Это понятие возникло при изучении процессов, протекающих в мозге, и при попытке смоделировать эти процессы. 
Искусственный нейрон — это такая функция, которая преобразует несколько входных фактов в один выходной.

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

В силу довольно скромной производительности блока EV3 мы не будем работать с полным разрешением камеры, мы снизим его в программе до 16x16 пикселов, что вполне достаточно для решения учебной задачи.



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

Первый алгоритм заключается в следующем:
1) В памяти робота перечисляются сущности, которые он сможет отличать друг от друга. Для каждой сущности создается нейрон сети с 16x16=256 входами и 1 выходом. Веса на входах нейронов в начале одинаковы у всех входов и всех нейронов.
2) Роботу показывается сущность из числа тех, которые перечислены в его памяти, он пытается угадать что это такое. Поначалу, конечно, он в большинстве случаев ошибается. Человек нажимает кнопку "Да", если робот угадал (в этом случае мы увеличиваем веса входов соответствующего нейрона, на которых были не белые пиксели), и кнопку "Нет", если не угадал (в этом случае уменьшаем веса входов соответствующего нейрона с закрашенными пикселами).
3) Робот пытается угадать снова и пересчитывает веса на входах нейронов до тех пор, пока не научится стабильно распознавать все сущности из имеющегося у него списка.

Код первой программы на Python выглядит следующим образом::

from ev3dev.ev3 import *
import pygame
import time
import pygame.camera
from random import random
from PIL import Image, ImageDraw, ImageFont

lcd = Screen()
btn = Button()

res = 16

S1 = TouchSensor("in1")
S2 = TouchSensor("in4")

buf = [ [0] * res for i in range(res)]

class number:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * res for i in range(res)]
               
myNumbers = [number(1), number(2), number(3), number(4)]

def camera_update(x):
    for i in range(x):
         
        image = cam.get_image()
        
        image = pygame.transform.scale(image,(res,res))
        image2buf(image)
        for i in range(res):
            for j in range(res):
                if buf[i][j] == 0:
                    lcd.draw.rectangle((i*8+25, j*8, i*8+7+25, j*8+7),fill='white')
                else:
                    lcd.draw.rectangle((i*8+25, j*8, i*8+7+25, j*8+7),fill='black')
        lcd.update()

def write(n):
    f = ImageFont.truetype('FreeMonoBold.ttf', 175)
    lcd.draw.text((30,-15), str(n), font=f)
    lcd.update()

def image2buf(surf):
    width, height = surf.get_size() 
    for y in range(height): 
        for x in range(width): 
            red, green, blue, alpha = surf.get_at((x, y)) 
            L = 0.3 * red + 0.59 * green + 0.11 * blue
            if L > 100:
                buf[x][y] = 0
            else:
                buf[x][y] = 1
      
pygame.init()
pygame.camera.init()
cameras = pygame.camera.list_cameras()
cam = pygame.camera.Camera(cameras[0])
cam.start()

f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,50), "N3uralV1s10n", font=f)
lcd.update()
Sound.speak("nerual vision programm 1").wait()
time.sleep(2)

lcd.clear()

str1 = "please put"
str2 = "first object"
str3 = "and press enter"
lcd.draw.text((0,30), str1, font=f)
lcd.draw.text((0,55), str2, font=f)
f = ImageFont.truetype('FreeMonoBold.ttf', 20)
lcd.draw.text((0,80), str3, font=f)
lcd.update()
Sound.speak("please put first object and press enter").wait()

while(True):
    if(btn.enter): break
lcd.clear()

while(True):
    camera_update(15)
     
    image = cam.get_image()
    
    image = pygame.transform.scale(image,(res,res))
    image2buf(image)
     
    for i in range(res):
        for j in range(res):
            if buf[i][j] == 0:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='white')
            else:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='black')

    lcd.update()
    
    for o in myNumbers:
        o.sum = 0
    for o in myNumbers:    
        for i in range(res):
            for j in range(res):
                o.sum += buf[i][j] * o.picture[i][j]

    max_sum = -100000

    for num in myNumbers:
        if num.sum > max_sum:
            max_sum = num.sum
            tmp_obj = num
    
    lcd.clear()
    write(tmp_obj.name)
    
    Sound.speak("It is "+str(tmp_obj.name)).wait()
    while(True):
        if(S1.value()): break
        if(S2.value()): break

    if(S1.value()): 
        Sound.speak("ok yes").wait()
        a = 1
    else: 
        Sound.speak("no no").wait()
        a = -1
        
    for i in range(res):
        for j in range(res):
            if(buf[i][j] == 1):
                tmp_obj.picture[i][j] += a
   
    Sound.speak("put a new object and press enter").wait()
    while(True):
        if(btn.enter): break
        if(btn.backspace):
           Sound.speak("Exit programm").wait() 
           exit()    
    lcd.clear()

cam.stop()   

Второй алгоритм несколько отличается:
1) Изначально память робота пуста.
2) Показываем ему объект и нажимаем кнопку "Запомни эту сущность".
3) Выбираем имя для объекта кнопками на блоке.
4) В памяти робота формируется нейрон с 16x16=256 входами, при этом веса входов, которые видят закрашенные пиксели выше, чем входов с белыми пикселами.
5) показываем роботу следующий объект, он пытается сопоставить его с теми, что уже знает.
6) если робот угадал, поощряем его, нажимая "Да" (выполнится усиление связей с пересчетом весов на входах соответствующего нейрона). Если робот не угадал уже знакомый ему объект, нажимаем "Нет" (ослабляем связи), если объект новый для робота - нажимаем "Запомни эту сущность" и переходим к п. 2

Код второй программы на Python выглядит так::

from ev3dev.ev3 import *
import pygame
import time
import pygame.camera
from random import random
from PIL import Image, ImageDraw, ImageFont

lcd = Screen()
btn = Button()

res = 16

S1 = TouchSensor("in1")
S2 = TouchSensor("in4")

buf = [ [0] * res for i in range(res)]

class number:
    def __init__(self, n):
        self.name = n
        self.sum = 0
        self.picture = [ [0] * res for i in range(res)]
               

myNumbers = []

def camera_update(x):
    for i in range(x):
         
        image = cam.get_image()
        
        image = pygame.transform.scale(image,(res,res))
        image2buf(image)
        for i in range(res):
            for j in range(res):
                if buf[i][j] == 0:
                    lcd.draw.rectangle(((i*8+25), j*8, (i*8+25)+7, j*8+7),fill='white')
                else:
                    lcd.draw.rectangle(((i*8+25), j*8, (i*8+25)+7, j*8+7),fill='black')
        lcd.update()

def write(n):
    f = ImageFont.truetype('FreeMonoBold.ttf', 175)
    lcd.draw.text((30,-15), str(n), font=f)
    lcd.update()

def image2buf(surf):
    width, height = surf.get_size() 
    for y in range(height): 
        for x in range(width): 
            red, green, blue, alpha = surf.get_at((x, y)) 
            L = 0.3 * red + 0.59 * green + 0.11 * blue
            if L > 100:
                buf[x][y] = 0
            else:
                buf[x][y] = 1
      
pygame.init()
pygame.camera.init()
cameras = pygame.camera.list_cameras()
cam = pygame.camera.Camera(cameras[0])
cam.start()

lcd.clear()

f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,50), "N3uralV1s10n", font=f)
lcd.update()
Sound.speak("neural vision programm 2").wait()
time.sleep(2)
lcd.clear()

str1 = "please put"
str2 = "first object"
str3 = "and press enter"
f = ImageFont.truetype('FreeMonoBold.ttf', 25)
lcd.draw.text((0,30), str1, font=f)
lcd.draw.text((0,55), str2, font=f)
f = ImageFont.truetype('FreeMonoBold.ttf', 20)
lcd.draw.text((0,80), str3, font=f)
lcd.update()
Sound.speak("please put first object and press enter").wait()

while(True):
    if(btn.enter): break
lcd.clear()

while(True):
    camera_update(15)
     
    image = cam.get_image()
    
    image = pygame.transform.scale(image,(res,res))
    image2buf(image)
        
    for i in range(res):
        for j in range(res):
            if buf[i][j] == 0:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='white')
            else:
                lcd.draw.rectangle(((i+25)*8, j*8, (i+25)*8+7, j*8+7),fill='black')

    lcd.update()
    
    for o in myNumbers:
        o.sum = 0
    for o in myNumbers:    
        for i in range(res):
            for j in range(res):
                o.sum += buf[i][j] * o.picture[i][j]

    max_sum = -100000
    
    for num in myNumbers:
        if num.sum > max_sum:
            max_sum = num.sum
            tmp_obj = num
    
    lcd.clear()
    
    if(len(myNumbers)!=0): 
        write(tmp_obj.name)
    
        Sound.speak("It is "+str(tmp_obj.name)).wait()
    else: Sound.speak("I do not know object").wait()
    while(True):
        if(S1.value() and len(myNumbers)!=0): break
        if(S2.value() and len(myNumbers)!=0): break
        if(btn.enter or len(myNumbers)==0): break
    a=0
    if(S1.value() and len(myNumbers)!=0): 
        Sound.speak("ok yes").wait()
        a = 1
    elif(S2.value() and len(myNumbers)!=0): 
        Sound.speak("no no").wait()
        a = -1
    else: 
        Sound.speak("new object").wait()
        time.sleep(1)
        i = 48
        while(True):
            if(btn.enter): break
            if(btn.right): i+=1
            if(btn.left): i-=1
            if(i>90): i=48
            if(i<48): i=90
            if(i>=58 and i<=64): 
                if(btn.right): i=65
                else: i=57
            lcd.clear()
            
            time.sleep(0.15)
            write(chr(i))
        myNumbers.append(number(chr(i)))
        Sound.speak("new object it is" + chr(i)).wait()
        for i in range(res):
            for j in range(res):
                myNumbers[len(myNumbers)-1].picture[i][j] = buf[i][j]
    if(a!=0):
        for i in range(res):
            for j in range(res):
                if(buf[i][j] == 1):
                    tmp_obj.picture[i][j] += a
   
    Sound.speak("put a new object and press enter").wait()
    while(True):
        if(btn.enter): break
        if(btn.backspace): 
           Sound.speak("Exit programm").wait()
           exit()
    lcd.clear()

cam.stop()   

воскресенье, 8 октября 2017 г.

Matrix has you... или малая офисная автоматизация

"Вот две таблетки: красная и синяя...Если ты возьмешь красную таблетку, то окажешься в стране чудес и я покажу тебе, как глубоко вниз уходит кроличья нора.”Выберешь синюю - забудешь. все что увидел здесь..."



Arduino, Raspberry Pi, китайский брат ее Orange и другие прикольные штуковины зачастую используются энтузиастами в оторванных от жизни проектах. Однако иногда жизнь сама подкидывает интересные задачки, в которых их можно с успехом применить.

После переезда в новый офис на работе у руководителя команды "Карандаш и Самоделкин" встал вопрос об использовании старого олдскульного сканера, не имеющего LAN-порта. Никто не жаждал цеплять его по USB к своему компьютеру и день за днем развлекаться в стиле "Чувак, мне тут пару листочков надо отсканировать, поможешь? Кинь скан на почту?".


В ходе легкого мозгового штурма возникла идея написать программу, которая в фоне, не тревожа "хозяина сканера", будет сканировать документы и складировать их на сетевой диск. Вот только принесший документы человек должен, по идее, нажать некую кнопку, которой на раритетном сканере опять же не оказалось.

Дело за малым - нужна кнопка, лучше всего USB-шная. Arduino Micro, которая умеет притворяться USB-устройствами? Слишком жирно для такой задачи. Ее младший брат - Digispark - вот один из  самых бюджетных вариантов, которого достаточно для реализации задумки.

Плата имеет 6 портов ввода-вывода, стоит у китайцев всего 50 рублей. На борту 8 КБ памяти для программного кода, частота камня от 1 МГц до 20 МГц, питание от 1,8В до 35В (в зависимости от модификации).

Для проекта также пригодилась пара кнопок, светодиод, несколько сопротивлений, динамик от сломанных наушников и провода. В качестве корпуса - футляр от панельки еще более олдскульной кассетной автомагнитолы Nakamichi. Ребята из "Карандаша и Самоделкина" за выходные спаяли все это в кучу и вышло вот что:



Устройство подключается по USB к компьютеру со сканером. Алгоритм работы девайса следующий:
1. Человек, которому нужно отсканировать документы, нажимает красную кнопку "Начать сканирование/Следующий лист".
2. Устройство издает торжествующе-подтверждающий писк, включает светодиод и, притворившись USB-клавиатурой, посылает на компьютер комбинацию Ctrl-Shift-2. Включенный светодиод означает что сканирование не завершено.
3. На компьютере по данной комбинации запускается в фоне программа на Python (код ниже), которая, используя вызов NAPS2, сканирует лист и сохраняет во временную папку.
4. Человек может повторить п. 1-3 если хочет добавить листы в тот же PDF-документ, иначе нажимает синюю кнопку "Завершить сканирование".
5. Устройство снова пищит, выключает светодиод и посылает на компьютер комбинацию Ctrl-Shift-3.
6. На компьютере по данной комбинации запускается в фоне вторая программа на Python (код также ниже), которая объединяет все листы из временной папки в единый PDF-документ на сетевом диске, добавив в имя файла дату и время.
7. Человек идет на свое рабочее место и забирает файл с сетевого диска. Радуется, что ему не пришлось "просить сканировать".

Экономический эффект потрясающий. Если посчитать грубо, то на ручное сканирование 1 листа человеку. "хозяину-сканера", раньше требовалось затратить около 2-3 минут с учетом предварительного просмотра документа, ручного сохранения на сетевой диск или отправку по почте. В неудачный день он мог бы сканировать для приходящих полчаса, отвлекаясь от своей работы. Даже самая неточная прикидка дает срок окупаемости всего "железа" в несколько часов.

Код для Digispark у нас получился вот такой:

#include "DigiKeyboard.h"

#define MY_OUT 2 
#define MY_KEY1 0
#define MY_KEY2 1

bool trigger = false;

void setup() { 
  pinMode(MY_OUT, OUTPUT); 
  pinMode(MY_KEY1, INPUT);
  pinMode(MY_KEY2, INPUT);

  tone(MY_OUT,659,500);
}

void loop() {
  DigiKeyboard.update();

  if (digitalRead(MY_KEY1) == true  && digitalRead(MY_KEY2) == false){
    DigiKeyboard.sendKeyStroke(KEY_2, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT);
    tone(MY_OUT,440);         
    DigiKeyboard.delay(500);
    noTone();
    digitalWrite(MY_OUT,true);    
    DigiKeyboard.delay(5000);
    trigger = true;
  }

  if (digitalRead(MY_KEY2) == true && digitalRead(MY_KEY1) == false && trigger){        
    DigiKeyboard.sendKeyStroke(KEY_3, MOD_SHIFT_LEFT | MOD_CONTROL_LEFT);
    tone(MY_OUT,523);
    DigiKeyboard.delay(500);
    noTone();
    digitalWrite(MY_OUT,false);    
    DigiKeyboard.delay(5000);
    trigger = false;
  }
  
  DigiKeyboard.delay(100);
}

Программа на Python, которая запускается по красной кнопке на ПК выглядит так:

# сканирование одного изображения во временную сетевую папку

import os, time
import datetime

# каталог, куда будут сохраняться отсканированные изображения
dir = r'O:\scan\tmp'
# путь к NAPS2
NAPSexe = r'C:\"Program Files (x86)"\NAPS2\NAPS2.Console.exe'
# имя временного файла
tmpPdf = "scan" # .pdf

try:
    files = os.listdir(dir)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dir")    
    time.sleep(60)

now_time = datetime.datetime.now()

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Идет сканирование... ")

n = 1

# смотрим сколько файлов уже отсканировано
files = os.listdir(dir)
for i in files:
    n += 1

# Сканируем одно изображение в pdf
fullNamePdf = dir + "\\"+tmpPdf+str(n)+".pdf"
os.system(NAPSexe + r' --force -o '+fullNamePdf)

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Сканирование завершено... ")

"Синяя" кнопка запускает программу на Python следующего содержания:

# сборка отсканированных страниц в многостраничный документ

import os, time
import datetime

# каталог, откуда будут забираться постранично отсканированные изображения
dirIn = r'O:\scan\tmp'
# каталог, куда будет собираться многостраничный документ
dirOut = r'O:\scan\docs'
# путь к NAPS2
NAPSexe = r'C:\"Program Files (x86)"\NAPS2\NAPS2.Console.exe'
# имя временного файла
tmpPdf = "scan" # .pdf

try:
    files = os.listdir(dirIn)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dirIn")    
    time.sleep(60)

try:
    files = os.listdir(dirOut)
except OSError:
    print("Сетевой путь не найден! Скорректируйте переменную dirOut")    
    time.sleep(60)

now_time = datetime.datetime.now()

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Идет сборка... ")

# Смотрим сколько страниц во временной папке
files = os.listdir(dirIn)
i = 0
cmd = ""
for scan in files:
    i += 1
    cmd += dirIn+"\\"+scan
    if i < len(files):
        cmd += ";"

fullNamePdf = dirOut + "\\scan-"+ now_time.strftime("%Y-%m-%d-%I-%M-%S") + ".pdf"
os.system(NAPSexe + r' -i ' + cmd + r' --force -n 0 -o '+fullNamePdf)

print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Сборка завершена... ")
print(now_time.strftime("%d.%m.%Y %I:%M:%S")," Удяляем временные файлы... ")

for scan in files:
    os.remove(dirIn+"\\"+scan)

 Вот такая вышла малая офисная автоматизация, а цвета кнопок, напоминающий красную и синюю таблетки в всем известном фильме, дали название нашему проекту.

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