Unreasonable Serialization 十二月 29th, 2011
Computer vision里使用二进制文件格式来存储文件是一个极其普遍的现象。OpenCV内部本来实现了XML接口,所以默认的Face classifiers(Adaboost)都是xml结构,最早的时候是文本格式,后来改成XML格式是出于xml的灵活性,同时文件本身因为结构标签化而变得易懂。
有时候我们存储的模型文件比较简单,比如PCA或者LDA,也就是一个或几个vector,类型还是一致的。SVM其实也差不多,一个文件头,一堆SV,这些地方使用txt格式存储无论从实现,和可读性上都说得过去。因为并非那么复杂,也勿需一些复杂的库。有人或说,处理txt文件性能不够好,如果小心的pack成二进制文件,一次fread就能完成所有工作。也有些人似乎害怕别人看到模型文件里的真正内容一样,似乎对于自己技术有一种自恋的戒心,也是,这林子大了什么鸟都有,防之又防防不胜防。于是铺天盖地的都是一打开都是乱码的文件以及交织着各种fread,fscanf等等的混乱代码。
存储为二进制格式没有关系,是否要写一个相应的说明文档呢?文件是在什么处理器,多少位的机器上存储的?什么编译器?什么语言?哪个实现?往一个文件里面写char, int, float, double,然后还会突然遇见往里面写bool的,我的意思是,这样写起来多麻烦啊。要么你直接用Python来pickle,或者找一个专门的库,tpl之类的。看一下文档就行,也不用自己写多少代码了,性能也不会比你自己实现差。
我Google了一下,似乎关于这个topic的资源确实不多。有时候很容易被人误解为是要一个类数据库之类的东西,如果使用C/C++,中间夹一个数据库还需要通信,太鸡肋了。简洁有力的比如tpl很好,但费针对性,Computer vision里面遇到的case无非存储一些配置文件,模型文件(一般都是文件头加上一对向量数据),有时候会遇到cascade结构的,其实也是一个头加一堆数据,有点像是数组实现的静态树。存储这样的东西需要多么复杂的东西么?
如果平台将来是固定的,比如你只在32位的单一处理器PC上运行,或者16位的嵌入式系统里,原声的二进制无非是高效的,怎么简洁怎么来。如果需求在这个之上,你需要一定的跨平台能力并且在性能上不损失太多,可能真的缺少一个针对性的小lib。
我实在是被手头这份代码在这些方面的疏忽弄得闹心,我打算借鉴一些好的库,实现一个小lib。稍后再进行详细讨论。
就这些。
一次试验 十二月 28th, 2011
最近在重写一个人脸检测器,流程比较简单,加载一系列的模型和分类文件,然后输入图片进行检测。其中模型文件有两类,一类是正面分类的(是人脸Or不是人脸),另外一类是负分类(不是人脸Or可能是人脸),这里的正负是私自取的名字,为了书写方便。其中还涉及到一些预处理和后处理,算法框架不算复杂,但之前写的时候可能因为仓促,内部实现写了很多类,外部接口用的是IMPL方式,层次关系大概是这样的:Base class –> Interface Class –> IMPL class。在IMPL class内部有包含八种其他的类:两种IMAGE class,四种RECT class,两种Classifier class。拜C++所赐,IMPL class的舒适化方法写了七八种,Classifier的初始化方法写了四五种,都是考虑从不同的源来加载和初始化。不知道原作者是如何想的,好似在写一些小类的时候无法摆脱“写库”的冲动,动不动就写了很多根本就不会设计的方法,或者一个十分小的RECT,居然也写getter和setter。刚开始看到的代码的时候,并不了解其中的流程,随便在Emacs里打开一个文件,第一印象就不好。之前看programmers at work, 大牛说你站在10步开外看一份代码,如果觉得代码很丑,那这代码就很可能有问题,而且是算法逻辑上的问题。当然,我手头上的这份原始码逻辑上应该没什么问题,仅丑而已。
所以我就开始给这份代码动手术,整容。
前些天,在回家的路上突然体味到DRY和KISS的重要,以至于我厌恶起那些动辄就罗列设计模式的教条式方式。我并不否认使用设计模式可能会使一个系统更加的灵活,但是写程序中最重要的事情莫过于你知道你在做什么?你的目标是什么?你有时候还要想你设计的系统是什么样子的,解决的是什么问题,应该具有什么功能等等,这些或许是一些老生常谈,但我看到很多人在实际工作的时候确实没有实现将这些问题想明白再动手的习惯。当然,有些问题还是会在实现的过程中暴露出来,但这是一个过程,你越习惯这样的方式,这个过程就会越来越顺利。
我这里就是要给出一个我刚制造的反例,在实验过程中暴露了设计的问题。
我将系统分割为三部分,接口,算法单元和数据处理单元。其中接口指的是开放的API,其中接着沿用IMPL的方式,但是提供原生的C API以及一个C++ Class作为Wrapper。去掉原始码中的基类,理由是基类的存在无异于鸡肋。我觉得想要给系统提供扩展性这个想法是没错的,但是要考虑到系统以后要以什么样的方式扩展,对应的给予扩展机制。无端的加一个基类,给我的感觉就是,好像这里将会有很多人脸检测器的实现,到时候可以多态的进行调用…………在我而言,发生这种事情的可能性太小,即使将来需要多个检测器,最好的方式还是模块化吧。这样无论调试还是复用都会更加灵活方便。
分割出一个算法单元来,基于的考虑是:大多数基于分类的检测算法里,最核心的算法往往很简单也很通用,卷积点乘等等。我这里其实也差不多,外加一个预处理和后处理的算法,这部分如果单独隔离开,以后更改或者复用也会更加的方便,没有必要使用一个类将这些算法和数据块包装在一起。
需要处理的数据比较多,比如IMAGEs,RECTs,Classifiers,Models等等,如何串联处理单元和数据单元呢?我当时的想法是,在中间加一层纯的数据结构,数据单元的功能是在这些数据结构上做操作,不涉及其他的数据单元,彼此独立性较高。所以最后成了如下结构:
proc module <- structs –> struct1_impl,struct2_impl,struct3_impl…
这个结构搭建起来之后,我发现我忽略了一个问题。这个抽象实现和代码实现中间有点脱轨。因为结构都在structs 里面,之后的每一个单独实现都需要include这个头文件,这个造成了不必要的编译压力。同时这里违反了高内聚和低耦合的原则,虽然我试图通过这种划分减少减少耦合从而是整个代码在结构上更加接近Plain的模式,但中间使用structs链接的方式导致每一个数据结构和自己的实现分离了,这在以后,会给代码维护造成一定的困难,同时对于单独的结构而言,违反了高内聚的原则,在形式上,比如RECT和它自身的一些方法没有必要在两个地方里面实现。单独在一个地方解决会更加紧凑。
我依然坚持最初的想法,将系统分割为那几个部分,但是需要简化的部分是中间联系处理模块和数据模块的结构。其实并非所有的结构都需要放在structs里面,最后的设计其实就是一个结构,这个结构正好对应处理模块的需求,不多也不少。右边的结构变成了散点分布。虽然也是各种IMAGE,RECT实现,但是做到最简,并且数据和方法完全隔离,虽然有时候觉得这样增加了一个传递参数,但是,总觉得这样层次更加鲜明,所以还是这样去做了。另外这样做的原因是想为之后的数据缓存和并行加速提供更大的灵活性。
现在的结构就是这样的。中间走了点弯路,但也收获了一些东西。最后说一个在看programmers at work时看到的笑话,关于匈牙利命名法,很多编程规范里都有,其实这最开始是一个笑话,说一个人写的变量让代码变得可读性非常差,就像使用匈牙利语写的一样。后来匈牙利法变成了一系列书写name的规则,虽说代码是会给别人看和理解的,但是知道名字便能读懂代码并非有直接联系,代码代表一系列的逻辑运算,我觉得在一些难懂的地方写好注释会比写冗长的名字更加漂亮。阅读lcc源代码就深有体会,代码非常简洁漂亮,那些名字都短到极致,但是你仍然不用费多大的劲就能看懂它,为什么?因为作者写了一本解释这个代码的书……汗吧。
就这些。
Tips record 十二月 22nd, 2011
在C或者C++源代码里查错或者使用一些技巧的时候,很可能要用到一些编译器处理的中间结果。这里记录一下,以便以后参考。
记录之前查了一下,有很多资料可考,但是没有和具体的例子联系起来,这样我以后看到了也不知道我当初为什么用到这个,所以我还是打算写一下,这里,这里,这里,等,给了一些其他参考链接。
上面说的中间结果是来自于预处理、编译、汇编、链接这四个地方的,关于其他的比如细致的或者运行时的一些中间结果不涉及,为了记录的速度,只考虑GCC,多文字,少代码。我的出发点是一些零散的日常需求,比如,有时候要定义一些类似的结构,比如
这个时候我不清楚,unsigned int这个参数能否成功的传递进去,这里就需要预处理之后的结果,在GCC下面只需要加一个-E选项就可以得到预处理之后的结果。
gcc –E test.c
其实如果熟悉预处理的话,这里就不用看了。我没有查看标准,但在GCC下面和MSVC下面测试,上面代码能按照我想象的工作,那么这就有意思了。也许有人认为用宏来这样写结构作用不大,比如这个Image结构,成员很少,我写的时候基本上也是走极简的路线,一点多余的东西都不肯往里面放,使用宏的话不但可以少些很多代码(比如以后定义float类型的image),还具有很大的扩展性。因为,加入unsigned int都能传递进去,说明宏这里的参数替换规则和函数的很不同,利用这一点,就能得到很大的扩展性。比如
这样就扩展了原有成员,不用重复敲代码。如果是C++的话,在这种情况下也没有必要用集成了,plain is good.在这里使用预处理的作用是……假如你对自己并没有那么多信心,同时你觉得查标准太麻烦,那么-E一下,立马就可以得到答案。
有时候写完代码,进行编译,如果代码是多个人的话,有时候会出现宏重定义的错误,或者并不仅仅是重定义。比如
如果成了这样子,不会产生任何警告和错误,编译通过,直接从代码里面看也很难发现。这时,查看预处理之后的文件也是比较有效的。
因为我们写代码大部分时间都是根据编译信息来订正。大家常说的忠告也是:treat warning as error.其他的我也不说了。在GCC里选项是-O,生成.o的目标代码。
需要查看汇编输出的时候,一般是想对代码进行优化,或者查看优化后的代码是否保持功能不变。查看在GCC里看生成的汇编代码需要加选项-S
gcc –S test.c
有时候你不清楚函数是以什么方式压栈,不清楚参数执行顺序,担心操作符是否有副作用等时候,你都可以写一个简单的源文件,看一下汇编代码,心里就会有底。三元操作符的执行顺序,逗号操作符的求值顺序,++操作符何时执行,switch-case为什么比if-else快等等都可以在这一步找到答案。
这里就不写代码了。
写程序,链接错误可能是最恼人的一类错误了。很多人心里并不清楚为什么会出现链接错误,什么原因导致的,就盲目的一阵心烦。特别是,当你使用一些IDE时,解决链接错误的trick往往是rebuild一下,这或许让人脑袋后吊一坨汗,但却是比较恶心。链接其实就是一个查找的过程,出错的原因无法是查找不到。这里查找的对象并非是写入的名字,很多编译器都会做中间处理,扩展函数名。不同的调用说明(calling convention)会导致扩展后的函数名不一样。这样就弄得二进制不兼容了。当然二进制不兼容常出现在调用链接库上,因为修改,导致函数偏移量改变。COM是一种解决策略,在Windows下很流行,但我觉得我要是开发小规模的东西的话,COM太大了。
链接过程是将不同的目标文件弄到一起,手动完成链接过程会得到一部分信息,其实大部分信息还是可以在预处理后的代码里面找到。所以在这里出现了错误,排错的话仍然可以回到第一步,看看调用说明是否一致。
就这些。