Orin 上用 Docker 跑通目标检测功能

文章目录

    • [1. 为什么在 Orin 上用 Docker](#1. 为什么在 Orin 上用 Docker)
    • [2. 这篇要做的具体功能](#2. 这篇要做的具体功能)
    • [3. 开始前先确认三件事](#3. 开始前先确认三件事)
      • [3.1 JetPack 版本](#3.1 JetPack 版本)
      • [3.2 Docker 本身能不能正常跑](#3.2 Docker 本身能不能正常跑)
      • [3.3 NVIDIA runtime 是否可用](#3.3 NVIDIA runtime 是否可用)
    • [4. Orin 上 Docker 怎么装和怎么校验](#4. Orin 上 Docker 怎么装和怎么校验)
      • [4.1 先做一个 smoke test](#4.1 先做一个 smoke test)
    • [5. 空间和目录先规划一下](#5. 空间和目录先规划一下)
    • [6. 功能设计先定死](#6. 功能设计先定死)
    • [7. 需要准备哪些文件](#7. 需要准备哪些文件)
    • [8. Dockerfile(完整代码)](#8. Dockerfile(完整代码))
    • [9. requirements.txt(完整代码)](#9. requirements.txt(完整代码))
    • [10. detect_trt.py(完整代码)](#10. detect_trt.py(完整代码))
    • [11. 构建镜像](#11. 构建镜像)
    • [12. 启动容器跑图片检测](#12. 启动容器跑图片检测)
    • [13. 再跑一段视频](#13. 再跑一段视频)
    • [14. 怎么看它是不是跑对了](#14. 怎么看它是不是跑对了)
    • [15. 想停、想改、想更新时怎么做](#15. 想停、想改、想更新时怎么做)
    • [16. 这套方式适合什么,不适合什么](#16. 这套方式适合什么,不适合什么)
    • [17. 最常见的坑](#17. 最常见的坑)
    • [18. 后面怎么继续扩](#18. 后面怎么继续扩)
    • [19. 小结](#19. 小结)

摘要:很多人买到 Orin 之后,先能在板子系统里把检测跑起来,但一到正式做功能,就会碰到环境污染、版本对不上、换板复现困难这些问题。本文把重点收回到一件更实在的事:在 Orin 上用 Docker 跑通目标检测功能本身。内容包括 Docker 怎么装、JetPack 和镜像怎么对版、GPU 怎么进容器,以及一个能直接跑的目标检测示例:输入图片或视频,容器里完成 TensorRT 推理,输出画框结果。适合已经做过基础环境确认,想把"板上脚本"收拢成稳定运行环境的人。


1. 为什么在 Orin 上用 Docker

在板子上直接 pip install 跑 demo,第一天很方便;一旦开始做正式功能,问题就会慢慢出来:

  • 这个板能跑,换一块板就缺包
  • 同事也装了一堆依赖,环境互相污染
  • 系统 Python 被改乱,后面升级 JetPack 更麻烦
  • 明明只是想稳定跑一个检测功能,结果时间都花在环境对齐上

Docker 的价值不只是"隔离",更实际的好处是:

  • 依赖写进镜像,复现容易
  • 运行方式固定,便于交接
  • 日志、挂载目录、输入输出路径更好管
  • 后面要改成开机自启,也更顺手

对 Orin 这种边缘设备来说,Docker 还有一个很现实的意义:把系统环境和业务环境分开。系统里主要保 JetPack、CUDA、TensorRT、驱动路径;业务依赖尽量收在容器里。这样你后面换模型、换脚本、换业务目录,都不至于把板子本体搞乱。


2. 这篇要做的具体功能

这篇不再讲 HTTP 服务,也不先拉 Web 页面,而是做一个更直接的功能:

在 Orin 上启动一个 Docker 容器,容器里读取图片或视频,用 TensorRT 做目标检测,并把画框后的结果保存到宿主机目录。

链路很简单:

宿主机输入文件 → Docker 容器 → TensorRT 推理 → 结果图/结果视频写回宿主机

这样做更适合作为 Orin 上 Docker 的第一篇功能文,原因有三个:

  • 没有额外的网络接口变量,排障更直接
  • 和前面的最小闭环文章衔接自然
  • 先把"容器 + GPU + 检测功能"打通,再谈服务化更顺

这篇默认你已经有一个在目标 Orin 上构建好的 TensorRT .engine,比如 yolov8n_fp16.engine。如果引擎还没建,先做完最小闭环再回来。


3. 开始前先确认三件事

3.1 JetPack 版本

先把版本记清楚:

bash 复制代码
cat /etc/nv_tegra_release
dpkg -l | rg nvidia-jetpack

在 Jetson 平台上,容器最怕的不是语法错,而是版本不对版。镜像里用户态 CUDA / TensorRT 组件、主机驱动、JetPack 版本如果差太多,常见现象就是:

  • 容器能起来,但 GPU 不可用
  • import tensorrt 报错
  • 库能 import,但一执行推理就崩

所以第一原则不是"先拉最新版",而是先找和当前 JetPack 对得上的 L4T 镜像

3.2 Docker 本身能不能正常跑

bash 复制代码
docker version
docker info

如果这里都不通,先把 Docker 装好再谈检测功能。

3.3 NVIDIA runtime 是否可用

Jetson 上容器想用 GPU,不是只要装了 Docker 就行,还要能把 NVIDIA runtime 带进去。

先看 runtime:

bash 复制代码
docker info | rg -i 'nvidia|runtimes'

如果你的 Docker 版本较新,也可以直接在启动时用 --gpus all;有些 Jetson 环境仍常见 --runtime nvidia 写法。具体以你板上版本为准,但核心判断标准只有一个:容器里能看到并使用 Jetson 的 CUDA / TensorRT 能力。


4. Orin 上 Docker 怎么装和怎么校验

如果系统还没装 Docker,可以先走系统包:

bash 复制代码
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER

加组后重新登录,再验证:

bash 复制代码
docker version

如果装完 Docker 但 GPU 进不了容器,排查顺序通常是:

  1. JetPack 是否装全
  2. NVIDIA Container Runtime 是否在
  3. Docker 运行参数是否对
  4. 拉的镜像是不是 x86 镜像

4.1 先做一个 smoke test

镜像名字不要死背,重点是 L4T 标签要和板上 JetPack 对齐。先用你当前版本对应的 L4T 基础镜像做个 smoke test:

bash 复制代码
docker run --rm --runtime nvidia <匹配当前JetPack的L4T镜像> python3 -c "print('container ok')"

如果容器能起,但一到 TensorRT 就报错,多半还是版本、挂载或 runtime 问题,不是你业务代码的问题。


5. 空间和目录先规划一下

Orin 上做 Docker,最容易被忽略的是磁盘。

先看:

bash 复制代码
df -h
sudo du -sh /var/lib/docker 2>/dev/null || true

如果你是小系统盘,建议至少把下面几类东西分开放:

  • 镜像和容器层
  • TensorRT 引擎
  • 输入图片和视频
  • 输出结果图和结果视频
  • 日志

我更建议这样准备目录:

bash 复制代码
mkdir -p ~/orin-docker-detector/{app,models,input,output}
cd ~/orin-docker-detector

目录含义:

目录 作用
app/ 放 Dockerfile 和检测脚本
models/ .engine 和标签文件
input/ 测试图片或视频
output/ 结果图和结果视频

这样后面挂载卷时比较清楚,也方便你直接清理结果,不会和系统目录搅在一起。


6. 功能设计先定死

为了让文章里的示例尽量短、容易跑,我把功能定成下面这样。

输入

命令行参数:

  • --engine 指向 TensorRT engine
  • --source 指向图片或视频文件
  • --save 指向输出结果文件

输出

  • 如果输入是图片,输出一张带框图片
  • 如果输入是视频,输出一段带框视频
  • 终端打印预处理、推理、后处理耗时

模型

  • 使用已经在本板构建好的 TensorRT engine
  • 示例按 YOLOv8 常见输出写
  • 容器启动后直接运行脚本,不额外包一层服务

这个设计的好处是,先把"容器 + GPU + 检测功能"这条线打通。等这条线通了,再往上接 HTTP、RTSP、前端页面,都只是加功能,不是重搭底座。


7. 需要准备哪些文件

先在 ~/orin-docker-detector/app 里准备下面几个文件:

文件 作用
Dockerfile 构建镜像
requirements.txt Python 依赖
detect_trt.py TensorRT 检测脚本
labels.txt 类别名,可选

另外在 ~/orin-docker-detector/models 里放:

  • yolov8n_fp16.engine
  • labels.txt(如果不想和 app 目录重复,也可只放在 models 里)

下面把完整内容都给出来。


8. Dockerfile(完整代码)

关键点只有两个:

  • 基础镜像要选 Jetson / L4T 对版镜像
  • 只把业务依赖装进容器,不在容器里重新编 TensorRT

保存为 Dockerfile

dockerfile 复制代码
# 这里的基础镜像标签一定要按你的 JetPack / L4T 版本替换
# 常见做法是使用对应版本的 l4t-ml 或 l4t-tensorrt 镜像
FROM nvcr.io/nvidia/l4t-ml:r36.2.0-py3

WORKDIR /app

ENV DEBIAN_FRONTEND=noninteractive \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

RUN apt-get update && apt-get install -y --no-install-recommends \
    python3-pip \
    libglib2.0-0 \
    libgl1 \
    libopencv-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt /app/requirements.txt
RUN pip3 install --no-cache-dir -r /app/requirements.txt

COPY detect_trt.py /app/detect_trt.py

ENTRYPOINT ["python3", "/app/detect_trt.py"]

如果你板子不是这个 L4T 版本,不要照抄 r36.2.0-py3。这类地方最容易引发"容器能起,推理不通"的假象。


9. requirements.txt(完整代码)

保存为 requirements.txt

txt 复制代码
numpy==1.26.4
opencv-python==4.10.0.84
pycuda==2024.1.2

这里没有把 tensorrt 写进 pip。Jetson 上更稳妥的做法通常是跟着 JetPack 自带或对版环境走,而不是随手从 PyPI 装一个不一定匹配的轮子。


10. detect_trt.py(完整代码)

这个脚本做的事不复杂:

  1. 读取图片或视频
  2. 做 letterbox 和归一化
  3. 调 TensorRT 推理
  4. 做 NMS 和坐标还原
  5. 把结果图或视频保存到你指定的位置

保存为 detect_trt.py

python 复制代码
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import time
from pathlib import Path

import cv2
import numpy as np
import pycuda.autoinit  # noqa: F401
import pycuda.driver as cuda
import tensorrt as trt

COCO80 = [
    "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck",
    "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench",
    "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra",
    "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
    "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove",
    "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
    "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange",
    "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
    "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse",
    "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink",
    "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier",
    "toothbrush",
]


class TrtYolo:
    def __init__(self, engine_path: str):
        logger = trt.Logger(trt.Logger.WARNING)
        with open(engine_path, "rb") as f, trt.Runtime(logger) as runtime:
            self.engine = runtime.deserialize_cuda_engine(f.read())
        if self.engine is None:
            raise RuntimeError(f"加载 engine 失败: {engine_path}")
        self.context = self.engine.create_execution_context()
        self.stream = cuda.Stream()
        self.inputs = []
        self.outputs = []
        self.bindings = []
        self.use_tensor_api = hasattr(self.engine, "num_io_tensors")

        if self.use_tensor_api:
            self._init_trt10()
        else:
            self._init_trt8()

        self.in_h = self.inputs[0]["shape"][2]
        self.in_w = self.inputs[0]["shape"][3]

    def _alloc(self, shape, dtype):
        size = int(np.prod(shape))
        host = cuda.pagelocked_empty(size, dtype)
        device = cuda.mem_alloc(host.nbytes)
        return host, device

    def _init_trt8(self):
        for i in range(self.engine.num_bindings):
            name = self.engine.get_binding_name(i)
            dtype = trt.nptype(self.engine.get_binding_dtype(i))
            shape = tuple(self.engine.get_binding_shape(i))
            host, device = self._alloc(shape, dtype)
            self.bindings.append(int(device))
            item = {"name": name, "host": host, "device": device, "shape": shape}
            if self.engine.binding_is_input(i):
                self.inputs.append(item)
            else:
                self.outputs.append(item)

    def _init_trt10(self):
        for i in range(self.engine.num_io_tensors):
            name = self.engine.get_tensor_name(i)
            dtype = trt.nptype(self.engine.get_tensor_dtype(name))
            shape = tuple(self.engine.get_tensor_shape(name))
            host, device = self._alloc(shape, dtype)
            self.context.set_tensor_address(name, int(device))
            item = {"name": name, "host": host, "device": device, "shape": shape}
            if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
                self.inputs.append(item)
            else:
                self.outputs.append(item)

    def infer(self, nchw: np.ndarray) -> np.ndarray:
        np.copyto(self.inputs[0]["host"], nchw.ravel())
        cuda.memcpy_htod_async(self.inputs[0]["device"], self.inputs[0]["host"], self.stream)
        if self.use_tensor_api:
            self.context.execute_async_v3(stream_handle=self.stream.handle)
        else:
            self.context.execute_async_v2(bindings=self.bindings, stream_handle=self.stream.handle)
        for out in self.outputs:
            cuda.memcpy_dtoh_async(out["host"], out["device"], self.stream)
        self.stream.synchronize()
        return self.outputs[0]["host"].reshape(self.outputs[0]["shape"])


def letterbox(im, new_shape=(640, 640), color=(114, 114, 114)):
    h, w = im.shape[:2]
    r = min(new_shape[0] / h, new_shape[1] / w)
    nh, nw = int(round(h * r)), int(round(w * r))
    resized = cv2.resize(im, (nw, nh), interpolation=cv2.INTER_LINEAR)
    top = (new_shape[0] - nh) // 2
    bottom = new_shape[0] - nh - top
    left = (new_shape[1] - nw) // 2
    right = new_shape[1] - nw - left
    out = cv2.copyMakeBorder(resized, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
    return out, r, (left, top)


def preprocess(bgr, input_hw):
    img, ratio, (dw, dh) = letterbox(bgr, input_hw)
    rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
    nchw = np.transpose(rgb, (2, 0, 1))[None, ...]
    return np.ascontiguousarray(nchw), ratio, dw, dh


def postprocess(output, ratio, dw, dh, conf_thres=0.25, iou_thres=0.45, orig_shape=None):
    pred = np.squeeze(output)
    if pred.ndim != 2:
        raise ValueError(f"意外输出形状: {output.shape}")
    if pred.shape[0] < pred.shape[1]:
        pred = pred.T

    boxes_xywh = pred[:, :4]
    cls_scores = pred[:, 4:]
    cls_ids = np.argmax(cls_scores, axis=1)
    scores = cls_scores[np.arange(len(cls_ids)), cls_ids]

    mask = scores >= conf_thres
    boxes_xywh = boxes_xywh[mask]
    scores = scores[mask]
    cls_ids = cls_ids[mask]
    if len(scores) == 0:
        return [], [], []

    x, y, w, h = boxes_xywh.T
    x1 = (x - w / 2 - dw) / ratio
    y1 = (y - h / 2 - dh) / ratio
    x2 = (x + w / 2 - dw) / ratio
    y2 = (y + h / 2 - dh) / ratio
    boxes = np.stack([x1, y1, x2, y2], axis=1)

    if orig_shape is not None:
        oh, ow = orig_shape
        boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, ow)
        boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, oh)

    idx = cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), conf_thres, iou_thres)
    if len(idx) == 0:
        return [], [], []
    idx = np.array(idx).reshape(-1)
    return boxes[idx], scores[idx], cls_ids[idx]


def draw_boxes(image, boxes, scores, cls_ids):
    out = image.copy()
    for box, score, cid in zip(boxes, scores, cls_ids):
        x1, y1, x2, y2 = [int(v) for v in box]
        label = COCO80[int(cid)] if int(cid) < len(COCO80) else str(int(cid))
        cv2.rectangle(out, (x1, y1), (x2, y2), (0, 180, 0), 2)
        cv2.putText(out, f"{label} {float(score):.2f}", (x1, max(0, y1 - 6)),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 180, 0), 1, cv2.LINE_AA)
    return out


def open_source(source: str):
    p = Path(source)
    if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp", ".webp"}:
        img = cv2.imread(str(p))
        if img is None:
            raise FileNotFoundError(f"读图失败: {source}")
        return "image", img
    cap = cv2.VideoCapture(str(p))
    if not cap.isOpened():
        raise RuntimeError(f"打开失败: {source}")
    return "video", cap


def main():
    ap = argparse.ArgumentParser(description="Orin Docker TensorRT detector")
    ap.add_argument("--engine", required=True)
    ap.add_argument("--source", required=True)
    ap.add_argument("--save", required=True)
    ap.add_argument("--conf", type=float, default=0.25)
    ap.add_argument("--iou", type=float, default=0.45)
    args = ap.parse_args()

    detector = TrtYolo(args.engine)
    input_hw = (detector.in_h, detector.in_w)
    kind, src = open_source(args.source)

    if kind == "image":
        t0 = time.perf_counter()
        nchw, ratio, dw, dh = preprocess(src, input_hw)
        t1 = time.perf_counter()
        output = detector.infer(nchw)
        t2 = time.perf_counter()
        boxes, scores, cls_ids = postprocess(output, ratio, dw, dh, args.conf, args.iou, src.shape[:2])
        t3 = time.perf_counter()
        vis = draw_boxes(src, boxes, scores, cls_ids)
        cv2.imwrite(args.save, vis)
        print(f"检出 {len(boxes)} 个目标,结果已保存: {args.save}")
        print(f"预处理 {(t1-t0)*1000:.2f} ms | 推理 {(t2-t1)*1000:.2f} ms | 后处理 {(t3-t2)*1000:.2f} ms")
        return

    cap = src
    writer = None
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        nchw, ratio, dw, dh = preprocess(frame, input_hw)
        output = detector.infer(nchw)
        boxes, scores, cls_ids = postprocess(output, ratio, dw, dh, args.conf, args.iou, frame.shape[:2])
        vis = draw_boxes(frame, boxes, scores, cls_ids)

        if writer is None:
            h, w = vis.shape[:2]
            fps = cap.get(cv2.CAP_PROP_FPS) or 25
            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
            writer = cv2.VideoWriter(args.save, fourcc, fps, (w, h))
        writer.write(vis)

    cap.release()
    if writer is not None:
        writer.release()
    print(f"视频结果已保存: {args.save}")


if __name__ == "__main__":
    main()

这个脚本默认按 YOLOv8 常见输出解析。换成你自己的模型时,最可能要改的是 postprocess,不是 Dockerfile。


11. 构建镜像

现在目录结构应该大概是这样:

bash 复制代码
~/orin-docker-detector/
├── app/
│   ├── Dockerfile
│   ├── requirements.txt
│   └── detect_trt.py
├── input/
├── models/
│   └── yolov8n_fp16.engine
└── output/

开始构建:

bash 复制代码
cd ~/orin-docker-detector/app
docker build -t orin-detector:1.0 .

构建慢、拉镜像慢,在 Orin 上很正常。这里出问题,优先查:

  • 网络
  • 磁盘空间
  • 基础镜像标签

12. 启动容器跑图片检测

最关键的是把模型目录、输入目录、输出目录挂进去,并把 GPU 能力带进去。

bash 复制代码
docker run --rm \
  --name orin-detector \
  --runtime nvidia \
  -v ~/orin-docker-detector/models:/models \
  -v ~/orin-docker-detector/input:/input \
  -v ~/orin-docker-detector/output:/output \
  orin-detector:1.0 \
  --engine /models/yolov8n_fp16.engine \
  --source /input/test.jpg \
  --save /output/test_result.jpg

如果你的环境更推荐 --gpus all,可以改成:

bash 复制代码
docker run --rm \
  --name orin-detector \
  --gpus all \
  -v ~/orin-docker-detector/models:/models \
  -v ~/orin-docker-detector/input:/input \
  -v ~/orin-docker-detector/output:/output \
  orin-detector:1.0 \
  --engine /models/yolov8n_fp16.engine \
  --source /input/test.jpg \
  --save /output/test_result.jpg

跑完后,结果图就在宿主机的:

bash 复制代码
~/orin-docker-detector/output/test_result.jpg

这就是这篇最核心的目标:用 Docker 在 Orin 上把目标检测功能本身跑通。


13. 再跑一段视频

图片通了以后,再拿一段短视频验证:

bash 复制代码
docker run --rm \
  --name orin-detector-video \
  --runtime nvidia \
  -v ~/orin-docker-detector/models:/models \
  -v ~/orin-docker-detector/input:/input \
  -v ~/orin-docker-detector/output:/output \
  orin-detector:1.0 \
  --engine /models/yolov8n_fp16.engine \
  --source /input/demo.mp4 \
  --save /output/demo_result.mp4

第一次建议先拿 10 到 30 秒的小视频,别上来就一段几十分钟的大文件。视频阶段更容易把磁盘写入、编码、时长这些变量一起带进来。


14. 怎么看它是不是跑对了

第一遍我只看三件事:

  • 容器能不能正常退出,不是中途崩掉
  • 宿主机 output/ 目录里有没有结果文件
  • 结果图或视频里的框是不是基本合理

再往下一步,才看耗时打印和系统负载:

bash 复制代码
tegrastats

重点看:

  • GPU 是否真有负载
  • CPU 有没有某个核吃满
  • 内存是否紧张
  • 温度是否上来后开始降频

如果结果图有框但很慢,先别急着怪 TensorRT。常见拖慢因素还有:

  • 输入视频太大
  • 输出目录在慢盘上
  • 功耗档没打上去
  • 板子本身已经在热降频

15. 想停、想改、想更新时怎么做

这篇的示例用的是 --rm,容器跑完就退出并自动删掉,不需要额外清理。真正会反复操作的,主要是下面两类。

改代码后重建

bash 复制代码
cd ~/orin-docker-detector/app
docker build -t orin-detector:1.0 .

看镜像和空间

bash 复制代码
docker images
docker system df

镜像改几次之后,很容易留下旧层。空间紧张时别只盯输入数据,Docker 自己的层也会长得很快。


16. 这套方式适合什么,不适合什么

适合:

  • 把图片或视频检测做成稳定可复现的容器功能
  • 多人协作时固定运行环境
  • 后面继续扩成服务、RTSP、前端页面
  • 板子上不想反复把系统 Python 搞乱

不太适合:

  • 第一天刚刷完系统、连 TensorRT 都还没通
  • 还没做过最小闭环就直接容器化
  • 想在容器里现场编译一堆大工程,又想板子马上稳定运行

换句话说,Docker 不是跳过基础工作的捷径,而是把基础工作成果收拢成可复用运行环境的工具。


17. 最常见的坑

现象 多见原因 先查什么
容器能起但 import tensorrt 失败 基础镜像和 JetPack 不对版 L4T 标签、主机版本
一执行推理就崩 engine 和当前板不匹配 engine 是否本板构建
跑完没有结果文件 输入路径或输出挂载路径写错 -v 挂载目录、--save 参数
能跑但框乱 预处理或输出解析不一致 postprocess、输入尺寸
视频很慢 输入过大、输出慢盘、功耗档低 tegrastatsnvpmodel
容器内没 GPU runtime / --gpus all / runtime 配置不对 docker info、启动参数
构建镜像老失败 系统盘小、网络慢、镜像过大 df -h、基础镜像来源

还有一个很典型:很多人把 x86 的教程照搬到 Orin。结果容器构建是成功了,但里面装进去的是错架构或错版本依赖。Jetson 平台最怕这种"看起来装好了"的错。


18. 后面怎么继续扩

这个容器化检测功能只是第一步,后面很好扩:

  1. 让容器直接读 RTSP 或 USB 相机
  2. 增加批量图片处理
  3. 把结果写入数据库或消息队列
  4. 再往上封成各种服务
  5. 再接前端页面或告警逻辑
  6. 再谈多路和调度系统

也就是说,这种写法虽然简单,但不是玩具。很多实际项目最开始就是先把检测能力收成一个稳定容器,再往外长别的模块。


19. 小结

Orin 上用 Docker,重点不在于记住几条命令,而在于把三个东西理顺:JetPack 对版、GPU 能进容器、目标检测功能能跑起来。

这篇给的做法,本质上就是把前面的板上脚本推理收成一个更稳定的运行边界。对外它不是服务,只是一个能重复执行的检测功能;但正因为边界更清楚,后面你再做服务化、视频流、前端显示,反而更省事。

如果前面的最小闭环已经做完,这一步非常值得补上。它会让你的 Orin 从"会跑模型"变成"能稳定跑功能"。

相关推荐
xd1855785558 小时前
家电选购参谋 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
秦先生在广东8 小时前
Moonshine Voice:专为实时语音而生的端侧 ASR 工具包
人工智能
不爱记笔记8 小时前
2026 WAIC大会亮点解析!超节点、机器人与AI应用全面开花
人工智能·ai·机器人·具身智能·deepseek
lichenyang4538 小时前
AI 入门看了很多,为什么还是不懂?从 Prompt、AI 工具到大模型原理一次讲清楚
人工智能
网易云信8 小时前
网易智企亮相 2026 世界人工智能大会:一站式企业 AI 应用覆盖三大企业现场
人工智能·aigc·线下活动
中微极客8 小时前
GPT-4o图像生成:从艺术创作到API工程实践
人工智能
土星云SaturnCloud8 小时前
MP_SENet轻量语音降噪模型在土星云边缘设备的部署实战
服务器·人工智能·ai·边缘计算·语音识别
Lifangyun_WD8 小时前
RTX 5090跑Stable Diffusion XL:生图速度、显存占用与商业应用边界
人工智能·stable diffusion·gpu算力·rtx 5090·gpu容器·gpu租赁
hey you~8 小时前
2026出海语音机器人全栈选型:从ASR引擎评测到GDPR合规落地
人工智能·机器人·语音识别