Tutorial: track a coloured object
Point a webcam at a bright ball and follow it around the frame — that's this tutorial. It's a satisfying little project that teaches a pattern you'll reuse constantly: isolate by colour → find the blob → locate its centre. We build it on a drawn frame first (so it runs here), then wire it to live video.
import scalacv.*
OpenCv.load()
The idea
Colour is a strong, cheap signal. If your target is a distinct colour — a green ball, a red marker — you can find it without any model:
- Convert to HSV, where "greenish" is a range of hue (robust to lighting), not a fragile RGB box.
- Threshold that hue range into a black-and-white mask.
- Find contours (blobs) in the mask and take the largest — that's your object.
- Read its centroid — the point to track.
Step 1 — a test frame
We'll draw a dark frame with a single green "ball" so the tutorial runs without a camera:
val frame =
Image.blank(320, 240, Scalar(30, 30, 30)) // dark background
.drawCircle(Point(210, 120), 28, Scalar.Green, Thickness.Filled) // the green ball
Step 2 — build a colour mask
Convert to HSV and keep only green hues. In OpenCV's HSV, hue runs 0–179; green sits around 60, so a 35–85 band catches it. We need the mask and the original frame later, so we work on a .copy:
val mask = frame.copy.toHsv.inRange(Scalar(35, 80, 80), Scalar(85, 255, 255))
mask is a one-channel black-and-white image: white where the ball is, black elsewhere.
Step 3 — find the biggest blob
Contours give us every white region; the ball is the largest. maxByOption handles the "nothing matched" case cleanly (no target in view):
val blobs = mask.contours() // Seq[Contour]; mask stays alive
val target = blobs.maxByOption(_.area) // Option[Contour] — None if the colour isn't present
target.map(_.area).getOrElse(0.0) > 0.0 // true — we found the ball
// res1: Boolean = true
Step 4 — locate its centre
A contour's centroid is its centre of mass — exactly the point to track. It's an Option because a degenerate (zero-area) blob has none:
val center: Option[Point] = target.flatMap(_.centroid)
mask.close() // done with the mask
center.map(p => (p.x.toInt, p.y.toInt)) // roughly the ball's centre, ~ (210, 120)
// res3: Option[Tuple2[Int, Int]] = Some((210, 120))
Step 5 — mark the target
Draw a marker on the original frame at the tracked point. drawCircle consumes the frame; if we found nothing, we leave the frame as-is:
val annotated: Either[CvError, Array[Byte]] =
center match
case Some(p) => frame.drawCircle(p, 8, Scalar.Red, Thickness.Stroke(3)).bytes(".png")
case None => frame.bytes(".png") // no target this frame
annotated.map(_.length).getOrElse(0) > 0
// res4: Boolean = true
That's the whole tracker in one frame. On video, you just run it on every frame.
Live: track across a video
Wrap the per-frame logic in Camera.foreach (which owns and closes each frame) and you have a live colour tracker. Here we print the target's position each frame:
def trackGreen(source: String): Unit =
Camera.usingFile(source) { cam =>
cam.foreach() { frame =>
// frame is owned by foreach; branch off a copy to build the mask.
val mask = frame.copy.toHsv.inRange(Scalar(35, 80, 80), Scalar(85, 255, 255))
try
mask.contours().maxByOption(_.area).flatMap(_.centroid) match
case Some(p) => println(s"target at (${p.x.toInt}, ${p.y.toInt})")
case None => println("target lost")
finally mask.close()
}
}
Every frame allocates and frees exactly one mask; nothing accumulates over a long video.
Make it yours
- Different colour — change the HSV band. Red straddles the 0/179 wrap-around, so it needs two ranges (
inRangetwice, then combine the masks) — a good exercise. - Reject noise — filter blobs by
areabeforemaxByOption, so a stray speck never becomes the "target." - Smooth the path — feed each centroid to a Kalman filter so the marker glides instead of jittering, and coasts through a frame where the ball is briefly hidden.
- Draw a trail — keep the last N centres and
drawCircleeach, fading older ones. - Record it — swap
foreachforrecordToto write an annotated video.
Next
- Colour masking — HSV thresholding in depth (including the red wrap-around).
- Contours — area, centroid, bounding box, convex hull.
- Tracking — Kalman smoothing and multi-object identity.