Copyright © 2019-2025 幻思创新 Fancinnov® 版权所有
版本号:V2.1.3

第一章 JetsonNX基础环境搭建

概述

本节教程将会教会你JetsonNX的基础环境搭建,保证各位在之后的学习道路上不再会为环境问题而苦恼

实验平台:FanciSwarm机载电脑无人机 ubuntu-20.04LTS

文件总览

20.04-rs-yolo.zip 提取码: g3w6

20.04-rs-yolo
├─ 70-ttyusb.rules
├─ auto_ap.sh
├─ datalink_serial.py
├─ librealsense.zip
├─ mavcrc.py
├─ mavcrc.pyc
├─ mavlink.py
├─ mavlink.pyc
├─ opencv-4.6.0.zip
├─ opencv_contrib-4.6.0.zip
├─ tensorrtx.zip
├─ torch-2.1.0a0+41361538.nv23.06-cp38-cp38-linux_aarch64.whl
├─ vision-release-0.16.zip
├─ yolov5.zip
└─ yolov5_det_trt_rs.py

ROS1安装

说明:① 系统已预装 ROS1,无需重复安装,可直接跳过 ROS1 安装步骤。
② 如何确认系统已安装ROS1: 第一步:打开终端,输入命令:roscore
第二步:查看运行结果,如下图: 这是图片

我们使用fishros一件环境配置工具进行ROS1-noetic版本的安装

  1. 在终端输入下面命令
wget http://fishros.com/install -O fishros && . fishros
  1. 出现下面打印之后

这是图片

我们首先选择5,系统源的更新

这是图片

这一步我们选择2

这是图片

这一步选择1,添加ros源

  1. 安装ros,我们再次输入下面命令
wget http://fishros.com/install -O fishros && . fishros
  1. 出现下面打印之后

这是图片

然后我们输入 1 一键安装 –> 不更换源安装 –> 选择你ubuntu版本对应的ros版本(这里我们选择ROS1的noetic) –> 桌面版进行安装

  1. 配置rosdep
source ~/.bashrc
sudo apt install python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential
sudo rosdep init
rosdep update

至此rosdep已经安装完毕

安装超级终端

超级终端便于同时运行多个程序,可以进行横向纵向分割

sudo apt-get install terminator -y

安装以及配置CUDA

  1. 添加源
sudo gedit /etc/apt/sources.list.d/nvidia-l4t-apt-source.list

加下面两行进去

deb http://repo.download.nvidia.com/jetson/common r35.4 main
deb http://repo.download.nvidia.com/jetson/t234 r35.4 main
  1. 更新源
sudo apt upgrade
sudo apt update
  1. 装jetson包
sudo apt install nvidia-jetpack
  1. 配置环境变量
gedit ~/.bashrc

下面添加:

export LD_LIBRARY_PATH=/usr/local/cuda/lib64
export PATH=/usr/local/cuda/bin:$PATH
export CUDA_HOME=/usr/local/cuda
  1. 更新环境变量
source ~/.bashrc
  1. 复制文件到cuda目录
cd /usr/include && sudo cp cudnn* /usr/local/cuda/include
cd /usr/lib/aarch64-linux-gnu && sudo cp libcudnn* /usr/local/cuda/lib64
  1. 查看cuda
nvcc -V (出现下述信息代表安装cuda成功)

这是图片

  1. 安装jtop(便于查询系统情况)
sudo apt install python3-pip
sudo -H pip3 install -U pip
sudo -H pip install jetson-stats

重启之后执行如下指令运行

jtop

安装opencv4.6.0 with cuda以支持GPU加速

下载地址:Release OpenCV 4.6.0 · opencv/opencv
扩展模块:Release 4.6.0 · opencv/opencv_contrib
(如果网不好,这两个依赖库也可以用幻思提供的)
解压放在home路径即可
确定 Jetson Orin NX 的算力为 8.7
这是图片

cd opencv-4.6.0/
mkdir build && cd build (如果用幻思的库,已经存在build文件夹,只需要cd build)

预编译opencv 4.6.0及其扩展模块 opencv_contrib-4.6.0,生成 Makefiles 文件,如果使用幻思的库,要先将build文件夹下的缓存文件全部删除,并且确保opencv和opencv_contrib都解压到home文件夹后再去执行下面指令

