Стриминг и распознавание лиц через веб-камеру
Перевод статьи “Stream and recognise people from a webcam with Go and Facebox".
От видеонаблюдения для предотвращения вторжения до распознавания личности человека за дверью и автоматического открытия дверей. Все это можно сделать с помощью нескольких строк на pyton, Go и используя Facebox.
Для начала нам нужно научиться получать видео с веб-камеры. Есть множество вариантов, как это можно сделать с Go. К сожалению, большинство из них тянут за собой CGO биндинги к OpenCV, при этом поддержка функциональности очень ограничена, а сами проекты довольно монструозны. А так как всегда нужно подбирать инструмент под задачу, то для захвата видео мы будем использовать python.
В python мы можем использовать с OpenCV для стриминга “Motion JPEG” через стандартный вывод.
“Motion JPEG(M-JPEG)” звучит как что-то сложное и фантастическое. Хотя на сам деле это всего лишь конкатенация фреймов в один JPEG с определенных использованием разделителей. Больше всего это похоже на CSV для видео. Как правило, за простоту приходится платить размерами файлов, потому что не используется видео сжатие.
M-PEG используется во многих устройствах: IP камерах, цифровых камерах. Возможно у вас уже есть устройство, которое поддерживает этот протокол. В таком случае вы можете использовать аналогичный подход для стриминга.
Ниже показан кусок кода capture.py
, который кадр за кадром захватывает видео с камеры и стримит его stdout
#!/usr/bin/env python
import cv2
import imutils
from imutils.video import VideoStream
import time, sys
vs = VideoStream(resolution=(320, 240)).start()
time.sleep(1.0)
while(True):
#read frame by frame the webcame stream
frame = vs.read()
# encode as a JPEG
res = bytearray(cv2.imencode(".jpeg", frame)[1])
size = str(len(res))
# stream to the stdout
sys.stdout.write("Content-Type: image/jpeg\r\n")
sys.stdout.write("Content-Length: " + size + "\r\n\r\n")
sys.stdout.write( res )
sys.stdout.write("\r\n")
# we use 'informs' as a boundary
sys.stdout.write("--informs\r\n")
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
vs.stop()
Небольшое отступление от перевода
На сомом деле нет ничего сложно, чтобы в этом месте использовать Go. К тому же, питоновская либа для работы c opencv тоже реализована через биндинги к сишному коду.
В качестве замены питоновского скрипта выше, можно использовать либу github.com/blackjack/webcam. Достаточно всего лишь чуть-чуть модернизировать пример из папки examples, чтобы все заработало:
package main
import (
"fmt"
"os"
"github.com/blackjack/webcam"
)
func main() {
cam, err := webcam.Open("/dev/video0")
if err != nil {
panic(err.Error())
}
defer cam.Close()
var format webcam.PixelFormat
for f, d := range cam.GetSupportedFormats() {
if d == "Motion-JPEG" {
format = f
break
}
}
_, _, _, err = cam.SetImageFormat(format, 320, 240)
if err != nil {
panic(err.Error())
}
err = cam.StartStreaming()
if err != nil {
panic(err.Error())
}
for {
err = cam.WaitForFrame(5)
switch err.(type) {
case nil:
case *webcam.Timeout:
continue
default:
panic(err.Error())
}
frame, err := cam.ReadFrame()
if len(frame) != 0 {
os.Stdout.Write([]byte("Content-Type: image/jpeg\r\n"))
os.Stdout.Write([]byte(fmt.Sprintf("Content-Length: %d\r\n\r\n", len(frame))))
os.Stdout.Write(frame)
os.Stdout.Write([]byte("\r\n"))
os.Stdout.Write([]byte("--informs\r\n"))
os.Stdout.Sync()
} else if err != nil {
panic(err.Error())
}
}
}
Конечно, в этом коде не хватает проверок. Например, когда мы вызываем cam.GetSupportedFormats
и проходим по всему списку, там вполне может не быть Motion-JPEG
. Точно также, размер 320 на 240 может не поддерживаться и неплохо было бы использовать функцию cam.GetSupportedFrameSizes
. Такой код нельзя использовать в продакшене, тем не менее, он работает и подходит нам в качестве примера.
Стриминг с помощью Go http сервер
Теперь у нас есть стрим, который мы можем прочитать и обработать в Go. Давайте создадим простой http сервер, который будет отправлять стрим с вебкамеры в браузер.
package main
import (
"log"
"net/http"
"os/exec"
)
const boundary = "informs"
func main() {
http.HandleFunc("/cam", cam)
log.Fatal(http.ListenAndServe(":8081", nil))
}
func cam(w http.ResponseWriter, r *http.Request) {
// указываем специальный multipart заголовок
w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary="+boundary)
// запускаем бинарник для работы с веб-камерой
cmd := exec.CommandContext(r.Context(), "./bin/webcam")
// отправляем вывод программы в response writer
cmd.Stdout = w
err := cmd.Run()
if err != nil {
log.Println("error capturing webcam", err)
}
}
Если перейти по ссылке http://localhost:8081/cam
, то запуститься новый процесс и начнется воспроизведение видео прямо в браузере.
Обратите внимание на использование context.Context
. С его помощью мы можем остановить процесс, когда http запрос будет прерван.
И еще одно замечание от переводчика. Так как мы теперь умеем получать стрим от камеры в нужном нам формате с помощью Go, то нам не нужно делать отдельный бинарник и вот это все. Можно добавить весь этот функционал непосредственно в программу, реализующую http хендлеры.
Обработка стрима и распознавание лиц
Теперь когда у нас есть возможность управлять стримом в Go, мы можем приступить к самой интересной части.
Основная идея в том, чтобы читать из стандартного вывода программы capture.py
и использовать пайп для чтения стрима фрейм за фреймом.
cmd := exec.CommandContext(r.Context(), "./capture.py")
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Println("error Getting the stdout pipe")
return
}
cmd.Start()
Теперь мы модем использовать multipart.Reader
для чтения одного кадра в память. Мы можем считывать и записывать кадры когда и сколько захотим.
mr := multipart.NewReader(stdout, boundary)
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
log.Println("error reading next part", err)
return
}
jp, err := ioutil.ReadAll(p)
if err != nil {
log.Println("error reading from bytes ", err)
continue
}
}
Дальше мы можем использовать Facebox SDK для работы с запущенным инстансом Facebox и, собственно, распознавать лица.
jpr := bytes.NewReader(jp)
// проверяем, что в кадре человек, которого мы можем распознать
faces, err := fbox.Check(jpr)
if err != nil {
log.Println("error calling facebox", err)
continue
}
// для всех распознанных мы можем выполнить определенные действия,
// например открыть двор
for _, face := range faces {
if face.Matched {
fmt.Println("I know you ", face.Name)
} else {
fmt.Println("I do not know you ")
}
}
Как только мы провели все манипуляции с кадром, его необходимо отдать пользователю. Нужно принять во внимание, что все выполняется в рамках одной рутины, а это неплохо так будет тормозить воспроизведение видео. К тому же, распознавание лиц очень затратная операция по ЦПУ. Если вы не хотите чтобы видео тормозило, то распознавание нужно выполнять в отдельной рутине.
Полный код http обработчика faceboxhandler
:
func facebox(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary="+boundary)
cmd := exec.CommandContext(r.Context(), "./capture.py")
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Println("error getting the stdout pipe")
return
}
cmd.Start()
mr := multipart.NewReader(stdout, boundary)
for {
p, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
log.Println("error reading next part", err)
return
}
jp, err := ioutil.ReadAll(p)
if err != nil {
log.Println("error reading from bytes ", err)
continue
}
jpr := bytes.NewReader(jp)
faces, err := fbox.Check(jpr)
if err != nil {
log.Println("error calling facebox", err)
continue
}
for _, face := range faces {
if face.Matched {
fmt.Println("I know you ", face.Name)
} else {
fmt.Println("I do not know you ")
}
}
w.Write([]byte("Content-Type: image/jpeg\r\n"))
w.Write([]byte("Content-Length: " + string(len(jp)) + "\r\n\r\n"))
w.Write(jp)
w.Write([]byte("\r\n"))
w.Write([]byte("--informs\r\n"))
}
cmd.Wait()
}
Чтобы запустить все что мы написали, нужно открыть в браузере http://localhost:8081/facebox
. На этой странице будет крутиться видео(с большим лагом, правда), а в консоли будут выводиться сообщения. Конечно, все это будет работать только если вы обучили свой Facebox.
Заключение
Теперь вы знаете как с помощью Go и Python можно быстро сделать видео сервис. А при небольшом усилии еще и добавить туда распознавание лиц.
- Для использования Facebox нужно создать аккаунт на machinebox.io
- Полный код примера можно найти на github: https://github.com/machinebox/webcamFacebox