File draw.py added (mode: 100644) (index 0000000..2c95a23) |
|
1 |
|
import math |
|
2 |
|
import turtle |
|
3 |
|
import random |
|
4 |
|
|
|
5 |
|
# add the shape first then set the turtle shape |
|
6 |
|
nao = "nao.gif" |
|
7 |
|
screen = turtle.Screen() |
|
8 |
|
screen.addshape(nao) |
|
9 |
|
|
|
10 |
|
|
|
11 |
|
turtle.tracer(50000, delay=0) |
|
12 |
|
turtle.register_shape("dot", ((-3, -3), (-3, 3), (3, 3), (3, -3))) |
|
13 |
|
turtle.register_shape("tri", ((-3, -2), (0, 3), (3, -2), (0, 0))) |
|
14 |
|
turtle.speed(0) |
|
15 |
|
turtle.title("Particle Filter Example") |
|
16 |
|
|
|
17 |
|
UPDATE_EVERY = 0 |
|
18 |
|
DRAW_EVERY = 2 |
|
19 |
|
|
|
20 |
|
|
|
21 |
|
class Maze(object): |
|
22 |
|
def __init__(self, maze): |
|
23 |
|
self.maze = maze |
|
24 |
|
self.width = len(maze[0]) |
|
25 |
|
self.height = len(maze) |
|
26 |
|
turtle.setworldcoordinates(0, 0, self.width, self.height) |
|
27 |
|
self.blocks = [] |
|
28 |
|
self.update_cnt = 0 |
|
29 |
|
self.one_px = float(turtle.window_width()) / float(self.width) / 2 |
|
30 |
|
|
|
31 |
|
self.beacons = [] |
|
32 |
|
for y, line in enumerate(self.maze): |
|
33 |
|
for x, block in enumerate(line): |
|
34 |
|
if block: |
|
35 |
|
nb_y = self.height - y - 1 |
|
36 |
|
self.blocks.append((x, nb_y)) |
|
37 |
|
if block == 2: |
|
38 |
|
self.beacons.extend(((x, nb_y), (x+1, nb_y), (x, nb_y+1), (x+1, nb_y+1))) |
|
39 |
|
|
|
40 |
|
def draw(self): |
|
41 |
|
for x, y in self.blocks: |
|
42 |
|
turtle.up() |
|
43 |
|
turtle.setposition(x, y) |
|
44 |
|
turtle.down() |
|
45 |
|
turtle.setheading(90) |
|
46 |
|
turtle.begin_fill() |
|
47 |
|
for _ in range(0, 4): |
|
48 |
|
turtle.fd(1) |
|
49 |
|
turtle.right(90) |
|
50 |
|
turtle.end_fill() |
|
51 |
|
turtle.up() |
|
52 |
|
|
|
53 |
|
turtle.color("#00ffff") |
|
54 |
|
for x, y in self.beacons: |
|
55 |
|
turtle.setposition(x, y) |
|
56 |
|
turtle.dot() |
|
57 |
|
turtle.update() |
|
58 |
|
|
|
59 |
|
def weight_to_color(self, weight): |
|
60 |
|
return "#%02x00%02x" % (int(weight * 255), int((1 - weight) * 255)) |
|
61 |
|
|
|
62 |
|
def is_in(self, x, y): |
|
63 |
|
if x < 0 or y < 0 or x > self.width or y > self.height: |
|
64 |
|
return False |
|
65 |
|
return True |
|
66 |
|
|
|
67 |
|
def is_free(self, x, y): |
|
68 |
|
if not self.is_in(x, y): |
|
69 |
|
return False |
|
70 |
|
|
|
71 |
|
yy = self.height - int(y) - 1 |
|
72 |
|
xx = int(x) |
|
73 |
|
return self.maze[yy][xx] == 0 |
|
74 |
|
|
|
75 |
|
def show_mean(self, x, y, confident=False): |
|
76 |
|
if confident: |
|
77 |
|
turtle.color("#00AA00") |
|
78 |
|
else: |
|
79 |
|
turtle.color("#cccccc") |
|
80 |
|
turtle.setposition(x, y) |
|
81 |
|
turtle.shape("circle") |
|
82 |
|
turtle.stamp() |
|
83 |
|
|
|
84 |
|
def show_particles(self, particles): |
|
85 |
|
self.update_cnt += 1 |
|
86 |
|
if UPDATE_EVERY > 0 and self.update_cnt % UPDATE_EVERY != 1: |
|
87 |
|
return |
|
88 |
|
|
|
89 |
|
turtle.clearstamps() |
|
90 |
|
turtle.shape('tri') |
|
91 |
|
|
|
92 |
|
draw_cnt = 0 |
|
93 |
|
px = {} |
|
94 |
|
for p in particles: |
|
95 |
|
draw_cnt += 1 |
|
96 |
|
if DRAW_EVERY == 0 or draw_cnt % DRAW_EVERY == 1: |
|
97 |
|
# Keep track of which positions already have something |
|
98 |
|
# drawn to speed up display rendering |
|
99 |
|
scaled_x = int(p.x * self.one_px) |
|
100 |
|
scaled_y = int(p.y * self.one_px) |
|
101 |
|
scaled_xy = scaled_x * 10000 + scaled_y |
|
102 |
|
if not scaled_xy in px: |
|
103 |
|
px[scaled_xy] = 1 |
|
104 |
|
turtle.setposition(*p.xy) |
|
105 |
|
turtle.setheading(90 - p.h) |
|
106 |
|
turtle.color(self.weight_to_color(p.w)) |
|
107 |
|
turtle.stamp() |
|
108 |
|
|
|
109 |
|
def show_robot(self, robot): |
|
110 |
|
turtle.color("green") |
|
111 |
|
turtle.shape(nao) |
|
112 |
|
turtle.setposition(*robot.xy) |
|
113 |
|
turtle.setheading(90 - robot.h) |
|
114 |
|
turtle.stamp() |
|
115 |
|
turtle.update() |
|
116 |
|
|
|
117 |
|
def random_place(self): |
|
118 |
|
x = random.uniform(0, self.width) |
|
119 |
|
y = random.uniform(0, self.height) |
|
120 |
|
return x, y |
|
121 |
|
|
|
122 |
|
def random_free_place(self): |
|
123 |
|
while True: |
|
124 |
|
x, y = self.random_place() |
|
125 |
|
if self.is_free(x, y): |
|
126 |
|
return x, y |
|
127 |
|
|
|
128 |
|
def distance(self, x1, y1, x2, y2): |
|
129 |
|
return math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2) |
|
130 |
|
|
|
131 |
|
def distance_to_nearest_beacon(self, x, y): |
|
132 |
|
d = 99999 |
|
133 |
|
for c_x, c_y in self.beacons: |
|
134 |
|
distance = self.distance(c_x, c_y, x, y) |
|
135 |
|
if distance < d: |
|
136 |
|
d = distance |
|
137 |
|
d_x, d_y = c_x, c_y |
|
138 |
|
|
|
139 |
|
return d |
File particle_filter.py added (mode: 100644) (index 0000000..2856bfb) |
|
1 |
|
from __future__ import absolute_import |
|
2 |
|
import random |
|
3 |
|
import math |
|
4 |
|
import bisect |
|
5 |
|
from draw import Maze |
|
6 |
|
|
|
7 |
|
# 0 - empty square |
|
8 |
|
# 1 - occupied square |
|
9 |
|
# 2 - occupied square with a beacon at each corner, detectable by the robot |
|
10 |
|
|
|
11 |
|
maze_data = ((1, 1, 0, 0, 2, 0, 0, 0, 0, 1), |
|
12 |
|
(1, 2, 0, 0, 1, 1, 0, 0, 0, 0), |
|
13 |
|
(0, 1, 1, 0, 0, 0, 0, 1, 0, 1), |
|
14 |
|
(0, 0, 0, 0, 1, 0, 0, 1, 1, 2), |
|
15 |
|
(1, 1, 0, 1, 1, 2, 0, 0, 1, 0), |
|
16 |
|
(1, 1, 1, 0, 1, 1, 1, 0, 2, 0), |
|
17 |
|
(2, 0, 0, 0, 0, 0, 0, 0, 0, 0), |
|
18 |
|
(1, 2, 0, 1, 1, 1, 1, 0, 0, 0), |
|
19 |
|
(0, 0, 0, 0, 1, 0, 0, 0, 1, 0), |
|
20 |
|
(0, 0, 1, 0, 0, 2, 1, 1, 1, 0)) |
|
21 |
|
|
|
22 |
|
PARTICLE_COUNT = 2000 # Total number of particles |
|
23 |
|
|
|
24 |
|
ROBOT_HAS_COMPASS = True |
|
25 |
|
# ------------------------------------------------------------------------ |
|
26 |
|
# Some utility functions |
|
27 |
|
|
|
28 |
|
|
|
29 |
|
def add_noise(level, *coords): |
|
30 |
|
return [x + random.uniform(-level, level) for x in coords] |
|
31 |
|
|
|
32 |
|
|
|
33 |
|
def add_little_noise(*coords): |
|
34 |
|
return add_noise(0.02, *coords) |
|
35 |
|
|
|
36 |
|
|
|
37 |
|
def add_some_noise(*coords): |
|
38 |
|
return add_noise(0.1, *coords) |
|
39 |
|
|
|
40 |
|
# This is just a gaussian kernel I pulled out of my hat, to transform |
|
41 |
|
# values near to robbie's measurement => 1, further away => 0 |
|
42 |
|
sigma2 = 0.9 ** 2 |
|
43 |
|
|
|
44 |
|
|
|
45 |
|
def w_gauss(a, b): |
|
46 |
|
error = a - b |
|
47 |
|
g = math.e ** -(error ** 2 / (2 * sigma2)) |
|
48 |
|
return g |
|
49 |
|
# ------------------------------------------------------------------------ |
|
50 |
|
|
|
51 |
|
|
|
52 |
|
def compute_mean_point(particles): |
|
53 |
|
""" |
|
54 |
|
Compute the mean for all particles that have a reasonably good weight. |
|
55 |
|
This is not part of the particle filter algorithm but rather an |
|
56 |
|
addition to show the "best belief" for current position. |
|
57 |
|
""" |
|
58 |
|
|
|
59 |
|
m_x, m_y, m_count = 0, 0, 0 |
|
60 |
|
for p in particles: |
|
61 |
|
m_count += p.w |
|
62 |
|
m_x += p.x * p.w |
|
63 |
|
m_y += p.y * p.w |
|
64 |
|
|
|
65 |
|
if m_count == 0: |
|
66 |
|
return -1, -1, False |
|
67 |
|
|
|
68 |
|
m_x /= m_count |
|
69 |
|
m_y /= m_count |
|
70 |
|
|
|
71 |
|
# Now compute how good that mean is -- check how many particles |
|
72 |
|
# actually are in the immediate vicinity |
|
73 |
|
m_count = 0 |
|
74 |
|
for p in particles: |
|
75 |
|
if world.distance(p.x, p.y, m_x, m_y) < 1: |
|
76 |
|
m_count += 1 |
|
77 |
|
|
|
78 |
|
return m_x, m_y, m_count > PARTICLE_COUNT * 0.95 |
|
79 |
|
# ------------------------------------------------------------------------ |
|
80 |
|
|
|
81 |
|
|
|
82 |
|
class Particle(object): |
|
83 |
|
def __init__(self, x, y, heading=None, w=1, noisy=False): |
|
84 |
|
if heading is None: |
|
85 |
|
heading = random.uniform(0, 360) |
|
86 |
|
if noisy: |
|
87 |
|
x, y, heading = add_some_noise(x, y, heading) |
|
88 |
|
|
|
89 |
|
self.x = x # x-Position |
|
90 |
|
self.y = y # y-Position |
|
91 |
|
self.h = heading # Ausrichtung |
|
92 |
|
self.w = w # Gewicht |
|
93 |
|
|
|
94 |
|
def __repr__(self): |
|
95 |
|
return "(%f, %f, w=%f)" % (self.x, self.y, self.w) |
|
96 |
|
|
|
97 |
|
@property |
|
98 |
|
def xy(self): |
|
99 |
|
return self.x, self.y |
|
100 |
|
|
|
101 |
|
@property |
|
102 |
|
def xyh(self): |
|
103 |
|
return self.x, self.y, self.h |
|
104 |
|
|
|
105 |
|
@classmethod |
|
106 |
|
def create_random(cls, count, maze): |
|
107 |
|
return [cls(*maze.random_free_place()) for _ in range(0, count)] |
|
108 |
|
|
|
109 |
|
def read_sensor(self, maze): |
|
110 |
|
""" |
|
111 |
|
Find distance to nearest beacon. |
|
112 |
|
""" |
|
113 |
|
return maze.distance_to_nearest_beacon(*self.xy) |
|
114 |
|
|
|
115 |
|
def advance_by(self, speed, checker=None, noisy=False): |
|
116 |
|
h = self.h |
|
117 |
|
if noisy: |
|
118 |
|
speed, h = add_little_noise(speed, h) |
|
119 |
|
h += random.uniform(-3, 3) # needs more noise to disperse better |
|
120 |
|
r = math.radians(h) |
|
121 |
|
dx = math.sin(r) * speed |
|
122 |
|
dy = math.cos(r) * speed |
|
123 |
|
if checker is None or checker(self, dx, dy): |
|
124 |
|
self.move_by(dx, dy) |
|
125 |
|
return True |
|
126 |
|
return False |
|
127 |
|
|
|
128 |
|
def move_by(self, x, y): |
|
129 |
|
self.x += x |
|
130 |
|
self.y += y |
|
131 |
|
# ------------------------------------------------------------------------ |
|
132 |
|
|
|
133 |
|
|
|
134 |
|
class Robot(Particle): |
|
135 |
|
speed = 0.2 |
|
136 |
|
|
|
137 |
|
def __init__(self, maze): |
|
138 |
|
super(Robot, self).__init__(*maze.random_free_place(), heading=90) |
|
139 |
|
self.chose_random_direction() |
|
140 |
|
self.step_count = 0 |
|
141 |
|
|
|
142 |
|
def chose_random_direction(self): |
|
143 |
|
heading = random.uniform(0, 360) |
|
144 |
|
self.h = heading |
|
145 |
|
|
|
146 |
|
def read_sensor(self, maze): |
|
147 |
|
""" |
|
148 |
|
Poor robot, it's sensors are noisy and pretty strange, |
|
149 |
|
it only can measure the distance to the nearest beacon(!) |
|
150 |
|
and is not very accurate at that too! |
|
151 |
|
""" |
|
152 |
|
return add_little_noise(super(Robot, self).read_sensor(maze))[0] |
|
153 |
|
|
|
154 |
|
def move(self, maze): |
|
155 |
|
""" |
|
156 |
|
Move the robot. Note that the movement is stochastic too. |
|
157 |
|
""" |
|
158 |
|
while True: |
|
159 |
|
self.step_count += 1 |
|
160 |
|
if self.advance_by(self.speed, noisy=True, checker=lambda r, dx, dy: maze.is_free(r.x+dx, r.y+dy)): |
|
161 |
|
break |
|
162 |
|
# Bumped into something or too long in same direction, |
|
163 |
|
# chose random new direction |
|
164 |
|
self.chose_random_direction() |
|
165 |
|
|
|
166 |
|
# ------------------------------------------------------------------------ |
|
167 |
|
|
|
168 |
|
world = Maze(maze_data) |
|
169 |
|
world.draw() |
|
170 |
|
|
|
171 |
|
# initial distribution assigns each particle an equal probability |
|
172 |
|
particles = Particle.create_random(PARTICLE_COUNT, world) |
|
173 |
|
nao = Robot(world) |
|
174 |
|
|
|
175 |
|
while True: |
|
176 |
|
# Read Robot's sensor |
|
177 |
|
r_d = nao.read_sensor(world) |
|
178 |
|
|
|
179 |
|
# TODO: Update particle weight according to how good every particle matches |
|
180 |
|
# sensorupdate() |
|
181 |
|
|
|
182 |
|
# ---------- Show current state ---------- |
|
183 |
|
world.show_particles(particles) |
|
184 |
|
# world.show_mean(m_x, m_y, m_confident) |
|
185 |
|
world.show_robot(nao) |
|
186 |
|
|
|
187 |
|
# ---------- Shuffle particles ---------- |
|
188 |
|
new_particles = [] |
|
189 |
|
|
|
190 |
|
# TODO: Normalise weights |
|
191 |
|
# normalize() # fuer Resampling |
|
192 |
|
|
|
193 |
|
# TODO: update Samples |
|
194 |
|
# resample() |
|
195 |
|
|
|
196 |
|
# ---------- Move things ---------- |
|
197 |
|
old_heading = nao.h |
|
198 |
|
nao.move(world) |
|
199 |
|
d_h = nao.h - old_heading |
|
200 |
|
|
|
201 |
|
# Move particles according to my belief of movement |
|
202 |
|
for p in particles: |
|
203 |
|
p.h += d_h # in case robot changed heading, swirl particle heading too |
|
204 |
|
p.advance_by(nao.speed) |