Fisch mit Webcam steuern

Übersicht BlitzMax, BlitzMax NG Codearchiv & Module

Neue Antwort erstellen

Vertex

Betreff: Fisch mit Webcam steuern

BeitragFr, Sep 07, 2012 1:05
Antworten mit Zitat
Benutzer-Profile anzeigen
Die Idee hinter dem Projekt ist ein Charakter mittels Webcam zu steuern. Die Steuerung erfolgt mit 3 auf Daumen und Zeigefinger aufgeklebten Markern. Die große Herausforderung ist, im Bild diese 3 Marker zu erkennen. Hier kommt die Circular Hough Transform (CHT) zum Einsatz, um Kreise zu erkennen.

Download mit Media: https://www.blitzforum.de/upload/file.php?id=11973
Beinhaltet den Source-Code, Escapi-DLL und -API, Schnellanleitung und Druckvorlage für die Marker.

Habe gerade keine Marker zu Hand, weswegen ich einfach in Paint 3 schwarze Punkte gezeichnet und mit der Webcam den Bildschirm abgefilmt habe:
user posted image

Die Markererkennung ist in folgendem Bild zu sehen:
user posted image
[Oberes Bild: Kantenerkennung (rot) und Markererkennung (grün); unteres Bild: Colornessbild (wie farbig sind die Pixel?)]

So sind die Marker auf der Hand anzubringen:
user posted image

Die Markererkennung beruht auf Methoden in der Bildverarbeitung, insbesondere dem Canny-Operator zur Kantenerkennung und der Circular Hough Transform (CHT) zur Kreiserkennung. Die Schritte sind:

1. Lese das Webcambild in ein Array ein. Das Array enthält die Farbwerte im RGB-Farbraum.
2. Projeziere das Bild zum Grauwertbild.
3. Canny-Operator zur Kantenerkennung
3.1 Tiefpassfilterung, d. h. hohe Frequenzen werden gefiltert. Hier mit einem 5x5 Gauß-Kern (bei Photoshop "Gaußscher Weichzeichner")
3.2 Gradientenbild erstellen. Hier mittels SobelX und SobelY.
3.3 Als Kantenpixel gelten diejenigen Pixel, deren Gradientenbeträge in unmittelbarer Nachbarschaft maximal sind.
4. Kreiserkennung mittels HCT
5. Markererkennung anhand von Kriterien, bspw. ob innere Randpixel dunkler sind, als äußere Randpixel

BlitzMax: [AUSKLAPPEN]
SuperStrict

Framework BRL.Blitz
Import BRL.Max2D
Import BRL.D3D9Max2D
Import BRL.PNGLoader
Import BRL.StandardIO
Import BRL.Random
Import BRL.Audio
Import BRL.WAVLoader
Import Pub.Win32
Import BRL.FreeAudioAudio

' Siehe http://www.blitzbasic.com/codearcs/codearcs.php?code=1899
' escapi.bmx ist unter http://www.hi-toro.com/blitz/escapi_max.zip zu finden
' Die escapi.dll kann unter http://sol.gfxile.net/escapi/index.html bzw.
' http://sol.gfxile.net/zip/escapi21.zip heruntergeladen werden
Import "escapi.bmx"

' Genauigkeit um auf 0 zu prüfen.
Const EPSILON : Float = 0.00001

' CIRCUMFERENCE[r]: Anzahl gerasteteter Pixel für einen Kreis mit Radius r,
' mit r = 0 ... 255. Die Anzahl wurde mit dem Bresehenham-Algorithmus berechnet.
Global CIRCUMFERENCE : Int[] = [..
4, 12, 20, 20, 28, 36, 36, 44, 52, 60, 60, 68, 76, 76, 84, ..
92, 100, 100, 108, 116, 116, 124, 132, 140, 140, 148, 156, 156, 164, 172, ..
172, 180, 188, 196, 196, 204, 212, 212, 220, 228, 236, 236, 244, 252, 252, ..
260, 268, 268, 276, 284, 292, 292, 300, 308, 308, 316, 324, 332, 332, 340, ..
348, 348, 356, 364, 372, 372, 380, 388, 388, 396, 404, 404, 412, 420, 428, ..
428, 436, 444, 444, 452, 460, 468, 468, 476, 484, 484, 492, 500, 500, 508, ..
516, 524, 524, 532, 540, 540, 548, 556, 564, 564, 572, 580, 580, 588, 596, ..
596, 604, 612, 620, 620, 628, 636, 636, 644, 652, 660, 660, 668, 676, 676, ..
684, 692, 700, 700, 708, 716, 716, 724, 732, 732, 740, 748, 756, 756, 764, ..
772, 772, 780, 788, 796, 796, 804, 812, 812, 820, 828, 828, 836, 844, 852, ..
852, 860, 868, 868, 876, 884, 892, 892, 900, 908, 908, 916, 924, 932, 932, ..
940, 948, 948, 956, 964, 964, 972, 980, 988, 988, 996, 1004, 1004, 1012, 1020, ..
1028, 1028, 1036, 1044, 1044, 1052, 1060, 1060, 1068, 1076, 1084, 1084, 1092, 1100, 1100, ..
1108, 1116, 1124, 1124, 1132, 1140, 1140, 1148, 1156, 1156, 1164, 1172, 1180, 1180, 1188, ..
1196, 1196, 1204, 1212, 1220, 1220, 1228, 1236, 1236, 1244, 1252, 1260, 1260, 1268, 1276, ..
1276, 1284, 1292, 1292, 1300, 1308, 1316, 1316, 1324, 1332, 1332, 1340, 1348, 1356, 1356, ..
1364, 1372, 1372, 1380, 1388, 1388, 1396, 1404, 1412, 1412, 1420, 1428, 1428, 1436, 1444]

' Größe des Webcambildes
Const CAPTURE_WIDTH : Int = 320
Const CAPTURE_HEIGHT : Int = 240

' Index der Webcam für ESCAPI
Const DEVICE_IDX : Int = 1 ' Bei mir "IP Camera [JPEG/MJPEG]"

' Soll der untere Streifen des Webcambildes grau übermalt werden?
' (Nur notwendig bei unlizensierten Treiber IP Camera)
Const USE_IP_CAMERA : Int = True

' Schwellwert der Gradientenbeträge. Siehe DetermineEdges
Const GRADIENT_THRESHOLD : Float = 150.0

' Minimaler und maximaler Radius der Kreise die erkannt werden sollen.
' Siehe CircularHoughTransform
Const MIN_RADIUS : Int = 3
Const MAX_RADIUS : Int = 15

' Kleinstes Verhältnis zwischen Anzahl der Kreispixel n und dem Kreisumfang
' Siehe IsCircle
Const RATIO_THRESHOLD : Float = 0.4

' Kleinster Helligkeitsunterschied zw. inneren und äußeren Randpixeln
' Siehe IsCircle
Const LUMINANCE_DIFFERENCE_THRESHOLD : Float = 0.3

' Größte Farbigkeit der inneren Randpixel
' Siehe IsCircle
Const COLORNESS_THRESHOLD : Byte = 40

' 3x3 normierter Gaußkern. Siehe Blur
Global gaussian3x3 : Float[3, 3]
gaussian3x3[0, 0] = 1.0 / 16.0 ; gaussian3x3[1, 0] = 2.0 / 16.0 ; gaussian3x3[2, 0] = 1.0 / 16.0
gaussian3x3[0, 1] = 2.0 / 16.0 ; gaussian3x3[1, 1] = 4.0 / 16.0 ; gaussian3x3[2, 1] = 2.0 / 16.0
gaussian3x3[0, 2] = 1.0 / 16.0 ; gaussian3x3[1, 2] = 2.0 / 16.0 ; gaussian3x3[2, 2] = 1.0 / 16.0

