Hello everyone! Paul McWhorter here from toptechboy.com, and welcome back to our thrilling adventure into computer vision and edge AI. Today, we are dropping Lesson 59 of our AI on the Edge series.
In our previous sessions, we got our Raspberry Pi 5 looking at the world using MediaPipe object detection, we learned how to lock onto targets using pan-and-tilt servos, and we even brought in speech-to-text and text-to-speech capabilities via the Fusion Hat so our system could actually listen and talk back to us.
But there has been one nagging limitation: Static tracking. Up until now, if your object of interest wasn’t sitting right inside the camera’s initial field of view, your Pi just sat there blindly staring straight ahead, waiting for magic to happen.
Well, no more! Today, we are leveling up our system to feature Active Scanning.
The Motivation: From Passive Waiting to Active Hunting
Think about how human vision works. If you are looking for your coffee mug in a messy room, you don’t just stare at the wall and hope it walks past your nose. You move your head, you scan the environment, and your eyes sweep back and forth across different zones until you spot what you are looking for.
That is exactly what we are building in Lesson 59. We are combining three powerful pillars:
-
Computer Vision (MediaPipe & OpenCV): Running blazing-fast object detection on the Raspberry Pi 5 using the PiCamera2 module.
-
Robotic Actuation (Pan and Tilt Servos): Giving our camera a physical body with a dynamic range of motion.
-
Conversational AI (Speech-to-Text & Text-to-Speech): Enabling voice command processing so you can simply tell your Pi what to find, while the system talks back to confirm its state.
How the Active Scanning System Works
In this lesson, our program operates across a few distinct operational modes managed seamlessly through threaded execution:
-
The Voice Listener Thread: Running independently in the background, this thread listens for your spoken commands using the speech-to-text engine. You can say things like “find”, “track”, “scan”, or name a specific object of interest (like “cup”, “person”, or “cell phone”). Saying “quit” gracefully terminates the application with a polite audio farewell.
-
Find Mode: The system rests or holds position while waiting for a target instruction or manual queue update.
-
Scan Mode: If the camera doesn’t immediately see your requested object of interest, it enters active scan mode. The pan servo sweeps smoothly back and forth across its horizontal range, stepping up or down the tilt angle after each pass. It systematically sweeps the room like a radar system.
-
Track Mode: The moment MediaPipe detects the target object with high confidence during a scan, the system instantly halts the scanning pattern, announces “Target Found” via text-to-speech, and switches over to active track mode. Using error correction based on the center of the bounding box versus the center of the screen, the servos continuously adjust to keep the target dead-centered in the frame.
What You Will Learn in the Video
In the accompanying video lesson, we break down the implementation step-by-step:
-
How to manage multi-threaded voice queues so your UI and camera feed don’t stutter while listening for audio.
-
Implementing state-machine logic to toggle cleanly between
find,scan, andtrackmodes. -
Tuning servo gains, deadbands, and sweep step-sizes to achieve smooth, organic robotic movement without jitter.
-
Integrating real-time bounding box feedback, frames-per-second (FPS) monitoring, and dynamic visual overlays using OpenCV.
Grab your Raspberry Pi 5, fire up your Fusion Hat, and let’s write some code that brings hardware to life! Watch the full video lesson above, and let’s dive straight in!
|
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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# ==================================================================== # DISCLAIMER: # This code is provided as-is for educational and experimental # purposes only. The author makes no representations or warranties of # any kind concerning the safety, suitability, or accuracy of this # code. Use at your own risk. The author assumes no liability for any # damages, system failures, security breaches, or network issues # resulting from the use or implementation of this script. # ==================================================================== import cv2 import time from picamera2 import Picamera2 import mediapipe as mp from mediapipe.tasks import python from mediapipe.tasks.python import vision from fusion_hat.servo import Servo from fusion_hat.stt import STT from fusion_hat.tts import Piper import threading from queue import Queue tts = Piper() tts.set_model('en_US-kristin-medium') def speakAsync(message): def runSpeech(): tts.say(message,stream=False) threading.Thread(target=runSpeech,daemon=True).start() stt = STT('en-us') ooiQueue = Queue() running = True currentMode = 'find' scanDirection = 1 piCam = Picamera2(1) 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) topBar = 65 windowPadding = 0 cv2.namedWindow('Camera', cv2.WINDOW_GUI_NORMAL) cv2.resizeWindow('Camera', W, H) tSettle = .1 time.sleep(tSettle) cv2.moveWindow('Camera', windowPadding, topBar) modelPath = "/home/pi/ai-lab-kit/mediapipe/efficientdet_lite0.tflite" scoreThreshold = .25 maxObj = 4 baseOptions = python.BaseOptions(model_asset_path = modelPath) options = vision.ObjectDetectorOptions(base_options = baseOptions, score_threshold=scoreThreshold, max_results=maxObj) myDetector = vision.ObjectDetector.create_from_options(options) targetClass = None panPin = 2 tiltPin = 3 panServo = Servo(panPin) tiltServo = Servo(tiltPin) panAngle = 0 tiltAngle = 0 panServo.angle(panAngle) tiltServo.angle(0) panGain = .015 tiltGain = .015 deadBand = 15 def listenForOoiThread(): global running, currentMode print("Voice Listener is Running") while running: ts=.1 time.sleep(ts) spokenText = stt.listen(stream=False) if spokenText: cleanText = spokenText.strip().lower() print("Heard OOI Request: ", cleanText) if cleanText == 'quit': running = False exitMsg = "Terminating Objest Tracking Program, Have a Nice Day" speakAsync(exitMsg) break if cleanText == 'find': currentMode = 'find' speakAsync('Switching to Find Mode') if cleanText == 'track': currentMode = 'track' speakAsync('Switching to Track Mode') if cleanText == 'scan': currentMode = 'scan' speakAsync('Switching to scan Mode') if cleanText != 'quit' and cleanText !='find' and cleanText != 'track' and cleanText !='scan': ooiQueue.put(cleanText) confirmMsg = "Now Tracking: "+cleanText speakAsync(confirmMsg) print("Voice Litener Thread Terminated") voiceThread = threading.Thread(target=listenForOoiThread, daemon=True) voiceThread.start() def updateServo(objectCenterX,objectCenterY, width, height): global panAngle, tiltAngle tSettle = .01 screenCenterX = width/2 screenCenterY = height/2 errorX = objectCenterX - screenCenterX errorY = objectCenterY - screenCenterY if abs(errorX) > deadBand: panAngle = panAngle - (errorX*panGain) if panAngle > 90: panAngle = 90 if panAngle < -90: panAngle = -90 panServo.angle(panAngle) if abs(errorY) > deadBand: tiltAngle = tiltAngle + (errorY*tiltGain) if tiltAngle > 45: tiltAngle = 45 if tiltAngle < -45: tiltAngle = -45 tiltServo.angle(tiltAngle) time.sleep(tSettle) tWait = 1 time.sleep(tWait) welcomeMsg = "Please speak your object of interest to track" tts.say(welcomeMsg,stream = False) while running: if not ooiQueue.empty(): targetClass = ooiQueue.get() print("Active OOI Updated to: ",targetClass) if currentMode == 'scan': panAngle = panAngle + (scanDirection*2) if panAngle >= 90: panAngle = 90 scanDirection = -1 tiltAngle = tiltAngle + 20 if tiltAngle >=45: tiltAngle = -45 tiltServo.angle(tiltAngle) if panAngle <=-90: panAngle = -90 scanDirection=1 tiltAngle=tiltAngle +20 if tiltAngle >=45: tiltAngle = -45 tiltServo.angle(tiltAngle) panServo.angle(panAngle) servoDelay= .02 time.sleep(servoDelay) deltaT = time.time() - tStart tStart=time.time() fps = fps*.95 + (1/deltaT)*.05 frame= piCam.capture_array() frame=cv2.flip(frame,-1) myImage = mp.Image(image_format = mp.ImageFormat.SRGB, data=frame) myDetections = myDetector.detect(myImage) targetInZone = False #print(myDetections.detections) cv2.circle(frame,(int(W/2),int(H/2)),deadBand*2,(0,255,0),2) for detection in myDetections.detections: if detection.categories: category=detection.categories[0] if category.category_name == targetClass and category.score >=.5: box = detection.bounding_box x = int(box.origin_x) y = int(box.origin_y) w = int(box.width) h = int(box.height) boxCenterX = x + int(w/2) boxCenterY = y + int(h/2) cv2.rectangle(frame,(x,y),(x+w,y+h), (255,0,0),fontThickness) labelText = "Target Aquired "+str(round(category.score*100))+'%' cv2.circle(frame,(boxCenterX,boxCenterY),6,(0,0,255),-1) cv2.putText(frame,labelText,(x,y-10),fontFace,fontScale*.8,(0,255,0),2) if currentMode == 'track': updateServo(boxCenterX,boxCenterY,W,H) if currentMode == 'scan': currentMode = 'track' print("Target Found in scan mode, switcing to track mode") speakAsync('Target Found, switching to track mode') updateServo(boxCenterX,boxCenterY,W,H) cv2.putText(frame,"Mode: "+currentMode,(int(W*.01),int(H*.12)),fontFace,fontScale,(0,255,255),2) 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 myDetector.close() cv2.destroyAllWindows() print('Program Terminated') |