Showing posts with label Computational geometry. Show all posts
Showing posts with label Computational geometry. Show all posts

Thursday, 16 July 2009

Implementando A* pathfinding en Delphi

Como ya sabéis, me encantan los retos y ejercicios de geometría computacional. Pues bien, aquí os traigo una implementación en Delphi de A* pathfinding, que consiste en encontrar y dibujar la ruta más corta entre 2 puntos saltando obstáculos. Para la resolución del pathfinding utilizaremos un algoritmo A* (A star) que es un algoritmo de búsqueda sobre grafos, intentando ir de un nodo principal a uno final con el coste mínimo. Para mi aplicación utilizaré un algoritmo creado por William Cairns donde he hecho pequeñas modificaciones y mejoras para adaptarlo a mis necesidades. Éste algoritmo se utiliza mucho en los videojuegos. Si alguna vez habéis podido leer algún libro sobre videojuegos (os lo recomiendo sobre todo por las estructuras de datos, OpenGL, etc.) nos muestran la manera que tienen para resolver que un jugador del tipo Starcraft , se mueva de un sitio a otro indicado salvando todos los obstáculos.
En la siguiente imagen, sacada de "Programación de videojuegos con SDL", podemos ver un arbol con posibles caminos dentro del mapa del juego:

En la web de policyAlmanac de U.S podemos encontrar un tutorial -> A* pathfinding para principiantes. El cuál nos muestra unas imágenes muy interesantes que me sirven para mi explicación:

Si nos fijamos en la imagen inicial:

Tenemos que ir del punto verde al punto rojo con el mínimo camino posible. Pero si nos fijamos en la imagen podemos ver que tenemos 2 caminos principales posibles, para éso hay que tener en cuenta la distancia Manhattan de manera que calculamos el número total de cuadros movidos horizontalmente y verticalmente para alcanzar el cuadrado destino desde el cuadro actual, sin hacer uso de movimientos diagonales. Al aplicar los diferentes algoritmos, obtenemos el diagrama siguiente con el cálculo realizado y el camino más corto:


Aplicación Thundax A Star PathFinding:

Aquí os dejo mi aplicación, la podéis descargar desde aquí. Por ejemplo, si creamos el ejemplo anterior, obtenemos algo parecido a:

Ahora, podemos poner los puntos donde queramos y los obstáculos que queramos y buscar el camino:


El código fuente de la unidad Astar.pas lo podéis encontrar aquí:




// originally written by William Cairns - http://www.cairnsgames.co.za
// http://www.pascalgamedevelopment.com/forums/profile.php?mode=viewprofile&u=65
// Enchanchements, additional code by Jernej L.
// http://www.gtatools.com
// please note that the path returned is REVERSED.
// Modified by Jordi Coll

unit Astar;

interface

uses
windows, dialogs, sysutils;

type
AstarRec = packed record
point: Tpoint;
weight: integer;
end;

PInspectBlock = function(X, Y, Fx, Fy: integer): integer;

var
Searching, Found: Boolean;
Astack: array of AstarRec;
Source, Goal: Tpoint;
freedom: integer;

CanGo: PInspectBlock;
GRID: array of array of integer;
GridDimensions: Tpoint;
maxval: integer;
patherror: boolean;
Path: array of Tpoint;
closestpoint: AstarRec;
IsClosest: boolean;

Offsets: array[0..7] of
record
DX, DY: Integer;
Cost: Integer;
end =
((DX: 0; DY: - 1; Cost: 10), //90° neighbour cubes
(DX: - 1; DY: 0; Cost: 10),
(DX: + 1; DY: 0; Cost: 10),
(DX: 0; DY: + 1; Cost: 10),
(DX: - 1; DY: - 1; Cost: 14), //45° diagonals
(DX: + 1; DY: - 1; Cost: 14),
(DX: - 1; DY: + 1; Cost: 14),
(DX: + 1; DY: + 1; Cost: 14));

procedure FindPath(const src, dest, Gridsize: Tpoint; const diagonals, pleasefallback: boolean; const grabcallback: PInspectBlock);

implementation

procedure
InspectBlock(X, Y: Integer);
var
I: Integer;
W: Integer;
AX, AY, AW, ABV: Integer;
begin
if (x = Source.x) and (y = Source.y) then
W := 0
else
W := GRID[x, y];
for I := 0 to freedom do
begin
AX := X + Offsets[I].DX;
AY := Y + Offsets[I].DY;
if (AX = Goal.X) and (AY = Goal.Y) then
begin
Found := True;
Exit;
end;

