开发者-导航 猿导航

NVIDIA Jetson TX2 边缘盒子运行姿态检测模型记录

发布时间:

在 Jetson TX2(JetPack 4.6.1 系统)上通过 Docker 部署 MediaPipe 的完整流程,包含环境配置、镜像拉取与容器启动参数说明,提供基于 RTSP 视频流的姿态检测测试代码及开合跳动作计数实现,解决 ARM 架构下 MediaPipe 运行的适配问题,适用于边缘设备视频流处理与人体动作分析场景。

目录

背景介绍 #

​ 最近在做关于视频流处理方面项目,接触到NVIDIA 的边缘盒子,就尝试着看在边缘盒子上能不能运行 yolov8 或 Google 的 mediapipe 。

yolov8 是无法在这个版本的盒子运行(无法使用 cuda,CPU 推理卡的厉害),而且 Jetpack 4.6.1 版本的盒子无法升级 5.xx 版本,yolov8 最低需要 Python 3.8 版本,所以就选择 mediapipe 。

使用 mediapipe 最好就是使用 docker 来处理,自己在系统安装环境非常麻烦(CUDA10.2 + aarch64 问题),而且坑特别多根本跳不完的坑。

设置 docker #

参考资料

  1. 安装nano(或任何其他文本编辑器):

    sudo apt-get install nano
  2. 开放时间/etc/docker/daemon.json

    sudo nano /etc/docker/daemon.json
  3. 编辑自:

    json

    {
        "runtimes": {
            "nvidia": {
                "path": "nvidia-container-runtime",
                "runtimeArgs": []
            }
        }
    }

    到:

    json

    {
        "runtimes": {
            "nvidia": {
                "path": "nvidia-container-runtime",
                "runtimeArgs": []
            }
        }, 
    
        "default-runtime": "nvidia"
    }
  4. 重启docker:

    sudo systemctl restart docker
  5. 现在需要将 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 文件

✅ 总结 #

这个命令做了几件事情:

  1. 启动一个基于 Jetson 的 MediaPipe 镜像。
  2. 容器可以访问 GPU、音频和 USB 设备。
  3. 容器共享宿主机网络和 X11 显示,可以直接用 GUI。
  4. 容器自动挂载宿主机文件和配置,运行 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)下:

最终 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 \

参考资料 #

  1. Jetson torch 下载中心
  2. Jetson 开发文档
  3. 手动安装 Google MediaPipe Github
  4. Google MediaPipe

扩展信息 #

查看版本信息

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 驱动的边缘应用程序所需的一整套工具和库。