AI Hints
Home›Computer Vision Tutorial›Video on the Line
Computer Vision · Lesson 60 of 111

Measuring Speed

Optical flow.

⏱ 2 min read▶ Video at 1:35:40🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

Air jets knock bad apples off the belt, and timing is everything. Fire too soon, or too late, and you'll hit the wrong fruit. The belt's speed drifts during the day. So we need to measure it, from the camera, in every frame.

Measuring Speed
From the video: Measuring Speed
  • Air jets knock out bad apples
  • Timing is everything
  • Measure speed from the camera

Tracking points

Here are two frames, where the belt has moved by nine pixels. Good features to track finds small corners that are easy to follow. Then Lucas Kanade finds where each of those moved to. We take the median, so that a few bad tracks can't spoil it. Run it. Two hundred points tracked. The speed comes out at exactly nine pixels per frame, and sideways drift is zero. Multiply that by your frame rate and calibration, and you've got millimetres per second.

flow.pypython
import cv2
import numpy as np
from belt import belt

a, _, _ = belt(n=6, seed=4)
shift = np.float32([[1, 0, 9], [0, 1, 0]])
b = cv2.warpAffine(a, shift, (640, 480), borderMode=cv2.BORDER_REPLICATE)

g1, g2 = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY), cv2.cvtColor(b, cv2.COLOR_BGR2GRAY)
points = cv2.goodFeaturesToTrack(g1, 200, 0.05, 10)
moved, ok, _ = cv2.calcOpticalFlowPyrLK(g1, g2, points, None)
flow = (moved - points)[ok.ravel() == 1].reshape(-1, 2)

print("Points tracked:", len(flow))
print(f"Belt speed: {np.median(flow[:, 0]):.1f} pixels per frame")
print(f"Sideways drift: {np.median(flow[:, 1]):.1f}")
Output
Points tracked: 200
Belt speed: 9.0 pixels per frame
Sideways drift: 0.0
Also uses belt.py
import numpy as np
import cv2

def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
    """Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
    rng = np.random.default_rng(seed)
    h, w = size
    img = np.full((h, w, 3), (70, 75, 80), np.uint8)
    img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
    boxes, spots, taken = [], [], []
    while len(boxes) < n:
        r = int(rng.integers(32, 48))
        x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
        if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
            continue
        taken.append((x, y, r))
        red = int(rng.integers(150, 225))
        cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
        cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
        boxes.append((x - r, y - r, x + r, y + r))
    for x, y, r in taken[:bruises]:
        cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
        spots.append((x, y, r))
    img = cv2.GaussianBlur(img, (3, 3), 0)
    return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spots

Summary

Follow small corners, and take the median. Next, let's use tracking to solve the owner's question properly.

Key takeaways
  • Track corners between frames
  • The median resists bad tracks
  • Convert with calibration
Next up: Counting on a Moving Belt — the line crossing trick