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读取

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)

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值