CINE文件读取-Phantom高速摄像机

CINE文件是由Phantom数码高速摄像机录制的视频格式,使用Phantom公司配套的软件Phantom CV可直接读取CINE文件并展示流场图像。

前段时间尝试直接读取CINE文件,找遍国内平台没有发现关于CINE文档结构的中文手册。结合github与CINE官方文档读取成功后,尝试做一个汉化版本,仅供参考。


1. CINE文件结构解析

1.1. CINE文件头 (CINEFILEHEADER structure)

1.2. 位图信息头(BITMAPINFOHEADER)

1.3. 相机设置信息Camera setup information

1.4. 标记的信息块(The tagged information blocks)

1.5. 图像数据(The image object)

1.5.1. 图像注释数据(The Annotation data)

1.5.2. 图像像素数据(Pixel array)

2. Python读取

扫描二维码关注公众号,回复: 15559669 查看本文章

2.1. 读取颜色过滤器阵列(CFA)

2.2. 10色深图像读取规则


1. CINE文件结构解析

官方文档链接:

The Cine File Format (force.com)

CINE文件分为四大部分:CINE文件头、位图信息头、相机设置信息、标记的信息块。

1.1. CINE文件头 (CINEFILEHEADER structure)

Camera setup information:包含版本信息、图像范围、绝对触发时间和文件中其他部分的偏移量。此文件头也包含在同步保存的.chd文件中,.chd文件是在Phantom软件将图像保存为CINE以外的文件格式时创建的。

1.2. 位图信息头(BITMAPINFOHEADER)

Windows structure for the image header:包含有关像素的图像尺寸和位深度的信息。与Windows系统下的BMP图像的结构类似,但某些字段的含义已扩展为支持每个颜色分量超过 8 位的图像。

1.3. 相机设置信息Camera setup information

包含在CINE录制过程中使用的采集参数。包含了图像的保存方式,要根据此部分设计读取功能。

SETUP结构包含采集参数、图像处理设置以及在CINE录制期间收集或使用的所有其他元数据。

相机、软件或其他外部设备会收集大量与图像采集同步的数据,例如:图像时间、曝光、模拟和二进制信号以及距离数据。范围数据包含诸如相机方向、到对象的距离等信息。

1.4. 标记的信息块(The tagged information blocks)

固定结构可以直接访问整个结构,使用起来非常简单快捷。为了允许未来的扩展或可选数据,我们将该模块添加到CINE格式中。

1.5. 图像数据(The image object)

包含两种类型的信息:图像注释数据和图像像素数据。

1.5.1. 图像注释数据(The Annotation data)

注释仅包括图像大小。如果图像未压缩,则可以从之前的信息中估计得到其大小,但如果图像被压缩,则无法计算大小,因此具有图像大小信息的注释块对于解析图像大小非常有用。

这里的压缩并不是传统意义上的图像压缩,而是储存方式的压缩,CINE存储时为了节省空间,会采用特殊的存储方式,因此无法从字节数估计。

1.5.2. 图像像素数据(Pixel array)

Phantom相机生成的图像的宽度倍数为16像素。在16位存储格式情况下,可能会根据不同录制需求有10、12、14位的实际深度和相应的值范围:[0,1023]、[0,4095]、[0,16383]。

下面这句很重要:

The values stored in the cine files are not left aligned, they are integer values stored as read from the sensor.

存储在电影文件中的值不是左对齐的,它们是存储为从传感器读取的整数值。这句话在读取图像像素数据的时候非常重要,如果不按照对应位处存储的方式去读取的话,则会导致什么都读不出,甚至出现读取错误!

2. Python读取

本文所有程序来自OTTOMATIC的GitHub主页:

GitHub - ottomatic-io/pycine: Reading Vision Research .cine files with python

在调试时遇到了两个问题,其他部分读取很顺利,参考CINE的官方文档都可以很顺利调试,整个过程需要两三天。下面具体说遇到的两个问题。

2.1. 读取颜色过滤器阵列(CFA)

第一个问题是源程序没有提供CFA=0的程序,恰巧我的文件就是灰度的,因此读起来会报错。

 考虑到我仅用于该任务,CFA与其他信息用不到,因此我读取header之后直接跳至pImage部分的第frame_index张图像进行处理。

frame_index = start_frame - 1
count_temp = count - frame_index
while count_temp:
    breakf.seek(header["pImage"][frame_index])

2.2. 10色深图像读取规则

 CINE的图像数据存储方式有非压缩压缩两种:

  1. 非压缩:如果位深度大于8,(10、12或14位),则值将作为16bits或者uint16_t、小端字节(little endian)存储在文件中,并用0填充到左侧(最高有效位)。 
  2. 压缩:在标准方法中,位深度大于8的任何像素都存储在单独的16位字中。在10位像素的情况下,通过将填充改为16位并在5个字节中存储4个像素,可以更有效地存储信息。 

