DEV Community

Cover image for Teaching a Webcam to See: Building a Real-Time Object Detection Pipeline with YOLOv8
Sandip Subedi
Sandip Subedi

Posted on

Teaching a Webcam to See: Building a Real-Time Object Detection Pipeline with YOLOv8

A few weeks ago, my portfolio was mostly tabular data — CSVs, groupby(), Seaborn charts, the occasional RAG pipeline. Useful skills, but all of it lived inside static datasets. I wanted to try something that moved: a model that could look at a live video feed and tell me what it was seeing, frame by frame, in real time.

This post walks through what I built, what broke along the way, and what I'd tell someone starting the same project today.

The goal

Detect people and vehicles — starting from a single photo, then working up to a live webcam feed. Nothing exotic. The interesting part wasn't the end result; it was the gap between "it works on one image" and "it works live, continuously, without falling over."

Stack: YOLOv8 (Ultralytics) for detection, OpenCV for video I/O and drawing, Python holding it all together.

Stage 1 — Prove it works on a single image first

Before touching a webcam, I ran YOLOv8 on one static test photo. This sounds like a trivial step, and it is — which is exactly why I didn't skip it.

import cv2
import matplotlib.pyplot as plt
from ultralytics import YOLO

image = cv2.imread("bike_test.jpg")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

plt.imshow(image)
plt.axis("off")
Enter fullscreen mode Exit fullscreen mode

One thing that trips up almost everyone the first time: OpenCV reads images in BGR, not RGB. Skip the cv2.cvtColor(..., cv2.COLOR_BGR2RGB) step and your colors come out looking sunburnt — reds and blues swapped. It's a one-line fix, but only if you know to look for it.

From there, loading and running YOLOv8 is almost anticlimactic:

model = YOLO("yolov8n.pt")  # the "n" (nano) variant — fastest, good enough for a first pass
results = model("bike_test.jpg")
results[0].save(filename="output.jpg")
Enter fullscreen mode Exit fullscreen mode

Three lines, and I had bounding boxes around a bicycle in the test image. Sanity-checking on a single frame here saved me from debugging model-loading issues and real-time performance issues at the same time later — one problem at a time.

Stage 2 — Take it live

Once the model worked on a photo, the next step was pointing it at a webcam instead of a file path. Conceptually it's the same call, just wrapped in a loop:

model = YOLO('yolov8n.pt')
cap = cv2.VideoCapture(0)  # 0 = default webcam

while True:
    success, frame = cap.read()
    if not success:
        break

    results = model(frame, stream=True)

    for result in results:
        for box in result.boxes:
            label = result.names[int(box.cls)]
            if label == 'person':
                x1, y1, x2, y2 = map(int, box.xyxy[0])
                cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                cv2.putText(frame, label, (x1, y1 - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

    cv2.imshow('Live Person Detection', frame)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()
Enter fullscreen mode Exit fullscreen mode

stream=True matters here — it tells Ultralytics to process frames as a generator instead of batching them, which keeps memory use flat instead of growing frame after frame. The first time I ran this and saw a green box lock onto me and follow me around the frame, it didn't feel like "a few lines of code" — it felt like the thing had noticed me.

Stage 3 — Reaching too far, and what I learned from pulling back

The natural next step felt obvious: why stop at "there's a person," when I could also guess their emotion and gender? I wired in DeepFace on top of the YOLO output, cropping each detected person and running facial analysis on the crop.

It worked — briefly, on paper. In practice, DeepFace pulls in TensorFlow as a dependency, and TensorFlow on Windows has a well-earned reputation for being finicky about versions, CUDA, and DLLs. I lost more time fighting installation errors than I spent on the actual computer vision.

I tried a leaner alternative — swapping DeepFace for two small models loaded directly through OpenCV's own dnn module (a Caffe model for gender, an ONNX model for emotion), which avoids the TensorFlow dependency entirely. It's a genuinely good pattern, and I'd recommend it to anyone who wants this feature. But for this project, I made a deliberate call to cut it. Not every feature that's technically possible is worth the added fragility, especially in something meant to be a clean, demonstrable portfolio piece. I'd rather ship two stages that run reliably every time than three stages where the last one might not run on your machine.

That's the real lesson from this project, more than any single line of code: knowing when to cut scope is part of the engineering, not a failure of it.

What I'd tell someone starting this today

  • Validate on one static example before going real-time. It isolates whether the problem is your model setup or your real-time loop.
  • cv2.cvtColor(..., cv2.COLOR_BGR2RGB) is not optional if you're displaying OpenCV images anywhere outside OpenCV's own imshow.
  • stream=True on video keeps Ultralytics from silently ballooning memory usage over a long-running loop.
  • Clamp your bounding boxes. max(0, x1) and min(frame.shape[1], x2) cost you two lines and save you from a crash the moment a detection box clips the edge of the frame.
  • Heavier isn't always better. A dependency like TensorFlow-via-DeepFace can cost you more in reliability than it gives you in features. Lighter, more surgical tools (OpenCV's own dnn module, in this case) are often the better trade for a portfolio project meant to just work when someone clones it.

What's next

I'm keeping the gender/emotion layer as a documented "next step" rather than shipping it half-working — I'd rather build it properly with the OpenCV DNN approach once I've got a clean way to package the model downloads. Beyond that, I want to add an on-screen FPS counter so I can actually quantify the cost of each added layer, instead of eyeballing "does this feel slower."

If you want to see the full notebook, it's on my GitHub — feedback and issues welcome, especially if you spot something I'd benefit from unlearning early rather than later.

Repo: github.com/sandipsubedi0
Connect: linkedin.com/in/sandip-subedi01

Top comments (0)