vision¶
org.horizon36596.simloop.vision
Added in v0.2.0-alpha.2
Distortion, CameraOccluders, tilted tags and synthetic frames were added in
v0.2.0-alpha.2. v0.2.0-alpha.1 does not contain them. Use documentation from the tag you
install; for development work, pin the exact source commit you tested. See
Installing it.
A simulated camera on the robot, at two levels:
- Geometry. Which game pieces and AprilTags fall inside the image and where, as numbers. Through a pinhole or a distorted lens, with field structures, the robot's own body and other pieces in the way. For testing the geometry half of a vision pipeline: which pixel is which point on the floor, tracking, and pose from tags.
- Synthetic frames. The same scene drawn as RGB pixels (
SimFrameRenderer), so a real pixel pipeline (an OpenCVVisionProcessor, a colour-blob detector, a contour splitter) can run unchanged in the sim. A synthetic frame is a drawing, not a photograph. Use it to check code paths and geometry. Never tune a colour threshold on it.
What you use¶
| Type | What it is |
|---|---|
CameraLens |
The intrinsics: focal lengths, principal point, image size and, optionally, OpenCV's five distortion coefficients. An OpenCV camera matrix plus distCoeffs. |
CameraMount |
The extrinsics: where the lens sits on the robot and which way it points. |
CameraOccluders |
Solid boxes the camera cannot see through: field boxes (field frame) and robot boxes (robot frame, they turn with the robot). |
SimCamera |
Sees game pieces. Returns CameraSightings. |
CameraSighting |
One piece in the image: centre pixel, apparent radius, range, kind, clipped or not, and how much of it is visible. |
AprilTagLayout |
Where every tag on your field is, plumb or tilted. You supply it. |
SimTagCamera |
Sees AprilTags. Returns TagSightings. |
TagSighting |
One tag: tx/ty in degrees, centre and corner pixels, range, area, camera-to-tag and robot-to-tag transforms. |
SimFrameRenderer |
Draws the scene as a SimFrame: floor tiles, boxes, pieces as shaded spheres, tags. |
SimFrame |
One synthetic frame: RGB bytes, a ground-truth piece and tag label per pixel, and a PNG writer. |
FrameStyle |
The flat colours and the one light frames are painted with. |
RigidTransform3, RobotFramePoint |
A 3-D rotation plus translation, and a point on the floor in the robot frame. |
FakeLimelight3A in fakehardware is built on SimTagCamera, so a Limelight and a
plain camera share one description.
Choose the output your test needs¶
| What you are testing | Use | What the test receives |
|---|---|---|
| Projection, tracking or tag transforms | SimCamera or SimTagCamera |
Sightings and geometry, without rendering pixels |
| Code that calls the SDK's Limelight API | FakeLimelight3A |
SDK result objects, with configured latency, noise and dropouts |
| A colour or contour processor | SimFrameRenderer |
RGB or RGBA bytes and per-pixel ground truth |
Use the geometry APIs when pixels are not part of the code under test. Synthetic tag patterns are not decodable AprilTags; use tag sightings or the Limelight fake for tag-result handling.
How it is used¶
From src/test/java/org/horizon36596/simloop/vision/SimCameraTest.java: a 70 degree, 640 x 400 camera,
6 in forward, 10 in up, tilted 22 degrees down.
CameraLens lens = CameraLens.fromHorizontalFov(70.0, 640, 400);
CameraMount front = CameraMount.front(6.0, 10.0, 22.0);
SimCamera camera = new SimCamera("front", lens, front);
GamePieceTracker tracker = new GamePieceTracker(4, FieldWorld.unbounded());
tracker.updateRobotPose(0.0, 0.0, 0.0);
tracker.addLoosePiece("piece", 30.0, 0.0, 1.4);
// each tick, after the pieces and the robot have moved:
List<CameraSighting> seen = camera.observe(tracker.getPieces(), 0.0, 0.0, 0.0);
camera.record("Vision", tracker.getPieces()); // logs the camera pose and the sightings
To check a pipeline's projection, intersect the sighting's centre ray with the plane through the piece's centre. The last argument is that plane's height above the floor in inches; for a ball resting on the floor, it is the ball's radius. A frame can contain no sightings, so check the list first:
if (!seen.isEmpty()) {
CameraSighting s = seen.get(0);
RobotFramePoint p = camera.groundPointInRobotFrame(
s.getCentreXPx(), s.getCentreYPx(), 1.4); // centre-plane height, inches
// p can be null if the ray does not reach the plane.
// Otherwise compare it with the true position in the ROBOT frame, not the field frame.
}
A distorted lens¶
Give the lens your camera's calibration, in OpenCV's order (k1, k2, p1, p2, k3). Every camera in this
package then projects through it, and groundPointInRobotFrame undistorts first, as a calibrated
pipeline does:
CameraLens calibrated = new CameraLens(fx, fy, cx, cy, 640, 480, k1, k2, p1, p2, k3); // YOUR calibration
// or: CameraLens.fromHorizontalFov(70.0, 640, 480).withDistortion(k1, k2, p1, p2, k3)
Use it to test your undistortion: let the sim lens be distorted and check that your pipeline, given the same coefficients, still puts balls where they are, and that a pipeline that ignores them does not.
Things in the way¶
Pass CameraOccluders to the five-argument observe. Field boxes are your structures; robot boxes are
your chassis, bumper or a raised arm, in the robot frame (X forward, Y left, Z up, origin on the
floor under the robot centre):
CameraOccluders blockers = CameraOccluders.of(
world.getObstacles(), // field frame
Arrays.asList(new ObstacleBox("arm", 8, 9, -6, 6, 2, 12))); // robot frame: turns with the robot
List<CameraSighting> seen = camera.observe(tracker.getPieces(), x, y, heading, blockers);
CameraOccluders copies both lists. Robot-frame boxes turn with the chassis, but an arm's joint
motion does not update them automatically. Build new occluders when a mechanism or field structure
changes shape or position.
Each piece gets 19 rays spread over its disc. getVisibleFraction() is the share not blocked by a box or
a nearer piece, and a piece below getMinVisibleFraction() (default 0.5, withMinVisibleFraction(...)
to change) is not reported. The four-argument observe keeps the old rule: nothing but other pieces'
centre pixels hides anything.
Tags¶
Build a layout from numbers you are entitled to use, then a SimTagCamera with a range window, a maximum
viewing angle and any boxes that block the view:
AprilTagLayout layout = AprilTagLayout.builder()
.addTag(1, 70.0, 30.0, 6.0, Math.PI, 4.0) // id, x, y, z (in), facing (rad), size (in): YOUR numbers
.addTiltedTag(2, 40.0, -20.0, 18.0, Math.PI, Math.toRadians(30.0), 4.0) // face tilted 30 deg up
.build();
SimTagCamera tags = new SimTagCamera(lens, front, 0.0, Double.POSITIVE_INFINITY, 90.0,
Collections.<ObstacleBox>emptyList());
List<TagSighting> visible = tags.observe(layout, robotX, robotY, robotHeading);
List<TagSighting> notBehindTheArm = tags.observe(layout, robotX, robotY, robotHeading, blockers);
Synthetic frames for a pixel pipeline¶
This fragment assumes you already have a tracker, robot pose, blockers, tag layout and processor.
Your test must provide OpenCV and load its native library before creating a Mat. SimLoop does not
install OpenCV or create a VisionPortal.
SimFrameRenderer renderer = SimFrameRenderer.builder(lens, front)
.style(FrameStyle.builder()
.pieceColour("red", 0xC02020) // made-up colours; see the warning below
.pieceColour("blue", 0x2040C0)
.build())
.supersampling(2) // 4 rays a pixel: smooth edges
.sensorNoise(36596, 2.0) // seeded; PLACEHOLDER spread, in 8-bit counts
.build();
SimFrame frame = renderer.render(tracker.getPieces(), x, y, heading, blockers, layout);
// Your real processor, unchanged. SimLoop has no OpenCV dependency; your test has it.
// Initialise once per processor, outside the frame loop. null is suitable only if your processor
// does not use CameraCalibration; otherwise supply the calibration it expects.
processor.init(frame.getWidthPx(), frame.getHeightPx(), null);
// For each frame; captureTimeNanos comes from your simulated capture clock, in nanoseconds.
Mat rgb = new Mat(frame.getHeightPx(), frame.getWidthPx(), CvType.CV_8UC3);
try {
rgb.put(0, 0, frame.getRgbBytes());
processor.processFrame(rgb, captureTimeNanos);
// Read and assert the processor's detections here, before releasing the input.
} finally {
rgb.release();
}
The bytes are RGB, not BGR. Use getRgbaBytes() with CV_8UC4 for a processor that expects RGBA,
or convert explicitly for one that expects BGR. Match the channel order used by your robot's pipeline.
Call init once, then processFrame for each capture; reinitialising per frame can erase tracking state.
Seeded noise also depends on the renderer's frame counter. To repeat a sequence byte for byte, create a fresh renderer with the same seed and render the same scenes in the same order. Two consecutive frames from one noisy renderer need not have identical pixels.
SimFrame.getPieceIdAt(x, y), pixelCountOfPiece(id) and centroidOfPiece(id) are ground truth for
checking what the detector found. writePng(out) saves a frame; the PNG carries a comment saying it is
synthetic. Rendering costs tens of milliseconds a frame at 640 x 480, so keep it to the tests that need
pixels.

