YOLO C语言代码如何实现目标检测?

99ANYc3cd6
预计阅读时长 30 分钟
位置: 首页 C语言 正文

YOLO 的完整 C 语言实现,尤其是像 YOLOv5、YOLOv8 这样的现代版本,通常不是一个从头开始的“Hello World”级别的项目。 它是一个高度优化的、复杂的计算机视觉系统,包含了大量的数学运算(卷积、池化)、网络结构定义、数据处理和后处理逻辑。

yolo c 语言代码
(图片来源网络,侵删)

实现 YOLO 的 C 语言代码主要有以下几种形式:

  1. 基于 Darknet 框架 (最经典、最完整的 C 实现)
  2. 使用 ONNX Runtime 或 TensorRT 的 C API (生产环境推荐)
  3. 从零开始编写一个极简版 YOLOv1 (用于学习原理)

下面我将分别对这三种方式进行介绍,并提供核心代码示例和资源链接。


基于 Darknet 框架 (最经典)

Darknet 是 YOLO 的原始作者 Joseph Redmon 开发的轻量级、开源的神经网络框架,它完全用 C 语言和 CUDA 编写,是学习和使用 YOLO 最经典的 C 语言环境。

核心特点

  • 纯 C/CUDA: 没有复杂的 C++ 依赖,非常适合在 NVIDIA GPU 上进行高性能推理。
  • 命令行工具: 通过简单的命令行即可完成训练和检测。
  • 模块化设计: 代码结构清晰,包含网络层定义 (layer.h)、数据加载 (data.c)、推理 (network.c) 等模块。

核心代码结构解析

当你克隆并打开 Darknet 的源码仓库时,你会看到以下关键文件:

yolo c 语言代码
(图片来源网络,侵删)
  • src/network.c: 网络的核心,包含了前向传播 (forward_network), 反向传播 (backward_network), 网络初始化等所有与网络计算相关的函数。
  • src/layer.h & src/layer.c: 网络层的定义,定义了各种层结构,如卷积层 (CONVOLUTIONAL)、池化层 (MAXPOOL)、激活层 (RELU)、连接层 (ROUTE) 等,每个层都有其特定的 forwardbackward 函数。
  • src/utils.c: 工具函数,包含数学工具、计时、日志等辅助功能。
  • src/detector.c: 检测器入口,包含了 predict_network 函数,这是进行目标检测调用的主要函数,它负责加载网络、预处理图像、执行推理和后处理(NMS)。
  • src/image.c: 图像处理,包含图像的加载、缩放、填充、颜色空间转换等操作。
  • src/parser.c: 配置文件解析,用于解析 .cfg 网络结构文件。

核心推理代码示例 (detector.c)

detector.c 中的 detect_image 函数是推理的缩影,下面是一个简化的逻辑流程:

// 在 detector.c 中
image detect_image(network *net, image im)
{
    // 1. 预处理图像
    // 调整图像大小以适应网络输入,并进行归一化等操作
    image sized = letterbox_image(im, net->w, net->h); // letterbox 会保持长宽比,填充背景
    network_predict(net, sized.data); // 这是核心的推理调用!
    // 2. 后处理
    // network_predict 之后,网络的输出就在 net->output 中
    // net->output 是一个 float 数组,包含了所有预测框的置信度和类别概率
    // 这里需要手动实现或调用 Darknet 提供的后处理函数
    // 通常包括:
    // a) 过滤掉低置信度的框
    // b) 将预测框的坐标从网络输入尺寸转换回原始图像尺寸
    // c) 执行非极大值抑制 来消除重叠的框
    // ... (后处理逻辑非常复杂,通常调用如 do_nms_obj 等)
    // 返回包含所有检测框信息的图像结构
    // return dets_image; 
}

network_predict 函数是关键,它遍历网络的所有层,逐层执行前向传播,最终得到输出。

如何开始

  1. 克隆 Darknet:
    git clone https://github.com/pjreddie/darknet.git
    cd darknet
  2. 编译:
    • 仅 CPU:
      make
    • 启用 CUDA (GPU):
      sed -i 's/OPENCV=0/OPENCV=1/' Makefile
      sed -i 's/GPU=0/GPU=1/' Makefile
      sed -i 's/CUDNN=0/CUDNN=1/' Makefile
      make
  3. 下载预训练模型:
    wget https://pjreddie.com/media/files/yolov3.weights
  4. 运行检测:
    ./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg

资源链接:


使用 ONNX Runtime 或 TensorRT (生产环境推荐)

在现代应用中,很少有人直接使用 Darknet,通常的做法是:

