树莓派智能小车实战:从YOLOv8部署到PID控制实现自动驾驶

树莓派智能小车实战:从YOLOv8部署到PID控制实现自动驾驶

1. 项目缘起:从“玩具车”到“智能小车”的蜕变

几年前,我手头有一堆闲置的树莓派和几个舵机轮子,心血来潮想做个能自己跑的小车。最初的版本简陋得不行,就是给树莓派接上电机驱动板,写个Python脚本控制前进后退,顶多算个遥控车。那时候,“智能”两个字离它还很远。后来,随着计算机视觉和边缘AI的兴起,尤其是像YOLO这样的目标检测模型变得轻量化,一个念头越来越强烈:能不能让这辆小车自己“看”路,自己决定往哪走?

这就是“GWD智能无人驾驶小车”项目的起点。GWD不是什么高深莫测的缩写,它就是我给这个项目起的代号,你可以理解为“搞我的小车”。它的核心目标很明确:在一辆基于树莓派的硬件小车上,实现一套成本可控、完全开源、从环境感知到决策控制全流程的微型无人驾驶系统。这不是为了复现特斯拉的FSD,而是为了深入理解自动驾驶技术栈的每一个环节——从摄像头图像采集、神经网络推理、到控制指令生成——是如何在资源极其有限的嵌入式设备上协同工作的。

这个项目适合谁呢?如果你是对嵌入式开发、计算机视觉或机器人学感兴趣的开发者、学生或极客,想亲手触摸AI落地的“最后一公里”,那么跟着这个项目走一遍,收获会远超看十篇论文。我们将使用树莓派作为主控,搭配普通的USB摄像头或树莓派专用摄像头模块,利用百度飞桨或PyTorch等框架部署轻量级模型,最终让小车能实现车道线识别、行人避障、交通标志识别等基础功能。整个过程,你会遇到驱动兼容、算力瓶颈、实时性挑战等一系列真实问题,而解决它们的过程,正是项目最大的价值所在。

2. 硬件选型与系统搭建:在性价比与性能间寻找平衡点

硬件是项目的骨架,选型直接决定了项目的上限和难度。对于智能小车来说,我们需要一个负责“思考”的大脑(主控)、一双“眼睛”(传感器)、灵活的“手脚”(执行器)以及连接它们的“神经网络”(电路与接口)。

2.1 主控核心:为什么依然是树莓派?

在众多嵌入式开发板中,树莓派几乎是这类项目的首选,原因很实际:

  1. 生态与社区支持无敌:任何你遇到的问题,几乎都能在论坛、GitHub上找到解决方案或讨论。从系统安装、驱动调试到软件库安装,都有成熟的路径可循。
  2. 接口丰富且标准:40Pin的GPIO排针提供了大量的数字IO、PWM、I2C、SPI、UART接口,可以轻松连接各种传感器和执行器。USB接口方便连接摄像头、无线网卡等外设。
  3. 算力与功耗的平衡:以树莓派4B或更新的树莓派5为例,其ARM Cortex-A系列处理器和VideoCore GPU能够流畅运行一个精简的Linux系统(如Raspberry Pi OS Lite),并为轻量级AI模型推理提供必要的算力支持,同时功耗和发热相对可控。

注意:关于树莓派5,虽然其性能更强,但早期系统镜像和部分外设驱动(尤其是摄像头)的成熟度可能不如树莓派4B。如果你追求稳定和资料丰富,树莓派4B 4GB/8GB版本是更稳妥的选择。如果追求更强性能应对更复杂的模型,可以选择树莓派5,但要做好花更多时间在环境适配上的心理准备。

2.2 “眼睛”的选择:USB摄像头 vs 树莓派专用摄像头

视觉是无人驾驶的基石,摄像头的选择关乎图像质量和处理延迟。

  • 树莓派专用摄像头(如OV5647、Camera Module 3)

    • 优点:通过CSI总线直接连接,带宽高、延迟极低,CPU占用率小。官方提供完整的驱动和工具(如libcamera),集成度高。
    • 缺点:型号固定,可能无法满足某些特殊需求(如全局快门)。安装时需要小心排线,CM5 Camera Module 3等新型号需要确认与旧版树莓派的兼容性。
    • 实操建议:对于绝大多数项目,推荐使用Camera Module 3。它自动对焦、成像质量好。安装后,需要通过sudo raspi-config启用摄像头接口,并使用libcamera-stilllibcamera-vid进行测试。在Python中,可以使用picamera2库来捕获图像,这是替代旧版picamera的新库。
  • 普通USB摄像头

    • 优点:即插即用,选择多样(广角、高帧率等),更换方便。
    • 缺点:通过USB总线传输,带宽和延迟不如CSI,在高分辨率高帧率下可能占用更多CPU资源。需要依赖通用的V4L2驱动。
    • 实操建议:如果你手头正好有,或者需要特定功能的USB摄像头,可以选用。在Linux下,使用ls /dev/video*查看设备,用sudo apt install v4l-utils安装工具,然后用v4l2-ctl --list-formats --device=/dev/video0查看支持的格式。在代码中,OpenCV的cv2.VideoCapture(0)通常可以很方便地调用。

我的踩坑经验:早期我用过一个便宜的USB摄像头,在室内光线充足时没问题,但到了窗外自然光下,自动曝光调整非常慢,导致图像一阵亮一阵暗,严重影响了后续车道线检测的稳定性。后来换用树莓派Camera Module 3,并手动在代码中设置了固定的曝光时间和增益,画面稳定性大幅提升。所以,摄像头不仅仅是“能拍到”,画面的一致性对算法至关重要

2.3 执行器与底盘:让想法变成动作

小车底盘可以选择现成的智能小车套件,也可以自己用亚克力板组装。核心是电机和驱动。

  • 电机:常用的是TT减速电机,价格便宜,扭力足够,通常需要配合编码器(如果要做精确的速度闭环控制)。
  • 电机驱动板:树莓派的GPIO引脚无法直接驱动电机,需要驱动板如L298N、TB6612FNG等。TB6612FNG体积小、效率高、发热少,是比L298N更好的选择。
  • 供电这是一个极易忽略但关键的点。树莓派和电机必须使用独立电源供电!电机启动和堵转时会产生巨大的电流波动和电压跌落,如果和树莓派共用电源,极易导致树莓派重启或损坏。通常做法是:一块大容量(如3000mAh以上)的2S或3S锂电池(7.4V或11.1V)给电机驱动供电,同时通过一个降压模块(如LM2596)降到5V给树莓派供电。树莓派5的供电要求更高,务必使用官方推荐的5V5A电源或同等质量的降压模块。

2.4 系统软件环境搭建:从烧录系统到基础配置

  1. 系统烧录:前往树莓派官网下载Raspberry Pi OS Lite(无桌面版,更节省资源)。使用Raspberry Pi Imager工具烧录到MicroSD卡。在烧录前,Imager工具可以让你预先配置Wi-Fi、主机名、开启SSH,这对于无头(无显示器)运行至关重要。
  2. 首次启动与网络连接:将SD卡插入树莓派,上电。如果你预配了Wi-Fi,可以通过路由器后台查找树莓派的IP地址,或用arp -a命令扫描。然后使用SSH客户端(如PuTTY)连接。
  3. 基础配置与换源
    sudo raspi-config
    在这里,你需要:扩展文件系统(Expand Filesystem)、更改密码、设置时区、启用摄像头(Interface Options -> Camera)、启用I2C/SPI(如果需要连接某些传感器)。 为了提高软件安装速度,建议更换为国内镜像源。编辑/etc/apt/sources.list/etc/apt/sources.list.d/raspi.list文件,将archive.raspberrypi.orgdeb.debian.org替换为清华或中科大的镜像地址。
  4. 安装必备软件包
    sudo apt update && sudo apt upgrade -y sudo apt install -y python3-pip python3-opencv git vim

3. 视觉感知核心:轻量级AI模型的部署与优化

有了稳定的图像输入,下一步就是教小车“看懂”图像。我们不可能在树莓派上运行庞大的原始YOLOv8或ResNet,必须使用经过优化和裁剪的轻量级模型。

3.1 模型选型:平衡精度、速度与易用性

  1. YOLO系列(如YOLOv5s, YOLOv8n):无疑是目标检测领域的标杆。YOLOv5和v8都提供了从纳米(n)到大型(l)不同尺度的预训练模型。对于树莓派4B,YOLOv5sYOLOv8n是可行的起点。它们能够检测人、车、交通标志等多种物体。
  2. 百度飞桨PaddlePaddle轻量化模型:如果你更熟悉国内生态,百度飞桨提供了丰富的轻量级预训练模型,如PaddleDetection中的PP-PicoDet、PP-YOLO Tiny等,这些模型针对边缘设备做了大量优化,并且有详细的部署文档。
  3. 专用车道线/分割模型:对于车道线检测,可以考虑使用轻量的语义分割模型,如UNet的轻量化变体,或者使用传统图像处理(如Canny边缘检测+Hough变换)与深度学习结合的方法。

我的选择与理由:在这个项目中,我最终选择了YOLOv8n。原因有三:第一,Ultralytics公司维护的YOLOv8生态非常活跃,API简洁易用;第二,它原生支持导出为ONNX格式,方便我们后续进行优化;第三,社区资源极其丰富,遇到任何问题都更容易找到答案。虽然飞桨模型可能在某些场景下效率更高,但YOLOv8的综合生态优势让我决定从它开始。

3.2 从训练到部署:在树莓派上运行YOLOv8

这个过程可以分为“准备模型”和“在树莓派上推理”两步。

第一步:在性能更强的电脑上准备模型我们通常不在树莓派上训练模型。在一台有GPU的电脑上:

  1. 安装Ultralytics库:pip install ultralytics
  2. 你可以使用官方的预训练模型直接进行推理,也可以在自己的数据集上微调。例如,收集一些小车视角的车道、行人、障碍物图片进行标注和训练,效果会更好。
  3. 关键步骤:模型导出。将训练好的PyTorch模型导出为ONNX格式,这是跨平台部署的中间态。
    from ultralytics import YOLO model = YOLO('path/to/your/best.pt') # 加载你的模型 model.export(format='onnx', imgsz=[640, 480]) # 导出为ONNX,指定输入图像尺寸
    更进一步,我们可以使用ONNX Runtime的工具进行量化,将FP32模型转换为INT8模型,能大幅提升在CPU上的推理速度,虽然会损失一点点精度。

第二步:在树莓派上部署与推理将导出的.onnx模型文件拷贝到树莓派。

  1. 安装推理引擎。对于ONNX模型,最常用的是onnxruntime。树莓派ARM架构需要安装对应的版本。
    pip install onnxruntime
    注意,默认安装的是CPU版本。树莓派的GPU(VideoCore)通常不直接支持加速ONNX推理,所以主要依赖CPU。
  2. 编写推理代码。核心是加载模型、预处理图像、运行推理、后处理结果。
    import cv2 import onnxruntime as ort import numpy as np class YOLOv8Detector: def __init__(self, model_path, conf_thres=0.5): self.session = ort.InferenceSession(model_path) self.input_name = self.session.get_inputs()[0].name self.conf_threshold = conf_thres # 获取模型输入尺寸,例如 [1, 3, 480, 640] self.input_shape = self.session.get_inputs()[0].shape self.h, self.w = self.input_shape[2], self.input_shape[3] def preprocess(self, image): # 调整大小、BGR2RGB、归一化、转换维度为 NCHW img = cv2.resize(image, (self.w, self.h)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1)) # HWC to CHW img = np.expand_dims(img, axis=0) # CHW to NCHW return img def detect(self, image): input_tensor = self.preprocess(image) outputs = self.session.run(None, {self.input_name: input_tensor}) # YOLOv8输出格式处理,这里需要根据具体导出模型调整 # outputs[0] 的shape通常是 [1, 84, 8400],需要解析出框、置信度、类别 predictions = self.postprocess(outputs[0], image.shape) return predictions def postprocess(self, outputs, orig_shape): # 简化的后处理:过滤低置信度框,应用NMS # 实际代码需要根据YOLOv8 ONNX输出的具体格式编写 boxes, scores, class_ids = [], [], [] # ... 解析逻辑 ... return boxes, scores, class_ids # 使用示例 detector = YOLOv8Detector('yolov8n.onnx') cap = cv2.VideoCapture(0) # 或使用picamera2 while True: ret, frame = cap.read() if not ret: break boxes, scores, class_ids = detector.detect(frame) # 在frame上绘制检测框 # ... cv2.imshow('Detection', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break
  3. 性能实测与瓶颈分析:在树莓派4B上,使用ONNX Runtime推理YOLOv8n(640x640输入),一帧图像的处理时间(包括预处理、推理、后处理)大约在300-500毫秒,也就是每秒2-3帧(FPS)。这对于实时控制来说太慢了。瓶颈主要在CPU推理上。

3.3 性能优化实战:把FPS提上去

为了让小车能实时反应,我们必须将处理速度提升到至少5-10 FPS。以下是我尝试过的几种优化策略:

  1. 降低输入分辨率:这是最有效的方法之一。将模型输入尺寸从640x640降到320x320甚至256x256,推理速度会成倍提升,当然小目标的检测精度会下降。需要根据小车的运行环境和主要检测目标(车道线通常是大目标,行人中等)来权衡。
  2. 模型量化:如前所述,使用INT8量化模型。这需要用到ONNX Runtime的量化工具,过程稍复杂,但能带来约2-3倍的速度提升,而精度损失通常在可接受范围内。
  3. 使用TensorRT或OpenVINO(如果平台支持):树莓派官方OS基于Debian,可以尝试安装OpenVINO工具套件,将ONNX模型转换为OpenVINO IR格式,并利用CPU的指令集进行优化。NVIDIA的Jetson系列则更适合TensorRT。对于树莓派,OpenVINO的优化效果有时比纯ONNX Runtime好。
  4. 多线程/异步处理:将图像采集、推理、控制逻辑放在不同的线程中。例如,主线程负责控制,一个线程专门跑一个循环,不断从摄像头抓取最新帧,另一个线程专门进行模型推理。这样即使推理慢(比如200ms一帧),控制线程也能基于稍旧但可用的检测结果进行决策,避免被推理阻塞。
  5. 探索更轻的模型:如果YOLOv8n仍然太重,可以转向专为移动端设计的模型,如Google的MobileDet,或前面提到的PP-PicoDet。这些模型在精度-速度的帕累托前沿上可能更有优势。

我的优化结果:经过将输入尺寸降至320x320,并结合INT8量化,我在树莓派4B上成功将YOLOv8n的推理速度稳定在了~120ms/帧,加上前后处理,整体流程约150ms,达到了接近7 FPS。这对于小车在低速下的避障和车道跟踪,已经具备了基本的实时性。

4. 决策与控制:从感知到行动的“驾驶脑”

当小车能“看到”行人和车道线后,它需要一颗“驾驶脑”来决策该加速、刹车还是转向。这部分是机器人学中“感知-决策-控制”闭环的体现。

4.1 车道保持:简单的PID控制器就够用

对于车道线保持,一个经典的方法是“视线追踪”(Look-Ahead Point Tracking)。

  1. 提取车道线:从YOLO检测出的车道线区域(或使用传统图像处理提取),拟合出左右车道线的方程(例如,使用二次曲线)。
  2. 计算目标点:在图像前方一定距离(例如,在图像高度的下半部分)画一条横线,计算这条横线与左右车道线的交点。取这两个交点的中点,作为小车应该瞄准的“目标点”。
  3. 计算横向偏差:将目标点的横坐标与图像中心线的横坐标进行比较,其差值就是横向偏差(Cross-Track Error, CTE)。
  4. PID控制转向:使用一个PID控制器,输入是横向偏差CTE,输出是前轮的转向角(或舵机的PWM占空比)。
    • P(比例):偏差越大,转向力度越大。纯P控制容易在中心线附近振荡。
    • I(积分):累积历史偏差,消除静态误差(如小车始终偏左一点)。
    • D(微分):根据偏差变化率提前制动,减少超调,使控制更平滑。
class PIDController: def __init__(self, Kp, Ki, Kd): self.Kp = Kp self.Ki = Ki self.Kd = Kd self.prev_error = 0 self.integral = 0 def compute(self, error, dt): self.integral += error * dt derivative = (error - self.prev_error) / dt if dt > 0 else 0 output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative self.prev_error = error return output # 在控制循环中 pid = PIDController(Kp=0.5, Ki=0.01, Kd=0.05) while True: # ... 获取图像,检测车道线,计算CTE ... cte = target_point_x - image_center_x steering_angle = pid.compute(cte, dt=0.1) # dt是循环周期 # 将steering_angle映射到舵机的PWM值 set_steering_servo(steering_angle)

调参心得:PID参数需要实地调试。先在静止状态下给一个阶跃偏差,看响应是否快速且无剧烈振荡。然后让小车低速运行,观察其能否平滑地回到车道中心。Ki值要非常小心,积分饱和会导致小车突然猛打方向。通常先调P,让小车有反应;再加D,抑制振荡;最后加很小的I来消除稳态误差。

4.2 行人避障:有限状态机与反应式行为

避障逻辑比车道保持复杂,需要引入简单的决策逻辑。一个有效的方法是使用有限状态机(FSM)。 我们可以定义小车的几个状态:

  • 巡航(CRUISE):默认状态,执行车道保持。
  • 检测到障碍(OBSTACLE_DETECTED):当YOLO检测到正前方一定距离内有“人”或“车”时进入此状态。
  • 刹车(BRAKING):立即停止电机,或大幅减速。
  • 绕行(AVOIDING):根据障碍物位置(偏左还是偏右),决定向左或向右微调方向,同时缓慢前进。
  • 恢复(RECOVERING):绕过障碍后,尝试重新回到原车道。
class SimpleFSM: def __init__(self): self.state = 'CRUISE' self.obstacle_on_left = False self.obstacle_on_right = False def update(self, detections): # 分析检测结果,判断障碍物位置 for box, cls in detections: if cls == 'person': box_center_x = (box[0] + box[2]) / 2 if box_center_x < image_width * 0.4: self.obstacle_on_left = True elif box_center_x > image_width * 0.6: self.obstacle_on_right = True else: # 障碍物在正前方 self.state = 'BRAKING' return # 状态转移逻辑 if self.state == 'CRUISE' and (self.obstacle_on_left or self.obstacle_on_right): self.state = 'AVOIDING' elif self.state == 'AVOIDING' and not (self.obstacle_on_left or self.obstacle_on_right): self.state = 'RECOVERING' elif self.state == 'RECOVERING' and abs(lateral_error) < 10: # 回到车道中心附近 self.state = 'CRUISE' def get_action(self): if self.state == 'CRUISE': return {'steer': pid_compute(), 'speed': 0.5} elif self.state == 'BRAKING': return {'steer': 0, 'speed': 0} elif self.state == 'AVOIDING': if self.obstacle_on_left: return {'steer': -30, 'speed': 0.3} # 向右绕 else: return {'steer': 30, 'speed': 0.3} # 向左绕 elif self.state == 'RECOVERING': return {'steer': pid_compute() * 1.5, 'speed': 0.4} # 更积极地回正

这个FSM非常简陋,但实现了基本的反应式避障。在真实环境中,你需要考虑障碍物的速度、距离估计(这需要单目测距或超声波传感器补充)等,让决策更智能。

4.3 控制频率与实时性:让大脑跟上眼睛

视觉处理慢(~150ms),控制循环必须更快(目标20-50ms)。这带来了一个关键问题:控制指令基于过时的感知数据。 解决方案是预测滤波

  • 预测:假设障碍物或车道线是匀速运动的,可以根据上一帧的位置和速度(通过跟踪算法如Kalman Filter获得),预测出当前时刻它应该在哪里。这样,即使检测结果还没出来,控制器也能使用预测值。
  • 滤波:对检测到的横向偏差CTE进行低通滤波(如移动平均),可以平滑掉检测噪声带来的控制指令抖动,让小车行驶更平稳。
import collections class MovingAverageFilter: def __init__(self, window_size=5): self.window = collections.deque(maxlen=window_size) def update(self, value): self.window.append(value) return sum(self.window) / len(self.window) cte_filter = MovingAverageFilter(window_size=3) # 在获取到新的cte后 smoothed_cte = cte_filter.update(raw_cte) steering_angle = pid.compute(smoothed_cte, dt)

5. 系统集成与实地调试:让代码在路上跑起来

将视觉、决策、控制模块集成,并让小车在真实物理世界中运行,是挑战最大的部分。

5.1 软件架构与多进程设计

