进迭时空 K1 MUSE Pi Pro 助力中小学steam教育做"看到有人就转"的人脸识别感应风扇

使用进迭时空 K1 MUSE Pi Pro 助力中小学steam教育

进迭时空 K1 MUSE Pi Pro 助力中小学steam教育做"看到有人就转"的人脸识别感应风扇封面
ygayct2026-11-09 11:13:59CERN-OHL-P v2
3
Star

PCBA

设计文件

fan_face_main.zip11.39KB(0次下载)

源代码

详细介绍

用进迭时空 K1 MUSE Pi Pro(SpacemiT M1,八核 RISC-V,融合 2 TOPS AI 算力)做一台感应风扇:USB 摄像头采集画面,Python 调用官方人脸识别例程做检测,一旦画面里出现人脸,就从 40Pin 扩展口的 GPIO73 输出 PWM 信号,驱动风扇电机模块转动;人离开画面超过设定时间,风扇自动停转。全部逻辑在板子上本地运行,不联网、不上云。

整个链路可以概括为:摄像头取帧 → YOLOv5n-face 检测人脸 → 检测结果映射为 PWM 占空比 → gpiozero 输出 → 风扇驱动板 → 风扇转动。

图 1 系统总体框图

硬件与软件清单

名称

规格 / 说明

用途

K1 MUSE Pi Pro

SpacemiT M1,8 核 RISC-V,LPDDR4X,Bianbu Desktop / Bianbu OS

主控,跑 Python 与模型推理

USB 摄像头

支持 UVC 即可;也可用 MIPI CSI 摄像头

采集视频帧

风扇电机模块

商品页「风扇电机」SKU,带驱动板,5V 供电,PWM 调速

执行机构

杜邦线

母对母 / 公对母若干

连接 40Pin 与模块

电源与外设

USB Type-C PD 5V/3A、HDMI 显示器、键鼠

供电与调试

Python 3.12

venv 虚拟环境

运行环境

opencv-python / onnxruntime / gpiozero

详见第一章依赖

视觉与 GPIO

本文按「环境配置 → 人脸识别 → GPIO 控制 → 风扇模块功能 → 运行效果」的顺序展开,代码可以直接复制使用,版式留待在网页编辑器里再排。

一、环境配置:先把 Python 环境搭起来

1.1 为什么必须用虚拟环境

Bianbu OS 预装了 Python 3(推荐跟随系统版本,K1 平台推荐 Python 3.12)。官方文档明确说明:系统 Python 受 PEP 668 保护,直接 pip install 会报 error: externally-managed-environment。第三方库必须装进 venv 虚拟环境,或者用 apt install python3-xxx 安装系统预编译包。

另一个容易踩的坑是架构:K1 是 RISC-V,部分含 C/C++ 扩展的 Python 包在 PyPI 上没有预编译 wheel,pip 会拉源码本机编译,耗时且容易失败。进迭时空提供了适配 RISC-V 的 PyPI 镜像源,务必把它配成 extra-index-url。

1.2 安装系统依赖并创建虚拟环境

sudo apt update
sudo apt install -y python3-venv python3-dev python3-gpiozero \
                    libgl1 libglib2.0-0 libopenblas-dev

python3 -m venv ~/.venv
source ~/.venv/bin/activate
pip install --upgrade pip

# 阿里云源为主源,进迭时空 RISC-V 源为额外源
pip config set global.index-url
https://mirrors.aliyun.com/pypi/simple
pip config set global.extra-index-url
https://git.spacemit.com/api/v4/projects/33/packages/pypi/simple

激活成功后,命令行提示符前会出现 (.venv)。用 python3 -c "import sys; print(sys.executable)" 可以确认解释器指向虚拟环境目录。

1.3 下载官方人脸识别例程

cd ~
wget -O face_det_
rec.zip https://cdn-resource.spacemit.com/file/event/face_det_rec.zip
unzip face_det_
rec.zip -d ~/
cd ~/face_det_rec
pip install -r requirements.txt

requirements.txt 中的关键依赖:

onnxruntime==1.20.1+spacemit
spacemit-ort==2.0.0+beta2
opencv-python==4.12.0.88
numpy==2.2.6
pillow==11.2.1

