Pascal Programmeercursus 5

Noud van Kruysbergen
0

Het doel van deze minicursus is dat je de basisbeginselen leert van het programmeren in Pascal met de ontwikkelomgeving Lazarus en dat je een aantal eenvoudige programma’s hebt kunnen maken met behulp van componenten.

Deel 1. De basis

Deel 2. Debuggen

Deel 3. Classes

Deel 4. E-mailen

Deel 5. Multimedia

– webcam als automatische bewegingsdetector

Deel 6. Databases

Deze cursus is geschreven in samenwerking met de Stichting Ondersteuning Programmeertaal Pascal. De eerste vier delen van de Pascal Programmeercursus zijn verschenen in de reguliere uitgaven van c’t. Onderaan deze pagina staan links naar de PDF-versies van deze artikelen. Hier gaan we verder met deel 5.

Michaël Van Canneyt, Noud van Kruysbergen

Inbraakdetectie met Lazarus

Beveilig je huis met een webcam en wat programmeerwerk

Een pc of notebook kan met een klein Lazarus-programma eenvoudig worden omgetoverd tot een inbraakalarm. Met de Windows-API kun je foto’s maken van een kamer en dan met een eenvoudig algoritme kijken of iets verandert. Als dat het geval is, krijg je een e-mail.

De meeste – zo niet alle – laptops en tablets hebben tegenwoordig een webcam. Een moderne webcam heeft een ingebouwde bewegingssensor, maar oudere webcams kunnen ook eenvoudig gebruikt worden als bewegingssensor met behulp van wat huis-tuin-en-keuken algoritmes. In dit artikel laten we zien hoe je dat met Lazarus kunt doen.

Lazarus heeft geen kant-en-klare componenten die kunnen dienen om een webcam mee aan te spreken. Op de website Lazarus Code and Component Repository (zie softlink) staat echter wel een voorbeeldprogramma geschreven door Bogdan Razvan Adrian dat werkt met de Windows-API voor video. Dat programma gaan we voor dit artikel aanpassen om als bewegingssensor te dienen en om een mail te versturen als er beweging wordt waargenomen, met in bijlage een foto.

Windows heeft 2 API’s voor het werken met video. Een oude, legacy API (Video for Windows) en de nieuwe DirectShow die onderdeel is van DirectX. De nieuwe API is veel krachtiger, maar ligt dichter bij de hardware en vergt het gebruik van callback-routines en interfaces en is dan ook moeilijker in gebruik dan de oudere Video for Windows.  Om het eenvoudig te houden, gebruiken we hier dan ook de oude API – die overigens nog steeds werkt onder Windows 7.

Het versturen van een mailtje met Lazarus hebben we in het vorige artikel in deze serie uitvoerig belicht, daar zullen we dan ook niet dieper op ingaan.

Video for Windows

De interface van Video for Windows werkt heel eenvoudig. Er wordt een Window-handle aangemaakt en daarin wordt dan de videostream van de webcam getoond. De webcam (of andere videobron) wordt bestuurd door het sturen van Windows-messages naar de handle van de webcam. Er is een aardig aantal messages dat naar de webcam-handle gestuurd kan worden, maar we zullen er hier maar een paar van gebruiken.

De API van Video for Windows is vertaald naar Object Pascal. De macro’s die in de C/C++-interface beschikbaar zijn, zijn ook vertaald. Deze verbergen het gebruik van messages en leveren een procedurele API op. Dat is allemaal beschikbaar in de VFW-unit die meegeleverd wordt met de broncode van dit artikel. Alle functies beginnen met de prefix cap – van het engelse capture.

