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 &”

this is the console

    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.