VEX V5 PYTHON | 08.1 COLOUR TRACKING
Guide objective
Configure a colour signature, choose a usable detection and turn the robot to centre a target in the camera image.
01 | FROM COLOUR TO A STEERING DECISION
A robot sees a coloured marker to the right of its camera image. It turns clockwise, takes another snapshot and repeats until the marker is centred.
This is a simple form of visual feedback. It can help align an intake, point at a goal marker or aim toward a known object.
Begin with turn-only alignment. Centring a target is easier to test than driving toward it, and does not require guessing its distance from apparent size.
02 | CHOOSE THE RIGHT SENSOR
The original Vision Sensor and AI Vision Sensor both support configured colour signatures. Their configuration types and image dimensions differ. AI object recognition and AprilTags are separate features from colour matching.
| Sensor | Colour signature type | Image width × height | Nominal centre X |
|---|---|---|---|
| Vision | Signature | 316 × 212 | 158 |
| AI Vision | Colordesc | 320 × 240 | 160 |
A colour signature describes a range of appearances to match. It is not a guarantee that the detected blob is the intended game object.
03 | CONFIGURE A SIGNATURE IN THE UTILITY
- Add the selected camera in VEXcode Devices and connect it to the correct smart port.
- Open its Vision or AI Vision configuration utility.
- Place one target in view under representative lighting.
- Select a clean region of the target colour and save a signature named TARGET.
- Check the resulting detections while moving the target across the image.
- Test nearby distractors, shadows, highlights and different distances.
- Save the configuration and use its generated Python names.
Avoid including background, glare or another colour in the sample. Very broad tolerances can detect everything; very narrow ones can lose the target when lighting changes.
Keep the utility-generated camera and signature declarations in their generated region. Put your own tracking logic below # Begin project code. Do not hand-edit the generated region.
04 | TAKE A SNAPSHOT BEFORE READING OBJECTS
A snapshot produces detections for the chosen signature. Check that its returned tuple contains objects before indexing it.
Vision Sensor example:
# Vision Sensor configured in Devices as vision_1, signature TARGET.
objects = vision_1.take_snapshot(vision_1__TARGET)
if objects:
print(objects[0].centerX, objects[0].width, objects[0].height)
else:
print("No matching colour")AI Vision Sensor example:
# AI Vision configured in Devices as ai_vision_1, signature TARGET.
objects = ai_vision_1.take_snapshot(ai_vision_1__TARGET)
if objects:
print(objects[0].centerX, objects[0].width, objects[0].height)
else:
print("No matching colour")These snippets read one snapshot; they are not movement programs. Use your project’s actual generated names.
Object properties describe that observation, not a continuously updating target. Take a new snapshot during every feedback cycle.
05 | UNDERSTAND THE BOUNDING BOX
A detected blob is summarized by a rectangle. Its centre gives an image location; width and height describe its apparent extent.
| Property | How the starter uses it |
|---|---|
| exists | Ignore absent entries |
| centerX / centerY | Validate location and calculate horizontal error |
| width / height | Reject small blobs and calculate approximate box area |
Box area is width × height. It is not the exact coloured-pixel area or a distance measurement. A larger box can belong to a larger object, a closer object or a merged background detection.
06 | SELECT ONE USABLE TARGET
The starter rejects boxes smaller than its configured width, height or area limits, then chooses the largest remaining box by area.
If another candidate has at least 80% of that area, it stops with AMBIGUOUS_TARGET. A horizontal jump greater than 60 pixels between selected detections also stops the routine.
Three consecutive usable observations are required before movement. These are simple filters, not persistent object identification. Similar objects can still exchange rank or cross without a detectable jump.
For initial tests, use one distinctive target against a plain background. Later you can add region-of-interest gates or association based on position and size.
07 | CALCULATE HORIZONTAL ERROR
Subtract the desired image centre from the selected target’s centre X.
error_px = target_center_x - image_width / 2.0
For AI Vision, target X = 230 gives 230 − 160 = +70 pixels. With an upright camera facing forward, turn clockwise to bring it toward the centre.
The camera centre is not automatically the intake centre. Camera mounting offsets may require a calibrated aim coordinate instead of half the image width.
08 | TURN PROPORTIONALLY AND USE A DEADBAND
The starter multiplies error by a gain and caps the turn request at 10%. Inside a ±6 pixel window, it brakes instead of chasing small fluctuations.
turn = clamp(0.12 * error_px, -10, 10) left_request = turn right_request = -turn
At +70 pixels, the request is +8.4%: left forward and right reverse. At −70 pixels, the requests reverse. PERCENT is a motor velocity request, not degrees per second.
Verify the sign at low speed before running the loop. An inverted image or unusual camera mounting can reverse the relationship. If error grows while turning, stop and check the sign.
09 | COMPLETE TURN-ONLY COLOUR ALIGNMENT STARTER
Configure exactly one camera on port 15 using Devices, with signature TARGET. Set SENSOR_KIND to AI or VISION. The camera setup function uses the corresponding generated names.
Manual drive configuration uses left ports 3/4 and right 1/2; inertial uses 20. Do not duplicate those devices in generated configuration. Keep generated imports and Brain above the following code.
MOTION_CONFIGURED begins False. Verify the drivetrain direction, upright camera orientation, colour signature and inertial calibration before enabling movement.
# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | TURN-ONLY COLOUR ALIGNMENT
# =============================================================================
# Keep generated imports, brain and camera/signature setup above this code.
# Configure ONE camera in Devices on port 15, with colour signature TARGET:
# Vision: vision_1 and vision_1__TARGET
# AI Vision: ai_vision_1 and ai_vision_1__TARGET
# Use the actual generated names if your project differs.
# Manual drivetrain/inertial configuration below; do not duplicate in Devices.
import math
# =============================================================================
# FOUR-MOTOR DRIVETRAIN AND INERTIAL
# =============================================================================
left_1 = Motor(Ports.PORT3, GearSetting.RATIO_6_1, False)
left_2 = Motor(Ports.PORT4, GearSetting.RATIO_6_1, False)
right_1 = Motor(Ports.PORT1, GearSetting.RATIO_6_1, True)
right_2 = Motor(Ports.PORT2, GearSetting.RATIO_6_1, True)
left_drive = MotorGroup(left_1, left_2)
right_drive = MotorGroup(right_1, right_2)
inertial = Inertial(Ports.PORT20)
controller = Controller(PRIMARY)
# =============================================================================
# SETTINGS: EXAMPLES TO CHECK AND TUNE
# =============================================================================
SENSOR_KIND = "AI" # "AI" or "VISION"
MOTION_CONFIGURED = False
MIN_WIDTH_PX = 8
MIN_HEIGHT_PX = 8
MIN_AREA_PX2 = 150
AMBIGUOUS_AREA_RATIO = 0.80
MAX_CENTRE_JUMP_PX = 60
STEER_KP = 0.12 # percent per horizontal pixel
MAX_TURN_PERCENT = 10.0
STEERING_SIGN = 1.0 # Verify: right-image target must turn clockwise
CENTRE_TOL_PX = 6
ACQUIRE_FRAMES = 3
SETTLE_MS = 250
MAX_SETTLE_RATE_DEG_S = 8.0
POLL_MS = 50
MAX_UPDATE_GAP_MS = 200
ALIGN_TIMEOUT_MS = 5000
ready = False
def camera_setup():
# Camera/signature declarations come from the Devices utility, not invented RGB.
if SENSOR_KIND == "VISION":
return vision_1, vision_1__TARGET, 316.0, 212.0
if SENSOR_KIND == "AI":
return ai_vision_1, ai_vision_1__TARGET, 320.0, 240.0
raise ValueError("Choose AI or VISION")
def clamp(value, low, high):
return max(low, min(high, value))
def stop_drive():
left_drive.stop(BRAKE)
right_drive.stop(BRAKE)
def side_output(group, output):
if output == 0:
group.stop(BRAKE)
else:
group.spin(FORWARD if output > 0 else REVERSE, abs(output), PERCENT)
def select_target(objects, image_width, image_height, previous_x=None):
candidates = []
for obj in objects:
if not obj.exists:
continue
values = (obj.centerX, obj.centerY, obj.width, obj.height)
if not all(math.isfinite(v) for v in values):
raise ValueError("Invalid image data")
x, y, w, h = values
if not (0 <= x <= image_width and 0 <= y <= image_height
and 0 < w <= image_width and 0 < h <= image_height):
raise ValueError("Image values outside bounds")
area = w*h
if w >= MIN_WIDTH_PX and h >= MIN_HEIGHT_PX and area >= MIN_AREA_PX2:
candidates.append((area, x, y, w, h))
if not candidates:
return "NO_TARGET", None
candidates.sort(key=lambda item: item[0], reverse=True)
if len(candidates) > 1 and candidates[1][0] >= candidates[0][0]*AMBIGUOUS_AREA_RATIO:
return "AMBIGUOUS_TARGET", None
target = candidates[0]
if previous_x is not None and abs(target[1]-previous_x) > MAX_CENTRE_JUMP_PX:
return "TARGET_JUMP", None
return "OK", target
def steering_request(center_x, image_width):
error = center_x-image_width/2.0
if abs(error) <= CENTRE_TOL_PX:
return 0.0, error
turn = clamp(STEERING_SIGN*STEER_KP*error, -MAX_TURN_PERCENT, MAX_TURN_PERCENT)
return turn, error
def align_colour(timeout_ms=ALIGN_TIMEOUT_MS):
stop_drive()
try:
camera, signature, image_width, image_height = camera_setup()
if (not all(math.isfinite(v) for v in (timeout_ms, STEER_KP,
MAX_TURN_PERCENT, ACQUIRE_FRAMES, MIN_WIDTH_PX, MIN_HEIGHT_PX,
MIN_AREA_PX2, AMBIGUOUS_AREA_RATIO, MAX_CENTRE_JUMP_PX,
CENTRE_TOL_PX, SETTLE_MS, MAX_SETTLE_RATE_DEG_S,
POLL_MS, MAX_UPDATE_GAP_MS)) or timeout_ms <= 0
or STEER_KP <= 0 or not 0 < MAX_TURN_PERCENT <= 25
or ACQUIRE_FRAMES < 1 or int(ACQUIRE_FRAMES) != ACQUIRE_FRAMES
or MIN_WIDTH_PX <= 0 or MIN_HEIGHT_PX <= 0 or MIN_AREA_PX2 <= 0
or not 0 < AMBIGUOUS_AREA_RATIO <= 1 or MAX_CENTRE_JUMP_PX <= 0
or not 0 < CENTRE_TOL_PX < image_width/2 or SETTLE_MS <= 0
or MAX_SETTLE_RATE_DEG_S < 0 or not 0 < POLL_MS < MAX_UPDATE_GAP_MS
or STEERING_SIGN not in (-1.0, 1.0)):
return "INVALID_SETTINGS"
started = previous_ms = brain.timer.time(MSEC)
previous_rotation = None
previous_x = None
acquired = 0
centred_since = None
while True:
if (not ready or not MOTION_CONFIGURED or not competition.is_enabled()
or not competition.is_autonomous() or controller.buttonB.pressing()):
return "CANCELLED_OR_NOT_READY"
if not camera.installed() or not inertial.installed() or inertial.is_calibrating():
return "SENSOR_NOT_READY"
before_snapshot = brain.timer.time(MSEC)
if before_snapshot-started >= timeout_ms:
return "TIMEOUT"
objects = camera.take_snapshot(signature)
now = brain.timer.time(MSEC)
# Recheck mode and budget before issuing output after the API call.
if (not competition.is_enabled() or not competition.is_autonomous()
or controller.buttonB.pressing()):
return "CANCELLED_OR_NOT_READY"
if now-started >= timeout_ms:
return "TIMEOUT"
if now-previous_ms > MAX_UPDATE_GAP_MS:
return "UPDATE_GAP"
rotation = inertial.rotation(DEGREES)
if not math.isfinite(rotation):
return "INVALID_ROTATION"
dt = (now-previous_ms)/1000.0
slow = (previous_rotation is not None and dt > 0 and
abs(rotation-previous_rotation)/dt <= MAX_SETTLE_RATE_DEG_S)
previous_rotation, previous_ms = rotation, now
status, target = select_target(objects, image_width, image_height, previous_x)
if status == "NO_TARGET" and previous_x is None:
stop_drive() # Wait for acquisition while stationary, within deadline.
acquired = 0
centred_since = None
wait(POLL_MS, MSEC)
continue
if status != "OK":
return "TARGET_LOST" if status == "NO_TARGET" else status
previous_x = target[1]
acquired += 1
if acquired < ACQUIRE_FRAMES:
stop_drive()
wait(POLL_MS, MSEC)
continue
turn, error = steering_request(target[1], image_width)
if abs(error) <= CENTRE_TOL_PX:
stop_drive()
if slow:
if centred_since is None:
centred_since = now
if now-centred_since >= SETTLE_MS:
return "CENTRED"
else:
centred_since = None
else:
centred_since = None
# Turn only: equal/opposite side requests, no forward approach.
side_output(left_drive, turn)
side_output(right_drive, -turn)
wait(POLL_MS, MSEC)
except Exception:
return "CONFIGURATION_OR_VISION_FAULT"
finally:
stop_drive()
# =============================================================================
# COMPETITION CALLBACKS
# =============================================================================
def autonomous():
stop_drive()
if ready and MOTION_CONFIGURED:
print("colour alignment", align_colour())
def driver_control():
stop_drive() # Teaching project: no driver movement.
competition = Competition(driver_control, autonomous)
stop_drive()
if MOTION_CONFIGURED and not competition.is_enabled() and inertial.installed():
inertial.calibrate() # Keep stationary during disabled setup; may block.
ready = not inertial.is_calibrating() and not competition.is_enabled()
while True:
wait(100, MSEC)
This program only rotates the chassis. It reports CENTRED after the selected target stays in the pixel window and estimated inertial turn rate stays low for 250 ms.
A missing target during initial acquisition leaves the robot stationary until timeout. After acquisition starts, target loss, ambiguity or a large jump ends the action. It does not keep turning on the last observation or launch a blind search.
One autonomous callback owns drivetrain output. Snapshot calls must return promptly enough for the update-gap check; that check cannot interrupt an API call that is still blocking.
10 | INTERPRET THE RESULT
CENTRED means the selected colour box met the image-alignment and low-turn-rate finish checks. It does not prove object identity, physical distance or a safe forward approach.
| Result | Meaning |
|---|---|
| CENTRED | Image alignment met the finish policy |
| TIMEOUT | Shared alignment budget expired |
| TARGET_LOST | Usable detection disappeared after acquisition began |
| AMBIGUOUS_TARGET | Two large candidates make selection uncertain |
| TARGET_JUMP | Selected centre changed too abruptly |
| SENSOR_NOT_READY / UPDATE_GAP | Feedback availability or timing failed |
| CANCELLED_OR_NOT_READY | Setup, mode, enable or B prevented continuation |
| CONFIGURATION_OR_VISION_FAULT | Generated names, image data or API access failed |
A later approach routine needs distance feedback or a validated size-to-distance model, clearance checks and its own stop conditions. Image width alone is not a universal range sensor.
11 | TEST LIGHTING AND REPEATABILITY
- Print snapshot values with movement disabled.
- Test left, centre and right target positions.
- Introduce a small distractor and then two similar targets.
- Remove the target after acquisition and verify stopping.
- Check a disconnected sensor and a simulated slow snapshot.
- Enable a conservative turn cap in a clear area.
- Repeat under brighter, dimmer and directional light.
Log sensor type, signature, target dimensions, centre error, turn request, returned status and lighting. Tune the signature before increasing steering gain.
12 | COMMON MISTAKES
- Indexing an empty tuple: test for detections first.
- Mixing image dimensions: Vision centre X is 158; AI Vision centre X is 160.
- Using last frame indefinitely: loss must stop or enter an explicit bounded recovery.
- Assuming biggest means correct: large background blobs can win.
- Confusing pixels and millimetres: centring and ranging are different tasks.
- Adding forward motion too early: verify turning and loss behaviour first.
CHALLENGES | DETECT, SELECT AND ALIGN
- Calculate: find AI Vision horizontal error at X = 90, 160 and 230.
- Compare: calculate the error for Vision at X = 230.
- Predict: find turn requests for errors −70, 0 and +120 pixels.
- Filter: compare boxes 10 × 10, 20 × 12 and 40 × 25 pixels against the starter limits.
- Mock: verify that an empty tuple cannot produce a motor command.
- Test: show that target loss and ambiguous candidates stop alignment.
- Measure: compare repeatability under three lighting conditions.
- Extend: propose an object-association rule without claiming it guarantees identity.
Calculation checks
AI Vision errors: −70, 0 and +70 pixels. Vision at X = 230 gives +72 pixels. Turn requests are −8.4%, 0% and +10% after the cap, assuming the verified positive steering sign.
A 10 × 10 box has area 100 and fails the 150-area threshold. The other two have areas 240 and 1000 and pass the size limits.
OFFICIAL REFERENCES
The selection rules, steering policy and starter are independently written teaching examples. Validate configuration and behaviour on your robot before relying on them.