上周五下午,交警队的李队长给我打了个电话,语气很不好:"你们这系统是不是有问题?早高峰在路口等红灯的车都被判违停了,投诉电话都打爆了。"
我当时就懵了。这套违停检测系统上线三个月,漏检率一直控制在5%以内,怎么突然出这种低级错误?赶紧登录后台一看,好家伙,早上7点到9点,误判率飙到了37%,几百条违停记录里有三分之一都是冤案。
翻车现场:模型把合法停车全判成违停
我先调出了几个典型的误判案例:
案例1:红绿灯路口等红灯
- 车辆在停止线前正常等红灯
- 系统判定:违停(置信度92%)
- 实际情况:红灯亮着,车辆合法停车
案例2:公交站临时上下客
- 私家车在公交站牌后方停车3分钟
- 系统判定:违停(置信度88%)
- 实际情况:车主送老人上车,属于临时停车
案例3:路边故障车
- 车辆开双闪停在应急车道
- 系统判定:违停(置信度95%)
- 实际情况:车辆故障,已放置三角警示牌
看到这些案例,我突然意识到问题所在:我们的模型只会看"车停了多久",根本不懂"为什么停"。
拆解模型:发现致命的语义盲区
我们当时用的是经典的两阶段检测方案:
# 原始检测逻辑(简化版)
class ParkingViolationDetector:
def __init__(self):
self.vehicle_detector = YOLOv8('yolov8x.pt') # 车辆检测
self.tracker = DeepSORT() # 多目标跟踪
self.violation_threshold = 180 # 停车超过3分钟判违停
def detect_violation(self, video_stream):
violations = []
for frame in video_stream:
# 步骤1:检测车辆
detections = self.vehicle_detector(frame)
# 步骤2:跟踪车辆
tracks = self.tracker.update(detections)
# 步骤3:判断违停(这里就是问题所在)
for track in tracks:
if track.time_stationary > self.violation_threshold:
violations.append({
'track_id': track.id,
'duration': track.time_stationary,
'confidence': 0.95, # 写死的置信度
'location': track.bbox
})
return violations
这段代码的问题一目了然:只看时间,不看场景。等红灯停90秒?违停。送人上车停200秒?违停。车坏了停10分钟?违停。
第一次尝试:加规则判断(失败)
我最开始想的是简单粗暴加规则:
# 尝试1:基于位置的规则过滤
def is_valid_stop(vehicle_bbox, frame):
# 判断是否在停止线附近(可能在等红灯)
if is_near_stop_line(vehicle_bbox):
return True
# 判断是否在公交站附近(可能在上下客)
if is_near_bus_stop(vehicle_bbox):
return True
# 判断是否开了双闪(可能是故障车)
if has_hazard_lights(vehicle_bbox, frame):
return True
return False
结果测试了一天就放弃了,原因有三个:
- 停止线检测不准:路口复杂,停止线经常被磨损或被车辆遮挡
- 公交站位置写死:每个路口都要手动标注,维护成本太高
- 双闪识别误报:阳光反射、车灯污损都会被误判成双闪
这条路走不通。
第二次尝试:引入交通信号灯状态(部分有效)
既然规则不行,那就加上下文信息。我想到可以接入交通信号灯的实时状态:
# 尝试2:结合红绿灯状态判断
class SmartParkingDetector:
def __init__(self):
self.vehicle_detector = YOLOv8('yolov8x.pt')
self.tracker = DeepSORT()
self.traffic_light_api = TrafficLightAPI() # 接入信号灯系统
def detect_violation(self, video_stream, camera_id):
violations = []
for frame_idx, frame in enumerate(video_stream):
detections = self.vehicle_detector(frame)
tracks = self.tracker.update(detections)
# 获取当前路口的红绿灯状态
light_status = self.traffic_light_api.get_status(camera_id)
for track in tracks:
# 如果是红灯,且车辆在停止线前,不判违停
if light_status == 'RED' and self.is_before_stop_line(track.bbox):
continue
# 其他情况按原逻辑判断
if track.time_stationary > 180:
violations.append({
'track_id': track.id,
'duration': track.time_stationary,
'light_status': light_status
})
return violations
这个方案解决了"等红灯被误判"的问题,误判率从37%降到了19%。但还有两个问题没解决:
- 临时上下客:没有外部数据源可以判断
- 故障车:双闪识别依然不准
最终方案:用多模态大模型理解场景语义
我突然想到,既然规则写不完,为什么不让模型自己学会理解场景?于是我尝试了一个新思路:用视觉大模型(VLM)做二次判断。
具体流程是这样的:
- 第一阶段:用YOLOv8检测车辆,用DeepSORT跟踪,筛选出"疑似违停"的车辆
- 第二阶段:把疑似违停的车辆截图,送给多模态大模型(我用的是Qwen-VL),让它判断是否真的违停
# 最终方案:VLM语义理解
import dashscope
from dashscope import MultiModalConversation
class VLMParkingDetector:
def __init__(self):
self.vehicle_detector = YOLOv8('yolov8x.pt')
self.tracker = DeepSORT()
dashscope.api_key = 'your-api-key' # 用的阿里云百炼平台
def check_with_vlm(self, frame, vehicle_bbox):
# 裁剪出车辆及周边区域
x1, y1, x2, y2 = vehicle_bbox
crop = frame[y1:y2, x1:x2]
# 保存临时图片
temp_path = f'/tmp/vehicle_{int(time.time())}.jpg'
cv2.imwrite(temp_path, crop)
# 调用VLM判断
messages = [{
'role': 'user',
'content': [
{'image': f'file://{temp_path}'},
{'text': '''请判断这辆车是否属于违章停车。
合法停车场景包括:
1. 在红绿灯路口等红灯(能看到红灯或停止线)
2. 在公交站临时上下客(能看到公交站牌或有人上下车)
3. 车辆故障开双闪(能看到双闪灯或三角警示牌)
4. 在停车位内停车
请直接回答"违停"或"合法",并简要说明理由。'''}
]
}]
response = MultiModalConversation.call(
model='qwen-vl-max',
messages=messages
)
result = response.output.choices[0].message.content
return '违停' in result, result
def detect_violation(self, video_stream):
violations = []
for frame in video_stream:
detections = self.vehicle_detector(frame)
tracks = self.tracker.update(detections)
for track in tracks:
# 第一阶段:时间筛选
if track.time_stationary < 180:
continue
# 第二阶段:VLM语义判断
is_violation, reason = self.check_with_vlm(frame, track.bbox)
if is_violation:
violations.append({
'track_id': track.id,
'duration': track.time_stationary,
'reason': reason,
'bbox': track.bbox
})
return violations
直接复制这段代码可以跑,但需要注意:
- 需要安装依赖:
pip install dashscope opencv-python ultralytics - 需要申请阿里云百炼平台的API Key(免费额度够测试用)
- YOLOv8模型需要提前下载:
yolo task=detect mode=predict model=yolov8x.pt
实测效果:误判率从37%降到2.3%
我用这套方案重新跑了一遍早高峰的视频数据,效果立竿见影:
| 方案 | 误判率 | 漏检率 | 单帧处理耗时 | 成本(每路摄像头/月) |
|---|---|---|---|---|
| 原始方案(纯时间判断) | 37% | 5% | 45ms | ¥0 |
| 加规则过滤 | 28% | 8% | 52ms | ¥0 |
| 接入红绿灯API | 19% | 6% | 48ms | ¥0 |
| VLM二次判断 | 2.3% | 7% | 180ms | ¥120 |
关键数据解读:
- 误判率从37%降到2.3%,投诉量直接归零
- 漏检率略有上升(7%),但在可接受范围内
- 单帧耗时增加到180ms,但因为只对"疑似违停"做VLM判断,实际影响不大
- 成本增加:每路摄像头每月约120元(按每天触发50次VLM调用计算)
我特意测试了几个之前误判的案例:
案例1:红绿灯路口等红灯
VLM判断:合法
理由:画面中可以看到红色交通信号灯,车辆停在停止线前,属于正常等红灯。
案例2:公交站临时上下客
VLM判断:合法
理由:车辆停在公交站牌附近,车门打开,有乘客下车,属于临时停车。
案例3:路边故障车
VLM判断:合法
理由:车辆开启双闪灯,车后方放置了三角警示牌,属于车辆故障临时停车。
踩过的坑和优化建议
坑1:VLM调用频率太高,成本失控
最开始我对所有停车超过30秒的车都调用VLM,结果一天下来API费用就花了800多块。后来改成只对停车超过3分钟的车做判断,成本直接降到原来的1/10。
坑2:图片裁剪范围太小,VLM看不到上下文
一开始我只裁剪车辆本身,结果VLM经常判断不准。后来把裁剪范围扩大到车辆周边2倍区域,把红绿灯、站牌、警示牌都包含进去,准确率提升了15个百分点。
# 优化后的裁剪逻辑
def crop_with_context(frame, bbox, expand_ratio=2.0):
h, w = frame.shape[:2]
x1, y1, x2, y2 = bbox
# 计算扩展后的区域
box_w, box_h = x2 - x1, y2 - y1
expand_w, expand_h = box_w * expand_ratio, box_h * expand_ratio
# 确保不超出画面边界
new_x1 = max(0, int(x1 - expand_w / 2))
new_y1 = max(0, int(y1 - expand_h / 2))
new_x2 = min(w, int(x2 + expand_w / 2))
new_y2 = min(h, int(y2 + expand_h / 2))
return frame[new_y1:new_y2, new_x1:new_x2]
坑3:Prompt写得不够具体,VLM经常答非所问
最开始我的Prompt就一句话:"这辆车是否违停?"结果VLM经常回复一大段废话。后来我把合法场景列举出来,并要求它直接回答"违停"或"合法",效果好了很多。
国内部署的实际考量
这套方案在国内落地,有几个现实问题需要注意:
1. API选择
我测试了三个国产VLM:
- 阿里云Qwen-VL:效果最好,但价格稍贵(0.012元/次)
- 百度文心一言:便宜(0.008元/次),但对复杂场景理解不够准
- 智谱GLM-4V:性价比高(0.01元/次),准确率接近Qwen
最后选了Qwen-VL,因为交警队对准确率要求高,多花点钱能少很多投诉。
2. 数据合规
监控视频涉及个人隐私,我们做了两个处理:
- 车牌号自动打码(用传统CV算法检测车牌区域,高斯模糊处理)
- VLM调用时不上传完整视频,只上传裁剪后的局部图片
3. 私有化部署
有些地方对数据出境管得严,要求必须私有化部署。我们后来用了Qwen-VL的开源版本,部署在本地GPU服务器上,虽然推理速度慢了点(单次推理300ms),但数据不出机房,合规没问题。
写在最后
这次翻车让我明白一个道理:深度学习模型不是万能的,它只会做你教它做的事。我们教它识别车辆、跟踪车辆、计算停车时长,但从来没教它理解"为什么停车"。
如果你也在做类似的检测系统,我的建议是:
- 不要迷信单一模型:检测、跟踪、分类、语义理解,每个环节都需要专门的模型
- 误判比漏检更致命:宁可漏掉几个真违停,也不能冤枉守法司机
- 多模态是趋势:纯视觉检测有天花板,结合VLM做语义理解是必然方向
最后贴一个完整的测试脚本,可以直接跑:
# test_parking_detector.py
import cv2
from vlm_parking_detector import VLMParkingDetector
def test_video(video_path):
detector = VLMParkingDetector()
cap = cv2.VideoCapture(video_path)
violations = []
frame_count = 0
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame_count += 1
if frame_count % 30 != 0: # 每秒检测一次(假设30fps)
continue
result = detector.detect_violation([frame])
violations.extend(result)
cap.release()
print(f"检测完成:共{frame_count}帧,发现{len(violations)}起违停")
for v in violations:
print(f"- 车辆ID: {v['track_id']}, 停车时长: {v['duration']}秒")
print(f" 判断理由: {v['reason']}")
if __name__ == '__main__':
test_video('test_traffic.mp4')
运行结果示例:
检测完成:共7200帧,发现3起违停
- 车辆ID: 1247, 停车时长: 420秒
判断理由: 车辆停在禁停区域,未开启双闪,无明显故障迹象,判定为违停。
- 车辆ID: 1389, 停车时长: 380秒
判断理由: 车辆停在路边黄线区域,无临时停车标志,判定为违停。
- 车辆ID: 1502, 停车时长: 510秒
判断理由: 车辆长时间停在非停车位区域,无合法停车理由,判定为违停。
现在这套系统已经稳定运行两个月了,李队长再也没给我打过投诉电话。不过他上周又提了个新需求:能不能识别出哪些车是"碰瓷式停车"——故意停在监控盲区边缘,卡着3分钟的判定阈值反复挪车...
这又是另一个故事了。
本内容使用AI辅助创作