De belangrijkste functies (of messages) die we nodig hebben, zijn de volgende:

  • capCreateCaptureWindow: maakt een window-handle aan waarmee het beeld van de camera getoond kan worden. Deze handle is nodig voor alle andere operaties.
  • capDriverConnect: verbindt de window-handle met een camera. Er kunnen tot 10 camera’s verbonden worden.
  • capDriverDisconnect: verbreekt de verbinding met de camera.
  • capDriverGetCaps: haalt de eigenschappen op van de camera.
  • capOverlay: start of stopt het tonen van het camerabeeld in het aangemaakte venster. Het weergeven gebeurt door middel van video-overlay.
  • capPreview: start of stopt het tonen van het camerabeeld in het aangemaakte venster. Het weergeven gebeurt door middel van softwarerendering.
  • capPreviewRate: stelt de framerate van de camera in.
  • capPreviewScale: zet het schalen van het beeld aan of uit.
  • capGrabFrameNoStop: zet het huidige frame van de camera in een buffer, maar stopt de camera niet.
  • capFileSaveDIB: bewaart het frame in de buffer in een BMP-bestand.
  • capFileSetCaptureFile: stelt een bestandsnaam in voor het opnemen van een filmpje (.avi).
  • capCaptureSequence: begint een filmpje op te nemen. De bestandsnaam moet ingesteld zijn via capFileSetCaptureFile.
  • capFileSaveAs: bewaart het bestand met het opgenomen filmpje.
  • capCaptureStop: stopt met het opnemen van een filmpje.

Er zijn nog meer functies, maar de bovenstaande zijn voldoende om een kleine applicatie mee te maken. De functie van elk van deze functies is duidelijk en behoeft geen verdere uitleg. Met uitzondering van de functie capCreateCaptureWindow moet aan elke functie de window-handle van het capture-window worden meegegeven.

Een voorbeeldprogramma

De applicatie de we gaan maken is heel eenvoudig: een scherm met daarin een panel dat de output van de webcam laat zien. Er zijn wat knoppen om de camera te starten en stoppen en wat standaard Windows-dialogen die de eigenschappen van de camera instellen. Er is een knop om de bewegingsdetectie te starten en stoppen en een statusbalk die gebruikt wordt om wat statusberichten te tonen.

Bij het opstarten van het main-formulier wordt de video-handle van Windows gemaakt met de functie capCreateCaptureWindow.  Deze functie krijgt de handle van een parent-window mee (in dit geval de handle van het pCapture-panel):

procedure TMainForm.CapCreate;

begin

// Destroy if necessary

CapDestroy;

with pCapture do

FCapHandle := capCreateCaptureWindow('Video Window',WS_CHILDWINDOW or WS_VISIBLE or WS_CLIPCHILDREN or WS_CLIPSIBLINGS, 5, 5, Width-10, Height-10, Handle, 0);

if Not CapCreated then

stCapture.Caption := 'ERROR creating capture window !!!';

end;

De capture-window-handle wordt als child-window met een rand van 5 pixels binnen het pCapture-panel gemaakt. De CapCreated-functie is een functie van de class TMainForm en controleert of FCapHandle verschilt van nul. Als de handle 0 is, is het aanmaken van het videocapture-venster mislukt.

Nadat het capture-venster is gemaakt, kan er contact met de webcam-driver worden gemaakt. Dat gebeurt met de functie CapConnect binnen de form die de functie capDriverConnect oproept met de capture-window-handle als argument:

procedure TMainForm.CapConnect;

Var

l : integer;

m : String;

begin

if Not CapCreated then Exit; // Disconnect if necessary

CapDisconnect; // Connect the Capture Driver

FConnected := capDriverConnect(FCapHandle, 0);

if Not FConnected then

M := 'ERROR connecting capture driver.'

else

begin

L := SizeOf(TCapDriverCaps);

capDriverGetCaps(FCapHandle,@FDriverCaps,l);

if FDriverCaps.fHasOverlay then

M := 'Driver connected, accepts overlay'

else

M := 'Driver connected, software rendering';

end

stCapture.Caption := M;

end;

Als de driver zonder problemen aan het capture-vernster gekoppeld is, wordt met capDriverGetCaps de driver-eigenschappen opgehaald. Het FDriverCaps-record van type TCapDriverCaps wordt gevuld met de eigenschappen van de webcam. Specifiek wordt nagekeken of de camera in staat is het beeld rechtstreeks in het geheugen van de grafische kaart te schrijven of niet  (fHasOverlay). Als dat mogelijk is, wordt deze eigenschap gebruikt, dat werkt aanzienlijk sneller.

Nadat er verbinding met de webcam is gemaakt, kan het eigenlijke weergeven van het camerabeeld beginnen. Dat gebeurt met de functies capPreview of capOverlay:

procedure TMainForm.CapEnableViewer;

Var

M : String;

begin

FLiveVideo := False;

if Not FConnected then Exit;

capPreviewScale(FCapHandle, True); // Allow stretching

if FDriverCaps.fHasOverlay then // Driver accepts overlay

begin

capPreviewRate(FCapHandle, 0); // Overlay framerate is auto

FLiveVideo := capOverlay(FCapHandle,True);

M := 'Hardware';

end

else // Driver doesn't accept overlay

begin

capPreviewRate(FCapHandle, 33); // Preview framerate in ms/frame

FLiveVideo := capPreview(FCapHandle, True);

M := 'Software';

end;

if FLiveVideo then

M := Format('Video Capture - Preview (%s)',[M])

else

M := 'ERROR configuring capture driver.';

stCapture.Caption := M

end;

Merk op dat aan capOverlay of capPreview de waarde True wordt meegegeven. Na het oproepen van deze functies (wat gebeurt bij OnCreate in het hoofdvenster) is de camera actief en wordt het beeld in het hoofdvenster weergegeven. De bReconnect-knop roept de 2 functies ook op en kan gebruikt worden om de camera alsnog te activeren als er iets is misgelopen.  Om het weergeven van het beeld van de camera te stoppen, worden opnieuw de functies capOverlay en capPreview gebruikt,  maar in plaats van True wordt nu als argument False meegegeven. De functie CapDisableViewer roept de correcte functie op:

procedure TMainForm.CapDisableViewer;

begin

if FLiveVideo then

begin

if FDriverCaps.fHasOverlay then

capOverlay(FCapHandle,False)

else

capPreview(FCapHandle,False);

FLiveVideo := False;

end;

end;

Om een filmpje op te nemen, volstaat het de functies capFileSetCaptureFile, capCaptureSequence en capFileSaveAs op te roepen. Tijdens het opnemen kun je het weergeven van het camerabeeld op scherm het beste stoppen. Dat doe je met de hierboven getoonde functie capDisableViewer. Als bestandsnaam wordt een naam met de starttijd van de opname gebruikt:

procedure TMainForm.CapRecord;

Const

FN = '"Clip-"yyyy-mm-ss-hh-nn-ss".avi"';

begin

// Stop if not yet stopped.

CapStop;

CapDisableViewer;

// Construct filename

FFileName := ExtractFilePath(Application.ExeName);

FFileName := FFileName+FormatDateTime(FN,Now);

stCapture.Caption := 'Recording '+FFileName;

bRecord.Caption := 'S&top';

// Set filename

capFileSetCaptureFile(FCapHandle,PChar(FFileName));

// Start recording

capCaptureSequence(FCapHandle);

// Save file.

capFileSaveAs(FCapHandle, PChar(FFileName));

FRecording := True;

end;

Het stoppen van een video-opname gebeurt met de functie capCaptureStop. Zodra de opname gestopt is, wordt aan de  bestandsnaam van het filmpje het tijdstip van het stoppen toegevoegd en wordt het beeld van de camera weer op het scherm getoond:

procedure TMainForm.CapStop;

Const

FN = '"---"yyyy-mm-ss-hh-nn-ss".avi"';

Var

RFN : String:

begin

if Not FRecording then Exit;

FRecording := False;

// Stop recording

capCaptureStop(FCapHandle);

// Rename file with timestamp

RFN := ChangeFileExt(FFileName, FormatDateTime(FN,Now));

RenameFile(FFileName, RFN);

// Show preview again on screen

CapEnableViewer;

stCapture.Caption := 'Recording stopped';

bRecord.Caption := '&Record';

end;

Bewegingsdetectie

Met dit alles kan de camera al gebruikt worden om filmpjes te maken en op de schijf op te slaan. Maar wat als de camera als bewegingssensor gebruikt moet worden? De camera-API van Video for Windows kan ook het huidige videoframe als een beeldje opslaan. Door dit op gezette tijdstippen te doen en te kijken of er tussen de opeenvolgende beeldjes een betekenisvol verschil zit, kan beweging voor de camera worden gedetecteerd en er bijvoorbeeld een mailtje met het beeld van de camera verstuurd worden.

Om te vermijden dat er teveel e-mails verstuurd worden, wordt er bij een geconstateerd beweging slechts om de minuut  een beeldje verstuurd. Om dat te doen is er dus een timer nodig (TMotion). De timer is disabled en kan met een druk op een knop  worden aangezet. In het timer-event wordt het volgende gedaan:

procedure TMainForm.TMotionTimer(Sender: TObject);

begin

Inc(FTicks);

SaveTempFrame;

if CheckDifferent then

begin

If MinutesBetween(Now,FLastSend)>1 then

begin

FLastSend := Now;

SendPicture;

end;

end;

end;

FTicks is een tellertje. De functie SaveTempFrame schrijft het huidige cameraframe naar de harde schijf. De functie CheckDifferent kijkt of er een vorig beeldje was en geeft True terug als er een significant verschil is tussen het vorige en het huidige beeldje. Als dat het geval is, wordt er gekeken of er genoeg tijd verstreken is en zo ja, dan wordt het beeldje verstuurd.

De interessante functies zijn SaveTempFrame en CheckDifferent. De eerste is heel eenvoudig:

Procedure TMainForm.SaveTempFrame;

begin

capGrabFrameNoStop(FCapHandle);

capFileSaveDIB(FCapHandle,PChar(FFrameFile));

end;

FFrameFile is de bestandsnaam, die wordt berekend als het programma start. De functie CheckDifferent is de moeilijkste functie in het programma. Het beeldje dat in SaveTempFrame bewaard werd moet geladen worden om te vergelijken met het vorige beeldje. Dit gebeurt door de pixels van het beeldje om te zetten in grijswaarden en dan pixel voor pixel te vergelijken met het vorige beeldje. De grijswaarde wordt berekend door het gemiddelde te nemen van de R-, G- en B-waarden van de kleur.

Het verschil tussen twee opeenvolgende beeldjes kan op twee manieren worden uitgedrukt: je kunt het aantal pixels tellen dat verschilt of het verschil in grijswaarden tussen de pixels tellen. Gewoon het aantal verschillende pixels tellen, levert slechte resultaten. De kleuren in de beeldjes die de camera maakt fluctueren. Als de grijswaarden van twee opeenvolgende stilstaande beeldjes pixel voor pixel vergeleken worden, levert dat altijd een bijna 100% verschil op omdat geen twee  pixels op dezelfde locatie hetzelfde blijven. Het blijkt dat de grijswaarden van de pixels van een stilstaand beeld ongeveer 5 % fluctueren. Daar houden we rekening mee door twee opeenvolgende pixels pas als verschillend te definiëren als de grijswaarde meer dan 5% verschilt.

Als het aantal verschillende pixels tussen twee opeenvolgende beeldjes geteld is, moet besloten worden of het een betekenisvol verschil is. Een beetje experimenteren leert dat als er voor de camera bewogen wordt, dat ongeveer 10% verschillende pixels oplevert. Het algoritme krijgt dan twee parameters mee: ten eerst de fluctuatie die mag optreden tussen twee kleurwaarden voor een enkel pixel en ten tweede het relatieve aantal pixels dat mag verschillen tussen twee beeldjes. In het programma zijn er twee zogeheten spinedits waarmee je die waarden kunt instellen als percentage. Deze waarden worden aan het begin van de functie CheckDifferent omgezet naar absolute waarden.

Het algoritme begint met het beeldje in een tijdelijke bitmap te laden en alloceert dan een array voor de grijswaarden. De kleuren in een FPC-beeldje zijn een record van word-sized R-,G- en B-waarden, dus het array bevat words voor de grijswaarden.

function TMainForm.CheckDifferent : boolean;

Const

MaxColor = Cardinal($FFFF);

Var