yolo c 语言代码
(图片来源网络,侵删)
  1. 用 Python (PyTorch/TensorFlow) 训练好 YOLO 模型。
  2. 将模型导出为标准格式,如 ONNX (Open Neural Network Exchange)。
  3. 使用 C/C++ 调用 ONNX RuntimeNVIDIA TensorRT 的 C API 来加载模型并进行高性能推理。

这种方法结合了 Python 生态的便利性和 C 语言的性能。

核心思路

  • 模型转换: PyTorch Model -> ONNX Model
  • C++ 加载: 在 C++ 项目中集成 ONNX Runtime 库。
  • 推理流程:
    1. 初始化 ONNX Runtime 会话。
    2. 读取输入图像,进行预处理(缩放、归一化、转换为 float32 格式)。
    3. 将处理后的图像数据作为输入,调用会话的 Run 方法。
    4. 获取模型的输出(一堆浮点数)。
    5. 手动实现后处理:这部分和 Darknet 类似,是 C 语言实现 YOLO 的核心难点。
    6. 解析后处理结果,绘制边界框。

核心代码示例 (使用 ONNX Runtime C API)

这是一个概念性的代码片段,展示了如何在 C++ 中使用 ONNX Runtime。

#include <onnxruntime_c_api.h>
#include <opencv2/opencv.hpp>
// 假设模型输入是 (1, 3, 640, 640)
const int input_width = 640;
const int input_height = 640;
const int input_channels = 3;
void run_yolov8_onnx(const char* model_path, const char* image_path) {
    // 1. 初始化 ONNX Runtime
    OrtEnv* env;
    OrtCreateEnv(ORT_LOGGING_LEVEL_WARNING, "YOLO", &env);
    OrtSession* session;
    OrtSessionOptions* session_options;
    OrtSessionOptionsCreate(&session_options);
    OrtSessionOptionsSetIntraOpNumThreads(session_options, 1);
    OrtSessionOptionsSetExecutionMode(session_options, ORT_SEQUENTIAL);
    OrtCreateSession(env, model_path, session_options, &session);
    // 2. 准备输入输出
    // ... (获取输入输出名称、形状和类型) ...
    OrtAllocator* allocator;
    OrtCreateAllocator(env, &allocator);
    // 3. 加载并预处理图像
    cv::Mat img = cv::imread(image_path);
    cv::Mat resized_img, blob;
    cv::resize(img, resized_img, cv::Size(input_width, input_height));
    blob = cv::dnn::blobFromImage(resized_img, 1.0/255.0, cv::Size(input_width, input_height), cv::Scalar(0,0,0), true, false, CV_32F);
    // 4. 创建输入 Tensor
    OrtMemoryInfo* memory_info;
    OrtCreateCpuMemoryInfo(ORT_CPU_DEVICE_ID, OrtMemTypeDefault, &memory_info);
    OrtValue* input_tensor = nullptr;
    OrtCreateTensorWithDataAsOrtValue(memory_info, blob.ptr<float>(), blob.total() * blob.elemSize(), input_node_dims, ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &input_tensor);
    // 5. 运行推理
    OrtValue* output_tensor = nullptr;
    session->Run(session, &input_name, &input_tensor, 1, output_names, 1, &output_tensor);
    // 6. 获取输出数据并执行后处理
    float* float_output = nullptr;
    OrtGetTensorMutableData(output_tensor, (void**)&float_output);
    // !!! 核心部分:后处理 !!!
    // 你需要自己编写 C++ 代码来解析 float_output
    // 这包括:
    // - 将输出张量重塑为 [num_boxes, 4 + num_classes] 的格式
    // - 应用置信度阈值过滤
    // - 将边界框坐标从归一化坐标转换回原图坐标
    // - 执行 NMS
    std::vector<Detection> detections = post_process(float_output, img.size());
    // 7. 释放资源
    // ... (释放所有 Ort 指针) ...
}

资源链接:


从零开始编写一个极简版 YOLOv1 (用于学习)

如果你想真正理解 YOLO 的工作原理,而不只是使用一个框架,可以尝试用 C 语言实现一个最原始的 YOLOv1,这个版本没有复杂的锚框和特征金字塔,非常适合学习。

极简 YOLOv1 的逻辑

  • 输入: 一个 S x S x (B*5 + C) 的张量。
    • S x S: 将图像划分为 S x S 的网格。
    • B: 每个网格预测的边界框数量 (YOLOv1 中 B=2)。
    • 5: 每个框的 5 个预测值 x, y, w, h, confidence
    • C: 类别数量。
  • 输出: 经过后处理的边界框列表。
  • 损失函数: 包含定位损失、置信度损失和分类损失。
  • 后处理:
    1. 过滤掉 confidence * class_prob 低于阈值的框。
    2. 对每个类别执行 非极大值抑制

