image
Большой брат следит за тобой, птица!


Статьи про нейрокурятник
Заголовок спойлера
  1. Вступление про обучение себя нейросетям
  2. Железо, софт и конфиг для наблюдения за курами
  3. Разметка датасетов
  4. Параллельное участие в соревнованиях, визуализации внутренностей нейросетей, развитие архитектур моделей
  5. Работающая модель для распознавания кур в курятнике
  6. Бот, который постит события из жизни кур




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


Когда-то давно читали статью про японца, который помог отцу с сортировкой огурцов; решили, что анализировать, как несутся куры у наших родителей, присылая им отчеты в мессенджер — идея из веселых.



Вообще планов много. То, что около гнезда произошло шевеление, может значить, что птица залезла в гнездо или вылезла из него. Это понять просто при помощи openCV, и это мы уже умеем. Сделать легко при помощи вот этого блога.


А что, если распознавать каждую птицу и анализировать, какая из них не несется? Оценивать продуктивность каждой отдельно взятой курицы? Если птица не несется и не имеет никакой другой уважительной причины для отдыха (например, короткий световой день, линька), то может, пора варить куриный суп?


Только представьте сообщение: “Нам кажется, что птица ch11 не несется без причины, быть может, нужно рассмотреть ее дальнейшую судьбу”. А потом окажется, что птица ch11 — это наша старая кошка Клюква, которая просто с курами живет.


Хакатон


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


Главная сложность заключалась в отсутствии проводного интернета и принципиальной невозможности его провести (глушь, что поделать). Но, когда не знаешь, на что подписываешься, надеешься на лучшее, да.


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


Основная часть оборудования — Raspberry Pi 3 и камера борд к нему, источник питания и usb-вентилятор (ибо процессинг изображений без вентилятора нагревает процессор аж до 80 градусов). Помимо этого кто-то должен был обеспечить pi интернетом.


Итак, среди альтернатив для хотспота — 3g/4g модем хуавей, старая xperia на андроиде. Модем хорош тем, что ему не нужен отдельный источник питания, а плох тем, что работает из коробки только с виндой. Есть, конечно же, статьи про то, как завести его на линуксе, но что-то не хотелось.


В условиях жестко ограниченного времени (оставались сутки до отъезда) был выбран телефон.
Провайдер не оказывал услугу статического IP в данном регионе. IP оказался динамическим, что было решено пофиксить при помощи динамического DNS сервиса.


И внезапно (кто бы сомневался), это не заработало. Ведь IP не просто динамический, он серый динамический. Это значит, что до него невозможно достучаться извне, порты закрыты.


Параллельно был перепилен питоновский скрипт для захвата и передачи на сервер изображений, но он все еще был сырой.


Тем временем была потрачена уже половина имевшегося времени.


Знакомый подсказал, что есть прекрасная штука, ssh back connect, что в общем-то спасло нас от разочарования. Времени оставалось совсем мало, поэтому не удавалось до конца разобраться, как все работает, нужно было, чтобы работало хоть как-то.


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


Главное, что к pi можно было подключиться отовсюду, где был интернет.


Подробнее о настройке


Немного отойдя от хакатона — марш-броска, я взялась донастраивать это дело дальше. Почитав гайды (по ключевым словам permanent autossh), я попыталась наладить autossh вместо reverse ssh, которое работало нестабильно и поддерживалось при помощи крона. Поначалу ничего с autossh у меня не вышло, я продолжала использовать первое решение с кроном, но проблема с плодящимися коннектами вынудила меня все-таки подружиться с autossh.


Чтобы все завелось, нужно лишь создать исполняемый файл (кто не умеет, гуглит create executable file linux) на удаленном устройстве с динамическим серым IP и добавить туда такую строчку:


/usr/bin/autossh -M 0 -o ServerAliveInterval=50 -o ServerAliveCountMax=2 -nNTf -R 2222:localhost:22 userB@hostB -p bbbb

В этой строчке 2222 можно заменить на любой ненужный вам порт, нужно заменить userB на юзера на вашем домашнем сервере (то есть на том, который не в курятнике), hostB — на хоста на вашем домашнем сервере, bbbb — порт вашего домашнего сервера, если отличен от стандартного (22).


