From 65e14fd7faf293bb3a92cc386736fbb8990e1149 Mon Sep 17 00:00:00 2001 From: Vaibhav Balloli Date: Wed, 15 Mar 2023 16:07:32 +0530 Subject: [PATCH 1/4] Initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 32 + .gitignore | 34 ++ .gitkeep | 4 + CITATION.cff | 18 + README.md | 16 +- SUPPORT.md | 25 +- docs/Makefile | 20 + docs/about.md | 23 + docs/conf.py | 117 ++++ docs/dashboard.md | 7 + docs/index.md | 43 ++ docs/make.bat | 35 ++ docs/mapping_cli.rst | 21 + docs/mapping_cli/mapping_cli.calibration.rst | 6 + docs/mapping_cli/mapping_cli.cli.rst | 6 + .../mapping_cli/mapping_cli.config.config.rst | 6 + docs/mapping_cli/mapping_cli.config.rst | 23 + docs/mapping_cli/mapping_cli.halts.rst | 6 + docs/mapping_cli/mapping_cli.locator.rst | 6 + docs/mapping_cli/mapping_cli.main.rst | 6 + ...apping_cli.maneuvers.face_verification.rst | 6 + .../mapping_cli.maneuvers.forward_eight.rst | 6 + .../mapping_cli.maneuvers.gaze.rst | 6 + .../mapping_cli.maneuvers.incline.rst | 6 + .../mapping_cli.maneuvers.maneuver.rst | 6 + .../mapping_cli.maneuvers.marker_sequence.rst | 6 + .../mapping_cli.maneuvers.pedestrian.rst | 6 + .../mapping_cli.maneuvers.perp.rst | 6 + .../mapping_cli/mapping_cli.maneuvers.rpp.rst | 6 + docs/mapping_cli/mapping_cli.maneuvers.rst | 23 + .../mapping_cli.maneuvers.seat_belt.rst | 6 + .../mapping_cli.maneuvers.traffic.rst | 6 + docs/mapping_cli/mapping_cli.mapper.rst | 6 + docs/mapping_cli/mapping_cli.rst | 23 + docs/mapping_cli/mapping_cli.segment.rst | 6 + docs/mapping_cli/mapping_cli.utils.rst | 6 + docs/mapping_cli/mapping_cli.validation.rst | 6 + docs/modules.rst | 8 + docs/requirements.txt | 21 + docs/static/hams_dashboard.jpeg | Bin 0 -> 275576 bytes docs/tutorials/camera_calibration.md | 18 + .../camera_calibration_notebook.ipynb | 74 +++ docs/tutorials/face_verification.md | 63 ++ .../face_verification_notebook.ipynb | 166 ++++++ docs/tutorials/gaze.md | 58 ++ docs/tutorials/gaze_tutorial.ipynb | 191 ++++++ docs/tutorials/index.md | 61 ++ docs/tutorials/install.md | 13 + docs/tutorials/map_build.md | 33 ++ docs/tutorials/map_building_notebook.ipynb | 123 ++++ docs/tutorials/seatbelt.md | 61 ++ docs/tutorials/seatbelt_notebook.ipynb | 169 ++++++ docs/tutorials/segment.md | 10 + docs/tutorials/segment_notebook.ipynb | 140 +++++ docs/tutorials/trajectory_generation.ipynb | 38 ++ docs/tutorials/trajectory_generation.md | 10 + mapping_cli/__init__.py | 1 + mapping_cli/calibration.py | 189 ++++++ mapping_cli/cli.py | 131 ++++ mapping_cli/config/__init__.py | 0 mapping_cli/config/config.py | 29 + mapping_cli/halts.py | 90 +++ mapping_cli/locator.py | 308 ++++++++++ mapping_cli/main.py | 426 +++++++++++++ mapping_cli/maneuvers/__init__.py | 0 mapping_cli/maneuvers/face_verification.py | 369 ++++++++++++ mapping_cli/maneuvers/forward_eight.py | 67 +++ mapping_cli/maneuvers/gaze.py | 401 +++++++++++++ mapping_cli/maneuvers/incline.py | 345 +++++++++++ mapping_cli/maneuvers/maneuver.py | 41 ++ mapping_cli/maneuvers/marker_sequence.py | 67 +++ mapping_cli/maneuvers/pedestrian.py | 460 +++++++++++++++ mapping_cli/maneuvers/perp.py | 558 ++++++++++++++++++ mapping_cli/maneuvers/rpp.py | 463 +++++++++++++++ mapping_cli/maneuvers/seat_belt.py | 209 +++++++ mapping_cli/maneuvers/traffic.py | 460 +++++++++++++++ mapping_cli/mapper.py | 211 +++++++ mapping_cli/segment.py | 347 +++++++++++ mapping_cli/utils.py | 536 +++++++++++++++++ mapping_cli/validation.py | 89 +++ requirements.txt | 19 + setup.py | 28 + 82 files changed, 7666 insertions(+), 25 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitkeep create mode 100644 CITATION.cff create mode 100644 docs/Makefile create mode 100644 docs/about.md create mode 100644 docs/conf.py create mode 100644 docs/dashboard.md create mode 100644 docs/index.md create mode 100644 docs/make.bat create mode 100644 docs/mapping_cli.rst create mode 100644 docs/mapping_cli/mapping_cli.calibration.rst create mode 100644 docs/mapping_cli/mapping_cli.cli.rst create mode 100644 docs/mapping_cli/mapping_cli.config.config.rst create mode 100644 docs/mapping_cli/mapping_cli.config.rst create mode 100644 docs/mapping_cli/mapping_cli.halts.rst create mode 100644 docs/mapping_cli/mapping_cli.locator.rst create mode 100644 docs/mapping_cli/mapping_cli.main.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.face_verification.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.forward_eight.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.gaze.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.incline.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.maneuver.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.marker_sequence.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.pedestrian.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.perp.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.rpp.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.seat_belt.rst create mode 100644 docs/mapping_cli/mapping_cli.maneuvers.traffic.rst create mode 100644 docs/mapping_cli/mapping_cli.mapper.rst create mode 100644 docs/mapping_cli/mapping_cli.rst create mode 100644 docs/mapping_cli/mapping_cli.segment.rst create mode 100644 docs/mapping_cli/mapping_cli.utils.rst create mode 100644 docs/mapping_cli/mapping_cli.validation.rst create mode 100644 docs/modules.rst create mode 100644 docs/requirements.txt create mode 100644 docs/static/hams_dashboard.jpeg create mode 100644 docs/tutorials/camera_calibration.md create mode 100644 docs/tutorials/camera_calibration_notebook.ipynb create mode 100644 docs/tutorials/face_verification.md create mode 100644 docs/tutorials/face_verification_notebook.ipynb create mode 100644 docs/tutorials/gaze.md create mode 100644 docs/tutorials/gaze_tutorial.ipynb create mode 100644 docs/tutorials/index.md create mode 100644 docs/tutorials/install.md create mode 100644 docs/tutorials/map_build.md create mode 100644 docs/tutorials/map_building_notebook.ipynb create mode 100644 docs/tutorials/seatbelt.md create mode 100644 docs/tutorials/seatbelt_notebook.ipynb create mode 100644 docs/tutorials/segment.md create mode 100644 docs/tutorials/segment_notebook.ipynb create mode 100644 docs/tutorials/trajectory_generation.ipynb create mode 100644 docs/tutorials/trajectory_generation.md create mode 100644 mapping_cli/__init__.py create mode 100644 mapping_cli/calibration.py create mode 100644 mapping_cli/cli.py create mode 100644 mapping_cli/config/__init__.py create mode 100644 mapping_cli/config/config.py create mode 100644 mapping_cli/halts.py create mode 100644 mapping_cli/locator.py create mode 100644 mapping_cli/main.py create mode 100644 mapping_cli/maneuvers/__init__.py create mode 100644 mapping_cli/maneuvers/face_verification.py create mode 100644 mapping_cli/maneuvers/forward_eight.py create mode 100644 mapping_cli/maneuvers/gaze.py create mode 100644 mapping_cli/maneuvers/incline.py create mode 100644 mapping_cli/maneuvers/maneuver.py create mode 100644 mapping_cli/maneuvers/marker_sequence.py create mode 100644 mapping_cli/maneuvers/pedestrian.py create mode 100644 mapping_cli/maneuvers/perp.py create mode 100644 mapping_cli/maneuvers/rpp.py create mode 100644 mapping_cli/maneuvers/seat_belt.py create mode 100644 mapping_cli/maneuvers/traffic.py create mode 100644 mapping_cli/mapper.py create mode 100644 mapping_cli/segment.py create mode 100644 mapping_cli/utils.py create mode 100644 mapping_cli/validation.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5ee4642 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment information** + - OS: [e.g. Windows 11] + - Python Version: [e.g. Python3.8] + - Python Environment: [e.g. venv, conda, etc.] + +**Additional context** +Add any other context about the problem here. diff --git a/.gitignore b/.gitignore index dfcfd56..0b7b761 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,37 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# HAMS +log.out +/dist +__pycache__/ +hdf5.dll +log.* +*.out +*.pyc +docs/_build/ +*.mp4 +*.csv +*.hog +*.avi +*.txt +*.pth +*.exe +*.dll +*.yml +*.yaml +*.toml +test_*/ +out/ +data/ +*.jpg +*.png +.ipynb_checkpoints/ + +Dockerfile +docker-compose.* +GPL +binaries +output/ +*output*/ \ No newline at end of file diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..5b8ab55 --- /dev/null +++ b/.gitkeep @@ -0,0 +1,4 @@ +requirements.txt +docs/requirements.txt +docs/static/ +.github/ \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..2b23687 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,18 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Nambi" + given-names: "Akshay" +- family-names: "Mehta" + given-names: "Ishit" +- family-names: "Ghosh" + given-names: "Anurag" +- family-names: "Lingam" + given-names: "Vijay" +- family-names: "Padmanabhan" + given-names: "Venkat" +title: "ALT: Towards Automating Driver License Testing using Smartphones" +version: 0.1.0 +doi: https://doi.org/10.1145/3356250.3360037 +date-released: 2017-12-18 #TODO +url: "https://github.com/microsoft/HAMS" \ No newline at end of file diff --git a/README.md b/README.md index 5cd7cec..5e4e7b7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ -# Project +# HAMS ALT -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. +![LICENSE](https://img.shields.io/github/license/microsoft/HAMS?style=for-the-badge) [![Dashboard](https://img.shields.io/website?down_message=Dashboard%20Offline&style=for-the-badge&up_color=green&up_message=Dashboard&url=https%3A%2F%2Fhams-dashboard.westus3.cloudapp.azure.com%2F)](https://hams-dashboard.westus3.cloudapp.azure.com) [![Documentation](https://img.shields.io/badge/docs-Documentation-blue?style=for-the-badge&logo=appveyor)](https://microsoft.github.io/HAMS) -As the maintainer of this project, please make a few updates: +This project contains the core components of the Automated License Testing(ALT) system from the HAMS group at Microsoft Research, India. -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +## Installation + +1. Run `pip install git+https://github.com/microsoft/HAMS` to install the latest version +2. Downlaod the binaries from the [HAMS Releases](https://github.com/microsoft/HAMS/releases) +3. Refer to additional requirements and instructions on each of the modules in the `Tutorials` section of the [documentation](https://microsoft.github.io/HAMS). ## Contributing diff --git a/SUPPORT.md b/SUPPORT.md index 291d4d4..5dbad34 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,25 +1,16 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. -- **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - # Support +Please use the instructions below to file issues related to this repository. + ## How to file issues and get help -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. +This project uses GitHub Issues to track bugs. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug as a new Issue following the standard templates. + +For help and questions about using this project, please email [Akshay Nambi](https://www.microsoft.com/en-us/research/people/akshayn/) at: [akshayn@microsoft.com](mailto:akshayn@microsoft.com) -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. +> Note: This project is not under active development, so no new feature requests will be added. Furthermore, please expect delay in response because of the same. ## Microsoft Support Policy -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. +Support for this **PROJECT** is limited to the resources listed above. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..5f8bb9d --- /dev/null +++ b/docs/about.md @@ -0,0 +1,23 @@ +# About ALT + +Inadequate driver skills and apathy towards/lack of awareness of safe driving practices are key contributing factors for the lack of road safety. The problem is exacerbated by the fact that the license issuing system is broken in India, with an estimated 59% of licenses issued without a test, making it a significant societal concern. The challenges arise from capacity and cost constraints, and corruption that plagues the driver testing process. While there have been efforts aimed at creating instrumented tracks to automate the license test, these have been stymied by the high cost of the infrastructure (e.g., pole-mounted high-resolution cameras looking down on the tracks) and poor test coverage (e.g., inability to monitor the driver inside the vehicle). + +HAMS-based testing offers a compelling alternative. It is a low-cost and affordable system based on a windshield-mounted smartphone, though for reasons of scalability (i.e., handling a large volume of tests), we can offload computation to an onsite server or to the cloud. The view inside the vehicle also helps expand the test coverage. For instance, the test can verify that the driver taking the test is the same as the one who had registered for it (essential for protecting against impersonation), verify that the driver is wearing their seat belt (an essential safety precaution), and check whether the driver scans their mirrors before effecting a maneuver such as a lane change (an example of multimodal sensing, with inertial sensing and camera-based monitoring being employed in tandem). + +HAMS-based testing allows the entire testing process to be performed without any human intervention. A test report, together with video evidence (to substantiate the test result in case of a dispute), is produced in an automated manner within minutes of the completion of the test. This manner of testing, with the test taken by the driver alone in the vehicle (i.e., no test inspector) has proved to be a boon in the context of the physical distancing norms arising from the COVID-19 pandemic. + +## Deployments + +To roll out HAMS-based driver testing, we first partnered with the Government of Uttarakhand and the Institute of Driving and Traffic Research (IDTR), run by Maruti-Suzuki. Testing is conducted on a track and includes a range of parameters including verification of driver identity, checking of the seat belt, fine-grained trajectory tracking during maneuvers such as negotiating a roundabout and performing parallel parking, and checking on mirror scanning during lane changing. + +**HAMS-based driver license testing @ Dehradun, Uttarakhand:** [HAMS-based license testing went live at Dehradun Regional Transport Office (RTO)](https://news.microsoft.com/en-in/features/microsoft-ai-automates-drivers-license-test-india/), the capital of Uttarakhand state in July 2019. Till date, 10000+ automated tests (as of 15 Feb 2021) have been conducted, with an accuracy of 98%. The objectivity and transparency of the automated testing process has won the praise of not just the RTO staff but also the majority of the candidates, including many that failed the test. The thoroughness of HAMS-based testing is underscored by the fact that now the passing rate is only 54% compared to over 90% with the prior manual testing. + +**Scaling HAMS deployments across India:** The success in Dehradun has spurred interest in HAMS-based automated testing across India and also overseas. RFPs issued by several states have called for capabilities such as continuous driver identification, gaze tracking, and mirror scan monitoring, that were not available before HAMS. HAMS-based testing has been rolled out in IDTR Aurangabad, Bihar, and is in process of being implemented at multiple RTOs across the country. + +````{eval-rst} +.. youtube:: zFuIP5hI4yU +```` + +````{eval-rst} +.. youtube:: XtgpWXM5Hfg +```` \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..6bedbda --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,117 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +# sys.path.insert(0, os.path.abspath('~/Microsoft/OLA-Alt/HAMSApp/')) +# sys.path.insert(1, os.path.abspath('../')) +# sys.path.append(os.path.abspath('/Users/balli/Microsoft/')) +# sys.path.append(os.path.abspath('/Users/balli/Microsoft/Ola-ALT/')) +# sys.path.append(os.path.abspath('/Users/balli/Microsoft/Ola-ALT/HAMSApp/')) +# sys.path.append(os.path.abspath('/Users/balli/Microsoft/Ola-ALT/HAMSApp/ALT/')) +sys.path.append('../') +# sys.path.insert(2, os.path.abspath('../../')) +# sys.path.insert(3, os.path.abspath('../../')) + + +# -- Project information ----------------------------------------------------- + +project = 'HAMS' +copyright = '2023, Microsoft Research' +author = 'Anurag Ghosh, Harsh Vijay, Vaibhav Balloli, Jonathan Samuel, Akshay Nambi' + +# The full version, including alpha/beta/rc tags +release = '0.1' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx.ext.doctest", + "sphinx.ext.autosummary", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.autosectionlabel", + "sphinxemoji.sphinxemoji", + "breathe", + "sphinx_markdown_builder", + "sphinx_copybutton", + "jupyter_sphinx", + # "myst_nb", + "myst_parser", + "sphinx_proof", + "sphinx_design", + "sphinxcontrib.video", + "sphinx_togglebutton", + "sphinx_tabs.tabs", + "nbsphinx", + "sphinxcontrib.youtube", +] +autodoc_typehints = "description" +autodoc_class_signature = "separated" +autosummary_generate = True +autodoc_default_options = { + "members": True, + "inherited-members": True, + "show-inheritance": False, +} +autodoc_inherit_docstrings = True +myst_enable_extensions = ["colon_fence"] + +nb_execution_mode = "off" +nbsphinx_allow_errors = True +nbsphinx_execute = "never" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme_options = { + "repository_url": "https://github.com/microsoft/HAMS", + "use_repository_button": True, + "use_download_button": True, +} + +html_title = "HAMS Docs" +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = "furo" +html_theme = "sphinx_book_theme" + +# removes the .txt suffix +html_sourcelink_suffix = "" + + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["static"] +# source_suffix = ['.rst', '.md'] + +autodoc_mock_imports = ['alabaster', 'amqp', 'anacondaclient', 'anacondaproject', 'anyjson', 'apscheduler', 'asncrypto', 'astroid', 'astropy', 'atomicwrites', 'attrs', 'autobahn', 'automat', 'babel', 'backcall', 'backportsos', 'backportsshutilgetterminalsize', 'bcrypt', 'beautifulsoup', 'billiard', 'bitarray', 'bkcharts', 'bleach', 'bokeh', 'boto', 'bottleneck', 'celery', 'celluloid', 'certifi', 'cffi', 'chardet', 'click', 'cloudpickle', 'clyent', 'cognitiveface', 'colorama', 'comtypes', 'constantly', 'contextlib', 'cryptography', 'cycler', 'cython', 'cytoolz', 'dask', 'decorator', 'defusedxml', 'distributed', 'dlib', 'dnspython', 'docutils', 'entrypoints', 'etxmlfile', 'eventlet', 'fastcache', 'filelock', 'filterpy', 'flask', 'flaskbcrypt', 'flasklogin', 'flaskmarshmallow', 'flasksession', 'flasksqlacodegen', 'flasksqlalchemy', 'flaskwkhtmltopdf', 'gevent', 'greenlet', 'hpy', 'haversine', 'heapdict', 'hexdump', 'hkdf', 'htmllib', 'humanize', 'hyperlink', 'idna', 'imageio', 'imagesize', 'importlibmetadata', 'incremental', 'inflect', 'ipykernel', 'ipython', 'ipythongenutils', 'ipywidgets', 'isort', 'itsdangerous', 'jdcal', 'jedi', 'jinja', 'jsonschema', 'jupyter', 'jupyterclient', 'jupyterconsole', 'jupytercore', 'jupyterlab', 'jupyterlabserver', 'keyring', 'kiwisolver', 'kombu', 'laika', 'lazyobjectproxy', 'libusb', 'llvmlite', 'locket', 'lxml', 'markupsafe', 'marshmallow', 'marshmallowsqlalchemy', 'matplotlib', 'mccabe', 'menuinst', 'mistune', 'mklfft', 'mklrandom', 'monotonic', 'moreitertools', 'mpmath', 'msgpack', 'multipledispatch', 'mysqlconnector', 'nbconvert', 'nbformat', 'networkx', 'nltk', 'nose', 'notebook', 'numba', 'numexpr', 'numpy', 'numpydoc', 'olefile', 'opencvcontribpython', 'openpyxl', 'packaging', 'pandas', 'pandocfilters', 'parso', 'partd', 'pathpy', 'pathlib', 'patsy', 'pdfkit', 'pep', 'pickleshare', 'pillow', 'pip', 'pluggy', 'ply', 'prometheusclient', 'prompttoolkit', 'psutil', 'py', 'pycodestyle', 'pycosat', 'pycparser', 'pycrypto', 'pycurl', 'pyflakes', 'pygments', 'pyhamcrest', 'pylint', 'pymysql', 'pynacl', 'pyodbc', 'pyopenssl', 'pyparsing', 'pyquaternion', 'pyreadline', 'pyrsistent', 'pysocks', 'pytest', 'pytestarraydiff', 'pytestastropy', 'pytestdoctestplus', 'pytestopenfiles', 'pytestremotedata', 'pythondateutil', 'pytz', 'pywavelets', 'pywin', 'pywinctypes', 'pywinpty', 'pyyaml', 'pyzmq', 'qtawesome', 'qtconsole', 'qtpy', 'redis', 'requests', 'rope', 'ruamelyaml', 'scikitimage', 'scikitlearn', 'scipy', 'seaborn', 'sendtrash', 'setuptools', 'shapelypost', 'simplegeneric', 'singledispatch', 'six', 'snowballstemmer', 'sortedcollections', 'sortedcontainers', 'soupsieve', 'sphinx', 'sphinxcontribwebsupport', 'spyder', 'spyderkernels', 'sqlalchemy', 'statsmodels', 'sympy', 'tables', 'tblib', 'terminado', 'testpath', 'toolz', 'tornado', 'tqdm', 'traitlets', 'txaio', 'typedast', 'tzlocal', 'unicodecsv', 'urllib', 'vine', 'wcwidth', 'webencodings', 'werkzeug', 'wfastcgi', 'wheel', 'widgetsnbextension', 'wincertstore', 'wininetpton', 'winunicodeconsole', 'wrapt', 'xlrd', 'xlsxwriter', 'xlwings', 'xlwt', 'zict', 'zipp', 'zopeinterface', 'mysql'] + +autodoc_mock_imports += ['ffmpeg', 'flask_sqlalchemy', 'flask_bcrypt', 'flask_cors', 'flask_session', 'sqlalchemy_utils', 'cv2', 'PIL', 'torch', 'torchvision', 'reporter', 'cognitive_face', 'shapely', 'sklearn', 'moviepy', 'natsort' ,'decord', 'hydra', 'typer', 'omegaconf', 'mapping_cli'] \ No newline at end of file diff --git a/docs/dashboard.md b/docs/dashboard.md new file mode 100644 index 0000000..8e35981 --- /dev/null +++ b/docs/dashboard.md @@ -0,0 +1,7 @@ +# Dashboard + +We have a live dashboard that shows the current deployment of the HAMS ALT systems across India and an aggregate monthly statistics of the tests occuring here. +You can visit the website from [aka.ms/hams-dashboard](https://aka.ms/hams-dashboard) + + +![HAMS ALT Dashboard](static/hams_dashboard.jpeg) \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..a111ad7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,43 @@ +# Welcome to HAMS's documentation! + +![LICENSE](https://img.shields.io/github/license/microsoft/HAMS?style=for-the-badge) [![Dashboard](https://img.shields.io/website?down_message=Dashboard%20Offline&style=for-the-badge&up_color=green&up_message=Dashboard&url=https%3A%2F%2Fhams-dashboard.westus3.cloudapp.azure.com%2F)](https://hams-dashboard.westus3.cloudapp.azure.com) [![Documentation](https://img.shields.io/badge/docs-Documentation-blue?style=for-the-badge&logo=appveyor)](https://microsoft.github.io/HAMS) + +Inadequate driver skills and apathy towards/lack of awareness of safe driving practices are key contributing factors for the lack of road safety. The problem is exacerbated by the fact that the license issuing system is broken in India, with an estimated 59% of licenses issued without a test, making it a significant societal concern. The challenges arise from capacity and cost constraints, and corruption that plagues the driver testing process. While there have been efforts aimed at creating instrumented tracks to automate the license test, these have been stymied by the high cost of the infrastructure (e.g., pole-mounted high-resolution cameras looking down on the tracks) and poor test coverage (e.g., inability to monitor the driver inside the vehicle). + +HAMS-based testing offers a compelling alternative. It is a low-cost and affordable system based on a windshield-mounted smartphone, though for reasons of scalability (i.e., handling a large volume of tests), we can offload computation to an onsite server or to the cloud. The view inside the vehicle also helps expand the test coverage. For instance, the test can verify that the driver taking the test is the same as the one who had registered for it (essential for protecting against impersonation), verify that the driver is wearing their seat belt (an essential safety precaution), and check whether the driver scans their mirrors before effecting a maneuver such as a lane change (an example of multimodal sensing, with inertial sensing and camera-based monitoring being employed in tandem). + +To cite this repository, please use the following: + +```bibtex +@inproceedings{nambi2019alt, + title={ALT: towards automating driver license testing using smartphones}, + author={Nambi, Akshay Uttama and Mehta, Ishit and Ghosh, Anurag and Lingam, Vijay and Padmanabhan, Venkata N}, + booktitle={Proceedings of the 17th Conference on Embedded Networked Sensor Systems}, + pages={29--42}, + year={2019} + } +``` + +To use these code, follow the tutorials [here](tutorials/index.md). To know more about our project, you can explore the documentation using the sidebar or from down below: + +````{eval-rst} +.. toctree:: + :maxdepth: 1 + :caption: More about HAMS + + about + dashboard + +.. toctree:: + :maxdepth: 1 + :caption: USAGE + + tutorials/install + tutorials/index + +.. toctree:: + :maxdepth: 2 + :caption: REFERENCE + + modules +```` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/mapping_cli.rst b/docs/mapping_cli.rst new file mode 100644 index 0000000..d846b34 --- /dev/null +++ b/docs/mapping_cli.rst @@ -0,0 +1,21 @@ +mapping\_cli package +==================== + +Submodules +---------- + +mapping\_cli.main module +------------------------ + +.. automodule:: mapping_cli.main + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: mapping_cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/mapping_cli/mapping_cli.calibration.rst b/docs/mapping_cli/mapping_cli.calibration.rst new file mode 100644 index 0000000..ce8be71 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.calibration.rst @@ -0,0 +1,6 @@ +mapping\_cli.calibration +======================== + +.. currentmodule:: mapping_cli + +.. autodata:: calibration \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.cli.rst b/docs/mapping_cli/mapping_cli.cli.rst new file mode 100644 index 0000000..206c03a --- /dev/null +++ b/docs/mapping_cli/mapping_cli.cli.rst @@ -0,0 +1,6 @@ +mapping\_cli.cli +================ + +.. currentmodule:: mapping_cli + +.. autodata:: cli \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.config.config.rst b/docs/mapping_cli/mapping_cli.config.config.rst new file mode 100644 index 0000000..29b1781 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.config.config.rst @@ -0,0 +1,6 @@ +mapping\_cli.config.config +========================== + +.. currentmodule:: mapping_cli.config + +.. autodata:: config \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.config.rst b/docs/mapping_cli/mapping_cli.config.rst new file mode 100644 index 0000000..51bddb7 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.config.rst @@ -0,0 +1,23 @@ +mapping\_cli.config +=================== + +.. automodule:: mapping_cli.config + + + + + + + + + + + + + + + + + + + diff --git a/docs/mapping_cli/mapping_cli.halts.rst b/docs/mapping_cli/mapping_cli.halts.rst new file mode 100644 index 0000000..e4c8e47 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.halts.rst @@ -0,0 +1,6 @@ +mapping\_cli.halts +================== + +.. currentmodule:: mapping_cli + +.. autodata:: halts \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.locator.rst b/docs/mapping_cli/mapping_cli.locator.rst new file mode 100644 index 0000000..84de09e --- /dev/null +++ b/docs/mapping_cli/mapping_cli.locator.rst @@ -0,0 +1,6 @@ +mapping\_cli.locator +==================== + +.. currentmodule:: mapping_cli + +.. autodata:: locator \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.main.rst b/docs/mapping_cli/mapping_cli.main.rst new file mode 100644 index 0000000..24d06fb --- /dev/null +++ b/docs/mapping_cli/mapping_cli.main.rst @@ -0,0 +1,6 @@ +mapping\_cli.main +================= + +.. currentmodule:: mapping_cli + +.. autodata:: main \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.face_verification.rst b/docs/mapping_cli/mapping_cli.maneuvers.face_verification.rst new file mode 100644 index 0000000..724b69a --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.face_verification.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.face\_verification +========================================= + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: face_verification \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.forward_eight.rst b/docs/mapping_cli/mapping_cli.maneuvers.forward_eight.rst new file mode 100644 index 0000000..34e7fce --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.forward_eight.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.forward\_eight +===================================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: forward_eight \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.gaze.rst b/docs/mapping_cli/mapping_cli.maneuvers.gaze.rst new file mode 100644 index 0000000..9dcf58d --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.gaze.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.gaze +=========================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: gaze \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.incline.rst b/docs/mapping_cli/mapping_cli.maneuvers.incline.rst new file mode 100644 index 0000000..6ad3261 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.incline.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.incline +============================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: incline \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.maneuver.rst b/docs/mapping_cli/mapping_cli.maneuvers.maneuver.rst new file mode 100644 index 0000000..e471cab --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.maneuver.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.maneuver +=============================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: maneuver \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.marker_sequence.rst b/docs/mapping_cli/mapping_cli.maneuvers.marker_sequence.rst new file mode 100644 index 0000000..f59ac73 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.marker_sequence.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.marker\_sequence +======================================= + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: marker_sequence \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.pedestrian.rst b/docs/mapping_cli/mapping_cli.maneuvers.pedestrian.rst new file mode 100644 index 0000000..cefeed3 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.pedestrian.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.pedestrian +================================= + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: pedestrian \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.perp.rst b/docs/mapping_cli/mapping_cli.maneuvers.perp.rst new file mode 100644 index 0000000..9e846a3 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.perp.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.perp +=========================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: perp \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.rpp.rst b/docs/mapping_cli/mapping_cli.maneuvers.rpp.rst new file mode 100644 index 0000000..bbfe7dc --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.rpp.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.rpp +========================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: rpp \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.rst b/docs/mapping_cli/mapping_cli.maneuvers.rst new file mode 100644 index 0000000..d5ad989 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.rst @@ -0,0 +1,23 @@ +mapping\_cli.maneuvers +====================== + +.. automodule:: mapping_cli.maneuvers + + + + + + + + + + + + + + + + + + + diff --git a/docs/mapping_cli/mapping_cli.maneuvers.seat_belt.rst b/docs/mapping_cli/mapping_cli.maneuvers.seat_belt.rst new file mode 100644 index 0000000..a07a4b8 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.seat_belt.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.seat\_belt +================================= + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: seat_belt \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.maneuvers.traffic.rst b/docs/mapping_cli/mapping_cli.maneuvers.traffic.rst new file mode 100644 index 0000000..b1e0ebb --- /dev/null +++ b/docs/mapping_cli/mapping_cli.maneuvers.traffic.rst @@ -0,0 +1,6 @@ +mapping\_cli.maneuvers.traffic +============================== + +.. currentmodule:: mapping_cli.maneuvers + +.. autodata:: traffic \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.mapper.rst b/docs/mapping_cli/mapping_cli.mapper.rst new file mode 100644 index 0000000..17fdc6f --- /dev/null +++ b/docs/mapping_cli/mapping_cli.mapper.rst @@ -0,0 +1,6 @@ +mapping\_cli.mapper +=================== + +.. currentmodule:: mapping_cli + +.. autodata:: mapper \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.rst b/docs/mapping_cli/mapping_cli.rst new file mode 100644 index 0000000..d3f9854 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.rst @@ -0,0 +1,23 @@ +mapping\_cli +============ + +.. automodule:: mapping_cli + + + + + + + + + + + + + + + + + + + diff --git a/docs/mapping_cli/mapping_cli.segment.rst b/docs/mapping_cli/mapping_cli.segment.rst new file mode 100644 index 0000000..1edc7ec --- /dev/null +++ b/docs/mapping_cli/mapping_cli.segment.rst @@ -0,0 +1,6 @@ +mapping\_cli.segment +==================== + +.. currentmodule:: mapping_cli + +.. autodata:: segment \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.utils.rst b/docs/mapping_cli/mapping_cli.utils.rst new file mode 100644 index 0000000..f117e75 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.utils.rst @@ -0,0 +1,6 @@ +mapping\_cli.utils +================== + +.. currentmodule:: mapping_cli + +.. autodata:: utils \ No newline at end of file diff --git a/docs/mapping_cli/mapping_cli.validation.rst b/docs/mapping_cli/mapping_cli.validation.rst new file mode 100644 index 0000000..431f9e3 --- /dev/null +++ b/docs/mapping_cli/mapping_cli.validation.rst @@ -0,0 +1,6 @@ +mapping\_cli.validation +======================= + +.. currentmodule:: mapping_cli + +.. autodata:: validation \ No newline at end of file diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..b14f712 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,8 @@ +HAMS API Reference +====================== + +.. autosummary:: + :toctree: mapping_cli + :recursive: + + mapping_cli diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..9d9cdb7 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,21 @@ +breathe==4.30.0 +furo +git+https://github.com/sphinx-contrib/video +ipywidgets +jupyter-sphinx +myst-nb +myst-parser +nbsphinx +pandoc +sphinx +sphinx-autobuild +sphinx-book-theme +sphinx-copybutton +sphinx-design +sphinx-proof +sphinx-tabs +sphinx-togglebutton +sphinx_markdown_builder +sphinxemoji +sphinxcontrib-youtube +pandoc diff --git a/docs/static/hams_dashboard.jpeg b/docs/static/hams_dashboard.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b83fb390d741533841bee781ed938653830282ab GIT binary patch literal 275576 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 literal 0 HcmV?d00001 diff --git a/docs/tutorials/camera_calibration.md b/docs/tutorials/camera_calibration.md new file mode 100644 index 0000000..dc9d328 --- /dev/null +++ b/docs/tutorials/camera_calibration.md @@ -0,0 +1,18 @@ +# Camera Calibration + +Camera calibration is an important first step in using most of `HAMS` modules. To do so, first print the [calibration board present in this file](https://github.com/abakisita/camera_calibration/blob/master/aruco_marker_board.pdf). + +Click a couple of images of the printed board and place them in a folder named `calibration_images`. Additionally, **measure** the `length` of the markers and the `separation` between two adjacent markers `in cm`. + +To run the camera calibration module from the command line, + +```bash +mapping-cli.exe generate-calib +``` + +```{button-link} ./camera_calibration_notebook.ipynb +:color: primary +:shadow: + +Running the camera calibration module on the collected images. +``` \ No newline at end of file diff --git a/docs/tutorials/camera_calibration_notebook.ipynb b/docs/tutorials/camera_calibration_notebook.ipynb new file mode 100644 index 0000000..ccf93cf --- /dev/null +++ b/docs/tutorials/camera_calibration_notebook.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running the HAMS Camera Calibration Module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from mapping_cli.calibration import camera_calibration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "images_path = \"\"\n", + "phone_name = \"\"\n", + "marker_length = 3.5\n", + "marker_separation = 0.5\n", + "\n", + "output_folder = \"output\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.makedirs(output_folder, exist_ok=True)\n", + "camera_calibration(phone_name, images_path, marker_length, marker_separation, output_folder)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The calibration file is now saved in the output folder under the name `calib_.yml`" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/face_verification.md b/docs/tutorials/face_verification.md new file mode 100644 index 0000000..deda108 --- /dev/null +++ b/docs/tutorials/face_verification.md @@ -0,0 +1,63 @@ +# Face Verification + +Face verification module is used to check if the test taker that is registered is the one driving the test vehicle. + +![Azure Portal Face API](../static/face.png) + +To run this, you'll need Microsoft's [Cognitive Face Python Library](https://pypi.org/project/cognitive-face/) and an API key which you can setup from [here](https://azure.microsoft.com/en-us/products/cognitive-services/face). On your Azure Portal, head on to the Face API resource and enter the `endpoint` in the `base_url` field below and copy the `KEY 1` to the `subscription_key` below(see the screenshot above for reference). + +

