Preliminary implementation
This commit is contained in:
commit
9ef231d582
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
stream*
|
||||
extract*.mp4
|
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
To stream from h264 hardware encoder accelerated USB webcam:
|
||||
|
||||
gst-launch-1.0 -v v4l2src device=/dev/video2 \
|
||||
! capsfilter caps="video/x-h264, width=1280, height=720, framerate=20/1" \
|
||||
! rtph264pay config-interval=10 pt=96 \
|
||||
! udpsink host=1.2.3.4 port=5000
|
||||
|
||||
Run server.py on 1.2.3.4
|
||||
|
||||
Files are written to currect directory
|
||||
|
||||
For video feed open: http://localhost:6001/?frameskip=5&subsampling=4&quality=50&thumbs=0
|
19
extract.py
Normal file
19
extract.py
Normal file
@ -0,0 +1,19 @@
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
_, path = sys.argv
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%d-%H-%M-%S"
|
||||
|
||||
clip_start = datetime.strptime(path[7:26], TIME_FORMAT)
|
||||
event_no = 1
|
||||
for line in open(path):
|
||||
frame, raw_start, raw_end = line.strip().split(";")
|
||||
event_start, event_end = datetime.strptime(raw_start, TIME_FORMAT), datetime.strptime(raw_end, TIME_FORMAT)
|
||||
cmd = "ffmpeg -ss %s -i %s -t %s -vcodec copy extracted-%s-%d.mp4" % (
|
||||
event_start-clip_start, path.replace(".events", ".mp4"), event_end-event_start,
|
||||
clip_start.strftime(TIME_FORMAT), event_no)
|
||||
print("executing:", cmd)
|
||||
os.system(cmd)
|
||||
event_no += 1
|
||||
|
199
server.py
Normal file
199
server.py
Normal file
@ -0,0 +1,199 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from flask import Flask, Response
|
||||
from gevent import pywsgi
|
||||
from threading import Thread, Event
|
||||
from time import sleep
|
||||
from flask import request
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
|
||||
TIME_FORMAT = "%Y-%m-%d-%H-%M-%S"
|
||||
MOTION_GREEN_THRESHOLD = 30
|
||||
MOTION_AREA_THRESHOLD = 30
|
||||
MOTION_FRAMES_THRESHOLD = 20
|
||||
|
||||
# 2 to power of SLIDE_WINDOW is the count of frames kept in memory for bg detection
|
||||
SLIDE_WINDOW = 5
|
||||
|
||||
# For thumbnail use only every n-th pixel
|
||||
THUMBNAIL_SUBSAMPLING = 4
|
||||
|
||||
# For motion detection sample only every n-th pixel
|
||||
MOTION_SUBSAMPLING = 8
|
||||
|
||||
# Set to True to automatically spawn Gstreamer process per camera and redirect packets to it
|
||||
AUTOMUX = False
|
||||
|
||||
PIPELINE = """udpsrc port=%(port)d caps = "application/x-rtp, encoding-name=(string)H264" \
|
||||
! rtph264depay \
|
||||
! tee name=t \
|
||||
! decodebin \
|
||||
! videoconvert \
|
||||
! appsink \
|
||||
t. ! h264parse \
|
||||
! mp4mux streamable=true faststart=true fragment-duration=1000 dts-method=1 \
|
||||
! filesink async=0 location=%(filename)s.mp4
|
||||
""".replace("\n", "")
|
||||
|
||||
def detect_stream(port, addr):
|
||||
now = 123
|
||||
app = Flask(__name__)
|
||||
events = deque(maxlen=THUMBNAIL_SUBSAMPLING)
|
||||
frame = None
|
||||
event = Event()
|
||||
|
||||
def generator(subsampling, frameskip, motion, quality, thumbs):
|
||||
while True:
|
||||
for j in range(0, frameskip):
|
||||
event.wait()
|
||||
yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n'
|
||||
if motion:
|
||||
for i in range(0, 2):
|
||||
for j in range(0, 2):
|
||||
frame[i::MOTION_SUBSAMPLING,j::MOTION_SUBSAMPLING,0] = 0
|
||||
frame[i::MOTION_SUBSAMPLING,j::MOTION_SUBSAMPLING,1] = 0
|
||||
frame[i::MOTION_SUBSAMPLING,j::MOTION_SUBSAMPLING,2] = thresh
|
||||
|
||||
|
||||
if thumbs:
|
||||
stacked = np.vstack([frame, np.hstack(events)])
|
||||
else:
|
||||
stacked = frame
|
||||
ret, jpeg = cv2.imencode('.jpg', stacked[::subsampling,::subsampling],
|
||||
(cv2.IMWRITE_JPEG_QUALITY, quality))
|
||||
yield jpeg.tostring()
|
||||
yield b'\r\n\r\n'
|
||||
|
||||
@app.route('/')
|
||||
def video_combined():
|
||||
return Response(generator(
|
||||
request.args.get('subsampling', default = 1, type = int),
|
||||
request.args.get('frameskip', default = 0, type = int),
|
||||
request.args.get('motion', default = 0, type = int),
|
||||
request.args.get('quality', default = 50, type = int),
|
||||
request.args.get('thumbs', default = 1, type = int),
|
||||
), mimetype='multipart/x-mixed-replace; boundary=frame')
|
||||
|
||||
print("Server listening on TCP port", port)
|
||||
|
||||
class WebThread(Thread):
|
||||
def run(self):
|
||||
print("Web server running for port", port)
|
||||
app.run(threaded=True, port=port+1000)
|
||||
|
||||
thread = WebThread()
|
||||
thread.start()
|
||||
|
||||
fh = None
|
||||
cap = None
|
||||
frames = []
|
||||
avg = None
|
||||
|
||||
motion_detected = False
|
||||
motion_start = None
|
||||
motion_seen = 0
|
||||
|
||||
while True:
|
||||
if not cap:
|
||||
now = datetime.now()
|
||||
timestamp = now.strftime(TIME_FORMAT)
|
||||
filename = "stream-%(timestamp)s-%(addr)s" % locals()
|
||||
if fh:
|
||||
fh.close()
|
||||
fh = open("%s.events" % filename, "w")
|
||||
pipeline = PIPELINE % locals()
|
||||
print("gst-launch-1.0 -v", pipeline.replace("appsink", "autovideosink"))
|
||||
frame_count = 0
|
||||
cap = cv2.VideoCapture(pipeline)
|
||||
ret, frame = cap.read()
|
||||
if ret == False:
|
||||
cap = None
|
||||
sleep(3)
|
||||
continue
|
||||
|
||||
frame_count += 1
|
||||
thumbnail = frame[::THUMBNAIL_SUBSAMPLING,::THUMBNAIL_SUBSAMPLING].copy()
|
||||
if not events:
|
||||
for j in range(0, THUMBNAIL_SUBSAMPLING):
|
||||
events.append(np.zeros(thumbnail.shape))
|
||||
|
||||
reference = np.uint16(frame[::MOTION_SUBSAMPLING,::MOTION_SUBSAMPLING,1])
|
||||
frames.append(reference)
|
||||
if avg is None:
|
||||
avg = np.copy(reference)
|
||||
else:
|
||||
avg += reference
|
||||
if len(frames) <= 2 ** SLIDE_WINDOW:
|
||||
continue
|
||||
|
||||
avg -= frames[0]
|
||||
frames = frames[1:]
|
||||
|
||||
delta = cv2.absdiff(reference, avg >> SLIDE_WINDOW)
|
||||
thresh = cv2.inRange(delta, MOTION_GREEN_THRESHOLD, 255)
|
||||
im2, contours, hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
|
||||
|
||||
motion_detected = False
|
||||
|
||||
for cnt in contours:
|
||||
x, y, w, h = [j*MOTION_SUBSAMPLING for j in cv2.boundingRect(cnt)]
|
||||
if w < MOTION_AREA_THRESHOLD or h < MOTION_AREA_THRESHOLD:
|
||||
continue
|
||||
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 4);
|
||||
motion_detected = True
|
||||
|
||||
|
||||
if motion_detected:
|
||||
if motion_seen < MOTION_FRAMES_THRESHOLD:
|
||||
motion_seen += 1
|
||||
elif not motion_start:
|
||||
events.append(thumbnail)
|
||||
motion_start = datetime.now()
|
||||
print("Event started:", motion_start)
|
||||
else:
|
||||
if motion_seen > 0:
|
||||
motion_seen -= 1
|
||||
if motion_seen == 0 and motion_start:
|
||||
motion_end = datetime.now()
|
||||
print("Got event:", motion_start, "to", motion_end)
|
||||
fh.write("%d;%s;%s\n" % (frame_count, motion_start.strftime(TIME_FORMAT), motion_end.strftime(TIME_FORMAT)))
|
||||
motion_start = None
|
||||
|
||||
event.set()
|
||||
event.clear()
|
||||
|
||||
if AUTOMUX:
|
||||
import os
|
||||
import socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.settimeout(5)
|
||||
sock.bind(("", 5000))
|
||||
next_port = 5001
|
||||
mapping = dict()
|
||||
print("Flushing iptables")
|
||||
os.system("iptables -F PREROUTING -t nat")
|
||||
os.system("iptables -F OUTPUT -t nat")
|
||||
os.system("sysctl -w net.ipv4.conf.all.route_localnet=1")
|
||||
print("Listening on UDP port 5000")
|
||||
while True:
|
||||
try:
|
||||
buf, (addr, port) = sock.recvfrom(20)
|
||||
except OSError: # timed out
|
||||
continue
|
||||
|
||||
if addr not in mapping:
|
||||
mapping[addr] = next_port
|
||||
print("Redirecting", addr, "to", next_port)
|
||||
os.system("iptables -I PREROUTING -t nat -p udp --dport 5000 -s %(addr)s -j REDIRECT --to-port %(next_port)d" % locals())
|
||||
os.system("iptables -t nat -I OUTPUT -o lo -p udp --dport 5000 -j REDIRECT --to %(next_port)d" % locals())
|
||||
|
||||
if not os.fork():
|
||||
print("Spawning process for", addr, next_port)
|
||||
detect_stream(next_port, addr)
|
||||
break
|
||||
next_port += 1
|
||||
else:
|
||||
detect_stream(5001, "127.0.0.1")
|
Loading…
Reference in New Issue
Block a user