Про параметры команды можете сами почитать, если интересно или хочется что-то поменять. Далее добавляем в крон (crontab -e) такую строку (если незнакомы с кроном то тут 1 2 3 4 друг собирал вводные), которая будет запускать autossh при ребуте:


@reboot /path/to/script/autosshtunnel.sh

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


Делается это по такому шаблону:


ssh -o TCPKeepAlive=yes -o ServerAliveInterval=50 user@box.example.com

К системе в курятнике я подключаюсь так:


ssh -o TCPKeepAlive=yes -o ServerAliveInterval=50 sshuser@localhost -p 2222

Это все касалось возможности удаленного подключения, теперь быстро поговорим про алармы о температуре. Чтобы настроить алармы на почту в debian системах типа убунту и распбиан — достаточно следовать этому гайду, нужно будет всего лишь установить ssmtp и поправить конфиг, это все. Простейший скрипт для аларм про перегрев на почту для распбиан может выглядеть вот так:


TEMPERATURE="$(/opt/vc/bin/vcgencmd measure_temp)"
NTEMPERATURE="$(echo $TEMPERATURE | tr -dc '0-9.')"
LIMIT="61.0"
if [ $(echo "$NTEMPERATURE > $LIMIT" | bc) -ne 0 ]; then
        echo "The critical CPU temperature has been reached $NTEMPERATURE" | sudo /usr/bin/ssmtp -vvv somename@somehost.com
fi

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


Теперь поговорим про основной скрипт, которым мы собираем изображения. Изображения мы считаем условно полезными, если заметили движение. Аналитику и распознавание будем прикручивать уже на эти изображения. Выше уже упоминался полезный блог, из которого мы взяли за основу скрипт, немного его переписав.


В самом гайде уже написано, что нужно для работы, но я повторюсь, что понадобится сделать билд OpenCV. Это может занять много времени (в моем случае заняло 5 часов). Помимо этого необходимо поставить так же и другие библиотеки, тоже там упомянутые, например numpy, imutils, — там не возникало подводных камней.


Основной скрипт мы переписали под свои нужды и внесли следующие изменения:

  • сменили Python 2 на Python 3;
  • вместо дропбокса использовали свой сервер;
  • сохраняются оригинальный и сжатый фрейм.


Готовый вариант pi_surveillance.py выглядит так (ну разве что надо еще сделать вынос констант из скрипта в конфиг):


# import the necessary packages
import sys
sys.path.append('/usr/local/lib/python2.7/site-packages')
from pyimagesearch.tempimage import TempImage
from picamera.array import PiRGBArray
from picamera import PiCamera
import argparse
import warnings
import datetime
import imutils
import json
import time
import cv2
import os
 
# construct the argument parser and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-c", "--conf", required=True,
	help="path to the JSON configuration file")
args = vars(ap.parse_args())
 
# filter warnings, load the configuration and check if we are going to use server
warnings.filterwarnings("ignore")
conf = json.load(open(args["conf"]))
client = None


if conf["use_server"]:
	#we do not use Dropbox
	print("[INFO] you are using server")

# initialize the camera and grab a reference to the raw camera capture
camera = PiCamera()
camera.resolution = tuple(conf["resolution"])
camera.framerate = conf["fps"]
rawCapture = PiRGBArray(camera, size=tuple(conf["resolution"]))
 
# allow the camera to warmup, then initialize the average frame, last
# uploaded timestamp, and frame motion counter
print("[INFO] warming up...")
time.sleep(conf["camera_warmup_time"])
avg = None
lastUploaded = datetime.datetime.now()
motionCounter = 0

