Maze Solving Robot Using OpenCV

by engineerkid1 in Circuits > Robots

153 Views, 2 Favorites, 0 Comments

Maze Solving Robot Using OpenCV

I built a maze solving robot using OpenCV

In this project, I built a maze solving robot that uses an Android phone camera and OpenCV to navigate through a maze autonomously.

Unlike traditional maze-solving robots that depend on multiple IR sensors, this robot uses computer vision to detect the path and make decisions in real time. The robot follows black maze lines using OpenCV image processing and solves the maze using the Left-Hand Rule algorithm.

For getting the raw frames, I used an IP Camera App that transmits the frames to my computer wirelessly. The computer then analyzes those frames and sends the movement command to the ESP32 on the robot via TCP (You can also use Bluetooth here) and it sends the commands to the Arduino which move the robot.

The robot has total 7 moves - forward, stop, left, right, u-turn, adjust left and adjust right.

The actual maze solving is explained in below steps.

There are two phases in the operation-

  1. Learning phase - In this phase the robot navigates the entire maze until it finds the end spot.
  2. Solving phase - In this phase the robot deduces the shortest path to reach the end spot and then traverses that path.

This project combines:

  1. Computer Vision
  2. Robotics
  3. OpenCV
  4. Arduino
  5. Autonomous Navigation

This can be a great hobby project or a mini project for your college work.

Supplies

ChatGPT Image May 14, 2026, 12_58_09 AM.png

Electronics

  1. Arduino Uno x 1
  2. ESP32 x 1
  3. 28BYJ-48 Stepper Motor x 2
  4. ULN 2003 Driver x 2
  5. Buck converter x 1 (12->5V)
  6. Robot Chassis x 1
  7. Wheels (Good Grip) x 2
  8. Castor Wheel (Smooth) x 1
  9. L-ion Battery Pack 12V x 1 (Contest people you see this)
  10. Jumper Wires

Software

  1. Arduino IDE
  2. Python with OpenCV and other dependencies installed
  3. IP camera App

Mechanical

  1. Black electrical tape
  2. White floor/chart paper

Building the Maze

maze.PNG

I built the maze by sticking a black electrical tape directly onto my white floor. You can use a white cardboard sheet or banner if you want.

I followed the attached maze for my project. Feel free to use any other maze of your choice, just remember below points.

Important:

  1. Avoid enclosed loops if using Left-Hand Maze Solving Rule
  2. Keep line width consistent
  3. Use high contrast between path and background
  4. Avoid glossy surfaces if possible so the robot gets a better grip

The robot works best when:

  1. The maze has proper junctions
  2. Turns are sharp and clean
  3. Lighting is consistent

Building the Robot

connections.PNG
robot on maze.PNG

The ESP32 communicates with the Arduino UNO using UART serial communication.

ESP32 Pin -> Arduino UNO Pin

GPIO1 (TX0) -> Pin 0 (RX)

GPIO3 (RX0) -> Pin 1 (TX)

GND -> GND

5V VCC -> 5V

Arduino UNO Pin -> ULN 2003 -> Left Motor

Pin 7 -> IN1

Pin 6 -> IN2

Pin 5 -> IN3

Pin 4 -> IN4

Arduino UNO Pin -> ULN 2003 -> Right Motor

Pin 8 -> IN1

Pin 9 -> IN2

Pin 10 -> IN3

Pin 11 -> IN4

Overall Architecture

IP Camera → OpenCV on PC → ESP32 (WiFi TCP) → Arduino UNO → ULN2003 Drivers → Stepper Motors

How the Maze Solving Algorithm Works

Zones2.PNG
in action.PNG
Stepper motot test stand.PNG

The robot solves the maze using computer vision instead of traditional IR sensors.

Rather than detecting lines using dedicated hardware sensors, the robot uses an IP camera feed and OpenCV to “see” the maze and make decisions in real time.

The overall idea is surprisingly simple:

  1. Look at different regions of the camera image
  2. Count black pixels in those regions
  3. Decide where the path exists
  4. Move accordingly

The Camera View

The maze consists of:

  1. Black path
  2. White floor

The camera continuously captures frames of the maze from above.

Each frame is converted into a black-and-white image so the robot can easily separate the maze path from the background.

Region-Based Detection

Instead of analyzing the entire image, the frame is divided into multiple smaller zones.

Each zone has a specific purpose.

Bottom Zone – Path Following

The bottom region is used to keep the robot centered on the path.

The algorithm compares the black pixel distribution in this region:

  1. If more black pixels appear on the left side, the robot adjusts left
  2. If more black pixels appear on the right side, the robot adjusts right
  3. If the path is centered, the robot moves forward

This works similarly to a basic OpenCV line follower robot project I made earlier.

Left and Right Zones – Turn Detection

The side regions detect available turns.

If the left zone contains enough black pixels:

  1. A left turn exists

If the right zone contains enough black pixels:

  1. A right turn exists

These regions allow the robot to identify intersections inside the maze.

Top Zone – Dead Ends and T-Intersections

The top region helps the robot understand what lies ahead.

This region is used to detect:

  1. Dead ends
  2. Straight paths
  3. T-intersections

If no black pixels are detected ahead:

  1. The robot concludes that it has reached a dead end
  2. It performs a U-turn

Top-Right Zone – Finish Detection

A small region near the top-right corner is used as the finish detector.

When this region along with the other regions detects the final black marker:

  1. The robot concludes that the maze is solved

The robot continuously follows this logic:

  1. Check for left turn
  2. Check for straight path
  3. If straight path exists → move forward
  4. Continuously make small corrections to stay centered
  5. Check for right turn
  6. If no path exists → perform U-turn

The camera feed is processed repeatedly in real time, allowing the robot to constantly update its movement decisions. And the turns and movements recorded are from the robot's perspective.

Recovery Logic for Overshooting

One major problem with the robot was inconsistent turning caused by the inexpensive stepper motors.

Sometimes the robot:

  1. Turned too little (undershoot)
  2. Turned too much (overshoot)

I designed a simple mechansim to test the stepper motor and driver using Autodesk Fusion 360 and quickly printed it using my trusted Bambu Lab A1 mini. Uploaded a simple script to tirn the motor continuously and let it run for several hours. I observed that the initial position of the servo and the final position were off by a very little marning. But this test was under no load. Now imagine what would be happening to my robot when it is under entire load of the chassis, phone, ardino, esp32 and not to forget the big Li-ION Battery (I did it again).

To solve this, I added a recovery system.

After every turn:

  1. The robot assumes it has undershot the turn
  2. It starts making small correction movements in the turning direction
  3. If no valid path is found after several attempts, it assumes the opposite problem occurred
  4. It then starts correcting in the opposite direction

This solution is time consuming but it allowed the robot to recover from positioning errors without using expensive encoders or sensors.

Shortest Path Logic

After solving the maze once, the robot stores the sequence of turns it took.

The algorithm below then simplifies unnecessary movements to generate a shorter and more efficient route.

simplification_rules = {

('left', 'u-turn', 'right'): 'u-turn',

('left', 'u-turn', 'forward'): 'right',

('right', 'u-turn', 'left'): 'u-turn',

('forward', 'u-turn', 'left'): 'right',

('forward', 'u-turn', 'forward'): 'u-turn',

('left', 'u-turn', 'left'): 'forward',

}

This optimized path is later used during the speed run phase.

In the video, my robot traversed {right, right, left, left, right, u-turn, left, left, right, u-turn, left, left, right}

So the shortest path is calculated as follows -

  1. {right, right, left, left, right, u-turn, left, left, right, u-turn, left, left, right} -> RUL=U
  2. {right, right, left, left, u-turn, left, u-turn, left, left, right} -> LUL=F
  3. {right, right, left, fwd, u-turn, left, right} -> FUL=R
  4. {right, right, left, right, right}