cmake -D CMAKE_BUILD_TYPE=RELEASE \
        -D CMAKE_INSTALL_PREFIX=/usr/local/ \
        -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.6.0/modules \
        -D WITH_CUDA=ON \
        -D CUDA_ARCH_BIN=8.7 \
        -D CUDA_ARCH_PTX="" \
        -D ENABLE_FAST_MATH=ON \
        -D CUDA_FAST_MATH=ON \
        -D WITH_CUBLAS=ON \
        -D WITH_LIBV4L=ON \
        -D WITH_GSTREAMER=ON \
        -D WITH_GSTREAMER_0_10=OFF \
        -D WITH_QT=ON \
        -D WITH_OPENGL=ON \
        -D CUDA_NVCC_FLAGS="--expt-relaxed-constexpr" \
        -D WITH_TBB=ON \
        ..

其中CMAKE_INSTALL_PREFIX=/usr/local/ 为安装地址,OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.6.0/modules 为扩展模块所在路径,
CUDA_ARCH_BIN=8.7 为 GPU 算力,
编译完成后如下所示
这是图片
然后make install 编译安装 opencv 4.6.0 及其扩展模块 opencv_contrib-4.6.0,此安装过程较为漫长,请耐心等待。

sudo make install -j8

安装完成后用

jtop

查看版本

这是图片

配置realsense(深度相机相关驱动)

如果已经在VINS教程中安装了realsense驱动则跳过此步骤!

  1. clone源码
git clone https://github.com/IntelRealSense/librealsense.git

如果下载失败可以用幻思库里的

  1. 安装所需的包以及编译源码
cd librealsense
sudo apt-get update
sudo apt-get install git cmake libssl-dev libusb-1.0-0-dev pkg-config libgtk-3-dev libglfw3-dev libgl1-mesa-dev libglu1-mesa-dev
mkdir build(如果用幻思提供的软件包,这个build已经存在,跳过此步)
cd build
cmake ../ (这一步如果网不好多试几次)
make -j4
sudo make install
  1. 配置USB规则
cd ..
sudo cp config/99-realsense-libusb.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
  1. 验证安装
rs-enumerate-devices   #成功的话会列出设备信息
realsense-viewer       #查看视频流
  1. 安装对应的python包
pip install pyrealsense2

YOLO-V5配置

sudo pip3 install torch-2.1.0a0+41361538.nv23.06-cp38-cp38-linux_aarch64.whl 
sudo apt-get install libopenblas-dev

cd vision-release-0.16
export BUILD_VERSION=0.16
python setup.py install --user
cd ..
$ python3
>>> import torch
>>> import torchvision
>>> print(torch.__version__)
>>> print(torchvision.__version__)

以上为检查torch版本信息,如果有证明安装成功。

pip install tqdm
pip install Ipython
pip install seaborn
git clone -b v7.0 https://github.com/ultralytics/yolov5.git
git clone -b yolov5-v7.0 https://github.com/wang-xinyu/tensorrtx.git

如果以上两个工程用git下载,应把幻思提供的压缩包中的.py和.pyc文件复制到/tensorrtx/yolov5/文件夹中,如果直接用幻思压缩包中的工程则无需复制。

cd yolov5/
wget https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5s.pt
cp ~/tensorrtx/yolov5/gen_wts.py .
pip install numpy==1.20.3
python gen_wts.py -w yolov5s.pt -o yolov5s.wts
pip install numpy==1.23.0

此时会生成 'yolov5s.wts' 文件.

cd ~/tensorrtx/yolov5/
mkdir build
cd build
cp ~/yolov5/yolov5s.wts . 
cmake .. # 如果使用幻思提供的tensorrtx包,最好先清空build文件夹再进行cmake ..
make
./yolov5_det -s yolov5s.wts yolov5s.engine s
pip install pycuda
pip install pyserial
cd ~/tensorrtx/yolov5
sudo chmod 777 /dev/ttyTHS0

将Jetson电源调到MAXN,运行下面

python yolov5_det_trt_rs.py 

全部工程自动运行

配置串口可执行权限:
用如下指令把权限配置文件复制到目录/etc/udev/rules.d/中

sudo cp 70-ttyusb.rules /etc/udev/rules.d/

重启电脑后生效

sh auto_ap.sh

开机自启动

  1. 配置ubuntu系统 auto login

这是图片

(2)配置自启动文件 auto_ap.sh
注意文件中的工程路径是否与自己的一致,不一致的自己修改一下。
(3)终端输入sh auto_ap.sh
检测slam工程是否在一分钟内正常启动
(4)配置auto_ap.sh开机自动运行

这是图片

打开Startup Application

