【Darknet】Darknet实战

本文详细介绍了使用Darknet进行目标检测的全过程,包括数据集准备(labelimage工具、标注文件转换)、Darknet的安装、配置文件修改、模型训练、GPU使用监控、模型效果评估(loss曲线、IoU、recall、mAP计算)等方面,旨在帮助读者掌握Darknet在实际项目中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

darknet

Darknet is an open source neural network framework written in C and CUDA. It is fast, easy to install, and supports CPU and GPU computation.

—— https://pjreddie.com/darknet/

本文是对使用 darknet 进行目标检测的小结,包括:

  1. 数据集准备:如何使用 labelimage 对数据进行标注,注意事项,文件格式转换
  2. darknet 使用:如何编译和修改配置文件
  3. 模型评估:如何查看 loss、计算 IoU、recall、mAP

数据集准备

如图

大多数情况下,数据集决定了任务的成败。一开始我认为标注数据是一件非常枯燥和乏味的事情,但是当模型指标一直上不去,检测和识别效果也一直不好时,回过头才会发现是因为数据标注的有问题。

这只有自己经历过之后才会有体会,得出这样几条经验:

  1. 保持类间差距大,类内差距小
  2. 一开始先标注少量的图片并训练模型查看效果,根据结果进行调整,否则等到标注了大量图片之后再回过头修改,得不偿失。

labelimage

图片标注使用的是开源的工具 LabelImg,可以查看文档自行编译,下载之后的文件结构如下:

.
├── build-tools
├── CONTRIBUTING.rst
├── data
    |—— predefined_classes.txt
├── labelImg.py
...
  • data/predefined_classes.txt文件配置进行图片的类别

界面展示及注意事项

启动 labelimg 之后的界面如下图所示

labelimg 界面

注意:

  1. 存放图片文件夹和存放标记文件的文件夹需要保持一致
  2. 正确选择标记文件的格式,是需要 yolo 格式还是 xml 格式,默认为 xml 格式
  3. 右侧可以选择默认标签,当密集标注一个类的时候很实用
  4. 打完几张标签之后请确认标签是否正确,再去文件目录下确认以下标记文件是否生成且格式正确

快捷键

+------------+--------------------------------------------+
| Space      | 保存                                        |
+------------+--------------------------------------------+
| w          | 创建矩形框                                   |
+------------+--------------------------------------------+
| d          | 下一张图片                                   |
+------------+--------------------------------------------+
| a          | 上一张图片                                   |
+------------+--------------------------------------------+

最常用的快捷键是上面这 4 个,用好快捷键可以调高打标效率。此外按住ctrl+鼠标滚轮可以调整图片大小,局部放大图片可以提高打标的精准度。

yolo格式和 xml 格式转换

xml2yolo.py

"""
1. 修改 classes 列表中的元素为当前标签列表
2. 修改 list_xml 指向的 xml 文件保存的位置
"""

#import xml.etree.ElementTree as ET
from xml.etree import ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
import glob

classes = ['rabbit']

def convert(size, box):
      dw = 1. / (size[0])
      dh = 1. / (size[1])
      x = (box[0] + box[1]) / 2.0 - 1
      y = (box[2] + box[3]) / 2.0 - 1
      w = box[1] - box[0]
      h = box[3] - box[2]
      x = x * dw
      w = w * dw
      y = y * dh
      h = h * dh
      return (x, y, w, h)


def convert_annotation(xml_file, txt_file):
      in_file = open(xml_file)

      tree = ET.parse(in_file)
      root = tree.getroot()
      size = root.find('size')
      w = int(size.find('width').text)
      h = int(size.find('height').text)

      for obj in root.iter('object'):

            cls = obj.find('name').text
            if cls not in classes:
                  continue
            cls_id = classes.index(cls)
            xmlbox = obj.find('bndbox')
            b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
                 float(xmlbox.find('ymax').text))
            bb = convert((w, h), b)
            txt_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')

list_xml = []
list_xml = glob.glob(r"/file_path/*.xml")

for xml_file in list_xml:
      xml_name = xml_file.split('.')[0]
      txt_file = open('%s.txt' % (xml_name), 'w')
      convert_annotation(xml_file, txt_file)
      txt_file.close()

yolo2xml.py

"""
1. 修改 class_name
2. 修改 src_img_dir/src_txt_dir/src_xml_dir 路径
"""
import os, sys
import glob
from PIL import Image

class_name = ['switch_closed','switch_open','whirled_switch_closed','whirled_switch_open',
        'lifting_switch_closed','lifting_switch_open','closure_switch_closed','closure_switch_open',
        'color_switch01_red','color_switch01_green','color_switch02_red','color_switch02_green',
        'isolation_switch_closed','isolation_switch_open','grounding_knife_switch01_closed','grounding_knife_switch01_open',
        'grounding_knife_switch02_closed','grounding_knife_switch02_open']

