├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── globals.py ├── lib ├── __init__.py ├── pylightio.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ └── top_level.txt ├── pylightio │ ├── LICENSE │ ├── __about__.py │ ├── __init__.py │ ├── external │ │ ├── __init__.py │ │ ├── cbor-1.0.0-py3.9.egg-info │ │ │ ├── PKG-INFO │ │ │ ├── SOURCES.txt │ │ │ ├── dependency_links.txt │ │ │ ├── installed-files.txt │ │ │ └── top_level.txt │ │ └── cbor │ │ │ ├── VERSION.py │ │ │ ├── __init__.py │ │ │ ├── cbor.py │ │ │ ├── cbor_rpc_client.py │ │ │ └── tagmap.py │ ├── formats │ │ ├── __init__.py │ │ └── lightfields.py │ ├── lookingglass │ │ ├── __init__.py │ │ ├── devices.py │ │ ├── lightfields.py │ │ └── services.py │ └── managers │ │ ├── __init__.py │ │ ├── devices.py │ │ └── services.py └── wheels │ ├── pynng-0.7.4+dev-cp310-cp310-macosx_10_9_universal2.whl │ ├── pynng-0.7.4+dev-cp311-cp311-macosx_10_9_universal2.whl │ ├── pynng-0.7.4+dev-cp312-cp312-macosx_10_9_universal2.whl │ ├── pynng-0.7.4+dev-cp38-cp38-macosx_10_9_universal2.whl │ └── pynng-0.7.4+dev-cp39-cp39-macosx_10_9_universal2.whl ├── lightfield_render.py ├── lightfield_viewport.py ├── logs └── __init__.py ├── preferences.py ├── presets ├── 001_v80_v384x512.preset ├── 002_v88_v384x512.preset ├── 003_v91_v420x560.preset ├── 004_v96_v384x512.preset ├── 005_v108_v420x560.preset └── __init__.py └── ui.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ["http://lookingglass.refr.cc/alicelg", "https://www.paypal.com/donate?hosted_button_id=N2TKY97VJJL96"] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Platform & Versions (please complete the following information):** 27 | - OS: [e.g. macOS, Windows, Linux] 28 | - Blender Version: [e.g. 3.0] 29 | - Alice/LG Version [e.g. 2.0] 30 | 31 | **Attach log files** 32 | Head over to Blender's addon directory ([Don't know where this is?](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html)), in that directory go to '/AliceLG/logs/', and attach the `pylightio.log` and `alice-lg.log` here: 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature or an enhancement of a feature. 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered (if any)** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # misc 4 | *.DS_Store 5 | /.vs 6 | /tmp 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alice/LG 2.3 - The Blender Add-on for Looking Glass Displays 2 | 3 | ### Let [Alice/LG](https://github.com/regcs/AliceLG/releases) take your Blender artworks through the Looking Glass! This short guide is to get you started. 4 | 5 | ## About the Add-on 6 | This add-on was created for the use of Blender with the Looking Glass holographic displays. I initially developed this add-on privately in my free time because I'm a fan of [Blender](https://www.blender.org/) as well as the amazing new holographic display technology created by the [Looking Glass Factory](https://lookingglassfactory.com/). The version 2.x of Alice/LG was developed in collaboration with the Looking Glass Factory. 7 | 8 | ## Main Features 9 | - integration into the common Blender workflow 10 | - an optional view frustum representing the Looking Glass volume in the scene 11 | - lightfield viewport in the Looking Glass with automatic and manual refresh modes 12 | - render any camera view as single quilt image or quilt animation 13 | - decide if you want to keep the separate view files or just the final quilt 14 | - support for multiple scenes and view layers 15 | - camera & quilt settings are saved with your Blender file 16 | - support for all available Looking Glass displays (including the new Looking Glass Go) 17 | - _Experimental feature:_ Detect incomplete render jobs (see below for details) 18 | 19 | ## System Requirements 20 | - Windows, Linux, or macOS 21 | - [Blender 2.93.6 or later](https://www.blender.org/download/) 22 | - [Looking Glass Bridge or HoloPlay Service (for legacy devices)](https://lookingglassfactory.com/software) 23 | 24 | ## Installation 25 | 26 | 1. [Download](https://lookingglassfactory.com/software) and install the Looking Glass Bridge (or HoloPlay Service) app on your PC or Mac. 27 | 28 | 2. Download the [zip file of the latest release](https://github.com/regcs/AliceLG/releases/) of this add-on. 29 | 30 | 3. Install _Alice/LG_ in Blender: 31 | - Open Blender 32 | - In the main menu, navigate to _Edit → Preferences → Add-ons → Install → Install Add-on_ 33 | - Select the downloaded zip file and click "Install" 34 | - Enable the add-on by activating the check box on the left 35 | - Expand the preferences of the add-on by clicking on the small arrow next to the checkbox 36 | - Click the "Install" button to install required Python modules to the add-on directory 37 | - Restart Blender 38 | 39 | ## How to Use 40 | 41 | The following provides some basic information which is going to help you to get started. But make sure, you also check out the [Getting Started Guide](https://docs.lookingglassfactory.com/artist-tools/blender) as well as the [very helpful tutorial](https://lookingglassfactory.com/tutorial/blender) on the Looking Glass Factory website! 42 | 43 | ### Add-on Controls 44 | 45 | After the installation you find a _Looking Glass_ tab in each Blender viewport. Depending on your current selections, the tab shows up to four control panels & subpanels: 46 | 47 | - **Looking Glass.** Contains the Looking Glass display selection, a view resolution selection, and a button to turn on/off the lightfield window. Furthermore, it has two subpanels: 48 | 49 | - **Camera Setup.** Select one of the cameras in the scene to setup a view for the Looking Glass and the quilt rendering. 50 | 51 | - **Quilt Setup & Rendering.** Controls for starting a quilt render. 52 | 53 | - **Light field Window.** The light field / hologram is displayed on your Looking Glass display via the Looking Glass Bridge (or HoloPlay Service) app in a separate window. In this category you find options to switch between two different modes for the lightfield Window: _Viewport_ and _Quilt Viewer_. It has the following sub-panel: 54 | 55 | - **Shading & Overlay Settings.** If the lightfield window is in _Viewport_ mode, it acts as your holographic Blender viewport. The settings for this (lightfield) viewport are defined here. 56 | 57 | ### Lightfield Window & Viewport 58 | 59 | The lightfield window is the place where the hologram is rendered. It can be opened via a click on the button: _Looking Glass → Lightfield Window_, if you have a Looking Glass connected and Looking Glass Bridge (or HoloPlay Service) is running. The light field window can operate in two modes: 60 | 61 | - **Viewport.** In viewport mode, it basically acts like a normal Blender viewport in the Looking Glass - except that it is holographic. You can choose between _Auto_ and _Manual_ refresh mode: In _Auto_ mode, the hologram is re-rendered everytime something in the scene changes, while in _Manual_ mode the hologram is only rendered if you click the refresh button. _NOTE: Due to some limitations of the rendering pipeline of Blender, this mode can be quite slow (< 5 fps). We are working on improving Blender with regard to this and, hopefully, future versions of Blender will increase the live view performance of Alice/LG._ 62 | 63 | - **Quilt Viewer.** In the quilt viewer mode, you can load or select a rendered quilt image and display it as a hologram in the Looking Glass. So, this mode is basically here to enjoy the fruits of your work. Playing animations is not supported yet. _NOTE: To display the quilt correctly, the correct quilt preset must be selected under _Looking Glass → Resolution_ 64 | 65 | ### Camera Setup & Quilt Rendering 66 | 67 | You can render still scenes and animation frames as complete quilt images. You can render for your currently connected device or for any other Looking Glass: 68 | 69 | **(1) Rendering for your currently connected device** 70 | - select your connected Looking Glass (if not already selected) and a quilt resolution under _Looking Glass → Resolution_ 71 | - select an existing camera in _Looking Glass → Camera Setup → Camera_ or create a new camera by clicking "+" in the same panel 72 | - check the _Looking Glass → Quilt Setup & Rendering → Use Device Settings_ checkbox 73 | - locate the camera to the specific view you would like to render 74 | - adjust the render and post-processing settings in the usual Blender panels (_NOTE: Image dimensions are overwritten by the add-on based on your connected Looking Glass or your manual settings under _Looking Glass → Quilt Setup & Rendering__) 75 | - click on _Looking Glass → Quilt Setup & Rendering → Render Quilt_ or _Looking Glass → Quilt Setup & Rendering → Render Animation Quilt_ in the add-on controls. 76 | 77 | **(2) Rendering for other Looking Glasses** 78 | - select an existing camera in _Looking Glass → Camera Setup → Camera_ or create a new camera by clicking "+" in the same panel 79 | - uncheck the _Looking Glass → Quilt Setup & Rendering → Use Device Settings_ checkbox, if it is checked 80 | - choose the Looking Glass you want to render for from the list _Looking Glass → Quilt Setup & Rendering → Device_ 81 | - choose the quilt preset/resolution you want to render for from the list _Looking Glass → Quilt Setup & Rendering → Quilt 82 | - locate the camera to the specific view you would like to render 83 | - adjust the render and post-processing settings in the usual Blender panels (_NOTE: Image dimensions are overwritten by the add-on based on your connected Looking Glass or your manual settings under _Looking Glass → Quilt Setup & Rendering__) 84 | - click on _Looking Glass → Quilt Setup & Rendering → Render Quilt_ or (if you want to render animation frames) _Looking Glass → Quilt Setup & Rendering → Render Animation Quilt_ in the add-on controls. 85 | 86 | The _Render Quilt_ option will render the different views separately. After the last view has been rendered, a quilt will be automatically assembled. For the _Render Animation Quilt_ option, the same will happen for each frame of the animation, which will result in one quilt per frame. After rendering, the created quilt image or animation frames have to be handled in the same way as normal renders. You can still render normal (non-holographic) images in Blender as you usually do. 87 | 88 | _NOTE: Option (2) can be used even if no Looking Glass is connected._ 89 | 90 | ### Incomplete Render Jobs (Experimental) 91 | 92 | The add-on attempts to detect Blender crashes during quilt rendering as well as quilt animation rendering and prompts you with an option to continue or to discard an incomplete render job the next time you open the crashed file. The successful detection of an incomplete render job and its continuation requires that: 93 | 94 | - the filename of the .blend-file did not change 95 | - the file was saved before starting the render job **OR** no significant changes happened to the setup (e.g., camera position, render settings, etc.) 96 | - the (temporary) view files of the incomplete render job are still on disk and not corrupted 97 | 98 | While the add-on tries to check some basic settings, the user is highly recommended to check if the current render settings fit to the settings used for the incomplete render job before clicking the "Continue" button. 99 | 100 | If you decide to discard the incomplete render jobs, the add-on will try to delete the view files of the incomplete render job. 101 | 102 | _NOTE: This feature is considered to be 'experimental'. It might not detect crashes under all circumstances and might not succeed to continue the rendering always. If you encounter a situation were this feature failed, please submit a detailed bug report._ 103 | 104 | ## Renderfarm Implementation (Experimental) 105 | 106 | The rendering of still holograms and holographic animations can be a time-consuming task. Therefore, this add-on provides a command line mechanism that was created for render farms which want to support the quilt rendering by Alice/LG on their systems. Since it has not been tested on a render farm environment yet, this feature is considered experimental. If you are working at a render farm and need help to implement this mechanism on your system, please [open an issue on the add-on's GitHub repository](https://github.com/regcs/AliceLG/issues). 107 | 108 | ### Basic Command Line Calls 109 | 110 | As a first prerequisite, Alice/LG needs to be installed and activated on the render farm's Blender installation. If you setting up your own local render farm and need to install the dependencies of the add-on, use the following command: 111 | 112 | - `--alicelg-install`: Install the python dependencies of the add-on. 113 | 114 | To initiate a quilt rendering from the command line, there are two main calls which are understood by Alice/LG: 115 | 116 | - `--alicelg-render`: Start the rendering of a single quilt. 117 | - `--alicelg-render-anim`: Start the rendering of a quilt animation. 118 | 119 | Both arguments require that Blender is started in background mode (i.e., using the '-b' command line argument) and with a .blend-file specified, which contains all the necessary render settings. Furthermore, both arguments must be preceded by a `--`. An example, which opens the file 'my_lg_hologram.blend' and starts rendering a single quilt looks like that: 120 | 121 | `blender -b my_lg_hologram.blend -- --alicelg-render` 122 | 123 | ### Additional Parameters 124 | 125 | The add-on also understands some additional parameters to fine-tune the rendering process. These parameters are very similar to Blender's internal command line rendering parameters: 126 | 127 | - `-o` or `--render-output` ``: Set the render path and file name. Automatically disables the "Add Metadata" option. 128 | - `-s` or `--frame-start` ``: Set start to frame ``, supports +/- for relative frames too. 129 | - `-e` or `--frame-end` ``: Set end to frame ``, supports +/- for relative frames too. 130 | - `-j` or `--frame-jump` ``: Set number of frames to step forward after each rendered frame 131 | - `-f` or `--render-frame`: Specify a single frame to render 132 | 133 | **It is important that these arguments are specified after the mandatory `--`** to notify Blender that the arguments are meant for the add-on. An example call which would start Blender in background mode, load the 'my_lg_hologram.blend' file, and render a quilt animation from frame 10 to 24 with the base file name `quilt_anim` would look like this: 134 | 135 | `blender -b my_lg_hologram.blend -- --alicelg-render-anim -o /tmp/quilt_anim.png -s 10 -e 24` 136 | 137 | Another example, which would only render the frame 16 as a single quilt, would like this: 138 | 139 | `blender -b my_lg_hologram.blend -- --alicelg-render -o /tmp/quilt.png -f 16` 140 | 141 | ## License & Dependencies 142 | 143 | The Blender add-on part of this project is licensed under the [GNU GPL v3 License](LICENSE). 144 | 145 | This Blender add-on partially relies on the following GPL-compatible open-source libraries / modules and their dependencies: 146 | 147 | - pyLightIO licensed under Apache Software License 2.0 148 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2021 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # -------------------- DEFINE ADDON ---------------------- 21 | bl_info = { 22 | "name": "Alice/LG", 23 | "author": "Christian Stolze", 24 | "version": (2, 3, 0), 25 | "blender": (2, 93, 6), 26 | "location": "View3D > Looking Glass Tab", 27 | "description": "Alice/LG takes your artworks through the Looking Glass (light field displays)", 28 | "category": "View", 29 | "warning": "", 30 | "doc_url": "https://github.com/regcs/AliceLG/blob/master/README.md", 31 | "tracker_url": "https://github.com/regcs/AliceLG/issues" 32 | } 33 | 34 | 35 | ######################################################## 36 | # Prepare Add-on Initialization 37 | ######################################################## 38 | 39 | # Load System Modules 40 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 41 | import importlib 42 | import sys, platform 43 | 44 | 45 | # Load Globals 46 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 47 | # required for proper reloading of the addon by using F8 48 | try: 49 | 50 | importlib.reload(globals) 51 | 52 | except: 53 | 54 | from .globals import * 55 | 56 | # Debugging Settings 57 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 58 | # this is only for debugging purposes 59 | LookingGlassAddon.debugging_use_dummy_device = False 60 | 61 | # console output: if set to true, the Alice/LG and pyLightIO logger messages 62 | # of all levels are printed to the console. If set to falls, only warnings and 63 | # errors are printed to console. 64 | LookingGlassAddon.debugging_print_pylio_logger_all = False 65 | LookingGlassAddon.debugging_print_internal_logger_all = False 66 | 67 | 68 | 69 | # --------------------- LOGGER ----------------------- 70 | import logging, logging.handlers 71 | 72 | # log uncaught exceptions 73 | # +++++++++++++++++++++++++++++++++++++++++++++ 74 | # define system exception hook for logging 75 | def log_exhook(exc_type, exc_value, exc_traceback): 76 | 77 | # log that an unhandled exception occured in Alice/LG's log file 78 | LookingGlassAddonLogger.critical("An unhandled error occured. Here is the traceback:\n", exc_info=(exc_type, exc_value, exc_traceback)) 79 | 80 | # then continue with the system behavior 81 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 82 | 83 | # overwrite the excepthook 84 | sys.excepthook = log_exhook 85 | 86 | # log file names 87 | # +++++++++++++++++++++++++++++++++++++++++++++ 88 | # this function is by @ranrande from stackoverflow: 89 | # https://stackoverflow.com/a/67213458 90 | def logfile_namer(default_name): 91 | if len(default_name.split(".")) == 2: 92 | base_filename, ext = default_name.split(".") 93 | return f"{base_filename}.{ext}" 94 | 95 | elif len(default_name.split(".")) == 3: 96 | base_filename, ext, date = default_name.split(".") 97 | return f"{base_filename}.{date}.{ext}" 98 | 99 | # logger for pyLightIO 100 | # +++++++++++++++++++++++++++++++++++++++++++++ 101 | # NOTE: This is just to get the logger messages invoked by pyLightIO. 102 | # To log messages for Alice/LG use the logger defined below. 103 | # create logger 104 | logger = logging.getLogger('pyLightIO') 105 | logger.setLevel(logging.DEBUG) 106 | 107 | # create console handler and set level to WARNING 108 | console_handler = logging.StreamHandler() 109 | 110 | if LookingGlassAddon.debugging_print_pylio_logger_all == True: console_handler.setLevel(logging.DEBUG) 111 | elif LookingGlassAddon.debugging_print_pylio_logger_all == False: console_handler.setLevel(logging.WARNING) 112 | 113 | # create timed rotating file handler and set level to debug: Create a new logfile every day and keep the last seven days 114 | logfile_handler = logging.handlers.TimedRotatingFileHandler(LookingGlassAddon.logpath + 'pylightio.log', when="D", interval=1, backupCount=7, encoding='utf-8') 115 | logfile_handler.setLevel(logging.INFO) 116 | logfile_handler.namer = logfile_namer 117 | 118 | # create formatter 119 | console_formatter = logging.Formatter('[%(name)s] [%(levelname)s] %(asctime)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') 120 | logfile_formatter = logging.Formatter('[%(levelname)s] %(asctime)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') 121 | 122 | # add formatter to ch 123 | console_handler.setFormatter(console_formatter) 124 | logfile_handler.setFormatter(logfile_formatter) 125 | 126 | # add console handler to logger 127 | logger.addHandler(console_handler) 128 | logger.addHandler(logfile_handler) 129 | 130 | # logger for Alice/LG 131 | # +++++++++++++++++++++++++++++++++++++++++++++ 132 | # NOTE: This is the addon's own logger. Use it to log messages on different levels. 133 | # create logger 134 | LookingGlassAddonLogger = logging.getLogger('Alice/LG') 135 | LookingGlassAddonLogger.setLevel(logging.DEBUG) 136 | 137 | # create console handler and set level to WARNING 138 | console_handler = logging.StreamHandler() 139 | 140 | if LookingGlassAddon.debugging_print_internal_logger_all == True: console_handler.setLevel(logging.DEBUG) 141 | if LookingGlassAddon.debugging_print_internal_logger_all == False: console_handler.setLevel(logging.INFO) 142 | 143 | # create timed rotating file handler and set level to debug: Create a new logfile every day and keep the last seven days 144 | logfile_handler = logging.handlers.TimedRotatingFileHandler(LookingGlassAddon.logpath + 'alice-lg.log', when="D", interval=1, backupCount=7, encoding='utf-8') 145 | logfile_handler.setLevel(logging.INFO) 146 | logfile_handler.namer = logfile_namer 147 | 148 | # create formatter 149 | console_formatter = logging.Formatter('[%(name)s] [%(levelname)s] %(asctime)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') 150 | logfile_formatter = logging.Formatter('[%(levelname)s] %(asctime)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') 151 | 152 | # add formatter to ch 153 | console_handler.setFormatter(console_formatter) 154 | logfile_handler.setFormatter(logfile_formatter) 155 | 156 | # add console handler to logger 157 | LookingGlassAddonLogger.addHandler(console_handler) 158 | LookingGlassAddonLogger.addHandler(logfile_handler) 159 | 160 | 161 | 162 | # ------------- LOAD INTERNAL MODULES ---------------- 163 | # append the add-on's path to Blender's python PATH 164 | sys.path.insert(0, LookingGlassAddon.path) 165 | sys.path.insert(0, LookingGlassAddon.libpath) 166 | 167 | 168 | 169 | 170 | 171 | 172 | ######################################################## 173 | # Add-on Initialization 174 | ######################################################## 175 | import bpy 176 | from bpy.types import AddonPreferences 177 | from bpy.app.handlers import persistent 178 | 179 | # define add-on name for display purposes 180 | LookingGlassAddon.name = bl_info['name'] + " v" + '.'.join(str(v) for v in bl_info['version']) 181 | 182 | # log a info message 183 | LookingGlassAddonLogger.info("----------------------------------------------") 184 | LookingGlassAddonLogger.info("Initializing '%s' ..." % LookingGlassAddon.name) 185 | LookingGlassAddonLogger.info(" [#] Add-on path: %s" % LookingGlassAddon.path) 186 | 187 | # Check Blender Version 188 | # +++++++++++++++++++++++++++++++++++++++++++++ 189 | # check, if a supported version of Blender is executed 190 | if bpy.app.version < bl_info['blender']: 191 | raise Exception("This version of Blender is not supported by " + bl_info['name'] + ". Please use v" + '.'.join(str(v) for v in bl_info['blender']) + " or higher.") 192 | 193 | 194 | # Load Internal Modules 195 | # +++++++++++++++++++++++++++++++++++++++++++++ 196 | # if NOT all the dependenceis are satisfied, debug will produce log messages. 197 | if not LookingGlassAddon.check_dependecies(debug=LookingGlassAddon.debugging_print_internal_logger_all): 198 | 199 | # reload/import all preferences' related code 200 | try: 201 | 202 | importlib.reload(preferences) 203 | 204 | except: 205 | 206 | from .preferences import * 207 | 208 | else: 209 | 210 | try: 211 | # reload all preferences' related code 212 | importlib.reload(preferences) 213 | 214 | # reload the modal operators for the viewport & quilt rendering 215 | importlib.reload(lightfield_viewport) 216 | importlib.reload(lightfield_render) 217 | 218 | # reload all UI related code 219 | importlib.reload(ui) 220 | 221 | except: 222 | 223 | # import all preferences' related code 224 | from .preferences import * 225 | 226 | # import the modal operators for the viewport & quilt rendering 227 | from .lightfield_viewport import * 228 | from .lightfield_render import * 229 | 230 | # import all UI related code 231 | from .ui import * 232 | 233 | 234 | 235 | 236 | 237 | # ----------------- ADDON INITIALIZATION -------------------- 238 | @persistent 239 | def LookingGlassAddonInitHandler(dummy1, dummy2): 240 | 241 | # update the logger levels according to the preferences 242 | LookingGlassAddon.update_logger_levels(None, None) 243 | 244 | # if NOT all dependencies are satisfied 245 | if not LookingGlassAddon.check_dependecies(): 246 | 247 | # check if Blender is run in background mode 248 | if LookingGlassAddon.background: 249 | 250 | # if the dependencies shall be installed 251 | if '--alicelg-install' in LookingGlassAddon.addon_arguments: 252 | bpy.ops.lookingglass.install_dependencies('EXEC_DEFAULT') 253 | 254 | else: 255 | 256 | # show the preference pane 257 | bpy.ops.preferences.addon_show(module=__package__) 258 | 259 | else: 260 | 261 | # ------------ BPY EXTENSIONS --------------- 262 | # EXTENSION OF BPY TYPES BY USEFULL PROPERTIES 263 | # Addon settings 264 | bpy.types.WindowManager.addon_settings = bpy.props.PointerProperty(type=LookingGlassAddonSettingsWM) 265 | bpy.types.Scene.addon_settings = bpy.props.PointerProperty(type=LookingGlassAddonSettingsScene) 266 | 267 | # Camera settings 268 | bpy.types.Camera.is_lightfield = bpy.props.BoolProperty(default=False, options=set(['HIDDEN'])) 269 | 270 | # ------------ INITIALIZATION --------------- 271 | # check if lockfile exists and set status variable 272 | LookingGlassAddon.has_lockfile = os.path.exists(bpy.path.abspath(LookingGlassAddon.tmp_path + os.path.basename(bpy.data.filepath) + ".lock")) 273 | 274 | # if the loaded file has a lockfile 275 | if LookingGlassAddon.has_lockfile: 276 | 277 | # initialize the RenderSettings 278 | # NOTE: This loads the last render settings from the lockfile 279 | RenderSettings(bpy.context.scene, False, LookingGlassAddon.has_lockfile, (bpy.context.preferences.addons[__package__].preferences.camera_mode == '1'), blocking=LookingGlassAddon.background) 280 | 281 | else: 282 | 283 | # get active device 284 | device = pylio.DeviceManager.get_active() 285 | 286 | # try to find the suitable default quilt preset 287 | if device: preset = pylio.LookingGlassQuilt.formats.find(device.default_quilt_width, device.default_quilt_height, device.default_quilt_rows, device.default_quilt_columns) 288 | 289 | # then update the selected quilt preset from the device's default quilt 290 | if device and preset: 291 | bpy.context.scene.addon_settings.quiltPreset = str(preset) 292 | bpy.context.scene.addon_settings.render_quilt_preset = str(preset) 293 | 294 | elif not (device and preset): 295 | 296 | # fallback solution, if the default quilt is not found: 297 | # We use the Looking Glass Go standard quilt (48 views) 298 | bpy.context.scene.addon_settings.quiltPreset = "5" 299 | bpy.context.scene.addon_settings.render_quilt_preset = "5" 300 | 301 | # check if Blender is run in background mode 302 | if LookingGlassAddon.background: 303 | 304 | # if the current blender session has a file 305 | if bpy.data.filepath != "": 306 | 307 | # if the a quilt shall be rendered 308 | if '--alicelg-render' in LookingGlassAddon.addon_arguments: 309 | bpy.ops.render.quilt('EXEC_DEFAULT', use_multiview=True, blocking=True) 310 | 311 | # if the a quilt shall be rendered 312 | elif '--alicelg-render-anim' in LookingGlassAddon.addon_arguments: 313 | bpy.ops.render.quilt('EXEC_DEFAULT', animation=True, use_multiview=True, blocking=True) 314 | 315 | else: 316 | 317 | # stop and delete old renderers, if they still exist (e.g., after loading a new file) 318 | if LookingGlassAddon.FrustumRenderer is not None: 319 | LookingGlassAddon.FrustumRenderer.stop() 320 | LookingGlassAddon.FrustumRenderer = None 321 | if LookingGlassAddon.ImageBlockRenderer is not None: 322 | LookingGlassAddon.ImageBlockRenderer.stop() 323 | LookingGlassAddon.ImageBlockRenderer = None 324 | if LookingGlassAddon.ViewportBlockRenderer is not None: 325 | LookingGlassAddon.ViewportBlockRenderer.stop() 326 | LookingGlassAddon.ViewportBlockRenderer = None 327 | 328 | # create and start the frustum and the block renderer 329 | LookingGlassAddon.FrustumRenderer = FrustumRenderer() 330 | LookingGlassAddon.ImageBlockRenderer = BlockRenderer() 331 | LookingGlassAddon.ViewportBlockRenderer = BlockRenderer() 332 | 333 | 334 | # start the renderers 335 | LookingGlassAddon.FrustumRenderer.start(bpy.context) 336 | 337 | # get the active window 338 | LookingGlassAddon.BlenderWindow = bpy.context.window 339 | 340 | # if the lightfield window was active 341 | if bpy.context.window_manager.addon_settings.ShowLightfieldWindow == True: 342 | 343 | # for each scene in the file 344 | for scene in bpy.context.blend_data.scenes: 345 | 346 | # set the lightfield window button state to 'deactivated' 347 | window_manager.addon_settings.ShowLightfieldWindow = False 348 | 349 | # if no Looking Glass was detected AND debug mode is not activated 350 | if not pylio.DeviceManager.count() and not LookingGlassAddon.debugging_use_dummy_device: 351 | 352 | # set the "use device" checkbox in quilt setup to False 353 | # (because there is no device we could take the settings from) 354 | bpy.context.scene.addon_settings.render_use_device = False 355 | 356 | # update the Looking Glass camera synchronization 357 | # NOTE: Looks weird, but is a way to trigger update function of the property, 358 | # which sets the app handlers if required. If not done, app handlers may stay 359 | # inactive when a file was loaded although they should be active. 360 | bpy.context.scene.addon_settings.toggleCameraSync = bpy.context.scene.addon_settings.toggleCameraSync 361 | 362 | 363 | 364 | 365 | # ---------- ADDON INITIALIZATION & CLEANUP ------------- 366 | def register(): 367 | 368 | # extract the arguments Blender was called with 369 | try: 370 | index = sys.argv.index("--") + 1 371 | except ValueError: 372 | index = len(sys.argv) 373 | 374 | # separate the passed arguments into Blender arguments (before "--") and 375 | # add-on arguments (after "--") 376 | LookingGlassAddon.blender_arguments = sys.argv[:index] 377 | LookingGlassAddon.addon_arguments = sys.argv[index:] 378 | 379 | # check if Blender is run in background mode 380 | if ('--background' in LookingGlassAddon.blender_arguments or '-b' in LookingGlassAddon.blender_arguments): 381 | 382 | # update the corresponding status variable 383 | LookingGlassAddon.background = True 384 | 385 | # if NOT all dependencies are satisfied 386 | if not LookingGlassAddon.check_dependecies(): 387 | 388 | # register the preferences operators 389 | bpy.utils.register_class(LOOKINGGLASS_OT_install_dependencies) 390 | 391 | # register the preferences panels 392 | bpy.utils.register_class(LOOKINGGLASS_PT_install_dependencies) 393 | 394 | # log info 395 | LookingGlassAddonLogger.info(" [#] Missing dependencies. Please install them in the preference pane or using the 'blender -- --alicelg-install' command line call.") 396 | 397 | # run initialization helper function as app handler 398 | # NOTE: this is needed to run certain modal operators of the addon on startup 399 | # or when a new file is loaded 400 | bpy.app.handlers.load_post.append(LookingGlassAddonInitHandler) 401 | 402 | # for the addon unregistering we need to remember that we started 403 | # in the dependency installer mode 404 | LookingGlassAddon.external_dependecies_installer = True 405 | 406 | else: 407 | 408 | # register all basic operators of the addon 409 | bpy.utils.register_class(LookingGlassAddonSettingsWM) 410 | bpy.utils.register_class(LookingGlassAddonSettingsScene) 411 | bpy.utils.register_class(LOOKINGGLASS_OT_refresh_display_list) 412 | bpy.utils.register_class(LOOKINGGLASS_OT_lightfield_window) 413 | bpy.utils.register_class(LOOKINGGLASS_OT_refresh_lightfield) 414 | bpy.utils.register_class(LOOKINGGLASS_OT_blender_viewport_assign) 415 | bpy.utils.register_class(LOOKINGGLASS_OT_add_camera) 416 | 417 | # Looking Glass quilt rendering 418 | bpy.utils.register_class(LOOKINGGLASS_OT_render_quilt) 419 | 420 | # Looking Glass viewport 421 | bpy.utils.register_class(LOOKINGGLASS_OT_render_viewport) 422 | bpy.utils.register_class(BlockRenderer.LOOKINGGLASS_OT_update_block_renderer) 423 | 424 | keyconfigs_addon = bpy.context.window_manager.keyconfigs.addon 425 | if keyconfigs_addon: 426 | # 3D Viewport 427 | LookingGlassAddon.keymap_view_3d = keyconfigs_addon.keymaps.new(name="3D View", space_type='VIEW_3D') 428 | LookingGlassAddon.keymap_items_view_3d_1 = LookingGlassAddon.keymap_view_3d.keymap_items.new("wm.update_block_renderer", 'MOUSEMOVE', 'ANY') 429 | LookingGlassAddon.keymap_items_view_3d_2 = LookingGlassAddon.keymap_view_3d.keymap_items.new("wm.update_block_renderer", 'LEFTMOUSE', 'ANY') 430 | # Image editor 431 | LookingGlassAddon.keymap_image_editor = keyconfigs_addon.keymaps.new(name="Image", space_type='IMAGE_EDITOR') 432 | LookingGlassAddon.keymap_items_image_editor_1 = LookingGlassAddon.keymap_image_editor.keymap_items.new("wm.update_block_renderer", 'MOUSEMOVE', 'ANY') 433 | LookingGlassAddon.keymap_items_image_editor_2 = LookingGlassAddon.keymap_image_editor.keymap_items.new("wm.update_block_renderer", 'LEFTMOUSE', 'ANY') 434 | 435 | # UI elements 436 | # add-on preferences 437 | bpy.utils.register_class(LOOKINGGLASS_PT_preferences) 438 | # addon panels 439 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_general) 440 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_camera) 441 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_render) 442 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_lightfield) 443 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_overlays_shading) 444 | # addon header buttons: 3D viewport 445 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_blocks_viewport_options) 446 | bpy.utils.register_class(LOOKINGGLASS_HT_button_viewport_blocks) 447 | bpy.types.VIEW3D_HT_header.append(LOOKINGGLASS_HT_button_viewport_blocks.draw_item) 448 | # addon header buttons: image editor 449 | bpy.utils.register_class(LOOKINGGLASS_PT_panel_blocks_imageeditor_options) 450 | bpy.utils.register_class(LOOKINGGLASS_HT_button_imageeditor_blocks) 451 | bpy.types.IMAGE_HT_header.append(LOOKINGGLASS_HT_button_imageeditor_blocks.draw_item) 452 | 453 | # log info 454 | LookingGlassAddonLogger.info(" [#] Registered add-on operators in Blender.") 455 | 456 | # setup the quilt presets 457 | LookingGlassAddon.setupQuiltPresets() 458 | 459 | # run initialization helper function as app handler 460 | # NOTE: this is needed to run certain modal operators of the addon on startup 461 | # or when a new file is loaded 462 | bpy.app.handlers.load_post.append(LookingGlassAddonInitHandler) 463 | 464 | # log info 465 | LookingGlassAddonLogger.info(" [#] Done.") 466 | 467 | # log info 468 | LookingGlassAddonLogger.info("Connecting to Looking Glass Bridge ...") 469 | 470 | # create a service using "Looking Glass Bridge" backend 471 | LookingGlassAddon.service = pylio.ServiceManager.add(pylio.lookingglass.services.LookingGlassBridge, client_name = LookingGlassAddon.name) 472 | 473 | # if a service was added 474 | if type(LookingGlassAddon.service) == pylio.lookingglass.services.LookingGlassBridge: 475 | 476 | # if the service is ready 477 | if LookingGlassAddon.service.is_ready(): 478 | 479 | # log info 480 | LookingGlassAddonLogger.info(" [#] Connected to Looking Glass Bridge version: %s" % LookingGlassAddon.service.get_version()) 481 | 482 | else: 483 | 484 | # log info 485 | LookingGlassAddonLogger.info(" [#] Connection failed.") 486 | 487 | # make the device manager use the created service instance 488 | pylio.DeviceManager.set_service(LookingGlassAddon.service) 489 | 490 | # create a set of emulated devices 491 | # NOTE: This automatically creates an emulated Looking Glass for 492 | # each device type that is defined in pyLightIO. 493 | pylio.DeviceManager.add_emulated() 494 | 495 | # if the service is ready OR dummy devices shall be added 496 | if LookingGlassAddon.service.is_ready() or LookingGlassAddon.debugging_use_dummy_device: 497 | 498 | # refresh the list of connected devices using the active pylio service 499 | pylio.DeviceManager.refresh() 500 | 501 | # if device are connected, make the first one the active one 502 | if LookingGlassAddon.debugging_use_dummy_device: pylio.DeviceManager.set_active(pylio.DeviceManager.to_list(None, None)[0].id) 503 | if pylio.DeviceManager.count(): pylio.DeviceManager.set_active(pylio.DeviceManager.to_list()[0].id) 504 | 505 | 506 | # # prepare the error string from the error code 507 | # if (errco == hpc.client_error.CLIERR_NOSERVICE.value): 508 | # errstr = "Looking Glass Bridge not running" 509 | # 510 | # elif (errco == hpc.client_error.CLIERR_SERIALIZEERR.value): 511 | # errstr = "Client message could not be serialized" 512 | # 513 | # elif (errco == hpc.client_error.CLIERR_VERSIONERR.value): 514 | # errstr = "Incompatible version of Looking Glass Bridge"; 515 | # 516 | # elif (errco == hpc.client_error.CLIERR_PIPEERROR.value): 517 | # errstr = "Interprocess pipe broken" 518 | # 519 | # elif (errco == hpc.client_error.CLIERR_SENDTIMEOUT.value): 520 | # errstr = "Interprocess pipe send timeout" 521 | # 522 | # elif (errco == hpc.client_error.CLIERR_RECVTIMEOUT.value): 523 | # errstr = "Interprocess pipe receive timeout" 524 | # 525 | # else: 526 | # errstr = "Unknown error"; 527 | 528 | 529 | def unregister(): 530 | 531 | # if the a service for display communication is active 532 | if LookingGlassAddon.service: 533 | 534 | # Unregister at Looking Glass Bridge 535 | pylio.ServiceManager.remove(LookingGlassAddon.service) 536 | 537 | # log info 538 | LookingGlassAddonLogger.info("Unregister the addon:") 539 | 540 | # log info 541 | LookingGlassAddonLogger.info(" [#] Stopping frustum and block renderers.") 542 | 543 | # stop the frustum and block renderers 544 | if LookingGlassAddon.FrustumRenderer: LookingGlassAddon.FrustumRenderer.stop() 545 | if LookingGlassAddon.ImageBlockRenderer: LookingGlassAddon.ImageBlockRenderer.stop() 546 | if LookingGlassAddon.ViewportBlockRenderer: LookingGlassAddon.ViewportBlockRenderer.stop() 547 | 548 | # log info 549 | LookingGlassAddonLogger.info(" [#] Removing all registered classes.") 550 | 551 | # if NOT all dependencies are satisfied 552 | if not LookingGlassAddon.check_dependecies() or LookingGlassAddon.external_dependecies_installer: 553 | 554 | # unregister only the preferences 555 | if hasattr(bpy.types, "LOOKINGGLASS_PT_install_dependencies"): bpy.utils.unregister_class(LOOKINGGLASS_PT_install_dependencies) 556 | if hasattr(bpy.types, "LOOKINGGLASS_OT_install_dependencies"): bpy.utils.unregister_class(LOOKINGGLASS_OT_install_dependencies) 557 | if hasattr(bpy.types, "LOOKINGGLASS_PT_preferences"): bpy.utils.unregister_class(LOOKINGGLASS_PT_preferences) 558 | 559 | # remove initialization helper app handler 560 | bpy.app.handlers.load_post.remove(LookingGlassAddonInitHandler) 561 | 562 | else: 563 | 564 | # remove initialization helper app handler 565 | bpy.app.handlers.load_post.remove(LookingGlassAddonInitHandler) 566 | 567 | # unregister all classes of the addon 568 | bpy.utils.unregister_class(LookingGlassAddonSettingsWM) 569 | bpy.utils.unregister_class(LookingGlassAddonSettingsScene) 570 | bpy.utils.unregister_class(LOOKINGGLASS_OT_refresh_display_list) 571 | bpy.utils.unregister_class(LOOKINGGLASS_OT_refresh_lightfield) 572 | bpy.utils.unregister_class(LOOKINGGLASS_OT_lightfield_window) 573 | bpy.utils.unregister_class(LOOKINGGLASS_OT_blender_viewport_assign) 574 | bpy.utils.unregister_class(LOOKINGGLASS_OT_add_camera) 575 | 576 | # Looking Glass quilt rendering 577 | bpy.utils.unregister_class(LOOKINGGLASS_OT_render_quilt) 578 | 579 | # remove the keymap 580 | keyconfigs_addon = bpy.context.window_manager.keyconfigs.addon 581 | if keyconfigs_addon: 582 | # 3D Viewport 583 | if LookingGlassAddon.keymap_view_3d: LookingGlassAddon.keymap_view_3d.keymap_items.remove(LookingGlassAddon.keymap_items_view_3d_2) 584 | if LookingGlassAddon.keymap_view_3d: LookingGlassAddon.keymap_view_3d.keymap_items.remove(LookingGlassAddon.keymap_items_view_3d_1) 585 | if LookingGlassAddon.keymap_view_3d: keyconfigs_addon.keymaps.remove(LookingGlassAddon.keymap_view_3d) 586 | # Image editor 587 | if LookingGlassAddon.keymap_image_editor: LookingGlassAddon.keymap_image_editor.keymap_items.remove(LookingGlassAddon.keymap_items_image_editor_2) 588 | if LookingGlassAddon.keymap_image_editor: LookingGlassAddon.keymap_image_editor.keymap_items.remove(LookingGlassAddon.keymap_items_image_editor_1) 589 | if LookingGlassAddon.keymap_image_editor: keyconfigs_addon.keymaps.remove(LookingGlassAddon.keymap_image_editor) 590 | 591 | # Looking Glass viewport 592 | bpy.utils.unregister_class(BlockRenderer.LOOKINGGLASS_OT_update_block_renderer) 593 | bpy.utils.unregister_class(LOOKINGGLASS_OT_render_viewport) 594 | 595 | # UI elements 596 | # addon header buttons 597 | bpy.types.IMAGE_HT_header.remove(LOOKINGGLASS_HT_button_imageeditor_blocks.draw_item) 598 | bpy.types.VIEW3D_HT_header.remove(LOOKINGGLASS_HT_button_viewport_blocks.draw_item) 599 | if hasattr(bpy.types, "LOOKINGGLASS_HT_button_viewport_blocks"): bpy.utils.unregister_class(LOOKINGGLASS_HT_button_viewport_blocks) 600 | if hasattr(bpy.types, "LOOKINGGLASS_HT_button_imageeditor_blocks"): bpy.utils.unregister_class(LOOKINGGLASS_HT_button_imageeditor_blocks) 601 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_blocks_viewport_options"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_blocks_viewport_options) 602 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_blocks_imageeditor_options"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_blocks_imageeditor_options) 603 | # addon panels 604 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_general"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_general) 605 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_camera"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_camera) 606 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_render"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_render) 607 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_lightfield"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_lightfield) 608 | if hasattr(bpy.types, "LOOKINGGLASS_PT_panel_overlays_shading"): bpy.utils.unregister_class(LOOKINGGLASS_PT_panel_overlays_shading) 609 | # preferences 610 | bpy.utils.unregister_class(LOOKINGGLASS_PT_preferences) 611 | # delete all variables 612 | if hasattr(bpy.types.Scene, "addon_settings"): del bpy.types.Scene.addon_settings 613 | 614 | 615 | # log info 616 | LookingGlassAddonLogger.info(" [#] Unloading the python dependencies.") 617 | 618 | # unload all libraries 619 | LookingGlassAddon.unload_dependecies() 620 | 621 | # log info 622 | LookingGlassAddonLogger.info(" [#] Shutting down the loggers.") 623 | 624 | # shut down both loggers (pylightio and Alice/LG) 625 | logger.handlers.clear() 626 | LookingGlassAddonLogger.handlers.clear() 627 | logging.shutdown() 628 | 629 | # log info 630 | LookingGlassAddonLogger.info(" [#] Done.") 631 | -------------------------------------------------------------------------------- /globals.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2021 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # MODULE DESCRIPTION: 21 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 22 | # This includes all global variables that need to be accessable from all files 23 | 24 | # ------------------- EXTERNAL MODULES ------------------- 25 | import bpy 26 | import sys, os, json 27 | from bpy.props import FloatProperty, PointerProperty 28 | from bpy.app.handlers import persistent 29 | 30 | # ---------------- GLOBAL ADDON LOGGER ------------------- 31 | import logging 32 | LookingGlassAddonLogger = logging.getLogger('Alice/LG') 33 | 34 | 35 | # ------------ GLOBAL VARIABLES --------------- 36 | # CLASS USED FOR THE IMPORTANT GLOBAL VARIABLES AND LISTS IN THIS ADDON 37 | class LookingGlassAddon: 38 | 39 | # this is only for debugging purposes 40 | debugging_use_dummy_device = False 41 | 42 | # console output: if set to true, the Alice/LG and pyLightIO logger messages 43 | # of all levels are printed to the console. If set to falls, only warnings and 44 | # errors are printed to console. 45 | debugging_print_pylio_logger_all = False 46 | debugging_print_internal_logger_all = False 47 | 48 | # addon name 49 | name = None 50 | 51 | # path to the addon directory 52 | path = bpy.path.abspath(os.path.dirname(os.path.realpath(__file__))) 53 | tmp_path = bpy.path.abspath(path + "/tmp/") 54 | libpath = bpy.path.abspath(path + "/lib/") 55 | logpath = bpy.path.abspath(path + "/logs/") 56 | presetpath = bpy.path.abspath(path + "/presets/") 57 | 58 | # external python dependencies of the add-on 59 | # NOTE: The tuple has the form (import name, install name, install version, install options) 60 | external_dependecies = [ 61 | ('pynng', 'pynng', '', []), 62 | ('cv2', 'opencv-python', '', ['--no-deps']), 63 | ('pylightio', 'pylightio', '', []), 64 | ] 65 | external_dependecies_installer = False 66 | 67 | # Blender arguments 68 | blender_arguments = "" 69 | addon_arguments = "" 70 | background = False 71 | 72 | # the pyLightIO service for display communication 73 | service = None 74 | 75 | # Lockfile 76 | has_lockfile = False 77 | 78 | # Was the frustum drawer initialized? 79 | FrustumInitialized = False 80 | FrustumRenderer = None 81 | 82 | # Was the hologram preview drawer initialized? 83 | ImageBlockRenderer = None 84 | ViewportBlockRenderer = None 85 | 86 | # The active Window and Viewport the user is currently working in 87 | BlenderWindow = None 88 | BlenderViewport = None 89 | 90 | # Rendering status 91 | RenderInvoked = False 92 | RenderAnimation = None 93 | 94 | # keymaps and mouse position 95 | keymap_view_3d = None 96 | keymap_items_view_3d_1 = None 97 | keymap_items_view_3d_2 = None 98 | keymap_image_editor = None 99 | keymap_items_image_editor_1 = None 100 | keymap_items_image_editor_2 = None 101 | mouse_window_x = 0 102 | mouse_window_y = 0 103 | mouse_region_x = 0 104 | mouse_region_y = 0 105 | 106 | 107 | # EXTEND PATH 108 | # +++++++++++++++++++++++++++++++++++++++ 109 | 110 | # append the add-on's path to Blender's python PATH 111 | sys.path.insert(0, libpath) 112 | 113 | 114 | 115 | # ADDON CHECKS 116 | # +++++++++++++++++++++++++++++++++++++++ 117 | # check if the specified module can be found in the "lib" directory 118 | @classmethod 119 | def is_installed(cls, module, debug=False): 120 | import importlib.machinery 121 | import sys 122 | if sys.version_info.major >= 3 or (sys.version_info.major == 3 and sys.version_info.minor >= 8): 123 | from importlib.metadata import version 124 | else: 125 | from importlib_metadata import version 126 | 127 | # extract info 128 | module_name, install_name, install_version, install_options = module 129 | 130 | # try to find the module in the "lib" directory 131 | module_spec = (importlib.machinery.PathFinder().find_spec(module_name, [cls.libpath])) 132 | if module_spec: 133 | if install_version: 134 | 135 | # check if the installed module version fits 136 | version_comparison = [ a >= b for a,b in zip(list(map(int, version(install_name).split('.'))), list(map(int, install_version.split('.'))))] 137 | if all(version_comparison): 138 | if debug: LookingGlassAddonLogger.info(" [#] Found module '%s' v.%s." % (module_name, version(install_name))) 139 | return True 140 | 141 | else: 142 | if debug: LookingGlassAddonLogger.info(" [#] Found module '%s' v.%s, but require version %s." % (module_name, version(install_name), install_version)) 143 | return False 144 | else: 145 | if debug: LookingGlassAddonLogger.info(" [#] Found module '%s' v.%s." % (module_name, version(install_name))) 146 | return True 147 | 148 | if debug: LookingGlassAddonLogger.info(" [#] Could not find module '%s'." % module_name) 149 | return False 150 | 151 | # check if all defined dependencies can be found in the "lib" directory 152 | @classmethod 153 | def check_dependecies(cls, debug=False): 154 | 155 | # status 156 | found_all = True 157 | 158 | # are all modules in the packages list available in the "lib" directory? 159 | for module in cls.external_dependecies: 160 | if not cls.is_installed(module, debug): 161 | found_all = False 162 | 163 | return found_all 164 | 165 | # unload all dependencies 166 | @classmethod 167 | def unload_dependecies(cls): 168 | 169 | # if the addon is not in installer mode 170 | if not LookingGlassAddon.external_dependecies_installer: 171 | 172 | # are all modules in the packages list available in the "lib" directory? 173 | for module in cls.external_dependecies: 174 | 175 | # get names 176 | module_name, install_name, install_version, install_options = module 177 | 178 | # unload the module 179 | del sys.modules[module_name] 180 | #del module_name 181 | 182 | 183 | 184 | # LOOKING GLASS QUILT PRESETS 185 | # +++++++++++++++++++++++++++++++++++++++ 186 | # set up quilt settings 187 | @classmethod 188 | def setupQuiltPresets(cls): 189 | 190 | # TODO: Would be better, if from .lib import pylightio could be called, 191 | # but for some reason that does not import all modules and throws 192 | # "AliceLG.lib.pylio has no attribute 'lookingglass" 193 | import pylightio as pylio 194 | 195 | # read the user-defined quilt presets from the add-on directory 196 | # and add them to the pylio quilt presets 197 | for file_name in sorted(os.listdir(cls.presetpath)): 198 | if file_name.endswith('.preset'): 199 | with open(cls.presetpath + file_name) as preset_file: 200 | pylio.LookingGlassQuilt.formats.add(json.load(preset_file)) 201 | 202 | # Low-resolution preview for faster live-view rendering 203 | # NOTE: Some code parts assume, that this is the last preset in the list. 204 | pylio.LookingGlassQuilt.formats.add({'description': "Low-resolution Preview", 'quilt_width': 1024, 'quilt_height': 1024, 'view_width': 256, 'view_height': 128, 'columns': 4, 'rows': 8, 'total_views': 32, 'hidden': True}) 205 | 206 | 207 | # GLOBAL LIGHTFIELD VIEWPORT DATA 208 | # +++++++++++++++++++++++++++++++++++++++ 209 | # the timeout value that determines when after the last depsgraph update 210 | # the lightfield window is updated with the selected higher resolution 211 | # quilt preset 212 | low_resolution_preview_timout = 0.4 213 | 214 | 215 | # GLOBAL QUILT VIEWER DATA 216 | # +++++++++++++++++++++++++++++++++++++++ 217 | quiltPixels = None 218 | quiltViewerLightfieldImage = None 219 | # TODO: Is there a better way to check for color management setting changes? 220 | quiltViewAsRender = None 221 | quiltImageColorSpaceSetting = None 222 | 223 | 224 | 225 | # GLOBAL ADDON FUNCTIONS 226 | # +++++++++++++++++++++++++++++++++++++++ 227 | # update the logger level 228 | @staticmethod 229 | def update_logger_levels(self, context): 230 | 231 | # update global variables for console output 232 | LookingGlassAddon.debugging_print_pylio_logger_all = bpy.context.preferences.addons[__package__].preferences.console_output 233 | LookingGlassAddon.debugging_print_internal_logger_all = bpy.context.preferences.addons[__package__].preferences.console_output 234 | 235 | # set logger levels according to the add-on preferences 236 | loggers_list = [logging.getLogger('pyLightIO'), logging.getLogger('Alice/LG')] 237 | for logger in loggers_list: 238 | for handler in logger.handlers: 239 | 240 | # if this is the TimedRotatingFileHandler 241 | if type(handler) == logging.handlers.TimedRotatingFileHandler: 242 | 243 | # if the level is DEBUG 244 | if bpy.context.preferences.addons[__package__].preferences.logger_level == '0': 245 | handler.setLevel(logging.DEBUG) 246 | 247 | # if the level is INFO 248 | elif bpy.context.preferences.addons[__package__].preferences.logger_level == '1': 249 | handler.setLevel(logging.INFO) 250 | 251 | # if the level is ERROR 252 | elif bpy.context.preferences.addons[__package__].preferences.logger_level == '2': 253 | handler.setLevel(logging.ERROR) 254 | 255 | 256 | # if this is the StreamHandler 257 | elif type(handler) == logging.StreamHandler: 258 | 259 | # update logger levels 260 | if bpy.context.preferences.addons[__package__].preferences.console_output: 261 | 262 | # if the level is DEBUG 263 | if bpy.context.preferences.addons[__package__].preferences.logger_level == '0': 264 | handler.setLevel(logging.DEBUG) 265 | 266 | # if the level is INFO 267 | elif bpy.context.preferences.addons[__package__].preferences.logger_level == '1': 268 | handler.setLevel(logging.INFO) 269 | 270 | # if the level is ERROR 271 | elif bpy.context.preferences.addons[__package__].preferences.logger_level == '2': 272 | handler.setLevel(logging.ERROR) 273 | 274 | else: 275 | 276 | # deactivate console output 277 | handler.setLevel(logging.CRITICAL + 1) 278 | 279 | # update the lightfield window to display a lightfield on the device 280 | @staticmethod 281 | def update_lightfield_window(window_mode, lightfield_image, flip_views=None, invert=None): 282 | ''' update the lightfield image that is displayed on the current device ''' 283 | ''' window_mode = 0: Lightfield Viewport, window_mode = 1: Quilt Viewer, window_mode = -1: demo quilt ''' 284 | 285 | # append the add-on's path to Blender's python PATH 286 | sys.path.insert(0, LookingGlassAddon.path) 287 | sys.path.insert(0, LookingGlassAddon.libpath) 288 | 289 | # TODO: Would be better, if from .lib import pylightio could be called, 290 | # but for some reason that does not import all modules and throws 291 | # "AliceLG.lib.pylio has no attribute 'lookingglass" 292 | import pylightio as pylio 293 | 294 | # update the variable for the current Looking Glass device 295 | device = pylio.DeviceManager.get_active() 296 | 297 | # if a valid device is connected 298 | if device: 299 | 300 | # if a LightfieldImage was given 301 | if lightfield_image: 302 | 303 | # VIEWPORT MODE 304 | ################################################################## 305 | if window_mode == 0: 306 | 307 | if flip_views is None: flip_views = False 308 | if invert is None: invert = False 309 | 310 | # let the device display the image 311 | if device.service: device.display(lightfield_image, flip_views=flip_views, invert=invert) 312 | 313 | # QUILT VIEWER MODE 314 | ################################################################## 315 | # if the quilt view mode is active AND an image is loaded 316 | elif window_mode == 1: 317 | 318 | if flip_views is None: flip_views = True 319 | if invert is None: invert = False 320 | 321 | # let the device display the image 322 | if device.service: device.display(lightfield_image, flip_views=flip_views, invert=invert) 323 | 324 | # if the demo quilt was requested 325 | elif lightfield_image is None: 326 | 327 | # let the device display the demo quilt 328 | if device.service: device.display(None) 329 | 330 | else: 331 | LookingGlassAddonLogger.error("Could not update the lightfield window. No LightfieldImage was given.") 332 | 333 | 334 | # Internal string cache as workaround for UnicodeDecodeError reported in issue #114 335 | # see: https://blender.stackexchange.com/questions/299978/how-to-fix-unicodedecodeerror 336 | # 337 | # NOTE: https://blender.stackexchange.com/a/245145 338 | # "The downside is the strings in STRING_CACHE will never be GCed (sort of the opposite of the original problem). 339 | # For most people though, I doubt the cache will grow large enough for it to become an issue." 340 | _enum_string_cache = {} 341 | 342 | @classmethod 343 | def enum_string_cache_items(cls, items): 344 | def intern_string(s): 345 | 346 | if not isinstance(s, str): 347 | return s 348 | 349 | if s not in cls._enum_string_cache: 350 | cls._enum_string_cache[s] = s 351 | 352 | return cls._enum_string_cache[s] 353 | 354 | return [tuple(intern_string(s) for s in item) for item in items] 355 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2020 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | 21 | # THIS FILE IS NEEDED FOR PYTHON: 22 | # https://docs.python.org/3/tutorial/modules.html#packages 23 | -------------------------------------------------------------------------------- /lib/pylightio.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: pyLightIO 3 | Version: 1.0.0 4 | Summary: An approach for a python framework to interface lightfield displays using a common API. 5 | Home-page: https://github.com/regcs/pyLightIO 6 | Author: Christian Stolze 7 | Author-email: reg.cs@t-online.de 8 | License: Apache License 2.0 9 | Project-URL: Bug Tracker, https://github.com/regcs/pyLightIO/issues 10 | Platform: UNKNOWN 11 | Classifier: Programming Language :: Python :: 3 12 | Classifier: License :: OSI Approved :: Apache License 2.0 13 | Classifier: Operating System :: OS Independent 14 | Requires-Python: >=3.7 15 | Description-Content-Type: text/markdown 16 | License-File: LICENSE 17 | 18 | # pyLightIO 19 | A python library designed for implementing lightfield display communication in your python applications. 20 | -------------------------------------------------------------------------------- /lib/pylightio.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README.md 3 | pyproject.toml 4 | setup.cfg 5 | setup.py 6 | src/pyLightIO.egg-info/PKG-INFO 7 | src/pyLightIO.egg-info/SOURCES.txt 8 | src/pyLightIO.egg-info/dependency_links.txt 9 | src/pyLightIO.egg-info/top_level.txt 10 | src/pylightio/__init__.py 11 | src/pylightio/_version.py 12 | src/pylightio/formats/__init__.py 13 | src/pylightio/formats/lightfields.py 14 | src/pylightio/lookingglass/__init__.py 15 | src/pylightio/lookingglass/devices.py 16 | src/pylightio/lookingglass/lightfields.py 17 | src/pylightio/lookingglass/services.py 18 | src/pylightio/managers/__init__.py 19 | src/pylightio/managers/devices.py 20 | src/pylightio/managers/services.py -------------------------------------------------------------------------------- /lib/pylightio.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/pylightio.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | pylightio 2 | -------------------------------------------------------------------------------- /lib/pylightio/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/pylightio/__about__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # package metadata 20 | __all__ = [ 21 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 22 | "__email__", "__license__", "__copyright__", 23 | ] 24 | 25 | __title__ = "pylightio" 26 | __summary__ = "My package is something." 27 | __uri__ = "https://github.com/regcs/pyLightIO" 28 | __version__ = "1.0.0" 29 | __author__ = u"Christian Stolze / regcs" 30 | __email__ = "-" 31 | __license__ = "Apache License 2.0" 32 | __copyright__ = "Copyright 2021 %s" % __author__ 33 | -------------------------------------------------------------------------------- /lib/pylightio/__init__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # import metadata 20 | from .__about__ import ( 21 | __author__, __copyright__, __email__, __license__, __summary__, __title__, 22 | __uri__, __version__ 23 | ) 24 | 25 | __all__ = [ 26 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 27 | "__email__", "__license__", "__copyright__", 28 | ] 29 | 30 | # import submodules 31 | from pylightio.formats import * 32 | from pylightio.managers import * 33 | from pylightio.lookingglass import * 34 | 35 | # logging module 36 | import logging 37 | 38 | # THIS CAN BE USED TOO MUTE LIBRARY LOGGER 39 | # OTHERWUSE BY DEFAULT EVENTS WITH LEVEL "WARNING" AND HIGHER WILL BE PRINTED 40 | logging.getLogger('pyLightIO').addHandler(logging.NullHandler()) 41 | -------------------------------------------------------------------------------- /lib/pylightio/external/__init__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | from pylightio.external import * 20 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: cbor 3 | Version: 1.0.0 4 | Summary: RFC 7049 - Concise Binary Object Representation 5 | Home-page: https://bitbucket.org/bodhisnarkva/cbor 6 | Author: Brian Olson 7 | Author-email: bolson@bolson.org 8 | License: Apache 9 | Description: 10 | An implementation of RFC 7049 - Concise Binary Object Representation (CBOR). 11 | 12 | CBOR is comparable to JSON, has a superset of JSON's ability, but serializes to a binary format which is smaller and faster to generate and parse. 13 | 14 | The two primary functions are cbor.loads() and cbor.dumps(). 15 | 16 | This library includes a C implementation which runs 3-5 times faster than the Python standard library's C-accelerated implementanion of JSON. This is also includes a 100% Python implementation. 17 | 18 | Platform: UNKNOWN 19 | Classifier: Development Status :: 5 - Production/Stable 20 | Classifier: Intended Audience :: Developers 21 | Classifier: License :: OSI Approved :: Apache Software License 22 | Classifier: Operating System :: OS Independent 23 | Classifier: Programming Language :: Python :: 2.7 24 | Classifier: Programming Language :: Python :: 3.4 25 | Classifier: Programming Language :: Python :: 3.5 26 | Classifier: Programming Language :: C 27 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 28 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.cfg 2 | setup.py 3 | c/cbor.h 4 | c/cbormodule.c 5 | cbor/VERSION.py 6 | cbor/__init__.py 7 | cbor/cbor.py 8 | cbor/cbor_rpc_client.py 9 | cbor/tagmap.py 10 | cbor.egg-info/PKG-INFO 11 | cbor.egg-info/SOURCES.txt 12 | cbor.egg-info/dependency_links.txt 13 | cbor.egg-info/top_level.txt -------------------------------------------------------------------------------- /lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/installed-files.txt: -------------------------------------------------------------------------------- 1 | ../cbor/VERSION.py 2 | ../cbor/__init__.py 3 | ../cbor/__pycache__/VERSION.cpython-39.pyc 4 | ../cbor/__pycache__/__init__.cpython-39.pyc 5 | ../cbor/__pycache__/cbor.cpython-39.pyc 6 | ../cbor/__pycache__/cbor_rpc_client.cpython-39.pyc 7 | ../cbor/__pycache__/tagmap.cpython-39.pyc 8 | ../cbor/cbor.py 9 | ../cbor/cbor_rpc_client.py 10 | ../cbor/tagmap.py 11 | PKG-INFO 12 | SOURCES.txt 13 | dependency_links.txt 14 | top_level.txt 15 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | cbor 2 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor/VERSION.py: -------------------------------------------------------------------------------- 1 | '1.0.1' 2 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor/__init__.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | try: 4 | # try C library _cbor.so 5 | from ._cbor import loads, dumps, load, dump 6 | except: 7 | # fall back to 100% python implementation 8 | from .cbor import loads, dumps, load, dump 9 | 10 | from .cbor import Tag 11 | from .tagmap import TagMapper, ClassTag, UnknownTagException 12 | from .VERSION import __doc__ as __version__ 13 | 14 | __all__ = [ 15 | 'loads', 'dumps', 'load', 'dump', 16 | 'Tag', 17 | 'TagMapper', 'ClassTag', 'UnknownTagException', 18 | '__version__', 19 | ] 20 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor/cbor.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # This is a modified version of the CBOR python module created by Brian 4 | # Olson. 5 | # 6 | # Original version : https://github.com/brianolson/cbor_py 7 | # 8 | # ##################### Original CBOR Version ######################### 9 | # 10 | # Copyright © 2014-2015 Brian Olson 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); 13 | # you may not use this file except in compliance with the License. 14 | # You may obtain a copy of the License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the License is distributed on an "AS IS" BASIS, 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | # See the License for the specific language governing permissions and 22 | # limitations under the License. 23 | # 24 | # ####################### END LICENSE BLOCK ############################ 25 | 26 | #!python 27 | # -*- Python -*- 28 | 29 | import datetime 30 | import re 31 | import struct 32 | import sys 33 | import numpy as np 34 | 35 | _IS_PY3 = sys.version_info[0] >= 3 36 | 37 | if _IS_PY3: 38 | from io import BytesIO as StringIO 39 | 40 | else: 41 | try: 42 | from cStringIO import StringIO 43 | except: 44 | from StringIO import StringIO 45 | 46 | 47 | CBOR_TYPE_MASK = 0xE0 # top 3 bits 48 | CBOR_INFO_BITS = 0x1F # low 5 bits 49 | 50 | 51 | CBOR_UINT = 0x00 52 | CBOR_NEGINT = 0x20 53 | CBOR_BYTES = 0x40 54 | CBOR_TEXT = 0x60 55 | CBOR_ARRAY = 0x80 56 | CBOR_MAP = 0xA0 57 | CBOR_TAG = 0xC0 58 | CBOR_7 = 0xE0 # float and other types 59 | 60 | CBOR_UINT8_FOLLOWS = 24 # 0x18 61 | CBOR_UINT16_FOLLOWS = 25 # 0x19 62 | CBOR_UINT32_FOLLOWS = 26 # 0x1a 63 | CBOR_UINT64_FOLLOWS = 27 # 0x1b 64 | CBOR_VAR_FOLLOWS = 31 # 0x1f 65 | 66 | CBOR_BREAK = 0xFF 67 | 68 | CBOR_FALSE = (CBOR_7 | 20) 69 | CBOR_TRUE = (CBOR_7 | 21) 70 | CBOR_NULL = (CBOR_7 | 22) 71 | CBOR_UNDEFINED = (CBOR_7 | 23) # js 'undefined' value 72 | 73 | CBOR_FLOAT16 = (CBOR_7 | 25) 74 | CBOR_FLOAT32 = (CBOR_7 | 26) 75 | CBOR_FLOAT64 = (CBOR_7 | 27) 76 | 77 | CBOR_TAG_DATE_STRING = 0 # RFC3339 78 | CBOR_TAG_DATE_ARRAY = 1 # any number type follows, seconds since 1970-01-01T00:00:00 UTC 79 | CBOR_TAG_BIGNUM = 2 # big endian byte string follows 80 | CBOR_TAG_NEGBIGNUM = 3 # big endian byte string follows 81 | CBOR_TAG_DECIMAL = 4 # [ 10^x exponent, number ] 82 | CBOR_TAG_BIGFLOAT = 5 # [ 2^x exponent, number ] 83 | CBOR_TAG_BASE64URL = 21 84 | CBOR_TAG_BASE64 = 22 85 | CBOR_TAG_BASE16 = 23 86 | CBOR_TAG_CBOR = 24 # following byte string is embedded CBOR data 87 | 88 | CBOR_TAG_URI = 32 89 | CBOR_TAG_BASE64URL = 33 90 | CBOR_TAG_BASE64 = 34 91 | CBOR_TAG_REGEX = 35 92 | CBOR_TAG_MIME = 36 # following text is MIME message, headers, separators and all 93 | CBOR_TAG_CBOR_FILEHEADER = 55799 # can open a file with 0xd9d9f7 94 | 95 | _CBOR_TAG_BIGNUM_BYTES = struct.pack('B', CBOR_TAG | CBOR_TAG_BIGNUM) 96 | 97 | # NOTE: This is a modification to the original CBOR: 98 | # - define a global list, that stores all objects that shall be CBOR serialized 99 | # - serialize all in the end to save time 100 | dumps_list = [] 101 | 102 | def dumps_int(val): 103 | global dumps_list 104 | 105 | "return bytes representing int val in CBOR" 106 | if val >= 0: 107 | # CBOR_UINT is 0, so I'm lazy/efficient about not OR-ing it in. 108 | if val <= 23: 109 | dumps_list.append(struct.pack('B', val)) 110 | return 111 | if val <= 0x0ff: 112 | dumps_list.append(struct.pack('BB', CBOR_UINT8_FOLLOWS, val)) 113 | return 114 | if val <= 0x0ffff: 115 | dumps_list.append(struct.pack('!BH', CBOR_UINT16_FOLLOWS, val)) 116 | return 117 | if val <= 0x0ffffffff: 118 | dumps_list.append(struct.pack('!BI', CBOR_UINT32_FOLLOWS, val)) 119 | return 120 | if val <= 0x0ffffffffffffffff: 121 | dumps_list.append(struct.pack('!BQ', CBOR_UINT64_FOLLOWS, val)) 122 | return 123 | outb = _dumps_bignum_to_bytearray(val) 124 | dumps_list.append(_CBOR_TAG_BIGNUM_BYTES + _encode_type_num(CBOR_BYTES, len(outb)) + outb) 125 | return 126 | val = -1 - val 127 | dumps_list.append(_encode_type_num(CBOR_NEGINT, val)) 128 | 129 | 130 | if _IS_PY3: 131 | def _dumps_bignum_to_bytearray(val): 132 | out = [] 133 | while val > 0: 134 | out.insert(0, val & 0x0ff) 135 | val = val >> 8 136 | return bytes(out) 137 | else: 138 | def _dumps_bignum_to_bytearray(val): 139 | out = [] 140 | while val > 0: 141 | out.insert(0, chr(val & 0x0ff)) 142 | val = val >> 8 143 | return b''.join(out) 144 | 145 | 146 | def dumps_float(val): 147 | global dumps_list 148 | 149 | dumps_list.append(struct.pack("!Bd", CBOR_FLOAT64, val)) 150 | return 151 | 152 | 153 | _CBOR_TAG_NEGBIGNUM_BYTES = struct.pack('B', CBOR_TAG | CBOR_TAG_NEGBIGNUM) 154 | 155 | 156 | def _encode_type_num(cbor_type, val): 157 | """For some CBOR primary type [0..7] and an auxiliary unsigned number, return CBOR encoded bytes""" 158 | assert val >= 0 159 | if val <= 23: 160 | return struct.pack('B', cbor_type | val) 161 | if val <= 0x0ff: 162 | return struct.pack('BB', cbor_type | CBOR_UINT8_FOLLOWS, val) 163 | if val <= 0x0ffff: 164 | return struct.pack('!BH', cbor_type | CBOR_UINT16_FOLLOWS, val) 165 | if val <= 0x0ffffffff: 166 | return struct.pack('!BI', cbor_type | CBOR_UINT32_FOLLOWS, val) 167 | if (((cbor_type == CBOR_NEGINT) and (val <= 0x07fffffffffffffff)) or 168 | ((cbor_type != CBOR_NEGINT) and (val <= 0x0ffffffffffffffff))): 169 | return struct.pack('!BQ', cbor_type | CBOR_UINT64_FOLLOWS, val) 170 | if cbor_type != CBOR_NEGINT: 171 | raise Exception("value too big for CBOR unsigned number: {0!r}".format(val)) 172 | outb = _dumps_bignum_to_bytearray(val) 173 | return _CBOR_TAG_NEGBIGNUM_BYTES + _encode_type_num(CBOR_BYTES, len(outb)) + outb 174 | 175 | 176 | if _IS_PY3: 177 | def _is_unicode(val): 178 | return isinstance(val, str) 179 | else: 180 | def _is_unicode(val): 181 | return isinstance(val, unicode) 182 | 183 | 184 | def dumps_string(val, is_text=None, is_bytes=None): 185 | global dumps_list 186 | 187 | import time 188 | start = time.time() 189 | if type(val) == type(bytes()): 190 | is_bytes = True 191 | if _is_unicode(val): 192 | val = val.encode('utf8') 193 | is_text = True 194 | is_bytes = False 195 | if (is_bytes) or not (is_text == True): 196 | dumps_list.append(_encode_type_num(CBOR_BYTES, len(val))) 197 | dumps_list.append(val) 198 | #print("BYTES TOOK: ", (time.time() - start) * 1000) 199 | return 200 | 201 | dumps_list.append(_encode_type_num(CBOR_TEXT, len(val))) 202 | dumps_list.append(val) 203 | #print("STRING TOOK: ", (time.time() - start) * 1000) 204 | return 205 | 206 | 207 | def dumps_memoryview(val): 208 | global dumps_list 209 | import time 210 | start = time.time() 211 | 212 | dumps_list.append(_encode_type_num(CBOR_BYTES, len(val))) 213 | dumps_list.append(val.tobytes()) 214 | #print("MEMORYVIEW BYTES TOOK: ", (time.time() - start) * 1000) 215 | return 216 | 217 | 218 | 219 | def dumps_bitmap(val, image_shape): 220 | global dumps_list 221 | 222 | import time 223 | start = time.time() 224 | 225 | # Bitmap file header 226 | BMP_ID = b"BM" 227 | SIZE_HDR = 14 228 | SIZE_DIB = 40 229 | HEIGHT = image_shape[0] 230 | WIDTH = image_shape[1] 231 | OFFSET = SIZE_HDR+SIZE_DIB 232 | # Bitmap image header 233 | CHANNELS = image_shape[2] 234 | PLANES = 1 235 | BPC = 8 # Bits per component 236 | BPP = CHANNELS*BPC # Bits per pixel 237 | COMPRESSION = 0 238 | SIZE_IMG = WIDTH*HEIGHT*CHANNELS 239 | SIZE_FIL = OFFSET+SIZE_IMG 240 | 241 | head = BMP_ID + struct.pack('IHHIIIIHHIIIIII', SIZE_FIL,0,0,OFFSET,SIZE_DIB,WIDTH,HEIGHT,PLANES,BPP,COMPRESSION,0,0,0,0,0) 242 | 243 | # add header to the CBOR encoding list 244 | dumps_list.append(_encode_type_num(CBOR_BYTES, len(head) + val.size)) 245 | dumps_list.append(head) 246 | 247 | # add zero padding to each row, since this is required by the BMP format 248 | if ((WIDTH * 3) % 4): dumps_list.append(np.pad(val.reshape((HEIGHT, WIDTH * 3)), ((0, 0), (0, 4 - (WIDTH * 3) % 4)), 'constant')) 249 | else: dumps_list.append(val) 250 | 251 | # print("NUMPY CONVERSION WITH SHAPE %s TOOK: %.3f" % (image_shape, (time.time() - start) * 1000)) 252 | return 253 | 254 | 255 | def dumps_array(arr, sort_keys=False): 256 | head = _encode_type_num(CBOR_ARRAY, len(arr)) 257 | parts = [dumps_list.append(dumps(x, sort_keys=sort_keys)) for x in arr] 258 | return head + b''.join(parts) 259 | 260 | 261 | if _IS_PY3: 262 | def dumps_dict(d, sort_keys=False, image_shape=None): 263 | global dumps_list 264 | 265 | import time 266 | start = time.time() 267 | head = _encode_type_num(CBOR_MAP, len(d)) 268 | dumps_list.append(head) 269 | if sort_keys: 270 | for k in sorted(d.keys()): 271 | v = d[k] 272 | dumps(k, sort_keys=sort_keys, image_shape=image_shape) 273 | dumps(v, sort_keys=sort_keys, image_shape=image_shape) 274 | else: 275 | for k,v in d.items(): 276 | dumps(k, sort_keys=sort_keys, image_shape=image_shape) 277 | dumps(v, sort_keys=sort_keys, image_shape=image_shape) 278 | #print("DICT CONVERSION TOOK: ", (time.time() - start) * 1000) 279 | return 280 | else: 281 | def dumps_dict(d, sort_keys=False): 282 | head = _encode_type_num(CBOR_MAP, len(d)) 283 | parts = [head] 284 | if sort_keys: 285 | for k in sorted(d.iterkeys()): 286 | v = d[k] 287 | parts.append(dumps(k, sort_keys=sort_keys)) 288 | parts.append(dumps(v, sort_keys=sort_keys)) 289 | else: 290 | for k,v in d.iteritems(): 291 | parts.append(dumps(k, sort_keys=sort_keys)) 292 | parts.append(dumps(v, sort_keys=sort_keys)) 293 | return b''.join(parts) 294 | 295 | 296 | def dumps_bool(b): 297 | global dumps_list 298 | 299 | if b: 300 | dumps_list.append(struct.pack('B', CBOR_TRUE)) 301 | else: 302 | dumps_list.append(struct.pack('B', CBOR_FALSE)) 303 | 304 | 305 | def dumps_tag(t, sort_keys=False): 306 | return _encode_type_num(CBOR_TAG, t.tag) + dumps(t.value, sort_keys=sort_keys) 307 | 308 | 309 | if _IS_PY3: 310 | def _is_stringish(x): 311 | return isinstance(x, (str, bytes)) 312 | def _is_intish(x): 313 | return isinstance(x, int) 314 | else: 315 | def _is_stringish(x): 316 | return isinstance(x, (str, basestring, bytes, unicode)) 317 | def _is_intish(x): 318 | return isinstance(x, (int, long)) 319 | 320 | 321 | 322 | # this variable is used to prevent nested dumps() calls from serializing too early 323 | # (i.e., only the level = 0 call should serialize the dumps_list) 324 | level = -1 325 | 326 | def dumps(ob, sort_keys=False, image_shape=None): 327 | global dumps_list, level 328 | #print(type(ob)) 329 | 330 | import time 331 | start = time.time() 332 | 333 | if level == -1: 334 | dumps_list.clear() 335 | 336 | # update caller level 337 | level += 1 338 | 339 | if ob is None: 340 | dumps_list.append(struct.pack('B', CBOR_NULL)) 341 | elif isinstance(ob, bool): 342 | result = dumps_bool(ob) 343 | elif _is_stringish(ob): 344 | result = dumps_string(ob) 345 | elif type(ob) == memoryview: 346 | result = dumps_memoryview(ob) 347 | elif type(ob) == np.ndarray and (not image_shape is None): 348 | result = dumps_bitmap(ob, image_shape) 349 | elif isinstance(ob, (list, tuple)): 350 | result = dumps_array(ob, sort_keys=sort_keys) 351 | # TODO: accept other enumerables and emit a variable length array 352 | elif isinstance(ob, dict): 353 | result = dumps_dict(ob, sort_keys=sort_keys, image_shape=image_shape) 354 | elif isinstance(ob, float): 355 | result = dumps_float(ob) 356 | elif _is_intish(ob): 357 | result = dumps_int(ob) 358 | elif isinstance(ob, Tag): 359 | result = dumps_tag(ob, sort_keys=sort_keys) 360 | else: 361 | raise Exception("don't know how to cbor serialize object of type %s", type(ob)) 362 | 363 | 364 | if level == 0: 365 | 366 | # reset caller level 367 | level = -1 368 | 369 | return b''.join(dumps_list) 370 | 371 | #print("COMPLETE CONVERSION TOOK: ", (time.time() - start) * 1000) 372 | else: 373 | level -= 1 374 | 375 | return result 376 | 377 | 378 | # same basic signature as json.dump, but with no options (yet) 379 | def dump(obj, fp, sort_keys=False): 380 | """ 381 | obj: Python object to serialize 382 | fp: file-like object capable of .write(bytes) 383 | """ 384 | # this is kinda lame, but probably not inefficient for non-huge objects 385 | # TODO: .write() to fp as we go as each inner object is serialized 386 | blob = dumps(obj, sort_keys=sort_keys) 387 | fp.write(blob) 388 | 389 | 390 | class Tag(object): 391 | def __init__(self, tag=None, value=None): 392 | self.tag = tag 393 | self.value = value 394 | 395 | def __repr__(self): 396 | return "Tag({0!r}, {1!r})".format(self.tag, self.value) 397 | 398 | def __eq__(self, other): 399 | if not isinstance(other, Tag): 400 | return False 401 | return (self.tag == other.tag) and (self.value == other.value) 402 | 403 | 404 | def loads(data): 405 | """ 406 | Parse CBOR bytes and return Python objects. 407 | """ 408 | if data is None: 409 | raise ValueError("got None for buffer to decode in loads") 410 | fp = StringIO(data) 411 | return _loads(fp)[0] 412 | 413 | 414 | def load(fp): 415 | """ 416 | Parse and return object from fp, a file-like object supporting .read(n) 417 | """ 418 | return _loads(fp)[0] 419 | 420 | 421 | _MAX_DEPTH = 100 422 | 423 | 424 | def _tag_aux(fp, tb): 425 | bytes_read = 1 426 | tag = tb & CBOR_TYPE_MASK 427 | tag_aux = tb & CBOR_INFO_BITS 428 | if tag_aux <= 23: 429 | aux = tag_aux 430 | elif tag_aux == CBOR_UINT8_FOLLOWS: 431 | data = fp.read(1) 432 | aux = struct.unpack_from("!B", data, 0)[0] 433 | bytes_read += 1 434 | elif tag_aux == CBOR_UINT16_FOLLOWS: 435 | data = fp.read(2) 436 | aux = struct.unpack_from("!H", data, 0)[0] 437 | bytes_read += 2 438 | elif tag_aux == CBOR_UINT32_FOLLOWS: 439 | data = fp.read(4) 440 | aux = struct.unpack_from("!I", data, 0)[0] 441 | bytes_read += 4 442 | elif tag_aux == CBOR_UINT64_FOLLOWS: 443 | data = fp.read(8) 444 | aux = struct.unpack_from("!Q", data, 0)[0] 445 | bytes_read += 8 446 | else: 447 | assert tag_aux == CBOR_VAR_FOLLOWS, "bogus tag {0:02x}".format(tb) 448 | aux = None 449 | 450 | return tag, tag_aux, aux, bytes_read 451 | 452 | 453 | def _read_byte(fp): 454 | tb = fp.read(1) 455 | if len(tb) == 0: 456 | # I guess not all file-like objects do this 457 | raise EOFError() 458 | return ord(tb) 459 | 460 | 461 | def _loads_var_array(fp, limit, depth, returntags, bytes_read): 462 | ob = [] 463 | tb = _read_byte(fp) 464 | while tb != CBOR_BREAK: 465 | (subob, sub_len) = _loads_tb(fp, tb, limit, depth, returntags) 466 | bytes_read += 1 + sub_len 467 | ob.append(subob) 468 | tb = _read_byte(fp) 469 | return (ob, bytes_read + 1) 470 | 471 | 472 | def _loads_var_map(fp, limit, depth, returntags, bytes_read): 473 | ob = {} 474 | tb = _read_byte(fp) 475 | while tb != CBOR_BREAK: 476 | (subk, sub_len) = _loads_tb(fp, tb, limit, depth, returntags) 477 | bytes_read += 1 + sub_len 478 | (subv, sub_len) = _loads(fp, limit, depth, returntags) 479 | bytes_read += sub_len 480 | ob[subk] = subv 481 | tb = _read_byte(fp) 482 | return (ob, bytes_read + 1) 483 | 484 | 485 | if _IS_PY3: 486 | def _loads_array(fp, limit, depth, returntags, aux, bytes_read): 487 | ob = [] 488 | for i in range(aux): 489 | subob, subpos = _loads(fp) 490 | bytes_read += subpos 491 | ob.append(subob) 492 | return ob, bytes_read 493 | def _loads_map(fp, limit, depth, returntags, aux, bytes_read): 494 | ob = {} 495 | for i in range(aux): 496 | subk, subpos = _loads(fp) 497 | bytes_read += subpos 498 | subv, subpos = _loads(fp) 499 | bytes_read += subpos 500 | ob[subk] = subv 501 | return ob, bytes_read 502 | else: 503 | def _loads_array(fp, limit, depth, returntags, aux, bytes_read): 504 | ob = [] 505 | for i in xrange(aux): 506 | subob, subpos = _loads(fp) 507 | bytes_read += subpos 508 | ob.append(subob) 509 | return ob, bytes_read 510 | def _loads_map(fp, limit, depth, returntags, aux, bytes_read): 511 | ob = {} 512 | for i in xrange(aux): 513 | subk, subpos = _loads(fp) 514 | bytes_read += subpos 515 | subv, subpos = _loads(fp) 516 | bytes_read += subpos 517 | ob[subk] = subv 518 | return ob, bytes_read 519 | 520 | 521 | def _loads(fp, limit=None, depth=0, returntags=False): 522 | "return (object, bytes read)" 523 | if depth > _MAX_DEPTH: 524 | raise Exception("hit CBOR loads recursion depth limit") 525 | 526 | tb = _read_byte(fp) 527 | 528 | return _loads_tb(fp, tb, limit, depth, returntags) 529 | 530 | def _loads_tb(fp, tb, limit=None, depth=0, returntags=False): 531 | # Some special cases of CBOR_7 best handled by special struct.unpack logic here 532 | if tb == CBOR_FLOAT16: 533 | data = fp.read(2) 534 | hibyte, lowbyte = struct.unpack_from("BB", data, 0) 535 | exp = (hibyte >> 2) & 0x1F 536 | mant = ((hibyte & 0x03) << 8) | lowbyte 537 | if exp == 0: 538 | val = mant * (2.0 ** -24) 539 | elif exp == 31: 540 | if mant == 0: 541 | val = float('Inf') 542 | else: 543 | val = float('NaN') 544 | else: 545 | val = (mant + 1024.0) * (2 ** (exp - 25)) 546 | if hibyte & 0x80: 547 | val = -1.0 * val 548 | return (val, 3) 549 | elif tb == CBOR_FLOAT32: 550 | data = fp.read(4) 551 | pf = struct.unpack_from("!f", data, 0) 552 | return (pf[0], 5) 553 | elif tb == CBOR_FLOAT64: 554 | data = fp.read(8) 555 | pf = struct.unpack_from("!d", data, 0) 556 | return (pf[0], 9) 557 | 558 | tag, tag_aux, aux, bytes_read = _tag_aux(fp, tb) 559 | 560 | if tag == CBOR_UINT: 561 | return (aux, bytes_read) 562 | elif tag == CBOR_NEGINT: 563 | return (-1 - aux, bytes_read) 564 | elif tag == CBOR_BYTES: 565 | ob, subpos = loads_bytes(fp, aux) 566 | return (ob, bytes_read + subpos) 567 | elif tag == CBOR_TEXT: 568 | raw, subpos = loads_bytes(fp, aux, btag=CBOR_TEXT) 569 | ob = raw.decode('utf8') 570 | return (ob, bytes_read + subpos) 571 | elif tag == CBOR_ARRAY: 572 | if aux is None: 573 | return _loads_var_array(fp, limit, depth, returntags, bytes_read) 574 | return _loads_array(fp, limit, depth, returntags, aux, bytes_read) 575 | elif tag == CBOR_MAP: 576 | if aux is None: 577 | return _loads_var_map(fp, limit, depth, returntags, bytes_read) 578 | return _loads_map(fp, limit, depth, returntags, aux, bytes_read) 579 | elif tag == CBOR_TAG: 580 | ob, subpos = _loads(fp) 581 | bytes_read += subpos 582 | if returntags: 583 | # Don't interpret the tag, return it and the tagged object. 584 | ob = Tag(aux, ob) 585 | else: 586 | # attempt to interpet the tag and the value into a Python object. 587 | ob = tagify(ob, aux) 588 | return ob, bytes_read 589 | elif tag == CBOR_7: 590 | if tb == CBOR_TRUE: 591 | return (True, bytes_read) 592 | if tb == CBOR_FALSE: 593 | return (False, bytes_read) 594 | if tb == CBOR_NULL: 595 | return (None, bytes_read) 596 | if tb == CBOR_UNDEFINED: 597 | return (None, bytes_read) 598 | raise ValueError("unknown cbor tag 7 byte: {:02x}".format(tb)) 599 | 600 | 601 | def loads_bytes(fp, aux, btag=CBOR_BYTES): 602 | # TODO: limit to some maximum number of chunks and some maximum total bytes 603 | if aux is not None: 604 | # simple case 605 | ob = fp.read(aux) 606 | return (ob, aux) 607 | # read chunks of bytes 608 | chunklist = [] 609 | total_bytes_read = 0 610 | while True: 611 | tb = fp.read(1)[0] 612 | if not _IS_PY3: 613 | tb = ord(tb) 614 | if tb == CBOR_BREAK: 615 | total_bytes_read += 1 616 | break 617 | tag, tag_aux, aux, bytes_read = _tag_aux(fp, tb) 618 | assert tag == btag, 'variable length value contains unexpected component' 619 | ob = fp.read(aux) 620 | chunklist.append(ob) 621 | total_bytes_read += bytes_read + aux 622 | return (b''.join(chunklist), total_bytes_read) 623 | 624 | 625 | if _IS_PY3: 626 | def _bytes_to_biguint(bs): 627 | out = 0 628 | for ch in bs: 629 | out = out << 8 630 | out = out | ch 631 | return out 632 | else: 633 | def _bytes_to_biguint(bs): 634 | out = 0 635 | for ch in bs: 636 | out = out << 8 637 | out = out | ord(ch) 638 | return out 639 | 640 | 641 | def tagify(ob, aux): 642 | # TODO: make this extensible? 643 | # cbor.register_tag_handler(tagnumber, tag_handler) 644 | # where tag_handler takes (tagnumber, tagged_object) 645 | if aux == CBOR_TAG_DATE_STRING: 646 | # TODO: parse RFC3339 date string 647 | pass 648 | if aux == CBOR_TAG_DATE_ARRAY: 649 | return datetime.datetime.utcfromtimestamp(ob) 650 | if aux == CBOR_TAG_BIGNUM: 651 | return _bytes_to_biguint(ob) 652 | if aux == CBOR_TAG_NEGBIGNUM: 653 | return -1 - _bytes_to_biguint(ob) 654 | if aux == CBOR_TAG_REGEX: 655 | # Is this actually a good idea? Should we just return the tag and the raw value to the user somehow? 656 | return re.compile(ob) 657 | return Tag(aux, ob) 658 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor/cbor_rpc_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import logging 3 | import random 4 | import socket 5 | import time 6 | 7 | import cbor 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class SocketReader(object): 14 | ''' 15 | Simple adapter from socket.recv to file-like-read 16 | ''' 17 | def __init__(self, sock): 18 | self.socket = sock 19 | self.timeout_seconds = 10.0 20 | 21 | def read(self, num): 22 | start = time.time() 23 | data = self.socket.recv(num) 24 | while len(data) < num: 25 | now = time.time() 26 | if now > (start + self.timeout_seconds): 27 | break 28 | ndat = self.socket.recv(num - len(data)) 29 | if ndat: 30 | data += ndat 31 | return data 32 | 33 | 34 | class CborRpcClient(object): 35 | '''Base class for all client objects. 36 | 37 | This provides common `addr_family`, `address`, and `registry_addresses` 38 | configuration parameters, and manages the connection back to the server. 39 | 40 | Automatic retry and time based fallback is managed from 41 | configuration parameters `retries` (default 5), and 42 | `base_retry_seconds` (default 0.5). Retry time doubles on each 43 | retry. E.g. try 0; wait 0.5s; try 1; wait 1s; try 2; wait 2s; try 44 | 3; wait 4s; try 4; wait 8s; try 5; FAIL. Total time waited just 45 | under base_retry_seconds * (2 ** retries). 46 | 47 | .. automethod:: __init__ 48 | .. automethod:: _rpc 49 | .. automethod:: close 50 | 51 | ''' 52 | 53 | def __init__(self, config=None): 54 | self._socket_family = config.get('addr_family', socket.AF_INET) 55 | # may need to be ('host', port) 56 | self._socket_addr = config.get('address') 57 | if self._socket_family == socket.AF_INET: 58 | if not isinstance(self._socket_addr, tuple): 59 | # python socket standard library insists this be tuple! 60 | tsocket_addr = tuple(self._socket_addr) 61 | assert len(tsocket_addr) == 2, 'address must be length-2 tuple ("hostname", port number), got {!r} tuplified to {!r}'.format(self._socket_addr, tsocket_addr) 62 | self._socket_addr = tsocket_addr 63 | self._socket = None 64 | self._rfile = None 65 | self._local_addr = None 66 | self._message_count = 0 67 | self._retries = config.get('retries', 5) 68 | self._base_retry_seconds = float(config.get('base_retry_seconds', 0.5)) 69 | 70 | def _conn(self): 71 | # lazy socket opener 72 | if self._socket is None: 73 | try: 74 | self._socket = socket.create_connection(self._socket_addr) 75 | self._local_addr = self._socket.getsockname() 76 | except: 77 | logger.error('error connecting to %r:%r', self._socket_addr[0], 78 | self._socket_addr[1], exc_info=True) 79 | raise 80 | return self._socket 81 | 82 | def close(self): 83 | '''Close the connection to the server. 84 | 85 | The next RPC call will reopen the connection. 86 | 87 | ''' 88 | if self._socket is not None: 89 | self._rfile = None 90 | try: 91 | self._socket.shutdown(socket.SHUT_RDWR) 92 | self._socket.close() 93 | except socket.error: 94 | logger.warn('error closing lockd client socket', 95 | exc_info=True) 96 | self._socket = None 97 | 98 | @property 99 | def rfile(self): 100 | if self._rfile is None: 101 | conn = self._conn() 102 | self._rfile = SocketReader(conn) 103 | return self._rfile 104 | 105 | def _rpc(self, method_name, params): 106 | '''Call a method on the server. 107 | 108 | Calls ``method_name(*params)`` remotely, and returns the results 109 | of that function call. Expected return types are primitives, lists, 110 | and dictionaries. 111 | 112 | :raise Exception: if the server response was a failure 113 | 114 | ''' 115 | mlog = logging.getLogger('cborrpc') 116 | tryn = 0 117 | delay = self._base_retry_seconds 118 | self._message_count += 1 119 | message = { 120 | 'id': self._message_count, 121 | 'method': method_name, 122 | 'params': params 123 | } 124 | mlog.debug('request %r', message) 125 | buf = cbor.dumps(message) 126 | 127 | errormessage = None 128 | while True: 129 | try: 130 | conn = self._conn() 131 | conn.send(buf) 132 | response = cbor.load(self.rfile) 133 | mlog.debug('response %r', response) 134 | assert response['id'] == message['id'] 135 | if 'result' in response: 136 | return response['result'] 137 | # From here on out we got a response, the server 138 | # didn't have some weird intermittent error or 139 | # non-connectivity, it gave us an error message. We 140 | # don't retry that, we raise it to the user. 141 | errormessage = response.get('error') 142 | if errormessage and hasattr(errormessage,'get'): 143 | errormessage = errormessage.get('message') 144 | if not errormessage: 145 | errormessage = repr(response) 146 | break 147 | except Exception as ex: 148 | if tryn < self._retries: 149 | tryn += 1 150 | logger.debug('ex in %r (%s), retrying %s in %s sec...', 151 | method_name, ex, tryn, delay, exc_info=True) 152 | self.close() 153 | time.sleep(delay) 154 | delay *= 2 155 | continue 156 | logger.error('failed in rpc %r %r', method_name, params, 157 | exc_info=True) 158 | raise 159 | raise Exception(errormessage) 160 | 161 | 162 | if __name__ == '__main__': 163 | import sys 164 | logging.basicConfig(level=logging.DEBUG) 165 | host,port = sys.argv[1].split(':') 166 | if not host: 167 | host = 'localhost' 168 | port = int(port) 169 | client = CborRpcClient({'address':(host,port)}) 170 | print(client._rpc(u'connect', [u'127.0.0.1:5432', u'root', u'aoeu'])) 171 | print(client._rpc(u'put', [[('k1','v1'), ('k2','v2')]])) 172 | #print(client._rpc(u'ping', [])) 173 | #print(client._rpc(u'gnip', [])) 174 | client.close() 175 | 176 | -------------------------------------------------------------------------------- /lib/pylightio/external/cbor/tagmap.py: -------------------------------------------------------------------------------- 1 | try: 2 | # try C library _cbor.so 3 | from ._cbor import loads, dumps, load, dump 4 | except: 5 | # fall back to 100% python implementation 6 | from .cbor import loads, dumps, load, dump 7 | 8 | from .cbor import Tag, CBOR_TAG_CBOR, _IS_PY3 9 | 10 | 11 | class ClassTag(object): 12 | ''' 13 | For some CBOR tag_number, encode/decode Python class_type. 14 | class_type manily used for isintance(foo, class_type) 15 | Call encode_function() taking a Python instance and returning CBOR primitive types. 16 | Call decode_function() on CBOR primitive types and return an instance of the Python class_type (a factory function). 17 | ''' 18 | def __init__(self, tag_number, class_type, encode_function, decode_function): 19 | self.tag_number = tag_number 20 | self.class_type = class_type 21 | self.encode_function = encode_function 22 | self.decode_function = decode_function 23 | 24 | 25 | # TODO: This would be more efficient if it moved into cbor.py and 26 | # cbormodule.c, happening inline so that there is only one traversal 27 | # of the objects. But that would require two implementations. When 28 | # this API has been used more and can be considered settled I should 29 | # do that. -- Brian Olson 20140917_172229 30 | class TagMapper(object): 31 | ''' 32 | Translate Python objects and CBOR tagged data. 33 | Use the CBOR TAG system to note that some data is of a certain class. 34 | Dump while translating Python objects into a CBOR compatible representation. 35 | Load and translate CBOR primitives back into Python objects. 36 | ''' 37 | def __init__(self, class_tags=None, raise_on_unknown_tag=False): 38 | ''' 39 | class_tags: list of ClassTag objects 40 | ''' 41 | self.class_tags = class_tags 42 | self.raise_on_unknown_tag = raise_on_unknown_tag 43 | 44 | def encode(self, obj): 45 | for ct in self.class_tags: 46 | if (ct.class_type is None) or (ct.encode_function is None): 47 | continue 48 | if isinstance(obj, ct.class_type): 49 | return Tag(ct.tag_number, ct.encode_function(obj)) 50 | if isinstance(obj, (list, tuple)): 51 | return [self.encode(x) for x in obj] 52 | if isinstance(obj, dict): 53 | # assume key is a primitive 54 | # can't do this in Python 2.6: 55 | #return {k:self.encode(v) for k,v in obj.iteritems()} 56 | out = {} 57 | if _IS_PY3: 58 | items = obj.items() 59 | else: 60 | items = obj.iteritems() 61 | for k,v in items: 62 | out[k] = self.encode(v) 63 | return out 64 | # fall through, let underlying cbor.dump decide if it can encode object 65 | return obj 66 | 67 | def decode(self, obj): 68 | if isinstance(obj, Tag): 69 | for ct in self.class_tags: 70 | if ct.tag_number == obj.tag: 71 | return ct.decode_function(obj.value) 72 | # unknown Tag 73 | if self.raise_on_unknown_tag: 74 | raise UnknownTagException(str(obj.tag)) 75 | # otherwise, pass it through 76 | return obj 77 | if isinstance(obj, list): 78 | # update in place. cbor only decodes to list, not tuple 79 | for i,v in enumerate(obj): 80 | obj[i] = self.decode(v) 81 | return obj 82 | if isinstance(obj, dict): 83 | # update in place 84 | if _IS_PY3: 85 | items = obj.items() 86 | else: 87 | items = obj.iteritems() 88 | for k,v in items: 89 | # assume key is a primitive 90 | obj[k] = self.decode(v) 91 | return obj 92 | # non-recursive object (num,bool,blob,string) 93 | return obj 94 | 95 | def dump(self, obj, fp): 96 | dump(self.encode(obj), fp) 97 | 98 | def dumps(self, obj): 99 | return dumps(self.encode(obj)) 100 | 101 | def load(self, fp): 102 | return self.decode(load(fp)) 103 | 104 | def loads(self, blob): 105 | return self.decode(loads(blob)) 106 | 107 | 108 | class WrappedCBOR(ClassTag): 109 | """Handles Tag 24, where a byte array is sub encoded CBOR. 110 | Unpacks sub encoded object on finding such a tag. 111 | Does not convert anyting into such a tag. 112 | 113 | Usage: 114 | >>> import cbor 115 | >>> import cbor.tagmap 116 | >>> tm=cbor.TagMapper([cbor.tagmap.WrappedCBOR()]) 117 | >>> x = cbor.dumps(cbor.Tag(24, cbor.dumps({"a":[1,2,3]}))) 118 | >>> x 119 | '\xd8\x18G\xa1Aa\x83\x01\x02\x03' 120 | >>> tm.loads(x) 121 | {'a': [1L, 2L, 3L]} 122 | >>> cbor.loads(x) 123 | Tag(24L, '\xa1Aa\x83\x01\x02\x03') 124 | """ 125 | def __init__(self): 126 | super(WrappedCBOR, self).__init__(CBOR_TAG_CBOR, None, None, loads) 127 | 128 | @staticmethod 129 | def wrap(ob): 130 | return Tag(CBOR_TAG_CBOR, dumps(ob)) 131 | 132 | @staticmethod 133 | def dump(ob, fp): 134 | return dump(Tag(CBOR_TAG_CBOR, dumps(ob)), fp) 135 | 136 | @staticmethod 137 | def dumps(ob): 138 | return dumps(Tag(CBOR_TAG_CBOR, dumps(ob))) 139 | 140 | 141 | class UnknownTagException(BaseException): 142 | pass 143 | -------------------------------------------------------------------------------- /lib/pylightio/formats/__init__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | from pylightio.formats.lightfields import * 20 | -------------------------------------------------------------------------------- /lib/pylightio/formats/lightfields.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # EXTERNAL PACKAGE DEPENDENCIES 20 | ################################################### 21 | from enum import Enum 22 | 23 | # INTERNAL PACKAGE DEPENDENCIES 24 | ################################################### 25 | # NONE 26 | 27 | # PREPARE LOGGING 28 | ################################################### 29 | import logging 30 | 31 | # get the library logger 32 | logger = logging.getLogger('pyLightIO') 33 | 34 | 35 | 36 | # LIGHTFIELD IMAGE CLASSES 37 | ############################################### 38 | # the following classes are used to represent, convert, and manipulate a set of 39 | # views using a defined lightfield format 40 | class LightfieldImage(object): 41 | 42 | # POSSIBLE FORMATS OF THE VIEWS 43 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | # Enum definition for the different formats the lightfield image can be 45 | # transformed to 46 | class decoderformat(Enum): 47 | pil_image = 1 # decode views to a lightfield as Pillow image 48 | numpyarray = 2 # decode views to a lightfield as numpy array 49 | bytesio = 3 # decode views to a lightfield as BytesIO object 50 | 51 | @classmethod 52 | def to_list(cls): 53 | return list(map(lambda enum: enum, cls)) 54 | 55 | @classmethod 56 | def is_valid(cls, value): 57 | ''' check if a given value is a member of this class ''' 58 | return (value in cls.to_list()) 59 | 60 | # CLASS METHODS 61 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 62 | @classmethod 63 | def new(cls, type, **kwargs): 64 | ''' create an empty lightfield image object of specified format ''' 65 | 66 | # try to find the class for the specified type, if it exists 67 | LightfieldImageFormat = [subclass for subclass in BaseLightfieldImageFormat.__subclasses__() if (subclass == type)] 68 | if LightfieldImageFormat: 69 | 70 | # create the LightfieldImage object of the specified type 71 | lightfield = LightfieldImageFormat[0](**kwargs) 72 | return lightfield 73 | 74 | raise TypeError("'%s' is no valid lightfield image type." % type) 75 | 76 | @classmethod 77 | def open(cls, filepath, type, **kwargs): 78 | ''' open a lightfield image object file of specified format from disk ''' 79 | 80 | # try to find the class for the specified format, if it exists 81 | LightfieldImageFormat = [subclass for subclass in BaseLightfieldImageFormat.__subclasses__() if (subclass == type)] 82 | if LightfieldImageFormat: 83 | 84 | # create a new lightfield image instance of the specified format 85 | lightfield = LightfieldImageFormat[0](**kwargs) 86 | 87 | # load the image 88 | lightfield.load(filepath) 89 | 90 | # return the lightfield image instance of the specified format 91 | return lightfield 92 | 93 | raise TypeError("'%s' is no valid lightfield image type." % type) 94 | 95 | @classmethod 96 | def from_buffer(cls, type, data, width, height, colorchannels, quilt_name = "", **kwargs): 97 | ''' creat a lightfield image object of specified format from a data block of given format ''' 98 | 99 | # try to find the class for the specified format, if it exists 100 | LightfieldImageFormat = [subclass for subclass in BaseLightfieldImageFormat.__subclasses__() if (subclass == type)] 101 | if LightfieldImageFormat: 102 | 103 | # create a new lightfield image instance of the specified format 104 | lightfield = LightfieldImageFormat[0](**kwargs) 105 | 106 | # load the image 107 | lightfield.from_buffer(data, width, height, colorchannels, quilt_name = quilt_name) 108 | 109 | # return the lightfield image instance of the specified format 110 | return lightfield 111 | 112 | raise TypeError("'%s' is no valid lightfield image type." % type) 113 | 114 | @classmethod 115 | def convert(self, lightfield, target_format): 116 | ''' convert a lightfield image object to another type ''' 117 | pass 118 | 119 | 120 | 121 | # class representing an individual view of a lightfield image 122 | # NOTE: At the moment this class is not too useful, but it might be, if we later 123 | # want to introduce more view specific calls or some kind of "view streaming" 124 | class LightfieldView(object): 125 | 126 | # POSSIBLE FORMATS OF THE VIEWS 127 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 128 | __format = None # format of this LightfieldView instance 129 | __data = None # image data of this view in the specified format 130 | 131 | 132 | 133 | # POSSIBLE FORMATS OF THE VIEWS 134 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 135 | # Enum definition for the supported image data formats 136 | class formats(Enum): 137 | pil_image = 1 # view is a Pillow image 138 | numpyarray = 2 # view is a numpy array 139 | bytesio = 3 # view is a BytesIO object 140 | 141 | @classmethod 142 | def to_list(cls): 143 | return list(map(lambda enum: enum, cls)) 144 | 145 | @classmethod 146 | def is_valid(cls, value): 147 | ''' check if a given value is a member of this class ''' 148 | return (value in cls.to_list()) 149 | 150 | 151 | 152 | # CLASS METHODS 153 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 154 | @classmethod 155 | def is_instance(cls, object): 156 | ''' verify if a given object is an instance of this class ''' 157 | return isinstance(object, cls) 158 | 159 | 160 | 161 | # INSTANCE METHODS 162 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 163 | def __init__(self, data, format): 164 | ''' initialize the view and pass data in ''' 165 | # if a valid format was passed 166 | if LightfieldView.formats.is_valid(format): 167 | 168 | # store the image data and the format 169 | self.data = data 170 | self.format = format 171 | 172 | else: 173 | 174 | raise TypeError("'%s' is no valid view format." % format) 175 | 176 | 177 | 178 | # CLASS PROPERTIES 179 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 180 | @property 181 | def format(self): 182 | return self.__format 183 | 184 | @format.setter 185 | def format(self, value): 186 | self.__format = value 187 | 188 | @property 189 | def data(self): 190 | return self.__data 191 | 192 | @data.setter 193 | def data(self, value): 194 | self.__data = value 195 | 196 | 197 | class BaseLightfieldImageFormat(object): 198 | 199 | # PRIVATE MEMBERS 200 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 201 | __views = None # list of LightfieldView objects belonging to this lightfield in the format {view: LightfieldView instance, updated: Boolean} 202 | __metadata = None # metadata of the lightfield format 203 | __colormode = None # colormode of the image data 204 | __colorchannels = None # number of color channels in the image data 205 | 206 | 207 | # INSTANCE METHODS - IMPLEMENTED BY BASE CLASS 208 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 209 | # NOTE: These methods are implemented by the base class but might be overwritten 210 | # by subclasses. If they do, these subclasses should still call the 211 | # base class method with return super().method() to avoid future conflicts. 212 | def set_views(self, list, format, index=0): 213 | ''' store the given list of LightfieldView objects in the internal view list ''' 214 | # if the given index is valid 215 | if index <= len(self.views): 216 | 217 | # if the format is supported 218 | if LightfieldView.formats.is_valid(format): 219 | 220 | # if all the elements of the list are no LightfieldView objects 221 | if all(not LightfieldView.is_instance(view) for view in list): 222 | 223 | # assume view data was passed 224 | list = [LightfieldView(view, format) for view in list] 225 | 226 | # log a debug message 227 | logger.debug('No LightfieldViews instance was passed to LightfieldImage.set_views().') 228 | 229 | # if all views in this list have the same format 230 | if all((view.format == format) for view in list): 231 | 232 | # remove all elements from the view list that shall be overwritten 233 | for i in range(index, len(list)): 234 | if i < len(self.views): self.views.pop(i) 235 | 236 | # insert the new elements 237 | for i, view in enumerate(list): 238 | self.views.insert(index+i, {'view': view, 'updated': True}) 239 | 240 | # store the format 241 | self.views_format = format 242 | 243 | # return the list of views 244 | return self.views 245 | 246 | raise TypeError("Multiple view formats were passed. All views of a '%s' must have the same format." % self) 247 | 248 | raise TypeError("'%s' is not a valid view format." % format) 249 | 250 | raise ValueError("The given view index is out of bounds. Pass a positive index smaller or equal to %i" % len(self.views)) 251 | 252 | def append_view(self, view, format=None): 253 | ''' append a LightfieldView object to the end of the list of views ''' 254 | 255 | # if the passed view is not a LightfieldView object 256 | if not LightfieldView.is_instance(view): 257 | 258 | # if the passed format is a valid view format 259 | if LightfieldView.formats.is_valid(format): 260 | 261 | # assume view data was passed and create a LightfieldView instance 262 | view = LightfieldView(view, format) 263 | 264 | # log a debug message 265 | logger.debug('No LightfieldViews instance was passed to LightfieldImage.append_views(). Created one from the given view data!') 266 | 267 | else: 268 | 269 | raise TypeError("'%s' is not a valid LightfieldView format." % format) 270 | 271 | # if all views in this list have the same format 272 | if all((v['view'].format == view.format) for v in self.views): 273 | 274 | # append the new element 275 | self.views.append({'view': view, 'updated': True}) 276 | 277 | # store the format 278 | self.views_format = view.format 279 | 280 | # return the list of views 281 | return self.views 282 | 283 | raise TypeError("Multiple view formats were passed. All views of a '%s' must have the same format." % self) 284 | 285 | def insert_view(self, index, view, format=None): 286 | ''' inserts a LightfieldView object at the given position the list of views ''' 287 | # if all views in this list have the same format 288 | if all((v['view'].format == view.format) for v in self.views): 289 | 290 | # if the passed view is not a LightfieldView object 291 | if not LightfieldView.is_instance(view): 292 | 293 | # if the passed format is a valid view format 294 | if LightfieldView.formats.is_valid(format): 295 | 296 | # assume view data was passed and create a LightfieldView instance 297 | view = LightfieldView(view, format) 298 | 299 | # log a debug message 300 | logger.debug('No LightfieldViews instance was passed to LightfieldImage.insert_views().') 301 | 302 | else: 303 | 304 | raise TypeError("'%s' is not a valid LightfieldView format." % format) 305 | 306 | # insert the new element 307 | self.views.insert(index, {'view': view, 'updated': True}) 308 | 309 | # store the format 310 | self.views_format = view.format 311 | 312 | # return the list of views 313 | return self.views 314 | 315 | raise TypeError("Multiple view formats were passed. All views of a '%s' must have the same format." % self) 316 | 317 | def remove_view(self, index): 318 | ''' remove a LightfieldView object from the list of views ''' 319 | self.views.pop(index) 320 | 321 | # return the list of views 322 | return self.views 323 | 324 | def clear_views(self): 325 | ''' clear the complete list of LightfieldView objects from this LightfieldImage ''' 326 | 327 | # remove all views 328 | for index, view in enumerate(self.views): 329 | self.remove_view(index) 330 | 331 | def get_view_data(self, updated=None, reset_updated=False): 332 | ''' return the image data of all views as a list of the views image data ''' 333 | if updated != None: views = [v['view'].data for v in self.views if v['updated'] == updated] 334 | else: views = [v['view'].data for v in self.views] 335 | 336 | # if the updated status shall be reset, do that 337 | if reset_updated == True: 338 | for v in self.views: v['updated'] = False 339 | 340 | # return the list of view data 341 | return views 342 | 343 | 344 | 345 | 346 | # INSTANCE METHODS - IMPLEMENTED BY SUBCLASSES 347 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 348 | # NOTE: These methods must be implemented by the subclasses, which represent 349 | # the specific lightfield image types. 350 | def __init__(self, **kwargs): 351 | ''' create a new and empty lightfield image object of specified type ''' 352 | # NOTE: this method MUST be called from any subclass from the subclasse's 353 | # __init__() prior to any further initializations 354 | 355 | # initialize instance properties 356 | self.views = [] 357 | self.metadata = {} 358 | self.colormode = 'RGBA' 359 | self.colorchannels = 4 360 | 361 | def load(self, filepath): 362 | ''' load the lightfield from a file ''' 363 | pass 364 | 365 | def from_buffer(self, data): 366 | ''' load the lightfield from a data block ''' 367 | pass 368 | 369 | def save(self, filepath): 370 | ''' save the lightfield image in its specific format to a disk file ''' 371 | pass 372 | 373 | def delete(self, lightfield): 374 | ''' delete the given lightfield image object ''' 375 | pass 376 | 377 | def decode(self, format): 378 | ''' return the image as the lightfield and return it ''' 379 | pass 380 | 381 | def free(self): 382 | ''' free the image lightfield image ''' 383 | pass 384 | 385 | # CLASS PROPERTIES 386 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 387 | @property 388 | def views(self): 389 | return self.__views 390 | 391 | @views.setter 392 | def views(self, value): 393 | self.__views = value 394 | 395 | @property 396 | def views_format(self): 397 | return self.__views_format 398 | 399 | @views_format.setter 400 | def views_format(self, value): 401 | self.__views_format = value 402 | 403 | @property 404 | def metadata(self): 405 | return self.__metadata 406 | 407 | @metadata.setter 408 | def metadata(self, value): 409 | self.__metadata = value 410 | 411 | @property 412 | def colormode(self): 413 | return self.__colormode 414 | 415 | @colormode.setter 416 | def colormode(self, value): 417 | self.__colormode = value 418 | 419 | @property 420 | def colorchannels(self): 421 | return self.__colorchannels 422 | 423 | @colorchannels.setter 424 | def colorchannels(self, value): 425 | self.__colorchannels = value 426 | -------------------------------------------------------------------------------- /lib/pylightio/lookingglass/__init__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | from pylightio.lookingglass.devices import * 20 | from pylightio.lookingglass.services import * 21 | from pylightio.lookingglass.lightfields import * 22 | -------------------------------------------------------------------------------- /lib/pylightio/lookingglass/lightfields.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # EXTERNAL PACKAGE DEPENDENCIES 20 | ################################################### 21 | import io, os 22 | import numpy as np 23 | 24 | # debuging 25 | import time 26 | 27 | # INTERNAL PACKAGE DEPENDENCIES 28 | ################################################### 29 | from pylightio.formats import * 30 | 31 | # PREPARE LOGGING 32 | ################################################### 33 | import logging 34 | 35 | # get the library logger 36 | logger = logging.getLogger('pyLightIO') 37 | 38 | 39 | 40 | # LIGHTFIELD IMAGE TYPES FOR LOOKING GLASS DEVICES 41 | ################################################### 42 | # the following classes are used to represent, convert, and manipulate a set of 43 | # views for Looking Glass devices 44 | class LookingGlassQuilt(BaseLightfieldImageFormat): 45 | 46 | # PRIVATE ATTRIBUTES 47 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 48 | 49 | __merged_numpy = None # a numpy array which holds all the view data 50 | 51 | 52 | # DEFINE PUBLIC CLASS ATTRIBUTES 53 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 54 | # supported quilt formats 55 | class formats: 56 | 57 | __dict = { 58 | 59 | # first gen devices 60 | 1: {'description': "2k Quilt, 32 Views", 'quilt_width': 2048, 'quilt_height': 2048, 'view_width': 512, 'view_height': 256, 'columns': 4, 'rows': 8, 'total_views': 32, 'hidden': False }, 61 | 2: {'description': "4k Quilt, 45 Views", 'quilt_width': 4096, 'quilt_height': 4096, 'view_width': 819, 'view_height': 455, 'columns': 5, 'rows': 9, 'total_views': 45, 'hidden': False }, 62 | 3: {'description': "8k Quilt, 45 Views", 'quilt_width': 8192, 'quilt_height': 8192, 'view_width': 1638, 'view_height': 910, 'columns': 5, 'rows': 9, 'total_views': 45, 'hidden': False }, 63 | 64 | #Looking Glass Portrait 65 | 4: {'description': "Portrait, 48 Views", 'quilt_width': 3360, 'quilt_height': 3360, 'view_width': 420, 'view_height': 560, 'columns': 8, 'rows': 6, 'total_views': 48, 'hidden': False }, 66 | 67 | # third gen devices 68 | 5: {'description': "Go, 66 Views", 'quilt_width': 4092, 'quilt_height': 4092, 'view_width': 372, 'view_height': 682, 'columns': 11, 'rows': 6, 'total_views': 66, 'hidden': False }, 69 | 6: {'description': "16 Landscape, 49 Views", 'quilt_width': 5999, 'quilt_height': 5999, 'view_width': 857, 'view_height': 857, 'columns': 7, 'rows': 7, 'total_views': 49, 'hidden': False }, 70 | 7: {'description': "16 Portrait, 66 Views", 'quilt_width': 5995, 'quilt_height': 6000, 'view_width': 545, 'view_height': 1000, 'columns': 11, 'rows': 6, 'total_views': 66, 'hidden': False }, 71 | 8: {'description': "32 Landscape, 49 Views", 'quilt_width': 8190, 'quilt_height': 8190, 'view_width': 1170, 'view_height': 1170, 'columns': 7, 'rows': 7, 'total_views': 49, 'hidden': False }, 72 | 9: {'description': "32 Portrait, 66 Views", 'quilt_width': 8184, 'quilt_height': 8184, 'view_width': 744, 'view_height': 1364, 'columns': 11, 'rows': 6, 'total_views': 66, 'hidden': False }, 73 | 10: {'description': "65, 72 views", 'quilt_width': 8192, 'quilt_height': 8190, 'view_width': 1024, 'view_height': 910, 'columns': 8, 'rows': 9, 'total_views': 72, 'hidden': False }, 74 | 75 | } 76 | 77 | @classmethod 78 | def add(cls, values): 79 | ''' add a new format by passing a dict ''' 80 | cls.__dict[len(cls.__dict) + 1] = values 81 | return len(cls.__dict) 82 | 83 | @classmethod 84 | def remove(cls, id): 85 | ''' remove an existing format ''' 86 | cls.__dict.pop(id, None) 87 | 88 | @classmethod 89 | def get(cls, id=None): 90 | ''' return the complete dictionary or the dictionary of a specific format ''' 91 | if not id: return cls.__dict 92 | else: return cls.__dict[id] 93 | 94 | @classmethod 95 | def set(cls, id, values): 96 | ''' modify an existing format by passing a dict ''' 97 | if id in cls.__dict.keys(): cls.__dict[id] = values 98 | 99 | @classmethod 100 | def find(cls, width, height, rows, columns): 101 | ''' try to find a format id based on the given parameters ''' 102 | for id, format in cls.__dict.items(): 103 | if (format['quilt_width'], format['quilt_height'], format['rows'], format['columns']) == (width, height, rows, columns): 104 | return id 105 | 106 | @classmethod 107 | def count(cls): 108 | ''' get number of formats ''' 109 | if id in cls.__dict.keys(): return len(cls.__dict) 110 | 111 | # NOTE: the following is useful for applications where the formats are 112 | # exposed in an UI to the user, but if not all formats shall be exposed 113 | @classmethod 114 | def hide(cls, id, value): 115 | ''' set the 'hidden' flag on this format ''' 116 | if id in cls.__dict.keys(): cls.__dict[id]['hidden'] = value 117 | 118 | @classmethod 119 | def is_hidden(cls, id): 120 | ''' returns True if the quilt format is a private one ''' 121 | if id in cls.__dict.keys(): return cls.__dict[id]['hidden'] 122 | 123 | 124 | # INSTANCE METHODS - IMPLEMENTED BY SUBCLASS 125 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 126 | def __init__(self, id=None, colormode='RGBA'): 127 | ''' create a new and empty lightfield image object of type LookingGlassQuilt ''' 128 | 129 | # first make the mandatory call to the __init__ method of the base class 130 | super().__init__() 131 | 132 | # if no quilt format id was passed 133 | if not id: 134 | 135 | # store color information 136 | self.colormode = colormode 137 | self.colorchannels = len(colormode) 138 | 139 | # store quilt metadata 140 | self.metadata['quilt_width'] = 0 141 | self.metadata['quilt_height'] = 0 142 | self.metadata['view_width'] = 0 143 | self.metadata['view_height'] = 0 144 | self.metadata['rows'] = 0 145 | self.metadata['columns'] = 0 146 | self.metadata['count'] = 0 147 | 148 | # if a valid id was passed 149 | elif id in LookingGlassQuilt.formats.get().keys(): 150 | 151 | # TODO: Implement this as arguments in a reasonable way 152 | # store color information 153 | self.colormode = colormode 154 | self.colorchannels = len(colormode) 155 | 156 | # store quilt metadata 157 | self.metadata['quilt_width'] = LookingGlassQuilt.formats.get(id)['view_width'] * LookingGlassQuilt.formats.get(id)['columns'] 158 | self.metadata['quilt_height'] = LookingGlassQuilt.formats.get(id)['view_height'] * LookingGlassQuilt.formats.get(id)['rows'] 159 | self.metadata['view_width'] = LookingGlassQuilt.formats.get(id)['view_width'] 160 | self.metadata['view_height'] = LookingGlassQuilt.formats.get(id)['view_height'] 161 | self.metadata['rows'] = LookingGlassQuilt.formats.get(id)['rows'] 162 | self.metadata['columns'] = LookingGlassQuilt.formats.get(id)['columns'] 163 | self.metadata['count'] = LookingGlassQuilt.formats.get(id)['total_views'] 164 | 165 | else: 166 | 167 | raise TypeError("There is no quilt format with the id '%i'. Please choose one of the following: %s" % (id, LookingGlassQuilt.formats.get())) 168 | 169 | def load(self, filepath): 170 | ''' load the quilt file from the given path and convert to numpy views ''' 171 | if os.path.exists(filepath): 172 | 173 | start = time.time() 174 | # use PIL to load the image from disk 175 | # NOTE: This makes nearly all of the execution time of the load() method 176 | # ToDo: replace PIL with OpenCV call, since we don't need both 177 | quilt_image = Image.open(filepath) 178 | if quilt_image: 179 | 180 | # try to detect quilt from quilt name 181 | found = self.__detect_from_quilt_suffix(os.path.basename(filepath)) 182 | if not found: 183 | # otherwise try to detect it from the quilt dimensions 184 | found = self.__detect_from_quilt_dimensions(quilt_width = quilt_image.width, quilt_height = quilt_image.height) 185 | 186 | # if no fitting quilt format was found 187 | if not found: raise TypeError("The loaded image is not in a supported format. Please check the image dimensions.") 188 | 189 | # TODO: This takes 0.5 to 1.5 s ... is there a faster way? 190 | # convert it to a numpy array 191 | quilt_np = np.asarray(quilt_image, dtype=np.uint8) 192 | # crop the image in case, the size is incorrect due to rounding 193 | # errors 194 | quilt_np = quilt_np[0:(self.metadata['rows'] * self.metadata['view_height']), 0:(self.metadata['columns'] * self.metadata['view_width']), :] 195 | 196 | # store the colormode 197 | self.colormode = quilt_image.mode 198 | 199 | # store the size and color depth in the meta data of the instance 200 | self.metadata['quilt_height'], self.metadata['quilt_width'], self.colorchannels = quilt_np.shape 201 | 202 | # then we reshape the quilt into the array of individual views ... 203 | views = np.flip(quilt_np.reshape(self.metadata['rows'], self.metadata['view_height'], self.metadata['columns'], self.metadata['view_width'], self.colorchannels).swapaxes(1, 2), 0).reshape(self.metadata['count'], self.metadata['view_height'], self.metadata['view_width'], self.colorchannels) 204 | 205 | # add each view image data array as a numpyarray view to the LookingGlassQuilt 206 | for data in views: 207 | view = self.append_view(data, LightfieldView.formats.numpyarray) 208 | 209 | return True 210 | 211 | raise TypeError("The quilt image was found but could not be opened. The image format is not supported.") 212 | 213 | raise FileNotFoundError("The quilt image was not found: %s" % filepath) 214 | 215 | def from_buffer(self, data, width, height, colorchannels, quilt_name = ""): 216 | ''' load the quilt from the given data block and convert to numpy views ''' 217 | 218 | # if this is a numpy array 219 | if type(data) == np.ndarray: 220 | 221 | start = time.time() 222 | 223 | # try to detect quilt from quilt name 224 | found = self.__detect_from_quilt_suffix(quilt_name) 225 | if not found: 226 | # otherwise try to detect it from the quilt dimensions 227 | found = self.__detect_from_quilt_dimensions(quilt_pixels = data.shape[0]) 228 | 229 | # if no fitting quilt format was found 230 | if not found: raise TypeError("The loaded image is not in a supported format. Please check the image dimensions.") 231 | 232 | # convert it to a numpy array 233 | quilt_np = data.reshape(height, width, colorchannels) 234 | 235 | # crop the image in case, the size is incorrect due to rounding 236 | # errors 237 | quilt_np = np.flip(quilt_np[0:(self.metadata['rows'] * self.metadata['view_height']), 0:(self.metadata['columns'] * self.metadata['view_width']), :], 0) 238 | 239 | # store the colormode 240 | if colorchannels == 3: self.colormode = 'RGB' 241 | if colorchannels == 4: self.colormode = 'RGBA' 242 | 243 | # store the size and color depth in the meta data of the instance 244 | self.metadata['quilt_height'], self.metadata['quilt_width'], self.colorchannels = quilt_np.shape 245 | 246 | # then we reshape the quilt into the array of individual views ... 247 | views = np.flip(quilt_np.reshape(self.metadata['rows'], self.metadata['view_height'], self.metadata['columns'], self.metadata['view_width'], self.colorchannels).swapaxes(1, 2), 0).reshape(self.metadata['count'], self.metadata['view_height'], self.metadata['view_width'], self.colorchannels) 248 | 249 | # add each view image data array as a numpyarray view to the LookingGlassQuilt 250 | for data in views: 251 | 252 | view = self.append_view(data, LightfieldView.formats.numpyarray) 253 | 254 | return True 255 | 256 | raise FileNotFoundError("The data block needs to be of type '%s'" % np.ndarray) 257 | 258 | def save(self, filepath, format): 259 | ''' save the lightfield image in its specific format to a disk file ''' 260 | 261 | pass 262 | 263 | def delete(self, lightfield): 264 | ''' delete the given lightfield image object ''' 265 | pass 266 | 267 | def set_views(self, list, format): 268 | ''' store the list of LightfieldViews and their format in the quilt ''' 269 | 270 | # we override the base class function to introduce an additional check: 271 | # if the given list has the correct length for this quilt 272 | if len(list) == self.metadata['count']: 273 | 274 | # and then call the base class function 275 | return super().set_views(list, format) 276 | 277 | raise ValueError("Invalid view set. %i views were passed, but %i were required." % (len(list), self.metadata['count'])) 278 | 279 | def decode(self, format, flip_views=False, custom_decoder = None): 280 | ''' return the lightfield image object in a specific format ''' 281 | 282 | # if a custom decoder function is passed 283 | if custom_decoder: 284 | 285 | # get the view data 286 | views = self.get_view_data() 287 | 288 | # call this function 289 | quilt = custom_decoder(views, views_format, format) 290 | 291 | # return the quilt data 292 | return quilt 293 | 294 | 295 | # TODO: HERE IS THE PLACE TO DEFINE STANDARD CONVERSIONS THAT CAN BE 296 | # USED IN MULTIPLE PROGRAMMS 297 | 298 | # if the image shall be returned as numpy array 299 | if format == LightfieldImage.decoderformat.numpyarray: 300 | 301 | # if the views are in a numpy array format 302 | if self.views_format == LightfieldView.formats.numpyarray: 303 | 304 | # create a numpy quilt from numpy views 305 | quilt_numpy = self.__from_views_to_quilt_numpy(flip_views=flip_views) 306 | 307 | # return the numpy array of the quilt 308 | return quilt_numpy 309 | 310 | # otherwise raise exception 311 | raise TypeError("The given views format '%s' is not supported." % self.views_format) 312 | 313 | # otherwise raise exception 314 | raise TypeError("The requested lightfield format '%s' is not supported." % format) 315 | 316 | 317 | # PRIVATE INSTANCE METHODS: CONVERT BETWEEN DECODERFORMATS 318 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 319 | def __detect_from_quilt_suffix(self, quilt_name): 320 | import re 321 | 322 | # values from the metadata 323 | columns = None 324 | rows = None 325 | aspect = None 326 | 327 | # if a quilt name was given 328 | if quilt_name: 329 | 330 | # try to extract some metadata information from the quiltname 331 | try: 332 | 333 | rows = int(re.search('_qs(\d+)x(\d+)a(\d+.?\d*)', quilt_name).group(1)) 334 | columns = int(re.search('_qs(\d+)x(\d+)a(\d+.?\d*)', quilt_name).group(2)) 335 | aspect = float(re.search('_qs(\d+)x(\d+)a(\d+.?\d*)', quilt_name).group(3)) 336 | 337 | except AttributeError: 338 | 339 | try: 340 | 341 | rows = int(re.search('_qs(\d+)x(\d+).', quilt_name).group(1)) 342 | columns = int(re.search('_qs(\d+)x(\d+).', quilt_name).group(2)) 343 | 344 | except AttributeError: 345 | 346 | pass 347 | 348 | # for each supported quilt format 349 | for qf in LookingGlassQuilt.formats.get().values(): 350 | 351 | # if the image dimensions matches one of the quilt formats 352 | # NOTE: We allow a difference of +/-1 px in width and height 353 | # to accomodate for rounding errors in view width/height 354 | if not (columns is None or rows is None) and columns == qf['columns'] and rows == qf['rows']: 355 | 356 | # store new row and column number in the metadata 357 | self.metadata['rows'] = qf['rows'] 358 | self.metadata['columns'] = qf['columns'] 359 | self.metadata['count'] = qf['rows'] * qf['columns'] 360 | self.metadata['view_width'] = qf['view_width'] 361 | self.metadata['view_height'] = qf['view_height'] 362 | 363 | logger.info("Detected quilt format from name.") 364 | 365 | return True 366 | 367 | return False 368 | 369 | def __detect_from_quilt_dimensions(self, quilt_width = None, quilt_height = None, quilt_pixels = None, quilt_name = ""): 370 | 371 | # for each supported quilt format 372 | for qf in LookingGlassQuilt.formats.get().values(): 373 | 374 | # if the image dimensions matches one of the quilt formats 375 | # NOTE: We allow a difference of +/-1 px in width and height 376 | # to accomodate for rounding errors in view width/height 377 | if not quilt_pixels is None and quilt_pixels in range((qf['quilt_width'] - 1) * (qf['quilt_height'] - 1) * 4, (qf['quilt_width'] + 1) * (qf['quilt_height'] + 1) * 4): 378 | 379 | # store new row and column number in the metadata 380 | self.metadata['rows'] = qf['rows'] 381 | self.metadata['columns'] = qf['columns'] 382 | self.metadata['count'] = qf['rows'] * qf['columns'] 383 | self.metadata['view_width'] = qf['view_width'] 384 | self.metadata['view_height'] = qf['view_height'] 385 | 386 | logger.info("Detected quilt format from pixel count.") 387 | 388 | return True 389 | 390 | # if the image dimensions matches one of the quilt formats 391 | # NOTE: We allow a difference of +/-1 px in width and height 392 | # to accomodate for rounding errors in view width/height 393 | if not (quilt_width is None or quilt_height is None) and quilt_width in range(qf['quilt_width'] - 1, qf['quilt_width'] + 1) and quilt_height in range(qf['quilt_height'] - 1, qf['quilt_height'] + 1): 394 | 395 | # store new row and column number in the metadata 396 | self.metadata['rows'] = qf['rows'] 397 | self.metadata['columns'] = qf['columns'] 398 | self.metadata['count'] = qf['rows'] * qf['columns'] 399 | self.metadata['view_width'] = qf['view_width'] 400 | self.metadata['view_height'] = qf['view_height'] 401 | 402 | logger.info("Detected quilt format from width and height.") 403 | 404 | return True 405 | 406 | return False 407 | 408 | # PRIVATE INSTANCE METHODS: VIEWS TO QUILTS 409 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 410 | 411 | # NOTE: this function is based on https://stackoverflow.com/questions/42040747/more-idiomatic-way-to-display-images-in-a-grid-with-numpy 412 | # NOTE: This call takes 15 to 30 ms -> can this be optimized? 413 | def __from_views_to_quilt_numpy(self, flip_views=False): 414 | ''' convert views given as numpy arrays to a quilt as a numpy array ''' 415 | 416 | start = time.time() 417 | 418 | # if no merged numpy array exists 419 | if self.__merged_numpy is None: 420 | 421 | # get the views 422 | views = self.get_view_data() 423 | views_format = self.views_format 424 | 425 | # create numpy array from the BytesIO buffer object 426 | self.__merged_numpy = np.asarray(views, dtype=np.uint8) 427 | 428 | # log info 429 | logger.debug(" [#] Prepared numpy array of shape %s in %.3f ms." % (self.__merged_numpy.shape, (time.time() - start) * 1000)) 430 | 431 | # step 1: get an array of shape (rows, columns, view_height, view_width) 432 | self.__merged_numpy = self.__merged_numpy.reshape(self.metadata['rows'], self.metadata['columns'], self.metadata['view_height'], self.metadata['view_width'], self.colorchannels) 433 | 434 | # step 2: swap the "columns" and "view_height" axis, so that we get an 435 | # array of shape (rows, view_height, columns, view_width, colorchannels) 436 | self.__merged_numpy = self.__merged_numpy.swapaxes(1, 2) 437 | 438 | # step 3: re-assign the numpy arrays for all underlying LightfieldView-objects 439 | # as (memory)views into the __merged_numpy array 440 | # NOTE: This step speeds up the quilt creation by some tens of milliseconds 441 | # since the next time the LightfieldView pixel data is updated 442 | # it directly updates the pixel data in the __merged_numpy array. 443 | i_x = i_y = 0 444 | for i, view in enumerate(self.views): 445 | 446 | # create subarray view into the quilt pixel data 447 | view['view'].data = self.__merged_numpy[i_y, :, i_x, :, :] 448 | 449 | # choose column and row 450 | if (i + 1) % self.metadata['columns'] == 0 and i > 0: 451 | i_x = 0 452 | i_y += 1 453 | else: 454 | i_x += 1 455 | 456 | # log info 457 | logger.debug(" [#] Prepeared quilt as numpy array in %.3f ms." % ((time.time() - start) * 1000)) 458 | 459 | # output the views 460 | return self.__merged_numpy 461 | 462 | 463 | 464 | # PRIVATE INSTANCE METHODS: CONVERT BETWEEN DECODERFORMATS 465 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 466 | 467 | 468 | # CLASS PROPERTIES 469 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 470 | @property # read-only property 471 | def merged_numpy(self): 472 | return self.__merged_numpy 473 | 474 | @merged_numpy.setter 475 | def merged_numpy(self, value): 476 | pass 477 | -------------------------------------------------------------------------------- /lib/pylightio/lookingglass/services.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # EXTERNAL PACKAGE DEPENDENCIES 20 | ################################################### 21 | import sys, os, io, struct 22 | import pynng, cv2 23 | import math 24 | import numpy as np 25 | 26 | # debugging 27 | import time 28 | 29 | # INTERNAL PACKAGE DEPENDENCIES 30 | ################################################### 31 | from pylightio.managers.services import BaseServiceType 32 | from pylightio.formats import * 33 | from pylightio.external import cbor 34 | 35 | # PREPARE LOGGING 36 | ################################################### 37 | import logging 38 | 39 | # get the library logger 40 | logger = logging.getLogger('pyLightIO') 41 | 42 | 43 | 44 | # SERVICE TYPES FOR LOOKING GLASS DEVICES 45 | ############################################### 46 | # Looking Glass Bridge for Looking Glass lightfield displays 47 | class LookingGlassBridge(BaseServiceType): 48 | 49 | # DEFINE CLASS PROPERTIES AS PROTECTED MEMBERS 50 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | type = 'lookingglassbridge' # the unique identifier string of this service type (required for the factory class) 52 | name = 'Looking Glass Bridge' # the name this service type 53 | 54 | # DEFINE CLASS PROPERTIES AS PRIVATE MEMBERS 55 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 56 | __socket = None # NNG socket 57 | __address = 'ipc:///tmp/holoplay-driver.ipc' # driver url (alternative: "ws://localhost:11222/driver", "ipc:///tmp/holoplay-driver.ipc") 58 | __dialer = None # NNG Dialer of the socket 59 | __devices = [] # list of devices supported by this service (#TODO: this needs to be implemented) 60 | __decoder_format = LightfieldImage.decoderformat.numpyarray # the decoder format in which the lightfield data is passed to the service 61 | 62 | # Error 63 | ################### 64 | # Enum definition for errors returned from the HoloPlayCore dynamic library. 65 | # 66 | # This encapsulates potential errors with the connection itself, 67 | # as opposed to hpc_service_error, which describes potential error messages 68 | # included in a successful reply from Looking Glass Bridge. 69 | class client_error(Enum): 70 | CLIERR_NOERROR = 0 71 | CLIERR_NOSERVICE = 1 72 | CLIERR_VERSIONERR = 2 73 | CLIERR_SERIALIZEERR = 3 74 | CLIERR_DESERIALIZEERR = 4 75 | CLIERR_MSGTOOBIG = 5 76 | CLIERR_SENDTIMEOUT = 6 77 | CLIERR_RECVTIMEOUT = 7 78 | CLIERR_PIPEERROR = 8 79 | CLIERR_APPNOTINITIALIZED = 9 80 | 81 | # INSTANCE METHODS 82 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 83 | def __init__(self, timeout = 5000, client_name = ""): 84 | ''' initialize the class instance and create the NNG socket ''' 85 | 86 | # open a Req0 socket 87 | self.__socket = pynng.Req0(recv_timeout = timeout, send_timeout = timeout) 88 | 89 | # if the NNG socket is open 90 | if self.__is_socket(): 91 | 92 | logger.info("Created socket: %s" % self.__socket) 93 | 94 | # connect to Looking Glass Bridge App 95 | if self.__connect(): 96 | 97 | # send initialization command 98 | response = self.__send_message(self.__init(client_name)) 99 | if response != None: 100 | 101 | # if no error was received 102 | if response[1]['error'] == 0: 103 | 104 | # fill version string of the Looking Glass Bridge 105 | self.version = response[1]['version'] 106 | 107 | # log info 108 | logger.info("Connected to Looking Glass Bridge v%s." % self.get_version()) 109 | 110 | def is_ready(self): 111 | ''' check if the service is ready: Is NNG socket created and connected to Looking Glass Bridge App? ''' 112 | if self.__is_connected(): 113 | return True 114 | 115 | return False 116 | 117 | def get_version(self): 118 | ''' return the looking glass bridge version ''' 119 | 120 | # if the NNG socket is connected to Looking Glass Bridge App and version still unknown 121 | if self.__is_connected() and not self.version: 122 | 123 | # request service version 124 | response = self.__send_message({'cmd': {'info': {}}, 'bin': ''}) 125 | if response != None: 126 | 127 | # if no error was received 128 | if response[1]['error'] == 0: 129 | 130 | # fill version string of the Looking Glass Bridge 131 | self.version = response[1]['version'] 132 | 133 | return self.version 134 | 135 | def get_devices(self): 136 | ''' send a request to the service and request the connected devices ''' 137 | ''' this function should return a list object ''' 138 | 139 | # if the service is ready 140 | if self.is_ready(): 141 | 142 | # request calibration data 143 | response = self.__send_message(self.__get_devices()) 144 | if response != None: 145 | 146 | # if no errors were received 147 | if response[1]['error'] == 0: 148 | 149 | # get the list of devices with status "ok" 150 | devices = [device for device in response[1]['devices'] if device['state'] == "ok"] 151 | 152 | # iterate through all devices 153 | for device in devices: 154 | 155 | # parse odd value-object format from calibration json 156 | device['calibration'].update({key: value['value'] if isinstance(value, dict) else value for (key, value) in device['calibration'].items()}) 157 | 158 | # calculate the derived values (e.g., tilt, pich, etc.) 159 | device['calibration'].update(self.__calculate_derived(device['calibration'])) 160 | 161 | # return the device list 162 | return devices 163 | 164 | def display(self, device, lightfield, flip_views=False, aspect=None, invert=False, custom_decoder = None): 165 | ''' display a given lightfield image object on a device ''' 166 | ''' Looking Glass Bridge expects a lightfield image in LookingGlassQuilt format ''' 167 | 168 | logger.info("Preparing lightfield image '%s' for display on '%s' ..." % (lightfield, device)) 169 | 170 | # if the service is ready 171 | if self.is_ready(): 172 | start_total = time.time() 173 | # if a lightfield was given 174 | if lightfield != None: 175 | 176 | # convert the lightfield into a suitable format for this service 177 | # NOTE: Looking Glass Bridge expects a byte stream 178 | start = time.time() 179 | decoded_lightfield_data = lightfield.decode(self.__decoder_format, flip_views=flip_views, custom_decoder=custom_decoder) 180 | 181 | # lightfield is decoded as numpy array 182 | if self.__decoder_format == LightfieldImage.decoderformat.numpyarray and type(decoded_lightfield_data) == np.ndarray: 183 | 184 | # flip the individual views vertically, if required 185 | start = time.time() 186 | if flip_views: 187 | merged_numpy = lightfield.merged_numpy.view()[:, ::-1, :, :, :] 188 | 189 | logger.debug(" [#] Flipping the numpy array of shape %s took %.3f ms." % (merged_numpy.shape, (time.time() - start) * 1000)) 190 | start = time.time() 191 | else: 192 | merged_numpy = lightfield.merged_numpy.view() 193 | 194 | # convert BGR(A) <-> RGB on little-endian systems to make the 195 | # data in the numpy buffer comply with the BITMAP file format 196 | # specifications 197 | start = time.time() 198 | if sys.byteorder == "little": 199 | 200 | if lightfield.colorchannels == 3: 201 | 202 | bytes = cv2.cvtColor(merged_numpy.reshape(lightfield.metadata['quilt_height'], lightfield.metadata['quilt_width'], lightfield.colorchannels), cv2.COLOR_BGR2RGB) 203 | 204 | logger.debug(" [#] Converting from BGR to RGB took %.3f ms." % ((time.time() - start) * 1000)) 205 | 206 | elif lightfield.colorchannels == 4: 207 | 208 | bytes = cv2.cvtColor(merged_numpy.reshape(lightfield.metadata['quilt_height'], lightfield.metadata['quilt_width'], lightfield.colorchannels), cv2.COLOR_BGRA2RGB) 209 | 210 | logger.debug(" [#] Converting from BGRA to RGB took %.3f ms." % ((time.time() - start) * 1000)) 211 | 212 | elif not sys.byteorder == "little" and lightfield.colorchannels == 4: 213 | 214 | # TODO: Actually we would not need this, if we could 215 | # read in RGB mode to gpu.types.Buffer, but we can't 216 | # due to a Blender bug / limitation: 217 | # 218 | # https://developer.blender.org/T91828 219 | bytes = cv2.cvtColor(merged_numpy.reshape(lightfield.metadata['quilt_height'], lightfield.metadata['quilt_width'], lightfield.colorchannels), cv2.COLOR_RGBA2RGB) 220 | 221 | logger.debug(" [#] Converting from RGBA to RGB took %.3f ms." % ((time.time() - start) * 1000)) 222 | 223 | else: 224 | 225 | bytes = merged_numpy.reshape(lightfield.metadata['quilt_height'], lightfield.metadata['quilt_width'], lightfield.colorchannels) 226 | 227 | logger.debug(" [#] Reading bytes from %s took %.3f ms." % (type(bytes), (time.time() - start) * 1000)) 228 | 229 | # parse the quilt metadata 230 | settings = {'vx': lightfield.metadata['columns'], 'vy':lightfield.metadata['rows'], 'vtotal': lightfield.metadata['rows'] * lightfield.metadata['columns'], 'aspect': aspect, 'invert': invert} 231 | 232 | # pass the quilt to the device 233 | logger.info(" [#] Lightfield image with shape %s is being sent to '%s'." % (bytes.shape, self)) 234 | self.__send_message(self.__show_quilt(device.configuration['index'], bytes, settings), image_shape=(lightfield.metadata['quilt_height'],lightfield.metadata['quilt_width'], 3)) 235 | logger.info(" [#] Done (total time: %.3f ms)." % ((time.time() - start_total) * 1000)) 236 | 237 | return True 238 | 239 | raise TypeError("The '%s' expected lightfield data conversion to %s, but %s was passed." % (self, np.ndarray, type(decoded_lightfield_data))) 240 | 241 | # otherwise show the demo quilt 242 | else: 243 | 244 | # pass the quilt to the device 245 | logger.info(" [#] Display of demo quilt is requested for '%s' ..." % self) 246 | self.__send_message(self.__show_demo(device.configuration['index'])) 247 | logger.info(" [#] Done.") 248 | 249 | return True 250 | 251 | raise RuntimeError("The '%s' is not ready. Is Looking Glass Bridge app running?" % (self)) 252 | 253 | def clear(self, device): 254 | ''' clear the display of a given device ''' 255 | 256 | # if the service is ready 257 | if self.is_ready(): 258 | 259 | # clear the display 260 | if self.__send_message(self.__hide(device.configuration['index'])): 261 | 262 | return True 263 | 264 | raise RuntimeError("The '%s' is not ready. Is Looking Glass Bridge app running?" % (self)) 265 | 266 | def __del__(self): 267 | ''' disconnect from Looking Glass Bridge App and close NNG socket ''' 268 | if self.__is_connected(): 269 | 270 | # disconnect and close socket 271 | self.__disconnect() 272 | self.__close() 273 | 274 | # PRIVATE INSTANCE METHODS 275 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 276 | # NOTE: Here is the place to define internal functions required only for this 277 | # specific service implementation 278 | 279 | def __is_socket(self): 280 | ''' check if the socket is open ''' 281 | return (self.__socket != None and self.__socket != 0) 282 | 283 | def __is_connected(self): 284 | ''' check if a connection to a service is active ''' 285 | return (self.__socket != None and self.__socket != 0 and self.__dialer) 286 | 287 | def __connect(self): 288 | ''' connect to looking glass bridge ''' 289 | 290 | # set default error value 291 | error = self.client_error.CLIERR_NOERROR.value 292 | 293 | # if there is not already a connection 294 | if self.__dialer == None: 295 | 296 | # try to connect to the Looking Glass Bridge 297 | try: 298 | 299 | self.__dialer = self.__socket.dial(self.__address, block = True) 300 | 301 | # TODO: Set proper error values 302 | error = self.client_error.CLIERR_NOERROR.value 303 | 304 | return True 305 | 306 | # if the connection was not established 307 | except:# pynng.exceptions.ConnectionRefused 308 | 309 | # Close socket and reset status variable 310 | self.__close() 311 | 312 | logger.error("Could not connect. Is Looking Glass Bridge running?") 313 | 314 | return False 315 | 316 | logger.info("Already connected to Looking Glass Bridge v%s." % self.get_version()) 317 | return True 318 | 319 | def __disconnect(self): 320 | ''' disconnect from looking glass bridge ''' 321 | 322 | # if a connection is active 323 | if self.__is_connected(): 324 | self.__dialer.close() 325 | self.__dialer = None 326 | logger.info("Closed connection to %s." % self.name) 327 | return True 328 | 329 | # otherwise 330 | logger.info("There is no active connection to close.") 331 | return False 332 | 333 | def __close(self): 334 | ''' close NNG socket ''' 335 | 336 | # Close socket and reset status variable 337 | if self.__is_socket(): 338 | self.__socket.close() 339 | 340 | # reset state variables 341 | self.__socket = None 342 | self.__dialer = None 343 | self.version = "" 344 | 345 | def __send_message(self, input_object, image_shape=None): 346 | ''' send a message to Looking Glass Bridge ''' 347 | 348 | # if a NNG socket is open 349 | if self.__is_socket(): 350 | start = time.time() 351 | 352 | # dump a CBOR message 353 | if image_shape is None: 354 | cbor_dump = cbor.dumps(input_object) 355 | else: 356 | cbor_dump = cbor.dumps(input_object, image_shape=image_shape) 357 | 358 | logger.debug(" [#] Encoding command as CBOR before sending took %.3f ms." % ((time.time() - start) * 1000)) 359 | start = time.time() 360 | 361 | # send it to the socket 362 | self.__socket.send(cbor_dump) 363 | 364 | logger.debug(" [#] Sending command of length %i took %.3f ms." % (len(cbor_dump), (time.time() - start) * 1000)) 365 | start = time.time() 366 | 367 | # receive the CBOR-formatted response 368 | if not ('show' in input_object['cmd'].keys()): 369 | response = self.__socket.recv() 370 | else: 371 | return#response = self.__socket.recv() 372 | 373 | logger.debug(" [#] Waiting for response took %.3f ms." % ((time.time() - start) * 1000)) 374 | 375 | # return the decoded CBOR response length and its conent 376 | return [len(response), cbor.loads(response)] 377 | 378 | def __calculate_derived(self, calibration): 379 | ''' calculate the values derived from the calibration json delivered by Looking Glass Bridge ''' 380 | 381 | calibration['aspect'] = calibration['screenW'] / calibration['screenH'] 382 | calibration['tilt'] = calibration['screenH'] / (calibration['screenW'] * calibration['slope']) 383 | calibration['pitch'] = - calibration['screenW'] / calibration['DPI'] * calibration['pitch'] * math.sin(math.atan(abs(calibration['slope']))) 384 | calibration['subp'] = calibration['pitch'] / (3 * calibration['screenW']) 385 | calibration['ri'], calibration['bi'] = (2,0) if calibration['flipSubp'] else (0,2) 386 | calibration['fringe'] = 0.0 387 | 388 | return calibration 389 | 390 | 391 | # PRIVATE STATIC METHODS 392 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 393 | @staticmethod 394 | def __init(client_name): 395 | ''' initialize the client at Looking Glass Bridge with the given name ''' 396 | 397 | # log info 398 | logger.debug("Registering at Looking Glass Bridge with INIT command:") 399 | 400 | # define CBOR command 401 | command = { 402 | 'cmd': { 403 | 'init': {'appid': client_name}, 404 | }, 405 | 'bin': '', 406 | } 407 | return command 408 | 409 | @staticmethod 410 | def __get_devices(): 411 | ''' tell Looking Glass Bridge to send the calibrations of all devices ''' 412 | 413 | # log info 414 | logger.debug("Requesting device list from Looking Glass Bridge with INFO command:") 415 | 416 | # define CBOR command 417 | command = { 418 | 'cmd': { 419 | 'info': {}, 420 | }, 421 | 'bin': '', 422 | } 423 | return command 424 | 425 | @staticmethod 426 | def __show_demo(dev_index): 427 | ''' tell Looking Glass Bridge to show the demo quilt ''' 428 | 429 | # log info 430 | logger.debug("Requesting demo quilt from Looking Glass Bridge with SHOW command:") 431 | 432 | # define CBOR command 433 | command = { 434 | 'cmd': { 435 | 'show': { 436 | 'targetDisplay': dev_index, 437 | }, 438 | }, 439 | } 440 | return command 441 | 442 | @staticmethod 443 | def __show_quilt(dev_index, bindata, settings): 444 | ''' tell Looking Glass Bridge to display the incoming quilt ''' 445 | 446 | # log info 447 | logger.debug("Requesting quilt display from Looking Glass Bridge with SHOW command:") 448 | 449 | # define CBOR command 450 | command = { 451 | 'cmd': { 452 | 'show': { 453 | 'targetDisplay': dev_index, 454 | 'source': 'bindata', 455 | 'quilt': { 456 | 'type': 'image', 457 | 'settings': settings 458 | } 459 | }, 460 | }, 461 | 'bin': bindata, 462 | } 463 | return command 464 | 465 | @staticmethod 466 | def __load_quilt(dev_index, name, settings = None): 467 | ''' tell Looking Glass Bridge to load a cached quilt ''' 468 | 469 | # log info 470 | logger.debug("Requesting to load cached quilt from Looking Glass Bridge with SHOW command:") 471 | 472 | # define CBOR command 473 | command = { 474 | 'cmd': { 475 | 'show': { 476 | 'targetDisplay': dev_index, 477 | 'source': 'cache', 478 | 'quilt': { 479 | 'type': 'image', 480 | 'name': name 481 | }, 482 | }, 483 | }, 484 | 'bin': bytes(), 485 | } 486 | 487 | # if settings were specified 488 | if settings: command['cmd']['show']['quilt']['settings'] = settings 489 | 490 | return command 491 | 492 | @staticmethod 493 | def __cache_quilt(dev_index, bindata, name, settings): 494 | ''' tell Looking Glass Bridge to cache the incoming quilt ''' 495 | 496 | # log info 497 | logger.debug("Requesting to cache quilt by Looking Glass Bridge with CACHE command:") 498 | 499 | # define CBOR command 500 | command = { 501 | 'cmd': { 502 | 'cache': { 503 | 'targetDisplay': dev_index, 504 | 'quilt': { 505 | 'type': 'image', 506 | 'name': name, 507 | 'settings': settings 508 | } 509 | } 510 | }, 511 | 'bin': bindata, 512 | } 513 | return command 514 | 515 | @staticmethod 516 | def __hide(dev_index): 517 | ''' tell Looking Glass Bridge to hide the displayed quilt ''' 518 | 519 | # log info 520 | logger.debug("Requesting to hide quilt from Looking Glass Bridge with HIDE command:") 521 | 522 | # define CBOR command 523 | command = { 524 | 'cmd': { 525 | 'hide': { 526 | 'targetDisplay': dev_index, 527 | }, 528 | }, 529 | 'bin': bytes(), 530 | } 531 | return command 532 | 533 | @staticmethod 534 | def __wipe(dev_index): 535 | ''' tell Looking Glass Bridge to clear the display (shows the logo quilt) ''' 536 | 537 | # log info 538 | logger.debug("Requesting to wipe the quilt from Looking Glass Bridge with HIDE command:") 539 | 540 | # define CBOR command 541 | command = { 542 | 'cmd': { 543 | 'targetDisplay': dev_index, 544 | 'wipe': {}, 545 | }, 546 | 'bin': bytes(), 547 | } 548 | return command 549 | -------------------------------------------------------------------------------- /lib/pylightio/managers/__init__.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | from pylightio.managers.devices import * 20 | from pylightio.managers.services import * 21 | -------------------------------------------------------------------------------- /lib/pylightio/managers/devices.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # EXTERNAL PACKAGE DEPENDENCIES 20 | ################################################### 21 | from enum import Enum 22 | 23 | # INTERNAL PACKAGE DEPENDENCIES 24 | ################################################### 25 | # NONE 26 | 27 | # PREPARE LOGGING 28 | ################################################### 29 | import logging 30 | 31 | # get the library logger 32 | logger = logging.getLogger('pyLightIO') 33 | 34 | 35 | # DEVICE MANAGER FOR LIGHTFIELD DISPLAYS 36 | ############################################### 37 | class DeviceManager(object): 38 | ''' 39 | The :class:`DeviceManager` class is the factory class for generating 40 | instances of the different device types implemented based on the 41 | :class:`pylightio.BaseDeviceType` class of pyLightIO. Each device manager is 42 | based on a service. This service handles all device communications, while 43 | the device manager provides all functions for organizing the devices 44 | internally. 45 | ''' 46 | 47 | # DEFINE CLASS PROPERTIES AS PRIVATE MEMBERS 48 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 49 | __dev_count = 0 # number of device instances 50 | __dev_list = [] # list for initialized device instances 51 | __dev_active = None # currently active device instance 52 | __dev_service = None # the service used by the device manager 53 | 54 | 55 | # CLASS METHODS 56 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 57 | @classmethod 58 | def get_service(cls): 59 | ''' 60 | Returns the service which is used by this device manager. 61 | ''' 62 | return cls.__dev_service 63 | 64 | @classmethod 65 | def set_service(cls, service): 66 | ''' 67 | Set the service to used by this device manager. 68 | 69 | :param service: The instance of a service class, which is based on the 70 | :class:`pylightio.BaseServiceType` class. 71 | :type service: class:`pylightio.BaseServiceType` 72 | 73 | :return: The new service of the device manager. 74 | :rtype: :class:`pylightio.BaseServiceType` 75 | ''' 76 | cls.__dev_service = service 77 | return cls.__dev_service 78 | 79 | @classmethod 80 | def refresh(cls, emulate_remaining = True): 81 | ''' 82 | Refresh the device list of the device manager. This calls the service's 83 | `get_devices()` method. 84 | 85 | :param emulate_remaining: If `True`, the device manager adds one emulated 86 | device of each type to the device list. 87 | :type emulate_remaining: bool, optional (default: `True`) 88 | :return: No return value. 89 | :rtype: None 90 | ''' 91 | 92 | # if the service ready 93 | if cls.__dev_service and cls.__dev_service.is_ready(): 94 | 95 | instances = [] 96 | 97 | # set all (not emulated) devices to "disconnected" 98 | # NOTE: We don't delete the devices, because that would be more 99 | # complex to handle when the user already used the specific 100 | # device type instance for their settings 101 | for d in cls.__dev_list: 102 | if d.emulated == False: 103 | d.connected = False 104 | 105 | # request devices 106 | devices = cls.__dev_service.get_devices() 107 | if devices: 108 | 109 | # for each device returned create a LookingGlassDevice instance 110 | # of the corresponding type 111 | for idx, device in enumerate(devices): 112 | 113 | # try to find the instance of this device 114 | instance = list(filter(lambda d: d.serial == device['calibration']['serial'], cls.__dev_list)) 115 | 116 | # if no instance of this device exists 117 | if not instance: 118 | 119 | # create a device instance of the corresponding type 120 | instance = cls.add_device(device['hardwareVersion'], device) 121 | 122 | else: 123 | 124 | # update the configuration 125 | instance[0].configuration = device 126 | 127 | # make sure the state of the device instance is "connected" 128 | instance[0].connected = True 129 | 130 | return None 131 | 132 | logger.error("No Looking Glass Bridge connection. The device list could not be obtained. ") 133 | 134 | @classmethod 135 | def add_device(cls, device_type, device_configuration = None): 136 | ''' 137 | Add a new device of type `device_type` to the device manager. 138 | 139 | :param emulate_remaining: If `True`, the device manager adds one emulated 140 | device of each type to the device list. 141 | :type emulate_remaining: bool, optional (default: `True`) 142 | :return: The added device. 143 | :rtype: :class:`pylightio.BaseDeviceType` or subclass of it 144 | ''' 145 | # try to find the class for the specified type, if it exists 146 | DeviceTypeClass = [subclass for subclass in BaseDeviceType.__subclasses__() if subclass.type == device_type] 147 | 148 | # call the corresponding type 149 | if DeviceTypeClass: 150 | 151 | # create the device instance 152 | device = DeviceTypeClass[0](cls.__dev_service, device_configuration) 153 | 154 | # increment device count 155 | # NOTE: this number is never decreased to prevent ambiguities of the id 156 | cls.__dev_count += 1 157 | 158 | # append registered device to the device list 159 | cls.__dev_list.append(device) 160 | 161 | return device 162 | 163 | # otherwise raise an exception 164 | raise ValueError("There is no Looking Glass of type '%s'." % device_type) 165 | 166 | 167 | @classmethod 168 | def remove_device(cls, device): 169 | ''' 170 | Remove a previously added device from the device manager. 171 | 172 | :param device: The :class:`pylightio.BaseDeviceType` to be removed. 173 | :type device: :class:`pylightio.BaseDeviceType` or a subclass of it 174 | :return: `True` if the device was successfully removed. 175 | :rtype: bool 176 | ''' 177 | 178 | # if the device is in the list 179 | if device in cls.__dev_list: 180 | 181 | # create the device instance 182 | logger.info("Removing device '%s' ..." % (device)) 183 | 184 | # if this device is the active device, set_active 185 | if cls.get_active() == device.id: cls.reset_active() 186 | 187 | cls.__dev_list.remove(device) 188 | 189 | return True 190 | 191 | # otherwise raise an exception 192 | raise ValueError("The device '%s' is not in the list." % device) 193 | 194 | return False 195 | 196 | @classmethod 197 | def add_emulated(cls, filter=None): 198 | ''' 199 | Add one emulated device for each supported device type to the device 200 | manager. A `filter` can be applied to add only specific device types. 201 | 202 | :param filter: List of :class:`pylightio.BaseDeviceType` to be added. 203 | :type filter: :class:`pylightio.BaseDeviceType` or a subclass of it 204 | ''' 205 | 206 | # for each device type which is not in "except" list 207 | for DeviceType in set(BaseDeviceType.__subclasses__()) - set([DeviceType for DeviceType in cls.__subclasses__() if DeviceType.type in filter ]): 208 | 209 | # if not already emulated 210 | if not (DeviceType.type in [d.type for d in cls.__dev_list if d.emulated == True]): 211 | 212 | # create an instance without passing a configuration 213 | # (that will created an emulated device) 214 | instance = cls.add_device(DeviceType.type) 215 | 216 | return True 217 | 218 | @classmethod 219 | def to_list(cls, show_connected = True, show_emulated = False, filter_by_type = None): 220 | ''' enumerate the devices of this device manager as list ''' 221 | return [d for d in cls.__dev_list if ((show_connected == None or d.connected == show_connected) and (show_emulated == None or d.emulated == show_emulated)) and (filter_by_type == None or type(d) == filter_by_type)] 222 | 223 | @classmethod 224 | def count(cls, show_connected = True, show_emulated = False, filter_by_type = None): 225 | ''' get number of devices ''' 226 | return len(cls.to_list(show_connected, show_emulated, filter_by_type)) 227 | 228 | @classmethod 229 | def get_active(cls): 230 | ''' 231 | Return the active device,i.e., the one currently used by the user. 232 | ''' 233 | return cls.__dev_active 234 | 235 | @classmethod 236 | def set_active(cls, id=None, key=None, value=None): 237 | ''' set the active device (i.e., the one currently used by the user) ''' 238 | 239 | # if a custom key and value are given 240 | if key is not None and value is not None: 241 | 242 | for device in cls.__dev_list: 243 | if hasattr(device, key) and (getattr(device, key) == value): 244 | cls.__dev_active = device 245 | return device 246 | 247 | # otherwise we use the id 248 | elif (id is not None): 249 | 250 | for device in cls.__dev_list: 251 | if (device.id == id): 252 | cls.__dev_active = device 253 | return device 254 | 255 | # else raise exception 256 | raise ValueError("The given device with id '%i' is not in the list." % id) 257 | 258 | # else raise exception 259 | raise ValueError("No valid keyword and value were given to identify the device.") 260 | 261 | @classmethod 262 | def get_device(cls, id=None, key=None, value=None): 263 | ''' get device instance based on the given key/value pair or id ''' 264 | 265 | # if a custom key and value are given 266 | if key is not None and value is not None: 267 | 268 | for device in cls.__dev_list: 269 | if hasattr(device, key) and (getattr(device, key) == value): 270 | return device 271 | 272 | # otherwise we use the id 273 | elif (id is not None): 274 | 275 | for device in cls.__dev_list: 276 | if (device.id == id): 277 | return device 278 | 279 | # else raise exception 280 | raise ValueError("The given device with id '%i' is not in the list." % id) 281 | 282 | # else raise exception 283 | raise ValueError("No valid keyword and value were given to identify the device.") 284 | 285 | @classmethod 286 | def reset_active(cls): 287 | ''' set the active device to None ''' 288 | cls.__dev_active = None 289 | 290 | @classmethod 291 | def exists(cls, serial=None, type=None): 292 | ''' check if the device instance already exists ''' 293 | if serial and serial in [d.serial for d in cls.__dev_list]: 294 | return True 295 | 296 | return False 297 | 298 | 299 | # BASE CLASS FOR DEVICE TYPES 300 | ############################################### 301 | # base class for the implementation of different lightfield display types. 302 | # all device types implemented must be a subclass of this base class 303 | class BaseDeviceType(object): 304 | 305 | 306 | # PRIVATE MEMBERS 307 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 308 | __id = None # ID of the device instance 309 | __type = None # the unique identifier string of each device type 310 | __service = None # the service the device was registered with 311 | __emulated = False # is the device instance emulated? 312 | __connected = True # is the device still connected? 313 | __presets = [] # list for the quilt presets 314 | 315 | __lightfield = None # the lightfield currently displayed on this device 316 | 317 | 318 | # INSTANCE METHODS - IMPLEMENTED BY BASE CLASS 319 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 320 | # NOTE: Here is the place to implement functions that should not be overriden 321 | # by the subclasses, which represent the specific device types 322 | 323 | def __init__(self, service, configuration=None): 324 | ''' initialize the device instance ''' 325 | 326 | # assign an id 327 | self.id = DeviceManager.count(None, None) 328 | 329 | # if a configuration was passed 330 | if configuration: 331 | 332 | # use it 333 | self.configuration = configuration 334 | 335 | # bind the specified service to the device instance 336 | self.service = service 337 | 338 | # set the state variables for connected devices 339 | self.connected = True 340 | self.emulated = False 341 | 342 | # create the device instance 343 | logger.info("Created class instance for the connected device '%s' of type '%s'." % (self, self.type)) 344 | 345 | else: 346 | 347 | # otherwise apply the device type's dummy configuration 348 | # and assume the device is emulated 349 | self.configuration = self.emulated_configuration 350 | 351 | # use it 352 | self.service = None 353 | 354 | # set the state variables for connected devices 355 | self.connected = False 356 | self.emulated = True 357 | 358 | # create the device instance 359 | logger.info("Emulating device '%s' of type '%s'." % (self, self.type)) 360 | 361 | def __str__(self): 362 | ''' the display name of the device when the instance is called ''' 363 | 364 | if self.emulated == False: return self.name + " (id: " + str(self.id) + ")" 365 | if self.emulated == True: return "[Emulated] " + self.name + " (id: " + str(self.id) + ")" 366 | 367 | 368 | # TEMPLATE METHODS - IMPLEMENTED BY SUBCLASS 369 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 370 | # NOTE: These methods must be implemented by the subclasses, which represent 371 | # the specific device types. 372 | def display(self, lightfield, custom_decoder = None, **kwargs): 373 | ''' do some checks if required and hand it over for displaying ''' 374 | # NOTE: This method should only pre-process the image, if the device 375 | # type requires that. Then call service methods to display it. 376 | 377 | pass 378 | 379 | 380 | 381 | # CLASS PROPERTIES - GENRAL 382 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 383 | @property 384 | def id(self): 385 | return self.__id 386 | 387 | @id.setter 388 | def id(self, value): 389 | self.__id = value 390 | 391 | @property 392 | def sevice(self): 393 | return self.__sevice 394 | 395 | @sevice.setter 396 | def sevice(self, value): 397 | self.__sevice = value 398 | 399 | @property 400 | def emulated(self): 401 | return self.__emulated 402 | 403 | @emulated.setter 404 | def emulated(self, value): 405 | self.__emulated = value 406 | 407 | @property 408 | def connected(self): 409 | return self.__connected 410 | 411 | @connected.setter 412 | def connected(self, value): 413 | self.__connected = value 414 | 415 | @property 416 | def presets(self): 417 | return self.__presets 418 | 419 | @presets.setter 420 | def presets(self, value): 421 | self.__presets = value 422 | 423 | # CLASS PROPERTIES 424 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 425 | @property 426 | def name(self): 427 | return self.__name 428 | 429 | @name.setter 430 | def name(self, value): 431 | self.__name = value 432 | 433 | @property 434 | def configuration(self): 435 | return self.__configuration 436 | 437 | @configuration.setter 438 | def configuration(self, value): 439 | self.__configuration = value 440 | 441 | @property 442 | def lightfield(self): 443 | return self.__lightfield 444 | 445 | @lightfield.setter 446 | def lightfield(self, value): 447 | self.__lightfield = value 448 | -------------------------------------------------------------------------------- /lib/pylightio/managers/services.py: -------------------------------------------------------------------------------- 1 | # ###################### BEGIN LICENSE BLOCK ########################### 2 | # 3 | # Copyright © 2021 Christian Stolze 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 | # ####################### END LICENSE BLOCK ############################ 18 | 19 | # EXTERNAL PACKAGE DEPENDENCIES 20 | ################################################### 21 | from enum import Enum 22 | 23 | # INTERNAL PACKAGE DEPENDENCIES 24 | ################################################### 25 | # NONE 26 | 27 | # PREPARE LOGGING 28 | ################################################### 29 | import logging 30 | 31 | # get the library logger 32 | logger = logging.getLogger('pyLightIO') 33 | 34 | 35 | 36 | # SERVICE MANAGER FOR LIGHTFIELD DISPLAYS 37 | ############################################### 38 | # the service manager is the factory class for generating service instances of 39 | # the different service types 40 | class ServiceManager(object): 41 | 42 | # PRIVATE MEMBERS 43 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | __active = None # active service 45 | __service_count = [] # number of created services 46 | __service_list = [] # list of created services 47 | 48 | 49 | # CLASS METHODS 50 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 51 | @classmethod 52 | def add(cls, service_type, client_name = ""): 53 | ''' open the service of the specified type ''' 54 | 55 | # try to find the class for the specified type, if it exists 56 | ServiceTypeClass = [subclass for subclass in BaseServiceType.__subclasses__() if (subclass == service_type or subclass.type == service_type)] 57 | 58 | # if a service of the specified type was found 59 | if ServiceTypeClass: 60 | 61 | # create the service instance 62 | service = ServiceTypeClass[0](client_name = client_name) 63 | 64 | # append registered device to the device list 65 | cls.__service_list.append(service) 66 | 67 | # make this service the active service if no service is active or this is the first ready service 68 | if (not cls.get_active() or (cls.get_active() and not cls.get_active().is_ready())): 69 | cls.set_active(service) 70 | 71 | # if the service is ready 72 | if service.is_ready(): 73 | 74 | logger.info("Added service '%s' to the service manager. Service is ready." % service) 75 | 76 | else: 77 | 78 | logger.warning("Added service '%s' to the service manager, but service is not ready." % service) 79 | 80 | return service 81 | 82 | # otherwise raise an exception 83 | raise ValueError("There is no service of type '%s'." % service_type) 84 | 85 | @classmethod 86 | def to_list(cls): 87 | ''' enumerate the services of this service manager as list ''' 88 | return cls.__service_list 89 | 90 | @classmethod 91 | def count(cls): 92 | ''' return number of services ''' 93 | return len(cls.to_list()) 94 | 95 | @classmethod 96 | def get_active(cls): 97 | ''' return the active service (i.e., the one currently used by the app / user) ''' 98 | return cls.__active 99 | 100 | @classmethod 101 | def set_active(cls, service): 102 | ''' set the active service (i.e., the one currently used by the app / user) ''' 103 | if service in cls.__service_list: 104 | cls.__active = service 105 | return service 106 | 107 | # else raise exception 108 | raise ValueError("The given device with id '%i' is not in the list." % id) 109 | 110 | @classmethod 111 | def reset_active(cls): 112 | ''' set the active service to None ''' 113 | cls.__service_active = None 114 | 115 | @classmethod 116 | def remove(cls, service): 117 | ''' remove the service from the ServiceManager ''' 118 | # NOTE: 119 | 120 | # if the device is in the list 121 | if service in cls.__service_list: 122 | 123 | # create the device instance 124 | logger.info("Removing service '%s' ..." % (service)) 125 | 126 | # if this device is the active device, set_active 127 | if cls.get_active() == service: cls.reset_active() 128 | 129 | cls.__service_list.remove(service) 130 | 131 | # then delete the service instance 132 | del service 133 | 134 | return True 135 | 136 | # BASE CLASS OF SERVICE TYPES 137 | ############################################### 138 | # the service type class used for handling lightfield display communication 139 | # all service type implementations must be a subclass of this base class 140 | class BaseServiceType(object): 141 | 142 | # DEFINE CLASS PROPERTIES AS PROTECTED MEMBERS 143 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 144 | type = None # the unique identifier string of a service type (required for the factory class) 145 | 146 | # DEFINE CLASS PROPERTIES AS PRIVATE MEMBERS 147 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 148 | __version = "" # version string of the service 149 | 150 | # INSTANCE METHODS - IMPLEMENTED BY BASE CLASS 151 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 152 | # NOTE: Here is the place to implement functions that should not be overriden 153 | # by the subclasses, which represent the specific service types 154 | 155 | def __str__(self): 156 | ''' the display name of the service when the instance is called ''' 157 | 158 | if self.get_version(): 159 | return "%s v%s" % (self.name, self.get_version()) 160 | else: 161 | return "%s" % (self.name) 162 | 163 | # TEMPLATE METHODS 164 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 165 | # NOTE: These methods must be implemented by the subclasses, which represent 166 | # the specific service type 167 | def __init__(self): 168 | ''' handle initialization of the class instance and the specific service ''' 169 | pass 170 | 171 | def is_ready(self): 172 | ''' handles check if the service is ready ''' 173 | pass 174 | 175 | def get_version(self): 176 | ''' method to obtain the service version ''' 177 | pass 178 | 179 | def get_devices(self): 180 | ''' method to request the connected devices ''' 181 | ''' this function should return a list of device configurations ''' 182 | pass 183 | 184 | def display(self, device, lightfield, aspect=None, custom_decoder = None): 185 | ''' display a given lightfield image object on a device ''' 186 | pass 187 | 188 | def clear(self, device): 189 | ''' clear the display of a given device ''' 190 | pass 191 | 192 | def __del__(self): 193 | ''' handles closing / deinitializing the service ''' 194 | pass 195 | 196 | # CLASS PROPERTIES 197 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 198 | @property 199 | def service(self): 200 | return self.__service 201 | 202 | @service.setter 203 | def service(self, value): 204 | self.__service = value 205 | 206 | @property 207 | def version(self): 208 | return self.__version 209 | 210 | @version.setter 211 | def version(self, value): 212 | self.__version = value 213 | -------------------------------------------------------------------------------- /lib/wheels/pynng-0.7.4+dev-cp310-cp310-macosx_10_9_universal2.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regcs/AliceLG/34e98368f16d2b2bd5939a579ae788b2c4ad925c/lib/wheels/pynng-0.7.4+dev-cp310-cp310-macosx_10_9_universal2.whl -------------------------------------------------------------------------------- /lib/wheels/pynng-0.7.4+dev-cp311-cp311-macosx_10_9_universal2.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regcs/AliceLG/34e98368f16d2b2bd5939a579ae788b2c4ad925c/lib/wheels/pynng-0.7.4+dev-cp311-cp311-macosx_10_9_universal2.whl -------------------------------------------------------------------------------- /lib/wheels/pynng-0.7.4+dev-cp312-cp312-macosx_10_9_universal2.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regcs/AliceLG/34e98368f16d2b2bd5939a579ae788b2c4ad925c/lib/wheels/pynng-0.7.4+dev-cp312-cp312-macosx_10_9_universal2.whl -------------------------------------------------------------------------------- /lib/wheels/pynng-0.7.4+dev-cp38-cp38-macosx_10_9_universal2.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regcs/AliceLG/34e98368f16d2b2bd5939a579ae788b2c4ad925c/lib/wheels/pynng-0.7.4+dev-cp38-cp38-macosx_10_9_universal2.whl -------------------------------------------------------------------------------- /lib/wheels/pynng-0.7.4+dev-cp39-cp39-macosx_10_9_universal2.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regcs/AliceLG/34e98368f16d2b2bd5939a579ae788b2c4ad925c/lib/wheels/pynng-0.7.4+dev-cp39-cp39-macosx_10_9_universal2.whl -------------------------------------------------------------------------------- /logs/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2020 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | 21 | # THIS FILE IS NEEDED FOR PYTHON: 22 | # https://docs.python.org/3/tutorial/modules.html#packages 23 | -------------------------------------------------------------------------------- /preferences.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2021 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | # MODULE DESCRIPTION: 21 | # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 22 | # This includes everything that is related to the add-on preferences, 23 | # installation of requirements, etc. 24 | 25 | # ------------------ INTERNAL MODULES -------------------- 26 | from .globals import * 27 | 28 | # ------------------- EXTERNAL MODULES ------------------- 29 | import bpy 30 | from bpy.types import AddonPreferences 31 | 32 | # ---------------- GLOBAL ADDON LOGGER ------------------- 33 | import logging 34 | LookingGlassAddonLogger = logging.getLogger('Alice/LG') 35 | 36 | # ------------- DEFINE ADDON PREFERENCES ---------------- 37 | # an operator that installs the python dependencies 38 | class LOOKINGGLASS_OT_install_dependencies(bpy.types.Operator): 39 | bl_idname = "lookingglass.install_dependencies" 40 | bl_label = "Install Dependencies" 41 | bl_description = "Install all Python dependencies required by this add-on to the add-on directory." 42 | bl_options = {'REGISTER', 'INTERNAL'} 43 | 44 | def execute(self, context): 45 | 46 | # if dependencies are missing 47 | if not LookingGlassAddon.check_dependecies(): 48 | 49 | import platform, subprocess 50 | import datetime 51 | 52 | # path to python 53 | python_path = bpy.path.abspath(sys.executable) 54 | 55 | # generate logfile 56 | logfile = open(bpy.path.abspath(LookingGlassAddon.logpath + 'side-packages-install.log'), 'a') 57 | LookingGlassAddonLogger.info("Installing missing side-packages. See '%s' for details." % (LookingGlassAddon.logpath + 'side-packages-install.log',)) 58 | 59 | # ensure that pip is installed 60 | # NOTE: This should not be required, but a Linux user reported that 61 | # on Blender 2.93 PIP was not bundled 62 | if platform.system() == 'Linux': subprocess.call([python_path, '-m', 'ensurepip'], stdout=logfile) 63 | 64 | # install the dependencies to the add-on's library path 65 | for module in LookingGlassAddon.external_dependecies: 66 | if not LookingGlassAddon.is_installed(module): 67 | 68 | # This is a workaround: 69 | # - pynng is currently not maintained anymore 70 | # - we compiled wheels ourselves and include them with Alice/LG 71 | # - pynng will be obsolete in newer versions of Alice/LG 72 | if module[1] == 'pynng': 73 | 74 | subprocess.call([python_path, '-m', 'pip', 'download', module[1], '--dest=' + os.path.join(LookingGlassAddon.libpath, 'wheels'), '--no-cache'], stdout=logfile) 75 | subprocess.call([python_path, '-m', 'pip', 'install', '--find-links=' + os.path.join(LookingGlassAddon.libpath, 'wheels'), module[1], '--target', LookingGlassAddon.libpath, '--no-cache', '--no-index'] + module[3], stdout=logfile) 76 | 77 | else: 78 | 79 | subprocess.call([python_path, '-m', 'pip', 'install', '--upgrade', module[1], '--target', LookingGlassAddon.libpath, '--no-cache'] + module[3], stdout=logfile) 80 | 81 | logfile.write("###################################" + '\n') 82 | logfile.write("Installer finished: " + str(datetime.datetime.now()) + '\n') 83 | logfile.write("###################################" + '\n') 84 | 85 | # close logfile 86 | logfile.close() 87 | 88 | return {'FINISHED'} 89 | 90 | # Preferences pane for this Addon in the Blender preferences 91 | class LOOKINGGLASS_PT_install_dependencies(AddonPreferences): 92 | bl_idname = __package__ 93 | 94 | # render mode 95 | render_mode: bpy.props.EnumProperty( 96 | items = [('0', 'Single Camera Mode', 'The quilt is rendered using a single moving camera.'), 97 | ('1', 'Multiview Camera Mode', 'The quilt is rendered using Blenders multiview mechanism.')], 98 | default='0', 99 | name="Render Mode", 100 | ) 101 | 102 | # logger level 103 | logger_level: bpy.props.EnumProperty( 104 | items = [('0', 'Debug messages', 'All messages are written to the log file. This is for detailed debugging and extended bug reports'), 105 | ('1', 'Info, Warnings, and Errors', 'All info, warning, and error messages are written to the log file. This is for standard bug reports'), 106 | ('2', 'Only Errors', 'Only error messages are written to the log file. This is for less verbose console outputs')], 107 | default='1', 108 | name="Logging Mode", 109 | update=LookingGlassAddon.update_logger_levels, 110 | ) 111 | console_output: bpy.props.BoolProperty( 112 | default=False, 113 | name="Log to console", 114 | description="Additionally log outputs to std out for debugging", 115 | update=LookingGlassAddon.update_logger_levels, 116 | ) 117 | 118 | # need this here, since the actual logger level property is not initialized 119 | # before the dependencies are installed. but we want to log all details 120 | logger_level = 0 121 | console_output = False 122 | 123 | # draw function 124 | def draw(self, context): 125 | 126 | # Notify the user and provide an option to install 127 | layout = self.layout 128 | layout.alert = True 129 | 130 | # draw an Button for Installation of python dependencies 131 | if not LookingGlassAddon.check_dependecies(): 132 | 133 | row = layout.row() 134 | row.alignment = 'EXPAND' 135 | row.scale_y = 0.5 136 | row.label(text="Some Python dependencies are missing for Alice/LG to work. These modules are") 137 | row = layout.row(align=True) 138 | row.alignment = 'EXPAND' 139 | row.scale_y = 0.5 140 | row.label(text="required to communicate with Looking Glass Bridge. If you click the button below,") 141 | row = layout.row(align=True) 142 | row.alignment = 'EXPAND' 143 | row.scale_y = 0.5 144 | row.label(text="the required modules will be installed to the addon's path. This may take a few") 145 | row = layout.row(align=True) 146 | row.alignment = 'EXPAND' 147 | row.scale_y = 0.5 148 | row.label(text="minutes, during which the Blender user interface will be unresponsive.") 149 | row = layout.row(align=True) 150 | row.alignment = 'EXPAND' 151 | row.operator("lookingglass.install_dependencies", icon='PLUS') 152 | 153 | else: 154 | 155 | row = layout.row() 156 | row.label(text="All required Python modules were installed.") 157 | row = layout.row() 158 | row.label(text="Please restart Blender to activate the changes!", icon='ERROR') 159 | 160 | 161 | # Preferences pane for this Addon in the Blender preferences 162 | class LOOKINGGLASS_PT_preferences(AddonPreferences): 163 | bl_idname = __package__ 164 | bl_label = "Alice/LG Preferences" 165 | 166 | # render mode 167 | render_mode: bpy.props.EnumProperty( 168 | items = [('0', 'Current Blender instance', 'The quilt is rendered in the current Blender instance.'), 169 | ('1', 'Multiple background instances', 'The quilt is rendered in multiple Blender instances running in the background.')], 170 | default='0', 171 | name="Render Mode", 172 | ) 173 | # camera mode for rendering 174 | camera_mode: bpy.props.EnumProperty( 175 | items = [('0', 'Single Camera Mode', 'The quilt is rendered using a single moving camera.'), 176 | ('1', 'Multiview Camera Mode', 'The quilt is rendered using Blenders multiview mechanism.')], 177 | default='0', 178 | name="Camera Mode", 179 | ) 180 | 181 | # logger level 182 | logger_level: bpy.props.EnumProperty( 183 | items = [('0', 'Debug messages', 'All messages are written to the log file. This is for detailed debugging and extended bug reports'), 184 | ('1', 'Info, Warnings, and Errors', 'All info, warning, and error messages are written to the log file. This is for standard bug reports'), 185 | ('2', 'Only Errors', 'Only error messages are written to the log file. This is for less verbose console outputs')], 186 | default='1', 187 | name="Logging Mode", 188 | update=LookingGlassAddon.update_logger_levels, 189 | ) 190 | 191 | console_output: bpy.props.BoolProperty( 192 | default=False, 193 | name="Log to console", 194 | description="Additionally log outputs to std out for debugging", 195 | update=LookingGlassAddon.update_logger_levels, 196 | ) 197 | # need this here, since the actual logger level property is not initialized 198 | # before the dependencies are installed. but we want to log all details 199 | logger_level = 0 200 | console_output = False 201 | 202 | # draw function 203 | def draw(self, context): 204 | layout = self.layout 205 | 206 | # # render mode 207 | # row_render_mode = layout.row() 208 | # column_1 = row_render_mode.column() 209 | # column_1.label(text="Render Mode:") 210 | # column_1.scale_x = 0.2 211 | # column_2 = row_render_mode.column() 212 | # column_2.prop(self, "render_mode", text="") 213 | # column_2.scale_x = 0.8 214 | 215 | # camera mode for rendering 216 | row_camera_mode = layout.row() 217 | column_1 = row_camera_mode.column() 218 | column_1.label(text="Camera Mode:") 219 | column_1.scale_x = 0.2 220 | column_2 = row_camera_mode.column() 221 | column_2.prop(self, "camera_mode", text="") 222 | column_2.scale_x = 0.8 223 | 224 | # logger level 225 | row_logger = layout.row() 226 | column_1 = row_logger.column() 227 | column_1.label(text="Logging Mode:") 228 | column_1.scale_x = 0.2 229 | column_2 = row_logger.column() 230 | column_2.prop(self, "logger_level", text="") 231 | column_2.scale_x = 0.55 232 | column_3 = row_logger.column() 233 | column_3.prop(self, "console_output") 234 | column_3.scale_x = 0.25 235 | -------------------------------------------------------------------------------- /presets/001_v80_v384x512.preset: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Portrait, 80 Views", 3 | "view_width": 384, 4 | "view_height": 512, 5 | "quilt_width": 3840, 6 | "quilt_height": 4096, 7 | "columns": 10, 8 | "rows": 8, 9 | "total_views": 80, 10 | "hidden": false 11 | } -------------------------------------------------------------------------------- /presets/002_v88_v384x512.preset: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Portrait, 88 Views", 3 | "view_width": 384, 4 | "view_height": 512, 5 | "quilt_width": 4224, 6 | "quilt_height": 4096, 7 | "columns": 11, 8 | "rows": 8, 9 | "total_views": 88, 10 | "hidden": false 11 | } -------------------------------------------------------------------------------- /presets/003_v91_v420x560.preset: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Portrait, 91 Views", 3 | "view_width": 420, 4 | "view_height": 520, 5 | "quilt_width": 5460, 6 | "quilt_height": 3640, 7 | "columns": 13, 8 | "rows": 7, 9 | "total_views": 91, 10 | "hidden": false 11 | } -------------------------------------------------------------------------------- /presets/004_v96_v384x512.preset: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Portrait, 96 Views", 3 | "view_width": 384, 4 | "view_height": 512, 5 | "quilt_width": 4608, 6 | "quilt_height": 4096, 7 | "columns": 12, 8 | "rows": 8, 9 | "total_views": 96, 10 | "hidden": false 11 | } -------------------------------------------------------------------------------- /presets/005_v108_v420x560.preset: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Portrait, 108 Views", 3 | "view_width": 420, 4 | "view_height": 560, 5 | "quilt_width": 5040, 6 | "quilt_height": 5040, 7 | "columns": 12, 8 | "rows": 9, 9 | "total_views": 108, 10 | "hidden": false 11 | } -------------------------------------------------------------------------------- /presets/__init__.py: -------------------------------------------------------------------------------- 1 | # ##### BEGIN GPL LICENSE BLOCK ##### 2 | # 3 | # Copyright © 2020 Christian Stolze 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | # ##### END GPL LICENSE BLOCK ##### 19 | 20 | 21 | # THIS FILE IS NEEDED FOR PYTHON: 22 | # https://docs.python.org/3/tutorial/modules.html#packages 23 | --------------------------------------------------------------------------------