├── docs
├── images
├── authors.rst
├── history.rst
├── readme.rst
├── contributing.rst
├── usage.rst
├── modules.rst
├── index.rst
├── Makefile
├── make.bat
├── installation.rst
├── imgui_datascience.rst
└── conf.py
├── run_example.py
├── images
├── yt.png
├── mplot.jpg
├── thumb.jpg
├── image_lister.png
└── image_explorer.jpg
├── imgui_datascience
├── imgui_datascience.py
├── images
│ ├── owl.jpg
│ ├── billiard.jpg
│ └── flower.jpg
├── source-sans-pro.regular.ttf
├── __init__.py
├── __main__.py
├── static_vars.py
├── imgui_fig.py
├── imgui_runner.py
├── imgui_image_lister.py
├── imgui_ext.py
├── example.py
├── imgui_cv.py
└── _imgui_cv_zoom.py
├── HISTORY.rst
├── requirements.txt
├── requirements_dev.txt
├── .idea
└── vcs.xml
├── AUTHORS.rst
├── MANIFEST.in
├── .editorconfig
├── .github
└── ISSUE_TEMPLATE.md
├── setup.cfg
├── LICENSE
├── tox.ini
├── tests
└── test_imgui_datascience.py
├── .travis.yml
├── .gitignore
├── Makefile
├── setup.py
├── CONTRIBUTING.rst
└── README.rst
/docs/images:
--------------------------------------------------------------------------------
1 | ../images
--------------------------------------------------------------------------------
/docs/authors.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../AUTHORS.rst
2 |
--------------------------------------------------------------------------------
/docs/history.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../HISTORY.rst
2 |
--------------------------------------------------------------------------------
/docs/readme.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../CONTRIBUTING.rst
2 |
--------------------------------------------------------------------------------
/run_example.py:
--------------------------------------------------------------------------------
1 | from imgui_datascience.example import example
2 | example()
3 |
--------------------------------------------------------------------------------
/docs/usage.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Usage
3 | =====
4 |
5 |
6 | import imgui_datascience
7 |
--------------------------------------------------------------------------------
/images/yt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/images/yt.png
--------------------------------------------------------------------------------
/images/mplot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/images/mplot.jpg
--------------------------------------------------------------------------------
/images/thumb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/images/thumb.jpg
--------------------------------------------------------------------------------
/imgui_datascience/imgui_datascience.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """Main module."""
4 |
--------------------------------------------------------------------------------
/images/image_lister.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/images/image_lister.png
--------------------------------------------------------------------------------
/images/image_explorer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/images/image_explorer.jpg
--------------------------------------------------------------------------------
/imgui_datascience/images/owl.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/imgui_datascience/images/owl.jpg
--------------------------------------------------------------------------------
/HISTORY.rst:
--------------------------------------------------------------------------------
1 | =======
2 | History
3 | =======
4 |
5 | 0.1.0 (2018-04-22)
6 | ------------------
7 |
8 | * First release on PyPI.
9 |
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | imgui_datascience
2 | =================
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | imgui_datascience
8 |
--------------------------------------------------------------------------------
/imgui_datascience/images/billiard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/imgui_datascience/images/billiard.jpg
--------------------------------------------------------------------------------
/imgui_datascience/images/flower.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/imgui_datascience/images/flower.jpg
--------------------------------------------------------------------------------
/imgui_datascience/source-sans-pro.regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pthom/imgui_datascience/HEAD/imgui_datascience/source-sans-pro.regular.ttf
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib
2 | opencv-python
3 | git+https://github.com/pyimgui/pyimgui.git@dev/version-2.0
4 | enum34
5 | xxhash
6 | pyopengl
7 | pygame
8 |
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | -r requirements.txt
2 | bumpversion
3 | wheel
4 | watchdog
5 | flake8
6 | tox
7 | coverage
8 | Sphinx
9 | twine
10 | pytest
11 | pytest-runner
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/AUTHORS.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Credits
3 | =======
4 |
5 | Development Lead
6 | ----------------
7 |
8 | * Pascal Thomet
9 |
10 | Contributors
11 | ------------
12 |
13 | None yet. Why not be the first?
14 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS.rst
2 | include CONTRIBUTING.rst
3 | include HISTORY.rst
4 | include LICENSE
5 | include README.rst
6 |
7 | recursive-include tests *
8 | recursive-exclude * __pycache__
9 | recursive-exclude * *.py[co]
10 |
11 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif *.ttf
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.bat]
14 | indent_style = tab
15 | end_of_line = crlf
16 |
17 | [LICENSE]
18 | insert_final_newline = false
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
3 |
4 | Contents
5 | ========
6 |
7 | .. toctree::
8 | :maxdepth: 2
9 | :caption: Contents:
10 |
11 | readme
12 | installation
13 | usage
14 | modules
15 | contributing
16 | authors
17 | history
18 |
19 | Indices and tables
20 | ==================
21 | * :ref:`genindex`
22 | * :ref:`modindex`
23 | * :ref:`search`
24 |
25 |
--------------------------------------------------------------------------------
/imgui_datascience/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import imgui
3 | from . import imgui_ext
4 | from . import imgui_cv
5 | from . import imgui_fig
6 | from .imgui_image_lister import ImGuiImageLister
7 | from . import imgui_runner
8 | from .imgui_runner import ImGuiLister_ShowStandalone
9 |
10 | __author__ = """Pascal Thomet"""
11 | __email__ = 'pthomet@gmail.com'
12 | __version__ = '0.3.1'
13 |
14 | from .static_vars import *
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | * ImGui [B[D[D[D[D[D[D[D[D[D[D[C[C[C[C(Py)[ImGui for Data Science version:
2 | * Python version:
3 | * Operating System:
4 |
5 | ### Description
6 |
7 | Describe what you were trying to get done.
8 | Tell us what happened, what went wrong, and what you expected to happen.
9 |
10 | ### What I Did
11 |
12 | ```
13 | Paste the command(s) you ran and the output.
14 | If there was a crash, please include the traceback here.
15 | ```
16 |
--------------------------------------------------------------------------------
/imgui_datascience/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from . import example
3 |
4 | def main(args=None):
5 | if args is None:
6 | args = sys.argv[1:]
7 | print("(py)imgui for datascience (https://github.com/pthom/imgui_datascience)")
8 | print("Run the example with the following command:")
9 | print("python -m imgui_datascience --example")
10 | if "--example" in args:
11 | example.example()
12 |
13 | if __name__ == "__main__":
14 | main()
15 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 0.3.1
3 | commit = True
4 | tag = True
5 |
6 | [bumpversion:file:setup.py]
7 | search = version='{current_version}'
8 | replace = version='{new_version}'
9 |
10 | [bumpversion:file:imgui_datascience/__init__.py]
11 | search = __version__ = '{current_version}'
12 | replace = __version__ = '{new_version}'
13 |
14 | [bdist_wheel]
15 | universal = 1
16 |
17 | [flake8]
18 | exclude = docs
19 |
20 | [aliases]
21 | test = pytest
22 |
23 | [tool:pytest]
24 | collect_ignore = ['setup.py']
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache Software License 2.0
2 |
3 | Copyright (c) 2018, Pascal Thomet
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 |
17 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27, py34, py35, py36, flake8
3 |
4 | [travis]
5 | python =
6 | 3.6: py36
7 | 3.5: py35
8 | 3.4: py34
9 | 2.7: py27
10 |
11 | [testenv:flake8]
12 | basepython = python
13 | deps = flake8
14 | commands = flake8 imgui_datascience
15 |
16 | [testenv]
17 | setenv =
18 | PYTHONPATH = {toxinidir}
19 | deps =
20 | -r{toxinidir}/requirements_dev.txt
21 | ; If you want to make tox run the tests with the same versions, create a
22 | ; requirements.txt with the pinned versions and uncomment the following line:
23 | ; -r{toxinidir}/requirements.txt
24 | commands =
25 | pip install -U pip
26 | py.test --basetemp={envtmpdir}
27 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = python -msphinx
7 | SPHINXPROJ = imgui_datascience
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 |
--------------------------------------------------------------------------------
/tests/test_imgui_datascience.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """Tests for `imgui_datascience` package."""
5 |
6 | import pytest
7 |
8 |
9 | from imgui_datascience import imgui_datascience
10 |
11 |
12 | @pytest.fixture
13 | def response():
14 | """Sample pytest fixture.
15 |
16 | See more at: http://doc.pytest.org/en/latest/fixture.html
17 | """
18 | # import requests
19 | # return requests.get('https://github.com/audreyr/cookiecutter-pypackage')
20 |
21 |
22 | def test_content(response):
23 | """Sample pytest test function with the pytest fixture as an argument."""
24 | # from bs4 import BeautifulSoup
25 | # assert 'GitHub' in BeautifulSoup(response.content).title.string
26 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Config file for automatic testing at travis-ci.org
2 |
3 | language: python
4 | python:
5 | - 3.6
6 | - 3.5
7 | - 3.4
8 | - 2.7
9 |
10 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
11 | install: pip install -U tox-travis
12 |
13 | # Command to run tests, e.g. python setup.py test
14 | script: tox
15 |
16 | # Assuming you have installed the travis-ci CLI tool, after you
17 | # create the Github repo and add it to Travis, run the
18 | # following command to finish PyPI deployment setup:
19 | # $ travis encrypt --add deploy.password
20 | deploy:
21 | provider: pypi
22 | distributions: sdist bdist_wheel
23 | user: pthom
24 | password:
25 | secure: PLEASE_REPLACE_ME
26 | on:
27 | tags: true
28 | repo: pthom/imgui_datascience
29 | python: 3.6
30 |
--------------------------------------------------------------------------------
/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=python -msphinx
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 | set SPHINXPROJ=imgui_datascience
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed,
20 | echo.then set the SPHINXBUILD environment variable to point to the full
21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the
22 | echo.Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/imgui_datascience/static_vars.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 |
4 | class Bunch(dict):
5 | """
6 | `Bunch` is a dictionary that supports attribute-style access, a la JavaScript.
7 | See original article here :
8 | http://code.activestate.com/recipes/52308-the-simple-but-handy-collector-of-a-bunch-of-named/?in=user-97991
9 | `pip install bunch` will install an official version
10 | """
11 |
12 | def __init__(self, **kw):
13 | dict.__init__(self, kw)
14 | self.__dict__ = self
15 |
16 | def __str__(self):
17 | state = ["%s=%r" % (attribute, value)
18 | for (attribute, value)
19 | in self.__dict__.items()]
20 | return '\n'.join(state)
21 |
22 |
23 | def static_vars(**kwargs):
24 | def decorate(func):
25 | statics = Bunch(**kwargs)
26 | setattr(func, "statics", statics)
27 | return func
28 |
29 | return decorate
30 |
31 |
32 | @static_vars(name="Martin")
33 | def _my_function_with_statics():
34 | statics = _my_function_with_statics.statics
35 | return "Hello, {0}".format(statics.name)
36 |
37 |
38 | class TestStaticVars(unittest.TestCase):
39 | def test(self):
40 | msg = _my_function_with_statics()
41 | self.assertEqual(msg, "Hello, Martin")
42 |
43 |
44 | if __name__ == '__main__':
45 | unittest.main()
46 |
--------------------------------------------------------------------------------
/imgui_datascience/imgui_fig.py:
--------------------------------------------------------------------------------
1 | import numpy
2 | import cv2
3 | import matplotlib
4 | from . import imgui_cv
5 | from .static_vars import static_vars
6 |
7 |
8 | @static_vars(fig_cache=dict())
9 | def _fig_to_image(figure):
10 | statics = _fig_to_image.statics
11 | fig_id = id(figure)
12 | if fig_id not in statics.fig_cache:
13 | # draw the renderer
14 | figure.canvas.draw()
15 | # Get the RGBA buffer from the figure
16 | w, h = figure.canvas.get_width_height()
17 | buf = numpy.fromstring(figure.canvas.tostring_rgb(), dtype=numpy.uint8)
18 | buf.shape = (h, w, 3)
19 | img_rgb = cv2.cvtColor(buf, cv2.COLOR_RGB2BGR)
20 | matplotlib.pyplot.close(figure)
21 | statics.fig_cache[fig_id] = img_rgb
22 | return statics.fig_cache[fig_id]
23 |
24 |
25 | def fig(figure, width=None, height=None, title=""):
26 | """
27 | imgui_fig.fig will display a matplotlib figure
28 |
29 | Note: this might fail on OSX, with the following message ::
30 |
31 | AttributeError: 'FigureCanvasMac' object has no attribute 'renderer'
32 |
33 | In this case, simply change the renderer to Tk, like this::
34 |
35 | import matplotlib
36 | matplotlib.use('TkAgg') # this has to be done *before* importing pyplot
37 | import matplotlib.pyplot
38 | """
39 | image = _fig_to_image(figure)
40 | return imgui_cv.image(image, width=width, height=height, title=title)
41 |
--------------------------------------------------------------------------------
/docs/installation.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 |
8 | Stable release
9 | --------------
10 |
11 | To install, run this command in your terminal:
12 |
13 | .. code-block:: console
14 |
15 | $ pip install imgui_datascience
16 |
17 | This is the preferred method to install, as it will always install the most recent stable release.
18 |
19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide
20 | you through the process.
21 |
22 | .. _pip: https://pip.pypa.io
23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
24 |
25 | Note for python 2
26 | -----------------
27 |
28 | If you are targeting python 2, you might have to install python-dev and python-tk :
29 |
30 | .. code-block:: console
31 |
32 | $ sudo apt-get install python-tk python-dev
33 |
34 |
35 | From sources
36 | ------------
37 |
38 | The sources can be downloaded from the `Github repo`_.
39 |
40 | You can either clone the public repository:
41 |
42 | .. code-block:: console
43 |
44 | $ git clone git://github.com/pthom/imgui_datascience
45 |
46 | Or download the `tarball`_:
47 |
48 | .. code-block:: console
49 |
50 | $ curl -OL https://github.com/pthom/imgui_datascience/tarball/master
51 |
52 | Once you have a copy of the source, you can install it with:
53 |
54 | .. code-block:: console
55 |
56 | $ python setup.py install
57 |
58 |
59 | .. _Github repo: https://github.com/pthom/imgui_datascience
60 | .. _tarball: https://github.com/pthom/imgui_datascience/tarball/master
61 |
--------------------------------------------------------------------------------
/docs/imgui_datascience.rst:
--------------------------------------------------------------------------------
1 | imgui\_datascience package
2 | ==========================
3 |
4 | Submodules
5 | ----------
6 |
7 | imgui\_datascience.imgui\_cv module
8 | -----------------------------------
9 |
10 | .. automodule:: imgui_datascience.imgui_cv
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | imgui\_datascience.imgui\_datascience module
16 | --------------------------------------------
17 |
18 | .. automodule:: imgui_datascience.imgui_datascience
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | imgui\_datascience.imgui\_ext module
24 | ------------------------------------
25 |
26 | .. automodule:: imgui_datascience.imgui_ext
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | imgui\_datascience.imgui\_fig module
32 | ------------------------------------
33 |
34 | .. automodule:: imgui_datascience.imgui_fig
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | imgui\_datascience.imgui\_image\_lister module
40 | ----------------------------------------------
41 |
42 | .. automodule:: imgui_datascience.imgui_image_lister
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | imgui\_datascience.imgui\_runner module
48 | ---------------------------------------
49 |
50 | .. automodule:: imgui_datascience.imgui_runner
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | imgui\_datascience.static\_vars module
56 | --------------------------------------
57 |
58 | .. automodule:: imgui_datascience.static_vars
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 |
64 | Module contents
65 | ---------------
66 |
67 | .. automodule:: imgui_datascience
68 | :members:
69 | :undoc-members:
70 | :show-inheritance:
71 |
--------------------------------------------------------------------------------
/.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 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 |
58 | # Flask stuff:
59 | instance/
60 | .webassets-cache
61 |
62 | # Scrapy stuff:
63 | .scrapy
64 |
65 | # Sphinx documentation
66 | docs/_build/
67 |
68 | # PyBuilder
69 | target/
70 |
71 | # Jupyter Notebook
72 | .ipynb_checkpoints
73 |
74 | # pyenv
75 | .python-version
76 |
77 | # celery beat schedule file
78 | celerybeat-schedule
79 |
80 | # SageMath parsed files
81 | *.sage.py
82 |
83 | # dotenv
84 | .env
85 |
86 | # virtualenv
87 | .venv
88 | venv/
89 | ENV/
90 | env/
91 | env2/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # pycharm
107 | .idea
108 |
109 | # vscode
110 | /.vscode/
111 |
112 | # imgui
113 | imgui.ini
114 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | try:
8 | from urllib import pathname2url
9 | except:
10 | from urllib.request import pathname2url
11 |
12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
13 | endef
14 | export BROWSER_PYSCRIPT
15 |
16 | define PRINT_HELP_PYSCRIPT
17 | import re, sys
18 |
19 | for line in sys.stdin:
20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
21 | if match:
22 | target, help = match.groups()
23 | print("%-20s %s" % (target, help))
24 | endef
25 | export PRINT_HELP_PYSCRIPT
26 |
27 | BROWSER := python -c "$$BROWSER_PYSCRIPT"
28 |
29 | help:
30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
31 |
32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
33 |
34 | clean-build: ## remove build artifacts
35 | rm -fr build/
36 | rm -fr dist/
37 | rm -fr .eggs/
38 | find . -name '*.egg-info' -exec rm -fr {} +
39 | find . -name '*.egg' -exec rm -f {} +
40 |
41 | clean-pyc: ## remove Python file artifacts
42 | find . -name '*.pyc' -exec rm -f {} +
43 | find . -name '*.pyo' -exec rm -f {} +
44 | find . -name '*~' -exec rm -f {} +
45 | find . -name '__pycache__' -exec rm -fr {} +
46 |
47 | clean-test: ## remove test and coverage artifacts
48 | rm -fr .tox/
49 | rm -f .coverage
50 | rm -fr htmlcov/
51 | rm -fr .pytest_cache
52 |
53 | lint: ## check style with flake8
54 | flake8 imgui_datascience tests
55 |
56 | test: ## run tests quickly with the default Python
57 | py.test
58 |
59 | test-all: ## run tests on every Python version with tox
60 | tox
61 |
62 | coverage: ## check code coverage quickly with the default Python
63 | coverage run --source imgui_datascience -m pytest
64 | coverage report -m
65 | coverage html
66 | $(BROWSER) htmlcov/index.html
67 |
68 | docs: ## generate Sphinx HTML documentation, including API docs
69 | rm -f docs/imgui_datascience.rst
70 | rm -f docs/modules.rst
71 | sphinx-apidoc -o docs/ imgui_datascience
72 | $(MAKE) -C docs clean
73 | $(MAKE) -C docs html
74 | $(BROWSER) docs/_build/html/index.html
75 |
76 | servedocs: docs ## compile the docs watching for changes
77 | watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
78 |
79 | release: dist ## package and upload a release
80 | twine upload dist/*
81 |
82 | dist: clean ## builds source and wheel package
83 | python setup.py sdist
84 | python setup.py bdist_wheel
85 | ls -l dist
86 |
87 | install: clean ## install the package to the active Python's site-packages
88 | python setup.py install
89 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """The setup script."""
5 |
6 | from setuptools import setup, find_packages
7 |
8 | with open('README.rst') as readme_file:
9 | readme = readme_file.read()
10 |
11 | with open('HISTORY.rst') as history_file:
12 | history = history_file.read()
13 |
14 | requirements = [ ]
15 |
16 | setup_requirements = ['pytest-runner', ]
17 |
18 | test_requirements = ['pytest', ]
19 |
20 | setup(
21 | author="Pascal Thomet",
22 | author_email='pthomet@gmail.com',
23 | classifiers=[
24 | 'Development Status :: 2 - Pre-Alpha',
25 | 'Intended Audience :: Developers',
26 | 'License :: OSI Approved :: Apache Software License',
27 | 'Natural Language :: English',
28 | "Programming Language :: Python :: 2",
29 | 'Programming Language :: Python :: 2.7',
30 | 'Programming Language :: Python :: 3',
31 | 'Programming Language :: Python :: 3.4',
32 | 'Programming Language :: Python :: 3.5',
33 | 'Programming Language :: Python :: 3.6',
34 | ],
35 | package_data={
36 | '': ['*.ttf', 'images/*.jpg', 'images/*.png'],
37 | },
38 | description="A set of utilities for data science using python, imgui, numpy and opencv",
39 | install_requires=['imgui', 'opencv-python', 'imgui[pygame]', 'pyopengl', 'matplotlib','pygame', 'enum34', 'xxhash'],
40 | license="Apache Software License 2.0",
41 | long_description="""
42 |
43 | A set of utilities for data science using python, imgui, numpy and opencv
44 |
45 | Features
46 | ========
47 |
48 | View the full demo (1'50") on youtube:
49 |
50 | https://www.youtube.com/watch?v=qstEZyLGsTQ&feature=youtu.be
51 |
52 | Run it after install:
53 |
54 | python -m imgui_datascience --example
55 |
56 |
57 | Display numpy.ndarray (aka opencv image)
58 | ----------------------------------------
59 |
60 | The following types are supported : RGB, RGBA, GRAY, float32, float64
61 |
62 | Display matplotlib figures
63 | --------------------------
64 |
65 | Inspect images
66 | --------------
67 | - show pixels color (or float values)
68 | - adjust visibility for float images
69 | - save images
70 | - zoom & pan (with possible sync between 2 images)
71 | """,
72 |
73 | include_package_data=True,
74 | keywords='imgui_datascience',
75 | name='imgui_datascience',
76 | packages=find_packages(include=['imgui_datascience']),
77 | entry_points={
78 | 'console_scripts': [
79 | 'my_project = my_project.__main__:main'
80 | ]
81 | },
82 | setup_requires=setup_requirements,
83 | test_suite='tests',
84 | tests_require=test_requirements,
85 | url='https://github.com/pthom/imgui_datascience',
86 | version='0.3.1',
87 | zip_safe=False,
88 | )
89 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | .. highlight:: shell
2 |
3 | ============
4 | Contributing
5 | ============
6 |
7 | Contributions are welcome, and they are greatly appreciated! Every little bit
8 | helps, and credit will always be given.
9 |
10 | You can contribute in many ways:
11 |
12 | Types of Contributions
13 | ----------------------
14 |
15 | Report Bugs
16 | ~~~~~~~~~~~
17 |
18 | Report bugs at https://github.com/pthom/imgui_datascience/issues.
19 |
20 | If you are reporting a bug, please include:
21 |
22 | * Your operating system name and version.
23 | * Any details about your local setup that might be helpful in troubleshooting.
24 | * Detailed steps to reproduce the bug.
25 |
26 | Fix Bugs
27 | ~~~~~~~~
28 |
29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
30 | wanted" is open to whoever wants to implement it.
31 |
32 | Implement Features
33 | ~~~~~~~~~~~~~~~~~~
34 |
35 | Look through the GitHub issues for features. Anything tagged with "enhancement"
36 | and "help wanted" is open to whoever wants to implement it.
37 |
38 | Write Documentation
39 | ~~~~~~~~~~~~~~~~~~~
40 |
41 | (Py)ImGui for Data Science could always use more documentation, whether as part of the
42 | official docs, in docstrings, or even on the web in blog posts, articles, and such.
43 |
44 | Submit Feedback
45 | ~~~~~~~~~~~~~~~
46 |
47 | The best way to send feedback is to file an issue at https://github.com/pthom/imgui_datascience/issues.
48 |
49 | If you are proposing a feature:
50 |
51 | * Explain in detail how it would work.
52 | * Keep the scope as narrow as possible, to make it easier to implement.
53 | * Remember that this is a volunteer-driven project, and that contributions
54 | are welcome :)
55 |
56 | Get Started!
57 | ------------
58 |
59 | Ready to contribute? Here's how to set up `imgui_datascience` for local development.
60 |
61 | 1. Fork the `imgui_datascience` repo on GitHub.
62 | 2. Clone your fork locally::
63 |
64 | $ git clone git@github.com:your_name_here/imgui_datascience.git
65 |
66 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
67 |
68 | $ mkvirtualenv imgui_datascience
69 | $ cd imgui_datascience/
70 | $ python setup.py develop
71 |
72 | 4. Create a branch for local development::
73 |
74 | $ git checkout -b name-of-your-bugfix-or-feature
75 |
76 | Now you can make your changes locally.
77 |
78 | 5. When you're done making changes, check that your changes pass flake8 and the
79 | tests, including testing other Python versions with tox::
80 |
81 | $ flake8 imgui_datascience tests
82 | $ python setup.py test or py.test
83 | $ tox
84 |
85 | To get flake8 and tox, just pip install them into your virtualenv.
86 |
87 | 6. Commit your changes and push your branch to GitHub::
88 |
89 | $ git add .
90 | $ git commit -m "Your detailed description of your changes."
91 | $ git push origin name-of-your-bugfix-or-feature
92 |
93 | 7. Submit a pull request through the GitHub website.
94 |
95 | Pull Request Guidelines
96 | -----------------------
97 |
98 | Before you submit a pull request, check that it meets these guidelines:
99 |
100 | 1. The pull request should include tests.
101 | 2. If the pull request adds functionality, the docs should be updated. Put
102 | your new functionality into a function with a docstring, and add the
103 | feature to the list in README.rst.
104 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check
105 | https://travis-ci.org/pthom/imgui_datascience/pull_requests
106 | and make sure that the tests pass for all supported Python versions.
107 |
108 | Tips
109 | ----
110 |
111 | To run a subset of tests::
112 |
113 | $ py.test tests.test_imgui_datascience
114 |
115 |
116 | Deploying
117 | ---------
118 |
119 | A reminder for the maintainers on how to deploy.
120 | Make sure all your changes are committed (including an entry in HISTORY.rst).
121 | Then run::
122 |
123 | $ bumpversion patch # possible: major / minor / patch
124 | $ git push
125 | $ git push --tags
126 |
127 | Travis will then deploy to PyPI if tests pass.
128 |
--------------------------------------------------------------------------------
/imgui_datascience/imgui_runner.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import imgui
3 | import time
4 | from . import imgui_ext
5 | from .imgui_image_lister import ImGuiImageLister
6 | from . import imgui_cv
7 | from .static_vars import static_vars
8 | from collections import deque
9 | from timeit import default_timer
10 |
11 | import os
12 |
13 | import pygame
14 | import OpenGL.GL as gl
15 |
16 | from imgui.integrations.pygame import PygameRenderer
17 | import imgui
18 |
19 | @static_vars(last_call_times=deque())
20 | def compute_fps():
21 | statics = compute_fps.statics
22 | now = default_timer()
23 | statics.last_call_times.append(now)
24 | window_length = 24 # the computed fps is the average for the last 24 frames
25 | if len(statics.last_call_times) > window_length:
26 | last = statics.last_call_times.popleft()
27 | fps = float(window_length) / (now - last)
28 | else:
29 | fps = 0
30 | return fps
31 |
32 |
33 | class Params:
34 | def __init__(self, win_size=(800, 600), win_title="Imgui - Title", windowed_full_screen=False,
35 | provide_default_window=True):
36 | self.win_size = win_size
37 | self.win_title = win_title
38 | self.windowed_full_screen = windowed_full_screen # "Full screen", but with a window title bar + close button
39 | # Those params are used for windowed_full_screen mode
40 | self.windows_taskbar_height = 60
41 | self.window_title_height = 32
42 | self.windowed_full_screen_x_margin = 20
43 | self.provide_default_window = provide_default_window
44 |
45 |
46 | _g_Imgui_extensions_root_window_size = (640, 480)
47 |
48 |
49 | def run(
50 | gui_loop_function,
51 | params=Params(),
52 | on_init = None,
53 | on_exit = None):
54 |
55 | if params.windowed_full_screen:
56 | os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (
57 | params.windowed_full_screen_x_margin / 2, params.window_title_height)
58 |
59 | imgui.create_context()
60 | pygame.init()
61 | pygame.display.set_caption(params.win_title)
62 | win_size = params.win_size
63 | if params.windowed_full_screen:
64 | info = pygame.display.Info()
65 | screen_size = (info.current_w - params.windowed_full_screen_x_margin, info.current_h)
66 | win_size = (screen_size[0], screen_size[1] - params.window_title_height - params.windows_taskbar_height)
67 |
68 | pygame.display.set_mode(win_size, pygame.DOUBLEBUF | pygame.OPENGL | pygame.RESIZABLE)
69 | imgui_ext._load_fonts()
70 |
71 | io = imgui.get_io()
72 | io.display_size = win_size
73 |
74 | pygame_renderer = PygameRenderer()
75 | # if on_exit:
76 | # pygame.register_quit(on_exit)
77 |
78 | if on_init:
79 | on_init()
80 |
81 | while 1:
82 | for event in pygame.event.get():
83 | if event.type == pygame.QUIT:
84 | if on_exit:
85 | on_exit()
86 | try:
87 | sys.exit()
88 | except SystemExit as e:
89 | time.sleep(0.5)
90 | # sys.exit()
91 | # sys.terminate()
92 | os._exit(1)
93 |
94 | pygame_renderer.process_event(event)
95 |
96 | imgui.new_frame()
97 | if params.provide_default_window:
98 | imgui.set_next_window_position(0, 0)
99 | imgui.set_next_window_size(win_size[0], win_size[1])
100 | imgui.begin("Default window")
101 | gui_loop_function()
102 | if params.provide_default_window:
103 | imgui.end()
104 | ImGuiImageLister._heartbeat()
105 |
106 | # note: cannot use screen.fill((1, 1, 1)) because pygame's screen
107 | # does not support fill() on OpenGL surfaces
108 | gl.glClearColor(1, 1, 1, 1)
109 | gl.glClear(gl.GL_COLOR_BUFFER_BIT)
110 | imgui.render()
111 | pygame_renderer.render(imgui.get_draw_data())
112 | pygame.display.flip()
113 |
114 | imgui_cv._clear_all_cv_textures()
115 | imgui_ext.__clear_all_unique_labels()
116 |
117 |
118 | def _none_gui_loop():
119 | pass
120 |
121 |
122 | def ImGuiLister_ShowStandalone():
123 | ImGuiImageLister.window_size = imgui.Vec2(1000, 800)
124 | ImGuiImageLister.position = imgui.Vec2(0, 0)
125 | ImGuiImageLister.opened = True
126 | ImGuiImageLister.max_size = True
127 |
128 | run(_none_gui_loop, Params(win_title="ImGuiLister", windowed_full_screen=True))
129 |
--------------------------------------------------------------------------------
/imgui_datascience/imgui_image_lister.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | import imgui
3 | from . import imgui_cv
4 | from . import imgui_ext
5 | from . import imgui_fig
6 |
7 |
8 | def image_size_fit_in_gui(image_size, gui_size, can_make_bigger=False):
9 | # type: (imgui.Vec2, imgui.Vec2, bool) -> imgui.Vec2
10 | if image_size.x <= gui_size.x and image_size.y <= gui_size.y and not can_make_bigger:
11 | return image_size
12 | else:
13 | k_item = image_size.x / image_size.y
14 | k_gui = gui_size.x / gui_size.y
15 | if k_item > k_gui:
16 | return imgui.Vec2(gui_size.x, image_size.y / image_size.x * gui_size.x)
17 | else:
18 | return imgui.Vec2(image_size.x / image_size.y * gui_size.y, gui_size.y)
19 |
20 |
21 | class _ImguiImageInfo:
22 | def __init__(self, image, additional_legend, image_adjustments):
23 | self.image = image
24 | self.additional_legend = additional_legend
25 | self.image_adjustments = image_adjustments
26 |
27 |
28 | class _ImguiImageLister:
29 | """
30 | Do not instantiate this class by yourself, use the global instance named ImguiImageLister
31 | """
32 |
33 | def __init__(self):
34 | self.images_info = OrderedDict()
35 | self.current_image = ""
36 | self.opened = False
37 | self.never_shown = True
38 | self.listbox_width = 240
39 | self.position = imgui.Vec2(500, 50)
40 | self.window_size = imgui.Vec2(1000, 800)
41 | self.max_size = False
42 |
43 | def show_toggle_window_button(self, show_at_startup=False):
44 | if show_at_startup and self.never_shown:
45 | self.opened = True
46 | self.never_shown = False
47 | if self.opened:
48 | if imgui.button("Hide image lister"):
49 | self.opened = False
50 | else:
51 | if imgui.button("Show image lister"):
52 | self.opened = True
53 |
54 | def push_image(self, name, image, additional_legend="", image_adjustments=imgui_cv.ImageAdjustments()):
55 | image_type_name = type(image).__name__
56 | if image_type_name == "Figure":
57 | as_image = imgui_fig._fig_to_image(image)
58 | else:
59 | as_image = image
60 | self.images_info[name] = _ImguiImageInfo(as_image, additional_legend, image_adjustments)
61 |
62 | def clear_all_images(self):
63 | self.images_info = OrderedDict()
64 | self.current_image = ""
65 |
66 | def _set_selected_image(self, key):
67 | self.current_image = key
68 |
69 | def _show_list(self):
70 | imgui.begin_group()
71 | # imgui.text("")
72 | changed, selected_key = imgui_ext.listbox_dict(self.images_info, self.current_image, title_top="Images",
73 | height_in_items=40, item_width=self.listbox_width)
74 | if changed:
75 | self._set_selected_image(selected_key)
76 | if imgui.button(imgui_ext.make_unique_label("Clear all")):
77 | self.clear_all_images()
78 | imgui.end_group()
79 |
80 | def _max_image_size(self):
81 | win_size = imgui.get_window_size()
82 | max_image_size = imgui.Vec2(win_size.x - (self.listbox_width + 40), win_size.y - 150)
83 | return max_image_size
84 |
85 | def _show_image(self):
86 | if self.current_image in self.images_info:
87 | imgui.begin_group()
88 | if imgui.button("X"):
89 | self.images_info.pop(self.current_image)
90 | self.current_image = ""
91 | else:
92 | image_info = self.images_info[self.current_image]
93 | if image_info.additional_legend != "":
94 | imgui.same_line()
95 | imgui.text(image_info.additional_legend)
96 | img = image_info.image
97 | image_size = imgui.Vec2(img.shape[1], img.shape[0])
98 | image_size = image_size_fit_in_gui(image_size, self._max_image_size(), can_make_bigger=True)
99 | imgui_cv.image_explorer(img, title=self.current_image,
100 | width=int(round(image_size.x)), height=int(round(image_size.y)),
101 | image_adjustments=image_info.image_adjustments)
102 | imgui.end_group()
103 |
104 | def actual_window_startup_size(self):
105 | if self.max_size:
106 | display_size = imgui.get_io().display_size
107 | return imgui.Vec2(display_size.x - 40, display_size.y - 20)
108 | else:
109 | return self.window_size
110 |
111 | def _select_first_image(self):
112 | items = list(self.images_info.items())
113 | if len(items) > 0:
114 | self.current_image = items[0][0]
115 |
116 | def _heartbeat(self):
117 | if not self.opened:
118 | return
119 | if self.current_image == "":
120 | self._select_first_image()
121 | imgui.set_next_window_position(self.position.x, self.position.y, imgui.APPEARING)
122 | imgui.set_next_window_size(self.actual_window_startup_size().x, self.actual_window_startup_size().y,
123 | imgui.APPEARING)
124 | expanded, self.opened = imgui.begin("Imgui Image Lister")
125 | self._show_list()
126 | imgui.same_line()
127 | self._show_image()
128 | imgui.end()
129 |
130 |
131 | ImGuiImageLister = _ImguiImageLister()
132 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | #
4 | # imgui_datascience documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another
17 | # directory, add these directories to sys.path here. If the directory is
18 | # relative to the documentation root, use os.path.abspath to make it
19 | # absolute, like shown here.
20 | #
21 | import os
22 | import sys
23 | sys.path.insert(0, os.path.abspath('..'))
24 |
25 | import imgui_datascience
26 |
27 | # -- General configuration ---------------------------------------------
28 |
29 | # If your documentation needs a minimal Sphinx version, state it here.
30 | #
31 | # needs_sphinx = '1.0'
32 |
33 | # Add any Sphinx extension module names here, as strings. They can be
34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
35 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
36 |
37 | # Add any paths that contain templates here, relative to this directory.
38 | templates_path = ['_templates']
39 |
40 | # The suffix(es) of source filenames.
41 | # You can specify multiple suffix as a list of string:
42 | #
43 | # source_suffix = ['.rst', '.md']
44 | source_suffix = '.rst'
45 |
46 | # The master toctree document.
47 | master_doc = 'index'
48 |
49 | # General information about the project.
50 | project = u'(Py)ImGui for Data Science'
51 | copyright = u"2018, Pascal Thomet"
52 | author = u"Pascal Thomet"
53 |
54 | # The version info for the project you're documenting, acts as replacement
55 | # for |version| and |release|, also used in various other places throughout
56 | # the built documents.
57 | #
58 | # The short X.Y version.
59 | version = imgui_datascience.__version__
60 | # The full version, including alpha/beta/rc tags.
61 | release = imgui_datascience.__version__
62 |
63 | # The language for content autogenerated by Sphinx. Refer to documentation
64 | # for a list of supported languages.
65 | #
66 | # This is also used if you do content translation via gettext catalogs.
67 | # Usually you set "language" from the command line for these cases.
68 | language = None
69 |
70 | # List of patterns, relative to source directory, that match files and
71 | # directories to ignore when looking for source files.
72 | # This patterns also effect to html_static_path and html_extra_path
73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
74 |
75 | # The name of the Pygments (syntax highlighting) style to use.
76 | pygments_style = 'sphinx'
77 |
78 | # If true, `todo` and `todoList` produce output, else they produce nothing.
79 | todo_include_todos = False
80 |
81 |
82 | # -- Options for HTML output -------------------------------------------
83 |
84 | # The theme to use for HTML and HTML Help pages. See the documentation for
85 | # a list of builtin themes.
86 | #
87 | html_theme = 'alabaster'
88 |
89 | # Theme options are theme-specific and customize the look and feel of a
90 | # theme further. For a list of options available for each theme, see the
91 | # documentation.
92 | #
93 | # html_theme_options = {}
94 |
95 | # Add any paths that contain custom static files (such as style sheets) here,
96 | # relative to this directory. They are copied after the builtin static files,
97 | # so a file named "default.css" will overwrite the builtin "default.css".
98 | html_static_path = ['_static']
99 |
100 |
101 | # -- Options for HTMLHelp output ---------------------------------------
102 |
103 | # Output file base name for HTML help builder.
104 | htmlhelp_basename = 'imgui_datasciencedoc'
105 |
106 |
107 | # -- Options for LaTeX output ------------------------------------------
108 |
109 | latex_elements = {
110 | # The paper size ('letterpaper' or 'a4paper').
111 | #
112 | # 'papersize': 'letterpaper',
113 |
114 | # The font size ('10pt', '11pt' or '12pt').
115 | #
116 | # 'pointsize': '10pt',
117 |
118 | # Additional stuff for the LaTeX preamble.
119 | #
120 | # 'preamble': '',
121 |
122 | # Latex figure (float) alignment
123 | #
124 | # 'figure_align': 'htbp',
125 | }
126 |
127 | # Grouping the document tree into LaTeX files. List of tuples
128 | # (source start file, target name, title, author, documentclass
129 | # [howto, manual, or own class]).
130 | latex_documents = [
131 | (master_doc, 'imgui_datascience.tex',
132 | u'(Py)ImGui for Data Science Documentation',
133 | u'Pascal Thomet', 'manual'),
134 | ]
135 |
136 |
137 | # -- Options for manual page output ------------------------------------
138 |
139 | # One entry per manual page. List of tuples
140 | # (source start file, name, description, authors, manual section).
141 | man_pages = [
142 | (master_doc, 'imgui_datascience',
143 | u'(PyImGui for Data Science Documentation',
144 | [author], 1)
145 | ]
146 |
147 |
148 | # -- Options for Texinfo output ----------------------------------------
149 |
150 | # Grouping the document tree into Texinfo files. List of tuples
151 | # (source start file, target name, title, author,
152 | # dir menu entry, description, category)
153 | texinfo_documents = [
154 | (master_doc, 'imgui_datascience',
155 | u'(Py)ImGui for Data Science Documentation',
156 | author,
157 | 'imgui_datascience',
158 | 'One line description of project.',
159 | 'Miscellaneous'),
160 | ]
161 |
162 |
163 |
164 |
--------------------------------------------------------------------------------
/imgui_datascience/imgui_ext.py:
--------------------------------------------------------------------------------
1 | import imgui
2 | import os
3 | from collections import OrderedDict
4 | from inspect import getsourcefile
5 | from os.path import abspath
6 |
7 | this_script_dir = os.path.dirname(abspath(getsourcefile(lambda: 0)))
8 |
9 |
10 | class FontId(object):
11 | Font_10, Font_14, Font_18, Font_22, Font_26, Font_30 = range(6)
12 |
13 | @staticmethod
14 | def all_fonts_dict():
15 | return OrderedDict([
16 | ('Font_10', FontId.Font_10),
17 | ('Font_14', FontId.Font_14),
18 | ('Font_18', FontId.Font_18),
19 | ('Font_22', FontId.Font_22),
20 | ('Font_26', FontId.Font_26),
21 | ('Font_30', FontId.Font_30)
22 | ])
23 |
24 |
25 | _ALL_LOADED_FONTS = {}
26 |
27 |
28 | def _load_one_font(font_size, font_file="source-sans-pro.regular.ttf"):
29 | io = imgui.get_io()
30 | font_full_path = ""
31 | font_dirs = [this_script_dir, "./", "./fonts/"]
32 | for font_dir in font_dirs:
33 | if os.path.exists(font_dir + "/" + font_file):
34 | font_full_path = font_dir + "/" + font_file
35 |
36 | if len(font_full_path) == 0:
37 | raise RuntimeError("Could not find font file")
38 | font = io.fonts.add_font_from_file_ttf(font_full_path, font_size)
39 | return font
40 |
41 |
42 | # def _LoadFontAwesome(font_size, font_file ="fontawesome-webfont.ttf", font_dir =""):
43 | # io = imgui.get_io()
44 | # if font_dir == "":
45 | # font_dir = os.path.dirname(__file__)
46 | #
47 | # icon_ranges = [IconsFontAwesome.ICON_MIN_FA, IconsFontAwesome.ICON_MAX_FA]
48 | # # TypeError: Argument 'glyph_ranges' has incorrect type (expected imgui.core._StaticGlyphRanges, got list)
49 | # font = io.fonts.add_font_from_file_ttf(font_dir + "/" + font_file, font_size, icon_ranges)
50 | # return font
51 |
52 |
53 | def push_font(font_id):
54 | global _ALL_LOADED_FONTS
55 | imgui.push_font(_ALL_LOADED_FONTS[font_id])
56 |
57 |
58 | def push_default_font():
59 | push_font(FontId.Font_18)
60 |
61 |
62 | def pop_font():
63 | imgui.pop_font()
64 |
65 |
66 | def _load_fonts():
67 | global _ALL_LOADED_FONTS
68 | io = imgui.get_io()
69 | io.fonts.add_font_default()
70 | _ALL_LOADED_FONTS[FontId.Font_10] = _load_one_font(10)
71 | _ALL_LOADED_FONTS[FontId.Font_14] = _load_one_font(14)
72 | _ALL_LOADED_FONTS[FontId.Font_18] = _load_one_font(18)
73 | _ALL_LOADED_FONTS[FontId.Font_22] = _load_one_font(22)
74 | _ALL_LOADED_FONTS[FontId.Font_26] = _load_one_font(26)
75 | _ALL_LOADED_FONTS[FontId.Font_30] = _load_one_font(30)
76 | # _ALL_LOADED_FONTS[FontId.FontAwesome_30] = _LoadFontAwesome(30)
77 |
78 |
79 | _ALL_UNIQUE_LABELS = []
80 |
81 |
82 | def make_unique_label(label, object_id=None):
83 | global _ALL_UNIQUE_LABELS
84 | if object_id is None:
85 | object_id = str(len(_ALL_UNIQUE_LABELS))
86 | result = label + "##" + object_id
87 | _ALL_UNIQUE_LABELS.append(result)
88 | return result
89 |
90 |
91 | def make_unique_empty_label():
92 | return make_unique_label("")
93 |
94 |
95 | def __clear_all_unique_labels():
96 | global _ALL_UNIQUE_LABELS
97 | _ALL_UNIQUE_LABELS = []
98 |
99 |
100 | def make_label_plus_icon(label, icon):
101 | global _ALL_UNIQUE_LABELS
102 | result = label + icon + "##" + len(_ALL_UNIQUE_LABELS)
103 | _ALL_UNIQUE_LABELS.add(result)
104 | return result
105 |
106 |
107 | def make_icon_plus_label(icon, label):
108 | global _ALL_UNIQUE_LABELS
109 | result = icon + label + "##" + len(_ALL_UNIQUE_LABELS)
110 | _ALL_UNIQUE_LABELS.add(result)
111 | return result
112 |
113 |
114 | class TogglableWindowParams:
115 | def __init__(self, window_title="", initial_show=True, size=(0, 0), pos=(0, 0)):
116 | self.window_title = window_title
117 | self.toggle_button_legend = ""
118 | self.size = size
119 | self.initialShow = initial_show
120 | self.pos = pos
121 | self.include_begin_code = True
122 |
123 |
124 | _ALL_TOGGLABLE_STATUS = {}
125 |
126 |
127 | def show_togglable_window(window_param, window_function_code):
128 | global _ALL_TOGGLABLE_STATUS
129 | if window_param.window_title not in _ALL_TOGGLABLE_STATUS:
130 | _ALL_TOGGLABLE_STATUS[window_param.window_title] = window_param.initialShow
131 |
132 | this_window_open_status = _ALL_TOGGLABLE_STATUS[window_param.window_title]
133 |
134 | if this_window_open_status:
135 | toggle_button_legend = "Hide " + window_param.window_title
136 | else:
137 | toggle_button_legend = "Show " + window_param.window_title
138 |
139 | if imgui.button(make_unique_label(toggle_button_legend)):
140 | this_window_open_status = not this_window_open_status
141 |
142 | if this_window_open_status:
143 | imgui.set_next_window_size(window_param.size[0], window_param.size[1])
144 | if window_param.include_begin_code:
145 | imgui.begin(window_param.window_title)
146 |
147 | window_function_code()
148 |
149 | if window_param.include_begin_code:
150 | imgui.end()
151 |
152 |
153 | def togglable_window_toggle(window_title, open_window=None):
154 | global _ALL_TOGGLABLE_STATUS
155 | if open_window is None:
156 | _ALL_TOGGLABLE_STATUS[window_title] = not _ALL_TOGGLABLE_STATUS[window_title]
157 | else:
158 | _ALL_TOGGLABLE_STATUS[window_title] = open_window
159 |
160 |
161 | def togglable_window_get_status(window_title):
162 | global _ALL_TOGGLABLE_STATUS
163 | return _ALL_TOGGLABLE_STATUS[window_title]
164 |
165 |
166 | def listbox_dict(dict_string_value, current_key, title_top="", title_right="", height_in_items=20, item_width=None):
167 | keys = [key for key, _ in dict_string_value.items()]
168 | if current_key in keys:
169 | current_idx = keys.index(current_key)
170 | else:
171 | current_idx = -1
172 | if item_width is not None:
173 | imgui.push_item_width(item_width)
174 | if title_top != "":
175 | imgui.text(title_top)
176 | changed, new_idx = imgui.listbox(title_right, current_idx, keys, height_in_items=height_in_items)
177 | if 0 <= new_idx < len(keys):
178 | new_key = keys[new_idx]
179 | else:
180 | new_key = ""
181 | return changed, new_key
182 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Note: please consider checking out the (much more advanced) successor of this library: https://github.com/pthom/imgui_bundle
2 |
3 | (Py)ImGui for Data Science
4 | ===============================================================================
5 |
6 |
7 | .. image:: https://img.shields.io/travis/pthom/imgui_datascience.svg
8 | :target: https://travis-ci.org/pthom/imgui_datascience
9 |
10 | .. image:: https://readthedocs.org/projects/imgui_datascience/badge/?version=latest
11 | :target: https://imgui_datascience.readthedocs.io/en/latest/?badge=latest
12 | :alt: Documentation Status
13 |
14 | A set of utilities for data science using python, imgui, numpy and opencv.
15 |
16 | * Free software: Apache Software License 2.0
17 | * Documentation: https://imgui_datascience.readthedocs.io.
18 | * Compatible with python 3 (not with python 2.7)
19 |
20 | * Demo:
21 |
22 | .. image:: images/yt.png
23 | :target: https://youtu.be/qstEZyLGsTQ
24 | :width: 200
25 | :alt: Demo on YouTube
26 |
27 |
28 | Acknowledgments
29 | ===============
30 |
31 | This library is based on the two following projects:
32 |
33 | `Dear ImGui `_ : an amazing 'Immediate Mode GUI' C++ library
34 |
35 | `pyimgui `_ : Python bindings for imgui (based on Cython).
36 |
37 | `imdebug, the image debugger `_ :a neat image debugger / viewing
38 | by William Baxter has provided some ideas for the image_explorer feature.
39 |
40 | Many thanks to their developers for their wonderful job.
41 |
42 | Install & test:
43 | ===============
44 |
45 | Code::
46 |
47 | git clone https://github.com/pthom/imgui_datascience.git
48 | cd imgui_datascience
49 | python3 -m venv env
50 | source env/bin/activate
51 | pip install -r requirements.txt
52 | python run_example.py
53 |
54 | Features
55 | ========
56 |
57 | Display numpy.ndarray (aka opencv image)
58 | ----------------------------------------
59 | The following types are supported : ``RGB, RGBA, GRAY, float32, float64``
60 |
61 | Code::
62 |
63 | # returns mouse_position in image coords
64 | mouse_position = imgui_cv.image(img, height=150, title="flowers")
65 |
66 | If the content of your image varies (for example an image
67 | from a camera), pass always_refresh=True.
68 |
69 | For example:
70 |
71 | Code::
72 |
73 |
74 | imgui_cv.image(video_image, always_refresh = True)
75 |
76 |
77 | Display matplotlib figures
78 | --------------------------
79 |
80 | .. image:: images/mplot.jpg
81 | :height: 200
82 |
83 | Code::
84 |
85 | figure = matplotlib.pyplot.figure()
86 | x = numpy.arange(0.1, 100, 0.1)
87 | y = numpy.sin(x) / x
88 | plot.plot(x, y)
89 |
90 | imgui_fig.fig(figure, height=250, title="f(x) = sin(x) / x")
91 |
92 |
93 | Inspect images
94 | --------------
95 | * show pixels color (or float values)
96 | * adjust visibility for float images
97 | * save images
98 | * zoom & pan (with possible sync between 2 images)
99 |
100 | .. image:: images/image_explorer.jpg
101 | :height: 200
102 |
103 | See https://www.youtube.com/watch?v=yKw7VaQNFCI&feature=youtu.be for an animated demo.
104 |
105 | Code::
106 |
107 | imgui_cv.image_explorer(img)
108 |
109 |
110 | A simple way to run imgui programs
111 | ----------------------------------
112 |
113 | The simplest way to run a program a start adding gui buttons is shown below
114 |
115 | Code::
116 |
117 | def gui_loop():
118 | imgui.button("Click me")
119 |
120 | def main():
121 | imgui_runner.run(gui_loop, imgui_runner.Params())
122 |
123 |
124 | A simple way to quickly inspect images
125 | --------------------------------------
126 |
127 | Below is the simplest to quickly display any type of numpy array (RGB, float, etc) and to be able to inspect it.
128 |
129 | Code::
130 |
131 | image = ... # cv2.imread("...")
132 | ImGuiImageLister.push_image("owl", image)
133 | ImGuiLister_ShowStandalone()
134 |
135 | .. image:: images/image_lister.png
136 | :height: 200
137 |
138 | Full demo
139 | --------
140 |
141 | You can run a full demo using either
142 |
143 | * Case 1 (from pip install):
144 |
145 | Code::
146 |
147 | pip install imgui_datascience
148 | python -m imgui_datascience --example
149 |
150 |
151 |
152 | * Case 2 (from checkout, with a virtualenv):
153 |
154 | Code::
155 |
156 | git clone https://github.com/pthom/imgui_datascience.git
157 | cd imgui_datascience
158 | virtualenv venv
159 | source venv/bin/activate
160 | pip install -r requirements.txt
161 | pip install -r requirements_dev.txt
162 | python run_example.py
163 |
164 |
165 | * View the full demo (1'50") on youtube
166 |
167 | .. image:: images/thumb.jpg
168 | :height: 100
169 |
170 | click on the link below
171 |
172 | https://www.youtube.com/watch?v=qstEZyLGsTQ&feature=youtu.be
173 |
174 | Gotchas
175 | =======
176 |
177 | Widget unique identifiers
178 | -------------------------
179 | Imgui identifies the widget through their label. If you have two buttons that have the same label,
180 | it might not differentiate them.
181 |
182 | A workaround is to add "##" + an id after your label
183 |
184 | Code::
185 |
186 | if imgui.button("Click Me"):
187 | print("Clicked first button")
188 | if imgui.button("Click Me##2"):
189 | print("Clicked second button")
190 |
191 | Another workaround is to use imgui_ext.make_unique_label
192 |
193 | Code::
194 |
195 | if imgui.button(imgui_ext.make_unique_label("Click Me")):
196 | print("Clicked first button")
197 | if imgui.button(imgui_ext.make_unique_label("Click Me")):
198 | print("Clicked second button")
199 |
200 |
201 | OpenGL
202 | ------
203 | This lib makes a heavy usage of OpenGL : it transfers the images from the RAM to you graphic card at each frame.
204 | The image textures are cached and only recreated if the image data has changed.
205 |
206 | The library will detect that an image has changed by using a hash of its data. Two hash variant are possible :
207 |
208 | * if imgui_cv.USE_FAST_HASH is set to True (which is default) : select 100 random pixels and hash them
209 | * otherwise, compute the hash of the whole image data (using xxhash for performance)
210 |
211 | You can change imgui_cv.USE_FAST_HASH value in order to change the behavior if needed.
212 |
213 | Credits
214 | =======
215 |
216 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
217 |
218 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter
219 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
220 |
--------------------------------------------------------------------------------
/imgui_datascience/example.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | import cv2
3 | from collections import deque
4 | import os
5 | import inspect
6 | import numpy as np
7 | from timeit import default_timer
8 | from inspect import getsourcefile
9 | from os.path import abspath
10 |
11 | import matplotlib
12 | matplotlib.use('TkAgg')
13 | import matplotlib.pyplot
14 |
15 | from . import * # <=> i.e from imgui_datascience import *
16 |
17 |
18 | THIS_SCRIPT_DIR = os.path.dirname(abspath(getsourcefile(lambda: 0)))
19 |
20 |
21 | @static_vars(clicked=False, check=False)
22 | def show_buttons():
23 | statics = show_buttons.statics
24 | if imgui.button("Button"):
25 | statics.clicked = not statics.clicked
26 | if statics.clicked:
27 | imgui.same_line()
28 | imgui.text("Thanks for clicking me!")
29 | changed, statics.check = imgui.checkbox("checkbox", statics.check)
30 |
31 |
32 | @static_vars(img=cv2.imread(THIS_SCRIPT_DIR + "/images/flower.jpg"))
33 | def demo_image():
34 | imgui.text("This image is provided by opencv / numpy.")
35 | imgui.text("You can click on it to show it with its original size")
36 | statics = demo_image.statics
37 | imgui_cv.image(statics.img, height=150, title="flowers") # returns mouse_position
38 |
39 |
40 | def make_contour_image(image):
41 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
42 | normalized = np.float32(gray) / 255.
43 | edges = cv2.Sobel(normalized, -1, 1, 1, ksize=3) + 0.5
44 | return edges
45 |
46 |
47 | @static_vars(
48 | img=cv2.imread(THIS_SCRIPT_DIR + "/images/billiard.jpg"),
49 | img_contours=None
50 | )
51 | def demo_image_explorer():
52 | statics = demo_image_explorer.statics
53 | if statics.img_contours is None:
54 | statics.img_contours = make_contour_image(statics.img)
55 | imgui.text("""imgui_cv.image_explorer() will show a detailed view of an opencv image.
56 | You can zoom, pan & see the colors of the pixels.
57 | You can optionally link the zoom of two images (using the zoom_key param)
58 | """)
59 | imgui_cv.image_explorer(statics.img, zoom_key="1")
60 | imgui.text("image_explorer is compatible with uint8 and float images")
61 | imgui.text("Click the '+' button below this image in order to see more info")
62 | imgui.text("Then, click the 'adjust' button in order to adjust the view of a float matrix")
63 | imgui_cv.image_explorer(statics.img_contours, zoom_key="1", hide_buttons=True)
64 |
65 |
66 | @static_vars(inited=False)
67 | def demo_image_lister():
68 | if not demo_image_lister.statics.inited:
69 | for name in ["owl", "billiard", "flower"]:
70 | ImGuiImageLister.push_image(name, cv2.imread(THIS_SCRIPT_DIR + "/images/" + name + ".jpg"))
71 | demo_image_lister.statics.inited = True
72 | imgui.text("""The image lister enable to keep a list of images in a separate window for further examination
73 | Just call 'ImGuiImageLister.show_toggle_window_button()' somewhere in your code,
74 | and add images via 'ImGuiImageLister.push_image(name, image)'""")
75 | ImGuiImageLister.show_toggle_window_button()
76 |
77 |
78 | @static_vars(
79 | imgs={}
80 | )
81 | def demo_image_explorer_types():
82 | imgui.text("imgui_cv.image and imgui_cv.image_explorer can support multiple image types")
83 | imgui.separator()
84 | statics = demo_image_explorer_types.statics
85 | if len(statics.imgs) == 0:
86 | img = cv2.imread(THIS_SCRIPT_DIR + "/images/owl.jpg")
87 | img_grey = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
88 | statics.imgs["RGB uint8"] = img
89 | statics.imgs["Gray uint8"] = img_grey
90 | statics.imgs["Float32"] = np.float32(img_grey) / 255.
91 | statics.imgs["Float64"] = np.float64(img_grey) / 255.
92 | statics.imgs["RGBA"] = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
93 |
94 | for key, img in statics.imgs.items():
95 | imgui_cv.image_explorer(img, title=key, height=200, hide_buttons=False)
96 | imgui.separator()
97 |
98 |
99 | def make_figure():
100 | import numpy
101 | figure = matplotlib.pyplot.figure()
102 | plot = figure.add_subplot(111)
103 | # draw a cardinal sine plot
104 | x = numpy.arange(0.1, 100, 0.1)
105 | y = numpy.sin(x) / x
106 | plot.plot(x, y)
107 | return figure
108 |
109 |
110 | @static_vars(figure=make_figure())
111 | def demo_figs():
112 | imgui.text("opencv images of matplotlib figures can be presented as thumbnails \n(click to show the original size)")
113 | imgui_fig.fig(demo_figs.statics.figure, height=250, title="f(x) = sin(x) / x")
114 |
115 |
116 | def demo_font():
117 | for font_name, font_id in imgui_ext.FontId.all_fonts_dict().items():
118 | imgui_ext.push_font(font_id)
119 | imgui.text(font_name)
120 | imgui_ext.pop_font()
121 |
122 |
123 | def demo_original_demo():
124 | imgui.text("The 'ImGui Demo' window (to the right of this window) \nis a good way to learn imgui. See its code at ")
125 | url = "https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp"
126 | imgui.input_text("", url, 300)
127 | imgui.same_line()
128 | if imgui.button("Open in browser"):
129 | import webbrowser
130 | webbrowser.open(url)
131 |
132 |
133 | def demo_cpp_to_python():
134 | imgui.text("Below is an example of two widgets and their code")
135 | imgui.separator()
136 | show_buttons()
137 | imgui.separator()
138 | python_code = inspect.getsource(show_buttons)
139 | python_advice = """
140 | Since imgui is well suited
141 | with static variables,
142 | a 'static_vars' decorator
143 | is provided
144 | """
145 | imgui.input_text_multiline("python code\n" + python_advice, python_code, len(python_code) * 2, 500, 150)
146 |
147 | imgui.text("\nThis python code is the equivalent of the following cpp code:\n\n")
148 | cpp_code = """void ShowButtons()
149 | {
150 | static bool clicked = false;
151 | if (ImGui::Button("Button"))
152 | clicked = ! clicked;
153 | if (clicked)
154 | {
155 | ImGui::SameLine();
156 | ImGui::Text("Thanks for clicking me!");
157 | }
158 | static bool check = true;
159 | ImGui::Checkbox("checkbox", &check);
160 | }"""
161 | imgui.input_text_multiline("cpp code", cpp_code, len(cpp_code) * 2, 500, 200)
162 |
163 |
164 | def demo_this_module_code():
165 | module = inspect.getmodule(demo_this_module_code)
166 | source = inspect.getsource(module)
167 | imgui.input_text_multiline("", source, len(source) * 2, 700, 400)
168 | if imgui.button("Copy to clipboard"):
169 | def put_text_to_clipboard(text):
170 | try:
171 | from Tkinter import Tk # python 2
172 | except ImportError:
173 | from tkinter import Tk # python 3
174 | r = Tk()
175 | r.withdraw()
176 | r.clipboard_clear()
177 | r.clipboard_append(text)
178 | r.update()
179 | r.destroy()
180 | put_text_to_clipboard(source)
181 |
182 |
183 | def demo_imguilister_standalone():
184 | def run_imguilister_standalone():
185 | image = cv2.imread(THIS_SCRIPT_DIR + "/images/owl.jpg")
186 | ImGuiImageLister.push_image("owl", image)
187 | ImGuiLister_ShowStandalone()
188 |
189 | imgui.text("""
190 | If you only need to inspect one or serveral images with a better tool than
191 | cv2.imshow(), all you need to write is a function like this one:
192 | """)
193 | source = inspect.getsource(run_imguilister_standalone)
194 | imgui.input_text_multiline(imgui_ext.make_unique_empty_label(), source, len(source) * 2, 400, 90)
195 | imgui.text("If you click this button, a new demo will be launched, using this code")
196 | if imgui.button("Demo standalone"):
197 | run_imguilister_standalone()
198 |
199 |
200 | @static_vars(flag_show_code = dict())
201 | def show_one_feature(feature_function, feature_intro, default_open=False):
202 | flag_show_code = show_one_feature.statics.flag_show_code
203 | flags = imgui.TREE_NODE_DEFAULT_OPEN if default_open else 0
204 | expanded, visible=imgui.collapsing_header(feature_intro, flags=flags)
205 | if expanded:
206 | imgui_ext.push_font(imgui_ext.FontId.Font_18)
207 | imgui.text(feature_intro)
208 | imgui_ext.pop_font()
209 | if feature_intro not in flag_show_code:
210 | flag_show_code[feature_intro] = False
211 | imgui.same_line(imgui.get_window_width() - 150)
212 | _, flag_show_code[feature_intro] = imgui.checkbox(imgui_ext.make_unique_label("View code"), flag_show_code[feature_intro])
213 | if flag_show_code[feature_intro]:
214 | code = inspect.getsource(feature_function)
215 | imgui.input_text_multiline(imgui_ext.make_unique_empty_label(), code, len(code) * 2, 600, 200)
216 | feature_function()
217 |
218 |
219 | def show_fps():
220 | imgui.set_next_window_position(0, 0, imgui.APPEARING)
221 | imgui.set_next_window_size(100, 40, imgui.APPEARING)
222 | imgui.begin("FPS")
223 | msg = "{0:.1f}".format(imgui_runner.compute_fps())
224 | imgui.text(msg)
225 | imgui.end()
226 |
227 |
228 | def gui_loop():
229 | imgui.set_next_window_position(0, 40, imgui.APPEARING)
230 | imgui.set_next_window_size(750, 680, imgui.APPEARING)
231 | imgui.begin("ImGui for data scientists")
232 | show_fps()
233 | show_one_feature(demo_image, "Using opencv images (numpy.ndarray)")
234 | show_one_feature(demo_figs, "Using matplotlib figures")
235 | show_one_feature(demo_image_explorer, "Using image explorer")
236 | show_one_feature(demo_image_explorer_types, "Image types")
237 | show_one_feature(demo_image_lister, "Image Lister")
238 | show_one_feature(demo_imguilister_standalone, "Image Lister Standalone")
239 | show_one_feature(demo_font, "Using different font sizes")
240 | show_one_feature(demo_cpp_to_python, "Python code advices / porting from cpp ")
241 | show_one_feature(demo_this_module_code, "Code for this demo")
242 | show_one_feature(demo_original_demo, "ImGui Demo")
243 | imgui.end()
244 |
245 | imgui.set_next_window_position(750, 40, imgui.APPEARING)
246 | imgui.show_test_window()
247 |
248 |
249 | def example():
250 | imgui_runner.run(gui_loop, imgui_runner.Params(windowed_full_screen=True, win_title="Dear Imgui !",
251 | provide_default_window=False))
252 |
--------------------------------------------------------------------------------
/imgui_datascience/imgui_cv.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import copy
3 | import xxhash
4 | import numpy as np
5 | import imgui
6 | import OpenGL.GL as gl
7 | from .static_vars import *
8 | from timeit import default_timer as timer
9 | from . import imgui_ext
10 | import math
11 | from typing import *
12 | from dataclasses import dataclass
13 |
14 | _start = timer()
15 |
16 | USE_FAST_HASH = True
17 |
18 | LOG_GPU_USAGE = False
19 |
20 | """
21 | Some type synonyms in order to make the code easier to understand
22 | """
23 | TextureId = int # this is an openGl texture id
24 | Image_RGB = np.ndarray # denotes a RGB image
25 | Image_AnyType = np.ndarray # denotes any image contained in a np.ndarray
26 | ImageAddress = int # this is the image memory address
27 |
28 |
29 | def _is_close(a: float, b: float) -> bool:
30 | return math.fabs(a - b) < 1E-6
31 |
32 |
33 | # noinspection PyShadowingNames
34 | class ImageAdjustments:
35 | factor: float
36 | delta: float
37 | def __init__(self, factor: float = 1., delta: float = 0.):
38 | self.factor = factor
39 | self.delta = delta
40 |
41 | def is_none(self):
42 | return _is_close(self.factor, 1.) and _is_close(self.delta, 0.)
43 |
44 | def adjust(self, image):
45 | if self.is_none():
46 | return image
47 | else:
48 | adjusted = ((image + self.delta) * self.factor).astype(image.dtype)
49 | return adjusted
50 |
51 | def __hash__(self):
52 | return hash((self.factor, self.delta))
53 |
54 | def __eq__(self, other):
55 | return self.factor == other.factor and self.delta == other.delta
56 |
57 |
58 | def _hash_image(image):
59 | """
60 | Two hash variant are possible :
61 | - if imgui_cv.USE_FAST_HASH is True : select 100 random pixels and hash them
62 | - otherwise : compute the hash of the whole image (using xxhash for performance)
63 | :param image:
64 | :return:hash
65 | """
66 | if USE_FAST_HASH:
67 | rng = np.random.RandomState(89)
68 | inds = rng.randint(low=0, high=image.size, size=100)
69 | b = image.flat[inds]
70 | result = hash(tuple(b.data))
71 | return result
72 | else:
73 | # cf https://stackoverflow.com/questions/16589791/most-efficient-property-to-hash-for-numpy-array
74 | h = xxhash.xxh64()
75 | h.update(image)
76 | result = h.intdigest()
77 | h.reset()
78 | return result
79 |
80 |
81 | class ImageAndAdjustments:
82 | image: Image_AnyType
83 | image_adjustment: ImageAdjustments
84 | def __init__(self, image, image_adjustments):
85 | self.image = image
86 | self.image_adjustments = image_adjustments
87 |
88 | def adjusted_image(self):
89 | return self.image_adjustments.adjust(self.image)
90 |
91 | def __hash__(self):
92 | hash_adjust = hash(self.image_adjustments)
93 | hash_image = _hash_image(self.image)
94 | result = hash((hash_adjust, hash_image))
95 | return result
96 |
97 | def __eq__(self, other):
98 | """
99 | For performance reasons, the __eq__ operator is made to take only the hash into account.
100 | @see _image_to_texture()
101 | """
102 | hash1 = hash(self)
103 | hash2 = hash(other)
104 | return hash1 == hash2
105 |
106 |
107 | class SizePixel:
108 | width: int
109 | height: int
110 | def __init__(self, width=0, height=0):
111 | self.width = int(width)
112 | self.height = int(height)
113 |
114 | @staticmethod
115 | def from_image(image):
116 | self = SizePixel()
117 | self.width = image.shape[1]
118 | self.height = image.shape[0]
119 | return self
120 |
121 | def as_tuple_width_height(self):
122 | return self.width, self.height
123 |
124 | # ALL_TEXTURES contains a dict of all the images that were transferred to the GPU
125 | # plus their last access time
126 |
127 | TimeSecond = float
128 |
129 |
130 | NB_GEN_TEXTURES = 0
131 |
132 | def _generate_texture_id() -> TextureId:
133 | texture_id = gl.glGenTextures(1)
134 | if LOG_GPU_USAGE:
135 | global NB_GEN_TEXTURES
136 | NB_GEN_TEXTURES = NB_GEN_TEXTURES + 1
137 | print(f"NB_GEN_TEXTURES = {NB_GEN_TEXTURES}")
138 | return texture_id
139 |
140 |
141 | @dataclass
142 | class ImageStoredOnGpu:
143 | image_and_adjustments: ImageAndAdjustments
144 | texture_id: TextureId
145 | time_last_access: TimeSecond = -10000.
146 | def __init__(self, image_and_adjustments: ImageAndAdjustments, time_last_access):
147 | self.image_and_adjustments = image_and_adjustments
148 | self.time_last_access = time_last_access
149 | self.texture_id = _generate_texture_id()
150 |
151 | AllTexturesDict = Dict[ImageAddress, ImageStoredOnGpu]
152 | ALL_TEXTURES: AllTexturesDict = {}
153 |
154 |
155 | def _to_rgb_image(img: Image_AnyType) -> Image_RGB:
156 | img_rgb = None
157 | if len(img.shape) >= 3:
158 | channels = img.shape[2]
159 | else:
160 | channels = 1
161 | if channels == 1:
162 | if img.dtype == np.uint8:
163 | img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
164 | elif img.dtype in [np.float32, np.float64]:
165 | img_grey = np.uint8(img * 255.)
166 | img_rgb = cv2.cvtColor(img_grey, cv2.COLOR_GRAY2BGR)
167 | elif channels == 3:
168 | if not img.dtype == np.uint8:
169 | raise ValueError("imgui_cv does only support uint8 images with multiple channels")
170 | img_rgb = img
171 | elif channels == 4:
172 | if not img.dtype == np.uint8:
173 | raise ValueError("imgui_cv does only support uint8 images with multiple channels")
174 | # we do not handle alpha very well...
175 | img_rgb = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
176 | return img_rgb
177 |
178 | NB_REFRESH_TEXTURES = 0
179 | def _image_rgb_to_texture_impl(img_rgb: Image_RGB, texture_id: TextureId):
180 | """
181 | Performs the actual transfer to the gpu and returns a texture_id
182 | """
183 | # inspired from https://www.programcreek.com/python/example/95539/OpenGL.GL.glPixelStorei (example 3)
184 | if LOG_GPU_USAGE:
185 | global NB_REFRESH_TEXTURES
186 | NB_REFRESH_TEXTURES = NB_REFRESH_TEXTURES + 1
187 | print(f"NB_REFRESH_TEXTURES = {NB_REFRESH_TEXTURES}")
188 | width = img_rgb.shape[1]
189 | height = img_rgb.shape[0]
190 |
191 | gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
192 | gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id)
193 | gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
194 | gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
195 | gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
196 | gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, width, height, 0, gl.GL_BGR, gl.GL_UNSIGNED_BYTE, img_rgb)
197 | gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
198 | return texture_id
199 |
200 |
201 |
202 |
203 | def _image_to_texture(
204 | image_and_adjustments: ImageAndAdjustments,
205 | always_refresh: bool,
206 | linked_user_image_address: ImageAddress
207 | ):
208 | """
209 | _image_to_texture will transfer the image to the GPU and return a texture Id
210 | Some GPU might choke if too many textures are transferred.
211 | For this reason :
212 | - a cache is maintained (ALL_TEXTURES)
213 | - a quick comparison is made before the transfer:
214 | @see _hash_image()
215 | @see ImageAndAdjustments.__eq__() : for performance reasons, the __eq__ operator
216 | is made to take only the hash into account.
217 | :param image_and_adjustments:
218 | :return: texture_id
219 | """
220 | now = timer()
221 | if linked_user_image_address == 0:
222 | image_address = id(image_and_adjustments.image)
223 | else:
224 | image_address = linked_user_image_address
225 |
226 | shall_refresh = False
227 |
228 | if image_address not in ALL_TEXTURES:
229 | ALL_TEXTURES[image_address] = ImageStoredOnGpu(image_and_adjustments, now)
230 | shall_refresh = True
231 |
232 | if always_refresh:
233 | shall_refresh = True
234 |
235 | image_stored_on_gpu: ImageStoredOnGpu = ALL_TEXTURES[image_address]
236 | image_stored_on_gpu.time_last_access = now
237 |
238 | if shall_refresh:
239 | image_and_adjustments_copy = copy.deepcopy(image_and_adjustments)
240 | img_adjusted = image_and_adjustments_copy.adjusted_image()
241 | img_rgb = _to_rgb_image(img_adjusted)
242 | _image_rgb_to_texture_impl(img_rgb, image_stored_on_gpu.texture_id)
243 | return image_stored_on_gpu.texture_id
244 |
245 |
246 | def _clear_all_cv_textures():
247 | global ALL_TEXTURES
248 | all_textures_updated = {}
249 | textures_to_delete = []
250 | now = timer()
251 | for image_address, image_stored_on_gpu in ALL_TEXTURES.items():
252 | age_seconds = now - image_stored_on_gpu.time_last_access
253 | if age_seconds < 0.3:
254 | all_textures_updated[image_address] = image_stored_on_gpu
255 | else:
256 | textures_to_delete.append(image_stored_on_gpu.texture_id)
257 | ALL_TEXTURES = all_textures_updated
258 | if len(textures_to_delete) > 0:
259 | gl.glDeleteTextures(textures_to_delete)
260 | # print("Delete {0} old texture(s), len={1}".format(len(textures_to_delete), len(ALL_TEXTURES)))
261 |
262 |
263 | def _image_viewport_size(image, width=None, height=None):
264 | image_width = image.shape[1]
265 | image_height = image.shape[0]
266 | if (width is not None) and (height is not None):
267 | viewport_size = SizePixel(width, height)
268 | elif width is not None:
269 | viewport_size = SizePixel(width, round(image_height / image_width * width))
270 | elif height is not None:
271 | viewport_size = SizePixel(round(image_width / image_height * height), height)
272 | else:
273 | viewport_size = SizePixel.from_image(image)
274 | return viewport_size
275 |
276 |
277 | @static_vars(
278 | zoomed_status={},
279 | zoom_click_times={},
280 | last_shown_image=None)
281 | def _image_impl(
282 | image_and_ajustments,
283 | width=None, height=None, title="",
284 | always_refresh = False,
285 | linked_user_image_address: ImageAddress = 0
286 | ):
287 |
288 | statics = _image_impl.statics
289 | statics.last_shown_image = image_and_ajustments
290 | zoom_key = imgui_ext.make_unique_label(title)
291 | if zoom_key not in statics.zoomed_status:
292 | statics.zoom_click_times[zoom_key] = 0
293 | statics.zoomed_status[zoom_key] = False
294 | if statics.zoomed_status[zoom_key]:
295 | viewport_size = SizePixel.from_image(image_and_ajustments.image)
296 | else:
297 | viewport_size = _image_viewport_size(image_and_ajustments.image, width, height)
298 |
299 | if zoom_key not in statics.zoomed_status:
300 | statics.zoomed_status[zoom_key] = False
301 | statics.zoom_click_times[zoom_key] = timer()
302 |
303 | texture_id = _image_to_texture(
304 | image_and_ajustments,
305 | always_refresh = always_refresh,
306 | linked_user_image_address=linked_user_image_address
307 | )
308 | if title == "":
309 | imgui.image_button(texture_id, viewport_size.width, viewport_size.height, frame_padding=0)
310 | is_mouse_hovering = imgui.is_item_hovered()
311 | else:
312 | imgui.begin_group()
313 | imgui.image_button(texture_id, viewport_size.width, viewport_size.height, frame_padding=0)
314 | is_mouse_hovering = imgui.is_item_hovered()
315 | imgui.text(title)
316 | imgui.end_group()
317 |
318 | if is_mouse_hovering and imgui.get_io().mouse_down[0]:
319 | last_time = statics.zoom_click_times[zoom_key]
320 | now = timer()
321 | if now - last_time > 0.3:
322 | statics.zoomed_status[zoom_key] = not statics.zoomed_status[zoom_key]
323 | statics.zoom_click_times[zoom_key] = now
324 |
325 | return mouse_position_last_image()
326 |
327 |
328 | def image(
329 | img,
330 | width=None,
331 | height=None,
332 | title="",
333 | image_adjustments=None,
334 | always_refresh = False,
335 | linked_user_image_address: ImageAddress = 0
336 | ):
337 |
338 | if image_adjustments is None:
339 | image_adjustments = ImageAdjustments()
340 | image_and_ajustments = ImageAndAdjustments(img, image_adjustments)
341 | return _image_impl(
342 | image_and_ajustments,
343 | width=width, height=height,
344 | title=title,
345 | always_refresh = always_refresh,
346 | linked_user_image_address = linked_user_image_address
347 | )
348 |
349 |
350 | def _is_in_image(pixel, image_shape):
351 | # type : (imgui.Vec2, shape) -> Bool
352 | w = image_shape[1]
353 | h = image_shape[0]
354 | x = pixel.x
355 | y = pixel.y
356 | return x >= 0 and x < w and y >= 0 and y < h
357 |
358 |
359 | def _is_in_last_image(pixel):
360 | last_image_shape = _image_impl.statics.last_shown_image.image.shape
361 | return _is_in_image(pixel, last_image_shape)
362 |
363 |
364 | def mouse_position_last_image():
365 | io = imgui.get_io()
366 | mouse = io.mouse_pos
367 | rect_min = imgui.get_item_rect_min()
368 | mouse_relative = imgui.Vec2(mouse.x - rect_min.x, mouse.y - rect_min.y)
369 | if not _is_in_last_image(mouse_relative):
370 | return None
371 | else:
372 | return mouse_relative
373 |
374 |
375 | def is_mouse_hovering_last_image(): # only works if the image was presented in its original size
376 | if not imgui.is_item_hovered_rect():
377 | return False
378 | mouse = mouse_position_last_image()
379 | if mouse is None:
380 | return False
381 | else:
382 | return True
383 |
384 |
385 | def image_explorer(image, width=None, height=None, title="", zoom_key="", hide_buttons=False,
386 | image_adjustments=None,
387 | always_refresh = False
388 | ):
389 | """
390 | :param image_adjustments:
391 | :param hide_buttons:
392 | :param image: opencv / np image.
393 | :param width:
394 | :param height:
395 | :param title: an optional title
396 | :param zoom_key: Set the same zoom_key for two image if you want to link their zoom settings
397 | :return: mouse location in image coordinates (None if the mouse is outside of the image)
398 | """
399 | if image_adjustments is None:
400 | image_adjustments = ImageAdjustments()
401 | from ._imgui_cv_zoom import image_explorer_autostore_zoominfo
402 | viewport_size = _image_viewport_size(image, width, height)
403 | imgui.begin_group()
404 | mouse_location_original_image = image_explorer_autostore_zoominfo(
405 | image,
406 | viewport_size,
407 | title,
408 | zoom_key,
409 | image_adjustments,
410 | hide_buttons=hide_buttons,
411 | always_refresh = always_refresh
412 | )
413 | imgui.end_group()
414 | return mouse_location_original_image
415 |
--------------------------------------------------------------------------------
/imgui_datascience/_imgui_cv_zoom.py:
--------------------------------------------------------------------------------
1 | from __future__ import division
2 | from enum import Enum
3 | import numpy as np
4 | import numpy.linalg
5 | import imgui
6 | from . import imgui_ext
7 | from . import imgui_cv
8 | from .imgui_cv import SizePixel
9 | from .static_vars import *
10 | from typing import *
11 | import cv2
12 | import math
13 | import copy
14 |
15 |
16 | def _is_close(a, b):
17 | return math.fabs(a - b) < 1E-6
18 |
19 |
20 | def compute_zoom_matrix(zoom_center, zoom_ratio):
21 | mat = np.eye(3)
22 | mat[0, 0] = zoom_ratio
23 | mat[1, 1] = zoom_ratio
24 | mat[0, 2] = zoom_center.x * (1. - zoom_ratio)
25 | mat[1, 2] = zoom_center.y * (1. - zoom_ratio)
26 | return mat
27 |
28 |
29 | def compute_pan_matrix(drag_delta, current_zoom):
30 | mat = np.eye(3)
31 | mat[0, 2] = drag_delta.x / current_zoom
32 | mat[1, 2] = drag_delta.y / current_zoom
33 | return mat
34 |
35 |
36 | def m33_to_m23(m):
37 | r = np.zeros((2, 3))
38 | for y in range(0, 2):
39 | for x in range(0, 3):
40 | r[y, x] = m[y, x]
41 | return r
42 |
43 |
44 | class ZoomOrPan(Enum):
45 | Pan = "Pan"
46 | Zoom = "Zoom"
47 |
48 |
49 | class ZoomInfo:
50 | def __init__(self):
51 | self.affine_transform = np.eye(3)
52 | self.zoom_or_pan = ZoomOrPan.Pan
53 | self.last_delta = imgui.Vec2(0., 0.)
54 |
55 | def __eq__(self, other):
56 | equal = True
57 | if not (self.affine_transform == other.affine_transform).all():
58 | equal = False
59 | if not self.last_delta == other.last_delta:
60 | equal = False
61 | if not self.zoom_or_pan == other.zoom_or_pan:
62 | equal = False
63 | return equal
64 |
65 | def set_scale_one(self, image_size, viewport_size):
66 | self.affine_transform = np.eye(3)
67 | self.affine_transform[0, 2] = (viewport_size.width / 2 - image_size.width / 2)
68 | self.affine_transform[1, 2] = (viewport_size.height / 2 - image_size.height / 2)
69 |
70 | def set_full_view(self, image_size, viewport_size):
71 | k_image = image_size.width / image_size.height
72 | k_viewport = float(viewport_size.width) / float(viewport_size.height)
73 | if k_image > k_viewport:
74 | zoom = float(viewport_size.width) / float(image_size.width)
75 | else:
76 | zoom = float(viewport_size.height) / float(image_size.height)
77 | self.affine_transform = np.eye(3)
78 | self.affine_transform[0, 0] = zoom
79 | self.affine_transform[1, 1] = zoom
80 |
81 | @staticmethod
82 | def make_full_view(image_size, viewport_size):
83 | _ = ZoomInfo()
84 | _.set_full_view(image_size, viewport_size)
85 | return _
86 |
87 | def mouse_location_original_image(self, mouse_location): # -> imgui.Vec2:
88 | mouse2 = np.array([[mouse_location.x], [mouse_location.y], [1.]])
89 | pt_original = np.dot(numpy.linalg.inv(self.affine_transform), mouse2)
90 | return imgui.Vec2(pt_original[0, 0], pt_original[1, 0])
91 |
92 |
93 | class ImageWithZoomInfo:
94 | def __init__(self, image, viewport_size, zoom_info=None, hide_buttons=False,
95 | image_adjustments=None):
96 | if image_adjustments is None:
97 | image_adjustments = imgui_cv.ImageAdjustments()
98 | self.image = image
99 | self.original_viewport_size = viewport_size
100 | self.force_viewport_size = False
101 | self.hide_buttons = hide_buttons
102 | self.image_adjustments = image_adjustments
103 | self.show_adjustments = False
104 | self.filename = ""
105 | self.zoom_info = ZoomInfo()
106 |
107 | if zoom_info is None:
108 | self.reset_zoom_info()
109 | else:
110 | self.zoom_info = zoom_info
111 |
112 | def can_show_big_viewport(self):
113 | s1 = SizePixel.from_image(self.image)
114 | s2 = self.original_viewport_size
115 | return (s1.width != s2.width) or (s1.height != s2.height)
116 |
117 | def is_not_full_view(self):
118 | fullview_affine_transform = ZoomInfo.make_full_view(
119 | SizePixel.from_image(self.image), self.current_viewport_size()).affine_transform
120 | current_affine_transform = self.zoom_info.affine_transform
121 | diff = np.absolute(fullview_affine_transform - current_affine_transform).max()
122 | return diff > 1E-6
123 |
124 | def reset_zoom_info(self):
125 | self.zoom_info.set_full_view(SizePixel.from_image(self.image), self.current_viewport_size())
126 |
127 | def current_viewport_size(self):
128 | if self.force_viewport_size:
129 | return SizePixel.from_image(self.image)
130 | else:
131 | return self.original_viewport_size
132 |
133 | def get_force_viewport_size(self):
134 | return self.force_viewport_size
135 |
136 | def set_force_viewport_size(self, value):
137 | self.force_viewport_size = value
138 | self.reset_zoom_info()
139 |
140 | def zoomed_image(self):
141 | m = m33_to_m23(self.zoom_info.affine_transform)
142 | zoomed = cv2.warpAffine(self.image, m, self.current_viewport_size().as_tuple_width_height(),
143 | flags=cv2.INTER_NEAREST)
144 | return zoomed
145 |
146 | def viewport_center_original_image(self): # -> imgui.Vec2:
147 | center = np.array([[self.current_viewport_size().width / 2.], [self.current_viewport_size().height / 2.], [1.]])
148 | center_original = np.dot(
149 | numpy.linalg.inv(self.zoom_info.affine_transform),
150 | center)
151 | return imgui.Vec2(center_original[0, 0], center_original[1, 0])
152 |
153 |
154 | def _display_zoom_or_pan_buttons(im): # : ImageWithZoomInfo):
155 | # display zoom or pan radio buttons
156 | current_mode = im.zoom_info.zoom_or_pan
157 | if imgui.radio_button(imgui_ext.make_unique_label("drag to pan"), current_mode == ZoomOrPan.Pan):
158 | im.zoom_info.zoom_or_pan = ZoomOrPan.Pan
159 | imgui.same_line()
160 | if imgui.radio_button(imgui_ext.make_unique_label("drag to zoom"), current_mode == ZoomOrPan.Zoom):
161 | im.zoom_info.zoom_or_pan = ZoomOrPan.Zoom
162 |
163 |
164 | def color_msg(color):
165 | msg = ""
166 | if isinstance(color, np.uint8):
167 | msg = "{0}".format(color)
168 | elif isinstance(color, np.float32):
169 | msg = "{0:.3f}".format(color)
170 | elif isinstance(color, np.float64):
171 | msg = "{0:.3f}".format(color)
172 | else:
173 | if len(color) == 3:
174 | bgr = color
175 | imgui.color_button("", bgr[2] / 255., bgr[1] / 255., bgr[0] / 255.)
176 | imgui.same_line()
177 | msg = "RGB({0},{1},{2})".format(bgr[2], bgr[1], bgr[0])
178 | elif len(color) == 4:
179 | bgra = color
180 | imgui.color_button("", bgra[2] / 255., bgra[1] / 255., bgra[0] / 255., bgra[3])
181 | imgui.same_line()
182 | msg = "RGBA({0},{1},{2},{3})".format(bgra[2], bgra[1], bgra[0], bgra[3])
183 | return msg
184 |
185 |
186 | # noinspection PyArgumentList,PyArgumentList
187 | def image_explorer_impl(
188 | im: ImageWithZoomInfo, title:str = "", always_refresh:bool = False) \
189 | -> Optional[imgui.Vec2]:
190 |
191 | """
192 | :return: imgui.Vec2 (mouse_location_original_image) or None (if not on image)
193 | """
194 | linked_user_image_address = id(im.image)
195 |
196 | if im.image.size == 0:
197 | imgui.text("empty image !")
198 | return imgui.Vec2(0, 0)
199 |
200 | zoomed_image = im.zoomed_image()
201 |
202 | if not im.hide_buttons:
203 | _display_zoom_or_pan_buttons(im)
204 | if title != "":
205 | imgui.same_line()
206 | imgui.text(" " + title)
207 | mouse_location = imgui_cv.image(
208 | zoomed_image,
209 | image_adjustments=im.image_adjustments,
210 | always_refresh=always_refresh,
211 | linked_user_image_address=linked_user_image_address
212 | )
213 | mouse_location_original_image = None
214 | viewport_center_original_image = im.viewport_center_original_image()
215 |
216 | if not im.hide_buttons and mouse_location is not None:
217 | mouse_drag_button = 0
218 | is_mouse_dragging = imgui.is_mouse_dragging(mouse_drag_button) and imgui.is_item_hovered()
219 | drag_delta = imgui.get_mouse_drag_delta(mouse_drag_button)
220 |
221 | mouse_location_original_image = im.zoom_info.mouse_location_original_image(mouse_location)
222 |
223 | # Handle dragging / zoom or pan
224 | if not is_mouse_dragging:
225 | im.zoom_info.last_delta = imgui.Vec2(0, 0)
226 | if is_mouse_dragging:
227 | drag_delta_delta = imgui.Vec2(drag_delta.x - im.zoom_info.last_delta.x,
228 | drag_delta.y - im.zoom_info.last_delta.y)
229 |
230 | if im.zoom_info.zoom_or_pan == ZoomOrPan.Zoom:
231 | k = 1.03
232 | if drag_delta.y < 0:
233 | zoom_ratio = k
234 | else:
235 | zoom_ratio = 1. / k
236 | im.zoom_info.affine_transform = np.dot(
237 | im.zoom_info.affine_transform,
238 | compute_zoom_matrix(mouse_location_original_image, zoom_ratio))
239 |
240 | if im.zoom_info.zoom_or_pan == ZoomOrPan.Pan:
241 | im.zoom_info.affine_transform = np.dot(
242 | im.zoom_info.affine_transform,
243 | compute_pan_matrix(drag_delta_delta, im.zoom_info.affine_transform[0, 0])
244 | )
245 |
246 | im.zoom_info.last_delta = drag_delta
247 |
248 | # Zoom & Pan buttons
249 |
250 | def perform_zoom(ratio):
251 | im.zoom_info.affine_transform = np.dot(
252 | im.zoom_info.affine_transform,
253 | compute_zoom_matrix(viewport_center_original_image, ratio)
254 | )
255 |
256 | import functools
257 | perform_zoom_plus = functools.partial(perform_zoom, 1.25)
258 | perform_zoom_minus = functools.partial(perform_zoom, 1. / 1.25)
259 |
260 | def perform_scale_one():
261 | im.zoom_info.set_scale_one(SizePixel.from_image(im.image), im.current_viewport_size())
262 |
263 | def perform_full_view():
264 | im.zoom_info.set_full_view(SizePixel.from_image(im.image), im.current_viewport_size())
265 |
266 | def perform_force_viewport_size():
267 | im.set_force_viewport_size(True)
268 |
269 | def perform_reset_viewport_size():
270 | im.set_force_viewport_size(False)
271 |
272 | def perform_hide_buttons():
273 | im.hide_buttons = True
274 |
275 | def perform_show_buttons():
276 | im.hide_buttons = False
277 |
278 | def show_zoom_button(name, action, same_line=True):
279 | if imgui.small_button(imgui_ext.make_unique_label(name)):
280 | action()
281 | if same_line:
282 | imgui.same_line()
283 |
284 | if im.hide_buttons:
285 | show_zoom_button("+", perform_show_buttons, False)
286 | imgui.same_line()
287 | imgui.text(title)
288 | else:
289 | show_zoom_button("-", perform_hide_buttons)
290 | if not im.hide_buttons:
291 | show_zoom_button("zoom +", perform_zoom_plus)
292 | show_zoom_button("zoom -", perform_zoom_minus)
293 | if im.can_show_big_viewport():
294 | show_zoom_button("scale 1", perform_scale_one)
295 | if im.is_not_full_view():
296 | show_zoom_button("full view", perform_full_view)
297 | if not im.show_adjustments:
298 | if imgui.small_button(imgui_ext.make_unique_label("Adjust")):
299 | im.show_adjustments = True
300 | # adjustments
301 | if im.show_adjustments:
302 | imgui.new_line()
303 | imgui.text("Adjust:")
304 | imgui.same_line()
305 | imgui.push_item_width(80)
306 | # noinspection PyArgumentList
307 | changed, im.image_adjustments.factor = imgui.slider_float(
308 | imgui_ext.make_unique_label("k"), im.image_adjustments.factor, 0., 32., format="%.3f", flags=(1<<4) | (1<<5))
309 | imgui.same_line()
310 | imgui.push_item_width(80)
311 | changed, im.image_adjustments.delta = imgui.slider_float(
312 | imgui_ext.make_unique_label("delta"), im.image_adjustments.delta, 0., 255., format="%.3f", flags=(1<<4) | (1<<5))
313 | imgui.same_line()
314 | if not im.image_adjustments.is_none():
315 | if imgui.small_button(imgui_ext.make_unique_label("reset")):
316 | im.image_adjustments = imgui_cv.ImageAdjustments()
317 | imgui.same_line()
318 | if imgui.small_button(imgui_ext.make_unique_label("hide adjust")):
319 | im.show_adjustments = False
320 | # Show image info
321 | image_type_msg = str(im.image.dtype) + str(im.image.shape)
322 | zoom = im.zoom_info.affine_transform[0, 0]
323 | import math
324 | if not _is_close(zoom, 1):
325 | zoom_msg = "Zoom:{0:.2f} ".format(zoom)
326 | else:
327 | zoom_msg = ""
328 | msg = zoom_msg + image_type_msg
329 | imgui.text(msg)
330 |
331 | if im.can_show_big_viewport():
332 | imgui.same_line()
333 | if im.get_force_viewport_size():
334 | show_zoom_button("reset viewport", perform_reset_viewport_size)
335 | else:
336 | show_zoom_button("fit viewport", perform_force_viewport_size)
337 | imgui.new_line()
338 | # Save button
339 | # imgui.same_line()
340 | imgui.push_item_width(60)
341 | changed, im.filename = imgui.input_text(imgui_ext.make_unique_label(""), im.filename, 1000)
342 | imgui.same_line()
343 | if imgui.small_button(imgui_ext.make_unique_label("save")):
344 | cv2.imwrite(im.filename, im.image)
345 | # Show pixel color info
346 | if mouse_location is not None:
347 | color = zoomed_image[int(round(mouse_location.y)), int(round(mouse_location.x))]
348 |
349 | mouse2 = np.array([[mouse_location.x], [mouse_location.y], [1.]])
350 | pt_original = np.dot(numpy.linalg.inv(im.zoom_info.affine_transform), mouse2)
351 | position_msg = "({0},{1})".format(int(round(pt_original[0, 0])), int(round(pt_original[1, 0])))
352 | imgui.text(position_msg + " " + color_msg(color))
353 | else:
354 | imgui.text("")
355 |
356 | return mouse_location_original_image
357 |
358 |
359 | @static_vars(
360 | all_ImageWithZoomInfo={},
361 |
362 | all_zoom_info={},
363 |
364 | previous_all_zoom_info={},
365 | all_previous_image_adjustments = {}
366 | )
367 | def image_explorer_autostore_zoominfo(
368 | image,
369 | viewport_size,
370 | title,
371 | zoom_key,
372 | image_adjustments,
373 | hide_buttons,
374 | always_refresh
375 | ):
376 | image_address = id(image)
377 |
378 | statics = image_explorer_autostore_zoominfo.statics
379 | image_key = imgui_ext.make_unique_label(title)
380 | if zoom_key == "":
381 | zoom_key = image_key
382 | if viewport_size is None:
383 | viewport_size = SizePixel.from_image(image)
384 | if zoom_key not in statics.all_zoom_info:
385 | statics.all_zoom_info[zoom_key] = ZoomInfo.make_full_view(SizePixel.from_image(image), viewport_size)
386 |
387 | flag_need_store_image = False
388 | if image_key not in statics.all_ImageWithZoomInfo:
389 | flag_need_store_image = True
390 | elif id(statics.all_ImageWithZoomInfo[image_key].image) != id(image):
391 | flag_need_store_image = True
392 |
393 | if flag_need_store_image:
394 | statics.all_ImageWithZoomInfo[image_key] = ImageWithZoomInfo(
395 | image,
396 | viewport_size,
397 | statics.all_zoom_info[zoom_key],
398 | hide_buttons=hide_buttons,
399 | image_adjustments=image_adjustments)
400 |
401 | imageWithZoomInfo = statics.all_ImageWithZoomInfo[image_key]
402 |
403 | def did_user_change_something():
404 | changed = False
405 | if zoom_key not in statics.all_zoom_info \
406 | or image_address not in statics.previous_all_zoom_info:
407 | changed = True
408 | else:
409 | old_zoom = statics.previous_all_zoom_info[image_address]
410 | current_zoom = statics.all_zoom_info[zoom_key]
411 | if old_zoom != current_zoom:
412 | changed = True
413 |
414 | if image_address not in statics.all_previous_image_adjustments:
415 | changed = True
416 | else:
417 | previous_image_adjustments = statics.all_previous_image_adjustments[image_address]
418 | current_image_adjustments = imageWithZoomInfo.image_adjustments
419 | if current_image_adjustments != previous_image_adjustments:
420 | changed = True
421 |
422 | return changed
423 |
424 | if did_user_change_something():
425 | always_refresh = True
426 |
427 | statics.previous_all_zoom_info[image_address] = copy.deepcopy(statics.all_zoom_info[zoom_key])
428 | statics.all_previous_image_adjustments[image_address] = copy.deepcopy(imageWithZoomInfo.image_adjustments)
429 | return image_explorer_impl(statics.all_ImageWithZoomInfo[image_key], title, always_refresh=always_refresh)
430 |
431 |
--------------------------------------------------------------------------------