├── .gitignore ├── Procfile ├── README.md ├── app.py ├── camera.py ├── makeup_artist.py ├── requirements.txt ├── runtime.txt ├── static └── js │ └── main.js ├── templates └── index.html └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.pyc 3 | staticfiles 4 | .env 5 | db.sqlite3 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn -k eventlet -w 1 app:app --log-file=- 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a toy project for learning how to use a couple of python libraries. 2 | 3 | ### Repo status 4 | 5 | This repo is **unmaintained**. I guess I never intended for others to use this (and never imagined my terrible code would be of any value to others), but I recognize that a nonzero number of people have stumbled upon this repo and may have found it useful as a reference. 6 | Given that, even though I do not plan on maintaining this repo, I am happy for people to 7 | 8 | 1) submit pull requests to fix known issues (there are quite a few), or to 9 | 2) fork and make your own version. I'd be happy to link to a better maintained fork for future readers' benefit. 10 | 11 | ### What it does 12 | 13 | 1) The web client sends video stream data (from the user's webcam) to a flask server using socketio 14 | 2) The server does some processing on the video stream 15 | 3) The client receives the processed video stream and re-displays the results in a different frame 16 | 17 | In the demo site, the server is simply flipping the image horizontally. You could imagine it doing something more sophisticated (e.g. applying some filters), but obviously I was too lazy to implement anything cool. 18 | 19 | ### Known issues 20 | 21 | - The server does not handle multiple clients well. If multiple clients connect, the server treats them as one and sends back mixed frames as a result. 22 | 23 | ### Demo 24 | [Live Demo](https://python-stream-video.herokuapp.com) 25 | 26 | ### Setup 27 | 28 | #### Optional 29 | 30 | - setup heroku (`brew install heroku`) 31 | - Use a python virtualenv 32 | 33 | #### Required 34 | - `git clone https://github.com/dxue2012/python-webcam-flask.git` 35 | - `pip install -r requirements.txt` 36 | 37 | ### Run locally 38 | 39 | IF YOU HAVE HEROKU: 40 | - `heroku local` 41 | IF NOT: 42 | - `gunicorn -k eventlet -w 1 app:app --log-file=-` 43 | 44 | - in your browser, navigate to localhost:5000 45 | 46 | ### Deploy to heroku 47 | 48 | - `git push heroku master` 49 | - heroku open 50 | 51 | ### Common Issues 52 | 53 | If you run into a 'protocol not found' error, see if [this stackoverflow answer helps](https://stackoverflow.com/questions/40184788/protocol-not-found-socket-getprotobyname). 54 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from sys import stdout 2 | from makeup_artist import Makeup_artist 3 | import logging 4 | from flask import Flask, render_template, Response 5 | from flask_socketio import SocketIO, emit 6 | from camera import Camera 7 | from utils import base64_to_pil_image, pil_image_to_base64 8 | 9 | 10 | app = Flask(__name__) 11 | app.logger.addHandler(logging.StreamHandler(stdout)) 12 | app.config['SECRET_KEY'] = 'secret!' 13 | app.config['DEBUG'] = True 14 | socketio = SocketIO(app) 15 | camera = Camera(Makeup_artist()) 16 | 17 | 18 | @socketio.on('input image', namespace='/test') 19 | def test_message(input): 20 | input = input.split(",")[1] 21 | camera.enqueue_input(input) 22 | image_data = input # Do your magical Image processing here!! 23 | #image_data = image_data.decode("utf-8") 24 | image_data = "data:image/jpeg;base64," + image_data 25 | print("OUTPUT " + image_data) 26 | emit('out-image-event', {'image_data': image_data}, namespace='/test') 27 | #camera.enqueue_input(base64_to_pil_image(input)) 28 | 29 | 30 | @socketio.on('connect', namespace='/test') 31 | def test_connect(): 32 | app.logger.info("client connected") 33 | 34 | 35 | @app.route('/') 36 | def index(): 37 | """Video streaming home page.""" 38 | return render_template('index.html') 39 | 40 | 41 | def gen(): 42 | """Video streaming generator function.""" 43 | 44 | app.logger.info("starting to generate frames!") 45 | while True: 46 | frame = camera.get_frame() #pil_image_to_base64(camera.get_frame()) 47 | yield (b'--frame\r\n' 48 | b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') 49 | 50 | 51 | @app.route('/video_feed') 52 | def video_feed(): 53 | """Video streaming route. Put this in the src attribute of an img tag.""" 54 | return Response(gen(), mimetype='multipart/x-mixed-replace; boundary=frame') 55 | 56 | 57 | if __name__ == '__main__': 58 | socketio.run(app) 59 | -------------------------------------------------------------------------------- /camera.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import binascii 3 | from time import sleep 4 | from utils import base64_to_pil_image, pil_image_to_base64 5 | 6 | 7 | class Camera(object): 8 | def __init__(self, makeup_artist): 9 | self.to_process = [] 10 | self.to_output = [] 11 | self.makeup_artist = makeup_artist 12 | 13 | thread = threading.Thread(target=self.keep_processing, args=()) 14 | thread.daemon = True 15 | thread.start() 16 | 17 | def process_one(self): 18 | if not self.to_process: 19 | return 20 | 21 | # input is an ascii string. 22 | input_str = self.to_process.pop(0) 23 | 24 | # convert it to a pil image 25 | input_img = base64_to_pil_image(input_str) 26 | 27 | ################## where the hard work is done ############ 28 | # output_img is an PIL image 29 | output_img = self.makeup_artist.apply_makeup(input_img) 30 | 31 | # output_str is a base64 string in ascii 32 | output_str = pil_image_to_base64(output_img) 33 | 34 | # convert eh base64 string in ascii to base64 string in _bytes_ 35 | self.to_output.append(binascii.a2b_base64(output_str)) 36 | 37 | def keep_processing(self): 38 | while True: 39 | self.process_one() 40 | sleep(0.01) 41 | 42 | def enqueue_input(self, input): 43 | self.to_process.append(input) 44 | 45 | def get_frame(self): 46 | while not self.to_output: 47 | sleep(0.05) 48 | return self.to_output.pop(0) 49 | -------------------------------------------------------------------------------- /makeup_artist.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | 3 | 4 | class Makeup_artist(object): 5 | def __init__(self): 6 | pass 7 | 8 | def apply_makeup(self, img): 9 | return img.transpose(Image.FLIP_LEFT_RIGHT) 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.7 2 | enum-compat==0.0.2 3 | enum34==1.1.6 4 | eventlet==0.20.0 5 | Flask==0.12.2 6 | Flask-SocketIO==2.9.1 7 | gevent==1.2.2 8 | gevent-websocket==0.10.1 9 | greenlet==0.4.12 10 | gunicorn==19.7.1 11 | itsdangerous==0.24 12 | Jinja2==2.9.6 13 | MarkupSafe==1.0 14 | numpy==1.13.1 15 | olefile==0.44 16 | opencv-python==3.2.0.7 17 | Pillow==4.2.1 18 | python-engineio==1.7.0 19 | python-socketio==1.7.7 20 | redis==2.10.5 21 | six==1.10.0 22 | Werkzeug==0.12.2 23 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.11 2 | -------------------------------------------------------------------------------- /static/js/main.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | let namespace = "/test"; 3 | let video = document.querySelector("#videoElement"); 4 | let canvas = document.querySelector("#canvasElement"); 5 | let ctx = canvas.getContext('2d'); 6 | photo = document.getElementById('photo'); 7 | var localMediaStream = null; 8 | 9 | var socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace); 10 | 11 | function sendSnapshot() { 12 | if (!localMediaStream) { 13 | return; 14 | } 15 | 16 | ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 300, 150); 17 | 18 | let dataURL = canvas.toDataURL('image/jpeg'); 19 | socket.emit('input image', dataURL); 20 | 21 | socket.emit('output image') 22 | 23 | var img = new Image(); 24 | socket.on('out-image-event',function(data){ 25 | 26 | 27 | img.src = dataURL//data.image_data 28 | photo.setAttribute('src', data.image_data); 29 | 30 | }); 31 | 32 | 33 | } 34 | 35 | socket.on('connect', function() { 36 | console.log('Connected!'); 37 | }); 38 | 39 | var constraints = { 40 | video: { 41 | width: { min: 640 }, 42 | height: { min: 480 } 43 | } 44 | }; 45 | 46 | navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { 47 | video.srcObject = stream; 48 | localMediaStream = stream; 49 | 50 | setInterval(function () { 51 | sendSnapshot(); 52 | }, 50); 53 | }).catch(function(error) { 54 | console.log(error); 55 | }); 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |