Where is it?
Pixels to degrees, and a controller that turns to face a tag.
Do this lesson in the simulatorPrint it: worksheet (PDF) · answers (PDF) · as a web page
Guide: AprilTags explained
A tag at cx = 160 is straight ahead. A tag at cx = 300 is off to the right. The camera turns pixels into a direction, and that is all a robot needs to steer.
Pixels to degrees
The camera sees 120 degrees from the left edge of the picture to the right, across 320 pixels: a wide-angle lens. Share the degrees out evenly and each pixel is 120 / 320 of a degree, which gives a simple estimate of the bearing of a tag:
bearing = (cx - 160) * 120 / 320 # degrees, positive to the right
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def bearing_of(cx):
return (cx - 160) * 120 / 320
# camera: the tag detector
set_cv("apriltag")
tag = apriltags()[0]
print("cx:", tag[1], "bearing:", round(bearing_of(tag[1]), 1), "degrees")
Marker 3 is really about 20 degrees to the right, and the formula says 12.4. A lens is not a ruler: pixels near the middle of the picture cover more angle than pixels near the edges (about 0.62 degrees each at the centre), so sharing the degrees out evenly reads low everywhere except dead centre and the very edges. For steering that does not matter. The sign is right, the number grows as the tag moves out, and it is zero when the tag is centred, so the controller you are about to write drives it to zero all the same. Challenge 3 has the exact version.
Turn to face it
The bearing is an error, and you know what to do with an error: a proportional controller on the rotation.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def bearing_of(cx):
return (cx - 160) * 120 / 320
# camera: the tag detector
set_cv("apriltag")
# do this 30 times (tick counts from 0)
for tick in range(30):
# every tag in view: [id, cx, cy, distance], nearest first
tags = apriltags()
if not tags:
# leave the loop
break
error = bearing_of(tags[0][1])
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(0, 0, error * 3)
# pause 0.05 s (the robot keeps doing what it was told)
wait(0.05)
# all motors off
stop()
# pause 0.3 s (the robot keeps doing what it was told)
wait(0.3)
tag = apriltags()[0]
print("now cx =", tag[1], "and I am facing", round(heading()))
The tag slides to the middle of the picture (cx 163), and the heading has changed by 18 degrees: close to the true 20, and more than the 12.4 the estimate printed. The camera never told you the heading. It told you the error, and the loop drove it to zero.
Stopping cleanly
The loop above just runs 30 times. Better to stop when the error is small, then settle and check, exactly as in lesson 3.1:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def bearing_of(cx):
return (cx - 160) * 120 / 320
# camera: the tag detector
set_cv("apriltag")
# again and again, for ever
while True:
# every tag in view: [id, cx, cy, distance], nearest first
tags = apriltags()
if not tags:
# spin clockwise on the spot at 30
turn_right(30)
# pause 0.1 s (the robot keeps doing what it was told)
wait(0.1)
# back to the top of the loop
continue
error = bearing_of(tags[0][1])
if abs(error) < 1.5:
# all motors off
stop()
# pause 0.3 s (the robot keeps doing what it was told)
wait(0.3)
if abs(bearing_of(apriltags()[0][1])) < 1.5:
# leave the loop
break
# back to the top of the loop
continue
# forward, sideways, rotation: -100 to 100 each, until the next command
drive(0, 0, max(-30, min(30, error * 3)))
# pause 0.05 s (the robot keeps doing what it was told)
wait(0.05)
# all motors off
stop()
print("centred, facing", round(heading()))
The if not tags branch keeps spinning if the tag is out of view. It is the beginning of a search, which lesson 4.5 finishes.
Task: face the marker
Turn until marker 3 is in the middle of the picture, then print centred. Do not drive.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
# camera: the tag detector
set_cv("apriltag")
print(apriltags())
Challenges
- Print the heading before and after, and compare the change with the bearing you computed.
- Face the marker, then turn 180 away from it and face it again.
- Work out the bearing exactly with
math.degrees(math.atan((cx - 160) / 92.4))(afterimport math). What does it give for marker 3? Compare the two formulas at cx 160, 200, 240, 280 and 320: where is the difference biggest?