' 5x5 normierter Gaußkern. Siehe Blur
Global gaussian5x5 : Float[5, 5]
gaussian5x5[0, 0] = 1.0 / 256.0 ; gaussian5x5[1, 0] = 4.0 / 256.0 ; gaussian5x5[2, 0] = 6.0 / 256.0 ; gaussian5x5[3, 0] = 4.0 / 256.0 ; gaussian5x5[4, 0] = 1.0 / 256.0
gaussian5x5[0, 1] = 4.0 / 256.0 ; gaussian5x5[1, 1] = 16.0 / 256.0 ; gaussian5x5[2, 1] = 24.0 / 256.0 ; gaussian5x5[3, 1] = 16.0 / 256.0 ; gaussian5x5[4, 1] = 4.0 / 256.0
gaussian5x5[0, 2] = 6.0 / 256.0 ; gaussian5x5[1, 2] = 24.0 / 256.0 ; gaussian5x5[2, 2] = 36.0 / 256.0 ; gaussian5x5[3, 2] = 24.0 / 256.0 ; gaussian5x5[4, 2] = 6.0 / 256.0
gaussian5x5[0, 3] = 4.0 / 256.0 ; gaussian5x5[1, 3] = 16.0 / 256.0 ; gaussian5x5[2, 3] = 24.0 / 256.0 ; gaussian5x5[3, 3] = 16.0 / 256.0 ; gaussian5x5[4, 3] = 4.0 / 256.0
gaussian5x5[0, 4] = 1.0 / 256.0 ; gaussian5x5[1, 4] = 4.0 / 256.0 ; gaussian5x5[2, 4] = 6.0 / 256.0 ; gaussian5x5[3, 4] = 4.0 / 256.0 ; gaussian5x5[4, 4] = 1.0 / 256.0

' Sobel-Kern in X-Richtung. Siehe CalculateGradients
Global sobelx : Int[3, 3]
sobelx[0, 0] = -1 ; sobelx[1, 0] = 0 ; sobelx[2, 0] = 1
sobelx[0, 1] = -2 ; sobelx[1, 1] = 0 ; sobelx[2, 1] = 2
sobelx[0, 2] = -1 ; sobelx[1, 2] = 0 ; sobelx[2, 2] = 1

' Sobel-Kern in Y-Richtung. Siehe CalculateGradients
Global sobely : Int[3, 3]
sobely[0, 0] = -1 ; sobely[1, 0] = -2 ; sobely[2, 0] = -1
sobely[0, 1] = 0 ; sobely[1, 1] = 0 ; sobely[2, 1] = 0
sobely[0, 2] = 1 ; sobely[1, 2] = 2 ; sobely[2, 2] = 1

' Existiert das (DEVICE_IDX + 1)-te CaptureDevice?
Local deviceCount : Int = CountCaptureDevices()
Print "Devices:"
For Local i : Int = 0 Until deviceCount
Print i + ") " + CaptureDeviceName(i)
Next
If deviceCount <= DEVICE_IDX Then Throw "Capture device " + DEVICE_IDX + " not found"


' Starte Webcamaufnahme
Global device : TCaptureDevice = OpenCaptureDevice(DEVICE_IDX, CAPTURE_WIDTH, CAPTURE_HEIGHT)
If device = Null Then Throw "Unable to open capture device '" + CaptureDeviceName(DEVICE_IDX) + "'"

Print "Connected with '" + CaptureDeviceName(device.device) + "'"

' Aufgenommenes Bild der Webcam
Global source : TImage

' RGB-Farbwert des Punkts (x, y) von source mit
' sourceRGB[x, y, 0] = 0 ... 255, Rotwert
' sourceRGB[x, y, 1] = 0 ... 255, Grünwert
' sourceRGB[x, y, 2] = 0 ... 255, Blauwert
' Siehe ReadSource
Global sourceRGB : Byte[CAPTURE_WIDTH, CAPTURE_HEIGHT, 3]

' Grauwert des Punkts (x, y) von sourceRGB mit
' sourceGray[x, y] = 0 ... 255
' Siehe ProjectToGray
Global sourceGray : Byte[CAPTURE_WIDTH, CAPTURE_HEIGHT]

' sourceGray tiefpassgefiltert. Grauwert des Punkts (x, y) mit
' sourceGray[x, y] = 0 ... 25
' Siehe Blur
Global sourceConvolved : Byte[CAPTURE_WIDTH, CAPTURE_HEIGHT]

' Gradient (gx, gy) des Punkts (x, y) mit
' sourceGradient[x, y, 0]: Betrag sqrt(gx² + gy²)
' sourceGradient[x, y, 1]: gx
' sourceGradient[x, y, 2]: gy
' Siehe CalculateGradients
Global sourceGradient : Float[CAPTURE_WIDTH, CAPTURE_HEIGHT, 3]

' (x, y) ist Kante, wenn sourceEdge[x, y] = True.
' Siehe DetermineEdges
Global sourceEdge : Int[CAPTURE_WIDTH, CAPTURE_HEIGHT]

' Akkumulator für die Hough-Transformation
' sourceHough[x, y, r]: Anzahl der Kantenpixel, die zum Kreis (x, y, r) gehören
' Siehe CircularHoughTransform
Global sourceHough : Int[CAPTURE_WIDTH, CAPTURE_HEIGHT, MAX_RADIUS - MIN_RADIUS]

Global circleQuality : Float[CAPTURE_WIDTH, CAPTURE_HEIGHT, MAX_RADIUS - MIN_RADIUS]

' Die gefundenen Kreise/Marker, siehe OrderCircles. Dabei gilt bei circles[idx, i]
' idx: SINGLE_POINT, TOP_POINT, BOTTOM_POINT und
' i = 0, i = 1: XY-Koordinaten des Kreismittelpunkts; i = 2: Radius.
' Es gibt immer einen einzelnen und zwei nebeneinanderliegende Marker:
Const SINGLE_POINT : Int = 0 ' Alleinstehender Marker
Const TOP_POINT : Int = 1 ' Obererer Marker
Const BOTTOM_POINT : Int = 2 ' Unterer Marker
Global circles : Int[3, 3]

' 3 Programmmodi
Const MODE_ANIMATE : Int = 0 ' Fischanimation. Aktivieren durch drücken von [A].
Const MODE_DEBUG : Int = 1 ' Kantenerkennung, Kreiserkennung, Farbigkeitsanalyse.
' Aktivieren durch drücken von [D].
Const MODE_PAUSE : Int = 2 ' Friert aktuelles Bild ein und es werden Pixelinformationen
' (RGB, HSV, Colorness) unter dem Mauszeiger angezeigt.
' Aktivieren durch drücken von [P].
Local mode : Int = MODE_DEBUG

If SetAudioDriver("FreeAudio") Then
Print "Current audio driver: FreeAudio"
Else
Print "Unable to set audio driver FreeAudio"
EndIf

LoadFish()

AppTitle = CaptureDeviceName(device.device)
Graphics 800, 600, 0, 60, GRAPHICS_BACKBUFFER

