Human Follower
This example uses a servo motor to point at humans detected by the “People detection” model.
You should first use the Sensecraft AI web site to load this model into your Grove Vision AI V2 board.
Steps:
Flash a person or face detection model from Sensecraft AI to the Grove Vision AI V2 using its USB-C connector. I used this model: https://sensecraft.seeed.cc/ai/view-model/60086-person-detection-swift-yolo
Copy
human_follower.mpyto yourCIRCUITPY/drive.Copy
grove_vision_ai_v2.mpytoCIRCUITPY/lib/Create
CIRCUITPY/code.pywith the following content:
import humanfollower
examples/human_follower.py
1# SPDX-FileCopyrightText: Copyright (c) 2025 Ned Konz for Metamagix
2#
3# SPDX-License-Identifier: MIT
4"""Human Follower Example for Grove Vision AI V2.
5
6This example uses a pre-trained object detection model (person or face detector)
7to control a servo motor that tracks detected people. The servo pans to follow
8the nearest person detected in the camera's field of view.
9
10Hardware Requirements:
11 - CircuitPython board (Seeed XIAO or Adafruit QtPy recommended)
12 - Grove Vision AI V2 board with camera
13 - Servo motor connected to D0
14 - LED for visual feedback (uses LED_BLUE by default)
15
16Setup:
17 1. Flash a person or face detection model from Sensecraft AI to the Grove Vision AI V2
18 using its USB-C connector.
19 I used this model:
20 https://sensecraft.seeed.cc/ai/view-model/60086-person-detection-swift-yolo
21
22 2. Copy `human_follower.mpy` to your `CIRCUITPY/` drive.
23
24 3. Copy `grove_vision_ai_v2.mpy` to `CIRCUITPY/lib/`
25
26 4. Create `CIRCUITPY/code.py` with the following content:
27
28 import human_follower
29
30The servo uses exponential smoothing for smooth motion, and the LED indicates
31when a person is detected.
32"""
33
34from __future__ import annotations
35
36import time
37
38import board
39import pwmio
40from adafruit_motor import servo
41from digitalio import DigitalInOut
42from micropython import const
43
44from grove_vision_ai_v2 import CMD_OK, ATDevice, Box
45
46# Configuration
47# Scaling (pixels to degrees)
48# FOV 72 degrees over 240 pixels:
49PIXEL_SCALE = -72.0 / 240
50HALF_IMAGE_WIDTH = const(120)
51
52# Pin usage (change as necessary)
53TX_PIN = board.TX
54RX_PIN = board.RX
55SERVO_PWM_PIN = board.D0
56LED_PIN = board.D1 # active low
57
58SMOOTH_ALPHA = const(0.4) # lower=slower
59
60# Globals
61now = time.monotonic # cached
62ai = ATDevice(TX_PIN, RX_PIN)
63pwm = pwmio.PWMOut(SERVO_PWM_PIN, duty_cycle=2**15, frequency=50)
64motor = servo.Servo(pwm)
65led = DigitalInOut(LED_PIN)
66
67
68class Smoother:
69 """A simple exponential smoother for numerical values.
70
71 Attributes:
72 value: The current smoothed value.
73 alpha: The smoothing factor (0.0 to 1.0). Higher alpha means less smoothing.
74 """
75
76 def __init__(self, initial_value: float, alpha: float) -> None:
77 if not 0.0 <= alpha <= 1.0:
78 raise ValueError("Alpha must be between 0.0 and 1.0")
79 self.value = initial_value
80 self.alpha = alpha
81
82 def update(self, new_value: float) -> float:
83 """Update the smoothed value with a new input.
84
85 Args:
86 new_value: The new raw value to incorporate into the smoothing.
87
88 Returns:
89 The newly smoothed value.
90 """
91 self.value = self.alpha * new_value + (1.0 - self.alpha) * self.value
92 return self.value
93
94
95smoothed_angle = Smoother(90, SMOOTH_ALPHA)
96
97
98def enable_led(value: bool) -> None:
99 """Enable or disable the indicator LED.
100
101 Args:
102 value: True to turn LED on, False to turn off.
103 Note: LED logic is inverted (low = on).
104 """
105 led.value = not bool(value)
106
107
108def set_motor(target_angle: float | None) -> None:
109 """Set the servo motor angle with exponential smoothing.
110
111 Args:
112 target_angle: Desired servo angle in degrees (0-180), or None to skip update.
113 """
114 if target_angle is not None:
115 motor.angle = smoothed_angle.update(target_angle)
116
117
118def centroid_to_angle(x: int) -> float:
119 """Convert image x-coordinate to servo angle.
120
121 Maps camera x-coordinate (0-240 pixels) to servo angle (0-180 degrees).
122 The center of the image (120 pixels) maps to 90 degrees (servo center).
123
124 Args:
125 x: X-coordinate in pixels (0-240).
126
127 Returns:
128 Servo angle in degrees (0-180).
129 """
130 return 90 + (x - HALF_IMAGE_WIDTH) * PIXEL_SCALE
131
132
133def get_best_box_angle(boxes: list[Box]) -> float | None:
134 """Select the best detection box and convert to servo angle.
135
136 Chooses the widest detection box (assumed to be the nearest person)
137 and returns the servo angle to center on it.
138
139 Args:
140 boxes: List of Box objects from AI inference.
141
142 Returns:
143 Servo angle in degrees, or None if no boxes detected.
144 """
145 if not boxes or len(boxes) == 0:
146 return None
147 # Choose the box with the best score
148 # boxes_sorted = sorted(boxes, key=lambda x: x.score, reverse=True)
149 # Choose the widest box (nearest person)
150 boxes_sorted = sorted(boxes, key=lambda x: x.w, reverse=True)
151 # Translate from x values 0-240 to angle values 180-0
152 return centroid_to_angle(boxes_sorted[0].x)
153
154
155# Turn on debug to watch communications
156# ai.debug = True
157
158# print(f"info={ai.info()}")
159# print(f"id={ai.id()}")
160# print(f"name={ai.name()}")
161
162
163def run():
164 led.switch_to_output(value=True)
165 target_angle = motor.angle = 90
166
167 while True:
168 try:
169 started = now()
170 err = ai.invoke(1, True, True)
171 duration = int((now() - started) * 1000)
172 if err != CMD_OK:
173 enable_led(False)
174 print(f"{duration} No response")
175 else:
176 best_angle = get_best_box_angle(ai.boxes)
177 if best_angle is not None:
178 enable_led(True)
179 target_angle = best_angle
180 print(f"{duration} Boxes: {ai.boxes}, Perf: {ai.perf}")
181 else:
182 enable_led(False)
183 print(f"{duration} No boxes")
184 except ValueError as e:
185 print(e)
186 except KeyboardInterrupt:
187 target_angle = motor.angle = 90
188 break
189
190 set_motor(target_angle)
191
192
193run()