如果只想跑通检测、暂时不需要 NPU 加速,也可以按官方课程文档的最小依赖安装:pip install opencv-python pillow,其余由 requirements 一次装齐更省事。

1.4 安装 GPIO 库并放通权限

pip install "gpiozero>=2.0.5" lgpio
sudo chmod a+rw /dev/gpiochip0

官方说明里强调 gpiozero 版本必须 ≥ 2.0.5。chmod 每次重启会失效,做成 udev 规则更省心:

echo 'KERNEL=="gpiochip*", MODE="0666"' | sudo tee /etc/udev/rules.d/99-gpio.rules
sudo udevadm control --reload-rules && sudo udevadm trigger

1.5 环境自检

python -c "import cv2, numpy, onnxruntime, gpiozero; print('env ok')"
pinout
          # 查看板型与排针编号
ls /dev/video*
  # 查看摄像头节点

pinout 会打印板卡型号与 40Pin 的 GPIO 映射;ls /dev/video* 用来确认摄像头编号,后面代码里的 CAM_INDEX 要按实际结果填写。

二、人脸识别:官方例程怎么跑

2.1 例程做了什么

官方《人脸识别应用》课程用两个量化 ONNX 模型串成一条流水线:

l  yolov5n-face_320_cut.q.onnx:YOLOv5n-face 人脸检测,输出人脸框 boxes 和裁剪出的人脸图 face_imgs;

l  arcface_mobilefacenet_cut.q.onnx:ArcFace / MobileFaceNet 提取 512 维(量化版)特征向量,与人脸库里的 .npy 模板做点积/余弦相似度比较,阈值 0.6;

l  人脸库放在 faces/ 目录,每个 .npy 文件对应一个人名。

官方例程目录结构:

face_det_rec/
├── base/
│
   ├── detection.py      # YOLOv5n-face 检测封装
│
   ├── recognition.py    # ArcFace 特征提取 + 人脸库
│
   └── utils.py
├── onnx_model/
           # 量化 ONNX 模型
├── faces/
                # 已注册的人脸特征 .npy
├── save_
face.py          # 人脸注册
├── face_det_
rec.py       # 实时识别主程序
└── requirements.txt

2.2 先注册人脸,再跑识别

cd ~/face_det_rec
source ~/.venv/bin/activate
python save_
face.py       # 摄像头对准人脸,按 s 输入名字,特征保存到 faces/
python face_det_
rec.py    # 实时检测识别,按 q 退出

注册时尽量让正面人脸落在检测框内,终端提示「请输入人脸名」后输入名字回车即可。识别程序检测到人脸会画框,并把匹配到的名字标在框上方。

图 7 人脸注册结果(官方)

图 8 人脸识别效果(官方)

2.3 本项目只取"检测到人脸"这一个信号

识别程序的核心接口是 det.infer_face(frame),返回 (face_imgs, boxes);没有检测到人脸时返回 (None, None)。官方主程序把推理放到独立线程,主线程只负责取帧和显示,这样模型推理的耗时不会卡住画面。

本项目要做的是"有人来就转风扇",所以触发条件取 boxes 非空,而不是"必须匹配到已注册的人":

face_imgs, boxes = det.infer_face(frame)
has_face = boxes is not None and len(boxes) > 0
   # 只要画面里有人脸就为 True

这样做的好处是未注册的访客也能触发风扇;ArcFace 的识别结果仍然保留,只用于画面上显示姓名和相似度,不参与开关判断。如果想改成"只给认识的人吹风",把触发条件换成 any(not n.startswith("unknown") for n in names) 即可。

三、GPIO 控制:选哪个脚、怎么输出 PWM

3.1 引脚选择:GPIO73(40Pin 第 15 脚)

40Pin 排针的丝印编号和代码里用的 GPIO 编号不是同一个数,这是最容易出错的地方。本文选用 GPIO73 = 物理 PIN15,理由有两条:

1.         官方《Python 用户指南》的 PWMLED 呼吸灯示例就接在 GPIO73;

2.         官方《外设模块参考说明》里二维云台舵机的 PWM 信号线也接在 GPIO-73。

供电与地:5V 在 PIN2 / PIN4(最大 1A,官方标注可为散热风扇供电),GND 在 PIN6 / 9 / 14 / 20 / 25 / 30 / 34 / 39,3.3V 在 PIN1 / PIN17。整个 GPIO 电平域是 3.3V。

