Detection with cascades and DNN modules
Haar cascades, the dnn module for Caffe, ONNX and YOLO models, blob preparation, and post-processing boxes into usable results.
Haar cascades and when to stop using them
import cv2
# cascades ship with OpenCV: the data path is available from cv2.data
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
eye_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_eye.xml")
profile_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + "haarcascade_profileface.xml")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray) # cascades benefit from even contrast
faces = face_cascade.detectMultiScale(
gray,
scaleFactor=1.1, # how much the window grows each pass: closer to 1 is slower
minNeighbors=5, # higher means fewer false positives
minSize=(30, 30),
maxSize=(400, 400),
)
print("faces found:", len(faces))
for x, y, w, h in faces:
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 255, 0), 2)
roi_gray = gray[y:y + h // 2, x:x + w]
roi_colour = image[y:y + h // 2, x:x + w]
eyes = eye_cascade.detectMultiScale(roi_gray, scaleFactor=1.1, minNeighbors=8)
for ex, ey, ew, eh in eyes:
cv2.rectangle(roi_colour, (ex, ey), (ex + ew, ey + eh), (255, 0, 0), 2)| Property | Haar cascade | DNN detector |
|---|---|---|
| Speed on CPU | Very fast | Moderate to slow |
| Accuracy | Low, many false positives | Much higher |
| Viewpoint robustness | Frontal only | Broad |
| Training | Cascade training tools | Needs a full training pipeline |
| Best use | Quick prototype, embedded CPU | Anything user-facing |
| Dependencies | None beyond OpenCV | Model files plus the dnn module |
- Cascades only detect frontal, upright, well-lit faces at a reasonable size. The published performance came from a specific benchmark and does not transfer.
scaleFactornear 1.05 searches more scales and is much slower. 1.1 is the usual compromise; 1.3 misses faces.minNeighborsis the precision-recall dial. Raising it removes false positives and starts missing real detections.- Use cascades for a quick prototype and replace them before shipping. Their false-positive rate on real scenes is far higher than a demo suggests.
The dnn module
import cv2
import numpy as np
# a pretrained detector, loaded via OpenCV's dnn module
net = cv2.dnn.readNetFromONNX("yolov8n.onnx")
# alternatives: readNetFromCaffe(prototxt, caffemodel), readNetFromTensorflow(pb)
USE_CUDA = False
if USE_CUDA:
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
print("CUDA backend enabled")
def detect(net, image, input_size=(640, 640), conf_threshold=0.4, iou_threshold=0.45):
h, w = image.shape[:2]
# blobFromImage handles scaling, mean subtraction and channel order
blob = cv2.dnn.blobFromImage(
image,
scalefactor=1 / 255.0,
size=input_size,
mean=(0, 0, 0),
swapRB=True, # OpenCV is BGR, models expect RGB
crop=False,
)
net.setInput(blob)
outputs = net.forward()
print("raw output shape", outputs.shape)
# YOLOv8 ONNX output is (1, 4 + num_classes, num_boxes)
predictions = outputs[0].T # (num_boxes, 4 + classes)
boxes, scores, class_ids = [], [], []
x_scale, y_scale = w / input_size[0], h / input_size[1]
for row in predictions:
class_scores = row[4:]
class_id = int(class_scores.argmax())
confidence = float(class_scores[class_id])
if confidence < conf_threshold:
continue
cx, cy, bw, bh = row[:4]
x = int((cx - bw / 2) * x_scale)
y = int((cy - bh / 2) * y_scale)
boxes.append([x, y, int(bw * x_scale), int(bh * y_scale)])
scores.append(confidence)
class_ids.append(class_id)
# non-maximum suppression removes overlapping duplicates locally
indices = cv2.dnn.NMSBoxes(boxes, scores, conf_threshold, iou_threshold)
kept = [int(i) for i in np.array(indices).flatten()] if len(indices) else []
return [(boxes[i], scores[i], class_ids[i]) for i in kept]
results = detect(net, image)
for box, score, class_id in results[:10]:
x, y, bw, bh = box
cv2.rectangle(image, (x, y), (x + bw, y + bh), (0, 255, 0), 2)
cv2.putText(image, f"{class_id} {score:.2f}", (x, max(12, y - 6)),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)swapRB=Trueis essential for a model trained on RGB: OpenCV gives you BGR, and without the swap the model sees red and blue exchanged and performs badly.- The blob's
sizemust match what the model was trained with. A mismatch is accepted silently and produces plausible-looking nonsense. NMSBoxesexpects boxes as[x, y, width, height]. Passing[x1, y1, x2, y2]makes suppression behave erratically rather than raising.- The output layout depends on the model's export settings. Print the shape once and write the decoder for that exact layout; every YOLO version differs.
Classical plus DNN
def hybrid_pipeline(image, net, cascade, min_area=1200):
"""Detect candidates cheaply, then confirm the interesting ones with a model."""
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
improved = cv2.equalizeHist(gray)
candidates = cascade.detectMultiScale(improved, 1.1, 5, minSize=(40, 40))
if len(candidates) == 0:
return []
crops, boxes = [], []
for x, y, w, h in candidates:
pad = int(0.15 * w)
x0, y0 = max(0, x - pad), max(0, y - pad)
x1, y1 = min(image.shape[1], x + w + pad), min(image.shape[0], y + h + pad)
crops.append(image[y0:y1, x0:x1])
boxes.append((x0, y0, x1 - x0, y1 - y0))
if not crops:
return []
batch = np.stack([cv2.dnn.blobFromImage(
cv2.resize(crop, (64, 64)), 1 / 255.0, (64, 64), (0, 0, 0),
swapRB=True).ravel() for crop in crops])
# a classifier stage rejects the cascade's false positives cheaply in a batch
net.setInput(batch)
scores = net.forward().ravel()
confirmed = [box for box, score in zip(boxes, scores) if score > 0.7]
return confirmed
def annotate(image, boxes, colour=(0, 255, 0)):
for x, y, w, h in boxes:
cv2.rectangle(image, (x, y), (x + w, y + h), colour, 2)
cv2.putText(image, f"{len(boxes)} detected", (10, 28),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, colour, 2)
return image⚠️
Every detector has a threshold, and every threshold encodes a decision about which error is worse. A missing-object threshold and a false-alarm threshold are different products. Choose the operating point from the cost of each error, and record the threshold with the model version.
FAQ
Can I run YOLO in real time with OpenCV's dnn module?
Yes with a small model such as a nano variant: expect tens of milliseconds per frame on a modern CPU and a few milliseconds with the CUDA backend. Use ONNX rather than a framework-specific format for the widest support.
Why are my detections shifted or scaled wrongly?
The blob size and the coordinate scaling do not match the model's input. Compute the scale factors from the actual input size you passed to
blobFromImage, and check swapRB.Related
Contours and object detection basics Video capture and processing
Last refreshed 2026-09-18.