区域监控是计算机视觉中的一个热门应用,它通过监控一段时间内进入和离开特定区域的实体数量来实现。常见的区域监控应用场景包括建筑工地安全、病人监控和禁区控制。
这个区域,通常被称为感兴趣区域(ROI),通常由用户设置为一个静态的多边形形状,覆盖摄像机捕捉到的场景的一部分。虽然这种方法在固定位置的摄像机上效果良好,但在感兴趣区域或摄像机本身移动的情况下,如自动驾驶车辆或机器人应用中,可能就不太适用。
在本文中,我们考虑专用车道监控的问题。城市中通常有为自行车、公交车和拼车车辆等类型车辆指定的车道。然而,执法限制常常导致错误车辆使用或阻塞这些车道。为了解决这个问题,我们将开发一个可以在车辆仪表盘摄像头上使用的专用车道监控应用。
训练模型
为了检测专用车道,我们使用Roboflow训练一个自定义分割模型。Roboflow是一个在线平台,帮助用户快速创建、训练和部署计算机视觉模型。他们的入门教程是学习平台关键功能的好起点。
与生成边界框预测的目标检测模型不同,分割模型预测目标物体的精确像素边界,提供关于其形状和大小的更丰富信息。除了物理物体外,它们在检测空间区域(如专用车道)方面也非常有效。
将我们的数据集上传到新的Roboflow项目后,我们使用智能多边形工具来标注专用车道。
我们创建第二个Roboflow项目,这次选择“目标检测”作为项目类型。这个模型将检测车辆。使用边界框工具上传并标注图像数据集。
一旦两个数据集都被标注并分割为训练/验证/测试集,就可以使用Roboflow训练和托管模型。
过滤检测结果
除了我们的计算机视觉模型外,我们还需要编写程序逻辑来过滤检测结果,只包括在专用车道内行驶的车辆。让我们用一个简单的Python脚本来实现这一点。
下面的代码将我们在Roboflow上托管的模型下载到本地机器,并使用它们对输入图像进行推理。
from inference import get_model
import cv2
# load hosted models from Roboflow
vehicle_model = get_model("sg-vehicles/1")
lane_model = get_model("sg-bus-lanes/1")
# process an image
image = cv2.imread("/your/image.png")
vehicle_results = vehicle_model.infer(image)[0]
lane_results = lane_model.infer(image)[0]
我们使用Supervision库来可视化我们的预测。
import supervision as sv
# convert predictions into supervision detection objects
vehicle_detections = sv.Detections.from_inference(vehicle_results)
lane_detections = sv.Detections.from_inference(lane_results)
# initialize annotators
bounding_box_annotator = sv.BoxAnnotator()
mask_annotator = sv.MaskAnnotator()
label_annotator = sv.LabelAnnotator()
# annotate image
image = bounding_box_annotator.annotate(scene=image, detections=vehicle_detections)
image = label_annotator.annotate(scene=image, detections=vehicle_detections)
image = mask_annotator.annotate(scene=image, detections=lane_detections)
image = label_annotator.annotate(scene=image, detections=lane_detections)
# show image
cv2.imshow("output", image)
如上图所示,虽然总共检测到四辆车,但实际上只有一辆车在专用车道内行驶。过滤不需要的车辆的一种方法是只考虑其边界框与专用车道分割掩码相交的检测结果。我们使用以下函数来实现这一点,该函数简单地检查掩码的任何像素是否位于给定窗口内:
import numpy as np
def filter_detection_by_mask(detection, mask):
num_objects = len(detection)
filter_mask = np.empty(num_objects, dtype='bool')
for idx in range(num_objects):
bbox = detection.xyxy[idx].astype(int)
filter_mask[idx] = np.any(mask[bbox[1]:bbox[3], bbox[0]:bbox[2]])
return detection[filter_mask]
# filter detections by mask
lane_mask = lane_detections.mask
lane_mask = np.any(lane_mask, axis=0)
vehicle_detections = filter_detection_by_mask(vehicle_detections, lane_mask)
可视化过滤后的检测结果,我们观察到只有白色货车保留下来。
跟踪区域入侵
为了测量每辆错误车辆在专用车道内行驶的时间长度,我们需要跟踪其在各帧中的位置。我们使用ByteTrack来实现这一点,ByteTrack是一种多目标跟踪算法,它在后续帧之间匹配目标检测并为每个检测分配一个唯一的跟踪ID。
我们定义以下函数,该函数接受过滤后的检测结果、一个ByteTrack实例和一个记录唯一检测结果出现帧数的Python字典。在一定帧数后未被检测到的跟踪对象将被移除。
import supervision as sv
# ByteTrack
tracker = sv.ByteTrack()
# dict of python tuples
# [0: number of detections, 1: frames since last detection]
tracked_objects = {}
def track_detections(detections, tracker, tracked_objects):
detections = tracker.update_with_detections(detections)
# remove old detections
max_misses = fps
expired_ids = []
for id in tracked_objects:
tracked_objects[id][1] += 1
if tracked_objects[id][1] > max_misses:
expired_ids.append(id)
for id in expired_ids:
tracked_objects.pop(id)
# add or modify based on current detections
for id in list(detections.tracker_id):
if id in tracked_objects:
tracked_objects[id][0] += 1
tracked_objects[id][1] = 0
else:
tracked_objects[id] = [1, 0]
return detections
vehicle_detections = track_detections(vehicle_detections)
我们修改我们的标注脚本,以打印专用车道内每辆车的跟踪ID和持续时间。总时间可以通过将车辆被跟踪的帧数乘以视频帧率(FPS)来计算。
使用Roboflow管道进行视频推理
现在我们的代码可以在图像上运行了,让我们修改它以处理视频文件。我们首先将代码转换为一个单一的Python类。
class LaneIntrusionModel:
def __init__(self):
self.vehicle_model = get_model("sg-vehicles/1")
self.lane_model = get_model("sg-bus-lanes/1")
self.tracker = sv.ByteTrack()
self.tracker.reset()
self.tracked_objects = {}
self.fps = 30
self.bounding_box_annotator = sv.BoxAnnotator()
self.mask_annotator = sv.MaskAnnotator()
self.label_annotator = sv.LabelAnnotator()
# public methods
def infer(self, image):
vehicle_results = self.vehicle_model.infer(image)
lane_results = self.lane_model.infer(image)
return list(zip(vehicle_results, lane_results))
def process_inference(self, image, vehicle_results, lane_results):
vehicle_detections, lane_detections = self.get_detections(image, vehicle_results, lane_results)
vehicle_detections = self.track_detections(vehicle_detections)
annotated_image = self.annotate_frame(image, vehicle_detections, lane_detections)
return annotated_image
## private methods
def get_detections(self, image, vehicle_results, lane_results):
vehicle_detections = sv.Detections.from_inference(vehicle_results)
lane_detections = sv.Detections.from_inference(lane_results)
lane_mask = lane_detections.mask
if lane_mask is None or lane_mask.shape[0] == 0:
return vehicle_detections[vehicle_detections.class_id == -1], lane_detections
lane_mask = np.any(lane_mask, axis=0)
vehicle_detections = filter_detection_by_mask(vehicle_detections, lane_mask)
return vehicle_detections, lane_detections
def track_detections(self, detections):
detections = self.tracker.update_with_detections(detections)
# remove old detections
max_misses = self.fps
expired_ids = []
for id in self.tracked_objects:
self.tracked_objects[id][1] += 1
if self.tracked_objects[id][1] > max_misses:
expired_ids.append(id)
for id in expired_ids:
self.tracked_objects.pop(id)
# add or modify based on current detections
for id in list(detections.tracker_id):
if id in self.tracked_objects:
self.tracked_objects[id][0] += 1
self.tracked_objects[id][1] = 0
else:
self.tracked_objects[id] = [1, 0]
return detections
def annotate_frame(self, image, vehicle_detections, lane_detections):
if len(vehicle_detections):
labels = []
for id in list(vehicle_detections.tracker_id):
time_tracked = self.tracked_objects[id][0]/self.fps
labels.append(f"vehicle:#{id} time:{time_tracked:.2f}s")
image = self.bounding_box_annotator.annotate(scene=image, detections=vehicle_detections)
image = self.label_annotator.annotate(scene=image, detections=vehicle_detections, labels=labels)
if len(lane_detections):
image = self.mask_annotator.annotate(scene=image, detections=lane_detections)
image = self.label_annotator.annotate(scene=image, detections=lane_detections)
return image
由于深度学习模型的计算需求,实时处理可能具有挑战性,通常需要专门的硬件(如GPU)。Roboflow提供了InferencePipeline工具包(https://inference.roboflow.com/using_inference/inference_pipeline/),通过在单独的线程上运行预处理、推理和后处理,并支持批量推理,来提高处理时间。我们首先定义以下回调函数:
from inference.core.interfaces.camera.entities import VideoFrame
from inference import InferencePipeline
from typing import Union, List, Optional, Any
model = LaneIntrusionModel()
def on_video_frame(video_frames: List[VideoFrame]) -> List[Any]:
images = [v.image for v in video_frames]
return model.infer(images)
def on_prediction(
predictions: Union[dict, List[Optional[dict]]],
video_frame: Union[VideoFrame, List[Optional[VideoFrame]]]
) -> None:
if not issubclass(type(predictions), list):
# this is required to support both sequential and batch processing with single code
# if you use only one mode - you may create function that handles with only one type
# of input
predictions = [predictions]
video_frame = [video_frame]
for result, frame in zip(predictions, video_frame):
annotated_image = model.process_inference(frame.image, result[0], result[1])
cv2.imshow("output", annotated_image)
cv2.waitKey(1)
然后,我们使用以下脚本在视频文件上运行管道:
pipeline = InferencePipeline.init_with_custom_logic(
max_fps=out_fps,
video_reference="/your/video.mp4",
on_video_frame=on_video_frame,
on_prediction=on_prediction,
)
# start the pipeline
pipeline.start()
# wait for the pipeline to finish
pipeline.join()
得到以下结果:
结论
总之,本教程展示了如何使用Roboflow Inference实现实时区域监控。分割模型允许动态检测图像场景中的感兴趣区域,从而在持续变化的环境中实现区域监控。
除了专用车道监控外,动态区域监控的其他应用场景可能包括:
- 送货无人机扫描着陆区的包裹
- 闭路电视摄像机监控高低潮之间的海岸线游泳者
实时视频处理的限制通常将这些应用中的模型类型限制为快速、单阶段的检测器,如YOLO和SSD,这些模型需要强大的训练数据才能有效。未来的ML优化和硬件工作可能需要将动态区域监控能力引入零样本检测模型。