阅读openNi的个人小结

阅读openNi的个人小结

我的类图

流程图概览

大致概览上来说,整个项目的结构是一个标准的中间件结构,上下层分别面向开发者和驱动层,所以圆框内的我略过,用了相对稳定和结实的散在C_API口,为了应对不同人群的胃口,打散了这些功能口的面向对象特性以后又多做了几层封装来包装这些接口;
1是为上下层的书写提供了一个标准和文档注释的说明整理地方
2是为上下层的用户提供了更多的选择和设计可能
3原本的设计用意是散在接口的上下层做库的包装,以上或者以下均可见,驱动层的架构的驱动仅仅是为接口的统一规格实现设计的中间件。如果涉及到加密,我认为主要的openNi实现库实行即可。
同时项目具备良好的跨平台性,实现连同大量的STL工具类也被封装了起来,放在了xnLib中,图中我标注在右边。
相对来说较重要的有
Event,(事件机制)
Lockable,LockGuard,CriticalSection,(线程标示,生命周期线程锁,临界区)
Hash,StringHash,(哈希映射表)
List,PriorityQueue,(容器)
ErrorLogger(日志收集器)
整个项目中的线程间问题,索引表以及其他的优质的跨平台实现是这个类库实现的。

建议的话我个人认为层间交互改为粘合剂语言而不使用C直接暴露接口,脚本语言如lua进行相关的库间数据传递及库的选择链接更具有安全和可扩展性,运行错误的情况在相应的胶合剂语言中可以直接get数据的错误情况,也更适合做多驱动选择时库遍历的链接的直观逻辑当发生错误时也不会抛出异常到系统一级,防止了最坏情况时的软件跳出,为用户体验做一个保险,当然,会稍微的损失一点效率,以及带来更复杂的架构维护和少量的转接口文件,相较与枚举的驱动功能属性接口,甚至可以直接通过方法的使用判断方法是否存在。简小了功能添加时的改动范围。

驱动层

首先梳理底层结构,底层的散在C_API比较特殊的是要提供的不仅是调用的一层,还需要回调,所以相对上层的稍稍复杂。
大致上在funcs结构体中的大量函数分为stream,Device,Driver。分别实现相关的系统功能,集线到底层的话就划分成了三个基类,分别是XXXBase…到了架构内部使用时这些接口用DriverHandler做了一层浅封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

