Надо оправдывать название компании — заняться хоть чем-то, что связано с видео. По предыдущему топику можно понять, что мы не только чайник делаем, но и пилим «умное освещение» для умного дома. На этой недели я был занят тем, что ковырял OpenCV — это набор алгоритмов и библиотек для работы с компьютерным зрением. Поиск обьектов на изображениях, распознание символов и все такое прочее.
На самом деле что-то в ней сделать — не такая сложная задача, даже для не-программиста. Вот я и расскажу, как.
Сразу говорю: в статье может встретиться страшнейший быдлокод, который может вас напугать или лишить сна до конца жизни.
Если вам еще не страшно — то добро пожаловать дальше.
Введение
Собственно, в чем состояла идея. Хотелось полностью избавится от ручного включения света. У нас есть квартира, и есть люди, которые по ней перемещаются. Людям нужен свет. Всем остальным предметам в квартире свет не нужен. Предметы не двигаются, а люди двигаются(если человек не двигается — он или умер, или спит. Мертвым и спящим свет тоже не нужен). Соответственно, надо освещать только те места в квартире, где наблюдается какое-то движение. Движение прекратилось — можно через полчаса-час выключить свет.
Как определять движение?
О сенсорах
Можно определять вот такими детекторами:
Называют они PIR — Пассивный Инфракрасный Сенсор. Или не пассивный, а пироэлектрический. Короче, в основе его лежит, по сути, единичный пиксель тепловизора — та самая ячейка, которая выдает сигнал, если на нее попадает дальний ик.
Простая схема после нее выдает импульс только если сигнал резко меняется — так что на горячий чайник он сигналить не будет, а вот на перемещающийся теплый объект — будет.
Такие детекторы устанавливают в 99% сигнализаций, и вы их все думаю, видели — это те штуки, которые висят под потолком:
Еще такие же штуки, но с обвязкой посложнее стоят в бесконтактных термометрах — тех, которые меряют температуру за пару секунд на лбу или в ухе.
И в пирометрах, тех же термометрах, но с бОльши диапазоном:
Хотя я что-то отвлекся. Такие сенсоры, конечно, штука хорошая. Но у них есть минус — он показывает движение во всем обьеме наблюдения, не уточняя где оно произошло — близко, далеко. А у меня большая комната. И хочется включать свет только в той части, где работает человек. Можно было, конечно поставить штук 5 таких сенсоров, но я отказался от этой идеи — если можно обойтись одной камерой примерно за такую же сумму, зачем ставить кучу сенсоров?
Ну и OpenCV хотелось поковырять, не без этого, да. Так что я нашел в закромах камеру, взял одноплатник(CubieBoard2 на A20) и поехало.
Установка
Естественно, для использования OpenCV сначала надо поставить. В большинстве современных систем(я говорю про *nix) она ставится одной командой типа apt-get install opencv. Но мы же пойдем простым путем, да? Да и например в системе для одноплатника, которую я использую ее нету.
Исчерпывающее руководство по установке можно найти вот тут, поэтому я не буду очень подробно останавливаться на ней.
Ставим cmake и GTK(вот его я как раз со спокойной совестью поставил apt-get install cmake libgtk2.0-dev).
Идем на офсайт и скачиваем последнюю версию. А вот если мы полезем на SourceForge по ссылке из руководства на Robocraft, то скачаем не последнюю версию(2.4.6.1), а 2.4.6, в которой абсолютно неожиданно не работает прием изображения с камеры через v4l2. Я этого не знал, поэтому 4 дня пытался заставить работать эту версию. Хоть бы написали где-то.
Дальше — стандартно:
Можно собрать примеры, которые идут в комплекте:
Собственно, большая часть моего кода взята из примера под названием motempl — это как раз и есть программа, реализующая функционал определения движения в кадре. Выглядит это вот так:
Допилка
Работает, но как это применить для включения света? Он показывает движение на экране, но нам-то надо, чтобы об этом узнал контроллер, который у нас управляет освещением. И желательно, чтобы он узнал не координаты точки, а место, в котором надо включить свет.
Для начала, немного поймем, как же эта штука работает. Чтобы показать видео с камеры в окошке, многое не требуется:
Эту программу можно скопировать в файл test.c и собрать его вот так:
Оно запустится, и покажет вам видео с камеры. Но из него даже не получится выйти — программа застряла в бесконечном цикле и только Ctrl+C прервет ее бессмысленную жизнь. Добавим обработчик кнопок:
И счетчик FPS:
Теперь у нас есть программа, которая показывает видео с камеры. Нам надо ей как-то указать те части экрана, в которых нужно определять движение. Не ручками же их в пикселях задавать.
Вот как оно работает:
Регионы переключаются цифровыми кнопками.
Но не будем же мы каждый раз при запуске программы устанавливать регионы наблюдения вручную? Сделаем сохранение в файл.
Привязываем эти функции, например на кнопки w и r, и при нажатии их сохраняем и открываем массив.
Осталась самая малость — собственно, определение в каком регионе произошло движение. Переносим наши наработки в исходник motempl.с, и находим куда нам можно вклиниться.
Вот функция, которая рисует круги на месте обнаружения движения:
А координаты центра определяются вот так:
Вставляем в этот кусок свой код:
Работает:
Осталось немного: направлять вывод не в консоль, а в UART, подключить к любому МК реле, которые будут управлять светом. Программа обнаруживает движение в регионе, отправляет номер региона контроллеру, а тот зажигает назначенную ему лампу. Но об этом — в следующей серии.
Исходник проекта я выложил на github, и буду не против, если кто-нибудь найдет время для исправления ошибок и улучшения программы:
github.com/vvzvlad/motion-sensor-opencv
Напоминаю, если вы не хотите пропустить эпопею с чайником и хотите увидеть все новые посты нашей компании, вы можете подписаться на на странице компании(кнопка «подписаться»)
И да, я опять писал пост в 5 утра, поэтому приму сообщения об ошибках. Но — в личку.
На самом деле что-то в ней сделать — не такая сложная задача, даже для не-программиста. Вот я и расскажу, как.
Сразу говорю: в статье может встретиться страшнейший быдлокод, который может вас напугать или лишить сна до конца жизни.
Если вам еще не страшно — то добро пожаловать дальше.
Введение
Собственно, в чем состояла идея. Хотелось полностью избавится от ручного включения света. У нас есть квартира, и есть люди, которые по ней перемещаются. Людям нужен свет. Всем остальным предметам в квартире свет не нужен. Предметы не двигаются, а люди двигаются(если человек не двигается — он или умер, или спит. Мертвым и спящим свет тоже не нужен). Соответственно, надо освещать только те места в квартире, где наблюдается какое-то движение. Движение прекратилось — можно через полчаса-час выключить свет. Как определять движение?
О сенсорах
Можно определять вот такими детекторами:Называют они PIR — Пассивный Инфракрасный Сенсор. Или не пассивный, а пироэлектрический. Короче, в основе его лежит, по сути, единичный пиксель тепловизора — та самая ячейка, которая выдает сигнал, если на нее попадает дальний ик.
Простая схема после нее выдает импульс только если сигнал резко меняется — так что на горячий чайник он сигналить не будет, а вот на перемещающийся теплый объект — будет.
Такие детекторы устанавливают в 99% сигнализаций, и вы их все думаю, видели — это те штуки, которые висят под потолком:
Еще такие же штуки, но с обвязкой посложнее стоят в бесконтактных термометрах — тех, которые меряют температуру за пару секунд на лбу или в ухе.
И в пирометрах, тех же термометрах, но с бОльши диапазоном:
Хотя я что-то отвлекся. Такие сенсоры, конечно, штука хорошая. Но у них есть минус — он показывает движение во всем обьеме наблюдения, не уточняя где оно произошло — близко, далеко. А у меня большая комната. И хочется включать свет только в той части, где работает человек. Можно было, конечно поставить штук 5 таких сенсоров, но я отказался от этой идеи — если можно обойтись одной камерой примерно за такую же сумму, зачем ставить кучу сенсоров?
Ну и OpenCV хотелось поковырять, не без этого, да. Так что я нашел в закромах камеру, взял одноплатник(CubieBoard2 на A20) и поехало.
Установка
Естественно, для использования OpenCV сначала надо поставить. В большинстве современных систем(я говорю про *nix) она ставится одной командой типа apt-get install opencv. Но мы же пойдем простым путем, да? Да и например в системе для одноплатника, которую я использую ее нету. Исчерпывающее руководство по установке можно найти вот тут, поэтому я не буду очень подробно останавливаться на ней.
Ставим cmake и GTK(вот его я как раз со спокойной совестью поставил apt-get install cmake libgtk2.0-dev).
Идем на офсайт и скачиваем последнюю версию. А вот если мы полезем на SourceForge по ссылке из руководства на Robocraft, то скачаем не последнюю версию(2.4.6.1), а 2.4.6, в которой абсолютно неожиданно не работает прием изображения с камеры через v4l2. Я этого не знал, поэтому 4 дня пытался заставить работать эту версию. Хоть бы написали где-то.
Дальше — стандартно:
tar -xjf OpenCV-*.tar.bz2 && cd OpenCV-* && cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local ./ && make && make install
Можно собрать примеры, которые идут в комплекте:
cd samples/c/ && chmod +x build_all.sh && ./build_all.sh
Собственно, большая часть моего кода взята из примера под названием motempl — это как раз и есть программа, реализующая функционал определения движения в кадре. Выглядит это вот так:
Допилка
Работает, но как это применить для включения света? Он показывает движение на экране, но нам-то надо, чтобы об этом узнал контроллер, который у нас управляет освещением. И желательно, чтобы он узнал не координаты точки, а место, в котором надо включить свет. Для начала, немного поймем, как же эта штука работает. Чтобы показать видео с камеры в окошке, многое не требуется:
#include <cv.h>
#include <highgui.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
CvCapture* capture = cvCaptureFromCAM(0);// Создаем обьект CvCapture(внутреннее название для обьекта, в который кладутся кадры с камеры), который называется capture. Сразу подключаем его к камере функцией cvCaptureFromCAM, 0 в параметрах которой означает, что видео надо брать с первой подвернувшейся камеры.
IplImage* image = cvQueryFrame( capture ); // Создаем обьект типа изображение(имя image) и кладем туда текущий кадр с камеры
cvNamedWindow("image window", 1); //Создаем окно с названием image window
for(;;) //запускаем в бесконечном цикле
{
image = cvQueryFrame( capture ); //получаем очередной кадр с камеры и записываем его в image
cvShowImage("image window", image);//Показываем в созданном окне(image window) кадр с камеры, который мы получили в предыдущем пункте
cvWaitKey(10); //ждем 10 мс нажатия кнопки. Тут оно без надобности, но без этого окно не создается. Я не против, если кто-то, более понимающий в этом, объяснит такое поведение.
}
}
Эту программу можно скопировать в файл test.c и собрать его вот так:
gcc -ggdb `pkg-config --cflags opencv` -o `basename test.c .c` test.c `pkg-config --libs opencv`
Опять же, честно говоря, я не совсем понимаю, что именно делает эта команда. Ну собирает. А почему именно такая?Оно запустится, и покажет вам видео с камеры. Но из него даже не получится выйти — программа застряла в бесконечном цикле и только Ctrl+C прервет ее бессмысленную жизнь. Добавим обработчик кнопок:
char c = cvWaitKey(10); //Ждем нажатия кнопки и записываем нажатую кнопку в переменную с.
if (c == 113 || c == 81) //Проверяем, какая кнопка нажата. 113 и 81 - это коды кнопки "q" - в английской и русской раскладках.
{
cvReleaseCapture( &capture ); //корретно освобождаем память и уничтожаем созданные обьекты.
cvDestroyWindow("capture"); //я тебя породил, я тебя и убью!
return 0; //выходит из программы.
}
И счетчик FPS:
CvFont font; //создаем структуру "шрифт"
cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8); //Инициализуем ее параметрами - название шрифта, размеры, сглаживание
struct timeval tv0; //Что-то связаннное с временем.
int fps=0;
int fps_sec=0;
char fps_text[2];
int now_sec=0;//Создаем переменные
...
gettimeofday(&tv0,0); //Получаем текущее время
now_sec=tv0.tv_sec; //Получаем из него секунды
if (fps_sec == now_sec) //Сравниваем, совпадает ли текущая секунда с той, в которой вы считаем фпс
{
fps++; //если совпадает, то прибавляем еще один кадр(это все крутится в цикле, который рисует кадры.)
}
else
{
fps_sec=now_sec; //если не совпадает, то обнуляем секунду
snprintf(fps_text,254,"%d",fps); //формируем текстовую строку с FPS
fps=0; // обнуляем счетчик
}
cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));//выводим в текущий кадр(image) в место с координатами 5х20, белым цветом, тем шрифтом, что мы задали ранее, переменную, в которой записан текущий фпс.
Полный текст программы
#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc_c.h"
#include <time.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main(int argc, char** argv)
{
IplImage* image = 0;
CvCapture* capture = 0;
struct timeval tv0;
int fps=0;
int fps_sec=0;
int now_sec=0;
char fps_text[2];
CvFont font;
cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8);
capture = cvCaptureFromCAM(0);
cvNamedWindow( "Motion", 1 );
for(;;)
{
IplImage* image = cvQueryFrame( capture );
gettimeofday(&tv0,0);
now_sec=tv0.tv_sec;
if (fps_sec == now_sec)
{
fps++;
}
else
{
fps_sec=now_sec;
snprintf(fps_text,254,"%d",fps);
fps=0;
}
cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));
cvShowImage( "Motion", image );
if( cvWaitKey(10) >= 0 )
break;
}
cvReleaseCapture( &capture );
cvReleaseImage(&image);
cvDestroyWindow( "Motion" );
return 0;
}
Теперь у нас есть программа, которая показывает видео с камеры. Нам надо ей как-то указать те части экрана, в которых нужно определять движение. Не ручками же их в пикселях задавать.
int dig_key=0;//переменная, хранящее нажатую кнопку
int region_coordinates[10][4]; //координаты регионов, в которых надо определять движение.
...
char c = cvWaitKey(20); //Ждем нажатия кнопки и записываем нажатую кнопку в переменную с.
if (c <=57 && c>= 48) //Проверяем, относится ли нажатая кнопка к цифрам
{
dig_key=c-48; //key "0123456789" //если относится, то записываем в переменную номер кнопки.
}
cvSetMouseCallback( "Motion", myMouseCallback, (void*) image); //говорим, что нам надо выполнить подпрограмму myMouseCallback при событиях, связанных с мышью в окне Motion и с изображением image
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0) //Рисуем прямоугольник. Если есть в переменной только одни координаты - рисуем точку по этим координатам.
cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][0]+1,region_coordinates[dig_key][1]+1), CV_RGB(0,0,255), 2, CV_AA, 0 );
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] != 0 && region_coordinates[dig_key][3] != 0) //А если в переменной двое наборов координат - рисуем полностью прямоугольник.
cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][2],region_coordinates[dig_key][3]), CV_RGB(0,0,255), 2, CV_AA, 0 );
void myMouseCallback( int event, int x, int y, int flags, void* param) //описываем что нам надо будет делать при событиях, связанных с мышью
{
IplImage* img = (IplImage*) param; //получаем картинку. Видимо, ему это надо для определение координат
switch( event ){ //вбираем действие в зависимости от событий
case CV_EVENT_MOUSEMOVE: break; //ничего не делаем при движении мыши. А можно, например, кидать в консоль координаты под курсором: printf("%d x %d\n", x, y);
case CV_EVENT_LBUTTONDOWN: //при нажатии левой кнопки мыши
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0) //если это второе нажатие(заполнена первая половина координат - х и у верхнего угла региона), то записываем в переменную вторую половину - х и у нижнего угла региона
{
region_coordinates[dig_key][2]=x; //dig_key - определяет, какой регион устанавливается сейчас. А меняется он нажатием цифровых кнопок.
region_coordinates[dig_key][3]=y;
}
if (region_coordinates[dig_key][0] == 0 && region_coordinates[dig_key][1] == 0)//если это первое нажатие(не заполнена первая половина координат ), то записываем в переменную первую половину.
{
region_coordinates[dig_key][0]=x;
region_coordinates[dig_key][1]=y;
}
break;
}
}
Вот как оно работает:
Регионы переключаются цифровыми кнопками.
Полный текст программы
#include "opencv2/video/tracking.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc_c.h"
#include <time.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int dig_key=0;
int region_coordinates[10][4];
void myMouseCallback( int event, int x, int y, int flags, void* param)
{
IplImage* img = (IplImage*) param;
switch( event ){
case CV_EVENT_MOUSEMOVE:
//printf("%d x %d\n", x, y);
break;
case CV_EVENT_LBUTTONDOWN:
//printf("%d x %d\n", region_coordinates[dig_key][0], region_coordinates[dig_key][1]);
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0)
{
region_coordinates[dig_key][2]=x;
region_coordinates[dig_key][3]=y;
}
if (region_coordinates[dig_key][0] == 0 && region_coordinates[dig_key][1] == 0)
{
region_coordinates[dig_key][0]=x;
region_coordinates[dig_key][1]=y;
}
break;
case CV_EVENT_RBUTTONDOWN:
break;
case CV_EVENT_LBUTTONUP:
break;
}
}
int main(int argc, char** argv)
{
IplImage* image = 0;
CvCapture* capture = 0;
struct timeval tv0;
int fps=0;
int fps_sec=0;
int now_sec=0;
char fps_text[2];
CvFont font;
cvInitFont(&font, CV_FONT_HERSHEY_COMPLEX_SMALL, 1.0, 1.0, 1,1,8);
capture = cvCaptureFromCAM(0);
cvNamedWindow( "Motion", 1 );
for(;;)
{
IplImage* image = cvQueryFrame( capture );
gettimeofday(&tv0,0);
now_sec=tv0.tv_sec;
if (fps_sec == now_sec)
{
fps++;
}
else
{
fps_sec=now_sec;
snprintf(fps_text,254,"%d",fps);
fps=0;
}
cvSetMouseCallback( "Motion", myMouseCallback, (void*) image);
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] == 0 && region_coordinates[dig_key][3] == 0)
cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][0]+1,region_coordinates[dig_key][1]+1), CV_RGB(0,0,255), 2, CV_AA, 0 );
if (region_coordinates[dig_key][0] != 0 && region_coordinates[dig_key][1] != 0 && region_coordinates[dig_key][2] != 0 && region_coordinates[dig_key][3] != 0)
cvRectangle(image, cvPoint(region_coordinates[dig_key][0],region_coordinates[dig_key][1]), cvPoint(region_coordinates[dig_key][2],region_coordinates[dig_key][3]), CV_RGB(0,0,255), 2, CV_AA, 0 );
cvPutText(image, fps_text, cvPoint(5, 20), &font, CV_RGB(255,255,255));
cvShowImage( "Motion", image );
char c = cvWaitKey(20);
if (c <=57 && c>= 48)
{
dig_key=c-48; //key "0123456789"
}
}
cvReleaseCapture( &capture );
cvReleaseImage(&image);
cvDestroyWindow( "Motion" );
return 0;
}
Но не будем же мы каждый раз при запуске программы устанавливать регионы наблюдения вручную? Сделаем сохранение в файл.
FILE *settings_file;
FILE* fd = fopen("regions.bin", "rb"); //открываем файл. "rb" - чтение бинарных данных
if (fd == NULL)
{
printf("Error opening file for reading\n"); //если файл не нашли
FILE* fd = fopen("regions.bin", "wb"); //пытаемся создать
if (fd == NULL)
{
printf("Error opening file for writing\n");
}
else
{
fwrite(region_coordinates, 1, sizeof(region_coordinates), fd); //если получилось - записываем туда нулевые координаты
fclose(fd); //закрываем файл
printf("File created, please restart program\n");
}
return 0;
}
size_t result = fread(region_coordinates, 1, sizeof(region_coordinates), fd); //читаем файл
if (result != sizeof(region_coordinates)) //если прочитали количество байт не равное размеру массива
printf("Error size file\n"); //вываливаем ошибку
fclose(fd); //закрываем файл
FILE* fd = fopen("regions.bin", "wb"); //открываем файл. "wb" - запись бинарных данных
if (fd == NULL) //если на нашли файл
printf("Error opening file for writing\n"); //ругаемся
fwrite(region_coordinates, 1, sizeof(region_coordinates), fd); //читаем файл в массив
fclose(fd); //закрываем файл
Привязываем эти функции, например на кнопки w и r, и при нажатии их сохраняем и открываем массив.
Осталась самая малость — собственно, определение в каком регионе произошло движение. Переносим наши наработки в исходник motempl.с, и находим куда нам можно вклиниться.
Вот функция, которая рисует круги на месте обнаружения движения:
cvCircle( dst, center, cvRound(magnitude*1.2), color, 3, CV_AA, 0 );
А координаты центра определяются вот так:
center = cvPoint( (comp_rect.x + comp_rect.width/2), (comp_rect.y + comp_rect.height/2) );
Вставляем в этот кусок свой код:
int i_mass; //создаем переменную цикла
for (i_mass = 0; i_mass <= 9; i_mass++) //перебираем все наши массивы в цикле, проверяя принадлежность точки к каждому из них.
{
if( comp_rect.x + comp_rect.width/2 <= region_coordinates[i_mass][2] && comp_rect.x + comp_rect.width/2 >= region_coordinates[i_mass][0] && comp_rect.y + comp_rect.height/2 <= region_coordinates[i_mass][3] && comp_rect.y + comp_rect.height/2 >= region_coordinates[i_mass][1] ) //проверяем, принадлежит ли точка, в которой обнаружено движение нашему прямоугольнику-региону.
{
cvRectangle(dst, cvPoint(region_coordinates[i_mass][0],region_coordinates[i_mass][1]), cvPoint(region_coordinates[i_mass][2],region_coordinates[i_mass][3]), CV_RGB(0,0,255), 2, CV_AA, 0 ); //если текущая точка принадлежит региону, то рисуем этот регион синим прямоугольником, показывая, что в нем произошло срабатывание.
printf("Detect motion in region %d\n",i_mass); //и ругаемся в консоль с номером региона
}
}
Работает:
Осталось немного: направлять вывод не в консоль, а в UART, подключить к любому МК реле, которые будут управлять светом. Программа обнаруживает движение в регионе, отправляет номер региона контроллеру, а тот зажигает назначенную ему лампу. Но об этом — в следующей серии.
Исходник проекта я выложил на github, и буду не против, если кто-нибудь найдет время для исправления ошибок и улучшения программы:
github.com/vvzvlad/motion-sensor-opencv
Напоминаю, если вы не хотите пропустить эпопею с чайником и хотите увидеть все новые посты нашей компании, вы можете подписаться на на странице компании(кнопка «подписаться»)
И да, я опять писал пост в 5 утра, поэтому приму сообщения об ошибках. Но — в личку.