基于RK3576开发板的人脸检测算法部署实战:从环境搭建到性能优化
1. 项目概述与核心价值
最近在做一个嵌入式视觉项目,需要在一块性能与功耗平衡的板子上跑实时人脸检测。经过一番选型,最终锁定了瑞芯微的RK3576开发板。这板子集成了NPU,对于跑轻量级神经网络模型来说,性价比相当不错。人脸检测作为人脸识别、属性分析这些上层应用的“守门员”,其准确性和速度直接决定了整个系统的可用性。在真实场景下,光线忽明忽暗、人脸角度刁钻、甚至被口罩帽子遮挡,都是家常便饭,这对检测算法和硬件平台都是不小的考验。
这个项目,就是基于RK3576开发板,从零开始搭建一套完整的人脸检测算法部署流程。我会带你走通从环境搭建、模型获取、代码编译到最终在板子上跑出结果的全过程。更重要的是,我会分享在嵌入式平台上做算法部署时,那些官方文档里不会写的“坑”和技巧,比如如何优化内存占用、如何确保推理稳定性、以及一些调试的心得。无论你是刚接触嵌入式AI的开发者,还是想寻找一个靠谱的硬件平台来落地视觉应用,这篇内容应该都能给你提供直接的参考。
2. 核心思路与方案选型解析
2.1 为什么选择RK3576开发板?
在做嵌入式AI项目时,硬件选型是第一步,也是最关键的一步。我选择RK3576,主要基于以下几点考量:
性能与功耗的平衡:RK3576内置的NPU算力对于运行像人脸检测这类经典的、经过优化的轻量级模型(如YOLO-Face、RetinaFace的轻量化版本)是绰绰有余的。官方数据显示其INT8算力足够支撑实时检测的需求。同时,它的CPU部分采用大小核架构,在负载不高时可以调度到小核运行,整体功耗控制得比较好,这对于需要长时间运行甚至电池供电的设备(如门禁面板、智能摄像头)至关重要。
成熟的工具链与社区支持:瑞芯微提供了相对完整的RKNN(Rockchip Neural Network)工具链,包括模型转换、量化、推理部署等一系列工具。虽然比不上一些顶级大厂的生态,但对于常见的框架(TensorFlow, PyTorch, ONNX)模型转换支持得还不错。此外,像EASY-EAI这类第三方方案商基于原厂SDK做了进一步封装,提供了更易用的API和丰富的例程,极大地降低了开发门槛。有社区和现成的轮子,意味着遇到问题时更有可能找到解决方案,或者至少能找到讨论的方向。
接口与扩展性:RK3576开发板通常配备了丰富的接口,如MIPI-CSI摄像头接口、HDMI显示输出、USB、以太网等。这方便了连接摄像头采集图像,或者将检测结果实时输出到屏幕进行预览,为构建一个完整的原型系统提供了硬件基础。
注意:选择开发板时,不要只看峰值算力。更要关注其在实际运行你目标模型时的持续稳定性能、内存带宽(影响模型加载和数据处理速度)以及发热情况。有些板子标称算力很高,但散热设计不好,全速运行几分钟就降频,实际体验会大打折扣。
2.2 人脸检测算法选型考量
在RK3576这类嵌入式平台上跑算法,模型选型必须遵循“轻量、高效、精度可接受”的原则。我们不太可能直接部署一个几百兆的ResNet骨干网络检测器。
轻量化网络结构是首选:通常会选择专门为移动端或嵌入式设备设计的网络,例如MobileNet系列、ShuffleNet系列作为特征提取的骨干网络(Backbone)。这些网络通过深度可分离卷积等技术,在精度损失不大的情况下,大幅减少了参数量和计算量。
单阶段检测器更受欢迎:相比于Faster R-CNN这类两阶段检测器,YOLO系列、SSD、RetinaFace等单阶段检测器速度更快,结构更简单,更适合实时应用。特别是近年来涌现的YOLO变种(如YOLOv5-nano, YOLOv8n)以及针对人脸优化的RetinaFace-MobileNet,都是在嵌入式平台上的热门选择。
模型量化是必选项:训练好的模型通常是FP32(单精度浮点数)格式,在嵌入式NPU上运行效率不高。RKNN工具链支持将模型量化为INT8(8位整数)格式。量化后模型体积大幅减小,推理速度显著提升,而精度损失通常在可接受范围内(一般下降1-3个百分点)。这是嵌入式部署提升性能的关键一步。
预训练模型与自定义训练:对于通用人脸检测,直接使用在大型人脸数据集(如WIDER FACE)上预训练好的轻量级模型是最高效的方式。如果你的应用场景非常特殊(例如只检测特定角度、有严重遮挡),则可能需要用自己的数据对模型进行微调(Fine-tuning),但前提是你要有足够且高质量的标注数据。
在本例程中,EASY-EAI提供的face_detect.model就是一个已经转换并量化好的、适用于RK3576 NPU的模型文件。我们无需关心其原始架构是什么,只需调用其封装好的API进行推理即可。这体现了使用成熟方案的优势:省去了复杂的模型训练、转换和调优过程,让我们能快速聚焦在应用集成上。
3. 开发环境搭建与工程管理详解
3.1 远程挂载开发:为什么这是最佳实践?
嵌入式开发的一个核心矛盾是:编译环境通常在x86架构的PC上更强大、更便捷,而运行环境却是ARM架构的开发板。直接在本机交叉编译然后反复拷贝文件到板子上,效率低下且容易出错。远程挂载开发(NFS)完美地解决了这个问题。
它的原理很简单:在PC上开启NFS(网络文件系统)服务,将你的项目源码目录共享出来。然后在开发板上,将这个远程目录像本地磁盘一样“挂载”到某个路径下。这样,你在PC上用熟悉的IDE(如VSCode)编辑代码,保存后,开发板上立即就能看到最新的文件。编译指令在板子的终端里执行,但实际读写的是PC硬盘上的文件。其优势显而易见:
- 编辑体验好:使用PC上强大的编辑器和工具链。
- 调试方便:编译产生的中间文件、日志、生成的可执行程序都直接在共享目录中,PC和板子都能即时访问。
- 避免拷贝错误:杜绝了因手动拷贝遗漏文件或版本不一致导致的问题。
实操心得:务必使用远程挂载方式管理你的工程源码。我曾因为图省事直接
scp拷贝,结果一次漏了配置文件,导致在板子上调试了半天才发现问题所在,白白浪费了大量时间。将开发板的/home目录下的某个子目录(如/home/orin-nano/Desktop/nfs/)挂载到PC的NFS目录,是所有操作的基础。
3.2 环境搭建具体步骤与避坑指南
下面我们一步步拆解环境搭建过程,并说明每个步骤的意图和可能遇到的坑。
步骤一:准备PC端NFS服务首先,确保你的PC(通常是Linux虚拟机或实体机)安装了NFS服务并正确配置。以Ubuntu为例:
sudo apt install nfs-kernel-server编辑/etc/exports文件,添加一行,指定要共享的目录和权限(假设允许开发板IP为192.168.1.100访问):
/home/your_username/nfsroot *(rw,sync,no_subtree_check,no_root_squash)这里*表示允许所有IP访问,在生产环境应替换为具体IP。然后重启服务:
sudo exportfs -a sudo systemctl restart nfs-kernel-server在/home/your_username/下创建nfsroot目录,并确保其有读写权限。
步骤二:获取源码工程进入NFS共享目录,这里就是未来所有工作的根目录。
cd ~/nfsroot mkdir GitHub cd GitHub使用git克隆官方提供的工具包仓库。这一步要求你的PC能够访问外网。
git clone https://github.com/EASY-EAI/EASY-EAI-Toolkit-3576.git注意:如果网络不畅导致克隆缓慢或失败,可以尝试使用代理或从Github网页直接下载ZIP包。但务必下载整个仓库,而不是只下载人脸检测的单个目录。因为工程内部可能存在依赖的公共头文件、库文件或其他资源,单独下载会导致编译失败。
步骤三:开发板挂载NFS目录通过串口、SSH或ADB连接到你的RK3576开发板。首先需要确保开发板和PC在同一个局域网,并且能互相ping通。
在开发板上,创建一个用于挂载的本地目录(如果不存在):
mkdir -p /home/orin-nano/Desktop/nfs执行挂载命令,将PC的NFS共享目录映射到开发板的这个本地目录:
sudo mount -t nfs -o nolock <PC的IP地址>:/home/your_username/nfsroot /home/orin-nano/Desktop/nfs/例如:sudo mount -t nfs -o nolock 192.168.1.50:/home/developer/nfsroot /home/orin-nano/Desktop/nfs/
关键参数解释:
-t nfs:指定文件系统类型为NFS。-o nolock:禁用文件锁。在嵌入式开发中,经常因为NFS锁服务问题导致挂载失败或操作卡顿,加上这个选项能避免很多麻烦。<PC的IP地址>:<NFS路径>:指定NFS服务器地址和共享路径。/home/orin-nano/Desktop/nfs/:开发板上的本地挂载点。
挂载成功后,执行cd /home/orin-nano/Desktop/nfs/GitHub/,你应该能看到克隆下来的EASY-EAI-Toolkit-3576目录。至此,一个高效的远程开发环境就搭建完成了。
4. 算法模型部署与例程编译运行
4.1 模型获取与放置
对于AI应用,模型文件(.model或.rknn)就是算法的“灵魂”。EASY-EAI已经为我们准备好了优化好的人脸检测模型。根据提供的百度网盘链接(提取码:1234)下载模型文件。通常下载下来的会是一个压缩包,解压后找到名为face_detect.model的文件。
接下来是关键一步:将模型文件放到正确的位置。根据例程的说明,需要将其复制到Release/目录下。但这个Release/目录是编译后生成的。更稳妥的做法是:
- 在源码目录下(
EASY-EAI-Toolkit-3576/Demos/algorithm-face_detect/)创建一个名为model的文件夹。 - 将
face_detect.model放入这个model/文件夹。 - 在编译脚本
build.sh或CMakeLists.txt中,配置将模型文件复制到输出目录的指令。这样每次编译后,模型都会自动出现在可执行程序同级目录下。
查看提供的build.sh脚本,我们发现它已经包含了拷贝模型的操作。所以我们只需确保模型文件在编译前存在于源码目录的指定位置(通常是./model/或./)。按照文档说明,直接将其复制到编译后生成的Release/目录也是一种方法,但不够“工程化”。我建议你研究一下build.sh脚本的内容,理解其拷贝逻辑,从而将模型放在源码树中统一管理。
4.2 例程编译详解
进入例程目录,执行编译命令:
cd /home/orin-nano/Desktop/nfs/GitHub/EASY-EAI-Toolkit-3576/Demos/algorithm-face_detect/ ./build.sh这个build.sh脚本通常做了以下几件事:
- 创建构建目录:如
build/或Release/。 - 调用CMake:根据
CMakeLists.txt配置,生成适用于当前平台(ARM架构)的Makefile。这里会指定交叉编译工具链、头文件路径、链接库路径等关键信息。 - 执行Make:进行编译和链接,生成可执行文件
test-face-detect。 - 拷贝资源文件:将模型文件、测试图片等从源码目录拷贝到可执行文件所在目录。
排查技巧:如果编译失败,首先查看错误信息。常见问题有:
- 找不到头文件:检查
CMakeLists.txt中include_directories指定的EASY-EAI API头文件路径是否正确。路径应指向SDK安装位置或源码包中的easyeai-api目录。- 链接库失败:检查
CMakeLists.txt中link_directories和target_link_libraries。确保库路径正确,并且库文件名无误(例如-lface_detect对应libface_detect.so)。- 权限问题:确保
build.sh脚本有可执行权限(chmod +x build.sh)。
编译成功后,会在Release/目录下看到test-face-detect可执行文件和face_detect.model模型文件。
4.3 运行例程与效果验证
在Release/目录下,运行程序并指定一张测试图片:
cd Release/ ./test-face-detect test.jpg程序会依次执行:
- 初始化:调用
face_detect_init,加载face_detect.model到NPU,创建推理上下文。 - 推理:调用
face_detect_run,读取test.jpg,进行预处理、NPU推理、后处理,得到人脸框和关键点坐标。 - 输出与绘制:在控制台打印推理耗时和检测到的人脸数量。同时,在原图上用绿色矩形框画出人脸位置,并在关键点(如眼睛、鼻尖、嘴角)处绘制紫色小圆点。
- 保存结果:将绘制好的图片保存为
result.jpg。 - 释放资源:调用
face_detect_release,释放NPU资源。
运行后,查看控制台输出。如果看到类似time_use is 16.000000和face num: 1的信息,说明运行成功。用scp命令将生成的result.jpg下载到PC查看,或者如果开发板连接了显示屏,可以直接用图像查看工具打开,确认检测框是否准确。
性能分析:输出的16ms意味着处理一帧图像约耗时16毫秒,换算成帧率(FPS)大约是 1000 / 16 ≈ 62.5 FPS。这是一个非常理想的实时性能。但这只是在处理单张静态图片时的速度。在实际视频流处理中,还需要考虑图像解码、前后帧调度等开销,实际帧率会略低,但满足实时性(>25 FPS)要求毫无压力。
5. API深度解析与集成指南
5.1 API调用流程与内存管理
EASY-EAI提供的API封装得非常简洁,遵循了典型的“初始化-运行-释放”三段式设计,这也是嵌入式C/C++编程的常见模式。理解这个流程对于正确集成和避免内存泄漏至关重要。
// 1. 初始化阶段:分配资源,加载模型 rknn_context ctx; // 声明上下文句柄 int ret = face_detect_init(&ctx, "face_detect.model"); if (ret != 0) { printf("Face detect init failed!n"); return -1; }关键点:rknn_context是一个不透明的结构体指针,它代表了在NPU上创建的一个推理会话(Session),内部包含了模型、权重、内存等信息。face_detect_init函数会负责从磁盘加载模型文件,解析并部署到NPU上,最后将这个会话的句柄通过ctx返回给我们。务必检查返回值,初始化失败通常是因为模型路径错误或模型文件损坏。
// 2. 运行阶段:输入数据,获取结果 cv::Mat input_image = cv::imread("test.jpg", 1); // 使用OpenCV读取图片 std::vector<det> results; // 准备接收检测结果 ret = face_detect_run(ctx, input_image, results); if (ret != 0) { printf("Face detect run failed!n"); // 注意:运行失败也需要执行释放! face_detect_release(ctx); return -1; } // 处理results中的检测框和关键点...关键点:face_detect_run是核心函数。它内部完成了图像预处理(如缩放、归一化、颜色空间转换)、NPU推理、以及后处理(解码输出层、非极大值抑制NMS)。结果保存在std::vector<det>中,每个det对象应包含人脸框的坐标(x, y, width, height)和置信度(score),可能还有关键点坐标(landmarks)。这里使用了OpenCV的cv::Mat作为输入,非常方便。
// 3. 释放阶段:清理资源 face_detect_release(ctx);关键点:这是最容易忽略但必须做的步骤。face_detect_release会释放ctx句柄关联的所有NPU内存和系统资源。如果忘记调用,每次运行程序都会泄露一部分内存,长时间运行会导致系统内存耗尽。良好的编程习惯是,在init之后,无论后续运行成功与否,在程序退出前都必须配对调用release。
5.2 工程集成配置要点
如果你想在自己的项目中调用这些API,需要在编译配置中正确链接库和头文件。根据文档,你需要:
- 头文件路径:将
easyeai-api/algorithm/face_detect目录添加到编译器的头文件搜索路径(-I选项)。 - 库文件路径:将
easyeai-api/algorithm/face_detect目录(里面存放着libface_detect.so)添加到链接器的库搜索路径(-L选项)。 - 链接库名称:在链接时,添加
-lface_detect参数。这告诉链接器去寻找libface_detect.so这个动态库。
一个简单的CMakeLists.txt配置示例如下:
cmake_minimum_required(VERSION 3.10) project(MyFaceDetectDemo) # 设置C++标准 set(CMAKE_CXX_STANDARD 11) # 查找OpenCV包(如果例程用了OpenCV) find_package(OpenCV REQUIRED) # 设置EASY-EAI API的头文件和库路径,假设它们放在项目根目录的 `3rdparty` 文件夹下 set(EASY_EAI_API_DIR ${CMAKE_SOURCE_DIR}/3rdparty/easyeai-api) include_directories( ${EASY_EAI_API_DIR}/algorithm/face_detect ${OpenCV_INCLUDE_DIRS} ) link_directories( ${EASY_EAI_API_DIR}/algorithm/face_detect ) # 添加可执行文件 add_executable(my_demo main.cpp) # 链接库 target_link_libraries(my_demo face_detect # 链接EASY-EAI人脸检测库 ${OpenCV_LIBS} # 链接OpenCV库 )实操心得:在嵌入式平台编译时,要确保链接的库(如
libface_detect.so)是针对ARM架构编译的,而不是你PC上的x86版本。通常SDK提供商都会提供预编译好的ARM版本库。如果自己编译,务必使用正确的交叉编译工具链。
6. 进阶应用与性能优化思考
6.1 从单张图片到视频流处理
例程演示的是处理单张静态图片。实际应用,如门禁考勤、视频监控,需要处理摄像头产生的连续视频流。这涉及到几个额外的环节:
视频流捕获:使用OpenCV的VideoCapture类,可以方便地捕获USB摄像头或网络摄像头的视频流。
cv::VideoCapture cap(0); // 打开索引为0的摄像头 if (!cap.isOpened()) { // 处理错误 } cv::Mat frame; while (true) { cap >> frame; // 读取一帧 if (frame.empty()) break; // 调用 face_detect_run 处理这一帧 // 绘制结果并显示 cv::imshow("Face Detection", frame_with_boxes); if (cv::waitKey(1) == 'q') break; // 按q退出 }性能瓶颈转移:在视频流处理中,图像解码(从摄像头读取的原始数据转换成cv::Mat)和结果显示(imshow)可能会成为新的性能瓶颈,特别是高分辨率下。需要关注:
- 尝试使用硬件加速的解码方式(如果平台支持)。
- 降低显示帧率或分辨率,或者不在板端显示,而是将结果通过网络发送。
- 采用多线程流水线:一个线程负责抓取帧,一个线程负责推理,一个线程负责显示/发送,充分利用多核CPU。
6.2 模型与参数调优可能性
虽然我们使用的是现成的模型,但了解其可能的调优点对深入应用有帮助:
输入分辨率:模型通常有固定的输入尺寸(如320x240, 640x480)。在调用face_detect_run前,API内部很可能已经将输入图像缩放到这个尺寸。更高的输入分辨率能检测更小的人脸,但计算量呈平方增长,速度变慢。你需要根据实际场景中人脸距离摄像头的远近,在精度和速度之间做权衡。
置信度阈值(Score Threshold):后处理阶段会过滤掉置信度低于某个阈值的人脸框。这个阈值可能被硬编码在模型或API内部。提高阈值,检出的人脸更可靠,但漏检率可能增加;降低阈值,能检出更多人脸,但误检(把非人脸物体当成人脸)也会增多。如果API允许配置,你需要根据场景调整。
非极大值抑制阈值(NMS Threshold):当一个人脸被多个重叠的框检测到时,NMS用于保留最好的一个。阈值控制着框之间的重叠度(IoU)多大时会被认为是同一个目标而抑制。在人群密集、人脸挨得很近的场景,过高的NMS阈值可能导致只检出一个脸。
经验分享:优化是一个迭代过程。最好的方法是准备一个具有代表性的测试数据集(包含你的实际场景图片),然后系统地调整上述参数,观察精度(Recall, Precision)和速度(FPS)的变化,找到最适合你应用的那个平衡点。RKNN工具链也提供性能分析工具,可以分析模型在各层的耗时,但对于封装好的API,这部分信息可能不直接可见。
6.3 系统集成与资源管理
在真正的产品中,人脸检测可能只是一个模块。你还需要集成其他功能,如人脸识别、活体检测、数据上传等。这就需要考虑:
资源竞争:RK3576的NPU、CPU、内存是共享资源。如果同时运行多个模型,需要合理规划。例如,可以错峰运行,或者评估多个轻量模型同时运行时的整体性能是否达标。
功耗与散热:持续全速运行NPU会导致芯片发热。在设备设计中需要考虑散热措施(如散热片、风扇)。对于电池设备,可能需要动态调整推理频率,在检测到无人时进入低功耗模式。
稳定性:工业级应用要求7x24小时稳定运行。需要增加看门狗(Watchdog)机制,监控程序运行状态;做好异常处理,确保即使某次推理失败,程序也不会崩溃,而是记录日志并尝试恢复。
7. 常见问题排查与调试技巧实录
在实际部署中,你几乎一定会遇到各种问题。这里记录了一些典型问题及其排查思路。
7.1 模型加载失败
现象:调用face_detect_init返回失败(返回值非0)。排查步骤:
- 检查模型路径:这是最常见的问题。确保传递给
init函数的模型文件路径字符串绝对正确。在嵌入式Linux上,可以使用ls -l <模型文件路径>命令确认文件是否存在,以及当前运行程序的用户是否有读取权限。 - 检查模型文件完整性:模型文件可能在下载或拷贝过程中损坏。尝试在PC上重新下载并传输到板子。可以计算一下文件的MD5值,与官方提供的进行比对。
- 检查模型兼容性:确认
face_detect.model是否确实是针对RK3576平台(及其特定NPU架构)转换和量化过的版本。不同平台、不同版本的RKNN SDK生成的模型可能不兼容。
7.2 推理结果异常(无检测框或框不准)
现象:程序运行不报错,但result向量为空,或者画出的框位置明显错误。排查步骤:
- 检查输入图像:确保
cv::imread成功读取了图片,并且图片是有效的、非空的。可以尝试先显示一下读取的图片。 - 检查图像格式:模型通常要求输入为RGB或BGR格式的3通道图像。如果读入的是灰度图(单通道)或RGBA图(4通道),可能导致预处理出错。使用
cv::cvtColor进行必要的转换。 - 理解坐标系统:API返回的框坐标
(x, y, width, height)是基于原始输入图像的尺寸,还是基于模型输入分辨率缩放后的尺寸?这需要查看API文档或示例代码的绘制部分来确认。本例中,绘制时直接使用了result[i].box的坐标,说明API返回的已经是原图坐标。 - 测试标准图片:用一张包含清晰正脸的标准测试图片(例如Lena图)运行,排除图片内容本身的问题。
7.3 程序运行缓慢,达不到预期帧率
现象:推理时间远高于标称的16ms。排查步骤:
- 区分推理时间与总时间:例程中测量的
time_use只包含了face_detect_run函数的执行时间。总耗时还包括了图像读取 (imread)、结果绘制 (rectangle,circle)、图像保存 (imwrite) 的时间。特别是imwrite保存高分辨率图片到SD卡,可能很慢。将耗时测量精确地包裹在推理函数前后。 - 检查CPU频率:有时系统为了省电,会将CPU运行在低频率模式。使用
sudo cpufreq-info或cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq命令查看当前CPU频率。可以尝试设置为性能模式:sudo cpufreq-set -g performance。 - 检查系统负载:使用
top或htop命令查看是否有其他进程占用了大量CPU或内存资源。 - NPU驱动与固件:确保开发板上的NPU驱动和固件是最新版本。旧版本可能存在性能问题或Bug。
7.4 内存泄漏与程序崩溃
现象:程序长时间运行后,系统可用内存越来越少,最终可能崩溃。排查步骤:
- 确保配对释放:反复检查代码,确保每一个成功的
face_detect_init调用,在程序退出前都有对应的face_detect_release调用。即使在face_detect_run失败后,也要释放已初始化的ctx。 - 使用工具检测:在Linux上,可以使用
valgrind工具来检测内存泄漏。但注意在嵌入式平台上可能需要进行交叉编译。 - 检查循环:如果在
while循环中持续处理视频帧,确保每一帧处理完后,没有在堆上分配而未释放的内存(例如,不小心在循环内new了对象但没delete)。
7.5 多线程安全
问题:如果我想在多个线程中同时调用人脸检测API,可以吗?答案:这完全取决于libface_detect.so库的实现是否是线程安全的。通常,对于这类封装了硬件加速器(NPU)的库,上下文(rknn_context ctx)不是线程安全的。这意味着:
- 你不应该让多个线程共享同一个
ctx句柄并同时调用face_detect_run,这会导致未定义行为或崩溃。 - 安全的做法是:每个线程创建自己独立的
ctx。即每个线程都调用一次face_detect_init获得自己的句柄,然后在线程内使用,最后在线程退出前调用face_detect_release。但这会占用多份NPU内存,可能受硬件限制。 - 另一种方案是使用任务队列:一个专用的推理线程持有唯一的
ctx,其他线程将待检测的图像帧放入队列,推理线程顺序处理并返回结果。这需要额外的线程间通信机制。
在尝试多线程前,务必查阅SDK文档或咨询供应商,确认库的线程安全规范。盲目使用会导致难以调试的随机性错误。
