第三章 点云数据读取与解析

好,咱们进入实战环节了。前面两章讲了理论基础,现在要真刀真枪地处理数据了。这一章我重点讲TI AWR系列的数据解析,以及串口和网络数据包的处理。说实话,这块坑不少,我当年刚入行时也栽过跟头。

3.1 TI AWR系列数据格式概览

TI的毫米波雷达,比如AWR1243、AWR1843、IWR6843这些,输出的数据格式其实大同小异。它们都遵循一个叫"TLV"的结构——Type、Length、Value。说白了就是:告诉你这段数据是什么类型、有多长、内容是什么。

我个人习惯把数据帧分成三层来看:

  • 帧头(Header):包含帧号、时间戳、目标数量等元信息
  • TLV列表:一个或多个TLV结构,每个TLV代表一种数据
  • 原始数据:可能是点云、可能是热图、也可能是CFAR检测结果

嗯,这里要注意:不同型号的雷达,帧头结构略有差异。比如AWR1843的帧头是40字节,而IWR6843是44字节。我刚开始做移植时,就因为没注意这个差异,解析出来的数据全是乱的。

3.2 数据帧结构分析

咱们拿AWR1843举例。它的数据帧结构长这样:

偏移量(字节) 长度(字节) 字段名 说明
0 4 magicWord 固定为0x01020304,用于帧同步
4 4 version 协议版本号
8 4 totalPacketLen 整个数据包长度
12 4 platform 雷达平台类型
16 4 frameNumber 帧序号
20 4 timeCpuCycles CPU时钟计数
24 4 numDetectedObj 检测到的目标数量
28 4 numTLVs TLV数量
32 8 reserved 保留字段

你看,前4个字节是magic word。我当年调试时,经常因为串口丢包导致magic word对不上,程序直接卡死。后来我加了个滑动窗口搜索机制,才解决了这个问题。

关键点:magic word是帧同步的命根子。如果解析时发现magic word不对,千万别硬解,直接丢弃当前数据,往后搜索下一个magic word。

3.3 TLV结构详解

帧头后面跟着的就是TLV列表了。每个TLV的结构如下:

偏移量(字节) 长度(字节) 字段名 说明
0 4 type 数据类型(1=点云,2=目标列表,3=热图...)
4 4 length 数据段长度(不含TLV头)
8 length value 实际数据

常见的TLV type值:

  • 1:点云数据(每个点包含x, y, z, doppler, snr等)
  • 2:目标列表(跟踪后的目标)
  • 3:距离-多普勒热图
  • 4:CFAR检测结果
  • 6:统计信息

为什么会有这么多类型?你想想看,不同应用场景需要的数据不一样。做目标跟踪的,只需要点云;做环境感知的,可能需要热图。TI把这些都塞进TLV里,你用哪个就解析哪个。

3.4 串口数据读取实战

串口是毫米波雷达最常用的通信方式。我建议用115200或921600的波特率。921600更快,但丢包率也高一些。

下面是我常用的串口读取代码(Python版):

import serial
import struct

class TI_Radar_Parser:
    def __init__(self, port, baudrate=921600):
        self.ser = serial.Serial(port, baudrate, timeout=0.1)
        self.buffer = bytearray()
        
    def read_frame(self):
        """读取一帧完整数据"""
        while True:
            # 搜索magic word
            idx = self.buffer.find(b'\x01\x02\x03\x04')
            if idx == -1:
                # 没找到,继续读
                data = self.ser.read(1024)
                if not data:
                    return None
                self.buffer.extend(data)
                continue
            
            # 找到magic word,检查长度是否足够
            if len(self.buffer) - idx < 40:
                # 帧头还没收全
                data = self.ser.read(1024)
                if not data:
                    return None
                self.buffer.extend(data)
                continue
            
            # 解析帧头
            header = self.buffer[idx:idx+40]
            total_len = struct.unpack('<I', header[8:12])[0]
            
            # 检查整帧是否收全
            if len(self.buffer) - idx < total_len:
                data = self.ser.read(4096)
                if not data:
                    return None
                self.buffer.extend(data)
                continue
            
            # 提取完整帧
            frame = self.buffer[idx:idx+total_len]
            self.buffer = self.buffer[idx+total_len:]
            
            return frame
我的经验:串口读取时,千万别用readline()。二进制数据里可能有0x0A(换行符),readline会把它截断。老老实实用read()加缓冲区管理。

3.5 网络数据包解析

有些场景下,雷达通过以太网传输数据。比如TI的DCA1000采集卡,就是用UDP把原始数据发到PC上。网络传输的好处是速度快,但坏处是——UDP会丢包。