上图展示的10bit图像压缩方式。四个压缩像素P0、P1、P2和P3存储在5个字节中:字节0、字节1、字节2、字节3和字节4(字节0位于最低内存地址,P0.9为像素0的第10位,即像素0的最高有效位)。这种压缩方式占用的空间是非压缩方式的80%,通常用在需要快速实时数据传输的场合中,当电影存储在CineMag中或通过10g以太网适配器读取图像时。

该段代码演示的是读取图像数据(The image object):其中packed就是上图的Byte0~5,unpacked就是上图中的P0~P3:

# 创建空二维数组储存图像像素
P = np.zeros([height, width], dtype="uint16")
# 读取图像长度
image_size = struct.unpack("I", f.read(4))[0]
data = f.read(image_size)
# 以数据流的方式读入
Byte = np.frombuffer(data, dtype="uint8",).astype(np.uint16)
P.flat[0::4] = ((Byte[0::5] & 0b11111111) << 2) | (Byte[1::5] >> 6)
P.flat[1::4] = ((Byte[1::5] & 0b00111111) << 4) | (Byte[2::5] >> 4)
P.flat[2::4] = ((Byte[2::5] & 0b00001111) << 6) | (Byte[3::5] >> 2)
P.flat[3::4] = ((Byte[3::5] & 0b00000011) << 8) | Byte[4::5]

f.read(4)读取图像注释数据,长度为4个字节。我读取的文件宽1280×高800,4个字节unpack之后image_size=1280000。这一步也可以看出压缩规则:每个像素是10bit,一共有1280×800=1024000个像素,因此共占用:1280×800×10bit=1280000(image_size)×8bit=10240000bit。

Byte = f.read(image_size),读取对应长度的1280000个字节。此时的Byte为二进制格式。

 将Byte以frombuffer数据流的方式、uint8的格式读入P,并转为uint16的格式,把1280000(image_size)×8bit看作1280000个0~255的数字。

以第一行为例:

  1. P.flat[0::4]:代表解压缩后的图像像素数据从第0位开始每隔4个像素存储在P中;
  2. Byte[0::5]和Byte[1::5]:代表第1个字节和第2个字节分别从从0位和从1位每隔5位读取,以8位的二进制形式(只有0和1);
  3. (Byte[0::5] & 0b11111111):“&”符号表示的是二进制中的位运算:按位与。0b表示二进制,与0b11111111进行按位与运算表明截取1位置的状态,截去0位置的状态。这是最简单的位运算,如果不理解的话就去看二进制位运算。举个例子:0b11001100& 0b11111111=0b11001100。该步运算的目的是从第0位开始取每隔5个字节Byte的全部位。
  4. ((Byte[0::5] & 0b11111111) << 2):“<<”符号表示的是二进制中的移位操作,“<<”是左移两位,右侧补0,若当前数据类型长度不足且不变,高位丢弃;“>>”是右移两位,高位补0,低位丢弃。该步运算的目的是将当前Byte(第0位开始每隔5个字节)读到的8bit左移2位,为该像素的前2位(在下一个)腾出空间。由于在读取data后已转换为uint16,左侧全部补0,因此在左移操作时高位会保留下来,不会被丢弃。
  5. (Byte[1::5] >> 6):该步运算的目的是将当前Byte(第1位开始每隔5个字节)读到的2bit右移6位,做好移至上个Byte左移2位腾出来的空位上的准备。
  6. ((packed[0::5] & 0b11111111) << 2) | (packed[1::5] >> 6):“|”是或操作,当两个位都为0时,结果才为0。该步运算的目的是拼接上两步操作得到的8bit与2bit,得到10bit的数据。

以上就是四行代码中第一行的解读,另三行同理,照着压缩结构图与步骤慢慢理解。

uint8类型读取uint16类型转换非常关键,缺一不可:

  1. 只以uint8的方式读不转为uint16的话,会导致左移操作时高位丢失,无法正确读取数据。
  2. 直接以uint16的类型读取也可以,但压缩示意图就变了,需要重新设计解压缩程序。理论上可行,但没有试过。既然官方文档是10bit压缩至1字节中的压缩结构,就不推荐用其他的数据类型读取了。

其他部分配合官方文档和程序都可以慢慢调试出来,在此不再详细分析。

如果文中有任何错误欢迎评论区和私信指出,有问题欢迎友好交流。

位运算(&、|、^、~、>>、 | 菜鸟教程 (runoob.com)

猜你喜欢

转载自blog.csdn.net/weixin_41467446/article/details/125233828