├── misc
├── demo.gif
└── apf_diagram.png
├── classes
├── __pycache__
│ ├── objects.cpython-37.pyc
│ └── positional.cpython-37.pyc
├── positional.py
└── objects.py
├── README.md
└── main.py
/misc/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccvector/aritificial-potential-field/HEAD/misc/demo.gif
--------------------------------------------------------------------------------
/misc/apf_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccvector/aritificial-potential-field/HEAD/misc/apf_diagram.png
--------------------------------------------------------------------------------
/classes/__pycache__/objects.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccvector/aritificial-potential-field/HEAD/classes/__pycache__/objects.cpython-37.pyc
--------------------------------------------------------------------------------
/classes/__pycache__/positional.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nccvector/aritificial-potential-field/HEAD/classes/__pycache__/positional.cpython-37.pyc
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Artificial Potential Field for path planning
2 |
3 | ### Algorithm in action:
4 |
5 | - Blue => Agent trajectory
6 | - Red => Obstacle
7 | - Green => Goal
8 |
9 |
10 |
11 |
12 | ### What is artificial potential field?
13 |
14 |
15 |
16 | At each instance the surrounding (reachable) coordinates are evaluated using a cost function. The cost function calculates the attraction/repulsion force of all the objects in the scenes (repulsion for obstacles and attraction for goal) and assigns a score value to each cell.
17 | This algorithm does not garauntee optimal shortest path as it follows local gradients. We can use random particles with pobablistic cell selection to get better results (computationally expensive but sometimes may give optimal results).
--------------------------------------------------------------------------------
/classes/positional.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | # Position class for position based calculations
4 | class Position:
5 | """
6 | Creates a Position object
7 |
8 | Parameters
9 | ----------
10 | x : double
11 | x coordinate
12 | y : double
13 | y coordinate
14 |
15 | """
16 |
17 | def __init__(self, x, y):
18 |
19 | self.x = x
20 | self.y = y
21 |
22 | # General type function
23 | def calculate_distance(self, other):
24 | """
25 | Calculates distance between current position and the position passed as parameter
26 | It can also be usedd as static function by specifying class name and giving two parameters
27 |
28 | Parameters
29 | ----------
30 | other : Position
31 | Position to check distance against
32 |
33 | Returns
34 | -------
35 | double
36 | Distance value
37 |
38 | """
39 |
40 | return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
41 |
42 | # General type function
43 | def calculate_distance_squared(self, other):
44 | """
45 | Calculates squared distance between current position and the position passed as parameter
46 | It can also be usedd as static function by specifying class name and giving two parameters
47 |
48 | Parameters
49 | ----------
50 | other : Position
51 | Position to check squared distance against
52 |
53 | Returns
54 | -------
55 | double
56 | Distance value squared
57 |
58 | """
59 |
60 | return (self.x - other.x)**2 + (self.y - other.y)**2
61 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from cv2 import cv2
3 | import math
4 |
5 | from classes.positional import Position
6 | from classes.objects import *
7 |
8 | # Main
9 | if __name__ == '__main__':
10 | # Defining world dimensions
11 | world_size = (640, 480)
12 | # Initializing blank canvas with white color
13 | image = np.ones((world_size[1],world_size[0],3),dtype=np.uint8) * 255
14 |
15 | # Defining agent and goal
16 | agent = Agent(Position(50, 50), scan_radius=10, possible_moves=30)
17 | goal = Goal(Position(450, 450), sigma=math.sqrt(world_size[0]**2 + world_size[1]**2))
18 |
19 | # Defining obstacles in a list
20 | sigma_obstacles = 5
21 | obstacles = [Obstacle(Position(250, 180), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles),
22 | Obstacle(Position(250, 280), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles),
23 | Obstacle(Position(250, 380), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles),
24 | Obstacle(Position(350, 180), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles),
25 | Obstacle(Position(350, 280), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles),
26 | Obstacle(Position(350, 380), sigma=sigma_obstacles, draw_radius=4*sigma_obstacles)]
27 |
28 | # Drawing objects
29 | agent.draw(image)
30 | goal.draw(image)
31 | for obstacle in obstacles:
32 | obstacle.draw(image)
33 |
34 | # Displaying initial frame and wait for intial key press
35 | cv2.imshow('Output', image)
36 | cv2.waitKey(1000)
37 |
38 | while Position.calculate_distance(agent.position, goal.position) > 10:
39 | possible_moves = agent.get_possible_moves()
40 | min_value = math.inf
41 | best_move = possible_moves[0] # initializing best move with first move
42 | # Finding move with the least value
43 | for move in possible_moves:
44 | move_value = goal.get_attraction_force(move)
45 | for obstacle in obstacles:
46 | move_value += obstacle.get_repulsion_force(move)
47 |
48 | if move_value < min_value:
49 | min_value = move_value
50 | best_move = move
51 |
52 | # Setting best move as agent's next position
53 | agent.position = best_move
54 |
55 | '''
56 | As we are not clearing up the initial frame at every iteration
57 | so we do not need to draw static objects again and again
58 |
59 | '''
60 | agent.draw(image)
61 |
62 | # Displaying updated frame
63 | cv2.imshow('Output', image)
64 | cv2.waitKey(20)
65 |
66 | # Hold on last frame
67 | cv2.waitKey(0)
68 |
69 |
--------------------------------------------------------------------------------
/classes/objects.py:
--------------------------------------------------------------------------------
1 | import math
2 | from cv2 import cv2
3 | from classes.positional import Position
4 |
5 | # Agent class for Agent attributes
6 | class Agent:
7 | """
8 | Creates an Agent object
9 |
10 | Parameters
11 | ----------
12 | position : Position
13 | Position of agent in world
14 | scan_radius : int, optional
15 | Step size of agent, by default 1
16 | possible_moves : int, optional
17 | Number of point generated around agent, by default 6
18 | draw_radius : int, optional
19 | Radius for visualization, by default 5
20 | draw_color : tuple, optional
21 | Color for visulization, by default (255,0,0)
22 |
23 | """
24 |
25 | def __init__(self, position, scan_radius=1, possible_moves=6, draw_radius=5, draw_color=(255,0,0)):
26 |
27 | # Property attributes
28 | self.position = position
29 | self._scan_radius = scan_radius
30 | self._possible_moves = possible_moves
31 |
32 | # Visual attributes
33 | self._draw_radius = draw_radius
34 | self._draw_color = draw_color
35 |
36 | def draw(self, image):
37 | cv2.circle(image, (int(self.position.x), int(self.position.y)),
38 | self._draw_radius, self._draw_color, -1) # Fill
39 |
40 | def get_possible_moves(self):
41 | """
42 | Makes a list of points around agent
43 |
44 | Returns
45 | -------
46 | list
47 | List of points around agent
48 |
49 | """
50 |
51 | angle_increment = (2*math.pi)/self._possible_moves # 2pi/n
52 | angle = -angle_increment # Going one step negative to start from zero
53 | possible_moves_list = []
54 | for _ in range(self._possible_moves):
55 | # Starting from angle 0
56 | angle += angle_increment
57 | possible_moves_list.append(Position(self._scan_radius * math.cos(angle) + self.position.x,
58 | self._scan_radius * math.sin(angle) + self.position.y))
59 |
60 | return possible_moves_list
61 |
62 | # Obstacle class for repulsion based objects
63 | class Obstacle:
64 | """
65 | Creates an Obstacle object
66 |
67 | Parameters
68 | ----------
69 | position : Position
70 | Position of Obstacle in the world
71 | mu : int, optional
72 | Peak of distribution, by default 1
73 | sigma : int, optional
74 | Spread of distribution, by default 1
75 | draw_radius : int, optional
76 | Radius for visualization, by default 5
77 | draw_color : tuple, optional
78 | Color for visualization, by default (0,0,255)
79 |
80 | """
81 |
82 | def __init__(self, position, mu=1, sigma=1, draw_radius=5, draw_color=(0,0,255)):
83 |
84 | # Property attributes
85 | self.position = position
86 | self._mu = mu
87 | self._sigma = sigma
88 |
89 | # Visual attributes
90 | self._draw_radius = draw_radius
91 | self._draw_color = draw_color
92 |
93 | def draw(self, image):
94 | cv2.circle(image, (int(self.position.x), int(self.position.y)),
95 | self._draw_radius, self._draw_color, -1) # Fill
96 |
97 | # Attribute type function
98 | def get_repulsion_force(self, position):
99 | """
100 | Repulsion force calculation function
101 |
102 | Parameters
103 | ----------
104 | position : Position
105 | Position of cell to check force at
106 |
107 | Returns
108 | -------
109 | double
110 | The value of repulsion at cell
111 |
112 | """
113 |
114 | # Implementing repulsion equation
115 | dist_value = (1/(self._sigma*math.sqrt(2*math.pi))) * math.exp(-(Position.calculate_distance_squared(self.position,position)/(2*self._sigma*self._sigma)))
116 | return dist_value
117 |
118 | # Goal class for Goal object
119 | class Goal:
120 | """
121 | Creates a Goal object
122 |
123 | Parameters
124 | ----------
125 | position : Position
126 | Position of Goal in the world
127 | mu : int, optional
128 | Peak of distribution, by default 1
129 | sigma : int, optional
130 | Spread of distribution, by default 1
131 | draw_radius : int, optional
132 | Radius for visualization, by default 5
133 | draw_color : tuple, optional
134 | Color for visualization, by default (0,255,0)
135 |
136 | """
137 |
138 | def __init__(self, position, mu=1, sigma=1, draw_radius=5, draw_color=(0,255,0)):
139 |
140 | # Property attributes
141 | self.position = position
142 | self._mu = mu
143 | self._sigma = sigma
144 |
145 | # Visual attributes
146 | self._draw_radius = draw_radius
147 | self._draw_color = draw_color
148 |
149 | def draw(self, image):
150 | cv2.circle(image, (int(self.position.x), int(self.position.y)),
151 | self._draw_radius, self._draw_color, -1) # Fill
152 |
153 | # Attribute type function
154 | def get_attraction_force(self, position):
155 | """
156 | Attraction force calculation function
157 |
158 | Parameters
159 | ----------
160 | position : Position
161 | Position of cell to check force at
162 |
163 | Returns
164 | -------
165 | double
166 | The value of attraction at cell
167 |
168 | """
169 |
170 | # Implementing attraction equation
171 | dist_value = -(1/(self._sigma*math.sqrt(2*math.pi))) * math.exp(-(Position.calculate_distance_squared(self.position,position)/(2*self._sigma*self._sigma)))
172 | return dist_value
173 |
--------------------------------------------------------------------------------