if (AX >= 0) and
(AY >= 0) and
(AX <= GridDimensions.x - 1) and
(AY <= GridDimensions.y - 1) = false then
continue;

if (ax = Source.x) and (ay = Source.y) then
continue;
if GRID[AX, AY] <> 0 then
continue;

ABV := CanGo(AX, AY, X, Y);
AW := W + Offsets[I].Cost + ABV;

if (ABV <> -1) then
begin
if ABV = 0 then
begin
Found := false;
Searching := false;
Exit;
end;

GRID[AX, AY] := AW;
if aw > maxval then
maxval := aw;

if (ABS(Goal.X - AX) + ABS(Goal.Y - AY)) < closestpoint.weight then
begin
closestpoint.point.x := ax;
closestpoint.point.y := ay;
closestpoint.weight := (ABS(Goal.X - AX) + ABS(Goal.Y - AY));
end;

setlength(Astack, length(Astack) + 1);
with Astack[length(Astack) - 1] do
begin
point.x := ax;
point.y := ay;
weight := aw;
end;

end;
end;
end;

procedure Step;
var
I, LC, X, Y: Integer;
begin
if Found then
Exit;
if not Searching then
begin
InspectBlock(Source.X, Source.Y);
Searching := True;
end
else
begin
if high(astack) = -1 then
begin patherror := true;
exit;
end;
LC := 0;
for i := 0 to length(Astack) - 1 do
begin
if astack[i].weight < astack[LC].weight then
LC := i;
end;
X := Astack[LC].point.x;
Y := Astack[LC].point.y;
move(astack[LC + 1], astack[LC], (length(Astack) - 1 - LC) * sizeof(AstarRec));
setlength(Astack, length(Astack) - 1);
InspectBlock(X, Y);
end;
end;

procedure CalcBestPath;
var
lowest: Tpoint;
lowvalue: integer;
finished: boolean;
function findbestprev(pt: Tpoint): Tpoint;
var
i, ax, ay: integer;
begin
for I := 0 to freedom do
begin
AX := pt.X + Offsets[I].DX;
AY := pt.Y + Offsets[I].DY;
if (AX < 0) or
(AY < 0) or
(AX > GridDimensions.x - 1) or
(AY > GridDimensions.y - 1) then
continue;
if (AX = source.X) and (AY = source.Y) then
begin
finished := True;
Exit;
end;
if GRID[AX, AY] > 0 then
begin
if GRID[AX, AY] < lowvalue then
begin
lowvalue := GRID[AX, AY];
lowest.x := ax;
lowest.y := ay;
end;

end;
end;
end;
begin
if Found = false then
exit;
finished := false;
lowvalue := maxint;
lowest := Goal;
repeat
findbestprev(lowest);
if not finished then
begin
setlength(Path, length(path) + 1);
Path[length(path) - 1] := lowest;
end;
until (finished);
end;

procedure LookForPath;
begin
repeat step;
until (found = true) or (patherror = true);
end;

procedure FindPath(const src, dest, Gridsize: Tpoint; const diagonals, pleasefallback: boolean; const grabcallback: PInspectBlock);
begin
Source := src;
Goal := dest;
freedom := 3;
if diagonals then
freedom := 7;

CanGo := grabcallback;
GridDimensions := Gridsize;
Searching := false;
Found := false;
patherror := false;
closestpoint.weight := maxint;
IsClosest := false;
setlength(Astack, 0);
setlength(Path, 0);
setlength(GRID, 0, 0);
setlength(GRID, gridsize.x, gridsize.y);
LookForPath;
if (patherror = true) and (pleasefallback = true) then
begin
Goal := closestpoint.point;
Searching := false;
Found := false;
patherror := false;
closestpoint.weight := maxint;
setlength(GRID, 0, 0);
setlength(GRID, gridsize.x, gridsize.y);
setlength(Path, length(path) + 1);
Path[length(path) - 1] := closestpoint.point;
LookForPath;
CalcBestPath;
IsClosest := true;
end
else if patherror = false then
CalcBestPath;
end;

end.






Una parte de la implementación del funcionamiento y la generación del dibujo con una TStringGrid lo podéis encontrar aquí:




function blocktester(X, Y, Fx, Fy: integer): integer;
begin
result := -1;
with Form1 do
begin
if (board.Cells[X, Y] = '') or (board.Cells[X, Y] = 'A') then
result := ((ABS(finalPos.x - X) + ABS(finalPos.y - Y)) * 3);
end;
end;

