├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo.py ├── docs ├── Makefile ├── _static │ └── styles │ │ └── my_theme.css ├── _templates │ └── autoapi │ │ ├── index.rst │ │ └── python │ │ ├── attribute.rst │ │ ├── class.rst │ │ ├── data.rst │ │ ├── exception.rst │ │ ├── function.rst │ │ ├── method.rst │ │ ├── module.rst │ │ ├── package.rst │ │ └── property.rst ├── abbrev_long.bib ├── conf.py ├── conf_overrides.py ├── conf_spec.py ├── index.rst ├── make.bat └── references.bib ├── docs_old ├── API.md ├── index.md ├── poseviz.md └── stylesheets │ └── extra.css ├── pyproject.toml ├── screenshot.jpg ├── screenshot2.jpg ├── screenshot_multicam.jpg └── src └── poseviz ├── __init__.py ├── colors.py ├── components ├── __init__.py ├── camera_viz.py ├── connected_points_viz.py ├── ground_plane_viz.py ├── main_viz.py ├── skeletons_viz.py └── smpl_viz.py ├── draw2d.py ├── init.py ├── mayavi_util.py ├── messages.py ├── poseviz.py ├── video_writing.py └── view_info.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | pypi-publish: 12 | name: Upload release to PyPI 13 | runs-on: ubuntu-latest 14 | environment: pypi 15 | permissions: 16 | id-token: write 17 | steps: 18 | - name: Check out repository 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Python 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: "3.x" 27 | 28 | - name: Install build dependencies 29 | run: python -m pip install --upgrade build 30 | 31 | - name: Build package distribution 32 | run: python -m build --sdist 33 | 34 | - name: Publish package distributions to PyPI 35 | uses: pypa/gh-action-pypi-publish@release/v1 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | *_cython.c 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 István Sárándi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PoseViz – 3D Human Pose Visualizer 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | Multi-person, multi-camera 3D human pose visualization tool built using 10 | [Mayavi](https://docs.enthought.com/mayavi/mayavi/). As used 11 | in [MeTRAbs](https://github.com/isarandi/metrabs) visualizations. 12 | 13 | **This repo does not contain pose estimation code, only the visualization part.** 14 | 15 | ## Gist of Usage 16 | 17 | ```python 18 | import poseviz 19 | import cameravision 20 | 21 | camera = cameravision.Camera(...) 22 | 23 | with poseviz.PoseViz(...) as viz: 24 | for frame in frames: 25 | bounding_boxes, poses3d = run_pose_estimation_model(frame) 26 | viz.update(frame=frame, boxes=bounding_boxes, poses=poses3d, camera=camera) 27 | ``` 28 | 29 | See also [```demo.py```](demo.py). 30 | 31 | The main feature of this tool is that the graphical event loop is hidden from the library user. We 32 | want to write code in terms of the *prediction loop* of the human pose estimator, not from the point 33 | of view of the visualizer tool. 34 | 35 | Behind the scenes, this is achieved through forking a dedicated visualization process and passing 36 | new scene information via multiprocessing queues. 37 | 38 | ## Installation 39 | 40 | PoseViz is available on PyPI. However, in my experience it is better to install the Mayavi dependency through conda instead of pip. 41 | 42 | ```bash 43 | conda install mayavi -c conda-forge 44 | pip install poseviz 45 | ``` 46 | 47 | Then run [demo.py](demo.py) to test if installation was successful. 48 | 49 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import smpl.numpy 4 | 5 | import poseviz 6 | 7 | import numpy as np 8 | import cameravision 9 | 10 | 11 | def main(): 12 | # Names of the body joints. Left joint names must start with 'l', right with 'r'. 13 | joint_names = ["l_wrist", "l_elbow"] 14 | 15 | # Joint index pairs specifying which ones should be connected with a line (i.e., the bones of 16 | # the body, e.g. wrist-elbow, elbow-shoulder) 17 | joint_edges = [[0, 1]] 18 | viz = poseviz.PoseViz(joint_names, joint_edges) 19 | 20 | # Iterate over the frames of e.g. a video 21 | for i in range(1): 22 | # Get the current frame 23 | frame = np.zeros([720, 1280, 3], np.uint8) 24 | 25 | # Make predictions here 26 | # ... 27 | 28 | # Update the visualization 29 | viz.update( 30 | frame=frame, 31 | boxes=np.array([[10, 20, 100, 100]], np.float32), 32 | poses=np.array([[[100, 100, 2000], [-100, 100, 2000]]], np.float32), 33 | camera=cameravision.Camera.from_fov(55, frame.shape[:2]), 34 | ) 35 | 36 | 37 | def main_smpl(): 38 | smpl_canonical = np.load( 39 | "/work/sarandi/projects/localizerfields/canonical_vertices_smpl.npy" 40 | ) * [1,-1,-1] * 1000 + [0, 0, 3000] 41 | 42 | # Names of the body joints. Left joint names must start with 'l', right with 'r'. 43 | joint_names = ["l_wrist", "l_elbow"] 44 | 45 | # Joint index pairs specifying which ones should be connected with a line (i.e., the bones of 46 | # the body, e.g. wrist-elbow, elbow-shoulder) 47 | joint_edges = [[0, 1]] 48 | faces = smpl.numpy.get_cached_body_model("smpl", "neutral").faces 49 | with poseviz.PoseViz(joint_names, joint_edges, body_model_faces=faces) as viz: 50 | # Iterate over the frames of e.g. a video 51 | for i in range(1): 52 | # Get the current frame 53 | frame = np.zeros([720, 1280, 3], np.uint8) 54 | 55 | # Make predictions here 56 | # ... 57 | 58 | # Update the visualization 59 | viz.update( 60 | frame=frame, 61 | boxes=np.array([[10, 20, 100, 100]], np.float32), 62 | vertices=np.array([smpl_canonical + [500, 0, 5000]]), 63 | camera=cameravision.Camera.from_fov(55, frame.shape[:2], world_up=(0, 1, 0)), 64 | ) 65 | 66 | viz.update( 67 | frame=frame, 68 | boxes=np.array([[10, 20, 100, 100]], np.float32), 69 | vertices=np.array( 70 | [smpl_canonical + [-500, 0, 3000], smpl_canonical + [-500, 0, 5000]] 71 | ), 72 | camera=cameravision.Camera.from_fov(55, frame.shape[:2], world_up=(0, 1, 0)), 73 | ) 74 | 75 | time.sleep(1000) 76 | 77 | 78 | if __name__ == "__main__": 79 | main_smpl() 80 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/styles/my_theme.css: -------------------------------------------------------------------------------- 1 | @import url("theme.css"); 2 | @import url("https://fonts.googleapis.com/css2?family=Mona+Sans:ital,wght@0,200..900;1,200..900&family=Geist:wght@100..900&&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Outfit:wght@100..900&display=swap"); 3 | 4 | @media (min-width: 960px) { 5 | .bd-page-width { 6 | max-width: 120rem; 7 | } 8 | } 9 | 10 | #rtd-footer-container { 11 | margin-top: 0 !important; 12 | } 13 | 14 | html[data-theme="light"] { 15 | --pst-color-table-row-hover-bg: #dfc6ff; 16 | --pst-color-link-hover: #845818; 17 | } 18 | 19 | html[data-theme="dark"] { 20 | --pst-color-table-row-hover-bg: #41296c; 21 | --pst-color-inline-code: #dd8cd4; 22 | } 23 | 24 | 25 | html[data-theme="dark"] dt:target { 26 | background-color: #4f4500; 27 | } 28 | 29 | html[data-theme="dark"] .linkcode-link { 30 | color: #9090ff; 31 | } 32 | 33 | html[data-theme="dark"] table.indextable tr.cap { 34 | background-color: #464646; 35 | } 36 | 37 | html[data-theme="dark"] a:visited { 38 | color: #9E67D0; 39 | } 40 | 41 | .navbar-brand .logo__title { 42 | font-family: "Mona Sans", sans-serif; 43 | font-size: 2.5rem; 44 | font-weight: 400; 45 | font-style: normal; 46 | } 47 | 48 | :root { 49 | --pst-font-family-monospace: "JetBrains Mono", monospace; 50 | --pst-font-family-heading: "Mona Sans", sans-serif; 51 | --pst-font-family-base: "Mona Sans", sans-serif; 52 | } 53 | 54 | body { 55 | font-weight: 450; 56 | } 57 | 58 | .bd-main .bd-content .bd-article-container { 59 | max-width: 100%; /* default is 60em */ 60 | } 61 | 62 | .bd-sidebar-primary { 63 | width: 18%; 64 | } -------------------------------------------------------------------------------- /docs/_templates/autoapi/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :titlesonly: 6 | 7 | {% for page in pages|selectattr("is_top_level_object") %} 8 | {{ page.include_path }} 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/attribute.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/data.rst" %} 2 | -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/class.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.name }} 4 | {{ "=" * obj.name | length }} 5 | 6 | {% endif %} 7 | {% set visible_children = obj.children|selectattr("display")|list %} 8 | {% set own_page_children = visible_children|selectattr("type", "in", own_page_types)|list %} 9 | {% if is_own_page and own_page_children %} 10 | .. toctree:: 11 | :hidden: 12 | 13 | {% for child in own_page_children %} 14 | {{ child.include_path }} 15 | {% endfor %} 16 | 17 | {% endif %} 18 | .. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}{% if obj.args %}({{ obj.args }}){% endif %} 19 | 20 | {% for (args, return_annotation) in obj.overloads %} 21 | {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} 22 | 23 | {% endfor %} 24 | {% if obj.bases %} 25 | {% if "show-inheritance" in autoapi_options %} 26 | 27 | Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} 28 | {% endif %} 29 | 30 | 31 | {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} 32 | .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} 33 | :parts: 1 34 | {% if "private-members" in autoapi_options %} 35 | :private-bases: 36 | {% endif %} 37 | 38 | {% endif %} 39 | {% endif %} 40 | {% if obj.docstring %} 41 | 42 | {{ obj.docstring|indent(3) }} 43 | {% endif %} 44 | {% for obj_item in visible_children %} 45 | {% if obj_item.type not in own_page_types %} 46 | 47 | {{ obj_item.render()|indent(3) }} 48 | {% endif %} 49 | {% endfor %} 50 | {% if is_own_page and own_page_children %} 51 | {% set visible_attributes = own_page_children|selectattr("type", "equalto", "attribute")|list %} 52 | {% if visible_attributes %} 53 | Attributes 54 | ---------- 55 | 56 | .. autoapisummary:: 57 | 58 | {% for attribute in visible_attributes %} 59 | {{ attribute.id }} 60 | {% endfor %} 61 | 62 | 63 | {% endif %} 64 | {% set visible_properties = own_page_children|selectattr("type", "equalto", "property")|list %} 65 | {% if visible_properties %} 66 | Properties 67 | ---------- 68 | 69 | .. autoapisummary:: 70 | 71 | {% for property in visible_properties %} 72 | {{ property.id }} 73 | {% endfor %} 74 | 75 | 76 | {% endif %} 77 | {% set visible_exceptions = own_page_children|selectattr("type", "equalto", "exception")|list %} 78 | {% if visible_exceptions %} 79 | Exceptions 80 | ---------- 81 | 82 | .. autoapisummary:: 83 | 84 | {% for exception in visible_exceptions %} 85 | {{ exception.id }} 86 | {% endfor %} 87 | 88 | 89 | {% endif %} 90 | {% set visible_classes = own_page_children|selectattr("type", "equalto", "class")|list %} 91 | {% if visible_classes %} 92 | Classes 93 | ------- 94 | 95 | .. autoapisummary:: 96 | 97 | {% for klass in visible_classes %} 98 | {{ klass.id }} 99 | {% endfor %} 100 | 101 | 102 | {% endif %} 103 | {% set visible_methods = own_page_children|selectattr("type", "equalto", "method")|list %} 104 | {% if visible_methods %} 105 | Methods 106 | ------- 107 | 108 | .. autoapisummary:: 109 | 110 | {% for method in visible_methods %} 111 | {{ method.id }} 112 | {% endfor %} 113 | 114 | 115 | {% endif %} 116 | 117 | {% endif %} 118 | {% endif %} 119 | 120 | 121 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/data.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.name }} 4 | {{ "=" * obj.name | length }} 5 | 6 | {% endif %} 7 | .. py:{{ obj.type }}:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.name }}{% endif %} 8 | {% if obj.annotation is not none %} 9 | 10 | :type: {% if obj.annotation %} {{ obj.annotation }}{% endif %} 11 | {% endif %} 12 | {% if obj.value is not none %} 13 | 14 | {% if obj.value.splitlines()|count > 1 %} 15 | :value: Multiline-String 16 | 17 | .. raw:: html 18 | 19 |
Show Value 20 | 21 | .. code-block:: python 22 | 23 | {{ obj.value|indent(width=6,blank=true) }} 24 | 25 | .. raw:: html 26 | 27 |
28 | 29 | {% else %} 30 | :value: {{ obj.value|truncate(100) }} 31 | {% endif %} 32 | {% endif %} 33 | 34 | {% if obj.docstring %} 35 | 36 | {{ obj.docstring|indent(3) }} 37 | {% endif %} 38 | {% endif %} 39 | 40 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/exception.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/class.rst" %} 2 | -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/function.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.name }} 4 | {{ "=" * obj.name | length }} 5 | 6 | {% endif %} 7 | .. py:function:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} 8 | {% for (args, return_annotation) in obj.overloads %} 9 | 10 | {%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} 11 | {% endfor %} 12 | {% for property in obj.properties %} 13 | 14 | :{{ property }}: 15 | {% endfor %} 16 | 17 | {% if obj.docstring %} 18 | 19 | {{ obj.docstring|indent(3) }} 20 | {% endif %} 21 | {% endif %} 22 | 23 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/method.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.name }} 4 | {{ "=" * obj.name | length }} 5 | 6 | {% endif %} 7 | .. py:method:: {% if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} 8 | {% for (args, return_annotation) in obj.overloads %} 9 | 10 | {%+ if is_own_page %}{{ obj.id }}{% else %}{{ obj.short_name }}{% endif %}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} 11 | {% endfor %} 12 | {% for property in obj.properties %} 13 | 14 | :{{ property }}: 15 | {% endfor %} 16 | 17 | {% if obj.docstring %} 18 | 19 | {{ obj.docstring|indent(3) }} 20 | {% endif %} 21 | {% endif %} 22 | 23 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/module.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.id }} 4 | {{ "=" * obj.id|length }} 5 | 6 | .. py:module:: {{ obj.name }} 7 | 8 | {% if obj.docstring %} 9 | .. autoapi-nested-parse:: 10 | 11 | {{ obj.docstring|indent(3) }} 12 | 13 | {% endif %} 14 | 15 | {% block submodules %} 16 | {% set visible_subpackages = obj.subpackages|selectattr("display")|list %} 17 | {% set visible_submodules = obj.submodules|selectattr("display")|list %} 18 | {% set visible_submodules = (visible_subpackages + visible_submodules)|sort %} 19 | {% if visible_submodules %} 20 | Submodules 21 | ---------- 22 | 23 | .. toctree:: 24 | :maxdepth: 1 25 | 26 | {% for submodule in visible_submodules %} 27 | {{ submodule.include_path }} 28 | {% endfor %} 29 | 30 | 31 | {% endif %} 32 | {% endblock %} 33 | {% block content %} 34 | {% set visible_children = obj.children|selectattr("display")|list %} 35 | {% if visible_children %} 36 | {% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} 37 | {% if visible_attributes %} 38 | {% if "attribute" in own_page_types or "show-module-summary" in autoapi_options %} 39 | Attributes 40 | ---------- 41 | 42 | {% if "attribute" in own_page_types %} 43 | .. toctree:: 44 | :hidden: 45 | 46 | {% for attribute in visible_attributes %} 47 | {{ attribute.include_path }} 48 | {% endfor %} 49 | 50 | {% endif %} 51 | .. autoapisummary:: 52 | 53 | {% for attribute in visible_attributes %} 54 | {{ attribute.id }} 55 | {% endfor %} 56 | {% endif %} 57 | 58 | 59 | {% endif %} 60 | {% set visible_exceptions = visible_children|selectattr("type", "equalto", "exception")|list %} 61 | {% if visible_exceptions %} 62 | {% if "exception" in own_page_types or "show-module-summary" in autoapi_options %} 63 | Exceptions 64 | ---------- 65 | 66 | {% if "exception" in own_page_types %} 67 | .. toctree:: 68 | :hidden: 69 | 70 | {% for exception in visible_exceptions %} 71 | {{ exception.include_path }} 72 | {% endfor %} 73 | 74 | {% endif %} 75 | .. autoapisummary:: 76 | 77 | {% for exception in visible_exceptions %} 78 | {{ exception.id }} 79 | {% endfor %} 80 | {% endif %} 81 | 82 | 83 | {% endif %} 84 | {% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} 85 | {% if visible_classes %} 86 | {% if "class" in own_page_types or "show-module-summary" in autoapi_options %} 87 | Classes 88 | ------- 89 | 90 | {% if "class" in own_page_types %} 91 | .. toctree:: 92 | :hidden: 93 | 94 | {% for klass in visible_classes %} 95 | {{ klass.include_path }} 96 | {% endfor %} 97 | 98 | {% endif %} 99 | .. autoapisummary:: 100 | 101 | {% for klass in visible_classes %} 102 | {{ klass.id }} 103 | {% endfor %} 104 | {% endif %} 105 | 106 | 107 | {% endif %} 108 | {% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} 109 | {% if visible_functions %} 110 | {% if "function" in own_page_types or "show-module-summary" in autoapi_options %} 111 | Functions 112 | --------- 113 | 114 | {% if "function" in own_page_types %} 115 | .. toctree:: 116 | :hidden: 117 | 118 | {% for function in visible_functions %} 119 | {{ function.include_path }} 120 | {% endfor %} 121 | 122 | {% endif %} 123 | .. autoapisummary:: 124 | 125 | {% for function in visible_functions %} 126 | {{ function.id }} 127 | {% endfor %} 128 | {% endif %} 129 | 130 | 131 | {% endif %} 132 | {% set this_page_children = visible_children|rejectattr("type", "in", own_page_types)|list %} 133 | {% if this_page_children %} 134 | {{ obj.type|title }} Contents 135 | {{ "-" * obj.type|length }}--------- 136 | 137 | {% for obj_item in this_page_children %} 138 | {{ obj_item.render()|indent(0) }} 139 | {% endfor %} 140 | {% endif %} 141 | {% endif %} 142 | {% endblock %} 143 | {% else %} 144 | .. py:module:: {{ obj.name }} 145 | 146 | {% if obj.docstring %} 147 | .. autoapi-nested-parse:: 148 | 149 | {{ obj.docstring|indent(6) }} 150 | 151 | {% endif %} 152 | {% for obj_item in visible_children %} 153 | {{ obj_item.render()|indent(3) }} 154 | {% endfor %} 155 | {% endif %} 156 | {% endif %} 157 | 158 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/package.rst: -------------------------------------------------------------------------------- 1 | {% extends "python/module.rst" %} 2 | -------------------------------------------------------------------------------- /docs/_templates/autoapi/python/property.rst: -------------------------------------------------------------------------------- 1 | {% if obj.display %} 2 | {% if is_own_page %} 3 | {{ obj.name }} 4 | {{ "=" * obj.name | length }} 5 | 6 | {% endif %} 7 | .. py:property:: {% if is_own_page %}{{ obj.id}}{% else %}{{ obj.short_name }}{% endif %} 8 | {% if obj.annotation %} 9 | 10 | :type: {{ obj.annotation }} 11 | {% endif %} 12 | {% for property in obj.properties %} 13 | 14 | :{{ property }}: 15 | {% endfor %} 16 | 17 | {% if obj.docstring %} 18 | 19 | {{ obj.docstring|indent(3) }} 20 | {% endif %} 21 | {% endif %} 22 | 23 | .. footbibliography:: -------------------------------------------------------------------------------- /docs/abbrev_long.bib: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%% Journals %%%%%%%%%%%%%%%% 2 | @string{IJCV = "International Journal of Computer Vision (IJCV)"} 3 | @string{CVIU = "Computer Vision and Image Understanding (CVIU)"} 4 | @string{PR = "Pattern Recognition"} 5 | @string{PRL = "Pattern Recognition Letters"} 6 | 7 | @string{ML = "Machine Learning"} 8 | @string{AI = "Artificial Intelligence"} 9 | @string{AR = "Autonomous Robots"} 10 | @string{MVA = "Machine Vision and Applications"} 11 | @string{IVC = "Image and Vision Computing"} 12 | @string{BBS = "Behavioral and Brain Sciences (BBS)"} 13 | @string{VR = "Vision Research"} 14 | @string{IR = "Information Retrieval"} 15 | @string{NN = "Neural Networks"} 16 | @string{CAG = "Computers \& Graphics"} 17 | @string{CVGIP = "Computer Vision, Graphics, and Image Processing (CVGIP)"} 18 | @string{CVGIPIU = "CVGIP: Image Understanding"} 19 | @string{PP = "Perception \& Psychophysics"} 20 | @string{FTCGV = "Foundations and Trends in Computer Graphics and Vision"} 21 | @string{AdvRob = "Advanced Robotics"} 22 | 23 | @string{Nature = "Nature"} 24 | @string{Science = "Science"} 25 | @string{Mechatronics = "Mechatronics"} 26 | @string{NRN = "Nature Reviews Neuroscience"} 27 | @string{NM = "Nature Methods"} 28 | @string{PHY = "Physical Review E"} 29 | @string{PsychRev = "Psychological Review"} 30 | 31 | @string{JMLR = "Journal of Machine Learning Research (JMLR)"} 32 | @string{JSC = "Journal of Scientific Computing"} 33 | @string{JCN = "Journal of Cognitive Neuroscience"} 34 | @string{JEPHPP = "Journal of Experimental Psychology: Human Perception and Performance"} 35 | @string{JECP = "Journal of Experimental Child Psychology"} 36 | @string{JB = "Journal of Biomechanics"} 37 | 38 | @string{EURASIP = "EURASIP Journal on Advances in Signal Processing"} 39 | @string{PRESENCE = "Presence: Teleoperators and Virtual Environments"} 40 | @string{BMB = "The Bulletin of Mathematical Biophysics"} 41 | 42 | @string{TVC = "The Visual Computer"} 43 | @string{TJSC = "The Journal of Supercomputing"} 44 | 45 | % IEEE 46 | @string{PIEEE = "Proceedings of the IEEE"} 47 | @string{RAL = "IEEE Robotics and Automation Letters (RA-L)"} 48 | @string{CGA = "IEEE Computer Graphics and Applications"} 49 | @string{IEEEA = "IEEE Access"} 50 | @string{TPAMI = "IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI)"} 51 | @string{PAMI = "IEEE Transactions on Pattern Analysis and Machine Intelligence (TPAMI)"} 52 | @string{TC = "IEEE Transactions on Communications"} 53 | @string{TCyb = "IEEE Transactions on Cybernetics"} 54 | @string{TSE = "IEEE Transactions on Software Engineering"} 55 | @string{TIV = "IEEE Transactions on Intelligent Vehicles"} 56 | @string{TIP = "IEEE Transactions on Image Processing"} 57 | @string{TOR = "IEEE Transactions on Robotics"} 58 | @string{TAC = "IEEE Transactions on Automatic Control"} 59 | @string{TITS = "IEEE Transactions on Intelligent Transportation Systems (T-ITS)"} 60 | @string{TOC = "IEEE Transactions on Computers"} 61 | @string{TVT = "IEEE Transactions on Vehicular Technologies"} 62 | @string{TNN = "IEEE Transactions on Neural Networks"} 63 | @string{THMS = "IEEE Transactions on Human-Machine Systems"} 64 | @string{TCSVT = "IEEE Transactions on Circuits and Systems for Video Technology"} 65 | @string{TBIOM = "IEEE Transactions on Biometrics, Behavior, and Identity Science (T-BIOM)"} 66 | @string{TIT = "IEEE Transactions on Information Theory"} 67 | @string{TVCG = "IEEE Transactions on Visualization and Computer Graphics (TVCG)"} 68 | @string{TSSC = "IEEE Transactions on Systems Science and Cybernetics"} 69 | @string{IRETIT= "IRE Transactions on Information Theory"} 70 | @string{IJTEHM= "IEEE Journal of Translational Engineering in Health and Medicine"} 71 | 72 | 73 | % ACM 74 | @string{TOCHI = "ACM Transactions on Computer-Human Interaction (TOCHI)"} 75 | @string{TOG = "ACM Transactions on Graphics (TOG)"} 76 | @string{CACM = "Communications of the ACM (CACM)"} 77 | @string{IMWUT = "Proceedings of the ACM on Interactive, Mobile, Wearable and Ubiquitous Technologies (IMWUT)"} 78 | @string{CSUR = "ACM Computing Surveys (CSUR)"} 79 | @string{THRI = "ACM Transactions on Human-Robot Interaction"} 80 | 81 | @string{AnnStat = "Annals of Statistics"} 82 | @string{JC = "Journal of Classification"} 83 | @string{IJRR = "International Journal of Robotics Research (IJRR)"} 84 | @string{RSS = "Robotics: Science and Systems (RSS)"} 85 | 86 | @string{PLOSOne = "PLOS One"} 87 | @string{SMO = "Sports Medicine -- Open"} 88 | @string{IJMIR = "International Journal of Multimedia Information Retrieval (IJMIR)"} 89 | 90 | @string{BiolCyb = "Biological Cybernetics"} 91 | @string{Psychomet = "Psychometrika"} 92 | @string{Biotelem = "Biotelemetry"} 93 | @string{NC = "Neural Computation"} 94 | @string{Neurocomputing = "Neurocomputing"} 95 | @string{PhilosMag = "London, Edinburgh, and Dublin Philosophical Magazine and Journal of Science"} 96 | 97 | @string{TST = "Tsinghua Science and Technology"} 98 | @string{VRIH = "Virtual Reality \& Intelligent Hardware (VRIH)"} 99 | @string{ISPRS = "ISPRS Journal of Photogrammetry and Remote Sensing (P\&RS)"} 100 | @string{MMS = "Multimedia Systems"} 101 | @string{SSS = "Social Studies of Science"} 102 | @string{SIREV = "SIAM Review"} 103 | 104 | @string{Sensors = "Sensors"} 105 | @string{Electronics = "Electronics"} 106 | 107 | @string{ARVC = "Annual Review of Vision Science"} 108 | @string{ARP = "Annual Review of Psychology"} 109 | @string{PRSLB = "Proceedings of the Royal Society of London. Series B, Biological Sciences"} 110 | @string{PRSA = "Proceedings of the Royal Society A"} 111 | 112 | @string{TJP = "The Journal of Physiology"} 113 | @string{USSRCMMP = "USSR Computational Mathematics and Mathematical Physics"} 114 | @string{CRHSAS = "Comptes rendus hebdomadaires des séances de l'Académie des sciences"} 115 | 116 | 117 | %%%%%%%%%%%%%%%%%%%%% Conferences %%%%%%%%%%%%%% 118 | @string{CVPR = "IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)"} 119 | @string{ICCV = "IEEE/CVF International Conference on Computer Vision (ICCV)"} 120 | @string{WACV = "IEEE/CVF Winter Conference on Applications of Computer Vision (WACV)"} 121 | 122 | @string{ECCV = "European Conference on Computer Vision (ECCV)"} 123 | @string{ACCV = "Asian Conference on Computer Vision (ACCV)"} 124 | @string{BMVC = "British Machine Vision Conference (BMVC)"} 125 | @string{DAGM = "DAGM Annual Pattern Recognition Symposium"} 126 | @string{GCPR = "DAGM German Conference on Pattern Recognition (GCPR)"} 127 | 128 | @string{NIPS = "Advances in Neural Information Processing Systems (NIPS)"} 129 | @string{NeurIPS = "Advances in Neural Information Processing Systems (NeurIPS)"} 130 | @string{NeurIPSDB = "Neural Information Processing Systems: Datasets and Benchmarks Track"} 131 | 132 | @string{TDV = "International Conference on 3D Vision (3DV)"} 133 | @string{ICML = "International Conference on Machine Learning (ICML)"} 134 | @string{ICLR = "International Conference on Learning Representations (ICLR)"} 135 | @string{ICPR = "International Conference on Pattern Recogntion (ICPR)"} 136 | @string{CAIP = "International Conference on Analysis of Images and Patterns (CAIP)"} 137 | @string{ICIAP = "International Conference on Image Analysis and Processing (ICIAP)"} 138 | @string{ICIAR = "International Conference on Image Analysis and Recognition (ICIAR)"} 139 | 140 | @string{ISCS = "IEEE International Symposium on Circuits and Systems (ISCAS)"} 141 | @string{FG = "IEEE International Conference on Automatic Face and Gesture Recognition (FG)"} 142 | @string{CDC = "IEEE Conference on Decision and Control (CDC)"} 143 | @string{IROS = "IEEE/RSJ International Conference on Intelligent Robots and Systems (IROS)"} 144 | @string{ICRA = "IEEE International Conference on Robotics and Automation (ICRA)"} 145 | @string{IVS = "IEEE Intelligent Vehicles Symposium (IV)"} 146 | @string{ICASSP = "IEEE Conference on Acoustics, Speech and Signal Processing (ICASSP)"} 147 | @string{ITW = "IEEE Information Theory Workshop (ITW)"} 148 | @string{ICIP = "IEEE International Conference on Image Processing (ICIP)"} 149 | @string{ICME = "IEEE International Conference on Multimedia \& Expo (ICME)"} 150 | @string{CITS = "IEEE Conference on Intelligent Transportation Systems (ITSC)"} 151 | 152 | @string{SIGGRAPH = "ACM Transactions on Graphics (Proceedings of ACM SIGGRAPH)"} 153 | @STRING{SIGGRAPHAsia = "ACM Transactions on Graphics (Proceedings of ACM SIGGRAPH Asia)"} 154 | @string{CHI = "ACM Conference on Human Factors in Computing Systems (CHI)"} 155 | @string{MMSys = "ACM Multimedia Systems Conference (MMSys)"} 156 | @string{SIGMOD = "ACM SIGMOD International Conference on Management of Data"} 157 | @string{MM = "ACM International Conference on Multimedia"} 158 | @string{KDD = "ACM SIGKDD Conference on Knowledge Discovery and Data Mining (KDD)"} 159 | @string{AAAI = "AAAI Conference on Artificial Intelligence"} 160 | @string{IJCAI = "International Joint Conference on Artificial Intelligence (IJCAI)"} 161 | 162 | @string{ACC = "American Control Conference (ACC)"} 163 | @string{WAPCV = "International Workshop on Attention in Cognitive Systems (WAPCV)"} 164 | @string{COLT92 = "Annual Workshop on Computational Learning Theory (COLT)"} 165 | 166 | @string{SIBGRAPI = "SIBGRAPI Conference on Graphics, Patterns and Images"} 167 | @string{ICIRA = "International Conference on Intelligent Robotics and Applications (ICIRA)"} 168 | 169 | @string{AISTAT = "International Conference on Artificial Intelligence and Statistics (AISTATS)"} 170 | @string{AISTATS = "International Conference on Artificial Intelligence and Statistics (AISTATS)"} 171 | 172 | @string{SCIA = "Scandinavian Conference on Image Analysis (SCIA)"} 173 | @string{EUROCOLT = "European Conference on Computational Learning Theory (EuroCOLT)"} 174 | @string{ICVS = "International Conference on Computer Vision Systems (ICVS)"} 175 | @string{EMMCVPR = "International Conference on Energy Minimization Methods in Computer Vision and Pattern Recognition (EMMCVPR)"} 176 | @string{IJCNN = "International Joint Conference on Neural Networks (IJCNN)"} 177 | 178 | @string{MICCAI = "International Conference on Medical Image Computing and Computer Assisted Intervention (MICCAI)"} 179 | @string{ICANN = "International Conference on Artificial Neural Networks (ICANN)"} 180 | @string{ISMIR = "International Society for Music Information Retrieval Conference (ISMIR)"} 181 | @string{AMDO = "International Conference on Articulated Motion and Deformable Objects (AMDO)"} 182 | @string{Allerton = "Annual Allerton Conference on Communication, Control, and Computing"} 183 | @string{OSDI = "USENIX Symposium on Operating Systems Design and Implementation (OSDI)"} 184 | 185 | @string{BRACIS = "Brazilian Conference on Intelligent Systems (BRACIS)"} 186 | @string{MIDL = "Medical Imaging with Deep Learning (MIDL)"} 187 | @string{TDBODYTECH = "International Conference and Exhibition on 3D Body Scanning and Processing Technologies (3DBODY.TECH)"} 188 | @string{IAS = "International Conference on Intelligent Autonomous Systems"} 189 | @string{CoRL = "Conference on Robot Learning"} 190 | @string{CRV = "Conference on Computer and Robot Vision"} 191 | @string{ICONIP = "International Conference on Neural Information Processing"} 192 | @string{SGP = "Symposium on Geometry Processing"} 193 | 194 | 195 | @string{WACV_until_2016 = "IEEE Workshop on Applications of Computer Vision (WACV)"} 196 | %%%%%%%%%%%%%%%%%%%%% Workshops %%%%%%%%%%%%%% 197 | @string{ICCVW = "IEEE International Conference on Computer Vision -- Workshops (ICCVW)"} 198 | @string{ECCVW = "European Conference on Computer Vision -- Workshops (ECCVW)"} 199 | @string{CVPRW = "IEEE Conference on Computer Vision and Pattern Recognition -- Workshops (CVPRW)"} 200 | @string{IROSW = "IEEE/RSJ International Conference on Intelligent Robots and Systems -- Workshops (IROSW)"} 201 | @string{WACVW = "IEEE Winter Conference on Applications of Computer Vision -- Workshops (WACVW)"} 202 | @string{MICCAIW = "International Conference on Medical Image Computing and Computer Assisted Intervention -- Workshops (MICCAIW)"} 203 | 204 | @string{MMWVSCC = "ACM Multimedia Conference (MM) -- Workshop on Visual Analysis in Smart and Connected Communities (VSCC)"} 205 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import importlib 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.abspath('.')) 7 | sys.path.insert(0, os.path.abspath('../src')) 8 | 9 | from conf_spec import project, project_slug, release 10 | 11 | release = release 12 | 13 | # -- Project information ----------------------------------------------------- 14 | linkcode_url = f'https://github.com/isarandi/{project_slug}' 15 | 16 | author = 'István Sárándi' 17 | copyright = f'{datetime.datetime.now().year}, {author}' 18 | 19 | # -- General configuration --------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | add_module_names = False 23 | python_use_unqualified_type_names = True 24 | extensions = [ 25 | 'sphinx.ext.autodoc', 26 | 'sphinx.ext.napoleon', 27 | 'sphinx.ext.autosummary', 28 | 'sphinx.ext.intersphinx', 29 | 'sphinx.ext.autodoc.typehints', 30 | 'sphinxcontrib.bibtex', 31 | 'autoapi.extension', 32 | 'sphinx.ext.viewcode', 33 | "sphinx_markdown_builder", 34 | 'sphinx.ext.inheritance_diagram', 35 | ] 36 | bibtex_bibfiles = ['abbrev_long.bib', 'references.bib'] 37 | bibtex_footbibliography_header = ".. rubric:: References" 38 | intersphinx_mapping = { 39 | 'python': ('https://docs.python.org/3', None), 40 | 'torch': ('https://pytorch.org/docs/main/', None), 41 | 'numpy': ('https://numpy.org/doc/stable/', None), 42 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 43 | } 44 | 45 | github_username = 'isarandi' 46 | github_repository = project_slug 47 | autodoc_show_sourcelink = False 48 | 49 | templates_path = ['_templates'] 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | python_display_short_literal_types = True 52 | 53 | html_title = project 54 | html_theme = 'pydata_sphinx_theme' 55 | html_theme_options = { 56 | "show_toc_level": 3, 57 | } 58 | html_static_path = ['_static'] 59 | html_css_files = ['styles/my_theme.css'] 60 | toc_object_entries_show_parents = "hide" 61 | 62 | autoapi_root = 'api' 63 | autoapi_member_order = 'bysource' 64 | autodoc_typehints = 'description' 65 | autoapi_own_page_level = 'attribute' 66 | autoapi_type = 'python' 67 | autodoc_default_options = { 68 | 'members': True, 69 | 'inherited-members': True, 70 | 'undoc-members': False, 71 | 'exclude-members': '__init__, __weakref__, __repr__, __str__' 72 | } 73 | autoapi_options = ['members', 'show-inheritance', 'special-members', 'show-module-summary'] 74 | autoapi_add_toctree_entry = True 75 | autoapi_dirs = ['../src'] 76 | autoapi_template_dir = '_templates/autoapi' 77 | 78 | autodoc_member_order = 'bysource' 79 | autoclass_content = 'class' 80 | 81 | autosummary_generate = True 82 | autosummary_imported_members = False 83 | 84 | 85 | def autodoc_skip_member(app, what, name, obj, skip, options): 86 | """ 87 | Skip members (functions, classes, modules) without docstrings. 88 | """ 89 | # Check if the object has a __doc__ attribute 90 | 91 | #if what == 'attribute': 92 | # return False 93 | if not getattr(obj, 'docstring', None): 94 | return True # Skip if there's no docstring 95 | elif what in ('class', 'function'): 96 | # Check if the module of the class has a docstring 97 | module_name = '.'.join(name.split('.')[:-1]) 98 | try: 99 | module = importlib.import_module(module_name) 100 | return not getattr(module, '__doc__', None) 101 | except ModuleNotFoundError: 102 | pass 103 | 104 | 105 | def setup(app): 106 | app.connect('autoapi-skip-member', autodoc_skip_member) 107 | app.connect('autodoc-skip-member', autodoc_skip_member) 108 | 109 | 110 | # noinspection PyUnresolvedReferences 111 | from conf_overrides import * 112 | -------------------------------------------------------------------------------- /docs/conf_overrides.py: -------------------------------------------------------------------------------- 1 | autoapi_own_page_level = 'class' -------------------------------------------------------------------------------- /docs/conf_spec.py: -------------------------------------------------------------------------------- 1 | project = 'PoseViz' 2 | project_slug = 'poseviz' 3 | release = 'v0.2.0' 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Documentation 2 | ===================== 3 | 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | :caption: Contents 8 | 9 | modules 10 | 11 | Indices and tables 12 | ================== 13 | 14 | * :ref:`genindex` 15 | * :ref:`modindex` 16 | * :ref:`search` 17 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/references.bib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isarandi/poseviz/e642db70400e75e089607c319d70436919f5a190/docs/references.bib -------------------------------------------------------------------------------- /docs_old/API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## poseviz.PoseViz 4 | 5 | The main class that creates a visualizer process and can then be used as the interface to update the 6 | visualization state. 7 | 8 | This class supports the Python **context manager** protocol to close the visualization at the end. 9 | 10 | ### Constructor 11 | 12 | ```python 13 | poseviz.PoseViz( 14 | joint_names, joint_edges, camera_type='free', n_views=1, world_up=(0, -1, 0), 15 | ground_plane_height=-1000, downscale=1, viz_fps=100, queue_size=64, write_video=False, 16 | multicolor_detections=False, snap_to_cam_on_scene_change=True, high_quality=True, 17 | draw_2d_pose=False, show_field_of_view=True, resolution=(1280, 720), 18 | use_virtual_display=False, show_virtual_display=True 19 | ) 20 | ``` 21 | 22 | #### Arguments: 23 | - **joint_names**: an iterable of strings, the names of the human body joints that will be 24 | visualized. Left joints must start with 'l', right joints with 'r', mid-body joints with something 25 | else. Currently only the first character is inspected in each joint name, to color the left, right 26 | and middle part of the stick figure differently. 27 | - **joint_edges**: an iterable of joint-index pairs, describing the bone-connectivity of the stick 28 | figure. 29 | - **camera_type**: 30 | - **n_views**: Integer, the number of cameras that will be displayed. 31 | - **world_up**: A 3-vector, the up vector in the world coordinate system in which the poses will be 32 | specified. 33 | - **ground_plane_height**: Scalar, the vertical position of the ground plane in the world coordinate 34 | system. 35 | - **downscale**: Integer, an image downscaling factor for display. May speed up the visualization. 36 | - **viz_fps**: Target frames-per-second of the visualization. If the updates would come faster than 37 | this, the visualizer will block, to ensure that visualization does not happen faster than this 38 | FPS. Of course if the speed of updates cannot deliver this fps, the visualization will also be 39 | slower. 40 | - **queue_size**: Integer, size of the internal queue used to communicate with the visualizer 41 | process. 42 | - **write_video**: Boolean, whether the visualization output will be recorded to a video file. 43 | - **multicolor_detections**: Boolean, color each box with a different color. Useful when tracking 44 | with fixed IDs. 45 | - **snap_to_cam_on_scene_change**: Boolean, whether to reinitialize the view camera to the original 46 | camera on each change of sequence (call to ```viz.reinit_camera_view()```). 47 | - **high_quality**: Boolean, whether to use high resolution spheres and tubes for the skeletons (may 48 | be faster to set it to False). 49 | - **draw_2d_pose**: Boolean, whether to draw the 2D skeleton on the displayed camera image. 50 | - **show_field_of_view**: Boolean, whether to visualize an extended pyramid indicating what is part 51 | of the field of view of the cameras. Recommended to turn off in multi-camera setups, as the 52 | visualization can get crowded otherwise. 53 | - **resolution**: The resolution of the visualization window (width, height) integer pair. 54 | - **use_virtual_display**: Boolean, whether to use a virtual display for visualization. There may be 55 | two reasons to do this. First, to allow higher resolution visualization than the screen 56 | resolution. Normally, Mayavi won't allow windows that are larger than the display screen (just 57 | automatically "maximizes" the window.). Second, it can be a way to do off-screen rendering. 58 | - **show_virtual_display**: Boolean, whether to show the virtual display or to hide it (off-screen 59 | rendering). Has no effect if `use_virtual_display``` is False. 60 | 61 | 62 | ### update 63 | 64 | ```python 65 | viz.update(frame, boxes, poses, camera, poses_true=(), poses_alt=(), block=True) 66 | ``` 67 | 68 | #### Arguments: 69 | 70 | -**frame** 71 | -**boxes** 72 | -**poses** 73 | -**camera** 74 | 75 | 76 | #### Return value: 77 | 78 | 79 | ### update 80 | 81 | ```python 82 | viz.update_multiview(view_infos: List[ViewInfo], block=True) 83 | ``` 84 | 85 | #### Arguments: 86 | 87 | 88 | #### Return value: 89 | 90 | 91 | ### update 92 | 93 | ```python 94 | viz.reinit_camera_view() 95 | ``` 96 | 97 | #### Arguments: 98 | 99 | 100 | #### Return value: 101 | 102 | 103 | ### update 104 | 105 | ```python 106 | viz.new_sequence_output(new_video_path, fps) 107 | ``` 108 | 109 | #### Arguments: 110 | 111 | #### Return value: 112 | 113 | 114 | ### update 115 | 116 | ```python 117 | viz.end_sequence() 118 | ``` 119 | 120 | #### Arguments: 121 | 122 | #### Return value: 123 | 124 | 125 | ## poseviz.Camera 126 | 127 | Class for specifying where to visualize the cameras and how to project poses onto them. 128 | 129 | ### Constructor 130 | 131 | ```python 132 | poseviz.Camera( 133 | optical_center=None, rot_world_to_cam=None, intrinsic_matrix=np.eye(3), 134 | distortion_coeffs=None, world_up=(0, 0, 1), extrinsic_matrix=None) 135 | ) 136 | ``` 137 | 138 | #### Arguments: 139 | 140 | - **optical_center**: The position of the camera as a 3-vector. 141 | - **rot_world_to_cam**: 3x3 matrix of rotation from world coordinate system to camera system 142 | - **intrinsic_matrix**: 3x3 matrix of the intrinsics 143 | - **distortion_coeffs**: a vector of 5 elements, radial and tangential distortion coefficients 144 | according 145 | to [OpenCV's order](https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html) 146 | (k1, k2, p1, p2, k3). 147 | - **world_up**: Up vector in the world (3-vector) 148 | - **extrinsic_matrix**: 4x4 matrix, an alternative way of specifying extrinsics instead of using ` 149 | optical center` and `rot_world_to_cam`. 150 | 151 | -------------------------------------------------------------------------------- /docs_old/index.md: -------------------------------------------------------------------------------- 1 | # PoseViz – 3D Human Pose Visualizer 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 | Multi-person, multi-camera 3D human pose visualization tool built using 10 | [Mayavi](https://docs.enthought.com/mayavi/mayavi/). As used 11 | in [MeTRAbs](https://github.com/isarandi/metrabs) visualizations. 12 | 13 | ## Gist of Usage 14 | 15 | ```python 16 | import poseviz 17 | 18 | viz = poseviz.PoseViz(...) 19 | camera = poseviz.Camera(...) 20 | for frame in frames: 21 | bounding_boxes, poses3d = run_pose_estimation_model(frame) 22 | viz.update(frame=frame, boxes=bounding_boxes, poses=poses3d, camera=camera) 23 | ``` 24 | 25 | The main feature of this tool is that the graphical event loop is hidden from the library user. We 26 | want to write code in terms of the *prediction loop* of the human pose estimator, not from the point 27 | of view of the visualizer tool. 28 | 29 | Behind the scenes, this is achieved through forking a dedicated visualization process and passing 30 | new scene information via multiprocessing queues. 31 | 32 | ## Installation 33 | 34 | PoseViz is released as a conda package (experimental, tested only on Linux): 35 | 36 | ```bash 37 | conda install poseviz -c isarandi 38 | ``` 39 | 40 | Alternatively, in case the above doesn't work, install Mayavi via conda (the Mayavi pip package has 41 | compilation problems), clone this repo and install PoseViz via pip: 42 | 43 | ```bash 44 | conda install mayavi -c conda-forge 45 | pip install git+https://github.com/isarandi/poseviz.git 46 | ``` -------------------------------------------------------------------------------- /docs_old/poseviz.md: -------------------------------------------------------------------------------- 1 | ::: poseviz.PoseViz 2 | rendering: 3 | show_root_heading: no 4 | members_order: source 5 | -------------------------------------------------------------------------------- /docs_old/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | /* Indentation. */ 2 | div.doc-contents:not(.first) { 3 | padding-left: 25px; 4 | border-left: 4px solid rgba(230, 230, 230); 5 | margin-bottom: 60px; 6 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61", 4 | "wheel", 5 | "setuptools_scm[toml]>=8" 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "poseviz" 11 | dynamic = ["version"] 12 | description = "3D human pose visualizer with multi-person, multi-view support, built on Mayavi." 13 | authors = [ 14 | { name = "István Sárándi", email = "istvan.sarandi@uni-tuebingen.de" } 15 | ] 16 | readme = "README.md" 17 | requires-python = ">=3.8" 18 | license = { file = "LICENSE" } 19 | 20 | dependencies = [ 21 | 'more-itertools', 22 | 'opencv-python', 23 | 'numpy', 24 | 'mayavi', 25 | 'imageio', 26 | 'cameravision', 27 | 'boxlib', 28 | 'framepump', 29 | ] 30 | 31 | [tool.setuptools] 32 | package-dir = { "" = "src" } 33 | 34 | [tool.setuptools.packages.find] 35 | where = ["src"] 36 | 37 | [tool.black] 38 | line-length = 99 39 | skip-string-normalization = true 40 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isarandi/poseviz/e642db70400e75e089607c319d70436919f5a190/screenshot.jpg -------------------------------------------------------------------------------- /screenshot2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isarandi/poseviz/e642db70400e75e089607c319d70436919f5a190/screenshot2.jpg -------------------------------------------------------------------------------- /screenshot_multicam.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isarandi/poseviz/e642db70400e75e089607c319d70436919f5a190/screenshot_multicam.jpg -------------------------------------------------------------------------------- /src/poseviz/__init__.py: -------------------------------------------------------------------------------- 1 | """PoseViz is a 3D visualization tool for human pose estimation.""" 2 | 3 | from poseviz.poseviz import PoseViz 4 | from poseviz.view_info import ViewInfo 5 | -------------------------------------------------------------------------------- /src/poseviz/colors.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | green = (0.17254901960784313, 0.62745098039215685, 0.17254901960784313) 4 | red = (0.83921568627450982, 0.15294117647058825, 0.15686274509803921) 5 | gray = (0.49803921568627452, 0.49803921568627452, 0.49803921568627452) 6 | cyan = (0.090196078431372548, 0.74509803921568629, 0.81176470588235294) 7 | yellow = (0.73725490196078436, 0.74117647058823533, 0.13333333333333333) 8 | blue = (0.12156862745098039, 0.46666666666666667, 0.70588235294117652) 9 | orange = (1.0, 0.49803921568627452, 0.054901960784313725) 10 | black = (0.0, 0.0, 0.0) 11 | 12 | 13 | def cycle_over_colors(range_zero_one=False): 14 | """Returns a generator that cycles over a list of nice colors, indefinitely.""" 15 | colors = ( 16 | (0.12156862745098039, 0.46666666666666667, 0.70588235294117652), 17 | (1.0, 0.49803921568627452, 0.054901960784313725), 18 | (0.17254901960784313, 0.62745098039215685, 0.17254901960784313), 19 | (0.83921568627450982, 0.15294117647058825, 0.15686274509803921), 20 | (0.58039215686274515, 0.40392156862745099, 0.74117647058823533), 21 | (0.5490196078431373, 0.33725490196078434, 0.29411764705882354), 22 | (0.8901960784313725, 0.46666666666666667, 0.76078431372549016), 23 | (0.49803921568627452, 0.49803921568627452, 0.49803921568627452), 24 | (0.73725490196078436, 0.74117647058823533, 0.13333333333333333), 25 | (0.090196078431372548, 0.74509803921568629, 0.81176470588235294), 26 | ) 27 | 28 | if not range_zero_one: 29 | colors = [[c * 255 for c in color] for color in colors] 30 | 31 | return itertools.cycle(colors) 32 | -------------------------------------------------------------------------------- /src/poseviz/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isarandi/poseviz/e642db70400e75e089607c319d70436919f5a190/src/poseviz/components/__init__.py -------------------------------------------------------------------------------- /src/poseviz/components/camera_viz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tvtk.api 3 | from mayavi import mlab 4 | from scipy.spatial.transform import Rotation 5 | import poseviz.colors 6 | import poseviz.mayavi_util 7 | 8 | 9 | class CameraViz: 10 | def __init__( 11 | self, camera_type, show_image, show_field_of_view=True, show_camera_wireframe=True 12 | ): 13 | self.viz_im = None 14 | self.is_initialized = False 15 | self.prev_cam = None 16 | self.camera_type = camera_type 17 | self.show_image = show_image 18 | self.prev_imshape = None 19 | self.show_field_of_view = show_field_of_view 20 | self.show_camera_wireframe = show_camera_wireframe 21 | self.mesh = None 22 | self.mesh2 = None 23 | self.mesh3 = None 24 | self.prev_highlight = False 25 | 26 | def initial_update(self, camera, image, highlight=False): 27 | image_corners, far_corners = self.calculate_camera_vertices(camera, image.shape) 28 | 29 | if self.camera_type != "original": 30 | triangles = np.array([[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 1]]) 31 | 32 | if self.show_camera_wireframe: 33 | self.mesh = mlab.triangular_mesh( 34 | *image_corners[:5].T, 35 | triangles, 36 | color=poseviz.colors.cyan if highlight else poseviz.colors.black, 37 | tube_radius=0.04 if highlight else 0.02, 38 | line_width=10 if highlight else 1, 39 | tube_sides=3, 40 | representation="wireframe", 41 | reset_zoom=False, 42 | ) 43 | 44 | if self.show_field_of_view: 45 | self.mesh2 = mlab.triangular_mesh( 46 | *far_corners.T, 47 | triangles, 48 | color=poseviz.colors.black, 49 | opacity=0.1, 50 | representation="surface", 51 | reset_zoom=False, 52 | ) 53 | self.mesh3 = mlab.triangular_mesh( 54 | *far_corners.T, 55 | triangles, 56 | color=poseviz.colors.gray, 57 | opacity=0.1, 58 | tube_radius=0.01, 59 | tube_sides=3, 60 | representation="wireframe", 61 | reset_zoom=False, 62 | ) 63 | 64 | if self.show_image: 65 | self.new_imshow(image.shape, image_corners) 66 | self.set_image_content(image) 67 | self.set_image_position(image_corners) 68 | 69 | self.prev_cam = camera 70 | self.prev_imshape = image.shape 71 | self.prev_highlight = highlight 72 | self.is_initialized = True 73 | 74 | def update(self, camera, image, highlight=False): 75 | if not self.is_initialized: 76 | return self.initial_update(camera, image, highlight) 77 | 78 | if ( 79 | self.prev_cam.allclose(camera) 80 | and image.shape == self.prev_imshape 81 | and self.prev_highlight == highlight 82 | ): 83 | # Only the image content has changed 84 | if self.show_image: 85 | self.set_image_content(image) 86 | return 87 | elif self.prev_highlight == highlight: 88 | image_points, far_corners = self.calculate_camera_vertices(camera, image.shape) 89 | if self.camera_type != "original": 90 | if self.show_camera_wireframe: 91 | self.mesh.mlab_source.set( 92 | x=image_points[:5, 0], y=image_points[:5, 1], z=image_points[:5, 2] 93 | ) 94 | if self.show_field_of_view: 95 | self.mesh2.mlab_source.set( 96 | x=far_corners[:, 0], y=far_corners[:, 1], z=far_corners[:, 2] 97 | ) 98 | self.mesh3.mlab_source.set( 99 | x=far_corners[:, 0], y=far_corners[:, 1], z=far_corners[:, 2] 100 | ) 101 | 102 | if self.show_image: 103 | if image.shape[:2] != self.prev_imshape[:2] or not np.allclose( 104 | camera.intrinsic_matrix, self.prev_cam.intrinsic_matrix 105 | ): 106 | self.viz_im.remove() 107 | self.new_imshow(image.shape, image_points) 108 | self.prev_imshape = image.shape 109 | 110 | self.set_image_content(image) 111 | self.set_image_position(image_points) 112 | self.prev_cam = camera 113 | else: 114 | # We change the highlight state by reinitializing it all 115 | for elem in [self.viz_im, self.mesh, self.mesh2, self.mesh3]: 116 | if elem is not None: 117 | elem.remove() 118 | self.initial_update(camera, image, highlight) 119 | 120 | def new_imshow(self, imshape, mayavi_image_points): 121 | mayavi_width = np.linalg.norm(mayavi_image_points[1] - mayavi_image_points[2]) 122 | mayavi_height = np.linalg.norm(mayavi_image_points[2] - mayavi_image_points[3]) 123 | extent = [0, mayavi_height, 0, mayavi_width, 0, 0] 124 | self.viz_im = mlab.imshow( 125 | np.ones(imshape[:2]), opacity=0.5, extent=extent, reset_zoom=False, interpolate=True 126 | ) 127 | 128 | def calculate_camera_vertices(self, camera, imshape): 129 | h, w = imshape[:2] 130 | image_corners = np.array([[0, 0], [w - 1, 0], [w - 1, h - 1], [0, h - 1]], np.float32) 131 | image_center = np.array(imshape[:2][::-1]) / 2 132 | image_points = np.concatenate([image_corners, [image_center]], axis=0) 133 | image_points_world = camera.image_to_world(image_points, camera_depth=150) 134 | points = np.array([camera.t, *image_points_world]) 135 | mayavi_image_points = poseviz.mayavi_util.world_to_mayavi(points) 136 | mayavi_far_corners = ( 137 | mayavi_image_points[0] + (mayavi_image_points[:5] - mayavi_image_points[0]) * 20 138 | ) 139 | return mayavi_image_points, mayavi_far_corners 140 | 141 | def set_image_content(self, image): 142 | reshaped = image.view().reshape([-1, 3], order="F") 143 | self.colors = tvtk.api.tvtk.UnsignedCharArray() 144 | self.colors.from_array(reshaped) 145 | self.viz_im.actor.input.point_data.scalars = self.colors 146 | 147 | def set_image_position(self, mayavi_image_points): 148 | down = unit_vector(mayavi_image_points[4] - mayavi_image_points[1]) 149 | right = unit_vector(mayavi_image_points[2] - mayavi_image_points[1]) 150 | R = np.column_stack([down, right, np.cross(down, right)]) 151 | z, x, y = Rotation.from_matrix(R).as_euler("ZXY", degrees=True) 152 | self.viz_im.actor.orientation = [x, y, z] 153 | self.viz_im.actor.position = mayavi_image_points[5] 154 | 155 | def remove(self): 156 | if self.is_initialized: 157 | for elem in [self.viz_im, self.mesh, self.mesh2, self.mesh3]: 158 | if elem is not None: 159 | elem.remove() 160 | self.is_initialized = False 161 | 162 | 163 | def unit_vector(v): 164 | return v / np.linalg.norm(v) 165 | -------------------------------------------------------------------------------- /src/poseviz/components/connected_points_viz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from mayavi import mlab 3 | 4 | 5 | class ConnectedPoints: 6 | def __init__( 7 | self, color, line_color, mode, scale_factor, opacity_points, opacity_lines, 8 | point_names, high_quality=False): 9 | self.coords = [] 10 | self.n_rendered_points = 0 11 | self.color = color 12 | self.mode = mode 13 | self.scale_factor = scale_factor 14 | self.line_color = line_color 15 | self.opacity_points = opacity_points 16 | self.opacity_lines = opacity_lines 17 | self.is_initialized = False 18 | self.points = None 19 | self.tube = None 20 | self.edges = [] 21 | self.point_name_components = [] 22 | self.point_names = point_names 23 | self.high_quality = high_quality 24 | 25 | def clear(self): 26 | self.coords.clear() 27 | self.edges.clear() 28 | 29 | def add_points(self, coords, edges, show_isolated_points=False): 30 | if not show_isolated_points: 31 | coords, edges = self.get_nonisolated(coords, edges) 32 | n_points = len(self.coords) 33 | self.coords.extend(coords) 34 | self.edges.extend([[n_points + i, n_points + j] for i, j in edges]) 35 | 36 | def get_nonisolated(self, coords, edges): 37 | selected_coords = [] 38 | new_point_ids = {} 39 | selected_edges = [] 40 | 41 | for (i, j) in edges: 42 | if i not in new_point_ids: 43 | new_point_ids[i] = len(selected_coords) 44 | selected_coords.append(coords[i]) 45 | if j not in new_point_ids: 46 | new_point_ids[j] = len(selected_coords) 47 | selected_coords.append(coords[j]) 48 | selected_edges.append([new_point_ids[i], new_point_ids[j]]) 49 | return selected_coords, selected_edges 50 | 51 | def update(self): 52 | if not self.is_initialized: 53 | return self.initial_update() 54 | c = np.asarray(self.coords) 55 | 56 | if not self.coords: 57 | c = c.reshape((0, 3)) 58 | 59 | if len(self.coords) == self.n_rendered_points: 60 | self.points.mlab_source.set(x=c[:, 0], y=c[:, 1], z=c[:, 2]) 61 | else: 62 | self.n_rendered_points = len(self.coords) 63 | self.points.mlab_source.reset(x=c[:, 0], y=c[:, 1], z=c[:, 2]) 64 | self.points.mlab_source.dataset.lines = self.edges 65 | 66 | def initial_update(self): 67 | if not self.coords: 68 | return 69 | c = np.asarray(self.coords) 70 | 71 | self.points = mlab.points3d( 72 | *c.T, scale_factor=self.scale_factor, color=self.color, 73 | opacity=self.opacity_points, 74 | mode=self.mode, resolution=16 if self.high_quality else 3, scale_mode='vector', 75 | reset_zoom=False) 76 | self.points.mlab_source.dataset.lines = self.edges 77 | self.tube = mlab.pipeline.tube( 78 | self.points, tube_radius=self.scale_factor / 5, 79 | tube_sides=12 if self.high_quality else 3) 80 | mlab.pipeline.surface( 81 | self.tube, color=self.line_color, opacity=self.opacity_lines, reset_zoom=False) 82 | 83 | # for i, (point, name, color) in enumerate( 84 | # zip(c, self.point_names, colors.cycle_over_colors())): 85 | # self.point_name_components.append(mlab.text3d( 86 | # *point, f'{i}', scale=0.02, color=color)) 87 | 88 | self.is_initialized = True 89 | 90 | def remove(self): 91 | if self.is_initialized: 92 | self.points.remove() 93 | self.tube.remove() 94 | for component in self.point_name_components: 95 | component.remove() 96 | self.is_initialized = False 97 | self.clear() 98 | self.point_name_components.clear() 99 | self.n_rendered_points = 0 -------------------------------------------------------------------------------- /src/poseviz/components/ground_plane_viz.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import poseviz.mayavi_util 3 | import tvtk.api 4 | from mayavi import mlab 5 | 6 | 7 | def draw_checkerboard(ground_plane_height): 8 | i, j = np.mgrid[:40, :40] 9 | image = ((i + j) % 2 == 0).astype(np.uint8) 10 | image[image == 0] = 0 11 | image[image == 1] = 255 - 96 12 | 13 | # image[image == 0] = 96 14 | # image[image == 1] = 256-96 15 | image = np.expand_dims(image, -1) * np.ones(3) 16 | 17 | size = 2 * 19 18 | extent = [0, size, 0, size, 0, 0] 19 | viz_im = mlab.imshow( 20 | np.ones(image.shape[:2]), opacity=0.3, extent=extent, reset_zoom=False, interpolate=False 21 | ) 22 | 23 | reshaped = image.reshape([-1, 3], order="F") 24 | colors = tvtk.api.tvtk.UnsignedCharArray() 25 | colors.number_of_components = 3 26 | colors.from_array(reshaped) 27 | viz_im.actor.input.point_data.scalars = colors 28 | viz_im.actor.orientation = [0, 0, 90] 29 | viz_im.actor.position = np.array([-1, 1, ground_plane_height * poseviz.mayavi_util.MM_TO_UNIT]) 30 | -------------------------------------------------------------------------------- /src/poseviz/components/main_viz.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import poseviz.colors as colors 4 | import poseviz.components.camera_viz 5 | import poseviz.components.skeletons_viz 6 | import poseviz.components.smpl_viz 7 | 8 | 9 | class MainViz: 10 | def __init__( 11 | self, 12 | joint_info_pred, 13 | joint_info_true, 14 | joint_info_alt, 15 | camera_type, 16 | show_image, 17 | high_quality, 18 | show_field_of_view=True, 19 | show_camera_wireframe=True, 20 | body_model_faces=None, 21 | ): 22 | 23 | if joint_info_pred[0] is not None: 24 | self.skeletons_pred = poseviz.components.skeletons_viz.SkeletonsViz( 25 | joint_info_pred, 26 | colors.blue, 27 | colors.cyan, 28 | colors.yellow, 29 | colors.green, 30 | 0.06, 31 | high_quality, 32 | opacity=0.95, 33 | ) 34 | else: 35 | self.skeletons_pred = None 36 | 37 | if joint_info_true[0] is not None: 38 | self.skeletons_true = poseviz.components.skeletons_viz.SkeletonsViz( 39 | joint_info_true, 40 | colors.red, 41 | colors.red, 42 | colors.red, 43 | colors.red, 44 | 0.06, 45 | high_quality, 46 | opacity=0.95, 47 | ) 48 | else: 49 | self.skeletons_true = None 50 | 51 | if joint_info_alt[0] is not None: 52 | self.skeletons_alt = poseviz.components.skeletons_viz.SkeletonsViz( 53 | joint_info_alt, 54 | colors.orange, 55 | colors.orange, 56 | colors.orange, 57 | colors.orange, 58 | 0.06, 59 | high_quality, 60 | opacity=0.95, 61 | ) 62 | else: 63 | self.skeletons_alt = None 64 | 65 | if body_model_faces is not None: 66 | self.meshes_pred = poseviz.components.smpl_viz.SMPLViz( 67 | color=colors.blue, faces=body_model_faces, add_wireframe=True, colormap="Blues_r" 68 | ) 69 | self.meshes_gt = poseviz.components.smpl_viz.SMPLViz( 70 | color=colors.red, faces=body_model_faces, add_wireframe=True 71 | ) 72 | self.meshes_pred2 = poseviz.components.smpl_viz.SMPLViz( 73 | color=colors.orange, 74 | faces=body_model_faces, 75 | add_wireframe=True, 76 | colormap="Oranges_r", 77 | ) 78 | else: 79 | self.meshes_pred = None 80 | self.meshes_gt = None 81 | self.meshes_pred2 = None 82 | 83 | self.camera_viz = poseviz.components.camera_viz.CameraViz( 84 | camera_type, show_image, show_field_of_view, show_camera_wireframe 85 | ) 86 | 87 | def update( 88 | self, 89 | camera_display, 90 | image, 91 | poses_pred=None, 92 | poses_true=None, 93 | poses_alt=None, 94 | vertices_pred=None, 95 | vertices_true=None, 96 | vertices_alt=None, 97 | highlight=False, 98 | ): 99 | 100 | if poses_pred is not None and self.skeletons_pred is not None: 101 | self.skeletons_pred.update(poses_pred) 102 | if poses_true is not None and self.skeletons_true is not None: 103 | self.skeletons_true.update(poses_true) 104 | if poses_alt is not None and self.skeletons_alt is not None: 105 | self.skeletons_alt.update(poses_alt) 106 | 107 | if vertices_pred is not None and self.meshes_pred is not None: 108 | self.meshes_pred.update(vertices_pred) 109 | if vertices_true is not None and self.meshes_gt is not None: 110 | self.meshes_gt.update(vertices_true) 111 | if vertices_alt is not None and self.meshes_pred2 is not None: 112 | self.meshes_pred2.update(vertices_alt) 113 | 114 | self.camera_viz.update(camera_display, image, highlight=highlight) 115 | 116 | def remove(self): 117 | if self.is_initialized: 118 | for elem in [ 119 | self.skeletons_pred, 120 | self.skeletons_true, 121 | self.skeletons_alt, 122 | self.meshes_pred, 123 | self.meshes_gt, 124 | self.meshes_pred2, 125 | ]: 126 | if elem is not None: 127 | elem.remove() 128 | 129 | self.camera_viz.remove() 130 | 131 | self.is_initialized = False 132 | -------------------------------------------------------------------------------- /src/poseviz/components/skeletons_viz.py: -------------------------------------------------------------------------------- 1 | import poseviz.components.connected_points_viz 2 | import poseviz.mayavi_util 3 | 4 | 5 | class SkeletonsViz: 6 | def __init__( 7 | self, 8 | joint_info, 9 | left_color, 10 | mid_color, 11 | right_color, 12 | point_color, 13 | scale_factor, 14 | high_quality=False, 15 | opacity=0.7, 16 | ): 17 | 18 | joint_names, joint_edges = joint_info 19 | edge_colors = dict(left=left_color, mid=mid_color, right=right_color) 20 | sides = ("left", "mid", "right") 21 | self.pointsets = { 22 | side: poseviz.components.connected_points_viz.ConnectedPoints( 23 | point_color, 24 | edge_colors[side], 25 | "sphere", 26 | scale_factor, 27 | opacity, 28 | opacity, 29 | joint_names, 30 | high_quality, 31 | ) 32 | for side in sides 33 | } 34 | 35 | def edge_side(edge): 36 | s1 = joint_side(edge[0]) 37 | s2 = joint_side(edge[1]) 38 | if s1 == "left" or s2 == "left": 39 | return "left" 40 | elif s1 == "right" or s2 == "right": 41 | return "right" 42 | else: 43 | return "mid" 44 | 45 | def joint_side(joint): 46 | name = joint_names[joint].lower() 47 | if name.startswith("l"): 48 | return "left" 49 | elif name.startswith("r"): 50 | return "right" 51 | else: 52 | return "mid" 53 | 54 | self.edges_per_side = { 55 | side: [e for e in joint_edges if edge_side(e) == side] for side in sides 56 | } 57 | 58 | self.indices_per_side = { 59 | side: sorted( 60 | set([index for edge in self.edges_per_side[side] for index in edge]) 61 | | set( 62 | [index for index, name in enumerate(joint_names) if joint_side(index) == side] 63 | ) 64 | ) 65 | for side in sides 66 | } 67 | 68 | def index_within_side(global_index, side): 69 | return self.indices_per_side[side].index(global_index) 70 | 71 | self.edges_within_side = { 72 | side: [ 73 | (index_within_side(i, side), index_within_side(j, side)) 74 | for i, j in self.edges_per_side[side] 75 | ] 76 | for side in sides 77 | } 78 | 79 | def update(self, poses): 80 | mayavi_poses = [poseviz.mayavi_util.world_to_mayavi(pose) for pose in poses] 81 | for side, pointset in self.pointsets.items(): 82 | pointset.clear() 83 | for coords in mayavi_poses: 84 | pointset.add_points( 85 | coords[self.indices_per_side[side]], 86 | self.edges_within_side[side], 87 | show_isolated_points=True, 88 | ) 89 | pointset.update() 90 | 91 | def remove(self): 92 | for pointset in self.pointsets.values(): 93 | pointset.remove() 94 | -------------------------------------------------------------------------------- /src/poseviz/components/smpl_viz.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | from mayavi import mlab 4 | 5 | import poseviz.mayavi_util 6 | 7 | 8 | class SMPLViz: 9 | def __init__(self, color, faces, add_wireframe=False, colormap="viridis"): 10 | self.opacity = 0.6 11 | self.faces = faces 12 | self.is_initialized = False 13 | self.n_rendered_verts = 0 14 | self.color = color 15 | self.add_wireframe = add_wireframe 16 | self.colormap = colormap 17 | 18 | def update(self, poses): 19 | if not self.is_initialized: 20 | return self.initial_update(poses) 21 | 22 | mayavi_poses = [poseviz.mayavi_util.world_to_mayavi(pose[:, :3]) for pose in poses] 23 | uncerts = [pose[:, 3] if pose.shape[1] == 4 else None for pose in poses] 24 | 25 | if len(mayavi_poses) == 0: 26 | c = np.array([[6, -3, 2]], np.float32) 27 | u = np.array([0], np.float32) 28 | else: 29 | c = np.concatenate(mayavi_poses, axis=0) 30 | if uncerts[0] is not None: 31 | u = np.concatenate(uncerts, axis=0) 32 | else: 33 | u = None 34 | 35 | if len(c) == self.n_rendered_verts: 36 | # Number of vertices is the same as prev frame, so we can just update the positions 37 | if u is not None: 38 | self.mesh.mlab_source.set(x=c[:, 0], y=c[:, 1], z=c[:, 2], scalars=u) 39 | else: 40 | self.mesh.mlab_source.set(x=c[:, 0], y=c[:, 1], z=c[:, 2]) 41 | else: 42 | self.n_rendered_verts = len(c) 43 | 44 | if len(mayavi_poses) == 0: 45 | triangles = np.zeros([1, 3], np.int32) 46 | else: 47 | num_vertices = poses[0].shape[0] 48 | triangles = np.concatenate( 49 | [np.asarray(self.faces) + i * num_vertices for i in range(len(mayavi_poses))], 50 | axis=0, 51 | ) 52 | 53 | if u is not None: 54 | self.mesh.mlab_source.reset( 55 | x=c[:, 0], y=c[:, 1], z=c[:, 2], triangles=triangles, scalars=u 56 | ) 57 | else: 58 | self.mesh.mlab_source.reset(x=c[:, 0], y=c[:, 1], z=c[:, 2], triangles=triangles) 59 | 60 | def initial_update(self, poses): 61 | if len(poses) == 0: 62 | return 63 | 64 | mayavi_poses = [poseviz.mayavi_util.world_to_mayavi(pose[:, :3]) for pose in poses] 65 | uncerts = [pose[:, 3] if pose.shape[1] == 4 else None for pose in poses] 66 | 67 | num_vertices = poses[0].shape[0] 68 | triangles = np.concatenate( 69 | [np.asarray(self.faces) + i * num_vertices for i in range(len(mayavi_poses))], axis=0 70 | ) 71 | c = np.concatenate(mayavi_poses, axis=0) 72 | if uncerts[0] is not None: 73 | u = np.concatenate(uncerts, axis=0) 74 | else: 75 | u = None 76 | 77 | if u is not None: 78 | self.mesh = mlab.triangular_mesh( 79 | *c.T, 80 | triangles, 81 | scalars=u, 82 | colormap="cool", 83 | vmin=0, 84 | vmax=0.1, 85 | opacity=self.opacity, 86 | representation="surface", 87 | reset_zoom=False, 88 | ) 89 | 90 | cmap = plt.get_cmap(self.colormap) 91 | cmaplist = np.array([cmap(i) for i in range(cmap.N)]) * 255 92 | self.mesh.module_manager.scalar_lut_manager.lut.table = cmaplist 93 | else: 94 | self.mesh = mlab.triangular_mesh( 95 | *c.T, 96 | triangles, 97 | color=self.color, 98 | opacity=self.opacity, 99 | representation="surface", 100 | reset_zoom=False, 101 | ) 102 | 103 | attrs = dict(backface_culling=True) 104 | 105 | if self.add_wireframe: 106 | attrs.update(edge_visibility=True, edge_color=(0.0, 0.0, 0.0), line_width=0.1) 107 | 108 | for key, value in attrs.items(): 109 | setattr(self.mesh.actor.property, key, value) 110 | 111 | self.is_initialized = True 112 | self.n_rendered_meshes = len(mayavi_poses) 113 | 114 | def remove(self): 115 | if self.is_initialized: 116 | self.mesh.remove() 117 | self.is_initialized = False 118 | -------------------------------------------------------------------------------- /src/poseviz/draw2d.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | 4 | 5 | def rounded_int_tuple(p): 6 | return tuple(np.round(p).astype(int)) 7 | 8 | 9 | def line(im, p1, p2, *args, **kwargs): 10 | cv2.line(im, rounded_int_tuple(p1), rounded_int_tuple(p2), *args, **kwargs) 11 | 12 | 13 | def draw_stick_figure_2d_inplace(im, coords, joint_edges, thickness=3, color=None): 14 | for i_joint1, i_joint2 in joint_edges: 15 | relevant_coords = coords[[i_joint1, i_joint2]] 16 | if not np.isnan(relevant_coords).any() and not np.isclose(0, relevant_coords).any(): 17 | line( 18 | im, 19 | coords[i_joint1], 20 | coords[i_joint2], 21 | color=color, 22 | thickness=thickness, 23 | lineType=cv2.LINE_AA, 24 | ) 25 | 26 | 27 | def rectangle(im, pt1, pt2, color, thickness): 28 | cv2.rectangle(im, rounded_int_tuple(pt1), rounded_int_tuple(pt2), color, thickness) 29 | 30 | 31 | def draw_box(im, box, color=(255, 0, 0), thickness=5): 32 | box = np.array(box) 33 | rectangle(im, box[:2], box[:2] + box[2:4], color, thickness) 34 | 35 | 36 | def resize_by_factor(im, factor, interp=None, dst=None): 37 | """Returns a copy of `im` resized by `factor`, using bilinear interp for up and area interp 38 | for downscaling. 39 | """ 40 | new_size = rounded_int_tuple([im.shape[1] * factor, im.shape[0] * factor]) 41 | if interp is None: 42 | interp = cv2.INTER_LINEAR if factor > 1.0 else cv2.INTER_AREA 43 | return cv2.resize(im, new_size, fx=factor, fy=factor, interpolation=interp, dst=dst) 44 | 45 | 46 | 47 | def resize(im, dst, interp=None): 48 | """Returns a copy of `im` resized by `factor`, using bilinear interp for up and area interp 49 | for downscaling. 50 | """ 51 | if interp is None: 52 | interp = cv2.INTER_LINEAR if dst.shape[0] > im.shape[0] else cv2.INTER_AREA 53 | return cv2.resize(im, (dst.shape[1], dst.shape[0]), interpolation=interp, dst=dst) 54 | -------------------------------------------------------------------------------- /src/poseviz/init.py: -------------------------------------------------------------------------------- 1 | import vtk 2 | from mayavi import mlab 3 | from tvtk.api import tvtk 4 | import ctypes 5 | import signal 6 | 7 | 8 | def initialize(size=(1280, 720)): 9 | fig = mlab.figure('PoseViz', bgcolor=(1, 1, 1), fgcolor=(0, 0, 0), size=size) 10 | fig.scene.renderer.render_window.set(alpha_bit_planes=1, multi_samples=0, full_screen=False) 11 | fig.scene.renderer.set(use_depth_peeling=True, maximum_number_of_peels=5, occlusion_ratio=0.01) 12 | 13 | fig.scene._tool_bar.setVisible(False) 14 | fig.scene.interactor.interactor_style = tvtk.InteractorStyleTerrain() 15 | # Suppress warning outputs that have no effect on the visualization. 16 | output = vtk.vtkFileOutputWindow() 17 | output.SetFileName('/dev/null') 18 | vtk.vtkOutputWindow().SetInstance(output) 19 | vtk.vtkObject.GlobalWarningDisplayOff() 20 | fig.scene.camera.clipping_range = [0.1, 1000] 21 | fig.scene.anti_aliasing_frames = 0 22 | return fig 23 | 24 | -------------------------------------------------------------------------------- /src/poseviz/mayavi_util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import transforms3d 3 | from mayavi import mlab 4 | 5 | import cameravision 6 | 7 | MM_TO_UNIT = 1 / 1000 8 | UNIT_TO_MM = 1000 9 | WORLD_UP = np.array([0, 0, 1]) 10 | WORLD_TO_MAYAVI_ROTATION_MAT = np.array([ 11 | [1, 0, 0], 12 | [0, 0, 1], 13 | [0, -1, 0]], dtype=np.float32) 14 | 15 | CAM_TO_MAYCAM_ROTATION_MAT = np.array([ 16 | [0, 1, 0], 17 | [1, 0, 0], 18 | [0, 0, -1]], dtype=np.float32) 19 | 20 | 21 | def rotation_mat(up): 22 | up = unit_vector(up) 23 | rightlike = np.array([1, 0, 0]) 24 | if np.allclose(up, rightlike): 25 | rightlike = np.array([0, 1, 0]) 26 | 27 | forward = unit_vector(np.cross(up, rightlike)) 28 | right = np.cross(forward, up) 29 | return np.row_stack([right, forward, up]) 30 | 31 | 32 | def unit_vector(vectors, axis=-1): 33 | norm = np.linalg.norm(vectors, axis=axis, keepdims=True) 34 | return vectors / norm 35 | 36 | 37 | def world_to_mayavi(points): 38 | points = np.asarray(points) 39 | if points.ndim == 1: 40 | return WORLD_TO_MAYAVI_ROTATION_MAT @ points * MM_TO_UNIT 41 | else: 42 | return points @ WORLD_TO_MAYAVI_ROTATION_MAT.T * MM_TO_UNIT 43 | 44 | 45 | def mayavi_to_world(points): 46 | points = np.asarray(points) 47 | if points.ndim == 1: 48 | return WORLD_TO_MAYAVI_ROTATION_MAT.T @ points * UNIT_TO_MM 49 | else: 50 | return points @ WORLD_TO_MAYAVI_ROTATION_MAT * UNIT_TO_MM 51 | 52 | 53 | def set_world_up(world_up): 54 | global WORLD_UP 55 | WORLD_UP = np.asarray(world_up) 56 | global WORLD_TO_MAYAVI_ROTATION_MAT 57 | WORLD_TO_MAYAVI_ROTATION_MAT = rotation_mat(WORLD_UP) 58 | 59 | 60 | def set_view_to_camera( 61 | camera, pivot=None, image_size=None, view_angle=None, allow_roll=True, 62 | camera_view_padding=0.2): 63 | if pivot is None: 64 | pivot = camera.t + camera.R[2] * 2500 65 | 66 | fig = mlab.gcf() 67 | mayavi_cam = fig.scene.camera 68 | if image_size is not None: 69 | half_height = 0.5 * image_size[1] 70 | offset = np.abs(camera.intrinsic_matrix[1, 2] - half_height) 71 | tan = (1 + camera_view_padding) * (half_height + offset) / camera.intrinsic_matrix[1, 1] 72 | mayavi_cam.view_angle = np.rad2deg(2 * np.arctan(tan)) 73 | elif view_angle is not None: 74 | mayavi_cam.view_angle = view_angle 75 | 76 | mayavi_cam.focal_point = world_to_mayavi(pivot) 77 | mayavi_cam.position = world_to_mayavi(camera.t) 78 | if allow_roll: 79 | mayavi_cam.view_up = -WORLD_TO_MAYAVI_ROTATION_MAT @ camera.R[1] 80 | mayavi_cam.compute_view_plane_normal() 81 | fig.scene.renderer.reset_camera_clipping_range() 82 | 83 | 84 | def get_current_view_as_camera(): 85 | azimuth, elevation, distance, focalpoint = mlab.view() 86 | azi = -azimuth - 90 87 | elev = -elevation + 90 88 | total_rotation_mat = transforms3d.euler.euler2mat(np.deg2rad(azi), np.deg2rad(elev), 0, 'szxy') 89 | R = WORLD_TO_MAYAVI_ROTATION_MAT.T @ total_rotation_mat @ WORLD_TO_MAYAVI_ROTATION_MAT 90 | 91 | fig = mlab.gcf() 92 | t = mayavi_to_world(fig.scene.camera.position) 93 | 94 | width, height = fig.scene.get_size() 95 | f = height / (np.tan(np.deg2rad(fig.scene.camera.view_angle) / 2) * 2) 96 | intrinsics = np.array( 97 | [[f, 0, (width - 1) / 2], 98 | [0, f, (height - 1) / 2], 99 | [0, 0, 1]], np.float32) 100 | return cameravision.Camera( 101 | intrinsic_matrix=intrinsics, rot_world_to_cam=R, optical_center=t, world_up=WORLD_UP) 102 | -------------------------------------------------------------------------------- /src/poseviz/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class UpdateScene: 6 | view_infos: any 7 | viz_camera: any 8 | viz_imshape: any 9 | 10 | 11 | @dataclass 12 | class AppendFrame: 13 | frame: any 14 | 15 | 16 | @dataclass 17 | class StartSequence: 18 | video_path: any =None 19 | fps: any = 30 20 | resolution: any = None 21 | camera_trajectory_path: any = None 22 | audio_source_path: any = None 23 | 24 | 25 | class EndSequence: 26 | pass 27 | 28 | 29 | class Pause: 30 | pass 31 | 32 | 33 | class Resume: 34 | pass 35 | 36 | 37 | class ReinitCameraView: 38 | pass 39 | 40 | 41 | class Quit: 42 | pass 43 | 44 | 45 | class Nothing: 46 | pass 47 | 48 | 49 | @dataclass 50 | class NewRingBuffers: 51 | ringbuffers: any 52 | -------------------------------------------------------------------------------- /src/poseviz/poseviz.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import ctypes 3 | import itertools 4 | import multiprocessing as mp 5 | import multiprocessing.shared_memory 6 | import multiprocessing.sharedctypes 7 | import os 8 | import queue 9 | import signal 10 | import threading 11 | from collections.abc import Sequence 12 | from contextlib import AbstractContextManager 13 | from typing import Optional, TYPE_CHECKING 14 | 15 | import cameravision 16 | import numpy as np 17 | import poseviz.colors 18 | import poseviz.draw2d 19 | import poseviz.view_info 20 | import simplepyutils as spu 21 | import framepump 22 | from poseviz import messages 23 | 24 | if TYPE_CHECKING: 25 | from poseviz import ViewInfo 26 | else: 27 | from poseviz.view_info import ViewInfo 28 | 29 | # We must not globally import mlab, because then it will be imported in the main process 30 | # and the new process will not be able to open a mayavi window. 31 | # This means, we also cannot import various modules like poseviz.components.main_viz 32 | # We import these in the methods of PoseVizMayaviSide. 33 | 34 | 35 | class PoseViz(AbstractContextManager): 36 | def __init__( 37 | self, 38 | joint_names=None, 39 | joint_edges=None, 40 | camera_type="free", 41 | n_views=1, 42 | world_up=(0, -1, 0), 43 | ground_plane_height=-1000, 44 | downscale=None, 45 | downscale_main=None, 46 | viz_fps=100, 47 | queue_size=64, 48 | draw_detections=True, 49 | multicolor_detections=False, 50 | snap_to_cam_on_scene_change=True, 51 | high_quality=True, 52 | draw_2d_pose=False, 53 | show_camera_wireframe=True, 54 | show_field_of_view=True, 55 | resolution=(1280, 720), 56 | use_virtual_display=False, 57 | show_virtual_display=True, 58 | show_ground_plane=True, 59 | paused=False, 60 | camera_view_padding=0.2, 61 | body_model_faces=None, 62 | show_image=True, 63 | out_video_path=None, 64 | out_fps=None, 65 | audio_path=None, 66 | max_pixels_per_frame=1920 * 1080, 67 | ): 68 | """The main class that creates a visualizer process, and provides an interface to update 69 | the visualization state. 70 | This class supports the Python **context manager** protocol to close the visualization at 71 | the end. 72 | 73 | Args: 74 | joint_names (iterable of strings): Names of the human body joints that will be 75 | visualized. Left joints must start with 'l', right joints with 'r', mid-body joints 76 | with something else. Currently only the first character is inspected in each joint 77 | name, to color the left, right and middle part of the stick figure differently. 78 | joint_edges (iterable of int pairs): joint index pairs, describing the 79 | bone-connectivity of the stick figure. 80 | camera_type (string): One of 'original', 'free' or 'bird' (experimental). 'original' 81 | forces the view-camera (i.e. from which we see the visualization) to stay or move 82 | where the displayed camera goes, typically so that we can see the scene through the 83 | recording camera from which the video was recorded. 'free' means that the 84 | view-camera is free to be moved around by the user. 'bird' ( experimental) tries to 85 | automatically move to a nice location to be able to see both the person and the 86 | recording camera from a third-person perspective. 87 | n_views (int): The number of cameras that will be displayed. 88 | world_up (3-vector): A 3-vector, the up vector in the world coordinate system in 89 | which the poses will be specified. 90 | ground_plane_height (float): The vertical position of the ground plane in the world 91 | coordinate system, along the up-vector. 92 | downscale (int): Image downscaling factor for display, to speed up the 93 | visualization. 94 | viz_fps (int): Target frames-per-second of the visualization. If the updates come 95 | faster than this, the visualizer will block and wait, to ensure that 96 | visualization does not 97 | happen faster than this FPS. Of course, if the speed of updates do not deliver this 98 | fps, the visualization will also be slower. 99 | queue_size (int): Size of the internal queue used to communicate with the 100 | visualizer process. 101 | draw_detections (bool): Whether to draw detection boxes on the images. 102 | multicolor_detections (bool): Whether to color each detection box with a different 103 | color. 104 | This is useful when tracking people with consistent person IDs, and has no effect 105 | if `draw_detections` is False. 106 | snap_to_cam_on_scene_change (bool): Whether to reinitialize the view camera to the 107 | original camera on each change of sequence (through a call to 108 | ```viz.reinit_camera_view()```). 109 | high_quality (bool): Whether to use high-resolution spheres and tubes for the 110 | skeletons (set to False for better speed). 111 | draw_2d_pose (bool): Whether to draw the 2D skeleton on the displayed camera image. 112 | show_camera_wireframe (bool): Whether to visualize each camera as a pyramid-like 113 | wireframe object. 114 | show_field_of_view (bool): Whether to visualize an extended pyramid shape indicating the 115 | field of view of the cameras. Recommended to turn off in multi-camera 116 | setups, as otherwise the visualization can get crowded. 117 | resolution ((int, int)): The resolution of the visualization window (width, 118 | height) pair. 119 | use_virtual_display (bool): Whether to use a virtual display for visualization. 120 | There may be two reasons to do this. First, to allow higher-resolution visualization 121 | than the screen resolution. Windows that are larger than the display screen can be 122 | difficult to create under certain GUI managers like GNOME. Second, this can be a 123 | way to do off-screen rendering. 124 | show_virtual_display (bool): Whether to show the virtual display or to hide it ( 125 | off-screen rendering). This has no effect if ```use_virtual_display``` is False. 126 | show_ground_plane (bool): Whether to visualize a checkerboard ground plane. 127 | paused (bool): Whether to start the visualization in paused state. 128 | camera_view_padding (float): When viewing the scence from a visualized camera position, 129 | it is often useful to also see beyond the edges of the video frame. The 130 | ```cameera_view_padding``` 131 | value adjusts what fraction of the frame size is applied as padding around it. 132 | Example with [```camera_view_padding=0```](/poseviz/images/padding_0.jpg) and [ 133 | ```camera_view_padding=0.2```](/poseviz/images/padding_0.2.jpg) 134 | out_video_path: Path where the output video should be generated. (It can also be started 135 | later with ```new_sequence_output```). If None, no video will be generated for now. 136 | out_fps: 'frames per second' setting of the output video to be generated. 137 | audio_path: Path to the audio source file, which may also be a video file whose audio 138 | will be copied over to the output video. 139 | """ 140 | 141 | n_threads_undist = 12 142 | queue_size_undist = min(max(16, n_views), 2 * n_views) 143 | queue_size_post = 2 144 | queue_size_waiter = queue_size 145 | self.q_messages_pre = queue.Queue(queue_size_waiter) 146 | if paused: 147 | self.pause() 148 | self.q_messages_post = mp.JoinableQueue(queue_size_post) 149 | 150 | self.undistort_pool = spu.ThrottledPool( 151 | n_threads_undist, use_threads=True, task_buffer_size=queue_size_undist 152 | ) 153 | 154 | self.ringbuffer_size = queue_size_undist + queue_size_waiter + queue_size_post + 1 155 | self.raw_arrays = [ 156 | multiprocessing.sharedctypes.RawArray( 157 | ctypes.c_uint8, self.ringbuffer_size * max_pixels_per_frame * 3 158 | ) 159 | for _ in range(n_views) 160 | ] 161 | self.ring_index = 0 162 | 163 | self.posedata_waiter_thread = threading.Thread( 164 | target=main_posedata_waiter, 165 | args=(self.q_messages_pre, self.q_messages_post), 166 | daemon=True, 167 | ) 168 | self.posedata_waiter_thread.start() 169 | 170 | self.downscale_main = downscale_main or downscale or (1 if high_quality else 2) 171 | self.downscale = downscale or 4 172 | self.snap_to_cam_on_scene_change = snap_to_cam_on_scene_change 173 | self.main_cam_value = mp.Value("i", 0) 174 | 175 | self.visualizer_process = mp.Process( 176 | target=_main_visualize, 177 | args=( 178 | self.q_messages_post, 179 | joint_names, 180 | joint_edges, 181 | camera_type, 182 | n_views, 183 | world_up, 184 | ground_plane_height, 185 | viz_fps, 186 | draw_detections, 187 | multicolor_detections, 188 | snap_to_cam_on_scene_change, 189 | high_quality, 190 | draw_2d_pose, 191 | show_camera_wireframe, 192 | show_field_of_view, 193 | resolution, 194 | use_virtual_display, 195 | show_virtual_display, 196 | show_ground_plane, 197 | self.main_cam_value, 198 | camera_view_padding, 199 | body_model_faces, 200 | show_image, 201 | self.raw_arrays, 202 | ), 203 | daemon=True, 204 | ) 205 | self.visualizer_process.start() 206 | 207 | if resolution[1] > 1080: 208 | input("Move the window to be partially outside the screen, then press Enter...") 209 | # Spent a lot of time trying to fix this, but it's very tricky and frustrating! 210 | # We have to manually drag the window partially off-screen, else it's impossible to 211 | # set the size 212 | # larger than the display resolution! If you try, it will just snap to the screen 213 | # borders (get maximized). 214 | # An alternative would be like https://unix.stackexchange.com/a/680848/291533, ie 215 | # we can take the window away from the window manager and then freely set its size 216 | # and position. 217 | # The downside is that we are no longer able to move the window with the mouse and it 218 | # is stuck in the foreground. 219 | # So here we rather go the manual route. 220 | # Further info: 221 | # https://www.reddit.com/r/kde/comments/mzza4d 222 | # /programmatically_moving_windows_off_the_screen/ 223 | # https://unix.stackexchange.com/questions/517396/is-it-possible-to-move-window-out 224 | # -of-the-screen-border#comment1396167_517396 225 | # https://github.com/jordansissel/xdotool/issues/186#issuecomment-470032261 226 | # Another option is some kind of virtual screen, off-screen rendering etc. 227 | # But then there are issues with hardware acceleration... It's quite complex. 228 | os.system( 229 | "/usr/bin/xdotool search --name ^PoseViz$" 230 | f" windowsize {resolution[0]} {resolution[1]}" 231 | ) 232 | 233 | if out_video_path is not None: 234 | self.new_sequence_output(out_video_path, out_fps, audio_path) 235 | 236 | def update( 237 | self, 238 | frame: np.ndarray, 239 | boxes=(), 240 | poses=(), 241 | camera: Optional[cameravision.Camera] = None, 242 | poses_true=(), 243 | poses_alt=(), 244 | vertices=(), 245 | vertices_true=(), 246 | vertices_alt=(), 247 | viz_camera=None, 248 | viz_imshape=None, 249 | block=True, 250 | uncerts=None, 251 | uncerts_alt=None, 252 | ): 253 | """Update the visualization for a new timestep, assuming a single-camera setup. 254 | 255 | Args: 256 | frame (uint8): RGB image frame [H, W, 3], the image to be displayed on the camera. 257 | boxes (seq of np.ndarray): bounding boxes of the persons 258 | poses (seq of np.ndarray): the world 259 | camera (poseviz.Camera): 260 | poses_true (seq of np.ndarray): 261 | poses_alt (seq of np.ndarray): 262 | block (bool): decides what to do if the buffer queue is full because the visualizer 263 | is slower than the update calls. If true, the thread will block (wait). If false, 264 | the current update call is ignored (frame dropping). 265 | """ 266 | if len(boxes) == 0: 267 | boxes = np.zeros((0, 4), np.float32) 268 | 269 | if uncerts is not None: 270 | vertices = [ 271 | np.concatenate([v, uncert[..., np.newaxis]], axis=-1) 272 | for v, uncert in zip(vertices, uncerts) 273 | ] 274 | if uncerts_alt is not None: 275 | vertices_alt = [ 276 | np.concatenate([v, uncert[..., np.newaxis]], axis=-1) 277 | for v, uncert in zip(vertices_alt, uncerts_alt) 278 | ] 279 | 280 | viewinfo = ViewInfo( 281 | frame, 282 | boxes, 283 | poses, 284 | camera, 285 | poses_true, 286 | poses_alt, 287 | vertices, 288 | vertices_true, 289 | vertices_alt, 290 | ) 291 | self.update_multiview([viewinfo], viz_camera, viz_imshape, block=block) 292 | 293 | def update_multiview( 294 | self, 295 | view_infos: Sequence[ViewInfo], 296 | viz_camera: Optional[cameravision.Camera] = None, 297 | viz_imshape=None, 298 | block: bool = True, 299 | ): 300 | """Update the visualization for a new timestep, with multi-view data. 301 | 302 | Args: 303 | view_infos: The view information for each view. 304 | viz_camera: The camera to be used for the visualization. 305 | viz_imshape: The shape of the image of the visualization camera. 306 | block: decides what to do if the buffer queue is full because the visualizer 307 | is slower than the update calls. If true, the thread will block (wait). If false, 308 | the current update call is ignored (frame dropping). 309 | """ 310 | 311 | for i in range(len(view_infos)): 312 | if len(view_infos[i].boxes) == 0: 313 | view_infos[i].boxes = np.zeros((0, 4), np.float32) 314 | 315 | d = self.downscale_main if i == self.main_cam_value.value else self.downscale 316 | if view_infos[i].frame is not None: 317 | new_imshape = poseviz.draw2d.rounded_int_tuple( 318 | np.array(view_infos[i].frame.shape[:2], np.float32) / d 319 | ) 320 | dst = np_from_raw_array( 321 | self.raw_arrays[i], 322 | (new_imshape[0], new_imshape[1], 3), 323 | self.ring_index, 324 | np.uint8, 325 | ) 326 | view_infos[i] = self.undistort_pool.apply_async( 327 | poseviz.view_info.downscale_and_undistort_view_info, 328 | (view_infos[i], dst, self.ring_index), 329 | ) 330 | else: 331 | view_infos[i] = self.undistort_pool.apply_async(lambda x: x, (view_infos[i],)) 332 | self.ring_index = (self.ring_index + 1) % self.ringbuffer_size 333 | 334 | try: 335 | self.q_messages_pre.put( 336 | messages.UpdateScene( 337 | view_infos=view_infos, viz_camera=viz_camera, viz_imshape=viz_imshape 338 | ), 339 | block=block, 340 | ) 341 | except queue.Full: 342 | pass 343 | 344 | def reinit_camera_view(self): 345 | """Waits until the current sequence has finished visualizing, then notifies PoseViz that 346 | a new sequence is now being visualized, so the view camera may need to be 347 | reinitialized. 348 | """ 349 | self.q_messages_pre.put(messages.ReinitCameraView()) 350 | 351 | def pause(self): 352 | self.q_messages_pre.put(messages.Pause()) 353 | 354 | def resume(self): 355 | self.q_messages_pre.put(messages.Resume()) 356 | 357 | def new_sequence_output( 358 | self, 359 | new_video_path: Optional[str] = None, 360 | fps: Optional[int] = None, 361 | new_camera_trajectory_path: Optional[str] = None, 362 | audio_source_path: Optional[str] = None, 363 | ): 364 | """Waits until the buffered information is visualized and then starts a new output video 365 | for the upcoming timesteps. 366 | 367 | Args: 368 | new_video_path: Path where the new video should be generated. 369 | fps: 'frames per second' setting of the new video to be generated 370 | new_camera_trajectory_path: Path where the camera trajectory should be saved. 371 | """ 372 | if new_video_path is not None or new_camera_trajectory_path is not None: 373 | self.q_messages_pre.put( 374 | messages.StartSequence( 375 | video_path=new_video_path, 376 | fps=fps, 377 | camera_trajectory_path=new_camera_trajectory_path, 378 | audio_source_path=audio_source_path, 379 | ) 380 | ) 381 | 382 | def finalize_sequence_output(self): 383 | """Notifies PoseViz that the current sequence is finished and the output video should be 384 | finalized.""" 385 | self.q_messages_pre.put(messages.EndSequence()) 386 | 387 | def close(self): 388 | """Closes the visualization process and window.""" 389 | self.undistort_pool.finish() 390 | self.q_messages_pre.put(messages.Quit()) 391 | self._join() 392 | self.posedata_waiter_thread.join() 393 | self.visualizer_process.join() 394 | 395 | def _join(self): 396 | self.undistort_pool.join() 397 | self.q_messages_pre.join() 398 | self.q_messages_post.join() 399 | 400 | def __exit__(self, exc_type, exc_val, exc_tb): 401 | """Context manager protocol.""" 402 | self.close() 403 | 404 | 405 | def np_from_raw_array(raw_array, elem_shape, index, dtype): 406 | return np.frombuffer( 407 | buffer=raw_array, 408 | dtype=dtype, 409 | count=np.prod(elem_shape), 410 | offset=index * np.prod(elem_shape) * np.dtype(dtype).itemsize, 411 | ).reshape(elem_shape) 412 | 413 | 414 | class PoseVizMayaviSide: 415 | def __init__( 416 | self, 417 | q_messages, 418 | joint_names, 419 | joint_edges, 420 | camera_type, 421 | n_views, 422 | world_up, 423 | ground_plane_height, 424 | fps, 425 | draw_detections, 426 | multicolor_detections, 427 | snap_to_cam_on_scene_change, 428 | high_quality, 429 | draw_2d_pose, 430 | show_camera_wireframe, 431 | show_field_of_view, 432 | resolution, 433 | use_virtual_display, 434 | show_virtual_display, 435 | show_ground_plane, 436 | main_cam_value, 437 | camera_view_padding, 438 | body_model_faces, 439 | show_image, 440 | raw_arrays, 441 | ): 442 | self.q_messages = q_messages 443 | self.video_writer = framepump.VideoWriter() 444 | 445 | self.camera_type = camera_type 446 | self.joint_info = (joint_names, joint_edges) 447 | self.initialized_camera = False 448 | self.n_views = n_views 449 | self.world_up = world_up 450 | self.draw_detections = draw_detections 451 | self.multicolor_detections = multicolor_detections 452 | self.main_cam = 0 453 | self.pose_displayed_cam_id = None 454 | self.fps = fps 455 | self.snap_to_cam_on_scene_change = snap_to_cam_on_scene_change 456 | self.ground_plane_height = ground_plane_height 457 | self.show_ground_plane = show_ground_plane 458 | self.step_one_by_one = False 459 | self.paused = False 460 | self.current_viewinfos = None 461 | self.high_quality = high_quality 462 | self.draw_2d_pose = draw_2d_pose 463 | self.show_camera_wireframe = show_camera_wireframe 464 | self.show_field_of_view = show_field_of_view 465 | self.resolution = resolution 466 | self.use_virtual_display = use_virtual_display 467 | self.show_virtual_display = show_virtual_display 468 | self.main_cam_value = main_cam_value 469 | self.camera_view_padding = camera_view_padding 470 | self.body_model_faces = body_model_faces 471 | self.i_pred_frame = 0 472 | self.camera_trajectory = [] 473 | self.camera_trajectory_path = None 474 | self.show_image = show_image 475 | self.raw_arrays = raw_arrays 476 | 477 | def run_loop(self): 478 | if self.use_virtual_display: 479 | import pyvirtualdisplay 480 | 481 | display = pyvirtualdisplay.Display( 482 | visible=self.show_virtual_display, size=self.resolution 483 | ) 484 | else: 485 | display = contextlib.nullcontext() 486 | 487 | with display: 488 | # Imports are here so Mayavi is loaded in the visualizer process, 489 | # and potentially under the virtual display 490 | from mayavi import mlab 491 | import poseviz.init 492 | import poseviz.components.main_viz 493 | import poseviz.components.ground_plane_viz 494 | import poseviz.mayavi_util 495 | 496 | poseviz.mayavi_util.set_world_up(self.world_up) 497 | fig = poseviz.init.initialize(self.resolution) 498 | fig.scene.interactor.add_observer("KeyPressEvent", self._on_keypress) 499 | if self.show_ground_plane: 500 | poseviz.components.ground_plane_viz.draw_checkerboard( 501 | ground_plane_height=self.ground_plane_height 502 | ) 503 | self.view_visualizers = [ 504 | poseviz.components.main_viz.MainViz( 505 | self.joint_info, 506 | self.joint_info, 507 | self.joint_info, 508 | self.camera_type, 509 | show_image=self.show_image, 510 | high_quality=self.high_quality, 511 | show_field_of_view=self.show_field_of_view, 512 | show_camera_wireframe=self.show_camera_wireframe, 513 | body_model_faces=self.body_model_faces, 514 | ) 515 | for _ in range(self.n_views) 516 | ] 517 | delay = max(10, int(round(1000 / self.fps))) 518 | 519 | # Need to store it in a variable to make sure it's not 520 | # destructed by the garbage collector! 521 | _ = mlab.animate(delay=delay, ui=False)(self.animate)(fig) 522 | mlab.show() 523 | 524 | def animate(self, fig): 525 | # The main animation loop of the visualizer 526 | # The infinite loop pops an incoming command or frame information from the queue 527 | # and processes it. If there's no incoming information, we just render the scene and 528 | # continue to iterate. 529 | from mayavi import mlab 530 | 531 | while True: 532 | # Get a new message (command) from the multiprocessing queue 533 | msg = self.receive_message() 534 | if isinstance(msg, messages.Nothing): 535 | if self.current_viewinfos is not None: 536 | self.update_view_camera() 537 | fig.scene.render() 538 | if self.paused: 539 | self.capture_frame() 540 | yield 541 | elif isinstance(msg, messages.UpdateScene): 542 | self.update_visu(msg.view_infos) 543 | self.update_view_camera(msg.viz_camera, msg.viz_imshape) 544 | self.capture_frame() 545 | 546 | if self.step_one_by_one: 547 | self.step_one_by_one = False 548 | self.paused = True 549 | 550 | self.i_pred_frame += 1 551 | self.q_messages.task_done() 552 | yield 553 | elif isinstance(msg, messages.ReinitCameraView): 554 | if self.snap_to_cam_on_scene_change: 555 | self.initialized_camera = False 556 | self.current_viewinfos = None 557 | self.q_messages.task_done() 558 | elif isinstance(msg, messages.StartSequence): 559 | if msg.video_path is not None: 560 | self.video_writer.start_sequence( 561 | video_path=msg.video_path, 562 | fps=msg.fps, 563 | audio_source_path=msg.audio_source_path, 564 | ) 565 | self.camera_trajectory_path = msg.camera_trajectory_path 566 | self.i_pred_frame = 0 567 | 568 | # os.system('/usr/bin/xdotool search --name ^PoseViz$' 569 | # f' windowsize --sync {msg.resolution[0]} {msg.resolution[1]}') 570 | self.q_messages.task_done() 571 | # yield 572 | elif isinstance(msg, messages.Pause): 573 | self.paused = True 574 | self.q_messages.task_done() 575 | elif isinstance(msg, messages.Resume): 576 | self.paused = False 577 | self.q_messages.task_done() 578 | elif isinstance(msg, messages.EndSequence): 579 | if self.camera_trajectory_path is not None: 580 | spu.dump_pickle(self.camera_trajectory, self.camera_trajectory_path) 581 | self.video_writer.end_sequence() 582 | self.q_messages.task_done() 583 | elif isinstance(msg, messages.Quit): 584 | if self.camera_trajectory_path is not None: 585 | spu.dump_pickle(self.camera_trajectory, self.camera_trajectory_path) 586 | mlab.close(all=True) 587 | self.video_writer.close() 588 | self.q_messages.task_done() 589 | return 590 | else: 591 | raise ValueError("Unknown message:", msg) 592 | 593 | def receive_message(self): 594 | """Gets a new command or frame visualization data from the multiprocessing queue. 595 | Returns messages.Nothing if the queue is empty or the visualization is paused.""" 596 | if self.paused: 597 | return messages.Nothing() 598 | 599 | try: 600 | return self.q_messages.get_nowait() 601 | except queue.Empty: 602 | return messages.Nothing() 603 | 604 | def update_visu(self, view_infos): 605 | self.update_num_views(len(view_infos)) # Update the number of views if necessary 606 | 607 | for i, v in enumerate(view_infos): 608 | if v.frame is not None: 609 | shape, dtype, index = v.frame 610 | v.frame = np_from_raw_array(self.raw_arrays[i], shape, index, dtype) 611 | 612 | pose_displayed_view_infos = ( 613 | view_infos 614 | if self.pose_displayed_cam_id is None 615 | else [view_infos[self.pose_displayed_cam_id]] 616 | ) 617 | 618 | all_poses = [p for v in pose_displayed_view_infos for p in v.poses] 619 | all_poses_true = [p for v in pose_displayed_view_infos for p in v.poses_true] 620 | all_poses_alt = [p for v in pose_displayed_view_infos for p in v.poses_alt] 621 | all_vertices = [p for v in pose_displayed_view_infos for p in v.vertices] 622 | all_vertices_true = [p for v in pose_displayed_view_infos for p in v.vertices_true] 623 | all_vertices_alt = [p for v in pose_displayed_view_infos for p in v.vertices_alt] 624 | 625 | self.current_viewinfos = view_infos 626 | 627 | if self.multicolor_detections: 628 | box_colors = poseviz.colors.cycle_over_colors(False) 629 | else: 630 | box_colors = itertools.repeat((31, 119, 180)) 631 | joint_names, joint_edges = self.joint_info 632 | for i_viz, (view_info, viz) in enumerate(zip(view_infos, self.view_visualizers)): 633 | poses = all_poses if viz is self.view_visualizers[0] else None 634 | poses_true = all_poses_true if viz is self.view_visualizers[0] else None 635 | poses_alt = all_poses_alt if viz is self.view_visualizers[0] else None 636 | vertices = all_vertices if viz is self.view_visualizers[0] else None 637 | vertices_true = all_vertices_true if viz is self.view_visualizers[0] else None 638 | vertices_alt = all_vertices_alt if viz is self.view_visualizers[0] else None 639 | 640 | max_size = np.max(view_info.frame.shape[:2]) 641 | if max_size < 512: 642 | thickness = 1 643 | elif max_size < 1024: 644 | thickness = 2 645 | else: 646 | thickness = 3 647 | 648 | if self.draw_detections: 649 | for color, box in zip(box_colors, view_info.boxes): 650 | poseviz.draw2d.draw_box(view_info.frame, box, color, thickness=thickness) 651 | 652 | if self.draw_2d_pose: 653 | pose_groups = [view_info.poses, view_info.poses_true, view_info.poses_alt] 654 | colors = [poseviz.colors.green, poseviz.colors.red, poseviz.colors.orange] 655 | for poses, color in zip(pose_groups, colors): 656 | for pose in poses: 657 | pose2d = view_info.camera.world_to_image(pose) 658 | poseviz.draw2d.draw_stick_figure_2d_inplace( 659 | view_info.frame, pose2d, joint_edges, thickness, color=color 660 | ) 661 | 662 | viz.update( 663 | view_info.camera, 664 | view_info.frame, 665 | poses, 666 | poses_true, 667 | poses_alt, 668 | vertices, 669 | vertices_true, 670 | vertices_alt, 671 | highlight=self.pose_displayed_cam_id == i_viz, 672 | ) 673 | 674 | def update_num_views(self, new_n_views): 675 | if new_n_views > self.n_views: 676 | self.view_visualizers += [ 677 | poseviz.components.main_viz.MainViz( 678 | self.joint_info, 679 | self.joint_info, 680 | self.joint_info, 681 | self.camera_type, 682 | show_image=True, 683 | high_quality=self.high_quality, 684 | show_field_of_view=self.show_field_of_view, 685 | show_camera_wireframe=self.show_camera_wireframe, 686 | body_model_faces=self.body_model_faces, 687 | ) 688 | for _ in range(new_n_views - self.n_views) 689 | ] 690 | self.n_views = new_n_views 691 | elif new_n_views < self.n_views: 692 | for viz in self.view_visualizers[new_n_views:]: 693 | viz.remove() 694 | del self.view_visualizers[new_n_views:] 695 | self.n_views = new_n_views 696 | 697 | def update_view_camera(self, camera=None, imshape=None): 698 | import poseviz.mayavi_util 699 | 700 | main_view_info = self.current_viewinfos[self.main_cam] 701 | 702 | if camera is not None: 703 | if imshape is None: 704 | imshape = main_view_info.frame.shape 705 | poseviz.mayavi_util.set_view_to_camera( 706 | camera, image_size=(imshape[1], imshape[0]), allow_roll=True, camera_view_padding=0 707 | ) 708 | elif self.camera_type == "original" or ( 709 | self.camera_type == "free" and not self.initialized_camera 710 | ): 711 | poseviz.mayavi_util.set_view_to_camera( 712 | main_view_info.camera, 713 | image_size=(main_view_info.frame.shape[1], main_view_info.frame.shape[0]), 714 | allow_roll=False, 715 | camera_view_padding=self.camera_view_padding, 716 | ) 717 | elif self.camera_type == "bird" and not self.initialized_camera: 718 | pivot = main_view_info.camera.t + main_view_info.camera.R[2] * 2000 719 | camera_view = main_view_info.camera.copy() 720 | camera_view.t = (camera_view.t - pivot) * 1.35 + pivot 721 | camera_view.orbit_around(pivot, np.deg2rad(-25), "vertical") 722 | camera_view.orbit_around(pivot, np.deg2rad(-15), "horizontal") 723 | poseviz.mayavi_util.set_view_to_camera( 724 | camera_view, 725 | pivot=pivot, 726 | view_angle=55, 727 | allow_roll=False, 728 | camera_view_padding=self.camera_view_padding, 729 | ) 730 | 731 | self.initialized_camera = True 732 | 733 | def capture_frame(self): 734 | from mayavi import mlab 735 | 736 | if self.camera_trajectory_path is not None: 737 | from poseviz.mayavi_util import get_current_view_as_camera 738 | 739 | viz_cam = get_current_view_as_camera() 740 | self.camera_trajectory.append((self.i_pred_frame, viz_cam)) 741 | 742 | if self.video_writer.is_active(): 743 | out_frame = mlab.screenshot(antialiased=False) 744 | # fig = mlab.gcf() 745 | # fig.scene.disable_render = True 746 | # fig.scene.off_screen_rendering = True 747 | # mlab.savefig(self.temp_image_path, size=self.resolution) 748 | # fig.scene.off_screen_rendering = False 749 | # fig.scene.disable_render = False 750 | # out_frame = imageio.imread(self.temp_image_path) 751 | # factor = 1 / 3 752 | # if self.camera_type != 'original' and False: 753 | # image = main_view_info.frame 754 | # image = np.clip(image.astype(np.float32) + 50, 0, 255).astype(np.uint8) 755 | # for pred in main_view_info.poses: 756 | # image = poseviz.draw2d.draw_stick_figure_2d( 757 | # image, main_view_info.camera.world_to_image(pred), self.joint_info, 3) 758 | # out_frame[:illust.shape[0], :illust.shape[1]] = illust 759 | self.video_writer.append_data(out_frame) 760 | 761 | def _on_keypress(self, obj, ev): 762 | key = obj.GetKeySym() 763 | if key == "x": 764 | # Pause 765 | self.paused = not self.paused 766 | elif key == "t": 767 | # Toggle view poses predicted from all views or just one 768 | if self.pose_displayed_cam_id is None: 769 | self.pose_displayed_cam_id = self.main_cam 770 | else: 771 | self.pose_displayed_cam_id = None 772 | elif key == "c": 773 | # Frame by frame stepping on key press 774 | self.step_one_by_one = True 775 | self.paused = False 776 | elif key == "n": 777 | # Cycle the view camera 778 | self.main_cam = (self.main_cam + 1) % self.n_views 779 | self.main_cam_value.value = self.main_cam 780 | self.initialized_camera = False 781 | elif key == "m": 782 | # Cycle which camera's predicted pose is displayed 783 | if self.pose_displayed_cam_id is None: 784 | self.pose_displayed_cam_id = self.main_cam 785 | else: 786 | self.pose_displayed_cam_id = (self.pose_displayed_cam_id + 1) % self.n_views 787 | elif key == "o": 788 | # Cycle both the view camera and which camera's predicted pose is displayed 789 | self.main_cam = (self.main_cam + 1) % self.n_views 790 | self.main_cam_value.value = self.main_cam 791 | self.pose_displayed_cam_id = self.main_cam 792 | self.initialized_camera = False 793 | elif key in ("d", "g"): 794 | # Snap to nearest camera 795 | import poseviz.mayavi_util 796 | 797 | display_cameras = [vi.camera for vi in self.current_viewinfos] 798 | viewing_camera = poseviz.mayavi_util.get_current_view_as_camera() 799 | self.main_cam = np.argmin( 800 | [np.linalg.norm(viewing_camera.t - c.t) for c in display_cameras] 801 | ) 802 | self.main_cam_value.value = self.main_cam 803 | self.initialized_camera = False 804 | if key == "g": 805 | # Also display the predicted pose from this nearest camera 806 | self.pose_displayed_cam_id = self.main_cam 807 | elif key == "u": 808 | # Show just the main cam pred 809 | self.pose_displayed_cam_id = self.main_cam 810 | elif key == "z": 811 | # Show just the main cam pred 812 | self.camera_type = "original" if self.camera_type != "original" else "free" 813 | self.initialized_camera = False 814 | else: 815 | try: 816 | self.main_cam = max(0, min(int(key) - 1, self.n_views - 1)) 817 | self.main_cam_value.value = self.main_cam 818 | self.initialized_camera = False 819 | except ValueError: 820 | pass 821 | 822 | 823 | def _main_visualize(*args, **kwargs): 824 | _terminate_on_parent_death() 825 | remove_qt_paths_added_by_opencv() 826 | monkey_patched_numpy_types() 827 | viz = PoseVizMayaviSide(*args, **kwargs) 828 | viz.run_loop() 829 | 830 | 831 | def _terminate_on_parent_death(): 832 | # Make sure the visualizer process is terminated when the parent process dies, for whatever reason 833 | prctl = ctypes.CDLL("libc.so.6").prctl 834 | PR_SET_PDEATHSIG = 1 835 | prctl(PR_SET_PDEATHSIG, signal.SIGTERM) 836 | 837 | # ignore sigterm and sigint 838 | # without this, a single CTRL+C leaves the process hanging 839 | #signal.signal(signal.SIGTERM, signal.SIG_IGN) 840 | signal.signal(signal.SIGINT, signal.SIG_IGN) 841 | 842 | 843 | def monkey_patched_numpy_types(): 844 | """VTK tries to import np.bool etc which are not available anymore.""" 845 | for name in ["bool", "int", "float", "complex", "object", "str"]: 846 | if name not in dir(np): 847 | setattr(np, name, getattr(np, name + "_")) 848 | 849 | 850 | def remove_qt_paths_added_by_opencv(): 851 | """Remove Qt paths added by OpenCV, which may cause conflicts with Qt as used by Mayavi.""" 852 | # See also https://forum.qt.io/post/654289 853 | import sys 854 | 855 | # noinspection PyUnresolvedReferences 856 | import cv2 857 | 858 | try: 859 | from cv2.version import ci_build, headless 860 | 861 | ci_and_not_headless = ci_build and not headless 862 | except: 863 | ci_and_not_headless = False 864 | 865 | if sys.platform.startswith("linux") and ci_and_not_headless: 866 | os.environ.pop("QT_QPA_PLATFORM_PLUGIN_PATH") 867 | os.environ.pop("QT_QPA_FONTDIR") 868 | 869 | 870 | def main_posedata_waiter(q_posedata_pre, q_posedata_post): 871 | while True: 872 | msg = q_posedata_pre.get() 873 | if isinstance(msg, messages.UpdateScene): 874 | msg.view_infos = [v.get() for v in msg.view_infos] 875 | 876 | q_posedata_post.put(msg) 877 | q_posedata_pre.task_done() 878 | if isinstance(msg, messages.Quit): 879 | return 880 | -------------------------------------------------------------------------------- /src/poseviz/video_writing.py: -------------------------------------------------------------------------------- 1 | # import os 2 | # import os.path as osp 3 | # import queue 4 | # import threading 5 | # 6 | # import imageio.v2 as imageio 7 | # from poseviz import messages 8 | # 9 | # 10 | # def main_video_writer(q): 11 | # writer = None 12 | # while True: 13 | # msg = q.get() 14 | # 15 | # if isinstance(msg, messages.AppendFrame) and writer is not None: 16 | # writer.append_data(msg.frame) 17 | # elif isinstance(msg, messages.StartSequence): 18 | # if writer is not None: 19 | # writer.close() 20 | # 21 | # os.makedirs(osp.dirname(msg.video_path), exist_ok=True) 22 | # writer = imageio.get_writer( 23 | # msg.video_path, 24 | # codec="h264", 25 | # input_params=["-r", str(msg.fps), '-thread_queue_size', '4096'], 26 | # output_params=["-crf", "15"], 27 | # audio_path=msg.audio_source_path, 28 | # audio_codec='copy' if msg.audio_source_path is not None else None, 29 | # macro_block_size=None, 30 | # ) 31 | # elif isinstance(msg, (messages.EndSequence, messages.Quit)): 32 | # if writer is not None: 33 | # writer.close() 34 | # writer = None 35 | # 36 | # q.task_done() 37 | # 38 | # if isinstance(msg, messages.Quit): 39 | # return 40 | # 41 | # 42 | # class VideoWriter: 43 | # def __init__(self, queue_size=32): 44 | # self.q = queue.Queue(queue_size) 45 | # self.thread = threading.Thread(target=main_video_writer, args=(self.q,), daemon=True) 46 | # self.thread.start() 47 | # self.active = False 48 | # 49 | # def start_sequence(self, msg: messages.StartSequence): 50 | # self.q.put(msg) 51 | # self.active = True 52 | # 53 | # def is_active(self): 54 | # return self.active 55 | # 56 | # def append_data(self, frame): 57 | # self.q.put(messages.AppendFrame(frame)) 58 | # 59 | # def end_sequence(self): 60 | # self.q.put(messages.EndSequence()) 61 | # self.active = False 62 | # 63 | # def finish(self): 64 | # self.q.put(messages.Quit()) 65 | # self.q.join() 66 | # self.thread.join() 67 | # self.active = False 68 | -------------------------------------------------------------------------------- /src/poseviz/view_info.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | 3 | import cameravision 4 | import numpy as np 5 | import poseviz.draw2d 6 | 7 | 8 | @dataclasses.dataclass 9 | class ViewInfo: 10 | """A container for storing information about a single view of a scene""" 11 | 12 | frame: any = None 13 | boxes: any = () 14 | poses: any = () 15 | camera: cameravision.Camera = None 16 | poses_true: any = () 17 | poses_alt: any = () 18 | vertices: any = () 19 | vertices_true: any = () 20 | vertices_alt: any = () 21 | 22 | 23 | def downscale_and_undistort_view_info(view_info, dst, index): 24 | scale_factor = np.array(dst.shape[:2], np.float32) / np.array( 25 | view_info.frame.shape[:2], np.float32 26 | ) 27 | 28 | if view_info.frame.dtype == np.uint16: 29 | view_info.frame = np.ascontiguousarray(view_info.frame.view(np.uint8)[..., ::2]) 30 | 31 | if np.any(scale_factor != 1): 32 | resize_dst = np.empty_like(dst) if view_info.camera.has_distortion() else dst 33 | 34 | view_info.frame = poseviz.draw2d.resize(view_info.frame, dst=resize_dst) 35 | if view_info.boxes is not None: 36 | view_info.boxes = np.asarray(view_info.boxes) 37 | view_info.boxes[:, :2] *= scale_factor 38 | view_info.boxes[:, 2:4] *= scale_factor 39 | view_info.camera = view_info.camera.scale_output(scale_factor, inplace=False) 40 | 41 | if view_info.camera.has_distortion(): 42 | old_camera = view_info.camera 43 | view_info.camera = old_camera.undistort( 44 | alpha_balance=0.8, imshape=dst.shape, inplace=False 45 | ) 46 | 47 | cameravision.reproject_image( 48 | view_info.frame, 49 | old_camera, 50 | view_info.camera, 51 | dst.shape, 52 | precomp_undist_maps=False, 53 | cache_maps=True, 54 | dst=dst, 55 | ) 56 | 57 | if view_info.boxes is not None: 58 | for i in range(len(view_info.boxes)): 59 | view_info.boxes[i, :4] = cameravision.reproject_box( 60 | view_info.boxes[i, :4], old_camera, view_info.camera 61 | ) 62 | elif view_info.frame is not dst: 63 | np.copyto(dst, view_info.frame) 64 | 65 | view_info.frame = (dst.shape, dst.dtype, index) 66 | return view_info 67 | --------------------------------------------------------------------------------