图 6 MUSE Pi Pro 40Pin 引脚定义(官方)

图 10 开发板 GPIO 接口(官方)

需要说明:gpiozero 输出的是软件 PWM(由 lgpio 在用户态模拟),官方也明确写了"即使某些 GPIO 引脚不支持硬件 PWM,只要支持基本输出功能,仍可通过 gpiozero 实现软件 PWM"。风扇调速对频率稳定性要求不高,软件 PWM 完全够用;若要驱动舵机或追求更稳的波形,可改用板上的硬件 PWM 脚:GPIO34/PWM4(PIN32)、GPIO35/PWM5(PIN36)、GPIO33/PWM3(PIN37)、GPIO46/PWM6(PIN38)、GPIO37/PWM7(PIN40),通过 /sys/class/pwm/pwmchipN/export、period、duty_cycle、enable 控制。

3.2 三行固定写法:显式指定引脚工厂

在 SpacemiT 板子上用 gpiozero,官方建议在程序开头固定加上这三行,显式指定底层使用 lgpio 的 /dev/gpiochip0:

from gpiozero.pins.lgpio import LGPIOFactory
from gpiozero import Device
Device.pin_factory = LGPIOFactory(chip=0)   # 显式指定 /dev/gpiochip0

3.3 最小验证程序:让风扇按不同占空比转

先不接摄像头,单独写一个 fan_test.py 验证引脚、接线和 PWM 是否正常:

# fan_test.py —— 单测风扇 PWM,接线检查用
from gpiozero.pins.lgpio import LGPIOFactory
from gpiozero import Device, PWMLED
from time import sleep

Device.pin_factory = LGPIOFactory(chip=0)

FAN_PIN = 73
fan = PWMLED(FAN_PIN)

try:
    for duty in (0.0, 0.3, 0.6, 0.9, 0.6, 0.3, 0.0):
        fan.value = duty          # value 取值 0.0 ~ 1.0
        print("[fan] duty = %.2f" % duty)
        sleep(2)
finally:
    fan.close()                  # 退出时务必关断 PWM

运行:

sudo chmod a+rw /dev/gpiochip0
source ~/.venv/bin/activate
python fan_
test.py

PWMLED.value 从 0 到 1 线性对应占空比 0% 到 100%,占空比越高,驱动板给电机的平均电压越高,转速越快。

图 3 PWM 占空比与转速关系示意

注意:GPIO 的驱动能力很弱,官方文档明确不建议把 LED、电机等功率器件直接挂在引脚上。风扇必须由模块自带的驱动板(或外接 MOS 管/三极管)驱动,GPIO 只提供控制信号,电机电源从 5V 引脚取。

图 9 PWM 信号接线示例(官方)

四、风扇模块功能:引脚定义、接线与联动代码

4.1 模块与引脚定义

商品页里选「风扇电机」这个 SKU,对应的是一片带驱动板的小风扇模块,典型规格是 5V 供电、板载驱动、三线接口,用一路 PWM 信号调速。本文按这类模块的通用三线定义接线:

模块丝印

线色(常见)

功能

接到开发板

VCC

红

5V 电源正

40Pin PIN2 或 PIN4(5V)

GND

黑

电源地 / 信号地

40Pin PIN30(或 PIN6、PIN9 等 GND)

PWM / S / IN

黄 / 白

PWM 调速信号,3.3V 逻辑

40Pin PIN15(GPIO73)

 

说明:淘宝商品页需要登录后才能查看详情,本文按该类风扇电机模块的通行引脚定义整理。接线前请以商品页「引脚定义」图和实物丝印为准,确认 VCC 是 5V 还是 3.3V、信号脚丝印是 PWM / S / IN 中的哪一个,再上电。

图 2 接线示意图

三条线里最容易漏的是共地:开发板 GND 必须和模块 GND 相连,否则 PWM 信号没有参考电平,风扇会出现不转、乱转或一直全速的现象。

4.2 两种常见变体的接法

l  四线 INA/INB 驱动(L9110S、MX1508 一类):INA 接 GPIO73 输出 PWM,INB 接一个普通 GPIO(例如 GPIO74 = PIN12)并固定输出低电平;也可以把 INB 直接接 GND。代码里加两行即可:

from gpiozero import LED
inb = LED(74)
      # PIN12 / GPIO74
inb.off()
          # 固定低电平,只让 INA 调速

l  两线裸电机(只有正负极):不能直接接 GPIO,必须外加 MOS 管或电机驱动板,GPIO 只控制 MOS 管栅极,5V 单独给电机供电,并在线圈两端并续流二极管。本文代码无需改动,只要 PWM 信号接到驱动级的控制端即可。

4.3 完整联动程序 fan_face_main.py

把下面代码保存为 ~/face_det_rec/fan_face_main.py,与官方的 base/、onnx_model/、faces/ 同级。它在官方例程的双线程结构上加了三件事:风扇控制器类、"有人开 / 无人延时关"的状态机、画面上的风扇状态显示。

# -*- coding: utf-8 -*-
"""
fan_face_
main.py
K1 MUSE Pi Pro + Bianbu OS:检测到人脸 -> GPIO73 输出 PWM -> 风扇转动
人离开画面超过 HOLD_SEC 秒后自动停转。

运行:
    source ~/.venv/bin/activate
    cd ~/face_det_rec
    python fan_face_main.py
"""
import cv2
import time
import queue
import threading

# gpiozero 引脚工厂:SpacemiT 板子上必须显式指定
from gpiozero.pins.lgpio import LGPIOFactory
from gpiozero import Device, PWMLED
Device.pin_factory = LGPIOFactory(chip=0)

from base.detection import Detection
from base.recognition import Recognition

# ---------------- 可调参数 ----------------
FAN_PIN
    = 73      # 40Pin PIN15 -> GPIO73
FAN_DUTY
   = 0.70    # 有人时的稳态占空比 0.0 ~ 1.0
START_DUTY = 0.90
    # 启动瞬时的高占空比,帮助电机克服静摩擦
START_SEC
 = 0.25    # 启动高占空比保持时间(秒)
HOLD_SEC
   = 3.0     # 人离开后延时停转(秒)
CAM_INDEX
  = 20      # 官方例程用的摄像头节点;USB 摄像头通常改成 0
SIM_THRES
  = 0.6     # 人脸特征相似度阈值

DET_MODEL = "onnx_model/yolov5n-face_320_cut.q.onnx"
REC_MODEL = "onnx_model/arcface_mobilefacenet_cut.q.onnx"
FACES_DIR = "faces"


class FanController:
    """把 PWMLED 包装成带'启动冲击'和'无人延时关闭'的风扇控制器。
    全部方法都是非阻塞的,不占用主线程画面刷新。"""
    def init(self, pin=FAN_PIN):
        self.pwm = PWMLED(pin)
        self.running = False
        self.last_seen = 0.0
        self.start_at = 0.0
        self.off()

    def on(self):
        """检测到人脸时调用:刷新时间戳,必要时执行一次软启动。"""
        now = time.time()
        self.last_seen = now
        if not self.running:
            self.running = True
            self.start_at = now
            self.pwm.value = START_DUTY
            print("[fan] ON  startup duty=%.2f" % START_DUTY)
        elif now - self.start_at >= START_SEC:
            self.pwm.value = FAN_DUTY

    def off(self):
        self.pwm.value = 0.0
        if self.running:
            print("[fan] OFF (timeout)")
        self.running = False

    def tick(self):
        """主循环每帧调用一次:处理启动回落与超时关断。"""
        if not self.running:
            return
        now = time.time()
        if now - self.last_seen > HOLD_SEC:
            self.off()
        elif now - self.start_at >= START_SEC:
            self.pwm.value = FAN_DUTY

    def close(self):
        self.pwm.value = 0.0
        self.pwm.close()


def detect_worker(frame_queue, result_queue):
    """独立线程跑模型推理,避免阻塞画面和风扇状态机。"""
    det = Detection(DET_MODEL)
    rec = Recognition(REC_MODEL, FACES_DIR)

    while True:
        frame = frame_queue.get()
        if frame is None:                 # 退出信号
            break

        face_imgs, boxes = det.infer_face(frame)
        has_face = boxes is not None and len(boxes) > 0
        names = {}

        if has_face:
            for i, (face_img, box) in enumerate(zip(face_imgs, boxes)):
                vec = rec.infer(face_img)
                best, score = "unknown_%d" % i, 0.0
                for key, value in rec.face_bank.items():
                    s = float((vec @ value.T)[0][0])
                    if s > score and s > SIM_THRES:
                        best, score = key, s
                names[i] = (best, score, box)

        # 只保留最新一帧的结果
        while not result_queue.empty():
            result_queue.get()
        result_queue.put({"has_face": has_face, "names": names})


