How to detect faces with OpenCV Haar cascades

Classic face detection in OpenCV uses trained cascade files to scan an image for frontal face patterns. Haar cascades are a lightweight fit for local smoke tests, simple camera prototypes, and legacy code paths where a neural-network detector is more setup than the task needs.

The Python smoke script loads the packaged haarcascade_frontalface_default.xml file through cv.data.haarcascades, converts the input image to grayscale, equalizes the histogram, and passes the result to detectMultiScale(). It writes a boxed copy of the image and prints the face count plus each detected rectangle.

Use a sample image or a consented local image with a clear frontal face for the first run. Haar cascades can miss rotated, covered, or side-profile faces and can return false positives, so the printed count is a detector check, not identity recognition.

Steps to detect faces with OpenCV Haar cascades:

  1. Create the input directory.
    $ mkdir -p input
  2. Download a sample face image.
    $ curl --location --silent --show-error --output input/person.jpg https://raw.githubusercontent.com/opencv/opencv/4.x/samples/data/lena.jpg

    Use a consented local image instead by copying it to input/person.jpg.

  3. Create the Haar cascade detection script.
    detect_faces_haar.py
    #!/usr/bin/env python3
    import argparse
    from pathlib import Path
     
    import cv2 as cv
     
     
    default_cascade = (
        Path(cv.data.haarcascades) / "haarcascade_frontalface_default.xml"
    )
     
    parser = argparse.ArgumentParser(
        description="Detect frontal faces with an OpenCV Haar cascade."
    )
    parser.add_argument("image", help="input image path")
    parser.add_argument(
        "--output",
        default="output/faces.jpg",
        help="annotated image path to write",
    )
    parser.add_argument(
        "--cascade",
        default=str(default_cascade),
        help="Haar cascade XML file",
    )
    parser.add_argument(
        "--scale-factor",
        type=float,
        default=1.1,
        help="image pyramid step used by detectMultiScale",
    )
    parser.add_argument(
        "--min-neighbors",
        type=int,
        default=5,
        help="neighbor count required to keep a detection",
    )
    args = parser.parse_args()
     
    image_path = Path(args.image)
    output_path = Path(args.output)
    cascade_path = Path(args.cascade)
     
    image = cv.imread(str(image_path))
    if image is None:
        raise SystemExit(f"Could not read image: {image_path}")
     
    classifier = cv.CascadeClassifier(str(cascade_path))
    if classifier.empty():
        raise SystemExit(f"Could not load cascade: {cascade_path}")
     
    gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)
    gray = cv.equalizeHist(gray)
     
    faces = classifier.detectMultiScale(
        gray,
        scaleFactor=args.scale_factor,
        minNeighbors=args.min_neighbors,
        minSize=(30, 30),
    )
     
    annotated = image.copy()
    for x, y, width, height in faces:
        cv.rectangle(
            annotated,
            (x, y),
            (x + width, y + height),
            (0, 255, 0),
            3,
        )
     
    output_path.parent.mkdir(parents=True, exist_ok=True)
    if not cv.imwrite(str(output_path), annotated):
        raise SystemExit(f"Could not write output image: {output_path}")
     
    height, width = image.shape[:2]
    print(f"cascade: {cascade_path.name}")
    print(f"image size: {width}x{height}")
    print(f"faces detected: {len(faces)}")
     
    for index, (x, y, face_width, face_height) in enumerate(faces, start=1):
        print(
            f"face {index}: x={x} y={y} "
            f"width={face_width} height={face_height}"
        )
     
    print(f"wrote: {output_path}")
  4. Run the detector and confirm that the cascade reports a face rectangle.
    $ python3 detect_faces_haar.py input/person.jpg --output output/faces.png
    cascade: haarcascade_frontalface_default.xml
    image size: 512x512
    faces detected: 1
    face 1: x=216 y=202 width=174 height=174
    wrote: output/faces.png

    A count of 0 means no region passed the cascade threshold. Try a clearer frontal face before lowering minNeighbors.

  5. Open output/faces.png in an image viewer.

    The green rectangle should surround the detected face. If the box is offset or a non-face region is marked, test a different scaleFactor or minNeighbors value before using the result downstream.

  6. Remove the smoke-test files when the detector check is complete.
    $ rm -f detect_faces_haar.py input/person.jpg output/faces.png