一、背景相机与激光雷达的联合标定至少需要一组时间对齐、空间对齐的图像与点云数据。本文聚焦于数据采集环节记录相机和雷达两路数据的采集流程。采集总原则相机和雷达在整个采集过程中位置完全固定不可移动、不可碰触、不可调整角度同一组数据中相机图像与雷达点云尽可能同时采集主要保证环境静态标定板不要发生动态的变化。多组数据依次采集每组的标定板姿态可以不同但相机和雷达本身不动这里快速验证只取一组。二、硬件连接与网络配置2.1 设备清单相机采集图像方法众多C/PythonNVR/直连Window/Ubuntu自带工具都行。最好还是用网线直连调SDK这样可以保证图像采集实时性并且图像质量不会损失如果简单走个流程验证一下的话怎么采集都可以。设备型号网络相机NVR海康威视 NVR 相机192.168.10.168:554RTSP/ :8000SDK激光雷达禾赛 QT128C2X192.168.1.2012.2 网络配置相机与雷达属于不同网段需分别配置电脑的两个网口或一个有线 一个无线接口设备本地 IP 配置有线网卡雷达192.168.1.111有线网卡相机192.168.10.142注意建议雷达和相机分接到不同的物理网口避免单网卡频繁切换网段也可以考虑把相机和雷达设置为同一网段。三、相机数据采集3.1 采集方式通过海康 HCNetSDK 预览主码流 PlayCtrl 解码抓帧获取 1920x1920 的高清图像。相比 RTSP 方式延迟从 ~1348ms 降到 ~170ms且画质清晰但是这个延迟还是有点离谱且画质仍有损失不如C下调用SDK效果好。3.2 环境conda activate boeye pipinstallHCNetSDK-python opencv-python numpy3.3 采集代码核心代码image_capture_sdk.pyimportctypesimportosimportsysimporttempfileimportthreadingfromtypingimportOptionalimportcv2importnumpyasnpfromHCNetSDK.SDK_Structimport(NET_DVR_USER_LOGIN_INFO,NET_DVR_DEVICEINFO_V40,NET_DVR_PREVIEWINFO,)_LIB_DIRos.path.join(os.path.dirname(__import__(HCNetSDK).__file__),Libs,linux)def_ensure_library_path():自动设置 LD_LIBRARY_PATH 并重载进程避免手动 exportif_LIB_DIRinos.environ.get(LD_LIBRARY_PATH,):returnos.environ[LD_LIBRARY_PATH]_LIB_DIR:os.environ.get(LD_LIBRARY_PATH,)os.execv(sys.executable,[sys.executable]sys.argv)_ensure_library_path()_REALDATACALLBACKctypes.CFUNCTYPE(None,ctypes.c_long,ctypes.c_uint,ctypes.POINTER(ctypes.c_byte),ctypes.c_uint,ctypes.c_long,)classHKSnapshot:海康相机 SDK 实时抓图主码流高清def__init__(self,ip,port8000,useradmin,password,channel1,is_nvrFalse,stream_type0,jpeg_quality95):self.ipip self.portport self.useruser self.passwordpassword self.channelchannel self.is_nvris_nvr self.stream_typestream_type# 0主码流self.jpeg_qualityjpeg_quality self._user_id-1self._play_port-1self._real_handle-1self._preview_buf[]self._preview_lockthreading.Lock()self._preview_readythreading.Event()self._callbackNone# 防止 GC 回收回调defconnect(self):sdkctypes.CDLL(os.path.join(_LIB_DIR,libhcnetsdk.so))self._playctypes.CDLL(os.path.join(_LIB_DIR,libPlayCtrl.so))self._sdksdk sdk.NET_DVR_Init()sdk.NET_DVR_SetConnectTime(3000,1)loginNET_DVR_USER_LOGIN_INFO()login.bUseAsynLogin0login.sDeviceAddressself.ip.encode(utf-8)login.wPortself.port login.sUserNameself.user.encode(utf-8)login.sPasswordself.password.encode(utf-8)deviceNET_DVR_DEVICEINFO_V40()self._user_idsdk.NET_DVR_Login_V40(ctypes.byref(login),ctypes.byref(device))ifself._user_id0:raiseConnectionError(f登录失败:{sdk.NET_DVR_GetLastError()})self._start_preview()returnselfdef_start_preview(self):playself._play portctypes.c_int32(-1)play.PlayM4_GetPort(ctypes.byref(port))self._play_portport.value play.PlayM4_SetStreamOpenMode(port,0)play.PlayM4_OpenStream(port,None,0,1024*1024)play.PlayM4_Play(port,0)def_on_data(handle,dtype,buf,size,user):ifsize0andbuf:datactypes.string_at(buf,size)withself._preview_lock:self._preview_buf.append(data)iflen(self._preview_buf)50:self._preview_ready.set()self._callback_REALDATACALLBACK(_on_data)pvNET_DVR_PREVIEWINFO()pv.lChannelself.channel pv.dwStreamTypeself.stream_type pv.dwLinkMode0# TCPpv.hPlayWnd0pv.bBlocked1self._real_handleself._sdk.NET_DVR_RealPlay_V40(self._user_id,ctypes.byref(pv),self._callback,0)self._preview_ready.wait(timeout5)self._flush_to_decoder()def_flush_to_decoder(self):withself._preview_lock:packetsself._preview_buf[:]self._preview_buf.clear()portctypes.c_int32(self._play_port)forpktinpackets:pbufctypes.create_string_buffer(pkt,len(pkt))self._play.PlayM4_InputData(port,pbuf,len(pkt))self._preview_ready.clear()def_grab_frame(self):importtime self._flush_to_decoder()time.sleep(0.1)portctypes.c_int32(self._play_port)w,hctypes.c_uint(0),ctypes.c_uint(0)self._play.PlayM4_GetPictureSize(port,ctypes.byref(w),ctypes.byref(h))bmp_sizew.value*h.value*41024bmp_bufctypes.create_string_buffer(bmp_size)bmp_outctypes.c_uint(bmp_size)self._play.PlayM4_GetBMP(port,bmp_buf,bmp_size,ctypes.byref(bmp_out))bmp_databmp_buf.raw[:bmp_out.value]withtempfile.NamedTemporaryFile(suffix.bmp,deleteFalse)astmp:tmp.write(bmp_data)tmp_pathtmp.nametry:imgcv2.imread(tmp_path)finally:os.unlink(tmp_path)returnimgdefsave(self,file_path):imgself._grab_frame()cv2.imwrite(file_path,img,[cv2.IMWRITE_JPEG_QUALITY,self.jpeg_quality])defdisconnect(self):ifself._play_port0:portctypes.c_int32(self._play_port)self._play.PlayM4_Stop(port)self._play.PlayM4_CloseStream(port)self._play.PlayM4_FreePort(port)self._play_port-1ifself._real_handle0:self._sdk.NET_DVR_StopRealPlay(self._real_handle)self._real_handle-1ifself._user_id0:self._sdk.NET_DVR_Logout_V30(self._user_id)self._user_id-1self._sdk.NET_DVR_Cleanup()3.4 交互式采集Enter保存一张图像ESC退出importsysimporttermiosimportttyimportglobdefinteractive_capture(cam):count1existingglob.glob(image_*.jpg)forfinexisting:try:numint(f.replace(image_,).replace(.jpg,))countmax(count,num1)exceptValueError:passprint( 交互式抓图模式 )print(按 Enter 保存图像按 ESC 退出)old_settingstermios.tcgetattr(sys.stdin)try:tty.setraw(sys.stdin.fileno())whileTrue:chsys.stdin.read(1)ifch\x1b:print(\r\n退出抓图模式)breakifchin(\r,\n):filenamefimage_{count:03d}.jpgtry:cam.save(filename)print(f\r已保存:{filename})exceptExceptionase:print(f\r保存失败:{e})count1finally:termios.tcsetattr(sys.stdin,termios.TCSADRAIN,old_settings)if__name____main__:withHKSnapshot(ip192.168.10.168,port8000,useradmin,passwordbolight3,channel2,is_nvrTrue,)ascam:interactive_capture(cam)3.5 运行conda activate boeye python image_capture_sdk.py启动后等待连接就绪约 1 秒按 Enter 采集一张文件自动按image_001.jpg、image_002.jpg顺序命名。四、激光雷达数据采集4.1 硬件连接连接12V 5A适配器注意下面这个卡口感觉设计上有缺陷描述的卡扣几乎没有作用有时还有静电可插拔次数太少远远没有雷蒙头接口用的舒服可能是为了满足某种特殊要求吧。将其它线缆都接通先将网卡IP配置成同网段然后给设备上电上电后确保能ping通设备。在浏览器输入IP访问记住里面的目标IP和端口信息等下图中的核心就是目标IP192.168.1.200和2364端口4.2 问题排查有时会出现能ping通但是采集不到数据的情况用PandarView查看点云提示如下排查方法如下断电重新插拔听雷达是否有电机转动本地IP要设置成192.168.1.200与192.168.1.201配置界面一致检查是否有进程占用2364端口出现过自己开发的程序没有关闭忘了清理端口的情况用PandarView显示配置如下4.3 点云采集先下载SDK雷达数据采集使用 Windows 平台下的 Qt 可执行程序调用SDK自己实现用claude code实现很简单通过点击按钮触发采集界面如下图所示。当前 QT128C2X 运行在 10 Hz默认 600 RPM双回波模式每帧约 23 万点。对机械雷达而言帧率只由电机转速决定600 RPM 10Hz是出厂默认。回波模式只改变每帧点数不影响帧率。需要更高帧率可通过 Web 界面或 PandarView 把转速调到 1200 RPM20 Hz但会牺牲水平角分辨率需要更长探测距离的密集点云则降低转速。日常 10Hz已能满足绝大多数自动驾驶、机器人、建图场景的需求。五、联合采集流程5.1 准备工作固定相机和雷达的相对位置后续整个采集过程都不可移动确认相机和雷达都已上电、网络可达放置标定板建议在视场中放置多个不同位置/角度的标定板采集多组数据5.2 同步采集步骤关键相机图像和雷达点云应在尽可能短的时间内采集建议间隔 1 秒采集过程中相机和雷达本身绝对不能移动。调整好标定板姿态保持静止同时或快速连续地操作在 Linux 终端按 Enter 采集相机图像在 Windows 端点击雷达采集按钮记录本组图像文件名与点云文件名的对应关系如image_001.jpg↔lidar_001.pcd更换标定板姿态重复步骤 1~3建议采集 10~20 组不同姿态的数据覆盖图像的不同区域5.3 数据组织建议calibration_data/ ├── image_001.jpg ←→ lidar_001.pcd ├── image_002.jpg ←→ lidar_002.pcd ├── ... └── pairs.txt # 文件名对应关系可选pairs.txt格式示例image_001.jpg, lidar_001.pcd image_002.jpg, lidar_002.pcd ...六、注意事项设备固定整个采集过程中相机和雷达位置、姿态绝对不可变化否则标定结果无效时间同步单组数据内图像和点云的采集间隔尽量短避免标定板被风吹动或人为晃动造成的数据不一致光照稳定避免采集过程中光照突变标定板要求建议使用专业标定板尺寸足够大图案清晰无反光覆盖性标定板应出现在图像和点云的不同位置左、中、右、上、下距离多样化近、中、远网络冲突相机192.168.10.x和雷达192.168.1.x属于不同网段确保电脑路由正确不要相互干扰七、参考资料禾赛 QT128C2X 用户手册海康威视设备网络 SDK 开发文档HCNetSDK-python 包
相机-雷达标定(一):最快验证-数据准备
一、背景相机与激光雷达的联合标定至少需要一组时间对齐、空间对齐的图像与点云数据。本文聚焦于数据采集环节记录相机和雷达两路数据的采集流程。采集总原则相机和雷达在整个采集过程中位置完全固定不可移动、不可碰触、不可调整角度同一组数据中相机图像与雷达点云尽可能同时采集主要保证环境静态标定板不要发生动态的变化。多组数据依次采集每组的标定板姿态可以不同但相机和雷达本身不动这里快速验证只取一组。二、硬件连接与网络配置2.1 设备清单相机采集图像方法众多C/PythonNVR/直连Window/Ubuntu自带工具都行。最好还是用网线直连调SDK这样可以保证图像采集实时性并且图像质量不会损失如果简单走个流程验证一下的话怎么采集都可以。设备型号网络相机NVR海康威视 NVR 相机192.168.10.168:554RTSP/ :8000SDK激光雷达禾赛 QT128C2X192.168.1.2012.2 网络配置相机与雷达属于不同网段需分别配置电脑的两个网口或一个有线 一个无线接口设备本地 IP 配置有线网卡雷达192.168.1.111有线网卡相机192.168.10.142注意建议雷达和相机分接到不同的物理网口避免单网卡频繁切换网段也可以考虑把相机和雷达设置为同一网段。三、相机数据采集3.1 采集方式通过海康 HCNetSDK 预览主码流 PlayCtrl 解码抓帧获取 1920x1920 的高清图像。相比 RTSP 方式延迟从 ~1348ms 降到 ~170ms且画质清晰但是这个延迟还是有点离谱且画质仍有损失不如C下调用SDK效果好。3.2 环境conda activate boeye pipinstallHCNetSDK-python opencv-python numpy3.3 采集代码核心代码image_capture_sdk.pyimportctypesimportosimportsysimporttempfileimportthreadingfromtypingimportOptionalimportcv2importnumpyasnpfromHCNetSDK.SDK_Structimport(NET_DVR_USER_LOGIN_INFO,NET_DVR_DEVICEINFO_V40,NET_DVR_PREVIEWINFO,)_LIB_DIRos.path.join(os.path.dirname(__import__(HCNetSDK).__file__),Libs,linux)def_ensure_library_path():自动设置 LD_LIBRARY_PATH 并重载进程避免手动 exportif_LIB_DIRinos.environ.get(LD_LIBRARY_PATH,):returnos.environ[LD_LIBRARY_PATH]_LIB_DIR:os.environ.get(LD_LIBRARY_PATH,)os.execv(sys.executable,[sys.executable]sys.argv)_ensure_library_path()_REALDATACALLBACKctypes.CFUNCTYPE(None,ctypes.c_long,ctypes.c_uint,ctypes.POINTER(ctypes.c_byte),ctypes.c_uint,ctypes.c_long,)classHKSnapshot:海康相机 SDK 实时抓图主码流高清def__init__(self,ip,port8000,useradmin,password,channel1,is_nvrFalse,stream_type0,jpeg_quality95):self.ipip self.portport self.useruser self.passwordpassword self.channelchannel self.is_nvris_nvr self.stream_typestream_type# 0主码流self.jpeg_qualityjpeg_quality self._user_id-1self._play_port-1self._real_handle-1self._preview_buf[]self._preview_lockthreading.Lock()self._preview_readythreading.Event()self._callbackNone# 防止 GC 回收回调defconnect(self):sdkctypes.CDLL(os.path.join(_LIB_DIR,libhcnetsdk.so))self._playctypes.CDLL(os.path.join(_LIB_DIR,libPlayCtrl.so))self._sdksdk sdk.NET_DVR_Init()sdk.NET_DVR_SetConnectTime(3000,1)loginNET_DVR_USER_LOGIN_INFO()login.bUseAsynLogin0login.sDeviceAddressself.ip.encode(utf-8)login.wPortself.port login.sUserNameself.user.encode(utf-8)login.sPasswordself.password.encode(utf-8)deviceNET_DVR_DEVICEINFO_V40()self._user_idsdk.NET_DVR_Login_V40(ctypes.byref(login),ctypes.byref(device))ifself._user_id0:raiseConnectionError(f登录失败:{sdk.NET_DVR_GetLastError()})self._start_preview()returnselfdef_start_preview(self):playself._play portctypes.c_int32(-1)play.PlayM4_GetPort(ctypes.byref(port))self._play_portport.value play.PlayM4_SetStreamOpenMode(port,0)play.PlayM4_OpenStream(port,None,0,1024*1024)play.PlayM4_Play(port,0)def_on_data(handle,dtype,buf,size,user):ifsize0andbuf:datactypes.string_at(buf,size)withself._preview_lock:self._preview_buf.append(data)iflen(self._preview_buf)50:self._preview_ready.set()self._callback_REALDATACALLBACK(_on_data)pvNET_DVR_PREVIEWINFO()pv.lChannelself.channel pv.dwStreamTypeself.stream_type pv.dwLinkMode0# TCPpv.hPlayWnd0pv.bBlocked1self._real_handleself._sdk.NET_DVR_RealPlay_V40(self._user_id,ctypes.byref(pv),self._callback,0)self._preview_ready.wait(timeout5)self._flush_to_decoder()def_flush_to_decoder(self):withself._preview_lock:packetsself._preview_buf[:]self._preview_buf.clear()portctypes.c_int32(self._play_port)forpktinpackets:pbufctypes.create_string_buffer(pkt,len(pkt))self._play.PlayM4_InputData(port,pbuf,len(pkt))self._preview_ready.clear()def_grab_frame(self):importtime self._flush_to_decoder()time.sleep(0.1)portctypes.c_int32(self._play_port)w,hctypes.c_uint(0),ctypes.c_uint(0)self._play.PlayM4_GetPictureSize(port,ctypes.byref(w),ctypes.byref(h))bmp_sizew.value*h.value*41024bmp_bufctypes.create_string_buffer(bmp_size)bmp_outctypes.c_uint(bmp_size)self._play.PlayM4_GetBMP(port,bmp_buf,bmp_size,ctypes.byref(bmp_out))bmp_databmp_buf.raw[:bmp_out.value]withtempfile.NamedTemporaryFile(suffix.bmp,deleteFalse)astmp:tmp.write(bmp_data)tmp_pathtmp.nametry:imgcv2.imread(tmp_path)finally:os.unlink(tmp_path)returnimgdefsave(self,file_path):imgself._grab_frame()cv2.imwrite(file_path,img,[cv2.IMWRITE_JPEG_QUALITY,self.jpeg_quality])defdisconnect(self):ifself._play_port0:portctypes.c_int32(self._play_port)self._play.PlayM4_Stop(port)self._play.PlayM4_CloseStream(port)self._play.PlayM4_FreePort(port)self._play_port-1ifself._real_handle0:self._sdk.NET_DVR_StopRealPlay(self._real_handle)self._real_handle-1ifself._user_id0:self._sdk.NET_DVR_Logout_V30(self._user_id)self._user_id-1self._sdk.NET_DVR_Cleanup()3.4 交互式采集Enter保存一张图像ESC退出importsysimporttermiosimportttyimportglobdefinteractive_capture(cam):count1existingglob.glob(image_*.jpg)forfinexisting:try:numint(f.replace(image_,).replace(.jpg,))countmax(count,num1)exceptValueError:passprint( 交互式抓图模式 )print(按 Enter 保存图像按 ESC 退出)old_settingstermios.tcgetattr(sys.stdin)try:tty.setraw(sys.stdin.fileno())whileTrue:chsys.stdin.read(1)ifch\x1b:print(\r\n退出抓图模式)breakifchin(\r,\n):filenamefimage_{count:03d}.jpgtry:cam.save(filename)print(f\r已保存:{filename})exceptExceptionase:print(f\r保存失败:{e})count1finally:termios.tcsetattr(sys.stdin,termios.TCSADRAIN,old_settings)if__name____main__:withHKSnapshot(ip192.168.10.168,port8000,useradmin,passwordbolight3,channel2,is_nvrTrue,)ascam:interactive_capture(cam)3.5 运行conda activate boeye python image_capture_sdk.py启动后等待连接就绪约 1 秒按 Enter 采集一张文件自动按image_001.jpg、image_002.jpg顺序命名。四、激光雷达数据采集4.1 硬件连接连接12V 5A适配器注意下面这个卡口感觉设计上有缺陷描述的卡扣几乎没有作用有时还有静电可插拔次数太少远远没有雷蒙头接口用的舒服可能是为了满足某种特殊要求吧。将其它线缆都接通先将网卡IP配置成同网段然后给设备上电上电后确保能ping通设备。在浏览器输入IP访问记住里面的目标IP和端口信息等下图中的核心就是目标IP192.168.1.200和2364端口4.2 问题排查有时会出现能ping通但是采集不到数据的情况用PandarView查看点云提示如下排查方法如下断电重新插拔听雷达是否有电机转动本地IP要设置成192.168.1.200与192.168.1.201配置界面一致检查是否有进程占用2364端口出现过自己开发的程序没有关闭忘了清理端口的情况用PandarView显示配置如下4.3 点云采集先下载SDK雷达数据采集使用 Windows 平台下的 Qt 可执行程序调用SDK自己实现用claude code实现很简单通过点击按钮触发采集界面如下图所示。当前 QT128C2X 运行在 10 Hz默认 600 RPM双回波模式每帧约 23 万点。对机械雷达而言帧率只由电机转速决定600 RPM 10Hz是出厂默认。回波模式只改变每帧点数不影响帧率。需要更高帧率可通过 Web 界面或 PandarView 把转速调到 1200 RPM20 Hz但会牺牲水平角分辨率需要更长探测距离的密集点云则降低转速。日常 10Hz已能满足绝大多数自动驾驶、机器人、建图场景的需求。五、联合采集流程5.1 准备工作固定相机和雷达的相对位置后续整个采集过程都不可移动确认相机和雷达都已上电、网络可达放置标定板建议在视场中放置多个不同位置/角度的标定板采集多组数据5.2 同步采集步骤关键相机图像和雷达点云应在尽可能短的时间内采集建议间隔 1 秒采集过程中相机和雷达本身绝对不能移动。调整好标定板姿态保持静止同时或快速连续地操作在 Linux 终端按 Enter 采集相机图像在 Windows 端点击雷达采集按钮记录本组图像文件名与点云文件名的对应关系如image_001.jpg↔lidar_001.pcd更换标定板姿态重复步骤 1~3建议采集 10~20 组不同姿态的数据覆盖图像的不同区域5.3 数据组织建议calibration_data/ ├── image_001.jpg ←→ lidar_001.pcd ├── image_002.jpg ←→ lidar_002.pcd ├── ... └── pairs.txt # 文件名对应关系可选pairs.txt格式示例image_001.jpg, lidar_001.pcd image_002.jpg, lidar_002.pcd ...六、注意事项设备固定整个采集过程中相机和雷达位置、姿态绝对不可变化否则标定结果无效时间同步单组数据内图像和点云的采集间隔尽量短避免标定板被风吹动或人为晃动造成的数据不一致光照稳定避免采集过程中光照突变标定板要求建议使用专业标定板尺寸足够大图案清晰无反光覆盖性标定板应出现在图像和点云的不同位置左、中、右、上、下距离多样化近、中、远网络冲突相机192.168.10.x和雷达192.168.1.x属于不同网段确保电脑路由正确不要相互干扰七、参考资料禾赛 QT128C2X 用户手册海康威视设备网络 SDK 开发文档HCNetSDK-python 包