Skip to content

Commit

Permalink
Implement support for "Sony" "SPI3D" "LUT" file format.
Browse files Browse the repository at this point in the history
  • Loading branch information
KelSolaar committed May 22, 2018
1 parent 7672712 commit 88d47b5
Show file tree
Hide file tree
Showing 7 changed files with 484 additions and 5 deletions.
39 changes: 34 additions & 5 deletions colour/io/luts/__init__.py
Expand Up @@ -15,14 +15,17 @@
from .lut import LUT1D, LUT2D, LUT3D
from .iridas_cube import read_LUT_IridasCube, write_LUT_IridasCube
from .sony_spi1d import read_LUT_SonySPI1D, write_LUT_SonySPI1D
from .sony_spi3d import read_LUT_SonySPI3D, write_LUT_SonySPI3D

__all__ = ['LUT1D', 'LUT2D', 'LUT3D']
__all__ += ['read_LUT_IridasCube', 'write_LUT_IridasCube']
__all__ += ['read_LUT_SonySPI1D', 'write_LUT_SonySPI1D']
__all__ += ['read_LUT_SonySPI3D', 'write_LUT_SonySPI3D']

EXTENSION_TO_LUT_FORMAT_MAPPING = CaseInsensitiveMapping({
'.cube': 'Iridas Cube',
'.spi1d': 'Sony SPI1D'
'.spi1d': 'Sony SPI1D',
'.spi3d': 'Sony SPI3D'
})
"""
Extension to *LUT* format.
Expand All @@ -34,6 +37,7 @@
LUT_READ_METHODS = CaseInsensitiveMapping({
'Iridas Cube': read_LUT_IridasCube,
'Sony SPI1D': read_LUT_SonySPI1D,
'Sony SPI3D': read_LUT_SonySPI3D,
})
LUT_READ_METHODS.__doc__ = """
Supported *LUT* reading methods.
Expand All @@ -43,7 +47,7 @@
- :cite:`AdobeSystems2013b`
LUT_READ_METHODS : CaseInsensitiveMapping
**{'Iridas Cube', 'Sony SPI1D'}**
**{'Iridas Cube', 'Sony SPI1D', 'Sony SPI3D'}**
"""


Expand All @@ -56,7 +60,7 @@ def read_LUT(path, method=None, **kwargs):
path : unicode
*LUT* path.
method : unicode, optional
**{None, 'Iridas Cube', 'Sony SPI1D'}**,
**{None, 'Iridas Cube', 'Sony SPI1D', 'Sony SPI3D'}**,
Reading method, if *None*, the method will be auto-detected according
to extension.
Expand Down Expand Up @@ -99,6 +103,21 @@ def read_LUT(path, method=None, **kwargs):
Size : (16,)
Comment 01 : Generated by "Colour 0.3.11".
Comment 02 : "colour.models.oetf_reverse_sRGB".
Reading a 3D *Sony* *.spi3d* *LUT*:
>>> path = os.path.join(
... os.path.dirname(__file__), 'tests', 'resources', 'sony_spi3d',
... 'ColourCorrect.spi3d')
>>> print(read_LUT(path))
LUT3D - ColourCorrect
---------------------
<BLANKLINE>
Dimensions : 3
Domain : [[0 0 0]
[1 1 1]]
Size : (4, 4, 4, 3)
Comment 01 : Adapted from a LUT generated by Foundry::LUT.
"""

if method is None:
Expand All @@ -112,6 +131,7 @@ def read_LUT(path, method=None, **kwargs):
LUT_WRITE_METHODS = CaseInsensitiveMapping({
'Iridas Cube': write_LUT_IridasCube,
'Sony SPI1D': write_LUT_SonySPI1D,
'Sony SPI3D': write_LUT_SonySPI3D,
})
LUT_WRITE_METHODS.__doc__ = """
Supported *LUT* reading methods.
Expand All @@ -121,7 +141,7 @@ def read_LUT(path, method=None, **kwargs):
- :cite:`AdobeSystems2013b`
LUT_WRITE_METHODS : CaseInsensitiveMapping
**{'Iridas Cube', 'Sony SPI1D'}**
**{'Iridas Cube', 'Sony SPI1D', 'Sony SPI3D'}**
"""


