Added my OpenGL viewer, olOpt for cool fast C++, and more!

master
Sofus Albert Høgsbro Rose 2017-01-19 17:55:14 -05:00
parent c1be1041b2
commit f8cd85bbc2
Signed by: so-rose
GPG Key ID: 3D01BE95F3EFFEB9
37 changed files with 2852 additions and 860 deletions

3
.gitattributes vendored 100644
View File

@ -0,0 +1,3 @@
*.exr filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
*.pyc

6
.pypirc 100644
View File

@ -0,0 +1,6 @@
[distutils]
index-servers=pypi
[pypi]
repository = https://upload.pypi.org/legacy/
username = so-rose

1
MANIFEST.in 100644
View File

@ -0,0 +1 @@
recursive-include openlut *.py

1
README 120000
View File

@ -0,0 +1 @@
README.md

View File

@ -16,10 +16,11 @@ I wanted it to cover this niche simply and consistently, something color managem
What About OpenColorIO? Why does this exist?
------
OpenColorIO is a wonderful library, but seems geared towards managing the complexity of many larger applications in a greater pipeline.
openlut is more simple; it doesn't care about the big picture - you simply read in images, transform them, then output them. openlut
also focuses greatly on the "how" of these transformations with tools that eg. create or resize LUTs, things missing in OCIO.
openlut is more simple; it doesn't care about the big picture - you just do consistent operations on images. openlut also has tools to deal
with these building blocks, unlike OCIO - resizing LUTs, etc. .
Since it's a library, though, it's perfectly feasable (if not easy) to build such a greater pipeline based on openlut's simple color transformations.
Indeed, OCIO is just a system these basic operations using LUTs - in somewhat unintuitive ways, in my opinion. You could setup a similar system
using openlut's toolkit.
Installation
@ -29,7 +30,7 @@ I'll put it on pip eventually (when I figure out how!). For now, just download t
To run openlut.py, first make sure you have the *Dependencies*. To run the test code at the bottom (make sure openlut is in the same
directory as testpath; it needs to load test.exr), you can then run:
`python3 openlut.py -t`
`python3 main.py -t`
To use in your code, simply `import` the module at the top of your file.
@ -56,7 +57,7 @@ The **Transform** objects themselves have plenty of features - like LUT, with `o
input matrices, or automatic spline-based interpolation of very small 1D LUTs - to make them helpful in and of themselves!
The best way to demonstrate from here, I think, is to show some test code: (run python3 openlut.py -t to see it work)
The best way to demonstrate this, I think, is to show some test code: (run python3 openlut.py -t to see it work)
```python
#Open any format image. Try it with exr/dpx/anything!

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python2
from __future__ import print_function
'''
The MIT License (MIT)
Copyright (c) 2016 Sofus Rose
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
#Image Dims: X (0-1919), Y (0-1079), C (0-2; RGB)
import sys, os
#~ import numpy as np
#~ from PIL import Image
#~ import tifffile as tff
#~ import pylut as pl
SEP=' '
SIZE=8 #The .cube resolution is this, squared.
BIT_DEPTH=16
def imgOpen(path) :
if path[-4:] == '.tif' or path[-4:] == 'tiff' :
return tff.TiffFile(path).asarray()
print("Not Supported!")
else :
return np.asarray(Image.open(path).convert('RGB'))
def rgbImg(img): return img.transpose(2, 0, 1) #X, Y, C --> C, X, Y
def xyImg(rgbImg): return rgbImg.transpose(1, 2, 0) #C, X, Y --> X, Y, C
def prHelp() :
print("ml_lhald.py: Generates modified (R/B swapped) HALD files for arbitrary grading, then converts them to .cube using pylut.")
print("\tGenerate modified HALD: ./mk_lhald.py gen <OPTIONAL: name, without extension, of png output>")
print("\tHALD --> CUBE: ./mk_lhald.py mk <path to HALD> <OPTIONAL: name, without extension, of cube output>\n\n")
print("Requires pylut. Install with 'pip2 install pylut'.")
sys.exit(1)
if __name__ == "__main__" :
if not sys.argv[1:]: prHelp()
if sys.argv[1] == "gen" :
iPath = "identity" if not sys.argv[2:] else sys.argv[2]
os.system('convert hald:{0} {1}.png'.format(SIZE, iPath)) # -separate -swap 0,2 -combine
print('Go ahead and grade the file "{}.png".'.format(iPath))
elif sys.argv[1] == "mk" and sys.argv[1:] :
fPath = ".converted_hald.ppm"
lPath = "grade" if not sys.argv[3:] else sys.argv[3]
os.system('convert -depth {2} {0} -compress none {1}'.format(sys.argv[2], fPath, BIT_DEPTH))
lines = ' '.join(line.strip() for line in open(fPath, 'r').readlines()[3:]).split(' ')
print(lines[:64*3])
lines = ['%.6f' % (float(line)/float(2 ** BIT_DEPTH)) for line in lines] #Direct .cube output only.
coords = [SEP.join(lines[i:i+3]) for i in range(0, len(lines)-2, 3)]
identity = ' '.join([line.split(SEP)[0] for line in coords[:SIZE**2]])
print(lines[:65], '\n\n', coords[:65], len(lines), len(coords))
#~ with open(lPath + '.3dl', 'w') as f :
#~ print(identity, end='\n', file=f)
#~ print(*coords, sep='\n', file=f)
print("Creating", lPath + '.cube')
#~ lut = pl.LUT.FromNuke3DLFile(lPath + '.3dl')
#~ #print(lut.ColorAtInterpolatedLatticePoint(0.00206,0.00227,0.00307))
#~ lut.ToCubeFile(lPath + '.cube')
with open(lPath + '.cube', 'w') as f :
print("LUT_3D_SIZE", SIZE ** 2, file=f)
print(*coords, sep='\n', file=f)
os.remove(fPath)
#~ os.remove(lPath + '.3dl')
else :
prHelp()

225
doc/Makefile 100644
View File

@ -0,0 +1,225 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/openlut.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/openlut.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/openlut"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/openlut"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."

281
doc/make.bat 100644
View File

@ -0,0 +1,281 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
set I18NSPHINXOPTS=%SPHINXOPTS% source
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. epub3 to make an epub3
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. xml to make Docutils-native XML files
echo. pseudoxml to make pseudoxml-XML files for display purposes
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
echo. coverage to run coverage check of the documentation if enabled
echo. dummy to check syntax errors of document sources
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
REM Check if sphinx-build is available and fallback to Python version if any
%SPHINXBUILD% 1>NUL 2>NUL
if errorlevel 9009 goto sphinx_python
goto sphinx_ok
:sphinx_python
set SPHINXBUILD=python -m sphinx.__init__
%SPHINXBUILD% 2> nul
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
:sphinx_ok
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\openlut.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\openlut.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "epub3" (
%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdf" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "latexpdfja" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
cd %BUILDDIR%/latex
make all-pdf-ja
cd %~dp0
echo.
echo.Build finished; the PDF files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
if "%1" == "coverage" (
%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
if errorlevel 1 exit /b 1
echo.
echo.Testing of coverage in the sources finished, look at the ^
results in %BUILDDIR%/coverage/python.txt.
goto end
)
if "%1" == "xml" (
%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The XML files are in %BUILDDIR%/xml.
goto end
)
if "%1" == "pseudoxml" (
%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
goto end
)
if "%1" == "dummy" (
%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
if errorlevel 1 exit /b 1
echo.
echo.Build finished. Dummy builder generates no files.
goto end
)
:end

344
doc/source/conf.py 100644
View File

@ -0,0 +1,344 @@
# -*- coding: utf-8 -*-
#
# openlut documentation build configuration file, created by
# sphinx-quickstart on Wed Jan 18 22:00:18 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'openlut'
copyright = u'2017, Sofus Rose'
author = u'Sofus Rose'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'0.0.1'
# The full version, including alpha/beta/rc tags.
release = u'0.0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = u'openlut v0.0.1'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
# html_domain_indices = True
# If false, no index is generated.
#
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'openlutdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'openlut.tex', u'openlut Documentation',
u'Sofus Rose', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'openlut', u'openlut Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'openlut', u'openlut Documentation',
author, 'openlut', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False

View File

@ -0,0 +1,22 @@
.. openlut documentation master file, created by
sphinx-quickstart on Wed Jan 18 22:00:18 2017.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to openlut's documentation!
===================================
Contents:
.. toctree::
:maxdepth: 2
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,7 @@
openlut
=======
.. toctree::
:maxdepth: 4
openlut

View File

@ -0,0 +1,22 @@
openlut.lib package
===================
Submodules
----------
openlut.lib.files module
------------------------
.. automodule:: openlut.lib.files
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: openlut.lib
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,77 @@
openlut package
===============
Subpackages
-----------
.. toctree::
openlut.lib
Submodules
----------
openlut.ColMap module
---------------------
.. automodule:: openlut.ColMap
:members:
:undoc-members:
:show-inheritance:
openlut.ColMat module
---------------------
.. automodule:: openlut.ColMat
:members:
:undoc-members:
:show-inheritance:
openlut.Func module
-------------------
.. automodule:: openlut.Func
:members:
:undoc-members:
:show-inheritance:
openlut.LUT module
------------------
.. automodule:: openlut.LUT
:members:
:undoc-members:
:show-inheritance:
openlut.Transform module
------------------------
.. automodule:: openlut.Transform
:members:
:undoc-members:
:show-inheritance:
openlut.gamma module
--------------------
.. automodule:: openlut.gamma
:members:
:undoc-members:
:show-inheritance:
openlut.gamut module
--------------------
.. automodule:: openlut.gamut
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: openlut
:members:
:undoc-members:
:show-inheritance:

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2ad056a1887cd7be2cb2eeddfa2194d092be4fbc3a62e358ae5a15ba0562ba3c
size 10596905

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39123fa7f59cb30f65aabf6801d1ac4642cf829c912de14dcc4a04005bf02cbe
size 7789396

53
main.py 100755
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3.5
'''
openlut: A package for managing and applying 1D and 3D LUTs.
Color Management: openlut deals with the raw RGB values, does its work, then puts out images with correct raw RGB values - a no-op.
Dependencies:
-numpy: Like, everything.
-wand: Saving/loading images.
-scipy - OPTIONAL: For spline interpolation.
Easily get all deps: sudo pip3 install numpy wand numba scipy
*Make sure you get the Python 3.X version of these packages!!!
LICENCE:
The MIT License (MIT)
Copyright (c) 2016 Sofus Rose
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
import sys
#~ from lib.files import Log #For Development
if __name__ == "__main__" :
if not sys.argv[1:]: print('Use -t to test!'); exit()
if sys.argv[1] == '-t' :
import tests.suite
tests.suite.runTest('img_test', 'testpath')

42
openlut-project 100644
View File

@ -0,0 +1,42 @@
[editor]
line_wrapping=false
line_break_column=72
auto_continue_multiline=true
[file_prefs]
final_new_line=true
ensure_convert_new_lines=false
strip_trailing_spaces=false
replace_tabs=false
[indentation]
indent_width=4
indent_type=1
indent_hard_tab_width=8
detect_indent=false
detect_indent_width=false
indent_mode=2
[project]
name=openlut
base_path=/home/sofus/subhome/src/openlut
description=
[long line marker]
long_line_behaviour=1
long_line_column=72
[files]
current_page=0
FILE_NAME_0=1995;Python;0;EUTF-8;1;1;0;%2Fhome%2Fsofus%2Fsubhome%2Fsrc%2Fopenlut%2Fopenlut.py;0;4
[VTE]
last_dir=/home/sofus/subhome/src/linemarch
[prjorg]
source_patterns=*.c;*.C;*.cpp;*.cxx;*.c++;*.cc;*.m;
header_patterns=*.h;*.H;*.hpp;*.hxx;*.h++;*.hh;
ignored_dirs_patterns=.*;CVS;
ignored_file_patterns=*.o;*.obj;*.a;*.lib;*.so;*.dll;*.lo;*.la;*.class;*.jar;*.pyc;*.mo;*.gmo;
generate_tag_prefs=0
external_dirs=

View File

@ -1,755 +0,0 @@
#!/usr/bin/env python3.5
'''
openlut: A package for managing and applying 1D and 3D LUTs.
Color Management: openlut deals with the raw RGB values, does its work, then puts out images with correct raw RGB values - a no-op.
Dependencies:
-numpy: Like, everything.
-wand: Saving/loading all images.
-numba: 38% speedup for matrix math.
-scipy - OPTIONAL: For spline interpolation.
Easily get all deps: sudo pip3 install numpy wand numba scipy
*Make sure you get the Python 3.X version of these packages!!!
LICENCE:
The MIT License (MIT)
Copyright (c) 2016 Sofus Rose
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
'''
import sys, os, math, abc, ctypes
import multiprocessing as mp
from functools import reduce
import operator as oper
MOD_SCIPY = False
try :
from scipy.interpolate import splrep, splev
MOD_SCIPY = True
except :
pass
import numpy as np
import numba
#~ import skimage as si
#~ import skimage.io
#~ si.io.use_plugin('freeimage')
#~ from PIL import Image
#~ import tifffile as tff
import wand
import wand.image
import wand.display
from wand.api import library
#~ library.MagickSetCompressionQuality.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
#~ library.MagickSetCompression.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
#~ COMPRESS_TYPES = dict(zip(wand.image.COMPRESSION_TYPES, tuple(map(ctypes.c_int, range(len(wand.image.COMPRESSION_TYPES))))))
#~ from lib.files import Log #For Development
class Transform :
def apply(self, cMap) :
"""
Applies this transformation to a ColMap.
"""
return ColMap(self.sample(cMap.asarray()))
@abc.abstractmethod
def sample(self, fSeq) :
"""
Samples the Transformation.
"""
def spSeq(seq, outLen) :
"""
Utility function for splitting a sequence into equal parts, for multithreading.
"""
perfSep = (1/outLen) * len(seq)
return list(filter(len, [seq[round(perfSep * i):round(perfSep * (i + 1))] for i in range(len(seq))])) if len(seq) > 1 else seq
class ColMap :
def __init__(self, rgbArr) :
self.rgbArr = rgbArr
def fromIntArray(imgArr) :
bitDepth = int(''.join([i for i in str(imgArr.dtype) if i.isdigit()]))
return ColMap(np.divide(imgArr.astype(np.float64), 2 ** bitDepth - 1))
#Operations - returns new ColMaps.
def apply(self, transform) :
'''
Applies a Transform object by running its apply method.
'''
return transform.apply(self)
#IO Functions
def open(path) :
'''
Opens 8 and 16 bit images of many formats.
'''
try :
openFunction = {
"exr" : ColMap.openWand,
"dpx" : ColMap.openWand,
}[path[path.rfind('.') + 1:]]
return openFunction(path) #Any fancy formats will go here.
except :
#Fallback to opening using Wand.
return ColMap.openWand(path)
#Vendor-specific open methods.
#~ def openSci(path) :
#~ return ColMap.fromIntArray(si.io.imread(path)[:,:,:3])
def openWand(path) :
'''
Open a file using the Wand ImageMagick binding.
'''
with wand.image.Image(filename=path) as img:
#Quick inverse sRGB transform, to undo what Wand did.
img.colorspace = 'srgb'
img.transform_colorspace('rgb')
img.colorspace = 'srgb' if img.format == 'DPX' else 'rgb' #Fix for IM's dpx bug.
return ColMap.fromIntArray(np.fromstring(img.make_blob("RGB"), dtype='uint{}'.format(img.depth)).reshape(img.height, img.width, 3))
def save(self, path, compress = None, depth = None) :
'''
Save the image. The filetype will be inferred from the path, and the appropriate backend will be used.
Compression scheme will be applied based on the backend compatiblity. Wand compression types can be used: Browse then
at http://docs.wand-py.org/en/0.4.3/wand/image.html#wand.image.COMPRESSION_TYPES .
'''
if depth is None: depth = 16
try :
saveFunction = {
"exr" : self.saveWand,
"dpx" : self.saveWand,
"tif" : self.saveWand,
"tiff": self.saveWand
}[path[path.rfind('.') + 1:]]
return saveFunction(path, compress, depth)
except :
#Fallback to saving using Wand.
self.saveWand(path, compress, depth)
#Vendor-specific save methods
def saveWand(self, path, compress = None, depth = 16) :
data = self.apply(LUT.lutFunc(Gamma.sRGB)) if path[path.rfind('.')+1:] == 'dpx' else self
i = data.asWandImg(depth)
i.colorspace = 'srgb' #Make sure it saves without a colorspace transformation.
#~ if compress :
#~ library.MagickSetCompression(i.wand, 'rle')
#~ i.compression = 'lzma'
#~ i.compression_quality = 80
i.save(filename=path)
#~ def saveSci(self, path, compress = None, depth = 16) :
#~ if compress is not None: raise ValueError('Scipy Backend cannot compress the output image!')
#~ si.io.imsave(path, self.asIntArray())
#~ def savePil(self, path, compress = None, depth = 8) :
#~ if compress is not None: raise ValueError('Scipy Backend cannot compress the output image!')
#~ if depth != 8: raise ValueError('Cannot save non-8 bit image using PIL.')
#~ self.asPilImg().save(path)
def show(self) :
#~ ColMap.pilShow(self.apply(LUT.lutFunc(Gamma.sRGB)).asPilImg())
ColMap.wandShow(self.asWandImg())
#~ def pilShow(pilImg) :
#~ pilImg.show()
def wandShow(wandImg) :
#Do a quick sRGB transform for viewing. Must be in 'rgb' colorspace for this to take effect.
wandImg.transform_colorspace('srgb')
wand.display.display(wandImg)
wandImg.transform_colorspace('rgb') #This transforms it back to linearity.
#Data Form Functions
#~ def asPilImg(self) :
#~ return Image.fromarray(self.asIntArray(8), mode='RGB')
def asWandImg(self, depth = 16) :
i = wand.image.Image(blob=self.asIntArray(depth).tostring(), width=np.shape(self.rgbArr)[1], height=np.shape(self.rgbArr)[0], format='RGB')
i.colorspace = 'rgb' #Specify, to Wand, that this image is to be treated as raw, linear, data.
return i
def asarray(self) :
"""
Returns the base float array.
"""
return self.rgbArr
def asIntArray(self, depth = 16, us = True) :
u = 'u' if us else ''
return np.multiply(self.rgbArr.clip(0, 1), 2.0 ** depth - 1).astype("{0}int{1}".format(u, depth))
#Overloads
def __repr__(self) :
return 'ColMap( \n\trgbArr = {0}\n)'.format('\n\t\t'.join([line.strip() for line in repr(self.rgbArr).split('\n')]))
class LUT(Transform) :
def __init__(self, dims = 1, size = 16384, title = "openlut_LUT", array = None, iRange = (0.0, 1.0)) :
'''
Create an identity LUT with given dimensions (1 or 3), size, and title.
'''
if array is not None :
LUT.lutArray(array, size, dims, title)
else :
if dims != 1 and dims != 3: raise ValueError("Dimensions must be 1 or 3!")
self.title = title #The title.
self.size = size #The size. 1D LUTs: size numbers. 3D LUTs: size x size x size numbers.
self.range = iRange #The input range - creates data or legal LUTs. Should work fine, but untested.
self.dims = dims #The dimensions. 1 or 3; others aren't accepted.
self.ID = np.linspace(self.range[0], self.range[1], self.size) #Read Only.
if dims == 1 :
self.array = np.linspace(self.range[0], self.range[1], self.size) #Size number of floats.
elif dims == 3 :
print("3D LUT Not Implemented!")
#~ self.array = np.linspace(self.range[0], self.range[1], self.size**3).reshape(self.size, self.size, self.size) #Should make an identity size x size x size array.
def lutFunc(func, size = 16384, dims = 1, title="openlut_FuncGen") :
'''
Creates a LUT from a simple function.
'''
if dims == 1 :
lut = LUT(dims=dims, size=size, title=title)
vFunc = np.vectorize(func, otypes=[np.float])
lut.array = vFunc(lut.array)
return lut
elif dims == 3 :
print("3D LUT Not Implemented!")
def lutArray(array, title="Array_Generated") :
'''
Creates a LUT from a float array. Elements must be in range [0, 1].
'''
if len(np.shape(array)) == 1 :
lut = LUT(dims=1, size=len(array), title=title)
lut.array = array
return lut
elif len(np.shape(array)) == 3 :
print("3D LUT Not Implemented!")
else :
raise ValueError("lutArray input must be 1D or 3D!")
#LUT Functions.
def __interp(q, cpu, spSeq, ID, array, spl) :
if spl :
q.put( (cpu, splev(spSeq, splrep(ID, array))) ) #Spline Interpolation. Pretty quick, considering.
else :
q.put( (cpu, np.interp(spSeq, ID, array)) )
def sample(self, fSeq, spl=True) :
'''
Sample the LUT using a flat float sequence (ideally a numpy array; (0..1) ).
Each n (dimensions) clump of arguments will be used to sample the LUT. So:
1D LUT: in1, in2, in3 --> out1, out2, out3
*Min 1 argument.
3D LUT: inR, inG, inB --> outR, outG, outB
*Min 3 arguments, len(arguments) % 3 must equal 0.
Returns a numpy array with identical shape to the input array.
'''
fSeq = np.array(fSeq)
if self.dims == 1 :
#~ return np.interp(spSeq, self.ID, self.array)
#If scipy isn't loaded, we can't use spline interpolation!
if (not MOD_SCIPY) or self.size > 1023: spl = False # Auto-adapts big LUTs to use the faster, more brute-forceish, linear interpolation.
#~ spl = True
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, mp.cpu_count())
for cpu in range(mp.cpu_count()) :
p = mp.Process(target=LUT.__interp, args=(q, cpu, splt[cpu], self.ID, self.array, spl))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0)
elif self.dims == 3 :
print("3D LUT Not Implemented!")
def resized(self, newSize) :
if newSize == self.size: return self
fac = newSize / self.size
useSpl = self.size < newSize #If the new size is lower, we use Linear interpolation. If the new size is higher, we use Spline interpolation.
if self.size < 128: useSpl = True #If the current size is too low, use spline regardless.
if self.dims == 1 :
newID = np.linspace(self.range[0], self.range[1], newSize)
return LUT.lutArray(self.sample(newID, spl=useSpl), title="Resized to {0}".format(newSize))
if self.dims == 3 :
print("3D LUT Not Implemented")
#IO Functions.
def open(path) :
'''
Opens any supported file format, located at path.
'''
openFunction = {
"cube" : LUT.openCube,
}[path[path.rfind('.') + 1:]]
return openFunction(path)
def openCube(path) :
'''
Opens .cube files. They must be saved with whitespaces. Referenced by open().
'''
lut = LUT() #Mutable luts are not reccommended for users.
with open(path, 'r') as f :
i = 0
for line in f :
#~ if not line.strip(): continue
sLine = line.strip()
if not sLine: continue
if sLine[0] == '#': continue
index = sLine[:sLine.find(' ')]
data = sLine[sLine.find(' ') + 1:]
if index == "TITLE": lut.title = data.strip('"'); continue
if index == "LUT_1D_SIZE": lut.dims = 1; lut.size = int(data); continue
if index == "LUT_3D_SIZE": lut.dims = 3; lut.size = int(data); continue
if index == "LUT_1D_INPUT_RANGE": lut.range = (float(data[:data.find(' ')]), float(data[data.find(' ') + 1:])); continue
if lut.dims == 1 and sLine[:sLine.find(' ')] :
lut.array[i] = float(sLine[:sLine.find(' ')])
i += 1
elif lut.dims == 3 :
print("3D LUT Not Implemened!")
return lut
def save(self, path) :
'''
Method that saves the LUT in a supported format, based on the path.
'''
saveFunction = {
"cube" : self.saveCube,
}[path[path.rfind('.') + 1:]]
saveFunction(path)
def saveCube(self, path) :
with open(path, 'w') as f :
print('TITLE', '"{}"'.format(self.title), file=f)
if self.dims == 1 :
print('LUT_1D_SIZE', '{}'.format(self.size), file=f)
print('LUT_1D_INPUT_RANGE', '{0:.6f} {1:.6f}'.format(*self.range), file=f)
print('# Created by openlut.\n', file=f)
for itm in self.array :
entry = '{0:.6f}'.format(itm)
print(entry, entry, entry, file=f)
elif self.dims == 3 :
print("3D LUT Not Implemented!")
#Overloaded functions
def __iter__(self) :
if dims == 1 :
return iter(self.array)
elif dims == 3 :
iArr = self.array.reshape(self.dims, self.size / self.dims) #Group into triplets.
return iter(iArr)
def __getitem__(self, key) :
return self.sample(key)
def __repr__(self) :
return 'LUT(\tdims = {0},\n\tsize = {1},\n\ttitle = "{2}"\n\tarray = {3}\n)'.format(self.dims, self.size, self.title, '\n\t\t'.join([line.strip() for line in repr(self.array).split('\n')]))
class Gamma(Transform) :
def __init__(self, func) :
self.func = func
#Gamma Methods
def __gamma(q, cpu, f, spSeq) :
q.put( (cpu, f(spSeq)) )
def sample(self, fSeq) :
fSeq = np.array(fSeq)
fVec = np.vectorize(self.func)
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, mp.cpu_count())
for cpu in range(mp.cpu_count()) :
p = mp.Process(target=Gamma.__gamma, args=(q, cpu, fVec, splt[cpu]))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0) if len(fSeq) > 1 else self.func(fSeq[0])
return fVec(fSeq) if len(fSeq) > 1 else self.func(fSeq[0])
#Static Gamma Functions (partly adapted from MLRawViewer)
def lin(x): return x
def sRGB(x) :
'''
sRGB formula. Domain must be within [0, 1].
'''
return ( (1.055) * (x ** (1.0 / 2.4)) ) - 0.055 if x > 0.0031308 else x * 12.92
def sRGBinv(x) :
'''
Inverse sRGB formula. Domain must be within [0, 1].
'''
return ((x + 0.055) / 1.055) ** 2.4 if x > 0.04045 else x / 12.92
def Rec709(x) :
'''
Rec709 formula. Domain must be within [0, 1].
'''
return 1.099 * (x ** 0.45) - 0.099 if x >= 0.018 else 4.5 * x
def ReinhardHDR(x) :
'''
Reinhard Tonemapping formula. Domain must be within [0, 1].
'''
return x / (1.0 + x)
def sLog(x) :
'''
sLog 1 formula. Domain must be within [0, 1]. See https://pro.sony.com/bbsccms/assets/
files/mkt/cinema/solutions/slog_manual.pdf .
'''
return ( 0.432699 * math.log(x + 0.037584, 10.0) + 0.616596) + 0.03
def sLog2(x) :
'''
sLog2 formula. Domain must be within [0, 1]. See https://pro.sony.com/bbsccms/assets/files/micro/dmpc/training/S-Log2_Technical_PaperV1_0.pdf .
'''
return ( 0.432699 * math.log( (155.0 * x) / 219.0 + 0.037584, 10.0) + 0.616596 ) + 0.03
def sLog3(x) :
'''
Not yet implemented. See http://community.sony.com/sony/attachments/sony/large-sensor-camera-F5-F55/12359/2/TechnicalSummary_for_S-Gamut3Cine_S-Gamut3_S-Log3_V1_00.pdf .
'''
return x
def DanLog(x) :
return (10.0 ** ((x - 0.385537) / 0.2471896) - 0.071272) / 3.555556 if x > 0.1496582 else (x - 0.092809) / 5.367655
def DanLoginv(x) :
pass
class TransMat(Transform) :
def __init__(self, *mats) :
'''
Initializes a combined 3x3 Transformation Matrix from any number of input matrices. These may be numpy arrays, matrices,
other TransMats, or any combination thereof.
'''
if len(mats) == 1 :
mat = mats[0]
if isinstance(mat, TransMat) :
self.mat = mat.mat #Support a copy constructor.
else :
self.mat = np.array(mat) #Simply set self.mat with the numpy array version of the mat.
elif len(mats) > 1 :
self.mat = TransMat.__mats(*[TransMat(mat) for mat in mats]).mat
elif not mats :
self.mat = np.identity(3)
def __mats(*inMats) :
'''
Initialize a combined Transform matrix from several input TransMats.
'''
return TransMat(reduce(TransMat.__mul__, reversed(inMats))) #Works because multiply is actually non-commutative dot.
#This is why we reverse inMats.
@numba.jit(nopython=True)
def __optDot(img, mat, shp, out) :
shaped = img.reshape((shp[0] * shp[1], shp[2])) #Flatten to 2D array for iteration over colors.
i = 0
while i < shp[0] * shp[1] :
res = np.dot(mat, shaped[i])
out[i] = res
i += 1
def __applMat(q, cpu, shp, mat, img3D) :
out = np.zeros((shp[0] * shp[1], shp[2]))
TransMat.__optDot(img3D, mat, shp, out)
q.put( (cpu, out.reshape(shp)) )
def sample(self, fSeq) :
shp = np.shape(fSeq)
if len(shp) == 1 :
return self.mat.dot(fSeq)
if len(shp) == 3 :
cpus = mp.cpu_count()
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, cpus)
for cpu in range(cpus) :
p = mp.Process(target=TransMat.__applMat, args=(q, cpu, np.shape(splt[cpu]), self.mat, splt[cpu]))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0)
#~ out = np.zeros((shp[0] * shp[1], shp[2]))
#~ TransMat.__optDot(fSeq, self.mat, shp, out)
#~ return out.reshape(shp)
#~ return np.array([self.mat.dot(col) for col in fSeq.reshape(shp[0] * shp[1], shp[2])]).reshape(shp)
#~ p = mp.Pool(mp.cpu_count())
#~ return np.array(list(map(self.mat.dot, fSeq.reshape(shp[0] * shp[1], shp[2])))).reshape(shp)
#~ return fSeq.dot(self.mat)
def inv(obj) :
if isinstance(obj, TransMat) : #Works on any TransMat object - including self.
return TransMat(np.linalg.inv(obj.mat))
else : #Works on raw numpy arrays as well.
return np.linalg.inv(obj)
def transpose(self) :
return TransMat(np.transpose(self.mat))
#Overloading
def __mul__(self, other) :
'''
* implements matrix multiplication.
'''
if isinstance(other, TransMat) :
return TransMat(self.mat.dot(other.mat))
elif isinstance(other, float) or isinstance(other, int) :
return TransMat(np.multiply(self.mat, other))
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return TransMat(self.mat.dot(np.array(other)))
else :
raise ValueError('Invalid multiplication arguments!')
__rmul__ = __mul__
def __add__(self, other) :
if isinstance(other, TransMat) :
return TransMat(self.mat + other.mat)
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return TransMat(self.mat + np.array(other))
else :
raise ValueError('Invalid addition arguments!')
__radd__ = __add__
def __pow__(self, other) :
'''
** implements direct multiplication. You usually don't want this.
'''
if isinstance(other, TransMat) :
return TransMat(self.mat * other.mat)
elif isinstance(other, float) or isinstance(other, int) :
return TransMat(np.multiply(self.mat, other))
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return TransMat(self.mat * np.array(other))
else :
raise ValueError('Invalid multiplication arguments!')
def __invert__(self) :
return self.inv()
def __len__(self) :
return len(self.mat)
def __getitem__(self, key) :
return self.mat[key]
def __iter__(self) :
return iter(self.mat)
def __repr__(self) :
return "\nTransMat (\n{0} )\n".format(str(self.mat))
#Static Transmat Matrices -- all go from ACES to <gamut name>. <gamut name>inv functions go from the gamut to ACES.
#Converted (CIECAT02) D65 Illuminant for all.
XYZ = np.array(
[
0.93863095, -0.00574192, 0.0175669,
0.33809359, 0.7272139, -0.0653075,
0.00072312, 0.00081844, 1.08751619
]
).reshape(3, 3)
XYZinv = np.array(
[
1.06236611, 0.00840695, -0.01665579,
-0.49394137, 1.37110953, 0.09031659,
-0.00033467, -0.00103746, 0.91946965
]
).reshape(3, 3)
sRGB = np.array(
[
2.52193473, -1.1370239, -0.38491083,
-0.27547943, 1.36982898, -0.09434955,
-0.01598287, -0.14778923, 1.1637721
]
).reshape(3, 3)
sRGBinv = np.array(
[
0.43957568, 0.38391259, 0.17651173,
0.08960038, 0.81471415, 0.09568546,
0.01741548, 0.10873435, 0.87385017
]
).reshape(3, 3)
aRGB = np.array(
[
1.72502307, -0.4228857, -0.30213736,
-0.27547943, 1.36982898, -0.09434955,
-0.02666425, -0.08532111, 1.11198537
]
).reshape(3, 3)
aRGBinv = np.array(
[
0.61468318, 0.20122762, 0.1840892,
0.12529321, 0.77491365, 0.09979314,
0.02435304, 0.06428329, 0.91136367
]
).reshape(3, 3)
if __name__ == "__main__" :
if not sys.argv: print('Use -t to test!')
if sys.argv[1] == '-t' :
print('Open openlut.py and scroll down to the end to see the code that\'s working!')
#Open any format image. Try it with exr/dpx/anything!
img = ColMap.open('testpath/test.exr') #Opens a test image 'test.exr', creating a ColMap object, automatically using the best image backend available to load the image at the correct bit depth.
'''
Gamma has gamma functions like Gamma.sRGB, called by value like Gamma.sRGB(val). All take one argument, the value (x), and returns the transformed value. Color doesn't matter for gamma.
TransMat has matrices, in 3x3 numpy array form. All are relative to ACES, with direction aptly named. So, TransMat.XYZ is a matrix from ACES --> XYZ, while TransMat.XYZinv goes from XYZ --> ACES. All use/are converted to the D65 illuminant, for consistency sake.
'''
#Gamma Functions: sRGB --> Linear.
gFunc = Gamma(Gamma.sRGBinv) #A Gamma Transform object using the sRGB-->Linear gamma formula. Apply to ColMaps!
gFuncManualsRGB = Gamma(lambda val: ((val + 0.055) / 1.055) ** 2.4 if val > 0.04045 else val / 12.92) #It's generic - specify any gamma function, even inline with a lambda!
#LUT from Function: sRGB --> Linear
oLut = LUT.lutFunc(Gamma.sRGBinv) #A LUT Transform object, created from a gamma function. Size is 16384 by default. LUTs are faster!
oLut.save('testpath/sRGB-->Lin.cube') #Saves the LUT to a format inferred from the extension. cube only for now!
#Opening LUTs from .cube files.
lut = LUT.open('testpath/sRGB-->Lin.cube') #Opens the lut we just made into a different LUT object.
lut.resized(17).save('testpath/sRGB-->Lin_tiny.cube') #Resizes the LUT, then saves it again to a much smaller file!
#Matrix Transformations
simpleMat = TransMat(TransMat.sRGBinv) #A Matrix Transform (TransMat) object, created from a color transform matrix for gamut transformations! This one is sRGB --> ACES.
mat = TransMat(TransMat.sRGBinv, TransMat.XYZ, TransMat.XYZinv, TransMat.aRGB) * TransMat.aRGBinv
#Indeed, specify many matrices which auto-multiply into a single one! You can also combine them after, with simple multiplication.
#Applying and saving.
img.apply(gFunc).save('testpath/openlut_gammafunc.png') #save saves an image using the appropriate image backend, based on the extension.
img.apply(lut).save('testpath/openlut_lut-lin-16384.png') #apply applies any color transformation object that inherits from Transform - LUT, Gamma, TransMat, etc., or make your own! It's easy ;) .
img.apply(lut.resized(17)).save('testpath/openlut_lut-lin-17.png') #Why so small? Because spline interpolation automatically turns on. It's identical to the larger LUT!
img.apply(mat).save('testpath/openlut_mat.png') #Applies the gamut transformation.
#As a proof of concept, here's a long list of transformations that should, in sum, do nothing :) :
img.apply(lut).apply(LUT.lutFunc(Gamma.sRGB)).apply(mat).apply(~mat).save('testpath/openlut_noop.png') #~mat is the inverse of mat. Easily undo the gamut operation!
#Format Test: All output images are in Linear ACES.
tImg = img.apply(mat)
tImg.save('testpath/output.exr')
tImg.save('testpath/output.dpx')
tImg.save('testpath/output.png')
tImg.save('testpath/output.jpg')
tImg.save('testpath/output.tif') #All sorts of formats work! Bit depth is 16, unless you say something else.
#Compression is impossible right now - wand is being difficult.
#Keep in mind, values are clipped from 0 to 1 when done. Scary transforms can make this an issue!
#Color management is simple: openlut doesn't touch your data, unless you tell it to with a Transform. So, the data that goes in, goes out, unless a Transform was applied.

