Kamera
Ballerfassung mit Hilfe einer Kamera
Informationen zur verwendeten Hardware
Kamera
Bei der Kamera handelt es sich um eine schwarzweiß-Hochgeschwindigkeitskamera der Firma Mikrotron, die MC1302. Diese Kamera ist mit bis zu 118 Bilder/s bei maximaler Auflösung (1280x1024Pixel) für die Ballerkennung völlig ausreichend, zumal bei geringerer Auflösung die Bildwiederholrate noch gesteigert werden könnte. Als Ansprechpartner bei Mikrotron stellte sich Hr. Ertl, ein ehemaliger Diplomant der FH München, zur Verfügung.
Beleuchtung
Bei der Beleuchtung ist darauf zu achten, dass möglichst LEDs verwendet werden, oder Lampen mit extrem hoher Eigenfrequenz, damit die Frequenz der Beleuchtung nicht mit der Bildrate der Kamera interferiert. Anbieten würden sich daher LED-Spots, z.B. folgende:
Fraglich ist, welche Farbe sich anbieten würde. Da bisherige Testdurchläufe mit einem Roten Ball erfolgten wären wohl die roten LEDs am effektivsten! <Überprüfen!>
Objektiv
Das zunächst verwendete Objektiv CANON LENS FD 20mm 1:2,8 hat den Nachteil, dass der nötige Abstand zum Objekt, dem Spielfeld, bei einer Auflösung von 640x480 (Grund für diese Auflösung weiter unten!) viel zu groß sein müsste. Bei einem Spielfeld, das 125cm x 68,5cm bemisst wäre ein Abstand von Kamera zu Spielfeld von etwa 3,38m nötig. Diese Annahme ergibt sich aus zwei Messungen und der Tatsache, dass sich der Blickwinkel nicht verändert und somit bei doppeltem Abstand auch eine doppelte Länge bzw. Breite erfasst werden kann (Abweichungen durch Scharfstellen und dem damit verbundenen verändern der Linsen vernachlässigt!).
CANON LENS FD 20mm 1:2,8
- 1. Messung:
Abstand: ca. 66cm, erfasstes Feld: 25cm x 18,7cm
- 2. Messung:
Abstand: ca. 110cm, erfasstes Feld: 40,3cm x 31,2cm
- Nötiger Abstand, um vollständiges Feld zu erfassen:
Der Quotient Abstand/Spielfeldlänge beträgt etwa 2,7. Daher ergibt sich eine notwendige Höhe von etwa 2,7*125cm= 3,38m, was sehr schwer umzusetzen wäre. Es ist also nötig, ein weitwinkligeres Objektiv zu verwenden, mit dem ein geringerer Abstand ausreicht.
Ein besseres Resultat wird mit einem Objektiv mit geringerer Brennweite erzielt. Dafür stehen zum einen ein Objektiv mit 12.5mm (PENTAX TV LENS 12.5mm 1:1.4) oder mit 8mm (MEGAPIXEL CCTV LENS 0814MP f8mm F1.4 A6314) zur Verfügung.
PENTAX TV LENS 12.5mm 1:1.4
Ein durchaus besseres Resultat liefert das Objektiv mit 12.5mm Brennweite. Bei einem Abstand von Objektiv zu Objekt von 113.5cm konnte Feld von 70cm Länge und 51cm Breite aufgenommen werden. Der Quotient 113.5cm/70cm=1,62 ergibt, dass ein Abstand von 1,62*1,25cm=2,03m nötig wäre, um das gesamte Kickerfeld aufzunehmen, also bedeutend weniger als die 3,38m mit dem CANON-Objektiv. Zusätzlich kann noch mit einem Spiegel gearbeitet werden, um die Kamera nicht 2m vom Spielfeld entfernt positionieren zu müssen.
MEGAPIXEL CCTV LENS 0814MP f8mm F1.4 A6314
Dieses Objektiv würde mit der 8mm Brennweite den nötigen Abstand zwischen Objektiv und Spielfeld weiter reduzieren. Leider stellte es sich als inkompatibel zur Kamera heraus, es war nicht möglich das Bild scharf zu stellen.
Rechner
RAM: 1GB CPU: AMD Athlon 64 3200 Framegrapper-Karte ohne eigenen Speicher
Infos zu verwendeter, vorläufiger Software
Kamera konfigurieren
Als erstes muss das Programm VCAM gestartet werden (vor camera control tool). Um die Kamera zu konfigurieren wird die Software Mikrotron camera control tool - MC1302_25 Version 1.2.9 verwendet. Gute Werte wurden erzielt, indem das User Profile 2 (für 640x480px - für 1280x1024 ggf. UserProvile3 laden) geladen wurde, (ggf. 'shutter-mode' auf Async timer gesetzt wurde) und die 'exposure time [sec]' den Lichtverhältnissen angepasst wurde (so klein wie möglich, so groß wie nötig - vgl. Berechnungen). Diese Software wurde durch durch einen Download von Mikrotron auf den aktuellen Stand gebracht (firmware: V1.43-F2.45).
Resultate der Kamera betrachten und Bildfolgen abspeichern
Um sofort überprüfen zu können, ob die verwendeten Einstellungen zu gebrauchen sind bietet sich die Verwendung des Programms VCAM von Mikrotron an. Dieses gibt zum einen die aktuellen Bilder der Kamera wieder, zum anderen gibt es auch die Möglichkeit, Bildfolgen direkt auf die Festplatte abzuspeichern, um sie anschliesend auszuwerten. Um das Bild der Kamera wiederzugebn muss zum einen ein zu den Einstellungen des 'camera control tool's passendes Profil geladen werden (für 640x480 beispielsweise MC1310_11_02_03 640x480 206fps über Camera -> Profile load/save) und anschliesend Camera -> Capture. Dabei fiel allerdings auf, dass selbst bei einer geringen Auflösung von 640x480Pixeln nicht alle Bilder mit eingestellter fps aufgenommen werden konnten. Zunächst wurde davon ausgegangen, dass die Geschwindigkeit der Festplatte das Bottleneck des Speichervorgangs darstellte. Folgende Messreien zeigen deutlich, dass bei einer hohen Bildwiederholrate nicht alle Bilder abgespeichert werden können (Speicherung einer Bildreihe von einer Uhr mit Zehntel Sekunden):
4x .7
4x .8
3x .9
4x .0
3x .1
4x .2
3x .3
3x .4
3x .5
4x .6
4x .7
Betrachtet man dabei die fett markierte Sekunde, so erhält man statt den eingestellten 137fps lediglich 35fps.
6x .4
6x .5
6x .6
5x .7
6x .8
4x .9
5x .0
5x .1
5x .2
4x .3
3x .4
5x .5
Auch hier bleibt man mit den 49fps weit unter den eingestellten 260fps. Allerdings fällt auf, dass sich die effektive Bildwiederholrate gegenüber der vorherigen Messung erhöht hat. Grund müsste laut Hr. Ertl die eingeschränkte Schreibgeschwindigkeit auf Festplatte sein. Da das Programm VCAM ein Demonstationsprogramm darstellt ist es nicht darauf ausgelegt, so viele Bilder innerhalb kürzester Zeit abzuspeichern. Da der Grapper keinen eigenen Speicher besitzt und die Bilder auch nicht im RAM zwischengelagert werden verwirft VCAM alle Bilder, die vor der Beendigung des vorherigen Schreibvorgangs geliefert werden. Dies wäre auch eine erklärung, warum in der 2. Messung mehr fps erreicht werden, da sich die Wartezeit zwischen beendetem Schreibvorgang und der Lieferung des nächsten Bildes kleiner wird.
Sollte das der alleinige Grund für die Reduzierte Abspeicherrate sein, so lässt sich dieses Problem vorläufig mit der Verwendung einer RAM-Disk, also einer virtuellen Festplattenpartition im RAM, lösen.
Ramdisk erstellen (Linux):
sudo mount -t ramfs ramfs /media/RAM-Disk
Geschwindigkeit RAM-Disk an Referenzrechner (AmiloPa2530): Testfile: 700MB
date1=`date '+%s%N'` && cp testdatei1 testdatei2 && expr '(' `date '+%s%N'` - $date1 ')' / 1000000
- HDD -> HDD: 42273msec
- HDD -> RAM: 12886msec
- RAM -> RAM: 875msec
- RAM -> HDD: 18567msec
Ramdisk erstellen (Win2000):
Wie in der Messung unter Linux festzustellen sollte das Problem, dass die Bilder nicht schnell genug auf die HDD geschrieben werden können, mit einer Ramdisk gelöst bzw. umgangen werden können. Von den 1GB Arbeitsspeicher wurde dafür eine Ramdisk von 700MB erstellt. Bei einer neuen Aufnahme einer Bildfolge ergab sich dabei folgendes Resultat:
5x .7
5x .8
5x .9
4x .0
5x .1
5x .2
5x .3
5x .4
5x .5
5x .6
Das ergibt eine Rate von 49fps bei einer eingestellten Rate von 205fps, was gegenüber der 2. Messung ohne Ramdisk keinerlei Verbesserung darstellt. Das Problem scheint also an anderer Stelle zu liegen. Nächster Lösungsansatz wird sein, die Bilder direkt über C-Code von der Kamera zu holen, in reserviertem Speicher abzulegen und danach in Dateien zu speichern.
Koordinaten des Balles aus Bild ermitteln
Was später automatisiert in Sourcecode gefasst passieren soll wird zunächst Step by Step in einem eigenem Programm durchgeführt: Das Auswerten jedes Bildes, um die Koordinaten des Balls zu ermitteln. Dafür wird ein Macro für die Software <MCR - genauer Softwarename!> geschrieben, mit dessen Hilfe folgende Schritte für jedes einzelne Bild durchlaufen werden:
- Bild [bmp] einlesen
- Bereiche mit bestimmter Farbe / bestimmtem Grauwert als aktiv markieren
- Aktive Bereiche, die unterhalb eines Grenzwertes sind löschen (z.B. <200px)
- Schwerpunkt der verbleibenden aktiven Punkte ermitteln (bei Ball -> Mittelpunkt)
- Koordinaten (Mittelpunkt) zu Liste hinzufügen
Das Macro, das dafür notwendig ist sieht folgendermasen aus:
imgdelete "*"
# Bilder 12 bis 41 laden
# for i0=1; i0 <= 40; i0 = i0+1
# imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_000"i0+10".bmp",i0
# endfor
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00011.bmp",1
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00012.bmp",2
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00013.bmp",3
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00014.bmp",4
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00015.bmp",5
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00016.bmp",6
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00017.bmp",7
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00018.bmp",8
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00019.bmp",9
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00020.bmp",10
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00021.bmp",11
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00022.bmp",12
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00023.bmp",13
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00024.bmp",14
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00025.bmp",15
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00026.bmp",16
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00027.bmp",17
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00028.bmp",18
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00029.bmp",19
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00030.bmp",20
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00031.bmp",21
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00032.bmp",22
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00033.bmp",23
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00034.bmp",24
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00035.bmp",25
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00036.bmp",26
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00037.bmp",27
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00038.bmp",28
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00039.bmp",29
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00040.bmp",30
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00041.bmp",31
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00042.bmp",32
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00043.bmp",33
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00044.bmp",34
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00045.bmp",35
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00046.bmp",36
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00047.bmp",37
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00048.bmp",38
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00049.bmp",39
imgload "c:\dokumente und einstellungen\administrator\desktop\kicker\bilder\080416_00050.bmp",40
# imgdisplay 1
for i1=1; i1 <= 40; i1 = i1+1
dislev i1,i1+100,90,213,1
endfor
# dislev 1,1+100,90,213,1
# dislev 2,2+100,90,213,1
# dislev 3,3+100,90,213,1
# dislev 4,4+100,90,213,1
# dislev 5,5+100,90,213,1
# dislev 6,6+100,90,213,1
# dislev 7,7+100,90,213,1
# dislev 8,8+100,90,213,1
# dislev 9,9+100,90,213,1
# dislev 10,10+100,90,213,1
# dislev 11,11+100,90,213,1
# dislev 12,12+100,90,213,1
for i2=1; i2 <= 40; i2 = i2+1
binscrap i2+100,i2+200,0,3000,0
endfor
# binscrap 1+100,1+200,0,3000,0
# binscrap 2+100,2+200,0,3000,0
# binscrap 3+100,3+200,0,3000,0
# binscrap 4+100,4+200,0,3000,0
# binscrap 5+100,5+200,0,3000,0
# binscrap 6+100,6+200,0,3000,0
# binscrap 7+100,7+200,0,3000,0
# binscrap 8+100,8+200,0,3000,0
# binscrap 9+100,9+200,0,3000,0
# binscrap 10+100,10+200,0,3000,0
# binscrap 11+100,11+200,0,3000,0
# binscrap 12+100,12+200,0,3000,0
MSload "kicker1"
# MSsetfeat "REGIONFEAT"
MSmeasmask 1+200,1,"KICKER1",0,1,10
for i3=2; i3 <= 40; i3 = i3+1
MSmeasmask i3+200,1,"KICKER1",1,1,10
endfor
# MSmeasmask 2+200,1,"KICKER1",1,1,10
# MSmeasmask 3+200,1,"KICKER1",1,1,10
# MSmeasmask 4+200,1,"KICKER1",1,1,10
# MSmeasmask 5+200,1,"KICKER1",1,1,10
# MSmeasmask 6+200,1,"KICKER1",1,1,10
# MSmeasmask 7+200,1,"KICKER1",1,1,10
# MSmeasmask 8+200,1,"KICKER1",1,1,10
# MSmeasmask 9+200,1,"KICKER1",1,1,10
# MSmeasmask 10+200,1,"KICKER1",1,1,10
# MSmeasmask 11+200,1,"KICKER1",1,1,10
# MSmeasmask 12+200,1,"KICKER1",1,1,10
datalist "KICKER1",0,0
# for i112=2; i112 <= 12; i112 = i112+1
# MSmeasmask i112+100,1,"KICKER1",1,1,10
# endfor
datalist "KICKER1",0,0
MSsave "kicker1"
Deutlich zu erkennen ist, dass für jeden der Schritte bei größerer Anzahl von Bildern leicht mit Schleifen gearbeitet werden, nur bei dem ersten Schritt, dem einlesen der einzelnen Bilder treten hier durch die unterschiedlichen Dateinamen Probleme auf. Um trotzdem nicht alle Befehle per Hand editieren zu müssen wurde diese Aufgabe von einem kleinen C-Programm übernommen:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_BILDER 10
int
main(int argc, char *argv[])
{
int i;
int start, ende;
if (argc <= 3)
return -1;
start = atoi(argv[2]);
ende = atoi(argv[3]);
for (i = start; i <= ende; i++) {
printf("%s%.4d.bmp\n", argv[1], i);
}
return 0;
}
Das Programm wird mit 3 Parametern aufgerufen. Der erste Parameter ist der Dateipfad bis zu der Stelle der Nummerierung. Der 2. und 3. Parameter ist Anfang und Ende der Schleife. Soll beispielsweise Bild 35 bis 250 eingelesen werden, wird 35 als 2. Parameter und 250 als 3. Parameter übergeben. Die Ausgabe kann man nun direkt in das Makro kopieren.
Ein Testdurchlauf mit diesem Makro ergab folgende Wertetabelle (2. und 3. Spalte):
No X/pixel Y/pixel Dist Richtung [px ]
1 599,5 251,83 /[px] x: y:
2 588,51 251,84 10,99 -10,99 -0,01
3 577 251,87 11,51 -11,51 -0,03
4 563,88 251,76 13,12 -13,12 0,11
5 550,94 251,44 12,94 -12,94 0,32
6 537,38 251,35 13,56 -13,56 0,09
7 524,5 251,07 12,88 -12,88 0,28
8 510,85 250,94 13,65 -13,65 0,13
9 497,96 250,79 12,89 -12,89 0,15
10 477,69 250,77 20,27 -20,27 0,02
11 458,23 250,81 19,46 -19,46 -0,04
12 444,73 250,94 13,50 -13,50 -0,13
13 431,89 250,88 12,84 -12,84 0,06
14 418,07 251,07 13,82 -13,82 -0,19
15 398,71 251,18 19,36 -19,36 -0,11
16 384,71 251,41 14,00 -14,00 -0,23
17 372,33 251,49 12,38 -12,38 -0,08
18 357,79 251,78 14,54 -14,54 -0,29
19 338,33 251,94 19,46 -19,46 -0,16
20 323,7 252,27 14,63 -14,63 -0,33
21 310,82 252,55 12,88 -12,88 -0,28
22 296,42 252,9 14,40 -14,40 -0,35
23 276,66 253,26 19,76 -19,76 -0,36
24 262,2 253,57 14,46 -14,46 -0,31
25 249,2 253,8 13,00 -13,00 -0,23
26 234,76 254,15 14,44 -14,44 -0,35
27 221,4 254,54 13,37 -13,36 -0,39
28 207,61 254,94 13,80 -13,79 -0,40
29 193,82 255,39 13,80 -13,79 -0,45
30 180,51 255,92 13,32 -13,31 -0,53
31 166,19 256,46 14,33 -14,32 -0,54
32 153,25 257,02 12,95 -12,94 -0,56
33 138,69 257,72 14,58 -14,56 -0,70
34 125,58 258,31 13,12 -13,11 -0,59
35 111,12 259,01 14,48 -14,46 -0,70
36 97,41 259,77 13,73 -13,71 -0,76
37 83,45 260,4 13,97 -13,96 -0,63
38 69,24 261,14 14,23 -14,21 -0,74
39 55,58 261,77 13,67 -13,66 -0,63
40 42,97 262,5 12,63 -12,61 -0,73
Aus diesen Koordinaten lässt sich ohne weiteres die zurückgelegte Distanz (Spalte 4) sowie die Änderung in x-Richtung (Spalte 5) wie auch in y-Richtung (Spalte 6) feststellen.
Eigener Sourcecode
Damit das vom Computer gesteuerte Team sofort auf Positionsänderungen des Balls reagieren kann muss die Auswertung der Koordinaten automatisiert werden. Dazu sind im grunde die Selben Schritte wie bei der Software nötig. Diese Schritte werden nach und nach in C-Code implementiert.
- Kameraprofil laden
- Bild und Bildinformationen von Kamera holen
- Binärbild mithilfe der Informationen mit Header versehen und als *.bmp abspeichern (Zwischenschritt um Erfolg zu prüfen
- Bildfolge abspeichern, fps überprüfen!!!
//TODO:
- Mit offenen Bibliotheken Koordinaten des Balls auf den Bildern ermitteln
- Optimierungen (z.B. nur Bildbereiche in bestimmten Umkreis der Koordinaten des letzten Bildes untersuchen o.ä.
Dabei entsteht folgender Code:
Kameraprofil laden und Binärbilder + Bildinformationen holen
//kamera.c
#include <stdio.h>
#include <time.h>
#include "windows.h"
#include "mvfgdrv.h"
#include "bitmap.h"
//C:\PROGRA~1\MIKROT~1\Inspecta\Inspecta-4DC.cam
int
APIENTRY WinMain( HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
LONG ret;
char buffer[256];
void *ptr;
FORMAT_INFO info;
time_t t1, t2;
int err = 0, i;
/* open camera in Testmode, number 0 */
ret = mvfg_open("C:\\PROGRA~1\\MIKROT~1\\Inspecta\\Inspecta-4DC.cam;MC1310_11_02_03 640x480 206fps", 0);
if (ret != MVFG_OK) {
mvfg_errmessage(ret);
return -1;
}
/* get some camera informations */
ret = mvfg_getparam(MVFGPAR_FORMAT_INFO,&info,0);
if (ret != MVFG_OK) {
mvfg_errmessage(ret);
return -1;
} else {
wsprintf(buffer, "Pixel breite: %d\nPixel hoehe: %d", info.lImageWidth,
info.lImageHeight);
MessageBox(NULL, buffer, "Informationen ueber aktuelle Einstellungen", MB_OK);
}
/* grab picture */
#if 0
time(&t1);
for (i = 0; i < 100; i++) {
ret = mvfg_grab(GRAB_WAIT, 0);
if (ret != MVFG_GRAB_READY)
++err;
}
time(&t2);
if (err > 0) {
wsprintf(buffer, "Es sind %d Fehler aufgetreten", err);
MessageBox(NULL, buffer, "Informationen ueber Bilder", MB_OK);
} else {
wsprintf(buffer, "Es wurden 100 Bilder in %d aufgenommen", t2 - t1);
MessageBox(NULL, buffer, "Informationen ueber Bilder", MB_OK);
}
#endif
ret = mvfg_grab(GRAB_WAIT, 0);
if (ret != MVFG_GRAB_READY)
MessageBox(NULL,"Fehler beim aufnehmen", "Informationen ueber Bilder", MB_OK);
/* ptr points to the address of the picture */
ptr = malloc(info.lFrameSize);
memcpy(ptr, mvfg_getbufptr(0), info.lFrameSize);
/*
* XXX save file as bitmap
* we have to build an bitmap header ...
*/
writeBitmapToFile(ptr, info.lFrameSize);
ret = mvfg_close(0);
if (ret != MVFG_OK) {
mvfg_errmessage(ret);
return -1;
} else {
MessageBox(NULL, "Bild wurde abgespeichert, Kamera Handle geschlossen", "Fertig", MB_OK);
}
return 0;
}
//bitmap.h zum anhängen des Bitmap-Headers:
#ifndef _BITMAP_H
#define _BITMAP_H
#include <stdio.h>
int writeBitmapToFile(void *, DWORD );
struct bitmapFileHeader {
WORD bfType;
DWORD bfSize;
DWORD bfReserved;
DWORD bfOffBits;
};
struct bitmapInformationBlock {
DWORD biSize;
LONG biWidth;
LONG biHeight;
WORD biPlanes;
WORD biBitCount;
DWORD biCompression;
DWORD biSizeImage;
LONG biXPelsPerMeter;
LONG biYPelsPerMeter;
DWORD biClrUsed;
DWORD biClrImportant;
};
#endif
//bitmap.cpp
#include <windows.h>
#include <stdio.h>
#include <time.h>
#include "bitmap.h"
int
writeBitmapToFile(void *buffer, DWORD bSize)
{
struct bitmapFileHeader header;
struct bitmapInformationBlock infoBlock;
FILE *fd;
header.bfOffBits = 54; // Offset vom Beginn der Datei in Byte
header.bfReserved = 0;
header.bfType = 19778; //Magic Sequenz ascii BM
header.bfSize = 300 * 1024;
infoBlock.biBitCount = 8; // Farbtiefe
infoBlock.biClrImportant = 0; //Anzahl der im Bild vorkommenden Farben
infoBlock.biClrUsed = 0;
infoBlock.biCompression = 0; //keine Compression
infoBlock.biHeight = 480;
infoBlock.biWidth = 640;
infoBlock.biPlanes = 1;
infoBlock.biSize = 40;
infoBlock.biSizeImage = bSize;
infoBlock.biXPelsPerMeter = 0;
infoBlock.biYPelsPerMeter = 0;
fd = fopen("file.bmp", "wb+");
fwrite(&header, 1, sizeof(header), fd);
fwrite(&infoBlock, 1, sizeof(infoBlock), fd);
fwrite(buffer, 1, bSize, fd);
fclose(fd);
return 1;
}
Binärbild auswerten und Koordinaten bekommen
/***************************************************************************
* Copyright (C) 2008 by Manuel Zimmermann *
* manuel.zimmermann@manyou.de *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#define OFFSET 0x0436
//Header 'wegschneiden'
#define XSIZE 640
#define YSIZE 480
#define LIMITVAL 0x80
//Alles was heller als LIMITVAL ist wird als aktiv betrachtet!
#include "time.h" //Geschwindigkeit feststellen
#include <fcntl.h> // open
#include <unistd.h> //read, close
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
bool mpauswert(char* buffer, int* xmittel, int* ymittel, int* picnr); // Ballmittelpunkt ermitteln
void pictofile(char* buffer, int* xmittel, int* ymittel, int* maxgrnr, int* aktkoor, short (*koordinaten)[XSIZE*YSIZE], int* picnr);
// Auswertresultate in xpm-File schreiben
int main(int argc, char *argv[])
{
for(int picnr=20; picnr<=65; picnr++) //für jedes Bild durchführen:
{
/*****************************************
Bild einlesen
*****************************************/
char filename[32]; //zu ladendes Bild
sprintf (filename, "bilder/test%.3d.bmp",picnr);
int m_iFd = open(filename, O_RDONLY);
char buffer[YSIZE*XSIZE]; //Bild in 2dim Array speichern
{
long rdvalue = lseek(m_iFd, OFFSET, SEEK_SET); //Bearbeitungsposition in Datei setzen, an Headeroffset
read(m_iFd, buffer, XSIZE*YSIZE); //Bild komplett in Buffer einlesen
}
close(m_iFd);
/*****************************************
Bild auswerten
*****************************************/
int xmittel=-1, ymittel=-1;
long anfangszeit = clock();
// Bild auswerten und Mittelpunktskoordinaten in xmittel/ymittel
for(int timei=0; timei<1; timei++) //Geschwindigkeit von Code testen!
{
mpauswert(buffer, &xmittel, &ymittel, &picnr); //picnr für Dateinamen test***.xpm
}
cout << picnr << "\t" << xmittel << "\t" << ymittel << "\t";
cout << ((float)(clock()-anfangszeit)/CLOCKS_PER_SEC) << /*"\t" << CLOCKS_PER_SEC <<*/ endl;
}
return EXIT_SUCCESS;
}
bool mpauswert(char* buffer, int* xmittel, int* ymittel, int* picnr)
{
int aktkoor=0;
short koordinaten[3][XSIZE*YSIZE]; //evtl. Größe geringer wählen!
int maxgrnr=-1, maxanz=-1;
/*****************************************
Zeilengruppen anlegen:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxx11xxxxxxxxxxxxx2222222xxxxxxxxx
xxxxxxxxxxxxxxxxxxxxx3333333333xxxxxxxx
xxxxxx444xxxxxxxxxxxxxx555555xxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
*****************************************/
int linegrnr=0;
//jede Zeile ausgeben, beginnt mit letzter Zeile!
for(int y=0; y<YSIZE; y++) //Jede Zeile
{
for(int x=0; x<XSIZE; x++) //Jeden Pixel in der Zeile
{
if((short)(buffer[y*XSIZE+x] & 0xff) > LIMITVAL)
{
koordinaten[0][aktkoor]=x;
koordinaten[1][aktkoor]=y;
if((koordinaten[0][aktkoor-1]==x-1)&&(koordinaten[1][aktkoor-1]==y))
{
(koordinaten[2][aktkoor]=linegrnr); //hängt an anderen aktivem Zeilenpixel
}else{
(koordinaten[2][aktkoor]=++linegrnr);
}
(aktkoor)++;
}
}
}
/*****************************************
Zeilengruppen vertikal verbinden:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxx11xxxxxxxxxxxxx2222222xxxxxxxxx
xxxxxxxxxxxxxxxxxxxxx2222222222xxxxxxxx
xxxxxx444xxxxxxxxxxxxxx222222xxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
*****************************************/
for(int i=0; i<(aktkoor); i++)
{
int j=i;
while(((koordinaten[1][++j])<=(koordinaten[1][i]+1))&&(j<=aktkoor))
{
if((koordinaten[2][i]!=koordinaten[2][j])&&(koordinaten[0][i]==koordinaten[0][j])&&(koordinaten[1][i]+1==koordinaten[1][j]))
//Wenn die X-Koordinaten identisch und sich die Y-Koordinaten um 1 unterscheiden und sie unterschiedlichen Gruppen angehören:
{
int temp=koordinaten[2][j]; //Gruppennummer die überschrieben wird
int k=i;
while((koordinaten[1][i]==koordinaten[1][++k])||(koordinaten[1][i]+1==koordinaten[1][k])) //Optimierung
{
if(koordinaten[2][k]==temp)
koordinaten[2][k]=koordinaten[2][i];
}
}
}
}
/*****************************************
Größte Gruppe ermitteln - ggf. auch Gruppe mit anz > 100 && < 300 o.ä.
*****************************************/
for(int i=0; i<=linegrnr; i++)
{
int j=0, anz=0;
while((koordinaten[0][j]!=0) || (koordinaten[1][j]!=0))
{
if(koordinaten[2][j++]==i) anz++;
}
if(anz>maxanz)
{
maxanz=anz;
maxgrnr=i;
}
}
/*****************************************
Mittelpunkt aller aktiven Pixel einer bestimmten Gruppe
*****************************************/
int i=0;
int xsum=0, ysum=0;
while(koordinaten[0][i]!=0 | koordinaten[1][i]!=0)
{
if(koordinaten[2][i]==maxgrnr)
{
xsum+=koordinaten[0][i];
ysum+=koordinaten[1][i];
}
i++;
}
*xmittel=(xsum/(maxanz));
*ymittel=YSIZE-(ysum/(maxanz));
/*****************************************
ggf. Resultat in Bilddatei ausgeben
*****************************************/
pictofile(buffer, xmittel, ymittel, &maxgrnr, &aktkoor, koordinaten, picnr);
if((*ymittel>=0)&&(*ymittel<YSIZE)&&(*xmittel<XSIZE)&&(*ymittel<XSIZE))
{
return 1;
}else{
return 0;
}
}
/*****************************************
Auswertung in XPM-Bilddatei
*****************************************/
void pictofile(char* buffer, int* xmittel, int* ymittel, int* maxgrnr, int* aktkoor, short (*koordinaten)[XSIZE*YSIZE], int* picnr)
{
char outname[32]; //Textfile, Übersicht aktive Pixel
sprintf (outname, "bilderout/test%.3d.xpm", *picnr);
ofstream myfile;
myfile.open (outname, fstream::binary);
myfile << "/* XPM */" << endl;
myfile << "static char * WheelbarrowFull_xpm[] = {" << endl;
myfile << "\"640 480 4 1\"," << endl;
myfile << "\"0 c #36c21f\"," << endl;
myfile << "\"1 c #ffffff\"," << endl;
myfile << "\"2 c #000000\"," << endl;
myfile << "\"3 c #ff0000\"," << endl;
for(int y=YSIZE-1; y>=0; y--) //Jede Zeile, letzte zuerst (da bei bmp letztes Pixel zuerst!)
{
myfile << "\"";
for(int x=0; x<XSIZE; x++) //Jeden Pixel in der Zeile
{
if((short)(buffer[y*XSIZE+x] & 0xff) > LIMITVAL)
{
//Ballmittelpunkt mit rotem + markieren
if((x==(*xmittel))&&((y==(480-(*ymittel)))||(y+1==(480-(*ymittel)))||(y-1==(480-(*ymittel)))||(y+2==(480-(*ymittel)))||(y-2==(480-(*ymittel))))|| (y==480-(*ymittel))&&((x==(*xmittel))||(x+1==(*xmittel))||(x-1==(*xmittel))||(x+2==(*xmittel))||(x-2==(*xmittel))))
{
myfile << 3;
}else{
for(int i=0; i<*aktkoor; i++)
{
if((koordinaten[0][i]==x)&&(koordinaten[1][i]==y)&&(koordinaten[2][i]==*maxgrnr)) myfile << 2; //aktiver Pixel && Ball
if((koordinaten[0][i]==x)&&(koordinaten[1][i]==y)&&(koordinaten[2][i]!=*maxgrnr)) myfile << 1; //aktiver Pixel und nicht Ball
}
}
}
else
{
myfile << 0;
}
}
if (y==0)
{
myfile << "\"};" << endl;
}else{
myfile << "\"," << endl;
}
}
myfile.close();
}
Daraus ergibt sich folgendes Resultat:
X(Code) Y(Code) X (SW) Y (SW)
600 198 600,91 197,01
585 202 585,4 200,89
569 205 569,61 204,52
553 209 553,49 208,17
538 213 538,37 211,73
523 216 523,71 215,15
509 219 509,14 218,52
495 223 495,12 221,8
481 226 481,37 224,92
468 229 468,13 228,11
454 232 454,99 231,13
442 235 442,13 233,99
429 238 429,4 236,89
416 241 416,57 239,8
403 244 403,83 242,67
390 247 391 245,69
378 250 378,38 248,6
365 252 365,56 251,53
352 255 352,78 254,41
339 258 340,08 257,35
327 261 327,29 260,2
314 264 314,49 263,1
301 267 301,74 266,02
288 270 288,92 268,92
275 273 276,21 271,85
263 276 263,53 274,76
250 279 250,75 277,72
237 282 238,04 280,62
225 285 225,26 283,6
212 287 212,68 286,53
199 290 200,01 289,39
187 293 187,41 292,26
174 296 174,78 295,17
162 299 162,21 298,03
149 302 149,57 301,01
136 305 137,14 303,95
124 308 124,67 306,81
112 311 112,08 309,68
99 313 99,65 312,53
87 316 87,24 315,42
74 319 74,77 318,28
62 322 62,45 321,24
50 325 50,15 324,08
37 328 37,8 326,93
25 331 25,46 329,75
14 334 14,55 332,6
Der eigene Sourcecode liefert also gegenüber der fertigen Software sehr gute Ergebnisse. Einziges Problem könnte die Dauer von fast 70% der verfügbaren Zeit sein, optimieren!!! (durch Optimierung auf ca 20% gesenkt!)
Resultat bei je 100 Auswertungen pro Bild:
Picnr. X Y time[sec] (für je 100 Auswertungen!)
20 600 198 0.24
21 585 202 0.18
22 569 206 0.21
23 553 209 0.19
24 538 213 0.18
25 523 216 0.18
26 508 219 0.16
27 494 223 0.17
28 481 226 0.17
29 467 229 0.17
30 454 232 0.16
31 441 235 0.16
32 429 238 0.16
33 416 241 0.15
34 403 244 0.15
35 390 247 0.15
36 378 250 0.15
37 365 252 0.15
38 352 255 0.16
39 339 258 0.15
40 327 261 0.15
41 314 264 0.16
42 301 267 0.15
43 288 270 0.15
44 275 273 0.16
45 263 276 0.14
46 250 279 0.16
47 237 282 0.16
48 224 285 0.15
49 212 287 0.15
50 199 290 0.15
51 187 293 0.15
52 174 296 0.15
53 161 299 0.15
54 149 302 0.14
55 136 305 0.15
56 124 308 0.15
57 111 311 0.15
58 99 314 0.14
59 86 316 0.15
60 74 319 0.14
61 62 322 0.15
62 49 325 0.14
63 37 328 0.15
64 25 331 0.14
65 14 334 0.14
Jedes Bild wurde 100x ausgewertet, die Zeit blieb dabei um die 0.2sec. Theoretisch haben wir zur Verarbeitung jedes Bildes 10msec (bei 100fps), da sollte ein Auswerten von jeweils 2msec i.O. sein.
Berechnungen
Nötige Auflösung
Bei der zu verwendenden Auflösung gilt: so groß wie nötig, so klein wie nötig. Je größer jedes auszuwertende Bild ist, desto mehr Rechenaufwand und Zeit wird für die Auswertung benötigt. Allerdings wird eine gewisse minimale Auflösung benötigt, damit der Ball gegenüber anderen Störeinflüssen wie Schatten, Flecken o.ä. eindeutig erkannt werden kann. Außerdem wirkt sich die Auflösung des Bildes auf die Genauigkeit der ermittelten Koordinaten aus.
Um diesen Anforderungen gerecht zu werden wurde eine Auflösung von 640x480 gewählt. Die Spielfeldlänge von 1,25m wird dabei in 480px aufgeteilt bzw. von diesen erfasst, die Breite von 68,5cm hingegen von 350px, es werden also nicht alle der 480 möglichen Pixeln für die Breite benötigt, da das Spielfeld nicht in einem Verhältnis von 4:3 also 1,33:1 sondern von 1,82:1 steht.
x-Achse: 640px/125cm = 5,12px/cm = 1px/2mm y-Achse: 5,12px/cm * 68,5cm = 350,7px
- Durchmesser des Balls: 3,5cm = 3,5cm * 5,12px/cm = 18px (genauer: 17,92px)
- Fläche des Balls: A = r² * PI = (17,92px/2)² * PI = 252px
Das heißt, dass bei der Ballerkennung etwa 250px den Ball darstellen, was zum einen ausreichend ist um ihn von Störungen zu unterscheiden, was zum anderen auch eine weit höhere Genauigkeit bringt als die Auflösung selbst (1px/2mm), da die Koordinaten aus dem Mittelpunkt dieser 250px ermittelt werden, also die Koordinaten auf mehrere Pixelkomastellen genau darstellen (was bei Pixeln zwar nicht viel Sinn macht da es nu ganze Pixel geht, aber beim Betrachten der Koordinaten zur verbesserten Genauigkeit beiträgt).
Nötige Exposure Time
Eine weiterer Parameter den man bestimmen muss ist der mögliche Maximalwert der exposure time. Darunter kann man sich die Belichtungszeit vorstellen. Die nötige exposure time ist abhängig von der Beleuchtung: Je geringer die Beleuchtung desto länger die Belichtungszeit, um ein gutes Bild mit ausreichender Helligkeit zu erhalten. Allerdings ist ein Bild mit hoher exposure time bei schnellen Bewegungen sehr verschwommen. Aber auch die Beleuchtung sollte gering gehalten werden, da Hitze, Stromkosten und das Blenden der Spieler Nebenwirkungen sind.
Um die Koordinaten des Balles ohne größere Verfälschung ermitteln zu können wird als Grenze eine exposure time verwendet, in der sich die Position des Balls während der Belichtungszeit maximal um 3px/Bild ändert. Geht man dabei von einer Maximalgeschwindigkeit des Balls von 4m/sec aus, so ergibt sich folgende maximale exposure time:
- 4m/sec = 4mm/msec = 2px/msec (bei 1px/2mm)
- Grenze 3px: 1,5msec = 1/667 sec
Dieser Wert ist gegenüber den momentan verwendeten Werten von etwa 1/280 sec sehr gering und setzt daher eine sehr gute Beleuchtung vorraus. Ggf. wäre es möglich, diesen Wert auch etwas zu vergrößern, was zwar zu einem verschwommenen Ball auf dem Bild führen würde, aber evtl. trotzdem ausreichen würde, um gute Koordinaten zu erhalten. Mit einer exposure time von 1/280 sec hätte man in einem Bild eine Positionsänderung des Balls bei einer Geschwindigkeit von 2px/msec von:
- 1sec/280 * 2px/msec = 7,14px (Frage an Hr. Jaschul: möglich?)
Nächste Schritte
Nun steht es an, einen direkten Vergleich von Ballerkennung via Kamera und Lichtgitter zu erreichen. Dazu wird ein Ball über eine Strecke, die von der Kamera als auch vom Lichtgitter überwacht wird, geschossen, und die erhaltenen Daten auf Genauigkeit und Zuverlässigkeit ausgewertet.