procedure TForm1.SearchClick(Sender: TObject);
var
i: integer;
begin
Astar.findpath(cellPos, finalPos, point(board.colcount, board.rowcount), true, true, @blocktester);
for i := 0 to high(astar.path) do
board.Cells[astar.path[i].x, astar.path[i].y] := '·';
if astar.IsClosest then
Statusbar1.Panels[1].text := 'Close path.';
if ((high(astar.path) = -1) and (astar.Found)) then
caption := 'immediatelly path.'
else
Statusbar1.Panels[1].text := 'Direct Path';
if not astar.Found then
Statusbar1.Panels[1].text := 'There is no path !';
end;

procedure TForm1.boardDrawCell(Sender: TObject; ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
begin
with Sender as TDrawGrid do
begin
if board.cells[acol, arow] = '1' then
Canvas.Brush.Color := shape1.Brush.color
else if board.cells[acol, arow] = '2' then
Canvas.Brush.Color := shape2.Brush.color
else if board.cells[acol, arow] = '-' then
Canvas.Brush.Color := shape3.Brush.color
else if board.cells[acol, arow] = '·' then
Canvas.Brush.Color := clyellow
else if (acol = mousePos.x) and (arow = mousePos.y) then
Canvas.Brush.Color := clwhite;
Canvas.FillRect(Rect);
Canvas.TextOut(rect.left + 12, rect.top + 8, board.cells[acol, arow]);
end;
if (state = [gdSelected]) then
with TStringGrid(Sender), Canvas do
begin
Brush.Color := clwhite;
FillRect(Rect);
TextRect(Rect, Rect.Left + 2, Rect.Top + 2, Cells[aCol, aRow]);
end;
end;


  • Enlaces de interés:
PathFinding Applet 1.

PathFinding Applet 2.



Saturday, 20 June 2009

Implementando el Smallest Enclosing Circle con Delphi

En este artículo os muestro mi implementación para el problema del Smallest Enclosing Circle o minimum Enclosing Circle. Mi solución está basada en la utilización del algoritmo Elzinga-Hearn (1972) que es el de los más rápidos con un coste O(n^2), aunque luego en 1978 Shamos publicó un algoritmo con coste O(n logn). Más tarde en el año 1983 Nimrod Megiddo mostró un algoritmo con coste O(n) utilizando las técnicas prune-and-search Con éste, se mostró uno de los algoritmos más bonitos en geometría Computacional.
Podemos encontrar la fuente de la información en el siguiente enlace: minimax.pdf de D. Jack Elzinga, Donald W. Hearn, "Geometrical Solutions for some minimax location problems," Transportation Science, 6, (1972), pp 379 - 394. El D. Jack Elzinga publicó varios trabajos sobre diversos algoritmos y podemos encontrar más información sobre ellos en la Universidad de Florida. Por la red podemos encontrar muy buenas explicaciones de como funciona su algoritmo y applets en java demostrando su cálculo:

El algoritmo como se describe el los diferentes enlaces, consiste en conseguir el mínimo circulo 2D que engloba todos los puntos del plano mostrados. Cabe decir, que éste algoritmo se utiliza en muchos campos: estadística, Sistemas de información geográfica, militares, etc, Además su implementación es bastante sencilla. En éste artículo os dejo mi ejemplo en Delphi, utilizando diferentes objetos: Círculos, puntos y líneas puedo realizar los diferentes cálculos para conseguir el cálculo del smallest enclosing circle.

Mi aplicación llamada ThundaxEnclosingCircle permite la entrada de puntos sobre un TImage o se pueden generar puntos aleatoriamente. Luego se realiza el cálculo y se muestra el circulo que engloba todos los puntos.

  • Implementando la solución
Para los tipos primitivos crearé los objetos TPointR, TLine y TCircle y como lista de puntos la TPointRList que la expliqué ayer en el post de aplicar la herencia con el TObjectList. La definición de los objetos es la siguiente:




uses
Types, Contnrs, Lib_Math, SysUtils, Math;

type
TPointR = class(TObject)
X: double;
Y: double;
constructor Point(x, y: double); overload;
constructor Point(point: TPointR); overload;
function ToString(): string;
end;

TLine = class(TObject)
p1, p2: TPointR;
end;

TCircle = class(TObject)
Center: TPointR;
radius: double;
constructor Circle(); overload;
constructor Circle(circ: TCircle); overload;
constructor Circle(center: TPointR; radius: double); overload;
procedure SetParam(x, y, radius : double);
end;

{ TCircle }

constructor TCircle.Circle;
begin
Self.Center := TPointR.Point(0, 0);
Self.radius := 0.0;
end;

constructor TCircle.Circle(circ: TCircle);
begin
Self.Center := circ.Center;
Self.radius := circ.radius;
end;

constructor TCircle.Circle(center: TPointR; radius: double);
begin
Self.Center := center;
Self.radius := radius;
end;

procedure TCircle.SetParam(x, y, radius: double);
begin
Self.center.X := x;
Self.center.y := y;
self.radius := radius;
end;

{ TPointR }

constructor TPointR.Point(x, y: double);
begin
Self.x := x;
Self.y := y;
end;

constructor TPointR.Point(point: TPointR);
begin
Self.x := point.x;
Self.Y := point.y;
end;

function TPointR.ToString: string;
begin
result := FloatToStr(x) + ' - ' + FloatToStr(y);
end;




Luego, necesito una serie de funciones estándar para realizar cálculos geométricos, como la distancia entre 2 puntos, obtener el centro de 3 puntos, obtener un círculo a partir de 3 puntos, obtener el ángulo entre puntos, etc. Todas éstas están implementadas aquí:




function distancia(p1, p2: TpointR): double;
procedure swap(var p1, p2: TPointR);
function ObtenerCentro(p1, p2, p3: TPointR; var centro: TPointR): boolean;
function obtenerCirculo(p1, p2, p3: TPointR; var circulo: Tcircle): boolean;
function ObtenerAngulo(p1, p2, p3: TPointR): double;
function mismoLado(linea: TLine; p1, p2: TPointR): double;

function distancia(p1, p2: TPointR): double;
begin
result := sqrt(sqr(p1.x - p2.x) + sqr(p1.y - p2.y));
end;

procedure swap(var p1, p2: TPointR);
var
temp: TPointR;
begin
temp := p1;
p1 := p2;
p2 := temp;
end;

function ObtenerCentro(p1, p2, p3: TPointR; var centro: TPointR): boolean;
var
x, y: double;
modulo1, modulo2: double;
encontrado : boolean;
begin
encontrado := true;
modulo1 := 0; modulo2 := 0;

if (Compare(p1.x,p2.x,'=') or Compare(p1.y,p2.y,'=')) then swap(p1, p3);
if (Compare(p2.x,p3.x,'=')) then swap(p1, p2);

if p1.x <> p2.x then modulo1 := (p2.y - p1.y) / (p2.x - p1.x)
else encontrado := false;

if p2.x <> p3.x then modulo2 := (p3.y - p2.y) / (p3.x - p2.x)
else encontrado := false;

if Compare(modulo1,0,'=') and Compare(modulo2,0,'=') then encontrado := false;
if Compare(modulo1,modulo2,'=') then encontrado := false;

if encontrado then
begin
x := (modulo1 * modulo2 * (p1.y - p3.y) + modulo2 * (p1.x + p2.x) - modulo1 * (p2.x + p3.x)) / (2 * (modulo2 - modulo1));
if modulo1 <> 0 then y := -(x - (p1.x + p2.x) / 2) / modulo1 + (p1.y + p2.y) / 2
else y := -(x - (p2.x + p3.x) / 2) / modulo2 + (p2.y + p3.y) / 2;
centro.x := x;
centro.y := y;
end;
result := encontrado;
end;

function obtenerCirculo(p1, p2, p3: TPointR; var circulo: Tcircle): boolean;
var
encontrado : boolean;
begin
encontrado := ObtenerCentro(p1, p2, p3, circulo.center);
circulo.radius := 0;
if encontrado then
circulo.radius := sqrt(sqr(p1.x - circulo.center.x) + sqr(p1.y - circulo.center.y));
result := encontrado;
end;

function ObtenerAngulo(p1, p2, p3: TPointR): double;
var
distancia1, distancia2 : double;
begin
distancia1 := distancia(p1,p2);
distancia2 := distancia(p2,p3);
result := arccos(ABS(((p1.x - p2.x) * (p2.x - p3.x) + (p1.y - p2.y) * (p2.y - p3.y)) /
(distancia1 * distancia2)));
end;

function mismoLado(linea: TLine; p1, p2: TPointR): double;
var
dx, dy, dx1, dy1, dx2, dy2: double;
begin
dx := linea.p2.x - linea.p1.x;
dy := linea.p2.y - linea.p1.y;
dx1 := p1.x - linea.p1.x;
dy1 := p1.y - linea.p1.y;
dx2 := p2.x - linea.p2.x;
dy2 := p2.y - linea.p2.y;
result := (dx * dy1 - dy * dx1) * (dx * dy2 - dy * dx2);
end;




Por último solo necesito el algoritmo de Elzina-Hearn, que es el que tengo implementado aquí:




procedure EnclosingCircle(Sender: TObject);
function EsAnguloAgudo(p1, p2, p3: integer; var q1, q2: integer): boolean;
var
angle: double;
EsAgudo : boolean;
begin
EsAgudo := false;
angle := ObtenerAngulo(p[p2], p[p1], p[p3]);
if Compare(angle , pi/2, '<') then
begin
angle := ObtenerAngulo(p[p1], p[p2], p[p3]);
if Compare(angle , pi/2, '<') then
begin
angle := ObtenerAngulo(p[p1], p[p3], p[p2]);
if Compare(angle , pi/2, '<') then
begin
q1 := 0; q2 := 0; EsAgudo := true;
end
else
begin
q1 := p1; q2 := p2;
end;
end
else
begin
q1 := p1; q2 := p3;
end;
end
else
begin
q1 := p2; q2 := p3;
end;
result := EsAgudo;
end;

function ContieneTodosLosPuntos(circulo: TCircle): integer;
var
i, punto: integer;
distanciaPunto: double;
begin
punto := -1;
for i := 0 to p.count - 1 do
begin
distanciaPunto := distancia(p[i], circulo.center);
if Compare(distanciaPunto, circulo.radius, '>') then
begin
punto := i; break;
end;
end;
result := punto;
end;

var
Encontrado: boolean;
distanciaMaxima, distPuntos: double;
CentinelaI, CentinelaJ, P1, Q, i, j, PuntoPos, Seguridad, e1, e2: integer;
circle: TCircle;
linea: TLine;
begin
distanciaMaxima := 0;
CentinelaI := 0;
CentinelaJ := 0;
for i := 0 to p.count - 2 do
for j := i + 1 to p.count - 1 do
begin
distPuntos := distancia(p[i], p[j]);
if Compare(distPuntos, distanciaMaxima,'>') then
begin
CentinelaI := i;
CentinelaJ := j;
distanciaMaxima := distPuntos;
end;
end;

circle := TCircle.Circle();
linea := TLine.create();

Encontrado := false;
while not Encontrado do
begin
circle.center.x := (p[CentinelaI].x + p[CentinelaJ].x) / 2;
circle.center.y := (p[CentinelaI].y + p[CentinelaJ].y) / 2;
circle.radius := distancia(p[CentinelaI], Circle.center);
PuntoPos := ContieneTodosLosPuntos(circle);
if PuntoPos >= 0 then
begin
Seguridad := 0;
while (not Encontrado) and (Seguridad < 1000) do
begin
inc(Seguridad);
if EsAnguloAgudo(CentinelaI, CentinelaJ, PuntoPos, e1, e2) then
begin
obtenerCirculo(p[CentinelaI], p[CentinelaJ], p[PuntoPos], circle);
P1 := ContieneTodosLosPuntos(circle);
if P1 >= 0 then
begin
distanciaMaxima := distancia(p[P1], p[CentinelaI]);
Q := CentinelaI;
distPuntos := distancia(p[P1], p[CentinelaJ]);
if distPuntos > distanciaMaxima then
begin
Q := CentinelaJ;
distanciaMaxima := distPuntos;
end;
if distancia(p[P1], p[PuntoPos]) > distanciaMaxima then
Q := PuntoPos;
linea.p1 := circle.center;
linea.p2 := TPointR(p[Q]);
if (Q <> CentinelaI) and (mismoLado(linea, p[P1], p[CentinelaI]) < 0) then
begin CentinelaJ := P1;
PuntoPos := q;
end
else if (Q <> CentinelaJ) and (mismoLado(linea, p[P1], p[CentinelaJ]) < 0) then
begin CentinelaI := P1;
PuntoPos := q;
end
else
begin CentinelaI := P1;
CentinelaJ := q;
end;
end
else
Encontrado := true;
end
else
begin CentinelaI := e1;
CentinelaJ := e2;
end;
end;
if Seguridad >= 1000 then
begin
showmessage('Bucles Infinitos (Sin Solución)');
exit;
end;
end
else
Encontrado := true;
end;
Edit2.text := 'Center: x= ' + FloatToStr(circle.center.x) +
' y= ' + FloatToStr(circle.center.y) +
' Radius= ' + FloatToStr(circle.radius);
end;




Una vez pasamos el algoritmo sobre los puntos, éste nos entrega el centro del círculo mínimo y su radio. Espero que os sirva de ayuda.

  • Enlaces de Interés:
Circle Covering Points
Geometrical Solution for some minimax location problem
Geometría Computacional - minimum Enclosing
Minimum Enclosing Solution in Ruby
Minimum Enclosing Solution in Matlab
Applet MinCircle Java