191
openlut/ColMap.py 100644
View File

@ -0,0 +1,191 @@
import sys, os, os.path
import numpy as np
#~ import skimage as si
#~ import skimage.io
#~ si.io.use_plugin('freeimage')
#~ from PIL import Image
#~ import tifffile as tff
import wand
import wand.image
import wand.display
from wand.api import library
#~ library.MagickSetCompressionQuality.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
#~ library.MagickSetCompression.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
#~ COMPRESS_TYPES = dict(zip(wand.image.COMPRESSION_TYPES, tuple(map(ctypes.c_int, range(len(wand.image.COMPRESSION_TYPES))))))
from . import gamma
from .LUT import LUT
from .Viewer import Viewer
class ColMap :
def __init__(self, rgbArr) :
self.rgbArr = np.array(rgbArr, dtype=np.float32) #Enforce 32 bit floats. Save memory.
def fromIntArray(imgArr) :
bitDepth = int(''.join([i for i in str(imgArr.dtype) if i.isdigit()]))
return ColMap(np.divide(imgArr.astype(np.float64), 2 ** bitDepth - 1))
#Operations - returns new ColMaps.
def apply(self, transform) :
'''
Applies a Transform object by running its apply method.
'''
#~ return transform.apply(self)
return ColMap(transform.sample(self.asarray()))
#IO Functions
@staticmethod
def open(path) :
'''
Opens 8 and 16 bit images of many formats.
'''
try :
openFunction = {
"exr" : ColMap.openWand,
"dpx" : ColMap.openWand,
}[path[path.rfind('.') + 1:]]
return openFunction(path) #Any fancy formats will go here.
except :
#Fallback to opening using Wand.
return ColMap.openWand(path)
#Vendor-specific open methods.
#~ def openSci(path) :
#~ return ColMap.fromIntArray(si.io.imread(path)[:,:,:3])
@staticmethod
def openWand(path) :
'''
Open a file using the Wand ImageMagick binding.
'''
with wand.image.Image(filename=path) as img:
#Quick inverse sRGB transform, to undo what Wand did - but not for exr's, which are linear bastards.
if img.format != 'EXR' :
img.colorspace = 'srgb'
img.transform_colorspace('rgb')
img.colorspace = 'srgb' if img.format == 'DPX' else 'rgb' #Fix for IM's dpx bug.
return ColMap.fromIntArray(np.fromstring(img.make_blob("RGB"), dtype='uint{}'.format(img.depth)).reshape(img.height, img.width, 3))
@staticmethod
def fromBinary(binData, fmt, width=None, height=None) :
'''
Using the Wand blob functionality, creates a ColMap from binary data. Set binData to sys.stdin.buffer.read() to activate piping!
'''
with wand.image.Image(blob=binData, format=fmt, width=width, height=height) as img:
return ColMap.fromIntArray(np.fromstring(img.make_blob("RGB"), dtype='uint{}'.format(img.depth)).reshape(img.height, img.width, 3))
@staticmethod
def toBinary(self, fmt, depth=16) :
'''
Using Wand blob functionality
'''
with self.asWandImg(depth) as img :
img.format = fmt
return img.make_blob()
@staticmethod
def save(self, path, compress = None, depth = None) :
'''
Save the image. The filetype will be inferred from the path, and the appropriate backend will be used.
Compression scheme will be applied based on the backend compatiblity. Wand compression types can be used: Browse then
at http://docs.wand-py.org/en/0.4.3/wand/image.html#wand.image.COMPRESSION_TYPES .
'''
if depth is None: depth = 16
try :
saveFunction = {
"exr" : self.saveWand,
"dpx" : self.saveWand,
"tif" : self.saveWand,
"tiff": self.saveWand
}[path[path.rfind('.') + 1:]]
return saveFunction(path, compress, depth)
except :
#Fallback to saving using Wand.
self.saveWand(path, compress, depth)
#Vendor-specific save methods
def saveWand(self, path, compress = None, depth = 16) :
data = self.apply(LUT.lutFunc(gamma.sRGB)) if path[path.rfind('.')+1:] == 'dpx' else self
i = data.asWandImg(depth)
i.colorspace = 'srgb' #Make sure it saves without a colorspace transformation.
#~ if compress :
#~ library.MagickSetCompression(i.wand, 'rle')
#~ i.compression = 'lzma'
#~ i.compression_quality = 80
i.save(filename=path)
#~ def saveSci(self, path, compress = None, depth = 16) :
#~ if compress is not None: raise ValueError('Scipy Backend cannot compress the output image!')
#~ si.io.imsave(path, self.asIntArray())
#Display Functions
@staticmethod
def display(path, width = 1200) :
'''
Shows an image at a path without making a ColMap.
'''
img = ColMap.open(path).rgbArr
aspectRatio = img.shape[0]/img.shape[1]
xRes = width
yRes = int(xRes * aspectRatio)
Viewer.run(img, xRes, yRes, title = os.path.basename(path))
def show(self, width = 1200) :
#Use my custom OpenGL viewer!
Viewer.run(self.rgbArr, width, int(width * self.rgbArr.shape[0]/self.rgbArr.shape[1]))
@staticmethod
def wandShow(wandImg) :
#Do a quick sRGB transform for viewing. Must be in 'rgb' colorspace for this to take effect.
wandImg.transform_colorspace('srgb')
wand.display.display(wandImg)
wandImg.transform_colorspace('rgb') #This transforms it back to linearity.
#Data Form Functions
def asWandImg(self, depth = 16) :
#~ i = wand.image.Image(blob=self.asarray().tostring(), width=np.shape(self.rgbArr)[1], height=np.shape(self.rgbArr)[0], format='RGB') #Float Array
i = wand.image.Image(blob=self.asIntArray(depth).tostring(), width=np.shape(self.rgbArr)[1], height=np.shape(self.rgbArr)[0], format='RGB')
i.colorspace = 'rgb' #Specify, to Wand, that this image is to be treated as raw, linear, data.
return i
def asarray(self) :
"""
Returns the base float array.
"""
return self.rgbArr
def asIntArray(self, depth = 16, us = True) :
u = 'u' if us else ''
return np.multiply(self.rgbArr.clip(0, 1), 2.0 ** depth - 1).astype("{0}int{1}".format(u, depth))
#Overloads
def __repr__(self) :
return 'ColMap( \n\trgbArr = {0}\n)'.format('\n\t\t'.join([line.strip() for line in repr(self.rgbArr).split('\n')]))