SetBlend ALPHABLEND
While Not KeyDown(KEY_ESCAPE)
' Moduswechsel
If KeyHit(KEY_P) Then
mode = MODE_PAUSE
ElseIf KeyHit(KEY_A) Then
mode = MODE_ANIMATE
ElseIf KeyHit(KEY_D) Then
mode = MODE_DEBUG
EndIf
' Snapshot
If KeyHit(KEY_S) And device.pix <> Null Then
SavePixmapPNG(device.pix, "snapshot.png", 0)
Print "Saved to snapshot.png"
EndIf

Select mode
Case MODE_ANIMATE
Cls()
ReadSource()
ProjectToGray()
Blur()
CalculateGradients()
DetermineEdges()
CircularHoughTransform()
FilterCircles()
OrderCircles()
DrawFish()
Flip()
Case MODE_DEBUG
Cls()
ReadSource()
ProjectToGray()
Blur()
CalculateGradients()
DetermineEdges()
DrawEdges()
CircularHoughTransform()
FilterCircles()
OrderCircles()
DrawCircles()
DrawColorness()
Flip()
Case MODE_PAUSE
Cls()
Analyse()
DrawEdges()
Flip()
End Select
Wend

CloseCaptureDevice(device)
device = Null
End

Rem
Ausgabe
source : TImage
sourceRGB[x, y, i] mit i = 0, 1, 2

Liest die Pixmap des CaptureDevices in das Array sourceRGB ein.
End Rem

Function ReadSource()
source = CaptureFrame(device)
If source = Null Then
Print "Device lost"
Else
' Durch IP Camera muss der Werbetext unten übermalt werden, sonst
' findet die Kreiserkennung in Buchstaben ebenfalls Kreise.
If USE_IP_CAMERA Then
SetColor 127, 127, 127
DrawRect 0, CAPTURE_HEIGHT - CAPTURE_HEIGHT * 0.09, CAPTURE_WIDTH, CAPTURE_HEIGHT * 0.09
GrabImage source, 0, 0
EndIf
EndIf

Local pixmap : TPixmap = LockImage(source, 0, True, False)

For Local x : Int = 0 Until PixmapWidth(pixmap)
For Local y : Int = 0 Until PixmapHeight(pixmap)
Local argb : Int = ReadPixel(pixmap, x, y)
sourceRGB[x, y, 0] = (argb & $00FF0000) Shr 16
sourceRGB[x, y, 1] = (argb & $0000FF00) Shr 8
sourceRGB[x, y, 2] = argb & $000000FF
Next
Next

UnlockImage(source)
End Function

Rem
Eingabe
sourceRGB[x, y, i]
Ausgabe
sourceGray[x, y]

Bildet sourceRGB im RGB-Farbraum auf Intensitätswerte in sourceGray ab.
Die Abbildung ist: (r, g, b) -> (r + g + b) / 3
End Rem

Function ProjectToGray()
For Local x : Int = 0 Until CAPTURE_WIDTH
For Local y : Int = 0 Until CAPTURE_HEIGHT
sourceGray[x, y] = (sourceRGB[x, y, 0] + sourceRGB[x, y, 1] + sourceRGB[x, y, 2]) / 3
Next
Next
End Function

Rem
Eingabe
sourceGray[x, y]
Ausgabe
sourceConvolved[x, y]

Glättet (= Tiefpassfilter) sourceGray und speichert das Ergebnis in sourceConvolved. Die
Glättung erfolgt als Vorschritt für die Kantenerkennung mittels Canny-Algorithmus,
um eine Überempfindlichkeit durch Rauschen zu verkleinern. Es wird mit
einem 5x5 Gaußkernel gefaltet. Die Ränder (hier (5-1)/2 px = 2 px groß) von sourceGray werden
ungeglättet gelassen.

Anmerkung: Eventuell ist der Gaußkernel separierbar. Wenn ja könnte einmal vertikal
und anschließend horizontal geglättet werden um Rechenzeit zu sparen. Wenn nicht, könnte
auch ein Binomialfilter zum Einsatz kommen, der definitiv separierbar ist.
End Rem

Function Blur()
For Local x : Int = 2 Until CAPTURE_WIDTH - 2
For Local y : Int = 2 Until CAPTURE_HEIGHT - 2
Local accu : Float = 0.0
For Local i : Int = -2 To 2
For Local j : Int = -2 To 2
accu :+ gaussian5x5[i + 2, j + 2] * sourceGray[x + i, y + j]
Next
Next

sourceConvolved[x, y] = accu
Next
Next
End Function

Rem
Eingabe
sourceConvolved[x, y]
Ausgabe
sourceGradient[x, y, i]

Berechnet für sourceConvolved die Gradienten und speichert sie in sourceGradient.
Das Gradientenbild wird für den Canny-Algorithmus zur Kantenerkennung benötigt.
Hohe Gradientenbeträge sind Kantenkandidaten. Die Berechnung erfolgt durch Faltung
mit SobelX-Kern, zur Erkennung vertikaler Kanten, und SobelY-Kern zur Erkennung
horizontaler Kanten.
End Rem

Function CalculateGradients()
For Local x : Int = 2 Until CAPTURE_WIDTH - 2
For Local y : Int = 2 Until CAPTURE_HEIGHT - 2
Local sx : Int = 0, sy : Int = 0
For Local i : Int = -1 To 1
For Local j : Int = -1 To 1
sx = sx + sobelx[i + 1, j + 1] * sourceConvolved[x + i, y + j]
sy = sy + sobely[i + 1, j + 1] * sourceConvolved[x + i, y + j]
Next
Next
sourceGradient[x, y, 0] = Sqr(sx * sx + sy * sy) ' Betrag des Gradienten
sourceGradient[x, y, 1] = sx ' X-Komponente des Gradienten
sourceGradient[x, y, 2] = sy ' Y-Komponente des Gradienten
Next
Next
End Function

Rem
Eingabe
sourceGradient[x, y, i]
Ausgabe
sourceEdge[x, y]

Im Gradientenbild wird nach Kanten gesucht und Kantenpixel in sourceEdge auf True gesetzt.
Die Berechnung erfolgt durch den Canny-Algorithmus. Ein Pixel ist dann eine Kante,
wenn sein Gradientenbetrag > GRADIENT_THRESHOLD ist und er in der Nachbarschaft den
den größten Gradientenbetrag besitzt. Die Nachbarschaft sind 2 angrenzende (Sub-)Pixel.
Auf den einen Nachbarpixel zeigt der Gradientenvektor auf den anderen zeigt der
negative Gradientenvektor. Durch die Nachbarschaftsbetrachtung werden Kanten nur 1 px breit.
End Rem

Function DetermineEdges()
' Siehe http://www.cvmt.dk/education/teaching/f09/VGIS8/AIP/canny_09gr820.pdf

' engl. "Non-maximum suppression", d. h. Kantenpixel muss Maximum (bzgl. Gradientenbetrag)
' in der Nachbarschaft haben.
For Local x : Int = 3 Until CAPTURE_WIDTH - 3
For Local y : Int = 3 Until CAPTURE_HEIGHT - 3
If sourceGradient[x, y, 0] < GRADIENT_THRESHOLD Then
' Gradientbetrag unterhalb von GRADIENT_THRESHOLD
sourceEdge[x, y] = False ' Keine Kante
Continue
EndIf

Local sx : Float = sourceGradient[x, y, 1]
Local sy : Float = sourceGradient[x, y, 2]

' Gradientenbetrag des Pixels, auf den der Gradientenvektor zeigt (ga)
' und auf den der negative Gradientenvektor zeigt (gb)
Local ga : Float, gb : Float

If Abs(sx) < EPSILON Then
' Gradient ist vertikal
If sy > 0.0 Then
' Gradient zeigt nach unten
Rem
o o gb o


0 o o
|
v
o o ga o
End Rem

ga = sourceGradient[x, y + 1, 0]
gb = sourceGradient[x, y - 1, 0]
Else
' Gradient zeigt nach oben
Rem
o o ga o
^
|
0 o o


o o gb o
End Rem

ga = sourceGradient[x, y - 1, 0]
gb = sourceGradient[x, y + 1, 0]
EndIf
ElseIf Abs(sy) < EPSILON Then
' Gradient ist horizontal
If sx > 0.0 Then
' Gradient zeigt nach rechts
Rem
o o o


0 o----->o
gb ga

o o o
End Rem

ga = sourceGradient[x + 1, y, 0]
gb = sourceGradient[x - 1, y, 0]
Else
' Gradient zeigt nach links
Rem
o o o


0<-----o o
ga gb

o o o
End Rem

ga = sourceGradient[x - 1, y, 0]
gb = sourceGradient[x + 1, y, 0]
EndIf
Else If sx > 0.0 Then
' Gradient ist rechts geneigt
Rem
ga
o o ^ o
P / Q
/
0 o o


o S o T o
gb
End Rem

' Subpixelbildung von P und Q zu ga, und S und T zu gb.
' Hier nur durch Mittelung von P und Q bzw. S und T.
' Korrekterweise müsste der Gradientenwinkel berücksichtigt werden.
If sy > 0.0 Then
ga = (sourceGradient[x + 1, y + 1, 0] + sourceGradient[x, y + 1, 0]) / 2.0
gb = (sourceGradient[x - 1, y - 1, 0] + sourceGradient[x, y - 1, 0]) / 2.0
Else
ga = (sourceGradient[x + 1, y - 1, 0] + sourceGradient[x, y - 1, 0]) / 2.0
gb = (sourceGradient[x - 1, y + 1, 0] + sourceGradient[x, y + 1, 0]) / 2.0
EndIf
Else If sx < 0.0 Then
' Gradient ist links geneigt
Rem
ga
o ^ o o
P \ Q
\
0 o o


o o S o T
gb
End Rem

' Berechnung analog zu sx > 0.0
If sy > 0.0 Then
ga = (sourceGradient[x - 1, y + 1, 0] + sourceGradient[x, y + 1, 0]) / 2.0
gb = (sourceGradient[x + 1, y - 1, 0] + sourceGradient[x, y - 1, 0]) / 2.0
Else
ga = (sourceGradient[x - 1, y - 1, 0] + sourceGradient[x, y - 1, 0]) / 2.0
gb = (sourceGradient[x + 1, y + 1, 0] + sourceGradient[x, y + 1, 0]) / 2.0
EndIf
Else

EndIf

' (x, y) ist nur dann eine Kante, wenn der Gradientenbetrag größer ist, als der seiner Nachbarschaft
sourceEdge[x, y] = sourceGradient[x, y, 0] > Max(ga, gb)
Next
Next
End Function

Rem
Zeichnet an (x, y) einen roten Pixel, wenn sourceEdge[x, y] = True
End Rem

Function DrawEdges()
' Alten Farbwert sichern
Local oldRed : Int, oldGreen : Int, oldBlue : Int
GetColor(oldRed, oldGreen, oldBlue)

SetColor 255, 0, 0
For Local x : Int = 0 Until CAPTURE_WIDTH
For Local y : Int = 0 Until CAPTURE_HEIGHT
If sourceEdge[x, y] Then Plot x, y
Next
Next

' Alten Farbwert zurücksetzen
SetColor(oldRed, oldGreen, oldBlue)
End Function

Rem
Eingabe
sourceEdge
Ausgabe
sourceHough

Hough-Transformation als Vorschritt zur Erkennung von Kreisen im Kantenbild sourceEdge.
Der Bildraum sourceEdge wird in den Parameter- bzw. Houghraum sourceHough transformiert.
sourceHough[x, y, r] wird als Akkumulator benutzt und gibt auskunft, wieviele Pixel
zum Kreis (x, y, r) mit Mittelpunkt (x, y) und Radius r gehören.
End Rem

Function CircularHoughTransform()
' Akkumulator auf 0 setzen
'MemClear sourceHough, CAPTURE_WIDTH * CAPTURE_HEIGHT * (MAX_RADIUS - MIN_RADIUS) ' funktioniert nicht?
For Local x : Int = 0 Until CAPTURE_WIDTH
For Local y : Int = 0 Until CAPTURE_HEIGHT
For Local r : Int = MIN_RADIUS Until MAX_RADIUS
sourceHough[x, y, r - MIN_RADIUS] = 0
Next
Next
Next

For Local x : Int = MAX_RADIUS Until CAPTURE_WIDTH - MAX_RADIUS
For Local y : Int = MAX_RADIUS Until CAPTURE_HEIGHT - MAX_RADIUS
If sourceEdge[x, y] Then
' Kantenpixel gefunden

' Kantenpixel kann zu mehreren Kreisen gehören.
' siehe http://www.cis.rit.edu/class/simg782/lectures/lecture_10/lec782_05_10.pdf
' Bei all diesen Kreisen (x, y, r) wird der Akkumulator sourceHough[x, y, r] um
' 1 erhöht.
For Local r : Int = MIN_RADIUS Until MAX_RADIUS
' Diskrete Rasterung des Kreises (x, y, r) im Houghraum sourceHough
RasterHoughCircle(x, y, r)
Next
EndIf
Next
Next
End Function

Rem
Eingabe
Kreismittelpunkt (mx, my) und Radius radius

Diskrete Rasterung des Kreises (mx, my, radius) im Houghraum sourceHough.
An den Punkten (x, y, r), an denen der Kreis gerastert wird, wird der Akkumulator
sourceHough[x, y, r] um 1 erhöht. Die Rasterung erfolgt mittels Bresenham-Algorithmus.
End Rem

Function RasterHoughCircle(mx : Int, my : Int, radius : Int)
' Implementiert nach http://de.wikipedia.org/wiki/Bresenham-Algorithmus#Kreisvariante_des_Algorithmus

Local f : Int = 1 - radius
Local ddF_x : Int = 0
Local ddF_y : Int = -2 * radius
Local x : Int = 0
Local y : Int = radius

SetHoughPoint(mx, my + radius, radius)
SetHoughPoint(mx, my - radius, radius)
SetHoughPoint(mx + radius, my, radius)
SetHoughPoint(mx - radius, my, radius)

While x < y
If f >= 0 Then
y = y - 1
ddF_y = ddF_y + 2
f = f + ddF_y
EndIf
x = x + 1
ddF_x = ddF_x + 2
f = f + ddF_x + 1

SetHoughPoint(mx + x, my + y, radius)
SetHoughPoint(mx - x, my + y, radius)
SetHoughPoint(mx + x, my - y, radius)
SetHoughPoint(mx - x, my - y, radius)
SetHoughPoint(mx + y, my + x, radius)
SetHoughPoint(mx - y, my + x, radius)
SetHoughPoint(mx + y, my - x, radius)
SetHoughPoint(mx - y, my - x, radius)
Wend
End Function

Rem
Eingabe
Punkt (x, y, r) im Houghraum sourceHough[x, y, r]

Erhöht im Houghraum sourceHough[x, y, r] den Punkt (x, y, r) um 1.
End Rem