def convert_yolo_coordinates_to_voc(x_c_n, y_c_n, width_n, height_n, img_width, img_height):
  ## remove normalization given the size of the image
  x_c = float(x_c_n) * img_width
  y_c = float(y_c_n) * img_height
  width = float(width_n) * img_width
  height = float(height_n) * img_height
  ## compute half width and half height
  half_width = width / 2
  half_height = height / 2
  ## compute left, top, right, bottom
  ## in the official VOC challenge the top-left pixel in the image has coordinates (1;1)
  left = int(x_c - half_width) + 1
  top = int(y_c - half_height) + 1
  right = int(x_c + half_width) + 1
  bottom = int(y_c + half_height) + 1
  return left, top, right, bottom


#  将标注的txt转换为 voc xml

# VEDAI 图像存储位置
src_img_dir = "./switch_mAP"
# VEDAI 图像的 ground truth 的 txt 文件存放位置
src_txt_dir = "./switch_mAP"
src_xml_dir = "./switch_mAP"

img_Lists = glob.glob(src_img_dir + '/*.txt')

# 文件名(含扩展名)
img_basenames = []  # e.g. 100
for item in img_Lists:
      img_basenames.append(os.path.basename(item))

img_names = []  # e.g. 100
for item in img_basenames:
      # 文件名与扩展名
      temp1, temp2 = os.path.splitext(item)
      img_names.append(temp1)

for img in img_names:
      
      im = ""
      suffix = ""
      # 同一个文件夹下存在多种图片格式,在这里加上格式判断
      if os.path.exists(src_img_dir + '/' + img + '.jpg'):
            im = Image.open((src_img_dir + '/' + img + '.jpg'))
            suffix = '.jpg'
      elif os.path.exists(src_img_dir + '/' + img + '.JPG'):
            im = Image.open((src_img_dir + '/' + img + '.JPG'))
            suffix = '.JPG'
      elif os.path.exists(src_img_dir + '/' + img + '.png'):
            im = Image.open((src_img_dir + '/' + img + '.png'))
            suffix = '.png'
      elif os.path.exists(src_img_dir + '/' + img + '.PNG'):
            im = Image.open((src_img_dir + '/' + img + '.PNG'))
            suffix = '.PNG'
      elif os.path.exists(src_img_dir + '/' + img + '.JPEG'):
            im = Image.open((src_img_dir + '/' + img + '.JPEG'))
            suffix = '.JPEG'
      elif os.path.exists(src_img_dir + '/' + img + '.jpeg'):
            im = Image.open((src_img_dir + '/' + img + '.jpeg'))
            suffix = '.jpeg'
                  
      width, height = im.size

      """
      以下部分为 xml 解析
      """

      # open the crospronding txt file
      #  提取每一行并分割
      gt = open(src_txt_dir + '/' + img + '.txt').read().splitlines()

      print(img + '\n')
      # write in xml file
      # os.mknod(src_xml_dir + '/' + img + '.xml')
      xml_file = open((src_xml_dir + '/' + img + '.xml'), 'a')
      xml_file.write('<annotation>\n')
      xml_file.write('    <folder>VOC2007</folder>\n')
      xml_file.write('    <filename>' + str(img) + suffix + '</filename>\n')
      xml_file.write('    <size>\n')
      xml_file.write('        <width>' + str(width) + '</width>\n')
      xml_file.write('        <height>' + str(height) + '</height>\n')
      xml_file.write('        <depth>3</depth>\n')
      xml_file.write('    </size>\n')

      # write the region of image on xml file
      for img_each_label in gt:
            spt = img_each_label.split(' ')  # 这里如果txt里面是以逗号‘,’隔开的,那么就改为spt = img_each_label.split(',')。

            name = class_name[int(spt[0])]
            x_c,y_c,width_n,height_n = spt[1:]
            xmin,ymin,xmax,ymax = convert_yolo_coordinates_to_voc(x_c,y_c,width_n,height_n,width,height)


            xml_file.write('    <object>\n')
            xml_file.write('        <name>' + name + '</name>\n')
            xml_file.write('        <pose>Unspecified</pose>\n')
            xml_file.write('        <truncated>0</truncated>\n')
            xml_file.write('        <difficult>0</difficult>\n')
            xml_file.write('        <bndbox>\n')
            xml_file.write('            <xmin>' + str(xmin) + '</xmin>\n')
            xml_file.write('            <ymin>' + str(ymin) + '</ymin>\n')
            xml_file.write('            <xmax>' + str(xmax) + '</xmax>\n')
            xml_file.write('            <ymax>' + str(ymax) + '</ymax>\n')
            xml_file.write('        </bndbox>\n')
            xml_file.write('    </object>\n')

      xml_file.write('</annotation>')