148
openlut/ColMat.py 100644
View File

@ -0,0 +1,148 @@
import multiprocessing as mp
from functools import reduce
import operator as oper
import numpy as np
import numba
from .Transform import Transform
class ColMat(Transform) :
def __init__(self, *mats) :
'''
Initializes a combined 3x3 Transformation Matrix from any number of input matrices. These may be numpy arrays, matrices,
other ColMats, or any combination thereof.
'''
if len(mats) == 1 :
mat = mats[0]
if isinstance(mat, ColMat) :
self.mat = mat.mat #Support a copy constructor.
else :
self.mat = np.array(mat) #Simply set self.mat with the numpy array version of the mat.
elif len(mats) > 1 :
self.mat = ColMat.__mats(*[ColMat(mat) for mat in mats]).mat
elif not mats :
self.mat = np.identity(3)
def __mats(*inMats) :
'''
Initialize a combined Transform matrix from several input ColMats.
'''
return ColMat(reduce(ColMat.__mul__, reversed(inMats))) #Works because multiply is actually non-commutative dot.
#This is why we reverse inMats.
@numba.jit(nopython=True)
def __optDot(img, mat, shp, out) :
'''
Dots the matrix with each tuple of colors in the img.
img: Numpy array of shape (height, width, 3).
mat: The 3x3 numpy array representing the color transform matrix.
shp: The shape of the image.
out: the output list. Built mutably for numba's sake.
'''
shaped = img.reshape((shp[0] * shp[1], shp[2])) #Flatten to 2D array for iteration over colors.
i = 0
while i < shp[0] * shp[1] :
res = np.dot(mat, shaped[i])
out[i] = res
i += 1
def __applMat(q, cpu, shp, mat, img3D) :
out = np.zeros((shp[0] * shp[1], shp[2]))
ColMat.__optDot(img3D, mat, shp, out)
q.put( (cpu, out.reshape(shp)) )
def sample(self, fSeq) :
shp = np.shape(fSeq)
if len(shp) == 1 :
return self.mat.dot(fSeq)
if len(shp) == 3 :
cpus = mp.cpu_count()
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, cpus)
for cpu in range(cpus) :
p = mp.Process(target=ColMat.__applMat, args=(q, cpu, np.shape(splt[cpu]), self.mat, splt[cpu]))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0)
#~ out = np.zeros((shp[0] * shp[1], shp[2]))
#~ ColMat.__optDot(fSeq, self.mat, shp, out)
#~ return out.reshape(shp)
#~ return np.array([self.mat.dot(col) for col in fSeq.reshape(shp[0] * shp[1], shp[2])]).reshape(shp)
#~ p = mp.Pool(mp.cpu_count())
#~ return np.array(list(map(self.mat.dot, fSeq.reshape(shp[0] * shp[1], shp[2])))).reshape(shp)
#~ return fSeq.dot(self.mat)
def inv(obj) :
if isinstance(obj, ColMat) : #Works on any ColMat object - including self.
return ColMat(np.linalg.inv(obj.mat))
else : #Works on raw numpy arrays as well.
return np.linalg.inv(obj)
def transpose(self) :
return ColMat(np.transpose(self.mat))
#Overloading
def __mul__(self, other) :
'''
* implements matrix multiplication.
'''
if isinstance(other, ColMat) :
return ColMat(self.mat.dot(other.mat))
elif isinstance(other, float) or isinstance(other, int) :
return ColMat(np.multiply(self.mat, other))
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return ColMat(self.mat.dot(np.array(other)))
else :
raise ValueError('Invalid multiplication arguments!')
__rmul__ = __mul__
def __add__(self, other) :
if isinstance(other, ColMat) :
return ColMat(self.mat + other.mat)
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return ColMat(self.mat + np.array(other))
else :
raise ValueError('Invalid addition arguments!')
__radd__ = __add__
def __pow__(self, other) :
'''
** implements direct multiplication. You usually don't want this.
'''
if isinstance(other, ColMat) :
return ColMat(self.mat * other.mat)
elif isinstance(other, float) or isinstance(other, int) :
return ColMat(np.multiply(self.mat, other))
elif isinstance(other, np.ndarray) or isinstance(other, np.matrixlib.defmatrix.matrix) :
return ColMat(self.mat * np.array(other))
else :
raise ValueError('Invalid multiplication arguments!')
def __invert__(self) :
return self.inv()
def __len__(self) :
return len(self.mat)
def __getitem__(self, key) :
return self.mat[key]
def __iter__(self) :
return iter(self.mat)
def __repr__(self) :
return "\nColMat (\n{0} )\n".format(str(self.mat))