Function SetHoughPoint(x : Int, y : Int, r : Int)
sourceHough[x, y, r - MIN_RADIUS] :+ 1
End Function

Rem
Eingabe
sourceHough[x, y, r]
Ausgabe
sourceHough[x, y, r]

Filtert Kreise in sourceHough. Das geschieht in 2 Stufen:

1. Ist sourceHough[x, y, r] ein Kreis? Wenn nein, setze sourceHough[x, y, r] = 0
2. Finde sich überschneidente Kreise. Überschneidende Kreise werden hinsichtlich
ihres Verhältnis Anzahl der Pixel zum Kreisumfang bewertet. Der Kreis, mit
dem größten Verhältnis wird beibehalten.
End Rem

Function FilterCircles()
' 1. Stufe: Ist sourceHough[x, y, r] ein Kreis? Wenn nein, setze sourceHough[x, y, r] = 0
For Local x : Int = MAX_RADIUS Until CAPTURE_WIDTH - MAX_RADIUS
For Local y : Int = MAX_RADIUS Until CAPTURE_HEIGHT - MAX_RADIUS
For Local r : Int = MIN_RADIUS Until MAX_RADIUS
If Not IsCircle(x, y, r) Then sourceHough[x, y, r - MIN_RADIUS] = 0
Next
Next
Next

' 2. Stufe: Finde sich überschneidende Kreise.
For Local x : Int = 0 Until CAPTURE_WIDTH
For Local y : Int = 0 Until CAPTURE_HEIGHT
For Local r : Int = MIN_RADIUS Until MAX_RADIUS
If sourceHough[x, y, r - MIN_RADIUS] > 0 Then
Local bestX : Int = x, bestY : Int = y, bestR : Int = r
' Verhältnis von Kreis 1 (x, y, r)
Local bestRatio : Float = Float(sourceHough[x, y, r - MIN_RADIUS]) / CIRCUMFERENCE[r]

For Local r2 : Int = MIN_RADIUS Until MAX_RADIUS
For Local x2 : Int = Max(0, x - MAX_RADIUS - r2) Until Min(CAPTURE_WIDTH, x + MAX_RADIUS + r2)
For Local y2 : Int = Max(0, y - MAX_RADIUS - r2) Until Min(CAPTURE_HEIGHT, y + MAX_RADIUS + r2)
Local dx : Int = x - x2
Local dy : Int = y - y2
Local d : Int = Floor(Sqr(dx * dx + dy + dy))
' Überschneiden sich Kreis 1 (x, y, r) mit Kreis 2 (x2, y2, r2)?
If d <= (r + r2) Then
' Verhältnis von Kreis 2 (x2, y2, r2)
Local ratio : Float = Float(sourceHough[x2, y2, r2 - MIN_RADIUS]) / CIRCUMFERENCE[r]

' Ist Verhältnis von Kreis 2 besser als das bislang beste?
If ratio > bestRatio Then
bestX = x2
bestY = y2
bestR = r2
EndIf
EndIf
Next
Next
Next

' Lösche alle sich schneidenden Kreise, außer dem, mit dem besten Verhältnis.
For Local r2 : Int = MIN_RADIUS Until MAX_RADIUS
For Local x2 : Int = Max(0, x - MAX_RADIUS - r2) Until Min(CAPTURE_WIDTH, x + MAX_RADIUS + r2)
For Local y2 : Int = Max(0, y - MAX_RADIUS - r2) Until Min(CAPTURE_HEIGHT, y + MAX_RADIUS + r2)
If bestX <> x2 Or bestY <> y2 Or bestR <> r2 Then sourceHough[x2, y2, r2 - MIN_RADIUS] = 0
Next
Next
Next
EndIf
Next
Next
Next
End Function

Rem
Eingabe
Kreismittelpunkt (mx, my), Radius r
Ausgabe
True, wenn (mx, my, r) ein Kreis ist
False, sonst

Folgende 4 Kriterien für einen Kreis sind festgelegt:

1. Anzahl der Pixel im Verhältnis zum Kreisumfang
2. Helligkeit der inneren Randpixeln ist größer, als die der äußeren
3. Helligkeitsdifferenz zw. äußeren und inneren Randpixeln überschreitet
LUMINANCE_DIFFERENCE_THRESHOLD
4. Farbigkeit der inneren Randpixel ist kleiner als COLORNESS_THRESHOLD

1. Anzahl der Pixel im Verhältnis zum Kreisumfang:

n >= (2 * Pi * r * RATIO_THRESHOLD), mit n = Anzahl der Pixel
Dabei ist n / (2 * Pi * r) das Verhältnis n zum Kreisumfang ist.
Das Verhältnis muss >= RATIO_THRESHOLD sein.

2. Helligkeit der inneren Randpixeln ist größer, als die der äußeren

Die Marker sind schwarzgefüllte Kreise mit weißem Rand. Das soll mit diesem
Kriterium geprüft werden.

Zwei Pixel oberhalb, rechts, unterhalb und links außerhalb des Kreises sind
als äußere Randpixel definiert; die inneren Randpixel analog innerhalb des
Kreises:

o
_ _
/ \
/ i \
o | i i | o
\ i /
\ _ _ /
i: innere Randpixel
o o: äußere Randpixel

Die Farbwerte oberer, rechter, usw. innerer sowie äußerer Randpixel werden
als Durchschnittswerte zusammengefasst.

Für den Durchschnittswert für jeweils der inneren und äußeren Randpixel
wird die Helligkeit mittels RGB-nach-HSV-Transformation ermittelt:

- v_i: V(alue)-Komponente der inneren Randpixel im HSV-Farbraum
- v_o: analog zu v_i für die äußeren Randpixel

Das 2. Kriterium besagt, dass

v_i > v_o

erfüllt sein muss.

3. Helligkeitsdifferenz zw. äußeren und inneren Randpixeln überschreitet
LUMINANCE_DIFFERENCE_THRESHOLD

Dieses Kriterium prüft darauf, ob die schwarze, innere Fläche genügend
Kontrast zur weißen, äußeren Fläche bietet.
Die schwarze und weiße Fläche werden nahezu gleich durch das Umgebungslicht
beleuchtet, wobei die schwarze Fläche nicht vollständig das Umgebungslicht
absorbiert. Setzt man beide in Differenz, kann so das Umgebungslicht
herausgerechnet werden.

Der Kontrast, unabhängig des Umgebungslichts, muss größer als
LUMINANCE_DIFFERENCE_THRESHOLD sein:

v_o - v_i > LUMINANCE_DIFFERENCE_THRESHOLD

4. Farbigkeit der inneren Randpixel ist kleiner als COLORNESS_THRESHOLD

Dieses Kriterium schließt farbige Marker aus, denn auch dunkelrote
Marker könnten das 2. und 3. Kriterium erfüllen. Da die weiße
Fläche das Umegbungslicht sehr viel stärker reflektiert, als die
schwarze Fläche, werden nur die inneren, schwarzen Randpixel
überprüft. Die Farbigkeit c_i der inneren Randpixel muss

c_i < COLORNESS_THRESHOLD

erfüllen.
End Rem

Function IsCircle:Int(mx : Int, my : Int, r : Int)
' Kriterium 1: Anzahl der Pixel im Verhältnis zum Kreisumfang
If sourceHough[mx, my, r - MIN_RADIUS] < (CIRCUMFERENCE[r] * RATIO_THRESHOLD) Then Return False

Local innerRGB : Int[3] ' Durchschnittswert der inneren Randpixel
Local outerRGB : Int[3] ' Durchschnittswert der äußeren Randpixel

For Local angle : Int = 0 Until 360 Step 90
Local dx : Int = Cos(angle) * 2
Local dy : Int = Sin(angle) * 2
Local x : Int = mx + r * Cos(angle)
Local y : Int = my + r * Sin(angle)

For Local i : Int = 0 To 2
innerRGB[i] :+ sourceRGB[x - dx, y - dy, i]
outerRGB[i] :+ sourceRGB[x + dx, y + dy, i]
Next
Next
For Local i : Int = 0 To 2
innerRGB[i] :/ 4
outerRGB[i] :/ 4
Next

Local innerHSV : Float[3]
Local outerHSV : Float[3]
RGBToHSV(innerRGB[0], innerRGB[1], innerRGB[2], innerHSV[0], innerHSV[1], innerHSV[2])
RGBToHSV(outerRGB[0], outerRGB[1], outerRGB[2], outerHSV[0], outerHSV[1], outerHSV[2])
' Kriterium 2 und 3: v_i < v_o und v_o - v_i > LUMINANCE_DIFFERENCE_THRESHOLD
If (innerHSV[2] > outerHSV[2]) Or (outerHSV[2] - innerHSV[2] <= LUMINANCE_DIFFERENCE_THRESHOLD) Then Return False

Local innerColorness : Byte = Colorness(innerRGB[0], innerRGB[1], innerRGB[2])
' Kriterium 4: Farbigkeit der inneren Randpixel ist kleiner als COLORNESS_THRESHOLD
If innerColorness >= COLORNESS_THRESHOLD Then Return False

' Alle Kriterien erfüllt
Return True
End Function

Rem
Eingabe
sourceHough[x, y, r]
Ausgabe
circles[idx, i]

Sucht die letzten 3 gefilterten Kreise in sourceHough[x, y, r] und ordnet sie
in circles[idx, i] wie folgt:

- Es gibt einen alleinstehenden Kreis. Die anderen zwei Kreise haben zueinander
eine kleinere X-Differenz als der alleinstehende Kreis zu den anderen Kreisen.
- Zwischen den zusammengehörenden Kreisen wird zwischen oberen und unteren
unterschieden.

Das Ergebnis wird in

- circles[SINGLE_POINT, i]: Einzelner Kreis
- circles[TOP_POINT, i]: oberer, der zusammengehörenden Kreise
- circles[BOTTOM_POINT, i]: unterer, der zusammengehörenden Kreise

mit i = 0: X-Koordinate, i = 1: Y-Koordinate des Kreismittelpunkts
und i = 2: Radius des Kreises.
End Rem

Function OrderCircles()
' Finde die letzten 3 Kreise im Houghraum
Local i : Int = 0
For Local x : Int = MAX_RADIUS Until CAPTURE_WIDTH - MAX_RADIUS
For Local y : Int = MAX_RADIUS Until CAPTURE_HEIGHT - MAX_RADIUS
For Local r : Int = MIN_RADIUS Until MAX_RADIUS
If sourceHough[x, y, r - MIN_RADIUS] > 0 Then
circles[i, 0] = x
circles[i, 1] = y
circles[i, 2] = r
i :+ 1
If i > 2 Then i = 0
EndIf
Next
Next
Next

Local singleIdx : Int
Local topIdx : Int
Local bottomIdx : Int

' X-Abstände zwischen Kreis 1 und 2: dx01, Kreis 1 und 3: dx02 und Kreis 2 und 3: dx12
Local dx01 : Int = Abs(circles[0, 0] - circles[1, 0])
Local dx02 : Int = Abs(circles[0, 0] - circles[2, 0])
Local dx12 : Int = Abs(circles[1, 0] - circles[2, 0])

If dx01 < dx02 Then
If dx01 < dx12 Then
singleIdx = 2
Else
singleIdx = 0
EndIf
Else
If dx02 < dx12 Then
singleIdx = 1
Else
singleIdx = 0
EndIf
EndIf
If singleIdx <> SINGLE_POINT Then ExchangeCircles(singleIdx, SINGLE_POINT)
If circles[TOP_POINT, 1] > circles[BOTTOM_POINT, 1] Then ExchangeCircles(TOP_POINT, BOTTOM_POINT)
End Function

Rem
Hilfsfunktion: Vertauscht die Kreise in circles[fromIdx, i] und
circles[toIdx, i]
End Rem

Function ExchangeCircles(fromIdx:Int, toIdx : Int)
Local temp : Int

For Local i : Int = 0 To 2
temp = circles[fromIdx, i]
circles[fromIdx, i] = circles[toIdx, i]
circles[toIdx, i] = temp
Next
End Function

Rem
Zeichnet die Kreise in circles[idx, i].
End Rem

Function DrawCircles()
SetColor 0, 255, 0
For Local i : Int = 0 To 2
Local x : Int = circles[i, 0]
Local y : Int = circles[i, 1]
Local r : Int = circles[i, 2]
DrawOval x - r, y - r, 2 * r, 2 * r
DrawText i, x + 10, y + 10
Next
SetColor 255, 255, 255
End Function

Rem
Pause-Modus. Zeigt dem Benutzer

- HSV-Tripel,
- RGB-Tripel und
- Colorness-Wert

des unter dem Mauszeiger liegenenden Pixels an.
End Rem

Function Analyse()
SetColor 255, 255, 255
DrawImage source, 0, 0

Local mx : Int = MouseX(), my : Int = MouseY()
If mx < 0 Or mx >= CAPTURE_WIDTH Or my < 0 Or my >= CAPTURE_HEIGHT Then Return

Local h : Float, s : Float, v : Float
RGBToHSV(sourceRGB[mx, my, 0], sourceRGB[mx, my, 1], sourceRGB[mx, my, 2], h, s, v)
DrawText "HSV: " + Int(h) + "°, " + Int(s * 100) + "%, " + Int(v * 100) + "%", mx + 10, my + 10
DrawText "RGB: " + sourceRGB[mx, my, 0] + ", " + sourceRGB[mx, my, 1] + ", " + sourceRGB[mx, my, 2], mx + 10, my + 25
DrawText "Coloness: " + Colorness(sourceRGB[mx, my, 0], sourceRGB[mx, my, 1], sourceRGB[mx, my, 2]), mx + 10, my + 40
SetColor sourceRGB[mx, my, 0], sourceRGB[mx, my, 2], sourceRGB[mx, my, 2]
DrawRect mx + 10, my, 7, 7
SetColor 255, 255, 255
End Function

Rem
Eingabe
Punkt (red, green, blue) im RGB-Farbraum
Ausgabe
Punkt (hue, saturation, value) im HSV-Farbraum mit
hue = 0° ... 360° (Farbwert),
saturation = 0 ... 1 (ungesättigt bis gesättigt) und
value = 0 ... 1 (dunkel bis hell)

Transformiert (red, green, blue) im RGB-Farbraum in (hue, saturation, value) im HSV-Farbraum.
End Rem

Function RGBToHSV(red : Int, green : Int, blue : Int, hue : Float Var, saturation : Float Var, value : Float Var)
' Implementiert nach http://de.wikipedia.org/wiki/HSV-Farbraum#Umrechnung_RGB_in_HSV

Local r : Float = red / 255.0
Local g : Float = green / 255.0
Local b : Float = blue / 255.0

Local maxRGB : Float = Max(Max(r, g), b)
Local minRGB : Float = Min(Min(r, g), b)

' Hue 0.0...360.0
If Abs(maxRGB - minRGB) <= EPSILON And (Abs(r - g) <= EPSILON And Abs(g - b) <= EPSILON) Then
hue = 0.0
ElseIf maxRGB = r Then
hue = 60.0 * (g - b) / (maxRGB - minRGB)
ElseIf maxRGB = g Then
hue = 60.0 * (2.0 + (b - r) / (maxRGB - minRGB))
ElseIf maxRGB = b Then
hue = 60.0 * (4.0 + (r - g) / (maxRGB - minRGB))
EndIf
If hue < 0.0 Then hue = hue + 360.0

' Saturation 0.0...1.0
If maxRGB <= EPSILON And (r <= EPSILON And g <= EPSILON And b <= EPSILON) Then
' maxRGB = 0 <==> r = g = b = 0
saturation = 0.0
Else
saturation = (maxRGB - minRGB) / maxRGB
EndIf

' Value 0.0...1.0
value = maxRGB
End Function

Rem
Eingabe
Farbwert (r, g, b)
Ausgabe
Farbigkeit

Die Farbigkeit ist hier definiert als (kürzester) Abstand des Punkts (r, g, b) im RGB-Würfel
zur Diagonalen (0, 0, 0), (255, 255, 255). Auf der Diagonalen befinden sich unbunte Farbwerte.
Je weiter der Abstand eines Farbwertes zur Diaognalen ist, desto bunter ist er.

Eigenschaften:

Farbigkeit: 0 (unbunt) ... ~208.2066 (bunt)
End Rem

Function Colorness:Byte(r : Byte, g : Byte, b : Byte)
Rem
Abstand d des Punkts p = (r, g, b)^T zur Geraden x = a + t * u ist:

|(p - a) x u|
d = -------------
|u|

siehe http://www.ina-de-brabandt.de/vektoren/a/abstand-punkt-gerade-formel.html
End Rem


Local u : Float[3]
u[0] = 255.0
u[1] = 255.0
u[2] = 255.0

' v = (p - a) x u = p x u
Local v : Float[3]
v[0] = g * u[2] - b * u[1]
v[1] = b * u[0] - r * u[2]
v[2] = r * u[1] - g * u[0]

