Spaces:
Sleeping
Sleeping
kerojohan commited on
Commit ·
81a2d8e
1
Parent(s): 737952c
Sync logic with bat_tracker v1.1.7
Browse files- app.py +1 -1
- bat_tracker/pipeline.py +164 -91
- bat_tracker/tracker.py +30 -21
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -13,7 +13,7 @@ import yaml
|
|
| 13 |
from bat_tracker.pipeline import run_pipeline
|
| 14 |
|
| 15 |
|
| 16 |
-
APP_VERSION = "v1.1.
|
| 17 |
APP_TITLE = f"Bat Tracker {APP_VERSION}"
|
| 18 |
APP_DESCRIPTION = (
|
| 19 |
"Sube un video IR monocromo para ejecutar el pipeline, revisar la region valida "
|
|
|
|
| 13 |
from bat_tracker.pipeline import run_pipeline
|
| 14 |
|
| 15 |
|
| 16 |
+
APP_VERSION = "v1.1.7"
|
| 17 |
APP_TITLE = f"Bat Tracker {APP_VERSION}"
|
| 18 |
APP_DESCRIPTION = (
|
| 19 |
"Sube un video IR monocromo para ejecutar el pipeline, revisar la region valida "
|
bat_tracker/pipeline.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import csv
|
|
|
|
| 4 |
import json
|
| 5 |
import sys
|
| 6 |
from collections import Counter, defaultdict, deque
|
|
@@ -146,6 +147,24 @@ def _classify_direction(start_inside: bool, end_inside: bool) -> str:
|
|
| 146 |
return "outside"
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def _infer_outside_direction_from_motion(
|
| 150 |
start: TrackPoint,
|
| 151 |
end: TrackPoint,
|
|
@@ -189,9 +208,7 @@ def _write_events_csv(
|
|
| 189 |
if valid_mask is not None:
|
| 190 |
s_in = _point_in_mask(start, valid_mask)
|
| 191 |
e_in = _point_in_mask(end, valid_mask)
|
| 192 |
-
direction =
|
| 193 |
-
if direction == "outside":
|
| 194 |
-
direction = _infer_outside_direction_from_motion(start, end, valid_mask.shape[:2])
|
| 195 |
else:
|
| 196 |
s_in = None
|
| 197 |
e_in = None
|
|
@@ -477,15 +494,13 @@ def _filter_track_points(
|
|
| 477 |
if require_start_or_end_in_valid_region and gate_mask is not None:
|
| 478 |
s_in = _point_in_mask(start, gate_mask)
|
| 479 |
e_in = _point_in_mask(end, gate_mask)
|
| 480 |
-
direction =
|
| 481 |
if not (s_in or e_in):
|
| 482 |
reject_reasons.append("valid_region_gate")
|
| 483 |
elif valid_mask is not None:
|
| 484 |
s_in = _point_in_mask(start, valid_mask)
|
| 485 |
e_in = _point_in_mask(end, valid_mask)
|
| 486 |
-
direction =
|
| 487 |
-
if direction == "outside":
|
| 488 |
-
direction = _infer_outside_direction_from_motion(start, end, valid_mask.shape[:2])
|
| 489 |
|
| 490 |
accepted = not reject_reasons
|
| 491 |
if not accepted:
|
|
@@ -565,6 +580,7 @@ def _auto_merge_track_points(points: List[TrackPoint], tracking_cfg: Dict) -> tu
|
|
| 565 |
max_overlap_mean_dist = float(tracking_cfg.get("merge_overlap_max_mean_distance", 60.0))
|
| 566 |
min_overlap_cos = float(tracking_cfg.get("merge_overlap_min_direction_cosine", 0.8))
|
| 567 |
local_overlap_min_cos = max(0.65, min_overlap_cos - 0.15)
|
|
|
|
| 568 |
|
| 569 |
parent: Dict[int, int] = {track_id: track_id for track_id in by_track}
|
| 570 |
|
|
@@ -586,93 +602,150 @@ def _auto_merge_track_points(points: List[TrackPoint], tracking_cfg: Dict) -> tu
|
|
| 586 |
|
| 587 |
merges_applied: List[Dict] = []
|
| 588 |
track_ids = sorted(by_track.keys())
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
elif (
|
| 641 |
mean_distance <= max_overlap_mean_dist
|
| 642 |
-
and
|
| 643 |
-
and
|
| 644 |
):
|
| 645 |
-
overlap_reason = "
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
remap: Dict[int, int] = {track_id: find(track_id) for track_id in track_ids}
|
| 678 |
if all(src == dst for src, dst in remap.items()):
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import csv
|
| 4 |
+
import heapq
|
| 5 |
import json
|
| 6 |
import sys
|
| 7 |
from collections import Counter, defaultdict, deque
|
|
|
|
| 147 |
return "outside"
|
| 148 |
|
| 149 |
|
| 150 |
+
def _classify_direction_full(
|
| 151 |
+
s_in: bool,
|
| 152 |
+
e_in: bool,
|
| 153 |
+
tps: List[TrackPoint],
|
| 154 |
+
valid_mask: np.ndarray | None,
|
| 155 |
+
frame_shape: tuple[int, int] | None = None,
|
| 156 |
+
) -> str:
|
| 157 |
+
direction = _classify_direction(s_in, e_in)
|
| 158 |
+
if direction == "inside" and valid_mask is not None and len(tps) > 2:
|
| 159 |
+
for tp in tps[1:-1]:
|
| 160 |
+
if not _point_in_mask(tp, valid_mask):
|
| 161 |
+
direction = "exit"
|
| 162 |
+
break
|
| 163 |
+
if direction == "outside" and frame_shape is not None:
|
| 164 |
+
direction = _infer_outside_direction_from_motion(tps[0], tps[-1], frame_shape)
|
| 165 |
+
return direction
|
| 166 |
+
|
| 167 |
+
|
| 168 |
def _infer_outside_direction_from_motion(
|
| 169 |
start: TrackPoint,
|
| 170 |
end: TrackPoint,
|
|
|
|
| 208 |
if valid_mask is not None:
|
| 209 |
s_in = _point_in_mask(start, valid_mask)
|
| 210 |
e_in = _point_in_mask(end, valid_mask)
|
| 211 |
+
direction = _classify_direction_full(s_in, e_in, tps, valid_mask, valid_mask.shape[:2])
|
|
|
|
|
|
|
| 212 |
else:
|
| 213 |
s_in = None
|
| 214 |
e_in = None
|
|
|
|
| 494 |
if require_start_or_end_in_valid_region and gate_mask is not None:
|
| 495 |
s_in = _point_in_mask(start, gate_mask)
|
| 496 |
e_in = _point_in_mask(end, gate_mask)
|
| 497 |
+
direction = _classify_direction_full(s_in, e_in, track_points, valid_mask)
|
| 498 |
if not (s_in or e_in):
|
| 499 |
reject_reasons.append("valid_region_gate")
|
| 500 |
elif valid_mask is not None:
|
| 501 |
s_in = _point_in_mask(start, valid_mask)
|
| 502 |
e_in = _point_in_mask(end, valid_mask)
|
| 503 |
+
direction = _classify_direction_full(s_in, e_in, track_points, valid_mask, valid_mask.shape[:2])
|
|
|
|
|
|
|
| 504 |
|
| 505 |
accepted = not reject_reasons
|
| 506 |
if not accepted:
|
|
|
|
| 580 |
max_overlap_mean_dist = float(tracking_cfg.get("merge_overlap_max_mean_distance", 60.0))
|
| 581 |
min_overlap_cos = float(tracking_cfg.get("merge_overlap_min_direction_cosine", 0.8))
|
| 582 |
local_overlap_min_cos = max(0.65, min_overlap_cos - 0.15)
|
| 583 |
+
proximity_override_dist = float(tracking_cfg.get("merge_overlap_proximity_override_distance", 0.0))
|
| 584 |
|
| 585 |
parent: Dict[int, int] = {track_id: track_id for track_id in by_track}
|
| 586 |
|
|
|
|
| 602 |
|
| 603 |
merges_applied: List[Dict] = []
|
| 604 |
track_ids = sorted(by_track.keys())
|
| 605 |
+
|
| 606 |
+
track_data: Dict[int, Dict] = {}
|
| 607 |
+
for tid in track_ids:
|
| 608 |
+
pts = by_track[tid]
|
| 609 |
+
start_vec, end_vec = _track_edge_vectors(pts)
|
| 610 |
+
track_data[tid] = {
|
| 611 |
+
"start": pts[0],
|
| 612 |
+
"end": pts[-1],
|
| 613 |
+
"start_vec": start_vec,
|
| 614 |
+
"end_vec": end_vec,
|
| 615 |
+
"frames": {p.frame: p for p in pts},
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
n_tracks = len(track_ids)
|
| 619 |
+
start_frames = [track_data[tid]["start"].frame for tid in track_ids]
|
| 620 |
+
end_frames = [track_data[tid]["end"].frame for tid in track_ids]
|
| 621 |
+
start_x = [track_data[tid]["start"].x for tid in track_ids]
|
| 622 |
+
start_y = [track_data[tid]["start"].y for tid in track_ids]
|
| 623 |
+
end_x = [track_data[tid]["end"].x for tid in track_ids]
|
| 624 |
+
end_y = [track_data[tid]["end"].y for tid in track_ids]
|
| 625 |
+
max_endpoint_dist_sq = max_endpoint_dist * max_endpoint_dist
|
| 626 |
+
|
| 627 |
+
sorted_positions = sorted(range(n_tracks), key=lambda idx: start_frames[idx])
|
| 628 |
+
heap: List[tuple[int, int]] = []
|
| 629 |
+
candidate_pairs: List[tuple[int, int]] = []
|
| 630 |
+
|
| 631 |
+
for b_pos in sorted_positions:
|
| 632 |
+
b_start_frame = start_frames[b_pos]
|
| 633 |
+
b_id = track_ids[b_pos]
|
| 634 |
+
|
| 635 |
+
while heap and heap[0][0] < b_start_frame - max_gap:
|
| 636 |
+
heapq.heappop(heap)
|
| 637 |
+
|
| 638 |
+
for a_end_frame, a_pos in heap:
|
| 639 |
+
a_id = track_ids[a_pos]
|
| 640 |
+
if a_end_frame < b_start_frame:
|
| 641 |
+
dx = end_x[a_pos] - start_x[b_pos]
|
| 642 |
+
dy = end_y[a_pos] - start_y[b_pos]
|
| 643 |
+
if dx * dx + dy * dy > max_endpoint_dist_sq:
|
| 644 |
+
continue
|
| 645 |
+
candidate_pairs.append((min(a_id, b_id), max(a_id, b_id)))
|
| 646 |
+
|
| 647 |
+
heapq.heappush(heap, (end_frames[b_pos], b_pos))
|
| 648 |
+
|
| 649 |
+
for track_a_id, track_b_id in candidate_pairs:
|
| 650 |
+
td_a = track_data[track_a_id]
|
| 651 |
+
td_b = track_data[track_b_id]
|
| 652 |
+
a_start = td_a["start"]
|
| 653 |
+
a_end = td_a["end"]
|
| 654 |
+
a_start_vec = td_a["start_vec"]
|
| 655 |
+
a_end_vec = td_a["end_vec"]
|
| 656 |
+
a_frames = td_a["frames"]
|
| 657 |
+
b_start = td_b["start"]
|
| 658 |
+
b_end = td_b["end"]
|
| 659 |
+
b_start_vec = td_b["start_vec"]
|
| 660 |
+
b_end_vec = td_b["end_vec"]
|
| 661 |
+
|
| 662 |
+
reason = None
|
| 663 |
+
reason_data: Dict[str, float | int] = {}
|
| 664 |
+
|
| 665 |
+
if a_end.frame < b_start.frame:
|
| 666 |
+
gap = b_start.frame - a_end.frame
|
| 667 |
+
dist = hypot(b_start.x - a_end.x, b_start.y - a_end.y)
|
| 668 |
+
if gap <= max_gap and dist <= max_endpoint_dist:
|
| 669 |
+
reason = "handoff"
|
| 670 |
+
reason_data = {"gap_frames": gap, "endpoint_distance": dist}
|
| 671 |
+
elif b_end.frame < a_start.frame:
|
| 672 |
+
gap = a_start.frame - b_end.frame
|
| 673 |
+
dist = hypot(a_start.x - b_end.x, a_start.y - b_end.y)
|
| 674 |
+
if gap <= max_gap and dist <= max_endpoint_dist:
|
| 675 |
+
reason = "handoff"
|
| 676 |
+
reason_data = {"gap_frames": gap, "endpoint_distance": dist}
|
| 677 |
+
else:
|
| 678 |
+
b_frames = td_b["frames"]
|
| 679 |
+
common_frames = sorted(set(a_frames.keys()).intersection(b_frames.keys()))
|
| 680 |
+
if len(common_frames) >= 1:
|
| 681 |
+
distances = []
|
| 682 |
+
for frame in common_frames:
|
| 683 |
+
pa = a_frames[frame]
|
| 684 |
+
pb = b_frames[frame]
|
| 685 |
+
distances.append(hypot(pa.x - pb.x, pa.y - pb.y))
|
| 686 |
+
|
| 687 |
+
mean_distance = sum(distances) / len(distances)
|
| 688 |
+
start_cos = _vector_cosine(a_start_vec, b_start_vec)
|
| 689 |
+
end_cos = _vector_cosine(a_end_vec, b_end_vec)
|
| 690 |
+
connector_cos = _vector_cosine(a_end_vec, b_start_vec)
|
| 691 |
+
global_cosines = [c for c in (start_cos, end_cos) if c is not None]
|
| 692 |
+
mean_cos = (sum(global_cosines) / len(global_cosines)) if global_cosines else None
|
| 693 |
+
|
| 694 |
+
overlap_reason = None
|
| 695 |
+
if (
|
| 696 |
+
proximity_override_dist > 0.0
|
| 697 |
+
and mean_distance <= proximity_override_dist
|
| 698 |
+
and len(common_frames) >= 1
|
| 699 |
+
):
|
| 700 |
+
overlap_reason = "overlap_proximity"
|
| 701 |
+
elif len(common_frames) >= min_overlap_common:
|
| 702 |
+
if mean_distance <= max_overlap_mean_dist and (
|
| 703 |
+
mean_cos is None or mean_cos >= min_overlap_cos or connector_cos is not None and connector_cos >= min_overlap_cos
|
| 704 |
+
):
|
| 705 |
+
overlap_reason = "overlap"
|
| 706 |
elif (
|
| 707 |
mean_distance <= max_overlap_mean_dist
|
| 708 |
+
and start_cos is not None
|
| 709 |
+
and start_cos >= local_overlap_min_cos
|
| 710 |
):
|
| 711 |
+
overlap_reason = "overlap_start_aligned"
|
| 712 |
+
elif (
|
| 713 |
+
len(common_frames) >= 1
|
| 714 |
+
and mean_distance <= max_overlap_mean_dist
|
| 715 |
+
and connector_cos is not None
|
| 716 |
+
and connector_cos >= local_overlap_min_cos
|
| 717 |
+
):
|
| 718 |
+
overlap_reason = "overlap_local"
|
| 719 |
+
|
| 720 |
+
if overlap_reason is not None:
|
| 721 |
+
direction_score = connector_cos
|
| 722 |
+
if direction_score is None:
|
| 723 |
+
direction_score = mean_cos if mean_cos is not None else 1.0
|
| 724 |
+
reason = overlap_reason
|
| 725 |
+
reason_data = {
|
| 726 |
+
"common_frames": len(common_frames),
|
| 727 |
+
"mean_distance": mean_distance,
|
| 728 |
+
"mean_direction_cosine": direction_score,
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
if reason is None:
|
| 732 |
+
continue
|
| 733 |
+
|
| 734 |
+
ra = find(track_a_id)
|
| 735 |
+
rb = find(track_b_id)
|
| 736 |
+
if ra == rb:
|
| 737 |
+
continue
|
| 738 |
+
union(track_a_id, track_b_id)
|
| 739 |
+
merged_to = min(find(track_a_id), find(track_b_id))
|
| 740 |
+
merges_applied.append(
|
| 741 |
+
{
|
| 742 |
+
"track_a": track_a_id,
|
| 743 |
+
"track_b": track_b_id,
|
| 744 |
+
"merged_to": merged_to,
|
| 745 |
+
"reason": reason,
|
| 746 |
+
**reason_data,
|
| 747 |
+
}
|
| 748 |
+
)
|
| 749 |
|
| 750 |
remap: Dict[int, int] = {track_id: find(track_id) for track_id in track_ids}
|
| 751 |
if all(src == dst for src, dst in remap.items()):
|
bat_tracker/tracker.py
CHANGED
|
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Dict, List, Tuple
|
| 5 |
|
|
|
|
|
|
|
|
|
|
| 6 |
from .detection import Detection
|
| 7 |
|
| 8 |
|
|
@@ -49,28 +52,34 @@ class GreedyTracker:
|
|
| 49 |
|
| 50 |
unmatched_track_ids = set(self._active.keys())
|
| 51 |
unmatched_det_idxs = set(range(len(detections)))
|
| 52 |
-
|
| 53 |
-
candidate_pairs: List[Tuple[float, int, int]] = []
|
| 54 |
-
max_distance_sq = self.max_distance_sq
|
| 55 |
-
for track_id, track in self._active.items():
|
| 56 |
-
dt_pred = max(1, frame_idx - track.last_frame) / self.fps
|
| 57 |
-
pred_x = track.x + track.vx * dt_pred
|
| 58 |
-
pred_y = track.y + track.vy * dt_pred
|
| 59 |
-
for det_idx, det in enumerate(detections):
|
| 60 |
-
dx = pred_x - det.x
|
| 61 |
-
dy = pred_y - det.y
|
| 62 |
-
d_sq = dx * dx + dy * dy
|
| 63 |
-
if d_sq <= max_distance_sq:
|
| 64 |
-
candidate_pairs.append((d_sq, track_id, det_idx))
|
| 65 |
-
|
| 66 |
-
candidate_pairs.sort(key=lambda t: t[0])
|
| 67 |
assignments: List[Tuple[int, int]] = []
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
for track_id, det_idx in assignments:
|
| 76 |
track = self._active[track_id]
|
|
|
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Dict, List, Tuple
|
| 5 |
|
| 6 |
+
import numpy as np
|
| 7 |
+
from scipy.optimize import linear_sum_assignment
|
| 8 |
+
|
| 9 |
from .detection import Detection
|
| 10 |
|
| 11 |
|
|
|
|
| 52 |
|
| 53 |
unmatched_track_ids = set(self._active.keys())
|
| 54 |
unmatched_det_idxs = set(range(len(detections)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
assignments: List[Tuple[int, int]] = []
|
| 56 |
+
if self._active and detections:
|
| 57 |
+
track_ids_list = list(self._active.keys())
|
| 58 |
+
n_tracks = len(track_ids_list)
|
| 59 |
+
n_dets = len(detections)
|
| 60 |
+
max_dist = self.max_distance
|
| 61 |
+
inf_cost = max_dist * 1e6
|
| 62 |
+
|
| 63 |
+
cost = np.full((n_tracks, n_dets), inf_cost, dtype=np.float64)
|
| 64 |
+
for i, track_id in enumerate(track_ids_list):
|
| 65 |
+
track = self._active[track_id]
|
| 66 |
+
dt_pred = max(1, frame_idx - track.last_frame) / self.fps
|
| 67 |
+
pred_x = track.x + track.vx * dt_pred
|
| 68 |
+
pred_y = track.y + track.vy * dt_pred
|
| 69 |
+
for j, det in enumerate(detections):
|
| 70 |
+
dx = pred_x - det.x
|
| 71 |
+
dy = pred_y - det.y
|
| 72 |
+
dist = (dx * dx + dy * dy) ** 0.5
|
| 73 |
+
if dist <= max_dist:
|
| 74 |
+
cost[i, j] = dist
|
| 75 |
+
|
| 76 |
+
row_ind, col_ind = linear_sum_assignment(cost)
|
| 77 |
+
for i, j in zip(row_ind, col_ind):
|
| 78 |
+
if cost[i, j] < inf_cost:
|
| 79 |
+
track_id = track_ids_list[i]
|
| 80 |
+
assignments.append((track_id, j))
|
| 81 |
+
unmatched_track_ids.discard(track_id)
|
| 82 |
+
unmatched_det_idxs.discard(j)
|
| 83 |
|
| 84 |
for track_id, det_idx in assignments:
|
| 85 |
track = self._active[track_id]
|
requirements.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
numpy>=1.24
|
|
|
|
| 2 |
opencv-python>=4.8
|
| 3 |
matplotlib>=3.7
|
| 4 |
PyYAML>=6.0
|
|
|
|
| 1 |
numpy>=1.24
|
| 2 |
+
scipy>=1.10
|
| 3 |
opencv-python>=4.8
|
| 4 |
matplotlib>=3.7
|
| 5 |
PyYAML>=6.0
|