【题外话】
最近实验室要我修改C3D(The 3D Biomechanics Data Standard)文件,虽然从网上找到了一个叫c3d4sharp的类库,这个类库单纯读取C3D文件的话还可以,但是如果要实现修改或者创建C3D文件就比较麻烦了。同时c3d4sharp实现得比较简单,很多C3D文件里有的数据都不支持。好在C3D文件总体不是很复杂,于是我就从头重新写了一个C3D文件读写的库,现在在codeplex上创建了个项目叫C3D.NET。
【文章索引】
【一、C3D文件格式的结构】
首先说C3D文件整体不是很复杂,也没有很多复杂的概念,C3D的文档格式可以从其官网下载或在线阅读。首先C3D文件是以Section为单位存储的,每一个Section固定为512字节。Section一定是按顺序存储的,不过有意思的是,Section的序号是从1开始的,而不是0。C3D文件分为三部分,分别是Section ID = 1的C3D文件头(固定为一个Section,512字节),Section ID一般等于2(在文件头内会给出)的C3D参数集合以及Section ID不知道等于几(在文件头和参数集合中都会给出)的C3D数据部分。
不过C3D也有很复杂的地方,一个是关于整型的使用,可以使用使用有符号的(Int16),也可以使用无符号的(UInt16),只不过后者能存储的数据量要多一些罢了,既然这样,不知为何当初还要采用有符号的整型。而且最关键的是,文档内没有任何标识能指出文档使用的是何种整型。官方给出的解决方法是,可以根据例如帧总数、帧索引等判断,如果读出负数,则采用无符号的,否则采用有符号的。
另一个是C3D文件能在不同类型的CPU上生成,这表现于不同CPU可能采用的字节序(Endian)和浮点数字不同,比如大家用的CPU都是采用Little-Endian以及IEEE754的浮点数标准。从网上查还发现有DEC (VAX)以及IBM等CPU采用不同的浮点数标准,详见我之前一篇文章:http://www.cnblogs.com/mayswind/p/3365594.html。而C3D则是支持3类CPU,Intel CPU采用Little-Endian以及IEEE754标准的浮点数,DEC (VAX)采用的Little-Endian以及特有的浮点数,MIPS (SGI)采用的Big-Endian以及IEEE754标准的浮点数,所以在读取文档的时候可能需要额外进行处理,在第三节会详细说明。
【二、C3D文件头的结构】
首先来说第一部分,也就是C3D的文件头,C3D的文件头一定只占1个Section,即固定的512字节,所以只要读取前512字节就可以把整个头数据获取到了。虽然每个Section有512字节之多,但是对于C3D的文件头只占了很少的一部分,在文件头中有大量空白的区域。其中第一部分是文件头参数部分,内容如下:
字节 | 类型 | 说明 |
00H | Byte | 参数集合开始的Section ID(通常为0x02,但也不一定) |
01H | Byte | 文件标识(固定为0x50) |
02H-03H | Int16 | 每帧里3D坐标点的个数 |
04H-05H | Int16 | 每帧里模拟测量的个数 |
06H-07H | Int16 | 第1帧的序号(最小为1) |
08H-09H | Int16 | 最后1帧的序号 |
0AH-0BH | Int16 | 最大插值差距 |
0CH-0FH | Single | 比例因子(正数表示存储的帧为整数帧,负数为浮点帧) |
10H-11H | Int16 | 数据区域开始的Section ID |
12H-13H | Int16 | 每帧模拟采样个数 |
14H-17H | Single | 帧率 |
在此之后的第二部分,也就是存储的事件,听上去应该占很多字节,但是由于限制了事件数量最多不能超过18个,同时事件名称最长为4字节,所以事件部分也只占很少的空间。由于C3D主要是为了记录运动的数据,可能在其中有很多比较关键的地方,事件就是用于标记出这些地方的。一个事件包括三个内容,分别是最长四字节的事件名称、一字节的事件是否应该显示的状态以及一个四字节的单精度浮点数表示事件出现的时间。
字节 | 类型 | 说明 |
12AH-12BH | Int16 | 事件名是否支持4字节(支持为0x3039,不支持为0) |
12CH-12DH | Int16 | 事件数量(最大为18) |
130H-176H | Single[] | 按事件顺序存储的每个事件发生的时间(第1个帧为0.0s) |
178H-188H | Byte[] | 按事件顺序存储的每个事件是否应该显示(1为显示,0为不显示) |
18CH-1D2H | Char[] | 按事件顺序存储的每个事件的名称(每个事件占4字节) |
【三、C3D文件参数集合的结构】
C3D文件存储了大量的参数,其采用了类似目录的方式存储了参数,不过还好只有一级。即参数部分只有参数组和参数,并且每个参数组里只能有参数不能再包含参数组,每个参数必须在一个参数组内。参数集合起始于文件头中的第一个字节表示的Section ID,通常为2,但是也不一定,有的文件会在文件头后留出空白,然后参数集合起始的Section ID就顺延了。所以判断是否为C3D文件千万不要一开始读进来个Int16然后判断是不是0x5002,而一定要判断第二个字节是不是0x50,确定参数集合的位置也一定要根据文件的第一字节来。
而对于参数集合,开头的4字节定义如下:
字节 | 类型 | 说明 |
00H | Byte | 第一个参数所在的Section在整个参数集合中的位置(通常为0x01,说明开头4字节之后就是第一个参数) |
01H | Byte | 参数集合部分标识(固定为0x50) |
02H | Byte | 参数集合所占Section数量 |
03H | Byte | 生成文件的CPU类型(0x54为Intel,0x55为DEC (VAX, PDP-11),0x56为MIPS (SGI/MIPS)) |
其中前2个字节官方说直接忽略就行,但是为了兼容在写入的时候还是要写进去的。第3字节其实我们按顺序读到头也不需要这个数量。这其中重要的是CPU类型,由于不同CPU类型采用的字节序以及存储的浮点数字有所不同,所以我们还需要根据CPU类型进行相应的处理。
对于Intel和DEC生成的文档,都是采用Little-Endian字节序存储的文档,所以一定要使用Little-Endian来读取Int16、Single等类型;而MIPS则采用的Big-Endian字节序存储文档,所以在读取的时候一定要判断当前计算机默认的字节序以及文档采用的字节序。
而对于Intel和MIPS生成的文档,对于浮点数字的存储都是采用标准的IEEE754浮点数字,对于.NET而言不需要进行任何处理;而DEC生成的文档则采用特有浮点数,需要将4个字节全部读取以后再进行特殊的转换,转换方法见我之前的文章:http://www.cnblogs.com/mayswind/p/3365594.html。
在此之下就存储着所有的参数了,参数分为两类,分别是参数组和参数。
对于参数组,要存储以下6个内容:
字节 | 类型 | 说明 |
00H | SByte | 参数组名称长度(如果为负数则表示该参数组锁定请不要修改,而长度为绝对值) |
01H | SByte | 参数组ID的负数 |
02H - ... | Char[] | 参数组名称(仅包含大写字母、0-9以及下划线_) |
... + 1 - ... + 2 | Int16 | 下一参数组/参数的偏移(包含本内容的2字节) |
... + 3 | Byte | 参数组描述长度 |
... + 4 - | Char[] | 参数组描述内容(ASCII码) |
C3D文件没有规定一个参数组后边跟另一个参数组还是跟该参数组里的所有参数,所以读取的时候要注意下。而参数的内容则与参数组基本一样,只是在下一参数组/参数的偏移与参数组描述长度之间存放着该参数的实际数据罢了,由于位置描述起来太麻烦了,这里就不写了。
字节 | 类型 | 说明 |
之前的内容 | ||
Int16 | 下一参数组/参数的偏移(包含本内容的2字节) | |
Byte | 参数存放内容的类型(-1 Char,1 Byte,2 Int16,4 Single),绝对值即为长度 | |
Byte | 参数内容维数(0-3) | |
Byte[] | 参数每一维大小(如果维数为0,就没有此部分) | |
Byte[] | 参数实际内容 | |
Byte | 参数组描述长度 | |
之后的内容 |
这里需要说明的就是,由于参数可以存放数组,所以增加了维数的标识,即当维数为0时,存放的内容为Char、Byte、Int16、Single等转换出的字节数组;而当维数为1时,存放的为Char[]、Byte[]、Int16[]、Single[]等转换出的字节数组,以此类推。而对数组的存储,其实就是数组每个元素依次进行存储,而对于多维数组,则是按行优先进行存储的,比如三维数组,先存储Data[0,0,1]再存储Data[0,0,2],依次类推。
不过需要说明的是,对于Char[]以及Char[,]这两种,如果表示的话其实应该对应的是String以及String[]。
【四、C3D文件数据区域的结构】
C3D数据区域以帧为单位存放的,其实相当于这个区域就是一个帧的集合。而C3D帧其实分为两种,一种是整数帧,而另一种是浮点帧。这两者的区别在于,前者存储的所有内容都是Int16,而后者则为Single,除此之外,前者的3D坐标点(X、Y、Z)还需要乘以比例因子才可以,而后者存储的内容相当于已经乘以了比例因子了。
数据区域起始于参数集合中的"POINT"参数组中的"DATA_START"参数,其表示数据区域起始的Section ID,除此之外,在文件头中也有一份副本。不过按照官方的说法,如果文件头和参数集合中都有的内容,优先读取参数集合中的数据。
对于每个帧,又包含两个部分,第一部分为3D坐标点部分,第二部分为模拟采样部分。
- 对于每帧的3D坐标点部分,存储着该帧所有3D坐标点的数据,每个3D坐标点包括4个Int16或Single数据,分别是X坐标、Y坐标、Z坐标以及Residual和Camera Mask,其中Residual和Camera Mask共占一个Int16。比较有意思的是,对于浮点帧,Residual和Camera Mask仍然也还是一个Int16,只不过存储的时候要将对应的数值转换为Single再进行存储。
- 对于浮点帧,存储的X、Y、Z坐标就是实际的坐标;而对于整数帧,存储的X、Y、Z的坐标还需要乘以比例因子才可以,比例因子存储于参数集合中的"POINT"参数组中的"SCALE"参数。
- Residual和Camera Mask共占一个Int16,将其转换为字节数组以后,高位字节(第1个字节)的最高位表示Residual的符号,即表示该坐标点是否有效,如果为0则表示有效,如果为1则表示无效,而剩余的7个字节则为Camera Mask,每一位表示一个摄像机,从低位到高位分别表示7个摄像机是否使用(为1为使用,为0为未使用)。而Residual的真实数据则为字节数组的第0字节乘以比例因子(浮点帧则为比例因子的绝对值)。
- 而模拟采样部分,则存储着该帧所有的模拟采样的数据,不过每个帧可能包含多个模拟采样,同时每个模拟采样可能又包含多个channel,存储的数据即为该channel下记录的数据。不过存储的数据与实际的数据还需要根据下述公式进行换算,其中data value为存储的数据,real world value为实际的数据。
- zero offset可以从"ANALOG"参数组中的"OFFSET"中获取,该数据为Int16的数组,第i位指的就是第i个channel的zero offset。
- channel scale可以从"ANALOG"参数组中的"SCALE"中获取,该数据为Single的数组,第i位指的就是dii个channel的scale。
- general scale是所有模拟采样都需要乘以的比例,该数据可以从"ANALOG"参数组中的"GEN_SCALE"中获取,为Single。
real world value = (data value - zero offset) * channel scale * general scale
【五、使用C3D.NET读写文件示例】
前头说了这么多,其实如果用C3D.NET来解析的话其实是非常简单的。大家可以从https://c3d.codeplex.com/下载C3D.NET的二进制文件或者源码,引用后主要的类都在C3D这个命名空间下。
对于遍历所有的3D坐标可以采用以下的方式,首先可以从文件或者从流中创建C3D文件,然后从文件头中读取存储的第1帧的序号,然后读取采样点的数量就可以了,当然也可以不从参数组中读取,直接使用file.AllFrames[i].Point3Ds.Length也可以:
C3DFile file = C3DFile.LoadFromFile("文件路径");
Int16 firstFrameIndex = file.Header.FirstFrameIndex;
Int16 pointCount = file.Parameters["POINT:USED"].GetData<Int16>(); for (Int16 i = ; i < file.AllFrames.Count; i++)
{
for (Int16 j = ; j < pointCount; j++)
{
Console.WriteLine("Frame {0} : X = {1}, Y = {2}, Z = {3}",
firstFrameIndex + i,
file.AllFrames[i].Point3Ds[j].X,
file.AllFrames[i].Point3Ds[j].Y ,
file.AllFrames[i].Point3Ds[j].Z);
}
}
而读取模拟采样的话,采用的方法也类似:
Single frameRate = file.Parameters["POINT", "RATE"].GetData<Single>();
Int16 analogChannelCount = file.Parameters["ANALOG", "USED"].GetData<Int16>();
Int16 analogSamplesPerFrame = (Int16)(file.Parameters["ANALOG", "RATE"].GetData<Int16>() / frameRate); for (Int16 i = ; i < file.AllFrames.Count; i++)
{
for (Int16 j = ; j < analogChannelCount; j++)
{
for (Int16 k = ; k < analogSamplesPerFrame; k++)
{
Console.WriteLine("Frame {0}, Sample {1} : {2}",
firstFrameIndex + i, j + ,
file.AllFrames[i].AnalogSamples[j][k]);
}
}
}
除了一次性将C3D文件内容全部读取出来的这种方式以外,还可以使用C3DReader来一帧一帧的读取。
using (FileStream fs = new FileStream("文件路径", FileMode.Open, FileAccess.Read))
{
C3DReader reader = new C3DReader(fs);
C3DHeader header = reader.ReadHeader();
C3DParameterDictionary dictionary = reader.ReadParameters();
Int32 index = header.FirstFrameIndex; while (true)
{
C3DFrame frame = reader.ReadNextFrame(dictionary); if (frame == null)
{
break;
} for (Int16 j = ; j < frame.Point3Ds.Length; j++)
{
Console.WriteLine("Frame {0} : X = {1}, Y = {2}, Z = {3}",
index++,
frame.Point3Ds[j].X,
frame.Point3Ds[j].Y,
frame.Point3Ds[j].Z);
}
}
}
对于创建一个C3D文件,只需要使用C3DFile.Create()就可以创建一个空的C3D文件的,不包含任何的参数集合。而保存C3D文件则直接使用file.SaveTo("文件路径")就可以了。
对于添加参数集合可以使用以下的代码:
//首先需要添加参数集合,ID为正数
file.Parameters.AddGroup(, "POINT", "");
//然后往指定ID的参数集合中添加参数即可
file.Parameters[].Add("USED", "").SetData<Int16>();
添加帧可以使用如下的代码:
file.AllFrames.Add(new C3DFrame(new C3DPoint3DData[] {
new C3DPoint3DData() { X = x, Y = y, Z = z, Residual = residual, CameraMask = cameraMask},
new C3DPoint3DData() { X = x, Y = y, Z = z, Residual = residual, CameraMask = cameraMask},
new C3DPoint3DData() { X = x, Y = y, Z = z, Residual = residual, CameraMask = cameraMask},
new C3DPoint3DData() { X = x, Y = y, Z = z, Residual = residual, CameraMask = cameraMask},
new C3DPoint3DData() { X = x, Y = y, Z = z, Residual = residual, CameraMask = cameraMask} }));
当然,也可以将C3DPoint3DData数组换成C3DAnalogSamples数组,或者两者同时添加也可以。
【相关链接】
- C3D.ORG:http://www.c3d.org/
- c3d4sharp - C3D File reading/writing tools written in C#:http://code.google.com/p/c3d4sharp/
- C3D.NET:https://c3d.codeplex.com/