Following a line

cx to rotation, the angle as a look-ahead, speed on the straights.

6.2Seeing moreRobot club20 min

Do this lesson in the simulator

Following a line is the servoing from Module 4 pointed at the mat: the line's cx is the error, rotation is the correction. The difference is that a line bends, so the loop never gets to relax.

One step

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def follow_step(speed=60, gain=0.4):
    # one tick of line following: steer from where the line crosses the picture
    # [cx, angle] of the line ahead, or [] if none
    seen = line()
    if not seen:
        return False
    cx, angle = seen
    # pixels left (-) or right (+) of centre
    error = cx - 160
    # forward, sideways, rotation: -100 to 100 each, until the next command
    drive(speed, 0, error * gain)
    return True

# camera: the line detector
set_cv("line")
# do this 30 times (tick counts from 0)
for tick in range(30):
    follow_step()
    # pause 0.1 s (the robot keeps doing what it was told)
    wait(0.1)
# all motors off
stop()
print("at", position(), "line now", line())

Run this in the simulator

follow_step does one tick and says whether it saw the line. The gain is in rotation per pixel: 0.4 means a line 50 pixels off centre (about 5 cm, at the scan row) gives rotation 20.

Two views from above: a line 50 pixels right of centre gives rotation +20, a line 50 pixels left gives -20line to the righterror = 210 - 160 = +50rotation +20: turn rightline to the lefterror = 110 - 160 = -50rotation -20: turn left
The error is how far the line is from the middle of the picture, 160. Times the gain, 0.4, it is the rotation: plus turns right, minus turns left, so the robot always turns towards the line.

The whole line

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

def follow_step(speed=60, gain=0.4):
    # [cx, angle] of the line ahead, or [] if none
    seen = line()
    if not seen:
        return False
    cx, angle = seen
    # forward, sideways, rotation: -100 to 100 each, until the next command
    drive(speed, 0, (cx - 160) * gain)
    return True

# camera: the line detector
set_cv("line")
while position()[1] < 85:
    if not follow_step(60, 0.4):
        # no line: creep on, it is probably a gap
        forward(30)
    # pause 0.1 s (the robot keeps doing what it was told)
    wait(0.1)
# all motors off
stop()
print("end:", position())

Run this in the simulator

position() counts from where the robot started, not from the corner of the mat. This robot starts 5 cm up the mat, so position()[1] < 85 stops it 90 cm up, inside the green zone. The end print, (-6.9, 85.7), means 6.9 cm to the left of the start and 85.7 cm further up: the last piece of line is 5 cm left of where the first piece began.

Watch the robot take the bends. The line jogs right and then back left, and at each bend it swings well off-centre before the robot has turned: 59 pixels at the first bend, 85 at the worst. A P controller only turns once there is an error, so it always lags a bend a little. That is fine as long as the line stays in view, and here it drops out of view for just one tick, at the very end. The run is done in 10.2 seconds.

From above: the lesson's line and the path the robot took, with the two places where the line was furthest from the middle of the picture59 pixels off85 pixels offstartgoalgrey: the linegreen: therobot's path
The robot only turns once the line is off centre, so at each bend it runs wide before it comes back: 59 pixels off at the first bend and 85 at the worst.

Using the angle

cx says where the line is; angle says where it is going. Feeding both in lets the robot start turning before the line has drifted off-centre:

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

# camera: the line detector
set_cv("line")
while position()[1] < 85:
    # [cx, angle] of the line ahead, or [] if none
    seen = line()
    if seen:
        cx, angle = seen
        # forward, sideways, rotation: -100 to 100 each, until the next command
        drive(70, 0, (cx - 160) * 0.3 + angle * 1.5)
    else:
        # drive forward at 30 (keeps going until the next command)
        forward(30)
    # pause 0.1 s (the robot keeps doing what it was told)
    wait(0.1)
# all motors off
stop()
print("end:", position())

Run this in the simulator

Faster and smoother round the bends: the run is done in 7.9 seconds instead of 10.2, the line is never more than 58 pixels off centre, and it never leaves the view. The angle term is the beginning of a derivative controller: it reacts to where the error is heading, not just where it is.

From above, just before a bend: the line crosses the middle, cx 160, but it heads off to the right, angle 29.2near rowfar rowcx onlyerror 0: rotation 0no turn yetcx and angleangle 29.2: rotation 44turns now
Here the line is dead centre on the near row, so the error is 0 and a follower that only uses cx goes straight on. The angle, 29.2, already shows the bend, so adding it starts the turn early.

Speed on the straights

A line follower that is fast on the straights and slow in the bends beats one at a fixed speed. The angle tells you which you are on: small angle, speed up.

Task: follow the line

Follow the line to the green zone, staying within 6 cm of it for at least 85% of the run.

# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()

# drive forward at 60 for 90 cm, then stop
forward(60, distance=90)

Challenges

  1. Make the speed depend on the angle: 80 when the line is straight, 40 in a bend.
  2. Print the largest cx error you saw during the run.
  3. Follow the line backwards from the end to the start (turn round first).