VisionPortal
Open an FTC camera, attach a processor, and check the preview
VisionPortal is the SDK's camera manager. It opens a webcam or supported camera, shows a preview, and sends frames to one or more processors. An AprilTag processor or Color Locator processor examines those frames and returns measurements for your .

FTC Docs example image. The labels are detection overlays; your own OpMode still needs to read and check detection data before the acts.
Setup order
The setup has three jobs, in this order:
- Create a processor. Choose what the camera should detect.
- Build a VisionPortal. Tell it which configured camera to open, then attach the processor.
- Read the processor result. Use the values in your OpMode only after confirming the preview and measurements.
The processor is not the camera, and creating a processor alone does not start a video stream.
Processor setup
For a first AprilTag preview, the FTC SDK offers a convenience method:
AprilTagProcessor aprilTag = AprilTagProcessor.easyCreateWithDefaults();val aprilTag = AprilTagProcessor.easyCreateWithDefaults()This makes the processor with its default options. Use the Builder API when you need to configure options such as overlays or a tag library. The matching FTC sample shows the exact options available in your installed SDK.
Portal setup
This example assumes a webcam whose Robot Controller configuration name is Webcam 1:
VisionPortal portal = new VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
.addProcessor(aprilTag)
.build();val portal = VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName::class.java, "Webcam 1"))
.addProcessor(aprilTag)
.build()If you use an Android Robot Controller phone camera or a different webcam name, follow the camera selection in the current SDK sample. A misspelled configured name prevents the camera from opening.
Preview checks
While the OpMode is in , open the camera stream. Check that:
- the image is live and upright;
- the target can be seen at the distance where the robot will use it;
- the processor draws an overlay when a target is recognized;
- telemetry changes when you move or hide the target.
Keep the robot stationary during this test. When you finish using the camera, close the portal if the OpMode or SDK sample owns its lifecycle. Do not start by steering a robot from unverified vision data.
Processor selection
- Use AprilTag to detect known printed markers and, when tag metadata and camera calibration permit, estimate pose relative to the camera.
- Use the FTC Color processor to estimate a predominant color in an image region.
- Use the FTC Color Locator to find blobs of a selected color and measure their image position and shape.
- Use EasyOpenCV when your team needs a custom OpenCV pipeline.
The current FTC color processors are part of VisionPortal. AprilTag, color processors, and camera preview may be combined in one portal when needed; each processor still consumes processing time. Start with one processor so it is easier to see which part is not working.
Custom processors
An EasyOpenCV OpenCvPipeline and an FTC VisionProcessor have different camera and frame-processing APIs. You cannot pass an OpenCvPipeline to VisionPortal.Builder.addProcessor().
You can reuse the OpenCV steps from a pipeline by moving them into a class that implements VisionProcessor. The main change is where each part goes:
- Move setup that only needs to happen once into
init(). - Move image operations from
OpenCvPipeline.processFrame()intoVisionProcessor.processFrame(). - Return a small result (or store a thread-safe result) for the OpMode to read. Use
onDrawFrame()if you need to draw on VisionPortal's preview.
Here is a small example that finds the center of blue pixels. It follows the same OpenCV pattern used in many EasyOpenCV pipelines: convert to HSV, threshold a color, then measure the mask. The bounds are just starting values; camera lighting and the target material change what works.
import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration;
import org.firstinspires.ftc.vision.VisionProcessor;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.imgproc.Moments;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
public class BlueCenterProcessor implements VisionProcessor {
private final Mat hsv = new Mat();
private final Mat mask = new Mat();
private volatile double centerX = Double.NaN;
private boolean released;
@Override
public void init(int width, int height, CameraCalibration calibration) {
// Runs once when VisionPortal initializes this processor.
}
@Override
public synchronized Object processFrame(Mat frame, long captureTimeNanos) {
if (released) return null;
// VisionPortal supplies RGBA frames; RGB2HSV accepts four channels and ignores alpha. HSV makes color thresholds easier to tune.
Imgproc.cvtColor(frame, hsv, Imgproc.COLOR_RGB2HSV);
Core.inRange(
hsv,
new Scalar(90, 80, 50), // lower H, S, V
new Scalar(130, 255, 255), // upper H, S, V
mask
);
Moments moments = Imgproc.moments(mask, true);
centerX = moments.get_m00() > 0
? moments.get_m10() / moments.get_m00()
: Double.NaN;
return null; // No preview overlay in this first example.
}
@Override
public void onDrawFrame(
android.graphics.Canvas canvas,
int onscreenWidth,
int onscreenHeight,
float scaleBmpPxToCanvasPx,
float scaleCanvasDensity,
Object userContext
) {
// Optional: draw a box or marker on the preview here.
}
public double getCenterX() {
return centerX;
}
public synchronized void release() {
if (released) return;
released = true;
centerX = Double.NaN;
hsv.release();
mask.release();
}
}import android.graphics.Canvas
import org.firstinspires.ftc.robotcore.internal.camera.calibration.CameraCalibration
import org.firstinspires.ftc.vision.VisionProcessor
import org.opencv.core.Core
import org.opencv.core.Mat
import org.opencv.imgproc.Moments
import org.opencv.core.Scalar
import org.opencv.imgproc.Imgproc
class BlueCenterProcessor : VisionProcessor {
private val hsv = Mat()
private val mask = Mat()
@Volatile private var centerX = Double.NaN
private var released = false
override fun init(width: Int, height: Int, calibration: CameraCalibration) {
// Runs once when VisionPortal initializes this processor.
}
@Synchronized
override fun processFrame(frame: Mat, captureTimeNanos: Long): Any? {
if (released) return null
// VisionPortal supplies RGBA frames; RGB2HSV accepts four channels and ignores alpha. HSV makes color thresholds easier to tune.
Imgproc.cvtColor(frame, hsv, Imgproc.COLOR_RGB2HSV)
Core.inRange(hsv, Scalar(90.0, 80.0, 50.0), Scalar(130.0, 255.0, 255.0), mask)
val moments: Moments = Imgproc.moments(mask, true)
centerX = if (moments.m00 > 0.0) moments.m10 / moments.m00 else Double.NaN
return null // No preview overlay in this first example.
}
override fun onDrawFrame(
canvas: Canvas,
onscreenWidth: Int,
onscreenHeight: Int,
scaleBmpPxToCanvasPx: Float,
scaleCanvasDensity: Float,
userContext: Any?
) {
// Optional: draw a box or marker on the preview here.
}
fun getCenterX() = centerX
@Synchronized
fun release() {
if (released) return
released = true
centerX = Double.NaN
hsv.release()
mask.release()
}
}Attach that processor just like an SDK processor, then read its latest result during INIT. This OpMode fragment assumes Webcam 1 is the name in your Robot Controller configuration:
BlueCenterProcessor blueProcessor = new BlueCenterProcessor();
VisionPortal portal = new VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName.class, "Webcam 1"))
.addProcessor(blueProcessor)
.build();
try {
while (opModeInInit()) {
double x = blueProcessor.getCenterX();
telemetry.addData("Blue center X", Double.isNaN(x) ? "not found" : x);
telemetry.update();
idle();
}
} finally {
portal.close();
blueProcessor.release();
}val blueProcessor = BlueCenterProcessor()
val portal = VisionPortal.Builder()
.setCamera(hardwareMap.get(WebcamName::class.java, "Webcam 1"))
.addProcessor(blueProcessor)
.build()
try {
while (opModeInInit()) {
val x = blueProcessor.getCenterX()
telemetry.addData("Blue center X", if (x.isNaN()) "not found" else x)
telemetry.update()
idle()
}
} finally {
portal.close()
blueProcessor.release()
}In SDK 12, VisionPortal.close() starts asynchronous camera shutdown. Both processFrame() and release() above use the same lock and a terminal released flag: cleanup waits for an active callback, and later callbacks skip the freed Mats. The camera owns the input frame; never release it yourself.
Adapt the processor interface and its three callbacks; most of your OpenCV operations can stay the same. Confirm the frame color order and threshold against the FTC sample for your installed SDK, then tune using the live preview. For a standard color-blob task, the SDK's Color Blob Locator guide may save you from writing this processor yourself. EasyOpenCV uses its own pipeline API; the VisionProcessor interface is the route for VisionPortal.
Calibration and SDK versions
A live preview does not prove that a pose estimate is accurate. AprilTag pose depends on correct tag size and camera calibration for the camera and resolution. Some cameras have built-in calibration; others require calibration. Read the official webcam and calibration guide before using pose for field localization.
FTC SDK 12.0 introduced AprilTag cluster detections. If your SDK is 12.0 or later, see AprilTag Detection before copying older code that assumes every detection has an .id field.