# capture frames from the camera
for f in camera.capture_continuous(rawCapture, format="bgr", use_video_port=True):
	# grab the raw NumPy array representing the image and initialize
	# the timestamp and occupied/unoccupied text
	frame = f.array
	timestamp = datetime.datetime.now()
	text = "Unoccupied"
 
	# resize the frame, 
	frame = imutils.resize(frame, width=1920)
	frameorig = imutils.resize(frame, width=1920)
	# convert it to grayscale, and blur it	
	gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
	gray = cv2.GaussianBlur(gray, (21, 21), 0)
 
	# if the average frame is None, initialize it
	if avg is None:
		print("[INFO] starting background model...")
		avg = gray.copy().astype("float")
		rawCapture.truncate(0)
		continue
 
	# accumulate the weighted average between the current frame and
	# previous frames, then compute the difference between the current
	# frame and running average
	cv2.accumulateWeighted(gray, avg, 0.5)
	frameDelta = cv2.absdiff(gray, cv2.convertScaleAbs(avg))

	# threshold the delta image, dilate the thresholded image to fill
	# in holes, then find contours on thresholded image
	thresh = cv2.threshold(frameDelta, conf["delta_thresh"], 255,
		cv2.THRESH_BINARY)[1]
	thresh = cv2.dilate(thresh, None, iterations=2)
	
	cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
	cnts = cnts[0] if imutils.is_cv2() else cnts[1]
 
	# loop over the contours
	# check if there is at least one contour, which is large enough
	# I know this isn't the best practice
	# I know about bool variables
	# I know about other things too. I just don't actually care
	# Yes, I am a liar, 'cause if I did not care, 
	# I wouldn't write anything of those ^
	for c in cnts:
		# if the contour is too small, ignore it
		if cv2.contourArea(c) < conf["min_area"]:
			continue
 		
		text = "Occupied"
		print("[INFO] room is occupied, motion counter is {mc}".format(mc=motionCounter))
 
	# initiate timestamp
	ts = timestamp.strftime("%A-%d-%B-%Y-%I:%M:%S%p")
	ts1 = timestamp.strftime("%A-%d-%B-%Y")
	# let's create paths on a server
	pathorig = "{base_path}/{timestamp}/origs".format(
		base_path=conf["server_base_path"], timestamp=ts1)
	pathres = "{base_path}/{timestamp}/res".format(
		base_path=conf["server_base_path"], timestamp=ts1)
	
	os.system('ssh -p bbbb "%s" "%s %s"' % ("userB@hostB", "sudo mkdir -p", pathorig))
	os.system('ssh -p bbbb "%s" "%s %s"' % ("userB@hostB", "sudo mkdir -p", pathres))
	

	# upload images on a server
	if (text == "Occupied"):

		motionCounter += 1
		if motionCounter >= conf["min_motion_frames"] and (timestamp - lastUploaded).seconds >= conf["min_upload_seconds"]:
			print("[INFO] time to upload, motion counter is {mc}".format(mc=motionCounter))
		
			# upload original
			t = TempImage()
			cv2.imwrite(t.path, frameorig)
			os.system('scp -P bbbb "%s" "%s:%s"' % (t.path, "userB@hostB", pathorig))
			t.cleanup()
		
			# upload resized image of 512 px
		
			framec = imutils.resize(frame, width=512)
				
			tc = TempImage()
			cv2.imwrite(tc.path, framec)
			os.system('scp -P bbbb "%s" "%s:%s"' % (tc.path, "userB@hostB", pathres))
			tc.cleanup()
			
			#reset motionCounter
			motionCounter = 0
			lastUploaded = datetime.datetime.now()	
 
	# otherwise, the room is not occupied
	else:
		motionCounter = 0

	# check to see if the frames should be displayed to screen
	if conf["show_video"]:
		# display the security feed
		cv2.imshow("Security Feed", frame)
		key = cv2.waitKey(1) & 0xFF
 
		# if the `q` key is pressed, break from the loop
		if key == ord("q"):
			break
 
	# clear the stream in preparation for the next frame
	rawCapture.truncate(0)

Как сейчас выглядит наш конфиг:


{
        "show_video": false,
        "use_server": true,
        "server_base_path": "/media/server/PIC_LOGS",
        "min_upload_seconds": 1.0,
        "min_motion_frames": 3,
        "camera_warmup_time": 2.5,
        "delta_thresh": 5,
        "resolution": [1920, 1080],
        "fps": 16,
        "min_area": 6000
}

А так — tempimage.py:


# import the necessary packages
import uuid
import os
import datetime

class TempImage:
        def __init__(self, basePath="./temps", ext=".jpg"):
                # construct the file path
                timestamp = datetime.datetime.now()
                ts = timestamp.strftime("-%I:%M:%S%p")
                self.path = "{base_path}/{rand}{tmstp}{ext}".format(base_path=basePath,
                        rand=str(uuid.uuid4())[:8], tmstp=ts, ext=ext)

        def cleanup(self):
                # remove the file
                os.remove(self.path)

