
1. 项目概述从单张到批量的效率跃迁在计算机视觉的工程实践中我们常常会遇到这样的场景模型训练好了单张图片的推理测试也跑通了结果看起来不错。但紧接着产品经理或者业务方递过来一个包含几千张甚至上万张图片的文件夹要求你“跑一下看看整体效果”。如果你还在用./darknet detector test ...一条命令一张图地手动操作或者写个蹩脚的 shell 脚本循环调用那效率就太低了不仅耗时耗力输出结果也散落各处难以整理和分析。“YOLOv3_Alexeyab darknet版本批量测试图片并保存”这个项目解决的正是这个从模型验证到批量部署的关键痛点。Alexeyab 版本的 Darknet 是 YOLOv3 的一个非常经典且稳定的实现社区活跃文档齐全。但它的原生测试接口更侧重于单张图片的即时演示。本项目要做的就是基于这个稳固的基石构建一个自动化、可配置的批量处理流水线。核心目标很明确输入一个图片目录自动遍历所有图片用训练好的 YOLOv3 模型进行目标检测并将带有检测框和标签的可视化结果图片以及结构化的检测结果如类别、置信度、坐标有序地保存下来。这不仅仅是运行一个脚本那么简单。它涉及到 Darknet 的 C 代码层面理解、Python 接口的灵活调用、文件系统的批量操作、结果的后处理与组织以及在整个过程中如何保持高效和稳定。对于算法工程师、研究员甚至是需要频繁进行模型效果评估和演示的同学来说掌握这套方法能将你从重复劳动中解放出来把精力更多地投入到模型调优和结果分析上。下面我就结合自己多次在项目中的实践拆解其中的核心思路、技术细节和那些容易踩坑的地方。2. 核心思路与方案选型为什么是 Python C 的组合当我们拿到批量测试的需求时首先会面临方案选型。Darknet 本身是用 C 写的它提供了几种调用方式我们需要选择最合适批量处理的那一种。2.1 可选方案对比方案一Shell 脚本循环调用原生 test 命令这是最直观的想法。写一个 bash 脚本遍历图片目录对每张图片执行类似./darknet detector test cfg/coco.data cfg/yolov3.cfg yolov3.weights -ext_output image.txt result.txt的命令。这个方案看似简单但问题很多每次调用都要重新加载模型和配置文件开销巨大进程频繁启动销毁效率极低输出结果图片和文本的命名和保存需要精巧的文本处理非常容易出错而且无法方便地获取结构化的检测数据供后续分析。方案二修改 Darknet 的 C 源码detector.c中的test_detector函数这是最彻底的方式。直接修改 Darknet 源代码在test_detector函数外层加一个循环读取指定目录下的所有图片。这样做效率最高因为模型只需加载一次。但缺点也很明显需要对 Darknet 的代码结构比较熟悉修改和调试 C 代码有一定门槛每次需求变动如更改输出格式、增加后处理都需要重新编译整个项目不够灵活而且“污染”了原版代码不利于后续同步官方更新。方案三使用 Darknet 的 Python 接口如darknet.pyAlexeyab 版本的 Darknet 通常包含一个python/目录里面提供了darknet.py这个文件。它通过 Python 的ctypes库调用编译好的 Darknet 动态链接库如libdarknet.so或darknet.dll。这个方案完美折衷了效率和灵活性在 Python 脚本中我们只需一次加载网络load_network、一次加载元数据load_meta然后就可以在一个循环里对成千上万张图片调用detect_image函数。所有图片处理逻辑、结果保存、数据分析都可以用 Python 轻松实现生态丰富调试方便。2.2 为什么选择方案三经过对比方案三Python接口无疑是实现批量测试的最佳实践。其核心优势在于高效模型和配置仅加载一次内存常驻避免了方案一的重复开销。灵活利用 Python 强大的生态如 OpenCV, NumPy, Pandas可以轻松实现图片读取、结果可视化、数据保存JSON/CSV、生成统计报告等复杂后处理。可维护业务逻辑批量处理流程与核心算法库Darknet分离。Darknet 作为底层引擎保持纯净业务脚本可以随意迭代互不影响。易扩展可以很容易地集成到更复杂的 MLOps 流水线中或与其他 Python 框架如 Flask 构建 API 服务结合。因此本项目的核心思路确定为利用 Darknet 官方提供的 Python 接口编写一个脚本实现图片目录的遍历、模型的批量推理、结果的可视化与结构化保存。接下来我们就深入细节看看具体怎么做。3. 环境准备与 Darknet 编译要点工欲善其事必先利其器。在开始写批量测试脚本之前确保你的 Darknet 环境是正确的。3.1 获取与编译 Alexeyab Darknet首先从 Alexeyab 的 GitHub 仓库克隆代码。建议使用git命令方便后续更新。git clone https://github.com/AlexeyAB/darknet.git cd darknet接下来是编译。编译选项直接影响我们能否成功使用 Python 接口。修改 Makefile这是最关键的一步。打开Makefile找到以下几行并进行修改GPU1 # 如果你有 NVIDIA GPU 并安装了 CUDA设为 1 以加速否则为 0 CUDNN1 # 如果安装了 cuDNN设为 1 以获得进一步加速 CUDNN_HALF0 # 半精度根据你的硬件和需求设置 OPENCV1 # 强烈建议设为 1。这样 Darknet 可以原生读写图片并且 Python 接口也能直接使用 OpenCV 的 Mat 对象方便很多。 LIBSO1 # 这个必须设为 1这是生成动态链接库libdarknet.so的关键Python 的 ctypes 需要它。其他如ARCHGPU架构等选项请根据你的实际硬件环境调整。对于大多数现代 GPUARCHcompute_XX,sm_XX中的XX需要匹配你的显卡算力。执行编译make -j$(nproc) # 利用多核加速编译编译成功后你会在目录下看到darknet可执行文件、libdarknet.so动态库关键等文件。3.2 Python 环境配置Darknet 的 Python 接口对 Python 版本有一定要求通常 Python 3.6 都可以。你需要安装必要的依赖pip install opencv-python numpyopencv-python用于图片的读取、处理和可视化画框。numpy是 Python 科学计算的基础Darknet 接口返回的检测框数据就是 numpy 数组。关键检查点确保python/目录下的darknet.py文件存在。这个文件定义了 Python 如何通过ctypes调用libdarknet.so。有时你可能需要根据你的系统路径修改darknet.py开头加载动态库的那一行lib CDLL(/path/to/your/darknet/libdarknet.so, RTLD_GLOBAL)如果libdarknet.so就在你的当前工作目录或者已经将其路径加入LD_LIBRARY_PATHLinux环境变量那么原版的./libdarknet.so通常也能工作。4. 批量测试脚本核心实现解析环境就绪后我们来编写核心的批量处理脚本。我将它命名为batch_detect.py。这个脚本将包含以下几个核心函数和逻辑。4.1 脚本整体结构import argparse import os import cv2 import numpy as np from darknet import * def load_network(config_path, data_path, weights_path): 加载网络、元数据和类别名 pass def image_detection(image_path, network, class_names, thresh): 对单张图片进行检测返回原始检测结果和带框的图片 pass def process_directory(input_dir, output_img_dir, output_txt_dir, network, class_names, thresh): 遍历目录处理所有图片 pass if __name__ __main__: # 解析命令行参数 # 调用 process_directory 函数 pass4.2 关键函数实现细节4.2.1 网络加载函数load_network函数负责初始化 Darknet。这里有个细节需要注意Darknet 的 Python 接口通常需要三个文件.cfg文件网络结构配置文件如yolov3.cfg。.data文件数据配置文件其中最重要的是names字段指定的类别名文件路径如coco.names。.weights文件训练好的模型权重。def load_network(config_path, data_path, weights_path): 加载网络模型和元数据 Args: config_path: .cfg 文件路径 data_path: .data 文件路径 weights_path: .weights 文件路径 Returns: network: 加载的网络 class_names: 类别名称列表 class_colors: 为每个类别生成的颜色列表用于画框 # 1. 解析 .data 文件获取类别名文件路径 # 这里简单处理直接读取 .data 文件找到 names 开头的行 with open(data_path, r) as f: for line in f.readlines(): if line.startswith(names): names_path line.strip().split()[1] break # 2. 读取类别名 with open(names_path, r) as f: class_names [line.strip() for line in f.readlines()] # 3. 为每个类别随机生成一个颜色BGR格式 import random random.seed(42) # 固定随机种子确保每次运行颜色一致 class_colors [] for _ in range(len(class_names)): class_colors.append((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))) # 4. 加载网络 network load_network_custom(config_path.encode(utf-8), weights_path.encode(utf-8), 0, 1) # 注意load_network_custom 的第三个参数是 batch size批量测试时设为1即可第四个参数是是否初始化网络通常为1。 # 5. 加载元数据主要包含类别数等信息 meta load_meta(data_path.encode(utf-8)) return network, meta, class_names, class_colors注意load_network_custom和load_meta是darknet.py中定义的函数它们内部会调用 C 库。传入路径时需要将 Python 字符串编码为字节.encode(utf-8)这是ctypes的常见要求。4.2.2 单张图片检测函数这是核心的检测单元。它接收图片路径调用 Darknet 进行推理并返回结构化的结果。def image_detection(image_path, network, meta, class_names, class_colors, thresh0.25): 检测单张图片 Args: image_path: 输入图片路径 network: 已加载的网络对象 meta: 元数据对象 class_names: 类别名列表 class_colors: 类别颜色列表 thresh: 置信度阈值 Returns: detections: 检测结果列表每个元素为 (类别名, 置信度, (x, y, w, h)) image_with_boxes: 画好检测框的 OpenCV 图片 (numpy array) # 1. 使用 OpenCV 读取图片 img cv2.imread(image_path) if img is None: print(f警告无法读取图片 {image_path}) return [], None # 2. 获取图片原始尺寸并计算缩放比例Darknet 要求输入为正方形 h, w, _ img.shape # Darknet 内部会进行 resize我们需要知道这个变换以将预测框坐标映射回原图 # 首先获取网络输入的尺寸在 .cfg 文件中定义的 width 和 height net_width network_width(network) net_height network_height(network) # 3. 将 OpenCV 图片转换为 Darknet 需要的 IMAGE 格式 darknet_image make_image(net_width, net_height, 3) # 将 img 从 OpenCV 的 BGR 格式转换为 RGB并 resize 到网络输入尺寸 img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img_resized cv2.resize(img_rgb, (net_width, net_height), interpolationcv2.INTER_LINEAR) # 将 numpy array 的数据拷贝到 darknet_image 结构体中 img_resized img_resized.transpose(2, 0, 1) # 从 (H, W, C) 转为 (C, H, W) copy_image_from_bytes(darknet_image, img_resized.tobytes()) # 4. 进行检测 # num_detections 是一个指针返回检测到的框数量 num_detections c_int(0) pnum_detections pointer(num_detections) # detect_image 返回一个指向 DETECTION 结构体数组的指针 detections_ptr detect_image(network, meta, darknet_image, threshthresh, hier_thresh.5, nms.45, pnum_detectionspnum_detections) # 5. 解析检测结果 detections [] num num_detections.value if num 0: # 将指针转换为 Python 可操作的结构体数组 detections_array (DETECTION * num).from_address(addressof(detections_ptr.contents)) for detection in detections_array: # 获取类别ID和置信度 class_id detection.best_class_idx confidence detection.prob[class_id] if confidence thresh: continue # 虽然 detect_image 已经根据 thresh 过滤但这里可以再加一层保险 # 获取边界框坐标相对于网络输入尺寸的归一化坐标 box detection.bbox center_x box.x # 中心点 x center_y box.y # 中心点 y width box.w # 宽度 height box.h # 高度 # 6. 关键步骤将归一化坐标转换回原图坐标 # 首先将归一化坐标转换为相对于网络输入尺寸的绝对像素坐标 abs_center_x int(center_x * net_width) abs_center_y int(center_y * net_height) abs_width int(width * net_width) abs_height int(height * net_height) # 然后计算边界框的左上角和右下角坐标相对于网络输入尺寸 left abs_center_x - abs_width // 2 right abs_center_x abs_width // 2 top abs_center_y - abs_height // 2 bottom abs_center_y abs_height // 2 # 最后将这些坐标映射回原始图片尺寸 # 这里假设网络在 resize 时是保持宽高比并填充黑边的letterbox方式这是 YOLO 的常见做法。 # 我们需要计算缩放因子和填充偏移量。 # 更精确的做法是使用 letterbox 函数逆变换这里提供一个简化版本假设直接拉伸不 letterbox scale min(net_width/w, net_height/h) new_w int(w * scale) new_h int(h * scale) pad_x (net_width - new_w) // 2 pad_y (net_height - new_h) // 2 # 逆变换坐标 left int((left - pad_x) / scale) right int((right - pad_x) / scale) top int((top - pad_y) / scale) bottom int((bottom - pad_y) / scale) # 确保坐标不超出图片边界 left max(0, left) top max(0, top) right min(w, right) bottom min(h, bottom) # 7. 保存结果 detections.append((class_names[class_id], float(confidence), (left, top, right, bottom))) # 8. 在原图上画框和标签 color class_colors[class_id] cv2.rectangle(img, (left, top), (right, bottom), color, 2) label f{class_names[class_id]}: {confidence:.2f} # 计算文字背景框的大小 (label_width, label_height), baseline cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2) cv2.rectangle(img, (left, top - label_height - baseline - 5), (left label_width, top), color, -1) cv2.putText(img, label, (left, top - baseline - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2) # 9. 释放 Darknet 图片内存 free_image(darknet_image) # 注意detect_image 返回的指针指向的内存由 Darknet 内部管理通常不需要手动释放除非接口文档特别说明。 return detections, img这个函数是批量处理的核心包含了坐标转换这个最容易出错的环节。上面的坐标逆变换采用了letterbox方式的近似如果你的 Darknet 在编译时或测试时使用了不同的预处理方式如直接拉伸需要相应调整转换逻辑。一个更稳健的方法是在训练和测试时使用相同的预处理代码并记录下缩放和填充参数。4.2.3 目录处理与主函数最后我们将上述功能整合实现目录遍历和结果保存。def process_directory(input_dir, output_img_dir, output_txt_dir, network, meta, class_names, class_colors, thresh0.25, ext_list[.jpg, .jpeg, .png, .bmp]): 批量处理目录下的所有图片 Args: input_dir: 输入图片目录 output_img_dir: 输出带检测框图片的目录 output_txt_dir: 输出检测结果文本文件的目录 network, meta, class_names, class_colors: 加载好的模型和元数据 thresh: 置信度阈值 ext_list: 支持的图片后缀名列表 # 创建输出目录 os.makedirs(output_img_dir, exist_okTrue) os.makedirs(output_txt_dir, exist_okTrue) # 遍历输入目录 for root, dirs, files in os.walk(input_dir): for file in files: # 检查文件后缀 if any(file.lower().endswith(ext) for ext in ext_list): input_path os.path.join(root, file) # 构建相对路径用于保持输出目录结构 rel_path os.path.relpath(input_path, input_dir) # 构建输出路径不带后缀的文件名 file_base os.path.splitext(rel_path)[0] output_img_path os.path.join(output_img_dir, file_base _det.jpg) output_txt_path os.path.join(output_txt_dir, file_base .txt) # 确保输出子目录存在 os.makedirs(os.path.dirname(output_img_path), exist_okTrue) os.makedirs(os.path.dirname(output_txt_path), exist_okTrue) print(f处理中: {input_path}) detections, image_with_boxes image_detection(input_path, network, meta, class_names, class_colors, thresh) if image_with_boxes is not None: # 保存带检测框的图片 cv2.imwrite(output_img_path, image_with_boxes) print(f 图片已保存至: {output_img_path}) # 保存检测结果到文本文件 (格式: class_name confidence x1 y1 x2 y2) with open(output_txt_path, w) as f: for class_name, confidence, (x1, y1, x2, y2) in detections: # 保存归一化坐标或绝对坐标这里保存绝对坐标方便查看。 # 如果需要用于 mAP 计算可能需要保存归一化坐标 (相对于图片宽高)。 f.write(f{class_name} {confidence:.4f} {x1} {y1} {x2} {y2}\n) print(f 检测结果已保存至: {output_txt_path}) print(f 检测到 {len(detections)} 个目标) else: print(f 处理失败: {input_path}) if __name__ __main__: parser argparse.ArgumentParser(descriptionYOLOv3 Darknet 批量图片检测) parser.add_argument(--config, typestr, requiredTrue, help模型配置文件路径 (.cfg)) parser.add_argument(--data, typestr, requiredTrue, help数据配置文件路径 (.data)) parser.add_argument(--weights, typestr, requiredTrue, help模型权重文件路径 (.weights)) parser.add_argument(--input_dir, typestr, requiredTrue, help输入图片目录) parser.add_argument(--output_img_dir, typestr, default./output/images, help输出图片目录 (默认: ./output/images)) parser.add_argument(--output_txt_dir, typestr, default./output/labels, help输出标签目录 (默认: ./output/labels)) parser.add_argument(--thresh, typefloat, default0.25, help检测置信度阈值 (默认: 0.25)) args parser.parse_args() # 加载网络 print(正在加载网络模型...) network, meta, class_names, class_colors load_network(args.config, args.data, args.weights) print(f模型加载完毕共 {len(class_names)} 个类别。) # 开始批量处理 process_directory(args.input_dir, args.output_img_dir, args.output_txt_dir, network, meta, class_names, class_colors, args.thresh) print(批量处理完成)5. 运行脚本与参数详解保存上述代码为batch_detect.py。你可以通过命令行来运行它参数非常灵活。一个典型的运行命令如下python batch_detect.py \ --config cfg/yolov3.cfg \ --data cfg/coco.data \ --weights yolov3.weights \ --input_dir /path/to/your/images \ --output_img_dir ./detection_results/images \ --output_txt_dir ./detection_results/labels \ --thresh 0.5参数解析--config: 指定网络结构配置文件。例如yolov3.cfg,yolov3-tiny.cfg或者你自己修改的 cfg 文件。--data: 指定数据配置文件。这个文件里最重要的就是names指向的类别名文件。例如coco.data的内容可能包含namesdata/coco.names。--weights: 训练好的模型权重文件。--input_dir: 你的图片文件夹路径。脚本会递归遍历这个目录下的所有子文件夹。--output_img_dir: 保存画了检测框的结果图片的目录。脚本会保持输入目录的文件夹结构。--output_txt_dir: 保存文本格式检测结果的目录。每张图片对应一个.txt文件每行格式为类别名 置信度 x1 y1 x2 y2。--thresh: 置信度阈值。低于此值的检测框将被过滤掉。根据你的模型性能和需求调整值越高检测框越少但更可信。运行后你会在指定的输出目录下看到结构化的结果。图片文件以_det.jpg结尾便于区分原图。文本文件则包含了所有检测目标的详细信息方便你后续进行统计分析比如用 Python 的 Pandas 库。6. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到一些问题。下面是我踩过的一些坑和解决方案。6.1 动态库加载失败问题运行脚本时在import darknet或load_network阶段报错如OSError: libdarknet.so: cannot open shared object file: No such file or directory。原因系统找不到libdarknet.so文件。解决确认编译成功在 Darknet 根目录执行ls -la libdarknet.so确认文件存在。设置环境变量Linux/Macexport LD_LIBRARY_PATH/path/to/your/darknet:$LD_LIBRARY_PATH然后在这个终端里运行你的 Python 脚本。或者将这一行添加到你的~/.bashrc中。修改darknet.py直接修改python/darknet.py开头的lib CDLL(...)这一行给出libdarknet.so的绝对路径。拷贝库文件将libdarknet.so拷贝到系统库路径下不推荐可能引起冲突。6.2 检测框坐标错位问题保存的结果图片上检测框的位置明显不对要么偏移要么大小不符。原因坐标转换逻辑错误。这是批量测试中最常见的坑。Darknet 内部对输入图片进行了预处理resize, letterbox预测的坐标是相对于预处理后图片的。你需要将其精确地映射回原始图片。解决确认预处理方式首先你要明确你的模型训练和测试时使用的预处理方式。Alexeyab Darknet 默认在detector.c的test_detector函数中使用了letterbox方式保持宽高比填充黑边。我们的脚本中也假设了这种方式。使用一致的预处理代码最可靠的方法是在你的 Python 脚本中使用与 Darknet C 代码中完全相同的letterbox函数进行图片预处理并记录下缩放因子 (scale) 和填充像素 (pad)。然后在转换坐标时使用这些记录的参数进行逆运算。你可以从 Darknet 源码的src/image.c中找到letterbox_image函数并在 Python 中实现它。简化验证为了快速验证你可以先用一张图片用 Darknet 原生的./darknet detector test ...命令测试保存结果。然后用你的脚本测试同一张图片对比两个结果图片上框的位置。如果一致说明坐标转换正确如果不一致就需要仔细调试你的转换代码。6.3 内存泄漏与性能优化问题处理大量图片如上万张时程序内存占用越来越大甚至崩溃。原因在循环中Darknet 的detect_image函数或我们创建的darknet_image可能没有正确释放内存。解决确保释放darknet_image在我们的image_detection函数中已经使用了free_image(darknet_image)。这是正确的。关于detect_image返回的指针根据 Darknet 源码和社区讨论detect_image返回的检测结果指针指向的内存在下次调用detect_image时会被覆盖或释放。通常不需要手动释放。但如果你在处理完一批图片后想主动释放可以尝试调用 C 库中的free_detections函数如果暴露了的话。更稳妥的做法是定期重启脚本进程比如每处理1000张图片但这会影响效率。性能优化GPU 编译确保编译时开启了GPU1和CUDNN1这是最大的性能提升点。批处理原生的detect_image一次处理一张图。Darknet 支持批量推理在.cfg文件中设置batch和subdivisions但 Python 接口通常不直接暴露批量检测函数。如果需要极致性能可以考虑修改darknet.py或直接使用 C 代码进行批量推理。I/O 优化图片的读取和保存cv2.imread,cv2.imwrite也可能是瓶颈尤其是图片很大时。可以考虑使用多线程或异步 I/O将读取下一张图片和保存上一张结果与当前推理计算重叠起来。6.4 类别名或颜色不对问题画出来的框标签不是英文或者颜色混乱。原因.names文件读取错误或者颜色生成随机种子不固定。解决检查.data文件中names指向的路径是否正确以及该.names文件内容是否与模型匹配。在load_network函数中我们通过random.seed(42)固定了随机种子确保每次运行为同一类别生成的颜色是一致的。如果你希望自定义颜色可以直接硬编码一个颜色列表。6.5 输出结果的组织问题输出文件杂乱难以与原图对应。解决我们的脚本使用了os.path.relpath和os.path.splitext来保持输入目录的原始结构。例如输入目录结构为input/a/1.jpg,input/b/2.jpg输出到output/images后结构会是output/images/a/1_det.jpg和output/images/b/2_det.jpg。文本结果同理。这样在分析大量数据时可以很容易地追溯到原图。7. 进阶扩展与实用技巧掌握了基础批量测试后你可以根据需求进行很多扩展让这个工具更加强大。7.1 结果汇总与统计报告批量测试后你可能有这些需求“哪个类别检测得最多”、“平均置信度是多少”、“有多少张图片没有检测到任何目标”。我们可以轻松地扩展脚本在process_directory函数中收集全局统计信息。import json from collections import defaultdict, Counter def process_directory_with_stats(input_dir, output_img_dir, output_txt_dir, network, meta, class_names, class_colors, thresh0.25): 带统计功能的批量处理 stats { total_images: 0, images_with_detections: 0, total_detections: 0, detections_per_class: defaultdict(int), confidence_sum_per_class: defaultdict(float), failed_images: [] } os.makedirs(output_img_dir, exist_okTrue) os.makedirs(output_txt_dir, exist_okTrue) for root, dirs, files in os.walk(input_dir): for file in files: if file.lower().endswith((.jpg, .jpeg, .png, .bmp)): stats[total_images] 1 input_path os.path.join(root, file) rel_path os.path.relpath(input_path, input_dir) file_base os.path.splitext(rel_path)[0] output_img_path os.path.join(output_img_dir, file_base _det.jpg) output_txt_path os.path.join(output_txt_dir, file_base .txt) os.makedirs(os.path.dirname(output_img_path), exist_okTrue) os.makedirs(os.path.dirname(output_txt_path), exist_okTrue) detections, image_with_boxes image_detection(input_path, network, meta, class_names, class_colors, thresh) if image_with_boxes is not None: cv2.imwrite(output_img_path, image_with_boxes) with open(output_txt_path, w) as f: for class_name, confidence, bbox in detections: f.write(f{class_name} {confidence:.4f} {bbox[0]} {bbox[1]} {bbox[2]} {bbox[3]}\n) # 更新统计信息 if detections: stats[images_with_detections] 1 stats[total_detections] len(detections) for class_name, confidence, _ in detections: stats[detections_per_class][class_name] 1 stats[confidence_sum_per_class][class_name] confidence else: stats[failed_images].append(input_path) # 计算平均置信度 stats[avg_confidence_per_class] {} for class_name in stats[confidence_sum_per_class]: if stats[detections_per_class][class_name] 0: stats[avg_confidence_per_class][class_name] stats[confidence_sum_per_class][class_name] / stats[detections_per_class][class_name] # 保存统计报告为 JSON report_path os.path.join(os.path.dirname(output_img_dir), detection_report.json) # 将 defaultdict 转换为普通 dict 以便 JSON 序列化 stats[detections_per_class] dict(stats[detections_per_class]) stats[confidence_sum_per_class] dict(stats[confidence_sum_per_class]) with open(report_path, w) as f: json.dump(stats, f, indent4) print(f统计报告已保存至: {report_path}) return stats这样处理完成后你会得到一个detection_report.json文件里面包含了所有关键统计信息方便你快速评估模型在测试集上的表现。7.2 支持视频流或摄像头输入批量测试脚本稍加修改就可以支持视频文件或摄像头的实时检测。核心是将图片读取循环替换为 OpenCV 的视频帧捕获循环。def detect_video(video_path, output_path, network, meta, class_names, class_colors, thresh0.25): cap cv2.VideoCapture(video_path) # 获取视频参数用于创建 VideoWriter fps int(cap.get(cv2.CAP_PROP_FPS)) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fourcc cv2.VideoWriter_fourcc(*mp4v) # 或 XVID out cv2.VideoWriter(output_path, fourcc, fps, (width, height)) while cap.isOpened(): ret, frame cap.read() if not ret: break # 将 frame (BGR) 转换为 RGB并进行检测 # 注意这里需要将 frame 传递给 image_detection 函数但我们的函数接收的是文件路径。 # 我们需要修改 image_detection 函数使其能接收一个 numpy array 作为输入。 # 我们可以创建一个新的函数 image_detection_from_array。 detections, processed_frame image_detection_from_array(frame, network, meta, class_names, class_colors, thresh) out.write(processed_frame) # 也可以选择显示实时画面 # cv2.imshow(Detection, processed_frame) # if cv2.waitKey(1) 0xFF ord(q): # break cap.release() out.release() cv2.destroyAllWindows()你需要实现image_detection_from_array函数它从内存中的 numpy 数组读取图像而不是从文件。这需要调整image_detection函数中关于图片读取的部分。7.3 集成到 Web 服务或自动化流水线由于核心逻辑已经封装在 Python 函数中你可以很容易地将其集成到 Flask 或 FastAPI 构建的 Web 服务中提供一个图片检测的 API。也可以将其作为 Jenkins 或 GitLab CI/CD 流水线中的一个环节在模型更新后自动对测试集进行批量评估并生成报告。实现批量测试并保存结果是模型部署前至关重要的一步。它不仅是简单的脚本编写更要求你对 Darknet 的运行机制、图片预处理、坐标转换有清晰的理解。通过本文拆解的方案和代码你应该能够搭建起一个稳定高效的批量测试流程。在实际使用中最需要花时间调试的往往是坐标转换部分务必用少量图片进行充分验证。当你的脚本能稳定运行后它将成为你模型迭代过程中一个可靠的效率工具。