Spaces:
Sleeping
Sleeping
"""Helpers for metric functions""" | |
import numpy as np | |
import pandas as pd | |
def calculate_iou(box1, box2): | |
""" | |
Calculate Intersection over Union (IoU) between two bounding boxes. | |
Args: | |
box1 (tuple): Coordinates of the first bounding box in the format (x1, y1, x2, y2). | |
box2 (tuple): Coordinates of the second bounding box in the format (x1, y1, x2, y2). | |
Returns: | |
float: Intersection over Union (IoU) score. | |
""" | |
# Extract coordinates | |
x1, y1, x2, y2 = box1 | |
x1_, y1_, x2_, y2_ = box2 | |
# Calculate the intersection area | |
intersection_area = max(0, min(x2, x2_) - max(x1, x1_)) * max(0, min(y2, y2_) - max(y1, y1_)) | |
# Calculate the areas of each bounding box | |
box1_area = (x2 - x1) * (y2 - y1) | |
box2_area = (x2_ - x1_) * (y2_ - y1_) | |
# Calculate IoU | |
iou = intersection_area / float(box1_area + box2_area - intersection_area) | |
return iou | |
def compute_intersection_1d(x, y): | |
# sort the boxes | |
x1, x2 = sorted(x) | |
y1, y2 = sorted(y) | |
# compute the intersection | |
intersection = max(0, min(x2, y2) - max(x1, y1)) | |
return intersection | |
def compute_union_1d(x, y): | |
# sort the boxes | |
x1, x2 = sorted(x) | |
y1, y2 = sorted(y) | |
# compute the union | |
union = max(x2, y2) - min(x1, y1) | |
return union | |
def compute_iou_1d(pred_box, true_box): | |
""" | |
Compute IoU for 1D boxes. | |
Args: | |
pred_box (float): Predicted box, [x1, x2] | |
true_box (float): Ground truth box, [x1, x2] | |
Returns: | |
float: IoU | |
""" | |
intersection = compute_intersection_1d(pred_box, true_box) | |
union = compute_union_1d(pred_box, true_box) | |
iou = intersection / union | |
return iou | |
def compute_iou_1d_single_candidate_multiple_targets(pred_box, true_boxes): | |
""" | |
Compute IoU for 1D boxes. | |
Args: | |
pred_box (float): Predicted box, [x1, x2] | |
true_boxes (np.ndarray): Ground truth boxes, shape: (N, 2) | |
Returns: | |
float: IoU | |
""" | |
ious = [] | |
for i, true_box in enumerate(true_boxes): | |
ious.append(compute_iou_1d(pred_box, true_box)) | |
return np.array(ious) | |
def compute_iou_1d_multiple_candidates_multiple_targets(pred_boxes, true_boxes): | |
""" | |
Compute IoU for 1D boxes. | |
Args: | |
pred_boxes (np.ndarray): Predicted boxes, shape: (N, 2) | |
true_boxes (np.ndarray): Ground truth boxes, shape: (N, 2) | |
Returns: | |
float: IoU | |
""" | |
iou_matrix = np.zeros((len(pred_boxes), len(true_boxes))) | |
for i, pred_box in enumerate(pred_boxes): | |
for j, true_box in enumerate(true_boxes): | |
iou_matrix[i, j] = compute_iou_1d(pred_box, true_box) | |
return iou_matrix | |
def compute_mean_iou_1d(pred_boxes, gt_boxes, threshold=0.5): | |
""" | |
Computes mean IOU for 1D bounding boxes. | |
Args: | |
pred_boxes (np.ndarray): Predicted boxes, shape: (N, 2) | |
gt_boxes (np.ndarray): Ground truth boxes, shape: (N, 2) | |
threshold (float): Threshold to consider a prediction correct | |
Returns: | |
float: Mean IOU | |
""" | |
# Compute IoU for each pair of boxes | |
iou_matrix = np.zeros((len(pred_boxes), len(gt_boxes))) | |
for i, pred_box in enumerate(pred_boxes): | |
for j, gt_box in enumerate(gt_boxes): | |
iou_matrix[i, j] = compute_iou_1d(pred_box, gt_box) | |
# Compute the max IoU for each predicted box | |
max_iou_indices = np.argmax(iou_matrix, axis=1) | |
max_iou = iou_matrix[np.arange(len(pred_boxes)), max_iou_indices] | |
# For each predicted box, compute TP and FP ground truth boxes | |
tp = np.zeros(len(pred_boxes)) | |
fp = np.zeros(len(pred_boxes)) | |
iou = np.zeros(len(pred_boxes)) | |
tp = np.where(iou_matrix >= threshold, 1, 0) | |
tp = max_iou >= threshold | |
fp = max_iou < threshold | |
iou = max_iou | |
mean_iou = np.mean(iou) | |
import ipdb; ipdb.set_trace() | |
def calculate_mAP_1d(pred_boxes, pred_scores, true_boxes, iou_thresh=0.5): | |
"""Calculate mean average precision for 1D boxes. | |
Args: | |
pred_boxes (numpy array): Predicted boxes, shape (num_boxes,) | |
pred_scores (numpy array): Predicted scores, shape (num_boxes,) | |
true_boxes (numpy array): Ground truth boxes, shape (num_boxes,) | |
iou_thresh (float): IoU threshold to consider a prediction correct | |
Returns: | |
float: Mean average precision (mAP) | |
""" | |
# Sort predicted boxes by score (in descending order) | |
sort_inds = np.argsort(pred_scores)[::-1] | |
pred_boxes = pred_boxes[sort_inds] | |
pred_scores = pred_scores[sort_inds] | |
# Compute true positives and false positives at each threshold | |
tp = np.zeros(len(pred_boxes)) | |
fp = np.zeros(len(pred_boxes)) | |
for i, box in enumerate(pred_boxes): | |
ious = np.abs(box - true_boxes) / np.maximum(1e-9, np.abs(box) + np.abs(true_boxes)) | |
if len(ious) > 0: | |
max_iou_idx = np.argmax(ious) | |
if ious[max_iou_idx] >= iou_thresh: | |
if tp[max_iou_idx] == 0: | |
tp[i] = 1 | |
fp[i] = 0 | |
else: | |
fp[i] = 1 | |
else: | |
fp[i] = 1 | |
# Compute precision and recall at each threshold | |
tp_cumsum = np.cumsum(tp) | |
fp_cumsum = np.cumsum(fp) | |
recall = tp_cumsum / len(true_boxes) | |
precision = tp_cumsum / (tp_cumsum + fp_cumsum) | |
# Compute AP as area under precision-recall curve | |
ap = 0 | |
for t in np.arange(0, 1.1, 0.1): | |
if np.sum(recall >= t) == 0: | |
p = 0 | |
else: | |
p = np.max(precision[recall >= t]) | |
ap += p / 11 | |
return ap | |
def segment_iou(target_segment, candidate_segments): | |
"""Compute the temporal intersection over union between a | |
target segment and all the test segments. | |
Parameters | |
---------- | |
target_segment : 1d array | |
Temporal target segment containing [starting, ending] times. | |
candidate_segments : 2d array | |
Temporal candidate segments containing N x [starting, ending] times. | |
Outputs | |
------- | |
tiou : 1d array | |
Temporal intersection over union score of the N's candidate segments. | |
""" | |
tt1 = np.maximum(target_segment[0], candidate_segments[:, 0]) | |
tt2 = np.minimum(target_segment[1], candidate_segments[:, 1]) | |
# Intersection including Non-negative overlap score. | |
segments_intersection = (tt2 - tt1).clip(0) | |
# Segment union. | |
segments_union = (candidate_segments[:, 1] - candidate_segments[:, 0]) \ | |
+ (target_segment[1] - target_segment[0]) - segments_intersection | |
# Compute overlap as the ratio of the intersection | |
# over union of two segments. | |
tIoU = segments_intersection.astype(float) / segments_union | |
return tIoU | |
def interpolated_prec_rec(prec, rec): | |
"""Interpolated AP - VOCdevkit from VOC 2011. | |
""" | |
mprec = np.hstack([[0], prec, [0]]) | |
mrec = np.hstack([[0], rec, [1]]) | |
for i in range(len(mprec) - 1)[::-1]: | |
mprec[i] = max(mprec[i], mprec[i + 1]) | |
idx = np.where(mrec[1::] != mrec[0:-1])[0] + 1 | |
ap = np.sum((mrec[idx] - mrec[idx - 1]) * mprec[idx]) | |
return ap | |
from tqdm import tqdm | |
def compute_average_precision_detection( | |
ground_truth, | |
prediction, | |
tiou_thresholds=np.linspace(0.5, 0.95, 10), | |
): | |
"""Compute average precision (detection task) between ground truth and | |
predictions data frames. If multiple predictions occurs for the same | |
predicted segment, only the one with highest score is matches as | |
true positive. This code is greatly inspired by Pascal VOC devkit. | |
Ref: https://github.com/zhang-can/CoLA/blob/\ | |
d21f1b5a4c6c13f9715cfd4ac1ebcd065d179157/eval/eval_detection.py#L200 | |
Parameters | |
---------- | |
ground_truth : df | |
Data frame containing the ground truth instances. | |
Required fields: ['video-id', 't-start', 't-end'] | |
prediction : df | |
Data frame containing the prediction instances. | |
Required fields: ['video-id, 't-start', 't-end', 'score'] | |
tiou_thresholds : 1darray, optional | |
Temporal intersection over union threshold. | |
Outputs | |
------- | |
ap : float | |
Average precision score. | |
""" | |
ap = np.zeros(len(tiou_thresholds)) | |
if prediction.empty: | |
return ap | |
npos = float(len(ground_truth)) | |
lock_gt = np.ones((len(tiou_thresholds),len(ground_truth))) * -1 | |
# Sort predictions by decreasing score order. | |
sort_idx = prediction['score'].values.argsort()[::-1] | |
prediction = prediction.loc[sort_idx].reset_index(drop=True) | |
# Initialize true positive and false positive vectors. | |
tp = np.zeros((len(tiou_thresholds), len(prediction))) | |
fp = np.zeros((len(tiou_thresholds), len(prediction))) | |
# Adaptation to query faster | |
ground_truth_gbvn = ground_truth.groupby('video-id') | |
# Assigning true positive to truly grount truth instances. | |
for idx, this_pred in prediction.iterrows(): | |
try: | |
# Check if there is at least one ground truth in the video associated. | |
ground_truth_videoid = ground_truth_gbvn.get_group(this_pred['video-id']) | |
except Exception as e: | |
fp[:, idx] = 1 | |
continue | |
this_gt = ground_truth_videoid.reset_index() | |
tiou_arr = segment_iou(this_pred[['t-start', 't-end']].values, | |
this_gt[['t-start', 't-end']].values) | |
# We would like to retrieve the predictions with highest tiou score. | |
tiou_sorted_idx = tiou_arr.argsort()[::-1] | |
for tidx, tiou_thr in enumerate(tiou_thresholds): | |
for jdx in tiou_sorted_idx: | |
if tiou_arr[jdx] < tiou_thr: | |
fp[tidx, idx] = 1 | |
break | |
if lock_gt[tidx, this_gt.loc[jdx]['index']] >= 0: | |
continue | |
# Assign as true positive after the filters above. | |
tp[tidx, idx] = 1 | |
lock_gt[tidx, this_gt.loc[jdx]['index']] = idx | |
break | |
if fp[tidx, idx] == 0 and tp[tidx, idx] == 0: | |
fp[tidx, idx] = 1 | |
tp_cumsum = np.cumsum(tp, axis=1).astype(float) | |
fp_cumsum = np.cumsum(fp, axis=1).astype(float) | |
recall_cumsum = tp_cumsum / npos | |
precision_cumsum = tp_cumsum / (tp_cumsum + fp_cumsum) | |
for tidx in range(len(tiou_thresholds)): | |
ap[tidx] = interpolated_prec_rec(precision_cumsum[tidx,:], recall_cumsum[tidx,:]) | |
return ap | |
def ap_wrapper( | |
true_clips, | |
pred_clips, | |
pred_scores, | |
tiou_thresholds=np.linspace(0.5, 0.95, 10), | |
): | |
assert isinstance(true_clips, np.ndarray) | |
assert len(true_clips.shape) == 2 and true_clips.shape[1] == 2 | |
assert isinstance(pred_clips, np.ndarray) | |
assert len(pred_clips.shape) == 2 and pred_clips.shape[1] == 2 | |
assert isinstance(pred_scores, np.ndarray) | |
assert len(pred_scores.shape) == 1 and len(pred_scores) == pred_clips.shape[0] | |
true_df = pd.DataFrame( | |
{ | |
"video-id": ["video1"] * len(true_clips), | |
"t-start": true_clips[:, 0], | |
"t-end": true_clips[:, 1], | |
} | |
) | |
pred_df = pd.DataFrame( | |
{ | |
"video-id": ["video1"] * len(pred_clips), | |
"t-start": pred_clips[:, 0], | |
"t-end": pred_clips[:, 1], | |
"score": pred_scores, | |
} | |
) | |
return compute_average_precision_detection( | |
true_df, | |
pred_df, | |
tiou_thresholds=tiou_thresholds, | |
) | |
def nms_1d(df: pd.DataFrame, score_col="score", iou_thresh=0.5): | |
"""Applies NMS on 1D (start, end) box predictions.""" | |
columns = set(df.columns) | |
# assert columns == set(["video_id", "start", "end", "score"]) | |
assert set(["start", "end", "video_id", score_col]).issubset(columns) | |
video_ids = df["video_id"].unique() | |
# Group by video_id | |
groups = df.groupby("video_id") | |
# Loop over videos | |
keep_indices = [] | |
net_success_fraction = [] | |
tqdm._instances.clear() | |
iterator = tqdm( | |
video_ids, | |
desc="Applying NMS to each video", | |
bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}', | |
) | |
for video_id in iterator: | |
# Get rows for this video | |
rows = groups.get_group(video_id) | |
# Sort by score | |
rows = rows.sort_values(score_col, ascending=False) | |
# Loop over rows until empty | |
n_clips = len(rows) | |
n_clips_selected_in_video = 0 | |
while len(rows): | |
# Add top row to keep_indices | |
top_row = rows.iloc[0] | |
keep_indices.append(rows.index[0]) | |
n_clips_selected_in_video += 1 | |
top_row = top_row.to_dict() | |
top_segment = np.array([top_row["start"], top_row["end"]]) | |
rows = rows.iloc[1:] | |
other_segments = rows[["start", "end"]].values | |
iou_values = segment_iou(top_segment, other_segments) | |
# Remove rows IoU > iou_thresh | |
rows = rows[iou_values < iou_thresh] | |
net_success_fraction.append(n_clips_selected_in_video / n_clips) | |
net_success_fraction = np.array(net_success_fraction).mean() | |
print("> Net success fraction: {:.2f}".format(net_success_fraction)) | |
return keep_indices | |
if __name__ == "__main__": | |
true_clips = np.array( | |
[ | |
[0.1, 0.7], | |
[3.4, 7.8], | |
[3.9, 5.4], | |
] | |
) | |
pred_clips = np.array( | |
[ | |
[0.2, 0.8], | |
[3.5, 7.9], | |
[3.9, 5.4], | |
[5.6, 6.7], | |
[6.0, 6.5], | |
], | |
) | |
pred_scores = np.array([0.9, 0.8, 0.7, 0.6, 0.5]) | |
# 1. Check IoU for a single pair of boxes | |
iou = compute_iou_1d(pred_clips[0], true_clips[0]) | |
# Manually check that the result is correct | |
# Clips are [0.1, 0.7] and [0.2, 0.8] | |
# Intersection: [0.2, 0.7] - length = 0.5 | |
# Union: [0.1, 0.8] - length = 0.7 | |
# Ratio: 0.5 / 0.7 = 0.714 | |
assert np.isclose(iou, 0.714, 3), "Incorrect IoU" | |
# 2. Check IoU for a single predicted box and multiple ground truth boxes | |
ious = compute_iou_1d_single_candidate_multiple_targets(pred_clips[0], true_clips) | |
assert np.allclose(ious, [0.714, 0.0, 0.0], 3), "Incorrect IoU" | |
# 3. Check mean IoU for multiple predicted boxes and multiple ground truth boxes | |
ious = compute_iou_1d_multiple_candidates_multiple_targets(pred_clips, true_clips) | |
assert ious.shape == (5, 3), "Incorrect shape" | |
ap = ap_wrapper( | |
true_clips, | |
pred_clips, | |
pred_scores, | |
tiou_thresholds=np.linspace(0.5, 0.95, 3), | |
) | |
# Take the mean of the APs across IoU thresholds | |
final_ap = np.mean(ap) | |
import ipdb; ipdb.set_trace() | |