我曾经在一个项目中,雷达通过千兆网传数据,UDP包动不动就丢个千分之一。你想想看,一帧数据分成几十个UDP包,丢一个整帧就废了。后来我加了重传机制和序列号校验,才算稳定下来。

网络数据包的解析流程:

  1. 绑定UDP端口,接收数据
  2. 检查UDP包头的序列号,判断是否丢包
  3. 根据包头的帧号,把同一帧的UDP包拼起来
  4. 拼完整后,按TLV格式解析

代码示例(C++版):

#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>

class UDPRadarReceiver {
private:
    int sockfd;
    struct sockaddr_in server_addr;
    std::map<uint32_t, std::vector<uint8_t>> frame_buffer;
    
public:
    UDPRadarReceiver(int port) {
        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        memset(&server_addr, 0, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    }
    
    bool receive_frame(std::vector<uint8_t>& out_frame) {
        uint8_t buf[65536];
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);
        
        int n = recvfrom(sockfd, buf, sizeof(buf), 0, 
                        (struct sockaddr*)&client_addr, &addr_len);
        if (n <= 0) return false;
        
        // 解析UDP包头,获取帧号和包序号
        uint32_t frame_num = *(uint32_t*)(buf + 4);
        uint32_t pkt_num = *(uint32_t*)(buf + 8);
        uint32_t total_pkts = *(uint32_t*)(buf + 12);
        
        // 把数据加入帧缓冲区
        auto& buffer = frame_buffer[frame_num];
        buffer.insert(buffer.end(), buf + 16, buf + n);
        
        // 检查是否收齐
        if (pkt_num == total_pkts - 1) {
            out_frame.swap(buffer);
            frame_buffer.erase(frame_num);
            return true;
        }
        return false;
    }
};
避坑指南:UDP接收缓冲区默认只有64KB。如果雷达数据量大,一定要用setsockopt调大缓冲区。我曾经因为没调这个,丢包率高达5%,排查了整整两天才发现。

3.6 点云数据解析实战

好了,前面都是准备工作。现在咱们来解析真正的点云数据。当TLV type=1时,value段就是点云数据。每个点的结构如下:

偏移量(字节) 长度(字节) 字段名 说明
0 4 x X坐标(米,浮点数)
4 4 y Y坐标(米,浮点数)
8 4 z Z坐标(米,浮点数)
12 4 doppler 多普勒速度(m/s)
16 4 snr 信噪比(dB)
20 4 noise 噪声水平

每个点24字节。如果TLV的length是240,那就有10个点。解析代码很简单:

def parse_point_cloud(tlv_value):
    """解析点云TLV数据"""
    points = []
    point_size = 24  # 每个点24字节
    
    for i in range(0, len(tlv_value), point_size):
        point_data = tlv_value[i:i+point_size]
        if len(point_data) < point_size:
            break
            
        x, y, z, doppler, snr, noise = struct.unpack('<6f', point_data)
        points.append({
            'x': x,
            'y': y,
            'z': z,
            'doppler': doppler,
            'snr': snr,
            'noise': noise
        })
    
    return points
注意:TI的坐标系统是:X轴朝前(雷达正前方),Y轴朝左,Z轴朝上。这个和很多自动驾驶数据集(比如KITTI)不一样。做算法移植时一定要确认坐标系,不然聚类结果会完全错乱。

3.7 数据校验与错误处理

数据解析不是一帆风顺的。我总结了几种常见问题:

  • 帧同步丢失:串口或网络丢包导致magic word找不到
  • 长度不匹配:TLV声明的长度和实际数据长度对不上
  • 类型错误:解析了不支持的TLV type
  • 字节序问题:TI数据是小端(little-endian),有些平台默认是大端

我的处理策略是:

  1. 每帧都做CRC校验(如果雷达支持的话)
  2. 解析时加try-catch,遇到异常直接跳过当前帧
  3. 记录错误日志,方便事后排查
  4. 设置超时机制,超过1秒没收到完整帧就重置缓冲区

嗯,说到日志,我建议把原始数据也存一份。有时候算法效果不好,回头看看原始数据,能发现很多问题。我就曾经因为雷达天线没对准,导致点云全是噪声,查了三天才发现是安装问题。

3.8 小结

这一章的内容就这些。说白了,数据解析就是个"拆包"的过程——把雷达发来的二进制流,按照协议拆成我们能用的点云数据。核心就三点:

  • 找到帧头(magic word)
  • 按TLV结构解析
  • 做好错误处理

下一章咱们讲点云预处理,包括去噪、下采样、坐标变换这些。到时候你会看到,解析出来的原始点云有多"脏",预处理有多重要。

好,今天就到这里。有问题可以随时交流。