C++与OpenCV实现车道线检测:从图像处理到霍夫变换的完整指南
1. 项目概述:从零实现一个C++车道线检测器
最近在整理一些计算机视觉的练手项目,发现车道线检测这个经典课题,虽然网上Python+OpenCV的教程一抓一大把,但用纯C++从零手撸一遍的完整分享却不多。很多朋友学了C++语法和OpenCV基础接口后,想做点实际的东西,往往卡在“如何把知识点串起来”这一步。这个项目就是针对这个痛点:我们不依赖任何高级的深度学习框架,就用最基础的C++和OpenCV,从读取一张道路图片开始,一步步实现一个能稳定检测出车道线的程序。整个过程会涉及图像预处理、边缘检测、霍夫变换、车道线拟合与可视化等核心环节,我会把每个步骤的原理、代码实现细节以及我调试过程中踩过的坑都讲清楚。无论你是想巩固C++和OpenCV实战能力的学生,还是对传统计算机视觉算法感兴趣的开发者,跟着走一遍,你收获的将不仅仅是一个能运行的代码,更是一套解决类似视觉问题的完整方法论。
2. 核心思路与方案选型
2.1 为什么选择传统图像处理方案?
在动手之前,我们先要定下技术路线。当前车道线检测的主流无疑是基于深度学习的端到端模型,效果惊艳。但我们这个项目的目标是“手撸”和“理解”,深度学习方案就像一个黑盒,虽然结果好,但内部复杂的网络结构和海量参数训练会分散我们对“车道线检测”这个核心问题本身的关注。传统的图像处理方案,流程清晰,每一步的输入输出、参数影响都肉眼可见,非常适合学习和掌握计算机视觉的基础思想。它的核心思路可以概括为“化繁为简”:将彩色的道路图像,通过一系列处理,最终简化为几条代表车道线的直线或曲线。这个“简化”的过程,正是算法魅力的所在。
2.2 技术路线拆解:一步步逼近车道线
基于传统方法的车道线检测,通常遵循一个经典的流水线。我们的方案也将围绕这个流水线展开:
- 图像预处理:原始图像包含太多干扰信息(车辆、护栏、路面纹理、阴影)。我们的目标是突出车道线,抑制其他。这通常通过灰度化、高斯模糊和边缘检测来实现。
- 感兴趣区域(ROI)提取:车道线只可能出现在图像的下半部分,且大致呈一个梯形区域。直接在全图处理既低效又容易引入天空、树木等干扰。定义一个ROI掩膜能极大提升后续处理的准确性和速度。
- 边缘检测:这是关键一步,目的是找出图像中灰度值突变的地方,也就是潜在的“线”。Canny边缘检测器是这一步骤的不二之选,因为它能通过高低阈值有效控制边缘的“粗细”和“连贯性”。
- 车道线提取:从一堆杂乱边缘点中找出属于左右车道线的点集。这里我们采用霍夫变换(Hough Transform)。它能将图像空间中的点(边缘点)映射到参数空间(如直线可以用斜率和截距表示),通过统计投票找出最有可能的直线参数。
- 车道线拟合与后处理:霍夫变换可能检测出多条短线段。我们需要将这些线段按左右车道分类,并分别用一条更平滑、更完整的直线(或曲线)来拟合它们,得到最终的车道线。
- 可视化:将检测到的车道线叠加回原始图像,直观地展示结果。
这个方案的优势在于每一步都可控、可解释。你可以调整高斯模糊的核大小来看噪声抑制效果,调整Canny阈值来观察边缘的完整性,修改霍夫变换参数来感受它对线段敏感度的变化。这种“手感”是调参工程师的宝贵经验。
3. 环境准备与工程搭建
3.1 开发环境与工具链选择
工欲善其事,必先利其器。一个顺手的开发环境能事半功倍。
- 编译器与IDE:我使用的是Visual Studio 2022,社区版免费且功能强大,对C++标准和Windows开发支持良好。当然,你也可以选择VSCode配合CMake和MinGW/GCC,这在跨平台项目中更灵活。选择哪个取决于你的习惯和项目需求。对于这个单文件或少量文件的项目,VS的便捷性更高。
- 核心库:OpenCV是我们的绝对核心。建议从OpenCV官网下载预编译好的Windows版本(例如OpenCV 4.8.0),或者使用vcpkg、MSYS2等包管理器安装。确保你的IDE能正确配置包含目录、库目录和附加依赖项。一个常见的坑是Debug和Release模式下的库文件要区分开(通常库名带
d的是Debug版)。 - 项目管理:创建一个新的控制台应用项目。为了清晰,我建议在项目里建立
src、include、data文件夹,分别存放源代码、头文件和测试图片。
3.2 OpenCV基础配置与验证
配置OpenCV是新手的第一道坎。这里以Visual Studio 2022为例,简述关键步骤:
- 属性表配置:在VS中,最好创建一个属性表(
.props文件)来管理OpenCV的配置。右键项目 -> 属性 -> 通用属性,可以创建新的属性表。在属性表中,你需要配置:VC++目录 -> 包含目录:添加OpenCV的include文件夹路径(例如D:\opencv\build\include)。VC++目录 -> 库目录:添加OpenCV的lib文件夹路径(例如D:\opencv\build\x64\vc16\lib,注意vc16对应VS2019/2022,x64对应64位平台)。链接器 -> 输入 -> 附加依赖项:添加需要链接的库文件名,例如opencv_world480.lib(Release)和opencv_world480d.lib(Debug)。world模块包含了大多数常用功能,适合学习。
- 系统路径:将OpenCV的
bin目录(例如D:\opencv\build\x64\vc16\bin)添加到系统的PATH环境变量中,这样运行时才能找到对应的DLL文件。 - 验证配置:写一个简单的程序测试是否成功。
#include <opencv2/opencv.hpp> #include <iostream> int main() { cv::Mat image = cv::imread("test.jpg"); if (image.empty()) { std::cout << "Could not open or find the image!" << std::endl; return -1; } cv::imshow("Display window", image); cv::waitKey(0); return 0; }如果能正常显示图片,恭喜你,环境搭建成功。如果报错“找不到opencv_world480.dll”,请检查系统PATH和环境变量是否生效,或者尝试将DLL文件直接复制到你的项目可执行文件(.exe)的同级目录下。
4. 核心算法实现与代码逐行解析
接下来,我们进入核心部分。我将按照处理流水线,分模块讲解代码实现。假设我们有一张名为lane.jpg的道路图片。
4.1 图像预处理:为边缘检测铺路
预处理的目标是得到一张只突出车道线边缘的“干净”图像。
#include <opencv2/opencv.hpp> #include <vector> int main() { // 1. 读取图像 cv::Mat originalImage = cv::imread("data/lane.jpg"); if (originalImage.empty()) { std::cerr << "Error: Could not load image!" << std::endl; return -1; } // 2. 灰度化:将3通道BGR图转为单通道灰度图,减少计算量 cv::Mat grayImage; cv::cvtColor(originalImage, grayImage, cv::COLOR_BGR2GRAY); // 3. 高斯模糊:用高斯滤波器平滑图像,抑制噪声和细小纹理 // 核大小(5,5)是一个常用起点,sigmaX=0表示由核大小自动计算 cv::Mat blurredImage; cv::GaussianBlur(grayImage, blurredImage, cv::Size(5, 5), 0); // 4. Canny边缘检测 cv::Mat edgesImage; double lowThreshold = 50; // 低阈值 double highThreshold = 150; // 高阈值,通常为低阈值的2-3倍 cv::Canny(blurredImage, edgesImage, lowThreshold, highThreshold); // 此时edgesImage是一个二值图,边缘点为白色(255),背景为黑色(0) }注意:Canny阈值的选择非常关键且需要根据具体图像调整。
lowThreshold太低会引入大量噪声边缘;太高则会丢失真正的弱边缘。一个实用的技巧是先用一个经验值,然后通过滑动条动态调整来观察效果。OpenCV的createTrackbar函数可以很方便地实现这一点,用于算法调试。
4.2 定义感兴趣区域(ROI)
车道线不会出现在图像顶部(天空)或两侧太远的地方。我们用一个多边形掩膜来限定处理区域。
// 5. 定义ROI掩膜(梯形区域,模拟车前视角) cv::Mat mask = cv::Mat::zeros(edgesImage.size(), edgesImage.type()); // 定义梯形的四个顶点,坐标需要根据你的图像分辨率调整 // 例如,对于一张720p的图像(1280x720) std::vector<cv::Point> roiVertices; int width = edgesImage.cols; int height = edgesImage.rows; // 梯形下底宽,上底窄 roiVertices.push_back(cv::Point(width * 0.1, height)); // 左下 roiVertices.push_back(cv::Point(width * 0.9, height)); // 右下 roiVertices.push_back(cv::Point(width * 0.6, height * 0.6)); // 右上 roiVertices.push_back(cv::Point(width * 0.4, height * 0.6)); // 左上 // 将多边形填充为白色(255),生成掩膜 std::vector<std::vector<cv::Point>> pts = {roiVertices}; cv::fillPoly(mask, pts, cv::Scalar(255)); // 6. 应用掩膜,只保留ROI内的边缘 cv::Mat maskedEdges; cv::bitwise_and(edgesImage, mask, maskedEdges);这里的顶点坐标(0.1*width, height)等是比例值,这样做的好处是算法能适应不同分辨率的输入图像,增强了鲁棒性。你可以通过imshow显示mask和maskedEdges,确保梯形区域覆盖了车道线可能出现的位置。
4.3 霍夫变换检测线段
这是从边缘点到数学直线的转换步骤。
// 7. 使用霍夫变换检测直线线段 std::vector<cv::Vec4i> lines; // 每条线由4个整数表示 (x1, y1, x2, y2) cv::HoughLinesP(maskedEdges, lines, 1, // rho: 像素精度,通常为1 CV_PI / 180, // theta: 弧度精度,1度 50, // 阈值:累加器阈值,值越小检测出的线越多 30, // minLineLength: 线段最小长度(像素) 10); // maxLineGap: 线段间最大允许间隔(像素),小于此间隔的线段会连接 // 8. 初步可视化:在原始图像上画出所有检测到的线段(绿色) cv::Mat linesImage = originalImage.clone(); for (const auto& line : lines) { cv::line(linesImage, cv::Point(line[0], line[1]), cv::Point(line[2], line[3]), cv::Scalar(0, 255, 0), // BGR颜色:绿色 2); // 线宽 }cv::HoughLinesP是概率霍夫变换,它直接返回线段的两个端点,比标准霍夫变换(返回rho和theta)更直观。参数调优是这里的重点:
- 阈值:决定多少边缘点对齐才被认为是一条线。太低会检测出很多杂乱短线;太高可能漏检。
- minLineLength:过滤掉太短的线段,它们很可能是噪声。
- maxLineGap:允许连接断开的线段。如果两条线段在同一直线上且间隔小于此值,它们会被连成一条。
4.4 车道线聚类与拟合
现在lines向量里包含了ROI内所有符合条件的线段,但它们可能是分散的、属于左右两条车道的。我们需要进行分类和合并。
// 9. 将线段按斜率分为左车道候选线和右车道候选线 std::vector<cv::Point> leftPoints, rightPoints; const double slopeThreshold = 0.3; // 斜率绝对值阈值,用于过滤近似水平线 for (const auto& line : lines) { int x1 = line[0], y1 = line[1], x2 = line[2], y2 = line[3]; // 计算斜率,注意防止除零 if (x2 == x1) continue; // 垂直线,在这个场景中较少见,可忽略或特殊处理 double slope = static_cast<double>(y2 - y1) / (x2 - x1); // 根据斜率正负和大小分类 if (std::abs(slope) > slopeThreshold) { if (slope < 0) { // 左车道线在图像中通常从左上向右下延伸,斜率为负 leftPoints.push_back(cv::Point(x1, y1)); leftPoints.push_back(cv::Point(x2, y2)); } else { // 右车道线通常从左下向右上延伸,斜率为正 rightPoints.push_back(cv::Point(x1, y1)); rightPoints.push_back(cv::Point(x2, y2)); } } // 斜率绝对值过小的线段(接近水平)很可能是路面标记或其他干扰,丢弃 } // 10. 分别对左右车道的点集进行直线拟合 cv::Mat resultImage = originalImage.clone(); cv::Vec4f leftLine, rightLine; // 拟合的直线参数 (vx, vy, x0, y0) 或用于cv::fitLine的其他表示 if (leftPoints.size() > 1) { // 使用最小二乘法拟合一条直线 cv::fitLine(leftPoints, leftLine, cv::DIST_L2, 0, 0.01, 0.01); // leftLine: [vx, vy, x0, y0] -> (vx, vy)是单位方向向量,(x0, y0)是直线上一点 drawFittedLine(resultImage, leftLine, cv::Scalar(255, 0, 0)); // 蓝色左线 } if (rightPoints.size() > 1) { cv::fitLine(rightPoints, rightLine, cv::DIST_L2, 0, 0.01, 0.01); drawFittedLine(resultImage, rightLine, cv::Scalar(0, 0, 255)); // 红色右线 }这里我们写一个辅助函数drawFittedLine来根据拟合的直线参数画出从ROI底部到顶部的长线。
void drawFittedLine(cv::Mat& image, const cv::Vec4f& lineParams, const cv::Scalar& color) { double vx = lineParams[0], vy = lineParams[1]; double x0 = lineParams[2], y0 = lineParams[3]; // 计算直线与图像底部和ROI顶部的交点 int y1 = image.rows; // 图像底部 int y2 = static_cast<int>(image.rows * 0.6); // ROI顶部,与之前定义对应 // 直线方程: (x - x0) / vx = (y - y0) / vy // 注意:vx可能为0,需要处理 int x1, x2; if (std::abs(vx) > 1e-5) { x1 = static_cast<int>(x0 + (y1 - y0) * (vx / vy)); x2 = static_cast<int>(x0 + (y2 - y0) * (vx / vy)); } else { // 近似垂直线,直接使用x0 x1 = static_cast<int>(x0); x2 = x1; } cv::line(image, cv::Point(x1, y1), cv::Point(x2, y2), color, 3, cv::LINE_AA); }4.5 完整流程整合与效果展示
将以上所有步骤整合到主函数中,并添加必要的图像显示,我们就可以看到最终效果了。
// 11. 显示各阶段结果(用于调试) cv::imshow("1. Original", originalImage); cv::imshow("2. Gray", grayImage); cv::imshow("3. Blurred", blurredImage); cv::imshow("4. Canny Edges", edgesImage); cv::imshow("5. Mask", mask); cv::imshow("6. Masked Edges", maskedEdges); cv::imshow("7. Hough Lines (All)", linesImage); cv::imshow("8. Final Lane Detection", resultImage); cv::waitKey(0);运行程序,你应该能看到一个多窗口的展示,清晰地呈现了从原始图像到最终车道线检测结果的每一步变化。最终的resultImage上应该有两根从车头延伸出去的蓝色和红色直线,分别代表左右车道线。
5. 参数调优与性能优化实战
算法框架搭好了,但直接运行效果可能不尽如人意。这部分我们来聊聊如何调参和优化,这是工程实践中最花时间也最能体现经验的地方。
5.1 关键参数敏感性分析与调试技巧
我们的算法中有几个“旋钮”,直接决定了检测效果的成败:
Canny边缘检测的双阈值 (
lowThreshold,highThreshold)- 现象:边缘断断续续,或者背景噪声太多。
- 调试:不要盲目猜测。我强烈建议在调试阶段,用
cv::createTrackbar创建滑动条来动态调整这两个阈值,实时观察edgesImage和最终车道线的变化。你会发现,对于光照均匀的白天图像,阈值可以设高一些(如100, 200);对于夜间或阴影多的图像,阈值需要降低(如30, 90)以捕捉更弱的边缘,但同时需要更严格的后续过滤(如霍夫变换阈值、斜率过滤)。
霍夫变换参数 (
threshold,minLineLength,maxLineGap)- 现象:检测不到车道线,或者检测出无数杂乱短线。
- 调试:同样使用滑动条。
threshold是最敏感的。可以先设一个较小的minLineLength(如20像素)和较大的maxLineGap(如20像素),然后慢慢提高threshold,直到杂线消失,主要车道线段依然保留。minLineLength用于过滤由路面裂缝或反光产生的小线段。
ROI顶点坐标
- 现象:车道线在图像顶部被截断,或者包含了太多无关区域。
- 调试:在图像上手动标出你认为车道线应该出现的区域,然后反推顶点坐标的比例。一个更稳健的方法是做一次透视变换(IPM, Inverse Perspective Mapping),将梯形ROI区域转换为鸟瞰图,这样车道线会近似平行,更利于处理。这是高级玩法,初期可以先固定比例。
5.2 应对复杂场景的增强策略
基础版本在清晰、笔直的道路上效果不错,但现实路况复杂得多。
- 弯道处理:我们目前拟合的是直线。对于弯道,直线拟合会失效。解决方案是将
cv::fitLine(直线拟合)改为多项式拟合,例如二阶多项式(抛物线)。使用cv::fitPoly可以对左右车道的点集分别拟合出一条曲线,这样就能描述弯道了。代价是计算量稍大,且需要更多的有效点。 - 光照与天气:晴天、阴天、夜晚、路面湿滑反光,都会极大影响边缘检测效果。单一的Canny参数难以通吃。可以考虑:
- 自适应阈值:使用
cv::adaptiveThreshold代替全局阈值的Canny(需要自己实现或找第三方库)。 - 颜色空间变换:车道线通常是白色或黄色。可以尝试在HSV或HSL颜色空间中,针对性地提取黄色和白色区域,生成一个颜色掩膜,再与边缘图结合,能显著提升在阴影下的鲁棒性。
- 图像增强:在灰度化前,先进行直方图均衡化(
cv::equalizeHist)或CLAHE(对比度受限的自适应直方图均衡),可以改善低对比度图像的边缘质量。
- 自适应阈值:使用
- 虚线检测:虚线会导致霍夫变换检测出多条短线段。我们现有的按斜率聚类和
maxLineGap连接机制在一定程度上能处理。更精细的做法是,在拟合前,对属于同一侧且斜率相近的线段,根据它们的端点位置进行合并或插值。
5.3 性能考量与代码优化
虽然这个demo对实时性要求不高,但养成良好的性能习惯很重要。
- 减少不必要的拷贝:OpenCV的
cv::Mat在赋值时是浅拷贝(只复制头信息)。使用clone()或copyTo()进行深拷贝时要明确其必要性。例如,显示中间结果的图像,如果不需要修改原图,用originalImage.clone();如果只是用于画图,可以考虑在副本上操作。 - 调整图像分辨率:对于高清视频流(如1920x1080),全分辨率处理速度慢。可以在第一步将图像缩放(
cv::resize)到一个固定的、较小的大小(如640x360)进行处理,最后将检测出的车道线坐标映射回原图进行绘制。这能极大提升帧率。 - 循环优化:对
vector<cv::Vec4i>的遍历是高效的。但如果点数极多,且需要复杂的分类逻辑,可以考虑使用更高效的数据结构或并行化(例如OpenMP),不过对于这个量级,通常不需要。
6. 从静态图像到视频流处理
让我们的程序“动”起来,处理视频文件或摄像头实时流,是更接近实际应用的步骤。
cv::VideoCapture cap; // 从文件打开 cap.open("data/lane_video.mp4"); // 或者从摄像头打开(通常为0) // cap.open(0); if (!cap.isOpened()) { std::cerr << "Error opening video stream or file" << std::endl; return -1; } cv::Mat frame; while (true) { cap >> frame; // 获取一帧 if (frame.empty()) break; // 视频结束 // --- 将之前对单张图片的所有处理流程封装成一个函数 processFrame(frame) --- cv::Mat outputFrame = processFrame(frame); cv::imshow("Lane Detection - Video", outputFrame); // 按ESC退出 char c = (char)cv::waitKey(25); if (c == 27) break; } cap.release(); cv::destroyAllWindows();将核心检测逻辑封装进processFrame函数。这里会遇到的新问题是检测结果的抖动。视频中每一帧独立检测,由于噪声和计算误差,拟合出的车道线会在帧与帧之间轻微跳动,看起来不流畅。
解决方法:滤波与跟踪一种简单有效的方案是使用移动平均或卡尔曼滤波。我们可以维护一个全局的状态变量(例如左右车道线的参数),在每一帧,将新检测到的线与上一帧的线进行加权平均。
// 全局状态(简单示例,用直线的斜率和截距表示) struct LaneLine { double slope; double intercept; bool detected; }; LaneLine leftLane, rightLane; // 初始化 // 在 processFrame 中,得到当前帧拟合的 newLeftLine, newRightLine 后 if (leftLane.detected) { // 加权平均:当前帧权重0.3,历史权重0.7 leftLane.slope = 0.7 * leftLane.slope + 0.3 * newLeftLine.slope; leftLane.intercept = 0.7 * leftLane.intercept + 0.3 * newLeftLine.intercept; } else { // 第一次检测到,直接赋值 leftLane.slope = newLeftLine.slope; leftLane.intercept = newLeftLine.intercept; leftLane.detected = true; } // 对右车道线做同样处理如果某一帧没有检测到车道线(例如点集数量不足),则直接使用上一帧的历史值进行绘制,直到连续多帧都检测失败再重置状态。这样能大大提升视频输出的稳定性。
7. 常见问题排查与实战心得
在实现和调试这个项目的过程中,你几乎一定会遇到下面这些问题。我把我的排查思路和解决方法记录下来,希望能帮你节省时间。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 完全检测不到任何线段(lines为空) | 1. Canny阈值过高,边缘图全黑。 2. ROI区域设置错误,掩膜覆盖了所有边缘。 3. 霍夫变换阈值过高。 | 1. 依次显示edgesImage,mask,maskedEdges,确认每一步都有正确的白色像素。2. 调低Canny阈值或霍夫变换阈值。 |
| 检测出大量杂乱无章的短线 | 1. Canny阈值过低,噪声过多。 2. 霍夫变换阈值过低。 3. minLineLength设置过小。 | 1. 提高Canny的高阈值。 2. 大幅提高霍夫变换的 threshold参数。3. 增加 minLineLength,比如设为50或100。 |
| 车道线断断续续,拟合出的线很短 | 1. Canny检测出的边缘本身不连续。 2. maxLineGap设置太小,断开的线段没有连接。 | 1. 适当降低Canny低阈值,或尝试高斯模糊时使用更大的核(如7x7)。 2. 增大 maxLineGap参数。 |
| 左右车道线分类错误(左右颠倒) | 斜率分类的逻辑基于图像坐标系。在图像中,y轴向下为正。左车道线斜率应为负,右车道线为正。 | 检查斜率计算和分类条件。打印几条线的斜率看看。确保你的图像坐标系理解正确。 |
| 拟合的直线方向不对或长度异常 | cv::fitLine返回的是单位方向向量。在drawFittedLine函数中,根据方向向量计算端点时,公式x = x0 + (y - y0) * (vx / vy)在vy很小时会导致数值不稳定。 | 在计算前判断std::abs(vy) > 1e-5。如果vy太小(线接近水平),则用y = y0 + (x - x0) * (vy / vx)来计算y坐标。或者,直接用两个端点拟合直线的一般式方程。 |
| 处理视频时闪烁严重 | 帧间结果差异大,没有做平滑处理。 | 引入滤波机制,如上一节所述的移动平均,对车道线参数进行帧间平滑。 |
| 在弯道处直线拟合效果差 | 模型局限。直线无法描述弯曲的车道。 | 升级拟合模型,使用二次曲线(cv::fitPoly)进行拟合。需要收集更多的点,并且ROI可能需要调整。 |
几点重要的实战心得:
- 可视化是调试的生命线:不要只盯着最终结果看。把每一步的中间图像(灰度图、边缘图、掩膜图、霍夫线段图)都显示出来。问题出在哪个环节,一目了然。
- 参数没有银弹:我给出的参数值(Canny的50/150,霍夫变换的50/30/10)只是一个起点。对于不同的视频、不同的光照条件,最优参数是不同的。一个健壮的程序应该允许参数动态调整,或者具备一定的自适应能力。
- 从简单场景开始:先用一条笔直、清晰、光照好的车道图片进行测试,确保算法骨架正确。然后再逐步挑战弯道、阴影、夜间、虚线等复杂场景。分阶段攻克难点。
- 理解原理比调参更重要:花时间弄懂Canny算法里高阈值和低阈值分别控制什么,霍夫变换的“投票”机制是怎样的。理解了原理,调参就有了方向,而不是盲目乱试。
- 代码的模块化:尽早把图像预处理、边缘检测、霍夫变换、车道拟合等步骤封装成独立的函数。这样不仅代码清晰,也方便你单独测试每个模块,或者未来替换成更先进的算法(比如把Canny换成深度学习边缘检测器)。
这个项目到这里就完成了从理论到实践的全过程。它就像一把钥匙,帮你打开了传统计算机视觉的大门。虽然基于深度学习的方法在精度和鲁棒性上早已超越它,但这里面蕴含的图像滤波、特征提取、模型拟合的思想,是通用的。下次当你看到更复杂的视觉系统时,你会更容易理解它在每个环节做了什么,以及为什么这么做。