Первым полученным изображением было изображение хвоста курицы в гнезде. Отличный подарок на майские для интроверта по жизни, который в хорошую погоду пялится в консоль. Изображение действительно обрадовало, несмотря на темень, отсутствие головы птицы в кадре и ненастроенность скрипта. Это куриный хвост (Только подумайте, за тысячу километров от тебя курица залезла в гнездо, не подозревая, что ты за ней наблюдаешь.):


image

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


image
image

Запускается скрипт с учетом того, что OpenCV установлен в виртуальной рабочей среде cv, вот так (надо бы еще и придумать, как правильно такое отправлять в бекграунд):


source ~/.profile
workon cv
cd ~/chickencoop
python3 /home/sshuser/chickencoop/pi_surveillance.py --conf conf.json

image

Продолжение следует...
Поделиться с друзьями
-->

Комментарии (28)


  1. ooby
    04.05.2017 07:16
    +9

    … и я получила заметно более вдохновляющие фотографии.
    Александр?


    1. snakers4
      04.05.2017 07:26
      +9

      Именно эту часть нейрокурятника делала моя девушка.
      У нее нет аккаунта на Хабре — я запостил под своим.
      xD


  1. kolu4iy
    04.05.2017 09:27
    +5

    Прекрасно! Просто прекрасно! Жгите дальше, пожалуйста.


  1. msa
    04.05.2017 10:34
    +18

    Прибежали в избу дети,
    второпях зовут отца:
    «тятя! тятя! Нейросети
    натворили п!$#&ца»


    а вообще очень круто


  1. bumos
    04.05.2017 13:14

    Изюмительно, еще можно добавить USB камеру — установить общий обзор(на улице), прикрутить Telegram бота и посредством запроса получать от бота картинки что происходит в данный момент в курятнике в целом! Если что скрипты есть!


    1. snakers4
      04.05.2017 13:15

      Вы знаете как к RPI USB камеру приделать?
      Присылайте скрипты =)


      1. bumos
        04.05.2017 13:38

        Я имел ввиду USB камеру к USB порту

        Фотка
        image


        1. eugenebabichenko
          04.05.2017 17:24
          +1

          Не скажу за сочетание борда и USB камеры, но пара USB камер на второй RPi работала отвратительно. Возможно, это связано с кривым USB-хабом, при подключении туда WiFi адаптера всё висло намертво.


  1. b0rmann
    04.05.2017 13:15

    честно говоря, у меня десятка два кур. и хрен я их одну от одной распознаю. так, значит, нейронная сеть мне в этом поможет? а может им номера на шею повесить?


    1. snakers4
      04.05.2017 13:16

      Ну нейронная сеть может сделать только то, что сам эксперт, который ее разрабатывает, может сделать без проблем.

      Кто-то же будет потом ее тренировать и сортировать изображения.

      Так что если вы сами не можете различить — то скорнее нет, чем да.


      1. redial
        05.05.2017 06:03

        Кур можно покрасить (маркировать частично). Видел неоднократно цветные пятна на спинах кур и гусей, Возможно это была «противоугонка».


        1. snakers4
          05.05.2017 19:46

          Это для различения своих от соседских. Если кур вы ещё различите, то вот гусей белых трудновато


          1. b0rmann
            09.05.2017 10:58

            вот гусей я как раз различаю. в том числе и по голосу.


    1. LynXzp
      05.05.2017 00:09
      +1

      Краска есть? Промаркируйте как резисторы.


      1. snakers4
        05.05.2017 06:05

        1. Мы не можем попасть туда (надо ехать) повторно
        2. Решение с распознавание м образов обобщается на другие задачи, а краска — нет ))


  1. Torvald3d
    04.05.2017 14:50

    Кстати, картинка с КДПВ — это кадр из фильма Горячие головы, вроде вторая часть. Там курицу натягивали на лук…


  1. delvin-fil
    04.05.2017 15:57

    а плох тем, что работает из коробки только с виндой. Есть, конечно же, статьи про то, как завести его на линуксе, но что-то не хотелось.

    Я вас умоляю, wvdial есть в любом дистре и настраивается за 1-2 минуты.

    В целом, идея интересна и применима не только в курятнике.


    1. snakers4
      04.05.2017 16:18

      Ок, будем знать.
      Комменты из секций trouble shooting как-то склонили сразу на сторону зла без попытки глубоко вникнуть и оценить более трезво.


  1. snakers4
    04.05.2017 17:26

    Тут в самом низу страницы у нас есть очень детальное обсуждение альтернатив нашему подходу.

    Вот только они все или требует инвестиций, или не масштабируются на другие применения, кроме кур =)


    1. LynXzp
      05.05.2017 00:13

      Знаете, мне кажется что с помощью RFID можно было бы решить проблему гораздо проще и точнее.


  1. Salavat
    04.05.2017 17:36

    Отлично! Можно еще предусмотреть изменение веса до того, как курица села, курица сидит, курица снесла яйцо. Итого узнаем вес яйца и курицы. Могут ли курицы халтурничать, сесть и слезть с гнезда (кто-то напугал, например)?


    1. snakers4
      04.05.2017 17:53
      +1

      С весом есть одна проблема (а может, и не проблема): у кур есть зоб, они набивают его до отказа, когда едят. Это приводит к значительным колебаниям веса птицы от кормежки к кормежке, по крайней мере, эти колебания сопоставимы с весом яйца.
      Понятно, что если замерять вес до и вес после, это не будет проблемой.
      Делать такое можно только в случае, когда ну очень интересно.
      А так халтура у них может присутствовать. И не только халтура, бывает, что у птицы яйцо разбивается в организме, что со временем приводит к смерти. Но это очень редкие кейсы, у грамотных людей их не бывает.


  1. quarckster
    05.05.2017 12:48
    -1

    sys.path.append('/usr/local/lib/python2.7/site-packages')
    
    зачем так?


    1. snakers4
      05.05.2017 19:38

      А зачем вы спрашиваете?
      Я просто не знаю, может, вы знаете решение лучше, но почему-то не предложили, или же вы просто интересуетесь.
      В любом случае мб вы найдёте ответ здесь http://stackoverflow.com/questions/10095037/why-use-sys-path-appendpath-instead-of-sys-path-insert1-path


      1. quarckster
        06.05.2017 23:46

        Затем, что это тупо. Этот путь и так уже присутствует в sys.path в python 2.7. В тексте написано, что перешли на python3, тогда ещё более странной выглядит эта строка. Cишные модули не заработают вообще. Если какие-то пакеты нужны из 2.7, так установите их для третьей версии. Ещё у вас там устаревший os.system используется, когда уже давно стандарт это subprocess.


  1. deemytch
    05.05.2017 13:52
    +1

    Из опыта своей сельской жизни: куры кудахчут. И у них, у каждой(!), свой голос. И мало того, когда они несутся, они кудахчут очень пронзительно, видимо этот процесс доставляет.

    Из опыта доступа к удалённым домашним компам и прочему барахлу типа роутеров — openvpn рулит. Всё остальное рано или поздно требует ручного вмешательства. Т.е. для меня, за 10 лет использования, только openvpn-сервер где-нибудь на статическом IP + клиенты на удалённых, до которых я никогда не доеду, сильно спрятанных и прочая и прочая, устройствах позволял более-менее расчитывать на беспроблемный доступ к ним в любое время суток (если клиенты включены, разумеется).


    1. snakers4
      05.05.2017 22:21

      Трудно сказать, от чего это зависит, но куры кудахчут не всегда. Склоняюсь к теории, что ручные, закормленные и наглые куры склонны не кудахтать. У них страха нет совсем.
      Если же они кудахчут после того, как снесли яйцо, это превращается в содом и гоморру, потому что сначала подхватывает петух, потом все остальные. И да, голоса у них разные. Часть кур кудахчет односложно, другая — двусложно.
      Про openvpn были мысли, видимо, действительно стоит попробовать.


  1. deemytch
    05.05.2017 13:56

    Насчёт хуавея — очень давно не было с ними никаких проблем под линуксом. Вика в помощь! https://wiki.archlinux.org/index.php?title=Special%3ASearch&search=huawei&go=Go