A : Array of Word;

R, C, I, PD, DC, TH, TC : Integer;

D, MD: Int64;

G : Word;

P : TFPColor;

begin

Result := Length(FLastImage)<>0;

FTempBMP.LoadFromFile(FFrameFile);

TC := FTempBMP.Height*FTempBMP.Width;

TH := Round(MaxColor/100*SETreshold.Value);

MD := TC*MaxColor;

SetLength(A,TC);

De MD is het maximaal mogelijke kleurverschil tussen 2 beeldjes (Dus $FFFF maal het aantal pixels). De waarde TH is het minimale verschil in kleur tussen twee pixels voordat ze als verschillend worden beschouwd. FLastImage is het array van grijswaarden die voor het vorige beeldje gebruikt werd.

Dan kan begonnen worden met het vergelijken van de pixels. Voor elke pixel wordt een grijswaarde berekend die wordt opgeslagen in het beeldje. Tegelijkertijd wordt het verschil met de vorige grijswaarde van het pixel berekend en opgeteld bij het totaal. Het totaal aantal verschillende pixels wordt indien nodig ook opgehoogd.

I := 0;

D := 0;

DC := 0;

For R := 0 to FTempBMP.Height-1 do

For C := 0 to FTempBMP.Width-1 do

begin

P := FTempBMP.Colors[C, R];

G := (P.blue+P.red+P.Green) div 3;

P.Blue := G;P.Red := G;

P.Green := G;

FTempBMP.Colors[C, R] := P;

A[i]:=G;

if (I<Length(FLastImage)) then

begin

PD := Abs(G-FLastImage[i]);

If (PD>TH) then

begin

inc(DC);

D := D+Abs(PD);

end;

end;

Inc(i);

end;

Als de loop afgelopen is, wordt het array met grijswaarden opgeslagen in FLastImage en wordt het resultaat van de functie berekend. De statusbalk wordt gebruikt om wat informatie te tonen: het aantal verschillende kleuren en het totaal aantal verschillende pixels. Als het functieresultaat aangeeft dat een beeldje verschilt, wordt dat beeldje (dat nu is geconverteerd naar een beeldje met alleen grijswaarden) ook opgeslagen op schijf:

FLastImage := A;

STCapture.Caption := Format('Try %d - Color:  %d (%f %%) Pixels: %d/%d (%f %%)', [FTicks, D, D/MD*100, DC, TC, DC/TC*100]);

if Result then

begin

Result := (D/MD*100)>SETrigger.Value;

if Result then

FTempBMP.SaveToFile(FBWFrameFile);

end;

end;

Alles wat nu nog moet gebeuren, is het opgeslagen beeldje versturen naar een e-mailadres. Dat gebeurt net als in het vorige deel van deze cursus met behulp van Synapse.  De werking daarvan moet in principe dan ook geen problemen opleveren:

procedure TMainForm.SendPicture;

Var

Mime : TMimeMess;

P : TMimePart;

B : Boolean;

AText,AServer,ATO : String;

L : TStringList;

begin

STCapture.Caption := 'Sending picture';

ATO := 'editor@blaisepascal.eu';

AServer := 'mail.blaisepascal.eu';

AText := FormatDateTime('dd/mm/yyyy hh:nn:ss',Now);

AText := Format('Camera heeft beweging ontdekt om %s',[AText]);

Mime := TMimeMess.Create;

try

Mime.Header.ToList.Text := ATo;

Mime.Header.Subject := 'Beweging ontdekt';

Mime.Header.From:=ATo;

P := Mime.AddPartMultipart('mixed',Nil);

L := TstringList.Create;

try

L.Text := AText;

Mime.AddPartText(L,P);

Mime.AddPartBinaryFromFile(FFrameFile,P);

Mime.EncodeMessage;

B := SendToRaw(ATo,ATo,AServer,Mime.Lines,'','');

finally

L.Free;

end;

if not B then

STCapture.Caption := 'Failed to send picture'

else

STCapture.Caption := 'Sent picture to '+ATo;

finally

Mime.Free;

end;

end;

Conclusie

