diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..61d40c5 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/.pypirc b/.pypirc new file mode 100644 index 0000000..c7921b3 --- /dev/null +++ b/.pypirc @@ -0,0 +1,6 @@ +[distutils] +index-servers=pypi + +[pypi] +repository = https://upload.pypi.org/legacy/ +username = so-rose diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f12ea35 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include openlut *.py diff --git a/README b/README new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/README @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/README.md b/README.md index 642cf74..598fec8 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/code_tests/mk_lhald.py b/code_tests/mk_lhald.py deleted file mode 100755 index b9db34f..0000000 --- a/code_tests/mk_lhald.py +++ /dev/null @@ -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 ") - print("\tHALD --> CUBE: ./mk_lhald.py mk \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() - diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..0a688a9 --- /dev/null +++ b/doc/Makefile @@ -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 ' where 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." diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..28adcea --- /dev/null +++ b/doc/make.bat @@ -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 ^` where ^ 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 diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..f228e67 --- /dev/null +++ b/doc/source/conf.py @@ -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. +# " v 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 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 diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..e88360c --- /dev/null +++ b/doc/source/index.rst @@ -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` + diff --git a/doc/source/modules.rst b/doc/source/modules.rst new file mode 100644 index 0000000..fb8bc43 --- /dev/null +++ b/doc/source/modules.rst @@ -0,0 +1,7 @@ +openlut +======= + +.. toctree:: + :maxdepth: 4 + + openlut diff --git a/doc/source/openlut.lib.rst b/doc/source/openlut.lib.rst new file mode 100644 index 0000000..04678ce --- /dev/null +++ b/doc/source/openlut.lib.rst @@ -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: diff --git a/doc/source/openlut.rst b/doc/source/openlut.rst new file mode 100644 index 0000000..d7837c3 --- /dev/null +++ b/doc/source/openlut.rst @@ -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: diff --git a/img_test/rock.exr b/img_test/rock.exr new file mode 100644 index 0000000..3c18c10 --- /dev/null +++ b/img_test/rock.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ad056a1887cd7be2cb2eeddfa2194d092be4fbc3a62e358ae5a15ba0562ba3c +size 10596905 diff --git a/img_test/station.exr b/img_test/station.exr new file mode 100644 index 0000000..d2abf67 --- /dev/null +++ b/img_test/station.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39123fa7f59cb30f65aabf6801d1ac4642cf829c912de14dcc4a04005bf02cbe +size 7789396 diff --git a/main.py b/main.py new file mode 100755 index 0000000..27d440c --- /dev/null +++ b/main.py @@ -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') diff --git a/openlut-project b/openlut-project new file mode 100644 index 0000000..e7fb11e --- /dev/null +++ b/openlut-project @@ -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= diff --git a/openlut.py b/openlut.py deleted file mode 100755 index 1bc395e..0000000 --- a/openlut.py +++ /dev/null @@ -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 . 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. diff --git a/openlut/ColMap.py b/openlut/ColMap.py new file mode 100644 index 0000000..8be04b0 --- /dev/null +++ b/openlut/ColMap.py @@ -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')])) diff --git a/openlut/ColMat.py b/openlut/ColMat.py new file mode 100644 index 0000000..ab1922c --- /dev/null +++ b/openlut/ColMat.py @@ -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)) + + diff --git a/openlut/Func.py b/openlut/Func.py new file mode 100644 index 0000000..f03465b --- /dev/null +++ b/openlut/Func.py @@ -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]) + diff --git a/openlut/LUT.py b/openlut/LUT.py new file mode 100644 index 0000000..1f8c9c8 --- /dev/null +++ b/openlut/LUT.py @@ -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')])) diff --git a/openlut/Transform.py b/openlut/Transform.py new file mode 100644 index 0000000..5afd887 --- /dev/null +++ b/openlut/Transform.py @@ -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. + """ diff --git a/openlut/Viewer.py b/openlut/Viewer.py new file mode 100644 index 0000000..bd07580 --- /dev/null +++ b/openlut/Viewer.py @@ -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. + diff --git a/openlut/__init__.py b/openlut/__init__.py new file mode 100644 index 0000000..95a9b7b --- /dev/null +++ b/openlut/__init__.py @@ -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'] + diff --git a/openlut/gamma.py b/openlut/gamma.py new file mode 100644 index 0000000..1c3c1dd --- /dev/null +++ b/openlut/gamma.py @@ -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 diff --git a/openlut/gamut.py b/openlut/gamut.py new file mode 100644 index 0000000..e69268d --- /dev/null +++ b/openlut/gamut.py @@ -0,0 +1,71 @@ +import numpy as np + +#Static Matrices -- all go from ACES to . 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) diff --git a/openlut/lib/Makefile b/openlut/lib/Makefile new file mode 100644 index 0000000..9549cc3 --- /dev/null +++ b/openlut/lib/Makefile @@ -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} diff --git a/openlut/lib/__init__.py b/openlut/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openlut/lib/files.py b/openlut/lib/files.py new file mode 100755 index 0000000..1b32eb0 --- /dev/null +++ b/openlut/lib/files.py @@ -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() diff --git a/openlut/lib/olOpt.cpp b/openlut/lib/olOpt.cpp new file mode 100644 index 0000000..182f409 --- /dev/null +++ b/openlut/lib/olOpt.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +//~ #include + +//~ #include + +#include +#include + +//~ #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 gam(py::array_t arr, const std::function &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(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(); +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..69d9b1a --- /dev/null +++ b/setup.py @@ -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' + ] +) diff --git a/src b/src new file mode 120000 index 0000000..e17f667 --- /dev/null +++ b/src @@ -0,0 +1 @@ +openlut \ No newline at end of file diff --git a/testpath/test.exr b/testpath/test.exr deleted file mode 100644 index cf03997..0000000 Binary files a/testpath/test.exr and /dev/null differ diff --git a/tests/suite.py b/tests/suite.py new file mode 100644 index 0000000..5a11306 --- /dev/null +++ b/tests/suite.py @@ -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() diff --git a/tests/testLUT.py b/tests/testLUT.py new file mode 100644 index 0000000..6e4211a --- /dev/null +++ b/tests/testLUT.py @@ -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))