这是图片

点击add

这是图片

先填个Name,再在Command处添加sh /home/nv/auto_ap.sh。

这是图片

此时已完成开机自启动配置
(5)推荐把Jetson NX的功率设置为MAXN

重启系统验证一下吧!看看yolo界面有没有在开机1分钟内正常运行。

第二章 JetsonNX视频流传输

概述

本节教程你将学会如何将JetsonNX上的CSI摄像头数据以TCP协议发送到PC端。

文件总览

TcpNoDelay_transtream_NX.py
TcpNoDelay_recvstream_PC.py

环境配置

确保JetsonNX环境已经按照“JetsonNX基础环境搭建”配置完毕
PC端环境配置:

python环境:3.10

安装opencv以及对应版本的numpy

pip install opencv-contrib-python==4.6.0.66
pip install numpy==1.24.0

运行程序

JetsonNX端

python3 TcpNoDelay_transtream_NX.py

这是图片

PC端

python TcpNoDelay_recvstream_PC.py

这是图片

代码讲解

JetsonNX端代码

import socket
import cv2
import numpy as np

# 配置 {#配置 }
TCP_IP = '0.0.0.0'
TCP_PORT = 5005
# 实例化套接字通信对象 {#实例化套接字通信对象 }
def gstreamer_pipeline(
    sensor_id=0,
    capture_width=1920,
    capture_height=1080,
    display_width=960,
    display_height=540,
    framerate=30,
    flip_method=0,
):
    return (
        "nvarguscamerasrc sensor-id=%d ! "
        "video/x-raw(memory:NVMM), width=(int)%d, height=(int)%d, framerate=(fraction)%d/1 ! "
        "nvvidconv flip-method=%d ! "
        "video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! "
        "videoconvert ! "
        "video/x-raw, format=(string)BGR ! appsink"
        % (
            sensor_id,
            capture_width,
            capture_height,
            framerate,
            flip_method,
            display_width,
            display_height,
        )
    )

# 初始化摄像头 {#初始化摄像头 }
cap = cv2.VideoCapture(gstreamer_pipeline(flip_method=2), cv2.CAP_GSTREAMER)
if not cap.isOpened():
    print("无法打开摄像头,尝试使用默认摄像头设备")
    cap = cv2.VideoCapture(0)

# 创建服务器套接字 {#创建服务器套接字 }
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((TCP_IP, TCP_PORT))
sock.listen(1)
print(f"服务器已启动,监听 {TCP_IP}:{TCP_PORT}")
try:
    while True:
        print("等待客户端连接...")
        conn, addr = sock.accept()
        print(f"客户端已连接:{addr}")
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

        try:
            while True:
                ret, frame = cap.read()
                if not ret:
                    print("无法读取摄像头画面")
                    break

                # 编码为JPEG
                encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 70]
                result, img_encoded = cv2.imencode('.jpg', frame, encode_param)
                if not result:
                    continue

                data = img_encoded.tobytes()

                # 发送图像大小(4字节)
                conn.sendall(len(data).to_bytes(4, 'big'))

                # 发送图像数据
                conn.sendall(data)

                # 显示本地画面(可选)
                cv2.imshow('Sending Image', frame)
                if cv2.waitKey(1) == 27:  # 按 ESC 退出
                    raise KeyboardInterrupt

        except (BrokenPipeError, ConnectionResetError) as e:
            print(f"[错误] 客户端断开连接: {e}")
        finally:
            conn.close()
            print("客户端连接已关闭,等待新连接...")

except KeyboardInterrupt:
    print("服务器正在关闭...")

finally:
    cap.release()
    cv2.destroyAllWindows()
    sock.close()
    print("所有资源已释放")

PC端代码

import cv2
import numpy as np
import subprocess
from multiprocessing import Array
def video_display(ffmpeg_command, frame_array, W_img, H_img):
    process = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE)
    
    while True:
        raw_frame = process.stdout.read(W_img * H_img * 3)  # RGB 3 channels
        if not raw_frame:
            break

        # 将帧复制到共享内存中
        frame = np.frombuffer(raw_frame, dtype=np.uint8).reshape(H_img, W_img, 3)
        np.copyto(np.frombuffer(frame_array.get_obj(), dtype=np.uint8).reshape(H_img, W_img, 3), frame)

        # 显示帧
        cv2.imshow('Real-Time Video', frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    cv2.destroyAllWindows()
    process.terminate()

if __name__ == '__main__':
    W_img = 640
    H_img = 480

    shared_frame = Array('B', W_img * H_img * 3)  # RGB 3通道,大小为像素数 × 3

    ffmpeg_command = [
        'ffplay',
        '-i', 'tcp://192.168.0.112:5005', # 这里的地址要使用JetsonNX的地址,需要自行替换。
        '-vf', 'setpts=N/30',
        '-fflags', 'nobuffer',
        '-flags', 'low_delay',
        '-framedrop'
    ]

    video_display(ffmpeg_command, shared_frame, W_img, H_img)

将视频流传输引入yolov5检测程序当中

把下面文件复制到tensorrtx/yolov5/文件夹中替换yolov5_det_trt.py文件

yolov5_det_trt_rs(stream).py

替换之后可以实现我们在运行第一章yolo识别程序的同时,将视频流传输到PC端

下面说明一下我们更改/增添的python代码

我们需要重新定义一个线程去监听端口等待客户端连接,因此需要引入多线程的库,将下面库放在yolov5_det_trt.py的导入库文件部分

import queue
import socket
import multiprocessing as mp
TCP_IP = '0.0.0.0'
TCP_PORT = 5005

然后将我们的线程类插入到yolov5_det_trt.py文件当中,可以加在程序主入口的上面部分

class TranstreamThread(threading.Thread):
    def __init__(self, TCP_IP, TCP_PORT):
        super().__init__()
        self.TCP_IP = TCP_IP
        self.TCP_PORT = TCP_PORT
        self.sock = None
        self.conn = None
        self.addr = None
        self.running = True
        self.connected = False
        self.queue = queue.Queue()

    def run(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # 允许端口重用
        self.sock.bind((self.TCP_IP, self.TCP_PORT))
        self.sock.listen(1)
        print(f"服务器已启动,监听 {self.TCP_IP}:{self.TCP_PORT}")

        while self.running:
            print("等待客户端连接...")
            try:
                self.conn, self.addr = self.sock.accept()  # 每次 accept 一个新的客户端
                print(f"客户端已连接:{self.addr}")
                self.connected = True

                # 设置 TCP_NODELAY
                self.conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

                # 处理当前客户端通信
                while self.running and self.connected:
                    try:
                        frame = self.queue.get(timeout=1)  # 等待图像帧
                        self._send_frame(frame)
                    except queue.Empty:
                        continue
            except socket.error as e:
                print(f"accept() 出错:{e}")
                self.connected = False
                continue

    def _send_frame(self, frame):
        try:
            size_data = len(frame).to_bytes(4, 'big')
            self.conn.sendall(size_data)
            self.conn.sendall(frame)
        except (socket.error, BrokenPipeError) as e:
            print(f"客户端断开连接:{e}")
            self.connected = False  # 客户端断开后标记为未连接
            self._close_connection()

    def send_frame(self, frame):
        if self.connected:
            self.queue.put(frame)
            return True
        return False

    def _close_connection(self):
        if self.conn:
            try:
                self.conn.close()
            except:
                pass
            self.conn = None

    def stop(self):
        self.running = False
        self.connected = False
        self._close_connection()
        if self.sock:
            self.sock.close()

if __name__ == "__main__":
    ###

接着我们在程序入口创建线程类的实例并且运行

# create a new thread to do warm_up {#create-a-new-thread-to-do-warm_up }
thread1 = inferThread(yolov5_wrapper)
thread1.start()
thread1.join()
thread2 = TranstreamThread(TCP_IP, TCP_PORT) # 这句是我们要加入的
thread2.start() # 这句是我们要加入的
print('data link is starting...\n')
drone_thread=threading.Thread(target=dl.drone)

最后,由于多了一个线程计算,主线程中我们需要更快的处理速度,将cv2.imshow函数后面的waitkey(10)改成waitkey(1),再中间加入我们的发送图像帧的代码。

if cv2.getWindowProperty(window_title, cv2.WND_PROP_AUTOSIZE) >= 0:
    cv2.imshow(window_title, img)
    # 推理完图像后,编码为 JPEG 并发送
    encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 70]
    result, img_encoded = cv2.imencode('.jpg', frame, encode_param)
    data = img_encoded.tobytes()

    thread2.send_frame(data)  # 通过队列异步发送
else:
    break 
keyCode = cv2.waitKey(1) & 0xFF
# Stop the program on the ESC key or 'q' {#stop-the-program-on-the-esc-key-or-q }
if keyCode == 27 or keyCode == ord('q'):
    break