悬赏40 积分
已完成
这是原本voc_ann
otation文件下,关于边界框的代码。
完整voc_annotation如下:
import os
import random
import xml.etree.ElementTree as ET
import numpy as np
from utils.utils import get_classes
#--------------------------------------------------------------------------------------------------------------------------------#
# annotation_mode 用于指定该文件运行时计算的内容
# annotation_mode 为 0 代表整个标签处理过程 , 包括获得 VOCdevkit/VOC2007/ImageSets 里面的 txt 以及训练用的 2007_train.txt 、 2007_val.txt
# annotation_mode 为 1 代表获得 VOCdevkit/VOC2007/ImageSets 里面的 txt
# annotation_mode 为 2 代表获得训练用的 2007_train.txt 、 2007_val.txt
#--------------------------------------------------------------------------------------------------------------------------------#
annotation_mode = 2
#-------------------------------------------------------------------#
# 必须要修改 , 用于生成 2007_train.txt 、 2007_val.txt 的目标信息
# 与训练和预测所用的 classes_path 一致即可
# 如果生成的 2007_train.txt 里面没有目标信息
# 那么就是因为 classes 没有设定正确
# 仅在 annotation_mode 为 0 和 2 的时候有效
#-------------------------------------------------------------------#
classes_path = "/opt/data/private/wym/Remote_Sense/faster-rcnn-pytorch-master/model_data/RS_classes.txt"
#--------------------------------------------------------------------------------------------------------------------------------#
# trainval_percent 用于指定 ( 训练集 + 验证集 ) 与测试集的比例 , 默认情况下 ( 训练集 + 验证集 ): 测试集 = 9:1
# train_percent 用于指定 ( 训练集 + 验证集 ) 中训练集与验证集的比例 , 默认情况下 训练集 : 验证集 = 9:1
# 仅在 annotation_mode 为 0 和 1 的时候有效
#--------------------------------------------------------------------------------------------------------------------------------#
trainval_percent = 0.9
train_percent = 0.9
#-------------------------------------------------------#
# 指向 VOC 数据集所在的文件夹
# 默认指向根目录下的 VOC 数据集
#-------------------------------------------------------#
VOCdevkit_path = "/opt/data/private/wym/Remote_Sense/faster-rcnn-pytorch-master/HRSC2016"
VOCdevkit_sets = [('2007' , 'train' ), ('2007' , 'val' )]
classes, _ = get_classes(classes_path)
#-------------------------------------------------------#
# 统计目标数量
#-------------------------------------------------------#
photo_nums = np.zeros(len (VOCdevkit_sets))
nums = np.zeros(len (classes))
def convert_annotation (year, image_id, list_file):
in_file = open (os.path.join(VOCdevkit_path, 'VOC%s/Train/Annotations/%s.xml' %(year, image_id)), encoding ='utf-8' )
tree=ET.parse(in_file)
root = tree.getroot()
import numpy as np
import math
for obj in root.iter('HRSC_Object' ):
difficult = 0
if obj.find('difficult' ) is not None :
difficult = obj.find('difficult' ).text
cls = obj.find('Class_ID' ).text
if cls not in classes or int (difficult) == 1 :
continue
cls_id = classes.index(cls)
# 获取轮船的中心坐标、宽、高和角度
cx = float (obj.find('mbox_cx' ).text)
cy = float (obj.find('mbox_cy' ).text)
w = float (obj.find('mbox_w' ).text)
h = float (obj.find('mbox_h' ).text)
angle = float (obj.find('mbox_ang' ).text)
# 计算轮船的四个角点
angle_rad = math.radians(angle) # 将角度转化为弧度
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
# 计算四个角的坐标
x1 = cx - w / 2 * cos_a - h / 2 * sin_a
y1 = cy + w / 2 * sin_a - h / 2 * cos_a
x2 = cx + w / 2 * cos_a - h / 2 * sin_a
y2 = cy - w / 2 * sin_a - h / 2 * cos_a
x3 = cx + w / 2 * cos_a + h / 2 * sin_a
y3 = cy - w / 2 * sin_a + h / 2 * cos_a
x4 = cx - w / 2 * cos_a + h / 2 * sin_a
y4 = cy + w / 2 * sin_a + h / 2 * cos_a
# 将计算得到的点转换为整型,存储坐标
b = (int (x1), int (y1), int (x2), int (y2), int (x3), int (y3), int (x4), int (y4))
list_file.write(" " + "," .join([str (a) for a in b]) + ',' + str (cls_id))
nums[classes.index(cls)] += 1
# for obj in root.iter('HRSC_Object'):
# difficult = 0
# if obj.find('difficult')!=None:
# difficult = obj.find('difficult').text
# cls = obj.find('Class_ID').text
# if cls not in classes or int(difficult)==1:
# continue
# cls_id = classes.index(cls)
# # xmlbox = obj.find('bndbox')
# b = (int(float(obj.find('box_xmin').text)), int(float(obj.find('box_ymin').text)),
# int(float(obj.find('box_xmax').text)), int(float(obj.find('box_ymax').text)))
# # b = (int(float(obj.find('mbox_cx').text)), int(float(obj.find('mbox_cy').text)),
# int(float(obj.find('mbox_w').text)), int(float(obj.find('mbox_h').text)), int(float(obj.find('mbox_ang').text)))
# list_file.write(" " + ",".join([str(a) for a in b]) + ',' + str(cls_id))
#
# nums[classes.index(cls)] = nums[classes.index(cls)] + 1
if __name__ == "__main__" :
random.seed(0 )
if annotation_mode == 0 or annotation_mode == 1 :
print ("Generate txt in ImageSets." )
xmlfilepath = os.path.join(VOCdevkit_path, 'VOC2007/Annotations' )
saveBasePath = os.path.join(VOCdevkit_path, 'VOC2007/ImageSets/Main' )
temp_xml = os.listdir(xmlfilepath)
total_xml = []
for xml in temp_xml:
if xml.endswith(".xml" ):
total_xml.append(xml)
num = len (total_xml)
list = range (num)
tv = int (num*trainval_percent)
tr = int (tv*train_percent)
trainval= random.sample(list,tv)
train = random.sample(trainval,tr)
print ("train and val size" ,tv)
print ("train size" ,tr)
ftrainval = open (os.path.join(saveBasePath,'trainval.txt' ), 'w' )
ftest = open (os.path.join(saveBasePath,'test.txt' ), 'w' )
ftrain = open (os.path.join(saveBasePath,'train.txt' ), 'w' )
fval = open (os.path.join(saveBasePath,'val.txt' ), 'w' )
for i in list:
name=total_xml[:-4 ]+' \n '
if i in trainval:
ftrainval.write(name)
if i in train:
ftrain.write(name)
else :
fval.write(name)
else :
ftest.write(name)
ftrainval.close()
ftrain.close()
fval.close()
ftest.close()
print ("Generate txt in ImageSets done." )
if annotation_mode == 0 or annotation_mode == 2 :
print ("Generate 2007_train.txt and 2007_val.txt for train." )
type_index = 0
for year, image_set in VOCdevkit_sets:
image_ids = open (os.path.join(VOCdevkit_path, 'VOC%s/ImageSets/Main/%s.txt' %(year, image_set)), encoding ='utf-8' ).read().strip().split()
list_file = open ('%s_%s.txt' %(year, image_set), 'w' , encoding ='utf-8' )
for image_id in image_ids:
list_file.write('%s/VOC%s/Train/AllImages/%s.jpg' %(os.path.abspath(VOCdevkit_path), year, image_id))
convert_annotation(year, image_id, list_file)
list_file.write(' \n ' )
photo_nums[type_index] = len (image_ids)
type_index += 1
list_file.close()
print ("Generate 2007_train.txt and 2007_val.txt for train done." )
def printTable (List1, List2):
for i in range (len (List1[0 ])):
print ("|" , end =' ' )
for j in range (len (List1)):
print (List1[j].rjust(int (List2[j])), end =' ' )
print ("|" , end =' ' )
print ()
str_nums = [str (int (x)) for x in nums]
tableData = [
classes, str_nums
]
colWidths = [0 ]*len (tableData)
len1 = 0
for i in range (len (tableData)):
for j in range (len (tableData)):
if len (tableData[j]) > colWidths:
colWidths = len (tableData[j])
printTable(tableData, colWidths)
if photo_nums[0 ] <= 500 :
print (" 训练集数量小于 500, 属于较小的数据量 , 请注意设置较大的训练世代 (Epoch) 以满足足够的梯度下降次数 (Step) 。 " )
if np.sum(nums) == 0 :
print (" 在数据集中并未获得任何目标 , 请注意修改 classes_path 对应自己的数据集 , 并且保证标签名字正确 , 否则训练将会没有任何效果! " )
print (" 在数据集中并未获得任何目标 , 请注意修改 classes_path 对应自己的数据集 , 并且保证标签名字正确 , 否则训练将会没有任何效果! " )
print (" 在数据集中并未获得任何目标 , 请注意修改 classes_path 对应自己的数据集 , 并且保证标签名字正确 , 否则训练将会没有任何效果! " )
print (" (重要的事情说三遍)。 " )
训练代码(train):
#-------------------------------------#
# 对数据集进行训练
#-------------------------------------#
import os
import datetime
import numpy as np
import torch
import torch.backends.cudnn as cudnn
import torch.optim as optim
from torch.utils.data import DataLoader
from nets.frcnn import FasterRCNN
from nets.frcnn_training import (FasterRCNNTrainer, get_lr_scheduler,
set_optimizer_lr, weights_init)
from utils.callbacks import EvalCallback, LossHistory
from utils.dataloader import FRCNNDataset, frcnn_dataset_collate
from utils.utils import get_classes, show_config
from utils.utils_fit import fit_one_epoch
'''
训练自己的目标检测模型一定需要注意以下几点:
1 、训练前仔细检查自己的格式是否满足要求 , 该库要求数据集格式为 VOC 格式 , 需要准备好的内容有输入图片和标签
输入图片为 .jpg 图片 , 无需固定大小 , 传入训练前会自动进行 resize 。
灰度图会自动转成 RGB 图片进行训练 , 无需自己修改。
输入图片如果后缀非 jpg, 需要自己批量转成 jpg 后再开始训练。
标签为 .xml 格式 , 文件中会有需要检测的目标信息 , 标签文件和输入图片文件相对应。
2 、损失值的大小用于判断是否收敛 , 比较重要的是有收敛的趋势 , 即验证集损失不断下降 , 如果验证集损失基本上不改变的话 , 模型基本上就收敛了。
损失值的具体大小并没有什么意义 , 大和小只在于损失的计算方式 , 并不是接近于 0 才好。如果想要让损失好看点 , 可以直接到对应的损失函数里面除上 10000 。
训练过程中的损失值会保存在 logs 文件夹下的 loss_%Y_%m_%d_%H_%M_%S 文件夹中
3 、训练好的权值文件保存在 logs 文件夹中 , 每个训练世代 (Epoch) 包含若干训练步长 (Step), 每个训练步长 (Step) 进行一次梯度下降。
如果只是训练了几个 Step 是不会保存的 ,Epoch 和 Step 的概念要捋清楚一下。
'''
if __name__ == "__main__" :
#-------------------------------#
# 是否使用 Cuda
# 没有 GPU 可以设置成 False
#-------------------------------#
Cuda = True
#---------------------------------------------------------------------#
# train_gpu 训练用到的 GPU
# 默认为第一张卡、双卡为 [0, 1] 、三卡为 [0, 1, 2]
# 在使用多 GPU 时 , 每个卡上的 batch 为总 batch 除以卡的数量。
#---------------------------------------------------------------------#
train_gpu = [0 ]
#---------------------------------------------------------------------#
# fp16 是否使用混合精度训练
# 可减少约一半的显存、需要 pytorch1.7.1 以上
#---------------------------------------------------------------------#
fp16 = True
#---------------------------------------------------------------------#
# classes_path 指向 model_data 下的 txt, 与自己训练的数据集相关
# 训练前一定要修改 classes_path, 使其对应自己的数据集
#---------------------------------------------------------------------#
classes_path = '/opt/data/private/wym/Remote_Sense/faster-rcnn-pytorch-master/model_data/RS_classes.txt'
#----------------------------------------------------------------------------------------------------------------------------#
# 权值文件的下载请看 README, 可以通过网盘下载。模型的 预训练权重 对不同数据集是通用的 , 因为特征是通用的。
# 模型的 预训练权重 比较重要的部分是 主干特征提取网络的权值部分 , 用于进行特征提取。
# 预训练权重对于 99% 的情况都必须要用 , 不用的话主干部分的权值太过随机 , 特征提取效果不明显 , 网络训练的结果也不会好
#
# 如果训练过程中存在中断训练的操作 , 可以将 model_path 设置成 logs 文件夹下的权值文件 , 将已经训练了一部分的权值再次载入。
# 同时修改下方的 冻结阶段 或者 解冻阶段 的参数 , 来保证模型 epoch 的连续性。
#
# 当 model_path = '' 的时候不加载整个模型的权值。
#
# 此处使用的是整个模型的权重 , 因此是在 train.py 进行加载的 , 下面的 pretrain 不影响此处的权值加载。
# 如果想要让模型从主干的预训练权值开始训练 , 则设置 model_path = '', 下面的 pretrain = True, 此时仅加载主干。
# 如果想要让模型从 0 开始训练 , 则设置 model_path = '', 下面的 pretrain = Fasle,Freeze_Train = Fasle, 此时从 0 开始训练 , 且没有冻结主干的过程。
#
# 一般来讲 , 网络从 0 开始的训练效果会很差 , 因为权值太过随机 , 特征提取效果不明显 , 因此非常、非常、非常不建议大家从 0 开始训练!
# 如果一定要从 0 开始 , 可以了解 imagenet 数据集 , 首先训练分类模型 , 获得网络的主干部分权值 , 分类模型的 主干部分 和该模型通用 , 基于此进行训练。
#----------------------------------------------------------------------------------------------------------------------------#
model_path = '/opt/data/private/wym/Remote_Sense/faster-rcnn-pytorch-master/model_data/voc_weights_resnet.pth'
#------------------------------------------------------#
# input_shape 输入的 shape 大小
#------------------------------------------------------#
input_shape = [1000 , 1000 ]
#---------------------------------------------#
# vgg
# resnet50
#---------------------------------------------#
backbone = "resnet50"
#----------------------------------------------------------------------------------------------------------------------------#
# pretrained 是否使用主干网络的预训练权重 , 此处使用的是主干的权重 , 因此是在模型构建的时候进行加载的。
# 如果设置了 model_path, 则主干的权值无需加载 ,pretrained 的值无意义。
# 如果不设置 model_path,pretrained = True, 此时仅加载主干开始训练。
# 如果不设置 model_path,pretrained = False,Freeze_Train = Fasle, 此时从 0 开始训练 , 且没有冻结主干的过程。
#----------------------------------------------------------------------------------------------------------------------------#
pretrained = True
#------------------------------------------------------------------------#
# anchors_size 用于设定先验框的大小 , 每个特征点均存在 9 个先验框。
# anchors_size 每个数对应 3 个先验框。
# 当 anchors_size = [8, 16, 32] 的时候 , 生成的先验框宽高约为:
# [90, 180] ; [180, 360]; [360, 720]; [128, 128];
# [256, 256]; [512, 512]; [180, 90] ; [360, 180];
# [720, 360]; 详情查看 anchors.py
# 如果想要检测小物体 , 可以减小 anchors_size 靠前的数。
# 比如设置 anchors_size = [4, 16, 32]
#------------------------------------------------------------------------#
anchors_size = [4 , 16 , 32 ]
#----------------------------------------------------------------------------------------------------------------------------#
# 训练分为两个阶段 , 分别是冻结阶段和解冻阶段。设置冻结阶段是为了满足机器性能不足的同学的训练需求。
# 冻结训练需要的显存较小 , 显卡非常差的情况下 , 可设置 Freeze_Epoch 等于 UnFreeze_Epoch, 此时仅仅进行冻结训练。
#
# 在此提供若干参数设置建议 , 各位训练者根据自己的需求进行灵活调整:
# ( 一 ) 从整个模型的预训练权重开始训练:
# Adam :
# Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 100,Freeze_Train = True,optimizer_type = 'adam',Init_lr = 1e-4 。 ( 冻结 )
# Init_Epoch = 0,UnFreeze_Epoch = 100,Freeze_Train = False,optimizer_type = 'adam',Init_lr = 1e-4 。 ( 不冻结 )
# SGD :
# Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 150,Freeze_Train = True,optimizer_type = 'sgd',Init_lr = 1e-2 。 ( 冻结 )
# Init_Epoch = 0,UnFreeze_Epoch = 150,Freeze_Train = False,optimizer_type = 'sgd',Init_lr = 1e-2 。 ( 不冻结 )
# 其中: UnFreeze_Epoch 可以在 100-300 之间调整。
# ( 二 ) 从主干网络的预训练权重开始训练:
# Adam :
# Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 100,Freeze_Train = True,optimizer_type = 'adam',Init_lr = 1e-4 。 ( 冻结 )
# Init_Epoch = 0,UnFreeze_Epoch = 100,Freeze_Train = False,optimizer_type = 'adam',Init_lr = 1e-4 。 ( 不冻结 )
# SGD :
# Init_Epoch = 0,Freeze_Epoch = 50,UnFreeze_Epoch = 150,Freeze_Train = True,optimizer_type = 'sgd',Init_lr = 1e-2 。 ( 冻结 )
# Init_Epoch = 0,UnFreeze_Epoch = 150,Freeze_Train = False,optimizer_type = 'sgd',Init_lr = 1e-2 。 ( 不冻结 )
# 其中:由于从主干网络的预训练权重开始训练 , 主干的权值不一定适合目标检测 , 需要更多的训练跳出局部最优解。
# UnFreeze_Epoch 可以在 150-300 之间调整 ,YOLOV5 和 YOLOX 均推荐使用 300 。
# Adam 相较于 SGD 收敛的快一些。因此 UnFreeze_Epoch 理论上可以小一点 , 但依然推荐更多的 Epoch 。
# ( 三 )batch_size 的设置:
# 在显卡能够接受的范围内 , 以大为好。显存不足与数据集大小无关 , 提示显存不足 (OOM 或者 CUDA out of memory) 请调小 batch_size 。
# faster rcnn 的 Batch BatchNormalization 层已经冻结 ,batch_size 可以为 1
#----------------------------------------------------------------------------------------------------------------------------#
#------------------------------------------------------------------#
# 冻结阶段训练参数
# 此时模型的主干被冻结了 , 特征提取网络不发生改变
# 占用的显存较小 , 仅对网络进行微调
# Init_Epoch 模型当前开始的训练世代 , 其值可以大于 Freeze_Epoch, 如设置:
# Init_Epoch = 60 、 Freeze_Epoch = 50 、 UnFreeze_Epoch = 100
# 会跳过冻结阶段 , 直接从 60 代开始 , 并调整对应的学习率。
# ( 断点续练时使用 )
# Freeze_Epoch 模型冻结训练的 Freeze_Epoch
# ( 当 Freeze_Train=False 时失效 )
# Freeze_batch_size 模型冻结训练的 batch_size
# ( 当 Freeze_Train=False 时失效 )
#------------------------------------------------------------------#
Init_Epoch = 0
Freeze_Epoch = 50
Freeze_batch_size = 12
#------------------------------------------------------------------#
# 解冻阶段训练参数
# 此时模型的主干不被冻结了 , 特征提取网络会发生改变
# 占用的显存较大 , 网络所有的参数都会发生改变
# UnFreeze_Epoch 模型总共训练的 epoch
# SGD 需要更长的时间收敛 , 因此设置较大的 UnFreeze_Epoch
# Adam 可以使用相对较小的 UnFreeze_Epoch
# Unfreeze_batch_size 模型在解冻后的 batch_size
#------------------------------------------------------------------#
UnFreeze_Epoch = 100
Unfreeze_batch_size = 12
#------------------------------------------------------------------#
# Freeze_Train 是否进行冻结训练
# 默认先冻结主干训练后解冻训练。
# 如果设置 Freeze_Train=False, 建议使用优化器为 sgd
#------------------------------------------------------------------#
Freeze_Train = False
#------------------------------------------------------------------#
# 其它训练参数:学习率、优化器、学习率下降有关
#------------------------------------------------------------------#
#------------------------------------------------------------------#
# Init_lr 模型的最大学习率
# 当使用 Adam 优化器时建议设置 Init_lr=1e-4
# 当使用 SGD 优化器时建议设置 Init_lr=1e-2
# Min_lr 模型的最小学习率 , 默认为最大学习率的 0.01
#------------------------------------------------------------------#
Init_lr = 1e-4
Min_lr = Init_lr * 0.01
#------------------------------------------------------------------#
# optimizer_type 使用到的优化器种类 , 可选的有 adam 、 sgd
# 当使用 Adam 优化器时建议设置 Init_lr=1e-4
# 当使用 SGD 优化器时建议设置 Init_lr=1e-2
# momentum 优化器内部使用到的 momentum 参数
# weight_decay 权值衰减 , 可防止过拟合
# adam 会导致 weight_decay 错误 , 使用 adam 时建议设置为 0 。
#------------------------------------------------------------------#
optimizer_type = "adam"
momentum = 0.9
weight_decay = 0
#------------------------------------------------------------------#
# lr_decay_type 使用到的学习率下降方式 , 可选的有 'step' 、 'cos'
#------------------------------------------------------------------#
lr_decay_type = 'cos'
#------------------------------------------------------------------#
# save_period 多少个 epoch 保存一次权值
#------------------------------------------------------------------#
save_period = 5
#------------------------------------------------------------------#
# save_dir 权值与日志文件保存的文件夹
#------------------------------------------------------------------#
save_dir = 'logs'
#------------------------------------------------------------------#
# eval_flag 是否在训练时进行评估 , 评估对象为验证集
# 安装 pycocotools 库后 , 评估体验更佳。
# eval_period 代表多少个 epoch 评估一次 , 不建议频繁的评估
# 评估需要消耗较多的时间 , 频繁评估会导致训练非常慢
# 此处获得的 mAP 会与 get_map.py 获得的会有所不同 , 原因有二:
# ( 一 ) 此处获得的 mAP 为验证集的 mAP 。
# ( 二 ) 此处设置评估参数较为保守 , 目的是加快评估速度。
#------------------------------------------------------------------#
eval_flag = True
eval_period = 5
#------------------------------------------------------------------#
# num_workers 用于设置是否使用多线程读取数据 ,1 代表关闭多线程
# 开启后会加快数据读取速度 , 但是会占用更多内存
# 在 IO 为瓶颈的时候再开启多线程 , 即 GPU 运算速度远大于读取图片的速度。
#------------------------------------------------------------------#
num_workers = 4
#----------------------------------------------------#
# 获得图片路径和标签
#----------------------------------------------------#
train_annotation_path = '2007_train.txt'
val_annotation_path = '2007_val.txt'
#----------------------------------------------------#
# 获取 classes 和 anchor
#----------------------------------------------------#
class_names, num_classes = get_classes(classes_path)
#------------------------------------------------------#
# 设置用到的显卡
#------------------------------------------------------#
os.environ["CUDA_VISIBLE_DEVICES" ] = ',' .join(str (x) for x in train_gpu)
ngpus_per_node = len (train_gpu)
print ('Number of devices: {}' .format(ngpus_per_node))
model = FasterRCNN(num_classes, anchor_scales = anchors_size, backbone = backbone, pretrained = pretrained)
if not pretrained:
weights_init(model)
if model_path != '' :
#------------------------------------------------------#
# 权值文件请看 README, 百度网盘下载
#------------------------------------------------------#
print ('Load weights {}.' .format(model_path))
#------------------------------------------------------#
# 根据预训练权重的 Key 和模型的 Key 进行加载
#------------------------------------------------------#
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu' )
model_dict = model.state_dict()
pretrained_dict = torch.load(model_path, map_location = device)
load_key, no_load_key, temp_dict = [], [], {}
for k, v in pretrained_dict.items():
if k in model_dict.keys() and np.shape(model_dict[k]) == np.shape(v):
temp_dict[k] = v
load_key.append(k)
else :
no_load_key.append(k)
model_dict.update(temp_dict)
model.load_state_dict(model_dict)
#------------------------------------------------------#
# 显示没有匹配上的 Key
#------------------------------------------------------#
print (" \n Successful Load Key:" , str (load_key)[:500 ], "…… \n Successful Load Key Num:" , len (load_key))
print (" \n Fail To Load Key:" , str (no_load_key)[:500 ], "…… \n Fail To Load Key num:" , len (no_load_key))
print (" \n\033 [1;33;44m 温馨提示 ,head 部分没有载入是正常现象 ,Backbone 部分没有载入是错误的。 \033 [0m" )
#----------------------#
# 记录 Loss
#----------------------#
time_str = datetime.datetime.strftime(datetime.datetime.now(),'%Y_%m_%d_%H_%M_%S' )
log_dir = os.path.join(save_dir, "loss_" + str (time_str))
loss_history = LossHistory(log_dir, model, input_shape = input_shape)
#------------------------------------------------------------------#
# torch 1.2 不支持 amp, 建议使用 torch 1.7.1 及以上正确使用 fp16
# 因此 torch1.2 这里显示 "could not be resolve"
#------------------------------------------------------------------#
if fp16:
from torch.cuda.amp import GradScaler as GradScaler
scaler = GradScaler()
else :
scaler = None
model_train = model.train()
if Cuda:
model_train = torch.nn.DataParallel(model_train)
cudnn.benchmark = True
model_train = model_train.cuda()
#---------------------------#
# 读取数据集对应的 txt
#---------------------------#
with open (train_annotation_path, encoding ='utf-8' ) as f:
train_lines = f.readlines()
with open (val_annotation_path, encoding ='utf-8' ) as f:
val_lines = f.readlines()
num_train = len (train_lines)
num_val = len (val_lines)
show_config(
classes_path = classes_path, model_path = model_path, input_shape = input_shape, \
Init_Epoch = Init_Epoch, Freeze_Epoch = Freeze_Epoch, UnFreeze_Epoch = UnFreeze_Epoch, Freeze_batch_size = Freeze_batch_size, Unfreeze_batch_size = Unfreeze_batch_size, Freeze_Train = Freeze_Train, \
Init_lr = Init_lr, Min_lr = Min_lr, optimizer_type = optimizer_type, momentum = momentum, lr_decay_type = lr_decay_type, \
save_period = save_period, save_dir = save_dir, num_workers = num_workers, num_train = num_train, num_val = num_val
)
#---------------------------------------------------------#
# 总训练世代指的是遍历全部数据的总次数
# 总训练步长指的是梯度下降的总次数
# 每个训练世代包含若干训练步长 , 每个训练步长进行一次梯度下降。
# 此处仅建议最低训练世代 , 上不封顶 , 计算时只考虑了解冻部分
#----------------------------------------------------------#
wanted_step = 5e4 if optimizer_type == "sgd" else 1.5e4
total_step = num_train // Unfreeze_batch_size * UnFreeze_Epoch
if total_step <= wanted_step:
if num_train // Unfreeze_batch_size == 0 :
raise ValueError (' 数据集过小 , 无法进行训练 , 请扩充数据集。 ' )
wanted_epoch = wanted_step // (num_train // Unfreeze_batch_size) + 1
print (" \n\033 [1;33;44m[Warning] 使用 %s 优化器时 , 建议将训练总步长设置到 %d 以上。 \033 [0m" %(optimizer_type, wanted_step))
print (" \033 [1;33;44m[Warning] 本次运行的总训练数据量为 %d,Unfreeze_batch_size 为 %d, 共训练 %d 个 Epoch, 计算出总训练步长为 %d 。 \033 [0m" %(num_train, Unfreeze_batch_size, UnFreeze_Epoch, total_step))
print (" \033 [1;33;44m[Warning] 由于总训练步长为 %d, 小于建议总步长 %d, 建议设置总世代为 %d 。 \033 [0m" %(total_step, wanted_step, wanted_epoch))
#------------------------------------------------------#
# 主干特征提取网络特征通用 , 冻结训练可以加快训练速度
# 也可以在训练初期防止权值被破坏。
# Init_Epoch 为起始世代
# Freeze_Epoch 为冻结训练的世代
# UnFreeze_Epoch 总训练世代
# 提示 OOM 或者显存不足请调小 Batch_size
#------------------------------------------------------#
if True :
UnFreeze_flag = False
#------------------------------------#
# 冻结一定部分训练
#------------------------------------#
if Freeze_Train:
for param in model.extractor.parameters():
param.requires_grad = False
# ------------------------------------#
# 冻结 bn 层
# ------------------------------------#
model.freeze_bn()
#-------------------------------------------------------------------#
# 如果不冻结训练的话 , 直接设置 batch_size 为 Unfreeze_batch_size
#-------------------------------------------------------------------#
batch_size = Freeze_batch_size if Freeze_Train else Unfreeze_batch_size
#-------------------------------------------------------------------#
# 判断当前 batch_size, 自适应调整学习率
#-------------------------------------------------------------------#
nbs = 16
lr_limit_max = 1e-4 if optimizer_type == 'adam' else 5e-2
lr_limit_min = 1e-4 if optimizer_type == 'adam' else 5e-4
Init_lr_fit = min (max (batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max)
Min_lr_fit = min (max (batch_size / nbs * Min_lr, lr_limit_min * 1e-2 ), lr_limit_max * 1e-2 )
#---------------------------------------#
# 根据 optimizer_type 选择优化器
#---------------------------------------#
optimizer = {
'adam' : optim.Adam(model.parameters(), Init_lr_fit, betas = (momentum, 0.999 ), weight_decay = weight_decay),
'sgd' : optim.SGD(model.parameters(), Init_lr_fit, momentum = momentum, nesterov =True , weight_decay = weight_decay)
}[optimizer_type]
#---------------------------------------#
# 获得学习率下降的公式
#---------------------------------------#
lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, UnFreeze_Epoch)
#---------------------------------------#
# 判断每一个世代的长度
#---------------------------------------#
epoch_step = num_train // batch_size
epoch_step_val = num_val // batch_size
if epoch_step == 0 or epoch_step_val == 0 :
raise ValueError (" 数据集过小 , 无法继续进行训练 , 请扩充数据集。 " )
train_dataset = FRCNNDataset(train_lines, input_shape, train = True )
val_dataset = FRCNNDataset(val_lines, input_shape, train = False )
gen = DataLoader(train_dataset, shuffle = True , batch_size = batch_size, num_workers = num_workers, pin_memory = True ,
drop_last = True , collate_fn = frcnn_dataset_collate)
gen_val = DataLoader(val_dataset , shuffle = True , batch_size = batch_size, num_workers = num_workers, pin_memory = True ,
drop_last = True , collate_fn = frcnn_dataset_collate)
train_util = FasterRCNNTrainer(model_train, optimizer)
#----------------------#
# 记录 eval 的 map 曲线
#----------------------#
eval_callback = EvalCallback(model_train, input_shape, class_names, num_classes, val_lines, log_dir, Cuda, \
eval_flag =eval_flag, period =eval_period)
#---------------------------------------#
# 开始模型训练
#---------------------------------------#
for epoch in range (Init_Epoch, UnFreeze_Epoch):
#---------------------------------------#
# 如果模型有冻结学习部分
# 则解冻 , 并设置参数
#---------------------------------------#
if epoch >= Freeze_Epoch and not UnFreeze_flag and Freeze_Train:
batch_size = Unfreeze_batch_size
#-------------------------------------------------------------------#
# 判断当前 batch_size, 自适应调整学习率
#-------------------------------------------------------------------#
nbs = 16
lr_limit_max = 1e-4 if optimizer_type == 'adam' else 5e-2
lr_limit_min = 1e-4 if optimizer_type == 'adam' else 5e-4
Init_lr_fit = min (max (batch_size / nbs * Init_lr, lr_limit_min), lr_limit_max)
Min_lr_fit = min (max (batch_size / nbs * Min_lr, lr_limit_min * 1e-2 ), lr_limit_max * 1e-2 )
#---------------------------------------#
# 获得学习率下降的公式
#---------------------------------------#
lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, UnFreeze_Epoch)
for param in model.extractor.parameters():
param.requires_grad = True
# ------------------------------------#
# 冻结 bn 层
# ------------------------------------#
model.freeze_bn()
epoch_step = num_train // batch_size
epoch_step_val = num_val // batch_size
if epoch_step == 0 or epoch_step_val == 0 :
raise ValueError (" 数据集过小 , 无法继续进行训练 , 请扩充数据集。 " )
gen = DataLoader(train_dataset, shuffle = True , batch_size = batch_size, num_workers = num_workers, pin_memory =True ,
drop_last =True , collate_fn =frcnn_dataset_collate)
gen_val = DataLoader(val_dataset , shuffle = True , batch_size = batch_size, num_workers = num_workers, pin_memory =True ,
drop_last =True , collate_fn =frcnn_dataset_collate)
UnFreeze_flag = True
set_optimizer_lr(optimizer, lr_scheduler_func, epoch)
fit_one_epoch(model, train_util, loss_history, eval_callback, optimizer, epoch, epoch_step, epoch_step_val, gen, gen_val, UnFreeze_Epoch, Cuda, fp16, scaler, save_period, save_dir)
loss_history.writer.close()
dataloader完整代码:
import cv2
import numpy as np
import torch
from PIL import Image
from torch.utils.data.dataset import Dataset
from utils.utils import cvtColor, preprocess_input
class FRCNNDataset(Dataset):
def __init__ (self , annotation_lines, input_shape = [600 , 600 ], train = True ):
self .annotation_lines = annotation_lines
self .length = len (annotation_lines)
self .input_shape = input_shape
self .train = train
def __len__ (self ):
return self .length
def __getitem__ (self , index):
index = index % self .length
#---------------------------------------------------#
# 训练时进行数据的随机增强
# 验证时不进行数据的随机增强
#---------------------------------------------------#
image, y = self .get_random_data(self .annotation_lines[index], self .input_shape[0 :2 ], random = self .train)
image = np.transpose(preprocess_input(np.array(image, dtype =np.float32)), (2 , 0 , 1 ))
box_data = np.zeros((len (y), 5 ))
if len (y) > 0 :
box_data[:len (y)] = y
box = box_data[:, :4 ]
label = box_data[:, -1 ]
return image, box, label
def rand (self , a=0 , b=1 ):
return np.random.rand()*(b-a) + a
def get_random_data (self , annotation_line, input_shape, jitter=.3 , hue=.1 , sat=0.7 , val=0.4 , random=True ):
line = annotation_line.split()
#------------------------------#
# 读取图像并转换成 RGB 图像
#------------------------------#
image = Image.open(line[0 ])
image = cvtColor(image)
#------------------------------#
# 获得图像的高宽与目标高宽
#------------------------------#
iw, ih = image.size
h, w = input_shape
#------------------------------#
# 获得预测框
#------------------------------#
box = np.array([np.array(list (map (int ,box.split(',' )))) for box in line[1 :]])
if not random:
scale = min (w/iw, h/ih)
nw = int (iw*scale)
nh = int (ih*scale)
dx = (w-nw)//2
dy = (h-nh)//2
#---------------------------------#
# 将图像多余的部分加上灰条
#---------------------------------#
image = image.resize((nw,nh), Image.BICUBIC)
new_image = Image.new('RGB' , (w,h), (128 ,128 ,128 ))
new_image.paste(image, (dx, dy))
image_data = np.array(new_image, np.float32)
#---------------------------------#
# 对真实框进行调整
#---------------------------------#
if len (box)>0 :
np.random.shuffle(box)
box[:, [0 ,2 ]] = box[:, [0 ,2 ]]*nw/iw + dx
box[:, [1 ,3 ]] = box[:, [1 ,3 ]]*nh/ih + dy
box[:, 0 :2 ][box[:, 0 :2 ]<0 ] = 0
box[:, 2 ][box[:, 2 ]>w] = w
box[:, 3 ][box[:, 3 ]>h] = h
box_w = box[:, 2 ] - box[:, 0 ]
box_h = box[:, 3 ] - box[:, 1 ]
box = box[np.logical_and(box_w>1 , box_h>1 )] # discard invalid box
return image_data, box
#------------------------------------------#
# 对图像进行缩放并且进行长和宽的扭曲
#------------------------------------------#
new_ar = iw/ih * self .rand(1 -jitter,1 +jitter) / self .rand(1 -jitter,1 +jitter)
scale = self .rand(.25 , 2 )
if new_ar < 1 :
nh = int (scale*h)
nw = int (nh*new_ar)
else :
nw = int (scale*w)
nh = int (nw/new_ar)
image = image.resize((nw,nh), Image.BICUBIC)
#------------------------------------------#
# 将图像多余的部分加上灰条
#------------------------------------------#
dx = int (self .rand(0 , w-nw))
dy = int (self .rand(0 , h-nh))
new_image = Image.new('RGB' , (w,h), (128 ,128 ,128 ))
new_image.paste(image, (dx, dy))
image = new_image
#------------------------------------------#
# 翻转图像
#------------------------------------------#
flip = self .rand()<.5
if flip: image = image.transpose(Image.FLIP_LEFT_RIGHT)
image_data = np.array(image, np.uint8)
#---------------------------------#
# 对图像进行色域变换
# 计算色域变换的参数
#---------------------------------#
r = np.random.uniform(-1 , 1 , 3 ) * [hue, sat, val] + 1
#---------------------------------#
# 将图像转到 HSV 上
#---------------------------------#
hue, sat, val = cv2.split(cv2.cvtColor(image_data, cv2.COLOR_RGB2HSV))
dtype = image_data.dtype
#---------------------------------#
# 应用变换
#---------------------------------#
x = np.arange(0 , 256 , dtype =r.dtype)
lut_hue = ((x * r[0 ]) % 180 ).astype(dtype)
lut_sat = np.clip(x * r[1 ], 0 , 255 ).astype(dtype)
lut_val = np.clip(x * r[2 ], 0 , 255 ).astype(dtype)
image_data = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val)))
image_data = cv2.cvtColor(image_data, cv2.COLOR_HSV2RGB)
#---------------------------------#
# 对真实框进行调整
#---------------------------------#
if len (box)>0 :
np.random.shuffle(box)
box[:, [0 ,2 ]] = box[:, [0 ,2 ]]*nw/iw + dx
box[:, [1 ,3 ]] = box[:, [1 ,3 ]]*nh/ih + dy
if flip: box[:, [0 ,2 ]] = w - box[:, [2 ,0 ]]
box[:, 0 :2 ][box[:, 0 :2 ]<0 ] = 0
box[:, 2 ][box[:, 2 ]>w] = w
box[:, 3 ][box[:, 3 ]>h] = h
box_w = box[:, 2 ] - box[:, 0 ]
box_h = box[:, 3 ] - box[:, 1 ]
box = box[np.logical_and(box_w>1 , box_h>1 )]
return image_data, box
# DataLoader 中 collate_fn 使用
def frcnn_dataset_collate (batch):
images = []
bboxes = []
labels = []
for img, box, label in batch:
images.append(img)
bboxes.append(box)
labels.append(label)
images = torch.from_numpy(np.array(images))
return images, bboxes, labels
已采纳
2025-3-11(该求助已完结,附件将在上传时间7天后自动删除)
在遥感目标检测任务中,将水平框转换为定向框(rotated bounding box)是一个常见的需求。你提到的代码已经在某些部分处理了目标的角度,并根据给定的宽度、高度和角度计算了定向框的四个角点。下面是对你的需求的详细解答。
### 如何将水平框转化为定向框?
假设你有一个标准的水平框,它的表示是 `(xmin, ymin, xmax, ymax)`,而你需要将它转换为一个定向框(即包含旋转角度的框)。转换的步骤如下:
1. **水平框的坐标**:
...
查看完整内容