简介:天津工业大学机器视觉课程期末作业,聚焦工业场景中的钢销直径测量问题,以MATLAB为工具完成从图像灰度化、滤波去噪、二值分割、边缘检测到直径提取的完整视觉测量流程,适合正在学习机器视觉、数字图像处理或准备相关课程设计的高年级本科生参考。压缩包共3个文件,包含2个MATLAB脚本与1份实验报告,总大小仅129KB,体量精简但代码和文档组织清晰,便于快速定位核心算法模块。脚本Untitled3.m与Measure_sun.m分别对应主流程和直径测量函数,实验报告详细记录了理论背景、算法选型、代码实现、实验结果与误差分析,既有可运行代码,也有精度讨论与优化思路。目前已有899人学习浏览,以真实期末作业完整展示了工业视觉检测从图像到尺寸数值的落地过程,可帮助读者掌握图像预处理、边缘检测、参数调优及实验报告撰写等关键技能,是一份很实用的机器视觉教学参考案例。

1. 机器视觉期末作业拆解:这份课设资料能帮你把流程跑通

机器视觉课程到了期末,最让人头疼的不是某个算法本身,而是怎么把一学期学的预处理、分割、特征提取串成一条能跑的完整流水线。我最近拆了一套天津工业大学的机器视觉期末作业资料包,里面是实验代码、测试图片和期末项目三块内容,覆盖了从灰度化、高斯滤波到 Canny 边缘检测、Hough 变换、模板匹配,再到零件缺陷检测的完整链条。对正在赶课设的同学来说,它等于一条可以直接照着走的机器视觉学习路线;对想往机器视觉方向转的人来说,它的价值在于代码注释和实验报告里把参数选择的过程完整保留了下来。这篇笔记按我实际拆解的路径写,每段代码讲清楚为什么这么写、参数怎么定,最后把我翻过车的几个地方也一并交代。

2. 预处理到分割的流水线:灰度化、滤波与形态学的选型逻辑

2.1 为什么要按「预处理→分割→特征」搭流水线

机器视觉作业和普通图像处理题最大的区别在于,它不是单张图调参,而是要面对一小批测试图片,要求同一套代码全部能出结果。这就逼着你把流程写成模块化的流水线,每个环节只做一件事,前一步的输出就是后一步的输入。这套作业里的主程序也是按这个思路组织的:先读图,再灰度化,滤波去噪,然后分割,最后提取特征做判定。

顺序不能乱,原因很直接。滤波放在灰度化之后、分割之前,是因为灰度图只有一个通道,高斯核作用在单通道上计算量小,而且噪声如果不先压下去,后面的 OTSU 阈值分割会把噪点一起归成前景,轮廓数量直接翻倍。分割放在滤波之后,是因为阈值分割对灰度跳变敏感,噪声会被当成假的前景区域。特征提取放最后,是因为只有拿到干净的二值图,面积、周长这类特征才有意义。

这套作业里的测试图基本都是工业零件在固定光源下拍的,场景单一,目标与背景对比度稳定,所以全局阈值就够用。但如果你的图是室外拍的或者光照不均,就要考虑自适应阈值或者先做光照校正,这一点我在避坑章里专门展开。

2.2 灰度化与滤波选型:均值、高斯、中值怎么挑

资料包里灰度化用的就是 OpenCV 默认的加权转换,公式是 Y = 0.299R + 0.587G + 0.114B,之所以不直接用三通道平均值,是因为人眼对绿色最敏感,加权能让灰度图更贴近人眼感知的亮度。这套作业的实验报告里还特意对比了均值灰度和加权灰度的分割结果,加权灰度的零件暗部细节保留明显更好——这就是为什么工业场景里几乎都用这个公式。

import cv2
import numpy as np

# 读图时直接转灰度
gray = cv2.imread('part_01.png', cv2.IMREAD_GRAYSCALE)
# 高斯滤波,核大小 5×5,sigma 取 0.8
blur = cv2.GaussianBlur(gray, (5, 5), 0.8)