So our shortest path became - {right, right, left, right, right}

Source Code

code.PNG

Arduino Code

#include <AccelStepper.h>

// Motor 1 connections (Left Motor - ULN2003)
const int M1_IN1 = 7;
const int M1_IN2 = 6;
const int M1_IN3 = 5;
const int M1_IN4 = 4;

// Motor 2 connections (Right Motor - ULN2003)
const int M2_IN1 = 8;
const int M2_IN2 = 9;
const int M2_IN3 = 10;
const int M2_IN4 = 11;
const int LED = 13;

// Define motors using AccelStepper in HALF4WIRE mode
AccelStepper motorLeft(AccelStepper::HALF4WIRE, M1_IN1, M1_IN3, M1_IN2, M1_IN4);
AccelStepper motorRight(AccelStepper::HALF4WIRE, M2_IN1, M2_IN3, M2_IN2, M2_IN4);

// Movement parameters
const int MAX_SPEED = 250; // steps per second
const int MAX_ACCEL = 500;
const int STEP_COUNT_FWD = 100;
const int STEP_COUNT_TURN_FWD = 3490;
const int STEP_COUNT_TURN = 2100;
const int STEP_COUNT_UTURN = 1600;

String input = "";

void setup()
{
Serial.begin(115200);
motorLeft.setAcceleration(MAX_ACCEL);
motorRight.setAcceleration(MAX_ACCEL);
motorLeft.setMaxSpeed(MAX_SPEED);
motorRight.setMaxSpeed(MAX_SPEED);
pinMode(LED, OUTPUT);
digitalWrite(LED, LOW);
}

void loop()
{
if (Serial.available() > 0)
{
input = Serial.readStringUntil('\n');
Serial.println(input);
input.trim();
if (input.equalsIgnoreCase("f"))
{
digitalWrite(LED, HIGH);
motorLeft.move(STEP_COUNT_FWD);
motorRight.move(STEP_COUNT_FWD);
runUntilDone();
}
else if (input.equalsIgnoreCase("s"))
{
motorLeft.stop();
motorRight.stop();
}
else if (input.equalsIgnoreCase("l"))
{
motorLeft.move(STEP_COUNT_TURN_FWD);
motorRight.move(STEP_COUNT_TURN_FWD);
runUntilDone();
motorLeft.move(-STEP_COUNT_TURN);
motorRight.move(STEP_COUNT_TURN);
runUntilDone();
}
else if (input.equalsIgnoreCase("r"))
{
motorLeft.move(STEP_COUNT_TURN_FWD);
motorRight.move(STEP_COUNT_TURN_FWD);
runUntilDone();
motorLeft.move(STEP_COUNT_TURN);
motorRight.move(-STEP_COUNT_TURN);
runUntilDone();
}
else if (input.equalsIgnoreCase("u"))
{
motorLeft.move(STEP_COUNT_TURN_FWD);
motorRight.move(STEP_COUNT_TURN_FWD);
runUntilDone();
motorLeft.move(-STEP_COUNT_TURN);
motorRight.move(STEP_COUNT_TURN);
runUntilDone();
motorLeft.move(-STEP_COUNT_TURN-1100);
motorRight.move(STEP_COUNT_TURN-1100);
runUntilDone();
}
else if (input.startsWith("al"))
{
digitalWrite(LED, LOW);
int val = input.substring(2).toInt();
motorLeft.move(-val);
motorRight.move(val);
runUntilDone();
}
else if (input.startsWith("ar"))
{
int val = input.substring(2).toInt();
motorLeft.move(val);
motorRight.move(-val);
runUntilDone();
}
}
motorLeft.run();
motorRight.run();
}

void runUntilDone()
{
while (motorLeft.distanceToGo() != 0 || motorRight.distanceToGo() != 0) {
motorLeft.run();
motorRight.run();
}
}

ESP32 Code

#include <WiFi.h>

const char* ssid = "Wifi_SSID";
const char* password = "ABCD_PASS";

