post_processing
dreem.inference.post_processing
¶
Helper functions for post-processing association matrix pre-tracking.
Classes:
| Name | Description |
|---|---|
ConfidenceFlagging |
Flag frames with low confidence based on entropy of association scores. |
DistanceWeighting |
Weight trajectory score by distances between objects across frames. |
IOUWeighting |
Weight trajectory score by IOU between object bboxes across frames. |
OrientationWeighting |
Weight trajectory score by angle difference between objects' orientations across frames. |
ConfidenceFlagging
¶
Bases: ProcessingStep
Flag frames with low confidence based on entropy of association scores.
This step computes the entropy of the scaled trajectory scores and flags frames where instances have high entropy (low confidence).
Expected state keys
- scaled_traj_score: torch.Tensor (n_query, n_traj) - log-softmax scaled scores
- n_query: int - number of query instances
- query_frame: Frame - the frame to potentially flag
Modified state keys
- query_frame: may have LOW_CONFIDENCE flag added
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize ConfidenceFlagging step. |
run |
Apply confidence flagging to query frame. |
Source code in dreem/inference/post_processing.py
class ConfidenceFlagging(ProcessingStep):
"""Flag frames with low confidence based on entropy of association scores.
This step computes the entropy of the scaled trajectory scores and flags
frames where instances have high entropy (low confidence).
Expected state keys:
- scaled_traj_score: torch.Tensor (n_query, n_traj) - log-softmax scaled scores
- n_query: int - number of query instances
- query_frame: Frame - the frame to potentially flag
Modified state keys:
- query_frame: may have LOW_CONFIDENCE flag added
"""
def __init__(self, confidence_threshold: float = 0.0):
"""Initialize ConfidenceFlagging step.
Args:
confidence_threshold: Threshold for flagging low confidence frames.
Set to 0 to disable flagging. Higher values are more strict
(flag more frames).
"""
super().__init__(name="confidence_flagging")
self.confidence_threshold = confidence_threshold
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply confidence flagging to query frame.
Args:
state: State dictionary with required keys.
Returns:
Modified state with potentially flagged query_frame.
"""
scaled_traj_score = state["scaled_traj_score"]
n_query = state["n_query"]
query_frame = state["query_frame"]
# Compute entropy for each row
entropy = -torch.sum(scaled_traj_score * torch.exp(scaled_traj_score), axis=1)
norm_entropy = entropy / torch.log(torch.tensor(n_query))
flag_threshold = 1 - self.confidence_threshold
# Flag rows with high entropy
flag = norm_entropy > flag_threshold
# Flag the frame if any instances have high entropy
if flag.any():
query_frame.add_flag(FrameFlagCode.LOW_CONFIDENCE)
return state
__init__(confidence_threshold=0.0)
¶
Initialize ConfidenceFlagging step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
confidence_threshold
|
float
|
Threshold for flagging low confidence frames. Set to 0 to disable flagging. Higher values are more strict (flag more frames). |
0.0
|
Source code in dreem/inference/post_processing.py
def __init__(self, confidence_threshold: float = 0.0):
"""Initialize ConfidenceFlagging step.
Args:
confidence_threshold: Threshold for flagging low confidence frames.
Set to 0 to disable flagging. Higher values are more strict
(flag more frames).
"""
super().__init__(name="confidence_flagging")
self.confidence_threshold = confidence_threshold
run(state)
¶
Apply confidence flagging to query frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Dict[str, Any]
|
State dictionary with required keys. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Modified state with potentially flagged query_frame. |
Source code in dreem/inference/post_processing.py
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply confidence flagging to query frame.
Args:
state: State dictionary with required keys.
Returns:
Modified state with potentially flagged query_frame.
"""
scaled_traj_score = state["scaled_traj_score"]
n_query = state["n_query"]
query_frame = state["query_frame"]
# Compute entropy for each row
entropy = -torch.sum(scaled_traj_score * torch.exp(scaled_traj_score), axis=1)
norm_entropy = entropy / torch.log(torch.tensor(n_query))
flag_threshold = 1 - self.confidence_threshold
# Flag rows with high entropy
flag = norm_entropy > flag_threshold
# Flag the frame if any instances have high entropy
if flag.any():
query_frame.add_flag(FrameFlagCode.LOW_CONFIDENCE)
return state
DistanceWeighting
¶
Bases: ProcessingStep
Weight trajectory score by distances between objects across frames.
This step applies a penalty to the trajectory score based on euclidean distance between bounding box centers.
Expected state keys
- traj_score: torch.Tensor (n_query, n_traj)
- query_boxes_px: torch.Tensor - raw bbox coords of current frame instances
- last_boxes_px: torch.Tensor - raw bbox coords of instances in context window
- h: int - height of the image in pixels
- w: int - width of the image in pixels
Modified state keys
- traj_score: updated with distance-based penalties
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize DistanceWeighting step. |
run |
Apply distance weighting to trajectory score. |
Source code in dreem/inference/post_processing.py
class DistanceWeighting(ProcessingStep):
"""Weight trajectory score by distances between objects across frames.
This step applies a penalty to the trajectory score based on euclidean distance
between bounding box centers.
Expected state keys:
- traj_score: torch.Tensor (n_query, n_traj)
- query_boxes_px: torch.Tensor - raw bbox coords of current frame instances
- last_boxes_px: torch.Tensor - raw bbox coords of instances in context window
- h: int - height of the image in pixels
- w: int - width of the image in pixels
Modified state keys:
- traj_score: updated with distance-based penalties
"""
def __init__(self, max_center_dist: float, penalty_multiplier: float = 1.0):
"""Initialize DistanceWeighting step.
Args:
max_center_dist: The euclidean distance threshold between bboxes in pixels.
penalty_multiplier: The multiplier for the penalty.
"""
super().__init__(name="distance_weighting")
self.max_center_dist = max_center_dist
self.penalty_multiplier = penalty_multiplier
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply distance weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
traj = state["traj_score"]
q_boxes_px = state["query_boxes_px"]
last_boxes_px = state["last_boxes_px"]
h, w = state["h"], state["w"]
assert (
q_boxes_px is not None
and last_boxes_px is not None
and h is not None
and w is not None
), (
"Need `query_boxes_px`, `last_boxes_px`, and `h`, `w` to weight by `max_center_dist`"
)
diag_length = (h**2 + w**2) ** (1 / 2) # diagonal length of the image in pixels
max_center_dist_normalized = self.max_center_dist / diag_length
k_ct = (q_boxes_px[:, :, :2] + q_boxes_px[:, :, 2:]) / 2
# nonquery boxes are the most recent occurrence of each instance; could be many frames ago
nonk_ct = (last_boxes_px[:, :, :2] + last_boxes_px[:, :, 2:]) / 2
# pairwise euclidean distance in units of pixels
dist = ((k_ct[:, None, :, :] - nonk_ct[None, :, :, :]) ** 2).sum(dim=-1) ** (
1 / 2
) # n_k x n_nonk
dist = dist.squeeze(-1) / diag_length # n_k x n_nonk
while dist.dim() < 2:
dist = dist.unsqueeze(0)
asso_scale = torch.abs(traj).mean(dim=1)
penalty = torch.where(
dist > max_center_dist_normalized, dist - max_center_dist_normalized, 0
) # n_k x n_nonk
scale = asso_scale / (penalty.mean(dim=1) + 1e-8)
scaled_penalty = -scale.unsqueeze(-1) * penalty # n_k x n_nonk
traj = traj + self.penalty_multiplier * scaled_penalty
state["traj_score"] = traj
return state
__init__(max_center_dist, penalty_multiplier=1.0)
¶
Initialize DistanceWeighting step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_center_dist
|
float
|
The euclidean distance threshold between bboxes in pixels. |
required |
penalty_multiplier
|
float
|
The multiplier for the penalty. |
1.0
|
Source code in dreem/inference/post_processing.py
def __init__(self, max_center_dist: float, penalty_multiplier: float = 1.0):
"""Initialize DistanceWeighting step.
Args:
max_center_dist: The euclidean distance threshold between bboxes in pixels.
penalty_multiplier: The multiplier for the penalty.
"""
super().__init__(name="distance_weighting")
self.max_center_dist = max_center_dist
self.penalty_multiplier = penalty_multiplier
run(state)
¶
Apply distance weighting to trajectory score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Dict[str, Any]
|
State dictionary with required keys. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Modified state with updated traj_score. |
Source code in dreem/inference/post_processing.py
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply distance weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
traj = state["traj_score"]
q_boxes_px = state["query_boxes_px"]
last_boxes_px = state["last_boxes_px"]
h, w = state["h"], state["w"]
assert (
q_boxes_px is not None
and last_boxes_px is not None
and h is not None
and w is not None
), (
"Need `query_boxes_px`, `last_boxes_px`, and `h`, `w` to weight by `max_center_dist`"
)
diag_length = (h**2 + w**2) ** (1 / 2) # diagonal length of the image in pixels
max_center_dist_normalized = self.max_center_dist / diag_length
k_ct = (q_boxes_px[:, :, :2] + q_boxes_px[:, :, 2:]) / 2
# nonquery boxes are the most recent occurrence of each instance; could be many frames ago
nonk_ct = (last_boxes_px[:, :, :2] + last_boxes_px[:, :, 2:]) / 2
# pairwise euclidean distance in units of pixels
dist = ((k_ct[:, None, :, :] - nonk_ct[None, :, :, :]) ** 2).sum(dim=-1) ** (
1 / 2
) # n_k x n_nonk
dist = dist.squeeze(-1) / diag_length # n_k x n_nonk
while dist.dim() < 2:
dist = dist.unsqueeze(0)
asso_scale = torch.abs(traj).mean(dim=1)
penalty = torch.where(
dist > max_center_dist_normalized, dist - max_center_dist_normalized, 0
) # n_k x n_nonk
scale = asso_scale / (penalty.mean(dim=1) + 1e-8)
scaled_penalty = -scale.unsqueeze(-1) * penalty # n_k x n_nonk
traj = traj + self.penalty_multiplier * scaled_penalty
state["traj_score"] = traj
return state
IOUWeighting
¶
Bases: ProcessingStep
Weight trajectory score by IOU between object bboxes across frames.
This step applies IOU-based weighting to the trajectory score using either multiplicative or max weighting methods.
Expected state keys
- traj_score: torch.Tensor (n_query, n_traj)
- last_ious: torch.Tensor - IOU values between current and previous frames
Modified state keys
- traj_score: updated with IOU-based weighting
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize IOUWeighting step. |
run |
Apply IOU weighting to trajectory score. |
Source code in dreem/inference/post_processing.py
class IOUWeighting(ProcessingStep):
"""Weight trajectory score by IOU between object bboxes across frames.
This step applies IOU-based weighting to the trajectory score using either
multiplicative or max weighting methods.
Expected state keys:
- traj_score: torch.Tensor (n_query, n_traj)
- last_ious: torch.Tensor - IOU values between current and previous frames
Modified state keys:
- traj_score: updated with IOU-based weighting
"""
def __init__(self, method: str | None = None):
"""Initialize IOUWeighting step.
Args:
method: Weighting method. One of:
- None or "": Skip IOU weighting (no-op)
- "mult": Multiplicative weighting: `iou*weight + traj_score`
- "max": Max weighting: `max(traj_score, iou)`
"""
super().__init__(name="iou_weighting")
self.method = method
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply IOU weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
if self.method is None or self.method == "":
return state
traj = state["traj_score"]
last_ious = state["last_ious"]
assert last_ious is not None, "Need `last_ious` to weight traj_score by `IOU`"
if self.method.lower() == "mult":
weights = torch.abs(last_ious - traj)
weighted_iou = weights * last_ious
weighted_iou = torch.nan_to_num(weighted_iou, 0)
traj = traj + weighted_iou
elif self.method.lower() == "max":
traj = torch.max(traj, last_ious)
else:
raise ValueError(
f"`method` must be one of ['mult' or 'max'] got '{self.method.lower()}'"
)
state["traj_score"] = traj
return state
__init__(method=None)
¶
Initialize IOUWeighting step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str | None
|
Weighting method. One of:
- None or "": Skip IOU weighting (no-op)
- "mult": Multiplicative weighting: |
None
|
Source code in dreem/inference/post_processing.py
def __init__(self, method: str | None = None):
"""Initialize IOUWeighting step.
Args:
method: Weighting method. One of:
- None or "": Skip IOU weighting (no-op)
- "mult": Multiplicative weighting: `iou*weight + traj_score`
- "max": Max weighting: `max(traj_score, iou)`
"""
super().__init__(name="iou_weighting")
self.method = method
run(state)
¶
Apply IOU weighting to trajectory score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Dict[str, Any]
|
State dictionary with required keys. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Modified state with updated traj_score. |
Source code in dreem/inference/post_processing.py
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply IOU weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
if self.method is None or self.method == "":
return state
traj = state["traj_score"]
last_ious = state["last_ious"]
assert last_ious is not None, "Need `last_ious` to weight traj_score by `IOU`"
if self.method.lower() == "mult":
weights = torch.abs(last_ious - traj)
weighted_iou = weights * last_ious
weighted_iou = torch.nan_to_num(weighted_iou, 0)
traj = traj + weighted_iou
elif self.method.lower() == "max":
traj = torch.max(traj, last_ious)
else:
raise ValueError(
f"`method` must be one of ['mult' or 'max'] got '{self.method.lower()}'"
)
state["traj_score"] = traj
return state
OrientationWeighting
¶
Bases: ProcessingStep
Weight trajectory score by angle difference between objects' orientations across frames.
This step applies a penalty based on the angular difference between principal axes of instances across frames.
Expected state keys
- traj_score: torch.Tensor (n_query, n_traj)
- query_principal_axes: torch.Tensor (n_query, 2) - principal axes of current frame
- last_principal_axes: torch.Tensor (n_traj, 2) - principal axes of last frame instances
Modified state keys
- traj_score: updated with angle-based penalties
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize WeightAngleDiff step. |
run |
Apply angle difference weighting to trajectory score. |
Source code in dreem/inference/post_processing.py
class OrientationWeighting(ProcessingStep):
"""Weight trajectory score by angle difference between objects' orientations across frames.
This step applies a penalty based on the angular difference between principal axes
of instances across frames.
Expected state keys:
- traj_score: torch.Tensor (n_query, n_traj)
- query_principal_axes: torch.Tensor (n_query, 2) - principal axes of current frame
- last_principal_axes: torch.Tensor (n_traj, 2) - principal axes of last frame instances
Modified state keys:
- traj_score: updated with angle-based penalties
"""
def __init__(self, max_angle_diff_rad: float, penalty_multiplier: float = 1.0):
"""Initialize WeightAngleDiff step.
Args:
max_angle_diff_rad: Maximum angle difference threshold in radians.
penalty_multiplier: The multiplier for the penalty.
"""
super().__init__(name="weight_angle_diff")
self.max_angle_diff_rad = max_angle_diff_rad
self.penalty_multiplier = penalty_multiplier
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply angle difference weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
traj = state["traj_score"]
query_axes = state["query_principal_axes"]
last_axes = state["last_principal_axes"]
# Inline the weight_by_angle_diff logic
assert query_axes is not None and last_axes is not None, (
"Need `query_principal_axes`, and `last_principal_axes` to weight by angle difference"
)
dot = (query_axes[:, None, :] * last_axes[None, :, :]).sum(dim=-1) # (q, nq)
# cross product is scalar in 2D
cross_z = (
query_axes[:, None, 0] * last_axes[None, :, 1]
- query_axes[:, None, 1] * last_axes[None, :, 0]
) # (q, nq)
# product of norms cancels out in arctan so no need to calculate
angle_diff = torch.abs(torch.atan2(cross_z, dot))
# wrap angle diff to [0, pi/2] since there is no head/tail disambiguation in general
angle_diff = torch.where(
angle_diff > torch.pi / 2, torch.pi - angle_diff, angle_diff
)
if angle_diff.dim() == 1:
angle_diff = angle_diff.unsqueeze(0)
# already been modified by other post processing so use abs as this only considers scale
weight = torch.abs(traj).mean(
dim=1
) # row wise aggregation of association scores; used to weight the angle diff
penalty = torch.where(
angle_diff > self.max_angle_diff_rad,
angle_diff - self.max_angle_diff_rad,
0,
) # gracefully handles nans by setting diff to 0
scale = weight / (penalty.mean(dim=1) + 1e-8)
# normalize angle difference to [0, 1]
scaled_penalty = -scale.unsqueeze(-1) * penalty
traj = traj + self.penalty_multiplier * scaled_penalty
state["traj_score"] = traj
return state
__init__(max_angle_diff_rad, penalty_multiplier=1.0)
¶
Initialize WeightAngleDiff step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
max_angle_diff_rad
|
float
|
Maximum angle difference threshold in radians. |
required |
penalty_multiplier
|
float
|
The multiplier for the penalty. |
1.0
|
Source code in dreem/inference/post_processing.py
def __init__(self, max_angle_diff_rad: float, penalty_multiplier: float = 1.0):
"""Initialize WeightAngleDiff step.
Args:
max_angle_diff_rad: Maximum angle difference threshold in radians.
penalty_multiplier: The multiplier for the penalty.
"""
super().__init__(name="weight_angle_diff")
self.max_angle_diff_rad = max_angle_diff_rad
self.penalty_multiplier = penalty_multiplier
run(state)
¶
Apply angle difference weighting to trajectory score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Dict[str, Any]
|
State dictionary with required keys. |
required |
Returns:
| Type | Description |
|---|---|
Dict[str, Any]
|
Modified state with updated traj_score. |
Source code in dreem/inference/post_processing.py
def run(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""Apply angle difference weighting to trajectory score.
Args:
state: State dictionary with required keys.
Returns:
Modified state with updated traj_score.
"""
traj = state["traj_score"]
query_axes = state["query_principal_axes"]
last_axes = state["last_principal_axes"]
# Inline the weight_by_angle_diff logic
assert query_axes is not None and last_axes is not None, (
"Need `query_principal_axes`, and `last_principal_axes` to weight by angle difference"
)
dot = (query_axes[:, None, :] * last_axes[None, :, :]).sum(dim=-1) # (q, nq)
# cross product is scalar in 2D
cross_z = (
query_axes[:, None, 0] * last_axes[None, :, 1]
- query_axes[:, None, 1] * last_axes[None, :, 0]
) # (q, nq)
# product of norms cancels out in arctan so no need to calculate
angle_diff = torch.abs(torch.atan2(cross_z, dot))
# wrap angle diff to [0, pi/2] since there is no head/tail disambiguation in general
angle_diff = torch.where(
angle_diff > torch.pi / 2, torch.pi - angle_diff, angle_diff
)
if angle_diff.dim() == 1:
angle_diff = angle_diff.unsqueeze(0)
# already been modified by other post processing so use abs as this only considers scale
weight = torch.abs(traj).mean(
dim=1
) # row wise aggregation of association scores; used to weight the angle diff
penalty = torch.where(
angle_diff > self.max_angle_diff_rad,
angle_diff - self.max_angle_diff_rad,
0,
) # gracefully handles nans by setting diff to 0
scale = weight / (penalty.mean(dim=1) + 1e-8)
# normalize angle difference to [0, 1]
scaled_penalty = -scale.unsqueeze(-1) * penalty
traj = traj + self.penalty_multiplier * scaled_penalty
state["traj_score"] = traj
return state