diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..6df7659df6b087da954ab84d5bc2640228a91c3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# MAC +.DS_Store +.idea \ No newline at end of file diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d9d0f12305b592b0776f0ec4e77d676c85fce961 --- /dev/null +++ b/common/README.md @@ -0,0 +1,70 @@ + +# Computer Vision Common code + +This code is designed to be shared across projects + + +## Setup + +### Relative imports + +If you are using pycharm, then add [common](./) as `Sources Root` (right click on common, then `Mark Directory As` > `Sources Root`). + + + +It will enable the relative imports. + + +### Requirements + +Install the required python modules +```bash +python -m pip install -r requirements.txt +``` + +## Dataset + +### Twitch + +#### Idea + +There are a lot of video posted by RoboMaster on their [twitch channel](https://www.twitch.tv/robomaster). + +**But**, there are 2 issues to convert it to an exploitable dataset: + - Videos are very long (a dozen of hours per video) + - Only short sequences (per chunks of ~5s) are from robot views + +We found that it was possible to detect the HUD on the videos, to check if the image is a robot view or not. + +#### Setup + +##### ffmpeg + +To split the video into frames, we use ffmpeg. + +**Note:** If you get an error using ffmpeg, be sure that you installed the right python package (it should be `ffmpeg-python`, not `ffmpeg` or `python-ffmpeg`) + +###### MAC + +```bash +brew install ffmpeg +``` + +###### Windows + +You should follow [this tutorial](https://video.stackexchange.com/questions/20495/how-do-i-set-up-and-use-ffmpeg-in-windows). + + +##### TwitchLeecher + +We use [TwitchLeecher](https://github.com/Franiac/TwitchLeecher/releases) to download the videos. Unfortunately, it is only available on Windows. If you find a way to download entire videos on Mac, please update this README. + +#### Procedure to process a video + +1. First, go on the [google sheet](https://docs.google.com/spreadsheets/d/1kIrMOjcKJ8hslZoVMx1D0H7QYj9nQLFvzUAQ1U4Le-I/edit#gid=0), and choose a video that nobody already did, and put your name in the 2nd column +2. Download the video with TwitchLeecher, in **720p** +3. Rename it using the video id on twitch, and place it in [../dataset/twitch/videos](../dataset/twitch/videos) +4. Launch the python script [./research/scripts/monitor_new_twitch_frames.py](./research/scripts/monitor_new_twitch_frames.py) through PyCharm. It will check for new frames in the [raw-frames directory](../dataset/twitch/raw-frames), and move those that are a robot view. +5. Launch the second python script [./research/scripts/split_video.py](./research/scripts/split_video.py), with the video id as parameter (In Pycharm, `Run` > `Edit Configurations...`, then in parameters enter the id). + +The frames will appear in the [../dataset/twitch/robots-views](../dataset/twitch/robots-views) folder. diff --git a/common/doc/add_common_to_source_root.png b/common/doc/add_common_to_source_root.png new file mode 100644 index 0000000000000000000000000000000000000000..42ab6eb3ff91ddb09ee39760011a747f7c24bd9b Binary files /dev/null and b/common/doc/add_common_to_source_root.png differ diff --git a/common/research/__pycache__/constants.cpython-37.pyc b/common/research/__pycache__/constants.cpython-37.pyc deleted file mode 100644 index cb8a14f78b84fe640a606939105fdd40b1af32ed..0000000000000000000000000000000000000000 Binary files a/common/research/__pycache__/constants.cpython-37.pyc and /dev/null differ diff --git a/common/research/dataset/twitch/is_robot_view.py b/common/research/dataset/twitch/is_robot_view.py deleted file mode 100644 index f671e6b481c5fabe0cdeb5a1d135887e93adc5f9..0000000000000000000000000000000000000000 --- a/common/research/dataset/twitch/is_robot_view.py +++ /dev/null @@ -1,53 +0,0 @@ -from os import remove -from pathlib import Path -from shutil import move - -import numpy as np -from scipy.spatial import distance -from skimage import io -from watchdog.events import FileSystemEvent, FileSystemEventHandler -from watchdog.observers import Observer - -from research.constants import TWITCH_DSET - - -ref_image = io.imread('mask.jpg') - -_MASK = ref_image[:, :, 1] > 50 -_REF_IMG_MASKED = ref_image*_MASK[:, :, np.newaxis] -_THRESHOLD = 23 - - -def is_image_from_robot_view(path_to_image: Path) -> bool: - img = io.imread(path_to_image) - img_masked = img * _MASK[:, :, np.newaxis] - return distance.euclidean(img_masked.flatten() / 255, _REF_IMG_MASKED.flatten() / 255) < _THRESHOLD - - -class NewFrameHandler(FileSystemEventHandler): - - def __init__(self): - self.res_dir = (TWITCH_DSET / 'robots-views') - self.res_dir.mkdir(exist_ok=True, parents=True) - - def on_created(self, event: FileSystemEvent): - if event.is_directory or not event.src_path.endswith('jpg'): - return - file_path = Path(event.src_path) - if is_image_from_robot_view(file_path): - res = self.res_dir / f'{file_path.parent.name}-{file_path.name}' - move(file_path, res) - else: - remove(file_path) - - def __hash__(self): - return hash(self.__class__.__name__) - - -if __name__ == '__main__': - obs = Observer() - obs.schedule(NewFrameHandler(), str(TWITCH_DSET / 'raw-frames'), recursive=True) - obs.start() - - while 1: - pass diff --git a/common/research/dataset/twitch/make_thumbnails.py b/common/research/dataset/twitch/make_thumbnails.py index 14b46e88cff56fe16aed319ef6a5fcd046eab674..2b947d4c75506fdc474aae21061fff922c4924f4 100644 --- a/common/research/dataset/twitch/make_thumbnails.py +++ b/common/research/dataset/twitch/make_thumbnails.py @@ -25,7 +25,7 @@ class ThumbnailsGenerator: def _launch_creation(self, frames): ffmpeg. \ - input(self.video_path). \ + input(str(self.video_path)). \ filter('fps', fps=self.FPS).\ output(f"{self.output_folder}/frame_%d.jpg", frames=frames). \ run(quiet=True) @@ -47,11 +47,4 @@ class ThumbnailsGenerator: obs.start() def _get_frame_number(self): - return int(ffmpeg.probe(self.video_path)['format']['duration'].split('.')[0]) - - -if __name__ == '__main__': - _video_name = sys.argv[1] - print(f'Fragmenting video {_video_name}') - ThumbnailsGenerator(_video_name).run() - + return int(ffmpeg.probe(str(self.video_path))['format']['duration'].split('.')[0]) diff --git a/common/research/dataset/twitch/robot_view.py b/common/research/dataset/twitch/robot_view.py new file mode 100644 index 0000000000000000000000000000000000000000..25dd33b23037bf43be4c95de902328cb26d93b71 --- /dev/null +++ b/common/research/dataset/twitch/robot_view.py @@ -0,0 +1,39 @@ +from os import remove +from pathlib import Path +from shutil import move + +import numpy as np +from scipy.spatial import distance +from skimage import io +from skimage.transform import resize + +from research.constants import TWITCH_DSET + +RES_DIR: Path = TWITCH_DSET / 'robots-views' +RES_DIR.mkdir(parents=True, exist_ok=True) + +ref_image = io.imread(f'{__file__}/../mask.jpg') + +_MASK = ref_image[:, :, 1] > 50 +_REF_IMG_MASKED = ref_image*_MASK[:, :, np.newaxis] +_THRESHOLD = 23 + + +def is_image_from_robot_view(path_to_image: Path) -> bool: + img = io.imread(path_to_image) + img_masked = img * _MASK[:, :, np.newaxis] + return distance.euclidean(img_masked.flatten() / 255, _REF_IMG_MASKED.flatten() / 255) < _THRESHOLD + + +def process_image(path_to_image: Path): + if is_image_from_robot_view(path_to_image): + res_path = RES_DIR / f'{path_to_image.parent.name}-{path_to_image.name}' + move(str(path_to_image), str(res_path)) + print(f'{path_to_image.stem} is a robot view, moving it') + else: + remove(str(path_to_image)) + + +def process_all_images_in_dir(dir_path: Path): + for path_to_image in dir_path.glob('*/*.jpg'): + process_image(path_to_image) diff --git a/common/research/scripts/monitor_new_twitch_frames.py b/common/research/scripts/monitor_new_twitch_frames.py new file mode 100644 index 0000000000000000000000000000000000000000..c70a9dea55780407acc4d05240ffc20080e344bf --- /dev/null +++ b/common/research/scripts/monitor_new_twitch_frames.py @@ -0,0 +1,9 @@ +from time import sleep + +from research.constants import TWITCH_DSET +from research.dataset.twitch.robot_view import process_all_images_in_dir + +if __name__ == '__main__': + while 1: + process_all_images_in_dir(TWITCH_DSET / 'raw-frames') + sleep(.1) diff --git a/common/research/scripts/split_video.py b/common/research/scripts/split_video.py new file mode 100644 index 0000000000000000000000000000000000000000..dffc460fdd0fe89e4d52024d1cd1860a9bd16bef --- /dev/null +++ b/common/research/scripts/split_video.py @@ -0,0 +1,8 @@ +import sys + +from research.dataset.twitch.make_thumbnails import ThumbnailsGenerator + +if __name__ == '__main__': + _video_name = sys.argv[1] + print(f'Fragmenting video {_video_name}') + ThumbnailsGenerator(_video_name).run() diff --git a/dataset/twitch/raw-frames/.gitignore b/dataset/twitch/raw-frames/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c96a04f008ee21e260b28f7701595ed59e2839e3 --- /dev/null +++ b/dataset/twitch/raw-frames/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/dataset/twitch/videos/.gitignore b/dataset/twitch/videos/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c96a04f008ee21e260b28f7701595ed59e2839e3 --- /dev/null +++ b/dataset/twitch/videos/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file