ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

OpenCV车道线检测:从Canny到HoughLinesP调参与拟合

OpenCV车道线检测:从Canny到HoughLinesP调参与拟合

简介:面向计算机视觉入门者与自动驾驶感知方向的学习者,这套基于OpenCV的车道线检测工程源码围绕真实道路图像,完整呈现从图像预处理、Canny边缘检测、轮廓提取到霍夫变换与线性拟合的检测链路,适合用来理解车道线识别的经典流程,也可作为课程实验或项目练手的参考实现。压缩包共83个文件,约2.37MB,以3个cpp源文件、3个头文件为核心,搭配Visual Studio工程配置与可执行文件,另含多张jpg、png道路场景图,便于直接编译运行并对照不同输入观察检测效果;少量日志与中间文件属于构建产物,不影响主体代码阅读。目前已有2883人学习下载,反映出其在车道线检测入门场景中的参考价值。读者可获得一套结构清晰、可调试的OpenCV实现,借助示例图像验证边缘检测、轮廓筛选与线段拟合各环节的参数影响,并在此基础上扩展HSV颜色空间、图像矫正或深度学习方案,逐步完善自己的车道线检测系统。

1. 一份“能跑”的车道线检测代码,卡在 99% 的地方不是算法

搜opencv 车道线检测的人,十个里有八个最后停在同一个地方:代码跑起来了,视频里画出来的线却歪歪扭扭。要么只有半截,要么在树影下面突然拐向路外,要么换个分辨率整屏全黑。问题不在算法,绝大多数流传的示例把参数、感兴趣区域和拟合逻辑固定绑定在某一段特定素材上,离开那段素材就失效。这份基于 OpenCV 的车道线检测代码(Python + numpy + cv2,不需要 GPU,也不依赖训练数据)值得拆解的地方,是它把预处理、边缘提取、霍夫变换、左右线分离和多项式拟合串成了一条可调、可换素材的管线。它适合已经会用 cv2 读写图像、想先把结构化道路场景跑通的人,也适合拿它当图像处理练手项目来复盘每一步在干什么。

2. 从一帧 BGR 图像到二值边缘图:六步管线的每一步都在丢掉什么

先把环境固定下来,再谈算法。用 opencv-python 4.x 就够,HoughLinesP在主干模块里,不需要装 contrib 包。真正容易出问题的是数据读取路径:cv2.imread遇到带中文的路径会静默返回None,后面的cvtColor直接抛异常,报错信息还指向灰度转换那一行,看起来很像是颜色空间的问题。我一般的做法是路径统一用英文,或者在读之前用np.fromfile+cv2.imdecode兜一层。视频源同理,cv2.VideoCapture打开失败不会抛异常,只会让read()一直返回False,所以第一件事必须是判断有没有打开成功。

import cv2 cap = cv2.VideoCapture('data/road.mp4') if not cap.isOpened(): raise RuntimeError('视频打开失败,先检查路径与编码格式') while True: ok, frame = cap.read() if not ok: # 读到结尾或解码失败都会走到这里 break # 后续处理 cap.release()

这段代码里的判断是整条管线的基础。少写一个isOpened(),后面排查半小时都找不到原因。read()返回两个值,第一个是布尔标志,第二个才是帧,很多人写成frame = cap.read()然后拿到的其实是个元组,cvtColor报错说通道数不对,就很令人费解。

2.1 灰度、模糊、边缘:三个操作各自负责什么

车道线检测的输入最终是二值边缘图,中间要经过灰度化和模糊两步。灰度化把所有颜色信息压成亮度,代价是黄色虚线和白色实线在灰度图上可能亮度接近,这是后面要补颜色阈值分支的原因。高斯模糊的作用是抑制高频噪声,让 Canny 不要被柏油路面的颗粒感触发一堆碎边缘。核大小必须是奇数,(5, 5)是常用起点,分辨率越高核可以越大,但超过(7, 7)之后细小虚线容易被抹掉。

Canny 的双阈值是这条管线里第一个真正需要调的参数。低阈值决定“多弱的边缘还算边缘”,高阈值决定“多强的边缘才算种子”。常见做法是按 1:2 到 1:3 的比例给,比如50/150。低阈值给太低,路面纹理、车道文字、修补痕迹全变成边缘,霍夫变换会淹没在噪声里;低阈值给太高,虚线只剩零星几段,minLineLength一卡就什么都不剩。我一般会先固定高阈值,把低阈值从 30 往上试到 80,看二值图里车道线是否连续、背景是否干净。

import cv2 import numpy as np def region_of_interest(edge, vertices): """用多边形掩膜裁掉天空、对向车道和车头引擎盖""" mask = np.zeros_like(edge) cv2.fillPoly(mask, vertices, 255) # 多边形内部填 255 return cv2.bitwise_and(edge, mask) # 与运算,保留掩膜内像素 def edge_pipeline(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) # 核必须是奇数 edges = cv2.Canny(blur, 50, 150) # 低:高 ≈ 1:2 ~ 1:3 h, w = edges.shape # 用比例而不是绝对坐标,换分辨率时不用改代码 verts = np.array([[ (int(w * 0.10), h), # 左下 (int(w * 0.45), int(h * 0.60)), # 左上,靠近灭点 (int(w * 0.55), int(h * 0.60)), # 右上 (int(w * 0.95), h), # 右下 ]], dtype=np.int32) return region_of_interest(edges, verts)

函数拆成两个是有理由的:region_of_interest在调试阶段要单独调用,把掩膜结果叠回原图看一眼,确认梯形没有把车道线切掉。直接串在管线里,出问题时你不知道是 Canny 没检出还是 ROI 裁掉了。fillPoly的顶点必须是np.int32,传 float 会报错,而且报错信息只说类型不匹配,不说哪一行。

2.2 ROI 梯形的四个顶点:太窄丢弯道,太宽引噪声

梯形顶点的位置决定了这条管线的上限。左上、右上两个点的纵坐标h * 0.60大致对应图像里的灭点高度,横向取 0.45 和 0.55,中间留出的 10% 宽度是给远处车道线的容错。这两个点给太高(比如 0.4),ROI 会覆盖到天空和远处建筑,霍夫变换检出一堆水平边缘;给太低(比如 0.7),远处车道线全被裁掉,拟合出来的线只能看到近处十几米,稍微一转弯就偏。

底边两个点取 0.10 和 0.95 是留了边距。取满 0 和 1 会把车头引擎盖边缘、雨刮器、仪表台反光全放进来,这些水平强边缘在霍夫变换里票数极高,能直接把结果带偏。如果你的素材是固定机位车载摄像头,这四个比例可以标定一次写进配置文件;如果是手机随手拍的、机位在变,就得接受 ROI 需要按场景微调,或者干脆上透视变换做鸟瞰图再处理。

针对灰度图丢颜色的短板,可以加一条并行的颜色分支:把图像转到 HSV,对白色(低饱和高亮度)和黄色(色调在 15~35 区间)各取一次inRange,结果与 Canny 边缘做bitwise_or。逆光和阴影场景下这条分支的贡献很明显,代价是白天正常光照时也会带进一些路面标线和广告牌,需要靠 ROI 和后面的斜率筛选再筛一遍。

3. HoughLinesP 的五个参数:调参顺序错了,改一天也调不出来

霍夫变换是这条管线的核心,也是最容易调参调错的地方。HoughLinesP是概率霍夫变换,它不返回无限长的直线,而是返回检测到的线段端点,这对车道线场景更合适——虚线本来就是一段一段的。参数有五个,但它们的敏感度差得很远,按错误的顺序调,你会一直在两个参数之间来回改。

3.1 五个参数各自负责什么

lines = cv2.HoughLinesP( edge, # 必须是 8 位单通道二值图,Canny 输出正好满足 rho=1, # 距离分辨率,单位像素 theta=np.pi / 180, # 角度分辨率,1 度 threshold=40, # 累加器阈值:一条线至少要有多少票 minLineLength=30, # 线段最短长度,短于此直接丢弃 maxLineGap=20, # 同一条线上允许的最大断裂间隙 )

rho和theta一般不动。rho=1表示按 1 像素精度累加,theta=np.pi/180表示按 1 度分桶。有人为了提速把theta改成np.pi/90,速度是快了,但角度精度掉到 2 度,拟合出来的线在远处会有明显抖动。除非跑在算力很紧的嵌入式板子上,这两个值保持默认。

threshold是票数门槛。一条候选直线在参数空间里被多少条边缘点“投票”支持,超过这个数才输出。它直接决定检出的线段数量:给 20,满屏都是碎线;给 100,虚线基本全丢。这个参数和图像分辨率强相关,720p 下 40 左右是个合理起点,1080p 要往上加到 60~80。

minLineLength卡掉短的。车道虚线在 720p 下每一节约 30~50 像素,所以给 30 是能保住虚线的下限。给到 80,实线还在,虚线全没了,结果就是拟合时一侧有数据一侧没有,画出来的线只剩半边。

maxLineGap是把断开的线段接起来的容忍度。虚线的间隔一般在 20~40 像素,maxLineGap给到 20 能把相邻两节虚线连成一条;给 0 就变成每节虚线各自成段,拟合时同一侧出现好几组参数,取平均之后斜率被拉平。

参数作用720p 起点调大的后果调小的后果
threshold累加器票数门槛40线段变少,虚线易丢碎线增多,噪声误检
minLineLength线段最短长度30虚线被丢弃短噪点被当车道线
maxLineGap允许断裂间隙20不同线被错误连通同一条线碎成多段
rho距离分辨率1精度下降,结果抖动计算量上升
theta角度分辨率π/180角度变粗,远处偏移计算量上升

3.2 调参的正确顺序:先阈值,再长度,最后间隙

我一般按threshold → minLineLength → maxLineGap的顺序调,每次只改一个,改完把检出线段画在原图上存一张截图对比。先调threshold是因为它决定“有没有”,后面两个决定“好不好”。一上来就调minLineLength,很可能是在一个本来就没检出几条线的结果上做筛选,越调越空。

调试时把 Hough 结果单独可视化,比看最终画线结果有用得多。最终结果经过了筛选和拟合,出了问题你分不清是检测阶段漏了还是拟合阶段错了。下面这个小函数在调试期会反复用到,把每条线段用不同颜色画出来,虚线断裂、误检的水平线一眼就能看见。

def draw_hough_debug(frame, lines): """调试用:把所有霍夫线段原样画出来,不做任何筛选""" debug = frame.copy() if lines is None: return debug for i, line in enumerate(lines): x1, y1, x2, y2 = line[0] color = (0, 255, 0) if i % 2 == 0 else (0, 165, 255) cv2.line(debug, (x1, y1), (x2, y2), color, 2) return debug

这段函数的价值在于给拟合阶段和检测阶段划清界限。如果调试图上左侧车道线是连续的一串绿色线段,但最终结果偏了,问题在斜率筛选或拟合;如果调试图本身就断断续续,回去调 Canny 和 Hough。很多“调不出来”的情况,都是把两个阶段的问题混在一起猜。

3.3 返回结构与三个容易踩的单位问题

HoughLinesP的返回值形状是(N, 1, 4),不是(N, 4)。直接for x1, y1, x2, y2 in lines会报too many values to unpack。要么用line[0]取,要么先lines.reshape(-1, 4)。返回None的情况也要处理——一帧里一条线都没检出是正常现象,尤其在强逆光或者车道线磨损的路段,不判空直接遍历就崩。

标准霍夫变换HoughLines返回的是(rho, theta),单位分别是像素和弧度,想还原成图像坐标得自己算x = rho*cos(theta)、y = rho*sin(theta),还要处理垂直线sin(theta)=0的除零。车道线场景里垂直方向不会出现,但如果你把 ROI 设错了、把画面边缘的杆子框进来,就会碰到。这也是我倾向用HoughLinesP的原因之一:端点坐标直接可用,省掉一层三角函数换算和边界判断。

第三个问题是坐标系。图像坐标的原点在左上角,y 轴向下增长。这导致左侧车道线在图像里的斜率是负的(x 随 y 增大而减小),右侧是正的。如果你按数学课本的习惯判断“左负右正”之外还加了额外符号处理,很容易把左右弄反,画出来的两条线交叉成 X。第一次写的时候我也犯过这个错,图上看就是两条线在画面中间打了个叉。

4. 左右车道分离与拟合:斜率筛选之后才是真正的难点

霍夫变换给出的是无序的线段集合,里面混着左车道线、右车道线、护栏、路面文字、前车尾部。下一步是把这些线段分到左右两侧,各自合成一条代表线。这一步的输入质量直接决定输出质量——如果第三章的参数没调好,这里再怎么调都是白费。

4.1 用斜率分左右,再用截距定位

分类依据是斜率。图像坐标系下,左侧车道线斜率小于 0,右侧大于 0。但只判符号不够:接近水平的线段(|k| < 0.4)多半是路面标线、斑马线或者护栏横杆,必须剔掉;接近垂直的线段(|k| > 5)一般是画面里的杆状物,也要剔。这两个阈值是按 720p、车载前视视角估的,换成俯视或者广角镜头要重新定。

import numpy as np def split_lanes(lines, k_min=0.4, k_max=5.0): """按斜率把线段分成左右两组,返回 (k, b) 参数对""" left, right = [], [] if lines is None: return left, right for line in lines.reshape(-1, 4): x1, y1, x2, y2 = line if x2 == x1: # 垂直线,斜率无穷大,直接跳过 continue k = (y2 - y1) / (x2 - x1) if abs(k) < k_min or abs(k) > k_max: continue # 太水平或太垂直都不是车道线 b = y1 - k * x1 # y = kx + b 的截距 if k < 0: left.append((k, b)) else: right.append((k, b)) return left, right

把线段存成(k, b)而不是两个端点,是为了后面做平均。同一条车道线上的线段,斜率接近、截距也接近,直接对参数取平均就能得到一条代表线;如果对端点取平均,一条从(100, 700)到(200, 500)的短线和一条从(80, 720)到(400, 400)的长线会被平均成一条谁都不像的线。参数空间平均的好处是它天然按“线的方向”聚类,不受线段长度影响,坏处是当同侧混进了一条斜率差异很大的误检线时,平均值会被拉偏——这种情况下用中位数比平均值稳,代价是多一次排序。

k_min=0.4这个阈值值得多说一句。它对应的是水平方向偏移 1 像素、垂直方向偏移 0.4 像素的线,视觉上大约是与水平线成 22 度的角。低于这个角度的线段,在透视投影下几乎不可能是车道线,但路面的接缝、减速带、前车保险杠下沿经常落在这个区间。给到 0.6 会更干净,代价是弯道内侧的线段角度变小,可能被误杀。

4.2 从参数对到一条完整线段:拟合与绘制

有了(k, b)的集合,取平均后代入两个 y 值就能得到端点。这里要选两个 y:底边用图像高度h(对应车前方最近处),顶边用 ROI 的上边界h * 0.60(对应灭点附近)。选这两个值是因为它们对应 ROI 的上下限,画出来正好铺满有效区域,不会有一段线画到 ROI 外面去显得突兀。

def fit_lane(params, y_bottom, y_top): """对 (k, b) 集合取中位数,返回一条线段的两个端点""" if not params: return None k, b = np.median(np.array(params), axis=0) if abs(k) < 1e-6: return None x_bottom = int((y_bottom - b) / k) x_top = int((y_top - b) / k) return x_bottom, y_bottom, x_top, y_top

除零判断不能省。虽然 4.1 里已经剔掉了x2 == x1的线段,但取中位数之后k有可能落到一个极小的值上(比如同侧只剩一条几乎水平的线),(y - b) / k会算出几万像素的 x,画线时cv2.line不报错,但会把整幅图刷成一条斜穿屏幕的色带,看起来像是“画崩了”。这个坑我第一次遇到时以为是坐标算错,查了半天才发现是数值问题。

中位数和平均数在这里的差别值得实测一次。找一段有护栏的素材,用平均数跑一遍,再改成中位数跑一遍,你会看到平均数版本在护栏进入 ROI 的那几帧里明显往右偏。中位数的代价是对称性——当同侧只有两条线且方向差异大时,中位数会直接选其中一条,输出会在两条之间跳变。这种情况一般出现在虚线场景的远处,靠下面的时序平滑能压住。

拟合完成后画线,用cv2.line加半透明叠加比直接画实线好看得多。cv2.addWeighted把原图和线图层按 0.8:1 混合,视觉上像高亮而不是涂鸦。如果要做成双线填充的效果(车道区域高亮),就用cv2.fillPoly把左右两条线和底边围成的四边形填充,再叠回去。

4.3 滑动窗口搜索:弯道和虚线场景的备用手段

霍夫变换的本质是找直线,遇到半径小的弯道就力不从心——弯道上的车道线在局部仍然是直的,但整条线的方向在变,霍夫的全局投票会把票数分散到多个角度上,threshold一卡就全丢了。这时候需要换一套基于像素搜索的思路:先在下半部分找出行车道线像素的横向分布直方图,峰值位置就是车道线的起点,然后从这个起点往上逐层开窗搜索,每找到一批像素就用二次多项式重新拟合,下一层的窗口中心跟着新的拟合曲线走。

def find_lane_pixels(binary, nwindows=9, margin=60, minpix=40): """自底向上滑动窗口,返回左右车道线的像素坐标""" histogram = np.sum(binary[binary.shape[0] // 2:, :], axis=0) midpoint = histogram.shape[0] // 2 leftx_base = np.argmax(histogram[:midpoint]) rightx_base = np.argmax(histogram[midpoint:]) + midpoint window_height = binary.shape[0] // nwindows nonzero = binary.nonzero() # 返回 (y, x) nonzeroy, nonzerox = np.array(nonzero[0]), np.array(nonzero[1]) leftx_current, rightx_current = leftx_base, rightx_base left_inds, right_inds = [], [] for window in range(nwindows): y_low = binary.shape[0] - (window + 1) * window_height y_high = binary.shape[0] - window * window_height for inds, xc in ((left_inds, leftx_current), (right_inds, rightx_current)): good = ((nonzeroy >= y_low) & (nonzeroy < y_high) & (nonzerox >= xc - margin) & (nonzerox < xc + margin)).nonzero()[0] inds.append(good) if len(good) > minpix: # 窗内像素够多才更新窗口中心 xc = int(np.mean(nonzerox[good])) leftx_current, rightx_current = leftx_current, rightx_current return nonzerox, nonzeroy, np.concatenate(left_inds), np.concatenate(right_inds)

margin是窗口半宽,给 60 对应 720p 下车道线在一个窗口高度内的横向漂移范围;弯道急的时候要加到 80~100,代价是更容易把相邻车道的线吸进来。minpix是窗口内最少像素数,低于这个数就不更新窗口中心,防止一个空窗口把搜索方向带偏——这个判断是整段代码里最容易被忽略的一行,删掉之后在虚线区段搜索会一路往画面边缘飘。

窗口数量nwindows和图像高度挂钩。给 9 个窗口、720p 图像,每个窗口高 80 像素,这个粒度对普通弯道够用;给 5 个窗口,每个 144 像素,拟合出来的曲线会明显折线化。给 15 个窗口并不会更准,只是更慢,因为窗口高度小到一定程度后每个窗口里的像素数不足以支撑稳定的均值。

拿到像素点后用np.polyfit(y, x, 2)拟合二次曲线,注意是x关于y的函数——车道线在图像里是近垂直的,用y当自变量才不会出现一个x对应多个y的情况。这个自变量选择在很多示例里写反了,正常路段看不出问题,一到弯道就拟合成一条横躺的抛物线。

5. 五类常见翻车现象:从图像上反推是哪一步出了问题

车道线检测的调试最怕的是“看起来不对但说不出哪里不对”。下面这五类现象是按出现频率排的,每一条都能从最终画面的形态反推到具体的代码位置。

5.1 阴影路段车道线突然劈叉

现象:车辆驶过树影或桥洞时,原本稳定的两条线突然向中间收拢或者各自分裂成两条,驶出阴影后恢复正常。

原因:阴影边界在 Canny 里是一条强边缘,方向接近垂直,斜率正好落在split_lanes的合法区间内,被当成左右车道线各投了一票。因为阴影边界比真实车道线还清晰,threshold拦不住,中位数在只有一两条真实线的情况下会被这一票带偏。

解决:在split_lanes里加一条位置约束,左侧线的截距必须落在图像左半部分,右侧线落在右半部分;同时对同一侧的多条线段先按b做一次一维聚类,把偏离主簇超过 100 像素的剔除。更稳的做法是把 Canny 换成Sobel的 x 方向梯度,阴影边界在 x 梯度上的响应比 Canny 弱得多,代价是虚线的响应也会下降,需要把threshold相应调低。

5.2 逆光时整屏全黑,一条线都没有

现象:迎着太阳行驶的几秒里,检测结果完全为空,调试图上 ROI 里干干净净。

原因:强逆光下路面整体过曝,灰度值趋近 255,车道线和路面的对比度被压到接近 0,Canny 的高低阈值都触发不了。这不是参数问题,是信息在采集阶段就丢了。

解决:改用cv2.createCLAHE做局部直方图均衡,把clipLimit设在 2.0~4.0,tileGridSize给(8, 8),在灰度图上做一遍再进 Canny。CLAVE 对局部对比度的恢复效果比全局equalizeHist好,后者会把过曝区域整体拉暗,反而让车道线更模糊。另一个方向是补颜色分支,黄色车道线在 HSV 空间的高饱和度特征在逆光下仍然部分保留。

5.3 虚线导致输出线上下抖动

现象:检测结果每帧都在小幅跳动,画出来的线像在呼吸。

原因:虚线的间隙让每一帧检出的线段数量和位置都在变,minLineLength附近抖动的线段时而入选时而被剔除,中位数结果随之跳变。这是单帧处理的固有问题,靠调参数只能减小幅度,消不掉。

解决:加时序平滑。最简单的做法是维护一个长度为 5 的滑动窗口,存最近 5 帧的(k, b),取平均后再画;好一点的做法是用指数加权,新帧权重 0.4、历史 0.6。如果左右线的截距在相邻帧之间跳变超过 50 像素,判定为异常帧直接沿用上一帧结果,这个阈值按帧率和车速调,低速时给 30 就够,高速时要放到 80 以上。

5.4 换个分辨率的视频就全线失效

现象:720p 素材上跑得好好的,换成 1080p 或者手机竖拍素材,检出的线全歪或者全空。

原因:threshold、minLineLength、margin这些参数都是像素单位,分辨率翻倍意味着同样的车道线在图像上占的像素数变了,参数全部失效。ROI 的四个顶点如果用绝对坐标,换分辨率直接框到画面外。

解决:所有像素单位参数都按图像高度做归一化,minLineLength = int(h * 0.04)、threshold = int(h * 0.055)这类写法,实测在 480p 到 1440p 之间迁移时只需要微调。ROI 顶点全部改成比例(第二章的写法),这是最省事的一步,也是最多人漏掉的一步。

5.5 同一侧画出好几条平行线

现象:左车道线位置出现了三条几乎平行的线,颜色叠加后变成一坨。

原因:拟合阶段没有做聚合,split_lanes返回的每一条(k, b)都被单独画了一遍,而不是先合并成一个代表值。这在调试阶段看着还行,上线就是明显的错误。

解决:fit_lane在取中位数之前先做一次筛选,把b与中位数偏差超过阈值的参数对丢掉再重算一次中位数。两次中位数的做法比一次稳,因为第一次中位数可能已经被离群值带偏,第二次在剔除离群值后能回到真实中心。阈值取 100 像素是个可用起点。

6. 透视变换到俯视图:把像素距离换算成米,检测才算有了物理意义

前面整套流程都在原始视角下工作,好处是直观、计算量小,坏处是无法回答“这条线偏了多少米”“弯道半径多大”这类问题。要做这个,得先把图像做一次透视变换,把前视视角下呈梯形的路面拉成俯视图,此时车道线在图上近似平行,像素距离和实际距离之间是线性关系。标定方法不复杂:在原始图上量出四条车道线构成的梯形四个顶点,在俯视图上指定这四个点要落到的矩形位置,cv2.getPerspectiveTransform给出变换矩阵。

import cv2 import numpy as np def to_bird_view(binary, w, h): src = np.float32([ # 原始图上的梯形,需按素材实测调整 [w * 0.45, h * 0.62], [w * 0.55, h * 0.62], [w * 0.90, h * 1.00], [w * 0.10, h * 1.00], ]) dst = np.float32([ # 俯视图上的矩形 [w * 0.25, 0], [w * 0.75, 0], [w * 0.75, h], [w * 0.25, h], ]) M = cv2.getPerspectiveTransform(src, dst) return cv2.warpPerspective(binary, M, (w, h), flags=cv2.INTER_NEAREST), M

src四个点的顺序必须和dst一致,都是左上、右上、右下、左下,否则变换出来是镜像或者旋转的。flags用INTER_NEAREST而不是默认的双线性,是因为输入是二值图,插值会引入 0~255 之间的灰阶值,后续按阈值处理时又得多一道判断。变换矩阵M要留着,反向投影把俯视图上的检测结果叠回原图时要用它的逆矩阵np.linalg.inv(M)。

有了俯视图,曲率半径就能算了。按二次曲线x = a*y² + b*y + c拟合后,曲率半径近似为((1 + (2a*y + b)²)^1.5) / |2a|,这里的y取图像底部位置。这个公式算出的是像素单位,要换算成米得先标定像素/米的比例:在真实道路上量一段已知长度(比如车道线虚线的标准间隔),数出它在俯视图上占多少像素,两者相除。横向比例和纵向比例通常不同,因为warpPerspective的画布尺寸和实际路面尺寸没有对应关系,必须分开标。标定做完之后,曲率半径换算成米、横向偏移换算成米才有意义,否则算出来的数字只是看起来专业。

这件事我吃过亏:早些年拿着像素单位的曲率半径去做转弯判断,阈值是按 demo 素材调的,换一段高速素材全部失效,因为车速和焦距都变了,像素半径和实际半径的换算系数完全不一样。从那以后我每次上车道线项目,都强制先把像素/米标定做完再动任何阈值,哪怕只是跑个 demo。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表