How to Perform Time-in-Zone Analytics with PolygonZone and LineZone
You can measure how long objects stay inside polygon regions or track line crossings by combining Supervision's PolygonZone and LineZone classes with a tracker like ByteTrack and timer utilities to calculate per-object dwell times.
Time-in-zone analytics measure object persistence in defined areas or duration to cross boundaries in video streams. The roboflow/supervision library provides specialized zone detection tools in src/supervision/detection/tools/polygon_zone.py and src/supervision/detection/line_zone.py that integrate with tracking and timing utilities to enable precise per-object analytics.
Core Components for Zone Analytics
PolygonZone
The PolygonZone class, implemented in src/supervision/detection/tools/polygon_zone.py, defines a closed polygon region and tests whether detection anchor points fall inside it. The trigger() method accepts a Detections object and returns a boolean mask indicating which detections are currently inside the zone. You can customize the anchor point via the triggering_anchors parameter, using values like sv.Position.BOTTOM_CENTER (default) or sv.Position.CENTER.
LineZone
The LineZone class, found in src/supervision/detection/line_zone.py, represents a directed line segment between two sv.Point coordinates. Its trigger() method returns two boolean masks—crossed_in and crossed_out—indicating objects that moved across the line in either direction. The class automatically maintains in_count, out_count, and per-class counters as attributes.
Trackers and Timers
Per-object timing requires stable identifiers across frames. The sv.ByteTrack class assigns a persistent tracker_id to each detection via update_with_detections(). To convert frame counts into seconds, Supervision provides timer utilities in examples/time_in_zone/utils/timers.py:
FPSBasedTimer: Calculates duration using the video's frame rate (constant FPS).ClockBasedTimer: Calculates duration using system clock time (variable frame rates).
Measuring Polygon Zone Dwell Time
To calculate how long each object stays inside a polygonal region, create a zone for each region and a corresponding timer. Trigger the zone to filter detections, then pass those detections to the timer's tick() method.
import cv2
import numpy as np
import supervision as sv
from ultralytics import YOLO
from utils.general import load_zones_config, find_in_list
from utils.timers import FPSBasedTimer
# Initialize model and tracker
model = YOLO("yolov8s.pt")
tracker = sv.ByteTrack(minimum_matching_threshold=0.5)
# Configure video source
video_info = sv.VideoInfo.from_video_path("traffic.mp4")
frames_generator = sv.get_video_frames_generator("traffic.mp4")
# Load polygon definitions from JSON
polygons = load_zones_config("zones.json")
zones = [
sv.PolygonZone(polygon=np.array(p), triggering_anchors=(sv.Position.CENTER,))
for p in polygons
]
# Create one timer per zone
timers = [FPSBasedTimer(video_info.fps) for _ in zones]
# Initialize annotators
palette = sv.ColorPalette.from_hex(["#E6194B", "#3CB44B", "#FFE119", "#3C76D1"])
color_annotator = sv.ColorAnnotator(color=palette)
label_annotator = sv.LabelAnnotator(color=palette, text_color=sv.Color.from_hex("#000000"))
for frame in frames_generator:
# Inference and tracking
results = model(frame, verbose=False)[0]
detections = sv.Detections.from_ultralytics(results)
detections = detections[find_in_list(detections.class_id, [])] # Optional filter
detections = tracker.update_with_detections(detections)
annotated = frame.copy()
# Process each polygon zone
for idx, zone in enumerate(zones):
# Draw zone boundary
annotated = sv.draw_polygon(annotated, zone.polygon, palette.by_idx(idx))
# Filter detections inside this zone
mask = zone.trigger(detections)
detections_in_zone = detections[mask]
# Calculate dwell time in seconds for each tracked object
time_in_zone = timers[idx].tick(detections_in_zone)
# Color detections by zone index
custom_lookup = np.full(detections_in_zone.class_id.shape, idx)
annotated = color_annotator.annotate(
annotated, detections_in_zone, custom_color_lookup=custom_lookup
)
# Create labels: "#<tracker_id> mm:ss"
labels = [
f"#{tid} {int(t // 60):02d}:{int(t % 60):02d}"
for tid, t in zip(detections_in_zone.tracker_id, time_in_zone)
]
annotated = label_annotator.annotate(
annotated, detections_in_zone, labels=labels,
custom_color_lookup=custom_lookup
)
cv2.imshow("Time-in-Zone", annotated)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cv2.destroyAllWindows()
Key implementation details:
zone.trigger()returns a boolean array aligned with theDetectionscontainer.timer.tick()accepts the filtered detections and returns an array of elapsed seconds pertracker_id.- The label format
#<tracker_id> MM:SSis the standard convention for displaying dwell time in Supervision annotations.
Tracking Line Zone Crossings
For counting objects crossing a boundary—such as entering or exiting a building—use LineZone with LineZoneAnnotator for visualization.
import cv2
import supervision as sv
from ultralytics import YOLO
model = YOLO("yolov8s.pt")
tracker = sv.ByteTrack()
# Define line across the frame (start -> end)
line = sv.LineZone(
start=sv.Point(x=0, y=500),
end=sv.Point(x=1280, y=500),
triggering_anchors=(
sv.Position.TOP_LEFT,
sv.Position.TOP_RIGHT,
sv.Position.BOTTOM_LEFT,
sv.Position.BOTTOM_RIGHT,
),
)
annotator = sv.LineZoneAnnotator()
frames = sv.get_video_frames_generator("traffic.mp4")
for frame in frames:
detections = sv.Detections.from_ultralytics(model(frame)[0])
detections = tracker.update_with_detections(detections)
# Get crossing masks and update internal counters
crossed_in, crossed_out = line.trigger(detections)
# Access counts via line.in_count and line.out_count
frame = annotator.annotate(frame, line)
cv2.imshow("Line-zone", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cv2.destroyAllWindows()
Key implementation details:
LineZone.trigger()returns two boolean masks indicating directional crossings.- Internal counters
line.in_countandline.out_countincrement automatically when crossings are detected. - By default,
LineZonechecks all four corners of bounding boxes; customize this viatriggering_anchors.
Combining Multiple Zone Types
You can mix polygon and line zones in a single pipeline by maintaining parallel lists of zones and timers, then iterating through them each frame:
zones = [poly_zone_1, poly_zone_2, line_zone]
timers = [FPSBasedTimer(fps), FPSBasedTimer(fps), None] # Line zones don't use timers
for frame in frames:
# ... inference and tracking ...
for i, zone in enumerate(zones):
if isinstance(zone, sv.PolygonZone):
mask = zone.trigger(detections)
seconds = timers[i].tick(detections[mask])
# Annotate dwell time
else:
in_mask, out_mask = zone.trigger(detections)
# Access zone.in_count and zone.out_count for display
# Annotate with LineZoneAnnotator
This pattern allows simultaneous measurement of dwell time in parking spots (polygon) and traffic flow through entry lines (line) within the same frame processing loop.
Summary
- PolygonZone (
src/supervision/detection/tools/polygon_zone.py) creates masked regions to isolate detections for dwell-time calculation usingtriggering_anchorslikesv.Position.CENTER. - LineZone (
src/supervision/detection/line_zone.py) tracks directional crossings viatrigger()and maintainsin_countandout_countstatistics. - Timer utilities in
examples/time_in_zone/utils/timers.pyconvert frame-based presence into seconds; chooseFPSBasedTimerfor fixed-rate video orClockBasedTimerfor variable-rate streams. - ByteTrack integration is required to maintain stable
tracker_idvalues across frames, enabling per-object time accumulation intimer.tick(). - Reference implementation:
examples/time_in_zone/ultralytics_file_example.pydemonstrates the complete polygon zone dwell-time pipeline.
Frequently Asked Questions
What is the difference between FPSBasedTimer and ClockBasedTimer?
FPSBasedTimer calculates elapsed time by dividing frame counts by the video's constant frame rate, making it ideal for recorded video files with stable FPS. ClockBasedTimer uses the system clock (time.time()) to measure actual seconds between calls, which is necessary for real-time streams or variable frame-rate sources where wall-clock time differs from frame count.
How does PolygonZone determine if an object is inside the zone?
PolygonZone tests specific anchor points of each bounding box against the polygon mask. By default, it uses sv.Position.BOTTOM_CENTER, but you can specify alternative anchors (e.g., sv.Position.CENTER or a tuple of positions) via the triggering_anchors constructor argument. The trigger() method returns a boolean mask where True indicates the anchor point lies inside the polygon.
Can LineZone track bidirectional movement?
Yes. LineZone tracks movement in both directions simultaneously. The trigger() method returns two separate boolean masks: crossed_in for objects moving from the outside to the inside (relative to the line's direction), and crossed_out for objects moving from inside to outside. The class automatically updates line.in_count and line.out_count accordingly.
How do I visualize the dwell time on video frames?
Supervision provides specialized annotators for zone visualization. Use sv.draw_polygon to render polygon boundaries, sv.ColorAnnotator to color-code detections by zone, and sv.LabelAnnotator to overlay text labels. Construct labels using the pattern f"#{tracker_id} {minutes:02d}:{seconds:02d}" to display the elapsed time calculated by your timer's tick() method, as demonstrated in the dwell-time example above.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →