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
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 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
```
pip3 install opencv-python
## Step 1 Cloning Raspberry Pi Camera Stream
Open up terminal and clone the Camera Stream repo:
*Linux / Mac OS*
```
cd /home/pi
git clone https://github.com/EbenKouao/pi-camera-stream-flask.git
source venv/bin/activate
```
## 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 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:
### Installing the dependencies
```
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
from imutils.video.pivideostream import PiVideoStream
import imutils
import time
from datetime import datetime
import numpy as np
class VideoCamera(object):
def __init__(self, flip = False):
self.vs = PiVideoStream().start()
def __init__(self, flip=False, file_type=".jpg", photo_string="stream_photo"):
self.vs = cv2.VideoCapture(0)
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):
self.vs.stop()
self.vs.release()
def flip_if_needed(self, frame):
if self.flip:
@ -23,6 +19,26 @@ class VideoCamera(object):
return frame
def get_frame(self):
frame = self.flip_if_needed(self.vs.read())
ret, jpeg = cv2.imencode('.jpg', frame)
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()
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
#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 flask import Flask, render_template, Response
from camera import VideoCamera
import time
import threading
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'
@ -27,12 +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')
@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__':
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>
<html>
<head>
<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>
<body>
<div class="main">
<img class="camera-bg" id="bg" class="center" src="{{ url_for('video_feed') }}">
</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>
<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>
<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');
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