说来惭愧,自上一次更新博客以来已经有半年多没更新了,中间其实积累了几个项目想要分享的,可惜太懒,日后慢慢补上吧。
这次想要分享的是刚刚结束不久的大创项目,这个令我又爱又恨的项目,值得来记录一下。
2020年5月27号那天,跟组员胡编乱造乱开脑洞地写了个大创项目申请书,谁知道整个实验室只有我们进省赛了,本来完全想水过去的项目,随着时间慢慢逼近又不得不做,记得中期答辩的时候我们拿着一个随意到不能再随意的ppt被评委怼得无话可说,就觉得肯定是做不出来的,看看后面能不能放弃吧。
当后面得知省赛队伍不可以放弃的时候,整个人都是绝望的,因为申请书上面写的强化学习,深度学习对我来说有点天花乱坠,更何况考研才是当前的主线。
时间来到一个月前,看到我的队友们已经把车子都造出来了,觉得我也应该花点时间来弄一下了,不然也浪费了队友这么大的功夫。
谁知道弄着弄着,在最后十天的时候,阴差阳错地弄出了一个模型,然后顺理成章地结合到了机器人上面,最后答辩还拿了个为数不多的优秀,现在想想那种体验就amazing。
在考研期间花十天时间弄这么一个项目,值也不值,值的是确实有点意思,也做出了点东西,跟以前的队友一起合作的感觉也很难得,跟实验室的小伙伴一起讨论着有意思的技术问题也很有趣,大家一起通宵调车到五点累了就躺下睡着,看到最后小车能够完成预期的效果也开心到不行;不值的是考研的朋友在这段时间已经在高数上面远超我的进度,而我最近却丢失了那种先在脑子里算出答案再落笔写过程的感觉,英语单词也前功尽弃只能从头开始。怎么说呢?既然都这样了那就好好结束这个项目,然后好好地准备考研,也算是给自己一个交代吧。
强力队友毓正哥主要负责设计机械结构,将整台机器人组装起来,同时该机械机构可以根据重力筛自动将大小球进行分离。下面是整体的效果,整体机械结构由两部分组成,第一部分是前面的旋转扫板,另一部分是后面的收集滤板以及收集仓。
大小球在前部旋转扫板的推动下被扫上后部的收集滤板。滤板的滤孔可将大小球进行上下分离。
当需要分类的大小球尺寸不同时,我们可以通过更换不同滤孔大小的滤板来实现大小球分离。
而当仅收集一种球类时,我们可以将滤板拆除,使收集到的球类全部落入收集仓中。
下面是整个机器人的运作视频。
实际上,刚开始思考的时候是想着能不能用传统的视觉方法来做,因为自己对深度学习这一块确实不熟悉,而且深度学习往往比较慢,不能满足实时的路径规划。
因此硬是要用传统方法做是做不出来的。于是思考目标检测算法。
最主流也最先想到的当然是Yolo-V4算法,毕竟实验室也有朋友之前用过,可以请教一下,于是就先试一试,效果如下图,可见整体来说其准确率还是非常高的,但是却有一个致命的缺点,在我们的电脑上处理一张图片需要30秒,这意味着我们的机器人每做一个动作要等30秒才能做另一个动作,显然不可行。(考虑过神经网络棒加速,但加速后依然满足不了实时的需求)。
#将视频导出为若干帧图片
DATA_DIR = "E:/大三上/大创/视频素材/ball_video_1.mp4" #视频数据主目录
SAVE_DIR = "E:/大三上/大创/第二次训练图片" #帧文件保存目录
GAP = 10 #每隔多少帧导出一张图片
import cv2 #OpenCV库
import os
def getphoto(video_in, video_save):
number = 0
cap = cv2.VideoCapture(video_in) # 打开视频文件
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 视频的帧数
fps = cap.get(cv2.CAP_PROP_FPS) # 视频的帧率
dur = n_frames / fps # 视频的时间
num_frame = 0
judge = cap.isOpened()
while judge:
flag, frame = cap.read() # flag是读取状态,frame下一帧
if cv2.waitKey(0) == 27:
break
if flag:
num_frame += 1
if num_frame % GAP == 0:
print("正在保存第%d张照片" % number)
cv2.imwrite(video_save + '/' + str(number) + '.jpg', frame) # cv2.imwrite(‘路径’ + ‘名字’ + ‘后缀’, 要存的帧)
number += 1
else:
break
print("视频时长: %d 秒" % dur)
print("视频共有帧数: %d 保存帧数为: %d" % (n_frames, number))
print("每秒的帧数(FPS): %.1lf" % fps)
def main_1(path):
video_in = path
video_save = SAVE_DIR
getphoto(video_in, video_save)
if __name__=='__main__':
paht= DATA_DIR#视频路径
main_1(paht)
if not (name in ["ball"]):
print(filename + "------------->label is error--->" + name)
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os
import xml.etree.ElementTree as ET
origin_xml_dir = './第二次标注/'# 设置原始标签路径为 Annos
new_xml_dir = './数据集准备/xml/'# 设置新标签路径 Annotations
for dirpaths, dirnames, filenames in os.walk(origin_xml_dir): # os.walk游走遍历目录名
for filename in filenames:
print("process...")
if os.path.isfile(r'%s%s' %(origin_xml_dir, filename)): # 获取原始xml文件绝对路径,isfile()检测是否为文件 isdir检测是否为目录
origin_ann_path = os.path.join(r'%s%s' %(origin_xml_dir, filename)) # 如果是,获取绝对路径(重复代码)
new_ann_path = os.path.join(r'%s%s' %(new_xml_dir, filename))
tree = ET.parse(origin_ann_path) # ET是一个xml文件解析库,ET.parse()打开xml文件。parse--"解析"
root = tree.getroot() # 获取根节点
for object in root.findall('object'): # 找到根节点下所有“object”节点
name = str(object.find('name').text) # 找到object节点下name子节点的值(字符串)
# 如果name等于str,则删除该节点
# =============================================================================
# if (name in ["middle ball"]):
# root.remove(object)
# =============================================================================
# 如果name等于str,则修改name
if(name in ["small ball","big ball"]):
object.find('name').text = "ball"
# 检查是否存在labelmap中没有的类别
for object in root.findall('object'):
name = str(object.find('name').text)
if not (name in ["ball"]):
print(filename + "------------->label is error--->" + name)
tree.write(new_ann_path)#tree为文件,write写入新的文件中。
import xml.etree.ElementTree as ET
import pickle
import os
from os import getcwd
import numpy as np
from PIL import Image
import shutil
import matplotlib.pyplot as plt
import imgaug as ia
from imgaug import augmenters as iaa
ia.seed(1)
def read_xml_annotation(root, image_id):
in_file = open(os.path.join(root, image_id))
tree = ET.parse(in_file)
root = tree.getroot()
bndboxlist = []
for object in root.findall('object'): # 找到root节点下的所有country节点
bndbox = object.find('bndbox') # 子节点下节点rank的值
xmin = int(bndbox.find('xmin').text)
xmax = int(bndbox.find('xmax').text)
ymin = int(bndbox.find('ymin').text)
ymax = int(bndbox.find('ymax').text)
# print(xmin,ymin,xmax,ymax)
bndboxlist.append([xmin, ymin, xmax, ymax])
# print(bndboxlist)
bndbox = root.find('object').find('bndbox')
return bndboxlist
# (506.0000, 330.0000, 528.0000, 348.0000) -> (520.4747, 381.5080, 540.5596, 398.6603)
def change_xml_annotation(root, image_id, new_target):
new_xmin = new_target[0]
new_ymin = new_target[1]
new_xmax = new_target[2]
new_ymax = new_target[3]
in_file = open(os.path.join(root, str(image_id) + '.xml')) # 这里root分别由两个意思
tree = ET.parse(in_file)
xmlroot = tree.getroot()
object = xmlroot.find('object')
bndbox = object.find('bndbox')
xmin = bndbox.find('xmin')
xmin.text = str(new_xmin)
ymin = bndbox.find('ymin')
ymin.text = str(new_ymin)
xmax = bndbox.find('xmax')
xmax.text = str(new_xmax)
ymax = bndbox.find('ymax')
ymax.text = str(new_ymax)
tree.write(os.path.join(root, str("%06d" % (str(id) + '.xml'))))
def change_xml_list_annotation(root, image_id, new_target, saveroot, id,img_name):
in_file = open(os.path.join(root, str(image_id) + '.xml')) # 这里root分别由两个意思
tree = ET.parse(in_file)
elem = tree.find('filename')
elem.text = (img_name + str("_%06d" % int(id)) + '.jpg')
xmlroot = tree.getroot()
index = 0
for object in xmlroot.findall('object'): # 找到root节点下的所有country节点
bndbox = object.find('bndbox') # 子节点下节点rank的值
# xmin = int(bndbox.find('xmin').text)
# xmax = int(bndbox.find('xmax').text)
# ymin = int(bndbox.find('ymin').text)
# ymax = int(bndbox.find('ymax').text)
new_xmin = new_target[index][0]
new_ymin = new_target[index][1]
new_xmax = new_target[index][2]
new_ymax = new_target[index][3]
xmin = bndbox.find('xmin')
xmin.text = str(new_xmin)
ymin = bndbox.find('ymin')
ymin.text = str(new_ymin)
xmax = bndbox.find('xmax')
xmax.text = str(new_xmax)
ymax = bndbox.find('ymax')
ymax.text = str(new_ymax)
index = index + 1
tree.write(os.path.join(saveroot, img_name + str("_%06d" % int(id)) + '.xml'))
def mkdir(path):
# 去除首位空格
path = path.strip()
# 去除尾部 \ 符号
path = path.rstrip("\\")
# 判断路径是否存在
# 存在 True
# 不存在 False
isExists = os.path.exists(path)
# 判断结果
if not isExists:
# 如果不存在则创建目录
# 创建目录操作函数
os.makedirs(path)
print(path + ' 创建成功')
return True
else:
# 如果目录存在则不创建,并提示目录已存在
print(path + ' 目录已存在')
return False
if __name__ == "__main__":
IMG_DIR = "./img_val"
XML_DIR = "./temp_valxml"
# =============================================================================
# AUG_XML_DIR = "./Annotations" # 存储增强后的XML文件夹路径
# =============================================================================
AUG_XML_DIR = "./val2017" # 存储增强后的XML文件夹路径
try:
shutil.rmtree(AUG_XML_DIR)
except FileNotFoundError as e:
a = 1
mkdir(AUG_XML_DIR)
# =============================================================================
# AUG_IMG_DIR = "./JPEGImages" # 存储增强后的影像文件夹路径
# =============================================================================
AUG_IMG_DIR = "./valxml" # 存储增强后的影像文件夹路径
try:
shutil.rmtree(AUG_IMG_DIR)
except FileNotFoundError as e:
a = 1
mkdir(AUG_IMG_DIR)
AUGLOOP = 20 # 每张影像增强的数量
boxes_img_aug_list = []
new_bndbox = []
new_bndbox_list = []
# 影像增强
seq = iaa.Sequential([
iaa.Flipud(0.5), # vertically flip 20% of all images
iaa.Fliplr(0.5), # 镜像
iaa.Multiply((1.2, 1.5)), # change brightness, doesn't affect BBs
iaa.GaussianBlur(sigma=(0, 3.0)), # iaa.GaussianBlur(0.5),
iaa.Affine(
translate_px={
"x": 15, "y": 15},
scale=(0.8, 0.95),
rotate=(-30, 30)
) # translate by 40/60px on x/y axis, and scale to 50-70%, affects BBs
])
for root, sub_folders, files in os.walk(XML_DIR):
for name in files:
print(name)
bndbox = read_xml_annotation(XML_DIR, name)
shutil.copy(os.path.join(XML_DIR, name), AUG_XML_DIR)
shutil.copy(os.path.join(IMG_DIR, name[:-4] + '.jpg'), AUG_IMG_DIR)
for epoch in range(AUGLOOP):
seq_det = seq.to_deterministic() # 保持坐标和图像同步改变,而不是随机
# 读取图片
img = Image.open(os.path.join(IMG_DIR, name[:-4] + '.jpg'))
# sp = img.size
img = np.asarray(img)
# bndbox 坐标增强
for i in range(len(bndbox)):
bbs = ia.BoundingBoxesOnImage([
ia.BoundingBox(x1=bndbox[i][0], y1=bndbox[i][1], x2=bndbox[i][2], y2=bndbox[i][3]),
], shape=img.shape)
bbs_aug = seq_det.augment_bounding_boxes([bbs])[0]
boxes_img_aug_list.append(bbs_aug)
# new_bndbox_list:[[x1,y1,x2,y2],...[],[]]
n_x1 = int(max(1, min(img.shape[1], bbs_aug.bounding_boxes[0].x1)))
n_y1 = int(max(1, min(img.shape[0], bbs_aug.bounding_boxes[0].y1)))
n_x2 = int(max(1, min(img.shape[1], bbs_aug.bounding_boxes[0].x2)))
n_y2 = int(max(1, min(img.shape[0], bbs_aug.bounding_boxes[0].y2)))
if n_x1 == 1 and n_x1 == n_x2:
n_x2 += 1
if n_y1 == 1 and n_y2 == n_y1:
n_y2 += 1
if n_x1 >= n_x2 or n_y1 >= n_y2:
print('error', name)
new_bndbox_list.append([n_x1, n_y1, n_x2, n_y2])
# 存储变化后的图片
image_aug = seq_det.augment_images([img])[0]
path = os.path.join(AUG_IMG_DIR,
name[:-4] + str( "_%06d" % (epoch + 1)) + '.jpg')
image_auged = bbs.draw_on_image(image_aug, thickness=0)
Image.fromarray(image_auged).save(path)
# 存储变化后的XML
change_xml_list_annotation(XML_DIR, name[:-4], new_bndbox_list, AUG_XML_DIR,
epoch + 1,name[:-4])
print( name[:-4] + str( "_%06d" % (epoch + 1)) + '.jpg')
new_bndbox_list = []
import xml.etree.ElementTree as ET
import os
import json
coco = dict()
coco['images'] = []
coco['type'] = 'instances'
coco['annotations'] = []
coco['categories'] = []
category_set = dict()
image_set = set()
category_item_id = 0
image_id = 'ball-'
id_num = 0
annotation_id = 0
def addCatItem(name):
global category_item_id
category_item = dict()
category_item['supercategory'] = 'none'
category_item_id += 1
category_item['id'] = category_item_id
category_item['name'] = name
coco['categories'].append(category_item)
category_set[name] = category_item_id
return category_item_id
def addImgItem(file_name, size):
global image_id,id_num
if file_name is None:
raise Exception('Could not find filename tag in xml file.')
if size['width'] is None:
raise Exception('Could not find width tag in xml file.')
if size['height'] is None:
raise Exception('Could not find height tag in xml file.')
image_item = dict()
temp = str(id_num)
image_item['id'] = image_id + temp
id_num += 1
image_item['file_name'] = file_name
image_item['width'] = size['width']
image_item['height'] = size['height']
coco['images'].append(image_item)
image_set.add(file_name)
return image_item['id']
def addAnnoItem(object_name, image_id, category_id, bbox):
global annotation_id
annotation_item = dict()
annotation_item['segmentation'] = []
seg = []
#bbox[] is x,y,w,h
#left_top
seg.append(bbox[0])
seg.append(bbox[1])
#left_bottom
seg.append(bbox[0])
seg.append(bbox[1] + bbox[3])
#right_bottom
seg.append(bbox[0] + bbox[2])
seg.append(bbox[1] + bbox[3])
#right_top
seg.append(bbox[0] + bbox[2])
seg.append(bbox[1])
annotation_item['segmentation'].append(seg)
annotation_item['area'] = bbox[2] * bbox[3]
annotation_item['iscrowd'] = 0
annotation_item['ignore'] = 0
annotation_item['image_id'] = image_id
annotation_item['bbox'] = bbox
annotation_item['category_id'] = category_id
annotation_id += 1
annotation_item['id'] = annotation_id
coco['annotations'].append(annotation_item)
def parseXmlFiles(xml_path):
for f in os.listdir(xml_path):
if not f.endswith('.xml'):
continue
bndbox = dict()
size = dict()
current_image_id = None
current_category_id = None
file_name = None
size['width'] = None
size['height'] = None
size['depth'] = None
xml_file = os.path.join(xml_path, f)
print(xml_file)
tree = ET.parse(xml_file)
root = tree.getroot()
if root.tag != 'annotation':
raise Exception('pascal voc xml root element should be annotation, rather than {}'.format(root.tag))
#elem is , , ,
for elem in root:
current_parent = elem.tag
current_sub = None
object_name = None
if elem.tag == 'folder':
continue
if elem.tag == 'filename':
file_name = elem.text
if file_name in category_set:
raise Exception('file_name duplicated')
#add img item only after parse tag
elif current_image_id is None and file_name is not None and size['width'] is not None:
if file_name not in image_set:
current_image_id = addImgItem(file_name, size)
print('add image with {} and {}'.format(file_name, size))
else:
raise Exception('duplicated image: {}'.format(file_name))
#subelem is , , , ,
for subelem in elem:
bndbox ['xmin'] = None
bndbox ['xmax'] = None
bndbox ['ymin'] = None
bndbox ['ymax'] = None
current_sub = subelem.tag
if current_parent == 'object' and subelem.tag == 'name':
object_name = subelem.text
if object_name not in category_set:
current_category_id = addCatItem(object_name)
else:
current_category_id = category_set[object_name]
elif current_parent == 'size':
if size[subelem.tag] is not None:
raise Exception('xml structure broken at size tag.')
size[subelem.tag] = int(subelem.text)
#option is , , , , when subelem is
for option in subelem:
if current_sub == 'bndbox':
if bndbox[option.tag] is not None:
raise Exception('xml structure corrupted at bndbox tag.')
bndbox[option.tag] = int(option.text)
#only after parse the
if bndbox['xmin'] is not None:
if object_name is None:
raise Exception('xml structure broken at bndbox tag')
if current_image_id is None:
raise Exception('xml structure broken at bndbox tag')
if current_category_id is None:
raise Exception('xml structure broken at bndbox tag')
bbox = []
#x
bbox.append(bndbox['xmin'])
#y
bbox.append(bndbox['ymin'])
#w
bbox.append(bndbox['xmax'] - bndbox['xmin'])
#h
bbox.append(bndbox['ymax'] - bndbox['ymin'])
print('add annotation with {},{},{},{}'.format(object_name, current_image_id, current_category_id, bbox))
addAnnoItem(object_name, current_image_id, current_category_id, bbox )
if __name__ == '__main__':
xml_path = "./trainxml"
json_file = './annotations/instances_train2017.json'
parseXmlFiles(xml_path)
json.dump(coco, open(json_file, 'w'))
我们打开nanodet-m.yml,需要修改下面的参数:
python tools/train.py CONFIG_PATH
#比如我们刚刚的CONFIG_PATH则为 config/nanodet-m.yml
python demo/demo.py image --config CONFIG_PATH --model MODEL_PATH --path IMAGE_PATH
python demo/demo.py video --config CONFIG_PATH --model MODEL_PATH --path VIDEO_PATH
最终我还是选择了思路三,因为感觉这个方法虽然最简单,但是又最有效果。
我们通过遍历不同方向,模拟出各个方向上可以简单的小球的个数。同时通过距离映射函数计算出每个小球的权重,进而计算出每个方向上面的权重之和。最后选出价值最大的方向作为前进的方向,而前进的距离则根据该前进方向上面最远的那个小球距离小车的距离,算法示意图如下所示。
下面详细说一下非线性映射函数怎么计算出来。
下面是该算法检测运动中的小球的动态图: