Skip to content
Buy 10+ on select items — save 10% auto-applied
Free US shipping on orders $35+
Order by 3pm ET — ships same-day from the US
Worldwide shipping via DHL
Skip to main content

Arduino Uno OpenCV: Face-Tracking Pan-Tilt Webcam

October 10, 2026 9 views

Arduino Uno OpenCV: Face-Tracking Pan-Tilt Webcam | ShillehTek
Project

Build an Arduino Uno pan-tilt webcam that uses Python and OpenCV face detection to steer two servos and show lock status with LEDs, with parts from ShillehTek.

2 hr Intermediate6 parts

Project Overview

Arduino Uno + OpenCV face tracking pan-tilt webcam: In this project, a Python script on your computer uses OpenCV to detect the biggest face in a webcam feed, then sends pan and tilt angles over USB to an Arduino Uno that drives two servos and three status LEDs so the camera follows you.

Face detection is far too heavy for an 8-bit microcontroller, but trivial for a laptop. This build splits the work: the computer does the vision and sends commands, while the Arduino focuses on smooth servo control and simple status outputs (red when nobody is in view, yellow while it is moving to catch up, green when a face is locked near the center). If you walk out of frame, the head starts a slow search sweep until it finds you again.

  • Time: ~2 hours
  • Skill level: Intermediate
  • What you will build: A webcam on a two-servo pan-tilt head that follows the nearest face, with a Python + OpenCV tracker, an Arduino servo controller, and red/yellow/green status LEDs.
Arduino Uno face-tracking pan-tilt webcam build with two servos and red, yellow, and green status LEDs
A webcam on two servos, with red, yellow and green status LEDs on the base.

Parts List

From ShillehTek

External

  • A light USB webcam (under about 100 g) and a Windows, macOS, or Linux computer with Python 3
  • A 9 to 12 V DC adapter to feed the LM2596
  • Zip ties or a small screw to fix the webcam to the tilt bracket

Note: Do not run two servos from the Uno's 5V pin. A sudden move can pull more current than USB can supply and reset the board mid-track. Set the LM2596 to 5.0 V before connecting anything, and join its ground to the Uno's ground. All video processing happens on your own computer; nothing is uploaded anywhere.

Step-by-Step Guide

Step 1 - Wire the servos, LEDs, and power

Arduino Uno wiring diagram for face-tracking pan-tilt webcam with servos on D8 and D9 and red, yellow, green LEDs on D2 to D4
The original wiring: servos on D8 and D9, LEDs on D2 to D4. That build used a 7805 regulator from 9 V; the LM2596 module takes its place here.

Goal: Two servos with their own supply, three status lights, and one USB link to the computer.

What to do: Connect the DC adapter to the LM2596 input and turn its trimmer until the output reads 5.0 V on a multimeter. LM2596 OUT+ goes to both servos' red wires. LM2596 OUT- goes to both servos' brown wires and Uno GND. Put a 470 µF capacitor across the servo rail (stripe to ground). Pan servo signal (orange) goes to D8 and tilt servo signal goes to D9.

For the LEDs: red LED to D2, yellow LED to D3, green LED to D4. Connect each LED long leg to the pin, and the short leg through a 220 Ω resistor to GND. Power and control the Uno over its USB cable.

Expected result: A clean 5 V servo rail that shares ground with the Uno, and three LEDs on the breadboard.

Step 2 - Upload the Arduino sketch

Goal: Read pan, tilt, and state messages from the computer, then drive two servos and three LEDs.

Code:

#include <Servo.h>

Servo panServo, tiltServo;
const byte PAN_PIN = 8, TILT_PIN = 9;
const byte RED = 2, YELLOW = 3, GREEN = 4;   // no face / moving to it / locked on

char buf[20];
byte n = 0;
bool inMsg = false;
unsigned long lastMsg = 0;

void showState(int s) {
  digitalWrite(RED, s == 0);
  digitalWrite(YELLOW, s == 1);
  digitalWrite(GREEN, s == 2);
}

void setup() {
  Serial.begin(115200);
  pinMode(RED, OUTPUT);
  pinMode(YELLOW, OUTPUT);
  pinMode(GREEN, OUTPUT);
  panServo.attach(PAN_PIN);
  tiltServo.attach(TILT_PIN);
  panServo.write(90);                         // straight ahead
  tiltServo.write(90);                        // level
  showState(0);
}

void loop() {
  while (Serial.available()) {                // messages look like <92,85,1>
    char c = Serial.read();
    if (c == '<') {
      inMsg = true;
      n = 0;
    } else if (c == '>' && inMsg) {
      buf[n] = 0;
      inMsg = false;
      int pan, tilt, state;
      if (sscanf(buf, "%d,%d,%d", &pan, &tilt, &state) == 3) {
        panServo.write(constrain(pan, 0, 180));
        tiltServo.write(constrain(tilt, 20, 160));
        showState(state);
        lastMsg = millis();
      }
    } else if (inMsg && n < sizeof(buf) - 1) {
      buf[n++] = c;
    }
  }
  if (millis() - lastMsg > 2000) showState(0);   // the PC script stopped talking
}

What to do: Upload to the Uno (board: Arduino Uno). The start and end markers mean a half-received message is ignored, so a glitch cannot send a servo to a random angle.

Expected result: Both servos snap to 90° and hold there, and the red LED is on.

Step 3 - Assemble the pan-tilt head

Goal: Mechanical center that matches software center.

What to do: With the sketch running and the servos holding 90°, build the bracket kit: fit the pan servo in the base and the tilt servo in the upper bracket. Press on the horns so the head faces straight ahead and the tilt plate is level.

