Mid-360 雷达获取数据并使用 Open3d+python 进行可视化

Mid-360雷达组装

最近有一个业务,要通过雷达来实时扫描显示点云,实现实时点云渲染,这里我们使用 Mid-360这款雷达:

主机IP配置

首先,我们要使用Livox航插一分三线将雷达与我们的主机连接起来,这个一分三线即分别实现电源连接,实时信号控制和数据传输,目前我们只使用到电源线和数据传输线。

Mid-360 通过以太网进行数据通信(UDP),支持静态IP 地址模式。所有 Mid-360 出厂默认 IP地址为 192.168.1.1XX(XX为 Mid-360 SN 码最后两位数字),子网掩码为 255.255.255.0,默认网关为 192.168.1.1

连接好后,我们需要设置一下电脑的IP地址,将 IP 地址设置为 192.168.1.50,子网掩码设置为 255.255.255.0.

渲染软件查看

随后,下载Livox Viewer软件,其可以展示雷达效果,同时可以设置雷达的IP地址,端口等。

https://www.livoxtech.com/cn/downloads

随后我们打开软件即可查看雷达扫描的点云数据

setting中,我们可以看到其设置,其IP地址是192.168.1.3 ,点云的输出端口是 56301

至此,雷达连接便完成了,接下来便要通过程序将雷达扫描到的数据使用我们自己的程序来获取出来。

程序采集点云

参数设置

其实,其实现思路很简单,即接收UDP数据包,解析数据包为xyz(点云数据),渲染点云即可。

代码如下,我们设置采集时长、雷达点云端口,随后便可以进行数据获取了

python 复制代码
PCAP_LOCAL_PORT = 56301
DURATION = 1  # 采集时间(秒)
OUTPUT_LAS = f"livox_{datetime.now().strftime('%Y%m%d_%H%M%S')}.las"  # 推荐 .laz
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0)
sock.bind(('0.0.0.0', PCAP_LOCAL_PORT))

UDP数据解析

抓取UDP数据包并进行解析

根据说明文档,我们可知Mid-360可以传输三种点云数据类型,如下:

📌 支持的数据类型(默认为 1
数据类型 采样类型 回波模式 坐标系模式 说明
1 点云数据 单回波 直角坐标系 (32bit) x,y,zint32_t,单位 mm
2 点云数据 单回波 直角坐标系 (16bit) x,y,zint16_t,单位 10mm
3 点云数据 单回波 球坐标系 深度+天顶角+方位角

其具体数据格式如下:

字段 偏移量 (字节) 大小 (字节) 数据类型 详细描述
version 0 1 uint8_t 包协议版本:当前为 0
length 1 2 uint16_t version 开始的整个 UDP 数据段长度(单位:字节)
time_interval 3 2 uint16_t 当前 UDP 包中最后一个点与第一个点的时间差 ,单位:0.1 微秒(μs) 即:1 个单位 = 0.1 μs = 100 ns
dot_num 5 2 uint16_t 当前 UDP 包中 data 字段包含的点数
udp_cnt 7 2 uint16_t UDP 包计数器 ,每发一个包加 1 每帧点云的第一个包从 0 开始
frame_cnt 9 1 uint8_t 帧计数器 ,每帧点云加 1(如 10Hz、15Hz、20Hz) 对于非重复扫描模式(如触发模式),此字段无效
data_type 10 1 uint8_t 数据类型 ,定义如下: • 1: 直角坐标系(32bit) 2: 直角坐标系(16bit)• 3: 球坐标系
time_type 11 1 uint8_t 时间戳类型 : • 0: 无时间同步• 1: PTP(IEEE 1588)• 2: GPS 时间
reserved 12 12 uint8_t[12] 保留字段,填充用,无意义
crc32 24 4 uint32_t 校验码 :对 timestamp + data 段进行 CRC-32 校验 算法详见 [6 CRC算法说明]
timestamp 28 8 uint64_t 时间戳(单位:纳秒 ns)
data 36 可变 -- 实际数据内容(点云、IMU 等) 长度 = length - 36 格式由 data_type 决定
🔍 补充说明
  • 总包头长度:36 字节(固定)
  • 数据起始位置:第 36 字节开始
  • 每包点数:通常为 48 点(受限于 UDP MTU)
  • 多包成帧 :一帧点云(如 1000 点)由多个 UDP 包组成,通过 frame_cntudp_cnt 拼接
  • 时间同步 :若使用 PTP 或 GPS,timestamp 可用于高精度时间对齐(如多传感器融合)

我们使用默认的数据类型即可,那么从第36个字节中的数据又该如何拆分呢,即几个自己是一个点云信息,其结构如下:

字段 偏移 类型 描述
X 0 int32_t 单位:毫米(mm)
Y 4 int32_t 单位:毫米(mm)
Z 8 int32_t 单位:毫米(mm)
反射率 12 uint8_t 范围:0~255
标签 13 uint8_t 保留字段(通常不用)

✅ 所以每个点是 14 字节:4+4+4+1+1 = 14

因此,其解析UDP数据包的代码如下:

python 复制代码
try:
    while time.time() - start_time < DURATION:
        try:
            data, addr = sock.recvfrom(1500)
            packet_count += 1

            if len(data) < 36:
                continue
            data_type = data[10]
            dot_num = int.from_bytes(data[5:7], 'little')  # 点数
            if data_type != 1:
                print(f"跳过 data_type={data_type}")
                continue
            point_data = data[36:]
            expected_bytes = dot_num * 14
            if len(point_data) < expected_bytes:
                print(f"数据长度不足: {len(point_data)} < {expected_bytes}")
                continue
            for i in range(dot_num):
                offset = i * 14
                pt = point_data[offset:offset+14]
                x = int.from_bytes(pt[0:4], 'little', signed=True)
                y = int.from_bytes(pt[4:8], 'little', signed=True)
                z = int.from_bytes(pt[8:12], 'little', signed=True)
                reflectivity = pt[12]
                tag = pt[13]
                # 过滤无效点(可选)
                if abs(x) < 100_000 and abs(y) < 100_000 and abs(z) < 100_000:
                    all_points.append([x, y, z, reflectivity, tag])
        except socket.timeout:
            continue
        except Exception as e:
            print(f"错误: {e}")
            continue
except KeyboardInterrupt:
    print("\n👋 采集中断")
python 复制代码
x = int.from_bytes(pt[0:4], 'little', signed=True) / SCALE
  • pt[0:4]:X 值(int32_t,小端序,有符号)
  • 除以 SCALE → 转成 米(m)
  • SCALE 通常是 1000.0(因为原始单位是 mm)

✅ 所以:x = 1234 mm → 1.234 m

python 复制代码
refl = pt[12] / 255.0
  • 反射率范围 0~255 → 归一化到 0~1,用于颜色显示
✅ 完整流程图
复制代码
UDP 数据包
    ↓
检查长度 ≥ 36?
    ↓ 是
读取 data_type = data[10]
    ↓ 是 data_type == 1?
读取 dot_num = data[5:7]
    ↓
point_data = data[36:]
    ↓
检查 len(point_data) ≥ dot_num * 14 ?
    ↓ 是
循环解析每个 14 字节点:
    x = int32(data[0:4]) / 1000.0
    y = int32(data[4:8]) / 1000.0
    z = int32(data[8:12]) / 1000.0
    refl = data[12] / 255.0
    → 添加到 points, colors
    ↓
通过 signal 发送给 GUI 线程

✅ 关键参数总结
参数 说明
PACKET_SIZE 1460 UDP 最大传输单元
data_type 1 只处理 32bit 直角坐标
dot_num data[5:7] 解析 每包点数(通常 48)
点大小 14 字节 x,y,z 各 4 字节 + refl + tag
坐标单位 mm → /1000 → m 转为米
反射率 0~255 → /255 → 0~1 归一化用于颜色
SCALE 1000.0 mm → m

UDP数据转换为las

UDP 数据转换存储为 las 文件

python 复制代码
if len(all_points) == 0:
    print("无有效点云数据")
else:
    print(f"采集完成!包数: {packet_count}, 总点数: {len(all_points):,}")
    # 转为 numpy 数组
    points = np.array(all_points, dtype=np.float32)
    xs = points[:, 0]  # mm
    ys = points[:, 1]
    zs = points[:, 2]
    reflectivity = points[:, 3].astype(np.uint8)
    tags = points[:, 4].astype(np.uint8)
    las = laspy.create(point_format=3, file_version='1.3')
    # 设置坐标(单位:毫米)
    las.x = xs
    las.y = ys
    las.z = zs
    # 设置反射率(Intensity)
    las.intensity = reflectivity
    # 设置自定义标签
    las.user_data = tags
    # 保存(自动压缩为 LAZ 如果扩展名为 .laz)
    las.write(OUTPUT_LAS)
    print(f"已保存为: {OUTPUT_LAS}")

完整代码

完整代码如下:

python 复制代码
import socket
import numpy as np
import laspy
from datetime import datetime
import time
PCAP_LOCAL_PORT = 56301
DURATION = 1  # 采集时间(秒)
OUTPUT_LAS = f"livox_{datetime.now().strftime('%Y%m%d_%H%M%S')}.las"  # 推荐 .laz
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(1.0)
sock.bind(('0.0.0.0', PCAP_LOCAL_PORT))
print(f"绑定到 {PCAP_LOCAL_PORT}")
all_points = []
start_time = time.time()
packet_count = 0
print("开始采集 Livox 数据...")
try:
    while time.time() - start_time < DURATION:
        try:
            data, addr = sock.recvfrom(1500)
            packet_count += 1

            if len(data) < 36:
                continue
            data_type = data[10]
            dot_num = int.from_bytes(data[5:7], 'little')  # 点数
            if data_type != 1:
                print(f"跳过 data_type={data_type}")
                continue
            point_data = data[36:]
            expected_bytes = dot_num * 14
            if len(point_data) < expected_bytes:
                print(f"数据长度不足: {len(point_data)} < {expected_bytes}")
                continue
            for i in range(dot_num):
                offset = i * 14
                pt = point_data[offset:offset+14]
                x = int.from_bytes(pt[0:4], 'little', signed=True)
                y = int.from_bytes(pt[4:8], 'little', signed=True)
                z = int.from_bytes(pt[8:12], 'little', signed=True)
                reflectivity = pt[12]
                tag = pt[13]
                # 过滤无效点(可选)
                if abs(x) < 100_000 and abs(y) < 100_000 and abs(z) < 100_000:
                    all_points.append([x, y, z, reflectivity, tag])
        except socket.timeout:
            continue
        except Exception as e:
            print(f"错误: {e}")
            continue
except KeyboardInterrupt:
    print("\n👋 采集中断")
sock.close()
# === 构造 LAS 文件 (laspy v2+) ===
if len(all_points) == 0:
    print("无有效点云数据")
else:
    print(f"采集完成!包数: {packet_count}, 总点数: {len(all_points):,}")
    # 转为 numpy 数组
    points = np.array(all_points, dtype=np.float32)
    xs = points[:, 0]  # mm
    ys = points[:, 1]
    zs = points[:, 2]
    reflectivity = points[:, 3].astype(np.uint8)
    tags = points[:, 4].astype(np.uint8)
    las = laspy.create(point_format=3, file_version='1.3')
    # 设置坐标(单位:毫米)
    las.x = xs
    las.y = ys
    las.z = zs
    # 设置反射率(Intensity)
    las.intensity = reflectivity
    # 设置自定义标签
    las.user_data = tags
    # 保存(自动压缩为 LAZ 如果扩展名为 .laz)
    las.write(OUTPUT_LAS)
    print(f"已保存为: {OUTPUT_LAS}")

存储的las文件展示效果如下:

Open3d实时采集渲染

这个就不赘述了,使用Open3d不断的接收新数据,并实时渲染

python 复制代码
import socket
import numpy as np
import open3d as o3d
from datetime import datetime
import time

# ====================== 配置 ======================
PCAP_LOCAL_PORT = 56301           # Livox UDP 端口
DURATION = 60                     # 采集总时长(秒)
PACKET_SIZE = 1500                # UDP 包大小
MAX_POINTS = 500_000              # 最大点数(防止内存溢出)
UPDATE_INTERVAL = 0.05            # 渲染刷新间隔(约 20 FPS)
SCALE = 1000.0                    # mm → m

# UDP Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(0.01)
sock.bind(('0.0.0.0', PCAP_LOCAL_PORT))
print(f"✅ 绑定到 UDP {PCAP_LOCAL_PORT}")

# Open3D 可视化
vis = o3d.visualization.Visualizer()
vis.create_window(window_name="Livox Real-Time Point Cloud", width=1200, height=800, visible=True)
render_opt = vis.get_render_option()
render_opt.background_color = np.array([0, 0, 0])  # 黑色背景
render_opt.point_size = 2                         # 点大小
render_opt.light_on = True

# 点云数据存储(支持颜色)
points_xyz = np.zeros((MAX_POINTS, 3), dtype=np.float32)
points_colors = np.zeros((MAX_POINTS, 3), dtype=np.float32)  # RGB [0,1]
points_idx = 0

# 统计
packet_count = 0
start_time = time.time()
last_update = start_time
last_print = start_time

print("📡 开始采集并实时渲染 Livox 数据...")

try:
    while time.time() - start_time < DURATION:
        new_points = []
        new_colors = []

        try:
            data, addr = sock.recvfrom(PACKET_SIZE)
            packet_count += 1

            if len(data) < 36:
                continue

            data_type = data[10]
            dot_num = int.from_bytes(data[5:7], 'little')  # 每包点数

            if data_type != 1:  # Livox 自定义点云格式
                print(f"⚠️ 跳过 data_type={data_type}")
                continue

            point_data = data[36:]
            expected_bytes = dot_num * 14
            if len(point_data) < expected_bytes:
                print(f"⚠️ 数据长度不足: {len(point_data)} < {expected_bytes}")
                continue

            # 解析点
            for i in range(dot_num):
                offset = i * 14
                pt = point_data[offset:offset+14]
                x = int.from_bytes(pt[0:4], 'little', signed=True) / SCALE
                y = int.from_bytes(pt[4:8], 'little', signed=True) / SCALE
                z = int.from_bytes(pt[8:12], 'little', signed=True) / SCALE
                reflectivity = pt[12]  # 0 ~ 255
                # tag = pt[13]

                # 过滤异常点(可调)
                if abs(x) < 100 and abs(y) < 100 and abs(z) < 100:
                    new_points.append([x, y, z])
                    gray = reflectivity / 255.0
                    new_colors.append([gray, gray, gray])  # 灰度映射

        except socket.timeout:
            pass
        except Exception as e:
            print(f"❌ 解析错误: {e}")
            continue

        # ====== 添加新点 ======
        if new_points:
            new_xyz = np.array(new_points, dtype=np.float32)
            new_col = np.array(new_colors, dtype=np.float32)

            end_idx = points_idx + len(new_xyz)
            if end_idx >= MAX_POINTS:
                # 环形缓冲:覆盖旧点
                remain = MAX_POINTS - points_idx
                points_xyz[points_idx:] = new_xyz[:remain]
                points_colors[points_idx:] = new_col[:remain]
                points_idx = len(new_xyz) - remain
                points_xyz[:points_idx] = new_xyz[remain:]
                points_colors[:points_idx] = new_col[remain:]
            else:
                points_xyz[points_idx:end_idx] = new_xyz
                points_colors[points_idx:end_idx] = new_col
                points_idx = end_idx

        # ====== 定期刷新渲染 ======
        now = time.time()
        if now - last_update >= UPDATE_INTERVAL:
            if points_idx > 0:
                pcd = o3d.geometry.PointCloud()
                pcd.points = o3d.utility.Vector3dVector(points_xyz[:points_idx])
                pcd.colors = o3d.utility.Vector3dVector(points_colors[:points_idx])
                vis.clear_geometries()
                vis.add_geometry(pcd)
                # 第一次自动调整视角
                if now - start_time < 1.0:
                    vis.reset_view_point(True)
            vis.poll_events()
            vis.update_renderer()
            last_update = now

        # ====== 每秒打印统计 ======
        if now - last_print >= 1.0:
            print(f"📊 累计: {points_idx:,} 点 | {packet_count} 包 | FPS: {1.0/(now-last_print+1e-5):.1f}")
            last_print = now

except KeyboardInterrupt:
    print("\n👋 采集中断")

finally:
    print(f"\n✅ 采集结束!总点数: {points_idx:,}, 总包数: {packet_count}")
    vis.destroy_window()
    sock.close()

    # === 可选:保存为 LAZ 文件 ===
    if points_idx > 0:
        try:
            import laspy
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            output_file = f"livox_{timestamp}.las"
            print(f"📦 正在保存为 {output_file}...")

            las = laspy.create(point_format=3, file_version='1.3')
            las.x = points_xyz[:points_idx, 0] * 1000  # m → mm
            las.y = points_xyz[:points_idx, 1] * 1000
            las.z = points_xyz[:points_idx, 2] * 1000
            las.intensity = (points_colors[:points_idx].mean(axis=1) * 255).astype(np.uint8)  # 反射率

            las.write(output_file)
            print(f"💾 已保存: {output_file}")
        except ImportError:
            print("⚠️ 未安装 laspy,跳过保存。pip install laspy[lazrs]")

码字不易,给个赞呗

相关推荐
芯岭技术5 小时前
XL5300测距模组与XL32F001/PY32F030单片机测距 最大7.6M距离测量
单片机·嵌入式硬件
m0_571372826 小时前
关于嵌入式学习——嵌入式硬件3
嵌入式硬件·学习
不会留有遗憾6 小时前
【FPGA】单总线——DS18B20
stm32·单片机·嵌入式硬件
hexiaoyan8277 小时前
光纤加速的板卡设计原理图:基于6U VPX XCVU9P+XCZU7EV的双FMC信号处理板卡
嵌入式硬件·fpga开发·光纤加速板卡·国产化板卡·xcvu9p板卡·xcvu9p
CC呢9 小时前
基于单片机智能家居语音控制系统
单片机·嵌入式硬件·智能家居·单片机设计
MingYue_SSS9 小时前
键盘上面有F3,四,R,F,V,按下没有反应,维修记录
嵌入式硬件·计算机外设·解决办法
Vae_Mars11 小时前
C语言中的运算符
数据库·单片机·mongodb
金色光环11 小时前
野火STM32Modbus主机读取寄存器/线圈失败(三)-尝试将存贮事件的地方改成数组(非必要解决方案)(附源码)
stm32·单片机·嵌入式硬件
♞沉寂12 小时前
51单片机:发光二极管与动态数码管控制
单片机·嵌入式硬件·51单片机