In this tutorial we'll be:
- creating a Flask-based web app for taking pictures using OpenCV;
- dockerizing this app; then
- building a multi-container app with Docker Compose for photo capture and storage.
Creating our web camera app
First off, we'll use Flask, a light web framework, to quickly set up a web application. This app will also use OpenCV for taking pictures.
For this part of the tutorial, we'll give a brief overview, since we're really focused on the dockerization aspect of this app, but all code can be found on github:
There are two main pages for the app, index
, where you can take a picture, and capture
, where you can see a captured picture, and download it or email it to yourself or someone else.
So we define routes for each of these pages, which will render the respective templates:
# route for index (picture-taking) page
@app.route('/index/')
def index():
return render_template('index.html')
# route for capturing picture
@app.route('/capture/')
def capture():
camera = get_camera()
stamp = camera.capture()
return redirect(url_for('show_capture', timestamp=stamp))
# route for showing captured picture
@app.route('/capture/image/<timestamp>', methods=['POST', 'GET'])
def show_capture(timestamp):
path = stamp_file(timestamp)
...
return render_template('capture.html',
stamp=timestamp, path=path, email_msg=email_msg)
Note that we omit the code that handles email sending, which will be a POST
request on the same route as that for showing a captured picture (implemented by show_capture
).
Besides these functions, a critical part of our code is the one that gets the camera feed:
def get_camera():
global camera
if not camera:
camera = Camera()
return camera
def gen(camera):
while True:
frame = camera.get_feed()
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
@app.route('/video_feed/')
def video_feed():
camera = get_camera()
return Response(gen(camera),
mimetype='multipart/x-mixed-replace; boundary=frame')
So Camera is a class we created, and which we'll get into in a minute. get_camera
implements what is basically a singleton pattern, lazily loading a single instance of the Camera when necessary.
video_feed
is a route used inside index
, in the index.html
template, as an image link:
<div id="camera">
<img src="{{ url_for('video_feed') }}" alt="video feed">
</div>
So the video_feed
returns a Response object containing an iterator which continuously generates frames from our Camera, and the corresponding mimetype multipart/x-mixed-replace
, which allows us to continuously replace one frame with the next.
gen
is a generator function which constantly gets a camera frame (a binary jpeg image) and returns binary data containing that image as well as the necessary headers.
Now, as for the Camera class: it basically instantiates an OpenCV VideoCapture object and gives frames as necessary, also capturing a particular frame when requested. Capturing implies saving the image locally (temporarily, at least), and returning a timestamp which serves as an unique identifier.
class Camera(object):
CAPTURES_DIR = "static/captures/"
def __init__(self):
self.video = cv.VideoCapture(0)
def get_frame(self):
success, frame = self.video.read()
...
return frame
def get_feed(self):
frame = self.get_frame()
if frame is not None:
ret, jpeg = cv.imencode('.jpg', frame)
return jpeg.tobytes()
def capture(self):
frame = self.get_frame()
timestamp = strftime("%d-%m-%Y-%Hh%Mm%Ss", localtime())
filename = Camera.CAPTURES_DIR + timestamp +".jpg"
if not cv.imwrite(filename, frame):
raise RuntimeError("Unable to capture image "+timestamp)
return timestamp
We can now run this code with python web.py
. Note that it's running on debug mode on localhost:8080
. That can be changed in a number of ways, such as editing the line below on main
, but be sure to check out the Flask docs for more details.
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
We can see below what our image capturing page and capture viewing page look like:
Dockerizing our camera app
When building our Dockerfile, we'll start from a Linux base (Ubuntu, in this case) instead of using a straight Python environment, as it's a little more flexible, and makes dependency management clearer. But, you always have the option of building off of the Python image instead. This is done in the first line: FROM ubuntu:18.04
.
Now, we embed some metadata into our image with the LABEL
instruction. In this case, we simply define the project maintainer, like so: LABEL maintainer="me@mail.com"
(this is of course a template, which should be replaced with your own information).
Now we get into configuring our custom environment. First we update and install necessary packages:
RUN apt-get update -y && apt-get install -y build-essential cmake \
libsm6 libxext6 libxrender-dev \
python3 python3-pip python3-dev
The libsm6 libxext6 libxrender-dev
sequence of packages is important for a proper installation of OpenCV later on. Note that we will be using python 3, and pip as a package manager.
Next is WORKDIR /app
. This creates a directory at root, and sets it as the working directory for any following commands. We copy our requirements file first, before any source code, with COPY ./requirements.txt /app/requirements.txt
. We do this to take advantage of Docker's layer-based cache system.
Finally, the instruction RUN pip3 install -r requirements.txt
installs all of our dependencies (Flask and OpenCV), and we are ready to run our app. Note that we don't have to specify the whole path to the requirements file, as it is already in our working directory.
So, as our container will run as an executable (as opposed to a shell), we define our entry point: ENTRYPOINT ["python3"]
, then run our application with CMD ["web.py"]
. Below, we can review our whole Dockerfile.
FROM ubuntu:18.04
LABEL maintainer="me@mail.com"
RUN apt-get update -y && apt-get install -y build-essential cmake \
libsm6 libxext6 libxrender-dev \
python3 python3-pip python3-dev
COPY ./requirements.txt /app/requirements.txt
WORKDIR /app
RUN pip3 install -r requirements.txt
COPY . /app
ENTRYPOINT ["python3"]
CMD ["web.py"]
Running docker build -t camera-app .
will now build our image named camera-app. We can then instantiate a container from it with the following command: docker run -d --name cam --device /dev/video0 -p 8080:8080 camera-app
.
-
-d
, or--detach
, makes the container run in the background. -
--device /dev/video0
adds a device from the host, in this case a camera, to the container. -
-p 8080:8080
, for--publish
, will publish or map a container's port (the one on the right side of the colon) to a port on the host (on the left).
Photo storage with docker-compose
Now, we have our Flask web app working with Docker. But, pictures captured do not persist and are not easily accessible, being in a temporary volume created by Docker.
So we will use a self-hosted file storage server with a web interface called droppy:
silverwind / droppy
**ARCHIVED** Self-hosted file storage
We use droppy because it is very lightweight, has a simple web interface, and has a very easy setup, since it provides a ready to use Docker image. But keep in mind another type of server for image storage could also be used with Docker, and would have a very similar setup.
Droppy will run as a separate container from the one running our camera app. But these two container can be orchestrated together easily, thanks to Docker Compose.
Our docker-compose file (which is a yaml data file) will have two important parts: volumes
and services
. In volumes
, we create a volume called captures
, which will store our captured images and be shared between the two containers.
In services
, we configure our two services, which will run off of our two images: camera-app, and droppy. In the file below, we can see these two services have some things in common. For both, we define
-
container_name
, the name of the container , once it is instantiated; -
image
, the image which will be used to instantiate the container; -
ports
, similarly to the-p
flag discussed previously, will map the necessary ports for the localhost; and -
volumes
will mount the volume we created (captures
) into the directory where the images are saved in the camera-app container, and also into the file storage directory in the droppy container.
This last instruction makes it so that, essentially, images are uploaded to droppy as soon as they are captured by our camera app. Besides these shared fields, we still have to map our camera device from the host to the camera app container, as we have before, on devices
.
version: '3'
volumes:
captures:
services:
droppy:
container_name: droppy
image: silverwind/droppy
ports:
- '127.0.0.1:8989:8989'
volumes:
- captures:/files
camera-app:
container_name: cam
image: camera-app
ports:
- '127.0.0.1:8080:8080'
devices:
- '/dev/video0:/dev/video0'
volumes:
- captures:/app/static/captures
Running this setup with docker-compose up
, we can now access our camera at localhost:8080
, and our droppy image server at localhost:8989
. Any image taken on the camera app will show up on droppy server, like we see below.
When you're done, make sure to use the -v
flag when running docker-compose down
if you wish to clear the volume we created, containing all images captured. Docker Compose makes integrating applications like this very straightforward, so have fun configuring this setup for your own needs, or running your very own containers!
Top comments (3)
Awesome!!
Adding "frame=cv.flip(frame, 1)" after "success, frame = self.video.read()" can make it works like a mirror.
Muito bom!!