Displaying the shortest path along a random walk - python-3.x

I'm a CS teacher, and we wrote some Python code in class to have a turtle draw a random walk. Someone asked if we could draw a new turtle that will trace the route (from starting point to ending point) that will take the minimum path. Current code is below.
I suspect we'll have to start a list of coordinates and then create some kind of tree. I'm sure there's already a library to do this, just not sure.
import turtle
import random
import time
from math import sqrt
leo = turtle.Turtle()
leo.speed(0)
leo.color("green")
leo.dot(6) # mark the starting location
leo.color("black")
directions = ['N', 'S', 'E', 'W']
steps = 100
step_size = 10
start = time.time()
for i in range(steps):
heading = random.choice(directions)
if heading == 'N':
leo.setheading(90)
elif heading == 'S':
leo.setheading(270)
elif heading == 'E':
leo.setheading(0)
else:
leo.setheading(180)
leo.forward(step_size)
(x,y) = leo.position()
distance = sqrt( x**2 + y**2 )
end = time.time()
leo.color("red") # mark the end location
leo.dot(6)
leo.hideturtle()
# drawing the final distance: start to end
raph = turtle.Turtle()
raph.color("blue")
raph.goto(x,y)
raph.hideturtle()
print("Total time:", end - start)
print("Total steps:", steps)
print("Total distance:", distance / step_size)

The approach is to:
1- Generate a random walk and paint it on the canvas
2- use that random walk to generate a lattice (a graph of coordinates and their connections to their neighbors.
2- use the start and end points to search the lattice for the shortest path. Here, using Breadth First Search, but you could use other searches.
3- paint the shortest path on the canvas, over the lattice
Here is an example of a walk, with the shortest path superimposed upon it:
The code used to generate it:
(extended from the code you posted)
import random
import turtle
class Walker:
"""a random walker on a virtual lattice
defines the rules of taking a step in a direction
records the sequence of directions of the steps takem
"""
# four directions
DIRECTIONS = ('N', 'S', 'E', 'W')
ID = 1
def __init__(self):
self.name = f"{self.__class__.__name__} no.: {Walker.ID}"
Walker.ID += 1
self.sequence_of_steps = [] # a sequence of directions
def take_step(self):
direction = random.choice(Walker.DIRECTIONS)
self.sequence_of_steps.append(direction)
def __str__(self):
return f"{self.name}"
class RandomWalk:
"""manages Walkers take_step"""
def __init__(self, walker=Walker, numwalkers=1, numsteps=100):
self.numsteps = numsteps
self.numwalkers = numwalkers
self.walkers = [walker() for _ in range(numwalkers)]
print(f'walking {self.numwalkers} {walker} for {self.numsteps} steps')
self.do_walk()
def do_walk(self):
for step in range(self.numsteps):
for walker in self.walkers:
walker.take_step()
def __str__(self):
return '\n'.join(str(walker.sequence_of_steps)
for walker in self.walkers)
def paint_walker_path(walker):
"""paints the path of one walker on the canvas"""
headings = {'N': 90, 'S': 270, 'E': 0, 'W': 180}
unit_move = 10 # pixels
direction_path = walker.sequence_of_steps
t = turtle.Turtle()
t.speed('fastest')
t.color('green')
t.dot(size=10)
t = turtle.Turtle()
t.color('gray')
report = turtle.Turtle()
report.penup()
report.hideturtle()
report.goto(200, 200)
report.write("step: ", True, font=('courier', 18, 'normal'))
report.write('0', font=('courier', 18, 'normal'))
for idx, direction in enumerate(direction_path):
t.setheading(headings[direction])
t.forward(unit_move)
t.dot(size=4)
report.undo()
report.penup()
report.pendown()
report.write(f"{idx}", font=('courier', 18, 'normal'))
t.hideturtle()
t.color('red')
t.dot(6)
t.goto(0, 0)
def paint_path(direction_path):
headings = {'N': 90, 'S': 270, 'E': 0, 'W': 180}
unit_move = 10 # pixels
t = turtle.Turtle()
t.speed('fastest')
t.color('black')
t = turtle.Turtle()
t.color('black')
t.pensize(2)
for direction in direction_path:
t.setheading(headings[direction])
t.forward(unit_move)
t.dot(size=4)
t.hideturtle()
class Coordinate:
# offsets are (col, row) i/e (x, y)
OFFSETS = {'N': (0, 1), 'S': (0, -1), 'E': (1, 0), 'W': (-1, 0)}
COORD_TO_DIR = {(0, 1): 'N', (0, -1): 'S', (1, 0): 'E', (-1, 0): 'W'}
def __init__(self, col, row):
self.col = col
self.row = row
self.coord = (self.col, self.row)
def get_adjacent(self, direction):
"""returns a new Coordinate object adjacent to self
in the given direction
"""
d_col, d_row = Coordinate.OFFSETS[direction]
return Coordinate(self.col + d_col, self.row + d_row)
def get_direction(self, destination):
"""returns the direction to take in order to move
from self to destination"""
offcol = destination.col - self.col
offrow = destination.row - self.row
assert abs(offcol) <= 1 and abs(offrow) <= 1, "adjacent coordinates must be close by"
return Coordinate.COORD_TO_DIR[(offcol, offrow)]
def __hash__(self):
return hash(self.coord)
def __eq__(self, other):
return self.coord == other.coord
def __str__(self):
return f"Coordinate {self.coord}"
def __repr__(self):
return str(self)
ORIGIN = Coordinate(0, 0)
class Lattice:
def __init__(self):
self.adjacency = {}
def get_final_dest_and_merge_sequence_of_steps(self, sequence_of_steps,
origin=ORIGIN):
current_coordinate = origin
for direction in sequence_of_steps:
adjacent_coordinate = current_coordinate.get_adjacent(direction)
try:
self.adjacency[current_coordinate].add(adjacent_coordinate)
except KeyError:
self.adjacency[current_coordinate] = {adjacent_coordinate}
try:
self.adjacency[adjacent_coordinate].add(current_coordinate)
except KeyError:
self.adjacency[adjacent_coordinate] = {current_coordinate}
current_coordinate = adjacent_coordinate
return current_coordinate
#staticmethod
def extract_sequence_of_steps(seq_of_coordinates):
steps = []
current_coord = seq_of_coordinates[0]
for next_destination in seq_of_coordinates[1:]:
steps.append(current_coord.get_direction(next_destination))
current_coord = next_destination
return steps
def __str__(self):
adjacency = []
for k, v in self.adjacency.items():
adjacency.append(f'{k},: {v}\n')
return ''.join(adjacency)
class BFS:
def __init__(self, lattice, start_coord, end_coord):
self.lattice = lattice
self.start_coord = start_coord
self.end_coord = end_coord
self.shortest_path = None # a sequence of Coordinate
self.bfs()
def bfs(self):
queue = []
visited = set()
queue.append([self.start_coord])
while queue:
path = queue.pop(0)
print("queue: ", queue, "path: ", path)
node = path[-1]
if node == self.end_coord:
self.shortest_path = path
break
if node not in visited:
for adjacent in self.lattice.adjacency.get(node, []):
new_path = list(path)
new_path.append(adjacent)
queue.append(new_path)
visited.add(node)
if __name__ == '__main__':
walk = RandomWalk(walker=Walker, numsteps=1000)
print(walk)
tom = walk.walkers[0]
paint_walker_path(tom)
print("Done with turtle tom")
lattice = Lattice()
end_node = lattice.get_final_dest_and_merge_sequence_of_steps(tom.sequence_of_steps)
print(end_node)
print(lattice)
search = BFS(lattice, ORIGIN, end_node)
print('search: ', search.shortest_path)
shortest_tom = Lattice.extract_sequence_of_steps(search.shortest_path)
paint_path(shortest_tom)
turtle.done()

Related

Making ticking sounds of different frequencies. Solves problem with alsa and jack by avoiding them

#!/usr/bin/env python3
import pygame
from array import (array)
from pygame.mixer import (Sound, get_init, pre_init)
class Electrode(Sound):
SEQUENCE = [
['A4', 440.00], ["A#", 466.00], ['B', 493.88], ['C', 523.25],
["C#", 554.00], ['D', 587.33], ["D#", 622.00], ['E', 659.25],
['F', 698.46], ["F#", 739.00], ['G', 783.99], ["G#", 830.00],
['A5', 880.00]
]
N = len(SEQUENCE)
SCALE={k:v for (k,v) in SEQUENCE}
def __init__(self, coefficient=0, volume=0.5):
(self.active, self.duration) = (True, 3)
self.coefficient(coefficient)
Sound.__init__(self, self.build_samples())
self.set_volume(volume)
def __call__(self):
if self.active:
self.play(self.duration)
return self
def coefficient(self, coefficient):
index = int(min(max(1, coefficient), Electrode.N-1))
self.key = Electrode.SEQUENCE[index][0]
self.frequency = Electrode.SCALE[self.key]
return self
def set(self, on):
self.active = True if on else False
return self
def build_samples(self):
period = int(round(get_init()[0] / self.frequency))
samples = array("h", [0] * period)
amplitude = 2 ** (abs(get_init()[1]) - 1) - 1
for time in range(period):
if time < (period / 2):
samples[time] = +amplitude
else:
samples[time] = -amplitude
return samples
if __name__ == "__main__":
from time import sleep
pre_init(44100, -16, 1, 1024)
pygame.init()
for n in range(0,Electrode.N):
Electrode(n)()
sleep(1)
Replaced pysine calls with pygame calls.

List in object gets overwritten

After some work I finished my first algorithm for a maze generator and now I am trying to make it visual in Pygame. I first let the algorithm generate a maze and then I make a visual representation of it.
Here I get into multiple problems, but I think they are all linked to the same thing which is that the first cell of the maze gets overwritten in some way. Because of this, what I get is totally not a maze at all but just some lines everywhere.
I tried putting the removal of walls in a seperate method, but that does not seem to work also. I looked if the remove walls method gets called at the first cell and it says it is true, but in some way the values of that cell gets overwritten.
Code:
import pygame
import random
WIDTH = 300
HEIGHT = 300
CellSize = 30
Rows = int(WIDTH/30)
Columns = int(HEIGHT/30)
current = None
grid = []
visited = []
DISPLAY = pygame.display.set_mode((WIDTH, HEIGHT))
class Cell:
def __init__(self, r, c):
self.r = r
self.c = c
self.x = CellSize * c
self.y = CellSize * r
self.sides = [True, True, True, True] # Top, Bottom, Left, Right
self.visited = False
self.neighbours = []
self.NTop = None
self.NBottom = None
self.NRight = None
self.NLeft = None
self.NTopIndex = None
self.NBottomIndex = None
self.NRightIndex = None
self.NLeftIndex = None
self.nr = None
self.nc = None
self.random = None
self.neighbour = None
def index(self, nr, nc):
self.nr = nr
self.nc = nc
if self.nr < 0 or self.nc < 0 or self.nr > Rows-1 or self.nc > Columns-1:
return -1
return self.nr + self.nc * Columns
def neighbour_check(self):
# Get neighbour positions in Grid
self.NTopIndex = self.index(self.r, self.c - 1)
self.NBottomIndex = self.index(self.r, self.c + 1)
self.NRightIndex = self.index(self.r + 1, self.c)
self.NLeftIndex = self.index(self.r - 1, self.c)
# Look if they are truly neighbours and then append to neighbour list
if self.NTopIndex >= 0:
self.NTop = grid[self.NTopIndex]
if not self.NTop.visited:
self.neighbours.append(self.NTop)
if self.NBottomIndex >= 0:
self.NBottom = grid[self.NBottomIndex]
if not self.NBottom.visited:
self.neighbours.append(self.NBottom)
if self.NRightIndex >= 0:
self.NRight = grid[self.NRightIndex]
if not self.NRight.visited:
self.neighbours.append(self.NRight)
if self.NLeftIndex >= 0:
self.NLeft = grid[self.NLeftIndex]
if not self.NLeft.visited:
self.neighbours.append(self.NLeft)
# Choose random neighbour
if len(self.neighbours) > 0:
self.random = random.randint(0, len(self.neighbours) - 1)
self.neighbour = self.neighbours[self.random]
# Remove the wall between self and neighbour
if self.neighbour == self.NTop:
if self == grid[0]:
print('TOP')
self.sides[0] = False
self.NTop.sides[1] = False
elif self.neighbour == self.NBottom:
if self == grid[0]:
print('BOTTOM')
self.sides[1] = False
self.NBottom.sides[0] = False
elif self.neighbour == self.NLeft:
if self == grid[0]:
print('LEFT')
self.sides[2] = False
self.NLeft.sides[3] = False
elif self.neighbour == self.NRight:
if self == grid[0]:
print('RIGHT')
self.sides[3] = False
self.NRight.sides[2] = False
else:
print('SIDES ERROR')
return self.neighbours[self.random]
else:
return -1
def draw(self):
global DISPLAY, CellSize
# Top
if self.sides[0]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y), (self.x + CellSize, self.y))
# Bottom
if self.sides[1]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y + CellSize), (self.x + CellSize, self.y + CellSize))
# Left
if self.sides[2]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x, self.y), (self.x, self.y + CellSize))
# Right
if self.sides[3]:
pygame.draw.line(DISPLAY, (0, 0, 0), (self.x + CellSize, self.y), (self.x + CellSize, self.y + CellSize))
class Maze:
def __init__(self):
global current
self.next = None
self.running = True
self.DISPLAY = None
self.display_running = True
def init_cells(self):
# Make grid and make cell 0 the begin of the algorithm
global current
for i in range(0, Columns):
for j in range(0, Rows):
cell = Cell(j, i)
grid.append(cell)
current = grid[0]
def init_maze(self):
global current, visited
print(grid[0].sides)
# Start Algorithm
while self.running:
# Check if the current cell is visited, if not make it visited and choose new neighbour
if not current.visited:
current.visited = True
visited.append(current)
self.next = current.neighbour_check()
if not self.next == -1:
# If it finds a neighbour then make it the new current cell
# self.next.visited = True
current = self.next
elif self.next == -1 and len(visited) > 0:
# If it doesn't then look trough the path and backtrack trough it to find a possible neighbour
if len(visited) > 1:
del visited[-1]
current = visited[-1]
# If the last cell of the visited list is Cell 0 then remove it
elif len(visited) <= 1:
del visited[-1]
elif len(visited) <= 0:
# Stop the Algorithm
self.running = False
print('Done')
def draw(self):
DISPLAY.fill((255, 255, 255))
# Get the maze made by the algorithm and draw it on the screen
for i in range(0, len(grid)):
grid[i].draw()
pygame.display.update()
while self.display_running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.display_running = False
maze = Maze()
maze.init_cells()
maze.init_maze()
maze.draw()
I put some print methods in it for debugging purposes.
And am still a beginner in programming, I know it could probably be way cleaner or that some naming of methods could be better.
What I want to happen is that in def init_maze the maze blueprints gets written out and that in def draw the blueprint gets drawn on the screen.

