Hey guys, Paul McWhorter here from TopTechBoy.com. Welcome back to our AI on the Edge series. If you’ve been following along, you already know how to pull high-frame-rate video off your Raspberry Pi 5 using the new picamera2 library, and you know how to use OpenCV to hunt down faces in a crowded frame.
But today, we are taking things a massive step forward. We aren’t just looking for faces anymore—we are looking inside the face to track the eyes.
This lesson highlights one of the most vital concepts in all of computer vision: The Region of Interest (ROI). If you try to scan an entire 1280×720 frame for tiny features like eyes, your frame rate will absolutely tank. Instead, we are going to act like real engineers. We will use a cascading logic approach: find the face first, isolate that exact box, and search only inside that small window for the eyes.
Go ahead and pour yourself a nice, cold glass of iced coffee or a hot cup of black coffee, get your code ready, and let’s break down exactly how this program works.
This is the code we developed in the video:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import cv2 import time from picamera2 import Picamera2 piCam = Picamera2() W=1280 H=720 tStart = time.time() fps = 0 RES = (W,H) piCam.preview_configuration.main.size = RES piCam.preview_configuration.main.format = "RGB888" piCam.preview_configuration.controls.FrameRate=60 piCam.preview_configuration.align() piCam.configure("preview") piCam.start() textLowerLeft = (int(W*.01),int(H*.05)) fontFace = cv2.FONT_HERSHEY_SIMPLEX fontThickness = int(W/425) fontScale = H*.0015 fontColor = (0,0,255) faceFinder = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') eyeFinder = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_eye.xml") while True: deltaT = time.time() - tStart tStart=time.time() fps = fps*.95 + (1/deltaT)*.05 frame= piCam.capture_array() frame=cv2.flip(frame,-1) gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY) faces = faceFinder.detectMultiScale(gray,scaleFactor=1.1, minNeighbors =5, minSize=(100,100)) for (x,y,w,h) in faces: cv2.rectangle(frame, (x,y),(x+w,y+h),(0,255,255),3) eyes =eyeFinder.detectMultiScale(gray[y:y+h,x:x+w],scaleFactor=1.1,minNeighbors=5,minSize=(15,15)) for (i,j,w,h) in eyes: cv2.rectangle(frame,(x+i,y+j),(x+i+w,y+j+h),(255,0,0),3) myText = "FPS: "+str(round(fps,1)) cv2.putText(frame,myText,textLowerLeft,fontFace,fontScale,fontColor,fontThickness) cv2.imshow("Camera", frame) cv2.moveWindow("Camera",0,60) if cv2.waitKey(1)==ord('q'): break cv2.destroyAllWindows() print('Program Terminated') |
Code Architecture & Codex Breakdown
Since you already have the script loaded up in your IDE, let’s dissect the critical logic gates that make this tracking script fast and accurate.
1. Setting Up the High-Performance Pipeline
We configure the Picamera2 frontend to grab a crisp RGB888 array at a resolution of 1280×720 targeting 60 FPS. By using .capture_array(), we bypass slow formatting overhead and feed raw pixel data directly into OpenCV. Because the camera orientation might be flipped depending on your desktop mounting rig, we use cv2.flip(frame, -1) to keep the spatial coordinates intuitive.
2. The Cascading Filter Matrix
Notice how we initialize two distinct classifiers using pre-trained Haar Cascades:
-
haarcascade_frontalface_default.xml(To grab the macro features of the face) -
haarcascade_eye.xml(To grab the micro features of the eyes)
|
1 2 |
faces = faceFinder.detectMultiScale(gray, scaleFactor=<span class="hljs-number">1.1</span>, minNeighbors=<span class="hljs-number">5</span>, minSize=(<span class="hljs-number">100</span>,<span class="hljs-number">100</span>)) |
We pass a minSize parameter of 100×100 pixels for the face detector. Why? Because we don’t care about background noise or tiny false positives across the room. We want to find you, sitting right in front of the workstation.
3. The Magic of the Region of Interest (ROI)
This is where the real engineering happens. Look closely at this inner loop:
|
1 2 |
eyes = eyeFinder.detectMultiScale(gray[y:y+h, x:x+w], scaleFactor=<span class="hljs-number">1.1</span>, minNeighbors=<span class="hljs-number">5</span>, minSize=(<span class="hljs-number">15</span>,<span class="hljs-number">15</span>)) |
Instead of passing the massive gray frame to the eye finder, we slice the array: gray[y:y+h, x:x+w]. This isolates a tiny sub-matrix containing nothing but your face. The search area drops exponentially, keeping our frame rates close to maximum velocity.
4. Re-Mapping Local to Global Coordinates
When the eye detector finds a match inside the sliced face frame, it returns local coordinates (i, j, w, h) relative to the top-left corner of that face box, not the whole screen. If you tried to draw a rectangle directly at (i, j), your eye boxes would be floating erratically in the top-left corner of your monitor!
To fix this spatial offset, we map them back to global coordinate space by adding the face’s original offsets:
-
Global X Position:
x + i -
Global Y Position:
y + j
General Knowledge: How Haar Cascades and ROIs Work Under the Hood
Now that you understand the mechanics of the script, let’s dive into the fundamental computer vision theory that makes legacy Edge AI tracking so efficient.
The Viola-Jones Framework
Haar Cascade classifiers are based on the Viola-Jones object detection framework. Instead of using massive, compute-heavy deep learning neural networks that require powerful discrete GPUs, Haar Cascades utilize simple, binary pixel-intensity features called Haar-like features.
These features act like digital templates looking for specific shifts in brightness:
-
Edge Features: Detects boundaries where a dark zone transitions into a light zone (like the bridge of your nose versus your cheek).
-
Line Features: Useful for identifying long, horizontal elements like eyebrows or the line of the mouth.
-
Center-Surround Features: Excellent for finding eyes, where the dark pupil is surrounded by lighter skin and sclera.
Why Slicing the Array Saves Your Processor
Every time you invoke .detectMultiScale(), OpenCV has to pass a sliding window across the image matrix at multiple scales, performing thousands of additions and subtractions per frame.
Mathematically, if an entire frame has a pixel area, scanning it scales linearly with that total area. By filtering for the face first and establishing a tight Region of Interest (ROI), you reduce the eye tracking search space down to a fractional area.
On resource-constrained hardware like an edge microcontroller or a single-board computer, isolating the matrix dimensions before calling nested lookups is the difference between a sluggish, unusable slideshow and a silky smooth tracking experience.