这里有个细节:cv2.imread 第二个参数传 IMREAD_GRAYSCALE,读进来的就是单通道灰度图,省得再调一次 cvtColor,少一次类型出错的概率。高斯滤波的核大小必须是奇数,常用 (3,3)、(5,5)、(7,7),核越大越模糊、边缘保留越差;sigma 控制权重衰减速度,sigma 越小中心像素权重越突出,保留细节越多。

那什么时候用中值滤波?高斯滤波是线性的,对高斯噪声效果好,但对椒盐噪声无能为力。如果测试图里有明显的小白点小黑点,换成 cv2.medianBlur(gray, 5) 更合适,中值滤波把邻域内的中值作为输出,椒盐噪声这种离群点会被直接抹掉。

滤波方式 适用噪声 边缘保留 计算成本
均值滤波 均匀噪声 差 最低
高斯滤波 高斯噪声 中 低
中值滤波 椒盐噪声 中 中

2.3 阈值分割与形态学:把目标从背景里抠出来

滤波完下一步是分割。这套作业没有用固定阈值,而是用 OTSU 大津法自动算阈值,因为测试图片之间光照有细微差异,写死阈值的话换一张图就翻车。OTSU 的原理是遍历所有灰度值,找类间方差最大的那个作为阈值,把图像分成前景背景两类,类间方差越大说明分得越开。

# OTSU 自动阈值
_, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 形态学开运算:先腐蚀再膨胀,去掉小块噪点
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
clean = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel, iterations=1)

cv2.threshold 的第二个参数填 0 是占位,真正的阈值由 OTSU 算出来,返回的第一个值是算出的阈值,建议 print 出来记录到报告里。实际拆这套作业时我发现,OTSU 出来的阈值通常在 90~130 之间,和测试图的光照条件一一对应。

形态学开运算的作用是去掉二值图里残留的孤立噪点,先腐蚀把细小的白色区域吃掉,再膨胀恢复目标的原始大小。这里结构元素选了椭圆而不是矩形,是因为零件边缘有圆弧,矩形核容易把圆角磨平。如果目标二值图里还有小空洞,可以再加一步闭运算,先膨胀再腐蚀,把孔洞填上。核大小从 3×3 开始试,噪点多就加大,但超过 7×7 会明显改变目标形状,后续特征提取就不准了。

到这一步,clean 就是一张干净的二值图,后面不管是找轮廓、做粒子分析还是提取特征,都以它为基础。预处理链在这里收尾,但分割只是手段,真正决定作业评分的是后面的边缘检测和特征提取,这是下一章要讲的内容。

3. 边缘检测与特征提取:Canny、Hough 和模板匹配的实际参数

边缘检测和特征提取是这份作业里最容易拉开分数差距的部分,原因在于它不是把函数名写对就行,双阈值、投票阈值这些参数之间的配合直接决定输出质量。下面按作业里出现的三个高频考点逐个拆:Canny 双阈值、Hough 直线检测、模板匹配识别电子秤数值。

3.1 Canny 双阈值怎么定:高低阈值比例和高斯核的关系

Canny 是机器视觉课设里必考的重点,也是调参最玄学的地方。原理上它分四步:高斯平滑、计算梯度幅值和方向、非极大值抑制、双阈值连接。前几步 OpenCV 都封装了,你真正要调的其实只有两个参数——高低阈值,以及前面高斯核的大小。

# Canny 边缘检测:低阈值 50,高阈值 150,比例 1:3
edges = cv2.Canny(blur, 50, 150)

低阈值和高阈值的比例通常取 1:2 到 1:3。梯度幅值超过高阈值的像素直接判为边缘,低于低阈值的直接丢弃,介于两者之间的,只有和已确定的边缘相连才会被保留。所以高阈值设太高,边缘会断;设太低,噪点全变成边缘,后面 Hough 投票会被噪声带偏。

提示:Canny 内部自带高斯平滑,如果前面已经做了 5×5 的 GaussianBlur,Canny 内部的高斯核就不要再设大,否则边缘会过度磨平。

我在拆这份资料时对比过几组参数:

低阈值 高阈值 结果
30 90 边缘完整但噪点偏多
50 150 边缘清晰,噪点少
80 240 边缘断裂,Hough 找不到直线

实际调参先固定高阈值 150 左右,低阈值从 30 往上加,直到边缘断裂处刚好能被连上,这是最省时间的做法。很多同学直接抄默认值 (100, 200),换到零件图上往往过钝,圆弧边缘全丢。

3.2 Hough 直线检测:累加器阈值、minLineLength 与 maxLineGap

Canny 出的是二值边缘图,要变成「直线」还得靠 Hough 变换。标准 Hough 变换在参数空间里对每个边缘点投票,直线方程 ρ 和 θ 投票多的就是直线。OpenCV 里常用概率 Hough,HoughLinesP 只对边缘点做部分采样,速度更快,而且直接输出线段端点。

# 概率 Hough 直线检测
lines = cv2.HoughLinesP(
    edges,
    rho=1,                  # 距离分辨率,1 像素
    theta=np.pi / 180,      # 角度分辨率,1 度
    threshold=80,           # 票数阈值,一条线至少 80 个点投票
    minLineLength=30,       # 小于 30 像素的线段丢弃
    maxLineGap=10           # 同一直线上间隔 10 像素以内的断点相连
)

这里面最影响结果的是 threshold,它表示一条直线至少要获得多少个边缘点的投票,设太大直线断掉,设太小满屏都是杂线。零件图上边缘点密集,80 左右比较稳。minLineLength 控制输出线段的最小长度,用来滤掉碎线;maxLineGap 是线段断裂处的最大间隔,Canny 边缘断成两段时,靠这个参数把它们连回去。

如果 lines 返回空,优先怀疑 Canny 阈值而不是 Hough 参数——边缘都没检出来,投票从哪来?这是新手最容易搞反的排查顺序,先打印 edges 目测连通性,再决定动哪个环节。

3.3 电子秤数值识别:模板匹配做字符分割的可行方案

这套作业里还有一个让很多人头疼的部分——识别电子秤显示数值。网上相关的「机器视觉代码识别电子秤数值」提问很多,套路其实相当固定:先定位屏幕区域,再做字符分割,最后逐个匹配模板。

digits = '0123456789'
templates = {}
for d in digits:
    tpl = cv2.imread(f'tpl/{d}.png', cv2.IMREAD_GRAYSCALE)
    templates[d] = tpl

result = []
for ch in char_blocks:          # char_blocks 是分割好的单个字符二值图
    h, w = ch.shape
    best_score, best_digit = -1, '?'
    for d, tpl in templates.items():
        # 模板缩放到和字符块一样大,再做归一化相关匹配
        tpl_r = cv2.resize(tpl, (w, h))
        score = cv2.matchTemplate(ch, tpl_r, cv2.TM_CCOEFF_NORMED)[0][0]
        if score > best_score:
            best_score, best_digit = score, d
    result.append(best_digit if best_score > 0.7 else '?')

char_blocks 怎么来?在二值图上用 cv2.findContours 找轮廓,取每个轮廓的 boundingRect,按 x 坐标排序,就是从左到右的数字顺序。模板匹配用的是归一化相关系数 TM_CCOEFF_NORMED,输出范围 -1 到 1,越接近 1 越像。每次匹配前必须把模板缩放到和字符块同样大小,否则尺寸对不上,分数全在 0.5 以下,全部判成问号。

这里有一个很重要的边界认识:模板匹配对字体非常敏感,电子秤数字是七段数码管,普通字体模板大概率匹配不上。正确做法是直接从电子秤截图切出 0~9 的数码管模板,而不是用电脑字体凑。如果你手头有这类资料,优先研究它的模板是怎么制作的,这比调匹配参数更关键。如果后续想做得更稳,可以改成按七段数码管的亮灭状态做结构查表,但课设阶段模板匹配已经够用。

4. 零件缺陷检测落地:OpenCV 与 LabVIEW 两条路的实现对比

4.1 缺陷检测的判定思路:面积、周长、圆形度三个特征

