USB Camera tool with Python and OpenCV
After completing my previous project that was a Time Lapse tool for PIcamera i’ve decided to create another tool that allows managing the functionality of a USB camera.
This tool was created to fulfill a simple demand: recording an X minutes length videos one after another (no frame loss), in the same time monitoring the destination folder and delete old videos when folder size reaches certain threshold.
I have used Logitech c930c but i suppose that it will work with every USB camera.
This is an initial release of my USB camera tool,
current version: 0.9.9
This tool was developed and tested on 2021-03-04-raspios-buster-armhf.img that was downloaded from the official site and installed on a USB stick.
Prerequisites to use the tool:
you have to install some packages, you can use the following commands:
- pip3 install pyyaml
- pip3 install opencv-python
- sudo apt-get install libatlas-base-dev
Usage:
Navigate to “start-opencv-vodeocamera-tool.py” directory and run:
“python3 start-opencv-vodeocamera-tool.py &”
Open ReadMe:
will open the readme file.
View configfile:
will open a config file in a new mousepad instance.
Preview:
will open a preview window to allow camera fixation.
Apply and save to configfile:
will save the changed configuration to configfile.
Start Camera:
is pretty self explanatory.
Stop Camera:
is pretty obvious too.
And now the backend,
this is the “start-opencv-vodeocamera-tool.py” file, it should be run from terminal and i suggest to run it with an “&”, so the process will start detached from terminal, for debugging it is obviously better to run it without the “&”.
for example: $ python3 start-opencv-vodeocamera-tool.py &
This file creates the graphical user interface and imports parameters from configuration file.
After changing values you should hit the “Apply and save to configfile”, that should be done before you hit the “Start Camera” button.
#!/usr/bin/python3
from tkinter import *
from tkinter import messagebox
import subprocess
import os
import yaml
fname = "configfile.yaml"
### variables from configfile
with open(fname, "r") as ymlfile:
configfile = yaml.safe_load(ymlfile)
current_fps = configfile['camera_parameters']['fps']
current_camera_number = configfile['camera_parameters']['camera_number']
current_interval = configfile['config']['interval']
current_destination_path = configfile['path_vars']['dest_folder']
current_percentage = configfile['development']['percentage']
current_width = configfile['camera_parameters']['width']
current_height = configfile['camera_parameters']['height']
###
top = Tk()
top.geometry("800x400")
top.resizable(width=False, height=False)
top.title("OpenCV USB Camera Tool")
### frame
labelframe = LabelFrame(top, text = "Cam Configuration", width=250, height=80)
labelframe.pack(fill = "both", expand = "yes")
###### Caption Section ######
### caption (radio buttons)
var = StringVar()
label = Label( top, textvariable=var, relief=FLAT )
var.set("Select video resolution")
label.pack()
label.place(x = 280,y = 40)
### caption (day light limit)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Video lenght in seconds")
label.pack()
label.place(x = 560,y = 40)
### caption (fps)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Frames per secons")
label.pack()
label.place(x = 560,y = 80)
### caption (camera number)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Camera number")
label.pack()
label.place(x = 560,y = 120)
### caption (Folder Size)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Max percent of drive")
label.pack()
label.place(x = 560,y = 160)
### caption (width)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Width")
label.pack()
label.place(x = 280,y = 70)
### caption (lenght)
var_day = StringVar()
label = Message( top, textvariable = var_day, relief = FLAT, width = 200 )
var_day.set("Height")
label.pack()
label.place(x = 390,y = 70)
###### Button Section ######
### button 1
def open_readme():
subprocess.call(['lxterminal', '-e', 'mousepad ./readme.txt'])
B1 = Button(top, text = "Open 'ReadMe'", command = open_readme, bg='dimgrey', fg='white', width=20)
B1.place(x = 10,y = 40)
### button 2
def view_configfile():
subprocess.call(['lxterminal', '-e', 'mousepad configfile.yaml'])
B2 = Button(top, text = "View configfile", command = view_configfile, bg='dimgrey', fg='white', width=20)
B2.place(x = 10,y = 80)
### button 3 (start camera)
def start_camera():
subprocess.call(['lxterminal', '-e', 'python3 video-save-5.py cam_configfile.yaml'])
subprocess.call(['lxterminal', '-e', 'bash limit-folder-size.sh'])
B3 = Button(top, text = "Start Camera", command = start_camera, bg='green', fg='white', width=20)
B3.place(x = 10,y = 200)
### button 4 (stop camera)
def stop_camera():
subprocess.call(['lxterminal', '-e', 'kill $(ps aux | grep "5.py" | awk "{print $2}")'])
subprocess.call(['lxterminal', '-e', 'kill $(ps aux | grep "size.sh" | awk "{print $2}")'])
B4 = Button(top, text = "Stop Camera", command = stop_camera, bg='red', fg='white', width=20)
B4.place(x = 10,y = 240)
### Button 5 apply button (save to configfile)
def apply_button():
#sel_resolution()
spbx1_day_light_limit()
spbx2_fps()
spbx3_camera()
spbx4_folder_size()
spbx5_width()
spbx6_height()
entrybox1()
with open(fname, "r") as ymlfile:
cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)
#cfg['camera_parameters']['resolution'] = sel_resolution.selected
cfg['camera_parameters']['fps'] = spbx2_fps.selectionn
cfg['camera_parameters']['camera_number'] = spbx3_camera.selectionn
cfg['config']['interval'] = spbx1_day_light_limit.selectionn
cfg['path_vars']['dest_folder'] = entrybox1.selection
cfg['development']['percentage'] = spbx4_folder_size.selectionn
cfg['camera_parameters']['width'] = spbx5_width.selectionn
cfg['camera_parameters']['height'] = spbx6_height.selectionn
with open(fname, 'w') as outfile:
yaml.dump(cfg, outfile, default_flow_style=False, sort_keys=False)
B4 = Button(top, text = "Apply and save to configfile", command = apply_button, bg='orange', fg='white', width=20)
B4.place(x = 10,y = 160)
### button 6 (preview)
def preview():
subprocess.call(['lxterminal', '-e', 'python3 preview.py'])
B4 = Button(top, text = "Preview (q to close)", command = preview, bg='dimgrey', fg='white', width=20)
B4.place(x = 10,y = 120)
root = top
###### SpinBox Section ######
### spinbox1 day light limit
def spbx1_day_light_limit():
spbx1_selection_day = int(spbx1var.get())
spbx1_day_light_limit.selectionn = spbx1_selection_day
spbx1var = IntVar()
spbx1var.set(current_interval)
spinbox_day = Spinbox( top, from_=1, to=1000, width=5, textvariable=spbx1var )
spinbox_day.place(x = 730,y = 40)
### spinbox2 fps
def spbx2_fps():
spbx2_frame_per_second = int(spbx2var.get())
spbx2_fps.selectionn = spbx2_frame_per_second
spbx2var = IntVar()
spbx2var.set(current_fps)
spinbox_fps = Spinbox( top, from_=1, to=1000, width=5, textvariable=spbx2var )
spinbox_fps.place(x = 730,y = 80)
### spinbox3 camera number
def spbx3_camera():
spbx3_camera_number = int(spbx3var.get())
spbx3_camera.selectionn = spbx3_camera_number
spbx3var = IntVar()
spbx3var.set(current_camera_number)
spinbox_camera = Spinbox( top, from_=0, to=18, width=5, textvariable=spbx3var )
spinbox_camera.place(x = 730,y = 120)
### spinbox4 camera number
def spbx4_folder_size():
spbx4_percentage = int(spbx4var.get())
spbx4_folder_size.selectionn = spbx4_percentage
spbx4var = IntVar()
spbx4var.set(current_percentage)
spinbox_percentage = Spinbox( top, from_=0, to=100, width=5, textvariable=spbx4var )
spinbox_percentage.place(x = 730,y = 160)
### spinbox5 width
def spbx5_width():
spbx5_xwidth = int(spbx5var.get())
spbx5_width.selectionn = spbx5_xwidth
spbx5var = IntVar()
spbx5var.set(current_width)
spinbox_width = Spinbox( top, from_=0, to=10000, width=5, textvariable=spbx5var )
spinbox_width.place(x = 280,y = 100)
### spinbox6 height
def spbx6_height():
spbx6_yheight = int(spbx6var.get())
spbx6_height.selectionn = spbx6_yheight
spbx6var = IntVar()
spbx6var.set(current_height)
spinbox_height = Spinbox( top, from_=0, to=10000, width=5, textvariable=spbx6var )
spinbox_height.place(x = 390,y = 100)
###### Entry Box Section ######
### entry box1 (day photo destination)
def entrybox1():
entrbx1 = str(e1_str.get())
entrybox1.selection = entrbx1
my_w = top
l1 = Label(my_w, text='Video Files Destination:', width=20) # added one Label
l1.place(x = 8, y = 310)
e1_str = StringVar()
e1_str.set(current_destination_path)
e1 = Entry(my_w, width=70,bg='yellow', textvariable=e1_str) # added one Entry box
e1.place(x = 195, y = 310)
label = Label(root)
label.pack()
### end
top.mainloop()
and this is the “video-save-5.py” file.
It is the main functionality file that holds the loop that actually does the job.
it retrieves the needed parameters from the configuration file.
NOTE: if you are running your camera on a PI with no screen or VNC connection but via SSH connection you can run this file straight forward, but you will have to adjust the parameters in the config file manually.
#!/usr/bin/python3
import numpy as np
import cv2
import time
import os
import sys
import yaml
fname = "configfile.yaml"
### variables from configfile
with open(fname, "r") as ymlfile:
configfile = yaml.safe_load(ymlfile)
current_interval = configfile['config']['interval']
current_fps = configfile['camera_parameters']['fps']
current_camera_number = configfile['camera_parameters']['camera_number']
current_width = configfile['camera_parameters']['width']
current_height = configfile['camera_parameters']['height']
current_dest_folder = configfile['path_vars']['dest_folder']
fps = current_fps
#width = 1280
#height = 720
video_codec = cv2.VideoWriter_fourcc("D", "I", "V", "X")
dest_file = current_dest_folder
interval = current_interval
today = time.strftime("%a-%d.%m.%Y-%H-%M-%S")
cap = cv2.VideoCapture(current_camera_number)
ret = cap.set(3, current_width)
ret = cap.set(4, current_height)
start = time.time()
video_file_count = 1
video_file = os.path.join(dest_file, today + ".avi")
#print("Capture video saved location : {}".format(video_file))
# Create a video write before entering the loop
video_writer = cv2.VideoWriter(
video_file, video_codec, fps, (int(cap.get(3)), int(cap.get(4)))
)
while cap.isOpened():
today = time.strftime("%a-%d.%m.%Y-%H-%M-%S")
start_time = time.time()
ret, frame = cap.read()
if ret == True:
# cv2.imshow("frame", frame)
if time.time() - start > interval:
start = time.time()
video_file_count += 1
video_file = os.path.join(dest_file, today + ".avi")
video_writer = cv2.VideoWriter(
video_file, video_codec, fps, (int(cap.get(3)), int(cap.get(4)))
)
# No sleeping! We don't want to sleep, we want to write
# time.sleep(10)
# Write the frame to the current video writer
video_writer.write(frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
else:
break
cap.release()
cv2.destroyAllWindows()
Next it is the function that allows you to parse YAML from ‘.sh’ file.
“parse-yaml.sh” this file will be called by “limit-folder-size.sh” to parse data from configfile.
#inspired by https://gist.github.com/pkuczynski/8665367
#!/bin/sh
parse_yaml() {
local prefix=$2
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 |
awk -F$fs '{
indent = length($1)/2;
vname[indent] = $2;
for (i in vname) {if (i > indent) {delete vname[i]}}
if (length($3) > 0) {
vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
}
}'
}
This is “preview.py”, an angle adjustment script, it addresses the config file to retrieve the resolution and camera number.
import numpy as np
import cv2 as cv
import yaml
fname = "configfile.yaml"
### variables from configfile
with open(fname, "r") as ymlfile:
configfile = yaml.safe_load(ymlfile)
current_camera_number = configfile['camera_parameters']['camera_number']
current_width = configfile['camera_parameters']['width']
current_height = configfile['camera_parameters']['height']
cap = cv.VideoCapture(current_camera_number)
if not cap.isOpened():
print("Cannot open camera")
exit()
while True:
# Capture frame-by-frame
ret, frame = cap.read()
cap.set(3, current_width)
cap.set(4, current_height)
# if frame is read correctly ret is True
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
# Our operations on the frame come here
gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
# Display the resulting frame
cv.imshow('frame', frame)
if cv.waitKey(1) == ord('q'):
break
# When everything done, release the capture
cap.release()
cv.destroyAllWindows()
Lastly we get to “configfile.yaml”, which is our configuration file, it is a YAML file, and if you choose to mess with it manually note that YAML is a very petty file to work with… :>>
path_vars:
dest_folder: /media/pi/samsung_ssd/videos/
config:
interval: 300
camera_parameters:
fps: 30
width: 1280
height: 720
camera_number: 1
development:
percentage: 20
folder: /media/pi/samsung_ssd/videos/
And of course we want to watch the destination folder to not explode, for that matter we will use this wonderful script that does a splendid job.
Create another file “limit-folder-size.sh” with the following code:
#!/bin/bash
### this section is for YAML parsing, inspired by https://gist.github.com/pkuczynski/8665367
# include parse_yaml function
. parse-yaml.sh
# read yaml file
eval $(parse_yaml configfile.yaml "config_")
# access yaml content
echo $config_development_percentage
echo $config_development_dest_folder
###
while [ True ]
do
#Usage = sh limit_folder_size_size.sh
#source=("/home/pi/Desktop/dash_cam_1.8.2/dc.config" = the directory to be limited / "$folder_to_limit"=the percentage of the total partition this directory is allowed to use / "1"=the number of files to be deleted every time the script loops (while $Directory_Percentage > $Max_Directory_Percentage)
#Directory to limit
Watched_Directory=$config_development_dest_folder
echo "Directory to limit="$Watched_Directory
#Percentage of partition this directory is allowed to use
Max_Directory_Percentage=$config_development_percentage
echo "Percentage of partition this directory is allowed to use="$Max_Directory_Percentage
#Current size of this directory
Directory_Size=$( du -sk "$Watched_Directory" | cut -f1 )
echo "Current size of this directory="$Directory_Size
#Total space of the partition = Used+Available
Disk_Size=$(( $(df $Watched_Directory | tail -n 1 | awk '{print $3}')+$(df $Watched_Directory | tail -n 1 | awk '{print $4}') ))
echo "Total space of the partition="$Disk_Size
#Curent percentage used by the directory
Directory_Percentage=$(echo "scale=2;100*$Directory_Size/$Disk_Size+0.5" | bc | awk '{printf("%d\n",$1 + 0.5)}')
echo "Curent percentage used by the directory="$Directory_Percentage
#number of files to be deleted every time the script loops (can be set to "1" if you want to be very accurate but the script is slower)
Number_Files_Deleted_Each_Loop=1
echo "number of files to be deleted every time the script loops="$Number_Files_Deleted_Each_Loop
#While the current percentage is higher than allowed percentage, we delete the oldest files
while [ $Directory_Percentage -gt $Max_Directory_Percentage ] ; do
#we delete the files
find $Watched_Directory -type f -printf "%T@ %p\n" | sort -nr | tail -$Number_Files_Deleted_Each_Loop | cut -d' ' -f 2- | xargs rm
##we delete the empty directories
#find $Watched_Directory -type d -empty -delete
#we re-calculate $Directory_Percentage
Directory_Size=$( du -sk "$Watched_Directory" | cut -f1 )
Directory_Percentage=$(echo "scale=2;100*$Directory_Size/$Disk_Size+0.5" | bc | awk '{printf("%d\n",$1 + 0.5)}')
done
sleep 300
done
This script runs every 5 minutes and queries the OS for folder size (of the destination folder), if folder size exceeds threshold then it deletes the OLDEST file.
Threshold defined in percentage.
If you have any issues with the tool or you have some suggestions and enlightenment please don’t be shy and contact me.