WiFiServer server(80);
WiFiClient client;

String command = "";

void setup()
{
// UART0
Serial.begin(115200);

WiFi.begin(ssid, password);

Serial.print("Connecting to WiFi");

while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}

Serial.println();
Serial.println("WiFi Connected");
Serial.print("ESP IP Address: ");
Serial.println(WiFi.localIP());

server.begin();
Serial.println("TCP Server Started");
}

void loop()
{
// Check for new client
if (!client || !client.connected())
{
client = server.available();
return;
}

// Read incoming command
while (client.available())
{
char c = client.read();

// End of command
if (c == '\n')
{
command.trim();

Serial.print("Received: ");
Serial.println(command);

processCommand(command);

command = "";
}
else
{
command += c;
}
}
}

void processCommand(String cmd)
{
// Basic movement commands
if (cmd == "f")
{
Serial.println("f");
}
else if (cmd == "b")
{
Serial.println("b");
}
else if (cmd == "l")
{
Serial.println("l");
}
else if (cmd == "r")
{
Serial.println("r");
}
else if (cmd == "s")
{
Serial.println("s");
}
else if (cmd == "u")
{
Serial.println("u");
}

// Adjust Right
else if (cmd.startsWith("ar(") && cmd.endsWith(")"))
{
int startIndex = cmd.indexOf('(') + 1;
int endIndex = cmd.indexOf(')');

String valueStr = cmd.substring(startIndex, endIndex);
int value = valueStr.toInt();

Serial.print("ar:");
Serial.println(value);
}

// Adjust Left
else if (cmd.startsWith("al(") && cmd.endsWith(")"))
{
int startIndex = cmd.indexOf('(') + 1;
int endIndex = cmd.indexOf(')');

String valueStr = cmd.substring(startIndex, endIndex);
int value = valueStr.toInt();

Serial.print("al:");
Serial.println(value);
}

else
{
Serial.println("Unknown Command");
}
}

Python Code

import cv2
import numpy as np
import time
from collections import deque
import threading
import queue
import socket

# ==============================
# CONFIGURABLE CONSTANTS
# ==============================
WIDTH = 480
HEIGHT = 640
PORT = 'COM3'
BAUD = 115200
TIMEOUT = 0.1
VIDEO_STREAM_URL = "http://192.168.1.11:8080/video"

esp_ip = '192.168.1.10' # ESP32's IP
esp_port = 80

LEFT_THRESHOLD = 7000
RIGHT_THRESHOLD = 7000
FORWARD_THRESHOLD = 7000
BOTTOM_THRESHOLD = 7000
TOP_RIGHT_THRESHOLD = 6000

FORWARD_INTERVAL = 1.0
ADJUSTMENT_INTERVAL = 2.0
TURN_DELAY = 30 #20
UTURN_DELAY = 40 #50

KP = 0.8
CENTER_TOLERANCE = 15
MIN_INPUT = -50
MAX_INPUT = 50
MIN_PWM = 1
MAX_PWM = 50

DIRECTION_STABILITY_FRAMES = 3

ATTEMPTFORLR = 50
ATTEMPTFORLRCOPY = ATTEMPTFORLR
ATTEMPTFORU = 100

cmd_queue = queue.Queue()
is_busy = False
recovering = False
last_turn = "left"
recovery_sequence = 0

robot_stopped = False
traversed_path = []
short_path = []
short_path_index = 0
last_path_append_time = 0
FORWARD_APPEND_COOLDOWN = 5.0

is_backtracking = False

def serial_writer():
global is_busy, sock
while True:
cmd, delay = cmd_queue.get()
is_busy = True
print(f"[Thread] Sending command: {cmd}")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((esp_ip, esp_port))
s.sendall(cmd.encode())
time.sleep(delay)
is_busy = False

threading.Thread(target=serial_writer, daemon=True).start()

cap = cv2.VideoCapture(VIDEO_STREAM_URL)

recent_moves = deque(maxlen=DIRECTION_STABILITY_FRAMES)
last_move_type = None
last_move_time = 0

