人体关键点检测4:C/C++实现人体关键点检测(人体姿势估计)含源码 可实时检测
目录
人体关键点检测4:C/C++实现人体关键点检测(人体姿势估计)含源码 可实时检测
1.项目介绍
2.人体关键点检测方法
(1)Top-Down(自上而下)方法
(2)Bottom-Up(自下而上)方法:
3.人体关键点检测模型
(1) 人体关键点检测模型的训练
(2) 将Pytorch模型转换ONNX模型
(3) 将ONNX模型转换为TNN模型
4.人体关键点检测C/C++部署
(1)项目结构
(2)配置开发环境(OpenCV+OpenCL+base-utils+TNN)
(3)部署TNN模型
(4)CMake配置
(5)main源码
(6)源码编译和运行
(7)Demo测试效果
5.项目源码下载
6.人体关键点检测Android版本
1.项目介绍
人体关键点检测(Human Keypoints Detection)又称为人体姿态估计2D Pose,是计算机视觉中一个相对基础的任务,是人体动作识别、行为分析、人机交互等的前置任务。一般情况下可以将人体关键点检测细分为单人/多人关键点检测、2D/3D关键点检测,同时有算法在完成关键点检测之后还会进行关键点的跟踪,也被称为人体姿态跟踪。
项目将实现人体关键点检测算法,其中使用YOLOv5模型实现人体检测(Person Detection),使用HRNet,LiteHRNet和Mobilenet-v2模型实现人体关键点检测。为了方便后续模型工程化和Android平台部署,项目支持高精度HRNet检测模型,轻量化模型LiteHRNet和Mobilenet模型训练和测试,并提供Python/C++/Android多个版本;项目分为数据集说明,模型训练和C++/Android部署等多个章节,本篇是项目《人体关键点检测(人体姿势估计)》系列文章之C/C++实现人体关键点检测(人体姿势估计),主要分享将Python训练好的模型移植到C/C++平台,搭建一个可实时的人体关键点检测C/C++ Demo,且支持多人关键点检测。
轻量化Mobilenet-v2模型在普通Android手机上可以达到实时的检测效果,CPU(4线程)约50ms左右,GPU约30ms左右 ,基本满足业务的性能需求。下表格给出HRNet,以及轻量化模型LiteHRNet和Mobilenet的计算量和参数量,以及其检测精度。
模型 | input-size | params(M) | GFLOPs | AP |
HRNet-w32 | 192×256 | 28.48M | 5734.05M | 0.7585 |
LiteHRNet18 | 192×256 | 1.10M | 182.15M | 0.6237 |
Mobilenet-v2 | 192×256 | 2.63M | 529.25M | 0.6181 |
【尊重原创,转载请注明出处】https://blog.csdn.net/guyuealian/article/details/134881831
C/C++版本人体关键点检测与Python版本的检测效果基本一致:
更多项目《人体关键点检测(人体姿势估计)》系列文章请参考:
- 人体关键点检测1:人体姿势估计数据集(含下载链接) https://blog.csdn.net/guyuealian/article/details/134703548
- 人体关键点检测2:Pytorch实现人体关键点检测(人体姿势估计)含训练代码和数据集 https://blog.csdn.net/guyuealian/article/details/134837816
- 人体关键点检测3:Android实现人体关键点检测(人体姿势估计)含源码 可实时检测 https://blog.csdn.net/guyuealian/article/details/134881797
- 人体关键点检测4:C/C++实现人体关键点检测(人体姿势估计)含源码 可实时检测 https://blog.csdn.net/guyuealian/article/details/134881831
- 手部关键点检测1:手部关键点(手部姿势估计)数据集(含下载链接)https://blog.csdn.net/guyuealian/article/details/133277630
- 手部关键点检测2:YOLOv5实现手部检测(含训练代码和数据集)https://blog.csdn.net/guyuealian/article/details/133279222
- 手部关键点检测3:Pytorch实现手部关键点检测(手部姿势估计)含训练代码和数据集https://blog.csdn.net/guyuealian/article/details/133277726
- 手部关键点检测4:Android实现手部关键点检测(手部姿势估计)含源码 可实时检测https://blog.csdn.net/guyuealian/article/details/133931698
- 手部关键点检测5:C++实现手部关键点检测(手部姿势估计)含源码 可实时检测https://blog.csdn.net/guyuealian/article/details/133277748
2.人体关键点检测方法
目前主流的人体关键点检测(人体姿势估计)方法主要两种:一种是Top-Down(自上而下)方法,另外一种是Bottom-Up(自下而上)方法;
(1)Top-Down(自上而下)方法
将人体检测和人体关键点检测(人体姿势估计)检测分离,在图像上首先进行人体目标检测,定位人体位置;然后crop每一个人体图像,再估计人体关键点;这类方法往往比较慢,但姿态估计准确度较高。目前主流模型主要有CPN,Hourglass,CPM,Alpha Pose,HRNet等。
(2)Bottom-Up(自下而上)方法:
先估计图像中所有人体关键点,然后在通过Grouping的方法组合成一个一个实例;因此这类方法在测试推断的时候往往更快速,准确度稍低。典型就是COCO2016年人体关键点检测冠军Open Pose。
通常来说,Top-Down具有更高的精度,而Bottom-Up具有更快的速度;就目前调研而言, Top-Down的方法研究较多,精度也比Bottom-Up(自下而上)方法高。本项目采用Top-Down(自上而下)方法,先使用YOLOv5模型实现人体检测,然后再使用HRNet进行人体关键点检测(人体姿势估计);
本项目基于开源的HRNet进行改进,关于HRNet项目请参考GitHub
HRNet: https://github.com/leoxiaobin/deep-high-resolution-net.pytorch
3.人体关键点检测模型
(1) 人体关键点检测模型的训练
本项目采用Top-Down(自上而下)方法,使用YOLOv5模型实现人体检测,并基于开源的HRNet实现人体关键点检测(人体姿态估计);为了方便后续模型工程化和Android平台部署,项目支持轻量化模型LiteHRNet和Mobilenet模型训练和测试,并提供Python/C++/Android多个版本;轻量化Mobilenet-v2模型在普通Android手机上可以达到实时的检测效果,CPU(4线程)约50ms左右,GPU约30ms左右 ,基本满足业务的性能需求
本篇博文主要分享Android版本的模型部署,不包含Python版本的训练代码和相关数据集,关于人体关键点检测的训练方法和数据集说明,可参考 :
Pytorch实现人体关键点检测(人体姿势估计)含训练代码和数据集 https://blog.csdn.net/guyuealian/article/details/134837816
下表格给出HRNet,以及轻量化模型LiteHRNet和Mobilenet的计算量和参数量,以及其检测精度AP; 高精度检测模型HRNet-w32,AP可以达到0.7585,但其参数量和计算量比较大,不合适在移动端部署;LiteHRNet18和Mobilenet-v2参数量和计算量比较少,合适在移动端部署;虽然LiteHRNet18的理论计算量和参数量比Mobilenet-v2低,但在实际测试中,发现Mobilenet-v2运行速度更快。轻量化Mobilenet-v2模型在普通Android手机上可以达到实时的检测效果,CPU(4线程)约50ms左右,GPU约30ms左右 ,基本满足业务的性能需求
模型 | input-size | params(M) | GFLOPs | AP |
HRNet-w32 | 192×256 | 28.48M | 5734.05M | 0.7585 |
LiteHRNet18 | 192×256 | 1.10M | 182.15M | 0.6237 |
Mobilenet-v2 | 192×256 | 2.63M | 529.25M | 0.6181 |
HRNet-w32参数量和计算量太大,不适合在Android手机部署,本项目Android版本只支持部署LiteHRNet和Mobilenet-v2模型;C++版本可支持部署HRNet-w32,LiteHRNet和Mobilenet-v2模型
(2) 将Pytorch模型转换ONNX模型
目前CNN模型有多种部署方式,可以采用TNN,MNN,NCNN,以及TensorRT等部署工具,鄙人采用TNN进行C/C++端上部署。部署流程可分为四步:训练模型->将模型转换ONNX模型->将ONNX模型转换为TNN模型->C/C++部署TNN模型。
训练好Pytorch模型后,我们需要先将模型转换为ONNX模型,以便后续模型部署。
- 原始项目提供转换脚本,你只需要修改model_file为你模型路径即可
- convert_torch_to_onnx.py实现将Pytorch模型转换ONNX模型的脚本
python libs/convert_tools/convert_torch_to_onnx.py
"""
This code is used to convert the pytorch model into an onnx format model.
"""
import os
import torch.onnx
from pose.inference import PoseEstimation
from basetrainer.utils.converter import pytorch2onnx
def load_model(config_file, model_file, device="cuda:0"):
pose = PoseEstimation(config_file, model_file, device=device)
model = pose.model
config = pose.config
return model, config
def convert2onnx(config_file, model_file, device="cuda:0", onnx_type="kp"):
"""
:param model_file:
:param input_size:
:param device:
:param onnx_type:
:return:
"""
model, config = load_model(config_file, model_file, device=device)
model = model.to(device)
model.eval()
model_name = os.path.basename(model_file)[:-len(".pth")]
onnx_file = os.path.join(os.path.dirname(model_file), model_name + ".onnx")
# dummy_input = torch.randn(1, 3, 240, 320).to("cuda")
input_size = tuple(config.MODEL.IMAGE_SIZE) # w,h
input_shape = (1, 3, input_size[1], input_size[0])
pytorch2onnx.convert2onnx(model,
input_shape=input_shape,
input_names=['input'],
output_names=['output'],
onnx_file=onnx_file,
opset_version=11)
print(input_shape)
if __name__ == "__main__":
config_file = "../../work_space/person/mobilenet_v2_17_192_256_custom_coco_20231124_090015_6639/mobilenetv2_192_192.yaml"
model_file = "../../work_space/person/mobilenet_v2_17_192_256_custom_coco_20231124_090015_6639/model/best_model_158_0.6181.pth"
convert2onnx(config_file, model_file)
(3) 将ONNX模型转换为TNN模型
目前CNN模型有多种部署方式,可以采用TNN,MNN,NCNN,以及TensorRT等部署工具,鄙人采用TNN进行C/C++端上部署
TNN转换工具:
- (1)将ONNX模型转换为TNN模型,请参考TNN官方说明:TNN/onnx2tnn.md at master · Tencent/TNN · GitHub
- (2)一键转换,懒人必备:一键转换 Caffe, ONNX, TensorFlow 到 NCNN, MNN, Tengine (可能存在版本问题,这个工具转换的TNN模型可能不兼容,建议还是自己build源码进行转换,2022年9约25日测试可用)
4.人体关键点检测C/C++部署
项目IDE开发工具使用CLion,相关依赖库主要有OpenCV,base-utils以及TNN和OpenCL(可选),其中OpenCV必须安装,OpenCL用于模型加速,base-utils以及TNN已经配置好,无需安装;
项目仅在Ubuntu18.04进行测试,Windows系统下请自行配置好开发环境。
(1)项目结构
(2)配置开发环境(OpenCV+OpenCL+base-utils+TNN)
项目IDE开发工具使用CLion,相关依赖库主要有OpenCV,base-utils以及TNN和OpenCL(可选),其中OpenCV必须安装,OpenCL用于模型加速,base-utils以及TNN已经配置好,无需安装;
项目仅在Ubuntu18.04进行测试,Windows系统下请自行配置和编译
- 安装OpenCV:图像处理
图像处理(如读取图片,图像裁剪等)都需要使用OpenCV库进行处理
安装教程:Ubuntu18.04安装opencv和opencv_contrib
OpenCV库使用opencv-4.3.0版本,opencv_contrib库暂时未使用,可不安装
- 安装OpenCL:模型加速
安装教程:Ubuntu16.04 安装OpenCV&OpenCL
OpenCL用于模型GPU加速,若不使用OpenCL进行模型推理加速,纯C++推理模型,速度会特别特别慢
- base-utils:C++库
GitHub:https://github.com/PanJinquan/base-utils (无需安装,项目已经配置了)
base_utils是个人开发常用的C++库,集成了C/C++ OpenCV等常用的算法
- TNN:模型推理
GitHub:https://github.com/Tencent/TNN (无需安装,项目已经配置了)
由腾讯优图实验室开源的高性能、轻量级神经网络推理框架,同时拥有跨平台、高性能、模型压缩、代码裁剪等众多突出优势。TNN框架在原有Rapidnet、ncnn框架的基础上进一步加强了移动端设备的支持以及性能优化,同时借鉴了业界主流开源框架高性能和良好拓展性的特性,拓展了对于后台X86, NV GPU的支持。手机端 TNN已经在手机QQ、微视、P图等众多应用中落地,服务端TNN作为腾讯云AI基础加速框架已为众多业务落地提供加速支持。
(3)部署TNN模型
项目实现了C/C++版本的人体检测和人体关键点检测,人体检测模型使用YOLOv5和车牌识别模型HRNet模型,模型推理采用TNN部署框架(支持多线程CPU和GPU加速推理);图像处理采用OpenCV库,模型加速采用OpenCL,在普通设备即可达到实时处理。
如果你想在这个 Demo部署你自己训练的模型,你可将训练好的Pytorch模型转换ONNX ,再转换成TNN模型,然后把原始的模型替换成你自己的TNN模型即可。
(4)CMake配置
这是CMakeLists.txt,其中主要配置OpenCV+OpenCL+base-utils+TNN这四个库,Windows系统下请自行配置和编译
cmake_minimum_required(VERSION 3.5)
project(Detector)
add_compile_options(-fPIC) # fix Bug: can not be used when making a shared object
set(CMAKE_CXX_FLAGS "-Wall -std=c++11 -pthread")
#set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
#set(CMAKE_CXX_FLAGS_DEBUG "-g")
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
# -DCMAKE_BUILD_TYPE=Debug
# -DCMAKE_BUILD_TYPE=Release
message(STATUS "No build type selected, default to Release")
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type (default Debug)" FORCE)
endif ()
# opencv set
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS} ./src/)
#MESSAGE(STATUS "OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}")
# base_utils
set(BASE_ROOT 3rdparty/base-utils) # 设置base-utils所在的根目录
add_subdirectory(${BASE_ROOT}/base_utils/ base_build) # 添加子目录到build中
include_directories(${BASE_ROOT}/base_utils/include)
include_directories(${BASE_ROOT}/base_utils/src)
MESSAGE(STATUS "BASE_ROOT = ${BASE_ROOT}")
# TNN set
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake buil ds it for you.
# Gradle automatically packages shared libraries with your APK.
# build for platform
# set(TNN_BUILD_SHARED OFF CACHE BOOL "" FORCE)
if (CMAKE_SYSTEM_NAME MATCHES "Android")
set(TNN_OPENCL_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_ARM_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(TNN_OPENMP_ENABLE ON CACHE BOOL "" FORCE) # Multi-Thread
#set(TNN_HUAWEI_NPU_ENABLE OFF CACHE BOOL "" FORCE)
add_definitions(-DTNN_OPENCL_ENABLE) # for OpenCL GPU
add_definitions(-DTNN_ARM_ENABLE) # for Android CPU
add_definitions(-DDEBUG_ANDROID_ON) # for Android Log
add_definitions(-DPLATFORM_ANDROID)
elseif (CMAKE_SYSTEM_NAME MATCHES "Linux")
set(TNN_OPENCL_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_CPU_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_X86_ENABLE OFF CACHE BOOL "" FORCE)
set(TNN_QUANTIZATION_ENABLE OFF CACHE BOOL "" FORCE)
set(TNN_OPENMP_ENABLE ON CACHE BOOL "" FORCE) # Multi-Thread
add_definitions(-DTNN_OPENCL_ENABLE) # for OpenCL GPU
add_definitions(-DDEBUG_ON) # for WIN/Linux Log
add_definitions(-DDEBUG_LOG_ON) # for WIN/Linux Log
add_definitions(-DDEBUG_IMSHOW_OFF) # for OpenCV show
add_definitions(-DPLATFORM_LINUX)
elseif (CMAKE_SYSTEM_NAME MATCHES "Windows")
set(TNN_OPENCL_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_CPU_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_X86_ENABLE ON CACHE BOOL "" FORCE)
set(TNN_QUANTIZATION_ENABLE OFF CACHE BOOL "" FORCE)
set(TNN_OPENMP_ENABLE ON CACHE BOOL "" FORCE) # Multi-Thread
add_definitions(-DTNN_OPENCL_ENABLE) # for OpenCL GPU
add_definitions(-DDEBUG_ON) # for WIN/Linux Log
add_definitions(-DDEBUG_LOG_ON) # for WIN/Linux Log
add_definitions(-DDEBUG_IMSHOW_OFF) # for OpenCV show
add_definitions(-DPLATFORM_WINDOWS)
endif ()
set(TNN_ROOT 3rdparty/TNN)
include_directories(${TNN_ROOT}/include)
include_directories(${TNN_ROOT}/third_party/opencl/include)
add_subdirectory(${TNN_ROOT}) # 添加外部项目文件夹
set(TNN -Wl,--whole-archive TNN -Wl,--no-whole-archive)# set TNN library
MESSAGE(STATUS "TNN_ROOT = ${TNN_ROOT}")
# Detector
include_directories(src)
set(SRC_LIST
src/Interpreter.cpp
src/pose_detector.cpp
src/object_detection.cpp
src/pose_filter.cpp
src/yolov5.cpp
)
add_library(dlcv SHARED ${SRC_LIST})
target_link_libraries(dlcv ${OpenCV_LIBS} base_utils)
MESSAGE(STATUS "DIR_SRCS = ${SRC_LIST}")
add_executable(Detector src/main.cpp)
target_link_libraries(Detector dlcv ${TNN} -lpthread)
(5)main源码
主程序中函数main实现提供了人体检测和人体关键点检测的使用方法,支持图片,视频和摄像头测试
- test_image_file(); // 测试图片文件
- test_video_file(); // 测试视频文件
- test_camera(); //测试摄像头
//
// Created by 390737991@qq.com on 2020/6/3.
//
#include "pose_detector.h"
#include "object_detection.h"
#include "yolov5.h"
#include "Types.h"
#include <iostream>
#include <string>
#include <vector>
#include "file_utils.h"
#include "image_utils.h"
using namespace dl;
using namespace vision;
using namespace std;
const int num_thread = 1; // 开启CPU线程数目
DeviceType device = GPU; // 选择运行设备CPU/GPU
// 目标检测SSD或者YOLOv5
const float scoreThresh = 0.5;
const float iouThresh = 0.3;
//const char *det_model_file = (char *) "../data/tnn/ssd/rfb1.0_person_320_320_sim.opt.tnnmodel";
//const char *det_proto_file = (char *) "../data/tnn/ssd/rfb1.0_person_320_320_sim.opt.tnnproto";
//ObjectDetectionParam model_param = PERSON_MODEL;//模型参数
//ObjectDetection *detector = new ObjectDetection(det_model_file, det_proto_file, model_param, num_thread, device);
const char *det_model_file = (char *) "../data/tnn/yolov5/yolov5s05_320.sim.tnnmodel";
const char *det_proto_file = (char *) "../data/tnn/yolov5/yolov5s05_320.sim.tnnproto";
YOLOv5Param dets_model_param = YOLOv5s05_320;//模型参数
YOLOv5 *detector = new YOLOv5(det_model_file,
det_proto_file,
dets_model_param,
num_thread,
device);
// 关键点检测
const float poseThresh = 0.3;
//const char *pose_model_file = (char *) "../data/tnn/pose/mobilenet_v2_17_192_256.sim.tnnmodel";
//const char *pose_proto_file = (char *) "../data/tnn/pose/mobilenet_v2_17_192_256.sim.tnnproto";
const char *pose_model_file = (char *) "../data/tnn/pose/hrnet_w32_17_192_256.sim.tnnmodel";
const char *pose_proto_file = (char *) "../data/tnn/pose/hrnet_w32_17_192_256.sim.tnnproto";
PoseParam pose_model_param = COCO_PERSON_PARAM;//模型参数
PoseDetector *pose = new PoseDetector(pose_model_file, pose_proto_file, pose_model_param, num_thread, device);
int test_image_file() {
//测试图片的目录
string image_dir = "../data/test_images";
std::vector<string> image_list = get_files_list(image_dir);
for (string image_path:image_list) {
cv::Mat bgr = cv::imread(image_path);
if (bgr.empty()) continue;
FrameInfo resultInfo;
// 进行目标检测
detector->detect(bgr, &resultInfo, scoreThresh, iouThresh);
// 进行关键点检测
pose->detect(bgr, &resultInfo, poseThresh);
// 可视化代码
pose->visualizeResult(bgr, resultInfo, pose_model_param.skeleton, false, 0);
}
printf("FINISHED.\n");
return 0;
}
/***
* 测试视频文件
* @return
*/
int test_video_file() {
//测试视频文件
string video_file = "../data/video/video-test.mp4";
cv::VideoCapture cap;
bool ret = get_video_capture(video_file, cap);
cv::Mat frame;
while (ret) {
cap >> frame;
if (frame.empty()) break;
FrameInfo resultInfo;
// 进行目标检测
detector->detect(frame, &resultInfo, scoreThresh, iouThresh);
// 进行关键点检测
pose->detect(frame, &resultInfo, poseThresh);
// 可视化代码
pose->visualizeResult(frame, resultInfo, pose_model_param.skeleton, false, 5);
}
cap.release();
printf("FINISHED.\n");
return 0;
}
/***
* 测试摄像头
* @return
*/
int test_camera() {
int camera = 0; //摄像头ID号(请修改成自己摄像头ID号)
cv::VideoCapture cap;
bool ret = get_video_capture(camera, cap);
cv::Mat frame;
while (ret) {
cap >> frame;
if (frame.empty()) break;
FrameInfo resultInfo;
// 进行目标检测
detector->detect(frame, &resultInfo, scoreThresh, iouThresh);
// 进行关键点检测
pose->detect(frame, &resultInfo, poseThresh);
// 可视化代码
pose->visualizeResult(frame, resultInfo, pose_model_param.skeleton, false, 5);
}
cap.release();
printf("FINISHED.\n");
return 0;
}
int main() {
test_image_file(); // 测试图片文件
//test_video_file(); // 测试视频文件
//test_camera(); //测试摄像头
return 0;
}
(6)源码编译和运行
编译脚本,或者直接:bash build.sh
#!/usr/bin/env bash
if [ ! -d "build/" ];then
mkdir "build"
else
echo "exist build"
fi
cd build
cmake ..
make -j4
sleep 1
./Detector
- 如果你要测试CPU运行的性能,请修改src/main.cpp
DeviceType device = CPU;
- 如果你要测试GPU运行的性能,请修改src/main.cpp (需配置好OpenCL)
DeviceType device = GPU;
PS:纯CPU C++推理模式比较耗时,需要几秒的时间,而开启OpenCL加速后,GPU模式耗时仅需十几毫秒,性能极大的提高。
(7)Demo测试效果
C++版本与Python版本的结果几乎是一致,下面是人体关键点的检测效果展示(支持单人和多人人体关键点检测):
5.项目源码下载
项目源码下载地址:C/C++实现人体关键点检测(人体姿势估计)含源码 可实时检测
整套项目源码内容包含:
- C/C++源码支持YOLOv5人体检测
- C/C++源码提供高精度版本HRNet人体关键点检测
- C/C++源码提供轻量化模型LiteHRNet和Mobilenet-v2人体关键点检测
- C/C++源码支持CPU和GPU,开启GPU(OpenCL)可以实时检测和识别(纯CPU推理速度很慢,模型加速需要配置好OpenCL,GPU推理约15ms左右)
- C/C++源码Demo支持图片,视频,摄像头测试
- 项目配置好了base-utils和TNN,而OpenCV和OpenCL需要自行编译安装
6.人体关键点检测Android版本
- 人体关键点检测3:Android实现人体关键点检测(人体姿势估计)含源码 可实时检测 https://blog.csdn.net/guyuealian/article/details/134881797
Android人体关键点检测APP Demo体验(下载):https://download.csdn.net/download/guyuealian/88610359