基于MATLAB的线激光三维彩色扫描仪

2023-10-28

        暑期做的一个项目,开始并不是很熟悉,在网上查找的资料也不是很具体,但是自习学习了理论知识之后还是比较容易的做出来这个项目,现在开源整个项目,由于篇幅有限,本文适合稍微有点点基础的朋友。源码见底部

先显示下最后结果:

一、硬件设计

主要有步进电机(建议精度高点,这涉及到后面的精确度问题,一般42电机即可)、步进电机驱动、彩色相机、一字型激光头、arduino、各部分的供电单元;基本的硬件框架包括能够固定在步进电机上的旋转台,以及一些整体的固定装置。我的效果如下(不一定和我一样,完成相关功能即可,我也是利用实验室有限资源):

二、整体设计思路

       本设计为线结构光三维扫描,是一种基于光学三角法的非接触式物体表面轮廓成像技术,利用线激光投影到被测物体表面,工业相机采集受到物体表面高度调制的形变条纹,经过计算得到表面轮廓三维数据。很多相关设计采用了数学上的几何关系来解算,例如之前参考过一个方法是结合镜头光轴与激光线的距离b以及夹角c0,再用镜头光轴垂直于镜头光心与激光头的连线,结合基本的几何关系以及正弦定理推算出物体距离深度信息,个人认为这种方法确实十分好的利用了激光三角法的基本原理,但是对于装置的硬件要求过高,例如实际上角度和距离并不是很准确的能够控制,加上是光心到激光头中心的这种不易测量的距离。随本设计采用标准的标定的方式,后面能够发现对硬件要求很低。但是由于我也是为了简单的模型设计,项目本身并没有实际参数的要求,故实际测量等对有些变量并没有很好地测量与控制,这并不影响我们对线激光三维成像原理与方法的理解;实际来看实验效果依旧不错,大家可以根据实际情况对硬件加以更严谨的测量和控制。

三、标定

        相机标定:    相机的标定为的是获取相机的内外参数,有一点需要指出,就是标定后相机的内参矩阵只有一个,但是外参矩阵的数目是和标定板是一样的,因为每一个标定板的平面都是被视为一个世界坐标系的XoY面(Z为0),相片摆放不同即表示各个世界坐标系不同,而内参矩阵是代表相机坐标系与世界坐标系的转换关系,相机坐标系是一直变化的,随着世界坐标系的变化外部转移矩阵自然一直变化。为简化标定过程,相机标定直接采用matlab相机标定工具箱,具体教程可参考如下链接  MATLAB--相机标定教程。得到的Intrinsic Matrix为内参矩阵,RotationMatrix 何Translation Vector 即为外部矩阵对应的旋转矩阵和平移向量;注意,我验证过,这里得到的矩阵结果,如代入下面的公式计算需要进行转置,程序上有体现。标定时我默认为倒数第二张标定板图片为唯一的世界坐标系(即后面的计算以此世界坐标系为基础),倒数第一张标定板图片我用来作为后面的激光光平面的标定的临时坐标系;最后两张标定板照片采集后分别保持不动,打开激光,并分别采集激光打在标定平面上的图片,以备后面光平面标定使用。

%%  获取标定结果  MATLAB 获取 经计算 需转置矩阵
load('cameraParams.mat');
intriMatrix = cameraParams.IntrinsicMatrix';
R = cameraParams.RotationMatrices(:, :, 21)';
T = cameraParams.TranslationVectors(21, :)';
%  Temporary coordinate system
R_t = cameraParams.RotationMatrices(:, :, 22)';
T_t = cameraParams.TranslationVectors(22, :)';

       光平面标定:  本设计目前发现的对硬件结构有一些基本的要求,主要体现在1、要保证旋转台水平;2、尽量保证激光线垂直于旋转台中心(这个我只是目测简单的校准,或许通过算法的改变可以解决这个硬件约束,但是硬件上稍微的一点约束,程序上的数学处理会少很多)。光平面的主要思路是,在唯一世界坐标系上的激光线(前述的激光打在标定板的图片)上找到两个点,再在临时坐标系上找到一点,利用相机坐标系不变的原理,将临时坐标系上的光点通过转换关系(外部矩阵)先转到相机坐标系,再从相机坐标系转到唯一世界坐标;光点的像素坐标获取主要使用steger算法和交比不变性:

%%  计算光平面方程
close all;
%   世界坐标系
chessPoint = cameraParams.ReprojectedPoints(:, :, 21);
second_row = chessPoint(2, 2);
fourth_row = chessPoint(4, 2);
delta_row = sqrt((chessPoint(1, 1) - chessPoint(2, 1))^2 + (chessPoint(1, 2) - chessPoint(2, 2))^2);
frame = imread('F:\旋转式3d激光扫描\Image\Calibration\cali21_laser.jpg');
background = imread('F:\旋转式3d激光扫描\Image\Calibration\cali21.jpg');
laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
[row, column, channel] = size(frame);
image_1=zeros(row,column);      % 等大的全黑背景
max = length(laserPixel);
for index = 1 : max
    image_1(laserPixel(index, 2), laserPixel(index, 1)) = 255;
end
%  find the first point on the light:  pixel coordinate ---> (column_index, row_index)
for row_index = round(second_row - delta_row/2) : round(second_row)
    column_index = find(image_1(row_index, :) == 255);
    if length(column_index) == 1  
        break;
    end  
end
x1 = (chessPoint(1, 1) - column_index)/delta_row*15;  
y1 = (chessPoint(1, 2) - row_index)/delta_row*15;
z1 = 0;
%  find the second point on the light:  pixel coordinate ---> (column_index, row_index)
for row_index = round(fourth_row - delta_row/2) : round(fourth_row)
    column_index = find(image_1(row_index, :) == 255);
    if length(column_index) == 1
        break;
    end  
end
x2 = (chessPoint(1, 1) - column_index)/delta_row*15;  
y2 = (chessPoint(1, 2) - row_index)/delta_row*15;
z2 = 0;
%%   临时坐标系
chessPoint = cameraParams.ReprojectedPoints(:, :, 22);
second_row = chessPoint(2, 2);
fourth_row = chessPoint(4, 2);
delta_row = sqrt((chessPoint(1, 1) - chessPoint(2, 1))^2 + (chessPoint(1, 2) - chessPoint(2, 2))^2);
frame = imread('F:\旋转式3d激光扫描\Image\Calibration\cali22_laser.jpg');
background = imread('F:\旋转式3d激光扫描\Image\Calibration\cali22.jpg');
laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
[row, column, channel] = size(frame);
image_1=zeros(row,column);      % 等大的全黑背景
max = length(laserPixel);
for index = 1 : max
    image_1(laserPixel(index, 2), laserPixel(index, 1)) = 255;
end
%  find the first point on the light:  pixel coordinate ---> (column_index, row_index)
for row_index = round(second_row - delta_row/2) : round(second_row)
    column_index = find(image_1(row_index, :) == 255);
    if length(column_index) == 1  
        break;
    end  
end

x3_t = (chessPoint(1, 1) - column_index)/delta_row*15;  
y3_t = (chessPoint(1, 2) - row_index)/delta_row*15;
z3_t = 0;
CCS3 = R_t * [x3_t; y3_t; z3_t] + T_t;
WCS3 = pinv(R) * (CCS3 - T);
x3 = WCS3(1, 1); y3 = WCS3(2, 1); z3 = WCS3(3, 1);
[a, b, c, d] = createLightPlane(x1, y1, z1, x2, y2, z2, x3, y3, z3, 0);  %  后面加上非0参数即可显示

     旋转平面的标定:即确定步进电机驱动脉冲与相机的帧的关系;我使用matlab与arduino通信的方式,一定的脉冲之后采集一张图片。关于matlab与arduino通信有很多博客提供了方法    激光扫描三维重建——3.matlab和arduino通信 。此处为了后面的色彩匹配与方便提取激光中线,采用的是第一圈开激光,第二圈关闭激光的方式(我的激光器是直连的,没有激光驱动,故采用分别亮灭的方式,但是这样不一定很好的保证两张图片的对应,因为步进电机的一圈不一定是完整的一圈);最好采用控制激光亮灭的方式拍两张图片,分别作为image和background;

Uno = arduino('COM5');
Uno.pinMode(7, 'output');

