├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── camera.py ├── capture.py ├── conf.py ├── images └── not_found.jpeg ├── requirements.txt ├── server.py └── templates ├── index.html ├── send_to_init.html └── stream.html /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | images/* 3 | !images/not_found.jpeg 4 | logs/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk update && apk add python3 4 | ADD ./ /data 5 | WORKDIR /data 6 | RUN pip3 install -r requirements.txt 7 | CMD python3 server.py -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2021 ramonus 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-opencv webcam streaming server # 2 | This is a simple python3 script that serves a tiny Flask video webserver that allows to take photos or see real time video streaming of a connected camera/webcam controlled with opencv. 3 | 4 | ## Requirements ## 5 | In order to execute the script you need to install opencv3 -> `import cv2` and some python modules. 6 | 7 | Move to the project folder and try: 8 | 9 | ### Try #1: ### 10 | ``` 11 | pip install -r requirements.txt 12 | ``` 13 | 14 | ### Try #2: ### 15 | ``` 16 | pip install flask opencv-python 17 | ``` 18 | If it gets any error, probably with `opencv-python` try to install them manually. 19 | 20 | ## Running ## 21 | To start the service `cd` to project folder and type `python server.py` or `python3 server.py` 22 | 23 | *It only runs on python3* 24 | 25 | Once done navigate to the ip of the server and access the port `5000`. 26 | 27 | http://localhost:5000 28 | 29 | ## Configuration ## 30 | ### Change running port ### 31 | The project is default configured to run at port 5000. To change the running port you must specify the argument `-p, --port [PORT]`. 32 | 33 | *Example:* 34 | 35 | ```python3 server.py -p 3000 ``` Runs on port 3000 36 | 37 | ### Change camera source ### 38 | To change the camera source of opencv you can go to the beginning of file `server.py` and add a `video_source=1` it can be 0,1,2... as many video inputs the device has in the declaration of object `Camera`. Or you can change the default video source on `camera.py` `Camera` class `__init__` method. 39 | 40 | -------------------------------------------------------------------------------- /camera.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import threading 3 | import time 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | thread = None 9 | 10 | class Camera: 11 | def __init__(self,fps=20,video_source=0): 12 | logger.info(f"Initializing camera class with {fps} fps and video_source={video_source}") 13 | self.fps = fps 14 | self.video_source = video_source 15 | self.camera = cv2.VideoCapture(self.video_source) 16 | # We want a max of 5s history to be stored, thats 5s*fps 17 | self.max_frames = 5*self.fps 18 | self.frames = [] 19 | self.isrunning = False 20 | def run(self): 21 | logging.debug("Perparing thread") 22 | global thread 23 | if thread is None: 24 | logging.debug("Creating thread") 25 | thread = threading.Thread(target=self._capture_loop,daemon=True) 26 | logger.debug("Starting thread") 27 | self.isrunning = True 28 | thread.start() 29 | logger.info("Thread started") 30 | 31 | def _capture_loop(self): 32 | dt = 1/self.fps 33 | logger.debug("Observation started") 34 | while self.isrunning: 35 | v,im = self.camera.read() 36 | if v: 37 | if len(self.frames)==self.max_frames: 38 | self.frames = self.frames[1:] 39 | self.frames.append(im) 40 | time.sleep(dt) 41 | logger.info("Thread stopped successfully") 42 | 43 | def stop(self): 44 | logger.debug("Stopping thread") 45 | self.isrunning = False 46 | def get_frame(self, _bytes=True): 47 | if len(self.frames)>0: 48 | if _bytes: 49 | img = cv2.imencode('.png',self.frames[-1])[1].tobytes() 50 | else: 51 | img = self.frames[-1] 52 | else: 53 | with open("images/not_found.jpeg","rb") as f: 54 | img = f.read() 55 | return img 56 | -------------------------------------------------------------------------------- /capture.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import datetime, time 3 | from pathlib import Path 4 | 5 | def capture_and_save(im): 6 | s = im.shape 7 | # Add a timestamp 8 | font = cv2.FONT_HERSHEY_SIMPLEX 9 | bottomLeftCornerOfText = (10,s[0]-10) 10 | fontScale = 1 11 | fontColor = (20,20,20) 12 | lineType = 2 13 | 14 | cv2.putText(im,datetime.datetime.now().isoformat().split(".")[0],bottomLeftCornerOfText,font,fontScale,fontColor, lineType) 15 | 16 | m = 0 17 | p = Path("images") 18 | for imp in p.iterdir(): 19 | if imp.suffix == ".png" and imp.stem != "last": 20 | num = imp.stem.split("_")[1] 21 | try: 22 | num = int(num) 23 | if num>m: 24 | m = num 25 | except: 26 | print("Error reading image number for",str(imp)) 27 | m +=1 28 | lp = Path("images/last.png") 29 | if lp.exists() and lp.is_file(): 30 | np = Path("images/img_{}.png".format(m)) 31 | np.write_bytes(lp.read_bytes()) 32 | cv2.imwrite("images/last.png",im) 33 | 34 | if __name__=="__main__": 35 | capture_and_save() 36 | print("done") -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | p = Path("logs") 4 | if not p.exists(): 5 | p.mkdir() 6 | 7 | dictConfig = { 8 | 'version': 1, 9 | 'disable_existing_loggers': True, 10 | 'formatters': { 11 | 'standard': { 12 | 'format': '%(asctime)s [%(levelname)s] %(name)s:: %(message)s', 13 | }, 14 | }, 15 | 'handlers': { 16 | 'default': { 17 | 'level': 'INFO', 18 | 'formatter': 'standard', 19 | 'class': 'logging.StreamHandler', 20 | 'stream': 'ext://sys.stdout', 21 | }, 22 | 'file': { 23 | 'class': 'logging.handlers.RotatingFileHandler', 24 | 'level': 'DEBUG', 25 | 'formatter': 'standard', 26 | 'filename': 'logs/logfile.log', 27 | 'mode': 'a', 28 | 'maxBytes': 5_242_880, 29 | 'backupCount': 3, 30 | 'encoding': 'utf-8', 31 | }, 32 | }, 33 | 'loggers': { 34 | '__main__': { 35 | 'handlers': ['default','file'], 36 | 'level': 'DEBUG', 37 | 'propagate': False, 38 | }, 39 | 'camera': { 40 | 'handlers': ['default', 'file'], 41 | 'level': 'DEBUG', 42 | 'propagate': False, 43 | }, 44 | } 45 | } -------------------------------------------------------------------------------- /images/not_found.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramonus/flask-video-stream/ae2307c19930fe65c0edd2d87feaccb129995193/images/not_found.jpeg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | flask -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, send_from_directory, Response 2 | # from flask_socketio import SocketIO 3 | from pathlib import Path 4 | from capture import capture_and_save 5 | from camera import Camera 6 | import argparse, logging, logging.config, conf 7 | 8 | logging.config.dictConfig(conf.dictConfig) 9 | logger = logging.getLogger(__name__) 10 | 11 | camera = Camera() 12 | camera.run() 13 | 14 | app = Flask(__name__) 15 | # app.config["SECRET_KEY"] = "secret!" 16 | # socketio = SocketIO(app) 17 | 18 | @app.after_request 19 | def add_header(r): 20 | """ 21 | Add headers to both force latest IE rendering or Chrome Frame, 22 | and also to cache the rendered page for 10 minutes 23 | """ 24 | r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" 25 | r.headers["Pragma"] = "no-cache" 26 | r.headers["Expires"] = "0" 27 | r.headers["Cache-Control"] = "public, max-age=0" 28 | return r 29 | 30 | @app.route("/") 31 | def entrypoint(): 32 | logger.debug("Requested /") 33 | return render_template("index.html") 34 | 35 | @app.route("/r") 36 | def capture(): 37 | logger.debug("Requested capture") 38 | im = camera.get_frame(_bytes=False) 39 | capture_and_save(im) 40 | return render_template("send_to_init.html") 41 | 42 | @app.route("/images/last") 43 | def last_image(): 44 | logger.debug("Requested last image") 45 | p = Path("images/last.png") 46 | if p.exists(): 47 | r = "last.png" 48 | else: 49 | logger.debug("No last image") 50 | r = "not_found.jpeg" 51 | return send_from_directory("images",r) 52 | 53 | 54 | def gen(camera): 55 | logger.debug("Starting stream") 56 | while True: 57 | frame = camera.get_frame() 58 | yield (b'--frame\r\n' 59 | b'Content-Type: image/png\r\n\r\n' + frame + b'\r\n') 60 | 61 | @app.route("/stream") 62 | def stream_page(): 63 | logger.debug("Requested stream page") 64 | return render_template("stream.html") 65 | 66 | @app.route("/video_feed") 67 | def video_feed(): 68 | return Response(gen(camera), 69 | mimetype="multipart/x-mixed-replace; boundary=frame") 70 | 71 | if __name__=="__main__": 72 | # socketio.run(app,host="0.0.0.0",port="3005",threaded=True) 73 | parser = argparse.ArgumentParser() 74 | parser.add_argument('-p','--port',type=int,default=5000, help="Running port") 75 | parser.add_argument("-H","--host",type=str,default='0.0.0.0', help="Address to broadcast") 76 | args = parser.parse_args() 77 | logger.debug("Starting server") 78 | app.run(host=args.host,port=args.port) 79 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |