Skip to content
This repository was archived by the owner on Jun 5, 2024. It is now read-only.

Commit 6ad5044

Browse files
committed
Fixed track interpolation and frame extraction for avi videos
1 parent 53574f3 commit 6ad5044

15 files changed

+1538
-108
lines changed

Diff for: diva_io/LICENSE

+674
Large diffs are not rendered by default.

Diff for: diva_io/README.md

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# DIVA IO Package
2+
3+
Version 0.2
4+
5+
Author: Lijun Yu
6+
7+
Email: lijun@lj-y.com
8+
9+
## Version History
10+
11+
* 0.2
12+
* **Real** random access in video loader.
13+
* Add annotation converter.
14+
* Warning control option.
15+
* 0.1
16+
* Initial release of video loader.
17+
18+
## Installation
19+
20+
### Integration
21+
22+
To use as a submodule in your git project, run
23+
24+
```sh
25+
git submodule add https://github.com/Lijun-Yu/diva-io.git diva_io
26+
```
27+
28+
### Requirements
29+
30+
Environment requirements are listed in `environment.yml`.
31+
For the `av` package, I recommend you install it via `conda` by
32+
33+
```sh
34+
conda install av -c conda-forge
35+
```
36+
37+
as building from `pip` would require a lot of dependencies.
38+
39+
## Video Loader
40+
41+
A robust video loader that deals with missing frames in the [MEVA dataset](http://mevadata.org).
42+
43+
This video loader is developed based on [`PyAV`](https://github.com/mikeboers/PyAV) package.
44+
The [`pims`](https://github.com/soft-matter/pims) package was also a good reference despite its compatibility issue with current `PyAV`.
45+
46+
For the videos in the MEVA, using `cv2.VideoCapture` would result in wrong frame ids as it never counts the missing frames.
47+
If you are using MEVA, I suggest you change to this video loader ASAP.
48+
49+
### Replace `cv2.VideoCapture`
50+
51+
According to my test, this video loader returns the exact same frame as `cv2.VideoCapture` unless missing frame or decoding error occured.
52+
To replace the `cv2.VideoCapture` objects in legacy codes, simply change from
53+
54+
```python
55+
import cv2
56+
cap = cv2.VideoCapture(video_path)
57+
```
58+
59+
to
60+
61+
```python
62+
from diva_io.video import VideoReader
63+
cap = VideoReader(video_path)
64+
```
65+
66+
`VideoReader.read` follows the schema of `cv2.VideoCapture.read` but automatically inserts the missing frames while reading the video.
67+
68+
### Iterator Interface
69+
70+
```python
71+
video = VideoReader(video_path)
72+
for frame in video:
73+
# frame is a diva_io.video.frame.Frame object
74+
image = frame.numpy()
75+
# image is an uint8 array in a shape of (height, width, channel[BGR])
76+
# ... Do something with the image
77+
```
78+
79+
### Random Access
80+
81+
Random access of a frame requires decoding from the nearest key frame (approximately every 60 frames for MEVA).
82+
Averagely, this introduces a constant overhead of 0.1 seconds, which is much faster than iterating from the beginning.
83+
84+
```python
85+
start_frame_id = 1500
86+
length = 100
87+
video.seek(start_frame_id)
88+
for frame in video.get_iter(length):
89+
image = frame.numpy()
90+
# ... Do something with the image
91+
```
92+
93+
### Video Properties
94+
95+
```python
96+
video.width # cap.get(cv2.CAP_PROP_FRAME_WIDTH)
97+
video.height # cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
98+
video.fps # cap.get(cv2.CAP_PROP_FPS)
99+
```
100+
101+
### Other Interfaces
102+
103+
For other usages, please see the comments in `video/reader.py`.
104+
105+
## Annotation
106+
107+
An annotation loader and converter for Kitware YML format in [meva-data-repo](https://gitlab.kitware.com/meva/meva-data-repo).
108+
109+
Clone the meva-data-repo and set
110+
111+
```python
112+
annotation_dir = 'path/to/meva-data-repo/annotation/DIVA-phase-2/MEVA/meva-annotations'
113+
```
114+
115+
### Convert Annotation
116+
117+
This is to convert the annotation from Kitware YML format to ActEV Scorer JSON format.
118+
Run the following command in shell outside the repo's director,
119+
120+
```sh
121+
python -m diva_io.annotation.converter <annotation_dir> <output_dir>
122+
```
123+
124+
### Read Annotation
125+
126+
```python
127+
from diva_io.annotation import KitwareAnnotation
128+
video_name = '2018-03-11.11-15-04.11-20-04.school.G300'
129+
annotation = KitwareAnnotation(video_name, annotation_dir)
130+
# deal with annotation.raw_data
131+
```

Diff for: diva_io/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__author__ = 'Lijun Yu'

Diff for: diva_io/annotation/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .kf1 import KitwareAnnotation

Diff for: diva_io/annotation/converter.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import json
3+
import argparse
4+
import os.path as osp
5+
from progressbar import progressbar
6+
from concurrent.futures import ProcessPoolExecutor
7+
from ..utils import get_logger
8+
from .kf1 import KitwareAnnotation
9+
10+
11+
def _get_video_list(annotation_dir):
12+
path = osp.join(annotation_dir, 'list-of-annotated-meva-clips.txt')
13+
with open(path) as f:
14+
video_list = [l.strip() for l in f][2:]
15+
return video_list
16+
17+
18+
def _worker(job):
19+
video_name, annotation_dir = job
20+
annotation = KitwareAnnotation(video_name, annotation_dir)
21+
return annotation.get_activities_official()
22+
23+
24+
def _get_official_format(video_list, annotation_dir):
25+
jobs = [(video_name, annotation_dir) for video_name in video_list]
26+
pool = ProcessPoolExecutor()
27+
activities = []
28+
for result in progressbar(pool.map(_worker, jobs)):
29+
activities.extend(result)
30+
reference = {'filesProcessed': video_list, 'activities': activities}
31+
file_index = {video_name: {'framerate': 30.0, 'selected': {0: 1, 9000: 0}}
32+
for video_name in video_list}
33+
return reference, file_index
34+
35+
36+
def _write_files(data_dict, output_dir):
37+
os.makedirs(output_dir, exist_ok=True)
38+
logger = get_logger(__name__)
39+
for filename, data in data_dict.items():
40+
path = osp.join(output_dir, filename + '.json')
41+
if osp.exists(path):
42+
logger.warning('Overwriting file %s', path)
43+
with open(path, 'w') as f:
44+
json.dump(data, f)
45+
46+
47+
def convert_annotation(annotation_dir, output_dir):
48+
video_list = _get_video_list(annotation_dir)
49+
reference, file_index = _get_official_format(video_list, annotation_dir)
50+
data_dict = {'reference': reference, 'file-index': file_index}
51+
_write_files(data_dict, output_dir)
52+
53+
54+
def main():
55+
parser = argparse.ArgumentParser(
56+
'Annotation Converter for KF1, from Kitware YML format to '
57+
'ActEV Scorer JSON format.')
58+
parser.add_argument('annotation_dir')
59+
parser.add_argument('output_dir')
60+
args = parser.parse_args()
61+
convert_annotation(args.annotation_dir, args.output_dir)
62+
63+
64+
if __name__ == "__main__":
65+
main()

Diff for: diva_io/annotation/kf1.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import yaml
2+
import os.path as osp
3+
from collections import defaultdict
4+
5+
6+
FIELDS = ['activities', 'geom', 'types']
7+
8+
9+
class KitwareAnnotation(object):
10+
11+
def __init__(self, video_name: str, annotation_dir: str):
12+
# Please explore the structure of raw_data yourself
13+
self.video_name = video_name
14+
self.raw_data = self._load_raw_data(video_name, annotation_dir)
15+
16+
def _split_meta(self, contents, key):
17+
meta = []
18+
i = 0
19+
while i < len(contents) and 'meta' in contents[i]:
20+
assert key not in contents[i]
21+
meta.append(contents[i]['meta'])
22+
i += 1
23+
data = [content[key] for content in contents[i:]]
24+
return meta, data
25+
26+
def _load_file(self, video_name, annotation_dir, field):
27+
date, time_1, time_2 = video_name.split('.')[:3]
28+
for time in [time_1, time_2]:
29+
path = osp.join(annotation_dir, date, time[:2], '%s.%s.yml' % (
30+
video_name, field))
31+
if not osp.exists(path):
32+
continue
33+
with open(path) as f:
34+
contents = yaml.load(f, Loader=yaml.FullLoader)
35+
return contents
36+
path = osp.join(annotation_dir, date, time_1[:2], '%s.%s.yml' % (
37+
video_name, field))
38+
raise FileNotFoundError(path)
39+
40+
def _load_raw_data(self, video_name, annotation_dir):
41+
raw_data = {'meta': {}}
42+
for field in FIELDS:
43+
contents = self._load_file(video_name, annotation_dir, field)
44+
key = field if field != 'activities' else 'act'
45+
raw_data['meta'][field], raw_data[field] = self._split_meta(
46+
contents, key)
47+
objs = defaultdict(dict)
48+
for obj in raw_data['geom']:
49+
obj['g0'] = [int(x) for x in obj['g0'].split()]
50+
objs[obj['id1']][obj['ts0']] = obj
51+
for obj in raw_data['types']:
52+
objs[obj['id1']]['type'] = [*obj['cset3'].keys()][0]
53+
for act in raw_data['activities']:
54+
for actor in act.get('actors', []):
55+
obj = objs[actor['id1']]
56+
geoms = []
57+
for ts in actor['timespan']:
58+
start, end = ts['tsr0']
59+
for time in range(start, end + 1):
60+
geoms.append(obj[time])
61+
actor['geoms'] = geoms
62+
actor['type'] = obj['type']
63+
return raw_data
64+
65+
def get_activities_official(self):
66+
activities = []
67+
for act in self.raw_data['activities']:
68+
act_id = act['id2']
69+
act_type = [*act['act2'].keys()][0]
70+
if act_type.startswith('empty'):
71+
continue
72+
start, end = act['timespan'][0]['tsr0']
73+
objects = []
74+
for actor in act['actors']:
75+
actor_id = actor['id1']
76+
bbox_history = {}
77+
for geom in actor['geoms']:
78+
frame_id = geom['ts0']
79+
x1, y1, x2, y2 = geom['g0']
80+
bbox_history[frame_id] = {
81+
'presenceConf': 1,
82+
'boundingBox': {
83+
'x': min(x1, x2), 'y': min(y1, y2),
84+
'w': abs(x2 - x1), 'h': abs(y2 - y1)}}
85+
for frame_id in range(start, end + 1):
86+
if frame_id not in bbox_history:
87+
bbox_history[frame_id] = {}
88+
obj = {'objectType': 'Vehicle', 'objectID': actor_id,
89+
'localization': {self.video_name: bbox_history}}
90+
objects.append(obj)
91+
activity = {
92+
'activity': act_type, 'activityID': act_id,
93+
'presenceConf': 1, 'alertFrame': start,
94+
'localization': {self.video_name: {start: 1, end + 1: 0}},
95+
'objects': objects}
96+
activities.append(activity)
97+
return activities

Diff for: diva_io/environment.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: diva_io
2+
channels:
3+
- pkgs/main
4+
- conda-forge
5+
dependencies:
6+
- python
7+
- numpy
8+
- av

Diff for: diva_io/utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .log import get_logger

Diff for: diva_io/utils/log.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import logging
2+
3+
4+
def get_logger(name, level=logging.INFO, log_file=None):
5+
logger = logging.getLogger(name)
6+
logger.setLevel(level)
7+
formatter = logging.Formatter(
8+
'%(asctime)s %(name)s %(levelname)s %(message)s')
9+
handlers = [logging.StreamHandler()]
10+
if log_file is not None:
11+
handlers.append(logging.FileHandler(log_file))
12+
for handler in handlers:
13+
handler.setLevel(level)
14+
handler.setFormatter(formatter)
15+
logger.addHandler(handler)
16+
return logger

Diff for: diva_io/video/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .reader import VideoReader

0 commit comments

Comments
 (0)