42
openlut/Func.py 100644
View File

@ -0,0 +1,42 @@
import multiprocessing as mp
import types
from functools import reduce
import numpy as np
from .Transform import Transform
from .lib import olOpt as olo
class Func(Transform) :
def __init__(self, func) :
self.func = func
#Func Methods
def __gamma(q, cpu, f, spSeq) :
q.put( (cpu, f(spSeq)) )
def sample(self, fSeq) :
fSeq = np.array(fSeq, dtype=np.float32) #Just some type assurances.
# Any float-returning C++ functions can be threaded with olo.gam(), but because of GIL, it won't work with Python functions.
if isinstance(self.func, types.BuiltinFunctionType) :
# \/ Just olo.gam, except fSeq is flattened to a 1D array, processed flat, then shaped back into a 3D array on the fly.
return olo.gam(fSeq.reshape(reduce(lambda a, b: a*b, fSeq.shape)), self.func).reshape(fSeq.shape) #OpenMP vectorized C++ motherfuckery!
else :
#We always have the slow af fallback.
fVec = np.vectorize(self.func)
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, mp.cpu_count())
for cpu in range(mp.cpu_count()) :
p = mp.Process(target=Func.__gamma, args=(q, cpu, fVec, splt[cpu]))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0) if len(fSeq) > 1 else self.func(fSeq[0])
#~ return fVec(fSeq) if len(fSeq) > 1 else self.func(fSeq[0])

223
openlut/LUT.py 100644
View File

@ -0,0 +1,223 @@
import multiprocessing as mp
from functools import reduce
import types
import numpy as np
MOD_SCIPY = False
try :
from scipy.interpolate import splrep, splev
MOD_SCIPY = True
except :
pass
from .Transform import Transform
from .lib import olOpt as olo
class LUT(Transform) :
def __init__(self, dims = 1, size = 16384, title = "openlut_LUT", iRange = (0.0, 1.0)) :
'''
Create an identity LUT with given dimensions (1 or 3), size, and title.
'''
if dims != 1 and dims != 3: raise ValueError("Dimensions must be 1 or 3!")
self.title = title #The title.
self.size = size #The size. 1D LUTs: size numbers. 3D LUTs: size x size x size numbers.
self.range = iRange #The input range - creates data or legal LUTs. Should work fine, but untested.
self.dims = dims #The dimensions. 1 or 3; others aren't accepted.
self.ID = np.linspace(self.range[0], self.range[1], self.size, dtype=np.float32) #Read Only.
if dims == 1 :
self.array = np.linspace(self.range[0], self.range[1], self.size, dtype=np.float32) #Size number of floats.
elif dims == 3 :
print("3D LUT Not Implemented!")
#~ self.array = np.linspace(self.range[0], self.range[1], self.size**3).reshape(self.size, self.size, self.size) #Should make an identity size x size x size array.
def lutFunc(func, size = 16384, dims = 1, title="openlut_FuncGen", iRange = (0.0, 1.0)) :
'''
Creates a LUT from a simple function.
'''
if dims == 1 :
lut = LUT(dims=dims, size=size, title=title, iRange=iRange)
#Use fast function sampling if the function is a C++ function.
vFunc = lambda arr: olo.gam(arr, func) if isinstance(func, types.BuiltinFunctionType) else np.vectorize(func, otypes=[np.float32])
lut.array = vFunc(lut.array)
return lut
elif dims == 3 :
print("3D LUT Not Implemented!")
def lutArray(array, title="Array_Generated") :
'''
Creates a LUT from a float array. Elements must be in range [0, 1].
'''
if len(np.shape(array)) == 1 :
lut = LUT(dims=1, size=len(array), title=title)
lut.array = array
return lut
elif len(np.shape(array)) == 3 :
print("3D LUT Not Implemented!")
else :
raise ValueError("lutArray input must be 1D or 3D!")
def lutMapping(idArr, mapArr, title="Mapped_Array") :
'''
Creates a 1D LUT from a nonlinear mapping. Elements must be in range [0, 1].
'''
return LUT.lutArray(splev(np.linspace(0, 1, num=len(idArr)), splrep(idArr, mapArr)))
#LUT Functions.
def __interp(q, cpu, spSeq, ID, array, spl) :
if spl :
q.put( (cpu, splev(spSeq, splrep(ID, array))) ) #Spline Interpolation. Pretty quick, considering.
else :
q.put( (cpu, np.interp(spSeq, ID, array)) )
def sample(self, fSeq, spl=True) :
'''
Sample the LUT using a flat float sequence (ideally a numpy array; (0..1) ).
Each n (dimensions) clump of arguments will be used to sample the LUT. So:
1D LUT: in1, in2, in3 --> out1, out2, out3
*Min 1 argument.
3D LUT: inR, inG, inB --> outR, outG, outB
*Min 3 arguments, len(arguments) % 3 must equal 0.
Returns a numpy array with identical shape to the input array.
'''
fSeq = np.array(fSeq)
if self.dims == 1 :
#~ return np.interp(spSeq, self.ID, self.array)
#If scipy isn't loaded, we can't use spline interpolation!
if (not MOD_SCIPY) or self.size > 1023: spl = False # Auto-adapts big LUTs to use the faster, more brute-forceish, linear interpolation.
out = []
q = mp.Queue()
splt = Transform.spSeq(fSeq, mp.cpu_count())
for cpu in range(mp.cpu_count()) :
p = mp.Process(target=LUT.__interp, args=(q, cpu, splt[cpu], self.ID, self.array, spl))
p.start()
for num in range(len(splt)) :
out.append(q.get())
return np.concatenate([seq[1] for seq in sorted(out, key=lambda seq: seq[0])], axis=0)
elif self.dims == 3 :
print("3D LUT Not Implemented!")
def resized(self, newSize) :
'''
Return the LUT, resized to newSize.
1D LUTs: If the new size is lower, we use Linear interpolation. If the new size is higher, we use Spline interpolation.
* If the current size is too low, use spline regardless.
'''
if newSize == self.size: return self
fac = newSize / self.size
useSpl = self.size < newSize #If the new size is lower, we use Linear interpolation. If the new size is higher, we use Spline interpolation.
if self.size < 128: useSpl = True #If the current size is too low, use spline regardless.
if self.dims == 1 :
newID = np.linspace(self.range[0], self.range[1], newSize)
return LUT.lutArray(self.sample(newID, spl=useSpl), title="Resized to {0}".format(newSize))
if self.dims == 3 :
print("3D LUT Not Implemented")
def inverted(self) :
'''
Return the inverse LUT.
'''
return LUT.lutArray(splev(np.linspace(self.range[0], self.range[1], num=self.size), splrep(self.array, np.linspace(self.range[0], self.range[1], num=self.size))))
#IO Functions.
def open(path) :
'''
Opens any supported file format, located at path.
'''
openFunction = {
"cube" : LUT.openCube,
}[path[path.rfind('.') + 1:]]
return openFunction(path)
def openCube(path) :
'''
Opens .cube files. They must be saved with whitespaces. Referenced by open().
'''
lut = LUT() #Mutable luts are not reccommended for users.
with open(path, 'r') as f :
i = 0
for line in f :
#~ if not line.strip(): continue
sLine = line.strip()
if not sLine: continue
if sLine[0] == '#': continue
index = sLine[:sLine.find(' ')]
data = sLine[sLine.find(' ') + 1:]
if index == "TITLE": lut.title = data.strip('"'); continue
if index == "LUT_1D_SIZE": lut.dims = 1; lut.size = int(data); continue
if index == "LUT_3D_SIZE": lut.dims = 3; lut.size = int(data); continue
if index == "LUT_1D_INPUT_RANGE": lut.range = (float(data[:data.find(' ')]), float(data[data.find(' ') + 1:])); continue
if lut.dims == 1 and sLine[:sLine.find(' ')] :
lut.array[i] = float(sLine[:sLine.find(' ')])
i += 1
elif lut.dims == 3 :
print("3D LUT Not Implemened!")
return lut
def save(self, path) :
'''
Method that saves the LUT in a supported format, based on the path.
'''
saveFunction = {
"cube" : self.saveCube,
}[path[path.rfind('.') + 1:]]
saveFunction(path)
def saveCube(self, path) :
with open(path, 'w') as f :
print('TITLE', '"{}"'.format(self.title), file=f)
if self.dims == 1 :
print('LUT_1D_SIZE', '{}'.format(self.size), file=f)
print('LUT_1D_INPUT_RANGE', '{0:.6f} {1:.6f}'.format(*self.range), file=f)
print('# Created by openlut.\n', file=f)
for itm in self.array :
entry = '{0:.6f}'.format(itm)
print(entry, entry, entry, file=f)
elif self.dims == 3 :
print("3D LUT Not Implemented!")
#Overloaded functions
def __iter__(self) :
if dims == 1 :
return iter(self.array)
elif dims == 3 :
iArr = self.array.reshape(self.dims, self.size / self.dims) #Group into triplets.
return iter(iArr)
def __getitem__(self, key) :
return self.sample(key)
def __repr__(self) :
return 'LUT(\tdims = {0},\n\tsize = {1},\n\ttitle = "{2}"\n\tarray = {3}\n)'.format(self.dims, self.size, self.title, '\n\t\t'.join([line.strip() for line in repr(self.array).split('\n')]))

View File

@ -0,0 +1,17 @@
import abc
import numpy as np
class Transform :
def spSeq(seq, outLen) :
"""
Utility function for splitting a sequence into equal parts, for multithreading.
"""
perfSep = (1/outLen) * len(seq)
return list(filter(len, [seq[round(perfSep * i):round(perfSep * (i + 1))] for i in range(len(seq))])) if len(seq) > 1 else seq
@abc.abstractmethod
def sample(self, fSeq) :
"""
Samples the Transformation.
"""

