一、安装Darknet

从官网配置darknet环境,这一步也可以看yolo作者的网站来进行,

git clone https://github.com/pjreddie/darknet.git

cd darknet

make

也可以用其他基于原作者实现的地址(有详细一些的文档),然后cd到根文件下,首次使用以及修改过.c文件、.cfg文件后都需要make一下。

在darknet中,有一个Makefile,这个文件定义了网络,如果需要使用opencv或者GPU、CUDNN都需要编辑Makefile,设置等于1;默认都是0的。注意,修改过Makefile以后必须重新make才能生效!

不好下的,戳这里百度网盘,密码:7zuk。

现在,框架有了,可以尝试一下检测图片,如果要检测图片,必须下载一个预训练权重,200+mb,可以直接用命令

wget https://pjreddie.com/media/files/yolov3.weights

或者[戳百度网盘]() 注意下载下来的权重文件要放到darknet根目录下。

然后使用下面的命令尝试是否可以正常运行(单张图片检测)

./darknet detect cfg/yolov3.cfg yolov3.weights data/dog.jpg

这个命令分解开来就是:

,,,

能看到下面这样一个过程,构建网络,然后测试data/dog.jpg,输出置信度。

layer filters size input output

0 conv 32 3 x 3 / 1 416 x 416 x 3 -> 416 x 416 x 32 0.299 BFLOPs

1 conv 64 3 x 3 / 2 416 x 416 x 32 -> 208 x 208 x 64 1.595 BFLOPs

.......

105 conv 255 1 x 1 / 1 52 x 52 x 256 -> 52 x 52 x 255 0.353 BFLOPs

106 detection

truth_thresh: Using default '1.000000'

Loading weights from yolov3.weights...Done!

data/dog.jpg: Predicted in 0.029329 seconds.

dog: 99%

truck: 93%

bicycle: 99%

如果设置使用了opencv会弹出来下面的图,如果没有,这张图会保存在darknet根目录下,可以自己打开看。

作者添加了类似上面的图片的供检测的图片,可以在目录中找到,data/eagle.jpg,data/dog.jpg,data/person.jpg, 和data/horses.jpg

对于多图片检测(一个一个的检测),输入:

./darknet detect cfg/yolov3.cfg yolov3.weights

然后等构建完网络和载入权重后,会要求你输入路径,输入后回车,检测完成可以直接输入下一个。

至此,yolo已经可以正常运行了。

二、制作自己的数据集

这一部分分为两种情况,第一是自己标注数据集,可以使用labelimg来进行标记;第二就是将其他数据集格式转换成yolo可以使用的数据集。

2.1 使用labelimg标注数据集

labelimg的下载安装戳这里

界面长这样

可以支持yolo格式,选一下就好了。

在标注之前,比较省事的是把自己要标注的图片名字改成编号,比较好方便使用,因为生成的txt标注文件是和图片同名的,后面不好改。

然后设置目录,现在是中文了,直接鼠标拉框就行了。

可以在右侧设置默认的标签,这样就不用每拉一个框就要点选了。

标记的txt文件中,是yolo格式的,分别是类别(0是设置的第一个类别)和坐标。

2.2 转换其他数据集

yolo支持VOC格式的数据集,所使用的标记文件都是单独的一个txt格式的文件,存放一张图片的标记信息,所以思路就是:

如果所有标记文件都在一个文件中,那就先提取出来单独的xml文件,然后将这些文件转换为txt格式;

如果标记文件是单独的xml文件,只需要将xml文件转换为txt文件就可以了;

上次实验室使用的是DETRAC数据集,下载地址,包括三个压缩文件

DETRAC-train-data

DETRAC-test-data

DETRAC-Train-Annotations-XML

第一步

提取出voc格式的xml文件

import xml.etree.ElementTree as ET

from xml.dom.minidom import Document

import os

import cv2

import time

def ConvertVOCXml(file_path="",file_name=""):

tree = ET.parse(file_name)

root = tree.getroot()

# print(root.tag)

num=0 #计数

#读xml操作

frame_lists=[]

output_file_name=""

for child in root:

if(child.tag=="frame"):

# 创建dom文档

doc = Document()

# 创建根节点

annotation = doc.createElement('annotation')

# 根节点插入dom树

doc.appendChild(annotation)

#print(child.tag, child.attrib["num"])

pic_id= child.attrib["num"].zfill(5)

#print(pic_id)

output_file_name=root.attrib["name"]+"__img"+pic_id+".xml"

# print(output_file_name)

folder = doc.createElement("folder")

folder.appendChild(doc.createTextNode("VOC2007"))

annotation.appendChild(folder)

filename = doc.createElement("filename")

pic_name="img"+pic_id+".jpg"

filename.appendChild(doc.createTextNode(pic_name))

annotation.appendChild(filename)

sizeimage = doc.createElement("size")

imagewidth = doc.createElement("width")

imageheight = doc.createElement("height")

imagedepth = doc.createElement("depth")

imagewidth.appendChild(doc.createTextNode("960"))

imageheight.appendChild(doc.createTextNode("540"))

imagedepth.appendChild(doc.createTextNode("3"))

sizeimage.appendChild(imagedepth)

sizeimage.appendChild(imagewidth)

sizeimage.appendChild(imageheight)

annotation.appendChild(sizeimage)

target_list=child.getchildren()[0] #获取target_list

#print(target_list.tag)

object=None

for target in target_list:

if(target.tag=="target"):

#print(target.tag)

object = doc.createElement('object')

bndbox = doc.createElement("bndbox")

for target_child in target:

if(target_child.tag=="box"):

xmin = doc.createElement("xmin")

ymin = doc.createElement("ymin")

xmax = doc.createElement("xmax")

ymax = doc.createElement("ymax")

xmin_value=int(float(target_child.attrib["left"]))

ymin_value=int(float(target_child.attrib["top"]))

box_width_value=int(float(target_child.attrib["width"]))

box_height_value=int(float(target_child.attrib["height"]))

xmin.appendChild(doc.createTextNode(str(xmin_value)))

ymin.appendChild(doc.createTextNode(str(ymin_value)))

if(xmin_value+box_width_value>960):

xmax.appendChild(doc.createTextNode(str(960)))

else:

xmax.appendChild(doc.createTextNode(str(xmin_value+box_width_value)))

if(ymin_value+box_height_value>540):

ymax.appendChild(doc.createTextNode(str(540)))

else:

ymax.appendChild(doc.createTextNode(str(ymin_value+box_height_value)))

if(target_child.tag=="attribute"):

name = doc.createElement('name')

pose=doc.createElement('pose')

truncated=doc.createElement('truncated')

difficult=doc.createElement('difficult')

name.appendChild(doc.createTextNode("car"))

pose.appendChild(doc.createTextNode("Left")) #随意指定

truncated.appendChild(doc.createTextNode("0")) #随意指定

difficult.appendChild(doc.createTextNode("0")) #随意指定

object.appendChild(name)

object.appendChild(pose)

object.appendChild(truncated)

object.appendChild(difficult)

bndbox.appendChild(xmin)

bndbox.appendChild(ymin)

bndbox.appendChild(xmax)

bndbox.appendChild(ymax)

object.appendChild(bndbox)

annotation.appendChild(object)

file_path_out=os.path.join(file_path,output_file_name)

f = open(file_path_out, 'w')

f.write(doc.toprettyxml(indent=' ' * 4))

f.close()

num=num+1

return num

def bboxes_draw_on_img(img, bbox, color=[255, 0, 0], thickness=2):

# Draw bounding box...

print(bbox)

p1 = (int(float(bbox["xmin"])), int(float(bbox["ymin"])))

p2 = (int(float(bbox["xmax"])), int(float(bbox["ymax"])))

cv2.rectangle(img, p1, p2, color, thickness)

def visualization_image(image_name,xml_file_name):

tree = ET.parse(xml_file_name)

root = tree.getroot()

object_lists=[]

for child in root:

if(child.tag=="folder"):

print(child.tag, child.text)

elif (child.tag == "filename"):

print(child.tag, child.text)

elif (child.tag == "size"): #解析size

for size_child in child:

if(size_child.tag=="width"):

print(size_child.tag,size_child.text)

elif (size_child.tag == "height"):

print(size_child.tag, size_child.text)

elif (size_child.tag == "depth"):

print(size_child.tag, size_child.text)

