- PVSM.RU - https://www.pvsm.ru -
Вмоей домашней системе видеонаблюдения используются самые разные видеокамеры, некоторые из них очень дешевые и очень старые, и поэтому неспособны на такие вещи как «обнаружение человека».
А для контроля за пространством вокруг эта функция довольно полезна.
Простой «детектор движения» в камерах тут не поможет — он реагирует на смену картинки, то есть буквально на любые изменения, включая тени от предметов, опавшие листья и прочее подобное.
Идея использовать PIR‑датчик тоже к успеху не привела: он реагирует на холодные струи дождя и на воздушные потоки разной температуры, что дает массу ложных срабатываний.
Итак, нам нужен «детектор человеков».
Первым вариантом решения стало использование CodeProject.AI [1].
Это AI‑сервер, который способен обрабатывать изображения, идентифицируя на них те или иные объекты. Взаимодействие с ним производится через WebAPI.
Несмотря на то, что на сайте указаны различные варианты использования — работают почему‑то только docker‑образы.
С использованием Docker установка сервера сводится по сути к двум командам:
Скачать образ:
# docker pull codeproject/ai-server
Запустить сервер:
docker run --name CodeProject.AI-Server -d -p 32168:32168 --gpus all ^
--mount type=bind,source=C:ProgramDataCodeProjectAIdockerdata,target=/etc/codeproject/ai ^
--mount type=bind,source=C:ProgramDataCodeProjectAIdockermodules,target=/app/modules ^
codeproject/ai-server:gpu
Поскольку я устанавливал его на старый ноутбук под Linux — вторая команда приняла немного другой вид:
# docker run --name CodeProject.AI -d -p 32168:32168
--mount type=bind,source=/etc/codeproject/ai,target=/etc/codeproject/ai
--mount type=bind,source=/opt/codeproject/ai,target=/app/modules
codeproject/ai-server
Запускаем без использования GPU (codeproject/ai‑server), потому что встроенная видеокарта тут никакой пользы не приносит.
API описано на странице проекта. [2]
Суть простая: отправляем POST‑запрос с файлом — получаем результат.
Например, используем curl:
#!/bin/sh
host='xx.xx.xx.xx'
if [ -f $1 ] ; then
json=`curl --trace logfile -F image=@$1 http://${host}:32168/v1/vision/detection`
echo -n "$1|"
echo $json
fi
Передавая скрипту параметр — графический файл, получим в формате JSON перечень обьектов, обнаруженных на картинке.
Теперь задача посложнее: как в нужный момент получить из видеокамер этот самый графический файл и отправить его для анализа на AI‑сервер.
Тут есть как минимум два разных подхода:
Во‑первых, можно настроить на самих камерах тот самый «детектор движения» с отправкой фотографии на почтовый сервер. Почтовый сервер, понятное дело, наш собственный, по сути просто скрипт, принимающий фотографию и отправляющий ее на детектор.
Минусы в том, что не все камеры позволяют это настроить, и в том, что разные могут отправлять эту фотографию по‑разному, то есть скрипт нужно адаптировать под разные типы камер или настраивать почти полноценный почтовый сервер, адаптируя скрипт теперь уже к нему и к форматам писем.
Второй способ зависит от регистратора. Как выяснилось, в настройках ряда китайских регистраторов есть интересный пункт alarm server: при получении события типа «Motion detect» на указанный адрес этого alarm server‑а отправляется сообщение в JSON‑формате с указанием номера канала и типа события.
{
'StartTime' => 'XXXX-XX-XX XX:XX:XX',
'SerialID' => 'XXXXXXXXXXXXXXXXXXXX',
'Type' => 'Alarm',
'Channel' => 4,
'Status' => 'Start',
'Address' => '0xXXXXXXX',
'Event' => 'MotionDetect',
'Descrip' => ''
};
При получении этого события можно запросить у самой камеры снимок, и отправить его на детектор.
У этого способа минусы в том, что у разных камер разные URL для запроса снимка (но это несложно решить), а у некоторых и вовсе нет такой возможности (даже если она как бы есть — снимок не делается).
Конечно можно запросить у камеры видеопоток, но с учетом компрессии вытащить из него полноценную картинку без артефактов не так‑то просто: это требует лишнего времени, а чем быстрее обрабатывается одно событие и чем меньше программ при этом запускается — тем ниже нагрузка на систему.
В общем, не пытайтесь обьять необьятное, как завещали классики.
Ограничимся тем, что работает.
Итак, нужны два скрипта‑сервера: alarm server и «почтовый».
Почтовый в кавычках потому что никакую почту никуда он пересылать не будет, его задача просто принять файл.
#!/usr/bin/perl
use lib '/home/user/lib' ;
use MyImgAI;
use MyTelegram;
use IO::Socket;
use bytes;
use Net::MQTT::Simple;
use JSON;
use Data::Dumper;
use Email::Simple;
use MIME::Parser;
use File::Basename;
$SIG{CHLD} = "IGNORE";
$| = 1;
# ===============================================
sub process_entity {
my ($entity, $from) = @_;
print " parse ($from) ";
# Если это multipart (несколько частей), рекурсивно обрабатываем каждую часть
if ($entity->is_multipart) {
foreach my $part ($entity->parts) {
process_entity($part,$from); # рекурсия для обработки всех частей
}
}
# Если это вложение (и оно закодировано как файл)
else {
my $filename = $entity->head->recommended_filename;
if ($filename) {
my $str = $entity->bodyhandle->as_string;
my $res = MyImgAI::detect($str,$from);
if(defined $res){
MyTelegram::send_image($res);
}
}
}
}
# ===============================================
my $server = IO::Socket::INET->new(LocalPort => 2525, Type => SOCK_STREAM, Reuse => 1, Listen => 10 ) or die "Couldn't be a tcp server : $@n";
while (my $client = $server->accept()) {
my $pid = fork;
if($pid == 0){
## новое подключение
binmode $client;
my $mode = 0;
my $umode = 0;
my $text = '';
## притворяемся почтовым сервером Exim
print $client "220 lo.lo ESMTP Exim 4.92.3 Fri, 04 Oct 2024 14:18:44 +0300rn";
while(my $str = <$client>){
## в режиме чтения тела письма - читаем и сохраняем
if($mode){
if($str =~ /^.[rn]/){ ## конец письма
$mode = 0;
print $client "250 OKrn";
my $email = Email::Simple->new($text); ## разбор письма
my $parser = MIME::Parser->new;
$parser->output_to_core(1);
#$parser->output_dir($output_dir);
my $entity = $parser->parse_data($text) || die "Errorn";
process_entity($entity,$from);
}
else{
$text .= $str;
}
}
## режим общения с клиентом
else {
if($umode == 1){
print $client "334 DYT3jf4sdDR5rn";
$umode = 2;
}
elsif($umode == 2){
print $client "235 OKrn";
$umode = 0;
}
if($str =~ /^EHLO/ || $str =~ /^HELO/){
print $client "250 OKrn";
}
elsif($str =~ /^MAIL FROM/ ){
print $client "250 OKrn";
}
elsif($str =~ /^RCPT TO/ ){
print $client "250 OKrn";
}
elsif($str =~ /^AUTH/ ){
print $client "334 DYT3jf4sdDR5rn";
$umode = 1;
}
elsif($str =~ /^QUIT/ ){
print $client "221 OKrn";
}
elsif($str =~ /^DATA/ ){
print $client "354 OKrn";
$mode = 1;
$text = '';
}
}
}
exit;
}
}
close($server);
«Почтовый» сервер просто слушает заданный порт, при подключении клиента — камеры — обменивается стандартными сообщениями, принимает тело письма, выделяет из него файл, если он там есть, сохраняя его в памяти, и передает дальше для обработки в модуль MyImgAI.
Если обработка прошла успешно и что‑то там такое найдено — это что‑то в виде образа файла отправляется в модуль отправки сообщений в Телеграм.
Если ничего нет — то больше ничего и не происходит.
Используется Perl, потому что это достаточно просто, а поскольку сервер запускается всего один раз — и достаточно быстро.
#!/usr/bin/perl
use lib '/home/user/lib' ;
use MyImgAI;
use MyTelegram;
use IO::Socket;
use bytes;
use Net::MQTT::Simple;
use JSON;
use Data::Dumper;
# =====================================================
$SIG{CHLD} = "IGNORE";
$| = 1;
# список каналов с URL
my $snap_urls = {
'0' => {
url => 'http://192.168.1.221/cgi-bin/getsnapshot.cgi',
delay => 0,
},
'1' => {
url => 'http://192.168.1.222/webcapture.jpg?user=admin&password=secret&command=snap&channel=0',
delay => 1,
},
'2' => {
url => 'http://192.168.1.223/cgi-bin/getsnapshot.cgi',
delay => 0,
},
'3' => {
url => 'http://192.168.1.224/cgi-bin/getsnapshot.cgi',
delay => 0,
},
#'9' => 'http://192.168.1.203/webcapture.jpg?command=snap&channel=1',
#'10' => 'http://192.168.1.211/webcapture.jpg?command=snap&channel=1',
#'11' => 'http://192.168.1.216/cgi-bin/getsnapshot.cgi', #pir
#'7' => 'http://192.168.1.210:80/tmpfs/auto.jpg',
};
# =====================================================
sub send_message {
my $ch = shift;
## форк для обработки
my $pid = fork();
if(!defined $pid || $pid > 0){
return;
}
## если этого канала нет в списках - завершаем процесс
my $param = $snap_urls->{ $ch };
exit if(!defined $param);
## иногда требуется задержка для запроса - чтобы цель подошла ближе к камере
my $url = $param->{url};
sleep( $param->{delay} ) if($param->{delay});
my $tiny = HTTP::Tiny->new;
my $response = $tiny->get($url);
exit unless $response->{success};
## если запрос успешен и фото получено - детекция и отправка
if (length $response->{content}) {
print STDERR "+";
my $now = time;
my $res = MyImgAI::detect($response->{content},$ch);
if(defined $res){
MyTelegram::send_image($res);
}
}
exit(0);
}
# =====================================================
my $server = IO::Socket::INET->new(LocalPort => 15002, Type => SOCK_STREAM, Reuse => 1, Listen => 10 ) or die "Couldn't be a tcp server : $@n";
while (my $client = $server->accept()) {
# $client is the new connection
binmode $client;
while(my $str = <$client>){
my $j_str = substr($str,20);
if($j_str =~ /({.+})/){
my $data = from_json($1);
send_message( $data->{Channel} );
}
}
}
close($server);
Сервер слушает порт, получает сообщение, выделяет номер канала, находит соответствующий URL запроса, получает картинку, и отправляет на детектор
Модуль детектора:
#!/usr/bin/perl
package MyImgAI;
use HTTP::Tiny;
use Data::Dumper;
use GD;
use JSON;
use Net::MQTT::Simple;
use Cache::Memcached::Fast;
my $url_ai = "http://127.0.0.1:32168/v1/vision/custom/ipcam-combined";
my $boundary = "------------------------74ff4ba03552faa9";
#==================================================================
sub detect {
my ($in,$ch) = @_;
my $ret = undef;
if (defined $in && length $in) {
# отправка картинки на AI
my $data = "--$boundaryrn".
"Content-Disposition: form-data; name="image"; filename="xx.jpg""."rn".
'Content-Type: image/jpeg'."rnrn".
$in.
"rn--$boundary--rnrn";
my $l = length($data);
print STDERR '.';
my $tiny = HTTP::Tiny->new;
my $r = $tiny->request('POST', $url_ai, {
content => $data,
headers => {
'Content-Length' => $l,
'content-type' => "multipart/form-data; boundary=$boundary",
'Accept' => '*/*',
},
});
## ответ получен
if($r->{success} == 1){
print STDERR 'o';
my $content = $r->{content};
if($content =~ /^({.*})$/){
my $d = from_json($1);
if($d->{count}){ ## что-то найдено
print STDERR "!";
## будем рисовать рамки
my $im = GD::Image->newFromJpegData($in,1);
my $red = $im->colorAllocate(255,0,0);
my $blue = $im->colorAllocate(0,0,255);
my $green = $im->colorAllocate(0,255,0);
my $black = $im->colorAllocate(0,0,0);
my ($width,$height) = $im->getBounds();
## look for new objects
my $found_new = 0;
my $cnt = 0;
foreach my $x (@{$d->{predictions}}){
$cnt++;
my $px_max = $x->{x_max}; #int(($x->{x_max} * 20 )/$width);
my $px_min = $x->{x_min}; #int(($x->{x_min} * 20 )/$width);
my $py_max = $x->{y_max}; #int(($x->{y_max} * 20 )/$height);
my $py_min = $x->{y_min}; #int(($x->{y_min} * 20 )/$height);
# поиск такого же обьекта в том же месте за последние N секунд
my $mm = Cache::Memcached::Fast->new({
servers => [ { address => 'localhost:11211', weight => 2.5 } ],
namespace => 'imgai:',
connect_timeout => 0.2,
io_timeout => 0.5,
close_on_error => 1,
max_failures => 3,
failure_timeout => 2,
nowait => 1,
hash_namespace => 1,
utf8 => 1,
max_size => 512 * 1024,
});
my $key = $ch.'_'.$x->{label}.'_'.$px_max.'_'.$px_min.'_'.$py_max.'_'.$py_min;
my $t = $mm->get($key);
if(!defined $t){ ## такого не было за 60 сек - значит новый!
$mm->set($key, time, 60);
$found_new++;
}
if($x->{label} eq 'person'){
$im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$red);
}
elsif($x->{label} eq 'car'){
$im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$blue);
}
else{
$im->rectangle($x->{x_min},$x->{y_min},$x->{x_max},$x->{y_max},$green);
}
} # predictions
## если были новые - возвращаем картинку с рамками
if($found_new > 0){
$ret = $im->jpeg();
}
} # if count > 0
}# is json
}
else{
#print STDERR "ERROR: $r->{status} $r->{reason}n";
}
}
return $ret;
}
1;
Картинка отправляется на сервер, если ответ получен и что‑то найдено — для каждого обьекта на картинке рисуем рамку, и заодно проверяем, что за последние 60 секунд его там не было. Если найдены новые обьекты — модуль возвращает картинку с рамками, если нет — undef (null);
Модуль Телеграма очень простой: через своего бота отправляем себе картинку
#!/usr/bin/perl
package MyTelegram;
use HTTP::Tiny;
use Data::Dumper;
use GD;
use JSON;
use Net::MQTT::Simple;
my $boundary = "------------------------74ff4ba057eefaa9";
# параметры телеграма
my $token = 'XXXXXXXXXX:XXXXXXXXXXXXXXXXXXXX_XXXXXXXXXXXXXX';
my $chat_id = 'YYYYYYYYY';
my $tlg = "https://api.telegram.org/bot$token/sendPhoto";
sub send_image {
my ($image) = @_;
my $data = "rn--$boundaryrn".
"Content-Disposition: form-data; name="photo"; filename="xx.jpg""."rn".
'Content-Type: image/jpeg'."rnrn".
$image.
"rn--$boundaryrn".
"Content-Disposition: form-data; name="chat_id""."rnrn".
$chat_id.
"rn--$boundary--rnrn";
my $tiny = HTTP::Tiny->new;
my $r = $tiny->request('POST', $tlg, {
content => $data,
headers => {
'content-type' => "multipart/form-data; boundary=$boundary",
'Accept' => '*/*',
},
});
}
1;
Оба модуля используются обоими серверами.
Всё в целом позволяет заставить даже примитивные камеры отслеживать появление в зоне контроля людей, машин, животных и так далее.
Что именно контролировать — несложно задать в скрипте, просто игнорируя лишние сущности по полю label.
Информация о «гостях» немедленно попадает в Телеграм.
Автор: JBFW
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/videonablyudenie/404370
Ссылки в тексте:
[1] Первым вариантом решения стало использование CodeProject.AI: https://www.codeproject.com/ai/index.aspx
[2] API описано на странице проекта.: https://www.codeproject.com/ai/docs/api/api_reference.html
[3] Источник: https://habr.com/ru/articles/864588/?utm_source=habrahabr&utm_medium=rss&utm_campaign=864588
Нажмите здесь для печати.