发布时间:
在 Jetson TX2(JetPack 4.6.1 系统)上通过 Docker 部署 MediaPipe 的完整流程,包含环境配置、镜像拉取与容器启动参数说明,提供基于 RTSP 视频流的姿态检测测试代码及开合跳动作计数实现,解决 ARM 架构下 MediaPipe 运行的适配问题,适用于边缘设备视频流处理与人体动作分析场景。
- 系统版本:JetPack 4.6.1 (对应 L4T R32.6.1)
- 架构:aarch64(ARM 64)
- CUDA 版本:10.2
- python 3.6
背景介绍 #
最近在做关于视频流处理方面项目,接触到NVIDIA
的边缘盒子,就尝试着看在边缘盒子上能不能运行 yolov8 或 Google 的 mediapipe 。
yolov8 是无法在这个版本的盒子运行(无法使用 cuda,CPU 推理卡的厉害),而且 Jetpack 4.6.1 版本的盒子无法升级 5.xx 版本,yolov8 最低需要 Python 3.8 版本,所以就选择 mediapipe 。
使用 mediapipe 最好就是使用 docker 来处理,自己在系统安装环境非常麻烦(CUDA10.2 + aarch64 问题),而且坑特别多根本跳不完的坑。
设置 docker #
-
安装nano(或任何其他文本编辑器):
sudo apt-get install nano
-
开放时间
/etc/docker/daemon.json
:sudo nano /etc/docker/daemon.json
-
编辑自:
json
{ "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } } }
到:
json
{ "runtimes": { "nvidia": { "path": "nvidia-container-runtime", "runtimeArgs": [] } }, "default-runtime": "nvidia" }
-
重启docker:
sudo systemctl restart docker
-
现在需要将 docker 添加用户到组:
sudo usermod -aG docker $USER
拉镜像 #
Python | OpenCV | mediapipe | 镜像标签 | 镜像文件 |
---|---|---|---|---|
3.6.9 | 4.8.0 | 0.8.5 | l4t32.7.1-py3.6.9-ocv4.8.0-mp0.8.5 | Dockerfile |
docker pull ghcr.io/lanzani/mediapipe:l4t32.7.1-py3.6.9-ocv4.8.0-mp0.8.5
启动容器 #
显示支持 #
运行容器之前:
export DISPLAY=:0
xhost +
然后运行你的容器。
启动命令: #
sudo docker run -it --rm \
--runtime nvidia \
--network host \
-v /tmp/argus_socket:/tmp/argus_socket \
-v /etc/enctune.conf:/etc/enctune.conf \
-v /etc/nv_tegra_release:/etc/nv_tegra_release \
-v /tmp/nv_jetson_model:/tmp/nv_jetson_model \
-v /tmp/.X11-unix/:/tmp/.X11-unix \
-v /tmp/.docker.xauth:/tmp/.docker.xauth \
-v /home/ubuntu/google-media/data:/opt \
--device /dev/snd \
--device /dev/bus/usb \
-e DISPLAY=:0 \
-e XAUTHORITY=/tmp/.docker.xauth \
ghcr.io/lanzani/mediapipe:l4t32.7.1-py3.6.9-ocv4.8.0-mp0.8.5 \
python3.6 /opt/pcv3.py
参数说明 #
1️⃣ 基本选项 #
参数 | 作用 | 说明 |
---|---|---|
sudo |
提权执行 docker | 因为 Docker 默认需要 root 权限,或者用户在 docker 组中 |
docker run |
启动容器 | 核心命令 |
-it |
交互式终端 | -i 保持 STDIN 打开,-t 分配伪终端,-t 换成 -d 可后台运行 |
--rm |
容器退出后自动删除 | 避免堆积临时容器,测试使用,没问题后可删除这个选择 |
2️⃣ GPU/NVIDIA 相关 #
参数 | 作用 | 说明 |
---|---|---|
--runtime nvidia |
使用 NVIDIA Docker runtime | 让容器访问 GPU。Jetson 的 L4T 环境通常也需要这个 |
--device /dev/snd |
音频设备 | 允许容器访问宿主机的音频设备,方便 TTS 或播放 |
--device /dev/bus/usb |
USB 设备 | 允许访问 USB 摄像头或其他设备 |
3️⃣ 网络 #
参数 | 作用 | 说明 |
---|---|---|
--network host |
容器共享宿主机网络 | 对于访问本地摄像头、USB 或 V4L2 流媒体方便 |
4️⃣ 挂载卷(Volume) #
Volume 主要是让容器访问宿主机文件或设备。
参数 | 作用 | 说明 |
---|---|---|
-v /tmp/argus_socket:/tmp/argus_socket |
Argus 摄像头 socket | Jetson 的摄像头 API(Argus)需要这个 socket |
-v /etc/enctune.conf:/etc/enctune.conf |
GPU/编码配置 | Jetson 的编码器调优文件 |
-v /etc/nv_tegra_release:/etc/nv_tegra_release |
系统版本信息 | 让容器识别 Jetson 系统版本 |
-v /tmp/nv_jetson_model:/tmp/nv_jetson_model |
Jetson 硬件信息 | 某些 SDK 需要读取型号信息 |
-v /tmp/.X11-unix/:/tmp/.X11-unix |
X11 socket | 允许 GUI 程序在宿主机显示 |
-v /tmp/.docker.xauth:/tmp/.docker.xauth |
X11 认证 | 避免权限问题 |
-v /home/ubuntu/google-media/data:/opt |
数据文件 | 容器内 /opt 可访问宿主机数据(你写的 pcv3.py 就在这里) |
5️⃣ 显示相关环境变量 #
参数 | 作用 | 说明 |
---|---|---|
-e DISPLAY=:0 |
X11 显示 | 指定 GUI 输出到宿主机显示器 |
-e XAUTHORITY=/tmp/.docker.xauth |
X11 认证 | 配合挂载 .docker.xauth 文件使用 |
6️⃣ 镜像和命令 #
参数 | 作用 | 说明 |
---|---|---|
ghcr.io/lanzani/mediapipe:l4t32.7.1-py3.6.9-ocv4.8.0-mp0.8.5 |
Docker 镜像 | 这个镜像包含 MediaPipe、OpenCV 4.8、Python3.6.9,适配 Jetson L4T 32.7.1 |
python3.6 /opt/pcv3.py |
容器启动后执行 | 执行你挂载在 /opt 的 Python 文件 |
✅ 总结 #
这个命令做了几件事情:
- 启动一个基于 Jetson 的 MediaPipe 镜像。
- 容器可以访问 GPU、音频和 USB 设备。
- 容器共享宿主机网络和 X11 显示,可以直接用 GUI。
- 容器自动挂载宿主机文件和配置,运行 Python 脚本
/opt/pcv3.py
。
测试代码 #
pcv3.py
# -*- coding: utf-8 -*-
import cv2
import sys
import os
import mediapipe as mp
print("pwd:", os.getcwd())
print("mediapipe pwd:", mp.__file__)
# === RTSP 地址(请改成你的摄像头地址) ===
RTSP_URL = "rtsp://admin:123456@192.168.0.1:554/stream1"
# === 初始化 MediaPipe Pose ===
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose
# 启动 RTSP 视频流
cap = cv2.VideoCapture(RTSP_URL)
if not cap.isOpened():
print("xxxx stop")
exit()
print("start")
# 初始化 Pose 模型(static_image_mode=False 表示视频流)
with mp_pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
) as pose:
while True:
success, frame = cap.read()
if not success:
print("dddiu")
break
# 转换为 RGB(MediaPipe 要求输入 RGB)
image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# 推理
results = pose.process(image_rgb)
# 可视化骨架
if results.pose_landmarks:
mp_drawing.draw_landmarks(
frame,
results.pose_landmarks,
mp_pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
connection_drawing_spec=mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=2)
)
# 显示画面
#cv2.imshow('Pose Detection (RTSP)', frame)
# 按 q 退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
开合跳代码 #
通过关键点,计算开合跳数量
# -*- coding: utf-8 -*-
import cv2
import threading
import queue
import math
import time
import requests
import mediapipe as mp
# ======================
# 配置
# ======================
RTSP_URL = "rtsp://admin:123456@127.0.0.1:554/stream1?transport=tcp"
SERVER_URL = "http://127.0.0.1:8080/upload"
JPEG_QUALITY = 80 # JPEG 编码质量
QUEUE_MAXSIZE = 1 # 队列长度 1,保持最新帧
# ======================
# 队列
# ======================
frame_queue = queue.Queue(maxsize=QUEUE_MAXSIZE)
upload_queue = queue.Queue(maxsize=QUEUE_MAXSIZE)
# 计算两点之间的欧式距离(用于判断双脚分开的程度)
def calc_distance(p1, p2):
return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
# ======================
# 上传函数
# ======================
def upload_jpeg_bytes(image_bytes, server_url):
try:
files = {'image': ('uploaded_image.jpg', image_bytes, 'image/jpeg')}
response = requests.post(server_url, files=files, timeout=2)
response.raise_for_status()
except Exception as e:
print("上传失败:", e)
# ======================
# 抓帧线程
# ======================
def frame_grabber():
cap = cv2.VideoCapture(RTSP_URL)
if not cap.isOpened():
print("无法打开 RTSP 流")
return
while True:
success, frame = cap.read()
if not success:
continue
# 丢弃旧帧
if frame_queue.full():
try:
frame_queue.get_nowait()
except queue.Empty:
pass
frame_queue.put(frame)
# ======================
# 上传线程
# ======================
def uploader():
while True:
try:
frame_bytes = upload_queue.get()
upload_jpeg_bytes(frame_bytes, SERVER_URL)
except Exception as e:
print("上传线程异常:", e)
# ======================
# 主线程处理
# ======================
def main_loop():
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose
jumping_jacks = 0 # 开合跳次数计数器
stage = "down" # 动作阶段:down=手脚收拢,up=手脚张开
with mp_pose.Pose(
static_image_mode=False,
model_complexity=1,
min_detection_confidence=0.5,
min_tracking_confidence=0.5
) as pose:
while True:
if frame_queue.empty():
time.sleep(0.001)
continue
frame = frame_queue.get()
# 转 RGB
image_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Mediapipe 推理
results = pose.process(image_rgb)
# 绘制骨架
if results.pose_landmarks:
mp_drawing.draw_landmarks(
frame,
results.pose_landmarks,
mp_pose.POSE_CONNECTIONS,
landmark_drawing_spec=mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
connection_drawing_spec=mp_drawing.DrawingSpec(color=(255, 0, 0), thickness=2)
)
# 如果检测到人体关键点
if results.pose_landmarks:
landmarks = results.pose_landmarks.landmark
# 取出关键点(左右手腕、鼻子、左右脚踝)
left_wrist = landmarks[15]
right_wrist = landmarks[16]
nose = landmarks[0] # 头顶附近点
left_ankle = landmarks[27]
right_ankle = landmarks[28]
# ===== 手部逻辑 =====
# 当手腕的 y 坐标小于鼻子时,说明手举过头顶
hands_up = left_wrist.y < nose.y and right_wrist.y < nose.y
# ===== 脚部逻辑 =====
# 计算左右脚踝之间的距离
ankle_dist = calc_distance(left_ankle, right_ankle)
print(r"ankle_dis",ankle_dist)
# 如果距离大于阈值,说明双脚分开
legs_open = ankle_dist > 0.10
# ===== 动作阶段判断 =====
# 如果手举起且脚分开,认为处于 "up" 状态
if hands_up and legs_open:
stage = "up"
# 如果手放下且脚收拢,并且之前是 "up",说明完成一次开合跳
elif not hands_up and not legs_open and stage == "up":
stage = "down"
jumping_jacks += 1 # 次数 +1
# 在左上角显示开合跳次数
cv2.putText(
frame,
f'Jumping Jacks: {jumping_jacks}', # 显示文本
(20, 50), # 位置 (x,y)
cv2.FONT_HERSHEY_SIMPLEX, # 字体
1.2, # 字号
(0, 255, 0), # 颜色 (绿色)
3 # 粗细
)
# 编码 JPEG 并放入上传队列
_, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), JPEG_QUALITY])
frame_bytes = buffer.tobytes()
if upload_queue.full():
try:
upload_queue.get_nowait()
except queue.Empty:
pass
upload_queue.put(frame_bytes)
# ======================
# 启动线程
# ======================
if __name__ == "__main__":
threading.Thread(target=frame_grabber, daemon=True).start()
threading.Thread(target=uploader, daemon=True).start()
main_loop()
注意事项 #
一、目录挂载问题 #
-v /home/ubuntu/google-media/data:/opt \
将宿主机的测试 python
代码挂载到容器 opt
目录下,所以/home/ubuntu/google-media/data
这个目录是自己创建的。
二、Can't find file: mediapipe/modules/pose_detection/pose_detection.tflite #
Mediapipe 模型加载问题,没有找到这个文件,python3.6 /opt/pcv3.py
是在容器的 opt
目录下执行的,在当前工作目录下默认是没有这个 mediapipe/modules/pose_detection/pose_detection.tflite
文件,简单粗暴解决办法:
通过 docker exec 容器ID
/bin/bash
,进入容器创建 /opt/mediapipe/modules/pose_detection/
从 dist-packages
拷贝一个份
mkdir -p /opt/mediapipe/modules/pose_detection/
cp /usr/local/lib/python3.6/dist-packages/mediapipe/modules/pose_detection/pose_detection.tflite /opt/mediapipe/modules/pose_detection/
启动日志:
WARNING: Logging before InitGoogleLogging() is written to STDERR
I20251016 06:39:17.998665 24 gl_context_egl.cc:163] Successfully initialized EGL. Major : 1 Minor: 5
I20251016 06:39:18.044948 42 gl_context.cc:331] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 32.6.1)
I20251016 06:39:18.045231 24 gl_context_egl.cc:163] Successfully initialized EGL. Major : 1 Minor: 5
I20251016 06:39:18.072178 43 gl_context.cc:331] GL version: 3.2 (OpenGL ES 3.2 NVIDIA 32.6.1)
W20251016 06:39:18.075790 42 tflite_model_loader.cc:32] Trying to resolve path manually as GetResourceContents failed: ; Can't find file: mediapipe/modules/pose_detection/pose_detection.tflite
INFO: Created TensorFlow Lite delegate for GPU.
三、eglChooseConfig() returned no matching EGL configuration for RGBA8888 D16 ES2 request #
AI 回答:
MediaPipe 默认会尝试用 GPU (OpenGL ES) 模式运行 Pose 模型。
但 Jetson TX2 某些驱动 / EGL 库 版本(尤其 JetPack 4.6.x)下:
- EGL 配置不匹配
- X server 或 DISPLAY 环境未正确设置
- 无法创建 GLES3 / GLES2 上下文
最终 GPU 初始化失败,导致 MediaPipe Pose 图计算图无法启动。
实际情况是在启动 docker 容器时缺少了一些必要参数:
-v /tmp/argus_socket:/tmp/argus_socket \
-v /etc/enctune.conf:/etc/enctune.conf \
-v /etc/nv_tegra_release:/etc/nv_tegra_release \
-v /tmp/nv_jetson_model:/tmp/nv_jetson_model \
-v /tmp/.X11-unix/:/tmp/.X11-unix \
-v /tmp/.docker.xauth:/tmp/.docker.xauth \
--device /dev/snd \
--device /dev/bus/usb \
-e DISPLAY=:0 \
-e XAUTHORITY=/tmp/.docker.xauth \
参考资料 #
扩展信息 #
查看版本信息
cat /etc/nv_tegra_release
查看系统架构
uname -m
查看 cuda 信息
nvcc --version
查看 CPU、GPU、内存使用情况
sudo tegrastats
诊断系统 OpenCV 是否支持 cuda,也可以自容器执行
python3.6 -c "import cv2; print('--- System OpenCV Info ---'); print(cv2.getBuildInformation())"
查看 mediapipe 版本信息
python3.6 -c "import mediapipe as mp; print(mp.__version__)"
PyTorch(适用于 JetPack 4.6.1):是一个针对使用 GPU 和 CPU 进行深度学习而优化的张量库
NVIDIA JetPack™ :是 NVIDIA Jetson™ 平台的官方软件堆栈,为您提供构建 AI 驱动的边缘应用程序所需的一整套工具和库。