不安装YOLO只安装 PyTorch,加载已有yolo数据,从无到有创建模型训练数据并加载使用(重要)

目录

遗留问题:

1、安装依赖

[2、训练脚本 train.py](#2、训练脚本 train.py)

[3、独立推理脚本 infer.py(完全独立,不需要数据集、不需要训练代码)](#3、独立推理脚本 infer.py(完全独立,不需要数据集、不需要训练代码))

关键知识点

[补充:导出 ONNX 代码(追加到 train.py 末尾)](#补充:导出 ONNX 代码(追加到 train.py 末尾))

[OpenCV‑C++ DNN 加载 detect.onnx 完整 Demo(VS2015)](#OpenCV‑C++ DNN 加载 detect.onnx 完整 Demo(VS2015))

[1. pro 工程配置要点(如果你用 Qt+VS2015)](#1. pro 工程配置要点(如果你用 Qt+VS2015))

[2. main.cpp 完整代码(复制直接编译)](#2. main.cpp 完整代码(复制直接编译))

[3. 非常关键的注意点(踩坑重点)](#3. 非常关键的注意点(踩坑重点))

[4. 和之前整套链路完整回顾](#4. 和之前整套链路完整回顾)


可参考课程如下

课程未看完。仅仅看到18节课

课程已看完,但没实操

遗留问题:

1.课程未看完。仅仅看到18节课

2.课程已看完,但没实操

注意:与本章节《PyTorch 完整流程:从零搭建模型 → 训练 → 保存权重 → 加载自定义模型推理(学习pytorch框架和YOLO笔记(重要)部分说明)》进行对比更加深入了解

需求说明:不安装 ultralytics/yolo 库,但是使用 YOLO 标注格式的数据集(images 图片 + labels txt 标注),自己手写 PyTorch 检测网络,读取 yolo 格式数据集、训练、保存权重、独立脚本推理。环境:miniconda python3.8 + CPU 版 PyTorch,不引入 YOLO 任何包。YOLO 数据集格式:

plaintext

复制代码
dataset/
├─images/
│   ├─001.jpg
│   └─002.jpg
└─labels/
    ├─001.txt  # 格式:class_id x_center y_center w h (全部归一化0~1)
    └─002.txt

1、安装依赖

bash

复制代码
conda create -n torch_detect python=3.8 -y
conda activate torch_detect
# cpu pytorch
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install opencv-python pillow

2、训练脚本 train.py

功能:

  1. 自定义简易检测 CNN 网络(不是 YOLO,自己手写)
  2. 读取YOLO 标注格式数据集(图片 + txt 标签)
  3. Dataset + DataLoader
  4. 损失、优化器、训练循环
  5. 保存 state_dict 权重(工程推荐方式)

python

复制代码
import os
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import cv2
from PIL import Image
import torchvision.transforms as T

# -------------------------- 1.自定义简易检测网络 --------------------------
class SimpleDetectNet(nn.Module):
    def __init__(self, num_classes=2):
        super().__init__()
        # backbone卷积提取特征
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2,2)

        # 输出:简化设计,每张图输出 1个框 [x,y,w,h] + 类别概率
        self.head = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64*16*16, 256),
            nn.ReLU(),
            nn.Linear(256, 4 + num_classes) #4个框坐标 + num_classes类别
        )

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        x = self.pool(torch.relu(self.conv3(x)))
        out = self.head(x)
        return out


# -------------------------- 2.读取YOLO格式数据集 Dataset --------------------------
class YoloFormatDataset(Dataset):
    def __init__(self, root_dir, img_size=128, num_classes=2):
        self.root = root_dir
        self.img_dir = os.path.join(root_dir, "images")
        self.label_dir = os.path.join(root_dir, "labels")
        self.img_list = [f for f in os.listdir(self.img_dir) if f.endswith((".jpg",".png"))]
        self.img_size = img_size
        self.num_classes = num_classes

        self.transform = T.Compose([
            T.Resize((img_size, img_size)),
            T.ToTensor(), #归一化0~1
        ])

    def __len__(self):
        return len(self.img_list)

    def __getitem__(self, index):
        img_name = self.img_list[index]
        img_path = os.path.join(self.img_dir, img_name)
        # 对应txt标签
        txt_name = os.path.splitext(img_name)[0] + ".txt"
        txt_path = os.path.join(self.label_dir, txt_name)

        #读取图片
        img = Image.open(img_path).convert("RGB")
        img_tensor = self.transform(img)

        #读取yolo标签 txt: class xc yc w h 全部0~1
        boxes = torch.zeros((1,5))
        if os.path.exists(txt_path):
            lines = open(txt_path,"r",encoding="utf-8").readlines()
            if len(lines)>0:
                line = lines[0].strip().split()
                cls_id = int(line[0])
                xc = float(line[1])
                yc = float(line[2])
                w = float(line[3])
                h = float(line[4])
                boxes[0] = torch.tensor([cls_id, xc, yc, w, h])

        label_cls = boxes[0][0].long()
        label_box = boxes[0][1:]
        return img_tensor, label_cls, label_box


# -------------------------- 3.数据集、模型、损失、优化器 --------------------------
device = torch.device("cpu")
NUM_CLASS = 2

dataset = YoloFormatDataset(root_dir="./my_yolo_dataset", img_size=128, num_classes=NUM_CLASS)
train_loader = DataLoader(dataset, batch_size=4, shuffle=True)

model = SimpleDetectNet(num_classes=NUM_CLASS).to(device)

# 分类损失 + 框坐标回归损失
loss_cls = nn.CrossEntropyLoss()
loss_box = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4)

EPOCHS = 10

# -------------------------- 4.训练循环 --------------------------
for epoch in range(EPOCHS):
    total_loss = 0.0
    for imgs, gt_cls, gt_bbox in train_loader:
        imgs = imgs.to(device)
        gt_cls = gt_cls.to(device)
        gt_bbox = gt_bbox.to(device)

        pred_out = model(imgs)
        pred_box = pred_out[:,0:4]
        pred_cls = pred_out[:,4:]

        l1 = loss_cls(pred_cls, gt_cls)
        l2 = loss_box(pred_box, gt_bbox)
        total = l1 + l2

        optimizer.zero_grad()
        total.backward()
        optimizer.step()

        total_loss += total.item()

    print(f"epoch {epoch+1:2d} | loss = {total_loss / len(train_loader):.4f}")

# -------------------------- 5.保存权重 state_dict(推荐) --------------------------
torch.save(model.state_dict(), "detect_weight.pth")
print("训练完成,权重保存 detect_weight.pth")

3、独立推理脚本 infer.py(完全独立,不需要数据集、不需要训练代码)

模拟工程部署:只加载权重,输入图片做预测

python

复制代码
import torch
import torch.nn as nn
from PIL import Image
import torchvision.transforms as T

class SimpleDetectNet(nn.Module):
    def __init__(self, num_classes=2):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 16, 3, padding=1)
        self.conv2 = nn.Conv2d(16, 32, 3, padding=1)
        self.conv3 = nn.Conv2d(32, 64, 3, padding=1)
        self.pool = nn.MaxPool2d(2,2)
        self.head = nn.Sequential(
            nn.Flatten(),
            nn.Linear(64*16*16, 256),
            nn.ReLU(),
            nn.Linear(256, 4 + num_classes)
        )

    def forward(self, x):
        x = self.pool(torch.relu(self.conv1(x)))
        x = self.pool(torch.relu(self.conv2(x)))
        x = self.pool(torch.relu(self.conv3(x)))
        out = self.head(x)
        return out

device = torch.device("cpu")
NUM_CLASS=2

#加载权重,网络类必须和训练完全一模一样
model = SimpleDetectNet(num_classes=NUM_CLASS).to(device)
model.load_state_dict(torch.load("detect_weight.pth", map_location=device))

model.eval() #推理模式
transform = T.Compose([T.Resize((128,128)), T.ToTensor()])

with torch.no_grad():
    img = Image.open("test.jpg").convert("RGB")
    img_tensor = transform(img).unsqueeze(0).to(device)

    out = model(img_tensor)
    pred_bbox = out[0,0:4] # xc yc w h 归一化0~1
    pred_cls = torch.argmax(out[0,4:])
    print(f"预测类别id:{pred_cls.item()}")
    print(f"预测框[xc,yc,w,h]:{pred_bbox.cpu().numpy()}")

关键知识点

  1. 数据集来源 :直接使用 YOLO 标注工具打出来的数据集(images + labels txt),不需要安装 YOLO 库,自己写 Dataset 解析 txt 标签。
  2. 网络是自己手写的简易检测网络,不是 YOLOv8 网络,只是读取 YOLO 格式标注文件。
  3. 工程分离:
    • train.py:读取 yolo 数据集、训练、保存权重,只训练阶段使用。
    • infer.py:独立脚本,只加载权重做推理,不需要数据集、不需要优化器、没有反向传播。
  4. 后续链路:训练得到detect_weight.pth → pytorch 导出 ONNX → OpenCV DNN (C++/Qt) 加载 ONNX 做部署

补充:导出 ONNX 代码(追加到 train.py 末尾)

python

复制代码
dummy_input = torch.randn(1,3,128,128,device=device)
torch.onnx.export(model, dummy_input, "detect.onnx", opset_version=12)
print("onnx模型导出 detect.onnx")

注意:上面网络是极简演示网络,精度很低,仅演示整套数据流;实际做检测需要复杂网络、nms、多目标、anchor 等逻辑。

如果你需要,我可以给你:OpenCV C++ DNN 加载这个 detect.onnx 完整 demo(适配 VS2015)

OpenCV‑C++ DNN 加载 detect.onnx 完整 Demo(VS2015)

约束说明

  1. VS2015,MSVC2015,C++11 语法,不使用 C++17 特性
  2. OpenCV 3.4.x/4.x 均可,使用cv::dnn::Net读取刚才导出的detect.onnx
  3. 网络输出:[batch, 6][x_c, y_c, w, h, cls0_score, cls1_score],坐标是归一化 0‑1
  4. 仅演示推理,无 NMS(我们这个 demo 网络只输出单目标)
  5. 输入要求:图片 resize 到 128×128;RGB→BGR;像素 0~1 归一化

1. pro 工程配置要点(如果你用 Qt+VS2015)

pro

复制代码
INCLUDEPATH += $$PWD/opencv/include
LIBS += -L$$PWD/opencv/lib -lopencv_world3416

VS 原生项目:配置 VC++ 目录,包含目录、库目录,链接器输入添加opencv_world3416.lib

2. main.cpp 完整代码(复制直接编译)

cpp

复制代码
#include <iostream>
#include <opencv2/opencv.hpp>

using namespace cv;
using namespace cv::dnn;
using namespace std;

int main()
{
    //==================== 路径配置,改成你自己的路径 ====================
    string onnxPath = "detect.onnx";
    string imgPath  = "test.jpg";

    // 模型输入尺寸,和pytorch训练保持一致 128x128
    const int INPUT_W = 128;
    const int INPUT_H = 128;

    //1.加载onnx模型
    Net net;
    try
    {
        net = readNetFromONNX(onnxPath);
    }
    catch (cv::Exception& e)
    {
        cout << "加载ONNX失败:" << e.what() << endl;
        system("pause");
        return -1;
    }

    // 优先使用CPU(你的机器无N卡)
    net.setPreferableBackend(DNN_BACKEND_OPENCV);
    net.setPreferableTarget(DNN_TARGET_CPU);

    //2.读取原图
    Mat src = imread(imgPath);
    if(src.empty())
    {
        cout << "图片读取失败!" << endl;
        system("pause");
        return -1;
    }
    Mat originImg = src.clone();
    int imgW = src.cols;
    int imgH = src.rows;

    //3.构建blob:resize、归一化0~1、RGB顺序匹配pytorch
    Mat blob;
    blobFromImage(src,
                  blob,
                  1.0 / 255.0,               // 像素缩放到0~1,和pytorch ToTensor一致
                  Size(INPUT_W, INPUT_H),
                  Scalar(0,0,0),
                  true,    // swapRB=true BGR→RGB,非常关键!
                  false);

    //4.推理
    net.setInput(blob);
    Mat out = net.forward();

    /*
        out.shape: [1, 6]
        out[0][0] → xc 归一化
        out[0][1] → yc 归一化
        out[0][2] → w 归一化
        out[0][3] → h 归一化
        out[0][4] → class0 score
        out[0][5] → class1 score
    */
    float* pdata = (float*)out.data;

    float xc_norm = pdata[0];
    float yc_norm = pdata[1];
    float w_norm  = pdata[2];
    float h_norm  = pdata[3];
    float s0 = pdata[4];
    float s1 = pdata[5];

    cout << "onnx推理输出:" << endl;
    cout << "xc:" << xc_norm << " yc:" << yc_norm << " w:" << w_norm << " h:" << h_norm << endl;
    cout << "cls0 score:" << s0 << " cls1 score:" << s1 << endl;

    // 获取预测类别
    int pred_cls = (s1 > s0) ? 1 : 0;
    cout << "预测类别 = " << pred_cls << endl;

    // ========== 归一化坐标 还原到原图像素坐标 ==========
    // yolo格式:xc,yc是中心点归一化
    float xc = xc_norm * imgW;
    float yc = yc_norm * imgH;
    float w  = w_norm  * imgW;
    float h  = h_norm  * imgH;

    int x1 = cvRound(xc - w / 2.0f);
    int y1 = cvRound(yc - h / 2.0f);
    int x2 = cvRound(xc + w / 2.0f);
    int y2 = cvRound(yc + h / 2.0f);

    // 边界保护,防止框跑出图片外
    x1 = max(0, x1);
    y1 = max(0, y1);
    x2 = min(imgW - 1, x2);
    y2 = min(imgH - 1, y2);

    //绘制框
    rectangle(originImg, Point(x1,y1), Point(x2,y2), Scalar(0,255,0), 2);
    putText(originImg, ("cls:"+to_string(pred_cls)), Point(x1, y1-5),
            FONT_HERSHEY_SIMPLEX,0.5,Scalar(0,0,255),1);

    imshow("detect_onnx_result", originImg);
    waitKey(0);

    system("pause");
    return 0;
}

3. 非常关键的注意点(踩坑重点)

  1. swapRB=true PyTorch PIL 读取是 RGB;OpenCV imread 是 BGR,blobFromImage第 6 个参数必须true做通道交换,否则预测完全错乱。

  2. 归一化:scalefactor = 1.0/255.0,和 PyTorch ToTensor()对齐。

  3. ONNX 导出时,输入尺寸固定为1×3×128×128,C++ 这边输入图片必须 resize 到128×128

  4. 这个网络是极简演示网络,只输出单个目标框,没有多目标、没有 NMS。真实业务需要网络输出多 anchor、增加 NMS 逻辑。

  5. VS2015 编译报错排查

  • LNK2019:opencv 库版本不匹配,检查 win32/x64,release/debug。
  • 读 onnx 返回空:onnx 文件损坏,确认 pytorch 导出时opset_version=12

4. 和之前整套链路完整回顾

plaintext

复制代码
PyTorch train.py(读取yolo标注txt) → detect_weight.pth → 导出detect.onnx
                                 ↓
                VS2015 + OpenCV DNN(C++)加载onnx做推理部署

如果你需要,我可以补充:

  • 配套完整val_eval.py评估脚本;
  • 或者给你加上简单 NMS 的 C++ 版本(适配多目标输出)。
相关推荐
云端漫步19871 小时前
HarmonyOS NEXT AI 智能生活助手:统一 AIService 封装
人工智能·华为·生活·harmonyos
风途科技~1 小时前
FMCW 调频连续波雷达|非接触式雷达水位计精准把控液位变化
大数据·人工智能
程序员AI工坊1 小时前
Agent 开发:ReAct 循环与工具调用实战——从单次调用到自主 Agent
人工智能·后端·python·langchain·agent·react
guo_xiao_xiao_1 小时前
YOLO[室内与室外多场景混凝土块目标检测]目标检测数据集
yolo·目标检测·目标跟踪
小刘快学习2 小时前
品牌舆情监测的接入思路:从采集到预警的链路
人工智能
Ai-_Man2 小时前
豆包智能体内容批量导出:哪些该存、存成什么、怎么存v
人工智能·ai·小程序·word
XS0301062 小时前
Spring AI:两种内置 Advisor 快速实现 RAG
java·人工智能·spring
阿部多瑞 ABU2 小时前
新-潘多拉降临操场
人工智能
小妖同学学AI2 小时前
拒绝手动SSH:用战斗机与AI混沌构建的硬核Homelab架构
人工智能