135
openlut/Viewer.py 100644
View File

@ -0,0 +1,135 @@
import pygame
from pygame.locals import *
from OpenGL.GL import *
from OpenGL.GLU import *
import sys, os, os.path
class Viewer :
def __init__(self, res, title="OpenLUT Image Viewer") :
self.res = res
pygame.init()
pygame.display.set_caption(title)
pygame.display.set_mode(res, DOUBLEBUF|OPENGL)
self.initGL()
def initGL(self) :
'''
Initialize OpenGL.
'''
glEnable(GL_TEXTURE_2D)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, self.res[0], self.res[1], 0, 0, 100)
glMatrixMode(GL_MODELVIEW)
#~ glClearColor(0, 0, 0, 0)
#~ glClearDepth(0)
#~ glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
#~ def resizeWindow(self, newRes) :
#~ self.res = newRes
#~ pygame.display.set_mode(self.res, RESIZABLE|DOUBLEBUF|OPENGL)
##~ glLoadIdentity()
##~ glOrtho(0, self.res[0], self.res[1], 0, 0, 100)
##~ glMatrixMode(GL_MODELVIEW)
def drawQuad(self) :
'''
Draws an image to the screen.
'''
glBegin(GL_QUADS)
glTexCoord2i(0, 0)
glVertex2i(0, 0)
glTexCoord2i(0, 1)
glVertex2i(0, self.res[1])
glTexCoord2i(1, 1)
glVertex2i(self.res[0], self.res[1])
glTexCoord2i(1, 0)
glVertex2i(self.res[0], 0)
glEnd()
def bindTex(self, img) :
'''
Binds the image contained the numpy float array img to a 2D texture on the GPU.
'''
id = glGenTextures(1)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
glBindTexture(GL_TEXTURE_2D, id)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.shape[1], img.shape[0], 0, GL_RGB, GL_FLOAT, img)
def display(self) :
'''
Repaints the window.
'''
#Clears the "canvas"
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
#Maybe do them here.
glEnable(GL_TEXTURE_2D)
self.drawQuad()
#Updates the display.
pygame.display.flip()
def close() :
#~ print()
pygame.quit()
def run(img, xRes, yRes, title = "OpenLUT Image Viewer") :
'''
img is an rgb array.
'''
v = Viewer((xRes, yRes), title)
v.bindTex(img)
FPS = None
clock = pygame.time.Clock()
while True :
for event in pygame.event.get() :
if event.type == pygame.QUIT: Viewer.close(); break
#~ if event.type == pygame.VIDEORESIZE :
#~ v.resizeWindow((event.w, event.h))
if event.type == pygame.KEYDOWN :
try :
{
}[event.key]()
except KeyError as key :
if str(key) == "27": Viewer.close(); break #Need to catch ESC to close the window.
print("Key not mapped!")
else :
#This else will only run if the event loop is completed.
v.display()
#Smooth playback at FPS.
if FPS: clock.tick(FPS)
else: clock.tick()
#~ print("\r", clock.get_fps(), end="", sep="")
continue
break #This break will only run if the event loop is broken out of.

View File

@ -0,0 +1,10 @@
#Set it up so that the users don't see the files containing the classes.
from .Transform import Transform
from .ColMap import ColMap
from .LUT import LUT
from .Func import Func
from .ColMat import ColMat
from .Viewer import Viewer
__all__ = ['ColMap', 'Transform', 'LUT', 'Func', 'ColMat', 'Viewer', 'gamma', 'gamut']

69
openlut/gamma.py 100644
View File

@ -0,0 +1,69 @@
import math
import numpy as np
from .lib import olOpt as olo
#Static Gamma Functions, borrowed from olo.
#inv goes from space to lin.
lin = olo.lin
sRGB = olo.sRGB
sRGBinv = olo.sRGBinv
Rec709 = olo.Rec709
ReinhardHDR = olo.ReinhardHDR
sLog = olo.sLog
sLog2 = olo.sLog2
DanLog = olo.DanLog
class PGamma :
'''
Static class containing python versions of the C++ gamma functions.
'''
def lin(x): return x
def sRGB(x) :
'''
sRGB formula. Domain must be within [0, 1].
'''
return ( (1.055) * (x ** (1.0 / 2.4)) ) - 0.055 if x > 0.0031308 else x * 12.92
def sRGBinv(x) :
'''
Inverse sRGB formula. Domain must be within [0, 1].
'''
return ((x + 0.055) / 1.055) ** 2.4 if x > 0.04045 else x / 12.92
def Rec709(x) :
'''
Rec709 formula. Domain must be within [0, 1].
'''
return 1.099 * (x ** 0.45) - 0.099 if x >= 0.018 else 4.5 * x
def ReinhardHDR(x) :
'''
Reinhard Tonemapping formula. Domain must be within [0, 1].
'''
return x / (1.0 + x)
def sLog(x) :
'''
sLog 1 formula. Domain must be within [0, 1]. See https://pro.sony.com/bbsccms/assets/
files/mkt/cinema/solutions/slog_manual.pdf .
'''
return ( 0.432699 * math.log(x + 0.037584, 10.0) + 0.616596) + 0.03
def sLog2(x) :
'''
sLog2 formula. Domain must be within [0, 1]. See https://pro.sony.com/bbsccms/assets/files/micro/dmpc/training/S-Log2_Technical_PaperV1_0.pdf .
'''
return ( 0.432699 * math.log( (155.0 * x) / 219.0 + 0.037584, 10.0) + 0.616596 ) + 0.03
def sLog3(x) :
'''
Not yet implemented. See http://community.sony.com/sony/attachments/sony/large-sensor-camera-F5-F55/12359/2/TechnicalSummary_for_S-Gamut3Cine_S-Gamut3_S-Log3_V1_00.pdf .
'''
return x
def DanLog(x) :
return (10.0 ** ((x - 0.385537) / 0.2471896) - 0.071272) / 3.555556 if x > 0.1496582 else (x - 0.092809) / 5.367655

71
openlut/gamut.py 100644
View File

@ -0,0 +1,71 @@
import numpy as np
#Static Matrices -- all go from ACES to <gamut name>. <gamut name>inv matrices go from the gamut to ACES.
#Converted (CIECAT02) D65 Illuminant for all.
XYZ = np.array(
[
0.93863095, -0.00574192, 0.0175669,
0.33809359, 0.7272139, -0.0653075,
0.00072312, 0.00081844, 1.08751619
]
).reshape(3, 3)
XYZinv = np.array(
[
1.06236611, 0.00840695, -0.01665579,
-0.49394137, 1.37110953, 0.09031659,
-0.00033467, -0.00103746, 0.91946965
]
).reshape(3, 3)
sRGB = np.array(
[
2.52193473, -1.1370239, -0.38491083,
-0.27547943, 1.36982898, -0.09434955,
-0.01598287, -0.14778923, 1.1637721
]
).reshape(3, 3)
sRGBinv = np.array(
[
0.43957568, 0.38391259, 0.17651173,
0.08960038, 0.81471415, 0.09568546,
0.01741548, 0.10873435, 0.87385017
]
).reshape(3, 3)
Rec709 = np.array(
[
2.52193473, -1.1370239, -0.38491083,
-0.27547943, 1.36982898, -0.09434955,
-0.01598287, -0.14778923, 1.1637721
]
).reshape(3, 3)
Rec709inv = np.array(
[
0.43957568, 0.38391259, 0.17651173,
0.08960038, 0.81471415, 0.09568546,
0.01741548, 0.10873435, 0.87385017
]
).reshape(3, 3)
aRGB = np.array(
[
1.72502307, -0.4228857, -0.30213736,
-0.27547943, 1.36982898, -0.09434955,
-0.02666425, -0.08532111, 1.11198537
]
).reshape(3, 3)
aRGBinv = np.array(
[
0.61468318, 0.20122762, 0.1840892,
0.12529321, 0.77491365, 0.09979314,
0.02435304, 0.06428329, 0.91136367
]
).reshape(3, 3)

View File

@ -0,0 +1,37 @@
#Macros
SHELL=/bin/sh
FILES = olOpt.cpp
#~ LIBS = openimageio
CXXFLAGS = -O3 -shared -Wall -std=gnu++14 -fopenmp -fPIC
PYTHONFLAGS = $(shell python3-config --cflags) $(shell python3-config --ldflags)
CXX = g++
LIBS =
DATE = $(shell date)
#Main Rules
all: olOpt
clean:
-rm -f *.so
#~ clean-deps:
#~ -rm -rf libs
#~ clean-all: clean clean-deps
install:
@echo "Not Yet Implemented"
debug:
#Executables
olOpt: ${FILES}
${CXX} -o $@.so ${FILES} ${CXXFLAGS} ${LIBS} ${PYTHONFLAGS}
@echo "Successfully compiled at" ${DATE}

View File

View File