Het is relatief makkelijk om een filmpje op te nemen met behulp van een webcam en Lazarus. De code hierboven laat zien dat het dan ook geen probleem meer is om die webcam als bewegingssensor te gebruiken. Het hier gepresenteerde algoritme is wellicht niet het best mogelijke, maar wel eenvoudig en begrijpelijk. Het is dan ook eenvoudig aan te passen, er is ruimte voor variatie: de grijswaarde zou anders berekend kunnen worden en het verschil tussen twee pixels kan ook anders geïnterpreteerd worden. Je zou bijvoorbeeld slechts een deel van het beeldje kunnen gebruiken. Er zijn waarschijnlijk ook betere algoritmen denkbaar dan alleen rechttoe rechtaan twee beeldjes te vergelijken. Zoek in Google maar eens naar de termen Motion Detection Algorithm, dat levert een groot aantal wetenschappelijke publicaties op over dit onderwerp.

(nkr)

Literatuur

[1] Detlef Overbeek, Noud van Kruysbergen, “Hallo Wereld!”, Programmeercursus Pascal, deel 1: de basis, c’t 4/2012, p.102

[2] Siegfried Zuhr, Noud van Kruysbergen, Zoek de vout, Programmeercursus Pascal, deel 2: het debuggen, c’t 5/2012, p.138

[3] Anton Vogelaar, Noud van Kruysbergen, Met klasse, Programmeercursus Pascal, deel 3: classes, c’t 6/2012, p.138

[4] Michaël Van Canneyt, Noud van Kruysbergen, Mailen met Lazarus, Programmeercursus Pascal, deel 4: Mailen met Lazarus, c’t 7-8/2012, p.138

Deel dit artikel

Lees ook

Digitale formulieren maken

Hoe geef je een digitaal formulier vorm en waar moet je om denken? Een korte workshop ‘digitale formulieren maken.’

Speciale tekens typen in Windows, Linux en macOS

Speciale tekens typen in Windows, Linux en macOS gaat makkelijker met wat achtergrondkennis, zodat je valkuilen kunt vermijden. Hoe zit het nu eigenli...

Interessant voor jou

Digitale formulieren maken

Digitale formulieren maken

workshops1 reacties
0 Praat mee
avatar
  Abonneer  
Laat het mij weten wanneer er
Thies Thate
Lezer
Thies Thate

Ook pascal cursus 5 is alleen geschikt voor een windows omgeving. Voor zover ik kan zien wordt er nergens enige hulp voor linux gebruikers gegeven. Graag advies. Ik probeerde nog wel de ZVDateTimeControls package in Lazarus/Linux te installeren (omdat daarin vermeld staat dat het ook geschikt is voor Linux.) Maar ik krijg bij het compileren een fatal internal error 200509181. =================== Inmiddels heb ik Cursus nr 1 werkende onder linux. De nachtegaal zingt met bewegende snavel. Hulp kwam van de BASS library van un4seen.com. Ze gaven de volgende hint: === === OK. To play an audio file with BASS, the… Lees verder »

Gijs van Gemert
Lezer
Gijs van Gemert

Wanneer komt deel 6? Het smaakt allemaal naar meer!

Noud van Kruysbergen
Lezer
Noud van Kruysbergen

Deel 6 staat op de planning. De Lazarus-ontwikkelaars zijn druk bezig geweest met het completeren en uitbrengen van Lazarus 1.0, dus vandaar dat deze cursus even op een laag pitje stond. Maar hij komt eraan!

Jan Vallenduuk
Lezer
Jan Vallenduuk

vanaf vorig jaar…. deel 6 is er nog steeds niet! het komt eraan…. (N v K). Het einde van de wereld komt er ook aan!

Johan Beckers
Lezer
Johan Beckers

Noud van Kruyusbergen stelt dat deel 6 vertraagd is omdat voorrang is gegeven aan het uitbrengen van Lazarus 1.0. Volgens Wikipedia is versie 1.0 uitgebracht op 28 augustus 2012. Dus vraag ik mij af waarom bijna acht maanden later deel 6 nog altijd niet is verschenen.