Person Detection
本教程部署一个真实的 AI 推理应用 Person Detection,完整演示:通过 Python SDK 订阅视频流推理结果、发现设备上的模型与流、配置应用权限,并验收一个会发布检测事件、联动设备灯控的完整应用。
不想自己 build 镜像?下载预编译包 person-detection.tar,解压即得 app.yaml 与 image.tar,按 §5 部署到设备即可。
1. 功能概述
Person Detection 订阅摄像头视频流,用 AI 检测模型逐帧推理,当画面中出现人时:
- 统计人数与置信度;
- 向事件总线发布
app/person-detection/detection与alerts/detection事件(带防抖冷却); - 联动设备补光灯(
device.set_white_light)。
本教程基于 ne503 源码仓库里的完整示例项目。先获取仓库并进入应用目录:
git clone https://github.com/camthink-ai/ne503.git
cd ne503/apps/person-detection
该目录已包含本教程所需的全部文件:
person-detection/
├── app.py # 应用主逻辑(SDK 订阅 + 检测 + 事件 + 灯控)
├── app.yaml # 应用清单(权限/资源/环境变量/阈值)
├── build.sh # 构建脚本(复制 SDK → buildx → save → .aipc)
├── Dockerfile # 容器构建定义
└── requirements.txt # Python 依赖
后续步骤逐个讲解这些文件的关键内容,以及需核对/修改的字段(视频流名、模型名、检测阈值)。
2. 应用结构与 SDK 用法
2.1 应用源码 app.py
下面是 Person Detection 的完整源码(即仓库 apps/person-detection/app.py)。它做五件事:初始化 SDK 客户端 → 订阅 sub 流推理结果 → 按 DETECTION_THRESHOLD 过滤 person → 向事件总线发布结构化检测结果 → 检测到人时联动补光灯;并监听 SIGTERM 优雅退出。
#!/usr/bin/env python3
"""Person Detection Application for AIPC Platform"""
import os
import sys
import time
import signal
import logging
from datetime import datetime
from typing import Optional
# AIPC SDK
from hailo_ipc_sdk import (
InferenceClient,
EventClient,
DeviceClient,
FdMediaClient as MediaClient,
Config,
InferenceResult,
)
logging.basicConfig(
level=getattr(logging, os.environ.get('LOG_LEVEL', 'INFO')),
format='[%(asctime)s] [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
class PersonDetectionApp:
"""Person Detection Application"""
def __init__(self):
self.running = True
self.app_id = Config.get_app_id()
self.debug = Config.is_debug()
# Configuration from environment
self.detection_threshold = float(os.environ.get('DETECTION_THRESHOLD', '0.2'))
self.alert_cooldown = int(os.environ.get('ALERT_COOLDOWN_SECONDS', '5'))
# SDK Clients
self.inference: Optional[InferenceClient] = None
self.events: Optional[EventClient] = None
self.device: Optional[DeviceClient] = None
self.media: Optional[MediaClient] = None
# State tracking
self.frame_count = 0
self.total_detections = 0
self.last_alert_time = 0
self.person_count_history = []
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
self.running = False
def initialize(self) -> bool:
try:
self.inference = InferenceClient()
models = self.inference.list_models()
logger.info(f"Available models: {[m.model_id for m in models]}")
if not any(m.model_id == "hailo_yolov8n_384_640" for m in models):
logger.warning("Required model 'hailo_yolov8n_384_640' NOT found")
self.events = EventClient()
try:
self.device = DeviceClient()
except Exception as e:
logger.warning(f"Device control not available: {e}")
self.device = None
try:
self.media = MediaClient()
available_streams = self.media.list_streams()
logger.info(f"Available video streams: {available_streams}")
except Exception as e:
logger.warning(f"Media client not available: {e}")
self.media = None
return True
except Exception as e:
logger.error(f"Failed to initialize clients: {e}")
return False
def run(self):
if not self.initialize():
self._cleanup()
return 1
logger.info("Subscribing to stream 'sub' with model 'hailo_yolov8n_384_640'")
first_frame_received = False
try:
# Subscribe to video stream inference results
for frame_seq, result in self.inference.subscribe(
stream="sub",
model="hailo_yolov8n_384_640",
fps=10,
):
if not self.running:
break
if not first_frame_received:
first_frame_received = True
logger.info(f"Received first inference result - frame {frame_seq}")
self._process_frame(frame_seq, result)
except KeyboardInterrupt:
pass
finally:
if not first_frame_received:
logger.warning("No inference results - check stream 'sub' and model 'hailo_yolov8n_384_640'")
self._cleanup()
return 0
def _process_frame(self, frame_seq: int, result: InferenceResult):
self.frame_count += 1
persons = [
obj for obj in result.objects
if obj.label == "person" and obj.score >= self.detection_threshold
]
if persons:
self.total_detections += 1
logger.info(f"[Frame {frame_seq}] Detected {len(persons)} person(s)")
self._publish_detection_event(frame_seq, result, persons)
# Device control: turn on light when person detected
if persons and self.device:
self._trigger_light()
if self.frame_count % 100 == 0:
self._print_statistics()
def _publish_detection_event(self, frame_seq: int, result: InferenceResult, persons: list):
event_data = {
"app_id": self.app_id,
"frame_sequence": frame_seq,
"timestamp_ns": result.timestamp_ns,
"timestamp_iso": datetime.now().isoformat(),
"person_count": len(persons),
"total_frames_processed": self.frame_count,
"total_detections": self.total_detections,
"objects": [
{
"label": obj.label,
"confidence": round(obj.score, 3),
"bbox": {
"x": round(obj.bbox.x, 3),
"y": round(obj.bbox.y, 3),
"width": round(obj.bbox.width, 3),
"height": round(obj.bbox.height, 3),
},
}
for obj in persons
],
}
self.events.publish(f"app/{self.app_id}/detection", event_data)
# Publish alert if cooldown expired
current_time = time.time()
if persons and (current_time - self.last_alert_time) >= self.alert_cooldown:
self.events.publish("alerts/detection", {
"type": "person_detected",
"app_id": self.app_id,
"person_count": len(persons),
"timestamp": datetime.now().isoformat(),
})
self.last_alert_time = current_time
def _trigger_light(self):
try:
self.device.set_white_light(50)
except Exception as e:
logger.debug(f"Light control failed: {e}")
def _print_statistics(self):
avg = sum(self.person_count_history) / len(self.person_count_history) if self.person_count_history else 0
logger.info(f"Statistics: frames={self.frame_count}, detections={self.total_detections}, avg_persons={avg:.2f}")
def _cleanup(self):
for client in (self.inference, self.events, self.device, self.media):
if client:
client.close()
def main():
sys.exit(PersonDetectionApp().run())
if __name__ == "__main__":
main()
关键逻辑对照(调整模型/流/阈值时的定位点):
| 模块 | 位置 | 说明 |
|---|---|---|
| 配置读取 | __init__ | DETECTION_THRESHOLD、ALERT_COOLDOWN_SECONDS 从环境变量读(默认 0.2 / 5);实际值由 app.yaml 的 env 注入,见 §2.2 |
| 模型/流发现 | initialize | list_models() / list_streams() 打印设备真实值,并校验 hailo_yolov8n_384_640 与 sub 是否可用 |
| 订阅推理 | run | infer.subscribe(stream="sub", model="hailo_yolov8n_384_640", fps=10)——stream 必须是 sub,main 只发 H264 会让调用永久挂死 |
| 检测过滤 | _process_frame | 只保留 label == "person" 且 score >= threshold 的目标 |
| 事件发布 | _publish_detection_event | 每帧发 app/person-detection/detection;alerts/detection 在冷却窗口内只发一次 |
| 灯控联动 | _trigger_light | 检测到人时 device.set_white_light(50)(50% 亮度) |
| 优雅退出 | _signal_handler / _cleanup | 收到 SIGTERM 置 running=False,跳出循环后关闭全部客户端 |
2.2 权限清单 app.yaml
应用必须在 app.yaml 声明所需权限,平台据此做容器隔离与沙箱。Person Detection 声明:视频流 sub.raw(sub 发布推理用的原始 NV12 帧,main 只用于 RTSP 拉流)、模型 hailo_yolov8n_384_640、事件发布/订阅主题、设备灯控。
# AIPC Platform Application Manifest
apiVersion: v1
kind: Application
metadata:
id: person-detection
name: Person Detection
version: 1.0.0
description: Real-time person detection with AI inference and event publishing
author: AIPC Team
spec:
image: aipc/person-detection:1.0.0
resources:
cpu: "50%"
memory: "256Mi"
permissions:
video:
- sub.raw # 发布原始 NV12 帧的流(main 只发 H264,无法订阅推理)
inference:
models:
- hailo_yolov8n_384_640 # 须匹配设备已加载模型
max_qps: 30
max_concurrent: 2
allow_register_model: false
events:
publish:
- app/person-detection/*
- alerts/detection
subscribe:
- system/*
- model/*/detections
device:
light: true # 补光灯联动
ir_cut: true
network:
mode: isolated # 容器网络隔离(无外网)
# 环境变量:app.py 通过 os.environ 读取
env:
- name: DETECTION_THRESHOLD
value: "0.3" # person 置信度门槛,低于此分数的目标被忽略
- name: ALERT_COOLDOWN_SECONDS
value: "5" # alerts/detection 事件的最小间隔(秒)
- name: LOG_LEVEL
value: "INFO" # 日志级别:DEBUG / INFO / WARNING / ERROR
volumes:
- host: /opt/aipc/data/person-detection
container: /app/data
readonly: false
- host: /opt/aipc/logs/person-detection
container: /app/logs
readonly: false
autostart: false
restart_policy: on-failure
restart_max_retries: 3
healthcheck:
enabled: true
interval: 30s
timeout: 5s
retries: 3
声明式权限模型意味着:应用在沙箱里只能访问这里列出的资源。任何未声明的流、模型、事件主题或设备控制,调用时都会被平台拒绝。完整字段参考见仓库
docs下的 Application Manifest 说明。
3. 构建镜像
3.1 构建文件
Dockerfile —— 基于 python:3.11-slim-bookworm,装系统依赖、把 SDK 本地装进镜像、再装应用依赖,并以非 root 用户运行:
FROM python:3.11-slim-bookworm
# 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl procps libglib2.0-0 libsm6 libxext6 libxrender-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# SDK 本地安装(由 build.sh 在构建前复制进来)
COPY hailo_ipc_sdk/ /app/hailo_ipc_sdk/
COPY setup.py README.md /app/
RUN pip install --no-cache-dir -e .
# 应 用代码与依赖
COPY app.py /app/app.py
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# 非 root 用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app/data /app/logs
ENV APP_ID=person-detection
ENV PYTHONUNBUFFERED=1
ENV LOG_LEVEL=INFO
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python3 -c "from hailo_ipc_sdk import InferenceClient; c = InferenceClient(); c.close()" || exit 1
USER appuser
CMD ["python3", "/app/app.py"]
requirements.txt —— 仅 numpy(SDK 已自带 protobuf/grpc,无需重复声明):
numpy>=1.21.0
设备容器运行时无外网,SDK 必须随镜像带入。build.sh 会在构建前把 sdk/python/hailo_ipc_sdk/ 复制进应用目录,Dockerfile 的 COPY hailo_ipc_sdk/ 将其打进镜像,再 pip install -e . 本地安装。
3.2 构建与打包
仓库自带的 build.sh 一键完成"构建镜像 → 导出 → 打包"全流程,无需手动敲多条 docker 命令。
cd apps/person-detection
# 必 须用 arm64:设备为 aarch64,构建 x86_64 镜像将无法导入设备
bash build.sh arm64
build.sh 内部依次执行 5 步:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 复制 SDK | 把 sdk/python/ 下的 hailo_ipc_sdk/、setup.py、README.md 复制进当前应用目录(Dockerfile 需要它们) |
| 2 | 构建镜像 | docker buildx build --platform linux/arm64 生成 aipc/person-detection:1.0.0 |
| 3 | 导出镜像 | docker save 导出为 image.tar |
| 4 | 打包 | zip 把 app.yaml + image.tar 打成 person-detection.aipc |
| 5 | 清理 | 删除 步骤 1 复制的 SDK 文件和中间的 image.tar,只保留 person-detection.aipc |
构建产物:
| 产物 | 大小 | 说明 |
|---|---|---|
Docker 镜像 aipc/person-detection:1.0.0 | ~434 MB | python:3.11-slim + grpcio/numpy/protobuf,留在本地 Docker |
person-detection.aipc | ~97 MB | 最终交付包(app.yaml + image.tar 的 zip) |
build.sh 步骤 5 会删掉 image.tar,而部署到设备需要 app.yaml 和 image.tar 两个独立文件。部署前先解压 .aipc 拿回这两个文件:unzip -o person-detection.aipc。
4. 发现并配置模型与视频流
app.py 里 subscribe(model=...) 的模型名、app.yaml 里 permissions.video 的流名,必须使用设备上的真实值,不得直接沿用示例值——不同设备、不同固件版本的名字可能不同,填错会直接报 StatusCode.NOT_FOUND。
下面分三步走:查模型 → 查流 → 把正确值填进应用。
4.1 查询设备上的模型
设备模型文件放在 /opt/aipc/models/,首次使用前要先扫描并加载到 NPU:
TOKEN="Bearer <token>" # 用默认凭据 admin/password 调 /api/login 获取
# 1. 扫描模型目录,把 .hef 注册到平台
curl -X POST http://<设备IP>:8080/api/v1/ai/models/scan -H "Authorization: $TOKEN"
# 2. 加载指定模型到 NPU(推理前必须加载)
curl -X POST http://<设备IP>:8080/api/v1/ai/models/hailo_yolov8n_384_640/load -H "Authorization: $TOKEN"
# 3. 列出当前可用模型,确认 model_id
curl http://<设备IP>:8080/api/v1/ai/models -H "Authorization: $TOKEN"
NE503 出厂自带检测模型 hailo_yolov8n_384_640(YOLOv8n,COCO 80 类,含 person)。记下 list 返回的真实 model_id,下一步填进 app.py。
4.2 查询设备上的视频流
流名用 SDK 查最直接(curl 没有专门的查流接口)。在 app.py 初始化阶段调用:
from hailo_ipc_sdk import FdMediaClient as MediaClient
print(MediaClient().list_streams()) # → ['main', 'sub']
NE503 通常有两个流,用途完全不同——此差异是后续配置的关键。