pygame sets position of all instances of class to the same value for some reason?

I'm making a snake game in pygame and i've run into a problem. Whenever the snake eats food, the player gains a point. From that point on a tail is created behind the snake whenever it moves. Only the tails the number of player points away from the snake head will be drawn while the rest are deleted. The problem arises when i'm creating the tails. Whenever i create an instance of the tail, i have to get the position of the snake head and subtract away a value equal to the snake size in the opposite direction. That's where the tail will be drawn. However the position of all the tails are set to the same value for some reason and i cant't figure out why that is. I'm using my own library so i cant post it in here but iv'e determined it's not the cause.
import pygame as pg
from random import randrange
import widget
# disp -> display properties
disp = widget.get_json("config", ["display"])
food = widget.Surface(image="../images/food.png", pos=[0, 0])
def set_food_pos(snake):
while True:
pos = [randrange(0, disp["size"][_], disp["cell"]) for _ in range(2)]
safe = 0
for tail in snake.tails:
if tail.pos != pos: safe += 1
if safe == len(snake.tails):
food.pos = pos
food.rect.topleft = food.pos
break
class Snake(widget.Sprite):
""" Snake: main playable sprite """
SIZE = [disp["cell"]] * 2
KEYS = [[276, 275], [273, 274]]
def __init__(self):
self.image = pg.image.load("../images/snake_head.png")
self.pos = widget.VEC(0, 0)
super().__init__(pg.sprite.GroupSingle)
self.axis, self.orient, self.do_move = 0, 1, False
self.past, self.delay = pg.time.get_ticks(), 150
self.speed, self.vel = disp["cell"], [-1, 1]
self.alive, self.points = True, 0
self.tails = [self]
def control(self, key):
axis = [0 if key in Snake.KEYS[0] else 1][0]
if axis != self.axis:
if self.do_move:
self.axis = axis
self.orient = Snake.KEYS[axis].index(key)
self.do_move = False
def time_base_movement(self):
now = pg.time.get_ticks()
if now - self.past >= self.delay:
self.do_move = True
self.pos[self.axis] += self.vel[self.orient] * self.speed
self.past = pg.time.get_ticks()
def eat_food(self):
if food.rect.contains(self.rect):
set_food_pos(self)
self.points += 1
def create_tail(self):
if self.points:
if self.do_move:
pos = [_ for _ in self.rect.topleft]
pos[self.axis] += self.vel[::-1][self.orient] * 20
tail = widget.Sprite(image="../images/snake_head.png", pos=pos)
self.tails.insert(0, tail)
def render_tails(self, surface):
if self.points > 0:
tails = self.tails[:-1]
for tail in tails[0:self.points]: tail.group.draw(surface)
[self.tails.remove(tail) for tail in tails[self.points:]]
def check_boundary_collision(self):
for _ in range(2):
if self.pos[_] > disp["size"][_] - Snake.SIZE[_]:self.alive = False
elif self.pos[_] < 0: self.alive = False
for tail in self.tails[:-1]:
if tail.rect.contains(self.rect): self.alive = False
def reset_properties(self):
if self.alive == False:
print([tail.pos for tail in self.tails[:-1]])
self.tails = [self]
self.do_move = False
self.pos = widget.VEC([0, 0])
self.rect.topleft = self.pos
self.axis, self.orient = 0, 1
self.points, self.alive = 0, True
set_food_pos(self)
def update(self):
if self.alive:
self.time_base_movement()
self.check_boundary_collision()
self.reset_properties()
self.rect.topleft = self.pos
I figured it out, it seems python doesn't create a new copy of an attribute each time it is assigned to a different attribue. Instead the new attribute points to the assigned attribute. The fix for this is the "deepcopy" method in the built in module "copy".
Exe: new_value = copy.deepcopy(old_value)

