Ball Tracking Robot
My project, the ball tracking robot, uses a combination of a Raspberry Pi, an ultrasonic sensor, and OpenCV to detect the presence of a red ball in the field of view of a camera and move towards it, navigating around obstacles. This project has potential applications in competitive robotics, as well as with robots that need to move towards a certain target, such as a person for rescue or delivery.
| Engineer | School | Area of Interest | Grade |
|---|---|---|---|
| Kavin V | Stratford Preparatory | Electrical Engineering | Incoming Freshman |

Final Milestone
For my third and final milestone, I had to put ultrasonic sensors on my robot to detect obstacles and move around them. Future work would incorporate this into my whole robot, This milestone is only the wiring and write sensor testing code.
The ultrasonic sensors, in order to detect obstacles, output a wave of sound at a frequency inaudible to humans. At the same time, my code gets the current time in seconds. When the sound wave bounces off a surface and is recieved back by the ultrasonic sensor, the sensor will output a signal to the Raspberry Pi, which will record the current time again. The time when the wave was cast is subtracted from the time the wave came back to the sensor, then multiplied by the speed of sound (34300 cm/s) to find the distance from the sensor (which is on the robot) to the obstacle (distance = speed * time.)
The sensor has 4 pins:
- VCC for power (it takes 5V from the Raspberry Pi),
- trig (meaning trigger – if this pin receives an input, the sensor will cast a sound wave),
- echo (which will output 5V when the sound wave is received back),
- and gnd (ground, which connects to the Raspberry Pi’s ground to make a complete circuit.)
I used a breadboard to organize my wiring. I also needed it because the ultrasonic sensor’s echo pin outputs 5 volts, which would fry the Raspberry Pi if outputted directly to it. I had to create voltage dividers on the breadboard to reduce the voltage to 3.3V, which the Raspberry Pi can handle, and then add another wire back to the GPIO pins of the Raspberry Pi.
Figure 1: A schematic showing the wiring of the ultrasonic sensors and voltage dividers
I faced a few challenges during this milestone. For one, my ultrasonic sensors would randomly stop and start working without explanation. I also had trouble understanding the concept of voltage dividers. However, I’m still happy I managed to do all of this in three days.
My biggest challenges at BSE were mostly related to my inexperience with both the hardware and software aspects of my project. I barely knew how to code, and I didn’t have any experience with designing circuits and wiring, major parts of my first and final milestones. However, with the help of online resources, I was able to push through. Other challenges I faced were ssh and VNC, the two methods I used to remotely access my raspberry pi, constantly breaking. My raspberry pi’s SD card was also corrupted twice, making me have to restart parts of my milestones. Although I wasn’t able to finish my third milestone, and potential modifications, such as image recognition, a package holder, and circle detection for the ball. I am happy with what I accomplished at Bluestamp.
Bluestamp has given me exposure to practical engineering, and equipped me with many new engineering skills. In the future, I want to explore more robotics and other fields of engineering, such as medical engineering, and utilize any opportunity to do so. Overall, Bluestamp was a great experience, and I took a lot away from it.
Here’s my code to test the ultrasonic sensors:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
trigRight = 27
echoRight = 17
trigFront = 6
echoFront = 13
trigLeft = 22
echoLeft = 5
GPIO.setup(trigRight, GPIO.OUT)
GPIO.setup(echoRight, GPIO.IN)
GPIO.setup(trigLeft, GPIO.OUT)
GPIO.setup(echoLeft, GPIO.IN)
GPIO.setup(trigFront, GPIO.OUT)
GPIO.setup(echoFront, GPIO.IN)
try:
while True:
GPIO.output(trigRight, GPIO.LOW)
GPIO.output(trigLeft, GPIO.LOW)
GPIO.output(trigFront, GPIO.LOW)
time.sleep(2)
GPIO.output(trigRight, GPIO.HIGH)
GPIO.output(trigLeft, GPIO.HIGH)
GPIO.output(trigFront, GPIO.HIGH)
time.sleep(0.00001)
GPIO.output(trigRight, GPIO.LOW)
GPIO.output(trigLeft, GPIO.LOW)
GPIO.output(trigFront, GPIO.LOW)
pulseOutRight, pulseOutLeft, pulseOutFront = None, None, None
pulseInRight, pulseInLeft, pulseInFront = None, None, None
while GPIO.input(echoRight) == 0:
pulseOutRight = time.time()
while GPIO.input(echoLeft) == 0:
pulseOutLeft = time.time()
while GPIO.input(echoFront) == 0:
pulseOutFront = time.time()
while GPIO.input(echoRight) == 1:
pulseInRight = time.time()
while GPIO.input(echoLeft) == 1:
pulseInLeft = time.time()
while GPIO.input(echoFront) == 1:
pulseInFront = time.time()
if pulseOutRight is not None and pulseInRight is not None:
pulseRight = (pulseInRight - pulseOutRight) / 2
distanceRight = 34300 * pulseRight # speed of sound (cm/s) = 34300
distanceRight = round(distanceRight, 2)
print("Object is at", distanceRight, "cm from the right ultrasonic sensor")
if pulseOutLeft is not None and pulseInLeft is not None:
pulseLeft = (pulseInLeft - pulseOutLeft) / 2
distanceLeft = 34300 * pulseLeft
distanceLeft = round(distanceLeft, 2)
print("Object is at", distanceLeft, "cm from the left ultrasonic sensor")
if pulseOutFront is not None and pulseInFront is not None:
pulseFront = (pulseInFront - pulseOutFront) / 2
distanceFront = 34300 * pulseFront
distanceFront = round(distanceFront, 2)
print("Object is at", distanceFront, "cm from the front ultrasonic sensor")
except KeyboardInterrupt:
print("Stopped")
finally:
GPIO.cleanup()
Second Milestone
For my second milestone, I had to attach a camera to my raspberry pi and use it to set up a ball tracking algorithm. This required:
- Mounting the camera on the robot,
- Connecting it to the raspberry pi,
- Installing several libraries for the camera,
- And writing an algorithm that would detect the ball by color and size, and turn and move towards it when sighted.
I also added the switch to my circuit, which I had originally planned to do in milestone 1. This was helpful in making sure I didn’t have to manually disconnect and reconnect my circuit to conserve battery.
A ribbon cable connnects the camera, which is glued onto the motor base, to the Raspberry Pi. The Raspberry Pi interfaces with the camera using the picamera2 library. It also uses the OpenCV computer vision library to erode and dilate the camera view in order to isolate the ball, then puts a mask over the camera view that extracts only the red color of the ball.
| Original | Eroded and Dilated | Masked |
|---|---|---|
![]() |
![]() |
![]() |
Figure 2: Comparison of the different stages of processing that the camera view goes through
From this masked image, the x and y coordinates of the ball can be found. If the coordinates are near the center of the camera view, the robot moves forward. Otherwise, depending on if the ball is on the left or right, the robot will turn towards the ball until the ball is centered in the camera view, then move forward. If the ball suddenly disappears from the camera view, the robot will take the last seen x and y of the ball, and depending on where that is in the camera view, turn left or right, searching for the ball. The Raspberry Pi’s GPIO pins are connected to the motors through a driver, and different functions are called to set these pins to high to turn on different motors.
Figure 3: A flowchart showing my code’s logic
This milestone, being almost entirely software-based, came with many setbacks. My Raspberry Pi’s micro-SD card was corrupted twice after its power randomly switched back off and on, requiring me to set up the Raspberry Pi multiple times, consuming a lot of my time. The Raspberry Pi’s ssh and VNC also kept breaking, making me switch to using OBS with a video capture card, HDMI cable, mouse, and keyboard. In addition, I had trouble finding what I needed in the documentation for both OpenCV and picamera2 due to inexperience with this and coding in general. I ran into a glitch near the end of completing this milestone where my code would run without errors, but the motors would not turn; this took several days to solve.
This is my code:
import cv2
import numpy as np
import RPi.GPIO as GPIO
from picamera2 import Picamera2, Preview, MappedArray
import time
# set up motors; this needs to be global for move functions to work and so is outside a function
GPIO.setmode(GPIO.BCM) # set the way that the GPIO pins are referred to in the code
motor1B = 23
motor1E = 24
ena = 25
motor2B = 26
motor2E = 16
enb = 12
GPIO.setup(motor1B, GPIO.OUT)
GPIO.setup(motor1E, GPIO.OUT)
GPIO.setup(ena, GPIO.OUT)
GPIO.setup(motor2B, GPIO.OUT)
GPIO.setup(motor2E, GPIO.OUT)
GPIO.setup(enb, GPIO.OUT)
pwmA = GPIO.PWM(ena, 100)
pwmB = GPIO.PWM(enb, 100)
pwmA.start(60)
pwmB.start(60)
#return motor1B, motor1E, motor2B, motor2E, ena, enb
def setupCam():
print ("in setupCam")
picam2 = Picamera2() # allows Picamera2 to be referred to as picam2
def preview(request):
with MappedArray(request, "main") as m: # put other code to retrieve 'main'
pass
picam2.pre_callback = preview
time.sleep(1) # wait 1 second
picam2.start(show_preview=True) # starts the camera along a live preview window
return picam2
def test(picam2):
print ("in test")
while True:
frame = picam2.capture_array() # captures one image using camera
segmentColor(frame)
def segmentColor(frame): #creates a mask over 'frame' with only the ball's color coming through
print ("in segment_colour")
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mask_1 = cv2.inRange(rgb, np.array([50,40,170]), np.array([100,70,240])) # range of RGB values to appear through the mask
mask = mask_1
kern_dilate = np.ones((8,8),np.uint8)
kern_erode = np.ones((3,3),np.uint8)
frame = cv2.resize(frame, (320, 240))
mask = cv2.resize(mask, (320, 240)) # resize to reduce resolution/improve performance
mask= cv2.erode(mask,kern_erode) # erode to approximate color
mask=cv2.dilate(mask,kern_dilate) # dilate to blur
(h,w) = mask.shape
cv2.imshow('frame',frame)
cv2.imshow('mask', mask) # Shows mask (B&W frame with identified red pixels)
findArea(mask, frame)
return mask, frame
def findArea(mask, frame): # find largest red object within the mask
print ("in findArea")
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
biggestContour = 0.0
contour_index = 0
for idx, contour in enumerate(contours): # find the biggest contours in the masked
area = cv2.contourArea(contour)
if area > biggestContour:
biggestContour = area
contour_index = idx
location=(0,0,2,2)
if len(contours) > 0: # if contours are in the masked view, find their x and y
location = cv2.boundingRect(contours[contour_index])
lastSeenX = 0
lastSeenY = 0
actOnBall(location, lastSeenX, lastSeenY)
return None
def actOnBall(location, lastSeenX, lastSeenY):
print ("in actOnBall")
x, y, width, height = location
print("x:"+str(x))
print("y:"+str(y))
if (width*height) > 300:
print ("ball")
if x<=160:
if 130<x<190:
pass
else:
# turn left until ball centered
print ("in turn left")
turnLeft()
else:
# turn right until ball centered
if 130<x<190:
pass
else:
print ("in turn right")
turnRight()
# move forward until contour area greater than a certain amount
moveForward()
if (width*height)>= 800: # if the size of the ball in the camera feed is big enough, stop
pass
else:
if lastSeenX<=320:
turnLeft()
else:
turnRight()
else:
print("no ball")
lastSeenX = x
lastSeenY = y
def turnRight(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnRight")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.LOW)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def turnLeft(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnLeft")
GPIO.output(motor1B,GPIO.LOW)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def moveBackward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveForward")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.1)
def moveForward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveBackward")
GPIO.output(motor1E,GPIO.HIGH)
GPIO.output(motor1B, GPIO.LOW)
GPIO.output(motor2E, GPIO.HIGH)
GPIO.output(motor2B, GPIO.LOW)
print("----------------")
time.sleep(0.1)
while True:
camready = setupCam()
test(camready)
if cv2.waitKey(1) & 0xff == ord('q'): # if key q pressed, break loop to close all windows
break
cv2.destroyAllWindows()
First Milestone
In my first milestone, I got the motors to turn on my robot. This involved:
- Constructing the base of the robot,
- Configuring the Raspberry Pi to work with ssh and the app VNC Viewer on my laptop for remote control,
- Attaching the motors to the motor driver and Raspberry Pi,
- And running basic code in Python to ensure the functionality of the components.
The base of the robot is made up of several components: two DC motors, a plastic base plate with holes for screws to attach parts, a 6 volt battery pack, two wheels to screw onto the chassis of the motors, a switch, and one more wheel that attaches to one end of the robot, and can rotate back and forth depending on the motion of the robot, providing stability. Screws hold these parts in place on the baseplate. Remaining space on the baseplate allows for additional components, such as a motor driver and Raspberry Pi, to be attached.
Figure 4: A schematic showing the state of my robot as of the first milestone
Power goes from the battery pack to the H-bridge motor driver (which serves as a terminal to toggle and redirect power) through the “5V” input slot. The motor driver requires at least 5 volts to power its own processor to redirect power. Output slots are used to connect the motors to the H-bridge. The H-bridge also has input pins, which can receive instructions from another device such as a Raspberry Pi to toggle power to certain output slots. The Raspberry Pi has GPIO (general purpose input-output) pins on it, which can receive and output instructions to and from another device. The pins of the two devices are connected via jumper wires, allowing for code on the Raspberry Pi to control the motor driver.
The motor driver has 6 input pins: ENA, ENB, IN1, IN2, IN3, and IN4. ENA and ENB stand for Enable Motor A and B. Each pin provides instructions for whether or not the motors should be turned on from the Raspberry Pi. These pins must be digital pins capable of outputting code instructions in the form of PWM (pulse width modulation) instructions, which tell the H-bridge what the duty cycle (or fraction of a certain period of time) the power should be turned on and at what strength in a format that the H-Bridge understands. The IN (input) pins provide a way for the Raspberry Pi to give instructions to the H-Bridge on the direction of the motor, and are connected to additional GPIO pins. The circuit is completed by connecting the ground of the Raspberry Pi to the ground of the H-bridge and battery pack.
I faced challenges with implementing the switch; I couldn’t solder it to my wires effectively, so I decided to leave it out and instead, for now, just disconnected the power to turn off the robot. The electrical engineering aspect of this milestone was also completely new to me, so I struggled with this; however, I was able to push through by learning what I needed to online. The Raspberry Pi also kept bugging out and not working with ssh and VNC Viewer, making me have to attach the Raspberry Pi to a monitor, mouse, and keyboard to use it. So far, I haven’t been able to find the cause of this problem.
In my next milestone, the robot will have a camera, and be able to recognize and move towards a red ball.
Here’s my code for testing the motors:
import RPi.GPIO as GPIO
import time
GPIO.setmode(GPIO.BCM)
MOTOR1B = 23
MOTOR1E = 24
ena = 25
MOTOR2B = 16
MOTOR2E = 26
enb = 12
GPIO.setup(MOTOR1B, GPIO.OUT)
GPIO.setup(MOTOR1E, GPIO.OUT)
GPIO.setup(ena, GPIO.OUT)
GPIO.setup(MOTOR2B, GPIO.OUT)
GPIO.setup(MOTOR2E, GPIO.OUT)
GPIO.setup(enb, GPIO.OUT)
pwmA = GPIO.PWM(ena, 100)
pwmB = GPIO.PWM(enb, 100)
pwmA.start(60)
pwmB.start(60)
GPIO.output(MOTOR1B,GPIO.HIGH)
GPIO.output(MOTOR1E, GPIO.LOW)
GPIO.output(MOTOR2B, GPIO.HIGH)
GPIO.output(MOTOR2E, GPIO.LOW)
time.sleep(10)
Schematics
Code
import cv2
import numpy as np
import RPi.GPIO as GPIO
from picamera2 import Picamera2, Preview, MappedArray
import time
# set up motors; this needs to be global for move functions to work and so is outside a function
GPIO.setmode(GPIO.BCM) # set the way that the GPIO pins are referred to in the code
motor1B = 23
motor1E = 24
ena = 25
motor2B = 26
motor2E = 16
enb = 12
GPIO.setup(motor1B, GPIO.OUT)
GPIO.setup(motor1E, GPIO.OUT)
GPIO.setup(ena, GPIO.OUT)
GPIO.setup(motor2B, GPIO.OUT)
GPIO.setup(motor2E, GPIO.OUT)
GPIO.setup(enb, GPIO.OUT)
pwmA = GPIO.PWM(ena, 100)
pwmB = GPIO.PWM(enb, 100)
pwmA.start(60)
pwmB.start(60)
def setupCam():
print ("in setupCam")
picam2 = Picamera2() # allows Picamera2 to be referred to as picam2
def preview(request):
with MappedArray(request, "main") as m: # put other code to retrieve 'main'
pass
picam2.pre_callback = preview
time.sleep(1) # wait 1 second
picam2.start(show_preview=True) # starts the camera along a live preview window
return picam2
def test(picam2):
print ("in test")
while True:
frame = picam2.capture_array() # captures one image using camera
segmentColor(frame)
def segmentColor(frame): #creates a mask over 'frame' with only the ball's color coming through
print ("in segment_colour")
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mask_1 = cv2.inRange(rgb, np.array([50,40,170]), np.array([100,70,240])) # range of RGB values to appear through the mask
mask = mask_1
kern_dilate = np.ones((8,8),np.uint8)
kern_erode = np.ones((3,3),np.uint8)
frame = cv2.resize(frame, (320, 240))
mask = cv2.resize(mask, (320, 240)) # resize to reduce resolution/improve performance
mask= cv2.erode(mask,kern_erode) # erode to approximate color
mask=cv2.dilate(mask,kern_dilate) # dilate to blur
(h,w) = mask.shape
cv2.imshow('frame',frame)
cv2.imshow('mask', mask) # Shows mask (B&W frame with identified red pixels)
findArea(mask, frame)
return mask, frame
def findArea(mask, frame): # find largest red object within the mask
print ("in findArea")
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
biggestContour = 0.0
contour_index = 0
for idx, contour in enumerate(contours): # find the biggest contours in the masked
area = cv2.contourArea(contour)
if area > biggestContour:
biggestContour = area
contour_index = idx
location=(0,0,2,2)
if len(contours) > 0: # if contours are in the masked view, find their x and y
location = cv2.boundingRect(contours[contour_index])
lastSeenX = 0
lastSeenY = 0
actOnBall(location, lastSeenX, lastSeenY)
return None
def actOnBall(location, lastSeenX, lastSeenY):
print ("in actOnBall")
x, y, width, height = location
print("x:"+str(x))
print("y:"+str(y))
if (width*height) > 300:
print ("ball")
if x<=160:
if 130<x<190:
pass
else:
# turn left until ball centered
print ("in turn left")
turnLeft()
else:
# turn right until ball centered
if 130<x<190:
pass
else:
print ("in turn right")
turnRight()
# move forward until contour area greater than a certain amount
moveForward()
if (width*height)>= 800: # if the size of the ball in the camera feed is big enough, stop
pass
else:
if lastSeenX<=320:
turnLeft()
else:
turnRight()
else:
print("no ball")
lastSeenX = x
lastSeenY = y
def turnRight(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnRight")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.LOW)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def turnLeft(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnLeft")
GPIO.output(motor1B,GPIO.LOW)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def moveBackward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveForward")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.1)
def moveForward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveBackward")
GPIO.output(motor1E,GPIO.HIGH)
GPIO.output(motor1B, GPIO.LOW)
GPIO.output(motor2E, GPIO.HIGH)
GPIO.output(motor2B, GPIO.LOW)
print("----------------")
time.sleep(0.1)
while True:
camready = setupCam()
test(camready)
if cv2.waitKey(0) import cv2
import numpy as np
import RPi.GPIO as GPIO
from picamera2 import Picamera2, Preview, MappedArray
import time
# set up motors; this needs to be global for move functions to work and so is outside a function
GPIO.setmode(GPIO.BCM) # set the way that the GPIO pins are referred to in the code
motor1B = 23
motor1E = 24
ena = 25
motor2B = 26
motor2E = 16
enb = 12
GPIO.setup(motor1B, GPIO.OUT)
GPIO.setup(motor1E, GPIO.OUT)
GPIO.setup(ena, GPIO.OUT)
GPIO.setup(motor2B, GPIO.OUT)
GPIO.setup(motor2E, GPIO.OUT)
GPIO.setup(enb, GPIO.OUT)
pwmA = GPIO.PWM(ena, 100)
pwmB = GPIO.PWM(enb, 100)
pwmA.start(60)
pwmB.start(60)
#return motor1B, motor1E, motor2B, motor2E, ena, enb
def setupCam():
print ("in setupCam")
picam2 = Picamera2() # allows Picamera2 to be referred to as picam2
def preview(request):
with MappedArray(request, "main") as m: # put other code to retrieve 'main'
pass
picam2.pre_callback = preview
time.sleep(1) # wait 1 second
picam2.start(show_preview=True) # starts the camera along a live preview window
return picam2
def test(picam2):
print ("in test")
while True:
frame = picam2.capture_array() # captures one image using camera
segmentColor(frame)
def segmentColor(frame): #creates a mask over 'frame' with only the ball's color coming through
print ("in segment_colour")
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
mask_1 = cv2.inRange(rgb, np.array([50,40,170]), np.array([100,70,240])) # range of RGB values to appear through the mask
mask = mask_1
kern_dilate = np.ones((8,8),np.uint8)
kern_erode = np.ones((3,3),np.uint8)
frame = cv2.resize(frame, (320, 240))
mask = cv2.resize(mask, (320, 240)) # resize to reduce resolution/improve performance
mask= cv2.erode(mask,kern_erode) # erode to approximate color
mask=cv2.dilate(mask,kern_dilate) # dilate to blur
(h,w) = mask.shape
cv2.imshow('frame',frame)
cv2.imshow('mask', mask) # Shows mask (B&W frame with identified red pixels)
findArea(mask, frame)
return mask, frame
def findArea(mask, frame): # find largest red object within the mask
print ("in findArea")
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
biggestContour = 0.0
contour_index = 0
for idx, contour in enumerate(contours): # find the biggest contours in the masked
area = cv2.contourArea(contour)
if area > biggestContour:
biggestContour = area
contour_index = idx
location=(0,0,2,2)
if len(contours) > 0: # if contours are in the masked view, find their x and y
location = cv2.boundingRect(contours[contour_index])
lastSeenX = 0
lastSeenY = 0
actOnBall(location, lastSeenX, lastSeenY)
return None
def actOnBall(location, lastSeenX, lastSeenY):
print ("in actOnBall")
x, y, width, height = location
print("x:"+str(x))
print("y:"+str(y))
if (width*height) > 300:
print ("ball")
if x<=160:
if 130<x<190:
pass
else:
# turn left until ball centered
print ("in turn left")
turnLeft()
else:
# turn right until ball centered
if 130<x<190:
pass
else:
print ("in turn right")
turnRight()
# move forward until contour area greater than a certain amount
moveForward()
if (width*height)>= 800: # if the size of the ball in the camera feed is big enough, stop
pass
else:
if lastSeenX<=320:
turnLeft()
else:
turnRight()
else:
print("no ball")
lastSeenX = x
lastSeenY = y
def turnRight(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnRight")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.LOW)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def turnLeft(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in turnLeft")
GPIO.output(motor1B,GPIO.LOW)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.05)
def moveBackward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveForward")
GPIO.output(motor1B,GPIO.HIGH)
GPIO.output(motor1E, GPIO.LOW)
GPIO.output(motor2B, GPIO.HIGH)
GPIO.output(motor2E, GPIO.LOW)
print("----------------")
time.sleep(0.1)
def moveForward(motor1B=23, motor1E=24, motor2B=26, motor2E=16):
print ("in moveBackward")
GPIO.output(motor1E,GPIO.HIGH)
GPIO.output(motor1B, GPIO.LOW)
GPIO.output(motor2E, GPIO.HIGH)
GPIO.output(motor2B, GPIO.LOW)
print("----------------")
time.sleep(0.1)
while True:
camready = setupCam()
test(camready)
if cv2.waitKey(1) & 0xff == ord('q'): # if key q pressed, break loop to close all windows
break
cv2.destroyAllWindows()
Bill of Materials
| Part | Note | Price | Link |
|---|---|---|---|
| Canakit Raspberry Pi 4 4GB Starter Kit - 32GB | Used to control and deploy code on the robot | $119.95 | Link |
| L298N H-Bridge Motor Driver | Used to output PWM signals from Raspberry Pi to motors | $6.99 | Link |
| Arducam 5MP OV5647 Camera | Used for detection of the ball by color | $8.99 | Link |
| HC-SR04 Ultrasonic Sensor (3) | Used for obstacle detection | $13.50 | Link |
| Emozny 2-Wheel Robot Car Kit | Is the base of the robot and includes the motors and wheels for movement | $12.99 | Link |
| Breadboard | To organize wiring and create voltage dividers | $5.00 | Link |
| AA Batteries | To power the motors | $6.64 | Link |
| INIU Power Bank | To power the Raspberry Pi | $22.99 | Link |
Resources:
https://learn.adafruit.com/python-virtual-environment-usage-on-raspberry-pi/basic-venv-usage
https://docs.opencv.org/3.4/d6/d00/tutorial_py_root.html
https://www.engineersgarage.com/opencv4-5-raspberry-pi-image-video-access-recording/
Starter Project: RGB Sliders
My starter project was the RGB Sliders, which I chose because I thought a customizable light would look cool, and I’d learn a lot from the slider mechanism. The device has three 7-pin sliders for different colors, red, green, and blue, which control the color of a 4-pin LED light, a board for the components, and a micro-USB port for power. The LED contains three diodes, devices that allow current to flow in one direction, made of silicon semiconductors, that each emit different wavelengths of color. This is due to electricity emitting photons of different wavelengths on the visible spectrum – red, green, and blue. Each diode is manufactured a certain way to emit a certain wavelength of light. The different diodes are concentrated close together enough that the illusion of the LED emitting one singular color is created, as the colors from the different diodes appear to blend together.
Current flows from the micro-USB port to the sliders, which control the flow of current to capacitors. These capacitors control the amount of power the diodes recieve and so their brightness. For example, if the green slider were set to 0 and the red and blue sliders each set to max, the green diode would not light up, while both the red and blue ones would be at full brightness. The red and blue would appear to blend together to create the color purple.

Figure 5: A diagram showing the internal circuits of the RGB Sliders
I found this project mostly easy to put together, as I just had to solder the sliders and LED to the board. However, I encountered a problem with the green color slider where it did not work. At first, I thought this was a problem with the slider not being soldered properly to the board, and added more solder to the pins. Later, though, I realized that the LED was positioned incorrectly. I attempted to desolder it, but was mostly unsuccessful, so instead kept melting the solder and slightly adjusting the angle of the LED until it worked.
Product used: Link