Expand All @@ -139,7 +159,7 @@ def write_LUT(LUT, path, decimals=7, method=None, **kwargs):
decimals : int, optional
Formatting decimals.
method : unicode, optional
**{None, 'Iridas Cube', 'Sony SPI1D'}**,
**{None, 'Iridas Cube', 'Sony SPI1D', 'Sony SPI3D'}**,
Writing method, if *None*, the method will be auto-detected according
to extension.
Expand Down Expand Up @@ -172,6 +192,15 @@ def write_LUT(LUT, path, decimals=7, method=None, **kwargs):
... np.array([-0.1, 1.5]),
... comments=['A first comment.', 'A second comment.'])
>>> write_LUT(LUT, 'My_LUT.spi1d') # doctest: +SKIP
Writing a 3D *Sony* *.spi3d* *LUT*:
>>> LUT = LUT3D(
... LUT3D.linear_table(16) ** (1 / 2.2),
... 'My LUT',
... np.array([[0, 0, 0], [1, 1, 1]]),
... comments=['A first comment.', 'A second comment.'])
>>> write_LUT(LUT, 'My_LUT.cube') # doctest: +SKIP
"""

if method is None:
Expand Down
170 changes: 170 additions & 0 deletions colour/io/luts/sony_spi3d.py
@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
"""
Sony .spi3d LUT Format Input / Output Utilities
===============================================
Defines *Sony* *.spi3d* *LUT* Format related input / output utilities objects.
- :func:`colour.io.read_LUT_SonySPI3D`
- :func:`colour.io.write_LUT_SonySPI3D`
"""

from __future__ import division, unicode_literals

import numpy as np
import os
import re

from colour.constants import DEFAULT_FLOAT_DTYPE
from colour.io.luts import LUT3D

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2018 - Colour Developers'
__license__ = 'New BSD License - http://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-science@googlegroups.com'
__status__ = 'Production'

__all__ = ['read_LUT_SonySPI3D', 'write_LUT_SonySPI3D']


def read_LUT_SonySPI3D(path):
"""
Reads given *Sony* *.spi3d* *LUT* file.
Parameters
----------
path : unicode
*LUT* path.
Returns
-------
LUT3D or LUT2D
:class:`LUT3D` or :class:`LUT2D` class instance.
Examples
--------
Reading a 3D *Sony* *.spi3d* *LUT*:
>>> path = os.path.join(
... os.path.dirname(__file__), 'tests', 'resources', 'sony_spi3d',
... 'ColourCorrect.spi3d')
>>> print(read_LUT_SonySPI3D(path))
LUT3D - ColourCorrect
---------------------
<BLANKLINE>
Dimensions : 3
Domain : [[0 0 0]
[1 1 1]]
Size : (4, 4, 4, 3)
Comment 01 : Adapted from a LUT generated by Foundry::LUT.
"""

title = re.sub('_|-|\.', ' ', os.path.splitext(os.path.basename(path))[0])
domain_min, domain_max = np.array([0, 0, 0]), np.array([1, 1, 1])
size = 2
indexes = []
table = []
comments = []

def _parse_array(array, dtype=DEFAULT_FLOAT_DTYPE):
"""
Converts given string array to :class:`ndarray` class.
"""

return np.array(list(map(dtype, array)))

with open(path) as spi3d_file:
lines = spi3d_file.readlines()
for line in lines:
line = line.strip()

if len(line) == 0:
continue

if line.startswith('#'):
comments.append(line[1:].strip())
continue

tokens = line.split()
if len(tokens) == 3:
assert len(set(tokens)) == 1, (
'Non-uniform "LUT" shape is unsupported!')

size = np.int_(tokens[0])
if len(tokens) == 6:
indexes.append(_parse_array(tokens[:3]))
table.append(_parse_array(tokens[3:]))

assert np.array_equal(
indexes,
np.int_(LUT3D.linear_table(size) * (size - 1)).reshape(
(-1, 3))), 'Indexes do not match expected "LUT3D" indexes!'

table = np.asarray(table).reshape((size, size, size, 3))

return LUT3D(
table, title, np.vstack([domain_min, domain_max]), comments=comments)


def write_LUT_SonySPI3D(LUT, path, decimals=7):
"""
Writes given *LUT* to given *Sony* *.spi3d* *LUT* file.
Parameters
----------
LUT : LUT3D
:class:`LUT3D` class instance to write at given path.
path : unicode
*LUT* path.
decimals : int, optional
Formatting decimals.
Returns
-------
bool
Definition success.
Examples
--------
Writing a 3D *Sony* *.spi3d* *LUT*:
>>> LUT = LUT3D(
... LUT3D.linear_table(16) ** (1 / 2.2),
... 'My LUT',
... np.array([[0, 0, 0], [1, 1, 1]]),
... comments=['A first comment.', 'A second comment.'])
>>> write_LUT_SonySPI3D(LUT, 'My_LUT.cube') # doctest: +SKIP
"""

assert isinstance(LUT, LUT3D), '"LUT" must be either a 3D "LUT"!'

assert np.array_equal(LUT.domain, np.array([
[0, 0, 0],
[1, 1, 1],
])), '"LUT" domain must be [[0, 0, 0], [1, 1, 1]]!'

def _format_array(array):
return '{1:d} {2:d} {3:d} {4:0.{0}f} {5:0.{0}f} {6:0.{0}f}'.format(
decimals, *array)

with open(path, 'w') as spi3d_file:
spi3d_file.write('SPILUT 1.0\n')

spi3d_file.write('3 3\n')

spi3d_file.write('{0} {0} {0}\n'.format(LUT.size))

indexes = np.int_(LUT.linear_table(LUT.size) * (LUT.size - 1)).reshape(
(-1, 3))
table = LUT.table.reshape((-1, 3))

for i, row in enumerate(indexes):
spi3d_file.write('{0}\n'.format(
_format_array(list(row) + list(table[i]))))

if LUT.comments:
for comment in LUT.comments:
spi3d_file.write('# {0}\n'.format(comment))

return True
68 changes: 68 additions & 0 deletions colour/io/luts/tests/resources/sony_spi3d/ColourCorrect.spi3d
@@ -0,0 +1,68 @@
SPILUT 1.0
3 3
4 4 4
0 0 0 0.000000 0.000000 0.000000
0 0 1 0.000000 0.000000 0.416653
0 0 2 0.000000 0.000000 0.833306
0 0 3 0.000001 0.000001 1.249959
0 1 0 -0.026231 0.377102 -0.026231
0 1 1 0.019686 0.244702 0.244702
0 1 2 0.014327 0.330993 0.647660
0 1 3 0.009022 0.372791 1.100331
0 2 0 -0.052463 0.754204 -0.052463
0 2 1 0.000000 0.616667 0.308333
0 2 2 0.039372 0.489403 0.489403
0 2 3 0.035773 0.578763 0.850258
0 3 0 -0.078694 1.131306 -0.078694
0 3 1 -0.035927 1.021908 0.316685
0 3 2 0.030904 0.831171 0.564415
0 3 3 0.059059 0.734105 0.734105
1 0 0 0.398947 -0.017706 -0.017706
1 0 1 0.333333 0.000000 0.333333
1 0 2 0.390623 0.000000 0.781246
1 0 3 0.404320 0.000000 1.212960
1 1 0 0.294597 0.294597 0.069582
1 1 1 0.416655 0.416655 0.416655
1 1 2 0.416655 0.416655 0.833308
1 1 3 0.416656 0.416656 1.249961
1 2 0 0.349416 0.657749 0.041083
1 2 1 0.340435 0.743769 0.340435
1 2 2 0.269700 0.494715 0.494715
1 2 3 0.347660 0.664327 0.980993
1 3 0 0.344991 1.050213 -0.007621
1 3 1 0.314204 1.120871 0.314204
1 3 2 0.308333 0.925000 0.616667
1 3 3 0.289386 0.739417 0.739417
2 0 0 0.797894 -0.035412 -0.035412
2 0 1 0.752767 -0.028479 0.362144
2 0 2 0.666667 0.000000 0.666667
2 0 3 0.746911 0.000000 1.120366
2 1 0 0.633333 0.316667 0.000000
2 1 1 0.732278 0.315626 0.315626
2 1 2 0.666667 0.333333 0.666667
2 1 3 0.781246 0.390623 1.171869
2 2 0 0.589195 0.589195 0.139164
2 2 1 0.594601 0.594601 0.369586
2 2 2 0.833311 0.833311 0.833311
2 2 3 0.833311 0.833311 1.249963
2 3 0 0.663432 0.930188 0.129920
2 3 1 0.682749 0.991082 0.374416
2 3 2 0.707102 1.110435 0.707102
2 3 3 0.519714 0.744729 0.744729
3 0 0 1.196841 -0.053117 -0.053117
3 0 1 1.162588 -0.050372 0.353948
3 0 2 1.089003 -0.031363 0.715547
3 0 3 1.000000 0.000000 1.000000
3 1 0 1.038439 0.310899 -0.052870
3 1 1 1.131225 0.297920 0.297920
3 1 2 1.086101 0.304855 0.695478
3 1 3 1.000000 0.333333 1.000000
3 2 0 0.891318 0.619823 0.076833
3 2 1 0.950000 0.633333 0.316667
3 2 2 1.065610 0.648957 0.648957
3 2 3 1.000000 0.666667 1.000000
3 3 0 0.883792 0.883792 0.208746
3 3 1 0.889199 0.889199 0.439168
3 3 2 0.894606 0.894606 0.669590
3 3 3 1.249966 1.249966 1.249966
# Adapted from a LUT generated by Foundry::LUT.

0 comments on commit 88d47b5

Please sign in to comment.