Skip to content
CrocoDocs
Starter GuideProgrammingEngineeringDesignPortfolio
Documentation

Introduction

IntroductionBIOBUZZ game overview

Getting started

Getting startedBlocksOnBot JavaAndroid StudioYour first OpModeJava basicsMotors and servosNonblocking codeDebugging robot code

Control systems

Control systemsJoystick mappingField-centric mecanum drivePID controlFeedforwardFlywheel velocity controlSystem identificationMotion profilingKalman filterLow-pass filter

Autonomous

AutonomousTimed and encoder movementOdometryLocalization troubleshootingMotion planningReliable autonomous sequencesPure PursuitSensor fusionAuto to TeleOp handoff

Season topics

Season topicsProjectile mathematicsTurret aimingShooting on the move

Code practices

Code practicesNaming conventionsCode organizationLoop timing and bulk readsFormattingComments and documentationTeam collaboration

Libraries

LibrariesNextFTCFTCLibSolversLibPedro PathingRoad RunnerFTC DashboardMercurial v2Mercurial v1PanelsSloth

Community software

Community softwareBlaze FTCOpenLoopHive Vision

Sensors and vision

Vision overviewVision basicsLimelight 3A
VisionPortal
Color blob locator
AprilTag detection
Starter Guide
Engineering
Design
Portfolio
Appearance
VisionPortal

Color blob locator

Step-by-step introduction to the FTC Color Locator processor

This lesson follows one real BIOBUZZ Pollen game element through the SDK's Color Locator. The example photo and pipeline result below are from an isolated product image; your camera will see different lighting, background, and scale. First make the robot report what its camera sees; do not make it drive from a detection until the result has been tested.

The processor groups connected pixels in a selected color range. Each connected group is a blob. For each blob, the processor can report measurements such as area and a fitted rectangle called boxFit.

The unprocessed BIOBUZZ Pollen product photo that will be used to discuss color-threshold and blob measurements.

This is a controlled product photo, not a VisionPortal camera capture. The example color range still needs tuning against your robot's camera and lighting.

Camera checks

Configure the webcam in the . This guide uses the name Webcam 1; your string must match your configured name exactly. If your team has not used VisionPortal before, run the SDK's ConceptVisionColorLocator_Rectangle sample first. It checks the camera and shows the preview without requiring your own detection code.

While the sample is in , open Camera Stream on the . Put one Pollen in view. Confirm the camera is live, upright, and bright enough to see the object.

Processor setup

Create the processor before waitForStart():

ColorBlobLocatorProcessor locator = new ColorBlobLocatorProcessor.Builder()
    .setTargetColorRange(new ColorRange(ColorSpace.HSV,
        new Scalar(15, 60, 70), new Scalar(40, 255, 255)))
    .setRoi(ImageRegion.entireFrame())
    .setDrawContours(true)
    .build();
val locator = ColorBlobLocatorProcessor.Builder()
    .setTargetColorRange(ColorRange(ColorSpace.HSV,
        Scalar(15.0, 60.0, 70.0), Scalar(40.0, 255.0, 255.0)))
    .setRoi(ImageRegion.entireFrame())
    .setDrawContours(true)
    .build()

Read the builder one choice at a time:

  1. Choose the HSV color range to look for.
  2. Choose the image region to search (ROI).
  3. Turn on the contour drawing so you can see what was found.
  4. Call build() to create the processor.

In SDK 12.0.0, the locator converts with OpenCV's COLOR_RGB2HSV: hue is 0–179, and saturation/value are 0–255 for these 8-bit frames. The same teaching bounds therefore work in the OpenCV example. Older SDK samples may use the unsuffixed ConceptVisionColorLocator name; SDK 12 has separate Rectangle and Circle samples.

The bounds are starting values from a product photo. Lighting and exposure on your robot can change the colors, so test and retune them with a live camera image.

Portal setup

VisionPortal manages the camera. .addProcessor(locator) gives its image frames to the processor:

VisionPortal portal = new VisionPortal.Builder()
    .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
    .addProcessor(locator)
    .build();
val portal = VisionPortal.Builder()
    .setCamera(hardwareMap.get(WebcamName::class.java, "Webcam 1"))
    .addProcessor(locator)
    .build()

Creating the processor and opening the camera are separate steps. If no camera is attached to the portal, there are no frames for the processor to inspect.

Blob filtering

The processor may find no blobs, one blob, or many. Get the list, remove candidates with implausible areas, and sort the rest largest first:

List<ColorBlobLocatorProcessor.Blob> blobs = locator.getBlobs();