def send_command(cmd, delay=0):
cmd_queue.put((cmd, delay))

def scale_pwm(value):
value = max(MIN_INPUT, min(value, MAX_INPUT))
abs_value = abs(value)
return int((abs_value / MAX_INPUT) * (MAX_PWM - MIN_PWM) + MIN_PWM)

def get_direction_and_centroid(frame):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blur, 60, 255, cv2.THRESH_BINARY_INV)

height, width = binary.shape

left_box = (int(width * 0.05), int(height * 0.55), int(width * 0.15), int(height * 0.25))
right_box = (int(width * 0.8), int(height * 0.55), int(width * 0.15), int(height * 0.25))
bottom_box = (int(width * 0.3), int(height * 0.85), int(width * 0.4), int(height * 0.15))
top_box = (int(width * 0.3), int(height * 0.05), int(width * 0.4), int(height * 0.15))
top_right_box = (int(width * 0.8), int(height * 0.05), int(width * 0.15), int(height * 0.15))

left_roi = binary[left_box[1]:left_box[1]+left_box[3], left_box[0]:left_box[0]+left_box[2]]
right_roi = binary[right_box[1]:right_box[1]+right_box[3], right_box[0]:right_box[0]+right_box[2]]
bottom_roi = binary[bottom_box[1]:bottom_box[1]+bottom_box[3], bottom_box[0]:bottom_box[0]+bottom_box[2]]
top_roi = binary[top_box[1]:top_box[1]+top_box[3], top_box[0]:top_box[0]+top_box[2]]
top_right_roi = binary[top_right_box[1]:top_right_box[1]+top_right_box[3], top_right_box[0]:top_right_box[0]+top_right_box[2]]

left_black = cv2.countNonZero(left_roi)
right_black = cv2.countNonZero(right_roi)
bottom_black = cv2.countNonZero(bottom_roi)
top_black = cv2.countNonZero(top_roi)
top_right_black = cv2.countNonZero(top_right_roi)

moments = cv2.moments(bottom_roi)
cx = int(moments["m10"] / moments["m00"]) + bottom_box[0] if moments["m00"] != 0 else WIDTH // 2

cv2.rectangle(frame, (left_box[0], left_box[1]), (left_box[0]+left_box[2], left_box[1]+left_box[3]), (0, 255, 0), 2)
cv2.rectangle(frame, (right_box[0], right_box[1]), (right_box[0]+right_box[2], right_box[1]+right_box[3]), (0, 255, 0), 2)
cv2.rectangle(frame, (bottom_box[0], bottom_box[1]), (bottom_box[0]+bottom_box[2], bottom_box[1]+bottom_box[3]), (0, 255, 255), 2)
cv2.rectangle(frame, (top_box[0], top_box[1]), (top_box[0]+top_box[2], top_box[1]+top_box[3]), (0, 0, 255), 2)
cv2.rectangle(frame, (top_right_box[0], top_right_box[1]), (top_right_box[0]+top_right_box[2], top_right_box[1]+top_right_box[3]), (255, 0, 255), 2)

