# How to Perform Time-in-Zone Analytics with PolygonZone and LineZone

> Learn time-in-zone analytics using Supervision's PolygonZone and LineZone. Track object dwell times and line crossings efficiently with ByteTrack.

- Repository: [Roboflow/supervision](https://github.com/roboflow/supervision)
- Tags: how-to-guide
- Published: 2026-04-06

---

**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`](https://github.com/roboflow/supervision/blob/main/src/supervision/detection/tools/polygon_zone.py) and [`src/supervision/detection/line_zone.py`](https://github.com/roboflow/supervision/blob/main/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`](https://github.com/roboflow/supervision/blob/main/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`](https://github.com/roboflow/supervision/blob/main/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`](https://github.com/roboflow/supervision/blob/main/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.

```python
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 the `Detections` container.
- `timer.tick()` accepts the filtered detections and returns an array of elapsed seconds per `tracker_id`.
- The label format `#<tracker_id> MM:SS` is 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.

```python
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_count` and `line.out_count` increment automatically when crossings are detected.
- By default, `LineZone` checks all four corners of bounding boxes; customize this via `triggering_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:

```python
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`](https://github.com/roboflow/supervision/blob/main/src/supervision/detection/tools/polygon_zone.py)) creates masked regions to isolate detections for dwell-time calculation using `triggering_anchors` like `sv.Position.CENTER`.
- **LineZone** ([`src/supervision/detection/line_zone.py`](https://github.com/roboflow/supervision/blob/main/src/supervision/detection/line_zone.py)) tracks directional crossings via `trigger()` and maintains `in_count` and `out_count` statistics.
- **Timer utilities** in [`examples/time_in_zone/utils/timers.py`](https://github.com/roboflow/supervision/blob/main/examples/time_in_zone/utils/timers.py) convert frame-based presence into seconds; choose `FPSBasedTimer` for fixed-rate video or `ClockBasedTimer` for variable-rate streams.
- **ByteTrack integration** is required to maintain stable `tracker_id` values across frames, enabling per-object time accumulation in `timer.tick()`.
- Reference implementation: [`examples/time_in_zone/ultralytics_file_example.py`](https://github.com/roboflow/supervision/blob/main/examples/time_in_zone/ultralytics_file_example.py) demonstrates 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.