Skip to content

Commit dc112ba

Browse files
committed
Add XRP orange ring example
1 parent ad298cf commit dc112ba

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#-------------------------------------------------------------------------------
2+
# SPDX-License-Identifier: MIT
3+
#
4+
# Copyright (c) 2025 SparkFun Electronics
5+
#-------------------------------------------------------------------------------
6+
# ex02_grab_orange_ring.py
7+
#
8+
# The XRP can act as a bridge to FIRST programs, which includes summer camps
9+
# with FIRST-style games. Learn more here:
10+
# https://experientialrobotics.org/bridge-to-first/
11+
#
12+
# FIRST-style games often include game elements with randomized locations that
13+
# can be detected with a camera. The exact game elements and tasks change every
14+
# year, but this example assumes there is an orange ring in front of the robot
15+
# that needs to be grabbed. This example demonstrates how to detect the ring,
16+
# calculate its distance and position relative to the robot in real-world units,
17+
# then drive the robot to grab it.
18+
#-------------------------------------------------------------------------------
19+
20+
# Import XRPLib defaults
21+
from XRPLib.defaults import *
22+
23+
# Import OpenCV and hardware initialization module
24+
import cv2 as cv
25+
from cv2_hardware_init import *
26+
27+
# Import time for delays
28+
import time
29+
30+
# Import math for calculations
31+
import math
32+
33+
# This is the pipeline implementation that attempts to find an orange ring in
34+
# an image, and returns the real-world distance to the object and its left/right
35+
# position relative to the center of the image in centimeters
36+
def my_pipeline(frame):
37+
# Convert the frame to HSV color space, which is often more effective for
38+
# color-based segmentation tasks than RGB or BGR color spaces
39+
hsv = cv.cvtColor(frame, cv.COLOR_BGR2HSV)
40+
41+
# Here we use the `cv.inRange()` function to find all the orange pixels.
42+
# This outputs a binary image where pixels that fall within the specified
43+
# lower and upper bounds are set to 255 (white), and all other pixels are
44+
# set to 0 (black). This is applied to the HSV image, so the lower and upper
45+
# bounds are in HSV color space. The bounds were determined experimentally:
46+
#
47+
# Hue: Orange hue is around 20, so we use a range of 15 to 25
48+
# Saturation: Anything above 50 is saturated enough
49+
# Value: Anything above 30 is bright enough
50+
lower_bound = (15, 50, 30)
51+
upper_bound = (25, 255, 255)
52+
inRange = cv.inRange(hsv, lower_bound, upper_bound)
53+
54+
# Noise in the image often causes `cv.inRange()` to return false positives
55+
# and false negatives, meaning there are some incorrect pixels in the binary
56+
# image. These can be cleaned up with morphological operations, which
57+
# effectively grow and shrink regions in the binary image to remove tiny
58+
# blobs of noise
59+
kernel = cv.getStructuringElement(cv.MORPH_RECT, (3, 3))
60+
morphOpen = cv.morphologyEx(inRange, cv.MORPH_OPEN, kernel)
61+
morphClose = cv.morphologyEx(morphOpen, cv.MORPH_CLOSE, kernel)
62+
63+
# Now we use `cv.findContours()` to find the contours in the binary image,
64+
# which are the boundaries of the regions in the binary image
65+
contours, hierarchy = cv.findContours(morphClose, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
66+
67+
# It's possible that no contours were found, so first check if any were
68+
# found before proceeding
69+
best_contour = None
70+
if contours:
71+
# It's possible that some tiny blobs of noise are still present in the
72+
# binary image, or other objects entirely, leading to extra contours. A
73+
# proper pipeline would make an effort to filter out unwanted contours
74+
# based on size, shape, or other criteria. This example keeps it simple;
75+
# the contour of a ring is a circle, meaning many points are needed to
76+
# represent it. A contour with only a few points is obviously not a
77+
# circle, so we can ignore it. This example assumes the ring is the only
78+
# large orange object in the image, so the first contour that's complex
79+
# enough is probably the one we're looking for
80+
for i in range(len(contours)):
81+
if len(contours[i]) < 50:
82+
continue
83+
best_contour = contours[i]
84+
break
85+
86+
# If no contour was found, return invalid values to indicate that
87+
if best_contour is None:
88+
return (-1, -1)
89+
90+
# Calculate the bounding rectangle of the contour, and use that to calculate
91+
# the center coordinates of the object
92+
left, top, width, height = cv.boundingRect(best_contour)
93+
center_x = left + width // 2
94+
center_y = top + height // 2
95+
96+
# Now we can calculate the real-world distance to the object based on its
97+
# size. We'll first estimate the diameter of the ring in pixels by taking
98+
# the maximum of the width and height of the bounding rectangle. This
99+
# compensates for the fact that the ring may be tilted
100+
diameter_px = max(width, height)
101+
102+
# If the camera has a perfect lens, the distance can be calculated with:
103+
#
104+
# distance_cm = diameter_cm * focal_length_px / diameter_px
105+
#
106+
# However almost every camera lens has some distortion, so there are
107+
# corrections needed to account for that. This example has been tested with
108+
# the HM01B0, and the calculation below gives a decent estimate of the
109+
# distance in centimeters
110+
focal_length_px = 180
111+
diameter_cm = 12.7
112+
distance_cm = diameter_cm * focal_length_px / diameter_px - 10
113+
114+
# Now with our distance estimate, we can calculate how far left or right the
115+
# object is from the center in the same real-world units. Assuming a perfect
116+
# lens, the position can be calculated as:
117+
#
118+
# position_x_cm = distance_cm * position_x_px / focal_length_px
119+
position_x_px = center_x - (frame.shape[1] // 2)
120+
position_x_cm = distance_cm * position_x_px / focal_length_px
121+
122+
# Draw the contour, bounding box, center, and text for visualization
123+
frame = cv.drawContours(frame, [best_contour], -1, (0, 0, 255), 2)
124+
frame = cv.rectangle(frame, (left, top), (left + width, top + height), (255, 0, 0), 2)
125+
frame = cv.drawMarker(frame, (center_x, center_y), (0, 255, 0), cv.MARKER_CROSS, 10, 2)
126+
frame = cv.putText(frame, f"({center_x}, {center_y})", (center_x - 45, center_y - 10), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
127+
frame = cv.putText(frame, f"{width}x{height}", (left, top - 10), cv.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
128+
frame = cv.putText(frame, f"D={distance_cm:.1f}cm", (left, top - 25), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2)
129+
frame = cv.putText(frame, f"X={position_x_cm:.1f}cm", (left, top - 40), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 2)
130+
131+
# Now we can return the distance and position of the object in cm, since
132+
# that's the only data we need from this pipeline
133+
return (distance_cm, position_x_cm)
134+
135+
# Move the servo out of the way of the camera
136+
servo_one.set_angle(90)
137+
138+
# Open the camera and wait a moment for at least one frame to be captured
139+
camera.open()
140+
time.sleep(0.1)
141+
142+
# Prompt the user to press a key to continue
143+
print("Detecting ring...")
144+
145+
# Loop until the object is found or the user presses a key
146+
while True:
147+
# Read a frame from the camera
148+
success, frame = camera.read()
149+
if success == False:
150+
print("Error reading frame from camera")
151+
break
152+
153+
# Call the pipeline function to find the object
154+
distance_cm, position_x_cm = my_pipeline(frame)
155+
156+
# Display the frame
157+
cv.imshow(display, frame)
158+
159+
# If the distance is valid, break the loop
160+
if distance_cm >= 0:
161+
break
162+
163+
# Check for key presses
164+
key = cv.waitKey(1)
165+
166+
# If any key is pressed, exit the loop
167+
if key != -1:
168+
break
169+
170+
# Print the distance and position of the object
171+
print(f"Found object at distance {distance_cm:.1f} cm, position {position_x_cm:.1f} cm from center")
172+
173+
# Release the camera, we're done with it
174+
camera.release()
175+
176+
# Move the servo to pick up the object
177+
servo_one.set_angle(45)
178+
179+
# Turn to face the object. We first calculate the angle to turn based on the
180+
# position of the object
181+
angle = -math.atan2(position_x_cm, distance_cm) * 180 / math.pi
182+
drivetrain.turn(angle)
183+
184+
# Drive forwards to the object. Drive a bit further than the distance to the
185+
# object to ensure the arm goes through the ring
186+
distance_cm += 10
187+
drivetrain.straight(distance_cm)
188+
189+
# Rotate the servo to pick up the ring
190+
servo_one.set_angle(90)
191+
192+
# Drive backwards to pull the ring off the rung
193+
drivetrain.straight(-10)

0 commit comments

Comments
 (0)