Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify API #106

Merged
merged 2 commits into from
Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions demos/keypoints_bounding_boxes/keypoints_bounding_boxes_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@
# Define constants
DETECTION_THRESHOLD = 0.6
DISTANCE_THRESHOLD = 0.8
HIT_INERTIA_MIN = 5
HIT_INERTIA_MAX = 50
HIT_COUNTER_MAX = 45
INITIALIZATION_DELAY = 4
POINT_TRANSIENCE = 2
POINTWISE_HIT_COUNTER_MAX = 10

############### OPENPOSE ##################

Expand Down Expand Up @@ -186,10 +185,9 @@ def keypoints_distance(detected_pose, tracked_pose):
distance_function=keypoints_distance,
distance_threshold=DISTANCE_THRESHOLD,
detection_threshold=DETECTION_THRESHOLD,
hit_inertia_min=HIT_INERTIA_MIN,
hit_inertia_max=HIT_INERTIA_MAX,
hit_counter_max=HIT_COUNTER_MAX,
initialization_delay=INITIALIZATION_DELAY,
point_transience=POINT_TRANSIENCE,
pointwise_hit_counter_max=POINTWISE_HIT_COUNTER_MAX,
)
KEYPOINT_DIST_THRESHOLD = video.input_height / 40

Expand Down
2 changes: 1 addition & 1 deletion demos/motmetrics4norfair/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ Demo on how to evaluate a Norfair tracker on the [MOTChallenge](https://motchall

## Important consideration

Hyperparameters were tuned for reaching a high `MOTA` on this dataset. They may not be ideal for more general use cases, use the default hyperparameters for those. ID switches suffer specially due to this optimization. If you want to improve ID switches use a higher margin between `hit_inertia_min` and `hit_inertia_max`, or just use the default hyperparameters.
Hyperparameters were tuned for reaching a high `MOTA` on this dataset. They may not be ideal for more general use cases, use the default hyperparameters for those. ID switches suffer specially due to this optimization. If you want to improve ID switches use a higher `hit_counter_max`, or just use the default hyperparameters.
17 changes: 9 additions & 8 deletions demos/motmetrics4norfair/motmetrics4norfair.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
detection_threshold = 0.01
distance_threshold = 0.9
diagonal_proportion_threshold = 1 / 18
pointwise_hit_counter_max=3
hit_counter_max=2

parser = argparse.ArgumentParser(
description="Evaluate a basic tracker on MOTChallenge data. Display on terminal the MOTChallenge metrics results "
Expand All @@ -20,25 +22,25 @@
help="Path to the MOT Challenge train dataset folder (test dataset doesn't provide labels)",
)
parser.add_argument(
"--make_video",
"--make-video",
action="store_true",
help="Generate an output video, using the frames provided by the MOTChallenge dataset.",
)
parser.add_argument(
"--save_pred",
"--save-pred",
action="store_true",
help="Generate a text file with your predictions",
)
parser.add_argument(
"--save_metrics",
"--save-metrics",
action="store_true",
help="Generate a text file with your MOTChallenge metrics results",
)
parser.add_argument(
"--output_path", type=str, nargs="?", default=".", help="Output path"
"--output-path", type=str, nargs="?", default=".", help="Output path"
)
parser.add_argument(
"--select_sequences",
"--select-sequences",
type=str,
nargs="+",
help="If you want to select a subset of sequences in your dataset path. Insert the names of the sequences you want to process.",
Expand Down Expand Up @@ -120,9 +122,8 @@ def keypoints_distance(detected_pose, tracked_pose):
distance_function=keypoints_distance,
distance_threshold=distance_threshold,
detection_threshold=detection_threshold,
hit_inertia_min=10,
hit_inertia_max=12,
point_transience=4,
pointwise_hit_counter_max=pointwise_hit_counter_max,
hit_counter_max=hit_counter_max,
)

# Initialize accumulator for this video
Expand Down
2 changes: 1 addition & 1 deletion demos/openpose/openpose_extrapolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def keypoints_distance(detected_pose, tracked_pose):
distance_function=keypoints_distance,
distance_threshold=distance_threshold,
detection_threshold=detection_threshold,
point_transience=2,
pointwise_hit_counter_max=2,
)
keypoint_dist_threshold = video.input_height / 25

Expand Down
7 changes: 3 additions & 4 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ The class in charge of performing the tracking of the detections produced by the

- `distance_function`: Function used by the tracker to determine the distance between newly detected objects and the objects the tracker is currently tracking. This function should take 2 input arguments, the first being a detection of type [`Detection`](#detection), and the second a tracked object of type [`TrackedObject`](#trackedobject). It returns a `float` with the distance it calculates.
- `distance_threshold`: Defines what is the maximum distance that can constitute a match. Detections and tracked objects whose distance are above this threshold won't be matched by the tracker.
- `hit_inertia_min (optional)`: Each tracked objects keeps an internal hit inertia counter which tracks how often it's getting matched to a detection, each time it gets a match this counter goes up, and each time it doesn't it goes down. If it doesn't get any match for a certain amount of frames, and it gets below the value set by this argument, the object is destroyed. Defaults to `10`.
- `hit_inertia_max (optional)`: Each tracked objects keeps an internal hit inertia counter which tracks how often it's getting matched to a detection, each time it gets a match this counter goes up, and each time it doesn't it goes down. If it goes below `hit_inertia_min` the object gets destroyed. This argument (`hit_inertia_max`) defines how large this inertia can grow, and therefore defines how long an object can live without getting matched to any detections, before it is destroyed. Defaults to `25`.
- `initialization_delay (optional)`: Each tracked object waits till its internal hit intertia counter goes over `hit_inertia_min` to be considered as a potential object to be returned to the user by the Tracker. The argument `initialization_delay` determines by how much the object's hit inertia counter must exceed `hit_inertia_min` to be considered as initialized, and get returned to the user as a real object. If set to 0, objects will get returned to the user as soon as they exceed their `hit_inertia_min`, which can be problematic as this is also the threshold for object destruction, and can result in objects appearing and immediately dissapearing. Defaults to `(hit_inertia_max - hit_inertia_min) / 2`.
- `hit_counter_max (optional)`: Each tracked objects keeps an internal hit counter which tracks how often it's getting matched to a detection, each time it gets a match this counter goes up, and each time it doesn't it goes down. If it goes below 0 the object gets destroyed. This argument (`hit_counter_max`) defines how large this inertia can grow, and therefore defines how long an object can live without getting matched to any detections, before it is destroyed. Defaults to `15`.
- `initialization_delay (optional)`: The argument `initialization_delay` determines by how large the object's hit counter must be in order to be considered as initialized, and get returned to the user as a real object. It must be smaller than `hit_counter_max` or otherwise the object would never be initialized. If set to 0, objects will get returned to the user as soon as they are detected for the first time, which can be problematic as this can result in objects appearing and immediately dissapearing. Defaults to `hit_counter_max / 2`.
- `pointwise_hit_counter_max (optional)`: Each tracked object keeps track of how often the points it's tracking have been getting matched. Points that are getting matched (`pointwise_hit_counter > 0`) are said to be live, and points which aren't (`pointwise_hit_counter = 0`) are said to not be live. This is used to determine things like which individual points in a tracked object get drawn by [`draw_tracked_objects`](#draw_tracked_objects) and which don't. This argument (`pointwise_hit_counter_max`) defines how large the inertia for each point of a tracker can grow. Defaults to `5`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that it must be smaller than hit_counter_max as a point can't live longer than the object it belongs to. I think this will make how hit_counter_max and pointwise_hit_counter_max relate clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that it must be smaller than hit_counter_max as a point can't live longer than the object it belongs to.

- `detection_threshold (optional)`: Sets the threshold at which the scores of the points in a detection being fed into the tracker must dip below to be ignored by the tracker. Defaults to `0`.
- `point_transience (optional)`: Each tracked object keeps track of how often the points it's tracking have been getting matched. Points that are getting matched are said to be live, and points which aren't are said to not be live. This is used to determine things like which individual points in a tracked object get drawn by [`draw_tracked_objects`](#draw_tracked_objects) and which don't. This argument determines how short lived individual points (not tracked objects) not getting matched are. Defaults to `4`.
- `filter_setup (optional)`: This parameter can be used to change the parameters of the Kalman Filter that is used by [`TrackedObject`](#trackedobject) instances. Defaults to [`FilterSetup()`](#filtersetup).
- `past_detections_length`: How many past detections to save for each tracked object. Norfair tries to distribute these past detections uniformly through the object's lifetime so they're more representative. Very useful if you want to add metric learning to your model, as you can associate an embedding to each detection and access them in your distance function. Defaults to `4`.

Expand Down
60 changes: 26 additions & 34 deletions norfair/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ def __init__(
self,
distance_function: Callable[["Detection", "TrackedObject"], float],
distance_threshold: float,
hit_inertia_min: int = 10,
hit_inertia_max: int = 25,
hit_counter_max: int = 15,
initialization_delay: Optional[int] = None,
pointwise_hit_counter_max: int = 4,
detection_threshold: float = 0,
point_transience: int = 4,
filter_setup: "FilterSetup" = FilterSetup(),
past_detections_length: int = 4
):
self.tracked_objects: Sequence["TrackedObject"] = []
self.distance_function = distance_function
self.hit_inertia_min = hit_inertia_min
self.hit_inertia_max = hit_inertia_max
self.hit_counter_max = hit_counter_max
self.pointwise_hit_counter_max = pointwise_hit_counter_max
self.filter_setup = filter_setup
if past_detections_length >= 0:
self.past_detections_length = past_detections_length
Expand All @@ -33,28 +32,27 @@ def __init__(

if initialization_delay is None:
self.initialization_delay = int(
(self.hit_inertia_max - self.hit_inertia_min) / 2
self.hit_counter_max / 2
)
elif (
initialization_delay < 0
or initialization_delay > self.hit_inertia_max - self.hit_inertia_min
or initialization_delay > self.hit_counter_max
):
raise ValueError(
f"Argument 'initialization_delay' for 'Tracker' class should be an int between 0 and (hit_inertia_max - hit_inertia_min = {hit_inertia_max - hit_inertia_min}). The selected value is {initialization_delay}.\n"
f"Argument 'initialization_delay' for 'Tracker' class should be an int between 0 and (hit_counter_max = {hit_counter_max}). The selected value is {initialization_delay}.\n"
)
else:
self.initialization_delay = initialization_delay

self.distance_threshold = distance_threshold
self.detection_threshold = detection_threshold
self.point_transience = point_transience
TrackedObject.count = 0

def update(self, detections: Optional[List["Detection"]] = None, period: int = 1):
self.period = period

# Remove stale trackers and make candidate object real if it has hit inertia
self.tracked_objects = [o for o in self.tracked_objects if o.has_inertia]
# Remove stale trackers and make candidate object real if the hit counter is positive
self.tracked_objects = [o for o in self.tracked_objects if o.hit_counter_is_positive]

# Update tracker
for obj in self.tracked_objects:
Expand All @@ -75,12 +73,11 @@ def update(self, detections: Optional[List["Detection"]] = None, period: int = 1
self.tracked_objects.append(
TrackedObject(
detection,
self.hit_inertia_min,
self.hit_inertia_max,
self.hit_counter_max,
self.initialization_delay,
self.pointwise_hit_counter_max,
self.detection_threshold,
self.period,
self.point_transience,
self.filter_setup,
self.past_detections_length
)
Expand Down Expand Up @@ -207,12 +204,11 @@ class TrackedObject:
def __init__(
self,
initial_detection: "Detection",
hit_inertia_min: int,
hit_inertia_max: int,
hit_counter_max: int,
initialization_delay: int,
pointwise_hit_counter_max: int,
detection_threshold: float,
period: int,
point_transience: int,
filter_setup: "FilterSetup",
past_detections_length: int
):
Expand All @@ -224,19 +220,15 @@ def __init__(
)
exit()
self.num_points = initial_detection_points.shape[0]
self.hit_inertia_min: int = hit_inertia_min
self.hit_inertia_max: int = hit_inertia_max
self.hit_counter_max: int = hit_counter_max
self.pointwise_hit_counter_max: int = pointwise_hit_counter_max
self.initialization_delay = initialization_delay
self.point_hit_inertia_min: int = math.floor(hit_inertia_min / point_transience)
self.point_hit_inertia_max: int = math.ceil(hit_inertia_max / point_transience)
if (self.point_hit_inertia_max - self.point_hit_inertia_min) < period:
self.point_hit_inertia_max = self.point_hit_inertia_min + period
if self.pointwise_hit_counter_max < period:
self.pointwise_hit_counter_max = period
self.detection_threshold: float = detection_threshold
self.initial_period: int = period
self.hit_counter: int = hit_inertia_min + period
self.point_hit_counter: np.ndarray = (
np.ones(self.num_points) * self.point_hit_inertia_min
)
self.hit_counter: int = period
self.point_hit_counter: np.ndarray = np.ones(self.num_points)
self.last_distance: Optional[float] = None
self.current_min_distance: Optional[float] = None
self.last_detection: "Detection" = initial_detection
Expand Down Expand Up @@ -271,16 +263,16 @@ def tracker_step(self):
def is_initializing(self):
if (
self.is_initializing_flag
and self.hit_counter > self.hit_inertia_min + self.initialization_delay
and self.hit_counter > self.initialization_delay
):
self.is_initializing_flag = False
TrackedObject.count += 1
self.id = TrackedObject.count
return self.is_initializing_flag

@property
def has_inertia(self):
return self.hit_counter >= self.hit_inertia_min
def hit_counter_is_positive(self):
return self.hit_counter >= 0

@property
def estimate(self):
Expand All @@ -290,14 +282,14 @@ def estimate(self):

@property
def live_points(self):
return self.point_hit_counter > self.point_hit_inertia_min
return self.point_hit_counter > 0

def hit(self, detection: "Detection", period: int = 1):
points = validate_points(detection.points)
self.conditionally_add_to_past_detections(detection)

self.last_detection = detection
if self.hit_counter < self.hit_inertia_max:
if self.hit_counter < self.hit_counter_max:
self.hit_counter += 2 * period

# We use a kalman filter in which we consider each coordinate on each point as a sensor.
Expand All @@ -319,8 +311,8 @@ def hit(self, detection: "Detection", period: int = 1):
H_pos = np.identity(points.size)
self.point_hit_counter += 2 * period
self.point_hit_counter[
self.point_hit_counter >= self.point_hit_inertia_max
] = self.point_hit_inertia_max
self.point_hit_counter >= self.pointwise_hit_counter_max
] = self.pointwise_hit_counter_max
self.point_hit_counter[self.point_hit_counter < 0] = 0
H_vel = np.zeros(H_pos.shape) # But we don't directly measure velocity
H = np.hstack([H_pos, H_vel])
Expand Down