Fix the webcam to the tilt plate with zip ties or a screw, as close to the tilt axis as you can. A camera hanging far forward makes the small servo work much harder. Leave a loop of slack in the webcam's USB cable so it never pulls against the pan movement.

Expected result: A head that looks straight at you when both servos are at 90°.

Step 4 - Run the Python OpenCV face tracker

Goal: Detect a face on the computer and stream servo angles and a state value to the Arduino over USB serial.

Code:

# face_tracker.py  -  pip install opencv-python pyserial
import time
import cv2
import serial

PORT = "COM5"              # Windows COMx, macOS /dev/cu.usbserial-xxxx or /dev/cu.usbmodemxxxx, Linux /dev/ttyACM0
CAMERA = 0                 # 0 = first webcam; try 1 if a laptop's built-in camera opens instead
KP = 0.02                  # degrees per pixel of error, per frame
DEAD = 30                  # pixels from the centre that count as "locked on"
PAN_DIR, TILT_DIR = 1, -1  # flip a sign if the camera turns away from your face

arduino = serial.Serial(PORT, 115200, timeout=0)
time.sleep(2)              # the Uno resets when the port opens
cam = cv2.VideoCapture(CAMERA)
cam.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
detector = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")

pan, tilt = 90.0, 90.0
last_seen, sweep = time.time(), 1

while True:
    ok, frame = cam.read()
    if not ok:
        break
    h, w = frame.shape[:2]
    gray = cv2.equalizeHist(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY))
    faces = detector.detectMultiScale(gray, scaleFactor=1.2, minNeighbors=6, minSize=(60, 60))

    if len(faces):
        x, y, fw, fh = map(int, max(faces, key=lambda f: f[2] * f[3]))   # biggest (closest) face
        ex = (x + fw / 2) - w / 2                               # horizontal error in pixels
        ey = (y + fh / 2) - h / 2                               # vertical error in pixels
        if abs(ex) > DEAD:
            pan -= PAN_DIR * KP * ex
        if abs(ey) > DEAD:
            tilt -= TILT_DIR * KP * ey
        state = 2 if abs(ex) <= DEAD and abs(ey) <= DEAD else 1
        last_seen = time.time()
        colour = (0, 255, 0) if state == 2 else (0, 200, 255)
        cv2.rectangle(frame, (x, y), (x + fw, y + fh), colour, 2)
    else:
        state = 0
        if time.time() - last_seen > 3:                          # nobody for 3 s: slow search sweep
            pan += sweep * 0.8
            if pan > 150 or pan < 30:
                sweep = -sweep
            tilt += (90 - tilt) * 0.05                          # drift back to level while searching

    pan = max(0, min(180, pan))
    tilt = max(20, min(160, tilt))
    arduino.write(f"<{int(pan)},{int(tilt)},{state}>".encode())

    cv2.circle(frame, (w // 2, h // 2), DEAD, (255, 255, 255), 1)
    cv2.imshow("ShillehTek face tracker - press q to quit", frame)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cam.release()
cv2.destroyAllWindows()
arduino.close()

What to do: Install the packages with pip install opencv-python pyserial. Close the Arduino Serial Monitor (only one program can use the port). Set PORT to the Uno's port as shown in the Arduino IDE, plug in the webcam, and run python face_tracker.py. The Haar cascade file ships inside OpenCV, so there is nothing else to download.

Expected result: A window shows the camera view with a box around your face. Move sideways and the head turns to follow, with the LED yellow while it catches up and green once your face sits inside the small center circle. Step out of view: red, and after three seconds the head starts sweeping left and right until it finds you.

Step 5 - Tune the response

Goal: Smooth, confident tracking instead of jitter or chasing.

What to do: If the head turns away from you, flip the sign of PAN_DIR (left to right) or TILT_DIR (up to down). It depends on which way round the servos are mounted. KP sets the speed: around 0.01 is calm, 0.04 is snappy. If the head swings past your face and back, it is too high because the camera is always a few frames behind the servos. A larger DEAD zone stops the head hunting when you are nearly centered.

For detection, good front lighting matters more than anything. Lower minNeighbors to catch faces sooner (at the cost of more false boxes) or raise minSize to ignore faces far away.

Expected result: The head settles on your face in well under a second and stays there while you move around the room.

Step 6 - Take it further

Goal: Extend the same platform to more capable tracking and new use cases.

What to do: The Haar cascade mainly finds faces looking at the camera. OpenCV's DNN detector (cv2.FaceDetectorYN with the small YuNet model file) also handles turned heads and dim rooms. Swap face detection for color tracking (an HSV threshold and cv2.findContours) to follow a ball or a marker. Save a clip with cv2.VideoWriter whenever a face appears for a simple person-aware camera. Or replace the Uno with an ESP32 that receives the same <pan,tilt,state> messages over Wi-Fi, so the head can sit anywhere in the room.

Expected result: A vision-guided pan-tilt platform you can point at whatever problem comes next.

Conclusion

You built a face-tracking camera by giving each part the job it is best at: OpenCV on the computer does the seeing, a short Python loop turns pixel errors into angles, and an Arduino Uno drives two MG90S servos on a pan-tilt bracket, with three LEDs indicating tracking state.

Want the exact parts used in this build? Grab them from ShillehTek.com. If you want help customizing this project or building something for your product, check out our IoT consulting services.

Credits

All photos and images in this tutorial are credited to Little_french_kev on Hackster.io. The original guide by Little_french_kev served as the reference for this ShillehTek version. We thank them for their excellent work in the maker community.

Parts for this build

Everything used in this tutorial. Uncheck what you already have.

5 of 6 in stock
0 parts selected $0.00