Skip to content

Commit 425ad9a

Browse files
committed
added ctmc evaluation script
1 parent 3c87a5b commit 425ad9a

6 files changed

Lines changed: 415 additions & 7 deletions

File tree

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,48 @@ The following table shows the available arguments:
288288

289289

290290
---
291+
## Usage for CTMC dataset evaluation
292+
293+
294+
The package supports evaluation of CTMC tracking
295+
results in bounding box format. The following examples are shown for an example directory that is
296+
structured in the CTMC-format as follows:
297+
298+
```bash
299+
ctmc_gt
300+
├── train
301+
│ ├── dataset_x
302+
│ │ ├── gt
303+
│ │ │ ├── gt.txt
304+
│ │ ├── TRA
305+
│ │ │ ├── man_track.txt
306+
│ ├── dataset_y
307+
│ │ ├── ...
308+
results
309+
├── train
310+
│ ├── dataset_x
311+
│ │ ├── res
312+
│ │ │ ├── res.txt
313+
│ │ ├── TRA
314+
│ │ │ ├── res_track.txt
315+
│ ├── dataset_y
316+
│ │ ├── ...
317+
```
318+
The directory ```ctmc``` contains the ground truth data. The subdirectories
319+
```dataset_x``` and ```dataset_y``` contain the data for the different
320+
datasets. Each dataset directory contains subdirectories for the sequences
321+
```gt```, ```TRA```. The files
322+
```gt.txt``` and ```man_track``` contain the ground truth bounding boxes (MotChallenge format) and the trajectories
323+
(CTC format) for the sequence.
324+
325+
The directory ```results``` contains the result data, having the same format as above, only differing in the file names
326+
```res.txt``` and ```res_track```.
327+
328+
To evaluate results against the ground truth, similar commands can be used.
329+
For example, to evaluate the sequence ```dataset_x```, run the command
330+
```bash
331+
ctc_evaluate_ctmc --gt "/ctmc_gt/train/dataset_x" --res "/results/train/dataset_x"
332+
```
291333

292334
## Notes
293335

