Using a custom image for mask generation
OpenCV provides powerful image processing capabilities that can transform black and white images into usable
data structures for maze generation algorithms. We load the image, then rescaling it to match our desired
maze dimensions
using cv2.resize() and then applying binary thresholding to ensure clean black and white values, creating a
binary mask where white pixels (255) represent walkable paths and black pixels (0) represent walls. This
thresholded image serves as a pixel-perfect mask that we can use to determine tile
placement - each pixel corresponds to a grid position in our maze.
Loading the image
We load the image by creating a utils.py file that reads the image, resizes it and stores it as a numpy array in a variables called image
# MODULES
import cv2
import random
# DD
image = cv2.imread("ref2.png", 0)
newW = int(image.shape[1] * 0.1)
newH = int(image.shape[0] * 0.1)
print(newW,newH)
image = cv2.resize(image,(newW,newH))
_,image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
image = image/255
Generating the maze
The parameters of the Algorithm
- RES: (int) - The pixel size of each tile/cell in the maze grid
- DIMS: (int,int) - Total number of columns and rows derived from the input image
dimensions (image.shape[1], image.shape[0])
- SCREEN: (int,int) - The width and height of the display window in pixels,
calculated as (DIMS[0]*RES, DIMS[1]*RES)
- TILE: Tile() - Basic unit representing each cell in the maze with properties
for walls [R,D,L,U], visited status, position, and color
- GRID: [[TILE, ..., n=DIMS[0]], ..., n=DIMS[1]] - A 2D array of Tile objects
representing the complete maze structure
- VISITOR: Tile() - The current tile being processed during maze generation using
depth-first search
- STACK: [TILE, ...] - Collection of tiles used for backtracking when the
algorithm reaches dead ends in the depth-first traversal
# MODULES
import pygame
import random
from utils import image
# DD
RES = 12
DIMS = (image.shape[1], image.shape[0])
SCREEN = (DIMS[0]*RES, DIMS[1]*RES)
display = pygame.display.set_mode(SCREEN)
# DD. TILE
# tile = Tile()
# interp. a square in the screen
class Tile:
def __init__(self,c,r):
self.c = c
self.r = r
self.x = self.c *RES
self.y = self.r * RES
self.rect = pygame.Rect(self.x, self.y, RES, RES)
# attr. related to maze generator
self.walls = [True, True, True, True]
# R, D, L, U
self.visited = False
self.nullcolor = False
self.color = "blue"
def drawTile(self):
self.getColor()
pygame.draw.rect(display, self.color, self.rect)
def drawWalls(self):
if not self.nullcolor:
# R
if self.walls[0]:
pygame.draw.line(display,"white",(self.x + RES, self.y),(self.x + RES, self.y + RES),1)
# self.x + RES, self.y ---> self.x + RES, self.y + RES
# D
if self.walls[1]:
pygame.draw.line(display,"white",(self.x, self.y + RES),(self.x + RES, self.y + RES),1)
# L
if self.walls[2]:
pygame.draw.line(display,"white",(self.x, self.y),(self.x, self.y + RES),1)
# U
if self.walls[3]:
pygame.draw.line(display,"white",(self.x, self.y),(self.x + RES, self.y),1)
def getColor(self):
if self.visited:
self.color = "black"
if visitor == self:
self.color = "black"
if self.nullcolor:
self.color = "black"
# DD. ROW
# row = [TILE, ..., n=DIMS[0]]
# interp. a collection of tiles that goes from left to right
row = []
for c in range(DIMS[0]):
tile = Tile(c, 0)
row.append(tile)
# TEMPLATE FOR ROW
# for tile in row:
# ... tile
# DD. GRID
# grid = [ROW, ..., n=DIMS[1]]
# interp. a 2D array of TILE
grid = []
for r in range(image.shape[0]):
row = []
for c in range(image.shape[1]):
tile = Tile(c, r)
if image[r][c] == 0.0:
tile.visited = True
tile.nullcolor = True
row.append(tile)
grid.append(row)
# TEMPLATE FOR GRID
# for row in grid:
# for tile in row:
# ... tile
# DD. VISITOR
# visitor = grid[int][int]
# interp. the current tile that is being visited
visitor = grid[0][0]
visitor.visited = True
# DD. STACK
# stack = [TILE, ...]
# interp. a collection of tiles in the order in which we visit them
stack=[visitor]
# CODE
def draw():
display.fill("#1e1e1e")
for row in grid:
for tile in row:
tile.drawTile()
for row in grid:
for tile in row:
tile.drawWalls()
pygame.display.flip()
def getNeigh(visitor):
r = visitor.r
c = visitor.c
candidates = []
# R
if c < DIMS[0]-1 and not grid[r][c+1].visited:
neigh = grid[r][c+1]
candidates.append(neigh)
# D
if r < DIMS[1]-1 and not grid[r+1][c].visited:
neigh = grid[r+1][c]
candidates.append(neigh)
# L
if c > 0 and not grid[r][c-1].visited:
neigh = grid[r][c-1]
candidates.append(neigh)
# U
if r > 0 and not grid[r-1][c].visited:
neigh = grid[r-1][c]
candidates.append(neigh)
return candidates
def teardownWalls(vs, ch):
# R
if vs.c - ch.c < 0:
vs.walls[0] = False
ch.walls[2] = False
# D
if vs.r - ch.r < 0:
vs.walls[1] = False
ch.walls[3] = False
# L
if vs.c - ch.c > 0:
vs.walls[2] = False
ch.walls[0] = False
# U
if vs.r - ch.r > 0:
vs.walls[3] = False
ch.walls[1] = False
def update():
global visitor
# get a list with the valid neighbors
potentialNeighbor = getNeigh(visitor)
# if the total number of neigh is > 0:
if len(potentialNeighbor) > 0:
# pick one of the neighbors at random
chosen = random.choice(potentialNeighbor)
# tear down the walls between the new chosen neigh and visitor
teardownWalls(visitor, chosen)
# set visited from neigh to True
chosen.visited = True
stack.append(chosen)
# make visitor = chosen
visitor = chosen
else:
# if the length of the stack is bigger than 0
if len(stack) > 0:
visitor = stack[-1]
stack.pop(-1)
else:
pass
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
for row in grid:
for tile in row:
if tile.rect.collidepoint(pygame.mouse.get_pos()):
visitor = tile
visitor.visited = True
stack.append(visitor)
while True:
draw()
update()
The Algorithm
- Initialize GRID based on input image mask (black pixels become obstacles, white pixels become
walkable areas)
- Set starting VISITOR at grid[0][0] and mark it as visited
- Add VISITOR to STACK for backtracking
- WHILE STACK is not empty:
-
- Get all unvisited neighbors of current VISITOR (Right, Down, Left, Up)
- IF valid neighbors exist:
- Pick one neighbor at random from candidates
- Tear down walls between VISITOR and chosen neighbor
- Mark chosen neighbor as visited
- Add chosen neighbor to STACK
- Move VISITOR to chosen neighbor
- ELSE (no unvisited neighbors):
- Backtrack by removing last TILE from STACK
- Set VISITOR to previous position in STACK
- Continue until all reachable tiles have been visited, creating a perfect maze with no loops