207 lines
6 KiB
Python
207 lines
6 KiB
Python
import os
|
|
import queue
|
|
import socket
|
|
import threading
|
|
|
|
import cv2
|
|
from flask import Flask, Response
|
|
|
|
from compLib.LogstashLogging import Logging
|
|
|
|
RTMP_SERVER = os.getenv("RTMP_SERVER", "rtmp://localhost/live/stream")
|
|
SERVE_VIDEO = os.getenv("SERVER_SRC", "/live")
|
|
BUILDING_DOCS = os.getenv("BUILDING_DOCS", "false")
|
|
|
|
app = Flask(__name__)
|
|
|
|
HTML = """
|
|
<html>
|
|
<head>
|
|
<title>Opencv Output</title>
|
|
</head>
|
|
<body>
|
|
<h1>Opencv Output</h1>
|
|
<img src="{{ VIDEO_DST }}">
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# it would be better to use jinja2 here, but I don't want to blow up the package dependencies...
|
|
HTML = HTML.replace("{{ VIDEO_DST }}", SERVE_VIDEO)
|
|
|
|
|
|
class __Streaming:
|
|
"""
|
|
Class that handles rtmp streaming for opencv.
|
|
|
|
DO NOT CREATE AN INSTANCE OF THIS CLASS YOURSELF!
|
|
|
|
This is automatically done when importing this module. Use Vision.Streaming which is
|
|
an instance of this class!
|
|
|
|
grab frames -> do your own processing -> publish frame -> view on http server
|
|
"""
|
|
|
|
class __NoBufferVideoCapture:
|
|
def __init__(self, cam):
|
|
self.cap = cv2.VideoCapture(cam)
|
|
self.cap.set(3, 640)
|
|
self.cap.set(4, 480)
|
|
self.q = queue.Queue(maxsize=5)
|
|
t = threading.Thread(target=self._reader)
|
|
t.daemon = True
|
|
t.start()
|
|
|
|
def _reader(self):
|
|
while True:
|
|
ret, frame = self.cap.read()
|
|
if not ret:
|
|
break
|
|
if not self.q.empty():
|
|
try:
|
|
self.q.get_nowait()
|
|
except queue.Empty:
|
|
pass
|
|
self.q.put(frame)
|
|
|
|
def read(self):
|
|
return self.q.get()
|
|
|
|
def __init__(self):
|
|
"""
|
|
Create instance of __Streaming class
|
|
|
|
This is done implicitly when importing the vision module and will only fail if you would
|
|
create an object of this class. (There can (SHOULD!) only be one VideCapture)
|
|
"""
|
|
Logging.get_logger().info("capturing rtmp stream is disabled in this version")
|
|
#self.__camera_stream = cv2.VideoCapture(RTMP_SERVER)
|
|
#self.__camera_stream = cv2.VideoCapture(0)
|
|
#self.__camera_stream.set(3, 640)
|
|
#self.__camera_stream.set(4, 480)
|
|
self.__camera_stream = self.__NoBufferVideoCapture(0)
|
|
self.__newest_frame = None
|
|
self.__lock = threading.Lock()
|
|
Logging.get_logger().info("Initialized vision")
|
|
|
|
def get_frame(self):
|
|
"""
|
|
Grab the newest frame from the rtmp stream.
|
|
|
|
:return: An opencv frame
|
|
"""
|
|
#ret, img16 = self.__camera_stream.read()
|
|
img16 = self.__camera_stream.read()
|
|
return img16
|
|
|
|
def publish_frame(self, image):
|
|
"""
|
|
Publish an opencv frame to the http webserver.
|
|
|
|
:param image: Opencv frame that will be published
|
|
:return: None
|
|
"""
|
|
with self.__lock:
|
|
if image is not None:
|
|
self.__newest_frame = image.copy()
|
|
|
|
def _newest_frame_generator(self):
|
|
"""
|
|
Private generator which is called directly from flask server.
|
|
|
|
:return: Yields image/jpeg encoded frames published from publish_frame function.
|
|
"""
|
|
while True:
|
|
# use a buffer frame to copy the newest frame with lock and then freeing it immediately
|
|
buffer_frame = None
|
|
with self.__lock:
|
|
if self.__newest_frame is None:
|
|
continue
|
|
|
|
buffer_frame = self.__newest_frame.copy()
|
|
|
|
# encode frame for jpeg stream
|
|
(flag, encoded_image) = cv2.imencode(".jpg", buffer_frame)
|
|
|
|
# if there was an error try again with the next frame
|
|
if not flag:
|
|
continue
|
|
|
|
# else yield encoded frame with mimetype image/jpeg
|
|
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' +
|
|
bytearray(encoded_image) + b'\r\n')
|
|
|
|
|
|
Streaming = None
|
|
if BUILDING_DOCS == "false":
|
|
# instantiate private class __Streaming
|
|
Streaming = __Streaming()
|
|
Logging.get_logger().info("created instance of streaming class")
|
|
|
|
|
|
@app.route("/live")
|
|
def __video_feed():
|
|
"""
|
|
Define route for serving jpeg stream.
|
|
|
|
:return: Return the response generated along with the specific media.
|
|
"""
|
|
return Response(Streaming._newest_frame_generator(),
|
|
mimetype="multipart/x-mixed-replace; boundary=frame")
|
|
|
|
|
|
@app.route("/")
|
|
def __index():
|
|
"""
|
|
Define route for serving a static http site to view the stream.
|
|
|
|
:return: Static html page
|
|
"""
|
|
return HTML
|
|
|
|
|
|
def get_ip():
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
try:
|
|
s.connect(('10.255.255.255', 1))
|
|
IP = s.getsockname()[0]
|
|
except Exception as e:
|
|
IP = '127.0.0.1'
|
|
print(f"Error could not query ip: {e}")
|
|
finally:
|
|
s.close()
|
|
return IP
|
|
|
|
|
|
def __start_flask():
|
|
"""
|
|
Function for running flask server in a thread.
|
|
|
|
:return:
|
|
"""
|
|
Logging.get_logger().info("starting flask server")
|
|
app.run(host="0.0.0.0", port=9898, debug=True, threaded=True, use_reloader=False)
|
|
ip = get_ip()
|
|
Logging.get_logger().info(f"Vision stream started and can be viewed on: {ip}:9898")
|
|
print(f"\033[92mVision stream started and can be viewed on: {ip}:9898\033[0m")
|
|
|
|
|
|
if BUILDING_DOCS == "false":
|
|
# start flask service in the background
|
|
__webserver_thread = threading.Thread(target=__start_flask)
|
|
__webserver_thread.start()
|
|
|
|
# for debugging and testing start processing frames and detecting a 6 by 9 calibration chessboard
|
|
if __name__ == '__main__' and BUILDING_DOCS == "false":
|
|
while True:
|
|
frame = Streaming.get_frame()
|
|
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
|
|
|
|
# processing
|
|
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
|
|
|
# find the chessboard corners
|
|
ret, corners = cv2.findChessboardCorners(gray, (6, 9), None)
|
|
|
|
cv2.drawChessboardCorners(frame, (6, 9), corners, ret)
|
|
Streaming.publish_frame(frame)
|