video = videoinput('winvideo', 2, 'YUY2_640x480'); 
triggerconfig(video, 'manual');
start(video);
pause(2);
fprintf('   Catch Frame...... \n');

laser = 0; %是否有激光
for i = 1 : 320
    OneStep(Uno, 0.005);
    frame = getsnapshot(video);
    frame = ycbcr2rgb(frame);
    if laser == 1
        imwrite(frame,strcat('F:\旋转式3d激光扫描\Image\NongfuSpring\Reconstruct\','image',num2str(i,'%d'),'.jpg'),'jpg');% 保存帧
    else
        imwrite(frame,strcat('F:\旋转式3d激光扫描\Image\NongfuSpring\Background\','background',num2str(i,'%d'),'.jpg'),'jpg');% 保存帧
    end
    
end
fprintf('   Complete!!! \n');

delete(instrfind({'Port'}, {'COM5'}));
stop(video);
delete(video);

四、三维点云获取

    对每张照片处理的时候,采用背景减除 image - background,将直接得到激光区域,减少了环境的干扰;对激光区域使用steger算法求出激光中心;分别对每一帧上的每一个激光中心进行三维坐标的解算;具体如下:

 由标定原理:

                                                                          

                                                          

                                                                         

由上面的公式得到:

                                                     (1)

由光平面方程:

                                                                                                     ( 2 )

由(1)和(2)联列,四个方程,四个未知数Xw, Yw, Zw, Zc  可解出我们输入像素点(u,v)的 三维坐标(Xw, Yw, Zw)。

求出单个像素点的空间坐标后,需要结合旋转台的旋转参数来解算出对应的唯一世界坐标系下的坐标;由于前述要求了激光线垂直于圆台中心,对于旋转体,核心是旋转半径 r = Xw^2 + Yw^2以及旋转角(与相片索引和旋转角分辨率相关);

色彩匹配即利用刚刚求出的各激光中心点对应的像素坐标,对应在background相片中提取色彩信息即可!

%%  生成点云
picture_num = 320;  % 
WCS = [];          % 所有点的坐标
color = fopen('apple.txt','w');
for frame_index = 1 : picture_num
    frame = imread(['F:\旋转式3d激光扫描\Image\Reconstruct\image',num2str(frame_index),'.jpg']);
    background = imread(['F:\旋转式3d激光扫描\Image\Background\background',num2str(frame_index),'.jpg']);
    fprintf('   Processing %d th image...\n', frame_index);
    laserPixel = findLaserCenter(frame - background, 1, 480, 325, 450, 0);
    for pixel_index = 1 : length(laserPixel)
        [Xw, Yw, Zw] = pcs2wcs(laserPixel(pixel_index, 1), laserPixel(pixel_index,2), intriMatrix, R, T, a, b, c, d, 0);
        r = sqrt((Xw - 40)^2 + Zw^2);
        theta = atan2(-Zw, 40 - Xw);
        Xw = r * cos(theta - frame_index/picture_num * 2 * pi);
        Zw = r * sin(theta - frame_index/picture_num * 2 * pi);
        red = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 1);
        green = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 2);
        blue = background(laserPixel(pixel_index, 2), laserPixel(pixel_index, 1), 3);
        WCS = [WCS; Xw Yw Zw];
        fprintf(color,'%d  %d  %d  %d  %d  %d\n',Xw, Yw, Zw, red, green, blue);
    end
end
%save('apple.txt', 'WCS', '-ascii');
fprintf('   Processing Complete! Saved as ‘apple.txt‘ \n');

五、实际效果

点云图像:

加上彩色信息的点云数据:

meshlab处理后的彩色信息:方法参考   使用Meshlab软件将点云(Point Cloud)转换为模型(Mesh)

六、总结

由于篇幅和个人水平有限,博客中代码段均只给出主函数部分,详细全部代码可以在评论处留下邮箱!欢迎各位同仁指正!!!

鉴于最近经常有小伙伴找资料,在这里建个群,资料群里有,欢迎大家交流!

QQ群号  :   519692535           

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于MATLAB的线激光三维彩色扫描仪 的相关文章

  • 正交变换——来龙去脉

    什么是正交变换 正交变换为何要满足下列条件 正交变换研究什么 1 表象 2 正交变换 研究 长度不变 3 性质 角度 长度 面积不变 4 基本形式 1 平移变换 2 旋转变换 3 轴反射变换 几种组合仍是正交变换 5 为何正交满足那样的表象
  • 【论文精读】KD-MVS

    今天读的是发表在ECCV2022上的自监督MVS文章 作者来自于旷视科技和清华大学 文章链接 arxiv 代码链接 https github com megvii research KD MVS 目录 Abstract 1 Introduc
  • 计算机视觉之三维重建(三)(单视图测量)

    2D变换 等距变换 旋转平移 保留形状 面积 通常描述刚性物体运动 相似变换 在等距变换的基础增加缩放特点 射影变换 共线性 四共线点的交比保持不变 仿射变换 面积比值 平行关系等不变 仿射变换是特殊的射影变换 影消点与影消线 2D无穷远点
  • ubunt 上进行c++ cuda编程

    目录 概述 cmake代码 头文件代码 头文件对应的cuda代码实现 c 的代码 运行结果 参考资料 概述 首先先通过一个简单的demo来演示cuda编程是怎么进行的 cmake代码 cmake minimum required VERSI
  • 分享一个很容易实现的某大学的结构光源码【DIY自己的三维扫描仪】

    这个分享是一个大学做的结构光的代码 用一个usb相机 再加一个投影仪 完全按照说明配置opencv QT 还有一些库 只要配置好了 很容易跑通 代码和UI也很好 也可以优化成自己想要的那种 里面资料很全 非常适合不同高度的人来学习 看完觉得
  • 【深度学习】【三维重建】windows11环境配置tiny-cuda-nn详细教程

    深度学习 三维重建 windows11环境配置tiny cuda nn详细教程 文章目录 深度学习 三维重建 windows11环境配置tiny cuda nn详细教程 前言 确定版本对应关系 源码编译安装tiny cuda nn 总结 前
  • 三维重建(三)相机参数标定与光束平差法(Bundle Adjustment)

    一 针孔成像模型涉及到的坐标系 由于相机的参数总数相对于某种光学模型而言的 这里用到的比较广泛的光学模型就是小孔成像的模型 下面针对小孔成像的光学模型涉及到的坐标系一一介绍 1 世界坐标系 世界坐标系即为三维空间中物体的坐标 用 Xw Yw
  • 医疗图像三维重建方法小结(python+VTK+ITK+Mayavi)

    医疗图像三维重建forpython 环境简介 方法 方法一 Poly3DCollection matplotlib 方法二 VTK ITK 方法三 Mayavi之contour3d 最终方法Mayavi TVTK 环境简介 语言是pytho
  • openmvg2.0编译与使用

    目录 写在前面 获取代码 github 网盘 编译 使用 稠密重建 参考 完 写在前面 1 openmvg是一个用于实现structure from motion的开源库 实现了完整的sfm pipeline 并有说明文档 https op
  • ContextCapture导入点云进行重建

    ContextCapture导入点云进行重建 ContextCapture导入点云进行重建 点云 PointCloud 导入点云 静态站点云 移动测量点云 输入文件 数据属性 字段 Fields ContextCapture的坐标系 管理点
  • 【三维重建】【深度学习】NeRF_Pytorch代码--预备基础知识

    三维重建 深度学习 NeRF Pytorch代码 预备基础知识 给定一个场景的多视角的图像 神经辐射场 NeRF 通过图像重建误差优化一个神经场景表征 优化后可以实现逼真的新视角合成效果 NeRF最先是应用在新视点合成方向 由于其超强的隐式
  • Photoscan/Metashape与Contextcapture联合建模

    Photoscan与Contextcapture联合建模以及激光与影像联合建模 使用Photoscan完成影像的地理坐标与投影坐标转换 Photoscan空三结果导出 Contextcapture导入空三区块 CC刺控制点并继续AT 倾斜摄
  • Halcon三维模型预处理(1):调平的三大手法

    面结构光拍摄生成的点云模型 往往相对系统坐标系是有角度的 首先讲一下调平的目的 1 为接下来的预处理切除背景面做准备 3 不做调平 后续处理会很麻烦 因为不清楚坐标系在平台的为位置 2 对于无序抓取项目 平台相对相机可能是有角度的 将抓取平
  • 火爆科研圈的三维重建技术:Neural radiance fields (NeRF)

    如果说最近两年最火的三维重建技术是什么 相信NeRF 1 是一个绝对绕不过去的名字 这项强到逆天的技术 一经提出 就被众多研究者所重视 对该技术进行深入研究并提出改进已经成为一个热点 仅仅过了不到两年的时间 NeRF及其变种已经成为重建领域
  • 基于opencv下的双目视觉三维重建与重投影残差计算

    一 重投影残差 1 1基本概念 重投影残差 按照字面意思解释就是在第二次重新投影的二维像素点与第一次投影得到的像素点之间的残差 欧式距离 大小 残差的大小反映的就是测量仪器的精度以及外界因素对测量结果的干扰 即测量误差的大小 如下图所示 假
  • 三维人脸重建(二)——Python生成ply文件

    目录 引言 一 ply格式模型介绍 二 代码 注 引言 本文主要以做坐标点为例 我们知道python对数据的处理主要是以numpy的形式 一般三维空间坐标的矩阵在python中为 x1 y1 z1 x2 y2 z2 xn yn zn 然后可
  • 泊松曲面重建(基于PCL)

    Possion重建是Kazhdan等2006年提出的网格重建方法 Possion重建的输入是点云及其法向量 输出是三维网格 表面重建流程 1 构建八叉树 采用的是自适应的空间网格划分的方法 根据点云的密度调整网格的深度 根据采样点集的位置定
  • 使用openMVS对样例数据进行重建

    openMVS根据github上的步骤进行编译 注意 如果没有GPU 用不了CUDA的话 那么需要在openMVS目录下找到CmakeLists txt文件 把CUDA设置为OFF SET OpenMVS USE CUDA OFF CACH
  • RealityCapture场景建模笔记

    Unity Photogrammetry Workflow 5 2 8 着色和贴纹理 Colorize or Texture 5 2 10 网格输出 Mesh export 输出附有颜色信息的Mesh 点云 5 2 8 着色和贴纹理 Col
  • 【论文精读】IGEV-MVS:Iterative Geometry Encoding Volume for Stereo Matching

    今天读的是发表于CVPR2023的文章 作者全部来自于华中科技大学 文章链接 Iterative Geometry Encoding Volume for Stereo Matching 项目地址 GitHub 目录 Abstract 1

随机推荐

  • 各种排序应用场合

    时间复杂度 O n n 插入排序 选择排序和冒泡排序 O nlogn 快速排序 堆排序和归并排序 影响排序效果的因素 待排序的数据规模 关键字的结构及其初始状态 稳定性的要求 语言工具的条件 存储结构 时间和辅助空间复杂度 应用场景 若n较
  • 【LeetCode刷题】228 汇总区间 java

    题目 给定一个 无重复元素 的 有序 整数数组 nums 返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 也就是说 nums 的每个元素都恰好被某个区间范围所覆盖 并且不存在属于某个范围但不属于 nums 的数字 x 列表中的每个
  • tnscmd.pl

    usr bin perl tnscmd a lame tool to prod the oracle tnslsnr process 1521 tcp tested under Linux x86 OpenBSD Sparc perl5 I
  • jmockit使用

    官网 http jmockit github io tutorial Introduction html runningTests org jmockit jmockit jmockit version test maven surefir
  • 华为OD机试 - 数组组成的最小数字

    题目描述 给定一个整型数组 请从该数组中选择3个元素组成最小数字并输出 如果数组长度小于3 则选择数组中所有元素来组成最小数字 输入描述 一行用半角逗号分割的字符串记录的整型数组 0 lt 数组长度 lt 100 0 lt 整数的取值范围
  • c语言——指针的运用与表示

    指针 1 定义 基类型 指针变量名 2 赋值 int a int pa a int a pa a int p2 p2 a 两个运算符 取地址 后面操作数必须是变量 指针运算符 后面的操作数必须是指针 int a 3 int pa a a 正
  • python--文件的循环导入及模块查找顺序

    目录 文件循环导入问题 模块的查找顺序 验证模块查找顺序的方法 绝对导入与相对导入 文件循环导入问题 上一篇我们介绍了模块 文件的导入 现在重点讲一下文件的导入 两个文件彼此导入彼此 循环导入的时候极有可能出现某个名字还没有被创建就是用的情
  • Docker(linux container) 所依赖的底层技术

    Docker linux container 所依赖的底层技术 1 Namespace 用来做PID的隔离 有了namespace 在docker container里头看来 就是一个完整的linux的世界 在host看来 containe
  • Jmeter---BeanShell解析JSON格式的响应数据

    首先 分析一下接口的响应数据 了解接口返回数据的结构类型 如下数据所示 我们想要获取的是normalList里面的status值 但是响应结果是列表 可能是会存在多个记录 所以在实现过程中我们通过遍历normalList中的所有数据 或者是
  • JAVA 线上故障排查完整套路,从 CPU、磁盘、内存、网络、GC 一条龙!

    原文链接 https mp weixin qq com s MZOcDjKm4oy9mGEG3R QYA 作者 fredal https fredal xin java error check CPU 磁盘 内存 GC问题 网络 线上故障主
  • 获取网卡名称存入数组 对网卡执行相关操作

    背景 由于服务器网卡获取ip异常 需要对做bond网卡进行相关网络操作 bond网卡操作 正常执行nmlic命令 环境问题对要执行的网卡本身只有四行命令 为了防止出现多业务网卡和vlan主网卡的情况 这里选择自动获取bond网卡执行相关操作
  • 修改@vue/cli搭建的项目中默认icon图标

    今天使用 vue cli搭建了一个项目 然后想修改一下网页的icon图标 但是在public文件夹下直接替换图标 不会改变网页的icon图标 文件夹目录如下 用项目logo的icon文件替换原始的icon 重启项目还是vue默认的icon
  • 「面试题」20+Vue面试题整理

    微信搜索 前端食堂 你的前端食堂 记得按时吃饭 本文已收录在前端食堂 Github https github com Geekhyt front end canteen 感谢Star 从镜片的厚度和黄黑相见的格子衬衫我察觉到 面前坐着的这位
  • JS des加密解密

    引用库 des解密 function decryptByDES ciphertext var keyHex CryptoJS enc Utf8 parse 秘钥 var decrypted CryptoJS DES decrypt ciph
  • 《计算机网络基础与应用》笔记

    文章目录 前言 1 1认识网络 1 2 认识网络标准及通信协议 1 3认识数据通信技术 1 4选择网络的拓扑结构 2 1考察网络传输介质 2 2考察网络设备 2 3实现网络结构化布线系统 2 4配置网络设置 2 5实现局域网的硬件连接 3
  • 深度学习-图像识别FPN(Feature Pyramid Networks)

    文章目录 一 FPN 二 FPN的整体架构 FPN应用于RPN层 四 FPN总结 一 FPN 卷积网络的一个重要特征 深层网络容易响应语义特征 浅层网络容易响应图像特征 但是到了物体检测领域 这个特征便成了一个重要的问题 高层网络虽然能响应
  • InitializingBean讲解

    InitializingBean讲解 Spring中有两种类型的Bean 一种是普通Bean 另一种是工厂Bean 即FactoryBean 工厂Bean跟普通Bean不同 其返回的对象不是指定类的一个实例 其返回的是该工厂Bean的get
  • cx_Oracle使用方法

    正确安装好cx oracle之后 要使用它来连接到oracle数据库进行操作 具体应该分3步走 第一步 导入cx Oracle 建立连接 gt gt gt import cx Oracle 导入模块 gt gt gt db cx Oracl
  • SpringBoot入门 快速创建并部署web后端

    这两天学习了springboot些框架开发 发现用它开发真的是简单便捷 就像是它的设计初衷所描述的那样 它虽然没有提出任何新的技术 但却将之前的spring技术集成了 他让spring变得更加好用 于是将最近的学习内容总结一下 分享给大家
  • 基于MATLAB的线激光三维彩色扫描仪

    暑期做的一个项目 开始并不是很熟悉 在网上查找的资料也不是很具体 但是自习学习了理论知识之后还是比较容易的做出来这个项目 现在开源整个项目 由于篇幅有限 本文适合稍微有点点基础的朋友 源码见底部 先显示下最后结果 一 硬件设计 主要有步进电