elif (child.tag == "object"): #解析object

singleObject={}

for object_child in child:

if (object_child.tag == "name"):

# print(object_child.tag,object_child.text)

singleObject["name"] = object_child.text

elif (object_child.tag == "bndbox"):

for bndbox_child in object_child:

if (bndbox_child.tag == "xmin"):

singleObject["xmin"] = bndbox_child.text

# print(bndbox_child.tag, bndbox_child.text)

elif (bndbox_child.tag == "ymin"):

# print(bndbox_child.tag, bndbox_child.text)

singleObject["ymin"] = bndbox_child.text

elif (bndbox_child.tag == "xmax"):

singleObject["xmax"] = bndbox_child.text

elif (bndbox_child.tag == "ymax"):

singleObject["ymax"] = bndbox_child.text

object_length=len(singleObject)

if(object_length>0):

object_lists.append(singleObject)

img = cv2.imread(image_name)

for object_coordinate in object_lists:

bboxes_draw_on_img(img,object_coordinate)

cv2.imshow("capture", img)

cv2.waitKey (0)

cv2.destroyAllWindows()

if ( __name__ == "__main__"):

#print("main")

basePath="DETRAC-Train-Annotations-XML"

totalxml=os.listdir(basePath)

total_num=0

flag=False

print("正在转换")

saveBasePath="xml_test"

if os.path.exists(saveBasePath)==False: #判断文件夹是否存在

os.makedirs(saveBasePath)

#ConvertVOCXml(file_path="samplexml",file_name="000009.xml")

# Start time

start = time.time()

log=open("xml_statistical.txt","w") #分析日志,进行排错

for xml in totalxml:

file_name=os.path.join(basePath,xml)

print(file_name)

num=ConvertVOCXml(file_path=saveBasePath,file_name=file_name)

print(num)

total_num=total_num+num

log.write(file_name+" "+str(num)+"\n")

# End time

end = time.time()

seconds=end-start

print( "Time taken : {0} seconds".format(seconds))

print(total_num)

log.write(str(total_num)+"\n")

visualization_image("Insight-MVT_Annotation_Train/MVI_40212/img00396.jpg","xml_test/MVI_40212__img00396.xml")

第二部

把图片跟标记放到一起

import os

import random

import shutil

#xml路径的地址

XmlPath=r'xml_test'

#原图片的地址

pictureBasePath=r"Insight-MVT_Annotation_Train"

#保存图片的地址

saveBasePath=r"picture_test"

total_xml = os.listdir(XmlPath)

num=len(total_xml)

list=range(num)

if os.path.exists(saveBasePath)==False: #判断文件夹是否存在

os.makedirs(saveBasePath)

for xml in total_xml:

xml_temp=xml.split("__")

folder=xml_temp[0]

filename=xml_temp[1].split(".")[0]+".jpg"

# print(folder)

# print(filename)

temp_pictureBasePath=os.path.join(pictureBasePath,folder)

filePath=os.path.join(temp_pictureBasePath,filename)

# print(filePath)

newfile=xml.split(".")[0]+".jpg"

newfile_path=os.path.join(saveBasePath,newfile)

print(newfile_path)

shutil.copyfile(filePath, newfile_path)

print("xml file total number",num)

第三部

需要将前两步提取出来的文件放在VOC的文件中,固定格式

VOC2007文件下:

其中,ImageSets的目录结构为:

到此,可以使用下面的代码,生成对应的txt文件(产生test、train、trainval等文件)

import os

import random

import time

xmlfilepath=r'./VOC2007/Annotations'

saveBasePath=r"./"

trainval_percent=0.8

train_percent=0.85

total_xml = os.listdir(xmlfilepath)

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("traub suze",tr)

ftrainval = open(os.path.join(saveBasePath,'VOC2007/ImageSets/Main/trainval.txt'), 'w')

ftest = open(os.path.join(saveBasePath,'VOC2007/ImageSets/Main/test.txt'), 'w')

ftrain = open(os.path.join(saveBasePath,'VOC2007/ImageSets/Main/train.txt'), 'w')

fval = open(os.path.join(saveBasePath,'VOC2007/ImageSets/Main/val.txt'), 'w')

# Start time

start = time.time()

for i in list:

name=total_xml[i][:-4]+'\n'

if i in trainval:

ftrainval.write(name)

if i in train:

ftrain.write(name)

else:

fval.write(name)

else:

ftest.write(name)

# End time

end = time.time()

seconds=end-start

print( "Time taken : {0} seconds".format(seconds))

ftrainval.close()

ftrain.close()

fval.close()

ftest .close()

此时,VOC中的train.txt里都是各个图片的名称,并没有路径,可以使用darknet/scripts/voc_label.py来生成训练使用的txt文件,前面创建VOC文件的时候,需要指定一个年份,VOC2007,所以在训练的时候需要修改一下需要使用的名称,以及类别,如下图:

sets根据自己的需要改,比如我这次就使用2007,train,test等等。classes改成自己需要的类别,比如这次使用的数据集,只需要有一个car就可以。

三、训练

复制原来的cfg文件,比如我打算用的是yolov3.cfg,就备份一下,然后修改cfg文件,

在net下面,如果想要训练,需要把#Testing下面的 batch和subdivision给注释掉,把train下面的取消注释。

除此之外,还有每一个【YOLO】层的classes改成自己训练的类数,classes=1;

每一个【YOLO】层前还有一个filters也需要改掉,filters=(类数+5)*3,自己计算一下,比如这次只有一个car类,需要修改filters=18;

这样的classes和filters一共有三处;

然后去cfg文件夹下复制一个voc.data出来进行修改,

classes是类别数量;

train是训练集文件,就是上面那个生成的是全部训练数据集路径加名称的txt文件;

valid同上,验证集文件路径;

name是接下来要创建的txt文件的路径,文件中只需要有类的名字就行,但是注意顺序要对应;

backup是每1000次训练,会生成对应的weights权重文件,备份的地方,需要自己去创建好然后写到这里;

接下来创建一个.names的文件

一行一个类别,注意对应之前标记的顺序,比如car是0,dog是1,就要按顺序写,下面是coco数据集的.names文件

然后就是下载预训练权重,这个跟前面用来测试的那个预训练权重不一样,注意下载

wget https://pjreddie.com/media/files/darknet53.conv.74

跟之前一样放在根目录下面;

使用命令开始训练

./darknet detector train data/voc.data cfg/yolov3.cfg darknet53.conv.74 -i 3

因为我使用的是学校的服务器,没办法使用多gpu同时训练,所以用-i指定编号3GPU训练,权重文件放在了根目录,所以不需要有特殊路径,.data和.cfg文件都使用自己刚刚创建修改好的文件,注意自己的路径;

下面这个命令是使用多GPU进行训练的命令

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

还有就是如果想要生成loss图像和iou图像,则需要在训练的时候使用命令保存log文件,先创建一个文件夹,比如saveLog,然后使用命令

./darknet detector train data/voc.data cfg/yolov3.cfg darknet53.conv.74 -i 3 2>1 |tee saveLog/train.log

四、可视化训练

1、先提取log文件中的信息

#!/usr/bin/python

#coding=utf-8

#该文件用于提取训练log,去除不可解析的log后使log文件格式化,生成新的log文件供可视化工具绘图

import inspect

import os

import random

import sys

def extract_log(log_file, new_log_file, key_word):

with open(log_file, 'r') as f:

with open(new_log_file, 'w') as train_log:

for line in f:

#去除多GPU的同步log;去除除零错误的log

if ('Syncing' in line) or ('nan' in line):

continue

if key_word in line:

train_log.write(line)

f.close()

train_log.close()

extract_log('./2048/train_log2.txt', './2048/log_loss2.txt', 'images')

extract_log('./2048/train_log2.txt', 'log_iou2.txt', 'IOU')

2、可视化loss

#!/usr/bin/python

#coding=utf-8

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

#根据自己的log_loss.txt中的行数修改lines, 修改训练时的迭代起始次数(start_ite)和结束次数(end_ite)。

lines = 4500

start_ite = 6000 #log_loss.txt里面的最小迭代次数

end_ite = 15000 #log_loss.txt里面的最大迭代次数

step = 10 #跳行数,决定画图的稠密程度

igore = 0 #当开始的loss较大时,你需要忽略前igore次迭代,注意这里是迭代次数

