最近终于有时间坐下来把之前学的东西总结一下了,当时学习的时候也是一脸懵逼,看了好多文章,文档,也自己完全做了一遍,希望能自己动手加深印象,也希望能通过案例来进一步的学习!在这里写这篇文章,主要目的是为了记录一下学习过程,方便以后复习。如果哪里有写的不对或者不完善的地方希望大家能提出宝贵的意见来!
前期准备:需要编译faster-rcnn
关于faster-rcnn的编译在上一篇文章中已经介绍过了,这一步没有完成的小伙伴可以看看上一篇文章!
第一步:VOC格式数据集的介绍
由于faster-rcnn使用的是VOC格式的数据集,案例中需要将我们的kitti数据集转换成VOC数据集,因此在这里以VOC2007为例来介绍一下VOC数据集。
VOC2007包含3个文件夹:
1.JPEGImages ----> 用来存放所有的原始图片
2.ImageSets(包含多个子文件夹,目标检测中只用到Main文件夹)
Main --->存放一些txt文件,用来标明训练时候的train数据集和val数据集
Layout
Segmentation
3.Annotation ---> 存放一些xml文件,xml文件中包含相对应的bounding box位置信息,以及类别,每个xml文件对应JPEGImages文件夹中的一张图片。内容如下:
<?xml version="1.0" ?>
<annotation>
<folder>VOC2007</folder>
<filename>000000.png</filename>
<source>
<database>The VOC2007 Database</database>
<annotation>PASCAL VOC2007</annotation>
</source>
<size>
<width>1224</width>
<height>370</height>
<depth>3</depth>
</size>
<object>
<name>pedestrian</name>
<difficult>0</difficult>
<bndbox>
<xmin>712</xmin>
<ymin>143</ymin>
<xmax>810</xmax>
<ymax>307</ymax>
</bndbox>
</object>
</annotation>
第二步:准备数据集
我在案例中使用的数据集是kitti数据集,kitti数据集是一个关于车辆检测的数据集。
关于kitti数据集的介绍可以参考https://blog.csdn.net/cuichuanchen3307/article/details/80596689这篇文章。
kitti数据集的下载可以参考我的另一篇文章https://blog.csdn.net/xw_2_xh/article/details/86553883
下载后得到两个文件:
原始图片集:data_object_image_2.zip,解压后得到训练集和测试集对应的元素图片,为png格式,将所有图片放到VOC数据集的JPEGImages文件夹下。
标签集为:data_object_label_2.zip,解压后得到训练集对应的标签文件,为txt文件,内容格式如下:
Car 0.60 3 -2.42 0.00 185.93 214.05 348.86 1.56 1.57 4.37 -6.96 1.73 7.83 -3.13
Car 0.00 1 -2.73 57.68 178.66 341.72 285.91 1.65 1.68 3.88 -6.88 1.77 12.36 3.05
每一行就是一个object,第一个数据Car表示类别信息为Car,其余为bounding box信息。
第三步:将kitti数据集转化为VOC数据集
1.转换kitti数据类别
由于“DontCare”,“Misc”,“Cyclist”三个类别在图片中太小且标注信息也不准确,在转换的时候我将其忽略。并且还将“Person_sitting”和“Pedestrian”合并为一个类别,统一标记为“Pedestrian”。利用如下python代码修改:
创建python文件modify_kitti_type.py
#-*-coding:utf-8-*-
import glob
import string
txt_list = glob.glob('G:/DeepLearning/dataset/kitti/training/label_2/*.txt') # 存储Labels文件夹所有txt文件路径
# 输出所有文件中的物体类别
def show_category(txt_list):
category_list= []
for item in txt_list:
try:
with open(item) as tdf:
for each_line in tdf:
labeldata = each_line.strip().split(' ') # 去掉前后多余的字符并把其分开
category_list.append(labeldata[0]) # 只要第一个字段,即类别
except IOError as ioerr:
print('File error:'+str(ioerr))
print(set(category_list)) # 输出集合
def merge(line):
each_line=''
for i in range(len(line)):
if i!= (len(line)-1):
each_line=each_line+line[i]+' '
else:
each_line=each_line+line[i] # 最后一条字段后面不加空格
each_line=each_line+'\n'
return (each_line)
print('before modify categories are:\n')
show_category(txt_list)
for item in txt_list:
new_txt=[]
try:
with open(item, 'r') as r_tdf:
for each_line in r_tdf:
labeldata = each_line.strip().split(' ')
'''if labeldata[0] in ['Truck','Van','Tram','Car']: # 合并汽车类
labeldata[0] = labeldata[0].replace(labeldata[0],'car')
if labeldata[0] in ['Person_sitting','Cyclist','Pedestrian']: # 合并行人类
labeldata[0] = labeldata[0].replace(labeldata[0],'pedestrian')'''
#print type(labeldata[4])
# Pedestrian 0.00 0 -0.20 712.40 143.00 810.73 307.92 1.89 0.48 1.20 1.84 1.47 8.41 0.01
if labeldata[4] == '0.00':
labeldata[4] = labeldata[4].replace(labeldata[4],'1.00')
if labeldata[5] == '0.00':
labeldata[5] = labeldata[5].replace(labeldata[5],'1.00')
if labeldata[0] == 'Truck':
labeldata[0] = labeldata[0].replace(labeldata[0],'truck')
if labeldata[0] == 'Van':
labeldata[0] = labeldata[0].replace(labeldata[0],'van')
if labeldata[0] == 'Tram':
labeldata[0] = labeldata[0].replace(labeldata[0],'tram')
if labeldata[0] == 'Car':
labeldata[0] = labeldata[0].replace(labeldata[0],'car')
if labeldata[0] in ['Person_sitting','Pedestrian']: # 合并行人类
labeldata[0] = labeldata[0].replace(labeldata[0],'pedestrian')
if labeldata[0] == 'Cyclist':
continue
if labeldata[0] == 'DontCare': # 忽略Dontcare类
continue
if labeldata[0] == 'Misc': # 忽略Misc类
continue
new_txt.append(merge(labeldata)) # 重新写入新的txt文件
with open(item,'w+') as w_tdf: # w+是打开原文件将内容删除,另写新内容进去
for temp in new_txt:
w_tdf.write(temp)
except IOError as ioerr:
print('File error:'+str(ioerr))
print('\nafter modify categories are:\n')
show_category(txt_list)
2.转换标注信息格式(将txt转为xml)
在这一步中将标签集的txt标注文件转换成xml文件,并去掉其中用不到的信息。把坐标值从float类型转化为int类型,最后将所有生成的xml文件存放到VOC数据集的Annotations文件夹中。使用的python代码如下:
创建python文件txt_to_xml.py(注意:要将代码中的路径替换成自己的)
#-*-coding:utf-8-*-
# 根据一个给定的XML Schema,使用DOM树的形式从空白文件生成一个XML
from xml.dom.minidom import Document
import cv2
import os
def generate_xml(name,split_lines,img_size,class_ind):
doc = Document() # 创建DOM文档对象
annotation = doc.createElement('annotation')
doc.appendChild(annotation)
title = doc.createElement('folder')
title_text = doc.createTextNode('VOC2007')#这里修改了文件夹名
title.appendChild(title_text)
annotation.appendChild(title)
img_name=name+'.png'#要用jpg格式
title = doc.createElement('filename')
title_text = doc.createTextNode(img_name)
title.appendChild(title_text)
annotation.appendChild(title)
source = doc.createElement('source')
annotation.appendChild(source)
title = doc.createElement('database')
title_text = doc.createTextNode('The VOC2007 Database')#修改为VOC
title.appendChild(title_text)
source.appendChild(title)
title = doc.createElement('annotation')
title_text = doc.createTextNode('PASCAL VOC2007')#修改为VOC
title.appendChild(title_text)
source.appendChild(title)
size = doc.createElement('size')
annotation.appendChild(size)
title = doc.createElement('width')
title_text = doc.createTextNode(str(img_size[1]))
title.appendChild(title_text)
size.appendChild(title)
title = doc.createElement('height')
title_text = doc.createTextNode(str(img_size[0]))
title.appendChild(title_text)
size.appendChild(title)
title = doc.createElement('depth')
title_text = doc.createTextNode(str(img_size[2]))
title.appendChild(title_text)
size.appendChild(title)
for split_line in split_lines:
line=split_line.strip().split()
if line[0] in class_ind:
object = doc.createElement('object')
annotation.appendChild(object)
title = doc.createElement('name')
title_text = doc.createTextNode(line[0])
title.appendChild(title_text)
object.appendChild(title)
title = doc.createElement('difficult')
title_text = doc.createTextNode('0')
title.appendChild(title_text)
object.appendChild(title)
bndbox = doc.createElement('bndbox')
object.appendChild(bndbox)
title = doc.createElement('xmin')
title_text = doc.createTextNode(str(int(float(line[4]))))
title.appendChild(title_text)
bndbox.appendChild(title)
title = doc.createElement('ymin')
title_text = doc.createTextNode(str(int(float(line[5]))))
title.appendChild(title_text)
bndbox.appendChild(title)
title = doc.createElement('xmax')
title_text = doc.createTextNode(str(int(float(line[6]))))
title.appendChild(title_text)
bndbox.appendChild(title)
title = doc.createElement('ymax')
title_text = doc.createTextNode(str(int(float(line[7]))))
title.appendChild(title_text)
bndbox.appendChild(title)
# 将DOM对象doc写入文件
f = open('G:/DeepLearning/dataset/kitti/training/Annotations/'+name+'.xml','w')
f.write(doc.toprettyxml(indent = ''))
f.close()
if __name__ == '__main__':
class_ind=('van', 'tram', 'car', 'pedestrian', 'truck')#修改为了5类
# cur_dir=os.getcwd()
labels_dir=os.path.join("G:/DeepLearning/dataset/kitti/training",'label_2')
for parent, dirnames, filenames in os.walk(labels_dir): # 分别得到根目录,子目录和根目录下文件
for file_name in filenames:
full_path=os.path.join(parent, file_name) # 获取文件全路径
#print full_path
f=open(full_path)
split_lines = f.readlines()
name= file_name[:-4] # 后四位是扩展名.txt,只取前面的文件名
#print name
img_name=name+'.png'
img_path=os.path.join('G:/DeepLearning/dataset/kitti/training/image_2',img_name) # 路径需要自行修改
#print img_path
img_size=cv2.imread(img_path).shape
generate_xml(name,split_lines,img_size,class_ind)
print('all txts has converted into xmls')
将转换后的xml文件存放到VOC数据集中的Annotations目录下。
3.生成训练验证集合测试集的先关列表文件
使用如下python代码生成训练验证集和测试集列表,其中训练测试部分的比例可在代码中自行修改:
创建python文件generate_train_test_txt.py(注意:要将代码中的路径替换成自己的路径)
#-*-coding:utf-8-*-
# 生成训练验证集和测试集列表
import pdb
import glob
import os
import random
import math
def get_sample_value(txt_name, category_name):
label_path = 'G:/DeepLearning/dataset/kitti/training/label_2/'
txt_path = label_path + txt_name+'.txt'
try:
with open(txt_path) as r_tdf:
if category_name in r_tdf.read():
return ' 1'
else:
return '-1'
except IOError as ioerr:
print('File error:'+str(ioerr))
txt_list_path = glob.glob('G:/DeepLearning/dataset/kitti/training/label_2/*.txt')
txt_list = []
for item in txt_list_path:
temp1,temp2 = os.path.splitext(os.path.basename(item))
txt_list.append(temp1)
txt_list.sort()
print(txt_list, end = '\n\n')
# 有博客建议train:val:test=8:1:1,先尝试用一下
num_trainval = random.sample(txt_list, math.floor(len(txt_list)*9/10.0)) # 可修改百分比
num_trainval.sort()
print(num_trainval, end = '\n\n')
num_train = random.sample(num_trainval,math.floor(len(num_trainval)*8/9.0)) # 可修改百分比
num_train.sort()
print(num_train, end = '\n\n')
num_val = list(set(num_trainval).difference(set(num_train)))
num_val.sort()
print(num_val, end = '\n\n')
num_test = list(set(txt_list).difference(set(num_trainval)))
num_test.sort()
print(num_test, end = '\n\n')
# pdb.set_trace()
Main_path = 'G:/DeepLearning/dataset/kitti/training/ImageSets/Main/'
train_test_name = ['trainval','train','val','test']
category_name = ['van', 'tram', 'car', 'pedestrian', 'truck']#修改类别
# 循环写trainvl train val test
for item_train_test_name in train_test_name:
list_name = 'num_'
list_name += item_train_test_name
train_test_txt_name = Main_path + item_train_test_name + '.txt'
try:
# 写单个文件
with open(train_test_txt_name, 'w') as w_tdf:
# 一行一行写
for item in eval(list_name):
w_tdf.write(item+'\n')
# 循环写Car Pedestrian Cyclist
for item_category_name in category_name:
category_txt_name = Main_path + item_category_name + '_' + item_train_test_name + '.txt'
with open(category_txt_name, 'w') as w_tdf:
# 一行一行写
for item in eval(list_name):
w_tdf.write(item+' '+ get_sample_value(item, item_category_name)+'\n')
except IOError as ioerr:
print('File error:'+str(ioerr))
上述代码会生成一系列的txt文件,生成的文件列表如下图所示:
训练用到的所有数据到这里就已经准备完成了!
第四步:修改faster-rcnn网络结构
在修改之前首先介绍一下faster-rcnn的目录结构:
caffe-fast-rcnn ---> caffe框架
data ---> 存放数据以及读取文件的cache
experiments ---> 存放配置文件以及运行的log文件,配置文件
lib ---> python接口
models ---> 存放了三种模型,ZF(小型网络)、VGG1024(中型网络)、VGG16(大型网络)
output ---> 输出的model存放的位置,训练后悔自动生成此文件夹
tools ---> 训练和测试的python文件
faster-rcnn有如下两种训练方法:
1、Alternative training(alt-opt)
2、Approximate joint training(end-to-end)
我训练时使用的是alt-opt训练方法,由于显卡内存问题,训练时的网络我使用的是ZF。相比ZF,VGG16可以达到更高的真确性,有条件的朋友可以试试!废话少说,来看看网络中需要修改的部分:
注意:如果使用VGG16网络,则修改py-faster-rcnn/models/pascal_voc/VGG16目录下的对应文件
1、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_fast_rcnn_train.pt文件(第14行)
name: "ZF"
layer {
name: 'data'
type: 'Python'
top: 'data'
top: 'rois'
top: 'labels'
top: 'bbox_targets'
top: 'bbox_inside_weights'
top: 'bbox_outside_weights'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 6" #将这里的类别修改为6(1个背景加上5标签个类别)
}
}
2、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_fast_rcnn_train.pt文件(第428行和451行)
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 6 #这里也将类别数修改为6
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 24 #这里修改为24(类别数*4)
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
3、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_rpn_train.pt文件(第14行)
name: "ZF"
layer {
name: 'input-data'
type: 'Python'
top: 'data'
top: 'im_info'
top: 'gt_boxes'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 6" #这里将类别数修改为6
}
}
4、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage2_fast_rcnn_train.pt文件(第14行)
name: "ZF"
layer {
name: 'data'
type: 'Python'
top: 'data'
top: 'rois'
top: 'labels'
top: 'bbox_targets'
top: 'bbox_inside_weights'
top: 'bbox_outside_weights'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 6" #这里也将类别数修改为6
}
}
5、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage1_fast_rcnn_train.pt文件(第380行和399行)
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 6 #这里将类别数修改为6
weight_filler {
type: "gaussian"
std: 0.01
}
bias_filler {
type: "constant"
value: 0
}
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
param { lr_mult: 1.0 }
param { lr_mult: 2.0 }
inner_product_param {
num_output: 24 #这里将类别数修改为24
weight_filler {
type: "gaussian"
std: 0.001
}
bias_filler {
type: "constant"
value: 0
}
}
}
6、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/stage2_fast_rcnn_train.pt文件(第11行)
name: "ZF"
layer {
name: 'input-data'
type: 'Python'
top: 'data'
top: 'im_info'
top: 'gt_boxes'
python_param {
module: 'roi_data_layer.layer'
layer: 'RoIDataLayer'
param_str: "'num_classes': 6" #这里也将类别数修改为6
}
}
到这里网络结构的修改就已经完成!
7、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt目录下的所有solver文件(这一步主要是调整学习率,也可以不调)
我在这一步中将所有solver文件的学习率调整为0.0001,修改如下:
base_lr: 0.0001
8、py-faster-rcnn/lib/datasets/pascal_voc.py(第31行)
self._classes = ('__background__', # always index 0
'van', 'tram', 'car', 'pedestrian','truck') #将这里的标签类别修改为自己的标签类别
9、py-faster-rcnn/lib/dataset/imdb.py(第102行的append_flipped_images函数)
def append_flipped_images(self):
num_images = self.num_images
widths = self._get_widths()
for i in xrange(num_images):
boxes = self.roidb[i]['boxes'].copy()
oldx1 = boxes[:, 0].copy()
oldx2 = boxes[:, 2].copy()
boxes[:, 0] = widths[i] - oldx2 - 1
boxes[:, 2] = widths[i] - oldx1 - 1
if not (boxes[:, 2] >= boxes[:, 0]).all():
print self.roidb[i]['boxes'].copy()
print widths[i]
print self.image_path_at(i)
print self.roidb[i]['image_name']
assert (boxes[:, 2] >= boxes[:, 0]).all()
entry = {'boxes' : boxes,
'gt_overlaps' : self.roidb[i]['gt_overlaps'],
'gt_classes' : self.roidb[i]['gt_classes'],
'flipped' : True}
self.roidb.append(entry)
self._image_index = self._image_index * 2
10、/py-faster-rcnn/tools/train_faster_rcnn_alt_opt.py(第80行)修改训练的迭代次数
注意:这里建议将迭代次数先调小,确保不出错后在调回来!!!
max_iters = [120000, 80000, 120000, 80000] #四个数分别对应rpn第一阶段,fast rcnn第一阶段,rpn第二阶段,fast rcnn第2阶段的迭代次数
第五步:训练修改后的faster-rcnn
由于rpn网络的训练时以imagenet为初始值进行训练的,执行/py-fater-rcnn/data/scripts/fetch_imagenet_model.sh脚本获取imagenet的caffemodel文件。下载的可能有点慢,也可以从我的百度云下载https://pan.baidu.com/s/1yvBSNon-NqDP7tvCR5WFBw
将下载后的文件解压,解压后的caffemodel文件如下:
将这几个caffemodel文件放到py-faster-rcnn/data/imagenet_models目录下。
在py-faster-rcnn目录下执行如下命令,是在服务器终端执行:
./experiments/scripts/faster_rcnn_alt_opt.sh 0 ZF pascal_voc
命令解释:命令中的第一个参数0指的是使用第0块GPU,ZF指使用ZF网络。
如果在py-faster-rcnn目录下出现output文件夹,且在output/faster_rcnn_alt_opt/voc_2007_trainval目录下有ZF_faster_rcnn_final.caffemodel文件,则表明训练成功!
注意:如果使用的是VGG16网络,则在训练时将训练命令中的ZF改为VGG16。
第六步:Faster-rcnn测试
在测试的时候我使用tools目录下的demo.py来测试训练出来的模型。需要修改测试的模型文件:
1、py-faster-rcnn/models/pascal_voc/ZF/faster_rcnn_alt_opt/faster_rcnn_test.pt文件(第392行和第401行)
layer {
name: "cls_score"
type: "InnerProduct"
bottom: "fc7"
top: "cls_score"
inner_product_param {
num_output: 6 #这里修改类别数为6
}
}
layer {
name: "bbox_pred"
type: "InnerProduct"
bottom: "fc7"
top: "bbox_pred"
inner_product_param {
num_output: 24 #这里修改类别数为24
}
}
2、 py-faster-rcnn/tools/demo.py(第27行和第31行)
CLASSES = ('__background__',
'van', 'tram', 'car', 'pedestrian','truck') #将这里的标签名修改为自己的标签名
NETS = {'vgg16': ('VGG16',
'vgg16_faster_rcnn_iter_80000.caffemodel'),
'zf': ('ZF',
'ZF_faster_rcnn_final.caffemodel')} #将这里的ZF网络生成的caffemodel文件名修改为训练后的caffemodel文件
3、py-faster-rcnn/tools/demo.py(在第101行的parse_args函数的--net部分修改默认网络)
def parse_args():
"""Parse input arguments."""
parser = argparse.ArgumentParser(description='Faster R-CNN demo')
parser.add_argument('--gpu', dest='gpu_id', help='GPU device id to use [0]',
default=0, type=int)
parser.add_argument('--cpu', dest='cpu_mode',
help='Use CPU mode (overrides --gpu)',
action='store_true')
parser.add_argument('--net', dest='demo_net', help='Network to use [vgg16]',
choices=NETS.keys(), default='zf') #可以将这里的default值修改为zf,这样测试时使用的默认网络就是zf
args = parser.parse_args()
return args
4、py-faster-rcnn/tools/demo.py(第145行)
im_names = ['001348.png','001562.png','004714.png','005509.png','005512.png','005861.png','012576.png','012924.png']
通过im_names设置测试图片名称,这些图片存放在py-faster-rcnn/data/demo目录下!
5、执行如下命令进行测试(在py-faster-rcnn目录下执行):
在执行测试命令前需要将训练生成的ZF_faster_rcnn_final.caffemodel文件拷贝到
py-faster-rcnn/data/faster_rcnn_models目录下
./tools/demo.py
第七步:训练中遇到的问题及解决办法
我在做的过程中遇到如下一些问题,大家可以参考一下:
1、训练时报如下错误:
File "/py-faster-rcnn/tools/../lib/datasets/imdb.py", line 108, in append_flipped_images
assert (boxes[:, 2] >= boxes[:, 0]).all()
AssertionError
解决方案:
这个是由于faster rcnn会对Xmin,Ymin,Xmax,Ymax进行减1操作,如果Xmin为0,减1后变为65535
修改py-faster-rcnn/lib/fast_rcnn/config.py文件的第61行,如下:
# Use horizontally-flipped images during training?
__C.TRAIN.USE_FLIPPED = False #不使图片实现翻转
2、测试阶段报错:
显示pose和truncated为空值
这是由于制作xml文件没有pose和truncated属性,因此在测试阶段会报错
解决方案:
修改py-faster-rcnn/lib/datasets/voc_eval.py文件(将pose和truncated两个属性注释)
def parse_rec(filename):
""" Parse a PASCAL VOC xml file """
tree = ET.parse(filename)
objects = []
for obj in tree.findall('object'):
obj_struct = {}
obj_struct['name'] = obj.find('name').text
#obj_struct['pose'] = obj.find('pose').text
#obj_struct['truncated'] = int(obj.find('truncated').text)
obj_struct['difficult'] = int(obj.find('difficult').text)
bbox = obj.find('bndbox')
obj_struct['bbox'] = [int(bbox.find('xmin').text),
int(bbox.find('ymin').text),
int(bbox.find('xmax').text),
int(bbox.find('ymax').text)]
objects.append(obj_struct)
return objects
这两个问题是我在实验的过程中遇到的问题,如果还有其他问题,可以参考如下这篇文章:
https://blog.csdn.net/mdjxy63/article/details/79821516
参考文献:
https://blog.csdn.net/u014256231/article/details/79801665
https://blog.csdn.net/mdjxy63/article/details/79821516
https://blog.csdn.net/cuichuanchen3307/article/details/80596689