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.

Use the coordinate scale for the selected device
SensorColour signature typeImage width × heightNominal centre X
VisionSignature316 × 212158
AI VisionColordesc320 × 240160

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

  1. Add the selected camera in VEXcode Devices and connect it to the correct smart port.
  2. Open its Vision or AI Vision configuration utility.
  3. Place one target in view under representative lighting.
  4. Select a clean region of the target colour and save a signature named TARGET.
  5. Check the resulting detections while moving the target across the image.
  6. Test nearby distractors, shadows, highlights and different distances.
  7. 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.

These values are in image pixels
PropertyHow the starter uses it
existsIgnore absent entries
centerX / centerYValidate location and calculate horizontal error
width / heightReject 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
Image coordinates and horizontal alignment errorA 320 by 240 AI Vision image has its centre line at X 160. A coloured target centred at X 230 is 70 pixels to the right, so a forward-mounted camera needs a clockwise correction.X increases →Centre X = 160Target X = 230Error = +70 pxAI Vision example: coordinates are pixels, not field millimetres.
The image is displayed wider for clarity. With an upright, forward-facing camera, a target to the image right calls for a clockwise turn.

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.

All normal exits and caught exceptions brake through finally
ResultMeaning
CENTREDImage alignment met the finish policy
TIMEOUTShared alignment budget expired
TARGET_LOSTUsable detection disappeared after acquisition began
AMBIGUOUS_TARGETTwo large candidates make selection uncertain
TARGET_JUMPSelected centre changed too abruptly
SENSOR_NOT_READY / UPDATE_GAPFeedback availability or timing failed
CANCELLED_OR_NOT_READYSetup, mode, enable or B prevented continuation
CONFIGURATION_OR_VISION_FAULTGenerated 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

  1. Print snapshot values with movement disabled.
  2. Test left, centre and right target positions.
  3. Introduce a small distractor and then two similar targets.
  4. Remove the target after acquisition and verify stopping.
  5. Check a disconnected sensor and a simulated slow snapshot.
  6. Enable a conservative turn cap in a clear area.
  7. 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

  1. Calculate: find AI Vision horizontal error at X = 90, 160 and 230.
  2. Compare: calculate the error for Vision at X = 230.
  3. Predict: find turn requests for errors −70, 0 and +120 pixels.
  4. Filter: compare boxes 10 × 10, 20 × 12 and 40 × 25 pixels against the starter limits.
  5. Mock: verify that an empty tuple cannot produce a motor command.
  6. Test: show that target loss and ambiguous candidates stop alignment.
  7. Measure: compare repeatability under three lighting conditions.
  8. 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.