AttributeError: 'float' object has no attribute 'get_coords'

I'm learning Python from this lecture: Lec 19 | MIT 6.00 Introduction to Computer Science and Programming. I'm using Python 3.6.2, lecture example runs on Python 2.x. Whats the proper way to set values of x and y in function ans_quest?
x, y = loc_list[-1].get_coords()
Can this method be called like this? This was the example in the lecture.
Full code:
import math, random, pylab, copy
class Location(object):
def __init__(self, x, y):
self.x = float(x)
self.y = float(y)
def move(self, xc, yc):
return Location(self.x+float(xc), self.y+float(yc))
def get_coords(self):
return self.x, self.y
def get_dist(self, other):
ox, oy = other.get_coords()
x_dist = self.x - ox
y_dist = self.y - oy
return math.sqrt(x_dist**2 + y_dist**2)
class Compass_Pt(object):
possibles = ('N', 'S', 'E', 'W')
def __init__(self, pt):
if pt in self.possibles: self.pt = pt
else: raise ValueError('in Compass_Pt.__init__')
def move(self, dist):
if self.pt == 'N': return (0, dist)
elif self.pt == 'S': return (0, -dist)
elif self.pt == 'E': return (dist, 0)
elif self.pt == 'W': return (-dist, 0)
else: raise ValueError('in Compass_Pt.move')
class Field(object):
''' Cartesian plane where object will be located '''
def __init__(self, drunk, loc):
self.drunk = drunk
self.loc = loc
def move(self, cp, dist):
old_loc = self.loc
xc, yc = cp.move(dist)
self.loc = old_loc.move(xc, yc)
def get_loc(self):
return self.loc
def get_drunk(self):
return self.drunk
class Drunk(object):
''' Point itself '''
def __init__(self, name):
self.name = name
def move(self, field, cp, dist = 1):
if field.get_drunk().name != self.name:
raise ValueError('Drunk.move called with drunk not in the field')
for i in range(dist):
field.move(cp, 1)
class Usual_Drunk(Drunk):
def move(self, field, dist = 1):
''' Drunk.move superclass method override. Sends additional cp attribute.'''
cp = random.choice(Compass_Pt.possibles)
Drunk.move(self, field, Compass_Pt(cp), dist)
class Cold_Drunk(Drunk):
def move(self, field, dist = 1):
cp = random.choice(Compass_Pt.possibles)
if cp == 'S':
Drunk.move(self, field, Compass_Pt(cp), 2*dist)
else:
Drunk.move(self, field, Compass_Pt(cp), dist)
class EW_Drunk(Drunk):
def move(self, field, time = 1):
cp = random.choice(Compass_Pt.possibles)
while cp != 'E' and cp != 'W':
cp = random.choice(Compass_Pt.possibles)
Drunk.move(self, field, Compass_Pt(cp), time)
def perform_trial(time, f):
start = f.get_loc()
distances = [0,0]
for t in range(1, time + 1):
f.get_drunk().move(f)
new_loc = f.get_loc()
distance = new_loc.get_dist(start)
distances.append(distance)
return distances
def perform_sim(time, num_trials, drunk_type):
dist_lists = []
loc_lists = []
for trial in range(num_trials):
d = drunk_type('Drunk' + str(trial))
f = Field(d, Location(0, 0))
distances = perform_trial(time, f)
locs = copy.deepcopy(distances)
dist_lists.append(distances)
loc_lists.append(locs)
return dist_lists, loc_lists
def ans_quest(max_time, num_trials, drunk_type, title):
dist_lists, loc_lists = perform_sim(max_time, num_trials, drunk_type)
means = []
for t in range(max_time + 1):
tot = 0.0
for dist_l in dist_lists:
tot += dist_l[t]
means.append(tot/len(dist_lists))
pylab.figure()
pylab.plot(means)
pylab.ylabel('distance')
pylab.xlabel('time')
pylab.title('{} Ave. Distance'.format(title))
lastX = []
lastY = []
for loc_list in loc_lists:
x, y = loc_list[-1].get_coords()
lastX.append(x)
lastY.append(y)
pylab.figure()
pylab.scatter(lastX, lastY)
pylab.ylabel('NW Distance')
pylab.title('{} Final location'.format(title))
pylab.figure()
pylab.hist(lastX)
pylab.xlabel('EW Value')
pylab.ylabel('Number of Trials')
pylab.title('{} Distribution of Final EW Values'.format(title))
num_steps = 50
num_trials = 10
ans_quest(num_steps, num_trials, Usual_Drunk, 'Usual Drunk ' + str(num_trials) + ' Trials')
ans_quest(num_steps, num_trials, Cold_Drunk, 'Cold Drunk ' + str(num_trials) + ' Trials')
ans_quest(num_steps, num_trials, EW_Drunk, 'EW Drunk ' + str(num_trials) + ' Trials')
pylab.show()
Error:
Traceback (most recent call last):
File "/home/tihe/Documents/CODING/Project Home/Python/biased_random_walks.py", line 194, in <module>
ans_quest(num_steps, num_trials, Usual_Drunk, 'Usual Drunk ' + str(num_trials) + ' Trials')
File "/home/tihe/Documents/CODING/Project Home/Python/biased_random_walks.py", line 175, in ans_quest
x, y = loc_list[-1].get_coords()
AttributeError: 'float' object has no attribute 'get_coords'
This method could be called like this if you had a list of Location objects. The error is because the loc_list is populated with distances and not Location objects. That happens in function perform_sim when instead of geting the location you are making a deep copy of distance.
Perhaps you could try something like this:
def perform_trial(time, f):
start = f.get_loc()
distances = [0,0]
locations = []
for t in range(1, time + 1):
f.get_drunk().move(f)
new_loc = f.get_loc()
locations.append(new_loc)
distance = new_loc.get_dist(start)
distances.append(distance)
return distances, locations
def perform_sim(time, num_trials, drunk_type):
dist_lists = []
loc_lists = []
for trial in range(num_trials):
d = drunk_type('Drunk' + str(trial))
f = Field(d, Location(0, 0))
distances, locations = perform_trial(time, f)
dist_lists.append(distances)
loc_lists.append(locations)
return dist_lists, loc_lists
I hope that helped you out.