cv2.line(frame, (WIDTH // 2, bottom_box[1]), (WIDTH // 2, bottom_box[1]+bottom_box[3]), (0, 0, 255), 2)
cv2.circle(frame, (cx, bottom_box[1]+bottom_box[3]//2), 5, (0, 0, 255), -1)

cv2.putText(frame, f"L:{left_black}", (left_box[0], left_box[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.putText(frame, f"B:{bottom_black}", (bottom_box[0], bottom_box[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
cv2.putText(frame, f"R:{right_black}", (right_box[0], right_box[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
cv2.putText(frame, f"T:{top_black}", (top_box[0], top_box[1]-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

direction_flags = {
"left": ((left_black > LEFT_THRESHOLD) or (right_black>left_black>2000)),
"right": (right_black > RIGHT_THRESHOLD),
"forward": (top_black > FORWARD_THRESHOLD),
"bottom": (bottom_black > BOTTOM_THRESHOLD),
"stop": (top_right_black > TOP_RIGHT_THRESHOLD)
}
#print("TOP BLAC:",top_right_black)
return direction_flags, cx, frame

def get_shortest_path(traversed_path):
stack = []
simplification_rules = {
('left', 'u-turn', 'right'): 'u-turn',
('left', 'u-turn', 'forward'): 'right',
('right', 'u-turn', 'left'): 'u-turn',
('forward', 'u-turn', 'left'): 'right',
('forward', 'u-turn', 'forward'): 'u-turn',
('left', 'u-turn', 'left'): 'forward',
}
i = 0
while i < len(traversed_path):
stack.append(traversed_path[i])
# Try to reduce the last 3 items if they match any rule
while len(stack) >= 3:
last_three = tuple(stack[-3:])
if last_three in simplification_rules:
# Replace last 3 with the reduced move
stack[-3:] = [simplification_rules[last_three]]
else:
break
i += 1
return stack

# ==============================
# MAIN LOOP
# ==============================

while True:
ret, frame = cap.read()
if not ret or frame is None:
print("Failed to grab frame. Retrying...")
break

frame = cv2.resize(frame, (WIDTH, HEIGHT))
current_time = time.time()

k = cv2.waitKey(1)
if k & 0xFF == ord('q'):
#close_ser()
cap.release()
cv2.destroyAllWindows()
break
if k & 0xFF == ord('e'):
print("Shortest Path: ", short_path, len(short_path))
short_path_index = 0
is_backtracking = True
robot_stopped = False

directions, cx, frame = get_direction_and_centroid(frame)
recent_moves.append(tuple(directions.items()))

stable = all(m == recent_moves[0] for m in recent_moves)

move = None
move_type = None

left = directions['left']
right = directions['right']
forward = directions['forward']
bottom = directions['bottom']
stop = directions['stop']

if stable and not is_busy and not recovering and not is_backtracking and not robot_stopped:
print("Traversed Path: ", traversed_path)
move = None
move_type = None
if stop and left and bottom:
move = "stop"
move_type = "stop"
print("Decision: Stop")
send_command("stop", 10)
last_turn = "stop"
print("Traversed Path: ",traversed_path)
short_path = get_shortest_path(traversed_path)
print("Shortest Path: ", short_path, len(short_path))
robot_stopped = True

elif left or (left and bottom) or (left and bottom and forward) or (left and right and bottom and forward):
move = "left"
move_type = "turn"
traversed_path.append(move)
print("Decision: Left turn")
send_command("l", TURN_DELAY)
last_turn = "left"
recovery_sequence = 0
recovering = True

elif (bottom and forward) or (bottom and forward and right):
deviation = cx - WIDTH // 2
if abs(deviation) < CENTER_TOLERANCE:
move = "forward"
move_type = "forward"
if bottom and forward and right:
if current_time - last_path_append_time > FORWARD_APPEND_COOLDOWN:
traversed_path.append("forward")
last_path_append_time = current_time
if move_type != last_move_type or (current_time - last_move_time) >= FORWARD_INTERVAL:
print("Decision: forward")
send_command("f", FORWARD_INTERVAL)
last_move_time = current_time
last_move_type = move_type
else:
direction = "right" if deviation > 0 else "left"
pwm_value = scale_pwm(KP * deviation)
move = f"adjust {direction} ({pwm_value})"
move_type = "adjust"
if move_type != last_move_type or (current_time - last_move_time) >= ADJUSTMENT_INTERVAL:
print("Decision:", move)
command = f"ar{pwm_value}" if direction == "right" else f"al{pwm_value}"
send_command(command, ADJUSTMENT_INTERVAL)
last_move_time = current_time
last_move_type = move_type
elif right or (right and bottom):
move = "right"
move_type = "turn"
traversed_path.append(move)
print("Decision: Right turn")
send_command("r", TURN_DELAY)
last_turn = "right"
recovery_sequence = 0
recovering = True
elif bottom:
deviation = cx - WIDTH // 2
if abs(deviation) < CENTER_TOLERANCE:
move = "forward"
move_type = "forward"
if move_type != last_move_type or (current_time - last_move_time) >= FORWARD_INTERVAL:
print("Decision: forward")
send_command("f", FORWARD_INTERVAL)
last_move_time = current_time
last_move_type = move_type
else:
direction = "right" if deviation > 0 else "left"
pwm_value = scale_pwm(KP * deviation)
move = f"adjust {direction} ({pwm_value})"
move_type = "adjust"
if move_type != last_move_type or (current_time - last_move_time) >= ADJUSTMENT_INTERVAL:
print("Decision:", move)
command = f"ar{pwm_value}" if direction == "right" else f"al{pwm_value}"
send_command(command, ADJUSTMENT_INTERVAL)
last_move_time = current_time
last_move_type = move_type

elif not forward and not left and not right and not bottom:
move = "u-turn"
move_type = "uturn"
traversed_path.append(move)
print("Decision: U-turn")
send_command("u", UTURN_DELAY)
last_turn = "u"
recovery_sequence = 0
recovering = True
recovery_loop_last_time = time.time()

elif recovering and not is_busy:
recovery_every_sec = 1
recovery_current_time = time.time()
if (last_turn == "left" or last_turn == "u"):
if(recovery_current_time-recovery_loop_last_time>recovery_every_sec) and not is_busy and ATTEMPTFORLR >= 0:
send_command("al30", ADJUSTMENT_INTERVAL)
if directions['forward'] == True and directions['bottom'] == True:
recovering = False
ATTEMPTFORLR = ATTEMPTFORLRCOPY
ATTEMPTFORLR = ATTEMPTFORLR - 1
recovery_loop_last_time = recovery_current_time
if(recovery_current_time-recovery_loop_last_time>recovery_every_sec) and not is_busy and ATTEMPTFORLR >= ATTEMPTFORLRCOPY*-2:
send_command("ar30", ADJUSTMENT_INTERVAL)
if directions['forward'] == True and directions['bottom'] == True:
recovering = False
ATTEMPTFORLR = ATTEMPTFORLRCOPY
ATTEMPTFORLR = ATTEMPTFORLR - 1
recovery_loop_last_time = recovery_current_time
elif last_turn == "right":
if(recovery_current_time-recovery_loop_last_time>recovery_every_sec) and not is_busy and ATTEMPTFORLR >= 0:
send_command("ar30", ADJUSTMENT_INTERVAL)
if directions['forward'] == True and directions['bottom'] == True:
recovering = False
ATTEMPTFORLR = ATTEMPTFORLRCOPY
ATTEMPTFORLR = ATTEMPTFORLR - 1
recovery_loop_last_time = recovery_current_time
if(recovery_current_time-recovery_loop_last_time>recovery_every_sec) and not is_busy and ATTEMPTFORLR >= ATTEMPTFORLRCOPY*-2:
send_command("al30", ADJUSTMENT_INTERVAL)
if directions['forward'] == True and directions['bottom'] == True:
recovering = False
ATTEMPTFORLR = ATTEMPTFORLRCOPY
ATTEMPTFORLR = ATTEMPTFORLR - 1
recovery_loop_last_time = recovery_current_time
elif recovering and is_busy:
if directions['bottom'] == False and directions['forward'] == False and directions['left'] == False and directions['right'] == False and recovery_sequence == 0:
print("Starting Recovery Switch Detection")
recovery_sequence += 1
elif recovery_sequence == 1 and (last_turn == "left" or last_turn == "u") and directions['right'] == True:
last_turn = "right"
print("Recovery Turn Switched")
recovery_sequence += 1
elif recovery_sequence == 1 and (last_turn == "right") and directions['left'] == True:
last_turn = "left"
print("Recovery Turn Switched")
recovery_sequence += 1

elif is_backtracking and not robot_stopped and not recovering and not is_busy:
if short_path_index < len(short_path):
looking_for = short_path[short_path_index]
else:
looking_for = "stop"
move = None
move_type = None

if stop and left and bottom:
print("Decision: Stop")
send_command("stop", 10)
robot_stopped = True

elif looking_for == "forward" and ((left and bottom and forward) or (right and bottom and forward) or (left and right and bottom and forward)):
deviation = cx - WIDTH // 2
if abs(deviation) < CENTER_TOLERANCE:
move = "forward"
move_type = "forward"
if move_type != last_move_type or (current_time - last_move_time) >= FORWARD_INTERVAL:
print("Looking For: ",looking_for," Decision: forward", short_path_index)
for i in range (0, 10):
send_command("f", FORWARD_INTERVAL)
last_move_time = current_time
last_move_type = move_type
short_path_index += 1
else:
direction = "right" if deviation > 0 else "left"
pwm_value = scale_pwm(KP * deviation)
move = f"adjust {direction} ({pwm_value})"
move_type = "adjust"
if move_type != last_move_type or (current_time - last_move_time) >= ADJUSTMENT_INTERVAL:
#print("Decision:", move)
command = f"ar{pwm_value}" if direction == "right" else f"al{pwm_value}"
send_command(command, ADJUSTMENT_INTERVAL)
last_move_time = current_time
last_move_type = move_type
elif looking_for == "left" and ((left and bottom) or (left and bottom and forward) or (left and right and bottom and forward)):
print("Looking For:",looking_for," Decision: Left turn", short_path_index)
send_command("l", TURN_DELAY)
last_turn = "left"
short_path_index += 1
recovery_sequence = 0
recovering = True

elif looking_for == "right" and ((right and bottom) or (right and bottom and forward) or (left and right and bottom and forward)):
print("Looking For:",looking_for," Decision: Right turn", short_path_index)
send_command("r", TURN_DELAY)
last_turn = "right"
short_path_index += 1
recovery_sequence = 0
recovering = True

elif (bottom and forward) or bottom:
deviation = cx - WIDTH // 2
if abs(deviation) < CENTER_TOLERANCE:
move = "forward"
move_type = "forward"
if move_type != last_move_type or (current_time - last_move_time) >= FORWARD_INTERVAL:
send_command("f", FORWARD_INTERVAL)
#print("Forward")
last_move_time = current_time
last_move_type = move_type
else:
direction = "right" if deviation > 0 else "left"
pwm_value = scale_pwm(KP * deviation)
move = f"adjust {direction} ({pwm_value})"
move_type = "adjust"
if move_type != last_move_type or (current_time - last_move_time) >= ADJUSTMENT_INTERVAL:
#print("Decision:", move)
command = f"ar{pwm_value}" if direction == "right" else f"al{pwm_value}"
send_command(command, ADJUSTMENT_INTERVAL)
last_move_time = current_time
last_move_type = move_type
elif not (forward and bottom and left and right):
print("Robot Stopped As Desired Path Not Found")
send_command("stop", 10)
robot_stopped = True
recovery_loop_last_time = time.time()

cv2.imshow("Maze View", frame)

Final Result & Future Improvements

ChatGPT Image May 14, 2026, 12_56_13 AM.png

The final robot can:

  1. Detect maze paths visually
  2. Make autonomous decisions
  3. Solve mazes using computer vision
  4. Replay optimized shortest paths

This project demonstrates how low-cost robotics combined with OpenCV can create powerful autonomous systems without expensive sensors.

Future Improvements

  1. Replace the 28BYJ-48 motors with faster and more accurate motors
  2. Move OpenCV processing to a Raspberry Pi or Jetson Nano for a fully standalone robot
  3. Increase camera frame rate to improve turn detection accuracy
  4. Better path planning algorithms

That's it for now. Thank you for reading this instructable. Hope this helped you. If it did then let me know by commenting. If you have any doubts or suggesstions, feel free to reach out.