// Pixel-area limits for practice. Tune them from your own preview.
ColorBlobLocatorProcessor.Util.filterByArea(50, 20000, blobs);
ColorBlobLocatorProcessor.Util.sortByArea(
    com.qualcomm.robotcore.util.SortOrder.DESCENDING,
    blobs
);
val blobs = locator.getBlobs()

// Pixel-area limits for practice. Tune them from your own preview.
ColorBlobLocatorProcessor.Util.filterByArea(50.0, 20000.0, blobs)
ColorBlobLocatorProcessor.Util.sortByArea(
    com.qualcomm.robotcore.util.SortOrder.DESCENDING,
    blobs
)

An area limit is measured in pixels. Changing camera resolution or ROI can change the useful limits, so begin broadly and adjust after looking at telemetry. The filter changes the list your examines; the camera preview may still draw contours that were filtered out.

Now check for an empty list before asking for its first item:

if (blobs.isEmpty()) {
    telemetry.addLine("No blob passed the area filter");
} else {
    ColorBlobLocatorProcessor.Blob largest = blobs.get(0);
    RotatedRect box = largest.getBoxFit();
    telemetry.addData("Center", "x=%.0f y=%.0f", box.center.x, box.center.y);
    telemetry.addData("Area", "%d pixels", largest.getContourArea());
}
if (blobs.isEmpty()) {
    telemetry.addLine("No blob passed the area filter")
} else {
    val largest = blobs[0]
    val box = largest.getBoxFit()
    telemetry.addData("Center", "x=%.0f y=%.0f", box.center.x, box.center.y)
    telemetry.addData("Area", "%d pixels", largest.getContourArea())
}

The boxFit center is in image pixels: the is the top-left, X increases to the right, and Y increases downward. It is not a distance in inches or a field location. Near the edge of the ROI, the fitted box can extend outside the ROI even though the contour stays inside it.

The next picture is a separate EasyOpenCV demonstration on the same product photo. It helps you see the idea of measuring an object's center; it is not a screenshot or exact overlay from FTC's built-in ColorBlobLocatorProcessor. For the built-in processor, use its live preview and your own telemetry as the source of truth.

A custom OpenCV example marks one detected Pollen's contour, bounding rectangle, and pixel center.

Complete preview OpMode

This example prints the largest detected yellow blob and keeps the robot still. Run it first as a TeleOp, open Camera Stream while in INIT, and then press START to view changing measurements.

package org.firstinspires.ftc.teamcode;

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode;
import com.qualcomm.robotcore.eventloop.opmode.TeleOp;
import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName;
import org.firstinspires.ftc.vision.VisionPortal;
import org.firstinspires.ftc.vision.opencv.ColorBlobLocatorProcessor;
import org.firstinspires.ftc.vision.opencv.ColorSpace;
import org.firstinspires.ftc.vision.opencv.ColorRange;
import org.firstinspires.ftc.vision.opencv.ImageRegion;
import org.opencv.core.Scalar;
import org.opencv.core.RotatedRect;
import java.util.List;

@TeleOp(name = "Guided Color Locator")
public class GuidedColorLocator extends LinearOpMode {
    @Override
    public void runOpMode() {
        ColorBlobLocatorProcessor locator = new ColorBlobLocatorProcessor.Builder()
            .setTargetColorRange(new ColorRange(ColorSpace.HSV,
                new Scalar(15, 60, 70), new Scalar(40, 255, 255)))
            .setRoi(ImageRegion.entireFrame())
            .setDrawContours(true)
            .build();

        VisionPortal portal = new VisionPortal.Builder()
            .setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
            .addProcessor(locator)
            .build();

        telemetry.addLine("Open Camera Stream to check the preview.");
        telemetry.addLine("Press START to display blob measurements.");
        telemetry.update();
        waitForStart();

        while (opModeIsActive()) {
            List<ColorBlobLocatorProcessor.Blob> blobs = locator.getBlobs();
            ColorBlobLocatorProcessor.Util.filterByArea(50, 20000, blobs);
            ColorBlobLocatorProcessor.Util.sortByArea(
                com.qualcomm.robotcore.util.SortOrder.DESCENDING, blobs
            );

            if (blobs.isEmpty()) {
                telemetry.addLine("No matching blobs");
            } else {
                ColorBlobLocatorProcessor.Blob largest = blobs.get(0);
                RotatedRect box = largest.getBoxFit();
                telemetry.addData("Center X (px)", "%.0f", box.center.x);
                telemetry.addData("Center Y (px)", "%.0f", box.center.y);
                telemetry.addData("Area (px)", "%d", largest.getContourArea());
            }
            telemetry.update();
        }

        portal.close();
    }
}
package org.firstinspires.ftc.teamcode

