资讯动态

固定视角停车位识别:透视矫正、H5模型与状态机去抖

发布时间:2026/9/12 16:02:16 来源:尧图企业网站定制
简介这套源码是一套基于Python与OpenCV的停车场车位识别项目针对监控画面中的车位占用状态进行检测适用于智能交通、停车场管理等领域也适合计算机视觉初学者和有经验的开发者学习、仿写或二次扩展。整个资源包共1111个文件压缩后约111.51MB主要包含1103张JPG图像样本、3个Python源码文件、1个H5模型、1个MP4演示视频以及少量辅助数据文件可覆盖从数据集、模型到演示验证的完整项目链路。该实现已在CSDN平台获得261人学习可作为OpenCV项目实战的参考案例。源码涵盖图像采集、灰度化与滤波预处理、边缘检测与形态学变换、车位定位、特征提取及占用状态判断等完整环节注释详尽、结构清晰结合演示视频和现场图像能帮助读者快速理解车位识别流程并为进一步优化算法提供了可扩展的基线。1. 停车位识别里最容易翻车的不是模型而是你怎么把一个车位切出来做停车位识别的人一开始都会掉进同一个坑把精力全砸在分类模型上结果换一个监控角度准确率直接跳水三层。原因很简单——车位的形状在画面里根本不是矩形而是被透视压缩过的四边形不同时段阴影方向不同边缘检测结果完全两样。这份源码给我的第一印象就是它没走这条弯路通过固定相机的棋盘格标定逻辑把每个车位先用透视几何锚定成固定区域再把区域切成小图交给一个极轻量的卷积模型car1.h5去判断空还是占。整套代码对新手友好但里面处理光照归一化和 ROI 投影的那部分经验对做过两年以上 CV 的人也有参考价值。本文会按车间位识别最关键的三层来拆预处理与透视矫正、车位区域语义标注、基于 H5 权重的前向推理最后补一个状态机去抖动的小技巧。2. 图像采集与预处理为什么先做光照抑制而不是先放大对比度2.1 不依赖视频流用图像序列代替帧读取源码工程目录里给出的是scene1410.jpg、scene1380.jpg这类连续的帧文件而不是一个 mp4。这种设计在实验阶段非常合理视频解码依赖显卡驱动和编解码库而图像序列只要cv2.imread一条路走到底保证在任何 Linux 服务器或 Windows 笔记本上行为一致。实际部署到停车场时从 RTSP 流取帧后再落盘成同样命名规则的文件就能无缝接上原有管线。import glob import cv2 import numpy as np frames sorted(glob.glob(scene*.jpg)) for i, fpath in enumerate(frames): img_bgr cv2.imread(fpath) if img_bgr is None: continue # 统一尺寸避免不同摄像头分辨率导致标注坐标失效 img_bgr cv2.resize(img_bgr, (1280, 720), interpolationcv2.INTER_AREA) gray cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)代码逻辑glob.glob拿到所有帧文件sorted让顺序按文件名自然排列保证时序稳定。cv2.resize统一到 720p是为了让后续的 spot 标注坐标系固定否则同一组四点坐标在不同分辨率下会整体漂移识别率直接崩。INTER_AREA是缩小图像时防摩尔纹的正确选择放大时才用INTER_LINEAR或INTER_CUBIC。2.2 光照归一化在 HSV 空间压阴影停车场最典型的干扰源是树影和相邻车辆投影。直接对 BGR 灰度图做对比度增强会把阴影边缘也当成强特征。常见做法是切到 HSV 空间只对 V 通道做 CLAHE限制对比度自适应直方图均衡再合回原图。因为色调 H 和饱和度 S 对光照变化不敏感保留它们就保留了车体颜色信息而这些信息对区分深色车 vs 空车位地面很有用。hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) v_eq clahe.apply(v) hsv_eq cv2.merge([h, s, v_eq]) img_norm cv2.cvtColor(hsv_eq, cv2.COLOR_HSV2BGR)参数说明clipLimit2.0决定对比度增幅太大会把地面纹理放大成伪边缘tileGridSize(8,8)表示把图像切成 8×8 的小块各自做直方图均衡兼顾局部与全局。整段代码的行间效果是车体色块被保留阴影区域的亮度渐变被压平。操作目的失败表现BGR → HSV把亮度与颜色分离直接均衡 BGR 通道会偏色V 通道 CLAHE抑制阴影梯度clipLimit 过大时车位线出现麻点H/S 通道不动保留车漆颜色信息S 通道被滤波后深色车与沥青难分做完这步下游的边缘检测拿到的输入是光照无关的结构图而不是原始灰度图。我见过很多人跳过这步直接 Canny结果同一车位在晴天和阴天检出率差 20 个百分点。2.3 梯度幅值图比 Canny 更适合车位场景的中间表示Canny 会把边缘细化为单像素骨架这对车道线提取没问题但对车位识别是灾难——车位边框在画面里往往模糊、褪色、被轮胎碾压开裂单像素骨架根本连不成闭合区域。更稳的做法是计算梯度幅值图保留边缘的能量信息gx cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize3) gy cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize3) mag, angle cv2.cartToPolar(gx, gy, angleInDegreesTrue) mag cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)cv2.Sobel输出浮点梯度cv2.cartToPolar同时算出幅值和方向角。这里的关键点在于后续判断车位内是否有车时我们用得更多的是幅值分布和 HOG 类特征而不是二值边缘。梯度幅值图天然包含哪个方向上有多少强度变化的信息配合后面模型输入时可以让网络同时看到纹理方向和强度两个维度。3. 车位标定与透视矫正把斜视监控画面变成俯视图再切割3.1 为什么固定相机必须做透视矫正监控摄像头通常架在立杆或建筑外墙上光轴与地面不垂直画面里的车位是斜四边形。如果直接按像素矩形截取同一个车位在不同帧里包含的背景区域比例不同——车停得靠左一点矩形框就切进旁边车位的白线。透视矫正的数学本质是求一个单应矩阵 H把像素平面映射到地平面[ \begin{bmatrix} X \ Y \ 1 \end{bmatrix} H \begin{bmatrix} x \ y \ 1 \end{bmatrix} ]H 有 8 个自由度所以只需要 4 组对应点就能解出来。源码中用with_parking.jpg作为基准图来标定这个图的作用是原始标定底图你在这张图上标注 4 个躺在地上的参考点比如停车位四个角再指定它们对应俯视图中的矩形位置就能求出 H。3.2 spot 字典的存储结构与标注逻辑工程里的spot70.jpg、spot2.jpg、spot71.jpg这类文件名每一张小图对应一个车位区域的裁剪结果。源码的实现方式是维护一个 Python 字典键是车位号值是该点位在矫正后俯视图中的多边形顶点spots { 0: [(35, 240), (85, 240), (85, 290), (35, 290)], # spot2 1: [(90, 240), (140, 240), (140, 290), (90, 290)], # spot70 # ... }实际标注时通常手动在with_parking.jpg上点选每个车位的四个角点存入字典。之所以用四边形而不是直接用cv2.boundingRect的矩形是因为车位在俯视图里偶尔也不完全垂直于图像轴保留四边形顶点可以精确贴合车位线。3.3 在原始图上标注与在矫正图上标注的取舍我拆这个源码时注意到一个容易踩的细节源码里既有原始视角的with_parking.jpg又有每个车位的独立小图。这说明作者做了两遍坐标映射——先在原始图上框出大致的车位区域透视矫正后再把原始坐标通过 H 矩阵映射到俯视图上以映射后的坐标为准进行切割。# 手选四个点左上、右上、右下、左下原点在左上 src_pts np.float32([[241, 175], [420, 178], [620, 245], [190, 250]]) dst_pts np.float32([[0, 0], [200, 0], [200, 120], [0, 120]]) H, _ cv2.findHomography(src_pts, dst_pts) warped cv2.warpPerspective(img_norm, H, (200, 120))findHomography使用 RANSAC 求解_里放的是内点掩码如果标定点在图中被遮挡掩码对应的内点数量会明显下降。透视矫正的目的是把每个车位的形状固定成标准矩形这样数据增强可以统一使用水平翻转和亮度抖动不用再学斜着的车这种变体。环节原始图坐标矫正图坐标标注人工点选角点通过 H 映射或重新点选优势直观、不容易点错所有车位统一方向劣势斜四边形切割依赖 H 矩阵精度这里有一个工程取舍如果车位分布在一个平面且相机高度足够一次求得全局 H 即可如果车位分布在两个不同高度的平台比如地面层和升降机层需要分别求 H否则高处车位的坐标会系统性偏移。源码场景里是单一平面所以全局 H 是够用的。4. 基于 H5 模型的空占判定理解 car1.h5 的加载与推理边界4.1 H5 文件不是 OpenCV 原生格式加载方式先说清car1.h5是用 Keras 训练并保存的模型权重文件。OpenCV 的cv2.dnn.readNet针对的是 TensorFlow 的 frozen graph.pb或 ONNX直接传入 h5 路径会报Unspecified error。我最初看这份源码时以为作者绕过了这个问题检查代码后发现他用的是 TensorFlow 的 API而不是 OpenCV 的 DNN 模块。import tensorflow as tf from tensorflow.keras.models import load_model model load_model(car1.h5) # 显式编译一次抑制后续 predict 时的警告 model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])代码逻辑load_model会从 h5 文件中还原完整的网络结构和权重。compile即使不训练也可以补上优化器和损失函数的配置否则某些版本在调用predict时会反复刷 warning不影响结果但干扰日志排查。4.2 模型前向推理四维张量形状与二分类输出每个车位小图在进入网络前需要处理成固定尺寸。源码中的 spot 图像是先做完透视矫正再切割所以可以直接用cv2.resize拉伸到模型输入尺寸不需要担心斜边形问题。假设模型要求输入 40×40def predict_spot(model, spot_img): spot_resized cv2.resize(spot_img, (40, 40)) x spot_resized.astype(np.float32) / 255.0 x np.expand_dims(x, axis0) # 增加 batch 维度 pred model.predict(x, verbose0)[0] occupied int(np.argmax(pred)) # 0 空闲1 占用 confidence float(pred[occupied]) return occupied, confidence参数说明astype(np.float32)是为了匹配 Keras 默认的 float 类型避免 CPU 上 float64 转换带来的性能损耗。np.expand_dims把 (40,40,3) 变成 (1,40,40,3)因为模型输入的 batch 维度是固定的。np.argmax(pred)把网络输出从形状 (2,) 的概率向量转成二分类标签。4.3 训练与推理的任务边界为什么分类比检测更稳当前场景的目标不是画出车辆的包围盒而是判断这一个车位里有没有车。这是一个二分类问题而非目标检测。很多同学拿到源码后的第一反应是用 YOLO 重新实现其实在固定视角、固定车位区域的前提下分类方案有三个明显优势维度分类网络目标检测网络标注成本每个车位只需要标签 0/1需要画完整车辆框推理速度小模型毫秒级YOLO 单帧至少几十毫秒抗遮挡性只看 ROI不被相邻车位干扰车体被护栏遮挡时框不稳定具体到工程参数关注两个值分类阈值和 batch 大小。代码默认np.argmax等价于阈值 0.5。实际部署时我会把阈值提到 0.65因为空车位误判成占用的代价显示车位已满顾客开车转圈远大于占用误判成空闲的代价。batch 预测时把多个 spot 图堆叠成 (N, 40, 40, 3) 一次性传给model.predict吞吐量比循环调用提高 4 倍以上。def predict_batch(model, spot_images): batch np.stack([cv2.resize(im, (40, 40)) for im in spot_images]) batch batch.astype(np.float32) / 255.0 proba model.predict(batch, batch_size32, verbose0) return np.argmax(proba, axis1)这里的batch_size32是典型的推理批次大小平衡点批次太大长尾帧延时升高批次太小CPU 矩阵运算的向量化优势发挥不出来。模型输出形状是 (N, 2)axis1取每个样本的空闲/占用概率索引。4.4 输入尺寸与标注一致性检查场景文件里spot70.jpg、spot2.jpg这些图的大小并不完全一致这是标注时的裁剪框大小差异造成的。如果直接 resize 到 40×40车位比例会被拉伸变形。常见做法是先获取基准值如BASE_W, BASE_H 40, 40然后按比例带 padding 地放入画布而不是直接cv2.resizedef letterbox(im, target_w40, target_h40): h, w im.shape[:2] scale min(target_w / w, target_h / h) nw, nh int(w * scale), int(h * scale) resized cv2.resize(im, (nw, nh)) canvas np.full((target_h, target_w, 3), 0, dtypenp.uint8) x0 (target_w - nw) // 2 y0 (target_h - nh) // 2 canvas[y0:y0nh, x0:x0nw] resized return canvasletterbox保留了车位内部结构的宽高比补边部分填 0 即黑色不会引入额外纹理。如果训练时用的是直接 resize 的图推理时也要保持一致否则模型看到的长宽比不同准确率会掉。5. 状态机去抖与遮挡兜底让识别结果不像心电图并验证标定精度5.1 时间序列滤波连续 N 帧确认才翻转状态单帧分类结果直接输出你会发现同一车位在视频流里频繁跳变某帧判定空闲下帧判定占用再下帧又空闲。这通常不是模型问题而是影子或车辆反光在 ROI 边缘引起的高频扰动。工程上要用连续帧一致性来过滤。class SpotState: def __init__(self, history_len5): self.history [] self.history_len history_len self.output 0 def update(self, pred): self.history.append(pred) if len(self.history) self.history_len: self.history.pop(0) if len(self.history) self.history_len: avg sum(self.history) / self.history_len # 只有超过 60% 的历史帧一致时才翻转最终状态 new_state 1 if avg 0.6 else 0 if new_state ! self.output: self.output new_state return self.outputhistory_len5意味着大约 1 秒的决策延迟假设 5fps 处理速度换来的是状态切换的稳定性。avg 0.6是容忍 5 帧里 2 帧发生误判而不改变输出但如果连续 3 帧判定变化则强制翻转。这个机制能过滤掉单帧噪声同时不会像卡尔曼滤波那样引入复杂的协方差调参。5.2 用背景差分兜底分类模型不确定时的第二意见如果分类模型的置信度卡在阈值附近比如概率 0.48 到 0.55 之间往返此时引入背景差分做仲裁。背景图就用with_parking.jpg——它代表空车位的基准外观。将当前帧的车位 ROI 与背景图对应区域做差分统计像素变化的面积比例def validate_by_diff(current_roi, bg_roi, diff_thresh0.15): diff cv2.absdiff(current_roi, bg_roi) gray_diff cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY) _, mask cv2.threshold(gray_diff, 25, 255, cv2.THRESH_BINARY) change_ratio np.count_nonzero(mask) / mask.size return change_ratio diff_threshdiff_thresh0.15的含义是车位区域内超过 15% 的像素与背景差异显著就判定为有车。这个阈值需要根据相机位置微调相机离车位越远车位在画面中占比越小像素级噪声占比越高阈值应相应上调到 0.2 左右。背景差分对静止车辆最有效如果车辆恰好停在车位正中央且颜色接近地面分类网络可能出错但差分图里一定有大量暗色像素块。5.3 坐标精度自检验证透视矫正后的标注是否漂移透视矫正矩阵 H 是在安装相机时标定的但长期运行后立杆可能因风力或地面沉降产生微小位移H 会缓慢失效。一个低成本的自检方法是白线对齐对矫正后的俯视图做形态学开运算提取白色车位线计算线与标注多边形边界的平均距离。def check_calibration(warped_img, spot_polygon): gray cv2.cvtColor(warped_img, cv2.COLOR_BGR2GRAY) _, binary cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) cleaned cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 计算多边形边界上每个像素对应的最近白线距离 mask np.zeros_like(gray) cv2.polylines(mask, [spot_polygon], True, 255, 2) overlap cv2.bitwise_and(cleaned, mask) return cv2.countNonZero(overlap) / cv2.countNonZero(mask)overlap / mask的结果范围是 0 到 1代表多边形边界与真实车位线的重合率。低于 0.4 说明标注点已经偏离实体车位线需要重新标定0.6 以上则可以放心运行。这个比值比肉眼观察截图更客观适合写进例行巡检脚本。5.4 低置信度样本收集把错误喂回训练集最后补一个实战中非常有价值的细节——把预测置信度低于 0.75 的 spot 小图落盘每天结束后人工挑出误判样本追加到训练数据中。因为整个推理管线支持 batch 预测收集阶段不需要额外的推理框架直接复用predict_batch的输出概率proba model.predict(batch, batch_size32, verbose0) conf np.max(proba, axis1) low_conf_idx np.where(conf 0.75)[0] for idx in low_conf_idx: fname flow_conf/{frame_id}_{spot_id}_{conf[idx]:.3f}.jpg cv2.imwrite(fname, spot_images[idx])落盘文件名里带上帧号、车位号、置信度三个信息后续整理数据集时不需要回看原视频。用这些低置信度样本微调模型比单纯增加随机翻转、裁剪等数据增强更有效因为它针对性解决的是这个相机角度下真正难看难分的样本。模型第一次训练可以全用公开数据但部署后的迭代必须引入现场分布。这个闭环才是固定视角车位识别能长期稳定运行的关键。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价