使用 darknet 训练模型

安装和编译

# 从 Github 下载
git clone https://github.com/pjreddie/darknet

进入darknet目录中,对编译文件Makefile进行如下修改

GPU=1
CUDNN=1
OPENCV=0
OPENMP=0
DEBUG=0

注:

  1. 画图或显示图片等操作需要配置OPENCV并设置为1
  2. 需要多线程相关操作需要将OPENMP设置为1

修改完成保存并执行make命令。

目录结构介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── backup
├── cfg
    |—— voc.data
    |—— yolov3-voc.cfg
├── darknet
├── data
    |—— voc.names
		|—— train.txt
    |—— val.txt
    |—— test.txt
├── scripts
...

编译完成之后,我们关注目录中的这几个文件和文件夹。

  1. backup 存放训练出来的权值文件
  2. cfg 保存配置文件,其中两个文件,在后面介绍
  3. darknet 是可执行文件
  4. scripts 下是一些脚本
  5. data/voc.names 存放类别标签
  6. train.txt 保存用于训练的图片全路径(自建,位置无特殊要求)
  7. val.txt 保存用于校正的图片全路径(自建,位置无特殊要求)
  8. test.txt 保存用于校正的图片全路径(自建,位置无特殊要求)

注: train.txt, val.txt, test.txt 中图片数量比例建议为 8:1:1

修改配置文件

类别文件 voc.names

修改data/voc.names,将我们的类别标签写这个文件,比如说有以下 5 类

dog
cat
magpie
pigeon
nest

注意:

  1. 类别标签要与训练集包含的图片类别一一对应,训练集中有以上 5 类则voc.names包含以上 5 类
  2. 类别标签需要连续,如果不连续就必须要修改类别标签改成连续的

配置 voc.data

修改cfg/voc.data文件

classes= class_number
train  = /path/train.txt
valid  = /path/val.txt
names = data/voc.names
backup = backup
  1. classes 配置类别数量,与上一步voc.names中类别数量一致
  2. train 配置为目录结构一章介绍的train.txt文件的位置
  3. vaild 配置为目录结构一章介绍的val.txt文件的位置
  4. names 配置为voc.names位置(默认不变即可)
  5. backup 配置为权值文件的位置(默认不变即可)

配置 yolov3-voc.cfg

在文件开头位置

[net]
# Testing
# batch=1 # 测试时开启,训练时关闭
# subdivisions=1 # 测试时开启,训练时关闭
# Training
batch=6 # 训练时开启,测试时关闭
subdivisions=2 # 训练时开启,测试时关闭 
...

learning_rate=0.0001 # 学习率,可以调整得小一点
burn_in=1000
max_batches = 50200
policy=steps
steps=40000,45000
scales=.1,.1
  1. batch 和 subdivisions 在测试和训练的时候请按照上面注释开启或者关闭
  2. batch 一批处理几张图片,没有超过显存的情况下越大越好
  3. subdivisions 表示在 batch 中再划分的数量

当前配置的意思是每轮迭代从所有训练集中抽取 6 张图片,这 6 张样本图片又被分成 2 次,每次 3 张送入到网络参与训练。

接着在文件中搜索yolo,会有三条结果。每个yolo上下都要修改filtersclasses,总共需要修改3 组共 6 个字段

[convolutional]
size=1
stride=1
pad=1
filters=30 # 3*(classes+5)
activation=linear

[yolo]
mask = 6,7,8
anchors = 10,13,  16,30,  33,23,  30,61,  62,45,  59,119,  116,90,  156,198,  373,326
classes=5 # 类别(classes)数量
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1

训练

方法一

./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74 -gpus 0

方法二

nohup ./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74 -gpus 0 >train.log 2>&1 &

我们需要保留训练的日志,所以用方法二更好。

查看 GPU 使用情况

我们可以使用nvidia-smi来查看 gpu 的使用情况,注意先查看 gpu 的使用情况。

Fri Mar 29 16:33:31 2019       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 410.78       Driver Version: 410.78       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 108...  Off  | 00000000:01:00.0  On |                  N/A |
| 59%   87C    P2   221W / 250W |   4874MiB / 11175MiB |     98%      Default |
+-------------------------------+----------------------+----------------------+
|   1  GeForce GTX 108...  Off  | 00000000:02:00.0 Off |                  N/A |
| 24%   42C    P8    16W / 250W |   5107MiB / 11178MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值