def main():
    fan = FanController()
    cap = cv2.VideoCapture(CAM_INDEX)
    if not cap.isOpened():
        raise RuntimeError("摄像头打不开,请用 ls /dev/video* 检查 CAM_INDEX")

    frame_queue = queue.Queue(maxsize=1)
    result_queue = queue.Queue(maxsize=1)
    threading.Thread(target=detect_worker,
                     args=(frame_queue, result_queue),
                     daemon=True).start()

    print("[init] camera=%d  pwm_gpio=%d  duty=%.2f  hold=%.1fs"
          % (CAM_INDEX, FAN_PIN, FAN_DUTY, HOLD_SEC))

    try:
        while True:
            ret, frame = cap.read()
            if not ret:
                break

            # 队列里始终只放最新一帧,避免推理积压
            if frame_queue.empty():
                frame_queue.put(frame)

            has_face = False
            if not result_queue.empty():
                res = result_queue.get()
                has_face = res["has_face"]
                for name, score, box in res["names"].values():
                    x1, y1, x2, y2 = [int(v) for v in box]
                    cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                    if not name.startswith("unknown"):
                        cv2.putText(frame, "%s %.2f" % (name, score),
                                    (x1, max(y1 - 10, 20)),
                                    cv2.FONT_HERSHEY_SIMPLEX, 0.7,
                                    (0, 255, 0), 2)

            # 触发逻辑:有人 -> 开;无人 -> 交给 tick 判断延时关断
            if has_face:
                fan.on()
            fan.tick()

            state = "ON " if fan.running else "OFF"
            color = (0, 200, 0) if fan.running else (0, 0, 255)
            cv2.putText(frame, "FAN %s  duty=%.2f" % (state, fan.pwm.value),
                        (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, color, 2)
            cv2.imshow("face -> fan", frame)
            if cv2.waitKey(1) & 0xFF ord('q'):
                break
    finally:
        frame_queue.put(None)
        cap.release()
        cv2.destroyAllWindows()
        fan.close()
        print("[exit] fan off, bye")


if
name "__main__":
    main()

运行:

sudo chmod a+rw /dev/gpiochip0
source ~/.venv/bin/activate
cd ~/face_det_rec
python fan_face_
main.py

图 4 主程序流程

4.4 几个参数的含义

参数

默认值

调整建议

FAN_PIN

73

换脚时改这里;注意写 GPIO 号,不是排针号

FAN_DUTY

0.70

风太大调小,风太小调大;低于约 0.3 部分电机可能起不来

START_DUTY / START_SEC

0.90 / 0.25

低占空比启动不了电机时,把这两个值调大

HOLD_SEC

3.0

人短暂离开就停转 → 调大;希望人走立刻停 → 设为 0

CAM_INDEX

20

MIPI 摄像头常用 20;USB 摄像头先 ls /dev/video*,多半是 0

SIM_THRES

0.6

只影响姓名显示,不影响风扇开关

五、运行效果

程序启动后终端先打印初始化信息:

[init] camera=20  pwm_gpio=73  duty=0.70  hold=3.0s
[fan] ON
  startup duty=0.90
[fan] OFF (timeout)

实际现象分三种状态:

1.         画面无人:PWM 占空比为 0,风扇静止,画面左上角显示红色 FAN OFF duty=0.00;

2.         有人进入画面:检测框出现,风扇先以 0.90 的占空比短促启动,0.25 秒后回落到 0.70 稳定转动,状态变成绿色 FAN ON duty=0.70;若人脸已注册,框上方还会显示姓名与相似度;

3.         人离开画面:last_seen 不再刷新,超过 3 秒后 tick() 把占空比归零,风扇停转,终端打印 [fan] OFF (timeout)。人还在画面内时每帧都会刷新时间戳,所以不会误停。

图 5 运行效果示意

由于模型推理放在独立线程,画面帧率会比纯摄像头预览低一些,属于正常现象;把检测输入分辨率调小、或使用量化模型(例程默认已是 .q.onnx)可以改善。本文没有给出具体帧率数值,因为它与摄像头分辨率、系统负载和散热条件相关,建议以自己板子上的实测为准。

还可以继续扩展:按识别到的人名给不同风速、按画面里人脸数量线性提高占空比、叠加温度阈值做"又热又有人"才转,或者把 HOLD_SEC 做成网页端可调参数。

六、踩过的坑与注意事项

1.         pip 装不进系统 Python:报 externally-managed-environment 是 PEP 668 保护,必须用 venv,不要加 --break-system-packages。

2.         GPIO 报 Permission denied:忘记 sudo chmod a+rw /dev/gpiochip0,重启后又会失效,建议写 udev 规则。

3.         排针号 ≠ GPIO 号:PIN15 对应 GPIO73,PIN32 对应 GPIO34/PWM4。代码里一律写 GPIO 后面的数字,接线时才看排针号。

4.         电平与供电:GPIO 是 3.3V 电平域,5V 只能从 PIN2/PIN4 取;电机绝不能由 GPIO 直接驱动,且必须共地。

5.         低占空比不转:直流电机有静摩擦,0.2~0.3 的占空比可能只让驱动板"嗡嗡响"。代码里加了 START_DUTY 启动脉冲,若仍不转就调大它或调大 FAN_DUTY。

6.         摄像头打不开:官方例程用 cv2.VideoCapture(20),那是 MIPI CSI 的节点;插 USB 摄像头时先 ls /dev/video*,把 CAM_INDEX 改成对应编号。

7.         人脸库是空的:不影响"有人就转",但画面只会画框、显示不出姓名。想显示姓名先跑一次 save_face.py。

8.         软件 PWM 的边界:gpiozero 用的是软件 PWM,驱动舵机会抖;风扇调速没问题。要求严格时序时改用硬件 PWM 脚与 sysfs 接口。

9.         模块引脚以实物为准:不同批次的风扇电机模块丝印可能是 PWM、S、IN、INA/INB 或只有两根线。上电前核对商品页引脚定义图,必要时按 4.2 节的变体接法调整。

七、总结

这个项目把进迭时空官方的人脸识别例程和 gpiozero 的 PWM 输出拼在一起,用的都是板子自带能力:Bianbu OS + Python 3.12 虚拟环境跑 YOLOv5n-face 与 ArcFace 量化模型,40Pin 的 GPIO73(PIN15)输出 PWM,5V 引脚给风扇模块供电。核心逻辑其实只有一句判断——boxes 非空就 fan.on(),再配一个 HOLD_SEC 的延时关断,就实现了"看到有人来就转风扇"。

工程文件只有一个新脚本 fan_face_main.py,其余全部复用官方 face_det_rec 源码包,方便复现和二次修改。接线时记住三件事:5V 供电、必须共地、信号脚接 PIN15。

参考资料

l  K1 MUSE Pi Pro 用户使用指南:https://www.spacemit.com/community/document/info?lang=zh&nodepath=ecohardware/k1_series/k1_muse_pi_pro/pi_pro_user_guide.md

l  人脸识别应用(AI 综合应用案例):https://www.spacemit.com/community/document/info?lang=zh&nodepath=courses/AI/02_AI综合应用案例/02_人脸识别应用.md

l  Python 使用指南(Bianbu OS 环境配置与 gpiozero):https://www.spacemit.com/community/document/info?lang=zh&nodepath=software/SDK/bianbu/development/python.md

l  MUSE Pi / MUSE Pi Pro 扩展 IO 定义:https://www.spacemit.com/community/document/info?lang=zh&nodepath=software/SDK/bianbu/user_guide/LXQt/MUSEPi_and_MUSEPiPro_expansion_IO_pinout.md

l  gpiozero 使用说明 / 外设模块参考说明(竞赛教程)

l  风扇电机模块(SKU 与引脚定义商品页):https://item.taobao.com/item.htm?id=1086825104957 ;https://item.taobao.com/item.htm?id=1085516335983

l  华秋开源硬件社区文章格式参考:https://p.eda.cn/d-1436671834299826176 ;https://p.eda.cn/d-1435260630108995584