A synthetic frame from SimFrameRendererTest: 320 x 240, two pieces, a wall box and a tag. Flat
colours, one light, no shadows. Not a camera image.
Units and frames¶
- Robot pose, piece positions, tag positions, field boxes: field frame, FTC-Cartesian, centre origin,
inches, heading radians CCW from
+X. CameraMountand robot boxes: inches in the robot frame (+forward,+left,+up, origin on the floor under the robot centre). Yaw radians CCW from forward. Pitch radians, positive tilts down.CameraMount.front(...)takes its pitch in degrees.- Pixels:
(0, 0)top-left,+xright,+ydown (the OpenCV convention). - Optical frame:
+ximage right,+yimage down,+zalong the boresight. - Distortion coefficients: dimensionless, OpenCV's model and order, acting on normalised image
coordinates
(X/Z, Y/Z). - Colours:
0xRRGGBB, 8 bits a channel. Sensor noise: standard deviation in 8-bit counts. SimCamera.fieldPose3d(...): metres, WPILib camera axes, so AdvantageScope's 3D Field can use it as a camera override and look through the camera.- Tag size: inches, the edge of the square a detector reports corners for. Tag tilt: radians, the
face normal's angle above horizontal.
TagSightingangles are degrees where the name says.
Visibility rules¶
A piece is sighted when it is on the field (not held), more than its own radius in front of the lens, inside the lens's valid radius, and its apparent disc overlaps the image. It is clipped when part of the disc is outside the image. Without occluders, a piece whose centre pixel is covered by a nearer piece's disc is not reported. With occluders, the visible-fraction rule above decides, and a box that holds the piece (a ball in a tray) or the lens (the camera's housing) does not hide it.
A tag is seen only if, in this order: it is within range; it faces the camera within
maxIncidenceDegrees (a tilted tag's normal is tilted); all four corners are in front of the lens; all
four corners are inside the image, after distortion; and no box (constructor boxes, then field and robot
boxes passed to observe) blocks the line from the lens to any corner. visibility(...) tells you which
rule failed. Distortion moves the corner and centre pixels; tx, ty and the transforms stay the
undistorted geometry a calibrated detector recovers.
Synthetic frames are not real frames¶
SimFrameRenderer draws flat colours under one light. It has no shadows, highlights, textures, motion
blur, rolling shutter, exposure time, white balance, lens blur, vignetting or compression. Its tag
interiors are a pattern made from the id, not a real tag family's code: a real AprilTag decoder will
not decode them, so test tag pose code with SimTagCamera or FakeLimelight3A instead.
So:
- A colour threshold that passes on synthetic frames says nothing about real lighting. If your thresholds can be exercised on synthetic frames, that shows the code path runs. It does not show the thresholds are right, and they must not be tuned there. Tune them on real photos from your camera.
- Pick piece colours that land inside your detector's bands so the code runs; do not move the bands to fit the colours.
- What synthetic frames can show: the pipeline runs end to end, a blob comes back where the piece is, edge-clipped and touching pieces are handled, occluded pieces are not reported, a distorted lens is undistorted correctly, and the whole thing is deterministic.
What this package will not do¶
- No photoreal image. See above. Synthetic frames must never be used to tune colour thresholds.
- No roll. The image's horizontal stays level with the floor.
- Only boxes, pieces and tags block the view. Anything you do not give as a box (a curved structure, another robot unless you give it a box, the far wall unless you give it a box) is transparent. Boxes are axis-aligned, in the field frame or the robot frame.
- Tag-on-tag occlusion is not modelled, and a tag turned within its own plane is not modelled.
- It ships no field. Not this season's, not any season's. Do not build a layout or boxes from the FIRST field CAD: its licence forbids redistribution.
- No noise in
SimCameraorSimTagCamera.FakeLimelight3Aadds seeded noise and dropouts;SimFrameRendereradds seeded pixel noise. A team's own sim glue adds detection noise on top ofSimCameraif it wants it. - No OpenCV. SimLoop hands you bytes; your test turns them into a
Mat. - No frame timing.
SimCameraandSimFrameRenderersee the scene at the instant you call them. Frame rate, exposure latency and processing delay are your sim glue's job (FakeLimelight3Ahas its own).