├── lib
├── pylightio.egg-info
│ ├── dependency_links.txt
│ ├── top_level.txt
│ ├── SOURCES.txt
│ └── PKG-INFO
├── pylightio
│ ├── external
│ │ ├── cbor
│ │ │ ├── VERSION.py
│ │ │ ├── __init__.py
│ │ │ ├── tagmap.py
│ │ │ ├── cbor_rpc_client.py
│ │ │ └── cbor.py
│ │ ├── cbor-1.0.0-py3.9.egg-info
│ │ │ ├── top_level.txt
│ │ │ ├── dependency_links.txt
│ │ │ ├── SOURCES.txt
│ │ │ ├── installed-files.txt
│ │ │ └── PKG-INFO
│ │ └── __init__.py
│ ├── formats
│ │ ├── __init__.py
│ │ └── lightfields.py
│ ├── managers
│ │ ├── __init__.py
│ │ ├── services.py
│ │ └── devices.py
│ ├── lookingglass
│ │ ├── __init__.py
│ │ ├── services.py
│ │ └── lightfields.py
│ ├── __about__.py
│ ├── __init__.py
│ └── LICENSE
├── wheels
│ ├── pynng-0.7.4+dev-cp38-cp38-macosx_10_9_universal2.whl
│ ├── pynng-0.7.4+dev-cp39-cp39-macosx_10_9_universal2.whl
│ ├── 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
└── __init__.py
├── .gitignore
├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── presets
├── 001_v80_v384x512.preset
├── 002_v88_v384x512.preset
├── 003_v91_v420x560.preset
├── 004_v96_v384x512.preset
├── 005_v108_v420x560.preset
└── __init__.py
├── logs
└── __init__.py
├── preferences.py
├── README.md
├── globals.py
└── __init__.py
/lib/pylightio.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/pylightio.egg-info/top_level.txt:
--------------------------------------------------------------------------------
1 | pylightio
2 |
--------------------------------------------------------------------------------
/lib/pylightio/external/cbor/VERSION.py:
--------------------------------------------------------------------------------
1 | '1.0.1'
2 |
--------------------------------------------------------------------------------
/lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/top_level.txt:
--------------------------------------------------------------------------------
1 | cbor
2 |
--------------------------------------------------------------------------------
/lib/pylightio/external/cbor-1.0.0-py3.9.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/lib/wheels/pynng-0.7.4+dev-cp38-cp38-macosx_10_9_universal2.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/regcs/AliceLG/HEAD/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/HEAD/lib/wheels/pynng-0.7.4+dev-cp39-cp39-macosx_10_9_universal2.whl
--------------------------------------------------------------------------------
/lib/wheels/pynng-0.7.4+dev-cp310-cp310-macosx_10_9_universal2.whl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/regcs/AliceLG/HEAD/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/HEAD/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/HEAD/lib/wheels/pynng-0.7.4+dev-cp312-cp312-macosx_10_9_universal2.whl
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/__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.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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/__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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/__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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/__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, 1),
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 |
--------------------------------------------------------------------------------