Compare commits

...

10 Commits

Author SHA1 Message Date
xoy dacad4db10 [Building the struct for a webcam video stream flask app] 2023-08-23 13:17:12 +02:00
EbenKouao 1228e5e5f0 add latest pi smart cam w/ motion sensor 2022-05-09 23:19:54 +01:00
EbenKouao 870fc34065
Merge pull request #20 from flipthedog/master
Added button to take picture + raspberry pi browser icon
2022-05-07 23:04:16 +01:00
Floris 71e26f8b5d added icon for extra fun 2022-05-05 07:40:27 -04:00
Floris 7c940fba67 Added functionality to take screenshots during stream 2022-05-05 07:08:12 -04:00
Eben Kouao 9b27f5d8b4 docs(common): link to more pi projects 2021-10-29 16:15:51 +01:00
Eben Kouao 9accc34541 ci(build): add gitignore 2021-10-29 15:54:30 +01:00
Eben e2b102b208 updating readme with autostart stream at boot 2020-10-11 01:44:04 +01:00
Eben fc4c270bda updating readme with autostart stream at boot 2020-10-11 01:38:24 +01:00
Eben 2b52cf7927 updating readme with autostart stream at boot 2020-04-26 02:46:07 +01:00
14 changed files with 300 additions and 257 deletions

BIN
.DS_Store vendored

Binary file not shown.

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
__pycache__/
test.py
static/images/*
venv/

View File

@ -1,79 +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 install libatlas-base-dev venv\Scripts\activate.bat
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
``` ```
pip3 install opencv-python *Linux / Mac OS*
## Step 1 Cloning Raspberry Pi Camera Stream
Open up terminal and clone the Camera Stream repo:
``` ```
cd /home/pi source venv/bin/activate
git clone https://github.com/EbenKouao/pi-camera-stream-flask.git
``` ```
## Step 2 Launch Web Stream ### Installing the dependencies
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 running your Pi Camera stream at Pi boot up. This removes the need to re-run the script every time you want to create the stream.
You can do this by going adding the boot up code the .bashrc file.
Via the Desktop GUI - right click in your /home/pi/ directory -> show hidden -> open .bashrc and add the code.
Or alternatively access via terminal:
``` ```
sudo nano /home/pi/.bashrc pip install -r requirements.txt
``` ```
Go the end of the and add the following (from above): ### Running the app (not recommended for production)
``` ```
sudo python3 /home/pi/pi-camera-stream-flask/main.py python main.py
``` ```
This would cause the following terminal command to auto-start upon Raspberry Pi boot up.
## Download Beta image of Raspberry Pi Camera Stream
Any troubles installing, try out the already compiled Raspberry Pi (Raspbian OS) Image of [Raspberry Pi Camera Stream](https://smartbuilds.io).
![Raspbian Camera Stream Image](img/readme/[].png)

View File

@ -1,21 +1,17 @@
#Modified by smartbuilds.io
#Date: 27.09.20
#Desc: This scrtipt script..
import cv2 import cv2
from imutils.video.pivideostream import PiVideoStream from datetime import datetime
import imutils
import time
import numpy as np import numpy as np
class VideoCamera(object): class VideoCamera(object):
def __init__(self, flip = False): def __init__(self, flip=False, file_type=".jpg", photo_string="stream_photo"):
self.vs = PiVideoStream().start() self.vs = cv2.VideoCapture(0)
self.flip = flip self.flip = flip
time.sleep(2.0) self.file_type = file_type
self.photo_string = photo_string
self.exposure_value = self.vs.get(cv2.CAP_PROP_EXPOSURE)
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:
@ -23,6 +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 = cv2.imencode('.jpg', frame) if not ret:
return None
frame = self.flip_if_needed(frame)
ret, jpeg = cv2.imencode(self.file_type, frame)
return jpeg.tobytes() return jpeg.tobytes()
def take_picture(self):
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

50
main.py
View File

@ -1,25 +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
from camera import VideoCamera from camera import VideoCamera
import time from util import list_files_in_dir, generate_url
import threading
import os
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'
@ -27,12 +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')
@app.route('/picture')
def 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__': 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

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,177 +1,62 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8">
<style> <title>Camera Live Feed</title>
body { <meta name="viewport" content="width=device-width, initial-scale=1">
margin: 0; <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
font-family: Arial, Helvetica, sans-serif; <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
} </head>
.navbar { <body>
overflow: hidden; <div class="main">
position: fixed; <img class="camera-bg" id="bg" class="center" src="{{ url_for('video_feed') }}">
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>
<div class="navbar">
<div class="navbar"> <div>
<a href="/images" title="Gallery">
<div class="ignoreCall"> <button>
<a id=decline class="but_def"> <i class="fa fa-picture-o fa-2x" aria-hidden="true"></i>
<button id="button">
<i style="background: red; color: white;" class="fa fa-times fa-2x" aria-hidden="true"></i>
</button> </button>
</a> </a>
</div> </div>
</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>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <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>
<script type="text/javascript"> </div>
var button = document.getElementById('button'); <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>
button.onclick = function() { </body>
var div = document.getElementById('newpost');
if (div.style.display !== 'none') {
div.style.display = 'none';
}
else {
div.style.display = 'block';
}
};
</script>
</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