期末项目部分,这套资料做的是零件缺陷检测。判定逻辑不复杂,核心是构造特征。常见思路是先分割出每个零件轮廓,再计算面积、周长、圆形度这三个特征,把异常件筛出来。

圆形度的公式是 4π × 面积 / 周长²,正圆的圆形度是 1,形状越不规则,值越低。零件表面有划痕、崩角或异物时,轮廓周长会明显变大,面积变化不大,圆形度就会掉下来。所以圆形度是最灵敏的缺陷指标,面积主要用于过滤噪声和过小的碎屑。

img = cv2.imread(img_path)          # 彩色原图,只用于画框
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0.8)
_, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
clean = cv2.morphologyEx(th, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))

contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
    area = cv2.contourArea(cnt)
    perimeter = cv2.arcLength(cnt, True)
    circularity = 4 * np.pi * area / (perimeter * perimeter) if perimeter > 0 else 0
    # 面积明显小于均值,或者圆形度过低,判为缺陷
    if area < 0.5 * mean_area or circularity < 0.8:
        x, y, w, h = cv2.boundingRect(cnt)
        cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)

这里 mean_area 是所有轮廓面积的平均值,先在一个循环里统一算出来再比较。cv2.arcLength 第二个参数必须传 True,表示当前轮廓闭合;不传的话按不闭合线段计算,周长偏小,圆形度虚高,缺陷就漏了。RETR_EXTERNAL 只取最外层轮廓,避免零件内部孔洞轮廓把判定带偏。如果零件本身允许有孔,就改用 RETR_CCOMP 把内外轮廓分开统计,并在特征里加一个孔洞数量。

注意:0.5 和 0.8 是业务参数,不是算法参数,改之前先和报告里对零件的合格标准对齐。答辩时老师最喜欢问这两个数怎么来的。

4.2 Python + OpenCV 最小可用实现:完整跑通的代码骨架

把前面几章的模块串起来,就是一个最小可用的缺陷检测脚本。这份资料里的主程序也是类似结构,我把它精简成骨架:

import cv2
import numpy as np

def detect_defect(img_path, min_area_ratio=0.5, min_circularity=0.8):
    gray = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
    blur = cv2.GaussianBlur(gray, (5, 5), 0.8)
    _, th = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    clean = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel)

    contours, _ = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    areas = [cv2.contourArea(c) for c in contours]
    mean_area = float(np.mean(areas)) if areas else 0

    bad_count = 0
    for cnt in contours:
        area = cv2.contourArea(cnt)
        perimeter = cv2.arcLength(cnt, True)
        circularity = 4 * np.pi * area / (perimeter * perimeter) if perimeter > 0 else 0
        if area < min_area_ratio * mean_area or circularity < min_circularity:
            bad_count += 1
    return bad_count, len(contours)

for i in range(1, 11):
    bad, total = detect_defect(f'images/part_{i:02d}.png')
    print(f'part_{i:02d}: {bad}/{total} defects')

min_area_ratio 设为 0.5,意思是面积小于平均面积一半的轮廓直接算缺陷,这个值对碎屑和崩块很有效。min_circularity 设 0.8,圆形零件低于这个值基本可以断定有表面损伤。这两个参数就是整个项目的业务规则,写报告时要把判定阈值和依据讲清楚,而不是只说「实现了缺陷检测」。

4.3 LabVIEW VDM 版:用 Particle Analysis 搭同样的检测

如果你学的是 LabVIEW 的机器视觉,NI Vision 里的 VDM 模块和上面这套逻辑是一一对应的。很多课程用「LabVIEW 机器视觉零件缺陷检测」做课设,接线思路比 Python 更直观,但参数藏在属性节点里,反而容易漏。

VDM 函数 对应 OpenCV 操作 关键参数
IMAQ Threshold 阈值分割 阈值范围、通道选择
IMAQ Morphology 形态学操作 结构元素、迭代次数
IMAQ Particle Analysis 轮廓特征提取 测量项勾选、忽略边界粒子选项

LabVIEW 的流程是 IMAQ Grab 取图,IMAQ Threshold 二值化,IMAQ Morphology 做开运算,最后 IMAQ Particle Analysis 输出每个粒子的面积、周长和圆形度。这里最容易踩的坑是 Particle Analysis 默认勾选了忽略与边界接触的粒子,缺陷在图像边缘时会被直接漏掉,导致计数少,这个坑我在避坑章里展开写。

两条路对比下来,Python + OpenCV 的优势是参数可控、可批量跑、方便把中间结果写进报告;LabVIEW 的优势是界面和报表省事,适合答辩演示。如果自己选,代码能力强就选 Python,流程可视化要求高就选 LabVIEW,两者判定逻辑完全一致,区别只在工具链。

5. 机器视觉作业避坑指南:五个翻车现场与修复记录

5.1 光照一偏,全局阈值就崩

现象:同一套代码,前几张图分割很干净,换到一张边角略暗的图,OTSU 把阴影区域也分成了前景,轮廓数直接翻倍。

原因:OTSU 是全局阈值,整幅图共用一个阈值。光照不均时,目标和背景的灰度分布不再各自成峰,类间方差最大化的假设失效,阈值被阴影区域拉偏。

解决:两种思路。一是先用顶帽变换去掉慢变化的背景, cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel) ,从原图中减掉背景,让目标变成近似均匀亮度的前景。二是直接用 cv2.adaptiveThreshold,每个像素用邻域内的均值或高斯加权均值做局部阈值,块大小 11~31 之间选,常数 C 取 2~5,值越大前景越少。报告里建议把两种结果各放一张图对比,这本身就是加分项。

5.2 Canny 边缘断成一段一段,Hough 找不到直线

现象:HoughLinesP 返回空数组,打印 edges 目测,零件边缘断成三段以上,尤其圆弧部分全没了。

原因:高阈值设太高,圆弧部分梯度幅值达不到,直接判死;或者前面高斯滤波的核太大,边缘被磨得梯度平滑,Canny 内部检不出强边缘。

解决:先调 Canny 阈值,固定高阈值为低阈值的 3 倍以内,从 (30, 90) 开始逐步抬高,直到边缘既有连续性又不带噪点。接着调 Hough 的 maxLineGap,把断裂间隙放宽到 10~15 像素,让断点连成一条线。我一般按这个顺序排查:Canny 阈值 → 高斯核大小 → Hough threshold → minLineGap,每一步打印中间结果确认,而不是一次调三个参数。

5.3 模板匹配对旋转和缩放零容忍

现象:电子秤数值识别准确率只有一半,中间几个数字总是判成问号,打印匹配分数发现最高的只有 0.6 左右。

原因:TM_CCOEFF_NORMED 模板匹配是严格位置相关的,目标相对模板旋转哪怕 5 度,相关系数就掉到 0.6 以下;缩放也一样,尺寸对不上分数就崩。模板匹配本身不具备旋转不变性和尺度不变性。

解决:先保证采集端正——电子秤固定摆放,相机光轴和屏幕垂直,角度差控制在 2 度以内。如果工装做不到,就做多角度模板,把每个数字从 -10 度到 +10 度每 2 度旋转生成一张,匹配时遍历角度取最高分。缩放问题在匹配前 resize 解决,代码在 3.3 里写过。还有一个技巧是匹配窗口只框住数字区域最稳定的一行,避开小数点和单位符号的干扰。

5.4 粒子分析默认忽略边界点数,缺陷被静默漏掉

现象:LabVIEW Particle Analysis 统计出的缺陷数比人工数少,检查代码没发现逻辑问题,单独看每个粒子的特征也都正常。

原因:IMAQ Particle Analysis 默认勾选了忽略与边界接触的粒子,也就是「忽略边界点数」默认开启。缺陷零件正好靠在 ROI 边缘时,整个粒子被排除在统计之外,计数自然偏少。这个坑很隐蔽,因为 OpenCV 的 findContours 没有这个默认行为,从 Python 转 LabVIEW 的人最容易在这里翻车。

解决:在 Particle Analysis 的属性节点里关掉忽略边界粒子选项;或者在取 ROI 时故意向外扩展 20 像素,统计完成后再按内部边界把粒子裁回来。排查计数问题时,先画一幅带粒子编号的标签图叠加到原图上,看缺了哪个编号,再去查设置,比盲改参数快得多。

5.5 中文路径让 cv2.imread 静默返回空

现象:图片放在带中文的文件夹里,cv2.imread 返回 None,程序不报错继续往下跑,到最后找轮廓时崩掉,报错信息跟读图毫无关系,特别难定位。

原因:OpenCV 的 imread 内部调用的编码器不支持中文字符串路径,这在 Windows 上是老问题了,Linux 上反而正常。

解决:用 numpy 从文件读字节再交给 imdecode,绕开 path 参数:

import numpy as np
import cv2

def imread_unicode(path):
    data = np.fromfile(path, dtype=np.uint8)
    return cv2.imdecode(data, cv2.IMREAD_COLOR)

从那以后我凡是写读图工具,一律用这个封装,不管路径是中文还是英文,不在读图这一步花时间。类似地,cv2.imwrite 写中文路径也会失败,可以用 cv2.imencode 配合 tofile 解决。

6. 进阶玩法:用直方图谷底自动定阈值,再把分割结果量化验证

6.1 用灰度直方图谷底反推阈值

OTSU 适合目标占比较大、双峰明显的图。但作业里有些测试图目标很小,背景占绝对优势,OTSU 会把目标和一部分背景噪声一起归为前景。这时候可以手动实现双峰谷底法:统计灰度直方图,找到两个峰之间的谷底作为阈值,前提是目标和背景的灰度范围分得开,零件图和电子秤屏都满足这个条件。

import numpy as np
import cv2

hist = cv2.calcHist([roi], [0], None, [256], [0, 256]).flatten()
# 直方图先做一步平滑,避免谷底落在局部抖动上
smooth = cv2.GaussianBlur(hist.reshape(1, -1), (1, 9), 1.5).flatten()
# 在 30~170 灰度区间内找最小点作为谷底
valley = int(np.argmin(smooth[30:170]) + 30)
_, bin_img = cv2.threshold(roi, valley, 255, cv2.THRESH_BINARY)

谷底位置由平滑后的直方图在预设灰度区间内的最小值决定,区间取 30~170 是为了避开纯黑纯白区域,把注意力放在前景背景交界的灰度带上。GaussianBlur 对一维数组也能作用,用 (1,9) 的核套在 reshape 后的行向量上,效果等于对直方图做滑动平均,比直接取 argmin 稳定得多。注意如果测试图是亮背景暗目标,阈值方向要反过来,二值化时用 THRESH_BINARY_INV。

6.2 分割结果的量化验证:用 IoU 给作业打分

作业答辩最容易被问的一句话是「你的分割算法可靠吗」。光贴几张成功图说服力不够,我一般会从测试图里挑 5~10 张,人工用画图工具把目标区域标成白色得到 GT 二值图,然后和算法输出的分割图逐像素算 IoU:

def iou(pred, gt):
    inter = np.logical_and(pred > 0, gt > 0).sum()
    union = np.logical_or(pred > 0, gt > 0).sum()
    return inter / union if union > 0 else 0

IoU 大于 0.85 算优秀,0.7~0.85 算可用,低于 0.7 就说明分割有系统性偏差——常见是阈值偏低导致背景噪声混入,或者形态学核过大把细小特征磨没了。把每张图的 IoU 列一张表写进报告,再配两张典型失败图分析原因,比单纯贴成功样例会扎实很多。

这套资料拆完,我最深的体会是:机器视觉课设的分数差异不在算法多高级,而在参数有没有形成「判断依据」。为什么用高斯不用均值、为什么圆形度阈值取 0.8、为什么忽略边界粒子会漏检,这些才是报告和答辩里的得分点。从那以后我每次拿到一份视觉代码,都强制先跑一遍中间结果可视化,把每步输出的图存下来对比,确认流水线每一环的参数都有依据,再谈效果优化。希望这些拆解和踩坑记录能帮你在自己的作业上少走几个来回,拿到资料后照着这个思路跑一遍,比我单写结论有用得多。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

更多推荐