import com.qualcomm.robotcore.eventloop.opmode.LinearOpMode
import com.qualcomm.robotcore.eventloop.opmode.TeleOp
import org.firstinspires.ftc.robotcore.external.hardware.camera.WebcamName
import org.firstinspires.ftc.vision.VisionPortal
import org.firstinspires.ftc.vision.opencv.ColorBlobLocatorProcessor
import org.firstinspires.ftc.vision.opencv.ColorSpace
import org.firstinspires.ftc.vision.opencv.ColorRange
import org.firstinspires.ftc.vision.opencv.ImageRegion
import org.opencv.core.Scalar
import org.opencv.core.RotatedRect

@TeleOp(name = "Guided Color Locator")
class GuidedColorLocator : LinearOpMode() {
    override fun runOpMode() {
        val locator = ColorBlobLocatorProcessor.Builder()
            .setTargetColorRange(ColorRange(ColorSpace.HSV,
                Scalar(15.0, 60.0, 70.0), Scalar(40.0, 255.0, 255.0)))
            .setRoi(ImageRegion.entireFrame())
            .setDrawContours(true)
            .build()

        val portal = VisionPortal.Builder()
            .setCamera(hardwareMap.get(WebcamName::class.java, "Webcam 1"))
            .addProcessor(locator)
            .build()

        telemetry.addLine("Open Camera Stream to check the preview.")
        telemetry.addLine("Press START to display blob measurements.")
        telemetry.update()
        waitForStart()

        while (opModeIsActive()) {
            val blobs = locator.getBlobs()
            ColorBlobLocatorProcessor.Util.filterByArea(50.0, 20000.0, blobs)
            ColorBlobLocatorProcessor.Util.sortByArea(
                com.qualcomm.robotcore.util.SortOrder.DESCENDING, blobs
            )

            if (blobs.isEmpty()) {
                telemetry.addLine("No matching blobs")
            } else {
                val largest = blobs[0]
                val box: RotatedRect = largest.getBoxFit()
                telemetry.addData("Center X (px)", "%.0f", box.center.x)
                telemetry.addData("Center Y (px)", "%.0f", box.center.y)
                telemetry.addData("Area (px)", "%d", largest.getContourArea())
            }
            telemetry.update()
            idle()
        }

        portal.close()
    }
}

The loop runs these steps:

  1. getBlobs() reads the current candidate list.
  2. The area filter removes very small or large candidates.
  3. Sorting puts the largest area at index 0.
  4. isEmpty() prevents an error when the list has no results.
  5. Telemetry lets you compare the measurement with the preview.

Compile against the SDK version installed on your robot. If a method or type differs, use that version's ConceptVisionColorLocator_Rectangle sample and API reference instead of combining code from different versions.

Detection checks

With the robot stationary:

  • Move the object left, center, and right. Does Center X move the same way?
  • Remove the object. Does the code report no blob?
  • Put a similar-colored object in view. Does the detector mistake it for the target?
  • Try the real distance and lighting, not only a close-up under room lights.
  • Tighten the ROI. Does it remove background detections without cutting off the object?

Only after these checks should you map pixels to a simple left/center/right decision. Distance and field position require camera geometry or another calibrated measurement; blob pixel coordinates alone do not provide either.

Troubleshooting

ResultCheck
Preview is blackConfigured camera name, cable, USB connection, and camera initialization.
Preview works but no blobs appearSelected color range, lighting, ROI, and area filter. Compare with the SDK sample.
Many false blobs appearReduce the ROI first; then use area, density, or aspect-ratio filters as shown in FTC Docs.
Results change when lights changeColor thresholds depend on illumination and exposure; test and tune in the expected environment.
Robot chooses the wrong sideCheck image width, camera rotation, and pixel thresholds before connecting detection to movement.

Sources

  • FTC Color Processing overview
  • FTC Color Blob Concepts
  • FTC Color Locator sample
  • FTC Color Locator filters
  • FTC processor initialization
  • AndyMark: BIOBUZZ scoring elements — source photo

VisionPortal

Open an FTC camera, attach a processor, and check the preview

AprilTag detection

Read FTC AprilTag detections safely, including SDK 12 clusters

On this page

Camera checksProcessor setupPortal setupBlob filteringComplete preview OpModeDetection checksTroubleshootingSources