supervision 多边形区域(PolygonZone)检测与计数完全指南:从锚点机制到源码级可视化注解
PolygonZone 与 PolygonZoneAnnotator 是 supervision 检测工具箱(detection/tools)中用于"在画面内划定任意多边形区域并统计进出对象数量"的核心组件,广泛应用于越界人流统计、交通车辆计数、禁入区域告警等场景。本文基于 polygon_zone 官方文档 及其底层源码 polygon_zone.py,系统讲解区域定义、触发(trigger)判断逻辑、多锚点语义、可视化注解参数,并给出可直接运行的单区域/多区域计数实战方案。
一、PolygonZone 是什么:用任意多边形定义"关注区域"
在目标检测流水线中,我们常常只关心画面中某个特定区域内的对象——例如十字路口的车辆、柜台前的人群。PolygonZone 正是为此设计:它接收一个多边形顶点数组,把该多边形栅格化为 2D 掩码(mask),再对一批检测框判断"哪些落在了区域内",并实时维护 current_count 计数。
在 src/supervision/detection/tools/polygon_zone.py 中,该类的文档将其定位为:
A class for defining a polygon-shaped zone within a frame for detecting objects.(用于在帧内定义多边形形状区域、进而检测其中对象的类。)
同时类文档包含一条重要提示:PolygonZone 建议与目标追踪(tracking)流水线配合使用,详见仓库中的 追踪教程。原因在"五、追踪集成"一节展开,这里先给出结论:追踪可为每个对象提供稳定的 tracker_id,避免同一对象在连续帧中被重复计数的语义歧义(官方文档说明标注为 warning,需结合追踪流水线阅读)。
类属性一览
| 属性 | 类型 / 默认值 | 含义 |
|---|---|---|
polygon |
np.ndarray,形状 (N, 2) |
多边形顶点,每行为一个 (x, y) 坐标(构造时会 astype(int) 取整) |
triggering_anchors |
Iterable[Position],默认 (sv.Position.BOTTOM_CENTER,) |
判定检测框是否进入区域时,考察检测框的哪些锚点 |
require_all_anchors |
bool,默认 True |
所有锚点均落入区域内才判为"在内";False 时任一锚点落入即触发 |
current_count |
int,初始 0 |
当前落在区域内的检测对象数量(每次 trigger 后更新) |
mask |
2D bool / uint8 掩码 | 多边形区域栅格化后的掩码,用于点是否落在区域内的快速查询 |
二、三分钟上手:定义区域并触发检测
文档中的最小示例非常直观。首先构造一个四边形顶点数组,实例化 PolygonZone,传入一批 Detections,调用 trigger 得到每个检测是否在区域内的布尔数组:
>>> import numpy as np
>>> import supervision as sv
>>> polygon = np.array([[100, 200], [200, 100], [300, 200], [200, 300]])
>>> polygon_zone = sv.PolygonZone(polygon=polygon)
>>> detections = sv.Detections(
... xyxy=np.array([[180, 100, 220, 200], [400, 400, 450, 500]])
... )
>>> is_detections_in_zone = polygon_zone.trigger(detections)
>>> is_detections_in_zone
array([ True, False])
>>> polygon_zone.current_count
1
两个检测框 [180, 100, 220, 200] 与 [400, 400, 450, 500],前者底部中心落入四边形内部,后者完全在区域外,因此返回 array([True, False]),current_count 更新为 1。
PolygonZone 与 PolygonZoneAnnotator 均从 supervision 顶层包 导出,即使用 sv.PolygonZone、sv.PolygonZoneAnnotator 即可访问,无需深层 import。
三、触发语义核心:锚点(triggering anchors)机制
PolygonZone 的区域判定并非"检测框与多边形求 IoU 交叠"的几何重叠测试,而是基于锚点命中测试。类文档对此有明确说明:
This is anchor-based, not a true geometric box/polygon intersection test: it fires only when a listed anchor point lands inside the mask.(这是基于锚点的判断,而非真正的检测框/多边形相交测试:只有当列出的锚点落在掩码内部时才触发。)
如果你的业务需要"完整重叠"语义(例如整框都在区域内才算),应改用 mask / IoU 方案。
3.1 可用的锚点集合
锚点类型来自 Position 枚举,共 10 种取值:
CENTER、CENTER_LEFT、CENTER_RIGHTTOP_CENTER、TOP_LEFT、TOP_RIGHTBOTTOM_LEFT、BOTTOM_CENTER、BOTTOM_RIGHTCENTER_OF_MASS(需要检测框携带 mask,否则抛ValueError)
默认锚点是 BOTTOM_CENTER——对行人、车辆这类"脚底/接地"对象,用底部中心点判定"是否已走入区域"最为自然。
3.2 多锚点 + 全命中 / 任一命中(require_all_anchors)
triggering_anchors 可传多个锚点,配合 require_all_anchors 可表达两类语义:
require_all_anchors=True(默认):所有锚点都在区域内,才认为该检测在区域内;require_all_anchors=False:任一锚点在区域内即触发;- 当
triggering_anchors只含一个锚点时,require_all_anchors不起作用(没有"多个"可聚合)。
文档中的第二个示例演示了大半框越过区域边界时,用"左上 + 右下"双锚点、任一命中来触发:
>>> polygon = np.array([[0, 0], [100, 0], [100, 100], [0, 100]])
>>> polygon_zone = sv.PolygonZone(
... polygon=polygon,
... triggering_anchors=[sv.Position.TOP_LEFT, sv.Position.BOTTOM_RIGHT],
... require_all_anchors=False,
... )
>>> detections = sv.Detections(xyxy=np.array([[80, 80, 120, 120]]))
>>> polygon_zone.trigger(detections)
array([ True])
检测框 [80, 80, 120, 120] 的左上角 (80, 80) 在 100×100 正方形区域内,右下角 (120, 120) 在区域外;require_all_anchors=False 下左上角命中即可返回 True。对应测试见 test_polygonzone.py,其中覆盖了 require_all_anchors 的多种组合(含"单锚点时该参数不生效"的用例 test_require_all_anchors_has_no_effect_with_single_anchor)。
四、trigger 源码剖析:计数是如何算出来的
trigger(detections) 的实现位于 polygon_zone.py#L95-L132,逻辑可分四步理解:
if len(detections) == 0: # 1) 空检测快速路径
self.current_count = 0
return np.array([], dtype=bool)
all_anchors = np.array([...]) # 2) 逐锚点计算坐标
# in_bounds: 锚点是否落在掩码范围内的布尔矩阵
# anchor_hits = in_bounds & mask[y_safe, x_safe]
# reduce = np.all if require_all_anchors else np.any
# is_in_zone = reduce(anchor_hits, axis=0) # 3) 按锚点轴聚合
# self.current_count = int(np.sum(is_in_zone)) # 4) 更新计数
空检测处理:没有检测时 current_count 直接归零并返回空布尔数组,避免下游统计脏数据。
锚点计算:对每个 triggering_anchors 调用 detections.get_anchors_coordinates(anchors)(实现见 detection/core.py#L2522)。该方法从检测框 xyxy 推导锚点坐标——例如 BOTTOM_CENTER 即 ((x0+x2)/2, y3)。值得注意的两个细节:
- 若
Detections.data中存在xyxyxyxy(ORIENTED_BOX_COORDINATES),锚点会基于旋转框的真实角点计算,落在旋转后的物体本体上,而非轴对齐外包围盒上; Position.CENTER_OF_MASS使用 mask 质心,要求Detections携带 mask,否则抛ValueError。
边界与半像素处理:锚点坐标先经 np.rint(...).astype(int) 就近取整到最近像素(对应测试 test_half_pixel_anchor_uses_nearest_pixel),再做 in_bounds 越界掩蔽,最后对掩码做向量化查表 mask[y_safe, x_safe]。越界锚点一律视为"未命中"(测试 test_out_of_bounds_anchor_excluded),而恰好落在多边形边界线上的锚点按掩码像素规则视为命中(测试 test_anchor_on_polygon_boundary_included)。
聚合与计数:按锚点轴执行 np.all 或 np.any,得到每个检测的 is_in_zone,current_count = int(np.sum(is_in_zone)) 即时更新。
源码注释还强调:锚点取自原始未裁剪的检测框。这样可避免"同一检测因被某个区域裁剪导致锚点坐标偏移、从而被多个互不重叠的区域重复计数"的伪影;当然,两个真正重叠的区域仍可能合法地同时包含同一个检测(测试
test_straddling_detection_assigned_to_one_zone验证了跨界框只归属单一区域)。
构造函数里的工程细节
在 init 中:
self.polygon = polygon.astype(int):顶点强制取整,保证与像素坐标系一致;self.triggering_anchors = list(triggering_anchors):一次物化为列表,从而可安全接收生成器(generator)而不被重复trigger调用耗尽——测试test_generator_triggering_anchors_is_materialized专门守护这一点;- 空锚点列表直接抛
ValueError("Triggering anchors cannot be empty.")(测试test_empty_anchors_raises验证); self.mask = polygon_to_mask(polygon=polygon, resolution_wh=(x_max + 2, y_max + 2)):掩码分辨率取多边形最大坐标 + 2 像素,即恰好能覆盖整个多边形;polygon_to_mask位于 converters.py#L46,底层用cv2.fillPoly把多边形填充为 1、其余为 0。
五、关于追踪(tracker_id)的官方提示与集成建议
polygon_zone.md 通过 :::supervision.detection.tools.polygon_zone.PolygonZone 指令直接渲染源码 docstring,因此类文档中以 warning 形式提示了 tracker 相关内容(指向仓库内 追踪教程)。
需要客观说明的是:从当前源码看,trigger() 本身并未读取 tracker_id,区域判定完全基于每帧检测框的锚点。官方 warning 的真实用意是:当你要统计"穿过区域的对象个数"(例如一辆车跨越多帧应只计一次)而不是"每帧画面中的瞬时数量"时,必须以追踪器提供的稳定 tracker_id 去重——推荐将 PolygonZone 与仓库内置的 ByteTrack(src/supervision/tracker/byte_tracker)配合,构建"检测 → 追踪 → 区域触发 → 按 id 去重"的流水线。若只统计逐帧瞬时在区人数(occupancy),直接 trigger 即可。
六、可视化注解:PolygonZoneAnnotator
PolygonZoneAnnotator 负责把多边形区域、当前计数以可读方式叠加到画面上,官方文档定位:
A class for annotating a polygon-shaped zone within a frame with a count of detected objects.(在帧内以检测对象计数标注多边形区域的类。)
6.1 全部可调参数
| 参数 | 默认值 | 含义 |
|---|---|---|
zone |
— | 待标注的 PolygonZone 实例(必填) |
color |
Color.WHITE(白色) |
多边形边线颜色 |
thickness |
2 |
多边形边线粗细 |
text_color |
Color.BLACK(黑色) |
区域内计数文字颜色 |
text_scale |
0.5 |
计数文字字号 |
text_thickness |
1 |
计数文字笔画粗细 |
text_padding |
10 |
计数文字背景框内边距 |
font |
cv2.FONT_HERSHEY_SIMPLEX |
文字字体 |
display_in_zone_count |
True |
是否在区域内绘制计数字样 |
opacity |
0 |
区域内部填充不透明度(0=仅描边不填充) |
构造时还会计算一次多边形中心 get_polygon_center(见 geometry/utils.py),作为文字默认锚点。
6.2 annotate 内部逻辑
annotate 的绘制分为两个分支(详见 draw/utils.py 中的 draw_polygon 与 draw_filled_polygon):
opacity == 0:仅用cv2.polylines画多边形描边;opacity > 0:先在拷贝帧上cv2.fillPoly填充,再用cv2.addWeighted以给定不透明度与原帧混合(实现见 draw/utils.py#L273),随后叠加描边——注意这会操作scene.copy(),不会原地破坏原帧。
随后若 display_in_zone_count 为真,用多边形中心作为锚点调用 draw_text(draw/utils.py#L315)绘制文字:默认文字内容为 str(self.zone.current_count),也可通过 annotate(scene, label=...) 传入自定义标签(如 "cars: 3")。draw_text 以区域颜色为文字背景色、text_color 为前景色,从而保证任何背景下都清晰可读。
文档给出的最小用法:
>>> import numpy as np
>>> import supervision as sv
>>> polygon = np.array([[100, 200], [200, 100], [300, 200], [200, 300]])
>>> polygon_zone = sv.PolygonZone(polygon=polygon)
>>> zone_annotator = sv.PolygonZoneAnnotator(zone=polygon_zone, thickness=2)
>>> scene = np.zeros((400, 400, 3), dtype=np.uint8)
>>> annotated_scene = zone_annotator.annotate(scene=scene)
>>> annotated_scene.shape
(400, 400, 3)
七、完整实战:视频帧中的车辆区域计数
下面把 PolygonZone 组合进一条"读帧 → 模型推理 → 区域判定 → 可视化 → 写回视频"的完整流水线。该方案与仓库内 count_in_zone 指南 及配套示例 count_people_in_zone 同源。若需示例视频,可用 assets 下载模块 拉取内置素材(如 VideoAssets.VEHICLES_2)。
import numpy as np
import supervision as sv
from supervision.assets import VideoAssets, download_assets
# 1. 准备视频(也可替换为你自己的 mp4 路径)
video_path = download_assets(VideoAssets.VEHICLES_2)
frame_generator = sv.get_video_frames_generator(video_path)
# 2. 划定车道区域(顶点顺序可任意,多边形需自闭合概念上的围合)
polygons = [
np.array([[718, 595], [927, 592], [851, 1062], [42, 1059]]),
np.array([[987, 595], [1199, 595], [1893, 1056], [1015, 1062]]),
]
# 3. 为每个区域创建 Zone 与 Annotator(可指定颜色与填充透明度)
zones = [sv.PolygonZone(polygon=p, triggering_anchors=[sv.Position.CENTER])
for p in polygons]
annotators = [sv.PolygonZoneAnnotator(zone=z, color=sv.Color.ROBOFLOW, opacity=0.2)
for z in zones]
# 4. 推理函数:可用任意返回 sv.Detections 的模型,此处以 RF-DETR 为例示意
def run_inference(frame):
# model = RFDETRMedium(); detections = model.predict(frame)
detections = sv.Detections(xyxy=np.array([[100, 100, 200, 260]])) # 占位
return detections
# 5. 逐帧处理:判定 + 标注 + 打印计数
for frame in frame_generator:
detections = run_inference(frame)
for zone, annotator in zip(zones, annotators):
zone.trigger(detections=detections) # 更新 current_count
frame = annotator.annotate(scene=frame)
sv.show_frame_in_notebook(frame) # 或 cv2.imshow / cv2.imwrite
关键点:必须先 zone.trigger(detections) 再 annotator.annotate(scene=...)——annotate 读取的是 zone.current_count,而它只在 trigger 调用后被刷新。视频场景若希望计数跨帧去重,请将 detections 先经 sv.ByteTrack().update_with_detections(...) 补充 tracker_id,再对每帧按"新出现的 id 是否已在区域内"做增量统计(参见 追踪示例)。
多区域并排展示时,建议为每个区域配不同 color(如 sv.ColorPalette.DEFAULT 循环取色),并配合 BoxAnnotator/LabelAnnotator(annotators)同时画出检测框与类别标签,输出监控大屏效果。
八、行为边界与测试佐证(防止踩坑)
仓库 test_polygonzone.py(共 302 行)对区域判定行为做了详尽锁定,以下边界请务必牢记:
- 空锚点列表直接抛异常——
triggering_anchors=[]在构造时即ValueError; - 锚点不可为生成器反复消费——构造函数内部
list()物化,重复trigger安全; - 半像素坐标就近取整——锚点经
np.rint落到最近整数像素再做掩码查询; - 越界锚点不算命中——坐标落在掩码范围外的锚点直接判否;
- 边界线锚点算命中——落在多边形边界像素上的锚点按掩码语义计为内部;
- 跨界框只归属一个非重叠区域——因为锚点取自原始框而非被某区域裁剪后的框;
require_all_anchors在单锚点下无效果——两种取值行为一致;- 重叠区域可合法重复包含同一检测——这是 anchor 语义下的正常现象,需在业务层自行决定去重策略。
若你的区域是多边形"环"、凹多边形或需同时处理"在线类"(是否越过某条线)语义,可对照阅读 line_zone(其配套 test_line_counter.py)与 polygon 工具 了解同族 API。
结语
PolygonZone + PolygonZoneAnnotator 是 supervision 提供的最直观的"任意形状关注区"解决方案:前者以锚点命中 + 全/任一聚合的方式逐帧维护 current_count,后者负责把区域轮廓与计数干净地渲染到画面上。掌握锚点语义(triggering_anchors)、聚合策略(require_all_anchors)以及"先 trigger 后 annotate"的调用顺序,再配合 ByteTrack 做跨帧去重,即可快速构建车流统计、客流密度、禁入告警等真实应用。深入源码可从 polygon_zone.py 起步,沿 Position 枚举、get_anchors_coordinates、polygon_to_mask 一路追到绘制层,而 count_in_zone 指南 与 示例工程 则提供了从坐标取点(如 PolygonZone Web 工具)到完整代码的最短落地路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00