Webcam
Setup
Für die Webcam kommt die Raspberry Pi Camera v2.1 in Verbindung mit einem Raspberry Pi Model B zum Einsatz. Es besitzt eine Auflösung von maximal 8 MP, bildet im 4:3-Verhätlnis ab und hat ein Vollformat-Brennweitenäquivalent von ca. 29 mm. Im manuellen Belichtungsmodus können maximal 10.0 s Belichtungszeit und ISO 800 gewählt werden. Untergebracht ist die Kamera in einem Außengehäuse einer günstigen Überwachsungskameraatrappe, wie sie bei Ebay, Amazon etc. erhältlich sind. Zwar hält diese nicht 100% dicht, da aber die Raspberry-Platine auf einer erhöhten Montageplatte befestigt ist, kann im Gehäuseboden angesammeltes Wasser nicht gefährlich werden. Über einen WLAN-Stick ist der Pi mit dem Internet und Heimnetzwerk verbunden.
Gesteuert wird die Webcam über ein überschaubares Python-Script, welches die Kamera mit dem picamera-Paket ansteuert und bis zu 6 Bilder/Minute aufnimmt. Da die Kamera aber nur über den manuellen Modus ihre maximale Belichtungszeit von 10 s erreicht, habe ich in das Script einen Belichtungsalgorithmus eingebaut, der weiter unten aufgeführt ist. Es ermittelt die Helligkeitswerte des zuletzt aufgenommenen Bildes und korrigiert den ISO-Wert und die Belichtungszeit für die nächste Aufnahme. Die unten gezeigte Version ist eine abgespeckte Variante, die ich für ein anderes Webcam-Projekt mit Bildarchiv und einer vorlaufenden Initialisierungsschleife implementiert habe, weshalb die ein oder anderen merkwürdig erscheinenden Inkonsistenten oder obsolete Zeilen auftauchen. Wer zum Script weitere Fragen oder Anregungen hat, kann sich mit mir gern über das Kontaktformular in Verbindung setzen.
Der Code sieht wie folgt aus:
#!/usr/bin/python
#
#Python-Script for capturing images repeatedly with the RPi-Camera
#2017-05-02
#Alexander Buetow
#+++Description+++
#The script controls the Raspberry Pi Cam in manual mode and takes a picture
#every 10s (configurable).
#
#Exposure is controlled by a costum logic, which sets the shutter speed and ISO
#based on the brightness information of the last image.
#
#To avoid image grain, ISO should be as low as possible (lower limit 100,
#configurable). If the sun sets and the shutter speed has reached its upper
#limit of 8s (configurable), the ISO gets adjusted accordingly. If it gets
#brighter, ISO gets lowered until the lower limit is reached. If the brightness
#still rises, shutter speed decreases.
#+++Libraries+++
from time import sleep,gmtime,strftime,time #some time functions
import os #to do system commands
from picamera import PiCamera #controls the Pi Cam
from PIL import Image, ImageStat #image processing library
from PIL.ExifTags import TAGS #zum auslesen der Exif-Daten
from fractions import Fraction
#+++Configuration+++
from conf import * #loads in the configuration ini
#+++Functions+++
def get_im_info(image):
"""
Puts out a tuple with the following (Exif) information data:
brt - perceived mean brightness [0,255]
"""
r,g,b = ImageStat.Stat(image).mean #get mean values for rgb
brt = (0.2126*r)+(0.7152*g)+(0.0722*b) #brightness calculation
return(brt)
def calc_corr(brt_lim,brt,exp,exp_min,exp_max,
iso,iso_min,iso_max,t):
"""
Calculation and setting of new exposure correction values for new image
input:
brt_lim - brightness limit value, tuple of 2
brt - actual brightness of an image [0,255]
exp - actual exposure time/shutter speed
exp_min - exposure/shutter speed minimum
exp_max - exposure/shutter speed maximum
iso - actual ISO
iso_min - ISO minimum
iso_max - ISO maximum
t - trim setting, as set in the configuration
"""
#To avoid image grain, the ISO has to be as low as possible (lower limit).
#If the sun is gone and the shutter speed has reached its upper limit, the
#ISO gets adjusted acordingly. If it gets brighter, ISO gets lowered until
#the lower limit is reached. If the brightness still rises, the shutter
#speed decreases.
#First: generate exposure correction multiplier, if light changes quickly
#and exposure time has to be set with a bigger step.
m=int(abs(brt_lim[1]-brt)*t)
if brt < brt_lim[0]:
if exp == exp_max and iso < iso_max:
iso=iso+(iso/100)*m
if iso > iso_max:
iso=iso_max
camera.iso = iso
elif exp < exp_max:
exp=exp+(exp/100)*m
if exp > exp_max:
exp=exp_max
camera.shutter_speed = exp
elif brt > brt_lim[1]:
if exp == exp_max and iso > iso_min:
iso=iso-(iso/100)*m
if iso < iso_min:
iso=iso_min
camera.iso = iso
elif exp > exp_min:
exp=exp-(exp/100)*m
if exp < exp_min:
exp=exp_min
camera.shutter_speed = exp
return(exp,iso,m) #returns a tuple with three values
def open_log(path):
"""
Creates daily logfiles and opens them
input:
path - where the logfile should be stored
"""
#create hourly log string
log_tar = path + 'capture_' + strftime('%Y%m%d', gmtime()) + '.log'
log_file = open(log_tar, 'a')
return(log_file)
#+++Programe+++
#initialisation:
if wb == '': wb = 'auto'
#try to mount RAM workspace for temporarly saving images while initialisation
#RAM is used to reduce wear on storage devices and to reduce latency
ramdir='/media/ramdisk/'
try:
os.system('sudo mount -t tmpfs none ' + ramdir)
except:
print('WARNING: Couldnt mount ramdisk. Writing on disk.')
ramdir=maindir
#enable logging if turned on
if debug:
f = open_log(ramdir)
f.write(strftime('%Y-%m-%d %H:%M:%S : ', gmtime()) +
'+++CAPTURING STARTED (debugging enabled)+++\n')
f.write('Settings: ISOmin: ' + str(iso_min) + ' | ISOmax: ' + str(iso_max) +
'\n')
f.write('Shuttermin: ' + str(exp_min) + ' | Shuttermax: ' + str(exp_max) +
'\n')
f.write('White Balance: ' + wb + ' | brightness range: ' + str(bright) +
' | multip. trim: ' + str(trim) + '\n')
f.write('Saving directory: ' + ramdir + '\n')
f.close()
multi = 'NA'
init_im = ramdir + 'init.jpg' #file string for init image
cp_im = ramdir + 'init_cp.jpg'
camera = PiCamera(resolution=full_res, framerate=Fraction(1,10)) #start camera module
sleep(10) #take some time for initialisation of the camera interface
camera.awb_mode = wb #set white balance
camera.iso = iso_min #set init iso
camera.shutter_speed = exp #set init exposure time
camera.exposure_mode = 'off'
camera.vflip = True #rotate image
camera.hflip = True
camera.drc_strength = drc #set drc mode
im_exp = exp
im_iso = iso_min
#Initialisation complete, start taking and storing images as configured!
while True: #taking pictures forever
sec = int(strftime('%S', gmtime())) #unix epoch
unix = int(time())
if sec%intv == 0: #if modulo = 0, do something
#take raw image
try:
camera.capture(init_im, quality=qual)
except:
sleep(1)
#split the process: scale the image in a seperate process, save it on
#the attached storage device and go on
if sec%60 == 0 or im_exp >= 2000000: #upload every 60s
os.system('cp ' + init_im + ' ' + cp_im)
os.system('./convert_and_upload.sh ' + str(unix) + ' ' + str(im_iso) + ' ' + str(round(float(im_exp)/1000000,3)) + ' &')
#get brightness info
im_brt=get_im_info(Image.open(init_im))
if debug:
f = open_log(ramdir)
f.write(strftime('%Y-%m-%d %H:%M:%S : ') +
'Image saved | Brightness: ' + str(int(im_brt)) + ' | ISO: ' +
str(im_iso) + ' | Shutter: ' + str(im_exp) + ' | Multiplier: ' +
str(multi) + '\n')
if sec%60 == 0 or im_exp == exp_max:
f.write(strftime('%Y-%m-%d %H:%M:%S : ') + 'Upload and convert image \n')
f.close()
#Calculating and setting corrected values for next round
im_exp,im_iso,multi=calc_corr(brt_lim=bright,brt=im_brt,exp=im_exp,
exp_min=exp_min,exp_max=exp_max,
iso=im_iso,iso_min=iso_min,iso_max=iso_max,t=trim)
else:
sleep(1)
Die Bilder werden über ein kleines Shell-Script für die Website verarbeitet. Dabei wird mit Hilfe des convert-Befehls eine Webversion und ein 200px Thumbnail jeweils mit Zeitstempeln sowie Infos zur aktuellen Belichtung erstellt. Beim anschließenden Upload werden die temporären Dateien zunächst hochgeladen und danach umbenannt. Das verhindert, dass manchmal unvollständig hochgeladene Bilder angezeigt werden. Zudem wird alle 15 min ein Bild im Archiv abgelegt und Bilder, welche älter als 48 h sind, gelöscht.
So sieht das alles aus:
#!/bin/bash
#Upload Script for brieselangwetter.de
#Alexander Buetow
#2017-05-02
UNIX=$1
IM_ISO=$2
IM_EXP=$3
SV_INT=900 #image saving interval in s for history, 15 min in this case
LOGIN='foo@bar.com'
TAR='/baz/cam'
if [ $IM_EXP == '0.0' ]
then
IM_EXP='<0.001'
fi
TIMES=$(date -d @$UNIX '+%d.%m.%Y %H:%M Uhr')
T_STR=$(date -d @$UNIX '+%Y%m%d_%H%M')
MNT="/media/ramdisk"
convert $MNT/init_cp.jpg \
-resize 1200 \
-quality 90% \
-fill '#0008' -draw 'rectangle 0,870,1200,900' \
-gravity SouthWest -pointsize 24 -fill grey -annotate 0 "\ Wetterstation Brieselang\ " \
-gravity South -annotate 0 "\ Belichtungszeit: $IM_EXP s ISO: $IM_ISO\ " \
-gravity SouthEast -annotate 0 "\ $TIMES\ " \
$MNT/temp_full.jpg
convert $MNT/temp_full.jpg \
-resize 200 \
-quality 70% \
-gravity SouthEast -pointsize 14 -fill white -annotate 0 "\ $TIMES\ " \
$MNT/temp_thumb.jpg
scp $MNT/temp_{full,thumb}.jpg $LOGIN:$TAR
ssh $LOGIN "bash -c 'cd $TAR; mv temp_full.jpg cam_full.jpg; mv temp_thumb.jpg cam_thumb.jpg;
find ./hist -mtime +2 -exec rm {} \;'"
if [ $(python -c 'print(int(round('$UNIX'/60)%('$SV_INT'/60)))') == 0 ]
then
ssh $LOGIN "bash -c 'cd $TAR
cp cam_full.jpg ./hist/${T_STR}_full.jpg
cp cam_thumb.jpg ./hist/thumb/${T_STR}_thumb.jpg'"
fi
Zum Schluss die Konfiguration:
#Configuration File for Raspberry Pi Webcam Initialisation - manual mode
#2017-05-02
#Alexander Buetow
#maindir = '/media/hdd/metcam/prod/'
#maindir = '/media/ssd/metcam/prod/'
maindir = '/media/ramdisk/'
#capturing/saving-interval, in s.
intv = 10
#resolution of the raw image (width, height). Don't change, unless you change
#the camera module with an other image sensor.
#IMPORTANT: If you change the original aspect ratio of the image, the image
#will have color artifacts. This bug lies within the picamera module and cannot
#been fixed.
#full_res = (3280,2464) #Raspberry Pi Camera v2.1
full_res = (1200,900)
#image quality
qual = 90
#initialisation options
iso = 100 #init ISO
iso_min = 100 #min ISO
iso_max = 800 #max ISO
iso_step = 20 #stepping of ISO value for exposure adjusting
exp = 100 #init exposure time, in us
exp_min = 100 #min exposure time, in us
exp_max = 10000000 #max exposure time, in us
#white balance
#the following white-balance modes are selectable:
#
#'off' - Turn off white balance calculation
#'auto' - Automatic mode (default)
#'sunlight' - Sunny mode
#'cloudy' - Cloudy mode
#'shade' - Shade mode
#'tungsten' - Tungsten lighting mode
#'fluorescent' - Fluorescent lighting mode
#'incandescent' - Incandescent lighting mode
#'flash' - Flash mode
#'horizon' - Horizon mode
wb = 'auto' #white-balance mode
#drc strengh
#options:
#'off' - off
#'low' - low
#'medium' - medium
#'high' - high
drc = 'off'
#target brightness level range (min, max), values between 0 and 255.
#If the range is narrow, the exposure algorithm will change
#the exposure settings more aggressive when light conditions change (low hysteresis).
bright = (150,150)
#trim-setting for the image multiplier, values greater-equal than 0. Low settings make the image
#exposure correction for the following image less aggressive. Settings higher then 1.5 or so is
#not recommended and can result in "overshooting" the exposure setting for the following image.
trim = 1
#debug mode (on = True, off = False) - Debugging log is saved in the specified
#maindir/image saving directory.
debug = False