个人笔记:
存储点云文件:pcl::io::savePCDFileASCII(“…/path”, cloud);
#include<iostream>
#include<pcl/io/pcd_io.h>//pcd读写相关的头文件夹
#include<pcl/point_types.h>//pcd中支持的点类型头文件,比如pcl::PointXYZ
//1、写入数据
int main01(int argc, char** argv)
{
//创建点云 描述我们将创建模板化PointCloud结构
pcl::PointCloud<pcl::PointXYZ> cloud;
//设置相关参数
/*
cloud.height = 1; 用来判断是否为有序点云,=1为无序点云
无序点云: cloud.width指点云的个数,结构点云:云数据集一行上点的个数
cloud.is_dense(bool) 判断points中数据是否有限或者判断点云中的点是否包含 Inf/NaN这种值(包含为false)
*/
cloud.width = 5;
cloud.height = 1;
cloud.is_dense = false;//不是稠密型
cloud.points.resize(cloud.width * cloud.height);//点云的总数
//用随机数的值填充PointCloud点云对象
//for(auto &point:cloud)
for (size_t i = 0; i < cloud.points.size(); ++i)
{
//0~1024之间不等于1024的一个随机浮点数
cloud.points[i].x = 1024 * rand() / (RAND_MAX + 1.0f);
cloud.points[i].y = 1024 * rand() / (RAND_MAX + 1.0f);
cloud.points[i].z = 1024 * rand() / (RAND_MAX + 1.0f);
}
//把PointCloud对象存储在test_pcd.pcd文件中
//pcl::io::savePCDFileASCII("D://0718box//test_pcd.pcd", cloud);
pcl::io::savePCDFileASCII("../test_pcd.pcd", cloud);
//打印输出存储点云的数据
std::cerr << "Saved:" << cloud.points.size() << "data points to test_pcd.cpp" << std::endl;
for (size_t i = 0; i < cloud.points.size(); ++i)
std::cerr << " " << cloud.points[i].x << " "
<< cloud.points[i].y << " " << cloud.points[i].z << std::endl;
return (0);
}
笔记:
出现原因:在读取点云时,debug下不能正常运行,release可以
debug:会增加调试代码
release用于发布 release下不报错是忽略了错误而不是没有错误
*打开点云文件:pcl::io::loadPCDFilepcl::PointXYZ(pcd_path, cloud)
步骤:
#include<iostream>
#include<pcl/io/pcd_io.h>
#include<pcl/point_types.h>
int main02(int argc, char** argv)
{
//创建一个PointCloud<pcl::PointXYZ>
//boost共享指针进行初始化
//pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud< pcl::PointXYZ >);
//pcl::PointCloud<pcl::PointXYZ>::Ptr cloud = std::make_shared<pcl::PointCloud<pcl::PointXYZ>>();
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
std::string pcd_path = "../test_pcd.pcd";
//打开点云文件
// 将test_pcd.pcd 加载到二进制blob中
//if (pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//test_pcd.pcd", *cloud) == -1)
if (pcl::io::loadPCDFile<pcl::PointXYZ>(pcd_path, *cloud) == -1)
{
//PCL_ERROR("Cloud not read file test_pcd.pcd \n");
std::cerr << "Cloud not read file test_pcd.pcd" << std::endl;
return (-1);
}
std::cout << " Loaded "
<< cloud->width * cloud->height
<< " data points from test_pcd.pcd with the following fields:"
<< std::endl;
//for (size_t i = 0; i < cloud->points.size(); ++i)
//{
// std::cout << " " << cloud->points[i].x
// << " " << cloud->points[i].y
// << " " << cloud->points[i].z << std::endl;
//}
//读取二进制blob并将其转换为模板化的PointCloud格式
for (const auto& point : *cloud)
{
std::cout << " "<<point.x << " " << point.y << " " << point.z << std::endl;
}
return (0);
}
点云连接 concatenate points 列的下方连接
字段连接 concatenate fields 行的基础后连接
**pcl::concatenateFields(cloud_a, n_cloud_b,p_n_cloud_c);**
在连接两个点云前,要确保两个数据集中字段的类型相同和维度相同,
同时了解如何连接两个不同点云的字段颜色,法线),这种操作的强制约束条件是两个数据集中的点数目必须一样
个人笔记:
1、std::cerr与std::cout的区别
cerr是一个ostream对象,C++标准错误输出流,关联到标准错误,写到cerr的数据是不缓冲的,
cree通常用于输出错误与其他不属于正常逻辑的输出内容
cree的目的:在紧急情况下,能得到输出功能。
缓冲区:减少刷屏的次数,不带缓冲,就会每写一个字母,输出一个字母,不断刷屏;有了缓冲区,就会看到很多个语句同时出现在屏幕上
2、pcl::Normal 是最常见的一种点的类型,代表了给定点的曲面法线(normal)和曲率的测量
pcl::PointCloud类的模板参数,定义了点云的存储类型
常用的点的类型:
pcl::PointXYZ,存储着点的x,y,z信息
pcl::PointXYZI,在上面的基础上,包含了一个域描述点的密集程度
pcl::PointWithRange,存储点的范围
pcl::PointXYZRGBA:存储3D信息的同时和RGB与Alpha(透明度)
用户也可以自己定义自己的类型
#pragma warning(disable:4996);
#include<iostream>
#include<pcl/io/pcd_io.h>
#include<pcl/point_types.h>
int main03(int argc, char** argv)
{
pcl::PointCloud<pcl::PointXYZ> cloud_a, cloud_b, cloud_c;//三个输入点云
pcl::PointCloud<pcl::Normal> n_cloud_b;// 两个输出点云,储存法线的点云
pcl::PointCloud<pcl::PointNormal> p_n_cloud_c;//储存xyz和法线的点云
//创建点云数据
cloud_a.width = 3;
cloud_a.height = cloud_b.height= n_cloud_b.height=1;//无序点云
cloud_a.points.resize(cloud_a.width * cloud_a.height);
cloud_b.width = 5;//点云连接---列下面连接
cloud_b.points.resize(cloud_b.width * cloud_b.height);
n_cloud_b.width = 3;//字段连接---行后面连接
n_cloud_b.points.resize(n_cloud_b.width * n_cloud_b.height);
//填充点云cloud_a的数据
for (size_t i = 0; i < cloud_a.points.size(); ++i)
{
cloud_a.points[i].x = 1024 * rand() / (RAND_MAX + 1.0f);
cloud_a.points[i].y = 1024 * rand() / (RAND_MAX + 1.0f);
cloud_a.points[i].z = 1024 * rand() / (RAND_MAX + 1.0f);
}
//填充点云cloud_b的数据
for (size_t i = 0; i < cloud_b.points.size(); ++i)
{
cloud_b.points[i].x = 1024 * rand() / (RAND_MAX + 1.0f);
cloud_b.points[i].y = 1024 * rand() / (RAND_MAX + 1.0f);
cloud_b.points[i].z = 1024 * rand() / (RAND_MAX + 1.0f);
}
//填充点云n_cloud_b的数据
for (size_t i = 0; i < n_cloud_b.points.size(); ++i)
{
n_cloud_b.points[i].normal[0] = 1024 * rand() / (RAND_MAX + 1.0f);
n_cloud_b.points[i].normal[1] = 1024 * rand() / (RAND_MAX + 1.0f);
n_cloud_b.points[i].normal[2] = 1024 * rand() / (RAND_MAX + 1.0f);
}
//输出点云数据
std::cerr << "Cloud A:" << std::endl;
for (size_t i = 0; i < cloud_a.points.size(); ++i)
{
std::cerr << cloud_a.points[i].x << " "
<< cloud_a.points[i].y << " " << cloud_a.points[i].z << " " << std::endl;
}
std::cerr << "Cloud B:" << std::endl;
for (size_t i = 0; i < cloud_b.points.size(); ++i)
{
std::cerr << cloud_b.points[i].x << " "
<< cloud_b.points[i].y << " " << cloud_b.points[i].z << " " << std::endl;
}
std::cerr << "n_Cloud_b:" << std::endl;
for (size_t i = 0; i < n_cloud_b.points.size(); ++i)
{
std::cerr << n_cloud_b.points[i].normal[0]<< " "
<< n_cloud_b.points[i].normal[1] << " "
<< n_cloud_b.points[i].normal[2] << " " << std::endl;
}
//点云连接 cloud_c = cloud_a + cloud_b
cloud_c = cloud_a + cloud_b;
std::cerr << "点云连接 Cloud C:" << std::endl;
for (size_t i = 0; i < cloud_c.points.size(); ++i)
{
std::cerr << cloud_c.points[i].x << " "
<< cloud_c.points[i].y << " " << cloud_c.points[i].z << " " << std::endl;
}
//字段连接 pcl::concatenateFields(cloud_a, n_cloud_b, p_n_cloud_c);
//错误的:p_n_cloud_c = cloud_a + n_cloud_b;
//解决办法:把cloud_a和n_cloud_b字段连接,一起创建p_n_cloud_c
pcl::concatenateFields(cloud_a, n_cloud_b, p_n_cloud_c);
std::cerr << "字段连接 p_n_Cloud C:" << std::endl;
for (size_t i = 0; i < p_n_cloud_c.points.size(); ++i)
{
std::cerr << p_n_cloud_c.points[i].x << " "
<< p_n_cloud_c.points[i].y << " " << p_n_cloud_c.points[i].z << " "
<< p_n_cloud_c.points[i].normal[0] << " "
<< p_n_cloud_c.points[i].normal[1] << " "
<< p_n_cloud_c.points[i].normal[2] << " "<<std::endl;
}
return (0);
}
boost::signals2实现了线程安全,是一种函数的回调机制
#include<pcl/point_cloud.h>//点云类定义头文件
#include<pcl/point_types.h>//点 类型定义头文件
#include<pcl/io/openni2_grabber.h>//OpenNI数据流获取头文件
#include<pcl/common/time.h>//时间头文件
#include<boost/thread/thread.hpp>//用来创建一个新线程,简化多线程的开发
//类SimpleOpenNIProcessor 的回调函数,对数据进行处理的回调函数的封装
class SimpleOpenNIProcessor
{
public:
void cloud_cb(const pcl::PointCloud<pcl::PointXYZRGBA>::ConstPtr& cloud)
{
static unsigned count = 0;
static double last = pcl::getTime();//获取当前时间
if (++count == 30)//30ms
{
double now = pcl::getTime();
std::cout << "中心像素的位置:" << cloud->points[(cloud->width >> 1)
* (cloud->height + 1)].z
<< "mm. Average framerate:" << double(count) / double(now - last)
<< " Hz" << std::endl;
last = now;
count = 0;
}
}
//pcl::Grabber
void run()
{
//创建一个:OpenNI2Grabber接口
pcl::Grabber* interface = new pcl::io::OpenNI2Grabber();
//定义回调函数
boost::function<void(const pcl::PointCloud <pcl::PointXYZRGB>::ConstPtr&)>f =
boost::bind(&SimpleOpenNIProcessor::cloud_cb, this,-1);
boost::signals2::connection conn = interface->registerCallback(f);//注册回调函数
interface->start();//开始接收点云数据
while (true)
//添加头文件:#include<boost/thread/thread.hpp>
boost::this_thread::sleep(boost::posix_time::seconds(1));
//停止采集
interface->stop();
}
};
int main04()
{
SimpleOpenNIProcessor sONIP;
sONIP.run();
return 0;
}
个人笔记:
1、cloud.makeShared();
pcl::PointCloud.makeshared()返回的是一个对当前点云深度复制后的对象的智能指针
也就是说
pcl::PointCloudcloud;
pcl::PointCloud::Ptr p=cloud.makeshared();
其中p并不指向cloud的空间,而是深拷贝了cloud放在新的内存空间上,然后返回新空间的指针
所以,用过p操作点云,cloud不会改变
2、加快ASCII格式存储,将pcd文件换成Binary格式存储
pcl::io::savePCDFileBinary(“path”,*cloud);
void get_PCD_Fromat()
{
pcl::PCLPointCloud2 cloud;
pcl::PCDReader reader;
reader.readHeader("D://0718box//test_pcd.pcd", cloud);
for (int i = 0; i < cloud.fields.size(); i++)
{
std::cout << cloud.fields[i].name<<std::endl;//cout:xyz型
}
}
void type_Conversion()
{
//创建存放点云的对象
pcl::PointCloud < pcl::PointXYZ>::Ptr cloudPointer(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>cloud;
cloud = *cloudPointer;
cloudPointer = cloud.makeShared();
}
头文件:#include<pcl/visualization/pcl_visualizer.h>
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sig(cloud, 255, 22, 99);
#include<pcl/visualization/pcl_visualizer.h>
void pcd_visualization()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//0718-04.pcd", *cloud);
pcl::visualization::PCLVisualizer viewer("pointcloud viewer");
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sig(cloud, 255, 22, 99);
viewer.addPointCloud(cloud, sig, "cloud");
//设置大小
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1);
//添加一个坐标系
viewer.addCoordinateSystem(500);
while (!viewer.wasStopped())
{
viewer.spinOnce();
}
}
pcl::getMinMax3D函数说明:
cloud为输入点云,而非指针(共享指针则写为*cloud),
输出min_pt为所有点中最小的x值,y值,z值,输出max_pt为为所有点中最大的x值,y值,z值.而非某个点。
头文件:#include<pcl/common/common.h>
void pcl_Max_Min()
{
//pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud;
cloud = pcl::PointCloud<pcl::PointXYZ>::Ptr(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//0718-04.pcd", *cloud);
pcl::PointXYZ minPt, maxPt;
pcl::getMinMax3D(*cloud, minPt, maxPt);
//pcl::getMinMax3D(*cloud, minPt, maxPt);
std::cout << "Max_x:" << maxPt.x << " " << "Max_y:" << maxPt.y << " " << "Max_z:" << maxPt.z << std::endl;
std::cout << "Min_x:" << maxPt.x << " " << "Min_y:" << minPt.y<< " " << "Min_z:" << minPt.z << std::endl;
}
#include<pcl/visualization/cloud_viewer.h>
#include<pcl/visualization/pcl_visualizer.h>
void pcl_Copy()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//0718-04.pcd", *cloud);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_Out(new pcl::PointCloud<pcl::PointXYZ>);
std::vector<int> index = {
1, 3, 5, 7, 8, 11, 56, 77 };
pcl::copyPointCloud(*cloud, index, *cloud_Out);
pcl::io::savePCDFileASCII("D://0718box//0718-04_test.pcd", *cloud_Out);
//打印输出点云数据
for (size_t i = 0; i < cloud_Out->points.size(); ++i)
{
std::cout << cloud->points[i].x << " " << cloud->points[i].y << " " << cloud->points[i].z << std::endl;
}
//可视化
pcl::visualization::PCLVisualizer viewer("Cloud Viewer");//创建对象
viewer.setBackgroundColor(0.05, 0.05, 0.05, 0);//背景为白色
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sig(cloud, 230, 66, 99);
viewer.addPointCloud(cloud_Out, sig, "cloud");
//设置大小
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5);
pcl::io::savePCDFileASCII("D://0718box//0718-04_test.pcd", *cloud_Out);
//直到关闭窗口结束循环
while (!viewer.wasStopped())
{
//每次循环调用内部的重绘函数
viewer.spinOnce();
}
}
//从点云中添加和删除点
void pcd_del()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//0718-04.pcd", *cloud);
pcl::PointCloud<pcl::PointXYZ>::iterator index = cloud->begin();
cloud->erase(index, index + 999);//区间删除
index = cloud->begin() + 999;//单个删除
cloud->erase(cloud->begin());
//可视化
pcl::visualization::PCLVisualizer viewer("Cloud Viewer");//创建对象
viewer.setBackgroundColor(0.05, 0.05, 0.05, 0);//背景为白色
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sig(cloud, 230, 66, 99);
viewer.addPointCloud(cloud, sig, "cloud");
//设置大小
//viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1);
//直到关闭窗口结束循环
while (!viewer.wasStopped())
{
//每次循环调用内部的重绘函数
viewer.spinOnce();
}
}
void pcd_add()
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile<pcl::PointXYZ>("D://0718box//0718-04_test.pcd", *cloud);
pcl::PointCloud<pcl::PointXYZ>::iterator index = cloud->begin();
pcl::PointXYZ point = {
1,1,1 };
//std::vector<int> index = { 1, 3, 5, 7, 8, 11, 56, 77 };
cloud->insert(cloud->begin() + 7, point);//在索引号为7的位置1上插入一点,原来的点后移一位
cloud->push_back(point);//在点云最后面插入一点
std::cout << cloud->points[7].x<<std::endl;
//打印输出点云数据
for (size_t i = 0; i < cloud->points.size(); ++i)
{
std::cout << cloud->points[i].x << " " << cloud->points[i].y << " " << cloud->points[i].z << std::endl;
}
//可视化
pcl::visualization::PCLVisualizer viewer("Cloud Viewer");//创建对象
viewer.setBackgroundColor(0.05, 0.05, 0.05, 0);//背景为白色
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> sig(cloud, 230, 66, 99);
viewer.addPointCloud(cloud, sig, "cloud");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 5);
//直到关闭窗口结束循环
while (!viewer.wasStopped())
{
//每次循环调用内部的重绘函数
viewer.spinOnce();
}
}
文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文
文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作 导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释: cwy_init/init_123..._达梦数据库导入导出
文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js
文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6
文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输
文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...
文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure
文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割
文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答
文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。
文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入
文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf