目录
2. 接口:根据架构设计抽象接口类(类中方法设为虚函数)--camera/lib/interface
4. APP类: 将lib中的类组合成基本的功能类 -- camera/app
Apollo开源代码链接:https://github.com/ApolloAuto/apollo
本文主要讲解Apollo/modules/perception/camera中的视觉感知部分的架构和算法。
第一章 架构设计与实现
一、架构设计与实现
概述:Apollo的架构进行了良好的接口设计,层层抽象,保证程序模块化;同时,有效地利用了多态机制,保证程序的灵活性和可扩展性。
可执行程序层 集成优化部分独立开发,主程序简洁
————————
app层 单链路实现,排pipeline、优化参数,不断修改迭代
————————
lib层 单元模块独立开发,通过多态实现不同版本的lib
主要设计和实现流程:1)架构设计 2)接口设计 3)lib库的单元模块实现 4)APP类的单链路实现 5)Tools可执行程序的单功能实现。
其中,多态机制主要通过第2~4步过程实现:
a. 定义接口基类,interface中将其成员函数声明为虚函数;
b. 继承接口基类,实现不同的lib版本;
c. 将lib组合成App类,App中定义接口类类型的指针作为数据成员,以便根据配置文件灵活调用不同版本的lib;
下面对5步设计和实现流程依次讲解。
1. 架构设计
引自:https://github.com/ApolloAuto/apollo/blob/master/docs/specs/perception_apollo_5.0.md
架构图中红色框图内部为本文讲解的视觉感知架构相关内容。
- 每一个小黑框就是一个lib模块;
- 箭头链接的一条链路就是一个app;
- 最后在app前面接入camera或离线图像,后面接上可视化或结果输出就成为可执行文件tools。
2. 接口:根据架构设计抽象接口类(类中方法设为虚函数)--camera/lib/interface
备注:
- base_init_options用于派生输入参数的接口
- base_camera_perception用于派生APP的接口
- base_inference_engine 接口预留了,但未使用
- base_landmark_detector\base_scene_parser(语义分割)\base_lane_tracker 接口预留,但功能未开发
3. 库:每个lib的具体实现(通过继承接口类设计的派生类)--camera/lib/traffic_light| obstacle & feature_extractor | lane | calibrator & calibration_service | motion & motion_service | dummy
以上各个lib中的算法是该架构下的一种实作,Apollo中实现了各个lib的基本功能;开发者根据实际情况可能需要不同的实现方式以达到更好的效果,因此,开发者可以自己继承接口类实现自己的lib。
备注:以下lib代码中存在,但是架构图中未体现
•tl_preprocessor根据高精地图tfl位置投影到图像上
•motion_service & plane_motion 提供车辆位置服务
4. APP类: 将lib中的类组合成基本的功能类 -- camera/app
讨论:既然可以直接在可执行程序中组合lib、排单链路的pipeline,为什么还要在可执行程序和lib库之间再抽象一层APP类?
实际开发过程中,主程序(集成及优化)和单链路功能(算法开发)往往是不同的人开发和维护的。主程序希望给到自己手里的接口不要改动,即集成优化的程序员不想关注太多功能算法开发的工作;但是,对于功能算法开发的程序员做单链路开发调试过程中,需要不断重排pipeline、优化参数等。通过增加APP类层,一方面,使得APP类与可执行程序之间的接口尽量可以保持不变(一般只有Init和Update),保证主程序简洁,集成人员可以快速集成、排调度等;另一方面,功能算法开发人员在APP类中排pipeline、优化参数,并通过单元测试进行功能验证。
APP层串联起了大粒度(主程序层)和小粒度(单元模块能),通过app层一定程度地解耦了大粒度(主程序层)和小粒度(单元模块能)的开发,使得主程序和单模块都可以各自独立开发。
一方面,app层对下面的小粒度(lib层),通过多态机制调用不同版本的lib实现,保证lib单元模块的实现可以独立开发。在定义APP时,在数据成员中定义需要的lib对应的接口(基类)类型的指针,以便利用多态的机制根据实际需要使用lib的不同版本(Apollo默认定义的lib或者用户自己实现的lib)。
例如obstacle_camera_perception.h中定义了需要组合的lib指针类型的数据成员:
std::shared_ptr<BaseObstacleTransformer> transformer_;
std::shared_ptr<BaseObstaclePostprocessor> obstacle_postprocessor_;
std::shared_ptr<BaseObstacleTracker> tracker_;
std::shared_ptr<BaseFeatureExtractor> extractor_;
std::shared_ptr<BaseLaneDetector> lane_detector_;
std::shared_ptr<BaseLanePostprocessor> lane_postprocessor_;
std::shared_ptr<BaseCalibrationService> calibration_service_;
从而,在使用时根据输入的配置将不同的lib组合成功能APP类。
另一方面,app层对上面的可执行程序层的接口简洁且保持不变,保证可执行程序的主函数可以独立开发和维护。APP层主要是排pipeline和调参数,app层的存在就是为修改提供方便的,所以可以尽情地优化修改app类的内容,没必要写不同版本的app类实现,虽然在Apollo5.5的APP中traffic_light_camera_perception \ lane_camera_perception \ obstacle_camera_perception均派生自接口类base_camera_perception,更多应该是保证不同APP接口的美观一致,而不是为了把APP层自身做成多态的。
5. 可执行程序-- camera/tools
有了以上APP和lib就可以准备构筑完整的工程了。tools中包含的具有一定完整功能的工程了,是用来编译具有main函数入口的可执行文件的。前面加上载入图像,中间实例化APP类实现视觉感知的核心内容,后面再加上结果可视化或结果保存\发送等,就是实现了完整的单功能。
备注:somehow, 在Apollo5.5中,tools里面的可执行文件中的视觉感知核心模块并没有使用app类,而是重新组合了lib类实现视觉感知功能,可以理解tools里面的可执行文件更像是单链路debug开发用的。
另外:
• camera/common 中为 lib 和 app 公用的类 & 函数• camera/test 单元测试(基于 google -- gtest )
二、数据传递
在各个APP和lib的类中传递的是结构体,视觉感知算法中主要包括两个结构体:其中一个结构体定义所有的感知结果,在主函数中实例化该结构体,然后在从神经网络输出结果到各种后处理算法的整个流水线上对感知结果进行各种操作,包括赋值和修改其参数,因此,函数中传递的一般为该结构体的指针,以便在对其内容进行修改(CameraFrame *frame);另一个结构体为配置参数,类根据其参数不同进行不同操作,所以函数中的参数一般定义为const的引用。
1. 感知结果结构体CameraFrame
定义在Apollo/modules/perception/camera/common/camera_frame.h中,其数据成员又包括时间戳、载入数据、目标检测结果结构体OD、车道线检测结果结构体LaneLine、交通灯检测结果结构体TrafficLight。部分代码如下:
// timestamp
double timestamp = 0.0;
DataProvider *data_provider = nullptr;
//.............
std::vector<base::ObjectPtr> proposed_objects;
// segmented objects
std::vector<base::ObjectPtr> detected_objects;
// tracked objects
std::vector<base::ObjectPtr> tracked_objects;
// detect lane mark info
std::vector<base::LaneLine> lane_objects;
// detected traffic lights
std::vector<base::TrafficLightPtr> traffic_lights;
//............
以上基本的数据成员的结构体定义在Apollo/modules/perception/base目录下,如TrafficLight的结构体(traffic_light.h)、车道线的结构体LaneLine (lane_struct.h)、OD的结构体Object(object.h)。
使用示例:Apollo/modules/perception/camera/tools/obstacle_detection/obstacle_detection.cc
int main() {
CameraFrame frame; //实例化数据对象
ObstacleDetectorOptions options; //实例化option对象
ObstacleTransformerOptions transformer_options;
//............中间省略了frame.data_provider和frame.camera_k_matrix赋值.............
BaseObstacleDetector *detector =
BaseObstacleDetectorRegisterer::GetInstanceByName(FLAGS_detector); //实例化lib
BaseObstacleTransformer *transformer =
BaseObstacleTransformerRegisterer::GetInstanceByName(FLAGS_transformer);
//................中间省略了detector和transformer的init.....................
EXPECT_TRUE(detector->Detect(options, &frame)); //在数据流中传递frame的指针
EXPECT_TRUE(transformer->Transform(transformer_options, &frame)); //在数据流中传递frame的指针
//........................
}
在主程序中实例化CameraFrame frame,随后,frame的指针在从app层向lib层(如上所属,tools里不太规范,跳过了app层,直接调用了lib)、以及lib之间进行数据流和修改的传递,以实现frame相应数据成员的赋值。
2.配置参数结构体
每个类声明时随之定义了自己关联的InitOptions类,并以参数的形式传入类的Init()成员函数,用于配置类的参数。每个InitOptions都继承自BaseInitOptions类(camera/lib/interface/路径下):
struct BaseInitOptions {
std::string root_dir;
std::string conf_file;
int gpu_id = 0;
bool use_cyber_work_root = false;
};
其中,root_dir+conf_file指明了配置文件config的加载路径。在每个类Init过程中,使用读取的配置文件中参数的初始化类。
在每个类中,包含由proto生成的结构体,里面数据成员与配置文件中的数据对应,用于读取载入的config文件里的参数。
第二章 设计模式
一、概述
本章讲解对象的创建型模式,是设计模式中的重要部分。
使用工厂模式,根据配置文件动态地创建对象。工厂模式包括简单工厂模式、工厂方法模式、抽象工厂模式(Apollo中未使用)。在Apollo中工厂方法模式分别实现了基于宏定义的工厂方法模式和基于类模板的工厂方法模式。
使用单例模式,保证在进程中只有一个该类的实例。
二、简单工厂模式
简单工厂模式由一个工厂对象决定创建出哪一种产品类的实例。
深度学习框架有限,软件架构支持的深度学习框架一般不会添加,适合使用简单工厂模式。代码位置Apollo/modules/perception/inference。
2.1 UML结构
参与者 | 作用 | 在Apollo中的示例 |
Client | 调用工厂创建对象,返回类型为抽象产品的指针 | |
工厂(Creator) | 创建对象的接口,调用该函数创建对象。包含逻辑判断,根据外界给定信息,决定创建哪个具体产品的对象. | CreateInferenceByName |
抽象产品(Product) | 工厂返回的类型, 负责描述所有实例所共有的公共接口。 | Inference |
具体产品(Concrete Product) | 绑定在基类(抽象产品)指针上的具体派生类指针 | CaffeNet\RTNet\PaddleNet类,用来做深度学习推理的框架CaffeNet\RTNet\PaddleNet类都继承自Inference基类 |
class CaffeNet : public Inference{}; // Apollo/modules/perception/inference/caffe/caffe_net.h
class PaddleNet : public Inference{}; // Apollo/modules/perception/inference/paddlepaddle/paddle_net.h
class RTNet : public Inference{}; // Apollo/modules/perception/inference/tensorrt/rt_net.h
2.2 实现方法
- Client使用示例:Apollo/modules/perception/camera/lib/obstacle/detector/yolo/yolo_obstacle_detector中:
std::shared_ptr<inference::Inference> inference_; //yolo_obstacle_detector.h
inference_.reset(inference::CreateInferenceByName(model_type, proto_file,weight_file, output_names,input_names, model_root)); //yolo_obstacle_detector.cc
在使用示例中,Apollo/modules/perception/camera/test/camera_lib_obstacle_detector_yolo_yolo_obstacle_detector_test.cc读取配置文件/Apollo/modules/perception/testdata/camera/lib/obstacle/detector/yolo/data/config.pt中model_type参数,发现是"RTNet",从而生成new RTNet(proto_file, weight_file, outputs, inputs);
- 简单工厂的具体实现(Apollo/modules/perception/inference/inference_factory.cc):
Inference *CreateInferenceByName(const std::string &name,
const std::string &proto_file,
const std::string &weight_file,
const std::vector<std::string> &outputs,
const std::vector<std::string> &inputs,
const std::string &model_root) {
if (name == "CaffeNet") {
return new CaffeNet(proto_file, weight_file, outputs, inputs);
} else if (name == "RTNet") {
return new RTNet(proto_file, weight_file, outputs, inputs);
} else if (name == "RTNetInt8") {
return new RTNet(proto_file, weight_file, outputs, inputs, model_root);
} else if (name == "PaddleNet") {
return new PaddleNet(proto_file, weight_file, outputs, inputs);
}
return nullptr;
}
优点:实现方便,适合负责创建的对象比较少的场景。在Inference中,推理框架一般不会频繁增加,因此使用简单工厂模式非常合适。
缺点:上面例子中用户传给架构一个name,简单工厂根据name通过抽象工程创建预设好的具体产品。由于工厂类集中了所有实例的创建逻辑,它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了,破坏“开闭原则”。
三、工厂方法模式(基于宏定义)
很多时候框架设计者不知道未来用户要继承基类实现什么类,甚至不知道用户未来要实现的类的名字,使用上面的简单工厂无法再满足需求。
通过使用工厂方法模式,可以在架构中利用基类名直接创建对象。工厂方法定义一个抽象工厂,由抽象工厂负责定义产品的生产接口,但不负责生产具体的产品,将生产任务交给不同的派生类工厂。这样不用通过指定具体类型来创建对象了(使用基类名称即可)。
Apollo中Apollo/modules/perception/camera使用基于宏定义的工厂方法创建对象。
3.1 UML结构
参与者 | 作用 | 在Apollo中的示例 |
Client | 留给用户的接口是抽象工厂(调用抽象工厂的接口创建对象)和抽象产品(创建后的对象的所绑定的指针),程序运行时根据传入配置文件不同,利用多态机制,调用抽象工厂的指针所动态绑定的具体工厂的指针,生成相应的具体产品,并将具体产品的指针绑定在抽象产品的指针上返回给用户。 | |
抽象工厂 | 创建对象调用的接口 | ObjectFactory(Register中定义) |
具体工厂 | 继承自抽象工厂,负责生产具体产品。将抽象工厂的指针绑定在具体工厂指针上,根据需要调用相应的具体工厂以生产相应的具体产品 | ObjectFactoryYoloObstacleDetector、ObjectFactorySsdObstacleDetector、ObjectFactoryOMTObstacleTracker(宏定义统一写在Register中,在基类对应的继承类文件中展开成不同的具体工厂的定义) |
抽象产品 | 抽象工厂返回类型为抽象产品的指针 | BaseObstacleDetecor和BaseObstacleTracker |
具体产品 | 绑定在基类(抽象产品)指针上的具体派生类指针 | YoloObstacleDetector、SsdObstacleDetector、OMTObstacleTracker |
优点:工厂方法模式实现“开闭原则”,保证可扩展性。
注意:相比于一般只能创建一种抽象产品的工厂方法模式,这里设计的工厂方法模式还支持创建不同的抽象产品(在抽象工厂中主要是基于任意返回类型Any实现)。
3.2 实现方法
3.2.1 Client使用示例:
以BaseObstacleDetector为例(Apollo/modules/perception/camera/lib/interface/base_obstacle_detector.h) ,在app中(Apollo/modules/perception/camera/app/obstacle_camera_perception.cc)中使用基类接口创建对象。
通过ObjectFactory的指针使用创建对象的方法,返回的是BaseObstacleDetector类的指针。
std::shared_ptr<BaseObstacleDetector> detector_ptr(BaseObstacleDetectorRegisterer::GetInstanceByName(plugin_param.name()));
std::shared_ptr<BaseObstacleTracker>tracker_=nullptr;
tracker_.reset(BaseObstacleTrackerRegisterer::GetInstanceByName(plugin_param.name()));
实际创建的对象则是根据配置文件中载入的参数动态地创建对象,Apollo/modules/perception/camera/test/camera_app_obstacle_camera_perception_test.cc中调用配置文件/Apollo/modules/perception/testdata/camera/app/conf/perception/camera/obstacle/obstacle.pt,
detector_param {
plugin_param{
name: "YoloObstacleDetector"
root_dir: "/apollo/modules/perception/production/data/perception/camera/models/yolo_obstacle_detector"
config_file: "config.pt"
}
camera_name : "front_6mm"
}
tracker_param {
plugin_param{
name: "OMTObstacleTracker"
root_dir: "/apollo/modules/perception/production/data/perception/camera/models/omt_obstacle_tracker"
config_file: "config.pt"
}
}
从而,根据配置文件确定创建BaseObstacleDetector的派生类YoloObstacleDetector的实例,以及BaseObstacleTracker的派生类OMTObstacleTracker的实例。
YoloObstacleDetector是Apollo中对BaseObstacleDetector的实现示例,用户可以根据自己需求继承BaseObstacleDetector类,比如,生成SsdObstacleDetector,采用的方法实质和YoloObstacleDetector类似。类似地,BaseObstacleDetector为Apollo的基类接口,用户也可以根据需求定义自己的基类。
通过调用以上接口实现根据配置文件参数创建对象,这一过程依赖于基类和派生类定义中展开宏定义,生成创建对象的接口、具体工厂等。调用宏定义的方法非常简单:
对于创建YoloObstacleDetector,只需要基类定义中(Apollo/modules/perception/camera/lib/interface/base_obstacle_detector.h):
PERCEPTION_REGISTER_REGISTERER(BaseObstacleDetector); //注册基类,保证基类也是可扩展的
派生类定义(Apollo/modules/perception/camera/lib/obstacle/detector/yolo/yolo_obstacle_detector.cc)中:
REGISTER_OBSTACLE_DETECTOR(YoloObstacleDetector); //注册(已经注册过的基类的)派生类,以拓展派生类
则factory_map中添加了{"BaseObstacleDetector",{"YoloObstacleDetector",ObjectFactory *}}.
类似地,对于创建OMTObstacleTracker,只需要对于目标追踪类,在Apollo/modules/perception/camera/lib/interface/base_obstacle_tracker.h中注册基类接口:
PERCEPTION_REGISTER_REGISTERER(BaseObstacleTracker);
在其派生类定义中注册派生类:
REGISTER_OBSTACLE_TRACKER(OMTObstacleTracker);
则factory_map中添加了{"BaseObstacleTracker",{"OMTObstacleTracker",ObjectFactory *}}.
宏定义:
宏定义保证程序简洁和高度复用性,根据用户输入的不同基类名称和派生类名称展开宏定义:
A. 在每个基类接口定义后面展开对应的宏定义,实现通过基类名称索引到调用抽象工厂创建对象的接口;
B. 在每个派生类定义后面展开对应的宏定义,实现实现根据派生类名展开成具体工厂(用于生产不同的具体产品);向factory_map注册,将具体工厂指针绑定到抽象工厂的指针上,从而,在创建对象时使用[基类][派生类]的名称索引到相应的具体工厂创建对象。
3.2.2 抽象工厂 & 抽象产品 、 map & 创建对象的接口
Apollo/modules/perception/lib/registerer/registerer.h中定义了抽象工厂、通过对象名索引到具体工厂的map、注册基类的宏定义、注册派生类的宏定义。
- 抽象工厂(Creator)
提供创建对象的接口,其中,通过Any机理可以支持返回任意类型。
class ObjectFactory {
public:
ObjectFactory() {}
virtual ~ObjectFactory() {}
virtual Any NewInstance() { return Any(); } //Any是可以表示任意类型的类,Any类的定义可参考,https://www.cnblogs.com/feixue/p/boost-any.html 此处idea from boost any but make it more simple and don't use type_info.
ObjectFactory(const ObjectFactory &) = delete;
ObjectFactory &operator=(const ObjectFactory &) = delete;
private:
};
- map
维护一个Global的factory_map( static std::map<string, std::map<std::string, ObjectFactory *>> factory_map;),Register注册实际是将基类-派生类-具体工厂的映射关系加入到该map中,创建对象时通过[基类名][派生类名]可以找到对应的具体工厂。
map的具体实现:
typedef std::map<std::string, ObjectFactory *> FactoryMap;
typedef std::map<std::string, FactoryMap> BaseClassMap;
BaseClassMap &GlobalFactoryMap() {
static BaseClassMap factory_map;
return factory_map;
}
备注:Register后factory_map内的元素示例:
static std::map<string, std::map<std::string, ObjectFactory *>> factory_map;
factory_map[“BaseObstacleDetector”]["YoloObstacleDetector"]=new ObjectFactoryYoloObstacleDetector(); // 每次注册即向factory_map添加新的元素
factory_map["BaseObstacleDetector"]["SsdObstacleDetector"]=new ObjectFactorySsdObstacleDetector();
factory_map[“BaseObstacleTracker”]["OMTObstacleTracker"]=new ObjectFactoryOMTObstacleTracker();
/*
factory_map={
{“BaseObstacleDetector”,{"YoloObstacleDetector",ObjectFactoryYoloObstacleDetector *}},
{"BaseObstacleDetector",{"SsdObstacleDetector",ObjectFactorySsdObstacleDetector *}},
{"BaseObstacleTracker",{"OMTObstacleTracker",ObjectFactoryOMTObstacleTracker *}}
}
*/
- 创建对象的接口
Apollo/modules/perception/lib/registerer/registerer.h中注册基类的宏定义PERCEPTION_REGISTER_REGISTERER(base_class)如下:
#define PERCEPTION_REGISTER_REGISTERER(base_class) \
class base_class##Registerer { \ //宏定义中##为连接符号,用于把参数和其他内容连在一起
typedef ::apollo::perception::lib::Any Any; \
typedef ::apollo::perception::lib::FactoryMap FactoryMap; \
\
public: \
static base_class *GetInstanceByName(const ::std::string &name) { \
FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#base_class]; \ //宏定义中#表示字符串化,把跟在后面的参数转化为字符串
FactoryMap::iterator iter = map.find(name); \
if (iter == map.end()) { \
for (auto c : map) { \
AERROR << "Instance:" << c.first; \
} \
AERROR << "Get instance " << name << " failed."; \
return nullptr; \
} \
Any object = iter->second->NewInstance(); \
return *(object.AnyCast<base_class *>()); \
} \
static std::vector<base_class *> GetAllInstances() { \
std::vector<base_class *> instances; \
FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#base_class]; \
instances.reserve(map.size()); \
for (auto item : map) { \
Any object = item.second->NewInstance(); \
instances.push_back(*(object.AnyCast<base_class *>())); \
} \
return instances; \
} \
static const ::std::string GetUniqInstanceName() { \
FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#base_class]; \
CHECK_EQ(map.size(), 1) << map.size(); \
return map.begin()->first; \
} \
static base_class *GetUniqInstance() { \
FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#base_class]; \
CHECK_EQ(map.size(), 1) << map.size(); \
Any object = map.begin()->second->NewInstance(); \
return *(object.AnyCast<base_class *>()); \
} \
static bool IsValid(const ::std::string &name) { \
FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#base_class]; \
return map.find(name) != map.end(); \
} \
};
注册基类的宏定义的作用是:是实现通过基类名调用抽象工厂创建对象的接口。将派生类名注册(添加)在map中,并通过基类名在map中索引到具体工厂创建对象的接口。注意:这里派生类的名仍然是不可见的,是通过统一的接口调用具体工厂创建对象的接口,即先隐式地定位到FactoryMap::iterator iter = map.find(name),其中派生类名name为变量;再隐式地指向派生类对应的具体工厂创建对象的接口iter->second->NewInstance(),其中,iter->second指向具体工厂,根据派生类名name不同,将不同的具体工厂指针绑定到抽象工厂指针中 。
下面仅以基类BaseObstacleDetector和派生类YoloObstacleDetector定义中的宏展开为例说明具体实现。
基类定义中(Apollo/modules/perception/camera/lib/interface/base_obstacle_detector.h):
PERCEPTION_REGISTER_REGISTERER(BaseObstacleDetector); //注册基类,保证基类也是可扩展的
其中,BaseObstacleDetector为抽象产品(Product)。
替换上面的宏内容后代码变为:
class BaseObstacleDetectorRegisterer {
typedef ::apollo::perception::lib::Any Any;
typedef ::apollo::perception::lib::FactoryMap FactoryMap;
public:
static BaseObstacleDetector *GetInstanceByName(const ::std::string &name) {
FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector'];
FactoryMap::iterator iter = map.find(name);
if (iter == map.end()) {
for (auto c : map) {
AERROR << "Instance:" << c.first;
}
AERROR << "Get instance " << name << " failed.";
return nullptr;
}
Any object = iter->second->NewInstance(); //通过基类接口创建对象
return *(object.AnyCast<BaseObstacleDetector *>());
}
static std::vector<BaseObstacleDetector *> GetAllInstances() {
std::vector<BaseObstacleDetector *> instances;
FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector'];
instances.reserve(map.size());
for (auto item : map) {
Any object = item.second->NewInstance();
instances.push_back(*(object.AnyCast<BaseObstacleDetector *>()));
}
return instances;
}
static const ::std::string GetUniqInstanceName() {
FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector'];
CHECK_EQ(map.size(), 1) << map.size();
return map.begin()->first;
}
static BaseObstacleDetector *GetUniqInstance() {
FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector'];
CHECK_EQ(map.size(), 1) << map.size();
Any object = map.begin()->second->NewInstance();
return *(object.AnyCast<BaseObstacleDetector *>());
}
static bool IsValid(const ::std::string &name) {
FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector'];
return map.find(name) != map.end();
}
};
3.2.3 具体工厂 & 具体产品、 Register(向map中添加元素)
Apollo/modules/perception/lib/registerer/registerer.h注册派生类的宏定义PERCEPTION_REGISTER_CLASS(clazz, name)如下:
#define PERCEPTION_REGISTER_CLASS(clazz, name) \
namespace { \
class ObjectFactory##name : public apollo::perception::lib::ObjectFactory { \
public: \
virtual ~ObjectFactory##name() {} \
virtual ::apollo::perception::lib::Any NewInstance() { \
return ::apollo::perception::lib::Any(new name()); \
} \
}; \
__attribute__((constructor)) void RegisterFactory##name() { \
::apollo::perception::lib::FactoryMap &map = \
::apollo::perception::lib::GlobalFactoryMap()[#clazz]; \
if (map.find(#name) == map.end()) map[#name] = new ObjectFactory##name(); \
} \
}
其作用是定义具体工厂和注册派生类的函数:a. 在具体工厂中创建具体产品。b. 在派生类的注册器上,将具体工厂指针绑定在抽象工厂的指针上(以利用多态机制),并将派生类名注册在map中,通过派生类名在map中索引到具体工厂。
派生类定义(Apollo/modules/perception/camera/lib/obstacle/detector/yolo/yolo_obstacle_detector.cc)中:
REGISTER_OBSTACLE_DETECTOR(YoloObstacleDetector); //注册(已经注册过的基类的)派生类,以拓展派生类
对应的宏定义在基类Apollo/modules/perception/camera/lib/interface/base_obstacle_detector.h中,即
#define REGISTER_OBSTACLE_DETECTOR(name) \
PERCEPTION_REGISTER_CLASS(BaseObstacleDetector, name)
所以,以上代码替换为:
PERCEPTION_REGISTER_CLASS(BaseObstacleDetector, YoloObstacleDetector);
根据上面宏定义展开PERCEPTION_REGISTER_CLASS(BaseObstacleDetector, name)得:
- 具体工厂(Concrete Creator)和具体产品(Concrete Product):
class ObjectFactoryYoloObstacleDetector:public apollo::perception::lib::ObjectFactory { //从抽象工厂派生具体工厂
public:
virtual ~ObjectFactoryYoloObstacleDetector() {}
virtual ::apollo::perception::lib::Any NewInstance() {
return ::apollo::perception::lib::Any(new YoloObstacleDetector()); //在具体工厂ObjectFactoryYoloObstacleDetector中创建具体产品,实质还是通过new创建对应类的指针。
}
};
- Register注册
__attribute__((constructor)) void RegisterFactoryYoloObstacleDetector() { //向factory_map中注册新的派生类对应的具体工厂
::apollo::perception::lib::FactoryMap &map =
::apollo::perception::lib::GlobalFactoryMap()['BaseObstacleDetector']; //key不存在的话则创建一个pair并调用默认构造函数
if (map.find('YoloObstacleDetector') == map.end())
map['YoloObstacleDetector'] = new ObjectFactoryYoloObstacleDetector(); //将具体工厂指针绑定在抽象工厂的指针的
}
备注:
a. __attribute__((constructor))表示该方法在main函数之前运行;
b. 嵌套地定义了双层map,即BaseClassMap类型的factory_map
整个过程总结:撰写基类BaseObstacleDetector及其派生类YoloObstacleDetector时会向factory_map添加了元素{"BaseObstacleDetector",{"YoloObstacleDetector",ObjectFactory *}},其中,ObjectFactory *绑定在对应的派生类具体工厂ObjectFactoryYoloObstacleDetector *上; 调用std::shared_ptr<BaseObstacleDetector> detector_ptr(BaseObstacleDetectorRegisterer::GetInstanceByName(plugin_param.name()))生成BaseObstacleDetector类的派生类的对象,通过配置文件确定需要生成派生类YoloObstacleDetector的对象,通过Register的map索引到factory_map['BaseObstacleDetector']['YoloObstacleDetector'],即ObjectFactoryYoloObstacleDetector;调用创建对象的方法ObjectFactoryYoloObstacleDetector->NewInstance()函数内调用的是new YoloObstacleDetector()。
视觉感知模块使用的基于宏定义的工厂方法模式与基于类模板实现工厂方法模式的异同可参见:https://mp.csdn.net/editor/html/109766770
四、单例模式
单例模式的宏定义在Apollo/cyber/common/macros.h中,
#define DISALLOW_COPY_AND_ASSIGN(classname) \
classname(const classname &) = delete; \
classname &operator=(const classname &) = delete;
#define DECLARE_SINGLETON(classname) \
public: \
static classname *Instance(bool create_if_needed = true) { \
static classname *instance = nullptr; \
if (!instance && create_if_needed) { \
static std::once_flag flag; \
std::call_once(flag, \
[&] { instance = new (std::nothrow) classname(); }); \
} \
return instance; \
} \
\
static void CleanUp() { \
auto instance = Instance(false); \
if (instance != nullptr) { \
CallShutdown(instance); \
} \
} \
\
private: \
classname(); \
DISALLOW_COPY_AND_ASSIGN(classname)
#endif // CYBER_COMMON_MACROS_H_
该宏定义直接在需要单例模式的类中展开,如:
Apollo/modules/perception/camera/common/object_template_manager.h中定义的ObjectTemplateManager类中包含了 DECLARE_SINGLETON(ObjectTemplateManager);
Apollo/modules/perception/lib/config_manager/config_manager.h中定义的ConfigManager类中包含了 DECLARE_SINGLETON(ConfigManager)。
更多设计模式内容可参见经典书籍:《设计模式:可复用面向对象软件的基础》
第三章 目标检测算法
代码位置: Apollo/modules/perception/camera/lib/obstacle
一、目标检测CNN网络Detector
1. Init:加载参数
- 输入CNN的图像预处理参数:裁剪、resize,确定输入CNN网络的图像长宽);
- 感知结果后处理参数:NMS阈值、符合要求的检测框尺寸阈值等;
- 加载CNN网络结构;
2. 推理
图像在GPU上做Resize -》推理 -》获取推理结果
具体inference使用简单工厂模式介绍详见: https://blog.csdn.net/Cxiazaiyu/article/details/106570190
二、目标追踪Tracker
因为目前主流的深度学习主要还是针对单帧图片进行推理,而非对视频进行处理,因此帧间信息的利用主要是依靠后处理。
目标追踪的主要目的是在多帧之间锁定相同的目标,并利用多帧的信息对锁定后的目标的位置、速度等信息进行优化。
目标追踪的入口类为OMTObstacleTracker,继承自接口类BaseObstacleTracker。路径:Apollo/modules/perception/camera/lib/obstacle/tracker/omt/omt_obstacle_tracker.h
主要的方法是bool OMTObstacleTracker::Associate2D(const ObstacleTrackerOptions &options,CameraFrame *frame),负责将当前帧检测到的目标与已经追踪到的所有目标std::vector<Target> targets_做对比,如果该帧内的物体与已经追踪到的某一Target有足够高的相似性,则认为是同一目标,并将新目标归并到相应的Target中,随后根据新添加的测量数据更新该Target的属性。
1. 评价两目标相似性
代码路径:Apollo/modules/perception/camera/lib/obstacle/tracker/common/similar.h
评估两个object的相似性:
a. 根据CNN抽取的feature相似性:float ScoreAppearance(const Target &target, TrackObjectPtr object);
对于检测到的每个物体,CNN输出一个object_feature。计算两帧之间检测到的目标的相似性的方法定义在Apollo/modules/perception/camera/lib/obstacle/tracker/common/similar.h文件中,并提供CPU和GPU两种实现方法。
b. 根据中心点距离评价相似性:float ScoreMotion(const Target &target, TrackObjectPtr object);
c. 根据box形状差异(长宽差异)评价相似性:float ScoreShape(const Target &target, TrackObjectPtr object);
d. 根据bbox的IOU计算:float ScoreOverlap(const Target &target, TrackObjectPtr track_obj);
最后,综合考虑Motion\Shape\Appearance\Overlap的评分后给出的综合评分,使用的方法void OMTObstacleTracker::GenerateHypothesis(const TrackObjectPtrs &objects),并根据评分排序给出关联最大的可能性。
2. 运动估计
针对跟踪到的目标融合多帧的信息,给出更准确的位置、航向等。
定义了Target结构体,存储多帧融合后的目标信息,包括航向direction、位置world_center、id等,以及TrackObjectPtrs tracked_objects成员变量,即该Target在每帧中对应的objects。
融合多帧信息时针对变量的特性使用了多种滤波方法,如使用FirstOrderRCLowPassFilter滤去direction中高频变化的量、使用KalmanFilterConstVelocity过滤世界坐标系下的空间位置、使用MeanFilter处理世界坐标系下的速度、使用MaxNMeanFilter处理世界坐标系下的目标的lenght-width-height。具体的滤波方法类存储在Apollo/modules/perception/camera/lib/obstacle/tracker/common/kalman_filter.h文件中。
第四章 交通灯感知算法
一、概述
Apollo交通灯感知源码位置:Apollo/modules/perception/camera/lib/traffic_light
采用的交通灯感知方案总共分为三步:
- 首先根据高精地图+定位所提供的交通灯空间位置投影到图像上选取ROI; (preprocessor)
- 随后在ROI区域内进行深度学习的交通灯检测和分类;(detector:detection+recognition)
- 最后,进行后处理决策。(tracker,备注:该类中继承了BaseTrafficLightTracker,但实现的功能是后处理决策,并没有针对每个交通灯做ID序号的追踪,实际针对交通灯的应用需求也无需给出每个交通灯的ID序号)
二、选取ROI
第一步选取ROI不是必需的,也可以在全图上直接进行检测分类。但是,全图检测分类方案中为了保证推理的实时性,往往将原图resize成较小图后放入CNN网络,这会造成大目标变为小目标,降低检测查准率和召回率,以及分类准确率。选取ROI的优点是:节省算力、保证实时性。
三、检测分类
Apollo示例中采用两个网络分别进行检测和分类,也可以使用一个网络同时给出检测和分类结果。
四、决策后处理
这里重点讲解决策后处理。该类名为SemanticReviser,交通灯的Semantic是指交通灯的“直行、左转、右转、掉头等”;Reviser是指针对每个Semantic的交通灯,对其颜色进行修正。
该模块的作用:
- 修正深度学习给出的个别灯颜色的误分类(a. 通过单帧内相同语义交通灯的颜色进行投票 b. 根据历史帧和当前帧中交通灯颜色信息)
- 据帧间信息给出绿灯是否在闪烁状态
具体操作方法解析:
1. 首先,根据语义对当前帧内所有交通灯进行归类。
semantic_decision.cpp中入口函数为:
bool SemanticReviser::Track(const TrafficLightTrackerOptions &options,CameraFrame *frame)
该函数根据语义对一帧内所有交通灯进行归类。场景示例:一帧内可能存在2~4个直行灯,这些灯的颜色及闪烁状态是完全一致的。
将相同语义的所有灯归入一个SemanticTable. 随后,对每一个SemanticTable调用函数
void SemanticReviser::ReviseByTimeSeries(double time_stamp, SemanticTable semantic_table, std::vector<base::TrafficLightPtr> *lights)
2. 其次,对当前帧内相同语义SemanticTable的所有灯的颜色进行投票,获得该SemanticTable对应的灯的颜色,并将少数票对应的颜色更改为投票获得的颜色结果。
场景示例:一帧内4个直行交通灯中3个被检测为红色,一个被误检为绿灯,则投票得到该SemanticTable颜色为红色。
在ReviseByTimeSeries函数中,调用函数
base::TLColor SemanticReviser::ReviseBySemantic(SemanticTable semantic_table, std::vector<base::TrafficLightPtr> *lights)
3. 最后,根据当前帧SemanticTable和历史history_semantic_(对每个语义的SemanticTable维护一个SemanticTable类型的history_semantic_)决定是否修正当前SemanticTable的输出结果,并更新history_semantic_,同时,给出绿灯的闪烁状态。
3.1 修正当前SemanticTable输出
在利用帧间信息修改交通灯颜色时用到了一些先验知识:
- 如果history颜色是Red,当前颜色是yellow,则将当前颜色修正为Red, 因为一般红灯之后应该是绿灯,而不是黄灯,大概率是当前帧将红灯误检为黄灯了;
- 如果当前给出的颜色是unknown(单帧内投票时如果第一种颜色和第二种一致则设为unknow)时,直接将当前颜色设为history的颜色;
- 如果历史帧为黑色,则需要连续给出多帧一致的颜色才将当前帧颜色更新为新的颜色,否则将当前帧颜色修改为黑色。如果当前帧为黑色,历史帧为其他颜色,则将当前帧颜色更新为历史帧颜色。
其中,修改交通灯颜色调用函数
void SemanticReviser::ReviseLights(std::vector<base::TrafficLightPtr> *lights,
const std::vector<int> &light_ids,
base::TLColor dst_color)
3.2 更新history_semantic_
将当前帧的时间戳更新进history_semantic_的时间戳。时间戳是一定更新的;如果根据history_semantic_颜色修正了当前SemanticTable的颜色,则history_semantic_的颜色不变;否则,将history_semantic_颜色Update为当前SemanticTable对应的颜色。
更新history_semantic_中的颜色调用函数
void SemanticReviser::UpdateHistoryAndLights(
const SemanticTable &cur, std::vector<base::TrafficLightPtr> *lights,
std::vector<SemanticTable>::iterator *history)
3.3 判断交通灯是否是闪烁状态
在闪烁状态时,将当前帧的黑色设为历史帧灯的颜色(如黑色出现之前为绿色,则将当前帧颜色设为绿色),在SemanticTable结构体中单独设置blink状态位,并通过存储的last_dark_time_stamp和last_bright_time_stamp两个属性判断是否是闪烁状态。如果出现 亮-》黑-》亮的模式,则在第二亮出现时将blink状态为置为true。需要当前时间戳(刚出现第二次亮的状态)与last_bright_time_stamp时间间隔大于阈值(把个别帧漏检导致的亮-》黑-》亮 模式过滤掉),则为闪烁。对应逻辑为:
case base::TLColor::TL_GREEN: //确定绿色是否在闪烁
UpdateHistoryAndLights(semantic_table, lights, &iter);
if (time_stamp - iter->last_bright_time_stamp > blink_threshold_s_ && //两次亮的时间间隔大于阈值
iter->last_dark_time_stamp > iter->last_bright_time_stamp) //之前对应 亮-黑 的模式
{
iter->blink = true;
}
iter->last_bright_time_stamp = time_stamp;
ADEBUG << "High confidence color " << s_color_strs[cur_color];
break;
if (pre_color != iter->color || //brigth颜色发生变化
fabs(iter->last_dark_time_stamp - iter->last_bright_time_stamp) >
non_blink_threshold_s_) //长时间保持bright或dark状态超过阈值
{
iter->blink = false;
}