在之前这篇和这篇博客中,我们详细地讨论了RealSense D435i的使用。在这篇博客中利用Kalibr对D435i进行了标定,这篇博客中利用D435i运行了ORB-SLAM3。那么接着上一篇博客的问题,我们想探究一下,能否获得D435i的Raw数据呢?这是一个很有意思的问题,也是本篇博客的主要内容。这里先给出结论:可以,但不是利用V4L2,而是用RealSense提供的API接口。
1.我们要准备什么
从硬件和软件两个层面来说,硬件当然是RealSense D435i了,把它插到电脑上即可。如果对硬件细节感兴趣,可以参考Realsense的官方Datasheet,里面介绍的很详细。而软件层面,主要是RealSense的驱动、SDK以及对应的开发接口(在这篇博客中我们以Python API为例,如果对C++接口感兴趣可以参考这个网页)等,关于环境配置可以参考这篇博客,这里就不再赘述。比如,配置好软硬件环境后,可以用在终端中输入realsense-viewer
,启动Viewer可视化数据,如下。
2.我们要获取什么
第二个需要回答的问题是我们要获取什么。那么在这里来说,我们需要获取的就是相机传感器的Bayer Raw原始数据。更进一步来说,Realsense D435i有一个RGB相机、两个红外相机。红外相机是黑白相机,不存在Bayer数据,所以我们要获取的就是RGB相机的Bayer Raw数据。当然红外相机的数据方式是一样的,也很容易可以获得。
另外,我们需要明确的是,相机都支持哪些输出,这个可以通过打开Realsense Viewer选择不同的分辨率、帧率和数据类型进行查看。比如RGB相机,在1280×720分辨率下,支持YUYV、Y16、RAW16等多种格式输出。 其中,RAW16就是我们想要的Bayer Raw数据。又比如对于红外相机,其只支持Y8和Y16两种输出模式,如下。 这里简单列举几个常用的模式:
Infrared
- 848×480 30fps Y8/Y16
- 1280×720 30fps Y8/Y16
Infrared Depth
- 848×480 30fps Z16
- 1280×720 30fps Z16
RGB
- 1280×720 30fps RGB8/BGR8/YUYV/Y16/Raw16
- 848×480 30fps RGB8/BGR8/YUYV/Y16/Raw16
3.我们要怎么获取
在明确了我们要获取的数据为RGB相机的Bayer Raw数据之后,下一步就是思考如何获取它们。从单纯可视化角度而言,直接用Realsense Viewer就可以获取。从开发角度而言,如果有ROS,那么可以利用Realsense的ROS接口获取。最后如果想直接获取,那么就是利用librealsense提供的各种接口来获取。这里我们以Python接口为例。最核心包含四步:
- Step1: 检查设备可用性并初始化
- Step2: 设置数据流格式
- Step3: 启动相机流水线并设置相关参数
- Step4: 循环读取帧内容
下面的代码展示了一个非常基础和简单的利用librealsense获取数据的脚本。
import pyrealsense2 as rs
import numpy as np
import cv2
if __name__ == '__main__':
# step1 检查并初始化设备可用性
ctx = rs.context()
if len(ctx.devices) == 0:
print("No realsense D435i was detected.")
exit()
device = ctx.devices[0]
serial_number = device.get_info(rs.camera_info.serial_number)
config = rs.config()
config.enable_device(serial_number)
# step2 根据配置文件设置数据流
config.enable_stream(rs.stream.color,
1920, 1080,
rs.format.bgr8, 30)
# step3 启动相机流水线并设置是否自动曝光
pipeline = rs.pipeline()
profile = pipeline.start(config)
color_sensor = pipeline.get_active_profile().get_device().query_sensors()[1] # 0-depth(两个infra)相机, 1-rgb相机,2-IMU
# 自动曝光设置
color_sensor.set_option(rs.option.enable_auto_exposure, True)
# step4 循环读取帧内容,如果需要并输出
print("Shooting ...")
while 1:
frame = pipeline.wait_for_frames()
frame_data = np.asanyarray(frame.get_color_frame().get_data())
timestamp_ms = frame.timestamp
cv2.imshow("received frames", frame_data)
cv2.waitKey(int(1))
直接运行上述代码(对应Github项目中的realsense_simple_demo.py
),正常情况下就能获得相机普通RGB数据,如下所示。
上面展示的就是获取数据的基本流程。而至于我们关心的Bayer Raw数据,其实获取方式是一样的,唯一不同的就是stream的数据类型要改成rs.format.raw16
,其余不变即可,如下。
可以看到,图片上有类似网格的情况,这就是Bayer Array了。如果放大会看得更清楚。
最后,我们只需要把获取到的数据保存即可。当然需要注意的是,由于是16bit的数据,不能保存成jpg格式,应该保存成无损的png格式(支持非8bit)。
当然,为了更方便使用,也编写了不同的脚本来更加方便地获取各类型的数据,完整见Github项目。下面是彩色相机的代码:
import pyrealsense2 as rs
import numpy as np
import cv2
import os
def getFormat(data_format):
format = rs.format.rgb8
if data_format.__contains__("rgb8"):
format = rs.format.rgb8
elif data_format.__contains__("bgr8"):
format = rs.format.bgr8
elif data_format.__contains__("raw8"):
format = rs.format.raw8
elif data_format.__contains__("raw10"):
format = rs.format.raw10
elif data_format.__contains__("raw16"):
format = rs.format.raw16
elif data_format.__contains__("y8"):
format = rs.format.y8
elif data_format.__contains__("y16"):
format = rs.format.y16
elif data_format.__contains__("z16"):
format = rs.format.z16
elif data_format.__contains__("yuyv"):
format = rs.format.yuyv
elif data_format.__contains__("xyz32f"):
format = rs.format.motion_xyz32f
return format
def isDirExist(path='output'):
"""
判断指定目录是否存在,如果存在返回True,否则返回False并新建目录
:param path: 指定目录
:return: 判断结果
"""
if not os.path.exists(path):
os.makedirs(path)
return False
else:
return True
if __name__ == '__main__':
config_file = "config.yml"
fs = cv2.FileStorage(config_file, cv2.FILE_STORAGE_READ)
# step0 一些运行前零碎操作
view_mode = int(fs.getNode("view_mode").real())
enable_AE_color = int(fs.getNode("enable_AE_color").real())
if view_mode == 2 or view_mode == 3:
output_dir_image = fs.getNode("output_dir_image").string().lower()
output_type_image = fs.getNode("output_type_image").string().lower()
isDirExist(output_dir_image)
if not output_type_image.startswith("."):
output_type_image = "." + output_type_image
# step1 检查并初始化设备可用性
ctx = rs.context()
if len(ctx.devices) == 0:
print("No realsense D435i was detected.")
exit()
device = ctx.devices[0]
serial_number = device.get_info(rs.camera_info.serial_number)
config = rs.config()
config.enable_device(serial_number)
# step2 根据配置文件设置数据流
data_format_color = fs.getNode("data_format_color").string().lower()
frame_width_color = int(fs.getNode("frame_width_color").real())
frame_height_color = int(fs.getNode("frame_height_color").real())
frame_rate_color = int(fs.getNode("frame_rate_color").real())
color_format = getFormat(data_format_color)
config.enable_stream(rs.stream.color,
frame_width_color, frame_height_color,
color_format, frame_rate_color)
# step3 启动相机流水线并设置是否自动曝光
pipeline = rs.pipeline()
profile = pipeline.start(config)
color_sensor = pipeline.get_active_profile().get_device().query_sensors()[1] # 0-depth(两个infra)相机, 1-rgb相机,2-IMU
# 自动曝光设置
if enable_AE_color == 1:
color_sensor.set_option(rs.option.enable_auto_exposure, True)
else:
manual_exposure_color = fs.getNode("manual_exposure_color").real()
color_sensor.set_option(rs.option.exposure, manual_exposure_color)
# step4 循环读取帧内容,如果需要并输出
print("Shooting ...")
while 1:
frame = pipeline.wait_for_frames()
frame_data = np.asanyarray(frame.get_color_frame().get_data())
timestamp_ms = frame.timestamp
if data_format_color.__contains__("rgb"):
frame_data = cv2.cvtColor(frame_data, cv2.COLOR_RGB2BGR)
if view_mode == 1 or view_mode == 3:
cv2.imshow("received frames", frame_data)
cv2.waitKey(int(1))
if view_mode == 2 or view_mode == 3:
cv2.imwrite(output_dir_image + "/" + str(timestamp_ms) + output_type_image, frame_data)
相比于上面的代码,加入了更多判断以应对不同情况。同时将一些参数以配置文件的形式进行储存,便于修改,如下所示。
4.拿到数据后我们要做什么
拿到原始的Bayer Raw数据之后,我们自然就需要自己对其进行解析,才能得到“看得过去”的彩色RGB影像。关于Raw数据的后处理,在这篇博客中也有提到。这里以实际数据进行说明,主要进行了Bayer Raw的Demosaic、白平衡、色彩缩放步骤,代码如下。
import cv2
import numpy as np
if __name__ == '__main__':
file_path = "./test_raw.png"
# step1 读取数据
# ----------------------------------------------------------------------------------
raw_data = cv2.imread(file_path, cv2.IMREAD_UNCHANGED)
# ----------------------------------------------------------------------------------
# step2 Bayer插值
# ----------------------------------------------------------------------------------
# Bayer array:
# G B
# R G
raw_data_bgr = cv2.cvtColor(raw_data, cv2.COLOR_BAYER_GB2BGR)
print('image size:', raw_data_bgr.shape)
# ----------------------------------------------------------------------------------
# step3 白平衡
# ----------------------------------------------------------------------------------
band_b = raw_data_bgr[:, :, 0]
band_g = raw_data_bgr[:, :, 1]
band_r = raw_data_bgr[:, :, 2]
band_r = band_r * 2.04
band_g = band_g
band_b = band_b * 1.32
# ----------------------------------------------------------------------------------
# step4 缩放颜色尺度
# ----------------------------------------------------------------------------------
max_r = np.max(band_r)
max_g = np.max(band_g)
max_b = np.max(band_b)
print('max red:', max_r, 'max green:', max_g, 'max blue:', max_b)
min_value = min(max_r, min(max_g, max_b))
print('min value:', min_value)
band_r_clip = np.where(band_r > min_value, min_value, band_r)
band_g_clip = np.where(band_g > min_value, min_value, band_g)
band_b_clip = np.where(band_b > min_value, min_value, band_b)
# ----------------------------------------------------------------------------------
# step5 合并波段并输出
# ----------------------------------------------------------------------------------
band_r_clip_int = np.zeros([band_r_clip.shape[0], band_r_clip.shape[1]], np.uint8)
band_g_clip_int = np.zeros([band_g_clip.shape[0], band_g_clip.shape[1]], np.uint8)
band_b_clip_int = np.zeros([band_b_clip.shape[0], band_b_clip.shape[1]], np.uint8)
for i in range(band_r_clip.shape[0]):
for j in range(band_r_clip_int.shape[1]):
band_r_clip_int[i, j] = int(band_r_clip[i, j] / 256)
band_g_clip_int[i, j] = int(band_g_clip[i, j] / 256)
band_b_clip_int[i, j] = int(band_b_clip[i, j] / 256)
bands = cv2.merge((band_b_clip_int, band_g_clip_int, band_r_clip_int))
cv2.imwrite("merged.png", bands)
# ----------------------------------------------------------------------------------
这里需要注意的是,虽然我们获取的数据是16bit的,但并不是说量化级数真的就是16bit。量化级数还是10bit,只是每个灰度级都乘以64,变成了16bit,占满存储级数。这点其实在这篇博客里有提到。如下图所示,可以看到,直接读取的16bit数据灰度级数存在间隔(像素个数为0),而且这个间隔是64。也就是说原本10bit量化灰度数据被缩放离散化到了16bit。
我们对其除以64,就可以得到正常的灰度直方图,如下所示。
感兴趣可以参考Github项目中的histogramRaw.py
脚本。
运行上述代码(对应Github项目的parseRaw.py
),就可以得到解析好的影像,如下所示。
和它对应的直接输出彩色图像如下。
可以看到差异还是比较大的,这是因为我们没有进行复杂的处理,包括lens shading的补偿等,只进行了基本的色彩解析。在一些较亮的地方,我们解析出来的图像细节方面更好一些,没有那么重的涂抹感(主要是因为锐化造成的),如下对比所示。
可以看出路灯的光晕和反光的区域更小。
至此,我们便完成了前面提到的任务:获取RealSense D435i的Bayer Raw数据,并对其进行了简单的处理。
5.参考资料
- [1] https://www.intel.com/content/dam/support/us/en/documents/emerging-technologies/intel-realsense-technology/Intel-RealSense-D400-Series-Datasheet.pdf
- [2] https://support.intelrealsense.com/hc/en-us/community/posts/1500001169042-How-to-capture-realsense-RGB-IRs-raw-images-
- [3] https://support.intelrealsense.com/hc/en-us/community/posts/360049233974-Can-you-access-raw-synchronized-D435-output-
- [4] https://blog.csdn.net/u013832707/article/details/108584866
- [5] https://github.com/IntelRealSense/librealsense/tree/master/wrappers/python/examples
- [6] https://github.com/IntelRealSense/librealsense/issues/450
- [7] https://blog.csdn.net/qq_42393859/article/details/108468235
- [8] https://www.freesion.com/article/8664151074/
- [9] https://github.com/IntelRealSense/realsense-ros/issues/1409
- [10] https://github.com/IntelRealSense/librealsense/issues/3878#issuecomment-569550710
- [11] https://github.com/IntelRealSense/librealsense/issues/7275
- [12] https://github.com/IntelRealSense/librealsense/issues/3376
本文作者原创,未经许可不得转载,谢谢配合