В процессе экспериментов с малышкой RPi на просторах инета мне попался скрипт motion.py, который ведет непрерывную видеозапись с камеры и сохраняет в выходном видео только движущиеся изображения. Это очень удобно как с точки зрения экономии места на флешке, так и для просмотра: очень быстро надоест проматывать запись с автомобильной стоянки за всю ночь, когда нужна буквально пара — другая информационных кадров. Если быть точным, то это не совсем видео в современном смысле, а скорее видео в классическом: RPi беспрерывно делает фотографии, которые склеиваются в формат motion jpeg.
Детектор движения у нас есть, сделаем полноценную систему видеонаблюдения того, что происходит за окном. И не только за окном: ночью в магазине, офисе, на складе, дома когда вы в отъезде — применений просто огромное количество. Устроено все очень просто: Raspberry Pi с видеокамерой и WiFi донглом для доступа в интернет, и пара скриптов: motion.py и conv.sh.
Скрипт motion.py: детектор движения
Питоновский скрипт motion.py я немного доработал под систему:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
#!/usr/bin/python # /home/pi/bin/motion.py # original script by brainflakes, improved by pageauc, peewee2 and Kesthal # www.raspberrypi.org/phpBB3/viewtopic.php?f=43&t=45235 # You need to install PIL to run this script # type "sudo apt-get install python-imaging-tk" in an terminal window to do this import StringIO import subprocess import os import time from datetime import datetime from PIL import Image # Motion detection settings: # Threshold - how much a pixel has to change by to be marked as "changed" # Sensitivity - how many changed pixels before capturing an image, needs to be higher if noisy view # ForceCapture - whether to force an image to be captured every forceCaptureTime seconds, values True or False # filepath - location of folder to save photos # filenamePrefix - string that prefixes the file name for easier identification of files. # diskSpaceToReserve - Delete oldest images to avoid filling disk. How much byte to keep free on disk. # cameraSettings - "" = no extra settings; "-hf" = Set horizontal flip of image; "-vf" = Set vertical flip; "-hf -vf" = both horizontal and vertical flip threshold = 10 sensitivity = 20 forceCapture = True forceCaptureTime = 60 * 60 # Once an hour # filepath = "/home/pi/picam" filepath = "/home/pi/cache" filenamePrefix = "img" diskSpaceToReserve = 40 * 1024 * 1024 # Keep 40 mb free on disk cameraSettings = "" # settings of the photos to save saveWidth = 1296 saveHeight = 972 saveQuality = 15 # Set jpeg quality (0 to 100) # Test-Image settings testWidth = 100 testHeight = 75 # this is the default setting, if the whole image should be scanned for changed pixel testAreaCount = 1 testBorders = [ [[1,testWidth],[1,testHeight]] ] # [ [[start pixel on left side,end pixel on right side],[start pixel on top side,stop pixel on bottom side]] ] # testBorders are NOT zero-based, the first pixel is 1 and the last pixel is testWith or testHeight # with "testBorders", you can define areas, where the script should scan for changed pixel # for example, if your picture looks like this: # # ....XXXX # ........ # ........ # # "." is a street or a house, "X" are trees which move arround like crazy when the wind is blowing # because of the wind in the trees, there will be taken photos all the time. to prevent this, your setting might look like this: # testAreaCount = 2 # testBorders = [ [[1,50],[1,75]], [[51,100],[26,75]] ] # area y=1 to 25 not scanned in x=51 to 100 # even more complex example # testAreaCount = 4 # testBorders = [ [[1,39],[1,75]], [[40,67],[43,75]], [[68,85],[48,75]], [[86,100],[41,75]] ] # in debug mode, a file debug.bmp is written to disk with marked changed pixel an with marked border of scan-area # debug mode should only be turned on while testing the parameters above debugMode = False # False or True # Capture a small test image (for motion detection) def captureTestImage(settings, width, height): command = "raspistill %s -w %s -h %s -t 200 -e bmp -n -o -" % (settings, width, height) imageData = StringIO.StringIO() imageData.write(subprocess.check_output(command, shell=True)) imageData.seek(0) im = Image.open(imageData) buffer = im.load() imageData.close() return im, buffer # Save a full size image to disk def saveImage(settings, width, height, quality, diskSpaceToReserve): #keepDiskSpaceFree(diskSpaceToReserve) time = datetime.now() filename = filepath + "/" + filenamePrefix + "-%04d%02d%02d-%02d%02d%02d.jpg" % (time.year, time.month, time.day, time.hour, time.minute, time.second) subprocess.call("raspistill %s -w %s -h %s -t 200 -e jpg -q %s -n -o %s" % (settings, width, height, quality, filename), shell=True) #print "Captured %s" % filename # Keep free space above given level def keepDiskSpaceFree(bytesToReserve): if (getFreeSpace() < bytesToReserve): for filename in sorted(os.listdir(filepath + "/")): if filename.startswith(filenamePrefix) and filename.endswith(".jpg"): os.remove(filepath + "/" + filename) print "Deleted %s/%s to avoid filling disk" % (filepath,filename) if (getFreeSpace() > bytesToReserve): return # Get available disk space def getFreeSpace(): st = os.statvfs(filepath + "/") du = st.f_bavail * st.f_frsize return du # Get first image image1, buffer1 = captureTestImage(cameraSettings, testWidth, testHeight) # Reset last capture time lastCapture = time.time() while (True): # Get comparison image #ti = datetime.now() #print "t-> %d" % ti.second image2, buffer2 = captureTestImage(cameraSettings, testWidth, testHeight) #ti = datetime.now() #print "T-> %d" % ti.second # Count changed pixels changedPixels = 0 takePicture = False if (debugMode): # in debug mode, save a bitmap-file with marked changed pixels and with visible testarea-borders debugimage = Image.new("RGB",(testWidth, testHeight)) debugim = debugimage.load() for z in xrange(0, testAreaCount): # = xrange(0,1) with default-values = z will only have the value of 0 = only one scan-area = whole picture for x in xrange(testBorders[z][0][0]-1, testBorders[z][0][1]): # = xrange(0,100) with default-values for y in xrange(testBorders[z][1][0]-1, testBorders[z][1][1]): # = xrange(0,75) with default-values; testBorders are NOT zero-based, buffer1[x,y] are zero-based (0,0 is top left of image, testWidth-1,testHeight-1 is botton right) if (debugMode): debugim[x,y] = buffer2[x,y] if ((x == testBorders[z][0][0]-1) or (x == testBorders[z][0][1]-1) or (y == testBorders[z][1][0]-1) or (y == testBorders[z][1][1]-1)): # print "Border %s %s" % (x,y) debugim[x,y] = (0, 0, 255) # in debug mode, mark all border pixel to blue # Just check green channel as it's the highest quality channel pixdiff = abs(buffer1[x,y][1] - buffer2[x,y][1]) if pixdiff > threshold: changedPixels += 1 if (debugMode): debugim[x,y] = (0, 255, 0) # in debug mode, mark all changed pixel to green # Save an image if pixels changed if (changedPixels > sensitivity): takePicture = True # will shoot the photo later if ((debugMode == False) and (changedPixels > sensitivity)): break # break the y loop if ((debugMode == False) and (changedPixels > sensitivity)): break # break the x loop if ((debugMode == False) and (changedPixels > sensitivity)): break # break the z loop if (debugMode): debugimage.save(filepath + "/debug.bmp") # save debug image as bmp print "debug.bmp saved, %s changed pixel" % changedPixels # else: # print "%s changed pixel" % changedPixels # Check force capture if forceCapture: if time.time() - lastCapture > forceCaptureTime: takePicture = True if takePicture: lastCapture = time.time() #ti = datetime.now() #print "s-> %d" % ti.second saveImage(cameraSettings, saveWidth, saveHeight, saveQuality, diskSpaceToReserve) #ti = datetime.now() #print "S-> %d" % ti.second # Swap comparison buffers image1 = image2 buffer1 = buffer2 |
Скрипт работает так. В цикле через утилиту raspistill, которая работает с камерой, непрерывно делаются фотоснимки, после чего сравнивается каждый текущий и предыдущий кадр. Сравнение выполняется в модуле Imagemagic. Если кадры различаются начиная с некоторого уровня порога, значит в зоне наблюдения присутствует движение и снимки начинают записываться в директорию cache SD — карты RPi в формате jpeg, причем название файла образовано от отметки текущего времени.
Скрипт motion.py стартует с загрузкой RPi и работает непрерывно. Частота появления файлов в директории cache будет зависеть от движения в кадре: если картинка сильно меняется то снимки будут появляться постоянно; если движения нет — то новых файлов не будет.
Далее, с определенной периодичностью, нужно смонтировать полученные кадры в видеоролик и отправить его в сеть. Эту задачу выполняет скрипт conv.sh.
Скрипт conv.sh: формирование видеороликов и выкладка их на файловый сервер
Задача второго скрипта — conv.sh — периодически запускать на обработку полученные снимки. Для этого он выполняет следующую последовательность действий:
- собирает снимки, накопленные к данному времени скриптом motion.py;
- вставляет в снимки титры с указанием даты и времени снимка;
- переименовывает снимки для того, чтобы к ним можно было применить групповую операцию;
- преобразует последовательность снимков в видеоролик;
- стирает обработанные снимки;
- выкладывает видеоролик на файловый сервер типа box.net или Яндекс.
Скрипт conv.sh содержит следующую последовательность команд:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#!/bin/bash prefix=/home/pi/cache tmp=/tmp out=https://dav.box.com/dav/rpi now=`date +"%Y%m%d-%H%M"` i=0 for f in `ls -tr $prefix/*.jpg 2>/dev/null` do newf=$prefix/`printf %06d $i`.jpg echo $f "-->" $newf #mv $f $newf timestamp=`stat -c %y $f` /usr/bin/convert $f -fill black -draw "rectangle 10,10 150,40" -fill white -pointsize 15 -draw "text 15,25 '${timestamp:0:19}'" $newf rm $f i=$((i+1)) done /home/pi/bin/ffmpeg -y -r 5 -i $prefix/%06d.jpg -r 5 -vcodec mjpeg -q:v 1 $tmp/$now.avi curl -u myname:mypasswd -T $tmp/$now.avi $out/$now.avi rm $tmp/$now.avi rm $prefix/0*.jpg |
Первым делом, составляется список всех снимков накопленных в директории cache и каждый снимок из списка пропускается через утилиту convert, которая вставляет в снимок отметку времени. Сразу замечу, что это накладная процедура, поэтому лучше обходиться без нее. В этом случае надо просто закомментировать вызов convert и раскомментировать вызов команды mv. И в том и другом случае файл снимка получает новое имя — возрастающее число. Это необходимо сделать для того, чтобы конвертор ffmpeg мог выполнить групповую операцию над файлами с маской %06d.jpg.
Поскольку ffmpeg из пакетов RPi имеет ограниченную функциональность, я компилировал утилиту из исходников и разместил полную версию в каталоге /home/pi/bin. После монтажа снимков в видеоролик, который будет временно располагаться в директории /tmp/*.avi, другая команда — curl отправляет ролик на файловый сервер. В нашем случае это сервер dav.box.com.
После этого подчищаются обработанные файлы и сам отправленный ролик.
Скрипт conv.sh запускается каждые 15 минут по cron. За это время не должно быть переполнения файловой системы; ну и сам размер клипа должен быть вменяемым для передачи через сеть.
В результате, мы можем залогиниться на box.net и посмотреть нарезку роликов, которые будут появляться каждые 15 минут. Само собой, это время можно изменить в настройках cron. Косвенно, размер каждого ролика определяет активность в кадре: чем больше движений тем длиннее ролик, а если ничего не происходит и картинка статичная, длина ролика будет вплоть до нулевой.
Во всей этой истории есть слабое место, а именно сборка клипа с помощью ffmpeg. Поскольку он не использует аппаратное ускорение, то загрузка ARM идет под 100%, и это весьма печально; хуже всего что при интенсивном движении в кадре и как следствие большом количестве файлов сборка может не завершиться к тому моменту когда conv.sh должен быть запущен в очередной раз. С этим приходилось мириться, пока я не нашел это решение, заточенное под GPU.
RPi style: как малышка Raspberry Pi уделает Intel при монтаже клипа
Как мы знаем, несмотря на свою миниатюрность, RPi обладает мощным ресурсом: это GPU, Graphic Processig Unit, в котором реализована аппаратная поддержка такого известного кодека как H.264. На программном уровне с GPU работает программная прослойка OpenMax, а в свою очередь реализация мультимедийной утилиты gstreamer на RPi использует OpenMax и соответственно ресурсы GPU.
Не буду описывать установку дополнительных пакетов, как это описано по ссылке: я поставил их без проблем. Что нам нужно сделать — это теперь выкинуть из скрипта conv.sh утилиту ffmpeg и заменить ее следующей строкой:
1 |
gst-launch-1.0 multifilesrc location=$prefix/%06d.jpg index=1 caps="image/jpeg,framerate=24/1" ! jpegdec ! omxh264enc ! avimux ! filesink location=$tmp/$now.avi |
Теперь все стало любо — дорого: выходной ролик вместо mjpeg получил навороченный формат h.264, а время монтажа сократилось с минут до нескольких секунд! Вот она, мощь рациональной архитектуры по сравнению с много-много CPU GHz.
В сети можно найти сравнительные тесты обработки видео в Raspberry Pi с использованием аппаратного ускорения и с использованием платформы Intel. Посмотрите, не поленитесь, результаты вас удивят.
Решила тоже прикупить себе Raspberry Pi, но для дачи, так как постоянная запись там не нужна, а такая маленькая палочка выручалочка всегда пригодиться, так как страшно оставлять вещи без присмотра. Спасибо, ваши скрипты очень помогли, все работает)
Здорово ) Пользуйтесь на здоровье!