[Building the struct for a webcam video stream flask app]

This commit is contained in:
xoy 2023-08-23 13:02:25 +02:00 committed by Felix Erstfeld
parent 1228e5e5f0
commit dacad4db10
14 changed files with 296 additions and 362 deletions

52
.gitignore vendored
View File

@ -1,50 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
env
.env/
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
dev.env
# vercel
.vercel
#logs
*.log
#_pycache
__pycache__/ __pycache__/
test.py
#image_files static/images/*
*.jpg venv/
*.jpeg
*.png

View File

@ -1,88 +1,38 @@
# Make you own Raspberry Pi Camera Stream # Flask Camera Live Stream
Create your own live stream from a Raspberry Pi using the Pi camera module. Build your own applications from here. [Original Project](https://github.com/EbenKouao/pi-camera-stream-flask)
## How it works ## Usage
The Pi streams the output of the camera module over the web via Flask. Devices connected to the same network would be able to access the camera stream via
### Creating the virtual environment
``` ```
<raspberry_pi_ip:5000> pip install venv
python -m venv venv
``` ```
## Screenshots ### Activate the virtual environment
| ![Setup](readme/pi-stream-client.jpg) | ![Live Pi Camera Stream](readme/pi-stream-screen-capture.jpg) |
| ------------------------------------- | ------------------------------------------------------------- |
| Pi Setup | Pi - Live Stream |
## Preconditions *Windows (CMD)*
* Raspberry Pi 4, 2GB is recommended for optimal performance. However you can use a Pi 3 or older, you may see a increase in latency.
* Raspberry Pi 4 Camera Module or Pi HQ Camera Module (Newer version)
* Python 3 recommended.
## Library dependencies
Install the following dependencies to create camera stream.
``` ```
sudo apt-get update venv\Scripts\activate.bat
sudo apt-get upgrade
sudo apt-get install libatlas-base-dev
sudo apt-get install libjasper-dev
sudo apt-get install libqtgui4
sudo apt-get install libqt4-test
sudo apt-get install libhdf5-dev
sudo pip3 install flask
sudo pip3 install numpy
sudo pip3 install opencv-contrib-python
sudo pip3 install imutils
sudo pip3 install opencv-python
``` ```
Note: This installation of opencv may take a while depending on your pi model. *Linux / Mac OS*
OpenCV alternate installation (in the event of failed opencv builds):
``` ```
sudo apt-get install libopencv-dev python3-opencv source venv/bin/activate
``` ```
## Step 1 Cloning Raspberry Pi Camera Stream ### Installing the dependencies
Open up terminal and clone the Camera Stream repo:
``` ```
cd /home/pi pip install -r requirements.txt
git clone https://github.com/EbenKouao/pi-camera-stream-flask.git
``` ```
## Step 2 Launch Web Stream ### Running the app (not recommended for production)
Note: Creating an Autostart of the main.py script is recommended to keep the stream running on bootup.
```bash cd modules
sudo python3 /home/pi/pi-camera-stream-flask/main.py
```
## Step 3 Autostart your Pi Stream
Optional: A good idea is to make the the camera stream auto start at bootup of your pi. You will now not need to re-run the script every time you want to create the stream. You can do this by going editing the /etc/profile to:
``` ```
sudo nano /etc/profile python main.py
``` ```
Go the end of the and add the following (from above):
```
sudo python3 /home/pi/pi-camera-stream-flask/main.py
```
This would cause the following terminal command to auto-start each time the Raspberry Pi boots up. This in effect creates a headless setup - which would be accessed via SSH.
Note: make sure SSH is enabled.
## More Projects / Next Steps
View the latest Build: [Pi Smart Cam with Motion Sensor](https://github.com/EbenKouao/pi-smart-cam)
Alternatively, view more projects that build on the Pi Camera on [smartbuilds.io](https://smartbuilds.io).

View File

@ -1,25 +1,17 @@
#Modified by smartbuilds.io import cv2
#Date: 27.09.20
#Desc: This scrtipt script..
import cv2 as cv
from imutils.video.pivideostream import PiVideoStream
import imutils
import time
from datetime import datetime from datetime import datetime
import numpy as np import numpy as np
class VideoCamera(object): class VideoCamera(object):
def __init__(self, flip=False, file_type=".jpg", photo_string="stream_photo"): def __init__(self, flip=False, file_type=".jpg", photo_string="stream_photo"):
# self.vs = PiVideoStream(resolution=(1920, 1080), framerate=30).start() self.vs = cv2.VideoCapture(0)
self.vs = PiVideoStream().start() self.flip = flip
self.flip = flip # Flip frame vertically self.file_type = file_type
self.file_type = file_type # image type i.e. .jpg self.photo_string = photo_string
self.photo_string = photo_string # Name to save the photo self.exposure_value = self.vs.get(cv2.CAP_PROP_EXPOSURE)
time.sleep(2.0)
def __del__(self): def __del__(self):
self.vs.stop() self.vs.release()
def flip_if_needed(self, frame): def flip_if_needed(self, frame):
if self.flip: if self.flip:
@ -27,14 +19,26 @@ class VideoCamera(object):
return frame return frame
def get_frame(self): def get_frame(self):
frame = self.flip_if_needed(self.vs.read()) ret, frame = self.vs.read()
ret, jpeg = cv.imencode(self.file_type, frame) if not ret:
self.previous_frame = jpeg return None
frame = self.flip_if_needed(frame)
ret, jpeg = cv2.imencode(self.file_type, frame)
return jpeg.tobytes() return jpeg.tobytes()
# Take a photo, called by camera button
def take_picture(self): def take_picture(self):
frame = self.flip_if_needed(self.vs.read()) ret, frame = self.vs.read()
ret, image = cv.imencode(self.file_type, frame) if not ret:
today_date = datetime.now().strftime("%m%d%Y-%H%M%S") # get current time return
cv.imwrite(str(self.photo_string + "_" + today_date + self.file_type), frame)
frame = self.flip_if_needed(frame)
today_date = datetime.now().strftime("%m%d%Y-%H%M%S")
cv2.imwrite(str("static/images/" + self.photo_string + "_" + today_date + self.file_type), frame)
def set_exposure(self, exposure_value):
self.vs.set(cv2.CAP_PROP_EXPOSURE, exposure_value)
self.exposure_value = exposure_value
def get_exposure(self):
return self.exposure_value

43
main.py
View File

@ -1,23 +1,26 @@
#Modified by smartbuilds.io from flask import Flask, render_template, Response
#Date: 27.09.20
#Desc: This web application serves a motion JPEG stream
# main.py
# import the necessary packages
from flask import Flask, render_template, Response, request, send_from_directory
from camera import VideoCamera from camera import VideoCamera
import os from util import list_files_in_dir, generate_url
pi_camera = VideoCamera(flip=False) # flip pi camera if upside down. camera = VideoCamera(flip=False)
# App Globals (do not edit)
app = Flask(__name__) app = Flask(__name__)
@app.route('/') @app.route('/')
def index(): def index():
return render_template('index.html') #you can customze index.html here return render_template('index.html')
@app.route('/images')
def images_view():
file_directory = 'images'
url_list = list()
for file in list_files_in_dir('static/'+file_directory):
url_list.append(generate_url(file_directory, file))
return render_template('images.html', urls=url_list)
def gen(camera): def gen(camera):
#get camera frame
while True: while True:
frame = camera.get_frame() frame = camera.get_frame()
yield (b'--frame\r\n' yield (b'--frame\r\n'
@ -25,15 +28,25 @@ def gen(camera):
@app.route('/video_feed') @app.route('/video_feed')
def video_feed(): def video_feed():
return Response(gen(pi_camera), return Response(gen(camera),
mimetype='multipart/x-mixed-replace; boundary=frame') mimetype='multipart/x-mixed-replace; boundary=frame')
# Take a photo when pressing camera button
@app.route('/picture') @app.route('/picture')
def take_picture(): def take_picture():
pi_camera.take_picture() camera.take_picture()
return "None"
@app.route('/moreexposure')
def more_exposure():
exposure = camera.get_exposure()
camera.set_exposure(exposure + 1)
return "None"
@app.route('/lessexposure')
def less_exposure():
exposure = camera.get_exposure()
camera.set_exposure(exposure - 1)
return "None" return "None"
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False) app.run(host='0.0.0.0', debug=False)

BIN
readme/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
blinker==1.6.2
click==8.1.7
colorama==0.4.6
Flask==2.3.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
numpy==1.25.2
opencv-python==4.8.0.76
Werkzeug==2.3.7

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

45
static/script.js Normal file
View File

@ -0,0 +1,45 @@
$(function() {
$('a#take-picture').on('click', function(e) {
e.preventDefault()
$.getJSON('/picture',
function(data) {
//do nothing
});
return false;
});
});
$(function() {
$('a#more-exposure').on('click', function(e) {
e.preventDefault()
$.getJSON('/moreexposure',
function(data) {
//do nothing
});
return false;
});
});
$(function() {
$('a#less-exposure').on('click', function(e) {
e.preventDefault()
$.getJSON('/lessexposure',
function(data) {
//do nothing
});
return false;
});
});
$(function() {
$('a#copy-video-stream-url').on('click', function(e) {
const textArea = document.createElement("textarea");
const video_stream_path = document.getElementById("bg").src;
textArea.value = video_stream_path;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
return false;
});
});

84
static/style.css Normal file
View File

@ -0,0 +1,84 @@
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: black;
font-family: Arial, Helvetica, sans-serif;
}
.navbar {
overflow: hidden;
position: fixed;
bottom: 0;
width: 100%;
margin: auto;
text-align: center;
background-color: black;
opacity:0.6;
}
.navbar div {
display: inline-block;
}
.navbar a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.navbar a.active {
background-color: #4CAF50;
color: white;
}
.main {
padding: 16px;
margin-bottom: 30px;
}
i.fa {
display: inline-block;
border-radius: 60px;
box-shadow: 0px 0px 2px #888;
padding: 0.5em 0.6em;
background: blue;
color: white;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
width: 35%
}
button {
background-color: Transparent;
background-repeat:no-repeat;
border: none;
cursor:pointer;
overflow: hidden;
outline:none;
}
.camera-bg {
display: block;
margin: auto;
max-height: 100vh;
max-width: 100vh;
width: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: fixed;
}

13
templates/images.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Camera Live Feed - Images</title>
</head>
<body>
{% for url in urls %}
<img src="{{ url }}" alt="Taken Image">
{% endfor %}
</body>
</html>

View File

@ -1,214 +1,62 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <meta charset="UTF-8">
<title>Camera Live Feed</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
body { <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
margin: 0;
font-family: Arial, Helvetica, sans-serif;
}
.navbar {
overflow: hidden;
position: fixed;
bottom: 0;
width: 100%;
margin: auto;
background-color: black;
opacity:0.6;
}
.navbar a {
float: left;
display: block;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
font-size: 17px;
}
.navbar a:hover {
}
.navbar a.active {
background-color: #4CAF50;
color: white;
}
.main {
padding: 16px;
margin-bottom: 30px;
}
.camera-movement{
float: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.lights-button{
float: right;
}
i.fa {
display: inline-block;
border-radius: 60px;
box-shadow: 0px 0px 2px #888;
padding: 0.5em 0.6em;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
width: 35%
}
button {
background-color: Transparent;
background-repeat:no-repeat;
border: none;
cursor:pointer;
overflow: hidden;
outline:none;
}
//CSS
.camera-bg {
position: fixed;
top: 0;
left: 0;
/* Preserve aspet ratio */
min-width: 100%;
min-height: 100%;
/* Full height */
height: 100%;
/* Center and scale the image nicely */
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
.top-right-logo {
position: absolute;
top: 3%;
left: 2%;
font-size: 38px;
color: white;
opacity: 0.5;
}
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: black;
}
</style>
</head> </head>
<title>Make - PiStream</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<body> <body>
<div class="main">
<div class="main" id="newpost"> <img class="camera-bg" id="bg" class="center" src="{{ url_for('video_feed') }}">
<img class="camera-bg" style="width: 100%; height:80%; background-attachment: fixed;" id="bg" class="center" src="{{ url_for('video_feed') }}">
<!--<img class="camera-bg" style="width: 100%; height:80%; background-attachment: fixed;" id="bg" class="center" src="https://www.psdbox.com/wp-content/uploads/2011/01/security-camera-photoshop-effect.jpg">-->
</div> </div>
<div class="top-right-logo">
<a></a>Raspberry Pi - Camera Stream</a>
</div>
<div class="navbar"> <div class="navbar">
<div>
<div class="ignoreCall"> <a href="/images" title="Gallery">
<a id=decline class="but_def"> <button>
<button id="button"> <i class="fa fa-picture-o fa-2x" aria-hidden="true"></i>
<i style="background: red; color: white;" class="fa fa-times fa-2x" aria-hidden="true"></i>
</button> </button>
</a> </a>
</div> </div>
<div class="picture"> <div>
<a href=# id=take-picture class="picture_class"> <a href="#" id="take-picture" title="Take a picture">
<button id="take-picture-button" onclick="take_picture()"> <button>
<i style="background: blue; color: white;" class="fa fa-camera fa-2x" aria-hidden="true"></i> <i class="fa fa-camera fa-2x" aria-hidden="true"></i>
</button>
</a>
</div>
<div>
<a href="#" id="more-exposure" title="More exposure">
<button>
<i class="fa fa-plus fa-2x" aria-hidden="true"></i>
</button>
</a>
</div>
<div>
<a href="#" id="less-exposure" title="Less exposure">
<button>
<i class="fa fa-minus fa-2x" aria-hidden="true"></i>
</button>
</a>
</div>
<div>
<a href="#" id="copy-video-stream-url" title="Copy the video stream url">
<button>
<i class="fa fa-clipboard fa-2x" aria-hidden="true"></i>
</button> </button>
</a> </a>
</div> </div>
</div> </div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type="text/javascript" src="{{ url_for('static', filename='script.js') }}"></script>
<script type="text/javascript">
// stop stream - called when pressing red X
var button = document.getElementById('button');
button.onclick = function() {
var div = document.getElementById('newpost');
if (div.style.display !== 'none') {
div.style.display = 'none';
}
else {
div.style.display = 'block';
}
};
// Take and save a photo, call picture function in main.py
$(function() {
$('a#take-picture').on('click', function(e) {
e.preventDefault()
$.getJSON('/picture',
function(data) {
//do nothing
});
return false;
});
});
</script>
<script type="text/javascript">
// take picture
var button = document.getElementById('take-pica-button');
button.onclick = function() {
var div = document.getElementById('newpost');
if (div.style.display !== 'none') {
div.style.display = 'none';
}
else {
div.style.display = 'block';
}
};
</script>
</body> </body>
</html> </html>

13
util.py Normal file
View File

@ -0,0 +1,13 @@
import os
from flask import url_for
def list_files_in_dir(directory_path: str) -> list[str]:
file_list = os.listdir(directory_path)
file_list = [file for file in file_list if os.path.isfile(os.path.join(directory_path, file))]
return file_list
def generate_url(directory_path: str, file_name: str) -> str:
url = url_for('static', filename=f'{directory_path}/{file_name}')
return url