AssertionError: Format for classes is `<label> file`

This is a python script for detecting features in a set of images for a SVM.
import os
import sys
import argparse
import _pickle as cPickle
import json
import cv2
import numpy as np
from sklearn.cluster import KMeans
def build_arg_parser():
parser = argparse.ArgumentParser(description='Creates features for given images')
parser.add_argument("--samples", dest="cls", nargs="+", action="append",
required=True, help="Folders containing the training images. \
The first element needs to be the class label.")
parser.add_argument("--codebook-file", dest='codebook_file', required=True,
help="Base file name to store the codebook")
parser.add_argument("--feature-map-file", dest='feature_map_file', required=True,
help="Base file name to store the feature map")
parser.add_argument("--scale-image", dest="scale", type=int, default=150,
help="Scales the longer dimension of the image down to this size.")
return parser
def load_input_map(label, input_folder):
combined_data = []
if not os.path.isdir(input_folder):
print ("The folder " + input_folder + " doesn't exist")
raise IOError
for root, dirs, files in os.walk(input_folder):
for filename in (x for x in files if x.endswith('.jpg')):
combined_data.append({'label': label, 'image': os.path.join(root, filename)})
return combined_data
class FeatureExtractor(object):
def extract_image_features(self, img):
kps = DenseDetector().detect(img)
kps, fvs = SIFTExtractor().compute(img, kps)
return fvs
def get_centroids(self, input_map, num_samples_to_fit=10):
kps_all = []
count = 0
cur_label = ''
for item in input_map:
if count >= num_samples_to_fit:
if cur_label != item['label']:
count = 0
else:
continue
count += 1
if count == num_samples_to_fit:
print ("Built centroids for", item['label'])
cur_label = item['label']
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)
num_dims = 128
fvs = self.extract_image_features(img)
kps_all.extend(fvs)
kmeans, centroids = Quantizer().quantize(kps_all)
return kmeans, centroids
def get_feature_vector(self, img, kmeans, centroids):
return Quantizer().get_feature_vector(img, kmeans, centroids)
def extract_feature_map(input_map, kmeans, centroids):
feature_map = []
for item in input_map:
temp_dict = {}
temp_dict['label'] = item['label']
print ("Extracting features for", item['image'])
img = cv2.imread(item['image'])
img = resize_to_size(img, 150)
temp_dict['feature_vector'] = FeatureExtractor().get_feature_vector(
img, kmeans, centroids)
if temp_dict['feature_vector'] is not None:
feature_map.append(temp_dict)
return feature_map
class Quantizer(object):
def __init__(self, num_clusters=32):
self.num_dims = 128
self.extractor = SIFTExtractor()
self.num_clusters = num_clusters
self.num_retries = 10
def quantize(self, datapoints):
kmeans = KMeans(self.num_clusters,
n_init=max(self.num_retries, 1),
max_iter=10, tol=1.0)
res = kmeans.fit(datapoints)
centroids = res.cluster_centers_
return kmeans, centroids
def normalize(self, input_data):
sum_input = np.sum(input_data)
if sum_input > 0:
return input_data / sum_input
else:
return input_data
def get_feature_vector(self, img, kmeans, centroids):
kps = DenseDetector().detect(img)
kps, fvs = self.extractor.compute(img, kps)
labels = kmeans.predict(fvs)
fv = np.zeros(self.num_clusters)
for i, item in enumerate(fvs):
fv[labels[i]] += 1
fv_image = np.reshape(fv, ((1, fv.shape[0])))
return self.normalize(fv_image)
class DenseDetector(object):
def __init__(self, step_size=20, feature_scale=40, img_bound=20):
self.detector = cv2.xfeatures2d.SIFT_create("Dense")
self.detector.setInt("initXyStep", step_size)
self.detector.setInt("initFeatureScale", feature_scale)
self.detector.setInt("initImgBound", img_bound)
def detect(self, img):
return self.detector.detect(img)
class SIFTExtractor(object):
def compute(self, image, kps):
if image is None:
print ("Not a valid image")
raise TypeError
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
kps, des = cv2.SIFT().compute(gray_image, kps)
return kps, des
# Resize the shorter dimension to 'new_size'
# while maintaining the aspect ratio
def resize_to_size(input_image, new_size=150):
h, w = input_image.shape[0], input_image.shape[1]
ds_factor = new_size / float(h)
if w < h:
ds_factor = new_size / float(w)
new_size = (int(w * ds_factor), int(h * ds_factor))
return cv2.resize(input_image, new_size)
if __name__=='__main__':
args = build_arg_parser().parse_args()
input_map = []
for cls in args.cls:
assert len(cls) >= 2, "Format for classes is `<label> file`"
label = cls[0]
input_map += load_input_map(label, cls[1])
downsample_length = args.scale
# Building the codebook
print ("===== Building codebook =====")
kmeans, centroids = FeatureExtractor().get_centroids(input_map)
if args.codebook_file:
with open(args.codebook_file, 'w') as f:
pickle.dump((kmeans, centroids), f)
# Input data and labels
print ("===== Building feature map =====")
feature_map = extract_feature_map(input_map, kmeans, centroids)
if args.feature_map_file:
with open(args.feature_map_file, 'w') as f:
pickle.dump(feature_map, f)
I receive the following error:
Traceback (most recent call last):
File "create_features.py", line 164, in <module>
assert len(cls) >= 2, ("Format for classes is `<label> file`")
AssertionError: Format for classes is `<label> file`
Any idea of what could be wrong? I'm just following the instructions of 'OpenCV with Python by Example' of Prateek Joshi. Pages 494-526
Assertion are used to check a condition. If the condition isn't satisfied, it throes AssertionError. In your case, len(cls) >= 2 isn't satisfied. It means that len(cls) is smaller than 2. Apparently, cls is a list of arguments passed to the programm. And the first element of this list must be a label. And when you add argument (a file), you should specify a label for this file.
For example, if you choose a label name my_label, you must add file with my_label my_file.

Resources