Solving Mazes With Python. Using Dijkstra’s Algorithm and OpenCV | by Max Reynolds | Towards Data Science

Mazes are often simple puzzles for humans, but they present a great programming problem that we can solve using shortest-path techniques like Dijkstra’s algorithm.
Dijkstra’s Algorithm is one of the more popular basic graph theory algorithms. It is used to find the shortest path between nodes on a directed graph. We start with a source node and known edge lengths between nodes.
We first assign a distance-from-source value to all the nodes. Node s receives a 0 value because it is the source; the rest receive values of ∞ to start.
Our node of interest is the smallest-value unprocessed node (shown in grey), which is s. First, we “relax” each adjacent vertex to our node of interest, updating their values to the minimum of their current value or the node of interest’s value plus the connecting edge length.
Node s is now finalized (black) and its neighbors a and b have taken on new values. The new node of interest is b, so we repeat the process of “relaxing” b’s adjacent nodes and finalizing the shortest-path value of b.
After going through each node, we eventually end up with a graph showing the shortest path length from the source to every node.
We can think of an image as a matrix of pixels. Each pixel (for simplicity’s sake) has an RGB value of 0,0,0 (black) or 255,255,255 (white). Our goal is to create a shortest path which starts in the white and does not cross into the black boundaries. To represent this goal we can treat each pixel as a node and draw edges between neighboring pixels with edge lengths based on RGB value differences. We will use the Euclidean squared distance formula and add 0.1 to ensure no 0-distance path lengths (a requirement for Dijkstra’s algorithm):
This formula makes the distance of crossing through the maze boundary prohibitively large. As we can see, the shortest path from source to destination will clearly be around the barrier, not through it.
We can use OpenCV, a popular image processing library for Python, to extract pixel values and show our maze images. Let’s also identify the coordinates of our starting and ending locations by adding points to our maze
import cv2import matplotlib.pyplot as pltimport numpy as npimg = cv2.imread('maze.png') # read an image from a file usingcv2.circle(img,(5,220), 3, (255,0,0), -1) # add a circle at (5, 220)cv2.circle(img, (25,5), 3, (0,0,255), -1) # add a circle at (5,5)plt.figure(figsize=(7,7))plt.imshow(img) # show the imageplt.show()
We create a Vertex class which will help us keep track of the coordinates. We also want to keep track of the parent node so that we can reconstruct the entire path once we find our distance values.
class Vertex:    def __init__(self,x_coord,y_coord):        self.x=x_coord        self.y=y_coord        self.d=float('inf') #current distance from source node        self.parent_x=None        self.parent_y=None        self.processed=False        self.index_in_queue=None
We need to create a matrix of Vertices, representing the 2D layout of pixels in an image. This will be the basis for our Dijkstra’s algorithm graph. We also maintain a min-heap priority queue to keep track of unprocessed nodes.
def find_shortest_path(img,src,dst):    pq=[] #min-heap priority queue    imagerows,imagecols=img.shape[0],img.shape[1]    matrix = np.full((imagerows, imagecols), None)     #access matrix elements by matrix[row][col]    #fill matrix with vertices    for r in range(imagerows):        for c in range(imagecols):            matrix[r][c]=Vertex(c,r)            matrix[r][c].index_in_queue=len(pq)            pq.append(matrix[r][c])    #set source distance value to 0    matrix[source_y][source_x].d=0    #maintain min-heap invariant (minimum d Vertex at list index 0)    pq = bubble_up(pq, matrix[source_y][source_x].index_in_queue)
We need several helper functions to help find edges and edge lengths between vertices:
#Implement euclidean squared distance formuladef get_distance(img,u,v):    return 0.1 + (float(img[v][0])-float(img[u][0]))**2+(float(img[v][1])-float(img[u][1]))**2+(float(img[v][2])-float(img[u][2]))**2#Return neighbor directly above, below, right, and leftdef get_neighbors(mat,r,c):    shape=mat.shape    neighbors=[]    #ensure neighbors are within image boundaries    if r > 0 and not mat[r-1][c].processed:         neighbors.append(mat[r-1][c])    if r < shape[0] - 1 and not mat[r+1][c].processed:            neighbors.append(mat[r+1][c])    if c > 0 and not mat[r][c-1].processed:        neighbors.append(mat[r][c-1])    if c < shape[1] - 1 and not mat[r][c+1].processed:            neighbors.append(mat[r][c+1])    return neighbors
Now, we can implement Dijkstra’s Algorithm and assign distance (d) values to all of the pixel Vertices in the maze image:
while len(pq) > 0:    u=pq[0] #smallest-value unprocessed node    #remove node of interest from the queue    pq[0]=pq[-1]     pq[0].index_in_queue=0    pq.pop()    pq=bubble_down(pq,0) #min-heap function, see source code         u.processed=True    neighbors = get_neighbors(matrix,u.y,u.x)    for v in neighbors:        dist=get_distance(img,(u.y,u.x),(v.y,v.x))        if u.d + dist < v.d:            v.d = u.d+dist            v.parent_x=u.x #keep track of the shortest path            v.parent_y=u.y            idx=v.index_in_queue            pq=bubble_down(pq,idx)             pq=bubble_up(pq,idx)
We can now call our shortest path function and draw the solution on our maze:
img = cv2.imread('maze.png') # read an image from a file using opencv (cv2) libraryp = find_shortest_path(img, (25,5), (5,220))drawPath(img,p)plt.figure(figsize=(7,7))plt.imshow(img) # show the image on the screen plt.show()
Let’s try other mazes from around the web.
The full source code is available on GitHub here and Part 2, creating a user interface for the maze solver is below:
medium.com
Registering for Medium membership supports my work.