Return Sqr(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / Sqr(u[0] * u[0] + u[1] * u[1] + u[2] * u[2])
End Function

Rem
Zeichnet Colorness-Bild unter dem Webcambild. Schwarz: keine Colorness bis
Weiß: volle Colorness.
End Rem

Function DrawColorness()
Local pixmap : TPixmap = CreatePixmap(CAPTURE_WIDTH, CAPTURE_HEIGHT, PF_I8)
Local pointer : Byte Ptr = PixmapPixelPtr(pixmap)

For Local x : Int = 0 Until CAPTURE_WIDTH
For Local y : Int = 0 Until CAPTURE_HEIGHT
Local colorness : Int = Colorness(sourceRGB[x, y, 0], sourceRGB[x, y, 1], sourceRGB[x, y, 2])
colorness = (Float(colorness) / 209.0) * 255
If colorness > 255 Then colorness = 255
pointer[y * CAPTURE_WIDTH + x] = colorness
Next
Next

DrawPixmap pixmap, 0, CAPTURE_HEIGHT
End Function

Rem
Partikel für die Blubberblasen des Fischs
End Rem

Type TParticle
Global bubble : TImage

Field sprite : TImage
Field lifeTime : Int
Field scale : Float
Field positionX : Float
Field positionY : Float
Field velocityX : Float
Field velocityY : Float

Method Update()
positionX :+ velocityX
positionY :+ velocityY
lifeTime :- 1
If lifeTime < 0 Then lifeTime = 0
End Method

Method Render()
SetScale scale, scale
SetAlpha 1.0 - 0.7 * ((10 - Min(10, lifeTime)) / 10.0)
DrawImage sprite, positionX, positionY
SetAlpha 1.0
SetScale 1.0, 1.0
End Method

Function Init()
bubble = LoadImage("animation/bubble.png")
End Function
End Type

Rem
Quelle für Partikel
End Rem

Type TEmitter
Global bubbleSound : TSound
Global bubbleChannel : TChannel

Field particles : TList = New TList
Field positionX : Float
Field positionY : Float

Method Emit()
bubbleSound.Play(bubbleChannel)
For Local i : Int = 0 Until Rand(3, 5)
Local particle : TParticle = New TParticle
Local angle : Float = Rand(270 - 20, 270 + 20)
Local vel : Float = Rand(10, 70) / 10.0
particle.sprite = TParticle.bubble
particle.positionX = Self.positionX
particle.positionY = Self.positionY
particle.velocityX = vel * Cos(angle)
particle.velocityY = vel * Sin(angle)
particle.scale = 0.03 * vel + 0.2
particle.lifeTime = Rand(30, 60)
particles.AddLast(particle)
Next
End Method

Method Update()
Local toRemove : TList = New TList
For Local particle : TParticle = EachIn particles
particle.Update()
If particle.lifeTime = 0 Then toRemove.AddLast(particle)
Next
For Local particle : TParticle = EachIn toRemove
particles.remove(particle)
Next
End Method

Method Render()
For Local particle : TParticle = EachIn particles
particle.Render()
Next
End Method

Function Init()
bubbleSound = LoadSound("animation/bubble.wav")
bubbleChannel = bubbleSound.Cue()
End Function
End Type

Global background : TImage
Global body : TImage
Global mouth : TImage
Global emitter : TEmitter

Function LoadFish()
background : TImage = LoadImage("animation/background.png")
body : TImage = LoadImage("animation/body.png")
SetImageHandle body, 270, 404
mouth : TImage = LoadImage("animation/mouth.png")
TParticle.Init()
TEmitter.Init()
emitter = New TEmitter
End Function

Rem
Zeichnet den Fisch an den 3 erkannten Kreisen/Markern:

- SINGLE_POINT: Anker in der Nähe der Schwanzflosse
- BOTTOM_POINT: Anker in der Nähe der Brustflosse
- TOP_POINT: Steuerung der Mundöffnung

Der innere Winkel zw. allen 3 Markern, mouthAngle,
bestimmt die Öffnung des Mundes.
End Rem

' Wenn bubbleActive = 0 und der innere Winkel groß genug
' ist, dann werden die Blasen emittiert und bubbleActive
' hoch gesetzt. Erst, wenn der innere Winkel klein genug
' ist, wird bubbleActive wieder herunter gezählt. Das
' verhindert permanentes emittieren von Blasen.
Global bubbleActive : Int = 0
Function DrawFish()
Local x1 : Int = circles[SINGLE_POINT, 0]
Local y1 : Int = circles[SINGLE_POINT, 1]

Local x2 : Int = circles[TOP_POINT, 0]
Local y2 : Int = circles[TOP_POINT, 1]

Local x3 : Int = circles[BOTTOM_POINT, 0]
Local y3 : Int = circles[BOTTOM_POINT, 1]

Local dx : Float = x1 - x3
Local dy : Float = y1 - y3
' SINGLE_POINT und BOTTOM_POINT geben Rotationswinkel und Größe des Fisches vor:
Local bodyAngle : Float = 270 - ATan2(dx, dy)
Local bodyScale : Float = (Sqr(dx * dx + dy * dy) / 350.0)

' Berechne inneren Winkel aller 3 Marker mittels Punktprodukt
Local vecA : Int[2], vecB : Int[2]
vecA[0] = x2 - x1
vecA[1] = y2 - y1
vecB[0] = x3 - x1
vecB[1] = y3 - y1
Local dotProduct : Int = vecA[0] * vecB[0] + vecA[1] * vecB[1]
Local mouthAngle : Float = ACos(dotProduct / (Sqr(vecA[0]^2 + vecA[1]^2)*Sqr(vecB[0]^2 + vecB[1]^2)))
If mouthAngle < 0.0 Then
mouthAngle = 0.0
ElseIf mouthAngle > 45.0 Then
mouthAngle = 45.0
EndIf

Local mouthX : Float = circles[SINGLE_POINT, 0] + 440 * bodyScale * Cos(bodyAngle - 10)
Local mouthY : Float = circles[SINGLE_POINT, 1] + 440 * bodyScale * Sin(bodyAngle - 10)

' Ist der innere Winkel groß genug?
If mouthAngle < 35.0 Then
bubbleActive = bubbleActive - 1
If bubbleActive < 0 Then bubbleActive = 0
ElseIf bubbleActive = 0 Then
bubbleActive = 5
emitter.positionX = mouthX
emitter.positionY = mouthY
emitter.Emit()
EndIf
emitter.Update()

' Im Hintergrund das Webcambild und die Kreise zeichnen
SetAlpha 1.0
SetColor 255, 255, 255
DrawImage source, 0, 0
DrawCircles()

' Zeichne Fisch und Blasen mit 70 % Alpha
SetAlpha 0.7
DrawImage background, 0, 0
Local sign : Float
If x1 < x2 Or x1 < x2 Then
sign = +1.0
Else
sign = -1.0
EndIf
SetTransform bodyAngle, bodyScale, sign * bodyScale
DrawImage body, circles[SINGLE_POINT, 0], circles[SINGLE_POINT, 1]
Local mouthScale : Float = bodyScale * (Tan(mouthAngle) + 0.2)
SetScale mouthScale, mouthScale
SetRotation bodyAngle
DrawImage mouth, mouthX, mouthY
SetTransform 0, 1, 1
SetHandle 0, 0
SetColor 255, 255, 255
SetAlpha 1.0
emitter.Render()
End Function
vertex.dreamfall.at | GitHub

Plasma

Betreff: fishy fishy fishy

BeitragFr, Sep 07, 2012 20:39
Antworten mit Zitat
Benutzer-Profile anzeigen
gute idee ,besser als meine
http://www.blitzbasic.com/Comm...007#282419

dusseligerweise wurden damals die pics gelöscht

Vertex

BeitragSa, Sep 08, 2012 10:49
Antworten mit Zitat
Benutzer-Profile anzeigen
Danke!

Ja, Deine DLL habe ich noch in Erinnerung (oh Gott, schon wieder 8 Jahre her?). Da konnte man doch irgendwas mit der Hand steuern. Du spricht in dem Topic von Motion Capture. Wie hast Du denn das grob umgesetzt? Ich war damals fasziniert, als Logitech zur Webcam ein Basketballspiel mitgeliefert hatte, in dem man den Basketball mit der Hand werfen konnte.
vertex.dreamfall.at | GitHub

Plasma

Betreff: hmm

BeitragSo, Sep 09, 2012 19:35
Antworten mit Zitat
Benutzer-Profile anzeigen
motion capt. war / ist eigendlich unmöglich.
das bemerkte ich viel zu spät .trotz assemblereinsatz zur bewegungserkennung war das ganze buggy und
lahm.
das problem waren die fehlenden Marker, sowie fundamentale wissenlücken in der optischen bildverarbeitung
2d ging halbwegs ,3d (tiefe) war unmöglich.
fast schon zu spät konnte ich eine sony ps webcam ergattern und treiber gabs im netz.
das stark verbesserte rauschverhalten des chips erleichterte mir die bewegungserkennung.
vorgehensweise war :
bild im stillstand als original .
farb / pixel veränderungen wurden mittels einstellbarem trigger rausgerechnet, das ganze bekam dann vier rahmen (kopf,hand-l,hand-r,body )
anhand der rahmengrösse erstellte ich images die ich zur collision nutzte (mittels blitz)

problem war dann die hand vor dem body oderdem kopf Rolling Eyes

ABER :
ich konnte meinen Fisch füttern oder mittels fingercollison in eine andere richtung schwimmen lassen .

Ein befreundeter bb user aus usa schickte mir später etwas bb code um echte bewegungsvectoren auszurechnen .(pixelgenau !!!!)
um diesen code zu verstehen brauchte man(n) aber mindestens sieben hirne Laughing
ein anderer user versuchte sich an gesichtserkennung , was er auch gut schaffte aber den sourcecode nie offenlegte (c++).
die idee einen 3d shooter mittels marker an basecap oder brille zu steuern ( um die ecke gugen z.b) fand ich auch gut , diese steuerung ist einfach umzusetzen und sehr effektiv und schnell.
zur damaligen zeit hatte ich aber nur ne olle Krücke .
etwas später fiel die hochkant gestellte blitz source festplatte um und hatte head crash .....
dann kaufte ich mir einen mac Razz

PS: echt ewig her ...

Propellator

BeitragSo, Sep 09, 2012 19:46
Antworten mit Zitat
Benutzer-Profile anzeigen
Eine eventuell einfachere Lösung für komplexes Motion-Tracking wäre die Library OpenCV, welche sich dank dem C-Interface eigentlich in BMax benutzen lassen sollte und auch in kommerziellen Kreisen grosses Ansehen geniesst.
Ich selbst weiss von der Materie nichts, aber der Link wäre eventuell hilfreich falls man das ganze weiterziehen will.
Ich kenne jemanden der damit einen USB-kontrollierbaren Schaumgummiraketenwerfer Ziele tracken lässt und dann diese mit den Motoren des Werfers anvisieren lassen kann, mit OpenCV (bzw SimpleCV für python). Pre-Alpha Video
Propellator - Alles andere ist irrelephant.
Elefanten sind die Könige der Antarktis.

Vertex

BeitragMi, Sep 12, 2012 10:06
Antworten mit Zitat
Benutzer-Profile anzeigen
Plasma, ja, dass mit den Bewegungsvektoren finde ich auch interessant. Das nennt sich Motion Estimation. Gleiche Bewegungsvektoren (bzgl. Länge und Richtung) könnten zu Regionen zusammengefasst werden und dann wieder mit in die Kollisionprüfung eingehen.

Propellator, OpenCV kannte ich bereits. Ich habe das mal für Java ausprobiert, aber da ist die Portierung noch nicht fortgeschritten genug. Außerdem hatte er meine Kamera nicht erkannt, weswegen ich auf BlitzMax ausgewischen bin. Aber vom Funktionsumfang ist in OpenCV schon alles implementiert, was ich hierfür brauchte (Kantendetektion, HCT usw.)

Vllt. auch ganz interessant: Handerkennung, siehe http://www.youtube.com/watch?v=mGpTE1RkEvQ . Grob läuft das so ab, dass zuerst die Kontur der Hand gefunden wird, dann die honvexe Hülle berechnet wird und die Finger die Eckpunkte der Hülle sind.
vertex.dreamfall.at | GitHub

Neue Antwort erstellen


Übersicht BlitzMax, BlitzMax NG Codearchiv & Module

Gehe zu:

Powered by phpBB © 2001 - 2006, phpBB Group