├── 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 (Py)mGui 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 | --------------------------------------------------------------------------------