ctc_metrics/metrics/validation/valid.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def no_empty_frames(
165165
is_valid = 1
166166
for i, f in enumerate(frames):
167167
if len(f) == 0:
168-
warnings.warn(f"Empty frame {i}.", UserWarning)
168+
warnings.warn(f"Empty frame {i}. Ok for CTMC datasets.", UserWarning)
169169
is_valid = 0
170170
return int(is_valid)
171171

@@ -217,7 +217,10 @@ def valid(
217217
# If tracks is empty, the result is invalid
218218
is_valid = no_empty_tracking_result(tracks)
219219
# Get the labels in each frame
220-
num_frames = max(tracks[:, 2].max() + 1, len(masks))
220+
if masks is not None:
221+
num_frames = max(tracks[:, 2].max() + 1, len(masks))
222+
else:
223+
num_frames = tracks[:, 2].max() + 1
221224
frames = [[] for _ in range(num_frames)]
222225
for track in tracks:
223226
label, birth, end, _ = track
@@ -232,7 +235,8 @@ def valid(
232235
# Check if end is not before birth
233236
is_valid *= valid_ends(tracks)
234237
# Check if all labels are in the frames they are used to be
235-
is_valid *= inspect_masks(frames, masks, labels_in_frames)
238+
if masks is not None:
239+
is_valid *= inspect_masks(frames, masks, labels_in_frames)
236240
# Check if frames are empty
237241
no_empty_frames(frames) # Should this make the validation irregular?
238242
return int(is_valid)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import argparse
2+
import os.path
3+
from os.path import join
4+
from ctc_metrics.metrics import valid
5+
from ctc_metrics.metrics import ALL_METRICS
6+
from ctc_metrics.utils.handle_results import print_results, store_results
7+
from ctc_metrics.utils.filesystem import parse_directories, read_tracking_file, load_ctmc_bounding_boxes
8+
from ctc_metrics.utils.representations import match_bboxes
9+
from ctc_metrics.scripts.evaluate import calculate_metrics
10+
11+
def match_computed_to_reference_masks(
12+
ref_boxes: list,
13+
comp_boxes: list,
14+
):
15+
"""
16+
Matches computed masks to reference masks.
17+
18+
Args:
19+
ref_boxes: The reference masks. A list of lists of the reference bboxes. [frame, id, x, y, w, h]
20+
comp_boxes: The computed masks. A list of lists of the computed bboxes. [frame, id, x, y, w, h]
21+
22+
23+
Returns:
24+
The results stored in a dictionary. The dictionary contains the
25+
following keys:
26+
- labels_ref: The reference labels. A list of lists containing
27+
the labels of the reference masks.
28+
- labels_comp: The computed labels. A list of lists containing
29+
the labels of the computed masks.
30+
- mapped_ref: The mapped reference labels. A list of lists
31+
containing the mapped labels of the reference masks.
32+
- mapped_comp: The mapped computed labels. A list of lists
33+
containing the mapped labels of the computed masks.
34+
- ious: The intersection over union values. A list of lists
35+
containing the intersection over union values between mapped
36+
reference and computed masks.
37+
"""
38+
labels_ref, labels_comp, mapped_ref, mapped_comp, ious = [], [], [], [], []
39+
40+
matches = [match_bboxes(*x) for x in zip(ref_boxes, comp_boxes)]
41+
for match in matches:
42+
labels_ref.append(match[0])
43+
labels_comp.append(match[1])
44+
mapped_ref.append(match[2])
45+
mapped_comp.append(match[3])
46+
ious.append(match[4])
47+
return {
48+
"labels_ref": labels_ref,
49+
"labels_comp": labels_comp,
50+
"mapped_ref": mapped_ref,
51+
"mapped_comp": mapped_comp,
52+
"ious": ious
53+
}
54+
55+
56+
def load_data(
57+
res: str,
58+
gt: str,
59+
):
60+
"""
61+
Load data that is necessary to calculate metrics from the given directories.
62+
63+
Args:
64+
res: The path to the results.
65+
gt: The path to the ground truth.
66+
trajectory_data: A flag if trajectory data is available.
67+
segmentation_data: A flag if segmentation data is available.
68+
threads: The number of threads to use. If 0, the number of threads
69+
is set to the number of available CPUs.
70+
71+
Returns:
72+
The computed tracks, the reference tracks, the trajectory data, the
73+
segmentation data, the computed masks and a flag if the results are
74+
valid.
75+
76+
"""
77+
# Read tracking files and parse mask files
78+
comp_tracking_file = join(res, "TRA", "res_track.txt")
79+
assert os.path.exists(comp_tracking_file), f"{comp_tracking_file} does not exist."
80+
comp_tracks = read_tracking_file(comp_tracking_file)
81+
ref_tracking_file = join(gt, "TRA", "man_track.txt")
82+
assert os.path.exists(ref_tracking_file), f"{ref_tracking_file} does not exist."
83+
ref_tracks = read_tracking_file(ref_tracking_file)
84+
comp_bb_file = join(res, "res", "res.txt")
85+
assert os.path.exists(comp_bb_file), f"{comp_bb_file} does not exist."
86+
comp_masks = load_ctmc_bounding_boxes(comp_bb_file)
87+
ref_bb_file = join(gt, "gt", "gt.txt")
88+
assert os.path.exists(ref_bb_file), f"{ref_bb_file} does not exist."
89+
ref_tra_masks = load_ctmc_bounding_boxes(ref_bb_file)
90+
assert len(ref_tra_masks) > 0, f"{gt}: Ground truth masks is 0!)"
91+
assert len(ref_tra_masks) == len(comp_masks), (
92+
f"{res}: Number of result masks ({len(comp_masks)}) unequal to "
93+
f"the number of ground truth masks ({len(ref_tra_masks)})!)")
94+
# Match golden truth tracking masks to result masks
95+
traj = match_computed_to_reference_masks(ref_tra_masks, comp_masks)
96+
is_valid = valid(None, comp_tracks, traj["labels_comp"])
97+
# Match golden truth segmentation masks to result masks
98+
return comp_tracks, ref_tracks, traj, comp_masks, is_valid
99+
100+
101+
def evaluate_sequence(
102+
res: str,
103+
gt: str,
104+
metrics: list = None,
105+
):
106+
"""
107+
Evaluates a single sequence.
108+
109+
Args:
110+
res: The path to the results.
111+
gt: The path to the ground truth.
112+
metrics: The metrics to evaluate.
113+
threads: The number of threads to use. If 0, the number of threads
114+
is set to the number of available CPUs.
115+
116+
Returns:
117+
The results stored in a dictionary.
118+
"""
119+
120+
print("Evaluate sequence: ", res, " with ground truth: ", gt, end="")
121+
# Verify all metrics
122+
if metrics is None:
123+
metrics = ALL_METRICS
124+
if "SEG" in metrics:
125+
metrics.remove("SEG") # SEG is not existing for CTMC
126+
127+
128+
comp_tracks, ref_tracks, traj, _, is_valid = load_data(res, gt)
129+
130+
results = calculate_metrics(
131+
comp_tracks, ref_tracks, traj, {}, metrics, is_valid)
132+
133+
print("with results: ", results, " done!")
134+
135+
return results
136+
137+
138+
def evaluate_all(
139+
res_root: str,
140+
gt_root: str,
141+
metrics: list = None,
142+
):
143+
"""
144+
Evaluate all sequences in a directory
145+
146+
Args:
147+
res_root: The root directory of the results.
148+
gt_root: The root directory of the ground truth.
149+
metrics: The metrics to evaluate.
150+
threads: The number of threads to use. If 0, the number of threads
151+
is set to the number of available CPUs.
152+
153+
Returns:
154+
The results stored in a dictionary.
155+
"""
156+
results = []
157+
ret = parse_directories(res_root, gt_root)
158+
for res, gt, name in zip(*ret):
159+
results.append([name, evaluate_sequence(res, gt, metrics)])
160+
return results
161+
162+
163+
def parse_args():
164+
""" Parse arguments """
165+
parser = argparse.ArgumentParser(description='Evaluates CTC-Sequences.')
166+
parser.add_argument('--res', type=str, required=True)
167+
parser.add_argument('--gt', type=str, required=True)
168+
parser.add_argument('-r', '--recursive', action="store_true")
169+
parser.add_argument('--csv-file', type=str, default=None)
170+
parser.add_argument('-n', '--num-threads', type=int, default=0)
171+
parser.add_argument('--valid', action="store_true")
172+
parser.add_argument('--det', action="store_true")
173+
parser.add_argument('--seg', action="store_true")
174+
parser.add_argument('--tra', action="store_true")
175+
parser.add_argument('--ct', action="store_true")
176+
parser.add_argument('--tf', action="store_true")
177+
parser.add_argument('--bc', action="store_true")
178+
parser.add_argument('--cca', action="store_true")
179+
parser.add_argument('--mota', action="store_true")
180+
parser.add_argument('--hota', action="store_true")
181+
parser.add_argument('--idf1', action="store_true")
182+
parser.add_argument('--chota', action="store_true")
183+
parser.add_argument('--mtml', action="store_true")
184+
parser.add_argument('--faf', action="store_true")
185+
parser.add_argument('--lnk', action="store_true")
186+
args = parser.parse_args()
187+
return args
188+
189+
190+
def main():
191+
"""
192+
Main function that is called when the script is executed.
193+
"""
194+
args = parse_args()
195+
# Prepare metric selection
196+
metrics = [metric for metric, flag in (
197+
("Valid", args.valid),
198+
("DET", args.det),
199+
("SEG", False),
200+
("TRA", args.tra),
201+
("CT", args.ct),
202+
("TF", args.tf),
203+
("BC", args.bc),
204+
("CCA", args.cca),
205+
("MOTA", args.mota),
206+
("HOTA", args.hota),
207+
("CHOTA", args.chota),
208+
("IDF1", args.idf1),
209+
("MTML", args.mtml),
210+
("FAF", args.faf),
211+
("LNK", args.lnk),
212+
) if flag]
213+
metrics = metrics if metrics else None
214+
# Evaluate sequence or whole directory
215+
if args.recursive:
216+
res = evaluate_all(
217+
res_root=args.res, gt_root=args.gt, metrics=metrics,
218+
)
219+
else:
220+
res = evaluate_sequence(
221+
res=args.res, gt=args.gt, metrics=metrics)
222+
# Visualize and store results
223+
print_results(res)
224+
if args.csv_file is not None:
225+
store_results(args.csv_file, res)
226+
227+
228+
if __name__ == "__main__":
229+
main()

ctc_metrics/utils/filesystem.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@
44
import numpy as np
55

66

7+
def load_ctmc_bounding_boxes(
8+
input_file: str
9+
):
10+
# Load BBoxes
11+
assert exists(input_file), f"{input_file} does not exist!"
12+
with open(input_file, "r") as f:
13+
lines = f.readlines()
14+
bboxes = [[int(y) for y in x.split(",")[0:6]] for x in lines] # Frame, ID, x, y, w, h,
15+
# Sort by frames
16+
max_frame = max([x[0] for x in bboxes])
17+
_frames = [[] for _ in range(max_frame+1)]
18+
for b in bboxes:
19+
_frames[b[0]].append(b[1:])
20+
# Sort every frame entry by its id
21+
frames = []
22+
for frame in _frames:
23+
inds = np.argsort([x[0] for x in frame])
24+
_frame = []
25+
for ind in inds:
26+
_frame.append(frame[ind])
27+
frames.append(_frame)
28+
29+
return frames
30+
31+
732
def parse_directories(
833
input_dir: str,
934
gt_dir: str = None,

0 commit comments

Comments
 (0)