核心后处理 C 语言代码示例

这是实现 YOLO 最关键、也最考验编程能力的部分,下面是一个简化的 NMS 函数框架。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// 定义一个边界框结构体
typedef struct {
    float x, y, w, h; // 中心坐标和宽高
    float confidence;
    int class_id;
} Box;
// 定义一个检测结果,包含框和类别信息
typedef struct {
    Box box;
    int class_id;
    float prob;
} Detection;
// 两个框的交并比
float iou(Box a, Box b) {
    // 计算交集区域
    float inter_x1 = fmax(a.x - a.w/2, b.x - b.w/2);
    float inter_y1 = fmax(a.y - a.h/2, b.y - b.h/2);
    float inter_x2 = fmin(a.x + a.w/2, b.x + b.w/2);
    float inter_y2 = fmin(a.y + a.h/2, b.y + b.h/2);
    if (inter_x2 < inter_x1 || inter_y2 < inter_y1) return 0.0f;
    float inter_area = (inter_x2 - inter_x1) * (inter_y2 - inter_y1);
    float a_area = a.w * a.h;
    float b_area = b.w * b.h;
    return inter_area / (a_area + b_area - inter_area);
}
// 非极大值抑制
void nms(Detection* dets, int total, float threshold, Detection* keep, int* num_out) {
    *num_out = 0;
    // 1. 按 confidence 降序排序
    // ... (实现一个排序算法,如冒泡排序或快速排序) ...
    // 为了示例,我们假设已经排序好了
    // 2. 遍历排序后的检测框
    for (int i = 0; i < total; i++) {
        if (dets[i].prob == 0) continue; // 已被抑制的框
        keep[(*num_out)++] = dets[i];
        // 3. 抑制与当前框 IoU 大于阈值的同类框
        for (int j = i + 1; j < total; j++) {
            if (dets[j].class_id == dets[i].class_id) {
                if (iou(dets[i].box, dets[j].box) > threshold) {
                    dets[j].prob = 0; // 标记为抑制
                }
            }
        }
    }
}
// 主函数调用示例
int main() {
    // 假设这是网络未经处理的原始输出,经过解析后得到的一系列检测框
    Detection detections[] = {
        {{10, 10, 20, 20}, 0.9, 0, 0.9}, // class 0
        {{12, 12, 22, 22}, 0.8, 0, 0.8}, // class 0
        {{100, 100, 30, 30}, 0.95, 1, 0.95}, // class 1
        { {101, 101, 31, 31}, 0.85, 1, 0.85}  // class 1
    };
    int total_dets = 4;
    Detection keep[10];
    int num_keep;
    nms(detections, total_dets, 0.5f, keep, &num_keep);
    printf("NMS Result: %d detections kept.\n", num_keep);
    for (int i = 0; i < num_keep; i++) {
        printf("Det %d: class=%d, conf=%.2f\n", i, keep[i].class_id, keep[i].prob);
    }
    return 0;
}

总结与建议

方法 优点 缺点 适用场景
Darknet 经典、完整、纯C/CUDA、社区成熟 API较旧、难以集成到现代C++项目、依赖OpenCV/CUDA 学习YOLO原理、在Linux/GPU环境下进行快速验证
ONNX/TensorRT 性能极高、模型格式标准、易于部署、与现代C++项目集成 后处理需自己实现、依赖第三方库(如OpenCV) 生产环境的嵌入式、服务器端应用
从零实现 深入理解YOLO细节、无外部依赖 工作量巨大、性能差、难以处理复杂模型 学术研究、计算机视觉教学、面试准备

给你的建议:

  • 如果你是初学者,想快速运行YOLO: 使用 Darknet,按照它的教程走一遍。
  • 如果你想做一个实际的产品或项目: 使用 PyTorch/TensorFlow 训练 + ONNX/TensorRT 推理 的模式,这是业界标准。
  • 如果你想深入理解YOLO的每一个细节: 尝试 从零实现一个YOLOv1,重点攻克后处理部分的代码,这个过程会让你对目标检测有非常深刻的认识。
-- 展开阅读全文 --
头像
织梦如何调用head标签?
« 上一篇 04-04
织梦删除文章失败,原因何在?
下一篇 » 04-04

相关文章

取消
微信二维码
支付宝二维码

目录[+]