一个健壮的软件架构能提升稳定性和可维护性。我推荐使用多进程(multiprocessing)而非多线程,因为Python的GIL锁会限制多线程的CPU并行能力,而视觉推理是CPU密集型任务。

  • 进程1:图像采集与发布。不断从摄像头读取图像,放入一个共享内存或队列中。
  • 进程2:视觉感知。从队列取图,进行推理,将检测结果(如车道线方程、障碍物框)放入另一个结果队列。
  • 进程3:决策与控制。从结果队列取数据,运行FSM和PID控制器,计算出最终的转向和速度指令,通过GPIO发送给电机驱动板。
  • 进程4(可选):日志与监控。记录数据,或者通过WebSocket将视频流和检测结果发送到电脑端进行实时监控。

使用multiprocessing.Queuemultiprocessing.Manager来安全地在进程间传递数据。注意,传递大型图像(numpy数组)开销很大,可以考虑只传递关键信息(如检测框坐标)或使用共享内存。

5.2 实地调试的“血泪”经验

  1. 光照是最大的敌人:早晨、中午、傍晚、阴天、树荫下,光照条件天差地别。在室内训练好的模型,到室外可能直接失效。解决方案:数据增强!在模型训练时,大量使用随机亮度、对比度、饱和度调整,甚至模拟阴影。也可以在图像预处理环节加入自动白平衡或直方图均衡化。
  2. 地面纹理干扰:人行道上的砖缝、地面的裂缝、水渍,都可能被误检为车道线。解决方案:在图像预处理中,使用颜色空间过滤(例如,在HSV空间筛选属于白色或黄色的像素),结合形态学操作,可以很大程度上过滤掉非车道线的纹理。
  3. 电机干扰导致树莓派重启:这是硬件上的经典问题。即使电源独立,电机产生的电磁噪声也可能通过地线或空间耦合干扰树莓派。解决方案:确保电机驱动板与树莓派之间的信号地连接良好;在电机电源线上加磁环;在树莓派电源输入端并联一个大电容(如1000uF)以缓冲电压波动。
  4. 控制延迟与“画龙”:小车总是左右摇摆,像画龙一样。这通常是PID参数不合适或控制频率太低导致的。解决方案:首先确保控制循环频率足够高(>20Hz)。然后重点调整PID的D参数,增加微分作用可以抑制振荡。也可以尝试在控制指令输出前加入一个小的死区(Dead Zone),当偏差很小时不输出转向,避免高频微抖。
  5. 无线图传与远程监控:为了调试方便,可以安装motion或使用flask搭建一个简单的视频流服务器,将摄像头画面和检测框叠加后推送到局域网,在电脑浏览器上实时查看。命令libcamera-vid -t 0 --inline --listen -o tcp://0.0.0.0:8888可以启动一个TCP视频流服务器。

5.3 进阶方向与扩展思考

当基础功能跑通后,这个项目还有巨大的深化空间:

  • 多传感器融合:加入超声波传感器或激光雷达(如RPLidar A1)进行近距离精确测距,弥补视觉在距离估计上的不足。使用IMU(惯性测量单元)获取小车自身的姿态角,实现更稳定的控制。
  • SLAM与建图:让小车在探索环境的同时构建地图,并实现基于地图的路径规划。这可以引入ROS(机器人操作系统),虽然树莓派跑完整的ROS有点吃力,但ROS2的轻量级版本是一个方向。
  • 端到端驾驶:不显式地检测车道线和障碍物,而是直接使用原始图像输入,通过一个神经网络直接输出转向角和速度。这需要大量的驾驶数据来训练,但思路非常有趣。
  • 仿真先行:在投入大量时间硬件调试前,可以先用仿真环境(如Gazebo、CARLA的简化版)验证算法逻辑,能极大提高开发效率。

这个“GWD智能无人驾驶小车”项目,就像一颗种子。从点亮第一个LED,到让小车颤颤巍巍地识别出第一条车道线,再到它能稳健地绕开一个纸箱,每一步都充满了解决问题的成就感。它带给你的,绝不仅仅是几行代码和一堆硬件,而是一套完整的、从感知到控制的嵌入式AI系统开发方法论。当你下次再看到关于自动驾驶的新闻时,你看到的将不再是黑科技,而是一个个可以拆解、可以理解、甚至可以亲手实现的技术模块。这,或许就是动手做项目最大的魅力。