首页
/ supervision 多边形区域(PolygonZone)检测与计数完全指南:从锚点机制到源码级可视化注解

supervision 多边形区域(PolygonZone)检测与计数完全指南:从锚点机制到源码级可视化注解

2026-09-07 19:50:45作者:滑思眉Philip

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

PolygonZonePolygonZoneAnnotator 均从 supervision 顶层包 导出,即使用 sv.PolygonZonesv.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 种取值:

  • CENTERCENTER_LEFTCENTER_RIGHT
  • TOP_CENTERTOP_LEFTTOP_RIGHT
  • BOTTOM_LEFTBOTTOM_CENTERBOTTOM_RIGHT
  • CENTER_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.allnp.any,得到每个检测的 is_in_zonecurrent_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_polygondraw_filled_polygon):

  • opacity == 0:仅用 cv2.polylines 画多边形描边;
  • opacity > 0:先在拷贝帧上 cv2.fillPoly 填充,再用 cv2.addWeighted 以给定不透明度与原帧混合(实现见 draw/utils.py#L273),随后叠加描边——注意这会操作 scene.copy(),不会原地破坏原帧。

随后若 display_in_zone_count 为真,用多边形中心作为锚点调用 draw_textdraw/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/LabelAnnotatorannotators)同时画出检测框与类别标签,输出监控大屏效果。


八、行为边界与测试佐证(防止踩坑)

仓库 test_polygonzone.py(共 302 行)对区域判定行为做了详尽锁定,以下边界请务必牢记:

  1. 空锚点列表直接抛异常——triggering_anchors=[] 在构造时即 ValueError
  2. 锚点不可为生成器反复消费——构造函数内部 list() 物化,重复 trigger 安全;
  3. 半像素坐标就近取整——锚点经 np.rint 落到最近整数像素再做掩码查询;
  4. 越界锚点不算命中——坐标落在掩码范围外的锚点直接判否;
  5. 边界线锚点算命中——落在多边形边界像素上的锚点按掩码语义计为内部;
  6. 跨界框只归属一个非重叠区域——因为锚点取自原始框而非被某区域裁剪后的框;
  7. require_all_anchors 在单锚点下无效果——两种取值行为一致;
  8. 重叠区域可合法重复包含同一检测——这是 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_coordinatespolygon_to_mask 一路追到绘制层,而 count_in_zone 指南示例工程 则提供了从坐标取点(如 PolygonZone Web 工具)到完整代码的最短落地路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
916
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388