@ -0,0 +1,587 @@
#!/usr/bin/env python3
'''
Copyright 2016 Sofus Rose
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
'''
import sys, os, time
import multiprocessing as mp
class Files :
"""
The Files object is an immutable sequence of files, which supports writing simultaneously to all the files.
"""
def __init__(self, *files) :
seq=[]
for f in files:
if isinstance(f, Files): seq += f.files
elif 'write' in dir(f): seq.append(f)
else: raise TypeError('Wrong Input Type: ' + repr(f))
self.files = tuple(seq) #Immutable tuple of file-like objects,
def write(self, inStr, exclInd=[]) :
"""
Writes inStr all file-like objects stored within the Files object. You may exclude certain entries with a sequence of indices.
"""
for f in enumerate(self.files) :
if f[0] in exclInd: continue
f[1].write(inStr)
def __add__(self, o, commut=False) :
"""
Implements merging with Files objects and appending file-like objects. Returns new Files object.
"""
if isinstance(o, Files) :
this, other = self.files, o.files
elif 'write' in dir(o) :
this, other = self.files, [o]
else :
return None
if commut: this, other = other, this
return Files(*this, *other) #this and other must be unpackable.
def __radd__(self, o) :
"""
Commutative addition.
"""
return self.__add__(o, commut=True) #Use the add operator. It's commutative!!
def __bool__(self) :
"""
False if empty.
"""
return bool(self.files)
def __getitem__(self, index) :
"""
Supports slicing and indexing.
"""
if isinstance(index, slice) :
return Files(self.files[index])
else :
return self.files[index]
def __len__(self) :
"""
Number of files in the Files object.
"""
return len(self.files)
def __repr__(self) :
return 'Files(' + ', '.join("'{}'".format(n.name) for n in self.files) + ')'
def __iter__(self) :
"""
Iterates through the file-like objects.
"""
return iter(self.files)
class ColLib :
"""
Simple hashmap to colors.. Make sure to activate colors in ~/.bashrc or enable ansi.sys!
"""
cols = { 'HEADER' : '\033[97m',
'OKBLUE' : '\033[94m',
'OKGREEN' : '\033[92m',
'WARNING' : '\033[93m',
'FAIL' : '\033[91m',
'CRIT' : '\033[31m',
'DEBUG' : '\033[35m',
'ENDC' : '\033[0m',
'BOLD' : '\033[1m',
'ITALIC' : '\033[3m',
'UNDERLINE' : '\033[4m'
}
debug = { 'info' : ('[INFO]', 'OKGREEN'),
'error' : ('[ERROR]', 'FAIL'),
'crit' : ('[CRIT]', 'CRIT'),
'warn' : ('[WARNING]', 'WARNING'),
'debug' : ('[DEBUG]', 'DEBUG'),
'run' : ('[RUN]', 'OKBLUE')
}
def colString(color, string) :
"""
Returns a colored string.
"""
return '{}{}{}'.format(cols[color], string, cols['ENDC'])
def dbgString(signal, string) :
"""
"""
return '{}{}{}'.format(debug[signal])
def printCol(color, colored, *output, **settings) :
"""
Simple print clone where the first printed parameter is colored.
"""
print(cols[color] + str(colored) + cols['ENDC'], *output, **settings)
def printDbg(signal, *output, **settings) :
"""
Pass in simple debug signals to print the corresponding entry.
"""
printCol(debug[signal][1], debug[signal][0] + ' ' + colored, *output, **settings)
class Log(ColLib) :
"""
Logging object, an instance of which is passed throughout afarm. **It has + changes state**, as the sole exception to the
'no globals' paradigm. You may pass in any file-like object, the only criteria being that it has a 'write' method. stdout is
used by default.
"""
def __init__(self, *file, verb=3, useCol=True, startTime=None) :
if not file: file = [sys.stdout]
if startTime is None: startTime = time.perf_counter()
self.verb = verb #From 0 to 3. 0: CRITICAL 1: ERRORS 2: WARNINGS 3: DEBUG. Info all.
self.file = Files(*file)
self.log = [] #Log list. Format: (verb, time in ms, debug_text, text)
self.sTimes = dict() #Dict of start times for various runs.
self._useCol = useCol #Whether or not to use colored output.
self._attrLock = mp.Lock() #The log access lock.
self._startTime = startTime #Global instance time. begins when the instance is created.
def getLogTime(self) :
"""
Gets the current logging time in seconds, from the time of instantiation of the Log object.
"""
return time.perf_counter() - self._startTime
def startTime(self, run) :
"""
Starts the timer for the specified run. Can use any immutable object to mark the run.
"""
self.sTimes[run] = self.getLogTime()
def getTime(self, run) :
"""
Gets the time since startTime for the specified run (an immutable object).
"""
if run in self.sTimes :
return self.getLogTime() - self.sTimes[run]
else :
raise ValueError('Run wasn\'t found!!')
def compItem(self, state, time, *text) :
"""
Returns a displayable log item as a string, formatted with or without color.
"""
decor = { 'info' : '',
'error' : '',
'crit' : ColLib.cols['BOLD'],
'warn' : '',
'debug' : ColLib.cols['BOLD'],
'run' : ColLib.cols['BOLD']
}[state]
timeCol = { 'info' : ColLib.cols['HEADER'],
'error' : ColLib.cols['WARNING'],
'crit' : ColLib.cols['FAIL'],
'warn' : ColLib.cols['HEADER'],
'debug' : ColLib.cols['DEBUG'] + ColLib.cols['BOLD'],
'run' : ColLib.cols['OKGREEN']
}[state]
if self._useCol :
return '{3}{5}{0}{4[ENDC]}\t{6}{1:.10f}{4[ENDC]}: {2}'.format( ColLib.debug[state][0],
time,
''.join(text),
ColLib.cols[ColLib.debug[state][1]],
ColLib.cols,
decor,
timeCol
)
else :
return '{0} {1:.10f}: {2}'.format(ColLib.debug[state][0], time, ''.join(text))
def write(self, *text, verb=2, state='info') :
"""
Adds an entry to the log file, as well as to the internal structure.
Possible state values:
*'info': To give information.
*'error': When things go wrong.
*'crit': When things go very wrong.
*'warn': To let the user know that something weird is up.
*'run': To report on an intensive process.
*'debug': For debugging purposes. Keep it at verbosity 3.
Possible verbosity values, and suggested usage:
*0: User-oriented, general info about important happenings.
*1: Helpful info about what is running/happening, even to the user.
*2: Deeper info about the programs functionality, for fixing problems.
*3: Developer oriented debugging.
"""
text = [str(t).strip() for t in text if str(t).strip()]
if not text: return #Empty write's are no good.
curTime = self.getLogTime()
with self._attrLock :
self.log.append( { 'verb' : verb,
'time' : curTime,
'state' : state,
'text' : ' '.join(str(t) for t in text)
}
)
if self.verb >= verb :
with self._attrLock :
print(self.compItem(state, curTime, ' '.join(text)), file=self.file)
def read(self, verb=None) :
"""
Reads the internal logging data structure, optionally overriding verbosity.
"""
if not verb: verb = self.verb
with self._attrLock :
return '\n'.join([self.compItem(l['state'], l['time'], l['text']) for l in self.log if verb >= l['verb']])
def reset(self, startTime=None) :
return Log(self.file, verb=self.verb, useCol=self._useCol, startTime=startTime)
def getFiles(self) :
"""
Get the list of files to dump Log output to.
"""
return self.file
def setFiles(self, *files) :
"""
Set a new list of files to dump Log output to.
"""
with self._attrLock :
self.file = Files(*files)
def addFiles(self, *files) :
"""
Add a list of files to dump Log output to.
"""
with self._attrLock :
self.file += Files(*files)
def setVerb(self, newVerb) :
"""
Call to set verbosity.
"""
with self._attrLock :
self.verb = newVerb
self.write('Verbosity set to', str(newVerb) + '.', verb=0, state='info')
def setCol(self, newUseCol) :
"""
Call to change color output.
"""
with self._attrLock :
self._useCol = newUseCol
self.write('Color Output set to', self._useCol, verb=0, state='info')
def __call__(self, verb, state, *text) :
"""
Identical to Log.write(), except it requires the verbosity level to be specified.
"""
self.write(verb=verb, state=state, *text)
def __repr__(self) :
return ( 'Log(' +
(', '.join("'{}'".format(f.name) for f in self.file.files) + ', ' if self.file else '') +
'verb={}, useCol={}, startTime={:.3f})'.format(self.verb, self._useCol, self._startTime)
)
def __str__(self) :
return self.read()
def __add__(self, o, commut=False) :
"""
Merges a Log object with another Log object, a Files object, or a file-like object.
*For Log object addition, the minimum startTime attibute is used to initialize the merged startTime.
*The Files objects of both Log objects are merged.
"""
if isinstance(o, Log) :
l = self.reset(min(self.getLogTime(), o.getLogTime())) #Max of self and other time.
l.log = self.log + o.log
l.log.sort(key=lambda item: item['time']) #Make sure to sort the internal log by time.
l.addFiles(o.file)
return l
elif isinstance(o, Files) :
l = self.reset(self.getLogTime())
l.log = self.log
l.setFiles(self.getFiles(), *o.files)
return l
elif 'write' in dir(o) :
l = self.reset(self.getLogTime())
l.file = o + self.file if commut else self.file + o
return l
else :
return None
def __radd__(self, o) :
return self.__add__(o, commut=True) #Use the add operator. It's commutative!!
def __bool__(self) :
"""
False if log is empty.
"""
return bool(self.log)
def __getitem__(self, i) :
"""
Supports slicing and indexing, from recent (0) to oldest (end).
"""
if isinstance(i, slice) :
l = self.reset(self.getLogTime())
for ind, itm in enumerate(self.log) :
if ind in list(range(i.start if i.start else 0, i.stop, i.step if i.step else 1)): l.log.append(itm)
return list(l)
else :
l = self.log[::-1][i]
return self.compItem(l['state'], l['time'], l['text'], noCol = not self._useCol)
def __len__(self) :
"""
Amount of items in the log.
"""
return len(self.log)
def __iter__(self) :
"""
Iterator never colors output.
"""
return iter(self.compItem(l['state'], l['time'], l['text'], noCol = True) for l in self.log)
class LogFile() :
"""
Similar to a normal file, except it splits into several files. On the frontend, however, it acts as if it were a single file.
*Writes to 'path'.log.
*When maxLen is exceeded, lines are pushed into 'path'.0.log, then 'path'.1.log, etc. .
"""
def __init__(self, path, maxLen=1000, trunc=False) : #Make maxLen 1000 later.
"""
Constructor accepts a path (extension will be rewritten to '.log'), a maximum length, and will optionally truncate
any previous logfiles.
"""
self.path = os.path.splitext(path)[0] + '.log'
self.bPath = os.path.splitext(self.path)[0]
self.maxLen = maxLen
self.name = '<{0}.log, {0}.0...n.log>'.format(self.bPath)
self.lines = 0
self.fileNum = 0
#If the logfile already exists, it's read + rewritten using the current maxLen.
if os.path.exists(self.path) :
if trunc: self.truncate(); return
inLines = open(self.path, 'r').readlines()[::-1]
os.remove(os.path.abspath(self.path)) #Remove the old path.log.
i = 0
while os.path.exists('{0}.{1}.log'.format(self.bPath, i)) :
inLines += open('{0}.{1}.log'.format(self.bPath, i), 'r').readlines()[::-1]
os.remove(os.path.abspath('{0}.{1}.log'.format(self.bPath, i)))
i += 1
self.write(''.join(reversed(inLines)))
def write(self, *inStr) :
apnd = list(filter(bool, ''.join(inStr).strip().split('\n')))
if not apnd: return #Nothing to append = don't even try!
if not os.path.exists(self.path): open(self.path, 'w').close() #Make sure path.log exists.
#Empty apnd line by line.
while len(apnd) > 0 :
toWrite = self.maxLen * (self.fileNum + 1) - self.lines #Lines needed to fill up path.log
if toWrite == 0 : #Time to make new files.
#Rename upwards. path.n.log -> path.(n+1).log, etc. . path.log becomes path.0.log.
for i in reversed(range(self.fileNum)) :
os.rename('{0}.{1}.log'.format(self.bPath, i), '{0}.{1}.log'.format(self.bPath, i+1))
os.rename('{0}.log'.format(self.bPath), '{0}.0.log'.format(self.bPath))
#Make new path.log.
open(self.path, 'w').close() #Just create the file.
#Number of files just increased.
self.fileNum += 1
else : #Fill up path.log.
print(apnd.pop(0), file=open(self.path, 'a'))
#Number of written lines just increasd.
self.lines += 1
def read(self) :
collec = []
for i in reversed(range(self.fileNum)) :
collec += open('{0}.{1}.log'.format(self.bPath, i), 'r').readlines()
collec += open(self.path, 'r').readlines()
return ''.join(collec)
def truncate(self) :
"""
Deletes all associated files + resets the instance.
"""
i = 0
os.remove(os.path.abspath(self.path)) #Remove the old path.log.
while os.path.exists('{0}.{1}.log'.format(self.bPath, i)) : #Remove all log files.
os.remove(os.path.abspath('{0}.{1}.log'.format(self.bPath, i)))
i += 1
self.lines = 0
self.fileNum = 0
def readlines(self) :
return self.read().split('\n')
def isatty(self) :
"""
Always returns false, as a LogFile is never associated with a tty.
"""
return False
def __iter__(self) :
return (line for line in self.readlines())
def __repr__(self) :
return 'LogFile({0}, maxLen={1})'.format(self.path, self.maxLen)
def coolTest() :
l = Log(Files(LogFile('first', 10, True), LogFile('second', 20, True)), LogFile('third', 30, True), sys.stdout)
l(0, 'info', 'Big Failure Oh NO!')
l(0, 'error', 'Big Failure Oh NO!')
l(0, 'crit', 'Big Failure Oh NO!')
l(0, 'warn', 'Big Failure Oh NO!')
l(0, 'debug', 'Big Failure Oh NO!')
l(0, 'run', 'Big Failure Oh NO!')
print('We got ourselves a log file here kids.')
for i in range(50) :
l(0, 'run', 'This is the', i, 'run today!')
print(l.getLogTime())
def logFileTest() :
l = LogFile('hi.log', maxLen = 3, trunc=True)
print('\n', repr(l), sep='')
print('hi', 'world', file=l, sep='\n')
print('\n', repr(l), sep='')
print('you', 'are', 'cool', 'friend', 'hi', file=l, sep='\n')
print('hello\nmotherfucker\nits\narnold\nyour\nold\nfriend\nrunbaby!!\nlittlebitch :)', file=l)
print('\n', repr(l), sep='')
print('Reading Log Files:', repr(l.read()))
print(l.name, '\n\n\n')
print(l.read().split('\n'))
print(len([x for x in l.read().split('\n') if x]))
l = LogFile('hi.log', 10)
print(l.read().split('\n'))
print(len([x for x in l.read().split('\n') if x]))
l = Log(LogFile('hi', 100, False))
print(repr(l))
for x in range(10) :
l(0, x)
#~ l.setCol(False)
l(0, 'info', 'Big Failure Oh NO!')
l(0, 'error', 'Big Failure Oh NO!')
l(0, 'crit', 'Big Failure Oh NO!')
l(0, 'warn', 'Big Failure Oh NO!')
l(0, 'debug', 'Big Failure Oh NO!')
l(0, 'run', 'Big Failure Oh NO!')
l = LogFile('hi', 500, False)
print('hihi', 'you', 'can\'t', 'beat', 'the', 'trunc', file=l, sep='\n')
def logTest() :
l = Log(open('hi.txt', 'w'))
#~ l.setCol(False)
print(repr(l))
print('1', file=l)
l(1, '2')
a = l.reset()
#~ l.setCol(True)
l.addFiles(sys.stderr)
print('\n', repr(l), sep='')
print('\n', repr(a), sep='')
a(2, '3')
a.write('4')
a.addFiles(sys.stdout)
print('\n a + l ', repr(a + l), sep='')
print(a + l)
print('\n', repr(a + sys.stderr), sep='')
print('\n', repr(sys.stderr + a), sep='')
print('\n', (l + a), sep='')
print('\n', (l + a)[0:3], sep='')
for item in (l + a) :
print(item)
print("\nLength of l + a: ", len(l + a))
print("\nl + a: ", repr(Log() + Files(sys.stdout)))
print('\n', l.read(verb=0), sep='')
if __name__ == "__main__" :
#~ unitTest()
#~ logFileTest()
coolTest()

View File

@ -0,0 +1,108 @@
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <pybind11/functional.h>
//~ #include <pybind11/eigen.h>
//~ #include <Eigen/LU>
#include <iostream>
#include <cmath>
//~ #include "samplers.h"
namespace py = pybind11;
using namespace std;
//Gamma functions, ported to C++ for efficiency.
float lin(float x) { return x; }
float sRGB(float x) { return x > 0.0031308 ? (( 1.055 * pow(x, (1.0f / 2.4)) ) - 0.055) : x * 12.92; }
float sRGBinv(float x) { return x > 0.04045 ? pow(( (x + 0.055) / 1.055 ), 2.4) : x / 12.92; }
float Rec709(float x) { return x >= 0.018 ? 1.099 * pow(x, 0.45) - 0.099 : 4.5 * x; }
float ReinhardHDR(float x) { return x / (1.0 + x); }
float sLog(float x) { return (0.432699 * log10(x + 0.037584) + 0.616596) + 0.03; }
float sLog2(float x) { return ( 0.432699 * log10( (155.0 * x) / 219.0 + 0.037584) + 0.616596 ) + 0.03; }
float DanLog(float x) { return x > 0.1496582 ? (pow(10.0, ((x - 0.385537) / 0.2471896)) - 0.071272) / 3.555556 : (x - 0.092809) / 5.367655; }
//gam lets the user pass in any 1D array, any one-arg C++ function, and get a result. It's multithreaded, vectorized, etc. .
py::array_t<float> gam(py::array_t<float> arr, const std::function<float(float)> &g_func) {
py::buffer_info bufIn = arr.request();
//To use with an image, MAKE SURE to flatten the 3D array to a 1D array, then back out to a 3D array after.
if (bufIn.ndim == 1) {
//Make numpy allocate the buffer.
auto result = py::array_t<float>(bufIn.size);
//Get the pointers that we can manipulate from C++.
auto bufOut = result.request();
float *ptrIn = (float *) bufIn.ptr,
*ptrOut = (float *) bufOut.ptr;
//The reason for all this bullshit as opposed to vectorizing is this pragma!!!
#pragma omp parallel for
for (size_t i = 0; i < bufIn.shape[0]; i++) {
//~ std::cout << g_func(ptrIn[i]) << std::endl;
ptrOut[i] = g_func(ptrIn[i]);
}
return result;
}
PYBIND11_PLUGIN(olOpt) {
py::module mod("olOpt", "Optimized C++ functions for openlut.");
mod.def( "gam",
&gam,
"The sRGB function, vectorized."
);
mod.def( "lin",
&lin,
"The linear function."
);
mod.def( "sRGB",
&sRGB,
"The sRGB function."
);
mod.def( "sRGBinv",
&sRGBinv,
"The sRGBinv function."
);
mod.def( "Rec709",
&Rec709,
"The Rec709 function."
);
mod.def( "ReinhardHDR",
&ReinhardHDR,
"The ReinhardHDR function."
);
mod.def( "sLog",
&sLog,
"The sLog function."
);
mod.def( "sLog2",
&sLog2,
"The sLog2 function."
);
mod.def( "DanLog",
&DanLog,
"The DanLog function."
);
return mod.ptr();
}

26
setup.py 100644
View File

@ -0,0 +1,26 @@
#!/usr/bin/env python3
from setuptools import setup
from setuptools import Extension
setup( name = 'openlut',
version = '0.0.2',
description = 'OpenLUT is a practical color management library.',
author = 'Sofus Rose',
author_email = 'sofus@sofusrose.com',
url = 'https://www.github.com/so-rose/openlut',
license = 'MIT Licence',
keywords = 'color image images processing',
install_requires = ['numpy', 'wand', 'scipy', 'pygame','PyOpenGL'],
classifiers = [
'Development Status :: 3 - Alpha',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.5'
]
)

1
src 120000
View File

@ -0,0 +1 @@
openlut

Binary file not shown.

54
tests/suite.py 100644
View File

@ -0,0 +1,54 @@
from openlut import *
def runTest(inPath, path) :
print('Open openlut.py and scroll down to the end to see the code that\'s working!')
#Open any format image. Try it with exr/dpx/anything!
img = ColMap.open(inPath + '/rock.exr') #Opens a test image 'test.exr', creating a ColMap object, automatically using the best image backend available to load the image at the correct bit depth.
'''
gamma has gamma functions like gamma.sRGB, called by value like gamma.sRGB(val). All take one argument, the value (x), and returns the transformed value. Color doesn't matter for gamma.
gamut has matrices, in 3x3 numpy array form. All are relative to ACES, with direction aptly named. So, gamut.XYZ is a matrix from ACES --> XYZ, while gamut.XYZinv goes from XYZ --> ACES. All use/are converted to the D65 illuminant, for consistency sake.
'''
#gamma Functions: sRGB --> Linear.
gFunc = Func(gamma.sRGBinv) #A Func Transform object using the sRGB-->Linear gamma formula. Apply to ColMaps!
gFuncManualsRGB = Func(lambda val: ((val + 0.055) / 1.055) ** 2.4 if val > 0.04045 else val / 12.92) #It's generic - specify any gamma function, even inline with a lambda!
#LUT from Function: sRGB --> Linear
oLut = LUT.lutFunc(gamma.sRGBinv) #A LUT Transform object, created from a gamma function. Size is 16384 by default. LUTs are faster!
oLut.save(path + '/sRGB-->Lin.cube') #Saves the LUT to a format inferred from the extension. cube only for now!
#Opening LUTs from .cube files.
lut = LUT.open(path + '/sRGB-->Lin.cube') #Opens the lut we just made into a different LUT object.
lut.resized(17).save(path + '/sRGB-->Lin_tiny.cube') #Resizes the LUT, then saves it again to a much smaller file!
#Matrix Transformations
simpleMat = ColMat(gamut.sRGBinv) #A Matrix Transform (ColMat) object, created from a color transform matrix for gamut transformations! This one is sRGB --> ACES.
mat = ColMat(gamut.sRGBinv, gamut.XYZ, gamut.XYZinv, gamut.aRGB) * gamut.aRGBinv
#Indeed, specify many matrices which auto-multiply into a single one! You can also combine them after, with simple multiplication.
#Applying and saving.
img.apply(gFunc).save(path + '/openlut_gammafunc.png') #save saves an image using the appropriate image backend, based on the extension.
img.apply(lut).save(path + '/openlut_lut-lin-16384.png') #apply applies any color transformation object that inherits from Transform - LUT, Func, ColMat, etc., or make your own! It's easy ;) .
img.apply(lut.resized(17)).save(path + '/openlut_lut-lin-17.png') #Why so small? Because spline interpolation automatically turns on. It's identical to the larger LUT!
img.apply(mat).save(path + '/openlut_mat.png') #Applies the gamut transformation.
#As a proof of concept, here's a long list of transformations that should, in sum, do nothing :) :
img.apply(lut).apply(LUT.lutFunc(gamma.sRGB)).apply(mat).apply(~mat).save('testpath/openlut_noop.png') #~mat is the inverse of mat. Easily undo the gamut operation!
#Format Test: All output images are in Linear ACES.
tImg = img.apply(mat)
tImg.save(path + '/output.exr')
tImg.save(path + '/output.dpx')
tImg.save(path + '/output.png')
tImg.save(path + '/output.jpg')
tImg.save(path + '/output.tif') #All sorts of formats work! Bit depth is 16, unless you say something else.
#Compression is impossible right now - wand is being difficult.
#Keep in mind, values are clipped from 0 to 1 when done. Scary transforms can make this an issue!
#Color management is simple: openlut doesn't touch your data, unless you tell it to with a Transform. So, the data that goes in, goes out, unless a Transform was applied.
if __name__ == "__main__" :
runTest()

36
tests/testLUT.py 100644
View File

@ -0,0 +1,36 @@
import os, sys
import unittest as ut
from os import path
sys.path.append(0, path.abspath('..'))
from openlut import *
def verifyLUT(lut, title, size, iRange) :
assertEqual(lut.title, title)
assertEqual(lut.size, size)
assertEqual(lut.range, iRange)
assertEqual(lut.dims, 1)
assertEqual(lut.ID, np.linspace(iRange[0], iRange[1], size))
assertEqual(str(lut.array), 'float32')
class testLUT(ut.TestCase) :
def test_init(self) :
lut = LUT(title="test", size=4096, iRange=(-0.125, 1.125))
verifyLUT(lut, 'test', 4096, (-0.125, 1.125))
#~ assertEqual(lut.title, 'test')
#~ assertEqual(lut.size, 4096)
#~ assertEqual(lut.range, (-0.125, 1.125))
#~ assertEqual(lut.dims, 1)
#~ assertEqual(lut.ID, np.linspace(-0.125, 1.125, 4096))
#~ assertEqual(lut.array, np.linspace(-0.125, 1.125, 4096))
def test_func(self) :
lut = LUT.lutFunc(gamma.sRGB, title='test', size=4096, iRange=(-0.125, 1.125))
verifyLUT(lut, 'test', 4096, (-0.125, 1.125))