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
地址,端口等。

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

在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,z 为 int32_t ,单位 mm |
2 | 点云数据 | 单回波 | 直角坐标系 (16bit) | x,y,z 为 int16_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_cnt
和udp_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]")
码字不易,给个赞呗