y_ticks = [0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4]#纵坐标的值,可以自己设置。

data_path = '2048/log_loss2.txt' #log_loss的路径。

result_path = './2048/avg_loss' #保存结果的路径。

####-----------------只需要改上面的,下面的可以不改动

names = ['loss', 'avg', 'rate', 'seconds', 'images']

result = pd.read_csv(data_path, skiprows=[x for x in range(lines) if (x

False, names=names)

result.head()

for name in names:

result[name] = result[name].str.split(' ').str.get(1)

result.head()

result.tail()

for name in names:

result[name] = pd.to_numeric(result[name])

result.dtypes

print(result['avg'].values)

fig = plt.figure()

ax = fig.add_subplot(1, 1, 1)

###-----------设置横坐标的值。

x_num = len(result['avg'].values)

tmp = (end_ite-start_ite - igore)/(x_num*1.0)

x = []

for i in range(x_num):

x.append(i*tmp + start_ite + igore)

#print(x)

print('total = %d\n' %x_num)

print('start = %d, end = %d\n' %(x[0], x[-1]))

###----------

ax.plot(x, result['avg'].values, label='avg_loss')

#ax.plot(result['loss'].values, label='loss')

plt.yticks(y_ticks)#如果不想自己设置纵坐标,可以注释掉。

plt.grid()

ax.legend(loc = 'best')

ax.set_title('The loss curves')

ax.set_xlabel('batches')

fig.savefig(result_path)

#fig.savefig('loss')

3、可视化iou

#!/usr/bin/python

#coding=utf-8

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

#根据log_iou修改行数

lines = 1736397

step = 5000

start_ite = 0

end_ite = 50200

igore = 1000

data_path = './my_coco3/log_iou.txt' #log_loss的路径。

result_path = './my_coco3/Region Avg IOU' #保存结果的路径。

names = ['Region Avg IOU', 'Class', 'Obj', 'No Obj', '.5_Recall', '.7_Recall', 'count']

#result = pd.read_csv('log_iou.txt', skiprows=[x for x in range(lines) if (x%10==0 or x%10==9)]\

result = pd.read_csv(data_path, skiprows=[x for x in range(lines) if (x

, error_bad_lines=False, names=names)

result.head()

for name in names:

result[name] = result[name].str.split(': ').str.get(1)

result.head()

result.tail()

for name in names:

result[name] = pd.to_numeric(result[name])

result.dtypes

####--------------

x_num = len(result['Region Avg IOU'].values)

tmp = (end_ite-start_ite - igore)/(x_num*1.0)

x = []

for i in range(x_num):

x.append(i*tmp + start_ite + igore)

#print(x)

print('total = %d\n' %x_num)

print('start = %d, end = %d\n' %(x[0], x[-1]))

####-------------

fig = plt.figure()

ax = fig.add_subplot(1,1,1)

ax.plot(x, result['Region Avg IOU'].values, label='Region Avg IOU')

#ax.plot(result['Avg Recall'].values, label='Avg Recall')

plt.grid()

ax.legend(loc='best')

ax.set_title('The Region Avg IOU curves')

ax.set_xlabel('batches')

fig.savefig(result_path)

五、训练参数解释

六、F&Q

关于什么时候停止训练?

一般在训练过程中,可以看到下图,当avg_loss在0.07的时候就可以停止了,或者当很一段时间avg_loss不再变化了,也可以停止训练。

yolov3安卓实现_从零实现YOLOv3相关推荐

  1. yolov3安卓实现_重磅!MobileNet-YOLOv3来了(含三种框架开源代码)

    点击上方" 重磅干货,第一时间送达 前戏 本文介绍一类开源项目: MobileNet-YOLOv3.其中分享Caffe.Keras和MXNet三家框架实现的开源项目. 看名字,就知道是Mob ...

  2. yolov3与yolov4效果对比_知识精讲 | Yolov3和Yolov4核心内容、代码梳理_创事记(5)

    Dropblock的研究者与Cutout进行对比验证时,发现有几个特点: 优点一:Dropblock的效果优于Cutout 优点二:Cutout只能作用于输入层,而Dropblock则是将Cutout ...

  3. yolov3前向传播(二)-- yolov3相关模块的解析与实现(二)

    yolov3相关模块的解析与实现(二) 接上一篇 三.上采样函数 作用:用于将特征图扩展到想要的尺寸大小,和其他特征叠加到一起使用. 上采样的方法为近邻差值法 上采样函数的实现 # 定义上采样函数 d ...

  4. yolov3损失函数改进_基于改进损失函数的YOLOv3网络

    目标检测具有广阔的发展前景和巨大的商业价值, 已经成为国内外相关从业者的研究热点, 在智能安防.自动驾驶等领域具有广泛应用. 经典的目标检测方法有Dalal于2005年提出的基于HOG特征的检测方法[ ...

  5. yolov3权重_目标检测之 YOLOv3 (Pytorch实现)

    1.文章目的 Github上已经有YOLOv3 Pytorch版本的实现,但我总觉得不是自己的东西直接拿过来用着不舒服.想着自己动手丰衣足食,因此,本文主要是出于学习的目的,将YOLO3网络自己搭建一 ...

  6. yolov3网络结构图_目标检测——YOLO V3简介及代码注释(附github代码——已跑通)...

    GitHub: liuyuemaicha/PyTorch-YOLOv3​github.com 注:该代码fork自eriklindernoren/PyTorch-YOLOv3,该代码相比master分 ...

  7. yolov3安卓实现_YOLOv3 的 TensorFlow 实现,GitHub 完整源码解析

    来自华盛顿大学的 Joseph Redmon 和 Ali Farhadi 提出的YOLOv3 通过在 YOLO 中加入设计细节的变化,这个新模型在取得相当准确率的情况下实现了检测速度的很大提升,一般它 ...

  8. yolov3为什么对大目标检测不好_从YOLOv1到YOLOv3,目标检测的进化之路

    引言:如今基于深度学习的目标检测已经逐渐成为自动驾驶,视频监控,机械加工,智能机器人等领域的核心技术,而现存的大多数精度高的目标检测算法,速度较慢,无法适应工业界对于目标检测实时性的需求,这时YOLO ...

  9. 安卓加java完成登录_从零学习安卓自动化(java+appium方向):完成登录操作+一个主流程(四)...

    首先要分析软件步骤 从大的方面来说 第一步要登录成功,第二步主流程选择购买一个课程(我太穷了,所以支付的时候选择取消订单代表购买成功了) 一登录 1.1启动软件 采坑点:有的时候你安装一个新版appi ...

最新文章

  1. html css js实现快递单打印_html+css+js实现计算器
  2. ETOPS:双发飞机延伸航程运行
  3. MySQL SQL模式(Mode)
  4. python关联分析代码_1行代码实现关联分析(Apriori)算法
  5. Http权威指南学习研究
  6. 【BZOJ-2435】道路修建 (树形DP?)DFS
  7. css-动画-transition-过渡动画
  8. linux开源游戏_2014年杰出的开源和Linux游戏
  9. ACCESS中通过邮件收集数据
  10. 最近面试 Java 后端开发的感受!现在的环境太。。。
  11. 使用eclipse进行debug
  12. win10简单方法安装杜比v4音效!win10 1909适用!
  13. 与机器对话,阿里达摩院挑战新一代人机对话技术
  14. 数据可视化软件进阶版--BI大屏
  15. 前端——将png图片做成icon
  16. 微信公众号 - Java推送公众号模板消息给用户
  17. 光电容积脉搏波描记法PPG
  18. 计算机基础知识比赛主持稿,计算机基础技能大赛.doc
  19. SpringCloud多线程链路追踪
  20. 免费的且功能强大的截屏软件---Snipaste

热门文章

  1. 第三次学JAVA再学不好就吃翔(part23)--private和this
  2. 初识Mysql(part14)--我需要知道的6个关于创建表的小知识
  3. python质数n以内_用Python寻找前n个质数
  4. Sklearn参数详解--决策树
  5. TypeScript 的命名空间 namespace
  6. 从ngrx store里selector出来的Observable,执行subscribe的单步调试
  7. fixture.detectChange是如何触发Component view的ngOnInit钩子的
  8. SAP Spartacus cx-table的fosuable和firstFocusable属性
  9. SAP Spartacus org unit table不同区域focus然后回车的行为差异
  10. scss里父选择器的标识符