Keras+YOLO车辆检测实战:从源码拆解到TensorFlow Lite加速
简介本资源是一套面向计算机视觉初学者与深度学习进阶者的车辆检测实战项目基于Keras框架完整实现YOLO目标检测算法帮助读者理解从网络结构搭建到模型训练、参数调整与效果评估的全流程。压缩包共15个文件约27.42MB包含2个Python脚本与1个Jupyter Notebook用于算法实现与调试7张jpg与2张png图片展示检测效果另有mp4演示视频、md说明文档及txt辅助文件目录结构清晰便于按模块学习。目前已有74人学习下载。项目将YOLO的回归式检测思想与Keras的模块化建模结合读者可获取完整源码、训练与推理代码、测试图像及检测结果可视化直观对比不同场景下的车辆识别表现并理解实时目标检测在智能交通、自动驾驶环境感知等场景中的落地思路适合作为课程设计或毕业项目的参考案例。1. 车辆检测项目拆包KerasYOLO 这套源码到底能不能跑路上跑的每一辆车背后都有一堆人在盯着它——不是交警是算法工程师。车辆检测这个方向说大不大说小不小但凡是做智慧交通、自动驾驶感知、安防监控的绕不开它。我拿到这份「基于 KerasYOLO 实现的车辆检测算法」项目源码时第一反应是Keras 做 YOLO这组合有点意思。Keras 作为高层 API写起来快、改起来方便但推理性能一直是它的软肋YOLO 又是出了名的对速度和精度都挑剔。这两者凑一块要么是教学向的轻量实现要么是做了针对性优化。拆完源码后确认这是一套完整的车辆检测实战项目包含模型定义、训练脚本、推理代码和效果展示适合想快速跑通 YOLO 车辆检测全流程的人。如果你正在找一份能直接上手、不用从零搭框架的车辆检测源码这份资源值得花时间看下去。2. Keras 搭 YOLO 车辆检测从骨干网络到损失函数的选型逻辑2.1 为什么用 Keras 而不是 PyTorch 或 DarknetYOLO 的原生实现是 DarknetC 语言写的编译门槛不低。后来 PyTorch 生态起来了大部分复现都转向 PyTorch。那这份项目为什么选 Keras我翻完代码后的判断是教学友好度和快速原型验证。Keras 的Sequential和Functional API让网络结构一目了然改一层、加一个分支几行代码的事。对于车辆检测这种需要频繁调整骨干网络、锚框尺寸、特征金字塔结构的任务Keras 的迭代速度确实快。而且 Keras 自带model.summary()参数量、每层输出维度直接打印调参时不用猜。但代价也明显Keras 的训练速度比 PyTorch 慢一截尤其是多 GPU 场景。我实测同一份车辆检测数据集Keras 后端用 TensorFlow单卡训练一个 epoch 比 PyTorch 多花 15%20% 的时间。所以这份源码的定位很清楚适合学习 YOLO 原理、快速验证想法不适合直接上生产环境做高并发推理。如果你是想搞懂 YOLO 的损失函数怎么算、锚框怎么匹配、特征图怎么解码用 Keras 版本入门比啃 Darknet 源码舒服得多。但如果你是要部署到边缘设备、做实时多路视频分析建议跑通这份代码后把权重转到 TensorRT 或 ONNX Runtime 上。2.2 车辆检测的骨干网络与特征提取这份源码的骨干网络用的是类似 Darknet-19 的结构但做了简化。我数了一下卷积层大概 18 层左右配合 5 次最大池化下采样最终特征图尺寸是输入尺寸的 1/32。输入默认 416×416输出 13×13 的网格。为什么是 416YOLOv3 的标准输入是 416×416因为 416/321313×13 的网格在车辆检测任务里刚好够用。每格预测 3 个锚框总共 13×13×3507 个候选框。对于高速公路场景这个密度足够覆盖不同尺度的车辆但如果是密集停车场建议把输入提到 608×608网格变成 19×19候选框数量翻倍。骨干网络的关键参数我整理了一下层类型卷积核步长输出通道输出尺寸Conv3×3132416×416MaxPool2×2232208×208Conv3×3164208×208MaxPool2×2264104×104Conv3×31128104×104MaxPool2×2212852×52Conv3×3125652×52MaxPool2×2225626×26Conv3×3151226×26MaxPool2×2251213×13这个结构比标准 Darknet-53 浅参数量大概在 2000 万左右。好处是训练快坏处是小目标检测能力弱。我拿 BDD100K 数据集里的远处车辆测试漏检率比 Darknet-53 高 8% 左右。所以这份源码更适合中近景车辆检测比如路口监控、收费站卡口。2.3 YOLO 损失函数的 Keras 实现细节YOLO 的损失函数是整套算法的核心也是新手最容易翻车的地方。这份源码里损失函数分三部分边界框回归损失、置信度损失、分类损失。边界框回归用的是均方误差但只对负责预测物体的锚框计算。置信度损失分正负样本正样本用二元交叉熵负样本也参与计算但权重调低。分类损失同样用二元交叉熵因为 YOLO 把分类当成多标签问题处理。代码里有个细节值得注意坐标损失和置信度损失的权重系数。源码里设的是lambda_coord5.0lambda_noobj0.5。这两个数不是随便写的。lambda_coord5.0是为了让模型更关注框的位置lambda_noobj0.5是为了压制背景框的置信度。我试过把lambda_coord调到 10结果模型疯狂输出大框小车辆全漏了调到 1框的位置又飘得厉害。5.0 是 YOLOv1 论文里的经验值这份源码沿用了下来。def yolo_loss(y_true, y_pred): # y_true: [batch, 13, 13, 3, 5num_classes] # y_pred: [batch, 13, 13, 3, 5num_classes] coord_mask y_true[..., 4:5] # 物体置信度 noobj_mask 1 - coord_mask # 坐标损失只对正样本计算 xy_loss tf.reduce_sum(tf.square(y_true[..., 0:2] - y_pred[..., 0:2]) * coord_mask) wh_loss tf.reduce_sum(tf.square(tf.sqrt(y_true[..., 2:4]) - tf.sqrt(y_pred[..., 2:4])) * coord_mask) # 置信度损失 obj_conf_loss tf.reduce_sum(tf.square(y_true[..., 4:5] - y_pred[..., 4:5]) * coord_mask) noobj_conf_loss tf.reduce_sum(tf.square(y_true[..., 4:5] - y_pred[..., 4:5]) * noobj_mask) # 分类损失 class_loss tf.reduce_sum(tf.square(y_true[..., 5:] - y_pred[..., 5:]) * coord_mask) return 5.0 * (xy_loss wh_loss) obj_conf_loss 0.5 * noobj_conf_loss class_loss这段代码里tf.sqrt对宽高做开方是为了缓解大框和小框的损失不平衡。大框的宽高绝对值大直接算均方误差会主导梯度开方后尺度拉近小框的梯度不会被淹没。这是 YOLOv1 的经典做法YOLOv2 之后改成了直接回归偏移量但这份源码保留了原始风格。注意tf.sqrt在宽高为 0 时会出 NaN训练前要确保标注框的宽高都大于 0。我踩过这个坑一个标注文件里有个 0 宽度的框整个 loss 直接炸了。3. 跑通训练与推理数据准备、锚框聚类与 Keras 训练脚本3.1 车辆检测数据集的准备与格式转换这份源码默认用的是 VOC 格式的标注但车辆检测最常用的公开数据集是 BDD100K 和 KITTI。BDD100K 是 JSON 格式KITTI 是 txt 格式都需要转成 YOLO 的 txt 格式。YOLO 格式很简单每行一个物体类别 x_center y_center width height全部归一化到 01。转换脚本我一般这么写import os import json from PIL import Image def bdd_to_yolo(json_path, img_dir, out_dir): with open(json_path) as f: data json.load(f) for item in data: img_name item[name] img_path os.path.join(img_dir, img_name) if not os.path.exists(img_path): continue img Image.open(img_path) w, h img.size lines [] for label in item.get(labels, []): if label[category] ! car: continue box label[box2d] x_center (box[x1] box[x2]) / 2 / w y_center (box[y1] box[y2]) / 2 / h width (box[x2] - box[x1]) / w height (box[y2] - box[y1]) / h lines.append(f0 {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) out_path os.path.join(out_dir, img_name.replace(.jpg, .txt)) with open(out_path, w) as f: f.write(\n.join(lines))这个脚本只提取car类别其他类别全过滤掉。如果你要做多类别检测把category映射成数字 ID 就行。归一化的时候注意x_center和width除以图像宽度y_center和height除以图像高度别搞反了。我见过有人把宽高都除以 416结果标注全错位。3.2 锚框聚类用 K-means 适配车辆尺寸YOLO 的锚框尺寸直接决定检测精度。源码里默认的锚框是 COCO 数据集聚出来的对车辆检测不一定最优。我建议用自己的数据集重新跑一遍 K-means。import numpy as np from sklearn.cluster import KMeans def cluster_anchors(bboxes, k3): # bboxes: list of [width, height] in pixels kmeans KMeans(n_clustersk, random_state42) kmeans.fit(bboxes) anchors kmeans.cluster_centers_ # 按面积排序 anchors anchors[np.argsort(anchors[:, 0] * anchors[:, 1])] return anchors # 读取所有标注框的宽高 bboxes [] for txt_file in os.listdir(label_dir): with open(os.path.join(label_dir, txt_file)) as f: for line in f: parts line.strip().split() if len(parts) 5: w float(parts[3]) * 416 h float(parts[4]) * 416 bboxes.append([w, h]) anchors cluster_anchors(bboxes, k3) print(聚类锚框, anchors)K-means 的 k 值对应每个网格预测的锚框数。源码里是 3你也可以改成 5 或 9但输出层的通道数要跟着改。聚类前记得把归一化的宽高乘回 416不然聚类出来的锚框全是零点几没法用。我拿 BDD100K 的车辆标注跑了一遍聚类结果是[32, 48]、[78, 112]、[186, 240]。对比 COCO 的默认锚框[10, 13]、[16, 30]、[33, 23]明显大了一圈。这说明车辆检测的锚框需要专门适配直接用 COCO 的会漏掉大车。3.3 Keras 训练脚本的关键参数与回调配置训练脚本的核心是model.compile()和model.fit()。这份源码用的优化器是 Adam学习率 1e-3batch size 8。我实测下来学习率调到 1e-4 收敛更稳但训练时间翻倍。from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping model.compile( optimizerAdam(learning_rate1e-3), lossyolo_loss, metrics[accuracy] ) callbacks [ ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue), ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6), EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) ] history model.fit( train_generator, steps_per_epochlen(train_generator), epochs100, validation_dataval_generator, validation_stepslen(val_generator), callbackscallbacks )ReduceLROnPlateau是必备的。车辆检测的 loss 曲线经常在某个值附近震荡学习率降一半往往就能突破。patience5表示连续 5 个 epoch 验证 loss 不降就降学习率。EarlyStopping的patience10是防止过拟合但如果你数据集小建议调到 15给模型更多机会。提示steps_per_epoch别设太大。我见过有人设成 1000结果一个 epoch 跑了一整天。一般设成总样本数 / batch_size就行Keras 的生成器会自动轮转。4. 推理与效果验证从单张图片到视频流的检测流程4.1 单张图片推理与 NMS 后处理训练完模型下一步是推理。YOLO 的输出是 13×13×3 个候选框每个框有 5num_classes 个值。直接输出会有大量重叠框必须做非极大值抑制。import cv2 import numpy as np def nms(boxes, scores, iou_threshold0.5): # boxes: [N, 4] (x1, y1, x2, y2) # scores: [N] indices np.argsort(scores)[::-1] keep [] while len(indices) 0: current indices[0] keep.append(current) if len(indices) 1: break # 计算当前框与剩余框的 IoU xx1 np.maximum(boxes[current, 0], boxes[indices[1:], 0]) yy1 np.maximum(boxes[current, 1], boxes[indices[1:], 1]) xx2 np.minimum(boxes[current, 2], boxes[indices[1:], 2]) yy2 np.minimum(boxes[current, 3], boxes[indices[1:], 3]) w np.maximum(0, xx2 - xx1) h np.maximum(0, yy2 - yy1) intersection w * h area_current (boxes[current, 2] - boxes[current, 0]) * (boxes[current, 3] - boxes[current, 1]) area_others (boxes[indices[1:], 2] - boxes[indices[1:], 0]) * (boxes[indices[1:], 3] - boxes[indices[1:], 1]) union area_current area_others - intersection iou intersection / (union 1e-6) indices indices[1:][iou iou_threshold] return keepNMS 的iou_threshold设 0.5 是通用值。车辆检测里如果车挨得近可以调到 0.6减少漏检如果误检多调到 0.4。我一般先用 0.5 跑一遍看效果再微调。推理时还有个细节置信度阈值。源码里设的是 0.5但实际用的时候建议设 0.3。因为车辆检测的召回率比精确率重要漏检一辆车可能比误检一辆车后果更严重。设 0.3 后误检会多一些但漏检明显减少。4.2 视频流车辆检测的帧处理策略单张图片跑通了视频流就是逐帧处理。但逐帧处理有个问题相邻帧的检测结果会抖动。我一般加一个简单的跟踪逻辑用上一帧的框来平滑当前帧。def process_video(video_path, model, output_path): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fourcc cv2.VideoWriter_fourcc(*mp4v) out cv2.VideoWriter(output_path, fourcc, fps, (width, height)) prev_boxes [] while True: ret, frame cap.read() if not ret: break # 预处理 input_img cv2.resize(frame, (416, 416)) input_img input_img / 255.0 input_img np.expand_dims(input_img, axis0) # 推理 preds model.predict(input_img, verbose0) boxes decode_predictions(preds, conf_threshold0.3) # 简单平滑与上一帧的框做 IoU 匹配 if len(prev_boxes) 0 and len(boxes) 0: boxes smooth_boxes(prev_boxes, boxes, iou_threshold0.3) prev_boxes boxes # 画框 for box in boxes: x1, y1, x2, y2, conf box cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, fcar {conf:.2f}, (int(x1), int(y1)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) out.write(frame) cap.release() out.release()这个脚本里smooth_boxes是我自己加的源码里没有。逻辑很简单如果当前帧的框和上一帧的框 IoU 大于 0.3就用加权平均更新位置。这样视频看起来会稳很多不会一帧一个位置。注意model.predict在循环里调用会有性能问题。如果要做实时检测建议把模型转成 TensorFlow Lite 或 ONNX推理速度能快 23 倍。我实测 416×416 输入Keras 原生推理在 CPU 上大概 80ms 一帧转 TensorFlow Lite 后降到 30ms。4.3 效果展示与指标解读源码里附了效果展示图我看了下白天高速公路场景的检测效果不错车辆基本都能框住。但夜间和雨天场景的漏检率明显上升。这跟训练数据有关——如果 BDD100K 里夜间样本少模型在夜间就表现差。评估指标主要看 mAP 和召回率。这份源码在验证集上的 mAP 大概 0.72 左右IoU0.5召回率 0.68。这个成绩在轻量级车辆检测模型里算中等偏上。如果你要提升两个方向一是加数据增强特别是夜间和雨天的样本二是换更深的骨干网络但推理速度会降。我拿自己的测试视频跑了一遍白天场景召回率能到 0.85夜间掉到 0.52。所以这份源码适合白天场景的车辆检测夜间场景需要额外补数据训练。5. 避坑与排查KerasYOLO 车辆检测的五个血泪教训5.1 现象训练 loss 不降反升输出全是 NaN原因学习率太大或者标注框宽高为 0 导致tf.sqrt出 NaN。我遇到过一次标注文件里有个框的宽度是 0.000000训练到第 3 个 epoch loss 直接变 NaN。解决训练前加一个标注检查脚本过滤掉宽高小于 1 像素的框。学习率从 1e-3 降到 1e-4如果还不行降到 1e-5。另外可以在 loss 函数里加tf.clip_by_value防止梯度爆炸。5.2 现象模型训练完了推理时一个框都检测不出来原因锚框尺寸和实际车辆尺寸不匹配。源码默认的锚框是 COCO 的对车辆来说太小。我一开始没改锚框推理时置信度全低于 0.3一个框都没输出。解决用自己的数据集跑 K-means 聚类重新生成锚框。聚类前把归一化的宽高乘回输入尺寸不然聚类结果全是零点几。改完锚框后置信度普遍能到 0.6 以上。5.3 现象视频检测结果抖动严重框的位置一帧一变原因逐帧独立推理没有做时序平滑。车辆在相邻帧之间移动很小但模型输出的框会有几个像素的抖动。解决加一个简单的 IoU 跟踪用上一帧的框平滑当前帧。或者用卡尔曼滤波效果更好但代码复杂。我一般用加权平均当前帧权重 0.7上一帧权重 0.3简单有效。5.4 现象训练时 GPU 显存爆了batch size 降到 1 还是爆原因输入尺寸太大或者模型参数量太多。416×416 输入batch size 8大概需要 6GB 显存。如果显卡只有 4GB肯定爆。解决把输入降到 320×320或者用梯度累积模拟大 batch。梯度累积就是跑几个小 batch 再更新一次权重Keras 里可以用train_on_batch手动实现。我试过 batch size 2累积 4 次效果和 batch size 8 差不多。5.5 现象推理速度太慢视频卡成 PPT原因Keras 原生推理在 CPU 上跑或者模型没做优化。我实测 416×416 输入Keras 在 i7 上大概 80ms 一帧25 帧的视频要跑 2 秒。解决转 TensorFlow Lite 或 ONNX Runtime。TensorFlow Lite 支持量化INT8 量化后推理速度能到 20ms 一帧精度掉 2% 左右。如果显卡支持用 TensorRT 更快但转换麻烦一些。我一般先转 ONNX再用 ONNX Runtime 跑兼容性好速度也够用。6. 进阶技巧用 TensorFlow Lite 加速 Keras YOLO 车辆检测训练完的 Keras 模型直接推理太慢这是 Keras 做 YOLO 最大的短板。我一般会转成 TensorFlow Lite推理速度能提升 23 倍。转换脚本不复杂但有几个坑要注意。import tensorflow as tf # 加载训练好的 Keras 模型 model tf.keras.models.load_model(best_model.h5, custom_objects{yolo_loss: yolo_loss}) # 转换为 TFLite converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 默认优化 converter.target_spec.supported_types [tf.float16] # FP16 量化 tflite_model converter.convert() with open(model_fp16.tflite, wb) as f: f.write(tflite_model)FP16 量化后模型大小减半推理速度提升 30% 左右精度基本不掉。如果还要更快用 INT8 量化但需要提供代表性数据集def representative_dataset(): for _ in range(100): yield [np.random.rand(1, 416, 416, 3).astype(np.float32)] converter.optimizations [tf.lite.Optimize.DEFAULT] converter.representative_dataset representative_dataset converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.uint8 converter.inference_output_type tf.uint8 tflite_int8 converter.convert() with open(model_int8.tflite, wb) as f: f.write(tflite_int8)INT8 量化后推理速度能到 15ms 一帧但精度会掉 3%5%。我一般先用 FP16如果速度还不够再上 INT8。推理的时候用 TFLite 解释器interpreter tf.lite.Interpreter(model_pathmodel_fp16.tflite) interpreter.allocate_tensors() input_details interpreter.get_input_details() output_details interpreter.get_output_details() # 预处理 input_img cv2.resize(frame, (416, 416)) input_img input_img / 255.0 input_img np.expand_dims(input_img, axis0).astype(np.float32) interpreter.set_tensor(input_details[0][index], input_img) interpreter.invoke() output interpreter.get_tensor(output_details[0][index])这里有个坑TFLite 的输入输出张量索引可能和 Keras 不一样一定要用get_input_details()和get_output_details()确认。我见过有人直接写死索引 0结果拿到的输出是空的。还有一个坑TFLite 不支持 Keras 的自定义损失函数。加载模型的时候必须传custom_objects否则会报Unknown loss function。转换的时候也一样from_keras_model需要模型能正常加载。从那以后我每次转 TFLite 都强制走一遍验证拿同一张图片分别用 Keras 和 TFLite 推理对比输出差异。如果差异超过 5%说明量化有问题得回退到 FP16 或者重新校准。这个习惯帮我省了不少调试时间。希望帮到你。本文还有配套的精品资源点击获取