+ face_verify.yml(click to open/close) + + ```yaml + base_url: "https://southcentralus.api.cognitive.microsoft.com/face/v1.0" + subscription_key: "" + calib_frame_period: 100 + test_frame_period: 100 + recog_confidence_threshold: 0.75 + video_acceptance_threshold: 0.75 + ``` +
+ +
+ Explanation of the above configuration values(click to open) + + ```{list-table} + :header-rows: 1 + + * - Parameter + - Description + - Example Value + * - base_url + - Base URL for the Cognitive Face API to access + - "https://southcentralus.api.cognitive.microsoft.com/face/v1.0" + * - subscription_key + - API Key from Azure Face API + - KEY + * - calib_frame_period + - Number of seconds of the calibration to read(int) + - 100 + * - test_frame_period + - Number of frames to skip in between successive evaluations(int) + - 100 + * - recog_confidence_threshold + - Threshold for similarity to consider a match(float) + - 0.75 + * - video_acceptance_threshold + - Successful similarity rate to consider face verification a success(float) + - 0.72 + ``` +
+ +Now, run the following command: + +```bash +python main.py --face-verify --front-video front_video.mp4 --calib-video calib_video.mp4 --config face_verify.yml --output-path results/ +``` + +```{button-link} ./face_verification_notebook.ipynb +:color: primary +:shadow: + +Running the seatbelt code on driver-facing video. +``` \ No newline at end of file diff --git a/docs/tutorials/face_verification_notebook.ipynb b/docs/tutorials/face_verification_notebook.ipynb new file mode 100644 index 0000000..44f1d11 --- /dev/null +++ b/docs/tutorials/face_verification_notebook.ipynb @@ -0,0 +1,166 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS Face Verification module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from ipywidgets import FileUpload\n", + "from omegaconf import OmegaConf\n", + "\n", + "from mapping_cli.maneuvers.face_verification import FaceVerification\n", + "from mapping_cli.config.config import Config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FileUpload\n", + "from IPython.display import display, Image\n", + "upload = FileUpload(accept='.mp4', multiple=False)\n", + "display(upload)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload the calibration video to registered the driver taking the test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"calibration.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload the video of the driver-facing camera to check if it's the registered driver that is taking the test" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"front_video.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modify the config variables depending on the need. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config = {\n", + " \n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, save the config to `face_verification.yaml`, create a directory to store the outputs named `output` and run the seabelt module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('face_verification.yaml', 'w') as f:\n", + " OmegaConf.save(OmegaConf.create(config), f)\n", + "os.makedirs('output', exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "face_verification = FaceVerification(inputs={\"fpath\": os.path.abspath('front_video.mp4'), \"calib_video\": os.path.abspath('calibration.mp4')}, config=Config('face_verification.yaml'), out_folder='output')\n", + "_, result, _ = face_verification.run()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Final Test Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Same Driver: {result}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/gaze.md b/docs/tutorials/gaze.md new file mode 100644 index 0000000..369f435 --- /dev/null +++ b/docs/tutorials/gaze.md @@ -0,0 +1,58 @@ +# Gaze Detection + +Here, you'll first need to setup [OpenFace](https://github.com/TadasBaltrusaitis/OpenFace). In order to setup `OpenFace`, follow the instructions on [their wiki](https://github.com/TadasBaltrusaitis/OpenFace/wiki) depending on your Operating system. + + +
+ seatbelt.yml(click to open/close) + + ```yaml + face_landmark_exe_path: "" + centre_threshold: 10 + left_threshold: 10 + right_threshold: 10 + maneuver: full + ``` +
+ +In the above config file, copy the path to the `FaceLandmarkVidMulti` landmark executable/binary from the installation. + +````{margin} **Note** +```{note} +If `FaceLandmarkVidMulti` already exists in the PATH variable, you don't need to enter the entire path. Just `FaceLandmarkVidMulti`(linux) or `FaceLandmarkVidMulti.exe`(Windows) is sufficient. +``` +```` + +
+ Explanation of the above configuration values(click to open) + + ```{list-table} + :header-rows: 1 + + * - Parameter + - Description + - Example Value + * - face_landmark_exe_path + - Path to the `FaceLandmarkVidMulti` executable + - "D:\\OpenFace_2.2.0_win_x64\\FaceLandmarkVidMulti.exe" + * - centre_threshold + - Number of center looking detections(integer) + - 10 + * - left_threshold + - Number of minimum left gaze detections(int) + - 10 + * - right_threshold + - Number of minimum right gaze detections(int) + - 10 + * - maneuver + - Name of the maneuver for which the gaze detection is being run on + - Traffic + ``` +
+ +```{button-link} ./gaze_notebook.ipynb +:color: primary +:shadow: + +Running the gaze detection code on the driver-facing video. +``` \ No newline at end of file diff --git a/docs/tutorials/gaze_tutorial.ipynb b/docs/tutorials/gaze_tutorial.ipynb new file mode 100644 index 0000000..d6517ec --- /dev/null +++ b/docs/tutorials/gaze_tutorial.ipynb @@ -0,0 +1,191 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS Gaze Detection module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from ipywidgets import FileUpload\n", + "from omegaconf import OmegaConf\n", + "\n", + "from mapping_cli.maneuvers.gaze import Gaze\n", + "from mapping_cli.config.config import Config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FileUpload\n", + "from IPython.display import display, Image\n", + "upload = FileUpload(accept='.mp4', multiple=False)\n", + "display(upload)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload a driver-facing test video to detect the seatbelt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"front_video.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload a driver-facing calibration video to detect the seatbelt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FileUpload\n", + "from IPython.display import display, Image\n", + "upload = FileUpload(accept='.mp4', multiple=False)\n", + "display(upload)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"calib_video.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modify the config variables depending on the need. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config = {\n", + " \"device\": \"cpu\",\n", + " \"skip_frames\": 25,\n", + " \"classifier_confidence_threshold\": 0.75,\n", + " \"detection_percentage\": 0.75,\n", + " \"model_path\": [\"models\", \"seatbelt_model.pth\"],\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, save the config to `gaze.yaml`, create a directory to store the outputs named `output` and run the gaze detection module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('gaze.yaml', 'w') as f:\n", + " OmegaConf.save(OmegaConf.create(config), f)\n", + "os.makedirs('output', exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "gaze = Gaze(inputs={\"fpath\":os.path.abspath('front_video.mp4'), \"calib_video\":os.path.abspath('calib_video.mp4')}, inertial_data=None, config=Config('gaze.yaml'), out_folder='./output')\n", + "decision, stats = gaze.run()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Vizualize the results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "display(Video(filename='output/front_gaze.mp4'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Statistics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Statistics: {stats}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md new file mode 100644 index 0000000..f804a7c --- /dev/null +++ b/docs/tutorials/index.md @@ -0,0 +1,61 @@ +# Tutorials + +::::{card-carousel} 1 + +:::{card} Camera Calibration +:link: ./camera_calibration +:link-type: doc +:text-align: center + +Running camera calibration +::: + +:::: + +::::{card-carousel} 2 + +:::{card} Map Building +:link: ./map_build +:link-type: doc +:text-align: center + +Running the Map building binary +::: + +:::{card} Trajectory Generation +:link-type: doc +:text-align: center +:link: ./trajectory_generation + +Running the face matching utility on the driver-facing video. +::: + +:::: + +::::{card-carousel} 3 + +:::{card} Segmentation +:link: ./segment +:link-type: doc +:text-align: center + +Segmenting the License Testing video based on the maneuvers. +::: + +:::{card} Face Verification +:link-type: doc +:text-align: center +:link: ./face_verification + +Running the face matching utility on the driver-facing video. +::: + +:::{card} Seatbelt +:link-type: doc +:text-align: center +:link: ./seatbelt + +Detecting seatbelt from video. +::: + +:::: \ No newline at end of file diff --git a/docs/tutorials/install.md b/docs/tutorials/install.md new file mode 100644 index 0000000..2f7ee97 --- /dev/null +++ b/docs/tutorials/install.md @@ -0,0 +1,13 @@ +# Installation + +1. Ensure you're running `Python3 >= 3.8` +2. To use the latest version, you can directly install from the GitHub repository. + ```bash + pip install git+https://github.com/microsoft/HAMS + ``` +3. Run the command below to ensure you've correctly installed the HAMS library. + ```python + import mapping_cli + print(mapping_cli.__version__) + ``` +4. Download the following files in order to run our library based on requirements mentioned in the [tutorials](../tutorials/index.md) \ No newline at end of file diff --git a/docs/tutorials/map_build.md b/docs/tutorials/map_build.md new file mode 100644 index 0000000..a62e3a3 --- /dev/null +++ b/docs/tutorials/map_build.md @@ -0,0 +1,33 @@ +# Map Building + +To setup the track, we first need to build maps of the individual maneuvers. + +First, setup the maneuver with the required aruco markers spread in such a manner that when the test taker performs the maneuver, the camera placed inside the test vehicle +is able to see at least 2 markers at all times. + +Now, collect images of the maneuver that is being tested - for best results, we recommend taking photos at approximately the level of the markers under sufficient sunlight i.e. +not too bright or not too dark. Take photos from different angles(within the maneuver), although the photos should not capture any other markers that are not part of this map. + +## File Requirements + +1. Path to the binaries / executable named `mapper_from_images`(see the `Installation` section from the sidebar to get download instructions) +2. Compile all the images of a map into a single folder - let's call this folder `images` +3. Generate the camera calibration file. To generate one, you can check the `Camera Calibration` module from the `Tutorials` section. Let's call this file `calib.yml` +4. Depending on the aruco markers setup on the map, add the dictionary. Typically, we use `TAG16h5` +5. Marker size: Size of the printed aruco markers + + +In order to directly run this module, execute the following command in your terminal + +```bash +mapping-cli.exe map +``` + +Follow the instructions below to generate and visualize the map + +```{button-link} ./map_building_notebook.ipynb +:color: primary +:shadow: + +Running the map building module on track images. +``` \ No newline at end of file diff --git a/docs/tutorials/map_building_notebook.ipynb b/docs/tutorials/map_building_notebook.ipynb new file mode 100644 index 0000000..37eb081 --- /dev/null +++ b/docs/tutorials/map_building_notebook.ipynb @@ -0,0 +1,123 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS Map Generation module" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from mapping_cli.mapper import run as mapper" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## File Requirements\n", + "\n", + "1. Path to the binaries / executable named `mapper_from_images`\n", + "2. Compile all the images of a map into a single folder - let's call this folder `images`\n", + "3. Generate the camera calibration file. To generate one, you can check the `Camera Calibration` module from the `Tutorials` section. Let's call this file `calib.yml`\n", + "4. Depending on the aruco markers setup on the map, add the dictionary. Typically, we use `TAG16h5`\n", + "5. Marker size: Size of the printed aruco markers \n", + "\n", + "Let's add these to the variables below. If you've changed the names, please change the variable values in the subsequent cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "exec_path = 'mapper_from_images'\n", + "img_folder = \"images\"\n", + "calib_file = \"calib.yml\"\n", + "aruco_dict = \"TAG16h5\"\n", + "marker_size = 29.2\n", + "\n", + "name = \"map_example\"\n", + "output_folder = \"output/\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "os.makedirs(output_folder, exist_ok=True)\n", + "mapper(exec_path, img_folder, calib_file, aruco_dict, marker_size, name, output_folder)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize the Point Cloud Map\n", + "\n", + "We'll use [Open3D](http://www.open3d.org/) to visualize the generated point cloud map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%pip install open3d\n", + "import open3d\n", + "from open3d.web_visualizer import draw" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "pcd = open3d.io.read_point_cloud(\"output/map_example.pcd\")\n", + "draw(pcd)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/seatbelt.md b/docs/tutorials/seatbelt.md new file mode 100644 index 0000000..827a3ec --- /dev/null +++ b/docs/tutorials/seatbelt.md @@ -0,0 +1,61 @@ +# Seatbelt Detection + +## Setup + +Given the driver-facing video, the seatbelt detection module uses an object detection pipeline to detect the seatbelt on the driver. + +In order to run the seatbelt module on the front facing video(let's call it `front_video.mp4`), first add a file named `seatbelt.yml` to your current folder. + +
+ seatbelt.yml(click to open/close) + + ```yaml + device: "cpu" + skip_frames: 25 + classifier_confidence_threshold: 0.75 + detection_percentage: 0.75 + model_path: ["models", "seatbelt_model.pth"] + ``` +
+ +
+ Explanation of the above configuration values(click to open) + + ```{list-table} + :header-rows: 1 + + * - Parameter + - Description + - Example Value + * - device + - Hardware for pytorch to run the model inference on + - "cpu" or "cuda:0" + * - skip_frames + - Number of frames to skip(integer) + - 25 + * - classifier_confidence_threshold + - Threshold to classify seatbelt detection(float) + - 0.75 + * - detection_percentage + - Percentage number of detections to consider the test as pass(float) + - 0.75 + * - model_path + - path to the saved model. Format: ['directory', 'file_name'] + - ["models", "seatbelt_model.pth"] + ``` +
+ +Now, run the following command: + +```bash +python main.py --seat-belt front_video.mp4 --config seatbelt.yml --output-path results/ +``` + +The following notebook has a code-walkthrough to run the Seatbelt module and visualize the results: + +```{button-link} ./seatbelt_notebook.ipynb +:color: primary +:shadow: + +Running the seatbelt code on driver-facing video. +``` \ No newline at end of file diff --git a/docs/tutorials/seatbelt_notebook.ipynb b/docs/tutorials/seatbelt_notebook.ipynb new file mode 100644 index 0000000..93e9434 --- /dev/null +++ b/docs/tutorials/seatbelt_notebook.ipynb @@ -0,0 +1,169 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS SeatBelt module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from ipywidgets import FileUpload\n", + "from omegaconf import OmegaConf\n", + "\n", + "from mapping_cli.maneuvers.seat_belt import SeatBelt\n", + "from mapping_cli.config.config import Config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FileUpload\n", + "from IPython.display import display, Image\n", + "upload = FileUpload(accept='.mp4', multiple=False)\n", + "display(upload)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload a driver-facing video to detect the seatbelt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"front_video.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modify the config variables depending on the need. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config = {\n", + " \"device\": \"cpu\",\n", + " \"skip_frames\": 25,\n", + " \"classifier_confidence_threshold\": 0.75,\n", + " \"detection_percentage\": 0.75,\n", + " \"model_path\": [\"models\", \"seatbelt_model.pth\"],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, save the config to `seatbelt.yaml`, create a directory to store the outputs named `output` and run the seabelt module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('seatbelt.yaml', 'w') as f:\n", + " OmegaConf.save(OmegaConf.create(config), f)\n", + "os.makedirs('output', exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "seatbelt = SeatBelt(inputs={\"fpath\":os.path.abspath('front_video.mp4')}, inertial_data=None, config=Config('seatbelt.yaml'), out_folder='./output')\n", + "_, found_belt, _ = seatbelt.run()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize the Outputs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if found_belt:\n", + " display(Image(filename='output/seatbelt_image.jpg'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Final Test Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import ast\n", + "with open('output/report.txt', 'r') as f:\n", + " report = json.load(f) \n", + " print(\"Pass: \", ast.literal_eval(report['Seatbelt'])[1])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/segment.md b/docs/tutorials/segment.md new file mode 100644 index 0000000..9ef3988 --- /dev/null +++ b/docs/tutorials/segment.md @@ -0,0 +1,10 @@ +# Segmentation + +Segmentation + +```{button-link} ./segment_notebook.ipynb +:color: primary +:shadow: + +Running the segmentation code on track-facing video. +``` \ No newline at end of file diff --git a/docs/tutorials/segment_notebook.ipynb b/docs/tutorials/segment_notebook.ipynb new file mode 100644 index 0000000..5c37b43 --- /dev/null +++ b/docs/tutorials/segment_notebook.ipynb @@ -0,0 +1,140 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS Video Segmentation module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from ipywidgets import FileUpload\n", + "from omegaconf import OmegaConf\n", + "\n", + "from mapping_cli.segment import segment\n", + "from mapping_cli.config.config import Config" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import FileUpload\n", + "from IPython.display import display, Video\n", + "\n", + "upload = FileUpload(accept='.mp4', multiple=False)\n", + "display(upload)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Upload a track-facing video to segment it into its respective maneuvers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open(\"back_video.mp4\", \"wb\") as f:\n", + " f.write(upload.value[0].content)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Modify the config variables depending on the need. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "config = {}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, save the config to `site.yaml`, create a directory to store the outputs named `output` and run the segmentation module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('seatbelt.yaml', 'w') as f:\n", + " OmegaConf.save(OmegaConf.create(config), f)\n", + "os.makedirs('output', exist_ok=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "segment(None, os.path.abspath('back_video.mp4'), 'output', Config('site.yaml'))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Segmentation Results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "videos = [path for path in os.listdir('output') if '.mp4' in path]\n", + "for video in videos:\n", + " display(Video(video))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/trajectory_generation.ipynb b/docs/tutorials/trajectory_generation.ipynb new file mode 100644 index 0000000..8303e53 --- /dev/null +++ b/docs/tutorials/trajectory_generation.ipynb @@ -0,0 +1,38 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running HAMS Trajectory Generation module" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hams", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.16 (default, Jan 17 2023, 22:25:28) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "4ecb028c9e4ec612609cae8571d5e3a76bf96bee660effba276681a0b0090bd9" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/trajectory_generation.md b/docs/tutorials/trajectory_generation.md new file mode 100644 index 0000000..70eedfd --- /dev/null +++ b/docs/tutorials/trajectory_generation.md @@ -0,0 +1,10 @@ +# Trajectory Generation + +Trajectory generation + +```{button-link} ./trajectory_generation_notebook.ipynb +:color: primary +:shadow: + +Running the trajectory generation module on the user test video. +``` \ No newline at end of file diff --git a/mapping_cli/__init__.py b/mapping_cli/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/mapping_cli/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/mapping_cli/calibration.py b/mapping_cli/calibration.py new file mode 100644 index 0000000..118c74d --- /dev/null +++ b/mapping_cli/calibration.py @@ -0,0 +1,189 @@ +""" +This code assumes that images used for calibration are of the same arUco marker board provided with code i.e. `DICT_6X6_1000` + +Credit: https://github.com/abakisita/camera_calibration +""" + +import logging +import os +from pathlib import Path + +import cv2 +import numpy as np +import yaml +from cv2 import aruco +from tqdm import tqdm + + +def camera_calibration( + phone_model: str, + calib_path: str, + marker_length: str, + marker_separation: str, + output_folder: str, +): + filename = os.path.join(output_folder, "calib_{}.yml".format(phone_model)) + markerLength = float(marker_length) + markerSeparation = float(marker_separation) + yaml_string = "%YAML:1.0 \n\ + --- \n\ + image_width: {} \n\ + image_height: {} \n\ + camera_matrix: !!opencv-matrix \n\ + rows: 3 \n\ + cols: 3 \n\ + dt: d \n\ + data: [ {}, {}, {}, {}, \n\ + {}, {}, {}, {}, {} ] \n\ + distortion_coefficients: !!opencv-matrix \n\ + rows: 1 \n\ + cols: 5 \n\ + dt: d \n\ + data: [ {}, {}, \n\ + {}, {}, \n\ + {} ]" + + # root directory of repo for relative path specification. + root = Path(__file__).parent.absolute() + + # Set this flsg True for calibrating camera and False for validating results real time + calibrate_camera = True + + # Set path to the images + calib_imgs_path = Path(calib_path) + # For validating results, show aruco board to camera. + aruco_dict = aruco.getPredefinedDictionary(aruco.DICT_6X6_1000) + + # create arUco board + board = aruco.GridBoard_create(4, 5, markerLength, markerSeparation, aruco_dict) + + """uncomment following block to draw and show the board""" + # img = board.draw((432,540)) + # cv2.imshow("aruco", img) + # cv2.waitKey() + + arucoParams = aruco.DetectorParameters_create() + + if calibrate_camera == True: + img_list = [] + calib_fnms = calib_imgs_path.glob("*.jpg") + logging.info("Using ...") + for idx, fn in enumerate(calib_fnms): + logging.info(f"{idx}, {fn}") + print("Reading: ", r"{}".format(str(os.path.join(calib_imgs_path.parent, fn)))) + img = cv2.imread(r"{}".format(str(os.path.join(calib_imgs_path.parent, fn)))) + assert img is not None + img_list.append(img) + h, w, c = img.shape + logging.info("Shape of the images is : {} , {}, {} ".format(h, w, c)) + logging.info("Calibration images") + + counter, corners_list, id_list = [], [], [] + first = True + for im in tqdm(img_list): + logging.info("In loop") + img_gray = cv2.cvtColor(im, cv2.COLOR_RGB2GRAY) + corners, ids, rejectedImgPoints = aruco.detectMarkers( + img_gray, aruco_dict, parameters=arucoParams + ) + cv2.aruco.drawDetectedMarkers(im, corners, ids) + # cv2.imshow("img", cv2.resize(im, None, fx=0.25,fy=0.25)) + # cv2.waitKey() + if first == True: + corners_list = corners + id_list = ids + first = False + else: + corners_list = np.vstack((corners_list, corners)) + id_list = np.vstack((id_list, ids)) + counter.append(len(ids)) + assert len(id_list) > 0 + print("Found {} unique markers".format(np.unique(id_list))) + + counter = np.array(counter) + logging.info("Calibrating camera .... Please wait...") + # mat = np.zeros((3,3), float) + ret, mtx, dist, rvecs, tvecs = aruco.calibrateCameraAruco( + corners_list, id_list, counter, board, img_gray.shape, None, None + ) + + logging.info( + "Camera matrix is \n", + mtx, + "\n And is stored in calibration.yaml file along with distortion coefficients : \n", + dist, + ) + + mtx = np.asarray(mtx).tolist() + dist = np.asarray(dist).tolist()[0] + logging.info("printing ", dist) + # logging.info("Printing w, h for confirmation : {}".format(w, h)) + yaml_string = yaml_string.format( + w, + h, + mtx[0][0], + mtx[0][1], + mtx[0][2], + mtx[1][0], + mtx[1][1], + mtx[1][2], + mtx[2][0], + mtx[2][1], + mtx[2][2], + dist[0], + dist[1], + dist[2], + dist[3], + dist[4], + ) + + with open(filename, "w") as text_file: + text_file.write(yaml_string) + + else: + camera = cv2.VideoCapture(0) + ret, img = camera.read() + + with open("calibration.yaml") as f: + loadeddict = yaml.load(f) + mtx = loadeddict.get("camera_matrix") + dist = loadeddict.get("dist_coeff") + mtx = np.array(mtx) + dist = np.array(dist) + + ret, img = camera.read() + img_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + h, w = img_gray.shape[:2] + newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h)) + + pose_r, pose_t = [], [] + while True: + ret, img = camera.read() + img_aruco = img + im_gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + h, w = im_gray.shape[:2] + dst = cv2.undistort(im_gray, mtx, dist, None, newcameramtx) + corners, ids, rejectedImgPoints = aruco.detectMarkers( + dst, aruco_dict, parameters=arucoParams + ) + if corners == None: + logging.info("pass") + else: + ret, rvec, tvec = aruco.estimatePoseBoard( + corners, ids, board, newcameramtx, dist + ) # For a board + logging.info("Rotation ", rvec, "Translation", tvec) + if ret != 0: + img_aruco = aruco.drawDetectedMarkers( + img, corners, ids, (0, 255, 0) + ) + # axis length 100 can be changed according to your requirement + img_aruco = aruco.drawAxis( + img_aruco, newcameramtx, dist, rvec, tvec, 10 + ) + + if cv2.waitKey(0) & 0xFF == ord("q"): + break + # cv2.imshow("World co-ordinate frame axes", img_aruco) + + # cv2.destroyAllWindows() diff --git a/mapping_cli/cli.py b/mapping_cli/cli.py new file mode 100644 index 0000000..2de7db5 --- /dev/null +++ b/mapping_cli/cli.py @@ -0,0 +1,131 @@ +import os + +import typer + +from mapping_cli import mapper +from mapping_cli.config.config import Config +from mapping_cli.maneuvers.face_verification import FaceVerification +from mapping_cli.maneuvers.gaze import Gaze +from mapping_cli.maneuvers.seat_belt import SeatBelt + +app = typer.Typer("HAMS CLI") + + +@app.command() +def map( + mapper_exe_path: str, + images_directory: str, + camera_params_path: str, + dictionary: str, + marker_size: str, + output_path: str, + cwd: str = None, +): + """Command to build a Map using the mapper exe and images + + Args: + mapper_exe_path (str): Mapper exe path. + images_directory (str): Image Directory Path. + camera_params_path (str): Camera config/param yml file path. + dictionary (str): Type of Dictionary. + marker_size (str): Size of the marker. + output_path (str): Output file name. + cwd (str): Working Directry. + """ + mapper.run( + mapper_exe_path, + images_directory, + camera_params_path, + dictionary, + marker_size, + output_path, + cwd, + ) + + +@app.command() +def error(map_file: str, dist_file: str): + """Command to get the error of map generated + + Args: + map_file (str): Map YML File Path + dist_file (str): Dist Text File Path + """ + mapper.distance_error(map_file, dist_file) + + +# @app.command() +# def find(key: str): +# typer.echo(conf.get_config_value(key)) + + +@app.command() +def seat_belt( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = None, + config=".", + config_file_name="seatbelt_config.yaml", +): + assert front_video is not None, typer.echo("Front Video Path is required") + inputs = {"fpath": front_video} + + sb = SeatBelt( + inputs, None, Config(os.path.join(config, config_file_name)), output_path + ) + percentage_detections, wearing_all_the_time, stats = sb.run() + + typer.echo(f"{percentage_detections}, {wearing_all_the_time}, {stats}") + + +@app.command() +def face_verify( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = None, + config=".", + config_file_name="face_verification.yaml", +): + assert front_video is not None, typer.echo("Front Video Path is required") + assert calib_video is not None, typer.echo("Calib Video Path is required") + inputs = { + "fpath": front_video, + "calib_video": calib_video, + } + + face_verify = FaceVerification( + inputs=inputs, + inertial_data=None, + config=Config(os.path.join(config, config_file_name)), + out_folder=output_path, + ) + face_verify.run() + + +@app.command() +def gaze( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config=".", + config_file_name="gaze", +): + assert front_video is not None, typer.echo("Front Video Path is required") + assert calib_video is not None, typer.echo("Calib Video Path is required") + inputs = { + "fpath": front_video, + "calib_video": calib_video, + } + + gaze = Gaze(inputs, None, Config(config_file_name), output_path) + gaze.run() + + +if __name__ == "__main__": + app() diff --git a/mapping_cli/config/__init__.py b/mapping_cli/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mapping_cli/config/config.py b/mapping_cli/config/config.py new file mode 100644 index 0000000..5017253 --- /dev/null +++ b/mapping_cli/config/config.py @@ -0,0 +1,29 @@ +import os + +from hydra import compose, initialize +from omegaconf import DictConfig, OmegaConf + + +class Config: + conf = None + + def __init__(self, path: str): + self.conf = OmegaConf.load(path) + + def get_config_value(self, key: str): + try: + return self.conf[key] + except: + raise KeyError(f"Cannot find the key {key} in config file") + + def __getitem__(self, key): + return self.get_config_value(key) + + def keys(self): + return self.conf.keys() + + def values(self): + return self.conf.values() + + def items(self): + return self.conf.items() diff --git a/mapping_cli/halts.py b/mapping_cli/halts.py new file mode 100644 index 0000000..3a87582 --- /dev/null +++ b/mapping_cli/halts.py @@ -0,0 +1,90 @@ +import argparse +import csv +import itertools +import os + +import matplotlib.pyplot as plt +import numpy as np +from pyquaternion import Quaternion +from scipy.signal import savgol_filter +from sklearn import decomposition + +# from mapping_cli.utils import smooth +# from mapping_cli.utils import smoothen_tr + + +def smooth_halt_signal(x, window_size=31, poly_order=2): + """ """ + print("Len: ", len(x)) + smooth_x = savgol_filter(x, window_size, poly_order) + # for i in range(5): + # smooth_x = savgol_filter(smooth_x, window_size, poly_order) + return smooth_x + + +def debug_halt_visualize( + traj, traj_1d, smooth_traj, velocity, smooth_vel, zero_frames, fname +): + """ """ + f, (ax1, ax2, ax3) = plt.subplots(1, 3) + ax1.plot(traj[:, 0], traj[:, 2], label="Original Camera Path") + ax2.plot(traj_1d, label="1D Camera Path") + ax2.plot(smooth_traj, label="Filtered Trajectory") + ax3.plot(velocity, label="Velocity") + ax3.plot(smooth_vel, label="Smooth Velocity") + ax3.scatter( + [i for i in range(zero_frames.shape[0]) if zero_frames[i]], + [smooth_vel[i] for i in range(zero_frames.shape[0]) if zero_frames[i]], + c="red", + ) + ax2.legend() + ax1.legend() + ax3.legend() + # plt.show() + + save_image_name = os.path.splitext(fname)[0] + "_halts.png" + plt.savefig(save_image_name) + + +def get_halts_worker(tx, ty, tz, vel_delta): + # if len(tx) < 199: + # # pass + # tx, ty, tz = smoothen_trajectory(tx, ty, tz, 21, 49, 2) + # else: + # tx, ty, tz = smoothen_trajectory(tx, ty, tz, 99, 199, 2) + + traj = np.array([x for x in zip(tx, ty, tz)]) + pca = decomposition.PCA(n_components=1) + traj_1d = traj + traj_1d = pca.fit_transform(traj).reshape((-1,)) + + # Smoothen trajectory + try: + smooth_traj = smooth_halt_signal(traj_1d) + except ValueError as e: + print("Trajectory length is too small!\n", e) + + # Get Velocity and smoothen it + velocity = np.abs(smooth_traj[:-vel_delta] - smooth_traj[vel_delta:]) + traj_1d = traj_1d[:-vel_delta] + smooth_traj = smooth_traj[:-vel_delta] + traj = traj[:-vel_delta] + + smooth_vel = smooth_halt_signal(velocity) + + # Get frames with no motion + zero_frames = np.zeros((traj_1d.shape[0]), dtype=bool) + zero_frames[smooth_vel <= 1e-3] = True + zero_frames[traj[:, 0] == 0] = False + + # Compensate for velocity delta + zero_frames = np.array(zero_frames.tolist() + [False] * vel_delta) + + # debug_halt_visualize(traj, traj_1d, smooth_traj, velocity, smooth_vel, zero_frames) + + return zero_frames + + +def get_halts(tx, ty, tz, vel_delta=15): + """ """ + return get_halts_worker(tx, ty, tz, vel_delta) diff --git a/mapping_cli/locator.py b/mapping_cli/locator.py new file mode 100644 index 0000000..8e88d5a --- /dev/null +++ b/mapping_cli/locator.py @@ -0,0 +1,308 @@ +import os +import subprocess +import sys + +import matplotlib +import shapely +import shapely.geometry + +matplotlib.use("Agg") +import glob +import json + +import cv2 +import matplotlib.pyplot as plt +import moviepy.video.io.ImageSequenceClip +import numpy as np +from cv2 import aruco +from natsort import natsorted + +from mapping_cli.utils import (get_marker_coord, read_aruco_traj_file, + smoothen_trajectory, yml_parser) + +DICT_CV2_ARUCO_MAP = { + "TAG16h5": cv2.aruco.DICT_APRILTAG_16h5, + "DICT_4X4_100": cv2.aruco.DICT_4X4_100, +} + + +def generate_trajectory_from_photos( + input_path: str, + maneuver: str, + map_file_path: str, + out_folder: str, + calibration: str, + size_marker: str, + aruco_test_exe: str, + cwd: str, + ignoring_points: str = "", + delete_video: bool = False, + input_extension: str = ".jpg", + framerate: int = 30, + box_plot: bool = True, + annotate: bool = True, +): + out_file = os.path.join(out_folder, "temp.mp4") + + image_files = [ + os.path.join(input_path, img) + for img in os.listdir(input_path) + if img.endswith(input_extension) + ] + image_files = natsorted(image_files) + print(image_files) + clip = moviepy.video.io.ImageSequenceClip.ImageSequenceClip( + image_files, fps=framerate + ) + clip.write_videofile(out_file) + + return get_locations( + out_file, + maneuver, + map_file_path, + out_folder, + calibration, + size_marker, + aruco_test_exe, + cwd, + ignoring_points=ignoring_points, + box_plot=box_plot, + annotate=annotate, + ) + if delete_video: + os.remove(out_file) + + +def parse_marker_set(marker_set_str): + return [int(x.strip()) for x in marker_set_str.split(",")] + + +def black_out(image_dir, out_dir, marker_set_str, marker_dict_str): + images_f = glob.glob(image_dir + "*.jpg") + dict_type = DICT_CV2_ARUCO_MAP[marker_dict_str] + aruco_dict = aruco.Dictionary_get(dict_type) + marker_set = parse_marker_set(marker_set_str) + flatten = lambda l: [item for sublist in l for item in sublist] + for image_f in images_f: + im = cv2.imread(image_f) + marker_info = cv2.aruco.detectMarkers(im, aruco_dict) + markers_in_image = flatten(marker_info[1].tolist()) + blacked_marker_ids = [ + i for i, m in enumerate(markers_in_image) if m not in marker_set + ] + if blacked_marker_ids != []: + cv2.fillPoly( + im, + pts=[marker_info[0][i][0].astype(int) for i in blacked_marker_ids], + color=(0, 0, 0), + ) + cv2.imwrite(out_dir + os.path.split(image_f)[1], im) + + +def get_locations( + input_video: str, + maneuver: str, + map_file: str, + out_folder: str, + calibration: str, + size_marker: str, + aruco_test_exe: str, + cwd: str, + ignoring_points: str = "", + plot: bool = True, + box_plot: bool = True, + smoothen: bool = True, + blacken: bool = False, + return_read: bool = False, + annotate: bool = True, +): + output_traj_path = os.path.join(out_folder, maneuver + "_CameraTrajectory.txt") + + if not os.path.exists(map_file): + map_file = os.path.join(cwd, map_file) + if not os.path.exists(calibration): + calibration = os.path.join(cwd, calibration) + + # aruco_test_exe = r"C:\Users\t-jj\Documents\Projects\electron-hams\data\aruco_binaries\aruco_test_markermap.exe" + sys.path.append(os.path.dirname(aruco_test_exe)) + exec_string = ( + f"{aruco_test_exe} {input_video} {map_file} {calibration} -s {size_marker}" + ) + + print(exec_string, cwd) + + my_env = os.environ.copy() + my_env["PATH"] = out_folder + ";" + cwd + ";" + my_env["PATH"] + + p = subprocess.Popen( + exec_string, + shell=True, + cwd=cwd, + env=my_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + # print("Hellloooo traj") + print("out: ", stdout, "\n", "err: ", stderr) + + # output = subprocess.check_output(exec_string, shell=True, cwd=out_folder) + output = stdout.decode("utf-8") + output = ( + output.replace(";\r\n", "") + .replace("]", "") + .replace("[", "") + .replace(",", "") + .replace("\r\n", "\n") + ) + + with open(output_traj_path, "w+") as f: + f.write(output) + f.close() + + plt_path = "" + json_path = "" + + if plot: + if box_plot: + plt_path, json_path = save_box_coords( + map_file, + output_traj_path, + out_folder, + maneuver, + ignoring_points, + smoothen, + annotate, + ) + else: + plt_path, json_path = save_line_coords( + map_file, output_traj_path, out_folder, maneuver, ignoring_points + ) + + if return_read: + return read_aruco_traj_file(output_traj_path, {}) + + return output_traj_path, plt_path, json_path + + +def plot_markers(markers): + markers_array = [] + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + plt.annotate(str(key), (marker_coord[0], marker_coord[1])) + markers_array.append([marker_coord[0], marker_coord[1]]) + + return markers_array + + +def save_box_coords( + manu_map, + box_trajectory, + out_folder, + manu, + ignoring_points: str = "", + smoothen: bool = False, + annotate: bool = True, +): + # inputs: mapfile, box_trajectory, out_json, manoueuvre + # output: set containing 4 corners of the box to the out_json + if len(ignoring_points) > 0: + ignoring_points_array = ignoring_points.split(",") + ignoring_points_array = list(map(int, ignoring_points_array)) + ignoring_points_dict = {i: True for i in ignoring_points_array} + + else: + ignoring_points_dict = dict() + + t_x = t_y = t_z = f_id = None + if smoothen: + f_id, t_x, t_y, t_z, _ = read_aruco_traj_file( + box_trajectory, ignoring_points_dict + ) + for k in ignoring_points_dict.keys(): + assert k not in f_id + # TODO Check window length + if len(f_id) < 99: + smooth_window = len(f_id) - 1 if len(f_id) % 2 == 0 else len(f_id) - 2 + else: + smooth_window = 99 + # smooth_window = 15 + t_x, t_y, t_z = smoothen_trajectory(t_x, t_y, t_z, smooth_window, 199, 2) + + print(ignoring_points_dict) + + # try: + # t_x, t_y, t_z = smoothen_trajectory(t_x, t_y, t_z, 3, 3, 2) + # except: + # pass + camera_coords = list(zip(t_x, t_z)) + + rect_align = shapely.geometry.MultiPoint( + camera_coords + ).minimum_rotated_rectangle # .envelope #minimum_rotated_rectangle + x, y = rect_align.exterior.coords.xy + rect_align = list(zip(x, y)) + + markers = plot_markers(yml_parser(manu_map)) + + for i in range(len(t_x)): + plt.scatter(t_x[i], t_z[i], color="green") + if annotate: + plt.annotate(str(f_id[i]), (t_x[i], t_z[i])) + for i in range(len(rect_align)): + plt.scatter(rect_align[i][0], rect_align[i][1], color="blue") + plt.gca().invert_yaxis() + + trackId = manu_map.split("/maps/")[0].split("/")[-1] + plt_path = os.path.join(out_folder, f"{manu}.png") + plt.savefig(plt_path) + + points = [list(a) for a in zip(t_x, t_z)] + value = {"box": rect_align, "markers": markers, "points": points} + json_path = os.path.join(out_folder, f"{manu}.json") + with open(json_path, "w") as f: + json.dump(value, f) + + print(rect_align) + + return plt_path, json_path + + +def save_line_coords( + manu_map, line_trajectory, out_folder, manu, ignoring_points: str = "" +): + if len(ignoring_points) > 0: + ignoring_points_array = ignoring_points.split(",") + ignoring_points_array = list(map(int, ignoring_points_array)) + ignoring_points_dict = {i: True for i in ignoring_points_array} + + else: + ignoring_points_dict = dict() + + f_id, t_x, _, t_z, _ = read_aruco_traj_file(line_trajectory, ignoring_points_dict) + line_eq = np.polyfit(t_x, t_z, 1) + + plot_markers(yml_parser(manu_map)) + for i in range(len(t_x)): + plt.scatter(t_x[i], t_z[i], color="green") + plt.annotate(str(f_id[i]), (t_x[i], t_z[i])) + + x_lim = (2, 10) + y_lim = (0, 5) + for x in np.arange(x_lim[0], x_lim[1], 0.1): + y = line_eq[0] * x + line_eq[1] + plt.scatter(x, y, color="blue") + plt.gca().invert_yaxis() + plt_path = os.path.join(out_folder, f"{manu}.png") + plt.savefig(plt_path) + + value = {"line_eq": line_eq.tolist(), "pts": {"tx": t_x, "tz": t_z}} + json_path = os.path.join(out_folder, f"{manu}.json") + with open(json_path, "w") as f: + json.dump(value, f) + + return plt_path, json_path diff --git a/mapping_cli/main.py b/mapping_cli/main.py new file mode 100644 index 0000000..5f3b579 --- /dev/null +++ b/mapping_cli/main.py @@ -0,0 +1,426 @@ +import imp +import os + +import typer + +from mapping_cli import mapper +from mapping_cli.calibration import camera_calibration +from mapping_cli.config.config import Config +from mapping_cli.locator import generate_trajectory_from_photos, get_locations +from mapping_cli.maneuvers.face_verification import FaceVerification +from mapping_cli.maneuvers.forward_eight import ForwardEight +from mapping_cli.maneuvers.gaze import Gaze +from mapping_cli.maneuvers.incline import Incline +from mapping_cli.maneuvers.marker_sequence import MarkerSequence +from mapping_cli.maneuvers.pedestrian import Pedestrian +from mapping_cli.maneuvers.perp import PERP +from mapping_cli.maneuvers.rpp import RPP +from mapping_cli.maneuvers.seat_belt import SeatBelt +from mapping_cli.maneuvers.traffic import Traffic +from mapping_cli.segment import segment as segment_test + +app = typer.Typer(name="HAMS") + + +@app.callback() +def callback(): + """ + HAMS CLI + """ + + +@app.command() +def map( + mapper_exe_path: str, + images_directory: str, + camera_params_path: str, + dictionary: str, + marker_size: str, + output_path: str, + cwd: str = None, +): + """Command to build a Map using the mapper exe and images + + Args: + mapper_exe_path (str): Mapper exe path. + images_directory (str): Image Directory Path. + camera_params_path (str): Camera config/param yml file path. + dictionary (str): Type of Dictionary. + marker_size (str): Size of the marker. + output_path (str): Output file name. + cwd (str): Working Directry. + """ + mapper.run( + mapper_exe_path, + images_directory, + camera_params_path, + dictionary, + marker_size, + output_path, + cwd, + ) + + +@app.command() +def error(map_file: str, dist_file: str): + """Command to get the error of map generated + + Args: + map_file (str): Map YML File Path + dist_file (str): Dist Text File Path + """ + mapper.distance_error(map_file, dist_file) + + +@app.command() +def get_trajectory_from_video( + input_path: str, + maneuver: str, + map_file: str, + out_folder: str, + calibration: str, + size_marker: str, + aruco_test_exe: str, + cwd: str = "", + ignoring_points: str = "", + box_plot: bool = True, +): + return get_locations( + input_path, + maneuver, + map_file, + out_folder, + calibration, + size_marker, + aruco_test_exe, + ignoring_points, + cwd=cwd if len(cwd) > 0 else out_folder, + box_plot=box_plot, + ) + + +@app.command() +def get_trajectory_from_photos( + input_path: str, + maneuver: str, + map_file: str, + out_folder: str, + calibration: str, + size_marker: str, + aruco_test_exe: str, + cwd: str = "", + ignoring_points: str = "", + box_plot: bool = True, +): + return generate_trajectory_from_photos( + input_path, + maneuver, + map_file, + out_folder, + calibration, + size_marker, + aruco_test_exe, + ignoring_points, + cwd=cwd if len(cwd) > 0 else out_folder, + box_plot=box_plot, + ) + + +@app.command() +def generate_calib( + phone_model: str, + calib_path: str, + marker_length: str, + marker_separation: str, + output_folder: str, +): + return camera_calibration( + phone_model, calib_path, marker_length, marker_separation, output_folder + ) + + +@app.command() +def seat_belt( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = None, + config_path="./mapping_cli/config/seatbelt_config.yaml", +): + assert front_video is not None, typer.echo("Front Video Path is required") + inputs = {"fpath": front_video} + + sb = SeatBelt(inputs, None, Config(config_path), output_path) + percentage_detections, wearing_all_the_time, stats = sb.run() + + typer.echo(f"{percentage_detections}, {wearing_all_the_time}, {stats}") + + media = [ + { + "title": "Seatbelt Image", + "path": os.path.join(output_path, "seatbelt_image.jpg"), + }, + ] + + return percentage_detections, wearing_all_the_time, stats, media + + +@app.command() +def face_verify( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = None, + config_path="./mapping_cli/config/face_verification.yaml", +): + assert front_video is not None, typer.echo("Front Video Path is required") + assert calib_video is not None, typer.echo("Calib Video Path is required") + inputs = { + "fpath": front_video, + "calib_video": calib_video, + } + + face_verify = FaceVerification(inputs, None, Config(config_path), output_path) + result = face_verify.run() + + media = [ + {"title": "Front Video", "path": front_video}, + {"title": "Calib Video", "path": calib_video}, + { + "title": "Face Registration", + "path": os.path.join(output_path, "face_registration.png"), + }, + { + "title": "Face Validated", + "path": os.path.join(output_path, "face_validated.png"), + }, + ] + + return result, media + + +@app.command() +def gaze( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/gaze.yaml", +): + assert front_video is not None, typer.echo("Front Video Path is required") + assert calib_video is not None, typer.echo("Calib Video Path is required") + inputs = { + "fpath": front_video, + "calib_video": calib_video, + } + + gaze = Gaze(inputs, None, Config(config_path), output_path) + decision, stats = gaze.run() + + media = [ + {"title": "Front Video", "path": front_video}, + {"title": "Calib Video", "path": calib_video}, + {"title": "Front Gaze", "path": os.path.join(output_path, "front_gaze.mp4"),}, + ] + + return decision, stats, media + + +@app.command() +def rpp( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/rpp.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + rpp = RPP(inputs, None, Config(config_path), output_path) + (decision, stats), media = rpp.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def perp( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/perp.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + perp = PERP(inputs, None, Config(config_path), output_path) + (decision, stats), media = perp.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def incline( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/incline.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + incline = Incline(inputs, None, Config(config_path), output_path) + (stats, decision), (media) = incline.run() + return decision, stats, media + + except Exception as e: + print(e) + raise e + + +@app.command() +def traffic( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/traffic.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + traffic = Traffic(inputs, None, Config(config_path), output_path) + decision, stats, media = traffic.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def pedestrian( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/pedestrian.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + pedestrian = Pedestrian(inputs, None, Config(config_path), output_path) + (decision, stats), (media) = pedestrian.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def marker_sequence( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/marker_sequence.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + marker_sequence = MarkerSequence(inputs, None, Config(config_path), output_path) + (decision, stats), (media) = marker_sequence.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def forward_eight( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/forward_eight.yaml", + cwd: str = "", +): + assert back_video is not None, typer.echo("Back Video Path is required") + try: + inputs = { + "back_video": back_video, + } + inputs["cwd"] = cwd if len(cwd) > 0 else output_path + + forward_eight = ForwardEight(inputs, None, Config(config_path), output_path) + (decision, stats), (media) = forward_eight.run() + return decision, stats, media + except Exception as e: + print(e) + raise e + + +@app.command() +def segment( + front_video: str = None, + back_video: str = None, + calib_video: str = None, + inertial_data: str = None, + output_path: str = "test_output", + config_path="./mapping_cli/config/site.yaml", +): + assert back_video is not None, typer.echo("Back video is required") + inputs = { + "back_video": back_video, + } + segment_paths, segment_warnings = segment_test( + None, back_video, output_path, Config(config_path) + ) + print(segment_paths, segment_warnings) + + return segment_paths, segment_warnings diff --git a/mapping_cli/maneuvers/__init__.py b/mapping_cli/maneuvers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mapping_cli/maneuvers/face_verification.py b/mapping_cli/maneuvers/face_verification.py new file mode 100644 index 0000000..7ef435e --- /dev/null +++ b/mapping_cli/maneuvers/face_verification.py @@ -0,0 +1,369 @@ +import logging +import os + +import cognitive_face as CF +import cv2 +from tqdm import tqdm + +from mapping_cli.maneuvers.maneuver import Maneuver + +site_config = None + + +class Person: + def __init__( + self, + vid_file, + base_url, + subscription_key, + calib_frame_period, + test_frame_period, + recog_confidence_threshold, + video_acceptance_threshold, + debug=False, + rep=None, + ): + SUBSCRIPTION_KEY = subscription_key + BASE_URL = base_url + + CF.BaseUrl.set(BASE_URL) + CF.Key.set(SUBSCRIPTION_KEY) + + self.vid_file = vid_file + + self.person_group_id = "test-persons" + self.create_group(self.person_group_id) + self.person_id = None + self.debug = debug + self.calib_frame_period = calib_frame_period + self.test_frame_period = test_frame_period + self.recog_confidence_threshold = recog_confidence_threshold + self.video_acceptance_threshold = video_acceptance_threshold + if vid_file: + self.add_video(vid_file) + + self.rep = rep + + def create_group(self, person_group_id): + exists = False + for p in CF.person_group.lists(): + if p["personGroupId"] == person_group_id: + exists = True + + if not exists: + CF.person_group.create(self.person_group_id) + + def add_face(self, im_file, persist=False): + # Persist if limit exceeded + # Ret val: 0 if success, 1 if no face, 2 if server issue + try: + if not self.person_id: + create_response = CF.person.create(self.person_group_id, "x") + self.person_id = create_response["personId"] + detect_result = CF.face.detect(im_file) + if len(detect_result) < 1: + if self.debug: + logging.info("No face detected!") + return 1, None + # idx = 0 + idx = -1 + min_left = 10000 + for i, r in enumerate(detect_result): + if r["faceRectangle"]["left"] < min_left: + idx = i + min_left = r["faceRectangle"]["left"] + # if > 1, take left most + target_str = "{},{},{},{}".format( + detect_result[idx]["faceRectangle"]["left"], + detect_result[idx]["faceRectangle"]["top"], + detect_result[idx]["faceRectangle"]["width"], + detect_result[idx]["faceRectangle"]["height"], + ) + CF.person.add_face( + im_file, self.person_group_id, self.person_id, target_face=target_str + ) + return 0, detect_result[idx]["faceRectangle"] + except Exception as e: + if self.debug: + logging.info(e) + if persist: + self.add_face(im_file, persist) + else: + return 2, None + return 2, None + + def add_video(self, vid_file): + vc = cv2.VideoCapture(vid_file) + vid_length = int(vc.get(cv2.CAP_PROP_FRAME_COUNT)) + self.reg_face = None + + if self.debug: + pbar = tqdm(total=vid_length) + pbar.set_description("Adding calib video") + + i = 0 + while True and i < vid_length: + ret, frame = vc.read() + if not ret: + return + if i % self.calib_frame_period == 0: + # TODO: Add face detection check before adding a new face + im_file = "temp_{}.jpg".format(i) + + # crop the driver because there could be people at the back who might peep + h, w, c = frame.shape + frame = frame[:, : int(0.7 * w), :] + cv2.imwrite(im_file, frame) + ret, result = self.add_face(im_file, True) + + if result is not None: + cv2.rectangle( + frame, + (result["left"], result["top"]), + ( + result["left"] + result["width"], + result["top"] + result["height"], + ), + (0, 255, 0), + 3, + ) + cv2.imwrite(im_file, frame) + + if ret == 0: + self.reg_face = frame + if os.path.exists(im_file): + os.remove(im_file) + i += 1 + if self.debug: + pbar.update(1) + + if self.debug: + pbar.close() + + def verify_face(self, im_file, persist=False): + """ + Not detected = 2 + Detected & Verified = 1 + Detected but not verified = 0 + """ + + person_id = self.person_id + + try: + detect_result = CF.face.detect(im_file) + + if len(detect_result) < 1: + return 2, -1, None + + idx = -1 + min_left = 10000 + for i, r in enumerate(detect_result): + if r["faceRectangle"]["left"] < min_left: + idx = i + min_left = r["faceRectangle"]["left"] + + face_id = detect_result[idx]["faceId"] + face_rect = detect_result[idx]["faceRectangle"] + + verify_result = CF.face.verify( + face_id, person_group_id=self.person_group_id, person_id=person_id + ) + + if ( + verify_result["isIdentical"] + and float(verify_result["confidence"]) > self.recog_confidence_threshold + ): + return 1, float(verify_result["confidence"]), face_rect + else: + return 0, float(verify_result["confidence"]), face_rect + + except Exception as e: + if self.debug: + logging.info(e) + if persist: + return self.verify_face(im_file, persist) + else: + return 2, -1, None + + def verify_video(self, vid_file, person_id=None): + self.ver_face_true = None + self.ver_face_true_conf = 0 + + self.ver_face_false = None + self.ver_face_false_conf = 1 + + self.video_log = {} + + vc = cv2.VideoCapture(vid_file) + vid_length = int(vc.get(cv2.CAP_PROP_FRAME_COUNT)) + if self.debug: + pbar = tqdm(total=vid_length) + pbar.set_description("Verifying test video") + verify_count = 0 + total_count = 0 + frame_no = 0 + + while True and frame_no < vid_length: + ret, frame = vc.read() + if not ret: + break + if frame_no % self.test_frame_period == 0: + im_file = "V_temp_{}.jpg".format(frame_no) + + # crop the driver because there could be people at the back who might peep + h, w, c = frame.shape + frame = frame[:, : int(0.7 * w), :] + cv2.imwrite(im_file, frame) + + verify_result, verify_confidence, face_rect = self.verify_face( + im_file, False + ) + + if face_rect is not None: + cv2.rectangle( + frame, + (face_rect["left"], face_rect["top"]), + ( + face_rect["left"] + face_rect["width"], + face_rect["top"] + face_rect["height"], + ), + (0, 255, 0), + 3, + ) + cv2.imwrite(im_file, frame) + + self.video_log[frame_no] = [verify_result, verify_confidence] + + if verify_result == 1: + if ( + verify_confidence > self.ver_face_true_conf + ): # get the most confident face + self.ver_face_true = frame + self.ver_face_true_conf = verify_confidence + verify_count += 1 + total_count += 1 + elif verify_result == 0: + if ( + verify_confidence < self.ver_face_false_conf + ): # get the least confident face + self.ver_face_false = frame + self.ver_face_false_conf = verify_confidence + total_count += 1 + + if os.path.exists(im_file): + os.remove(im_file) + frame_no += 1 + if self.debug: + pbar.update(1) + + if self.debug: + pbar.close() + logging.info( + "{} faces verified out of {}".format(verify_count, total_count) + ) + + self.rep.add_report("face_verify_log", self.video_log) + + if verify_count > self.video_acceptance_threshold * total_count: + self.verified = True + return True + else: + self.verified = False + return False + + def save_faces(self, out_folder): + if self.reg_face is not None: + cv2.imwrite( + os.path.join(out_folder, "face_registration.png"), self.reg_face + ) + if self.verified: + cv2.imwrite( + os.path.join(out_folder, "face_validated.png"), self.ver_face_true + ) + elif not self.verified and self.ver_face_false is not None: + cv2.imwrite( + os.path.join(out_folder, "face_validated.png"), self.ver_face_false + ) + + +def verify_two_videos( + vid_file_1, + vid_file_2, + out_folder, + base_url, + subscription_key, + calib_frame_period, + test_frame_period, + recog_confidence_threshold, + video_acceptance_threshold, + debug=True, + rep=None, +): + person = Person( + vid_file_1, + base_url, + subscription_key, + calib_frame_period, + test_frame_period, + recog_confidence_threshold, + video_acceptance_threshold, + debug=debug, + rep=rep, + ) + verified = person.verify_video(vid_file_2) + person.save_faces(out_folder) + return verified + + +def main( + calib_file, + test_file, + out_folder, + base_url, + subscription_key, + calib_frame_period, + test_frame_period, + recog_confidence_threshold, + video_acceptance_threshold, + rep, +): + if verify_two_videos( + calib_file, + test_file, + out_folder, + base_url, + subscription_key, + calib_frame_period, + test_frame_period, + recog_confidence_threshold, + video_acceptance_threshold, + debug=True, + rep=rep, + ): + out_str = "Face Verification: Pass" + if rep: + rep.add_report("face_verify", "Pass") + logging.info(out_str) + return True + else: + out_str = "Face Verification: Fail" + if rep: + rep.add_report("face_verify", "Fail") + logging.info(out_str) + return False + + +class FaceVerification(Maneuver): + def run(self): + return main( + self.inputs["calib_video"], + self.inputs["fpath"], + self.out_folder, + self.config["base_url"], + self.config["subscription_key"], + self.config["calib_frame_period"], + self.config["test_frame_period"], + self.config["recog_confidence_threshold"], + self.config["video_acceptance_threshold"], + rep=self.report, + ) diff --git a/mapping_cli/maneuvers/forward_eight.py b/mapping_cli/maneuvers/forward_eight.py new file mode 100644 index 0000000..ea6e05f --- /dev/null +++ b/mapping_cli/maneuvers/forward_eight.py @@ -0,0 +1,67 @@ +from decord import VideoReader + +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import detect_marker + + +def get_marker_from_frame(frame, marker_list, marker_dict): + markers = detect_marker(frame, marker_dict) + if len(markers) > 0: + permuted_markers = list( + filter( + lambda marker: marker in marker_list, + list(sorted([int(i) for i in markers])), + ) + ) + else: + return None + return permuted_markers + + +class ForwardEight(Maneuver): + def run(self) -> None: + vid = VideoReader(self.inputs["back_video"]) + frames = range(0, len(vid), self.config["skip_frames"]) + marker_list = self.config["marker_list"] + verification_sequence = self.config["marker_order"] + markers_detected = [] + + for i in frames: + vid.seek_accurate(i) + frame = vid.next().asnumpy() + permuted_markers = get_marker_from_frame( + frame, marker_list, self.config["marker_dict"] + ) + if permuted_markers is not None: + if isinstance(permuted_markers, (list, tuple)): + val_markers = [] + for permuted_marker in permuted_markers: + if permuted_marker not in markers_detected: + val_markers.append(permuted_marker) + + if len(val_markers) > 0: + markers_detected += val_markers + + elif permuted_markers != markers_detected[-1]: + markers_detected += list(permuted_markers) + sequence_verified = False + print("Detected: ", markers_detected) + + verified_sequence = None + + for sequence_verification in verification_sequence: + str_seq = "".join([str(i) for i in sequence_verification]) + iter_seq_verification = iter(str_seq) + str_markers = "".join([str(i) for i in markers_detected]) + res = all( + next((ele for ele in iter_seq_verification if ele == chr), None) + is not None + for chr in list(str_markers) + ) + if res and len(markers_detected) >= len(sequence_verification) - 1: + sequence_verified = True + verified_sequence = sequence_verification + break + + print("Verified: ", sequence_verified) + return (sequence_verified, markers_detected), {} diff --git a/mapping_cli/maneuvers/gaze.py b/mapping_cli/maneuvers/gaze.py new file mode 100644 index 0000000..8807c11 --- /dev/null +++ b/mapping_cli/maneuvers/gaze.py @@ -0,0 +1,401 @@ +import csv +import logging +import math +import os +import time + +import cv2 +import ffmpeg +import numpy as np +import pandas as pd + +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import euclidean_distance_batch + + +def get_eucledian_distances( + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + test_gaze_angle=[0, 0], +): + eu_dist_left = math.sqrt( + (test_gaze_angle[0] - left_gaze_angle_centroid) ** 2 + + (test_gaze_angle[1] - left_gaze_angle_centroid) ** 2 + ) + eu_dist_right = math.sqrt( + (test_gaze_angle[0] - right_gaze_angle_centroid) ** 2 + + (test_gaze_angle[1] - right_gaze_angle_centroid) ** 2 + ) + eu_dist_centre = math.sqrt( + (test_gaze_angle[0] - centre_gaze_angle_centroid) ** 2 + + (test_gaze_angle[1] - centre_gaze_angle_centroid) ** 2 + ) + eu_dist_arr = [eu_dist_left, eu_dist_right, eu_dist_centre] + return eu_dist_arr + + +def start_calib( + calib_vid_path: str, + output_path: str, + name: str, + face_landmark_exe_path: str, + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, +): + start_time = time.time() + run_openface( + calib_vid_path, output_path, f"{name}_calib.csv", face_landmark_exe_path + ) + logging.info("Finished recording calibration video...") + ( + left_gaze_angle_centroid, + centre_gaze_angle_centroid, + right_gaze_angle_centroid, + ) = kmeans_clustering( + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + os.path.join(output_path, f"{name}_calib.csv"), + calib_vid_path, + ) + logging.info("calib_time = " + str(time.time() - start_time)) + return ( + left_gaze_angle_centroid, + centre_gaze_angle_centroid, + right_gaze_angle_centroid, + ) + + +def run_openface( + input_vid_path, saveto_folder, output_filename, face_landmark_exe_path: str +): + logging.info("OpenFace: Processing video...") + call_string = "{} -f {} -out_dir {} -of {}".format( + face_landmark_exe_path, input_vid_path, saveto_folder, output_filename + ) + logging.info(call_string) + # print + # subprocess.Popen(call_string, cwd=working_dir, shell=True) + os.system(call_string) + # logging.info(x) + + +def kmeans_clustering( + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + csvfile, + calib_vid, +): + # get array in form [[gaze_x0, gaze_y0], [gaze_x1, gaze_y1]] + video = cv2.VideoCapture(calib_vid) + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + video_frame_width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH)) + f = open(csvfile, "r") + reader = csv.reader(f) + next(reader) + gaze_angles = [] + row_count = 0 + for row in reader: + row_count += 1 + if float(row[299]) > float(video_frame_width / 2): + # print("not_appending:condition1: ", row[0], row[1], row[299]) + continue + if float(row[11]) == 0.0 and float(row[12]) == 0: + continue + if row_count > 1 and not (len(gaze_angles) == 0): + if row[0] == prev_row[0]: + if float(row[299]) > float(prev_row[299]): + # print("not_appending:condition2: ", row[0], row[1], row[299]) + prev_row = row + continue + # print("appending", row[11], row[12]) + gaze_angles.append([float(row[11]), float(row[12])]) + prev_row = row + gaze_angles = np.array(gaze_angles) + # logging.info(gaze_angles.shape, total_frames) + if gaze_angles.shape[0] > (int(total_frames * 3 / 4)): + # if gaze_angles.shape[0] > 0: + dim = gaze_angles.shape[-1] # find the dimensionality of given points + k = 3 + indices = np.random.choice(gaze_angles.shape[0], k, replace=False) + centroids_curr = np.array( + [gaze_angles[i] for i in indices] + ) # randomly select any data points from the input file as current centroids + centroids_old = np.zeros(centroids_curr.shape) + error = euclidean_distance_batch(centroids_curr, centroids_old) + cumulative_error = np.sum([error[i][i] for i in range(k)]) + # Iterate until the error between centroids_old and centroids_curr converges + while not (cumulative_error == 0): + # assign cluster + distance_array = euclidean_distance_batch(gaze_angles, centroids_curr) + cluster_array = np.argmin(distance_array, axis=1) + # find new centroid + centroids_old = centroids_curr + for i in range(k): + cluster_i = np.array( + [ + gaze_angles[j] + for j in range(len(cluster_array)) + if cluster_array[j] == i + ] + ) + centroid_i = np.mean(cluster_i, axis=0) + if i == 0: + temp_centroids_curr = np.array([centroid_i]) + else: + temp_centroids_curr = np.append( + temp_centroids_curr, [centroid_i], axis=0 + ) + centroids_curr = temp_centroids_curr + # find error + error = euclidean_distance_batch(centroids_curr, centroids_old) + cumulative_error = np.sum([error[i][i] for i in range(k)]) + list_centroids_curr = list(centroids_curr) + sorted_coord_centroids = sorted( + list_centroids_curr, key=lambda list_centroids_curr: list_centroids_curr[0] + ) + right_gaze_angle_centroid = ( + sorted_coord_centroids[0][0], + sorted_coord_centroids[0][1], + ) + centre_gaze_angle_centroid = ( + sorted_coord_centroids[1][0], + sorted_coord_centroids[1][1], + ) + left_gaze_angle_centroid = ( + sorted_coord_centroids[2][0], + sorted_coord_centroids[2][1], + ) + print("Changing: ", left_gaze_angle_centroid) + logging.info("\n\n\n\n") + logging.info( + f"{left_gaze_angle_centroid}, {centre_gaze_angle_centroid}, {right_gaze_angle_centroid}" + ) + logging.info("\n\n\n\n") + return ( + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + ) + + +def classify_gaze( + vid_path, + gazeangles_csv_path, + gazeclassified_csv_path, + gazeclassified_vid_path, + left_threshold, + right_threshold, + centre_threshold, + maneuver, + output_folder, + rep, +): + # classify gaze by reading from csv generated by run_openface() and overlay this info on the video generated by run_openface() + direction_map = {"0": "left", "1": "right", "2": "centre"} + cap = cv2.VideoCapture(vid_path) + width = int(cap.get(3)) # float + height = int(cap.get(4)) + logging.info(gazeclassified_vid_path) + fourcc = cv2.VideoWriter_fourcc(*"XVID") + out = cv2.VideoWriter(gazeclassified_vid_path, fourcc, 25, (width, height)) + output_csv = open(gazeclassified_csv_path, mode="w") + csv_writer = csv.writer(output_csv) + csv_writer.writerow(["frame_no", "gaze_x", "gaze_y", "classified direction"]) + df = pd.read_csv(gazeangles_csv_path,) + frame_count = 0 + right_count = 0 + left_count = 0 + centre_count = 0 + ret = True + while ret: + ret, frame = cap.read() + if frame is None: + break + found_face = False + if frame_count <= len(df): + frame_count += 1 + # print("Frame count: ", frame_count) + try: + if frame_count == 5: + cv2.imwrite(os.path.join(output_folder, "face.jpg"), frame) + try: + curr_gaze_angle = df.loc[ + df["frame"] == frame_count, + ["gaze_angle_x", "gaze_angle_y", "face_id"], + ] + except: + curr_gaze_angle = df.loc[ + df["frame"] == frame_count, + [" gaze_angle_x", " gaze_angle_y", " face_id"], + ] + # print("Gaze Angle: ", curr_gaze_angle) + curr_gaze_angle = curr_gaze_angle.values.tolist() + # print("Gaze Angle: ", curr_gaze_angle) + for i in curr_gaze_angle: + found_face = True + if not (float(i[0]) == 0.0) and not (float(i[1]) == 0.0): + print("gaze: ", i) + found_face = True + break + curr_gaze_angle = i + except Exception as e: + print("Exception: ", e) + # exit(0) + cv2.putText( + frame, + "No Face Found", + (int(frame.shape[1] / 2), frame.shape[0] - 50), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 2, + ) + out.write(frame) + continue + if found_face == False: + cv2.putText( + frame, + "No Face Found", + (int(frame.shape[1] / 2), frame.shape[0] - 50), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 2, + ) + out.write(frame) + continue + else: + curr_gaze_angle[1] = 0.0 # added + eu_dist_arr = get_eucledian_distances( + curr_gaze_angle[0], curr_gaze_angle[1], curr_gaze_angle[2] + ) + looking_dir = direction_map[str(eu_dist_arr.index(min(eu_dist_arr)))] + print( + f"curr_gaze_angle: {curr_gaze_angle} looking direction: {looking_dir}" + ) + cv2.rectangle( + frame, + (10, frame.shape[0] - 100), + (10 + 300, frame.shape[0] - 10), + (0, 0, 0), + -1, + ) + cv2.putText( + frame, + "gaze_x: %.3f" % (curr_gaze_angle[0]), + (15, frame.shape[0] - 60), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 1, + ) + cv2.putText( + frame, + "gaze_y: %.3f" % (curr_gaze_angle[1]), + (15, frame.shape[0] - 30), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (255, 255, 255), + 1, + ) + cv2.putText( + frame, + "gaze_dir: " + looking_dir, + (int(frame.shape[1] / 2), frame.shape[0] - 50), + cv2.FONT_HERSHEY_SIMPLEX, + 1, + (0, 0, 255), + 2, + ) + if looking_dir == "right": + right_count += 1 + elif looking_dir == "left": + left_count += 1 + elif looking_dir == "centre": + centre_count += 1 + if frame_count == 50: + cv2.imwrite(os.path.join(output_folder, "face.jpg"), frame) + # disp_frame = cv2.resize(frame, (1080, 720)) + # cv2.imwrite('face.jpg', frame) + out.write(frame) + csv_writer.writerow( + [frame_count, curr_gaze_angle[0], curr_gaze_angle[1], looking_dir] + ) + # cv2.imshow("disp", disp_frame) + # if cv2.waitKey(1) & 0xFF == ord('q'): + # break + else: + break + # if rep: + stats = {"right": right_count, "left": left_count, "centre": centre_count} + decision = "Fail" + if ( + right_count > right_threshold + and left_count > left_threshold + and centre_count > centre_threshold + ): + decision = "Pass" + rep.add_report("{}_gaze".format(maneuver), {"stats": stats, "decision": decision}) + with open(gazeclassified_csv_path.replace(".csv", ".txt"), mode="w") as f: + logging.info("\n\n") + logging.info("right_count: " + str(right_count)) + logging.info("left_count: " + str(left_count)) + logging.info("centre_count: " + str()) + logging.info("right_count: " + str(right_count)) + logging.info("left_count: " + str(left_count)) + logging.info("centre_count: " + str(centre_count)) + logging.info("\n\n") + cap.release() + cv2.destroyAllWindows() + # call_string = "ffmpeg -i {} -c:v copy -c:a copy -y {}".format(gazeclassified_vid_path, os.path.splitext(gazeclassified_vid_path)[0] + ".mp4") + ffmpeg.input(gazeclassified_vid_path).output( + os.path.splitext(gazeclassified_vid_path)[0] + ".mp4" + ).run() + # os.system(call_string) + return decision, stats + + +class Gaze(Maneuver): + def run(self) -> None: + left_gaze_angle_centroid = (0.6074, 0.0) + right_gaze_angle_centroid = (-0.0820, 0.0) + centre_gaze_angle_centroid = (0.1472, 0.0) + ( + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + ) = start_calib( + self.inputs["calib_video"], + self.out_folder, + "gaze", + self.config["face_landmark_exe_path"], + left_gaze_angle_centroid, + right_gaze_angle_centroid, + centre_gaze_angle_centroid, + ) + print( + f"Left: {left_gaze_angle_centroid} Right: {right_gaze_angle_centroid} Centre: {centre_gaze_angle_centroid}" + ) + run_openface( + self.inputs["fpath"], + self.out_folder, + "front_gaze", + self.config["face_landmark_exe_path"], + ) + decision, stats = classify_gaze( + self.inputs["fpath"], + os.path.join(self.out_folder, "front_gaze.csv"), + os.path.join(self.out_folder, "front_gaze_output.csv"), + os.path.join(self.out_folder, "front_gaze.avi"), + self.config["left_threshold"], + self.config["right_threshold"], + self.config["centre_threshold"], + self.config["maneuver"], + self.out_folder, + self.report, + ) + return decision, stats diff --git a/mapping_cli/maneuvers/incline.py b/mapping_cli/maneuvers/incline.py new file mode 100644 index 0000000..96f0140 --- /dev/null +++ b/mapping_cli/maneuvers/incline.py @@ -0,0 +1,345 @@ +import json +import os + +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import numpy as np + +from mapping_cli.halts import get_halts +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import (aggregate_direction, debug_directions_visualize, + generate_trajectory, get_marker_coord, + majority_vote_smoothen, plot_line, + smoothen_trajectory, yml_parser) + + +def select_traj_pts_by_direction(tx, tz, directions, label): + selected_pts = [] + for x, z, d in zip(tx, tz, directions): + if d == label: # remember, this label is reverse + selected_pts.append([x, z]) + return selected_pts + + +def incline_rollback_logic(tx, tz, directions, max_rollback_allowed_metres, rep): + selected_pts = select_traj_pts_by_direction( + tx, tz, directions, 0 + ) # remember, this label is reverse + max_di = 0 + for p in selected_pts: + for p_d in selected_pts: + di = (p[0] - p_d[0]) ** 2 + (p[1] - p_d[1]) ** 2 + if max_di < di: + max_di = di + incline_rollback_decision = "Fail" + if selected_pts == []: + incline_rollback_decision = "Fail" + elif selected_pts != [] and np.sqrt(max_di) <= max_rollback_allowed_metres: + incline_rollback_decision = "Pass" + print("(incline) Rollback (m): {}".format(np.sqrt(max_di))) + print("(incline) Rollback Decision: {}".format(incline_rollback_decision)) + rep.add_report( + "incline_rollback", + {"value_in_m": np.sqrt(max_di), "decision": incline_rollback_decision}, + ) + return incline_rollback_decision + + +def post_process_direction_vector(stats, directions): + # aggregate seq + uniq_vals = [] + pivot_val = directions[0] + pivot_len = 1 + i = 1 + while i < len(directions): + while i < len(directions) and pivot_val == directions[i]: + pivot_len += 1 + i += 1 + if i < len(directions): + uniq_vals.append([pivot_val, pivot_len]) + pivot_val = directions[i] + pivot_len = 1 + i += 1 + if i == len(directions): + uniq_vals.append([pivot_val, pivot_len]) + break + + # delete from seq + if len(uniq_vals) >= 3: + del_elem = [] + for idx in range(0, len(uniq_vals) - 2): + # R = 0, H = -1, F = 1 + if ( + uniq_vals[idx + 0][0] == 0 + and uniq_vals[idx + 1][0] == -1 + and uniq_vals[idx + 2][0] == 0 + ): # RHR --> RR + stats["R"] -= 1 + stats["H"] -= 1 + uniq_vals[idx][1] += uniq_vals[idx + 1][1] + uniq_vals[idx][1] += uniq_vals[idx + 2][1] + del_elem.append(idx + 1) + del_elem.append(idx + 2) + for idx in del_elem[::-1]: + del uniq_vals[idx] + + # recreate seq + directions = [] + for a, b in uniq_vals: + directions += [a] * b # pythonic? + + return stats, directions + + +def plot_legend(): + red_patch = mpatches.Patch(color="red", label="Reverse") + blue_patch = mpatches.Patch(color="blue", label="Forward") + black_patch = mpatches.Patch(color="black", label="Halt") + plt.legend( + handles=[blue_patch, red_patch, black_patch], + prop={"size": 10}, + loc="upper right", + ) + + +def plot_markers(markers): + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + + +def plot_limits(x_limits, y_limits): + plt.xlim(x_limits) + plt.ylim(y_limits) + plt.gca().set_aspect("equal", "box") + + +def get_line(line_info_json_f): + line_info = None + with open(line_info_json_f) as f: + line_info = json.load(f) + l_stop = np.polyfit(line_info["pts"]["tx"], line_info["pts"]["tz"], 1) + return l_stop + + +def percentage_of_points_behind_line(selected_pts, l_stop, markers_side): + behind_lines_decision = [] + for x, z in selected_pts: + val_halt = -1 * l_stop[0] * x + z - l_stop[1] + if np.sign(val_halt) != np.sign(markers_side): + behind_lines_decision.append(1) + else: + behind_lines_decision.append(0) + + percentage_obey = 0.0 + for x in behind_lines_decision: + if x == 1: + percentage_obey += 1 + if behind_lines_decision == []: + percentage_obey = 0.0 + else: + percentage_obey /= len(behind_lines_decision) + return behind_lines_decision, percentage_obey + + +def get_side_of_marker_from_line(markers, l_stop): + markers_side = [] + # In Standard form: -m*x + y - c = 0 + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + # placing marker coords into the line + # to determine their side + if ( + -1 * l_stop[0] * marker_coord[0] + marker_coord[1] - l_stop[1] > 0 + ): # Might want to have a looser criterion + markers_side.append(1) + else: + markers_side.append(-1) + if not all(map(lambda x: x == markers_side[0], markers_side)): + raise Exception("All markers for this maneuver should be behind the line!") + else: + return markers_side[0] + + +def plot_traj( + tx, + tz, + directions, + line_json, + x_limits, + y_limits, + rep, + behind_lines_obey_threshold, + markers, + save_image_name=None, +): + if markers is not None: + plot_markers(markers) + + ax = plt.gca() + + line_info_json_f = line_json + l_stop = get_line(line_info_json_f) + line_viz_x_lim = x_limits + + plot_line(plt, l_stop, line_viz_x_lim, "green") + + markers_side = get_side_of_marker_from_line(markers, l_stop) + + selected_pts = select_traj_pts_by_direction(tx, tz, directions, -1) ## halt is -1 + + behind_lines_decision, percentage_obey = percentage_of_points_behind_line( + selected_pts, l_stop, markers_side + ) + + decision = "Fail" + if percentage_obey >= behind_lines_obey_threshold: + decision = "Pass" + rep.add_report( + "{}_behind_line".format("incline"), + {"value": percentage_obey, "decision": decision}, + ) + print( + "({}) Halt Behind Line %: {} Decision: {}".format( + "incline", percentage_obey, decision + ) + ) + + color_f = ["red", "blue", "black"] + size_f = [25, 25, 40] + len_plot = min(min(len(tx), len(tz)), len(directions)) + plt.scatter( + tx[:len_plot], + tz[:len_plot], + c=[color_f[directions[i]] for i in range(0, len_plot)], + s=[size_f[directions[i]] for i in range(0, len_plot)], + marker=".", + ) + + plot_legend() + plot_limits(x_limits, y_limits) + + # if markers is not None: + # rotate_plot(ax, markers, manu) + + plt.savefig(save_image_name, dpi=200) + plt.close() + + +class Incline(Maneuver): + def run(self) -> None: + map_path = self.config.get_config_value("map_file_path") + if not os.path.exists(map_path): + map_path = os.path.join(self.inputs["cwd"], map_path) + + calib_path = self.config["calibration_file_path"] + if not os.path.exists(calib_path): + calib_path = os.path.join(self.inputs["cwd"], calib_path) + + traj = generate_trajectory( + self.inputs["back_video"], + "incline", + map_path, + self.out_folder, + calib_path, + self.config["size_marker"], + self.config["aruco_test_exe"], + self.inputs["cwd"], + ) + + _, tx, ty, tz, camera_matrices = traj + direction, stats = self.get_direction_stats( + traj, + self.config["rev_fwd_halt_segment_min_frame_len"], + self.config["min_frame_len"], + ) + + # smoothen trajectory + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 25, 25, 2) + + # get stats + decision = incline_rollback_logic( + tx, tz, direction, self.config["max_rollback_allowed_metres"], self.report + ) + + # rollback + + line_path = self.config["line_file_path"] + if not os.path.exists(line_path): + line_path = os.path.join(self.inputs["cwd"], line_path) + + markers = yml_parser(map_path) + # plot + plot_traj( + tx, + tz, + direction, + line_path, + self.config["x_limits"], + self.config["y_limits"], + self.report, + self.config["behind_lines_obey_threshold"], + markers, + os.path.join(self.out_folder, "incline.png"), + ) + + return ( + (stats, decision), + ( + { + "back_video": self.inputs["back_video"], + "trajectory": os.path.join(self.out_folder, "incline.png"), + } + ), + ) + + def get_direction_stats( + self, traj, rev_fwd_halt_segment_min_frame_len, min_frame_len + ): + """ """ + f_ids, tx, ty, tz, camera_matrices = traj + directions = [1] + Xs = [] + for i in range(1, len(tx)): + translation = ( + np.array([tx[i], ty[i], tz[i], 1.0]).reshape(4, 1).astype(np.float64) + ) + X = camera_matrices[i - 1][:3, :3].dot( + translation[:3, 0] - camera_matrices[i - 1][:3, -1] + ) + # print(X) + direction = X[2] + + if [tx[i], ty[i], tz[i]] == [tx[i - 1], ty[i - 1], tz[i - 1]]: + directions.append(directions[-1]) + else: + if direction >= 0: + directions.append(1) + else: + directions.append(0) + + directions = directions[1:] + + # Get Forward-Reverse + directions = np.array(directions + [-1]) + + # Get halts + halt_info = get_halts(tx, ty, tz) + directions[halt_info == True] = -1 + directions = directions.tolist() + directions = majority_vote_smoothen( + directions, rev_fwd_halt_segment_min_frame_len + ) + + stats, directions = aggregate_direction(directions, min_frame_len) + stats, _ = aggregate_direction(directions, min_frame_len) + + stats, directions = post_process_direction_vector(stats, directions) + + # debug_directions_visualize(plt, tx, ty, tz, directions) + + return directions, stats diff --git a/mapping_cli/maneuvers/maneuver.py b/mapping_cli/maneuvers/maneuver.py new file mode 100644 index 0000000..05b59db --- /dev/null +++ b/mapping_cli/maneuvers/maneuver.py @@ -0,0 +1,41 @@ +import logging +import os + +from mapping_cli.config.config import Config +from mapping_cli.utils import Report + + +class Maneuver: + def __init__( + self, + inputs=None, + inertial_data=None, + config: Config = None, + out_folder: str = None, + ) -> None: + super().__init__() + self.inputs = inputs + self.inertial_data = inertial_data + self.config = config + self.out_folder = out_folder + + self.report = Report(os.path.join(self.out_folder, "report.txt")) + self.log(f"{self.__class__} Started") + + def run(self) -> None: + raise NotImplementedError + + def save(self): + raise NotImplementedError + + def log(self, *args, **kwargs): + if getattr(self, "logger", None) is not None: + logging.info(*args, **kwargs) + else: + logging.basicConfig( + filename=os.path.join(self.out_folder, f"hams_alt.log"), + filemode="w", + level=logging.INFO, + ) + logging.info(*args, **kwargs) + self.logger = {} diff --git a/mapping_cli/maneuvers/marker_sequence.py b/mapping_cli/maneuvers/marker_sequence.py new file mode 100644 index 0000000..42d9990 --- /dev/null +++ b/mapping_cli/maneuvers/marker_sequence.py @@ -0,0 +1,67 @@ +from decord import VideoReader + +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import detect_marker + + +def get_marker_from_frame(frame, marker_list, marker_dict): + markers = detect_marker(frame, marker_dict) + if len(markers) > 0: + permuted_markers = list( + filter( + lambda marker: marker in marker_list, + list(sorted([int(i) for i in markers])), + ) + ) + else: + return None + return permuted_markers + + +class MarkerSequence(Maneuver): + def run(self) -> None: + vid = VideoReader(self.inputs["back_video"]) + frames = range(0, len(vid), self.config["skip_frames"]) + marker_list = self.config["marker_list"] + verification_sequence = self.config["marker_order"] + markers_detected = [] + + for i in frames: + vid.seek_accurate(i) + frame = vid.next().asnumpy() + permuted_markers = get_marker_from_frame( + frame, marker_list, self.config["marker_dict"] + ) + if permuted_markers is not None: + if isinstance(permuted_markers, (list, tuple)): + val_markers = [] + for permuted_marker in permuted_markers: + if permuted_marker not in markers_detected: + val_markers.append(permuted_marker) + + if len(val_markers) > 0: + markers_detected += val_markers + + elif permuted_markers != markers_detected[-1]: + markers_detected += list(permuted_markers) + sequence_verified = False + print("Detected: ", markers_detected) + + verified_sequence = None + + for sequence_verification in verification_sequence: + str_seq = "".join([str(i) for i in sequence_verification]) + iter_seq_verification = iter(str_seq) + str_markers = "".join([str(i) for i in markers_detected]) + res = all( + next((ele for ele in iter_seq_verification if ele == chr), None) + is not None + for chr in list(str_markers) + ) + if res and len(markers_detected) >= len(sequence_verification) - 1: + sequence_verified = True + verified_sequence = sequence_verification + break + + print("Verified: ", sequence_verified) + return (sequence_verified, markers_detected), {} diff --git a/mapping_cli/maneuvers/pedestrian.py b/mapping_cli/maneuvers/pedestrian.py new file mode 100644 index 0000000..8071011 --- /dev/null +++ b/mapping_cli/maneuvers/pedestrian.py @@ -0,0 +1,460 @@ +import json +import math +import os + +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import numpy as np +import shapely + +from mapping_cli.halts import get_halts +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import (aggregate_direction, debug_directions_visualize, + generate_trajectory, get_marker_coord, + majority_vote_smoothen, plot_line, + rotate_rectangle, + rotation_matrix_to_euler_angles, + smoothen_trajectory, yml_parser) + + +def get_side_of_marker_from_line(markers, l_stop): + markers_side = [] + # In Standard form: -m*x + y - c = 0 + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + # placing marker coords into the line + # to determine their side + if ( + -1 * l_stop[0] * marker_coord[0] + marker_coord[1] - l_stop[1] > 0 + ): # Might want to have a looser criterion + markers_side.append(1) + else: + markers_side.append(-1) + if not all(map(lambda x: x == markers_side[0], markers_side)): + raise Exception("All markers for this maneuver should be behind the line!") + else: + return markers_side[0] + + +def select_traj_points_by_time(tx, tz, segment_json, fps): + if not os.path.exists(segment_json): + raise Exception("segment_json not part of input arguments!") + with open(segment_json) as f: + seg_vals = json.load(f) + print("Seg vals: ", seg_vals.keys()) + frame_num_pedestrian_light_vid_start_abs = math.ceil( + seg_vals["pedestrian"]["start"][0] + ) + + frame_num_red_light_off_abs = fps + if frame_num_red_light_off_abs == -1: + print("WARNING!!!!! pedestrian Light hook is not implemented") + return list(zip(tx, tz)) + + if ( + frame_num_pedestrian_light_vid_start_abs > frame_num_red_light_off_abs + ): # my video somehow cut afterwards + return [] + + end_frame_red_light_check = ( + frame_num_red_light_off_abs - frame_num_pedestrian_light_vid_start_abs + ) + if end_frame_red_light_check > len(tx): + end_frame_red_light_check = len(tx) + return list(zip(tx[:end_frame_red_light_check], tz[:end_frame_red_light_check])) + + +def percentage_of_points_behind_line(selected_pts, l_stop, markers_side): + behind_lines_decision = [] + for x, z in selected_pts: + val_halt = -1 * l_stop[0] * x + z - l_stop[1] + if np.sign(val_halt) != np.sign(markers_side): + behind_lines_decision.append(1) + else: + behind_lines_decision.append(0) + + percentage_obey = 0.0 + for x in behind_lines_decision: + if x == 1: + percentage_obey += 1 + if behind_lines_decision == []: + percentage_obey = 0.0 + else: + percentage_obey /= len(behind_lines_decision) + return behind_lines_decision, percentage_obey + + +def post_process_direction_vector(stats, directions): + # aggregate seq + uniq_vals = [] + pivot_val = directions[0] + pivot_len = 1 + i = 1 + while i < len(directions): + while i < len(directions) and pivot_val == directions[i]: + pivot_len += 1 + i += 1 + if i < len(directions): + uniq_vals.append([pivot_val, pivot_len]) + pivot_val = directions[i] + pivot_len = 1 + i += 1 + if i == len(directions): + uniq_vals.append([pivot_val, pivot_len]) + break + + # delete from seq + if len(uniq_vals) >= 3: + del_elem = [] + for idx in range(0, len(uniq_vals) - 2): + # R = 0, H = -1, F = 1 + if ( + uniq_vals[idx + 0][0] == 0 + and uniq_vals[idx + 1][0] == -1 + and uniq_vals[idx + 2][0] == 0 + ): # RHR --> RR + stats["R"] -= 1 + stats["H"] -= 1 + uniq_vals[idx][1] += uniq_vals[idx + 1][1] + uniq_vals[idx][1] += uniq_vals[idx + 2][1] + del_elem.append(idx + 1) + del_elem.append(idx + 2) + for idx in del_elem[::-1]: + del uniq_vals[idx] + + # recreate seq + directions = [] + for a, b in uniq_vals: + directions += [a] * b # pythonic? + + return stats, directions + + +def get_line(line_info_json_f): + line_info = None + with open(line_info_json_f) as f: + line_info = json.load(f) + l_stop = np.polyfit(line_info["pts"]["tx"], line_info["pts"]["tz"], 1) + return l_stop + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["box"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_car_box(cam_x, cam_y, camera_matrix, manu, car_dims, site_config): + car_top_left = [cam_x - car_dims["x_offset"], cam_y - car_dims["z_offset"]] + car_top_right = [car_top_left[0] + car_dims["width"], car_top_left[1]] + car_bottom_right = [ + car_top_left[0] + car_dims["width"], + car_top_left[1] + car_dims["length"], + ] + car_bottom_left = [car_top_left[0], car_top_left[1] + car_dims["length"]] + + angle = rotation_matrix_to_euler_angles(camera_matrix)[1] + + if site_config.maneuvers_config["viz"][manu]["markers_vertical"]: + angle *= -1.0 + + car_pts = np.array([car_top_left, car_top_right, car_bottom_right, car_bottom_left]) + car_rotated_pts = rotate_rectangle(car_pts, np.array([cam_x, cam_y]), angle) + + return car_rotated_pts + + +def plot_traj( + tx, + tz, + manu, + car_dims, + line_json, + config, + maneuver: Maneuver, + markers=None, + save_image_name=None, + camera_matrices=None, + max_iou_idx=None, + segment_json=None, +): + if markers is not None: + plot_markers(markers) + + ax = plt.gca() + + # poly = mpatches.Polygon(box, alpha=1, fill=False, edgecolor='black') + # ax.add_patch(poly) + + # Plot Car + if max_iou_idx is not None: + position = max_iou_idx + car_pts = get_car_box( + tx[position], + tz[position], + camera_matrices[position][:3, :3], + manu, + car_dims, + ) + car_patch = mpatches.Polygon(car_pts, alpha=1, fill=False, edgecolor="red") + ax.add_patch(car_patch) + + line_info_json_f = line_json + + if config["line_type"] == "stop": + l_stop = get_line(line_info_json_f) + + stop_ped_offset = config["stop_ped_line_dist"] + l_ped = [l_stop[0], l_stop[1] + stop_ped_offset] + elif config["line_type"] == "ped": + l_ped = get_line(line_info_json_f) + stop_line_offset = config["stop_ped_line_dist"] + l_stop = [l_ped[0], l_ped[1] + stop_line_offset] + + line_viz_x_lim = config["xlim"] + + plot_line(plt, l_stop, line_viz_x_lim, "blue") + plot_line(plt, l_ped, line_viz_x_lim, "yellow") + + ## markers are always ahead of stop line, + ## ped line is gonna be ahead of stop line + ## X X * X $ | + ## * $ | + ## * $ | + ## * $ | + ## Legend: Marker : X, StopLine : |, PedLine1 : $, Pedline2 : * + ## Define markers_side_ped such that both PL1 and PL2 are satisfied + ## + ## As we can assume that ped_line will always be ahead of stop_line, + ## every point on ped_line will be sign(stop_line(point)) == sign(stop_line(marker)) + ## + ## Let's find a point on stop_line, that will always be behind ped_line + ## Use that point to find the side and then invert it + markers_side = get_side_of_marker_from_line(markers, l_stop) # say this is 1 + point_on_stop_line = (0, l_stop[0] * 0 + l_stop[1]) + markers_side_ped = -1 * np.sign( + -1 * l_ped[0] * point_on_stop_line[0] + point_on_stop_line[1] - l_ped[1] + ) + + selected_pts = select_traj_points_by_time(tx, tz, segment_json, config["fps"]) + + behind_lines_decision_ped, percentage_obey = percentage_of_points_behind_line( + selected_pts, l_ped, markers_side_ped + ) + behind_lines_decision_stop, percentage_stop = percentage_of_points_behind_line( + selected_pts, l_stop, markers_side + ) + label_decision = [] + for i in range(len(behind_lines_decision_ped)): + if behind_lines_decision_ped[i] == 1 and behind_lines_decision_stop[i] == 1: + label_decision.append(0) + elif behind_lines_decision_ped[i] == 1 and behind_lines_decision_stop[i] == 0: + label_decision.append(1) + elif behind_lines_decision_ped[i] == 0 and behind_lines_decision_stop[i] == 0: + label_decision.append(2) + else: + raise Exception("Error: Ped Line is not ahead of Stop Line in track!") + + obey_decision = "Fail" + if percentage_obey >= config["behind_lines_obey_threshold"]: + obey_decision = "Pass" + + stop_decision = "Fail" + if percentage_stop > config["behind_lines_stop_threshold"]: + stop_decision = "Pass" + + maneuver.report.add_report("pedestrianLight_obey_decision", obey_decision) + maneuver.report.add_report("pedestrianLight_stop_decision", stop_decision) + maneuver.report.add_report( + "pedestrianLight_outcome", + { + "percentage_behind_both_lines": percentage_obey, + "percentage_behind_stop_line": percentage_stop, + }, + ) + + print( + "({}) Halt Behind Stop Line %: {} Decision: {}".format( + manu, percentage_stop, stop_decision + ) + ) + print( + "({}) Halt Behind Ped Line %: {} Decision: {}".format( + manu, percentage_obey, obey_decision + ) + ) + + color_f = ["green", "yellow", "brown"] + size_f = [25, 25, 40] + len_plot = len(selected_pts) + print(len(selected_pts), len(label_decision)) + plt.scatter( + [x[0] for x in selected_pts], + [x[1] for x in selected_pts], + c=[color_f[label_decision[i]] for i in range(0, len_plot)], + s=[size_f[label_decision[i]] for i in range(0, len_plot)], + marker="*", + ) + + plot_legend() + plot_limits(config["xlim"], config["ylim"]) + + # if markers is not None: + # rotate_plot(ax, markers, manu) + + plt.savefig(save_image_name, dpi=200) + plt.close() + return obey_decision and stop_decision + + +def plot_legend(): + red_patch = mpatches.Patch(color="red", label="Reverse") + blue_patch = mpatches.Patch(color="blue", label="Forward") + black_patch = mpatches.Patch(color="black", label="Halt") + plt.legend( + handles=[blue_patch, red_patch, black_patch], + prop={"size": 10}, + loc="upper right", + ) + + +def plot_markers(markers): + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + + +def plot_limits(x_limits, y_limits): + plt.xlim(x_limits) + plt.ylim(y_limits) + plt.gca().set_aspect("equal", "box") + + +class Pedestrian(Maneuver): + def run(self): + map_path = self.config.get_config_value("map_file_path") + if not os.path.exists(map_path): + map_path = os.path.join(self.inputs["cwd"], map_path) + + calib_path = self.config["calibration_file_path"] + if not os.path.exists(calib_path): + calib_path = os.path.join(self.inputs["cwd"], calib_path) + + try: + traj = generate_trajectory( + self.inputs["back_video"], + self.config.get_config_value("maneuver"), + map_path, + self.out_folder, + calib_path, + self.config["size_marker"], + self.config["aruco_test_exe"], + self.inputs["cwd"], + ) + except Exception as e: + raise e + + _, tx, ty, tz, camera_matrices = traj + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 25, 25, 2) + markers = yml_parser(map_path) + + # get_direction_stats + direction, stats = self.get_direction_stats( + tx, + ty, + tz, + camera_matrices, + self.config["rev_fwd_halt_segment_min_frame_len"], + self.config["min_frame_len"], + ) + + # fwd rev halts + halts = get_halts(tx, ty, tz) + stats["halts"] = halts + + line_path = self.config["line_file_path"] + if not os.path.exists(line_path): + line_path = os.path.join(self.inputs["cwd"], line_path) + + decision = plot_traj( + tx, + tz, + "pedestrian", + self.config["car_dims"], + line_path, + self.config, + self, + markers, + segment_json=os.path.join(self.out_folder, "manu_json_seg_int.json"), + save_image_name=os.path.join(self.out_folder, "pedestrian.png"), + ) + + decision = decision == "Pass" + + return ( + (decision, stats), + { # TODO: Add pass/fail + "trajectory": f"{os.path.join(self.out_folder, 'pedestrian.png')}" + }, + ) + + def get_direction_stats( + self, + tx, + ty, + tz, + camera_matrices, + rev_fwd_halt_segment_min_frame_len, + min_frame_len, + ): + """ """ + directions = [1] + Xs = [] + for i in range(1, len(tx)): + translation = ( + np.array([tx[i], ty[i], tz[i], 1.0]).reshape(4, 1).astype(np.float64) + ) + X = camera_matrices[i - 1][:3, :3].dot( + translation[:3, 0] - camera_matrices[i - 1][:3, -1] + ) + # print(X) + direction = X[2] + + if [tx[i], ty[i], tz[i]] == [tx[i - 1], ty[i - 1], tz[i - 1]]: + directions.append(directions[-1]) + else: + if direction >= 0: + directions.append(1) + else: + directions.append(0) + + directions = directions[1:] + + # Get Forward-Reverse + directions = np.array(directions + [-1]) + + # Get halts + halt_info = get_halts(tx, ty, tz) + directions[halt_info == True] = -1 + directions = directions.tolist() + directions = majority_vote_smoothen( + directions, rev_fwd_halt_segment_min_frame_len + ) + + stats, directions = aggregate_direction(directions, min_frame_len) + stats, _ = aggregate_direction(directions, min_frame_len) + + stats, directions = post_process_direction_vector(stats, directions) + + debug_directions_visualize(plt, tx, ty, tz, directions) + + return directions, stats diff --git a/mapping_cli/maneuvers/perp.py b/mapping_cli/maneuvers/perp.py new file mode 100644 index 0000000..e6b3a26 --- /dev/null +++ b/mapping_cli/maneuvers/perp.py @@ -0,0 +1,558 @@ +import itertools +import json +import os + +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import numpy as np +import shapely +from matplotlib.collections import PathCollection +from matplotlib.transforms import Affine2D + +from mapping_cli.halts import get_halts +from mapping_cli.locator import get_locations, read_aruco_traj_file +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import (get_graph_box, get_marker_coord, + get_plt_rotation_from_markers, + majority_vote_smoothen, rotate_rectangle, + rotation_matrix_to_euler_angles, + smoothen_trajectory, yml_parser) + + +class PERP(Maneuver): + def run(self) -> None: + # 1 Get trajectory + + traj = get_locations( + self.inputs["back_video"], + "perp", + self.config["map_file_path"], + self.out_folder, + self.config["calib_file_path"], + self.config["marker_size"], + self.config["aruco_test_exe"], + cwd=self.inputs["cwd"], + plot=False, + return_read=True, + ) + if traj is False: + raise Exception("Bad trajectory generated at perp. No results") + + _, tx, ty, tz, camera_matrices = traj + if len(traj) < 30: + if len(tx) > 99: + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 199, 299, 1) + else: + try: + tx, ty, tz = smoothen_trajectory( + tx, ty, tz, int(99 / 3), int(199 // 3), 2 + ) + except: + window = len(tx) - 3 if len(tx) % 2 == 0 else len(tx) - 4 + tx, ty, tz = smoothen_trajectory(tx, ty, tz, window, window, 2) + else: + raise Exception("Insufficient trajectory - check segmentation output") + + map_file = self.config["map_file_path"] + if not os.path.exists(map_file): + map_file = os.path.join(self.inputs["cwd"], map_file) + + markers = yml_parser(map_file) + # 2 In box + box_path = self.config["box_file_path"] + if not os.path.exists(box_path): + box_path = os.path.join(self.inputs["cwd"], box_path) + fig, ax = plt.subplots() + is_inside, max_iou, max_iou_idx = is_car_inside( + tx, + ty, + tz, + camera_matrices, + self.config["car_dims"], + markers_vertical=self.config["markers_vertical"], + box_overlap_threshold=self.config["box_overlap_threshold"], + box_path=box_path, + ) + box = get_maneuver_box(box_path) + box_patch = mpatches.Polygon(box, alpha=1, fill=False, edgecolor="red") + ax.add_patch(box_patch) + if max_iou_idx is not None: + print("Adding car patch") + position = max_iou_idx + car_pts = get_car_box( + tx[position], + tz[position], + camera_matrices[position][:3, :3], + self.config["car_dims"], + self.config["is_vertical"], + ) + car_patch = mpatches.Polygon( + car_pts, alpha=1, fill=False, edgecolor="green", label="vehicle" + ) + ax.add_patch(car_patch) + + # plt.savefig('car.png') + # fig.s + _, stats = get_direction_stats( + tx, + ty, + tz, + camera_matrices, + self.config["rev_fwd_halt_segment_min_frame_len"], + self.config["min_frame_len"], + markers, + self.out_folder, + ) + # 3 Standard path check + + plt.clf() + _, std_path_decision = check_standard_path( + tx, + ty, + tz, + camera_matrices, + self.config["car_dims"], + self.config["is_vertical"], + max_iou_idx, + markers, + self.config, + "perp", + self.out_folder, + self.inputs["cwd"], + True, + ) + print( + f"is inside: {is_inside}, stats: {stats}, std path decision: {std_path_decision}" + ) + return ( + (is_inside and std_path_decision, stats), + { + "back_video": self.inputs["back_video"], + "perp_standard_path": f"{os.path.join(self.out_folder, 'perp_trajectory.png')}", + "perp_trajectory_path": f"{os.path.join(self.out_folder, 'standard_path_debug_perp.png')}", + }, + ) + + +def post_process_direction_vector(stats, directions): + # aggregate seq + uniq_vals = [] + pivot_val = directions[0] + pivot_len = 1 + i = 1 + while i < len(directions): + while i < len(directions) and pivot_val == directions[i]: + pivot_len += 1 + i += 1 + if i < len(directions): + uniq_vals.append([pivot_val, pivot_len]) + pivot_val = directions[i] + pivot_len = 1 + i += 1 + if i == len(directions): + uniq_vals.append([pivot_val, pivot_len]) + break + + # delete from seq + if len(uniq_vals) >= 3: + del_elem = [] + for idx in range(0, len(uniq_vals) - 2): + # R = 0, H = -1, F = 1 + if ( + uniq_vals[idx + 0][0] == 0 + and uniq_vals[idx + 1][0] == -1 + and uniq_vals[idx + 2][0] == 0 + ): # RHR --> RR + stats["R"] -= 1 + stats["H"] -= 1 + uniq_vals[idx][1] += uniq_vals[idx + 1][1] + uniq_vals[idx][1] += uniq_vals[idx + 2][1] + del_elem.append(idx + 1) + del_elem.append(idx + 2) + for idx in del_elem[::-1]: + del uniq_vals[idx] + + # recreate seq + directions = [] + for a, b in uniq_vals: + directions += [a] * b # pythonic? + + return stats, directions + + +def get_direction_stats( + tx, + ty, + tz, + camera_matrices, + rev_fwd_halt_segment_min_frame_len, + min_frame_len, + markers, + out_folder, + maneuver=None, + args=None, +): + """ """ + + directions = [1] + Xs = [] + for i in range(1, len(tx)): + translation = ( + np.array([tx[i], ty[i], tz[i], 1.0]).reshape(4, 1).astype(np.float64) + ) + X = camera_matrices[i - 1][:3, :3].dot( + translation[:3, 0] - camera_matrices[i - 1][:3, -1] + ) + # print(X) + direction = X[2] + + if [tx[i], ty[i], tz[i]] == [tx[i - 1], ty[i - 1], tz[i - 1]]: + directions.append(directions[-1]) + else: + if direction >= 0: + directions.append(1) + else: + directions.append(0) + + directions = directions[1:] + + # Get Forward-Reverse + directions = np.array(directions + [-1]) + + # Get halts + halt_info = get_halts(tx, ty, tz) + directions[halt_info == True] = -1 + directions = directions.tolist() + directions = majority_vote_smoothen(directions, rev_fwd_halt_segment_min_frame_len) + + stats, directions = aggregate_direction(directions, min_frame_len) + stats, _ = aggregate_direction(directions, min_frame_len) + + stats, directions = post_process_direction_vector(stats, directions) + + # if args and args.debug: + debug_directions_visualize(tx, ty, tz, directions, markers, out_folder) + + return directions, stats + + +def plot_markers(markers): + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + plt.annotate(str(key), (marker_coord[0], marker_coord[1])) + + +def plot_legend(): + red_patch = mpatches.Patch(color="red", label="Reverse") + blue_patch = mpatches.Patch(color="blue", label="Forward") + black_patch = mpatches.Patch(color="black", label="Halt") + plt.legend( + handles=[blue_patch, red_patch, black_patch], + prop={"size": 10}, + loc="upper right", + ) + + +def debug_directions_visualize(tx, ty, tz, directions, markers, out_folder): + """ """ + colors = ["red", "blue", "black"] + le = min(min(len(tx) - 1, len(tz) - 1), len(directions) - 1) + plt.scatter( + [tx[i] for i in range(0, le)], + [tz[i] for i in range(0, le)], + c=[colors[directions[i]] for i in range(0, le)], + ) + # plt.show() + plot_markers(markers) + plot_limits() + plot_legend() + plt.savefig(os.path.join(out_folder, "perp_trajectory.png")) + plt.clf() + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["c"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_car_box(cam_x, cam_y, camera_matrix, car_dims, markers_vertical): + car_top_left = [cam_x - car_dims["x_offset"], cam_y - car_dims["z_offset"]] + car_top_right = [car_top_left[0] + car_dims["width"], car_top_left[1]] + car_bottom_right = [ + car_top_left[0] + car_dims["width"], + car_top_left[1] + car_dims["length"], + ] + car_bottom_left = [car_top_left[0], car_top_left[1] + car_dims["length"]] + + angle = rotation_matrix_to_euler_angles(camera_matrix)[1] + + if markers_vertical: + angle *= -1.0 + + car_pts = np.array([car_top_left, car_top_right, car_bottom_right, car_bottom_left]) + car_rotated_pts = rotate_rectangle(car_pts, np.array([cam_x, cam_y]), angle) + + return car_rotated_pts + + +def get_car_box_iou(camera_coords, camera_matrix, car_dims, markers_vertical, box_path): + box = get_maneuver_box(box_path) + car_pts = get_car_box( + camera_coords[0], camera_coords[1], camera_matrix, car_dims, markers_vertical + ) + + box_poly = shapely.geometry.Polygon(box.tolist()) + car_poly = shapely.geometry.Polygon(car_pts.tolist()) + + iou = car_poly.intersection(box_poly).area / car_poly.union(box_poly).area + norm_iou = car_poly.area / box_poly.area + + return iou / norm_iou + + +def aggregate_direction(direction_vector, halt_len): + """ + Input: vector containing 'forward', 'reverse' + Output: # of forwards, # of reverse + """ + direction_count = {"F": 0, "R": 0, "H": 0} + # Group consecutive directions + grouped_class = [ + list(l) + for _, l in itertools.groupby(enumerate(direction_vector), key=lambda x: x[1]) + ] + new_direction_vector = [] + for c_idx, c in enumerate(grouped_class): + if c[0][1] == 0: + direction_count["R"] += 1 + new_direction_vector += [0 for i in range(len(c))] + elif c[0][1] == 1: + direction_count["F"] += 1 + new_direction_vector += [1 for i in range(len(c))] + elif c[0][1] == -1: + if len(c) > halt_len: + direction_count["H"] += 1 + new_direction_vector += [-1 for i in range(len(c))] + else: + # Start segment + if c_idx > 0: + prev_direction = grouped_class[c_idx - 1][0][1] + else: + next_direction = grouped_class[c_idx + 1][0][1] + prev_direction = next_direction + + # End segment + if c_idx < len(grouped_class) - 1: + next_direction = grouped_class[c_idx + 1][0][1] + else: + next_direction = prev_direction + + new_direction_vector += [prev_direction for i in range(len(c) // 2)] + new_direction_vector += [next_direction for i in range(len(c) // 2)] + + return direction_count, new_direction_vector + + +def is_car_inside( + tx, + ty, + tz, + camera_matrices, + car_dims, + markers_vertical, + box_overlap_threshold, + box_path, +): + assert len(tx) == len(ty) == len(tz) + max_iou = -1 + max_iou_idx = 0 + counts = 0 + for i in range(len(tx)): + iou = get_car_box_iou( + [tx[i], tz[i]], + camera_matrices[i][:3, :3], + car_dims, + markers_vertical, + box_path, + ) + if iou > box_overlap_threshold: + counts += 1 + if iou > max_iou: + max_iou = iou + max_iou_idx = i + max_iou = float(np.round(max_iou, 2)) + if max_iou > box_overlap_threshold: + return True, max_iou, max_iou_idx + else: + return False, max_iou, max_iou_idx + + +def rotate_plot(ax, config, origin_marker, axis_marker, is_vertical): + rot_deg = get_plt_rotation_from_markers(origin_marker, axis_marker, is_vertical) + if rot_deg == 0.0: + return + r = Affine2D().rotate_deg(rot_deg) + + for x in ax.images + ax.lines + ax.collections + ax.patches: + trans = x.get_transform() + x.set_transform(r + trans) + if isinstance(x, PathCollection): + transoff = x.get_offset_transform() + x._transOffset = r + transoff + + if config["invert_y"]: + ax.invert_yaxis() + if config["invert_x"]: + ax.invert_xaxis() + + +def plot_limits(): + plot_limits = {"xlim": [-20, 20], "ylim": [-20, 20]} + plt.xlim(*plot_limits["xlim"]) + plt.ylim(*plot_limits["ylim"]) + plt.gca().set_aspect("equal", "box") + + +def plot_car(ax, max_iou_idx, tx, ty, tz, camera_matrices, car_dims, is_vertical): + if max_iou_idx is not None: + print("Adding car patch") + position = max_iou_idx + car_pts = get_car_box( + tx[position], + tz[position], + camera_matrices[position][:3, :3], + car_dims, + is_vertical, + ) + car_patch = mpatches.Polygon( + car_pts, alpha=1, fill=False, edgecolor="green", label="vehicle" + ) + ax.add_patch(car_patch) + + +def debug_plot( + tx, + ty, + tz, + camera_matrices, + car_dims, + is_vertical, + max_iou_idx, + manu, + markers, + box, + labels, + out_f, + config, + origin_marker, + axis_marker, +): + plt.scatter(tx[:-1], tz[:-1], marker=".", c=[[1, 0, 0]]) + + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + + ax = plt.gca() + + for label in labels: + poly = mpatches.Polygon(box[label], alpha=1, fill=False, edgecolor="blue") + ax.add_patch(poly) + + plot_limits() + plot_car(ax, max_iou_idx, tx, ty, tz, camera_matrices, car_dims, is_vertical) + + rotate_plot(ax, config, origin_marker, axis_marker, is_vertical) + plt.savefig(os.path.join(out_f, "standard_path_debug_{}.png".format(manu))) + + +def check_standard_path( + tx, + ty, + tz, + camera_matrices, + car_dims, + is_vertical, + max_iou_idx, + markers, + config, + manu, + out_f, + cwd, + debug=False, +): + plt.cla() + plt.clf() + labels = config["standard_node_visit_order"] + box_path = config["box_file_path"] + if not os.path.exists(box_path): + box_path = os.path.join(cwd, box_path) + boxes = [ + shapely.geometry.Polygon(get_graph_box(box_path, label).tolist()) + for label in labels + ] + values = [] + for i in range(len(tx)): + pos = shapely.geometry.Point([tx[i], tz[i]]) + for j, box in enumerate(boxes): + if box.contains(pos): + # print(labels[j]) + values.append(labels[j]) + break + + if len(values) == 0: + return None, False + manu_order = [values[0]] + for i in range(1, len(values) - 1): + if values[i] != values[i - 1]: + manu_order.append(values[i]) + + origin_marker = markers["aruco_bc_markers"][config["origin_marker"]] + axis_marker = markers["aruco_bc_markers"][config["axis_marker"]] + + with open(box_path) as f: + debug_plot( + tx, + ty, + tz, + camera_matrices, + car_dims, + is_vertical, + max_iou_idx, + manu, + markers, + json.load(f), + labels, + out_f, + config, + origin_marker, + axis_marker, + ) + + # is the subsequence 'BC' in the visit order of the sequence or not? + # C is the box we want to be in, B is the box ahead (from + # where we want the car to reverse) + # Acceptable: + # ABC, ABCB, ACBC, ACBCB etc + # Non Acceptable: + # ABAC etc + manu_order = "".join(manu_order) + substrings = config["acceptable_order_substrings"] + print("Manu order: ", manu_order, " substrings: ", substrings) + for substring in substrings: + if substring in manu_order: + return manu_order, True + return manu_order, False diff --git a/mapping_cli/maneuvers/rpp.py b/mapping_cli/maneuvers/rpp.py new file mode 100644 index 0000000..f96b10e --- /dev/null +++ b/mapping_cli/maneuvers/rpp.py @@ -0,0 +1,463 @@ +import itertools +import json +import os + +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import numpy as np +import shapely +from matplotlib.collections import PathCollection +from matplotlib.transforms import Affine2D + +from mapping_cli.halts import get_halts +from mapping_cli.locator import get_locations, read_aruco_traj_file +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import (get_graph_box, get_marker_coord, + get_plt_rotation_from_markers, + majority_vote_smoothen, rotate_rectangle, + rotation_matrix_to_euler_angles, + smoothen_trajectory, yml_parser) + + +class RPP(Maneuver): + def run(self) -> None: + # 1 Get trajectory + traj = get_locations( + self.inputs["back_video"], + "rpp", + self.config["map_file_path"], + self.out_folder, + self.config["calib_file_path"], + self.config["marker_size"], + self.config["aruco_test_exe"], + cwd=self.inputs["cwd"], + plot=False, + return_read=True, + annotate=False, + ) + if traj is False: + return False + + _, tx, ty, tz, camera_matrices = traj + + if len(tx) > 99: + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 99, 199, 2) + else: + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 33, 199 // 3, 2) + + map_file = self.config["map_file_path"] + if not os.path.exists(map_file): + map_file = os.path.join(self.inputs["cwd"], map_file) + markers = yml_parser(map_file) + # 2 In box + box_path = self.config["box_file_path"] + if not os.path.exists(box_path): + box_path = os.path.join(self.inputs["cwd"], box_path) + is_inside, max_iou, _ = is_car_inside( + tx, + ty, + tz, + camera_matrices, + self.config["car_dims"], + markers_vertical=self.config["markers_vertical"], + box_overlap_threshold=self.config["box_overlap_threshold"], + box_path=box_path, + ) + + _, stats = get_direction_stats( + tx, + ty, + tz, + camera_matrices, + self.config["rev_fwd_halt_segment_min_frame_len"], + self.config["min_frame_len"], + self.out_folder, + ) + # 3 Standard path check + + _, std_path_decision = check_standard_path( + tx, + ty, + tz, + camera_matrices, + markers, + self.config, + "rpp", + self.out_folder, + self.inputs["cwd"], + True, + ) + print( + f"is inside: {is_inside}, stats: {stats}, std path decision: {std_path_decision}" + ) + return ( + (is_inside and std_path_decision, stats), + { + "back_video": self.inputs["back_video"], + "rpp_standard_path": f"{os.path.join(self.out_folder, 'rpp_trajectory.png')}", + "rpp_trajectory_path": f"{os.path.join(self.out_folder, 'standard_path_debug_rpp.png')}", + }, + ) + + +def post_process_direction_vector(stats, directions): + # aggregate seq + uniq_vals = [] + pivot_val = directions[0] + pivot_len = 1 + i = 1 + while i < len(directions): + while i < len(directions) and pivot_val == directions[i]: + pivot_len += 1 + i += 1 + if i < len(directions): + uniq_vals.append([pivot_val, pivot_len]) + pivot_val = directions[i] + pivot_len = 1 + i += 1 + if i == len(directions): + uniq_vals.append([pivot_val, pivot_len]) + break + + # delete from seq + if len(uniq_vals) >= 3: + del_elem = [] + for idx in range(0, len(uniq_vals) - 2): + # R = 0, H = -1, F = 1 + if ( + uniq_vals[idx + 0][0] == 0 + and uniq_vals[idx + 1][0] == -1 + and uniq_vals[idx + 2][0] == 0 + ): # RHR --> RR + stats["R"] -= 1 + stats["H"] -= 1 + uniq_vals[idx][1] += uniq_vals[idx + 1][1] + uniq_vals[idx][1] += uniq_vals[idx + 2][1] + del_elem.append(idx + 1) + del_elem.append(idx + 2) + for idx in del_elem[::-1]: + del uniq_vals[idx] + + # recreate seq + directions = [] + for a, b in uniq_vals: + directions += [a] * b # pythonic? + + return stats, directions + + +def get_direction_stats( + tx, + ty, + tz, + camera_matrices, + rev_fwd_halt_segment_min_frame_len, + min_frame_len, + out_folder, + maneuver=None, + args=None, +): + """ """ + + directions = [1] + Xs = [] + for i in range(1, len(tx)): + translation = ( + np.array([tx[i], ty[i], tz[i], 1.0]).reshape(4, 1).astype(np.float64) + ) + X = camera_matrices[i - 1][:3, :3].dot( + translation[:3, 0] - camera_matrices[i - 1][:3, -1] + ) + # print(X) + direction = X[2] + + if [tx[i], ty[i], tz[i]] == [tx[i - 1], ty[i - 1], tz[i - 1]]: + directions.append(directions[-1]) + else: + if direction >= 0: + directions.append(1) + else: + directions.append(0) + + directions = directions[1:] + + # Get Forward-Reverse + directions = np.array(directions + [-1]) + + # Get halts + halt_info = get_halts(tx, ty, tz) + directions[halt_info == True] = -1 + directions = directions.tolist() + directions = majority_vote_smoothen(directions, rev_fwd_halt_segment_min_frame_len) + + stats, directions = aggregate_direction(directions, min_frame_len) + stats, _ = aggregate_direction(directions, min_frame_len) + + stats, directions = post_process_direction_vector(stats, directions) + + # if args and args.debug: + debug_directions_visualize(tx, ty, tz, directions, out_folder) + + return directions, stats + + +def debug_directions_visualize(tx, ty, tz, directions, out_folder): + """ """ + colors = ["red", "blue", "black"] + le = min(min(len(tx) - 1, len(tz) - 1), len(directions) - 1) + plt.scatter( + [tx[i] for i in range(0, le)], + [tz[i] for i in range(0, le)], + c=[colors[directions[i]] for i in range(0, le)], + ) + plt.show() + plt.savefig(os.path.join(out_folder, "rpp_trajectory.png")) + plt.clf() + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["c"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_car_box(cam_x, cam_y, camera_matrix, car_dims, markers_vertical): + car_top_left = [cam_x - car_dims["x_offset"], cam_y - car_dims["z_offset"]] + car_top_right = [car_top_left[0] + car_dims["width"], car_top_left[1]] + car_bottom_right = [ + car_top_left[0] + car_dims["width"], + car_top_left[1] + car_dims["length"], + ] + car_bottom_left = [car_top_left[0], car_top_left[1] + car_dims["length"]] + + angle = rotation_matrix_to_euler_angles(camera_matrix)[1] + + if markers_vertical: + angle *= -1.0 + + car_pts = np.array([car_top_left, car_top_right, car_bottom_right, car_bottom_left]) + car_rotated_pts = rotate_rectangle(car_pts, np.array([cam_x, cam_y]), angle) + + return car_rotated_pts + + +def get_car_box_iou(camera_coords, camera_matrix, car_dims, markers_vertical, box_path): + box = get_maneuver_box(box_path) + car_pts = get_car_box( + camera_coords[0], camera_coords[1], camera_matrix, car_dims, markers_vertical + ) + + box_poly = shapely.geometry.Polygon(box.tolist()) + car_poly = shapely.geometry.Polygon(car_pts.tolist()) + + iou = car_poly.intersection(box_poly).area / car_poly.union(box_poly).area + norm_iou = car_poly.area / box_poly.area + + return iou / norm_iou + + +def aggregate_direction(direction_vector, halt_len): + """ + Input: vector containing 'forward', 'reverse' + Output: # of forwards, # of reverse + """ + direction_count = {"F": 0, "R": 0, "H": 0} + # Group consecutive directions + grouped_class = [ + list(l) + for _, l in itertools.groupby(enumerate(direction_vector), key=lambda x: x[1]) + ] + new_direction_vector = [] + for c_idx, c in enumerate(grouped_class): + if c[0][1] == 0: + direction_count["R"] += 1 + new_direction_vector += [0 for i in range(len(c))] + elif c[0][1] == 1: + direction_count["F"] += 1 + new_direction_vector += [1 for i in range(len(c))] + elif c[0][1] == -1: + if len(c) > halt_len: + direction_count["H"] += 1 + new_direction_vector += [-1 for i in range(len(c))] + else: + # Start segment + if c_idx > 0: + prev_direction = grouped_class[c_idx - 1][0][1] + else: + next_direction = grouped_class[c_idx + 1][0][1] + prev_direction = next_direction + + # End segment + if c_idx < len(grouped_class) - 1: + next_direction = grouped_class[c_idx + 1][0][1] + else: + next_direction = prev_direction + + new_direction_vector += [prev_direction for i in range(len(c) // 2)] + new_direction_vector += [next_direction for i in range(len(c) // 2)] + + return direction_count, new_direction_vector + + +def is_car_inside( + tx, + ty, + tz, + camera_matrices, + car_dims, + markers_vertical, + box_overlap_threshold, + box_path, +): + assert len(tx) == len(ty) == len(tz) + max_iou = -1 + max_iou_idx = 0 + counts = 0 + for i in range(len(tx)): + iou = get_car_box_iou( + [tx[i], tz[i]], + camera_matrices[i][:3, :3], + car_dims, + markers_vertical, + box_path, + ) + if iou > box_overlap_threshold: + counts += 1 + if iou > max_iou: + max_iou = iou + max_iou_idx = i + max_iou = float(np.round(max_iou, 2)) + if max_iou > box_overlap_threshold: + return True, max_iou, max_iou_idx + else: + return False, max_iou, max_iou_idx + + +def rotate_plot(ax, config, origin_marker, axis_marker, is_vertical): + rot_deg = get_plt_rotation_from_markers(origin_marker, axis_marker, is_vertical) + if rot_deg == 0.0: + return + r = Affine2D().rotate_deg(rot_deg) + + for x in ax.images + ax.lines + ax.collections + ax.patches: + trans = x.get_transform() + x.set_transform(r + trans) + if isinstance(x, PathCollection): + transoff = x.get_offset_transform() + x._transOffset = r + transoff + + if config["invert_y"]: + ax.invert_yaxis() + if config["invert_x"]: + ax.invert_xaxis() + + +def plot_limits(): + plot_limits = {"xlim": [-20, 20], "ylim": [-20, 20]} + plt.xlim(*plot_limits["xlim"]) + plt.ylim(*plot_limits["ylim"]) + plt.gca().set_aspect("equal", "box") + + +def debug_plot( + tx, + tz, + manu, + markers, + box, + labels, + out_f, + config, + origin_marker, + axis_marker, + is_vertical, +): + plt.scatter(tx[:-1], tz[:-1], marker=".", c=[[1, 0, 0]]) + + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + + ax = plt.gca() + + for label in labels: + poly = mpatches.Polygon(box[label], alpha=1, fill=False, edgecolor="blue") + ax.add_patch(poly) + + plot_limits() + + rotate_plot(ax, config, origin_marker, axis_marker, is_vertical) + plt.savefig(os.path.join(out_f, "standard_path_debug_{}.png".format(manu))) + + +def check_standard_path( + tx, ty, tz, camera_matrices, markers, config, manu, out_f, cwd, debug=False +): + # tx, ty, tz = smoothen_trajectory(tx, ty, tz, 99, 199, 2) + assert len(tx) > 0 + plt.cla() + plt.clf() + labels = config["standard_node_visit_order"] + box_path = config["box_file_path"] + if not os.path.exists(box_path): + box_path = os.path.join(cwd, box_path) + boxes = [ + shapely.geometry.Polygon(get_graph_box(box_path, label).tolist()) + for label in labels + ] + values = [] + for i in range(len(tx)): + pos = shapely.geometry.Point([tx[i], tz[i]]) + for j, box in enumerate(boxes): + if box.contains(pos): + # print(labels[j]) + values.append(labels[j]) + break + else: + print("Strange") + + assert len(tx) > 0 + manu_order = [values[0]] + for i in range(1, len(values) - 1): + if values[i] != values[i - 1]: + manu_order.append(values[i]) + + origin_marker = markers["aruco_bc_markers"][config["origin_marker"]] + axis_marker = markers["aruco_bc_markers"][config["axis_marker"]] + + with open(box_path) as f: + debug_plot( + tx, + tz, + manu, + markers, + json.load(f), + labels, + out_f, + config, + origin_marker, + axis_marker, + config["is_vertical"], + ) + + # is the subsequence 'BC' in the visit order of the sequence or not? + # C is the box we want to be in, B is the box ahead (from + # where we want the car to reverse) + # Acceptable: + # ABC, ABCB, ACBC, ACBCB etc + # Non Acceptable: + # ABAC etc + manu_order = "".join(manu_order) + substrings = config["acceptable_order_substrings"] + for substring in substrings: + if substring in manu_order: + return manu_order, True + return manu_order, False diff --git a/mapping_cli/maneuvers/seat_belt.py b/mapping_cli/maneuvers/seat_belt.py new file mode 100644 index 0000000..7970aaf --- /dev/null +++ b/mapping_cli/maneuvers/seat_belt.py @@ -0,0 +1,209 @@ +import os +import subprocess +from distutils.sysconfig import get_python_lib +from pathlib import Path +from typing import Dict + +import cv2 +import torch +import torchvision +from PIL import Image +from torchvision.models.detection.faster_rcnn import FastRCNNPredictor +from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor +from torchvision.transforms import functional as F +from tqdm import tqdm + +from mapping_cli.config.config import Config +from mapping_cli.maneuvers.maneuver import Maneuver + +torchvision.ops.misc.ConvTranspose2d = torch.nn.ConvTranspose2d + +# if os.path.isfile(get_python_lib() + "/plateDetect"): +# BASE_DIR = get_python_lib() + "/plateDetect" +# else: +# BASE_DIR = os.path.dirname(__file__) + + +class SeatBelt(Maneuver): + def save(self): + return super().save() + + def run(self): + out_folder: str = self.out_folder + use_gpu = False + if "gpu" in str(self.config.get_config_value("device")) or "cuda" in str( + self.config.get_config_value("device") + ): + use_gpu = True + + exitcode, new_fpath = frame_dropper(self.inputs["fpath"], out_folder, use_gpu) + if exitcode != 0: + raise Exception(f"Check input video file path! Received code {exitcode}") + + device = torch.device(self.config.get_config_value("device")) + model = get_model_instance_segmentation(2) + BASE_DIR = Path(__file__).parent.parent + model.load_state_dict( + torch.load( + os.path.join(BASE_DIR, *self.config["model_path"]), + map_location=torch.device(self.config.get_config_value("device")), + ) + ) + model.to(device) + model.eval() + + input_file = new_fpath + output_file = os.path.join( + out_folder, os.path.splitext(os.path.split(input_file)[1])[0] + "_out.mp4" + ) + + vid_iter = cv2.VideoCapture(input_file) + W = int(vid_iter.get(cv2.CAP_PROP_FRAME_WIDTH)) + H = int(vid_iter.get(cv2.CAP_PROP_FRAME_HEIGHT)) + vid_length = int(vid_iter.get(cv2.CAP_PROP_FRAME_COUNT)) + + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + skip_frames = self.config.get_config_value("skip_frames") + out_vid_iter = cv2.VideoWriter(output_file, fourcc, 25 / skip_frames, (W, H),) + + where = [] + num_seatbelt = 0 + image_false = None + image_true = None + + pbar = tqdm(total=vid_length) + pbar.set_description("Checking for seatbelt:") + idx = 0 + + while True: + ret, frame = vid_iter.read() + pbar.update(1) + if not ret: + break + + if idx % skip_frames == 0: + image = frame[int(2 * (H / 3)) : H, 0 : int(W / 2)] + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + image = Image.fromarray(image) + image, target = get_transform()(image, {}) + + outputs = model([image.to(device)]) + boxes = outputs[0]["boxes"].to(torch.device("cpu")).detach().numpy() + confs = outputs[0]["scores"].to(torch.device("cpu")).detach().numpy() + good_boxes = boxes[ + confs + > self.config.get_config_value("classifier_confidence_threshold") + ] + + if len(good_boxes) > 0: + num_seatbelt += 1 + where.append(1) + image_true = frame + else: + where.append(0) + image_false = frame + + for i, box in enumerate(good_boxes): + # I cropped the image, so the box coords are w.r.t. to cropped. Convert to original + box[0] += 0 + box[1] += int(2 * (H / 3)) + box[2] += 0 + box[3] += int(2 * (H / 3)) + cv2.rectangle( + frame, + (int(box[0]), int(box[1])), + (int(box[2]), int(box[3])), + (255, 0, 0), + 2, + ) + out_vid_iter.write(frame) + idx += 1 + out_vid_iter.release() + vid_iter.release() + pbar.close() + stats = {} + stats["vid_length"] = vid_length // skip_frames + stats["num_seatbelt"] = num_seatbelt + yes = 0 + for x in where: + if x == 1: + yes += 1 + if where != []: + percentage_detections = (yes * 1.0) / len(where) + else: + percentage_detections = 0.0 + wearing_all_the_time = True + if percentage_detections < self.config.get_config_value("detection_percentage"): + wearing_all_the_time = False + if wearing_all_the_time and image_true is not None: + cv2.imwrite(os.path.join(out_folder, "seatbelt_image.jpg"), image_true) + elif not wearing_all_the_time and image_false is not None: + cv2.imwrite(os.path.join(out_folder, "seatbelt_image.jpg"), image_false) + + self.report.add_report( + "Seatbelt", f"{percentage_detections}, {wearing_all_the_time}, {stats}" + ) + return percentage_detections, wearing_all_the_time, stats + + +def frame_dropper(fpath: str, out_folder: str, gpu_id: bool = False): + new_fpath = os.path.join(os.path.split(fpath)[0], "seatbelt_temp.mp4") + + ffmpeg_exec = "ffmpeg.exe" if os.name == "nt" else "ffmeg" + + if gpu_id: + call_string = '{} -y -i {} -filter:v "setpts=1/25 * PTS" -an -b:v 4000K -vcodec h264_nvenc -gpu {} {}'.format( + ffmpeg_exec, fpath, int(gpu_id), new_fpath + ) + else: + call_string = '{} -y -i {} -filter:v "setpts=1/25 * PTS" -an -b:v 4000K "{}"'.format( + ffmpeg_exec, fpath, new_fpath + ) + my_env = os.environ.copy() + my_env["PATH"] = out_folder + ";" + my_env["PATH"] + + if os.name == "nt": + exitcode = subprocess.call(call_string, shell=True, cwd=out_folder, env=my_env) + elif os.name == "posix": + exitcode = subprocess.call(call_string, shell=True) + + return exitcode, new_fpath + + +def get_model_instance_segmentation(num_classes): + # load an instance segmentation model pre-trained pre-trained on COCO + model = torchvision.models.detection.maskrcnn_resnet50_fpn(pretrained=False) + # get number of input features for the classifier + in_features = model.roi_heads.box_predictor.cls_score.in_features + # replace the pre-trained head with a new one + model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes) + # now get the number of input features for the mask classifier + in_features_mask = model.roi_heads.mask_predictor.conv5_mask.in_channels + hidden_layer = 256 + # and replace the mask predictor with a new one + model.roi_heads.mask_predictor = MaskRCNNPredictor( + in_features_mask, hidden_layer, num_classes + ) + return model + + +def get_transform(): + transforms = [] + transforms.append(ToTensor()) + return Compose(transforms) + + +class Compose(object): + def __init__(self, transforms): + self.transforms = transforms + + def __call__(self, image, target): + for t in self.transforms: + image, target = t(image, target) + return image, target + + +class ToTensor(object): + def __call__(self, image, target): + image = F.to_tensor(image) + return image, target diff --git a/mapping_cli/maneuvers/traffic.py b/mapping_cli/maneuvers/traffic.py new file mode 100644 index 0000000..44d233e --- /dev/null +++ b/mapping_cli/maneuvers/traffic.py @@ -0,0 +1,460 @@ +import json +import math +import os + +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +import numpy as np +import shapely + +from mapping_cli.halts import get_halts +from mapping_cli.maneuvers.maneuver import Maneuver +from mapping_cli.utils import (aggregate_direction, debug_directions_visualize, + generate_trajectory, get_marker_coord, + majority_vote_smoothen, plot_line, + rotate_rectangle, + rotation_matrix_to_euler_angles, + smoothen_trajectory, yml_parser) + + +def get_side_of_marker_from_line(markers, l_stop): + markers_side = [] + # In Standard form: -m*x + y - c = 0 + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + # placing marker coords into the line + # to determine their side + if ( + -1 * l_stop[0] * marker_coord[0] + marker_coord[1] - l_stop[1] > 0 + ): # Might want to have a looser criterion + markers_side.append(1) + else: + markers_side.append(-1) + if not all(map(lambda x: x == markers_side[0], markers_side)): + raise Exception("All markers for this maneuver should be behind the line!") + else: + return markers_side[0] + + +def select_traj_points_by_time(tx, tz, segment_json, fps): + if not os.path.exists(segment_json): + raise Exception("segment_json not part of input arguments!") + with open(segment_json) as f: + seg_vals = json.load(f) + print("Seg vals: ", seg_vals.keys()) + frame_num_traffic_light_vid_start_abs = math.ceil(seg_vals["traffic"]["start"][0]) + + frame_num_red_light_off_abs = fps + if frame_num_red_light_off_abs == -1: + print("WARNING!!!!! traffic Light hook is not implemented") + return list(zip(tx, tz)) + + if ( + frame_num_traffic_light_vid_start_abs > frame_num_red_light_off_abs + ): # my video somehow cut afterwards + return [] + + end_frame_red_light_check = ( + frame_num_red_light_off_abs - frame_num_traffic_light_vid_start_abs + ) + if end_frame_red_light_check > len(tx): + end_frame_red_light_check = len(tx) + return list(zip(tx[:end_frame_red_light_check], tz[:end_frame_red_light_check])) + + +def percentage_of_points_behind_line(selected_pts, l_stop, markers_side): + behind_lines_decision = [] + for x, z in selected_pts: + val_halt = -1 * l_stop[0] * x + z - l_stop[1] + if np.sign(val_halt) != np.sign(markers_side): + behind_lines_decision.append(1) + else: + behind_lines_decision.append(0) + + percentage_obey = 0.0 + for x in behind_lines_decision: + if x == 1: + percentage_obey += 1 + if behind_lines_decision == []: + percentage_obey = 0.0 + else: + percentage_obey /= len(behind_lines_decision) + return behind_lines_decision, percentage_obey + + +def post_process_direction_vector(stats, directions): + # aggregate seq + uniq_vals = [] + pivot_val = directions[0] + pivot_len = 1 + i = 1 + while i < len(directions): + while i < len(directions) and pivot_val == directions[i]: + pivot_len += 1 + i += 1 + if i < len(directions): + uniq_vals.append([pivot_val, pivot_len]) + pivot_val = directions[i] + pivot_len = 1 + i += 1 + if i == len(directions): + uniq_vals.append([pivot_val, pivot_len]) + break + + # delete from seq + if len(uniq_vals) >= 3: + del_elem = [] + for idx in range(0, len(uniq_vals) - 2): + # R = 0, H = -1, F = 1 + if ( + uniq_vals[idx + 0][0] == 0 + and uniq_vals[idx + 1][0] == -1 + and uniq_vals[idx + 2][0] == 0 + ): # RHR --> RR + stats["R"] -= 1 + stats["H"] -= 1 + uniq_vals[idx][1] += uniq_vals[idx + 1][1] + uniq_vals[idx][1] += uniq_vals[idx + 2][1] + del_elem.append(idx + 1) + del_elem.append(idx + 2) + for idx in del_elem[::-1]: + del uniq_vals[idx] + + # recreate seq + directions = [] + for a, b in uniq_vals: + directions += [a] * b # pythonic? + + return stats, directions + + +def get_line(line_info_json_f): + line_info = None + with open(line_info_json_f) as f: + line_info = json.load(f) + l_stop = np.polyfit(line_info["pts"]["tx"], line_info["pts"]["tz"], 1) + return l_stop + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["box"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_car_box(cam_x, cam_y, camera_matrix, manu, car_dims, site_config): + car_top_left = [cam_x - car_dims["x_offset"], cam_y - car_dims["z_offset"]] + car_top_right = [car_top_left[0] + car_dims["width"], car_top_left[1]] + car_bottom_right = [ + car_top_left[0] + car_dims["width"], + car_top_left[1] + car_dims["length"], + ] + car_bottom_left = [car_top_left[0], car_top_left[1] + car_dims["length"]] + + angle = rotation_matrix_to_euler_angles(camera_matrix)[1] + + if site_config.maneuvers_config["viz"][manu]["markers_vertical"]: + angle *= -1.0 + + car_pts = np.array([car_top_left, car_top_right, car_bottom_right, car_bottom_left]) + car_rotated_pts = rotate_rectangle(car_pts, np.array([cam_x, cam_y]), angle) + + return car_rotated_pts + + +def plot_traj( + tx, + tz, + manu, + car_dims, + line_json, + config, + maneuver: Maneuver, + markers=None, + save_image_name=None, + camera_matrices=None, + max_iou_idx=None, + segment_json=None, +): + if markers is not None: + plot_markers(markers) + + ax = plt.gca() + + # poly = mpatches.Polygon(box, alpha=1, fill=False, edgecolor='black') + # ax.add_patch(poly) + + # Plot Car + if max_iou_idx is not None: + position = max_iou_idx + car_pts = get_car_box( + tx[position], + tz[position], + camera_matrices[position][:3, :3], + manu, + car_dims, + ) + car_patch = mpatches.Polygon(car_pts, alpha=1, fill=False, edgecolor="red") + ax.add_patch(car_patch) + + line_info_json_f = line_json + + if config["line_type"] == "stop": + l_stop = get_line(line_info_json_f) + + stop_ped_offset = config["stop_ped_line_dist"] + l_ped = [l_stop[0], l_stop[1] + stop_ped_offset] + elif config["line_type"] == "ped": + l_ped = get_line(line_info_json_f) + stop_line_offset = config["stop_ped_line_dist"] + l_stop = [l_ped[0], l_ped[1] + stop_line_offset] + + line_viz_x_lim = config["xlim"] + + plot_line(plt, l_stop, line_viz_x_lim, "blue") + plot_line(plt, l_ped, line_viz_x_lim, "yellow") + + ## markers are always ahead of stop line, + ## ped line is gonna be ahead of stop line + ## X X * X $ | + ## * $ | + ## * $ | + ## * $ | + ## Legend: Marker : X, StopLine : |, PedLine1 : $, Pedline2 : * + ## Define markers_side_ped such that both PL1 and PL2 are satisfied + ## + ## As we can assume that ped_line will always be ahead of stop_line, + ## every point on ped_line will be sign(stop_line(point)) == sign(stop_line(marker)) + ## + ## Let's find a point on stop_line, that will always be behind ped_line + ## Use that point to find the side and then invert it + markers_side = get_side_of_marker_from_line(markers, l_stop) # say this is 1 + point_on_stop_line = (0, l_stop[0] * 0 + l_stop[1]) + markers_side_ped = -1 * np.sign( + -1 * l_ped[0] * point_on_stop_line[0] + point_on_stop_line[1] - l_ped[1] + ) + + selected_pts = select_traj_points_by_time(tx, tz, segment_json, config["fps"]) + + behind_lines_decision_ped, percentage_obey = percentage_of_points_behind_line( + selected_pts, l_ped, markers_side_ped + ) + behind_lines_decision_stop, percentage_stop = percentage_of_points_behind_line( + selected_pts, l_stop, markers_side + ) + label_decision = [] + for i in range(len(behind_lines_decision_ped)): + if behind_lines_decision_ped[i] == 1 and behind_lines_decision_stop[i] == 1: + label_decision.append(0) + elif behind_lines_decision_ped[i] == 1 and behind_lines_decision_stop[i] == 0: + label_decision.append(1) + elif behind_lines_decision_ped[i] == 0 and behind_lines_decision_stop[i] == 0: + label_decision.append(2) + else: + raise Exception("Error: Ped Line is not ahead of Stop Line in track!") + + obey_decision = "Fail" + if percentage_obey >= config["behind_lines_obey_threshold"]: + obey_decision = "Pass" + + stop_decision = "Fail" + if percentage_stop > config["behind_lines_stop_threshold"]: + stop_decision = "Pass" + + maneuver.report.add_report("trafficLight_obey_decision", obey_decision) + maneuver.report.add_report("trafficLight_stop_decision", stop_decision) + maneuver.report.add_report( + "trafficLight_outcome", + { + "percentage_behind_both_lines": percentage_obey, + "percentage_behind_stop_line": percentage_stop, + }, + ) + + print( + "({}) Halt Behind Stop Line %: {} Decision: {}".format( + manu, percentage_stop, stop_decision + ) + ) + print( + "({}) Halt Behind Ped Line %: {} Decision: {}".format( + manu, percentage_obey, obey_decision + ) + ) + + color_f = ["green", "yellow", "brown"] + size_f = [25, 25, 40] + len_plot = len(selected_pts) + print(len(selected_pts), len(label_decision)) + plt.scatter( + [x[0] for x in selected_pts], + [x[1] for x in selected_pts], + c=[color_f[label_decision[i]] for i in range(0, len_plot)], + s=[size_f[label_decision[i]] for i in range(0, len_plot)], + marker="*", + ) + + plot_legend() + plot_limits(config["xlim"], config["ylim"]) + + # if markers is not None: + # rotate_plot(ax, markers, manu) + + plt.savefig(save_image_name, dpi=200) + plt.close() + + +def plot_legend(): + red_patch = mpatches.Patch(color="red", label="Reverse") + blue_patch = mpatches.Patch(color="blue", label="Forward") + black_patch = mpatches.Patch(color="black", label="Halt") + plt.legend( + handles=[blue_patch, red_patch, black_patch], + prop={"size": 10}, + loc="upper right", + ) + + +def plot_markers(markers): + for key in markers["aruco_bc_markers"]: + marker_obj = markers["aruco_bc_markers"][key] + marker_coord = get_marker_coord(marker_obj) + plt.scatter( + marker_coord[0], marker_coord[1], c=[[0, 0, 0]], marker=".", s=100.0 + ) + + +def plot_limits(x_limits, y_limits): + plt.xlim(x_limits) + plt.ylim(y_limits) + plt.gca().set_aspect("equal", "box") + + +class Traffic(Maneuver): + def run(self): + map_path = self.config.get_config_value("map_file_path") + if not os.path.exists(map_path): + map_path = os.path.join(self.inputs["cwd"], map_path) + + calib_path = self.config["calibration_file_path"] + if not os.path.exists(calib_path): + calib_path = os.path.join(self.inputs["cwd"], calib_path) + try: + traj = generate_trajectory( + self.inputs["back_video"], + self.config.get_config_value("maneuver"), + map_path, + self.out_folder, + calib_path, + self.config["size_marker"], + self.config["aruco_test_exe"], + self.inputs["cwd"], + ) + except Exception as e: + print("Error generating traffic trajectory: ", e) + raise e + + _, tx, ty, tz, camera_matrices = traj + tx, ty, tz = smoothen_trajectory(tx, ty, tz, 25, 25, 2) + markers = yml_parser(map_path) + + # get_direction_stats + direction, stats = self.get_direction_stats( + tx, + ty, + tz, + camera_matrices, + self.config["rev_fwd_halt_segment_min_frame_len"], + self.config["min_frame_len"], + ) + + # fwd rev halts + halts = get_halts(tx, ty, tz) + + # plot + # plot_traj( + # tx, tz, "traffic", self.config['car_dims'], self.config['line_file_path'], self.config, "traffic", camera_matrices=camera_matrices, max_iou_idx=self.config['max_iou'], segment_json=os.path.join(self.out_folder, f"segment.json") + # ) + + line_path = self.config["line_file_path"] + if not os.path.exists(line_path): + line_path = os.path.join(self.inputs["cwd"], line_path) + + plot_traj( + tx, + tz, + "traffic", + self.config["car_dims"], + line_path, + self.config, + self, + markers, + segment_json=os.path.join(self.out_folder, "manu_json_seg_int.json"), + save_image_name=os.path.join(self.out_folder, "traffic.png"), + ) + + return ( + True, + stats, + { # TODO: Add pass/fail + "trajectory": f"{os.path.join(self.out_folder, 'traffic.png')}" + }, + ) + + def get_direction_stats( + self, + tx, + ty, + tz, + camera_matrices, + rev_fwd_halt_segment_min_frame_len, + min_frame_len, + ): + """ """ + directions = [1] + Xs = [] + for i in range(1, len(tx)): + translation = ( + np.array([tx[i], ty[i], tz[i], 1.0]).reshape(4, 1).astype(np.float64) + ) + X = camera_matrices[i - 1][:3, :3].dot( + translation[:3, 0] - camera_matrices[i - 1][:3, -1] + ) + # print(X) + direction = X[2] + + if [tx[i], ty[i], tz[i]] == [tx[i - 1], ty[i - 1], tz[i - 1]]: + directions.append(directions[-1]) + else: + if direction >= 0: + directions.append(1) + else: + directions.append(0) + + directions = directions[1:] + + # Get Forward-Reverse + directions = np.array(directions + [-1]) + + # Get halts + halt_info = get_halts(tx, ty, tz) + directions[halt_info == True] = -1 + directions = directions.tolist() + directions = majority_vote_smoothen( + directions, rev_fwd_halt_segment_min_frame_len + ) + + stats, directions = aggregate_direction(directions, min_frame_len) + stats, _ = aggregate_direction(directions, min_frame_len) + + stats, directions = post_process_direction_vector(stats, directions) + + debug_directions_visualize(plt, tx, ty, tz, directions) + + return directions, stats diff --git a/mapping_cli/mapper.py b/mapping_cli/mapper.py new file mode 100644 index 0000000..f19ae8e --- /dev/null +++ b/mapping_cli/mapper.py @@ -0,0 +1,211 @@ +import logging +import os +import signal +import subprocess +from threading import Timer +from time import sleep + +import typer +from click import command + +from mapping_cli import utils +from mapping_cli.validation import * + +LOG_FILENAME = "log.out" +logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG) + + +def run( + mapper_exe_path: str, + images_directory: str, + camera_params_path: str, + dictionary: str, + marker_size: str, + output_path: str, + cwd: str = None, +): + """Function to build a Map using the mapper exe and images + + Args: + mapper_exe_path (str): Mapper exe path. + images_directory (str): Image Directory Path. + camera_params_path (str): Camera config/param yml file path. + dictionary (str): Type of Dictionary. + marker_size (str): Size of the marker. + output_path (str): Output file name. + """ + try: + check_if_mapper_exe_is_valid(mapper_exe_path) + check_if_images_dir_exists(images_directory) + check_if_camera_config_is_valid(camera_params_path) + + typer.echo("Hey There! Starting to run the mapper!") + + num_of_images = len( + [ + name + for name in os.listdir(images_directory) + if os.path.isfile(os.path.join(images_directory, name)) + ] + ) + typer.echo(f"{num_of_images} num of images") + + def killFunc(): + os.kill(p.pid, signal.CTRL_C_EVENT) + + try: + call_string = '"{}" "{}" "{}" {} {} {}'.format( + mapper_exe_path, + images_directory, + camera_params_path, + marker_size, + dictionary, + output_path, + ) + + if cwd is None: + cwd = os.path.curdir + + my_env = os.environ.copy() + my_env["PATH"] = cwd + ";" + my_env["PATH"] + + p = subprocess.Popen( + call_string, + cwd=cwd, + env=my_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + timer = Timer(num_of_images, p.kill) + + timer.start() + stdout, stderr = p.communicate() + retcode = p.returncode + typer.echo(f"{retcode} {stdout} {stderr} {cwd}") + + if retcode == 0: + decoded_error = stderr.decode(encoding="utf-8") + typer.echo(f"Error! {decoded_error}") + + if "Dictionary::loadFromFile" in decoded_error: + typer.Exit(code=1) + return "Invalid Doctionary" + elif "CameraParameters::readFromXML" in decoded_error: + typer.Exit(code=2) + return "Invalid Camera Calibration File" + + return decoded_error + except Exception as e: + typer.echo(f"Error: {e}") + timer.cancel() + finally: + typer.echo("Cancelling Timer") + timer.cancel() + try: + call_string = "{} {} {} {} {} {}".format( + mapper_exe_path, + images_directory, + camera_params_path, + marker_size, + dictionary, + output_path, + ) + + if cwd is None: + cwd = os.path.curdir + + my_env = os.environ.copy() + my_env["PATH"] = cwd + ";" + my_env["PATH"] + + p = subprocess.Popen( + call_string, + cwd=cwd, + env=my_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + timer = Timer(num_of_images, p.kill) + + timer.start() + stdout, stderr = p.communicate() + retcode = p.returncode + typer.echo(f"{retcode} {stdout} {stderr} {cwd}") + + if retcode == 0: + decoded_error = stderr.decode(encoding="utf-8") + typer.echo(f"Error! {decoded_error}") + + if "Dictionary::loadFromFile" in decoded_error: + typer.Exit(code=1) + return "Invalid Doctionary" + elif "CameraParameters::readFromXML" in decoded_error: + typer.Exit(code=2) + return "Invalid Camera Calibration File" + + return decoded_error + except Exception as e: + typer.echo(f"Error: {e}") + timer.cancel() + finally: + typer.echo("Cancelling Timer") + timer.cancel() + exit(0) + + typer.echo(f"Done! Generated the map and saved as {output_path}") + + except Exception as e: + typer.echo(f"Error : {e}") + + +def distance_error(map_file, dist_file): + try: + aruco_map = utils.yml_parser(map_file)["aruco_bc_markers"] + markers = [] + gt_distances = [] + measured_distances = [] + errors = [] + + with open(dist_file) as f: + for line_idx, line in enumerate(f): + line = line.strip() + content = line.split(" ") + assert ( + len(content) == 3 + ), f"Check your file again, {line_idx} has {len(content)}: {line} wrong formatting of input" + a, corner_a = content[0].split("_") # read the values + b, corner_b = content[1].split("_") + gt = float(content[2]) + markers.append([a + "_" + corner_a, b + "_" + corner_b]) + gt_distances.append(gt) + if int(a) not in aruco_map.keys(): + raise KeyError( + f"Marker {a} in distance file not found in the map. Expected marker list: {aruco_map.keys()}" + ) + if int(b) not in aruco_map.keys(): + raise KeyError( + f"Marker {b} in distance file not found in the map. Expected marker list: {aruco_map.keys()}" + ) + + measured_distance = utils.euclidean_distance( + aruco_map[int(a)][int(corner_a) - 1], + aruco_map[int(b)][int(corner_b) - 1], + ) + measured_distances.append(measured_distance) + err = abs(gt - measured_distance) + errors.append(err) + logging.info( + "Ground Truth: {} Measured Dist: {} Err: {}".format( + gt, measured_distance, err + ) + ) + avg_err = sum(errors) / len(errors) + + typer.echo(f"The Calculated average error is {avg_err}") + return avg_err, None + + except Exception as e: + logging.exception(e) + typer.echo(f"Error : {e}") + return None, str(e) diff --git a/mapping_cli/segment.py b/mapping_cli/segment.py new file mode 100644 index 0000000..8c42215 --- /dev/null +++ b/mapping_cli/segment.py @@ -0,0 +1,347 @@ +"""Video segmentation module +""" + +import json +import os +import time +from typing import Dict + +import cv2 +import numpy as np +from decord import VideoReader + +from mapping_cli.utils import detect_marker, trim_video + + +def get_manu_frame_segments(back_video, out_fldr, configs): + manus_frame_nums = { + x: {"start": [], "end": []} + for x in configs["maneuver_order"] # manu : { start : [], end : [] } + } + marker_list_json = {} + + skip_frames = configs["skip_frames"] + vidcap = cv2.VideoCapture(back_video) + # success, image = vidcap.read() + vid_length = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT)) + maneuvers_in_order = configs["maneuver_order"] + fps = vidcap.get(cv2.CAP_PROP_FPS) + frame_count = vidcap.get(cv2.CAP_PROP_FRAME_COUNT) + reader = VideoReader(back_video) + + json_manu_f = os.path.join(out_fldr, "manu_json_seg_int.json") + json_marker_f = os.path.join(out_fldr, "manu_json_marker_list.json") + if os.path.exists(json_manu_f): + # with open(json_manu_f) as f: + # manus_frame_nums = json.load(f) + # with open(json_marker_f) as f: + # marker_list_json = json.load(f) + pass + else: + marker_list_json = {} + + count = 0 + idx = 0 + for i in range(len(reader)): + if idx % skip_frames == 0: + image = reader.next().asnumpy() + marker_list = detect_marker(image, configs["marker_type"]) + marker_list_json[idx] = [int(x) for x in marker_list] + for manu in maneuvers_in_order: + if ( + list( + value in marker_list for value in configs[manu]["startMarkers"] + ).count(True) + >= configs[manu]["startMarkersLen"] + ): + manus_frame_nums[manu]["start"].append(float(idx) / fps) + break + + elif ( + list( + value in marker_list for value in configs[manu]["endMarkers"] + ).count(True) + >= configs[manu]["endMarkersLen"] + ): + manus_frame_nums[manu]["end"].append(float(idx) / fps) + break + # success, image = vidcap.read() + idx += 1 + if idx > 1e5: + raise Exception("Too much time") + + with open(json_marker_f, "w") as f: + json.dump(marker_list_json, f) + with open(json_manu_f, "w") as f: + json.dump(manus_frame_nums, f) + + print("Manus: ", manus_frame_nums) + for k, v in manus_frame_nums.items(): + assert v["start"], f"Maneuver {k} has empty start segmentation number" + assert v["end"], f"Maneuver {k} has empty start segmentation number" + + # transform_manus_with_constraints( + # manus_frame_nums, configs, maneuvers_in_order, len(reader), configs["fps"] + # ) + + return manus_frame_nums, vid_length + + +def transform_manus_with_constraints( + manus_frame_nums, manu_info, manus_in_order, vid_length, video_fps +): + try: + outlier_fn_dispatcher = { + "outlier_from_std_dev": outlier_from_std_dev, + "outlier_from_time_threshold": outlier_from_time_threshold, + "pad_time_to_small_seq": pad_time_to_small_seq, + "outlier_from_max_time_difference": outlier_from_max_time_difference, + "ensure_arr_vals_greater_than_prev_manu": ensure_arr_vals_greater_than_prev_manu, + } # Can do eval(fn_string)(args) but unsafe + for manu in manu_info.keys(): + # print(manu) + if manu_info[manu]["outlier_fns_list_start"] is not None: + for fn in manu_info[manu]["outlier_fns_list_start"]: + fn_name = fn[0] + args = fn[1] + if fn_name == "outlier_from_std_dev": + manus_frame_nums[manu]["start"] = outlier_fn_dispatcher[ + fn_name + ](manus_frame_nums[manu]["start"], args["s_threshold"]) + elif fn_name == "outlier_from_time_threshold": + manu_to_test, seg, seg_idx, time_slack = ( + args["manu"], + args["seg_type"], + args["seg_idx"], + args["time_slack"], + ) + manus_frame_nums[manu]["start"] = outlier_fn_dispatcher[ + fn_name + ]( + manus_frame_nums[manu]["start"], + manus_frame_nums[manu_to_test][seg][seg_idx], + time_slack, + False, + video_fps, + ) + elif fn_name == "outlier_from_max_time_difference": + manus_frame_nums[manu]["start"] = outlier_fn_dispatcher[ + fn_name + ]( + manus_frame_nums[manu]["start"], + args["time_slack"], + video_fps, + args["take_first_seg"], + ) + elif fn_name == "ensure_arr_vals_greater_than_prev_manu": + prev_manu, prev_seg, prev_seg_idx = ( + args["prev_manu"], + args["prev_seg_type"], + args["prev_seg_idx"], + ) + try: + prev_thresh = manus_frame_nums[prev_manu][prev_seg][ + prev_seg_idx + ] + except: + prev_thresh = None + manus_frame_nums[manu]["start"] = outlier_fn_dispatcher[ + fn_name + ](manus_frame_nums[manu]["start"], prev_thresh) + elif fn_name == "pad_time_to_small_seq": + manus_frame_nums[manu]["start"] = outlier_fn_dispatcher[ + fn_name + ]( + manus_frame_nums[manu]["start"], + args["min_size"], + args["time_slack"], + False, + video_fps, + None, + ) + if manu_info[manu]["outlier_fns_list_end"] is not None: + for fn in manu_info[manu]["outlier_fns_list_end"]: + fn_name = fn[0] + args = fn[1] + if fn_name == "outlier_from_std_dev": + manus_frame_nums[manu]["end"] = outlier_fn_dispatcher[fn_name]( + manus_frame_nums[manu]["end"], args["s_threshold"] + ) + elif fn_name == "outlier_from_time_threshold": + manu_to_test, seg, seg_idx, time_slack = ( + args["manu"], + args["seg"], + args["seg_idx"], + args["time_slack"], + ) + manus_frame_nums[manu]["end"] = outlier_fn_dispatcher[fn_name]( + manus_frame_nums[manu]["end"], + manus_frame_nums[manu_to_test][seg][seg_idx], + time_slack, + True, + video_fps, + ) + elif fn_name == "outlier_from_max_time_difference": + manus_frame_nums[manu]["end"] = outlier_fn_dispatcher[fn_name]( + manus_frame_nums[manu]["end"], + args["time_slack"], + video_fps, + args["take_first_seg"], + ) + elif fn_name == "ensure_arr_vals_greater_than_prev_manu": + prev_manu, prev_seg, prev_seg_idx = ( + args["prev_manu"], + args["prev_seg_type"], + args["prev_seg_idx"], + ) + try: + prev_thresh = manus_frame_nums[prev_manu][prev_seg][ + prev_seg_idx + ] + except: + prev_thresh = None + manus_frame_nums[manu]["end"] = outlier_fn_dispatcher[fn_name]( + manus_frame_nums[manu]["end"], prev_thresh + ) + elif fn_name == "pad_time_to_small_seq": + manus_frame_nums[manu]["end"] = outlier_fn_dispatcher[fn_name]( + manus_frame_nums[manu]["end"], + args["min_size"], + args["time_slack"], + True, + video_fps, + ) + + for manu in manus_in_order: + if manu_info[manu]["constraint_for_start"] is not None: + constraint = manu_info[manu]["constraint_for_start"] + constraint_manu = constraint["constraint_manu"] + constraint_type = constraint["constraint_type"] + if constraint_type == "hard" or manus_frame_nums[manu]["start"] == []: + if constraint_manu == "vid_start": + value = 0 + else: + seg_type = constraint["seg_type"] + seg_idx = constraint["seg_idx"] + value = manus_frame_nums[constraint_manu][seg_type][seg_idx] + + manus_frame_nums[manu]["start"] = [value] + + if manu_info[manu]["constraint_for_end"] is not None: + constraint = manu_info[manu]["constraint_for_end"] + constraint_manu = constraint["constraint_manu"] + constraint_type = constraint["constraint_type"] + if constraint_type == "hard" or manus_frame_nums[manu]["end"] == []: + if constraint_manu == "vid_end": + value = vid_length - 1 + else: + seg_type = constraint["seg_type"] + seg_idx = constraint["seg_idx"] + value = manus_frame_nums[constraint_manu][seg_type][seg_idx] + + manus_frame_nums[manu]["end"] = [value] + except Exception as e: + print("Segmentation transformation exception: ", e) + + return manus_frame_nums + + +def outlier_from_std_dev(arr, s_threshold): + m = np.median(arr) + s = np.std(arr) + arr = filter(lambda x: x < m + s_threshold * s, arr) + arr = filter(lambda x: x > m - s_threshold * s, arr) + return list(arr) + + +def outlier_from_time_threshold(arr, val, time_skip, after_manu, video_fps): + if after_manu == True: + time_skip = val + time_skip * video_fps + return list(filter(lambda x: x < time_skip, arr)) + else: + time_skip = val - time_skip * video_fps + return list(filter(lambda x: x > time_skip, arr)) + + +def pad_time_to_small_seq(arr_prev, min_size, time_slack, flag, video_fps): + if arr_prev == []: + return [] + if len(arr_prev) <= min_size: + if flag == True: + for i in range(arr_prev[0] + 1, arr_prev[0] + time_slack * video_fps): + arr_prev.append(i) + else: + new_arr = [] + for i in range(arr_prev[0] - time_slack * video_fps, arr_prev[0] - 1): + new_arr = new_arr.append(i) + new_arr.extend(arr_prev) + return new_arr + return arr_prev + + +def ensure_arr_vals_greater_than_prev_manu(arr, thresh): + if thresh is None: + return arr + return list(filter(lambda x: x > thresh, arr)) + + +def outlier_from_max_time_difference(arr, time_slack, video_fps, take_first_seg): + seg_dist = time_slack * video_fps + idx = None + + for i in range(0, len(arr) - 1): + if arr[i + 1] - arr[i] >= seg_dist: + idx = i + break + + if idx is None: + return arr + if take_first_seg: + return arr[: idx + 1] + return arr[idx + 1 :] + + +def segment( + front_video_path, back_video_path: str, output_path: str, configs: Dict +) -> None: + """Segment video into frames""" + # Create output folder if it doesn't exist + t = time.time() + if not os.path.exists(output_path): + os.makedirs(output_path) + + assert ( + "maneuver_order" in configs.keys() + ), f"Missing maneuver_order in configs keys: {configs.keys()}" + + maneuver_order = configs["maneuver_order"] + + maneuver_frame_numbers, _ = get_manu_frame_segments( + back_video_path, output_path, configs + ) + segment_warnings = [] + segment_paths = {} + for i in range(len(maneuver_order)): + if i != len(maneuver_order) - 1: + if ( + maneuver_frame_numbers[maneuver_order[i]]["end"] + > maneuver_frame_numbers[maneuver_order[i + 1]]["start"] + ): + segment_warnings.append( + f"{maneuver_order[i]} crossing {maneuver_order[i+1]} limits" + ) + print( + f"Start;;;;; {maneuver_frame_numbers[maneuver_order[i]]['start']} || {maneuver_frame_numbers[maneuver_order[i]]['end']}" + ) + path = trim_video( + back_video_path, + maneuver_frame_numbers[maneuver_order[i]]["start"][0], + maneuver_frame_numbers[maneuver_order[i]]["end"][-1], + configs["use_gpu"], + output_path, + maneuver_order[i] + ".mp4", + ) + segment_paths[str(maneuver_order[i])] = path + + print("Time: ", time.time() - t) + return segment_paths, segment_warnings diff --git a/mapping_cli/utils.py b/mapping_cli/utils.py new file mode 100644 index 0000000..c9242b9 --- /dev/null +++ b/mapping_cli/utils.py @@ -0,0 +1,536 @@ +import itertools +import json +import logging +import math +import os +import subprocess +from typing import Dict + +import cv2 +import ffmpeg +import filelock +import numpy as np +import scipy +import shapely +import yaml + + +def yml_parser(map_file): + ret = None + with open(map_file, "r") as stream: + stream.readline() # YAML 1.0 bug + try: + ret = yaml.safe_load(stream) + except yaml.YAMLError as exc: + logging.info(exc) + raise exc + ## YAML 1.0 bug + # YAML library handles YAML 1.1 and above, + # to fix one of the bug in the generated YAML + for x in ret["aruco_bc_markers"]: + invalid_key = list(filter(lambda x: x.startswith("id"), x.keys()))[0] + key, value = invalid_key.split(":") + x[key] = value + del x[invalid_key] + marker_dict = {} + for dct in ret["aruco_bc_markers"]: + marker_dict[int(dct["id"])] = dct["corners"] + ret["aruco_bc_markers"] = marker_dict + return ret + + +def euclidean_distance(A, B): + return np.linalg.norm(np.array(A) - np.array(B), 2) + + +def euclidean_distance_batch(A, B): + dist_array = scipy.spatial.distance.cdist(A, B, metric="euclidean") + return dist_array + + +def generate_trajectory( + input_video: str, + maneuver: str, + map_file_path: str, + out_folder: str, + calibration: str, + size_marker: str, + aruco_test_exe: str, + cwd: str, +): + """ + Generate a trajectory from a video and a maneuver + :param input_video: input video path + :param maneuver: maneuver trajectory to be generated + :param map_file_path: map file path + :param out_folder: output folder + :param calibration: calibration + :param size_marker: size of the marker + :param aruco_test_exe: aruco test executable + :return: + """ + print(os.getcwd()) + output_traj_path = os.path.join(out_folder, maneuver + "_trajectory.txt") + exec_string = f"{aruco_test_exe} {input_video} {map_file_path} {calibration} -s {size_marker} {output_traj_path}" + + assert os.path.exists(input_video), f"{input_video} does not exist" + assert os.path.exists(map_file_path), f"{map_file_path} does not exist" + assert os.path.exists(calibration), f"{calibration} does not exist" + # assert os.path.exists(aruco_test_exe), f"{aruco_test_exe} does not exist" + + try: + # output = subprocess.check_output( + # exec_string, shell=True, stderr=subprocess.STDOUT + # ) + + my_env = os.environ.copy() + my_env["PATH"] = out_folder + ";" + cwd + ";" + my_env["PATH"] + + p = subprocess.Popen( + exec_string, + shell=True, + cwd=cwd, + env=my_env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + + except subprocess.CalledProcessError as e: + raise RuntimeError( + "command '{}' return with error (code {}): {}".format( + e.cmd, e.returncode, e.output + ) + ) + + # popen = subprocess.Popen(exec_string, shell=True, stdout=subprocess.PIPE) + # out, err = popen.communicate() + + # print(exec_string, out, err) + + output = stdout.decode("utf-8") + output = ( + output.replace(";\r\n", "") + .replace("]", "") + .replace("[", "") + .replace(",", "") + .replace("\r\n", "\n") + ) + + with open(output_traj_path, "w+") as f: + f.write(output) + f.close() + + return read_aruco_traj_file(output_traj_path, {}) + + +def get_marker_coord(marker_obj): + return [marker_obj[0][0], marker_obj[0][2]] + + +def aggregate_direction(direction_vector, halt_len): + """ + Input: vector containing 'forward', 'reverse' + Output: # of forwards, # of reverse + """ + direction_count = {"F": 0, "R": 0, "H": 0} + # Group consecutive directions + grouped_class = [ + list(l) + for _, l in itertools.groupby(enumerate(direction_vector), key=lambda x: x[1]) + ] + new_direction_vector = [] + for c_idx, c in enumerate(grouped_class): + if c[0][1] == 0: + direction_count["R"] += 1 + new_direction_vector += [0 for i in range(len(c))] + elif c[0][1] == 1: + direction_count["F"] += 1 + new_direction_vector += [1 for i in range(len(c))] + elif c[0][1] == -1: + if len(c) > halt_len: + direction_count["H"] += 1 + new_direction_vector += [-1 for i in range(len(c))] + else: + # Start segment + if c_idx > 0: + prev_direction = grouped_class[c_idx - 1][0][1] + else: + next_direction = grouped_class[c_idx + 1][0][1] + prev_direction = next_direction + + # End segment + if c_idx < len(grouped_class) - 1: + next_direction = grouped_class[c_idx + 1][0][1] + else: + next_direction = prev_direction + + new_direction_vector += [prev_direction for i in range(len(c) // 2)] + new_direction_vector += [next_direction for i in range(len(c) // 2)] + + return direction_count, new_direction_vector + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["box"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_C_coord(rvec, tvec): + """ + Input: rotation vector, translation vector + Returns: Camera coordinates + """ + rvec = cv2.Rodrigues(np.array(rvec))[0] + tvec = np.array(tvec) + return -np.dot(np.transpose(rvec), tvec) + + +def read_aruco_traj_file(filename, ignoring_points_dict): + f_id = [] + t_x = [] + t_y = [] + t_z = [] + camera_matrices = [] + with open(filename, "r") as f: + line = f.readline() + while line: + if not line[0].isdigit(): + line = f.readline() + continue + else: # rvec- rotational vector, tvec - translational vector + row = line.split(" ") + cur_id = int(row[0].strip()) + if not cur_id in ignoring_points_dict: + rvec = [ + float(row[1].strip()), + float(row[2].strip()), + float(row[3].strip()), + ] + tvec = [ + float(row[4].strip()), + float(row[5].strip()), + float(row[6].strip()), + ] + + cvec = get_C_coord(rvec, tvec) + cvec = cvec.reshape(3, 1) + + temp = np.hstack((cv2.Rodrigues(np.array(rvec))[0], cvec)) + camera_matrix = np.vstack((temp, np.array([0.0, 0.0, 0.0, 1.0]))) + + f_id.append(int(row[0].strip())) + camera_matrices.append(camera_matrix) + t_x.append(cvec[0][0]) + t_y.append(cvec[1][0]) + t_z.append(cvec[2][0]) + + line = f.readline() + + # if len(t_x) == 0: + # raise Exception("Trajectory length 0") + + return f_id, t_x, t_y, t_z, camera_matrices + + +def stitch(input_imgs_path: str, input_file_extension: str, output_file_name: str): + ffmpeg.input(os.path.join(input_imgs_path, f"*.{input_file_extension}")).output( + output_file_name + ).run() + return output_file_name + + +def detect_marker(frame, marker_dict): + DICT_CV2_ARUCO_MAP = { + "TAG16h5": cv2.aruco.DICT_APRILTAG_16h5, + "DICT_4X4_100": cv2.aruco.DICT_4X4_100, + } + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + aruco_dict = cv2.aruco.Dictionary_get(DICT_CV2_ARUCO_MAP[marker_dict]) + parameters = cv2.aruco.DetectorParameters_create() + _, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=parameters) + markers = [] + if ids is not None: + for i in ids: + markers.append(i[0]) + return markers + + +def trim_video( + video_path: str, + start_segment, + end_segment, + use_gpu: bool, + out_folder: str, + output_name: str, +): + device_str = "" + if use_gpu: + device_str = " -gpu 0 " + call_string = f"ffmpeg -y -hide_banner -loglevel panic -ss {start_segment} -i {video_path} -t {end_segment-start_segment} -b:v 4000K -vcodec h264_nvenc {device_str}{os.path.join(out_folder, output_name)}" + print("call string: ", call_string) + my_env = os.environ.copy() + my_env["PATH"] = out_folder + ";" + my_env["PATH"] + p = subprocess.Popen(call_string, shell=True, cwd=out_folder, env=my_env) + stdout, stderr = p.communicate() + print(stdout, stderr) + return os.path.join(out_folder, output_name) + + +class Report: + def __init__(self, textfile): + self.textfile = textfile + self.lockfile = textfile + ".lock" + + def open_file(self): + if os.path.exists(self.textfile): + with open(self.textfile, "r") as f: + self.jsonf = json.load(f) + else: + self.jsonf = {} + + def add_report(self, key, val): + with filelock.FileLock(self.lockfile): + self.open_file() + self.jsonf[key] = val + with open(self.textfile, "w") as f: + json.dump(self.jsonf, f, indent=2) + + +def stitch(input_imgs_path: str, input_file_extension: str, output_file_name: str): + ffmpeg.input(os.path.join(input_imgs_path, f"*.{input_file_extension}")).output( + output_file_name + ).run() + return output_file_name + + +def detect_marker(frame, marker_dict): + DICT_CV2_ARUCO_MAP = { + "TAG16h5": cv2.aruco.DICT_APRILTAG_16h5, + "DICT_4X4_100": cv2.aruco.DICT_4X4_100, + } + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + aruco_dict = cv2.aruco.Dictionary_get(DICT_CV2_ARUCO_MAP[marker_dict]) + parameters = cv2.aruco.DetectorParameters_create() + _, ids, _ = cv2.aruco.detectMarkers(gray, aruco_dict, parameters=parameters) + markers = [] + if ids is not None: + for i in ids: + markers.append(i[0]) + return markers + + +def yml_parser(map_file): + ret = None + + with open(map_file, "r") as stream: + stream.readline() # YAML 1.0 bug + try: + ret = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + ## YAML 1.0 bug + # YAML library handles YAML 1.1 and above, + # to fix one of the bug in the generated YAML + for x in ret["aruco_bc_markers"]: + invalid_key = list(filter(lambda x: x.startswith("id"), x.keys()))[0] + key, value = invalid_key.split(":") + x[key] = value + del x[invalid_key] + + marker_dict = {} + for dct in ret["aruco_bc_markers"]: + marker_dict[int(dct["id"])] = dct["corners"] + ret["aruco_bc_markers"] = marker_dict + return ret + + +def get_marker_coord(marker_obj): + return [marker_obj[0][0], marker_obj[0][2]] + + +def get_maneuver_box(box_path): + val = None + with open(box_path) as f: + d = json.load(f) + val = d["box"] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_C_coord(rvec, tvec): + """ + Input: rotation vector, translation vector + Returns: Camera coordinates + """ + rvec = cv2.Rodrigues(np.array(rvec))[0] + tvec = np.array(tvec) + return -np.dot(np.transpose(rvec), tvec) + + +# def read_aruco_traj_file(filename): +# f_id = [] +# t_x = [] +# t_y = [] +# t_z = [] +# camera_matrices = [] +# with open(filename, 'r') as f: +# line = f.readline() +# while line: +# if not line[0].isdigit(): +# line = f.readline() +# continue +# else: #rvec- rotational vector, tvec - translational vector +# row = line.split(' ') +# rvec = [float(row[1].strip()), float(row[2].strip()), float(row[3].strip())] +# tvec = [float(row[4].strip()), float(row[5].strip()), float(row[6].strip())] + +# cvec = get_C_coord(rvec, tvec) +# cvec = cvec.reshape(3, 1) + +# temp = np.hstack(( cv2.Rodrigues( np.array(rvec) )[0], cvec)) +# camera_matrix = np.vstack((temp, np.array([0.0, 0.0, 0.0, 1.0]))) + +# f_id.append(int(row[0].strip())) +# camera_matrices.append(camera_matrix) +# t_x.append( cvec[0][0] ) +# t_y.append( cvec[1][0] ) +# t_z.append( cvec[2][0] ) +# line = f.readline() +# return f_id, t_x, t_y, t_z, camera_matrices + + +def majority_vote_smoothen(vector, window): + """ + Input: vector of 1's and 0's + Output: smoothing applied to window + """ + vector_smooth = [] + for i in range(0, len(vector), window): + forw = vector[i : i + window].count(1) + rev = vector[i : i + window].count(0) + halt = vector[i : i + window].count(-1) + x = None + if forw > 2 * rev and forw > halt: + x = 1 + for i in range(window): + vector_smooth.append(1) + elif rev > 2 * forw and rev > halt: + x = 2 + for i in range(window): + vector_smooth.append(0) + else: + x = 3 + for i in range(window): + vector_smooth.append(-1) + # print(forw, rev, halt, x) + return vector_smooth[: len(vector)] + + +def debug_directions_visualize(plt, tx, ty, tz, directions): + """ """ + colors = ["red", "blue", "black"] + le = min(min(len(tx) - 1, len(tz) - 1), len(directions) - 1) + plt.scatter( + [tx[i] for i in range(0, le)], + [tz[i] for i in range(0, le)], + c=[colors[directions[i]] for i in range(0, le)], + ) + plt.show() + + +def rotate_point(x, y, rot_radian): + return [ + x * math.cos(rot_radian) - y * math.sin(rot_radian), + y * math.cos(rot_radian) + x * math.sin(rot_radian), + ] + + +def rotate_rectangle(points, camera_center, rot_radian): + # Displace to origin + o_points = np.zeros(points.shape) + for i in range(4): + o_points[i, :] = points[i, :] - camera_center + # Rotate points + for i in range(4): + o_points[i, 0], o_points[i, 1] = rotate_point( + o_points[i, 0], o_points[i, 1], rot_radian + ) + # Displace again + for i in range(4): + o_points[i, :] = o_points[i, :] + camera_center + return o_points + + +def rotation_matrix_to_euler_angles(R): + assert is_rotation_matrix(R) + sy = np.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) + singular = sy < 1e-6 + if not singular: + x = np.arctan2(R[2, 1], R[2, 2]) + y = np.arctan2(-R[2, 0], sy) + z = np.arctan2(R[1, 0], R[0, 0]) + else: + x = np.arctan2(-R[1, 2], R[1, 1]) + y = np.arctan2(-R[2, 0], sy) + z = 0 + return np.array([x, y, z]) + + +def is_rotation_matrix(R): + Rt = np.transpose(R) + should_be_identity = np.dot(Rt, R) + I = np.identity(3, dtype=R.dtype) + n = np.linalg.norm(I - should_be_identity) + return n < 1e-6 + + +def plot_line(plt, l_stop, x_lim, color): + for x in np.arange(x_lim[0], x_lim[1], 0.1): + y = l_stop[0] * x + l_stop[1] + plt.scatter(x, y, color=color) + + +def smoothen_trajectory(tx, ty, tz, outlier_window, poly_fit_window, poly_fit_degree): + # Applying median filter + tx = scipy.signal.medfilt(tx, outlier_window) + ty = scipy.signal.medfilt(ty, outlier_window) + tz = scipy.signal.medfilt(tz, outlier_window) + # Applying savitzky golay filter + tx = scipy.signal.savgol_filter(tx, poly_fit_window, poly_fit_degree) + ty = scipy.signal.savgol_filter(ty, poly_fit_window, poly_fit_degree) + tz = scipy.signal.savgol_filter(tz, poly_fit_window, poly_fit_degree) + return tx, ty, tz + + +def get_graph_box(box_path, label): + with open(box_path) as f: + d = json.load(f) + val = d[label] + val = list( + zip(*shapely.geometry.Polygon(val).minimum_rotated_rectangle.exterior.coords.xy) + ) + return np.array(val) + + +def get_plt_rotation_from_markers(origin_marker, axis_marker, is_vertical): + try: + marker_origin = origin_marker + marker_axis = axis_marker + m = (marker_axis[1] - marker_origin[1]) / (marker_axis[0] - marker_origin[0]) + deg = np.rad2deg(np.arctan(m)) + if is_vertical: + deg = 90 - deg + else: + deg = -1 * deg # make anti clockwise + return deg + except: + return 0.0 diff --git a/mapping_cli/validation.py b/mapping_cli/validation.py new file mode 100644 index 0000000..cf93eee --- /dev/null +++ b/mapping_cli/validation.py @@ -0,0 +1,89 @@ +from os.path import exists + + +def check_if_mapper_exe_is_valid(mapper_exe_path: str): + """Validate mapper exe file path and check if file exists. + + Args: + mapper_exe_path (str): Mapper exe path + + Raises: + ValueError: Mapper exe File Path cannot be empty + ValueError: Unsuported Mapper exe File Type. Only supports .exe + FileNotFoundError: Mapper exe File does not exist + """ + + if len(mapper_exe_path) == 0: + raise ValueError("Mapper exe File Path cannot be empty") + + if not mapper_exe_path.endswith(".exe"): + raise ValueError("Unsuported Mapper exe File Type. Only supports .exe") + + if not exists(mapper_exe_path): + raise FileNotFoundError("Mapper exe File does not exist") + + +def check_if_images_dir_exists(images_directory: str): + """Validate output file path. + + Args: + images_directory (str): Images Directory path + + Raises: + ValueError: Images Dir Path cannot be empty + FileNotFoundError: Images Dir Path not found or does not exist + """ + + if len(images_directory) == 0: + raise ValueError("Images Dir Path cannot be empty") + + if not exists(images_directory): + raise FileNotFoundError("Images Dir Path not found or does not exist") + + +def check_if_camera_config_is_valid(camera_params_path: str): + """Validate camera config params path. + + Args: + camera_params_path (str): Camera params path + """ + if len(camera_params_path) == 0: + raise ValueError("Camera Params path cannot be empty") + + if not camera_params_path.endswith(".yml"): + raise ValueError("Camera params file not .yml type") + + if not exists(camera_params_path): + raise FileNotFoundError("Camera params path not found or does not exist") + + +def check_if_map_yml_is_valid(map_yml_path: str): + """Validate map yml path. + + Args: + map_yml_path (str): Map YML File Path + """ + if len(map_yml_path) == 0: + raise ValueError("Map YML File Path cannot be empty") + + if not map_yml_path.endswith(".yml"): + raise ValueError("Map YML File not .yml type") + + if not exists(map_yml_path): + raise FileNotFoundError("Map YML File Path not found or does not exist") + + +def check_if_txt_file_is_valid(txt_path: str): + """Validate txt path. + + Args: + txt_path (str): Txt File Path + """ + if len(txt_path) == 0: + raise ValueError("Txt File Path cannot be empty") + + if not txt_path.endswith(".txt"): + raise ValueError("Txt File not .yml type") + + if not exists(txt_path): + raise FileNotFoundError("Txt File Path not found or does not exist") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..caba62f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +typer +PyYAML +numpy +opencv-python +torch +torchvision +tqdm +Pillow +argparse +hydra-core +scipy +matplotlib +pandas +cognitive-face +filelock +ffmpeg-python +scikit-learn +pyquaternion +decord \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2c81c28 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +from os import path +from setuptools import find_packages, setup +from mapping_cli import __version__ + +with open('requirements.txt') as f: + required = f.read().splitlines() +curdir = path.abspath(path.dirname(__file__)) +with open(path.join(curdir, "README.md"), encoding="utf-8") as f: + long_description = f.read() +setup( + name="hams", + packages=find_packages(), + version=__version__, + license="GPLv3+", + description="HAMS", + long_description=long_description, + long_description_content_type="text/markdown", + author="Vaibhav Balloli, Anurag Ghosh, Harsh Vijay, Jonathan Samuel, Akshay Nambi", + author_email="t-vballoli@microsoft.com", + install_requires=required, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python :: 3.8", + ] +) \ No newline at end of file From 1ba5436bd4e48cd56fae127b27a3e1cb7622846a Mon Sep 17 00:00:00 2001 From: Vaibhav Balloli Date: Wed, 15 Mar 2023 16:09:40 +0530 Subject: [PATCH 2/4] Create .github/dependabot.yml Signed-off-by: Vaibhav Balloli --- .github/dependabot.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ac6621f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" From 930e5a313c01fac6f3477def3e7a7b2baa2f7761 Mon Sep 17 00:00:00 2001 From: Vaibhav Balloli Date: Wed, 15 Mar 2023 16:10:56 +0530 Subject: [PATCH 3/4] Update dependabot.yml Signed-off-by: Vaibhav Balloli --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ac6621f..91abb11 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ version: 2 updates: - - package-ecosystem: "" # See documentation for possible values + - package-ecosystem: "pip" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" From 214f46f2540532f25380a8a39875913bfb45ca01 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Mar 2023 10:41:40 +0000 Subject: [PATCH 4/4] Bump breathe from 4.30.0 to 4.35.0 Bumps [breathe](https://github.com/michaeljones/breathe) from 4.30.0 to 4.35.0. - [Release notes](https://github.com/michaeljones/breathe/releases) - [Changelog](https://github.com/breathe-doc/breathe/blob/main/CHANGELOG.rst) - [Commits](https://github.com/michaeljones/breathe/compare/v4.30.0...v4.35.0) --- updated-dependencies: - dependency-name: breathe dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 9d9cdb7..f91e79e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -breathe==4.30.0 +breathe==4.35.0 furo git+https://github.com/sphinx-contrib/video ipywidgets