struct Funcs
{
// As Driver
void (ONI_C_DECL* oniDriverCreate)(OniDriverServices* driverServices);
void (ONI_C_DECL* oniDriverDestroy)();
OniStatus (ONI_C_DECL* oniDriverInitialize)(OniDriverDeviceConnected connectedCallback, OniDriverDeviceDisconnected disconnectedCallback,
OniDriverDeviceStateChanged deviceStateChangedCallback, void* pCookie);
void (ONI_C_DECL* oniDriverRun)();
OniStatus (ONI_C_DECL* oniDriverTryDevice)(const char* uri);



class DriverHandler
{
private:

Funcs funcs;
XN_LIB_HANDLE m_libHandle;
bool m_valid;

public:
DriverHandler(const char* library, xnl::ErrorLogger& errorLogger);
~DriverHandler();
bool isValid() const {return m_valid;}

void create(OniDriverServices* driverServices) const
{

(*funcs.oniDriverCreate)(driverServices);
}
void destroy() const
{

(*funcs.oniDriverDestroy)();
}

设计模式上在OniCProperties中使用了枚举类型的属性定义,针对不同的设备,在此注册相关的功能,然后在自己的Base实现中在getProperty,setProperty中实现特殊的功能,此处也可以将这个属性结构进行单独的类归纳,然后分别让streamBase等等实现多重继承接口,我个人认为会更加简洁。

大致上来说驱动的模块的主题逻辑流程集中在初始化的过程中,所以先说下初始化的时候驱动模块的使用情况
其中管理及统筹这些驱动,并且实现多设备环境的类主要是Context,可以理解为上下文,环境,由于不是直接面向用户,所以使用g_context的全局用法,相对单例来说,缺少了一些全局的安全性但是对于暴露给用户层的散在接口和OpenNI头文件来说,这一部分是隐藏的,所以没有关系,其中的init方法是,标准的全局驱动层开始调用并且执行工作的主要代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
m_initializationCounter++;
if (m_initializationCounter > 1)
{
xnLogVerbose(XN_MASK_ONI_CONTEXT, "Initialize: Already initialized");
return ONI_STATUS_OK;
}

XnStatus rc = resolvePathToOpenNI();
if (rc != XN_STATUS_OK)
{
return OniStatusFromXnStatus(rc);
}

rc = configure();
if (rc != XN_STATUS_OK)
{
return OniStatusFromXnStatus(rc);
}

s_valid = TRUE;
rc = loadLibraries();
if (rc == XN_STATUS_OK)
{
m_errorLogger.Clear();
}

函数上分为三步,找到openNi库和配置文件,根据配置文件确定驱动库路径及特殊的加载路径的识别,最后进行相关库的加载绑定设备状态回调在回调中进行了m_devices的增删改减管理及库的初始化。以上得到了可执行的驱动对象并纳入了m_deviceDrivers容器管理,等到执行openDevice时,使用所有的已得到的devices对象配对Drivers对象,再通过try(),driver对应的open实现进行设备的初始化,原本就有devicesList的管理,所以多设备的情况和代码是完全支持的,只是在目前的实例里面没有使用而已。在架构图中,DriverHandler主要被Device和Stream使用,正向调用的函数就不赘述了,回调有device的连接,断连,状态改变,stream的新帧(最关键),以及属性改变事件。

其中newFrameCallBack是主要工作流程中的主线。

主要逻辑层

从类图上我们可以看到大致的类的复杂度和集团情况,大致上从集团关联上来说
(无主要实现,仅做转接口作用的我直接忽略了一些)

Context(全局上下文实现集合)

Recoder,FileRecoder,RecordAssembler,Memento(录像功能的实现类)
其中对应的播放器实现作为一个常驻驱动存在,oniFile

VideoStream,FrameHolder,SyncedStreamsFrameHolder,StreamFrameHolder(数据流的主要处理和多态管理)

Sensor(介于驱动层和流数据之间的事件驱动数据管理,回调处理绑定)

Device(创建,初始化,驱动层功能,管理设备范畴内的stream及sensor,向context包装传递使用接口)

DriverHandler(驱动层散C口包装接口工具)

FrameManager(帧数据池处理小工具类)

其中有大量的优秀小类设计及结构体设计我没有提及,涉及到设计本身散在有大量的跨平台线程管理,生命周期式的线程锁,临界区,hash容器,事件注册及传递,跨平台线程间消息队列,日志管理器的优秀实现,info子类信息结构体,由于内容量大,且偏向工具我不赘述。

抛开功能从模块设计上讲,较为突出的有几个地方。
1.帧持有接口 FrameHolder实现
(1)本身的readFrame调用是Stream调用的接口,但是由于接口存在异线程需求,形成了纯虚实现的FrameHolder接口,进行了Stream口数据输出模块的简易分割,而又不失整体性和易用性。
(2)为其他的功能接口提供了可扩展的读取实现口,包括我绘制图形所需要的双buffer口提供了良好的实现地点。

2.录像 Recoder的虚实现,主类,及装配类,及memento未实现功能类,形成了一个及其便于文件读写调试的整体装配模式结构,并具备一定读写错误修正的能力。
(1)

1
2
3
4
5
6
7
8
9
#define EMIT(expr) 
if (ONI_STATUS_OK == (m_assembler.emit_##expr)){
if (ONI_STATUS_OK != m_assembler.serialize(m_file)){
return;
}
}
else{
return;
}

实际上使用时expr换成了RecordType中的枚举,所以在真正的装配类assembler中函数的命名与原本的命名规则完全不一致变成了emit_RECORD_NODE_ADDED这样形式的名字,每段以这样的宏在写入文件阶段装配分解为写入文件带来了良好的异常处理机制。

(2)
搭配了undoPoint这样的储存记录点的机制,确保文件的头尾格式和相关重要部分不至于缺失。

1
2
3
4
5
Memento undoPoint(this);
EMIT(XXX)
undoPoint.Reuse();
EMIT(XXXX)
undoPoint.Release();

(3) FileRecoder中 线程+消息回调的处理结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// The main function of Recorder's thread.
static XN_THREAD_PROC threadMain(XN_THREAD_PARAM pThreadParam);

// Obtains the next message from the queue of messages and executes an
// action associated with that message.
void messagePump();

// Sends a message to the threadMain.
void send(
Message::Type type,
VideoStream* pStream = NULL,
const void* pData = NULL,
XnUInt32 propertyId = 0u,
XnSizeT dataSize = 0u,
int priority = ms_priorityNormal)
;


// Message handlers:
void onInitialize();
void onTerminate();
void onAttach(XnUInt32 nodeId, VideoStream* pStream);
void onDetach(XnUInt32 nodeId);
void onStart (XnUInt32 nodeId);
void onRecord(XnUInt32 nodeId, XnCodecBase* pCodec, const OniFrame* pFrame, XnUInt32 frameId, XnUInt64 timestamp);
void onRecordProperty(
XnUInt32 nodeId,
XnUInt32 propertyId,
const void* pData,
XnSizeT dataSize)
;

然后,如果我们以后对文件写入的格式有所改动的话,遵循原本的装配模式,加入自己的需求信息及文件头以及在原本的消息机制基础上创建一条用于加密的线程及相关队列也是可以的。

数据流向

研读之前给我定了一些查阅的目标任务,很遗憾我一开始看得很散,东一摊西一摊,所以相关的东西能讲的稍稍有限,觉得有一个比较有意思的数据流的传递过程及中途的拷贝,这个能从微观上体现出项目的主要时序和流程结构,就跟大家稍稍理一下:
当然,数据一定是先从底层驱动开始流动,那么以mars为例,实际上,我们写的驱动转接层结构我已经讲过了,有XXXBase的基类,向下延伸实现到这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

void MarsStreamImpl::mainLoop()
{
m_running = TRUE;
while (m_running)
{
int width, height;
void *data = populateFrameBuffer(width, height);

LARGE_INTEGER qpc = {0};
QueryPerformanceCounter(&qpc);
double timestamp = static_cast<double>(qpc.QuadPart - m_perfCounter) / m_perfFreq;

List<BaseMarsStream *>::ConstIterator iter = m_streamList.Begin();
while( iter != m_streamList.End())
{
if (((BaseMarsStream *)(*iter))->isRunning())
{
((BaseMarsStream *)(*iter))->frameReady(data, width, height, timestamp);
}
++iter;
}
xnOSSleep(40);
}
return;
}

当然代码一贴就相对来说容易理解,我们的中间件也是一个线程,读取帧放置在了populateFrameBuffer(width, height)中,很遗憾在这里已经做了一次拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void *MarsStreamImpl::populateFrameBuffer(int &buffWidth, int &buffHeight)
{
buffWidth = 0;
buffHeight = 0;
if (m_sensorType == ONI_SENSOR_COLOR)
{
if (m_pFrameBuffer.color)
{
buffWidth = DEFAULT_COLOR0_RESOLUTIONX;
buffHeight = DEFAULT_COLOR0_RESOLUTIONY;

short *imageData = (short *)malloc(DEFAULT_COLOR_RESOLUTIONBUF);
int rs = LibDepth_RcvVideoFrame(imageData, DEFAULT_COLOR_RESOLUTIONBUF);
if (rs != LDV_SUCCESS)
{
return NULL;
}

/* YUYV to RGB888 */


free(imageData);
imageData = NULL;

return reinterpret_cast<void *>(m_pFrameBuffer.color);
}
}
else if (m_sensorType == ONI_SENSOR_DEPTH)
{
if (m_pFrameBuffer.depth)
{
buffWidth = DEFAULT_DEPTH_RESOLUTIONX;
buffHeight = DEFAULT_DEPTH_RESOLUTIONY;

FrameData_t *frame_data_p = (FrameData_t *)malloc(sizeof(FrameData_t) * DEFAULT_DEPTH_RESOLUTIONX * DEFAULT_DEPTH_RESOLUTIONY);
int rs = LibDepth_RcvDepthFrame(frame_data_p, buffWidth, buffHeight);
if(rs != LDV_SUCCESS)
{
return NULL;
}


}
}
else
{
/* for Get IR data,and the same as Depth */
}

return NULL;
}

LibDepth_RcvVideoFrame从命名分格就能猜出这个是驱动层的接口
相对来说这次的拷贝是为了脱离驱动层得到一个独立的可处理的数据

接下来就进了frameReady();我这里暂且以Depth为例子来说,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

void DepthMarsStream::frameReady(void *data, int width, int height, double timestamp)
{
if (data == NULL)
{
return;
}
OniFrame *pFrame = getServices().acquireFrame();



XnUInt16 *data_in = reinterpret_cast<XnUInt16 *>(data);
if (m_pStreamImpl->getImageRegistrationMode() == ONI_IMAGE_REGISTRATION_DEPTH_TO_COLOR)
{
//copyDepthPixelsWithImageRegistration(data_in, width, height, pFrame);
copyDepthPixelsStraight(data_in, width, height, pFrame);
}
else
{
copyDepthPixelsStraight(data_in, width, height, pFrame);
}

raiseNewFrame(pFrame);
getServices().releaseFrame(pFrame);
}

所以copyDepthPixelsStraight成了数据拷贝的第二次

然后再到raiseNewFrame从命名上讲,就能感觉到这个函数的传递工能以及下面的传递结构实现并不会常规走,所以,很显然的这是一个保存的函数指针的回调触发,所以初始化的时候将什么东西初始化,实际上是在createStream()使用的的时候也就是流创建时,最后一路小找就能找到

1
2

m_sensors[sensorType]->setDriverStream(streamHandle);

所以这个逻辑实际上在创建时合理地将stream通过Sensor同底层的驱动回调绑在了一起,所以我对sensor的理解更倾向于我上面写的理解而字面意义的价值相对较轻

绑完回调一路找发现了这样的传递

1
2
3
4
5
6

void ONI_CALLBACK_TYPE Sensor::newFrameCallback(void* /*streamHandle*/, OniFrame* pFrame, void* pCookie)
{
Sensor* pThis = (Sensor*)pCookie;
pThis->m_newFrameEvent.Raise(pFrame);
}

1
2

xnl::Event1Arg<OniFrame*> m_newFrameEvent;

所以这个时候使用了xnLib里面的事件设计机制,然后一路省略到了stream的streamNewFrame()越来越接近上层的口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

void VideoStream::newFrameThreadMainloop()
{
XnStatus rc = XN_STATUS_OK;
// Wait on frame
while (m_running)
{
rc = xnOSWaitEvent(m_newFrameInternalEvent, XN_WAIT_INFINITE);
if ((rc == XN_STATUS_OK) && m_running)
{
m_newFrameEvent.Raise();
// HACK: To avoid starvation of other threads.
xnOSSleep(1);
}
}
}

所以不仅外层传来的是事件机制,内层也独立了一套,这套事件机制其实作用于从stream中分割出去的虚实现接口FrameHolder
直接作用在processNewFrame,然后再反馈给stream主体,这样进行了一次次的数据交互循环,
当我们readFream调用时,stream把实现同样虚到了Holder的口中得到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// Verify called with relevant stream.
if (pStream != m_pStream)
{
return ONI_STATUS_BAD_PARAMETER;
}

// Make sure frame holder is enabled.
if (!m_enabled)
{
*pFrame = NULL;
return ONI_STATUS_ERROR;
}

// If frame already exists, wait() will return immidiately.
m_pStream->waitForNewFrameEvent();

// Return the last frame and set it to NULL.
lock();
*pFrame = m_pLastFrame;
m_pLastFrame = NULL;
unlock();

return ONI_STATUS_OK;

所以相对来说,用户层的使用在这里因为这次拷贝和等待,变得与底层的数据真正分离,当然这样的考虑基于安全性来说很正确。