[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__/
#image_files
*.jpg
*.jpeg
*.png
test.py
static/images/*
venv/

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
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
## Usage
### Creating the virtual environment
```
<raspberry_pi_ip:5000>
pip install venv
python -m venv venv
```
## Screenshots
| ![Setup](readme/pi-stream-client.jpg) | ![Live Pi Camera Stream](readme/pi-stream-screen-capture.jpg) |
| ------------------------------------- | ------------------------------------------------------------- |
| Pi Setup | Pi - Live Stream |
### Activate the virtual environment
## Preconditions
* 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.
*Windows (CMD)*
```
sudo apt-get update
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
venv\Scripts\activate.bat
```
Note: This installation of opencv may take a while depending on your pi model.
OpenCV alternate installation (in the event of failed opencv builds):
*Linux / Mac OS*
```
sudo apt-get install libopencv-dev python3-opencv
source venv/bin/activate
```
## Step 1 Cloning Raspberry Pi Camera Stream
Open up terminal and clone the Camera Stream repo:
### Installing the dependencies
```
cd /home/pi
git clone https://github.com/EbenKouao/pi-camera-stream-flask.git
pip install -r requirements.txt
```
## Step 2 Launch Web Stream
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:
### Running the app (not recommended for production)
```
sudo nano /etc/profile
```
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).
python main.py
```

View File

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

45
main.py
View File

@ -1,23 +1,26 @@
#Modified by smartbuilds.io
#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 flask import Flask, render_template, Response
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.route('/')
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):
#get camera frame
while True:
frame = camera.get_frame()
yield (b'--frame\r\n'
@ -25,15 +28,25 @@ def gen(camera):
@app.route('/video_feed')
def video_feed():
return Response(gen(pi_camera),
return Response(gen(camera),
mimetype='multipart/x-mixed-replace; boundary=frame')
# Take a photo when pressing camera button
@app.route('/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"
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>
<html>
<head>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
}
<head>
<meta charset="UTF-8">
<title>Camera Live Feed</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
.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>
<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>
<div class="main" id="newpost">
<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 class="top-right-logo">
<a></a>Raspberry Pi - Camera Stream</a>
</div>
<div class="navbar">
<div class="ignoreCall">
<a id=decline class="but_def">
<button id="button">
<i style="background: red; color: white;" class="fa fa-times fa-2x" aria-hidden="true"></i>
</button>
</a>
<body>
<div class="main">
<img class="camera-bg" id="bg" class="center" src="{{ url_for('video_feed') }}">
</div>
<div class="picture">
<a href=# id=take-picture class="picture_class">
<button id="take-picture-button" onclick="take_picture()">
<i style="background: blue; color: white;" class="fa fa-camera fa-2x" aria-hidden="true"></i>
</button>
</a>
<div class="navbar">
<div>
<a href="/images" title="Gallery">
<button>
<i class="fa fa-picture-o fa-2x" aria-hidden="true"></i>
</button>
</a>
</div>
<div>
<a href="#" id="take-picture" title="Take a picture">
<button>
<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>
</a>
</div>
</div>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.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>
<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>
</body>
</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